wip: snapshot full local workspace state
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

This commit is contained in:
2026-04-17 18:58:11 +08:00
parent fea4ef012f
commit b8d3cb4644
181 changed files with 16964 additions and 17486 deletions

View File

@@ -1,416 +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<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 }
}
```
**使用示例**
```typescript
// 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
**用途**:统一表单验证逻辑
```typescript
// 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
**用途**:统一日期格式化
```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
```
# 代码架构重构 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<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 }
}
```
**使用示例**
```typescript
// 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
**用途**:统一表单验证逻辑
```typescript
// 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
**用途**:统一日期格式化
```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
```