From f6c03fc542036b39a6b808c76d61ccb6042adf3b Mon Sep 17 00:00:00 2001 From: zhangtuo Date: Wed, 21 Jan 2026 11:34:39 +0800 Subject: [PATCH] docs: add code architecture refactoring PRD --- .../code-architecture/REFACTORING-PRD.md | 416 ++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 .claude/specs/code-architecture/REFACTORING-PRD.md diff --git a/.claude/specs/code-architecture/REFACTORING-PRD.md b/.claude/specs/code-architecture/REFACTORING-PRD.md new file mode 100644 index 0000000..4c31d94 --- /dev/null +++ b/.claude/specs/code-architecture/REFACTORING-PRD.md @@ -0,0 +1,416 @@ +# 代码架构重构 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 +```