Files
life-echo/docs/plans/backend-architectural-refactor-plan.md

576 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: 架构重构计划
overview: 将现有 Life Echo API 从混合分层结构重构为 feature-first flat 架构,引入 ports/adapters 实现 provider-agnostic 基础设施抽象,集成 Alembic 做 schema 变更治理,统一日志到 loguru。
todos:
- id: phase0-core
content: "Phase 0: 搭建 app/core/ 骨架config, db, logging, errors, security, dependencies, middleware"
status: pending
- id: phase0-txn
content: "Phase 0: 重写 get_async_db() 去掉自动 commit定义事务边界规则"
status: pending
- id: phase0-alembic
content: "Phase 0: 初始化 Alembic手写空 baselinestamp head不依赖 autogenerate"
status: pending
- id: phase0-loguru
content: "Phase 0: 集成 loguru + InterceptHandler + request_id middleware"
status: pending
- id: phase0-errors
content: "Phase 0: 实现全局异常体系 + exception handlers仅错误响应统一成功响应不包装"
status: pending
- id: phase1-ports
content: "Phase 1: 定义 6 个 port protocolsms, llm, image_gen, storage, tts, asr"
status: pending
- id: phase1-adapters
content: "Phase 1: 封装现有 SDK 代码为 adapter 实现"
status: pending
- id: phase1-di
content: "Phase 1: 在 core/dependencies.py 注册 DI factory"
status: pending
- id: phase2-content
content: "Phase 2: 迁移 content featurefaq, legal, home"
status: pending
- id: phase2-plan
content: "Phase 2: 迁移 plan feature"
status: pending
- id: phase2-quota
content: "Phase 2: 提取 quota 为独立 featureQuotaService + router"
status: pending
- id: phase2-user
content: "Phase 2: 迁移 user featureUser model"
status: pending
- id: phase2-auth
content: "Phase 2: 迁移 auth featureRefreshToken + SmsVerificationCode models依赖 user + sms port"
status: pending
- id: phase2-payment
content: "Phase 2: 迁移 payment feature"
status: pending
- id: phase2-memoir
content: "Phase 2: 迁移 memoir featurebooks + chapters + memoir_state 合并)"
status: pending
- id: phase2-conversation
content: "Phase 2: 迁移 conversation feature + WebSocket 拆为 ws/ 子包6 个文件)"
status: pending
- id: phase3-cleanup
content: "Phase 3: 清理旧目录,更新 Docker/CI/测试路径"
status: pending
isProject: false
---
# Life Echo API 架构重构计划
## 一、现状诊断
当前项目位于 `api/` 下,采用混合分层结构:
```
api/
main.py
routers/ # 16 个路由文件,部分含大量业务逻辑
services/ # 混合了业务服务和外部 SDK 调用
database/ # 单文件 models.py9 个模型)+ database.py
agents/ # AI agent 层conversation, memory, memoir
middleware/ # 仅 auth.py
payment/ # 唯一较完整的 feature 模块
tasks/ # Celery 后台任务
migrations/ # 手写 SQL无版本管理
```
**核心问题**
- 路由层过胖,直接操作 DB 和调用外部服务
- 外部服务耦合SMS腾讯云、存储腾讯 COS、TTSOpenAI直接写死 SDK
- 无统一配置类,`os.getenv()` 散落各处
- 无 Alembic手写 SQL 迁移无版本追踪
- 标准库 logging无结构化日志
- 无全局异常处理
- WebSocket 路由 ~1000 行,职责过重
**已有较好抽象的部分**(可复用/迁移):
- `payment/`:已有 service facade + 双 provider
- `services/llm_service.py`:通过 LangChain 已做 provider 切换
- ASR已有 provider 切换机制
---
## 二、目标目录结构
```
api/
app/
main.py # 仅做组装include_router, middleware, lifespan
core/
config.py # 统一 BaseSettingspydantic-settings
db.py # Base, engine, session factory, get_async_db
logging.py # loguru 配置
errors.py # 全局异常体系 + exception handlers
security.py # JWT 签发/校验、密码哈希
middleware.py # CORS, request_id, logging middleware
dependencies.py # 全局共享依赖get_current_user, port DI factory
pagination.py # 通用分页
features/
auth/
router.py # 注册、登录、SMS 登录、刷新、登出
schemas.py
service.py # 认证业务逻辑
models.py # RefreshToken, SmsVerificationCode
repo.py
deps.py # feature 特有依赖(如 sms_port 注入)
conversation/
router.py # 对话 CRUD
schemas.py
service.py # 对话编排(调用 agent、quota 检查等)
models.py # Conversation, Segment
repo.py
ws/ # WebSocket 子包(从 1067 行 websocket.py 拆分)
router.py # WebSocket endpoint仅做连接生命周期
connection_manager.py # 连接注册/注销、active_connections 字典
message_types.py # MessageType 枚举 + 消息序列化
pipeline.py # 消息分发管道audio_segment -> ASR -> agent -> TTS -> 响应
profile_collector.py # 资料收集模式:缺失字段检测、提取、回填
quota_guard.py # 配额校验 hook从 routers/quota 独立)
memoir/
router.py # books, chapters, memoir_state 合并
schemas.py
service.py # 回忆录编排(章节生成、状态流转)
models.py # Book, Chapter, MemoirState
repo.py
processor.py # 从 agents/memoir_processor.py 迁入
payment/
router.py
schemas.py
service.py # 统一支付门面(已有良好抽象,迁移即可)
models.py # Order
repo.py
deps.py
user/
router.py # profile, subscription, feedback
schemas.py
service.py
models.py # User
repo.py
quota/
service.py # PLAN_QUOTAS 配置 + check_can_send/check_can_organize
schemas.py # QuotaCheckResponse
router.py # GET /api/quota/check
deps.py # get_quota_service 注入
plan/
router.py
schemas.py
service.py
content/
router.py # faq, legal, home轻量静态内容
schemas.py
agents/
conversation_agent.py
memory_agent.py
prompts/
conversation_prompts.py
memory_prompts.py
profile_prompts.py
ports/ # 能力接口定义Protocol / ABC
sms.py # SmsSender protocol
llm.py # LLMProvider protocol
image_gen.py # ImageGenerator protocol
storage.py # ObjectStorage protocol
tts.py # TTSProvider protocol
asr.py # ASRProvider protocol
adapters/ # 具体 provider 实现
sms/
tencent.py
llm/
deepseek.py # 基于现有 llm_service.py
image_gen/
liblib.py
storage/
tencent_cos.py
tts/
openai_tts.py
asr/
whisper_local.py
tencent_asr.py
payment/
wechat.py
alipay.py
tasks/
celery_app.py
memoir_tasks.py
alembic/
env.py
versions/
alembic.ini
tests/
...
```
### 目录结构修订说明
**修订 1`core/db.py` 显式列出。** 存放 `Base = declarative_base()`、engine/session factory、`get_async_db` 生成器。所有 feature models 共享此 Base。
**修订 2`core/response.py` 移除。** 统一响应包装只用于错误响应(在 `core/errors.py` 的 exception handler 中处理)。成功响应直接返回 Pydantic model / `FileResponse` / 原始 dict不强制 `{code, data, message}` 包装,避免对 FastAPI schema 自动生成、文件下载、支付回调、WebSocket 造成额外负担。
**修订 3auth models 与 user models 分离。** `RefreshToken``SmsVerificationCode` 归入 `features/auth/models.py`,因为它们是 identity/session 资产,认证流程 (`auth/service.py`) 是唯一强依赖方。`User` 模型留在 `features/user/models.py`auth 通过 import user models 引用 User允许的依赖方向auth -> user
**修订 4WebSocket 从单个 `ws_handler.py` 拆为 `ws/` 子包。** 当前 `routers/websocket.py` 有 1067 行同时承担连接管理、消息类型定义、音频分段处理、ASR 转写、配额校验、用户资料收集、Agent 编排、DB 落库、Celery 任务提交。仅改名为 `ws_handler.py` 会原样搬运复杂度。拆分为 6 个文件,每个职责单一。
**修订 5`quota` 提升为独立 feature。** 当前 `routers/quota.py` 包含 `PLAN_QUOTAS` 配置、`get_segment_count``get_chapter_count``check_can_send_message``check_can_submit_organize`,被 conversationWebSocket line 628、memoir任务提交前 line 1050、plan 共同依赖。放在任何单个 feature 内都会造成反向依赖。独立为 `features/quota/`,其他 feature 的 service 通过注入 `QuotaService` 调用。
---
## 三、事务边界与 Session 管理
### 3.1 现状问题
当前存在两种矛盾的事务策略:
- `get_async_db()` 在请求结束时自动 commit[database.py line 76](api/database/database.py)),但 router 内部又有显式 `await db.commit()`(如 [auth.py line 194](api/routers/auth.py)),导致双重 commit 或语义不清。
- Celery 任务直接用同步 `SessionLocal()`[memoir_tasks.py line 18](api/tasks/memoir_tasks.py)),手动管理 session 生命周期,无统一规则。
- WebSocket handler 用 `async for db in get_async_db()` 获取独立 session[websocket.py line 276](api/routers/websocket.py)),绕过 FastAPI 依赖注入。
### 3.2 新规则
**核心原则repo 不提交service 统一管理事务边界。**
- `core/db.py` 中的 `get_async_db()` 改为"干净 session":只负责创建和关闭,不自动 commit/rollback。
- 事务控制权交给 service 层service 方法内显式 `await db.commit()` 或使用 `async with db.begin():` 块。
- `repo.py` 只做查询和 `db.add()` / `db.delete()`,绝不调用 `commit()``flush()`
- router 层不接触 session通过 `Depends` 注入 serviceservice 内部持有 session
**Celery 任务**:在 `tasks/` 中定义 `get_sync_db()` context manager同样遵循"service commit"规则。任务函数调用 service 的同步版本或直接在任务函数中管理事务。
**WebSocket**`ws/pipeline.py` 中每个消息处理周期获取独立 session通过 `async with AsyncSessionLocal() as session:`),处理完成后由 pipeline 显式 commit。
```python
# core/db.py - 新的 session 生成器
async def get_async_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
# features/auth/service.py - service 管理事务
class AuthService:
def __init__(self, db: AsyncSession, sms: SmsSender):
self._db = db
self._sms = sms
async def register(self, request: RegisterRequest) -> TokenResponse:
user = User(...)
self._db.add(user)
refresh_token = RefreshToken(...)
self._db.add(refresh_token)
await self._db.commit()
await self._db.refresh(user)
return TokenResponse(...)
```
---
## 四、依赖方向规则
用以下有向图描述允许的依赖方向(箭头 = "依赖于"
```mermaid
graph TD
Router["features/*/router.py"] --> Service["features/*/service.py"]
Service --> Repo["features/*/repo.py"]
Service --> Ports["ports/*"]
Service --> QuotaSvc["features/quota/service.py"]
Repo --> Models["features/*/models.py"]
Adapters["adapters/*"] -.->|"实现"| Ports
Core["core/*"] --> Nothing["(无外部依赖)"]
Router --> Core
Service --> Core
Tasks["tasks/*"] --> Service
Agents["agents/*"] --> Ports
WsPipeline["conversation/ws/pipeline.py"] --> Service
WsPipeline --> Ports
```
**硬性禁止**
- `router.py` 不得直接 import adapter 或操作 DB session
- `service.py` 不得 import 具体 adapter只依赖 port protocol
- `ports/` 不得 import `adapters/`(方向反转)
- feature 之间不得互相 import router跨 feature 调用通过 service 注入
- `repo.py` 不得调用 `commit()` / `rollback()` / `flush()`
---
## 四、Provider-Agnostic Ports 设计
### 4.1 核心可移植契约Core Portable Contract
每个 port 定义为 `typing.Protocol`,仅包含业务真正依赖的最小稳定能力:
**SMS** (`ports/sms.py`)
- `send_verification_code(phone: str, code: str) -> bool`
**LLM** (`ports/llm.py`)
- `complete(messages: list[Message], temperature: float, ...) -> str`
- `stream(messages: list[Message], ...) -> AsyncIterator[str]`
**ImageGen** (`ports/image_gen.py`)
- `generate(prompt: str, size: ImageSize, ...) -> ImageResult`
- `check_status(task_id: str) -> TaskStatus`
**Storage** (`ports/storage.py`)
- `upload(key: str, data: bytes, content_type: str) -> str`
- `get_url(key: str, expires: int) -> str`
- `delete(key: str) -> None`
**TTS** (`ports/tts.py`)
- `synthesize(text: str, voice: str) -> bytes`
**ASR** (`ports/asr.py`)
- `transcribe(audio: bytes, format: str) -> str`
### 5.2 Provider Extensions 规则
**绝对禁止 service 直接拿 adapter 扩展方法。** 这会打穿 port 边界,让 service 再次耦合到具体 provider。
处理厂商特有能力的正确方式:
- **方案 A首选扩充现有 port。** 如果能力具备跨 provider 通用性(如 image_gen 的 inpaint在 port protocol 上新增方法,各 adapter 按能力实现或抛 `NotImplementedError`
- **方案 B定义第二个 port。** 如果能力确实只属于某个 provider 的特殊功能(如 Liblib 的模板管理),定义一个独立的窄 port`ImageTemplateManager`),只在需要的 feature deps 中注入。service 依赖的仍然是 port protocol而非 adapter 类。
- **方案 Cadapter 内部自行消化。** 厂商特有的配置项(如 Liblib template UUID、COS 生命周期策略)作为 adapter 构造参数传入,不暴露给 service。adapter 在实现 port 方法时自行使用这些配置。
**判断标准**:如果 service 代码中出现了 `from adapters.xxx import ...`,说明边界被打穿了。
### 4.3 DI 装配
`core/dependencies.py` 中根据 `config.py` 的配置选择 adapter
```python
def get_sms_sender() -> SmsSender:
if settings.sms_provider == "tencent":
return TencentSmsSender(settings.tencent_sms)
raise ValueError(f"Unknown SMS provider: {settings.sms_provider}")
```
feature 的 `deps.py` 或 router 通过 `Depends(get_sms_sender)` 注入。
---
## 五、Alembic 集成与 Schema 变更治理
### 5.1 初始化
-`api/` 根目录执行 `alembic init alembic`
- 修改 `alembic/env.py`,导入所有 feature 的 `models.py` 到统一 `Base.metadata`
- `alembic.ini``sqlalchemy.url` 读取 `DATABASE_URL`
### 6.2 初始基线(不依赖 autogenerate
当前 schema 来源是 `Base.metadata.create_all()` + 9 个手写 SQL 迁移文件,两者可能已经出现漂移。直接 autogenerate 大概率产出不准确的 diff。
**正确步骤**
1. 在 staging/dev 环境连接真实数据库,用 `pg_dump --schema-only` 导出当前实际 schema 作为基准参照。
2. 手写一个空的 Alembic revision 作为 baseline`alembic revision -m "baseline_empty"`upgrade/downgrade 均为 pass。
3. 对现有数据库执行 `alembic stamp head`,将当前库标记为已在 head。
4. **此后**才开始用 autogenerate 生成增量迁移,每次 review 时对照 `pg_dump` 确认无遗漏。
5.`migrations/` 目录下的 9 个手写 SQL 归档到 `migrations_legacy/`,不再使用。
### 5.3 变更治理规则
- 所有 schema 变更必须通过 Alembic migration 合入,禁止直接改 DB
- `autogenerate` 只做草稿PR 中必须人工 review
- 破坏性变更(删列、改类型)采用 expand/contract先加新列 -> 双写 -> 迁移数据 -> 切读路径 -> 删旧列
- data migration 和 schema migration 分开写
- 每次部署前校验 `alembic current` == `alembic heads`
- `main.py` 移除 `Base.metadata.create_all()`,改为部署流程执行 `alembic upgrade head`
### 6.4 Model 拆分
`database/models.py` 的 9 个模型拆入对应 feature
- `User` -> `features/user/models.py`
- `RefreshToken`, `SmsVerificationCode` -> `features/auth/models.py`identity/session 资产auth 是唯一强依赖方)
- `Conversation`, `Segment` -> `features/conversation/models.py`
- `Book`, `Chapter`, `MemoirState` -> `features/memoir/models.py`
- `Order` -> `features/payment/models.py`
所有模型共享同一个 `Base = declarative_base()`(放 `core/db.py`Alembic `env.py` 统一 import 所有 feature models。
**跨 feature model 引用规则**auth/models.py 中的 `RefreshToken.user_id` 外键指向 `users.id`(表名级引用),不需要 import User 类。如需 ORM relationship通过字符串引用 `relationship("User", ...)`
---
## 六、日志统一化loguru
### 6.1 配置
`core/logging.py` 中:
- 移除标准库 `logging.basicConfig`
- 配置 loguruJSON 格式输出、按级别分文件、rotation/retention
- 拦截标准库 logging`InterceptHandler`),使 uvicorn / sqlalchemy / celery 日志统一走 loguru
### 6.2 Request Logging Middleware
新增 middleware每个请求注入 `request_id`UUID通过 loguru 的 `contextualize` 绑定到所有日志:
```python
with logger.contextualize(request_id=request_id, user_id=user_id):
response = await call_next(request)
```
### 6.3 Provider 调用日志
在每个 adapter 中统一记录provider 名称、操作、耗时、成功/失败、错误分类。
---
## 七、全局异常处理
`core/errors.py` 中定义异常体系:
```python
class AppError(Exception):
status_code: int
error_code: str
message: str
class NotFoundError(AppError): ...
class AuthenticationError(AppError): ...
class ProviderError(AppError): ...
```
`main.py` 注册全局 exception handler统一返回格式
```json
{"error_code": "PROVIDER_ERROR", "message": "...", "request_id": "..."}
```
---
## 八、统一配置
将散落的 `os.getenv()` 收归 `core/config.py`,使用 pydantic-settings `BaseSettings`
```python
class Settings(BaseSettings):
database_url: str
redis_url: str
secret_key: str
sms_provider: str = "tencent"
storage_provider: str = "tencent_cos"
image_gen_provider: str = "liblib"
# 嵌套配置
tencent_sms: TencentSmsConfig
tencent_cos: TencentCosConfig
...
model_config = SettingsConfigDict(env_file=".env", env_nested_delimiter="__")
```
---
## 九、迁移执行策略
### Phase 0 - 基础设施搭建(无功能变更)
1. **迁移到 uv**`uv init`,将 `requirements.txt` 的依赖迁入 `pyproject.toml``[project.dependencies]`,删除 `requirements.txt`,生成 `uv.lock` 并纳入版本控制
2. 创建 `app/core/` 骨架config, db, logging, errors, security, dependencies, middleware
3. 重写 `get_async_db()` 去掉自动 commit定义 `get_sync_db()` context manager 供 Celery 使用
4. 初始化 Alembic手写空 baseline revision + `stamp head`(不用 autogenerate
5. 集成 loguru配置 InterceptHandler 拦截标准库 logging
6. 添加 request_id middleware
7. 定义全局异常体系(仅统一错误响应格式)
**验证点**`uv sync --dev` 能安装全部依赖,应用能以新 core 启动,所有现有 API 不受影响。注意 `get_async_db()` 的 commit 语义变更需要同步修改所有现有 router 中缺少显式 commit 的写操作。
### Phase 1 - Ports 定义 + Adapters 迁移
1. 定义 6 个 port protocolsms, llm, image_gen, storage, tts, asr
2. 将现有 SDK 代码封装为 adapter实现对应 protocol
3.`core/dependencies.py` 中注册 DI factory
4. payment adapter 从 `payment/` 直接迁移(已有好的抽象)
**验证点**:所有 adapter 通过 port protocol 类型检查,现有功能不变。
### Phase 2 - Feature 模块重构(逐个 feature
按依赖关系从外到内逐个迁移,建议顺序:
1. `content`faq, legal, home— 最简单,验证 feature 骨架
2. `plan` — 轻量,少依赖
3. `quota` — 提升为独立 feature定义 `QuotaService`
4. `user` — 拆出 User model
5. `auth` — 拆出 RefreshToken/SmsVerificationCode models依赖 user models + sms port
6. `payment` — 迁移现有 payment/ 模块
7. `memoir` — 合并 books/chapters/memoir_state迁移 processor
8. `conversation` — 最复杂,包含 WebSocket 深度拆分(见下)
**每个 feature 迁移步骤**
- 创建 `models.py`(从全局 models.py 拆出)
- 创建 `repo.py`(从 router 中提取 DB 操作,禁止 commit
- 创建 `service.py`(从 router 中提取业务逻辑,负责事务提交)
- 创建 `schemas.py`(收集 Pydantic schema
- 精简 `router.py`(只保留 HTTP 边界:参数解析、响应状态码、调用 service
- 更新 `main.py``include_router`
**conversation WebSocket 拆分详细步骤**
当前 `routers/websocket.py`1067 行)需拆为 `features/conversation/ws/` 子包:
- `message_types.py`:提取 `MessageType` 枚举(当前 line 32-45+ 消息构造工具函数
- `connection_manager.py`:提取 `ConnectionManager` 类(当前 line 49-120只保留连接注册/注销/发送,移除 agent/runner 实例持有
- `profile_collector.py`:提取 `_get_missing_profile_fields``_get_filled_profile_fields``_apply_extracted_profile`(当前 line 854-898和资料收集模式分支line 917-952
- `quota_guard.py`:封装配额校验逻辑(当前 line 627-637调用 `quota/service.py`
- `pipeline.py`:核心消息处理管道,编排 ASR -> 落库 -> Agent -> TTS -> 响应,依赖上述模块 + portsasr, tts, llm
- `router.py`WebSocket endpoint仅做连接生命周期管理accept, 主循环, disconnect将每条消息委托给 pipeline
**验证点**:每迁移一个 feature运行测试确保该 feature API 行为不变。
### Phase 3 - 清理与加固
1. 删除旧 `routers/`, `services/`, `database/models.py`, `middleware/`, `migrations/`
2. 删除 `main.py` 中的 `init_db()` / `create_all()`
3. 更新 Dockerfile改用 uv 安装依赖(`COPY pyproject.toml uv.lock ./` + `uv sync --frozen --no-dev`),或用 `uv export --no-dev > requirements.txt` 导出后继续用 pip
4. 更新 docker-compose、deploy.sh 中的路径和启动命令
5. 更新 Celery worker 启动命令(`uv run celery ...`
6. 更新所有测试 import 路径
7. 补充缺失的测试
---
## 十一、硬性规则(写入项目规范)
1. `**common/` / `utils/` / `shared/` / `helpers/` 禁止创建**:只有 truly cross-cutting 能力进 `core/`,业务 DTO/校验/错误码归 feature
2. **router 禁止操作 DB**:所有数据访问通过 repo所有业务逻辑通过 service
3. **service 禁止 import adapter**:只依赖 port protocol通过 DI 注入。代码中出现 `from adapters.xxx import ...` 即为违规
4. **feature 间禁止 import router**:跨 feature 调用通过 service 注入
5. **所有 schema 变更走 Alembic**:禁止直接 DDL
6. **新增 provider 必须实现 port protocol**:不得在 feature 中直接调用 SDK
7. **事务边界repo 不提交service 管事务**`repo.py` 只做 `add/delete/query``commit/rollback` 由 service 或 UoW 统一执行。`get_async_db()` 不自动 commit
8. **port 边界不可打穿**service 需要厂商增强能力时,必须扩充 port 或定义第二个窄 port禁止直接引用 adapter 扩展方法
9. **quota 是独立 feature**conversation、memoir、payment 如需配额检查,通过注入 `QuotaService`,不得直接 import quota 内部函数
10. **成功响应不强制包装**`core/errors.py` 只统一错误响应格式 `{error_code, message, request_id}`,成功响应直接返回 Pydantic model / FileResponse / 原始结构