Files
dreamweaver/.claude/specs/code-architecture/REFACTORING-PRD.md
2026-01-21 11:34:39 +08:00

11 KiB
Raw Blame History

代码架构重构 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 使用 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. 前端共享包frontendadmin-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