Some checks are pending
Build and Push Docker Images / changes (push) Waiting to run
Build and Push Docker Images / build-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-admin-frontend (push) Blocked by required conditions
11 KiB
11 KiB
代码架构重构 PRD
版本: 1.0 日期: 2025-01-21 状态: 待实施
1. 背景与目标
1.1 现状问题
经过代码审计,发现以下架构债务:
| 问题 | 位置 | 影响 |
|---|---|---|
| 缺少 Schemas 层 | Backend API | 类型不安全,文档生成差 |
| Fat Controller | stories.py 562行 |
难测试,职责混乱 |
| 重复加载逻辑 | Frontend 5+ 组件 | 代码冗余,维护困难 |
| 无 Repository 层 | Backend | DB 查询散落,难复用 |
1.2 重构目标
- 后端:引入 Pydantic Schemas 层,分离请求/响应模型
- 前端:抽取 Vue Composables,消除重复逻辑
- 原则:渐进式重构,不破坏现有功能
2. 后端重构:Schemas 层
2.1 目标结构
backend/app/
├── api/ # 路由层(瘦身)
│ ├── stories.py
│ └── ...
├── schemas/ # 新增:Pydantic 模型
│ ├── __init__.py
│ ├── auth.py
│ ├── story.py
│ ├── profile.py
│ ├── universe.py
│ ├── memory.py
│ ├── push_config.py
│ └── common.py # 分页、错误响应等通用结构
├── models/ # SQLAlchemy ORM(原 db/)
└── services/ # 业务逻辑
2.2 Schema 设计规范
命名约定
| 类型 | 命名模式 | 示例 |
|---|---|---|
| 创建请求 | {Entity}Create |
StoryCreate |
| 更新请求 | {Entity}Update |
ProfileUpdate |
| 响应模型 | {Entity}Response |
StoryResponse |
| 列表响应 | {Entity}ListResponse |
StoryListResponse |
示例:Story Schema
# schemas/story.py
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
class StoryCreate(BaseModel):
"""创建故事请求"""
type: Literal["keywords", "enhance", "storybook"]
data: str = Field(..., min_length=1, max_length=2000)
education_theme: str | None = None
child_profile_id: str | None = None
universe_id: str | None = None
class StoryResponse(BaseModel):
"""故事响应"""
id: int
title: str
story_text: str | None
pages: list[dict] | None
image_url: str | None
mode: str
created_at: datetime
class Config:
from_attributes = True # 支持 ORM 模型转换
class StoryListResponse(BaseModel):
"""故事列表响应"""
stories: list[StoryResponse]
total: int
page: int
page_size: int
2.3 迁移步骤
| 阶段 | 任务 | 文件 |
|---|---|---|
| P1 | 创建 schemas/common.py |
分页、错误响应 |
| P2 | 创建 schemas/story.py |
故事相关模型 |
| P3 | 创建 schemas/profile.py |
档案相关模型 |
| P4 | 创建 schemas/universe.py |
宇宙相关模型 |
| P5 | 创建 schemas/auth.py |
认证相关模型 |
| P6 | 创建 schemas/memory.py |
记忆相关模型 |
| P7 | 创建 schemas/push_config.py |
推送配置模型 |
| P8 | 重构 api/stories.py |
使用新 schemas |
| P9 | 重构其他 API 文件 | 逐个迁移 |
2.4 验收标准
- 所有 API endpoint 使用
response_model参数 - 请求体使用 Pydantic 模型而非
Body(...) - OpenAPI 文档自动生成完整类型
- 现有测试全部通过
ruff check无错误
3. 前端重构:Composables
3.1 目标结构
frontend/src/
├── composables/ # 新增:可复用逻辑
│ ├── index.ts
│ ├── useAsyncData.ts # 异步数据加载
│ ├── useFormValidation.ts # 表单验证
│ └── useDateFormat.ts # 日期格式化
├── components/
├── stores/
└── views/
3.2 Composable 设计
3.2.1 useAsyncData
用途:统一处理 API 数据加载的 loading/error 状态
// composables/useAsyncData.ts
import { ref, type Ref } from 'vue'
interface AsyncDataOptions<T> {
immediate?: boolean // 立即执行,默认 true
initialData?: T // 初始数据
onError?: (e: Error) => void
}
interface AsyncDataReturn<T> {
data: Ref<T | null>
loading: Ref<boolean>
error: Ref<string>
execute: () => Promise<void>
reset: () => void
}
export function useAsyncData<T>(
fetcher: () => Promise<T>,
options: AsyncDataOptions<T> = {}
): AsyncDataReturn<T> {
const { immediate = true, initialData = null, onError } = options
const data = ref<T | null>(initialData) as Ref<T | null>
const loading = ref(false)
const error = ref('')
async function execute() {
loading.value = true
error.value = ''
try {
data.value = await fetcher()
} catch (e) {
const message = e instanceof Error ? e.message : '加载失败'
error.value = message
onError?.(e instanceof Error ? e : new Error(message))
} finally {
loading.value = false
}
}
function reset() {
data.value = initialData
loading.value = false
error.value = ''
}
if (immediate) {
execute()
}
return { data, loading, error, execute, reset }
}
使用示例:
// views/MyStories.vue(重构前 40 行 → 重构后 10 行)
import { useAsyncData } from '@/composables'
import { api } from '@/api/client'
const { data: response, loading, error } = useAsyncData(
() => api.get<StoryListResponse>('/api/stories')
)
const stories = computed(() => response.value?.stories ?? [])
3.2.2 useFormValidation
用途:统一表单验证逻辑
// composables/useFormValidation.ts
import { ref, reactive } from 'vue'
type ValidationRule = (value: unknown) => string | true
interface FieldRules {
[field: string]: ValidationRule[]
}
export function useFormValidation<T extends Record<string, unknown>>(
initialData: T,
rules: FieldRules
) {
const form = reactive({ ...initialData })
const errors = reactive<Record<string, string>>({})
function validate(): boolean {
let valid = true
for (const [field, fieldRules] of Object.entries(rules)) {
const value = form[field]
for (const rule of fieldRules) {
const result = rule(value)
if (result !== true) {
errors[field] = result
valid = false
break
} else {
errors[field] = ''
}
}
}
return valid
}
function reset() {
Object.assign(form, initialData)
Object.keys(errors).forEach(k => errors[k] = '')
}
return { form, errors, validate, reset }
}
// 常用验证规则
export const required = (msg = '此字段必填') =>
(v: unknown) => (v !== null && v !== undefined && v !== '') || msg
export const minLength = (min: number, msg?: string) =>
(v: unknown) => (typeof v === 'string' && v.length >= min) || msg || `至少 ${min} 个字符`
export const maxLength = (max: number, msg?: string) =>
(v: unknown) => (typeof v === 'string' && v.length <= max) || msg || `最多 ${max} 个字符`
3.2.3 useDateFormat
用途:统一日期格式化
// composables/useDateFormat.ts
export function useDateFormat() {
function formatDate(dateStr: string | Date, format: 'date' | 'datetime' | 'relative' = 'date'): string {
const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr
if (format === 'relative') {
return formatRelative(date)
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
if (format === 'date') {
return `${year}-${month}-${day}`
}
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
function formatRelative(date: Date): string {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMins < 1) return '刚刚'
if (diffMins < 60) return `${diffMins} 分钟前`
if (diffHours < 24) return `${diffHours} 小时前`
if (diffDays < 7) return `${diffDays} 天前`
return formatDate(date, 'date')
}
return { formatDate, formatRelative }
}
3.3 迁移步骤
| 阶段 | 任务 | 影响文件 |
|---|---|---|
| P1 | 创建 composables/useAsyncData.ts |
新文件 |
| P2 | 创建 composables/useDateFormat.ts |
新文件 |
| P3 | 创建 composables/useFormValidation.ts |
新文件 |
| P4 | 创建 composables/index.ts |
统一导出 |
| P5 | 重构 views/MyStories.vue |
使用 useAsyncData |
| P6 | 重构 views/ChildProfiles.vue |
使用 useAsyncData |
| P7 | 重构 views/Universes.vue |
使用 useAsyncData |
| P8 | 重构 components/CreateStoryModal.vue |
使用 useFormValidation |
| P9 | 同步重构 admin-frontend/ |
复制 composables |
3.4 验收标准
composables/目录包含 3 个核心 composable- 至少 3 个 view 组件使用
useAsyncData CreateStoryModal使用useFormValidationnpm run build无类型错误- 现有功能正常运行
4. 风险与缓解
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| 重构引入 bug | 高 | 每个阶段运行测试 |
| 类型不兼容 | 中 | 使用 from_attributes = True |
| 前端构建失败 | 中 | 逐文件迁移,及时 commit |
5. 时间估算
| 模块 | 工作量 | 预计时间 |
|---|---|---|
| Backend Schemas | 10 个新文件 + 9 个 API 重构 | 3-4 小时 |
| Frontend Composables | 4 个新文件 + 6 个组件重构 | 2-3 小时 |
| 测试验证 | 全量回归 | 1 小时 |
| 总计 | 6-8 小时 |
6. 后续迭代
本次重构完成后,可继续:
- Repository 层:抽象 DB 查询逻辑
- Service 层瘦身:拆分
provider_router.py(433行) - 前端共享包:
frontend和admin-frontend共用 UI 组件 - API 版本化:引入
/api/v1/前缀
附录:文件清单
新增文件
backend/app/schemas/
├── __init__.py
├── common.py
├── auth.py
├── story.py
├── profile.py
├── universe.py
├── memory.py
└── push_config.py
frontend/src/composables/
├── index.ts
├── useAsyncData.ts
├── useFormValidation.ts
└── useDateFormat.ts
修改文件
backend/app/api/
├── stories.py
├── profiles.py
├── universes.py
├── memories.py
├── push_configs.py
├── reading_events.py
└── auth.py
frontend/src/views/
├── MyStories.vue
├── ChildProfiles.vue
├── Universes.vue
└── ...
frontend/src/components/
└── CreateStoryModal.vue