# 代码架构重构 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 ```python # 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 状态 ```typescript // composables/useAsyncData.ts import { ref, type Ref } from 'vue' interface AsyncDataOptions { immediate?: boolean // 立即执行,默认 true initialData?: T // 初始数据 onError?: (e: Error) => void } interface AsyncDataReturn { data: Ref loading: Ref error: Ref execute: () => Promise reset: () => void } export function useAsyncData( fetcher: () => Promise, options: AsyncDataOptions = {} ): AsyncDataReturn { const { immediate = true, initialData = null, onError } = options const data = ref(initialData) as Ref 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 } } ``` **使用示例**: ```typescript // views/MyStories.vue(重构前 40 行 → 重构后 10 行) import { useAsyncData } from '@/composables' import { api } from '@/api/client' const { data: response, loading, error } = useAsyncData( () => api.get('/api/stories') ) const stories = computed(() => response.value?.stories ?? []) ``` #### 3.2.2 useFormValidation **用途**:统一表单验证逻辑 ```typescript // composables/useFormValidation.ts import { ref, reactive } from 'vue' type ValidationRule = (value: unknown) => string | true interface FieldRules { [field: string]: ValidationRule[] } export function useFormValidation>( initialData: T, rules: FieldRules ) { const form = reactive({ ...initialData }) const errors = reactive>({}) 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 **用途**:统一日期格式化 ```typescript // 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` 使用 `useFormValidation` - [ ] `npm 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. 后续迭代 本次重构完成后,可继续: 1. **Repository 层**:抽象 DB 查询逻辑 2. **Service 层瘦身**:拆分 `provider_router.py` (433行) 3. **前端共享包**:`frontend` 和 `admin-frontend` 共用 UI 组件 4. **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 ```