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
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:
@@ -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
|
||||
```
|
||||
|
||||
@@ -150,4 +150,4 @@
|
||||
|
||||
## 推荐
|
||||
|
||||
建议 Web MVP 使用方案 A(Soft Aurora),兼顾温暖与信任。方案 B/C 可作为后续主题或 A/B 测试备选。
|
||||
建议 Web MVP 使用方案 A(Soft Aurora),兼顾温暖与信任。方案 B/C 可作为后续主题或 A/B 测试备选。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -227,4 +227,4 @@
|
||||
- 组件做 Variant
|
||||
- 全部使用 Auto Layout
|
||||
- 1440/1200/1024/768 建立栅格
|
||||
- 状态页复制并标注
|
||||
- 状态页复制并标注
|
||||
|
||||
@@ -296,4 +296,4 @@
|
||||
|
||||
- 设计系统库(色板、文字、组件)
|
||||
- 全流程高保真页面
|
||||
- 原型链接(Figma 中生成)
|
||||
- 原型链接(Figma 中生成)
|
||||
|
||||
@@ -56,4 +56,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -85,4 +85,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -55,4 +55,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -108,4 +108,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -30,4 +30,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -25,4 +25,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -81,4 +81,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -75,4 +75,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -70,4 +70,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -214,4 +214,4 @@ a { color: var(--primary-600); text-decoration: none; }
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
.grid-3 { grid-template-columns: 1fr; }
|
||||
.row { grid-template-columns: 1fr; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +61,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -61,4 +61,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -56,4 +56,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -85,4 +85,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -55,4 +55,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -108,4 +108,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -30,4 +30,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -25,4 +25,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -81,4 +81,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -75,4 +75,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -70,4 +70,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -214,4 +214,4 @@ a { color: var(--primary-600); text-decoration: none; }
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
.grid-3 { grid-template-columns: 1fr; }
|
||||
.row { grid-template-columns: 1fr; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +61,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -61,4 +61,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -56,4 +56,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -85,4 +85,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -55,4 +55,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -108,4 +108,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -30,4 +30,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -25,4 +25,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -81,4 +81,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -75,4 +75,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -70,4 +70,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -214,4 +214,4 @@ a { color: var(--primary-600); text-decoration: none; }
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
.grid-3 { grid-template-columns: 1fr; }
|
||||
.row { grid-template-columns: 1fr; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +61,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -61,4 +61,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,429 +1,429 @@
|
||||
# 孩子档案数据模型
|
||||
|
||||
## 概述
|
||||
|
||||
孩子档案是记忆智能系统的核心,存储孩子的基础信息、兴趣偏好和阅读行为数据。
|
||||
|
||||
---
|
||||
|
||||
## 一、数据库模型
|
||||
|
||||
### 1.1 主表: child_profiles
|
||||
|
||||
```sql
|
||||
CREATE TABLE child_profiles (
|
||||
-- 主键
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- 外键: 所属用户
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- 基础信息
|
||||
name VARCHAR(50) NOT NULL,
|
||||
avatar_url VARCHAR(500),
|
||||
birth_date DATE,
|
||||
gender VARCHAR(10) CHECK (gender IN ('male', 'female', 'other')),
|
||||
|
||||
-- 显式偏好 (家长填写)
|
||||
interests JSONB DEFAULT '[]',
|
||||
-- 示例: ["恐龙", "太空", "公主", "动物"]
|
||||
|
||||
growth_themes JSONB DEFAULT '[]',
|
||||
-- 示例: ["勇气", "分享"]
|
||||
|
||||
-- 隐式偏好 (系统学习)
|
||||
reading_preferences JSONB DEFAULT '{}',
|
||||
-- 示例: {
|
||||
-- "preferred_length": "medium", -- short/medium/long
|
||||
-- "preferred_style": "adventure", -- adventure/fairy/educational
|
||||
-- "tag_weights": {"恐龙": 5, "公主": 2, "太空": 3}
|
||||
-- }
|
||||
|
||||
-- 统计数据
|
||||
stories_count INTEGER DEFAULT 0,
|
||||
total_reading_time INTEGER DEFAULT 0, -- 秒
|
||||
|
||||
-- 时间戳
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
-- 约束
|
||||
CONSTRAINT unique_child_per_user UNIQUE (user_id, name)
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_child_profiles_user_id ON child_profiles(user_id);
|
||||
```
|
||||
|
||||
### 1.2 兴趣标签枚举
|
||||
|
||||
预定义的兴趣标签,前端展示用:
|
||||
|
||||
```python
|
||||
INTEREST_TAGS = {
|
||||
"animals": {
|
||||
"zh": "动物",
|
||||
"icon": "🐾",
|
||||
"subtags": ["恐龙", "猫咪", "狗狗", "兔子", "海洋动物"]
|
||||
},
|
||||
"fantasy": {
|
||||
"zh": "奇幻",
|
||||
"icon": "✨",
|
||||
"subtags": ["公主", "王子", "魔法", "精灵", "龙"]
|
||||
},
|
||||
"adventure": {
|
||||
"zh": "冒险",
|
||||
"icon": "🗺️",
|
||||
"subtags": ["太空", "海盗", "探险", "寻宝"]
|
||||
},
|
||||
"vehicles": {
|
||||
"zh": "交通工具",
|
||||
"icon": "🚗",
|
||||
"subtags": ["汽车", "火车", "飞机", "火箭"]
|
||||
},
|
||||
"nature": {
|
||||
"zh": "自然",
|
||||
"icon": "🌳",
|
||||
"subtags": ["森林", "海洋", "山川", "四季"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 成长主题枚举
|
||||
|
||||
```python
|
||||
GROWTH_THEMES = [
|
||||
{"key": "courage", "zh": "勇气", "description": "克服恐惧,勇敢面对"},
|
||||
{"key": "sharing", "zh": "分享", "description": "学会与他人分享"},
|
||||
{"key": "friendship", "zh": "友谊", "description": "交朋友,珍惜友情"},
|
||||
{"key": "honesty", "zh": "诚实", "description": "说真话,不撒谎"},
|
||||
{"key": "independence", "zh": "独立", "description": "自己的事情自己做"},
|
||||
{"key": "kindness", "zh": "善良", "description": "帮助他人,关爱弱小"},
|
||||
{"key": "patience", "zh": "耐心", "description": "学会等待,不急躁"},
|
||||
{"key": "curiosity", "zh": "好奇", "description": "探索未知,爱问为什么"}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、SQLAlchemy 模型
|
||||
|
||||
```python
|
||||
# backend/app/db/models.py
|
||||
|
||||
from sqlalchemy import Column, String, Date, Integer, ForeignKey, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
import uuid
|
||||
|
||||
class ChildProfile(Base):
|
||||
__tablename__ = "child_profiles"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
# 基础信息
|
||||
name = Column(String(50), nullable=False)
|
||||
avatar_url = Column(String(500))
|
||||
birth_date = Column(Date)
|
||||
gender = Column(String(10))
|
||||
|
||||
# 偏好
|
||||
interests = Column(JSON, default=list)
|
||||
growth_themes = Column(JSON, default=list)
|
||||
reading_preferences = Column(JSON, default=dict)
|
||||
|
||||
# 统计
|
||||
stories_count = Column(Integer, default=0)
|
||||
total_reading_time = Column(Integer, default=0)
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
user = relationship("User", back_populates="child_profiles")
|
||||
story_universes = relationship("StoryUniverse", back_populates="child_profile", cascade="all, delete-orphan")
|
||||
|
||||
@property
|
||||
def age(self) -> int | None:
|
||||
"""计算年龄"""
|
||||
if not self.birth_date:
|
||||
return None
|
||||
today = date.today()
|
||||
return today.year - self.birth_date.year - (
|
||||
(today.month, today.day) < (self.birth_date.month, self.birth_date.day)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Pydantic Schema
|
||||
|
||||
```python
|
||||
# backend/app/schemas/child_profile.py
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import date
|
||||
from uuid import UUID
|
||||
|
||||
class ChildProfileCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=50)
|
||||
birth_date: date | None = None
|
||||
gender: str | None = Field(None, pattern="^(male|female|other)$")
|
||||
interests: list[str] = Field(default_factory=list)
|
||||
growth_themes: list[str] = Field(default_factory=list)
|
||||
|
||||
class ChildProfileUpdate(BaseModel):
|
||||
name: str | None = Field(None, min_length=1, max_length=50)
|
||||
birth_date: date | None = None
|
||||
gender: str | None = Field(None, pattern="^(male|female|other)$")
|
||||
interests: list[str] | None = None
|
||||
growth_themes: list[str] | None = None
|
||||
avatar_url: str | None = None
|
||||
|
||||
class ChildProfileResponse(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
avatar_url: str | None
|
||||
birth_date: date | None
|
||||
gender: str | None
|
||||
age: int | None
|
||||
interests: list[str]
|
||||
growth_themes: list[str]
|
||||
stories_count: int
|
||||
total_reading_time: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ChildProfileListResponse(BaseModel):
|
||||
profiles: list[ChildProfileResponse]
|
||||
total: int
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、API 实现
|
||||
|
||||
```python
|
||||
# backend/app/api/profiles.py
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from uuid import UUID
|
||||
|
||||
router = APIRouter(prefix="/api/profiles", tags=["profiles"])
|
||||
|
||||
@router.get("", response_model=ChildProfileListResponse)
|
||||
async def list_profiles(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取当前用户的所有孩子档案"""
|
||||
profiles = await db.execute(
|
||||
select(ChildProfile)
|
||||
.where(ChildProfile.user_id == current_user.id)
|
||||
.order_by(ChildProfile.created_at)
|
||||
)
|
||||
profiles = profiles.scalars().all()
|
||||
return ChildProfileListResponse(profiles=profiles, total=len(profiles))
|
||||
|
||||
@router.post("", response_model=ChildProfileResponse, status_code=201)
|
||||
async def create_profile(
|
||||
data: ChildProfileCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""创建孩子档案"""
|
||||
# 检查是否超过限制 (每用户最多5个孩子档案)
|
||||
count = await db.scalar(
|
||||
select(func.count(ChildProfile.id))
|
||||
.where(ChildProfile.user_id == current_user.id)
|
||||
)
|
||||
if count >= 5:
|
||||
raise HTTPException(400, "最多只能创建5个孩子档案")
|
||||
|
||||
profile = ChildProfile(user_id=current_user.id, **data.model_dump())
|
||||
db.add(profile)
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
return profile
|
||||
|
||||
@router.get("/{profile_id}", response_model=ChildProfileResponse)
|
||||
async def get_profile(
|
||||
profile_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取单个孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
return profile
|
||||
|
||||
@router.put("/{profile_id}", response_model=ChildProfileResponse)
|
||||
async def update_profile(
|
||||
profile_id: UUID,
|
||||
data: ChildProfileUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""更新孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(profile, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
return profile
|
||||
|
||||
@router.delete("/{profile_id}", status_code=204)
|
||||
async def delete_profile(
|
||||
profile_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""删除孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
|
||||
await db.delete(profile)
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、隐式偏好学习
|
||||
|
||||
### 5.1 行为事件
|
||||
|
||||
```python
|
||||
class ReadingEvent(BaseModel):
|
||||
"""阅读行为事件"""
|
||||
profile_id: UUID
|
||||
story_id: UUID
|
||||
event_type: Literal["started", "completed", "skipped", "replayed"]
|
||||
reading_time: int # 秒
|
||||
timestamp: datetime
|
||||
```
|
||||
|
||||
### 5.2 偏好更新算法
|
||||
|
||||
```python
|
||||
async def update_reading_preferences(
|
||||
db: AsyncSession,
|
||||
profile_id: UUID,
|
||||
story: Story,
|
||||
event: ReadingEvent
|
||||
):
|
||||
"""根据阅读行为更新隐式偏好"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
prefs = profile.reading_preferences or {}
|
||||
tag_weights = prefs.get("tag_weights", {})
|
||||
|
||||
# 权重调整
|
||||
weight_delta = {
|
||||
"completed": 1.0, # 完整阅读,正向
|
||||
"replayed": 1.5, # 重复播放,强正向
|
||||
"skipped": -0.5, # 跳过,负向
|
||||
"started": 0.1 # 开始阅读,弱正向
|
||||
}
|
||||
|
||||
delta = weight_delta.get(event.event_type, 0)
|
||||
|
||||
for tag in story.tags:
|
||||
current = tag_weights.get(tag, 0)
|
||||
tag_weights[tag] = max(0, current + delta) # 不低于0
|
||||
|
||||
# 更新阅读长度偏好
|
||||
if event.event_type == "completed":
|
||||
word_count = len(story.content)
|
||||
if word_count < 300:
|
||||
length_pref = "short"
|
||||
elif word_count < 600:
|
||||
length_pref = "medium"
|
||||
else:
|
||||
length_pref = "long"
|
||||
|
||||
# 简单的移动平均
|
||||
prefs["preferred_length"] = length_pref
|
||||
|
||||
prefs["tag_weights"] = tag_weights
|
||||
profile.reading_preferences = prefs
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、数据迁移
|
||||
|
||||
```python
|
||||
# backend/alembic/versions/xxx_add_child_profiles.py
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'child_profiles',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('user_id', sa.UUID(), nullable=False),
|
||||
sa.Column('name', sa.String(50), nullable=False),
|
||||
sa.Column('avatar_url', sa.String(500)),
|
||||
sa.Column('birth_date', sa.Date()),
|
||||
sa.Column('gender', sa.String(10)),
|
||||
sa.Column('interests', sa.JSON(), server_default='[]'),
|
||||
sa.Column('growth_themes', sa.JSON(), server_default='[]'),
|
||||
sa.Column('reading_preferences', sa.JSON(), server_default='{}'),
|
||||
sa.Column('stories_count', sa.Integer(), server_default='0'),
|
||||
sa.Column('total_reading_time', sa.Integer(), server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_child_profiles_user_id', 'child_profiles', ['user_id'])
|
||||
|
||||
def downgrade():
|
||||
op.drop_index('idx_child_profiles_user_id')
|
||||
op.drop_table('child_profiles')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、隐私与安全
|
||||
|
||||
### 7.1 数据加密
|
||||
|
||||
敏感字段(姓名、出生日期)在存储时加密:
|
||||
|
||||
```python
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
class EncryptedChildProfile:
|
||||
"""加密存储的孩子档案"""
|
||||
|
||||
@staticmethod
|
||||
def encrypt_name(name: str, key: bytes) -> str:
|
||||
f = Fernet(key)
|
||||
return f.encrypt(name.encode()).decode()
|
||||
|
||||
@staticmethod
|
||||
def decrypt_name(encrypted: str, key: bytes) -> str:
|
||||
f = Fernet(key)
|
||||
return f.decrypt(encrypted.encode()).decode()
|
||||
```
|
||||
|
||||
### 7.2 访问控制
|
||||
|
||||
- 孩子档案只能被创建者访问
|
||||
- 删除用户时级联删除所有孩子档案
|
||||
- API 层强制校验 `user_id` 归属
|
||||
|
||||
### 7.3 数据保留
|
||||
|
||||
- 用户可随时删除孩子档案
|
||||
- 删除后 30 天内可恢复(软删除)
|
||||
- 30 天后永久删除
|
||||
# 孩子档案数据模型
|
||||
|
||||
## 概述
|
||||
|
||||
孩子档案是记忆智能系统的核心,存储孩子的基础信息、兴趣偏好和阅读行为数据。
|
||||
|
||||
---
|
||||
|
||||
## 一、数据库模型
|
||||
|
||||
### 1.1 主表: child_profiles
|
||||
|
||||
```sql
|
||||
CREATE TABLE child_profiles (
|
||||
-- 主键
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- 外键: 所属用户
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- 基础信息
|
||||
name VARCHAR(50) NOT NULL,
|
||||
avatar_url VARCHAR(500),
|
||||
birth_date DATE,
|
||||
gender VARCHAR(10) CHECK (gender IN ('male', 'female', 'other')),
|
||||
|
||||
-- 显式偏好 (家长填写)
|
||||
interests JSONB DEFAULT '[]',
|
||||
-- 示例: ["恐龙", "太空", "公主", "动物"]
|
||||
|
||||
growth_themes JSONB DEFAULT '[]',
|
||||
-- 示例: ["勇气", "分享"]
|
||||
|
||||
-- 隐式偏好 (系统学习)
|
||||
reading_preferences JSONB DEFAULT '{}',
|
||||
-- 示例: {
|
||||
-- "preferred_length": "medium", -- short/medium/long
|
||||
-- "preferred_style": "adventure", -- adventure/fairy/educational
|
||||
-- "tag_weights": {"恐龙": 5, "公主": 2, "太空": 3}
|
||||
-- }
|
||||
|
||||
-- 统计数据
|
||||
stories_count INTEGER DEFAULT 0,
|
||||
total_reading_time INTEGER DEFAULT 0, -- 秒
|
||||
|
||||
-- 时间戳
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
-- 约束
|
||||
CONSTRAINT unique_child_per_user UNIQUE (user_id, name)
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_child_profiles_user_id ON child_profiles(user_id);
|
||||
```
|
||||
|
||||
### 1.2 兴趣标签枚举
|
||||
|
||||
预定义的兴趣标签,前端展示用:
|
||||
|
||||
```python
|
||||
INTEREST_TAGS = {
|
||||
"animals": {
|
||||
"zh": "动物",
|
||||
"icon": "🐾",
|
||||
"subtags": ["恐龙", "猫咪", "狗狗", "兔子", "海洋动物"]
|
||||
},
|
||||
"fantasy": {
|
||||
"zh": "奇幻",
|
||||
"icon": "✨",
|
||||
"subtags": ["公主", "王子", "魔法", "精灵", "龙"]
|
||||
},
|
||||
"adventure": {
|
||||
"zh": "冒险",
|
||||
"icon": "🗺️",
|
||||
"subtags": ["太空", "海盗", "探险", "寻宝"]
|
||||
},
|
||||
"vehicles": {
|
||||
"zh": "交通工具",
|
||||
"icon": "🚗",
|
||||
"subtags": ["汽车", "火车", "飞机", "火箭"]
|
||||
},
|
||||
"nature": {
|
||||
"zh": "自然",
|
||||
"icon": "🌳",
|
||||
"subtags": ["森林", "海洋", "山川", "四季"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 成长主题枚举
|
||||
|
||||
```python
|
||||
GROWTH_THEMES = [
|
||||
{"key": "courage", "zh": "勇气", "description": "克服恐惧,勇敢面对"},
|
||||
{"key": "sharing", "zh": "分享", "description": "学会与他人分享"},
|
||||
{"key": "friendship", "zh": "友谊", "description": "交朋友,珍惜友情"},
|
||||
{"key": "honesty", "zh": "诚实", "description": "说真话,不撒谎"},
|
||||
{"key": "independence", "zh": "独立", "description": "自己的事情自己做"},
|
||||
{"key": "kindness", "zh": "善良", "description": "帮助他人,关爱弱小"},
|
||||
{"key": "patience", "zh": "耐心", "description": "学会等待,不急躁"},
|
||||
{"key": "curiosity", "zh": "好奇", "description": "探索未知,爱问为什么"}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、SQLAlchemy 模型
|
||||
|
||||
```python
|
||||
# backend/app/db/models.py
|
||||
|
||||
from sqlalchemy import Column, String, Date, Integer, ForeignKey, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
import uuid
|
||||
|
||||
class ChildProfile(Base):
|
||||
__tablename__ = "child_profiles"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
# 基础信息
|
||||
name = Column(String(50), nullable=False)
|
||||
avatar_url = Column(String(500))
|
||||
birth_date = Column(Date)
|
||||
gender = Column(String(10))
|
||||
|
||||
# 偏好
|
||||
interests = Column(JSON, default=list)
|
||||
growth_themes = Column(JSON, default=list)
|
||||
reading_preferences = Column(JSON, default=dict)
|
||||
|
||||
# 统计
|
||||
stories_count = Column(Integer, default=0)
|
||||
total_reading_time = Column(Integer, default=0)
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
user = relationship("User", back_populates="child_profiles")
|
||||
story_universes = relationship("StoryUniverse", back_populates="child_profile", cascade="all, delete-orphan")
|
||||
|
||||
@property
|
||||
def age(self) -> int | None:
|
||||
"""计算年龄"""
|
||||
if not self.birth_date:
|
||||
return None
|
||||
today = date.today()
|
||||
return today.year - self.birth_date.year - (
|
||||
(today.month, today.day) < (self.birth_date.month, self.birth_date.day)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Pydantic Schema
|
||||
|
||||
```python
|
||||
# backend/app/schemas/child_profile.py
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import date
|
||||
from uuid import UUID
|
||||
|
||||
class ChildProfileCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=50)
|
||||
birth_date: date | None = None
|
||||
gender: str | None = Field(None, pattern="^(male|female|other)$")
|
||||
interests: list[str] = Field(default_factory=list)
|
||||
growth_themes: list[str] = Field(default_factory=list)
|
||||
|
||||
class ChildProfileUpdate(BaseModel):
|
||||
name: str | None = Field(None, min_length=1, max_length=50)
|
||||
birth_date: date | None = None
|
||||
gender: str | None = Field(None, pattern="^(male|female|other)$")
|
||||
interests: list[str] | None = None
|
||||
growth_themes: list[str] | None = None
|
||||
avatar_url: str | None = None
|
||||
|
||||
class ChildProfileResponse(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
avatar_url: str | None
|
||||
birth_date: date | None
|
||||
gender: str | None
|
||||
age: int | None
|
||||
interests: list[str]
|
||||
growth_themes: list[str]
|
||||
stories_count: int
|
||||
total_reading_time: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ChildProfileListResponse(BaseModel):
|
||||
profiles: list[ChildProfileResponse]
|
||||
total: int
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、API 实现
|
||||
|
||||
```python
|
||||
# backend/app/api/profiles.py
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from uuid import UUID
|
||||
|
||||
router = APIRouter(prefix="/api/profiles", tags=["profiles"])
|
||||
|
||||
@router.get("", response_model=ChildProfileListResponse)
|
||||
async def list_profiles(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取当前用户的所有孩子档案"""
|
||||
profiles = await db.execute(
|
||||
select(ChildProfile)
|
||||
.where(ChildProfile.user_id == current_user.id)
|
||||
.order_by(ChildProfile.created_at)
|
||||
)
|
||||
profiles = profiles.scalars().all()
|
||||
return ChildProfileListResponse(profiles=profiles, total=len(profiles))
|
||||
|
||||
@router.post("", response_model=ChildProfileResponse, status_code=201)
|
||||
async def create_profile(
|
||||
data: ChildProfileCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""创建孩子档案"""
|
||||
# 检查是否超过限制 (每用户最多5个孩子档案)
|
||||
count = await db.scalar(
|
||||
select(func.count(ChildProfile.id))
|
||||
.where(ChildProfile.user_id == current_user.id)
|
||||
)
|
||||
if count >= 5:
|
||||
raise HTTPException(400, "最多只能创建5个孩子档案")
|
||||
|
||||
profile = ChildProfile(user_id=current_user.id, **data.model_dump())
|
||||
db.add(profile)
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
return profile
|
||||
|
||||
@router.get("/{profile_id}", response_model=ChildProfileResponse)
|
||||
async def get_profile(
|
||||
profile_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取单个孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
return profile
|
||||
|
||||
@router.put("/{profile_id}", response_model=ChildProfileResponse)
|
||||
async def update_profile(
|
||||
profile_id: UUID,
|
||||
data: ChildProfileUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""更新孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(profile, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
return profile
|
||||
|
||||
@router.delete("/{profile_id}", status_code=204)
|
||||
async def delete_profile(
|
||||
profile_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""删除孩子档案"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
if not profile or profile.user_id != current_user.id:
|
||||
raise HTTPException(404, "档案不存在")
|
||||
|
||||
await db.delete(profile)
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、隐式偏好学习
|
||||
|
||||
### 5.1 行为事件
|
||||
|
||||
```python
|
||||
class ReadingEvent(BaseModel):
|
||||
"""阅读行为事件"""
|
||||
profile_id: UUID
|
||||
story_id: UUID
|
||||
event_type: Literal["started", "completed", "skipped", "replayed"]
|
||||
reading_time: int # 秒
|
||||
timestamp: datetime
|
||||
```
|
||||
|
||||
### 5.2 偏好更新算法
|
||||
|
||||
```python
|
||||
async def update_reading_preferences(
|
||||
db: AsyncSession,
|
||||
profile_id: UUID,
|
||||
story: Story,
|
||||
event: ReadingEvent
|
||||
):
|
||||
"""根据阅读行为更新隐式偏好"""
|
||||
profile = await db.get(ChildProfile, profile_id)
|
||||
prefs = profile.reading_preferences or {}
|
||||
tag_weights = prefs.get("tag_weights", {})
|
||||
|
||||
# 权重调整
|
||||
weight_delta = {
|
||||
"completed": 1.0, # 完整阅读,正向
|
||||
"replayed": 1.5, # 重复播放,强正向
|
||||
"skipped": -0.5, # 跳过,负向
|
||||
"started": 0.1 # 开始阅读,弱正向
|
||||
}
|
||||
|
||||
delta = weight_delta.get(event.event_type, 0)
|
||||
|
||||
for tag in story.tags:
|
||||
current = tag_weights.get(tag, 0)
|
||||
tag_weights[tag] = max(0, current + delta) # 不低于0
|
||||
|
||||
# 更新阅读长度偏好
|
||||
if event.event_type == "completed":
|
||||
word_count = len(story.content)
|
||||
if word_count < 300:
|
||||
length_pref = "short"
|
||||
elif word_count < 600:
|
||||
length_pref = "medium"
|
||||
else:
|
||||
length_pref = "long"
|
||||
|
||||
# 简单的移动平均
|
||||
prefs["preferred_length"] = length_pref
|
||||
|
||||
prefs["tag_weights"] = tag_weights
|
||||
profile.reading_preferences = prefs
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、数据迁移
|
||||
|
||||
```python
|
||||
# backend/alembic/versions/xxx_add_child_profiles.py
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'child_profiles',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('user_id', sa.UUID(), nullable=False),
|
||||
sa.Column('name', sa.String(50), nullable=False),
|
||||
sa.Column('avatar_url', sa.String(500)),
|
||||
sa.Column('birth_date', sa.Date()),
|
||||
sa.Column('gender', sa.String(10)),
|
||||
sa.Column('interests', sa.JSON(), server_default='[]'),
|
||||
sa.Column('growth_themes', sa.JSON(), server_default='[]'),
|
||||
sa.Column('reading_preferences', sa.JSON(), server_default='{}'),
|
||||
sa.Column('stories_count', sa.Integer(), server_default='0'),
|
||||
sa.Column('total_reading_time', sa.Integer(), server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_child_profiles_user_id', 'child_profiles', ['user_id'])
|
||||
|
||||
def downgrade():
|
||||
op.drop_index('idx_child_profiles_user_id')
|
||||
op.drop_table('child_profiles')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、隐私与安全
|
||||
|
||||
### 7.1 数据加密
|
||||
|
||||
敏感字段(姓名、出生日期)在存储时加密:
|
||||
|
||||
```python
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
class EncryptedChildProfile:
|
||||
"""加密存储的孩子档案"""
|
||||
|
||||
@staticmethod
|
||||
def encrypt_name(name: str, key: bytes) -> str:
|
||||
f = Fernet(key)
|
||||
return f.encrypt(name.encode()).decode()
|
||||
|
||||
@staticmethod
|
||||
def decrypt_name(encrypted: str, key: bytes) -> str:
|
||||
f = Fernet(key)
|
||||
return f.decrypt(encrypted.encode()).decode()
|
||||
```
|
||||
|
||||
### 7.2 访问控制
|
||||
|
||||
- 孩子档案只能被创建者访问
|
||||
- 删除用户时级联删除所有孩子档案
|
||||
- API 层强制校验 `user_id` 归属
|
||||
|
||||
### 7.3 数据保留
|
||||
|
||||
- 用户可随时删除孩子档案
|
||||
- 删除后 30 天内可恢复(软删除)
|
||||
- 30 天后永久删除
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -174,4 +174,4 @@
|
||||
- PRD 里的“记忆系统”完整章节
|
||||
- 数据模型(含字段 + 时序衰减)
|
||||
- 交互与界面草案
|
||||
- 后端实现拆解(任务清单 + 里程碑)
|
||||
- 后端实现拆解(任务清单 + 里程碑)
|
||||
|
||||
@@ -126,4 +126,4 @@ CREATE TABLE push_events (
|
||||
## 八、相关文档
|
||||
|
||||
- [记忆智能系统 PRD](./MEMORY-INTELLIGENCE-PRD.md)
|
||||
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
|
||||
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
|
||||
|
||||
@@ -228,4 +228,4 @@ def downgrade():
|
||||
## 九、相关文档
|
||||
|
||||
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
|
||||
- [记忆智能系统 PRD](./MEMORY-INTELLIGENCE-PRD.md)
|
||||
- [记忆智能系统 PRD](./MEMORY-INTELLIGENCE-PRD.md)
|
||||
|
||||
@@ -1,130 +1,130 @@
|
||||
# DreamWeaver 产品愿景与全流程规划
|
||||
|
||||
## 一、产品定位
|
||||
|
||||
### 1.1 愿景
|
||||
**梦语织机** - 为 3-8 岁儿童打造的 AI 故事创作平台,让每个孩子都能拥有专属的成长故事。
|
||||
|
||||
### 1.2 核心价值
|
||||
| 维度 | 价值主张 |
|
||||
|------|----------|
|
||||
| 个性化 | 基于关键词/主角定制,每个故事独一无二 |
|
||||
| 教育性 | 融入成长主题(勇气、友谊、诚实等) |
|
||||
| 沉浸感 | AI 封面 + 语音朗读,多感官体验 |
|
||||
| 亲子互动 | 家长参与创作,增进亲子关系 |
|
||||
|
||||
### 1.3 目标用户
|
||||
**主要用户:家长(25-40岁)**
|
||||
- 需求:为孩子找到有教育意义的睡前故事
|
||||
- 痛点:市面故事千篇一律,缺乏个性化
|
||||
- 场景:睡前、旅途、周末亲子时光
|
||||
|
||||
**次要用户:幼儿园/早教机构**
|
||||
- 需求:批量生成教学故事素材
|
||||
- 痛点:内容制作成本高
|
||||
|
||||
---
|
||||
|
||||
## 二、竞品分析
|
||||
|
||||
| 产品 | 优势 | 劣势 | 我们的差异化 |
|
||||
|------|------|------|--------------|
|
||||
| 凯叔讲故事 | 内容丰富、品牌知名 | 无个性化、订阅贵 | AI 个性化生成 |
|
||||
| 喜马拉雅儿童 | 海量音频、多平台 | 内容同质化 | 用户参与创作 |
|
||||
| ChatGPT | AI 能力强 | 非儿童专属、无配套 | 垂直场景优化 |
|
||||
| Midjourney | 图像质量高 | 无故事整合 | 故事+图像+音频一体 |
|
||||
|
||||
---
|
||||
|
||||
## 三、产品路线图
|
||||
|
||||
### Phase 1: MVP 完善 ✅ 已完成
|
||||
- [x] 关键词生成故事
|
||||
- [x] 故事润色增强
|
||||
- [x] AI 封面生成
|
||||
- [x] 语音朗读
|
||||
- [x] 故事收藏管理
|
||||
- [x] OAuth 登录
|
||||
- [x] 工程鲁棒性改进
|
||||
|
||||
### Phase 2: 体验增强
|
||||
| 功能 | 优先级 | 用户价值 |
|
||||
|------|--------|----------|
|
||||
| 故事编辑 | P0 | 用户可修改 AI 生成内容 |
|
||||
| 角色定制 | P0 | 孩子成为故事主角 |
|
||||
| 故事续写 | P1 | 形成系列故事 |
|
||||
| 多语言支持 | P1 | 英文故事学习 |
|
||||
| 故事分享 | P1 | 社交传播 |
|
||||
|
||||
### Phase 3: 供应商平台化
|
||||
| 功能 | 优先级 | 技术价值 |
|
||||
|------|--------|----------|
|
||||
| 供应商管理后台 | P0 | 可视化配置 AI 供应商 |
|
||||
| 适配器插件化 | P0 | 新供应商零代码接入 |
|
||||
| 供应商健康监控 | P1 | 自动故障转移 |
|
||||
| A/B 测试框架 | P1 | 供应商效果对比 |
|
||||
| 成本分析面板 | P2 | API 调用成本追踪 |
|
||||
|
||||
### Phase 4: 社区与增长
|
||||
| 功能 | 优先级 | 增长价值 |
|
||||
|------|--------|----------|
|
||||
| 故事广场 | P0 | 内容发现 |
|
||||
| 点赞/收藏 | P0 | 社区互动 |
|
||||
| 创作者主页 | P1 | 用户留存 |
|
||||
| 故事模板 | P1 | 降低创作门槛 |
|
||||
|
||||
### Phase 5: 商业化
|
||||
| 功能 | 优先级 | 商业价值 |
|
||||
|------|--------|----------|
|
||||
| 会员订阅 | P0 | 核心收入 |
|
||||
| 故事导出 | P0 | 增值服务 |
|
||||
| 实体书打印 | P1 | 高客单价 |
|
||||
| API 开放 | P2 | B 端收入 |
|
||||
|
||||
---
|
||||
|
||||
## 四、核心指标 (KPIs)
|
||||
|
||||
### 4.1 用户指标
|
||||
| 指标 | 定义 | 目标 |
|
||||
|------|------|------|
|
||||
| DAU | 日活跃用户 | Phase 2: 1000+ |
|
||||
| 留存率 | 次日/7日/30日 | 40%/25%/15% |
|
||||
| 创作转化率 | 访问→创作 | 30%+ |
|
||||
|
||||
### 4.2 业务指标
|
||||
| 指标 | 定义 | 目标 |
|
||||
|------|------|------|
|
||||
| 故事生成量 | 日均生成数 | 5000+ |
|
||||
| 分享率 | 故事被分享比例 | 10%+ |
|
||||
| 付费转化率 | 免费→付费 | 5%+ |
|
||||
|
||||
### 4.3 技术指标
|
||||
| 指标 | 定义 | 目标 |
|
||||
|------|------|------|
|
||||
| API 成功率 | 供应商调用成功率 | 99%+ |
|
||||
| 响应时间 | 故事生成 P95 | <30s |
|
||||
| 成本/故事 | 单个故事 API 成本 | <$0.05 |
|
||||
|
||||
---
|
||||
|
||||
## 五、风险与应对
|
||||
|
||||
| 风险 | 影响 | 概率 | 应对策略 |
|
||||
|------|------|------|----------|
|
||||
| AI 生成内容不当 | 高 | 中 | 内容审核 + 家长控制 + 敏感词过滤 |
|
||||
| API 成本过高 | 高 | 中 | 多供应商比价 + 缓存优化 + 分级限流 |
|
||||
| 供应商服务中断 | 高 | 低 | 多供应商冗余 + 自动故障转移 |
|
||||
| 用户增长缓慢 | 中 | 中 | 社区运营 + 分享裂变 + SEO |
|
||||
| 竞品模仿 | 低 | 高 | 快速迭代 + 深耕垂直 + 数据壁垒 |
|
||||
|
||||
---
|
||||
|
||||
## 六、下一步讨论议题
|
||||
|
||||
1. **供应商平台化架构** - 如何设计插件化的适配器系统?
|
||||
2. **Phase 2 功能优先级** - 先做哪个功能?
|
||||
3. **技术选型** - nanobanana vs flux vs 其他图像供应商?
|
||||
4. **商业模式** - 免费/付费边界在哪里?
|
||||
|
||||
请确认以上产品愿景是否符合预期,我们再深入讨论供应商平台化的技术架构。
|
||||
# DreamWeaver 产品愿景与全流程规划
|
||||
|
||||
## 一、产品定位
|
||||
|
||||
### 1.1 愿景
|
||||
**梦语织机** - 为 3-8 岁儿童打造的 AI 故事创作平台,让每个孩子都能拥有专属的成长故事。
|
||||
|
||||
### 1.2 核心价值
|
||||
| 维度 | 价值主张 |
|
||||
|------|----------|
|
||||
| 个性化 | 基于关键词/主角定制,每个故事独一无二 |
|
||||
| 教育性 | 融入成长主题(勇气、友谊、诚实等) |
|
||||
| 沉浸感 | AI 封面 + 语音朗读,多感官体验 |
|
||||
| 亲子互动 | 家长参与创作,增进亲子关系 |
|
||||
|
||||
### 1.3 目标用户
|
||||
**主要用户:家长(25-40岁)**
|
||||
- 需求:为孩子找到有教育意义的睡前故事
|
||||
- 痛点:市面故事千篇一律,缺乏个性化
|
||||
- 场景:睡前、旅途、周末亲子时光
|
||||
|
||||
**次要用户:幼儿园/早教机构**
|
||||
- 需求:批量生成教学故事素材
|
||||
- 痛点:内容制作成本高
|
||||
|
||||
---
|
||||
|
||||
## 二、竞品分析
|
||||
|
||||
| 产品 | 优势 | 劣势 | 我们的差异化 |
|
||||
|------|------|------|--------------|
|
||||
| 凯叔讲故事 | 内容丰富、品牌知名 | 无个性化、订阅贵 | AI 个性化生成 |
|
||||
| 喜马拉雅儿童 | 海量音频、多平台 | 内容同质化 | 用户参与创作 |
|
||||
| ChatGPT | AI 能力强 | 非儿童专属、无配套 | 垂直场景优化 |
|
||||
| Midjourney | 图像质量高 | 无故事整合 | 故事+图像+音频一体 |
|
||||
|
||||
---
|
||||
|
||||
## 三、产品路线图
|
||||
|
||||
### Phase 1: MVP 完善 ✅ 已完成
|
||||
- [x] 关键词生成故事
|
||||
- [x] 故事润色增强
|
||||
- [x] AI 封面生成
|
||||
- [x] 语音朗读
|
||||
- [x] 故事收藏管理
|
||||
- [x] OAuth 登录
|
||||
- [x] 工程鲁棒性改进
|
||||
|
||||
### Phase 2: 体验增强
|
||||
| 功能 | 优先级 | 用户价值 |
|
||||
|------|--------|----------|
|
||||
| 故事编辑 | P0 | 用户可修改 AI 生成内容 |
|
||||
| 角色定制 | P0 | 孩子成为故事主角 |
|
||||
| 故事续写 | P1 | 形成系列故事 |
|
||||
| 多语言支持 | P1 | 英文故事学习 |
|
||||
| 故事分享 | P1 | 社交传播 |
|
||||
|
||||
### Phase 3: 供应商平台化
|
||||
| 功能 | 优先级 | 技术价值 |
|
||||
|------|--------|----------|
|
||||
| 供应商管理后台 | P0 | 可视化配置 AI 供应商 |
|
||||
| 适配器插件化 | P0 | 新供应商零代码接入 |
|
||||
| 供应商健康监控 | P1 | 自动故障转移 |
|
||||
| A/B 测试框架 | P1 | 供应商效果对比 |
|
||||
| 成本分析面板 | P2 | API 调用成本追踪 |
|
||||
|
||||
### Phase 4: 社区与增长
|
||||
| 功能 | 优先级 | 增长价值 |
|
||||
|------|--------|----------|
|
||||
| 故事广场 | P0 | 内容发现 |
|
||||
| 点赞/收藏 | P0 | 社区互动 |
|
||||
| 创作者主页 | P1 | 用户留存 |
|
||||
| 故事模板 | P1 | 降低创作门槛 |
|
||||
|
||||
### Phase 5: 商业化
|
||||
| 功能 | 优先级 | 商业价值 |
|
||||
|------|--------|----------|
|
||||
| 会员订阅 | P0 | 核心收入 |
|
||||
| 故事导出 | P0 | 增值服务 |
|
||||
| 实体书打印 | P1 | 高客单价 |
|
||||
| API 开放 | P2 | B 端收入 |
|
||||
|
||||
---
|
||||
|
||||
## 四、核心指标 (KPIs)
|
||||
|
||||
### 4.1 用户指标
|
||||
| 指标 | 定义 | 目标 |
|
||||
|------|------|------|
|
||||
| DAU | 日活跃用户 | Phase 2: 1000+ |
|
||||
| 留存率 | 次日/7日/30日 | 40%/25%/15% |
|
||||
| 创作转化率 | 访问→创作 | 30%+ |
|
||||
|
||||
### 4.2 业务指标
|
||||
| 指标 | 定义 | 目标 |
|
||||
|------|------|------|
|
||||
| 故事生成量 | 日均生成数 | 5000+ |
|
||||
| 分享率 | 故事被分享比例 | 10%+ |
|
||||
| 付费转化率 | 免费→付费 | 5%+ |
|
||||
|
||||
### 4.3 技术指标
|
||||
| 指标 | 定义 | 目标 |
|
||||
|------|------|------|
|
||||
| API 成功率 | 供应商调用成功率 | 99%+ |
|
||||
| 响应时间 | 故事生成 P95 | <30s |
|
||||
| 成本/故事 | 单个故事 API 成本 | <$0.05 |
|
||||
|
||||
---
|
||||
|
||||
## 五、风险与应对
|
||||
|
||||
| 风险 | 影响 | 概率 | 应对策略 |
|
||||
|------|------|------|----------|
|
||||
| AI 生成内容不当 | 高 | 中 | 内容审核 + 家长控制 + 敏感词过滤 |
|
||||
| API 成本过高 | 高 | 中 | 多供应商比价 + 缓存优化 + 分级限流 |
|
||||
| 供应商服务中断 | 高 | 低 | 多供应商冗余 + 自动故障转移 |
|
||||
| 用户增长缓慢 | 中 | 中 | 社区运营 + 分享裂变 + SEO |
|
||||
| 竞品模仿 | 低 | 高 | 快速迭代 + 深耕垂直 + 数据壁垒 |
|
||||
|
||||
---
|
||||
|
||||
## 六、下一步讨论议题
|
||||
|
||||
1. **供应商平台化架构** - 如何设计插件化的适配器系统?
|
||||
2. **Phase 2 功能优先级** - 先做哪个功能?
|
||||
3. **技术选型** - nanobanana vs flux vs 其他图像供应商?
|
||||
4. **商业模式** - 免费/付费边界在哪里?
|
||||
|
||||
请确认以上产品愿景是否符合预期,我们再深入讨论供应商平台化的技术架构。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,169 +1,169 @@
|
||||
# DreamWeaver 产品路线图
|
||||
|
||||
## 产品愿景
|
||||
|
||||
**梦语织机** - 为 3-8 岁儿童打造的 AI 故事创作平台,让每个孩子都能拥有专属的成长故事。
|
||||
|
||||
### 核心价值主张
|
||||
- **个性化**: 基于关键词生成独一无二的故事
|
||||
- **教育性**: 融入成长主题(勇气、友谊、诚实等)
|
||||
- **沉浸感**: AI 封面 + 语音朗读,多感官体验
|
||||
- **亲子互动**: 家长参与创作,增进亲子关系
|
||||
|
||||
---
|
||||
|
||||
## 用户画像
|
||||
|
||||
### 主要用户:家长(25-40岁)
|
||||
- **需求**: 为孩子找到有教育意义的睡前故事
|
||||
- **痛点**: 市面故事千篇一律,缺乏个性化
|
||||
- **场景**: 睡前、旅途、周末亲子时光
|
||||
|
||||
### 次要用户:幼儿园/早教机构
|
||||
- **需求**: 批量生成教学故事素材
|
||||
- **痛点**: 内容制作成本高
|
||||
- **场景**: 课堂教学、活动策划
|
||||
|
||||
---
|
||||
|
||||
## 功能规划
|
||||
|
||||
### Phase 1: MVP 完善(当前)
|
||||
> 目标:核心体验闭环,用户可完整使用
|
||||
|
||||
| 功能 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 关键词生成故事 | ✅ 已完成 | 输入关键词,AI 生成故事 |
|
||||
| 故事润色增强 | ✅ 已完成 | 用户提供草稿,AI 润色 |
|
||||
| AI 封面生成 | ✅ 已完成 | 根据故事生成插画 |
|
||||
| 语音朗读 | ✅ 已完成 | TTS 朗读故事 |
|
||||
| 故事收藏管理 | ✅ 已完成 | 保存、查看、删除 |
|
||||
| OAuth 登录 | ✅ 已完成 | GitHub/Google 登录 |
|
||||
|
||||
### Phase 2: 体验增强
|
||||
> 目标:提升用户粘性,增加互动性
|
||||
|
||||
| 功能 | 优先级 | 说明 |
|
||||
|------|--------|------|
|
||||
| **故事编辑** | P0 | 用户可修改 AI 生成的故事内容 |
|
||||
| **角色定制** | P0 | 输入孩子姓名/性别,成为故事主角 |
|
||||
| **故事续写** | P1 | 基于已有故事继续创作下一章 |
|
||||
| **多语言支持** | P1 | 英文故事生成(已有 i18n 基础) |
|
||||
| **故事分享** | P1 | 生成分享图片/链接 |
|
||||
| **收藏夹/标签** | P2 | 故事分类管理 |
|
||||
|
||||
### Phase 3: 社区与增长
|
||||
> 目标:构建用户社区,实现自然增长
|
||||
|
||||
| 功能 | 优先级 | 说明 |
|
||||
|------|--------|------|
|
||||
| **故事广场** | P0 | 公开优质故事,用户可浏览 |
|
||||
| **点赞/收藏** | P0 | 社区互动基础 |
|
||||
| **故事模板** | P1 | 预设故事框架(冒险/友谊/成长) |
|
||||
| **创作者主页** | P1 | 展示用户创作的故事集 |
|
||||
| **评论系统** | P2 | 用户交流反馈 |
|
||||
|
||||
### Phase 4: 商业化
|
||||
> 目标:建立可持续商业模式
|
||||
|
||||
| 功能 | 优先级 | 说明 |
|
||||
|------|--------|------|
|
||||
| **会员订阅** | P0 | 免费/基础/高级三档 |
|
||||
| **故事导出** | P0 | PDF/电子书格式导出 |
|
||||
| **实体书打印** | P1 | 对接印刷服务,生成实体绘本 |
|
||||
| **API 开放** | P2 | 为 B 端客户提供 API |
|
||||
| **企业版** | P2 | 幼儿园/早教机构定制 |
|
||||
|
||||
---
|
||||
|
||||
## 技术架构演进
|
||||
|
||||
### 当前架构 (Phase 1)
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Vue 3 │────▶│ FastAPI │────▶│ PostgreSQL │
|
||||
│ Frontend │ │ Backend │ │ (Neon) │
|
||||
└─────────────┘ └──────┬──────┘ └─────────────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Gemini │ │ Minimax │ │ Flux │
|
||||
│ (Text) │ │ (TTS) │ │ (Image) │
|
||||
└────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
### Phase 2 架构演进
|
||||
```
|
||||
新增组件:
|
||||
- Redis: 缓存 + 会话 + Rate Limit
|
||||
- Celery: 异步任务队列(图片/音频生成)
|
||||
- S3/OSS: 静态资源存储
|
||||
```
|
||||
|
||||
### Phase 3 架构演进
|
||||
```
|
||||
新增组件:
|
||||
- Elasticsearch: 故事全文搜索
|
||||
- CDN: 静态资源加速
|
||||
- 消息队列: 社区通知推送
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 里程碑规划
|
||||
|
||||
### M1: MVP 完善 ✅
|
||||
- [x] 核心功能闭环
|
||||
- [x] 工程鲁棒性改进
|
||||
- [x] 测试覆盖
|
||||
|
||||
### M2: 体验增强
|
||||
- [ ] 故事编辑功能
|
||||
- [ ] 角色定制(孩子成为主角)
|
||||
- [ ] 故事续写
|
||||
- [ ] 多语言支持
|
||||
- [ ] 分享功能
|
||||
|
||||
### M3: 社区上线
|
||||
- [ ] 故事广场
|
||||
- [ ] 用户互动(点赞/收藏)
|
||||
- [ ] 创作者主页
|
||||
|
||||
### M4: 商业化
|
||||
- [ ] 会员体系
|
||||
- [ ] 故事导出
|
||||
- [ ] 实体书打印
|
||||
|
||||
---
|
||||
|
||||
## 竞品分析
|
||||
|
||||
| 产品 | 优势 | 劣势 | 我们的差异化 |
|
||||
|------|------|------|--------------|
|
||||
| 凯叔讲故事 | 内容丰富、品牌知名 | 无个性化、订阅贵 | AI 个性化生成 |
|
||||
| 喜马拉雅儿童 | 海量音频、多平台 | 内容同质化 | 用户参与创作 |
|
||||
| ChatGPT | AI 能力强 | 非儿童专属、无配套 | 垂直场景优化 |
|
||||
|
||||
---
|
||||
|
||||
## 风险与应对
|
||||
|
||||
| 风险 | 影响 | 应对策略 |
|
||||
|------|------|----------|
|
||||
| AI 生成内容不当 | 高 | 内容审核 + 家长控制 |
|
||||
| API 成本过高 | 中 | 缓存优化 + 分级限流 |
|
||||
| 用户增长缓慢 | 中 | 社区运营 + 分享裂变 |
|
||||
| 竞品模仿 | 低 | 快速迭代 + 深耕垂直 |
|
||||
|
||||
---
|
||||
|
||||
## 下一步行动
|
||||
|
||||
**Phase 2 优先实现功能:**
|
||||
|
||||
1. **故事编辑** - 用户体验核心痛点
|
||||
2. **角色定制** - 差异化竞争力
|
||||
3. **故事分享** - 自然增长引擎
|
||||
|
||||
是否需要我为这些功能生成详细的技术规格文档?
|
||||
# DreamWeaver 产品路线图
|
||||
|
||||
## 产品愿景
|
||||
|
||||
**梦语织机** - 为 3-8 岁儿童打造的 AI 故事创作平台,让每个孩子都能拥有专属的成长故事。
|
||||
|
||||
### 核心价值主张
|
||||
- **个性化**: 基于关键词生成独一无二的故事
|
||||
- **教育性**: 融入成长主题(勇气、友谊、诚实等)
|
||||
- **沉浸感**: AI 封面 + 语音朗读,多感官体验
|
||||
- **亲子互动**: 家长参与创作,增进亲子关系
|
||||
|
||||
---
|
||||
|
||||
## 用户画像
|
||||
|
||||
### 主要用户:家长(25-40岁)
|
||||
- **需求**: 为孩子找到有教育意义的睡前故事
|
||||
- **痛点**: 市面故事千篇一律,缺乏个性化
|
||||
- **场景**: 睡前、旅途、周末亲子时光
|
||||
|
||||
### 次要用户:幼儿园/早教机构
|
||||
- **需求**: 批量生成教学故事素材
|
||||
- **痛点**: 内容制作成本高
|
||||
- **场景**: 课堂教学、活动策划
|
||||
|
||||
---
|
||||
|
||||
## 功能规划
|
||||
|
||||
### Phase 1: MVP 完善(当前)
|
||||
> 目标:核心体验闭环,用户可完整使用
|
||||
|
||||
| 功能 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 关键词生成故事 | ✅ 已完成 | 输入关键词,AI 生成故事 |
|
||||
| 故事润色增强 | ✅ 已完成 | 用户提供草稿,AI 润色 |
|
||||
| AI 封面生成 | ✅ 已完成 | 根据故事生成插画 |
|
||||
| 语音朗读 | ✅ 已完成 | TTS 朗读故事 |
|
||||
| 故事收藏管理 | ✅ 已完成 | 保存、查看、删除 |
|
||||
| OAuth 登录 | ✅ 已完成 | GitHub/Google 登录 |
|
||||
|
||||
### Phase 2: 体验增强
|
||||
> 目标:提升用户粘性,增加互动性
|
||||
|
||||
| 功能 | 优先级 | 说明 |
|
||||
|------|--------|------|
|
||||
| **故事编辑** | P0 | 用户可修改 AI 生成的故事内容 |
|
||||
| **角色定制** | P0 | 输入孩子姓名/性别,成为故事主角 |
|
||||
| **故事续写** | P1 | 基于已有故事继续创作下一章 |
|
||||
| **多语言支持** | P1 | 英文故事生成(已有 i18n 基础) |
|
||||
| **故事分享** | P1 | 生成分享图片/链接 |
|
||||
| **收藏夹/标签** | P2 | 故事分类管理 |
|
||||
|
||||
### Phase 3: 社区与增长
|
||||
> 目标:构建用户社区,实现自然增长
|
||||
|
||||
| 功能 | 优先级 | 说明 |
|
||||
|------|--------|------|
|
||||
| **故事广场** | P0 | 公开优质故事,用户可浏览 |
|
||||
| **点赞/收藏** | P0 | 社区互动基础 |
|
||||
| **故事模板** | P1 | 预设故事框架(冒险/友谊/成长) |
|
||||
| **创作者主页** | P1 | 展示用户创作的故事集 |
|
||||
| **评论系统** | P2 | 用户交流反馈 |
|
||||
|
||||
### Phase 4: 商业化
|
||||
> 目标:建立可持续商业模式
|
||||
|
||||
| 功能 | 优先级 | 说明 |
|
||||
|------|--------|------|
|
||||
| **会员订阅** | P0 | 免费/基础/高级三档 |
|
||||
| **故事导出** | P0 | PDF/电子书格式导出 |
|
||||
| **实体书打印** | P1 | 对接印刷服务,生成实体绘本 |
|
||||
| **API 开放** | P2 | 为 B 端客户提供 API |
|
||||
| **企业版** | P2 | 幼儿园/早教机构定制 |
|
||||
|
||||
---
|
||||
|
||||
## 技术架构演进
|
||||
|
||||
### 当前架构 (Phase 1)
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Vue 3 │────▶│ FastAPI │────▶│ PostgreSQL │
|
||||
│ Frontend │ │ Backend │ │ (Neon) │
|
||||
└─────────────┘ └──────┬──────┘ └─────────────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Gemini │ │ Minimax │ │ Flux │
|
||||
│ (Text) │ │ (TTS) │ │ (Image) │
|
||||
└────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
### Phase 2 架构演进
|
||||
```
|
||||
新增组件:
|
||||
- Redis: 缓存 + 会话 + Rate Limit
|
||||
- Celery: 异步任务队列(图片/音频生成)
|
||||
- S3/OSS: 静态资源存储
|
||||
```
|
||||
|
||||
### Phase 3 架构演进
|
||||
```
|
||||
新增组件:
|
||||
- Elasticsearch: 故事全文搜索
|
||||
- CDN: 静态资源加速
|
||||
- 消息队列: 社区通知推送
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 里程碑规划
|
||||
|
||||
### M1: MVP 完善 ✅
|
||||
- [x] 核心功能闭环
|
||||
- [x] 工程鲁棒性改进
|
||||
- [x] 测试覆盖
|
||||
|
||||
### M2: 体验增强
|
||||
- [ ] 故事编辑功能
|
||||
- [ ] 角色定制(孩子成为主角)
|
||||
- [ ] 故事续写
|
||||
- [ ] 多语言支持
|
||||
- [ ] 分享功能
|
||||
|
||||
### M3: 社区上线
|
||||
- [ ] 故事广场
|
||||
- [ ] 用户互动(点赞/收藏)
|
||||
- [ ] 创作者主页
|
||||
|
||||
### M4: 商业化
|
||||
- [ ] 会员体系
|
||||
- [ ] 故事导出
|
||||
- [ ] 实体书打印
|
||||
|
||||
---
|
||||
|
||||
## 竞品分析
|
||||
|
||||
| 产品 | 优势 | 劣势 | 我们的差异化 |
|
||||
|------|------|------|--------------|
|
||||
| 凯叔讲故事 | 内容丰富、品牌知名 | 无个性化、订阅贵 | AI 个性化生成 |
|
||||
| 喜马拉雅儿童 | 海量音频、多平台 | 内容同质化 | 用户参与创作 |
|
||||
| ChatGPT | AI 能力强 | 非儿童专属、无配套 | 垂直场景优化 |
|
||||
|
||||
---
|
||||
|
||||
## 风险与应对
|
||||
|
||||
| 风险 | 影响 | 应对策略 |
|
||||
|------|------|----------|
|
||||
| AI 生成内容不当 | 高 | 内容审核 + 家长控制 |
|
||||
| API 成本过高 | 中 | 缓存优化 + 分级限流 |
|
||||
| 用户增长缓慢 | 中 | 社区运营 + 分享裂变 |
|
||||
| 竞品模仿 | 低 | 快速迭代 + 深耕垂直 |
|
||||
|
||||
---
|
||||
|
||||
## 下一步行动
|
||||
|
||||
**Phase 2 优先实现功能:**
|
||||
|
||||
1. **故事编辑** - 用户体验核心痛点
|
||||
2. **角色定制** - 差异化竞争力
|
||||
3. **故事分享** - 自然增长引擎
|
||||
|
||||
是否需要我为这些功能生成详细的技术规格文档?
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
# DreamWeaver 工程鲁棒性改进计划
|
||||
|
||||
## 概述
|
||||
本计划旨在提升 DreamWeaver 项目的工程质量,包括测试覆盖、稳定性、可观测性等方面。
|
||||
|
||||
## 任务列表
|
||||
|
||||
### P0 - 关键问题修复
|
||||
|
||||
#### Task-1: 修复 Rate Limit 内存泄漏 ✅
|
||||
- **文件**: `backend/app/api/stories.py`
|
||||
- **方案**: 已迁移至 Redis 分布式限流,内存泄漏问题不再存在
|
||||
|
||||
#### Task-2: 添加核心 API 测试 ✅
|
||||
- **文件**: `backend/tests/`
|
||||
- **范围**: test_auth, test_stories, test_profiles, test_universes, test_push_configs, test_reading_events, test_provider_router
|
||||
|
||||
### P1 - 稳定性提升
|
||||
|
||||
#### Task-3: 添加 API 重试机制 ✅
|
||||
- **方案**: 所有适配器已使用 `tenacity` 指数退避重试 (gemini, openai, cqtai, antigravity, minimax, elevenlabs)
|
||||
|
||||
#### Task-4: 添加结构化日志 ✅
|
||||
- **文件**: `backend/app/core/logging.py`
|
||||
- **方案**: structlog JSON/Console 双模式,所有适配器和 provider_router 已集成
|
||||
|
||||
### P2 - 代码优化
|
||||
|
||||
#### Task-5: 重构 Provider Router ✅
|
||||
- **文件**: `backend/app/services/provider_router.py`
|
||||
- **方案**: 已实现统一 `_route_with_failover` 函数
|
||||
|
||||
#### Task-6: 配置外部化 ✅
|
||||
- **文件**: `backend/app/core/config.py`, `backend/app/services/provider_router.py`
|
||||
- **方案**: 所有模型名已移至 Settings,支持环境变量覆盖
|
||||
|
||||
#### Task-7: 修复脆弱的 URL 解析 ✅
|
||||
- **状态**: `drawing.py` 已被适配器系统取代,不再存在
|
||||
|
||||
## 新增依赖 (已添加)
|
||||
```toml
|
||||
# pyproject.toml [project.dependencies]
|
||||
cachetools>=5.0.0 # Task-1: TTL cache
|
||||
tenacity>=8.0.0 # Task-3: 重试机制
|
||||
structlog>=24.0.0 # Task-4: 结构化日志
|
||||
|
||||
# [project.optional-dependencies.dev]
|
||||
pytest-cov>=4.0.0 # Task-2: 覆盖率报告
|
||||
```
|
||||
# DreamWeaver 工程鲁棒性改进计划
|
||||
|
||||
## 概述
|
||||
本计划旨在提升 DreamWeaver 项目的工程质量,包括测试覆盖、稳定性、可观测性等方面。
|
||||
|
||||
## 任务列表
|
||||
|
||||
### P0 - 关键问题修复
|
||||
|
||||
#### Task-1: 修复 Rate Limit 内存泄漏 ✅
|
||||
- **文件**: `backend/app/api/stories.py`
|
||||
- **方案**: 已迁移至 Redis 分布式限流,内存泄漏问题不再存在
|
||||
|
||||
#### Task-2: 添加核心 API 测试 ✅
|
||||
- **文件**: `backend/tests/`
|
||||
- **范围**: test_auth, test_stories, test_profiles, test_universes, test_push_configs, test_reading_events, test_provider_router
|
||||
|
||||
### P1 - 稳定性提升
|
||||
|
||||
#### Task-3: 添加 API 重试机制 ✅
|
||||
- **方案**: 所有适配器已使用 `tenacity` 指数退避重试 (gemini, openai, cqtai, antigravity, minimax, elevenlabs)
|
||||
|
||||
#### Task-4: 添加结构化日志 ✅
|
||||
- **文件**: `backend/app/core/logging.py`
|
||||
- **方案**: structlog JSON/Console 双模式,所有适配器和 provider_router 已集成
|
||||
|
||||
### P2 - 代码优化
|
||||
|
||||
#### Task-5: 重构 Provider Router ✅
|
||||
- **文件**: `backend/app/services/provider_router.py`
|
||||
- **方案**: 已实现统一 `_route_with_failover` 函数
|
||||
|
||||
#### Task-6: 配置外部化 ✅
|
||||
- **文件**: `backend/app/core/config.py`, `backend/app/services/provider_router.py`
|
||||
- **方案**: 所有模型名已移至 Settings,支持环境变量覆盖
|
||||
|
||||
#### Task-7: 修复脆弱的 URL 解析 ✅
|
||||
- **状态**: `drawing.py` 已被适配器系统取代,不再存在
|
||||
|
||||
## 新增依赖 (已添加)
|
||||
```toml
|
||||
# pyproject.toml [project.dependencies]
|
||||
cachetools>=5.0.0 # Task-1: TTL cache
|
||||
tenacity>=8.0.0 # Task-3: 重试机制
|
||||
structlog>=24.0.0 # Task-4: 结构化日志
|
||||
|
||||
# [project.optional-dependencies.dev]
|
||||
pytest-cov>=4.0.0 # Task-2: 覆盖率报告
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user