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,44 +1,48 @@
{
"permissions": {
"allow": [
"Skill(codex)",
"Bash(pip install:*)",
"Bash(alembic upgrade:*)",
"Bash(uvicorn:*)",
"Bash(npm run dev)",
"Bash(python:*)",
"Bash(ruff check:*)",
"Bash(tasklist:*)",
"Bash(findstr:*)",
"Bash(pushd:*)",
"Bash(popd)",
"Bash(curl:*)",
"WebSearch",
"WebFetch(domain:www.novelai.net)",
"WebFetch(domain:www.storywizard.ai)",
"WebFetch(domain:www.oscarstories.com)",
"WebFetch(domain:www.moshi.com)",
"WebFetch(domain:www.calm.com)",
"WebFetch(domain:www.epic.com)",
"WebFetch(domain:www.headspace.com)",
"WebFetch(domain:www.getepic.com)",
"WebFetch(domain:www.tonies.com)",
"Bash(del:*)",
"Bash(netstat:*)",
"Bash(taskkill:*)",
"Bash(codex-wrapper:*)",
"Bash(dir /b /s /a-d /o-d)",
"Bash(dir:*)",
"Bash(.venv/Scripts/python:*)",
"Bash(.venv/Scripts/ruff check:*)",
"Bash(npm run build:*)",
"Bash(test -f \"F:\\\\Code\\\\dreamweaver-python\\\\backend\\\\.env\")",
"Bash(pytest:*)",
"Bash(npm run type-check:*)",
"Bash(npx vue-tsc:*)",
"Bash(ls:*)",
"Bash(git init:*)",
"Bash(git add:*)"
]
}
}
{
"permissions": {
"allow": [
"Skill(codex)",
"Bash(pip install:*)",
"Bash(alembic upgrade:*)",
"Bash(uvicorn:*)",
"Bash(npm run dev)",
"Bash(python:*)",
"Bash(ruff check:*)",
"Bash(tasklist:*)",
"Bash(findstr:*)",
"Bash(pushd:*)",
"Bash(popd)",
"Bash(curl:*)",
"WebSearch",
"WebFetch(domain:www.novelai.net)",
"WebFetch(domain:www.storywizard.ai)",
"WebFetch(domain:www.oscarstories.com)",
"WebFetch(domain:www.moshi.com)",
"WebFetch(domain:www.calm.com)",
"WebFetch(domain:www.epic.com)",
"WebFetch(domain:www.headspace.com)",
"WebFetch(domain:www.getepic.com)",
"WebFetch(domain:www.tonies.com)",
"Bash(del:*)",
"Bash(netstat:*)",
"Bash(taskkill:*)",
"Bash(codex-wrapper:*)",
"Bash(dir /b /s /a-d /o-d)",
"Bash(dir:*)",
"Bash(.venv/Scripts/python:*)",
"Bash(.venv/Scripts/ruff check:*)",
"Bash(npm run build:*)",
"Bash(test -f \"F:\\\\Code\\\\dreamweaver-python\\\\backend\\\\.env\")",
"Bash(pytest:*)",
"Bash(npm run type-check:*)",
"Bash(npx vue-tsc:*)",
"Bash(ls:*)",
"Bash(git init:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git remote add:*)",
"Bash(git push:*)",
"Bash(git branch:*)"
]
}
}

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
```

View File

@@ -150,4 +150,4 @@
## 推荐
建议 Web MVP 使用方案 ASoft Aurora兼顾温暖与信任。方案 B/C 可作为后续主题或 A/B 测试备选。
建议 Web MVP 使用方案 ASoft Aurora兼顾温暖与信任。方案 B/C 可作为后续主题或 A/B 测试备选。

File diff suppressed because it is too large Load Diff

View File

@@ -227,4 +227,4 @@
- 组件做 Variant
- 全部使用 Auto Layout
- 1440/1200/1024/768 建立栅格
- 状态页复制并标注
- 状态页复制并标注

View File

@@ -296,4 +296,4 @@
- 设计系统库(色板、文字、组件)
- 全流程高保真页面
- 原型链接Figma 中生成)
- 原型链接Figma 中生成)

View File

@@ -56,4 +56,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -71,4 +71,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -85,4 +85,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -55,4 +55,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -108,4 +108,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -30,4 +30,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -25,4 +25,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -81,4 +81,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -17,4 +17,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -75,4 +75,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -70,4 +70,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -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; }
}
}

View File

@@ -61,4 +61,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -61,4 +61,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -56,4 +56,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -71,4 +71,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -85,4 +85,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -55,4 +55,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -108,4 +108,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -30,4 +30,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -25,4 +25,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -81,4 +81,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -17,4 +17,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -75,4 +75,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -70,4 +70,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -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; }
}
}

View File

@@ -61,4 +61,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -61,4 +61,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -56,4 +56,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -71,4 +71,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -85,4 +85,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -55,4 +55,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -108,4 +108,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -30,4 +30,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -25,4 +25,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -81,4 +81,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -17,4 +17,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -75,4 +75,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -70,4 +70,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -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; }
}
}

View File

@@ -61,4 +61,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -61,4 +61,4 @@
</div>
</div>
</body>
</html>
</html>

View File

@@ -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

View File

@@ -174,4 +174,4 @@
- PRD 里的“记忆系统”完整章节
- 数据模型(含字段 + 时序衰减)
- 交互与界面草案
- 后端实现拆解(任务清单 + 里程碑)
- 后端实现拆解(任务清单 + 里程碑)

View File

@@ -126,4 +126,4 @@ CREATE TABLE push_events (
## 八、相关文档
- [记忆智能系统 PRD](./MEMORY-INTELLIGENCE-PRD.md)
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)

View File

@@ -228,4 +228,4 @@ def downgrade():
## 九、相关文档
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
- [记忆智能系统 PRD](./MEMORY-INTELLIGENCE-PRD.md)
- [记忆智能系统 PRD](./MEMORY-INTELLIGENCE-PRD.md)

View File

@@ -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

View File

@@ -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. **故事分享** - 自然增长引擎
是否需要我为这些功能生成详细的技术规格文档?

View File

@@ -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: 覆盖率报告
```

View File

@@ -299,5 +299,5 @@ TASK-024 [x]: 性能优化 - 减少 backdrop-filter
阶段一 (TASK-001 ~ 003) → 阶段二 (TASK-004 ~ 012) → 阶段三 (TASK-013 ~ 021) → 阶段四 (TASK-022 ~ 024)
每个任务完成后运行 npm run build 确保无类型错误。