21 KiB
后端语义治理与架构重构计划
范围:
python-api/app/全目录
目标:根治需求调整与 Celery→Async Scheduler 迁移导致的命名腐败、语义漂移、类型漂移问题
原则:长期稳定优先,不计短期开发成本
状态:计划中(待团队评审后执行)
一、问题诊断
经过深度代码扫描,当前代码存在六大类语义腐败,按严重程度排序:
P0:调度器核心模型的 params: dict[str, Any] 类型漂移
- 症状:所有 Handler 从裸字典里
params.get("shots"),且因 Redis 序列化反复写if isinstance(shots, str): shots = json.loads(shots) - 风险:这是 Scheduler 取代 Celery 后最脆弱的环节,任何字段名改动都会导致运行时崩溃
- 关键文件:
app/scheduler/models.py,app/scheduler/registry.py,app/scheduler/handlers/video_handler.py
P0:shot/segment/scene 的三重命名 + 四处重复定义
- 症状:
ScriptShot(Schema)、ShotData(API)、ShotTask(Service)、ShotUnit(Scheduler)四者并存,字段名scene/scene_desc、type/shot_type混用 - 风险:任何分镜字段改动必须改 4 个类,极易遗漏
- 关键文件:
app/schemas/script.py,app/api/v1/video.py,app/services/kling_video_service.py,app/scheduler/models.py
P0:Kling 供应商语义大规模泄漏到业务层和 API 层
- 症状:
element_id(Kling 主体 ID)、kling_task_id、Omni prompt 语法<<<element_1>>>直接出现在 API Schema、Scheduler 模型、数据库模型中 - 风险:一旦更换视频供应商,影响面会穿透所有层级
- 关键文件:
app/api/v1/video.py,app/api/v1/tasks.py,app/models/avatar.py,app/scheduler/handlers/video_handler.py
P1:task / task_id 的五重语义混用
- 症状:FastAPI BackgroundTask、Scheduler Task、Kling API Task、AnyToCopy Task、Volcengine Task 都叫
task - 风险:日志堆栈中无法区分层级,调试极其困难
- 关键文件:
app/api/v1/tasks.py,app/scheduler/,app/ai/providers/klingai_provider.py,app/services/
P1:历史残留命名与双轨运行
- 症状:
# 兼容旧字段、video_task_id、image_task_id、cache_err等 Celery 时代残留;script任务仍走 BackgroundTask,其他任务走 Scheduler - 风险:双轨运行导致统一监控、重试、日志无法覆盖全部任务类型
- 关键文件:
app/scheduler/handlers/video_handler.py,app/scheduler/handlers/image_handler.py,app/api/v1/tasks.py
P2:CRUD 层裸字典更新
- 症状:
avatar_crud.update(db, db_obj=avatar, obj_in={"status": "element_pending"}) - 风险:拼写错误、状态值非法无法被静态检查捕获
- 关键文件:
app/crud/base.py,app/crud/avatar.py,app/scheduler/handlers/avatar_handler.py
二、架构目标:六层语义治理
我们将整个后端严格划分为 6 个语义层级,每一层只使用属于该层的术语:
┌─────────────────────────────────────────────────────────┐
│ Layer 6: Presentation (API Schema / 前端适配层) │
│ 术语: Segment, Human, Job, Script, Cover │
├─────────────────────────────────────────────────────────┤
│ Layer 5: Application (API 路由 / BackgroundJob) │
│ 术语: Segment, Human, Job, Project │
├─────────────────────────────────────────────────────────┤
│ Layer 4: Orchestration (Scheduler / SlotManager) │
│ 术语: Job, JobRecord, Slot, Handler │
├─────────────────────────────────────────────────────────┤
│ Layer 3: Domain (Service / 业务逻辑) │
│ 术语: Segment, Human, VideoComposition, Caption │
├─────────────────────────────────────────────────────────┤
│ Layer 2: Adapter (Provider Client / 供应商适配) │
│ 术语: KlingJob, KlingElement, VolcJob, ProviderTaskId │
├─────────────────────────────────────────────────────────┤
│ Layer 1: Infrastructure (DB / Redis / HTTP / FileSys) │
│ 术语: 仅使用底层技术术语 │
└─────────────────────────────────────────────────────────┘
核心禁令
element、omni、kling_task_id等供应商术语禁止出现在 Layer 3 以上shot禁止出现在 Layer 3 以上(Kling 术语,业务层统一叫segment)task禁止出现在 Layer 4(Scheduler 内部统一叫job)dict[str, Any]禁止出现在跨层传递的接口中
三、重构阶段(Phase 1-5)
每个 Phase 独立成组,建议按顺序执行。每个 Phase 完成后必须跑通 pytest 和 make lint。
Phase 1:Schema 统一 + 状态机 Enum 化
目标:建立"单一真相源",消除 shot/segment/scene 的四重定义 预估工时:3-4 天 影响面:全项目基础类型
Task 1.1:新建统一术语 Schema
- 新建
app/schemas/segment.pyclass Segment(BaseModel): id: str type: Literal["segment", "empty_shot"] scene: str # 统一为 scene,删除 scene_desc voiceover: str duration: int | None = None human_id: str | None = None # 业务术语,对应 Kling 的 element_id status: SegmentStatus = SegmentStatus.PENDING provider_task_id: str | None = None video_url: str | None = None local_path: str | None = None query_fail_count: int = 0 - 新建
app/schemas/enums.py,定义以下 Enum:JobStatus: pending, running, completed, failedSegmentStatus: pending, submitted, completed, failedAvatarCloneStatus: pending, voice_processing, voice_failed, element_pending, element_processing, element_failed, succeedKlingTaskStatus: submitted, processing, succeed, failed(仅限 Provider 层使用)
- 新建
app/schemas/job.py,定义JobParamsUnion:VideoJobParams(含segments: list[Segment])ImageJobParamsSubtitleJobParamsCopyJobParamsAvatarCloneJobParams
Task 1.2:删除重复定义
- 删除
app/scheduler/models.py中的ShotUnit - 删除/重构
app/services/kling_video_service.py中的ShotTask(字段迁移到Segment) - 删除
app/api/v1/video.py中的ShotData,改为引用Segment - 将
app/schemas/script.py中的ScriptShot改为Segment的别名或类型适配器
Task 1.3:字段名统一
- 批量将
scene_desc重命名为scene(覆盖kling_video_service.py,video_handler.py等) - 批量将
shot_type重命名为type(在业务层/Schema 层;Provider 层保留shot_type仅用于 Kling API 调用) app/api/v1/tasks.py中的shots: list[dict]改为segments: list[Segment]
Task 1.4:状态字符串 Enum 化
app/scheduler/models.py中TaskRecord.status类型改为JobStatusapp/services/kling_video_service.py中VideoGenerationJob.status类型改为JobStatusapp/models/avatar.py中Avatar.status类型改为AvatarCloneStatusapp/ai/providers/klingai_provider.py中所有 Kling 状态字符串操作改为KlingTaskStatus
验收标准
grep -rn "class ShotUnit\|class ShotTask\|class ShotData\|class ScriptShot" app/返回为空(除了注释或别名声明)grep -rn "scene_desc" app/ | grep -v "__pycache__"返回为空mypy app/schemas/无类型错误pytest通过
Phase 2:Scheduler 层全面"去 task 化"
目标:消除 task 的五重语义混用,建立 Job 专属语义域
预估工时:3-4 天
影响面:app/scheduler/ 目录及引用方
Task 2.1:核心模型与 Registry 重命名
app/scheduler/models.py:TaskRecord→JobRecordapp/scheduler/registry.py:TaskRegistry→JobRegistry- 所有
task_id参数/字段 →job_id - 所有
task_type参数/字段 →job_type running_task_ids→running_job_ids
- 所有
app/scheduler/engine.py:AsyncEngine中所有task→job
Task 2.2:Registry 承担全部序列化职责
- 在
JobRegistry.get()中统一完成 JSON 反序列化- 解析
params字段时,根据job_type路由到正确的JobParamsPydantic 模型 - 保证 Handler 拿到的
job.params永远是强类型对象
- 解析
- 删除
video_handler.py和image_handler.py中所有的isinstance(shots, str): json.loads逻辑 - 删除
_download_and_upload中的手动 JSON 类型判断
Task 2.3:StateChange 取代裸字典
- 在
app/scheduler/models.py中定义:@dataclass(frozen=True) class StateChange: job_id: str field: str value: Any - 修改
app/scheduler/engine.py:_apply_changes(self, changes: list[dict[str, Any]])→_apply_changes(self, changes: list[StateChange])- 序列化逻辑移入
StateChange.to_redis_command()方法
- 修改
app/scheduler/handlers/base.py:tick()返回类型改为list[StateChange] - 修改所有 Handler:
changes.append({"task_id": ..., "field": ...})→changes.append(StateChange(job_id=..., field=..., value=...))
Task 2.4:API 层适配
app/api/v1/tasks.py中:内部变量名task_id在引用 Scheduler 时改为job_id(API URL/tasks/{task_id}保持不变,仅内部变量和注释调整)app/api/v1/avatar.py中:引用TaskRegistry的地方改为JobRegistry
验收标准
grep -rn "TaskRecord\|TaskRegistry\|running_task_ids" app/scheduler/返回为空grep -rn "isinstance(.*shots.*str)" app/scheduler/handlers/返回为空grep -rn '"task_id":' app/scheduler/handlers/返回为空(仅StateChangedataclass 内部可保留)pytest通过
Phase 3:建立"供应商防火墙"(Adapter 层隔离)
目标:将 Kling/Volc 术语彻底下压到 Provider 层,业务层以上使用纯业务语义 预估工时:4-5 天 影响面:API Schema、DB 模型、Scheduler 模型、Provider 层
Task 3.1:API 层删除 Kling 术语泄漏
app/api/v1/video.py:element_id: int | None = Field(None, description="Kling主体ID")→human_id: int | None = Field(None, description="数字人主体ID")
app/api/v1/tasks.py:- 同上,所有
element_id→human_id VideoParams中的shots→segments
- 同上,所有
Task 3.2:DB 模型增加供应商抽象
app/models/avatar.py:element_id: Mapped[int | None]→provider_element_id: Mapped[int | None]voice_task_id→provider_voice_job_idelement_task_id→provider_element_job_id- 新增
provider: Mapped[str] = mapped_column(default="kling")(为未来多供应商做准备)
- 生成 Alembic 迁移脚本(字段重命名 + 新增字段)
Task 3.3:Scheduler 模型供应商抽象
app/scheduler/models.py(Phase 2 后的JobRecord及Segment):kling_task_id→provider_task_id- 如需追溯供应商,增加
provider: str = "kling"
Task 3.4:Provider 返回值模型化
- 新建
app/ai/providers/kling_dto.py:KlingVideoResultKlingImageResultKlingVoiceResultKlingElementResult
- 修改
app/ai/providers/klingai_provider.py:- 所有返回裸
dict[str, Any]的方法改为返回对应的Kling*Result - 状态字段类型改为
KlingTaskStatus
- 所有返回裸
Task 3.5:Prompt 语法迁移到 Provider 层
- 删除
app/scheduler/handlers/video_handler.py中的:_build_omni_prompt()_build_empty_shot_video_prompt()
- 在
app/ai/providers/klingai_provider.py中新建KlingPromptBuilder类:class KlingPromptBuilder: @staticmethod def omni_segment(scene: str, voiceover: str, element_id: str | None = None) -> str @staticmethod def empty_shot(scene: str, voiceover: str) -> str video_handler.py调用时只传业务字段(scene,voiceover,human_id),由 Provider 层负责映射为 Kling 语法
Task 3.6:Service 层适配器映射
app/services/kling_video_service.py:- 删除
avatar_id废弃字段 human_id→ 在调用 Provider 时映射为element_id
- 删除
app/services/qiniu_service.py:file_type="avatar"→file_type="clone_video"或"human_video"
验收标准
grep -rn "element_id" app/api/ app/schemas/ app/scheduler/models.py | grep -v "provider_element_id"返回为空grep -rn "kling_task_id" app/api/ app/schemas/ app/scheduler/models.py返回为空grep -rn "<<<element_1>>>\|<<<voice_1>>>" app/scheduler/ app/services/返回为空(仅在 Provider 层保留)- Alembic 迁移脚本可正常升级/降级
pytest通过
Phase 4:清理历史残留 + 消除双轨运行
目标:删除所有 Celery 时代残留,将 script 任务纳入 Scheduler 统一调度
预估工时:2-3 天
影响面:Handler、API 路由、历史命名
Task 4.1:删除兼容旧字段代码
app/scheduler/handlers/video_handler.py:- 删除
shot["video_task_id"] = kling_task_id # 兼容旧字段 - 删除初始化 shots 时的
"video_task_id": None
- 删除
app/scheduler/handlers/image_handler.py:- 删除
params["image_task_id"] = kling_task_id
- 删除
app/services/kling_video_service.py:- 删除
avatar_id字段
- 删除
Task 4.2:修正历史残留命名
app/core/redis_client.py:删除文档字符串中的RateLimiter字样app/api/v1/tasks.py:cache entry→registry entrycache_err→registry_errFailed to update cache→Failed to update registry
app/core/token_manager.py:_background_tasks→_refresh_tasks- 删除
app/services/dto.py
Task 4.3:将 script 任务迁移到 Scheduler
- 新建
app/scheduler/handlers/script_handler.py- 将
app/api/v1/tasks.py中_run_script_task的逻辑迁移至此 - 继承
AsyncHandler,name = "script",不占用 Slot(或占用独立script_slots)
- 将
- 修改
app/api/v1/tasks.py:script任务改为registry.create(job_type="script", ...)- 删除
BackgroundTasks相关参数和_run_script_task函数
- 修改
app/scheduler/main.py:注册ScriptHandler
验收标准
grep -rn "兼容旧字段\|video_task_id\|image_task_id" app/scheduler/ app/services/返回为空grep -rn "cache_err\|cache entry" app/api/v1/tasks.py返回为空app/services/dto.py不存在app/api/v1/tasks.py中无BackgroundTasks导入和使用pytest通过
Phase 5:CRUD 层强类型化
目标:消灭 CRUD 层的裸字典更新 预估工时:2 天 影响面:CRUD Base、Avatar CRUD、Scheduler Handler
Task 5.1:CRUD Base 类型约束
app/crud/base.py:obj_in: dict[str, Any]→obj_in: CreateSchemaType | UpdateSchemaType- 保留
dict仅作为update的 fallback,但所有业务调用方优先使用 Schema
Task 5.2:Avatar Schema 定义
- 新建
app/schemas/avatar.py:class AvatarCreate(BaseModel): name: str video_url: str status: AvatarCloneStatus = AvatarCloneStatus.PENDING class AvatarUpdate(BaseModel): name: str | None = None status: AvatarCloneStatus | None = None provider_voice_job_id: str | None = None provider_element_job_id: str | None = None provider_element_id: int | None = None fail_reason: str | None = None app/crud/avatar.py:改为class CRUDAvatar(CRUDBase[Avatar, AvatarCreate, AvatarUpdate])
Task 5.3:Handler 调用方改造
app/scheduler/handlers/avatar_handler.py:- 所有
_update_avatar(avatar_id, {"status": "..."})改为_update_avatar(avatar_id, AvatarUpdate(status=AvatarCloneStatus.XXX)) - 删除裸字典辅助函数
_update_avatar中的**obj_in展开,改用obj_in.model_dump(exclude_unset=True)
- 所有
验收标准
grep -rn 'obj_in=\{' app/scheduler/handlers/avatar_handler.py返回为空mypy app/crud/无类型错误pytest通过
四、自动化防护网(Phase 5 之后部署)
4.1 预提交钩子:禁词检查
在 .pre-commit-config.yaml 或 Makefile 中增加 lint-semantic:
lint-semantic:
@echo "Checking semantic boundaries..."
@! grep -rn "element_id" app/api/ app/schemas/ app/scheduler/models.py | grep -v "provider_element_id" || (echo "ERROR: element_id leaked to upper layers"; exit 1)
@! grep -rn "kling_task_id" app/api/ app/schemas/ app/scheduler/models.py || (echo "ERROR: kling_task_id leaked to upper layers"; exit 1)
@! grep -rn "scene_desc" app/ | grep -v "__pycache__" || (echo "ERROR: scene_desc not fully renamed"; exit 1)
@! grep -rn "TaskRecord\|TaskRegistry\|running_task_ids" app/scheduler/ || (echo "ERROR: Scheduler task naming not fully migrated"; exit 1)
@! grep -rn "<<<element_1>>>\|<<<voice_1>>>" app/scheduler/ app/services/ || (echo "ERROR: Kling prompt syntax leaked"; exit 1)
@echo "Semantic check passed"
4.2 mypy 渐进严格化
- 在
pyproject.toml中为app/scheduler/和app/schemas/单独开启strict = true - 逐步扩展至
app/api/和app/services/
4.3 AGENTS.md 术语表(Glossary)
在 AGENTS.md 中新增"统一术语表"章节(见下文),所有 AI Agent 修改代码前必须查阅。
五、风险与回滚策略
| 风险 | 影响 | mitigation |
|---|---|---|
Phase 1 删除 ScriptShot 影响前端类型生成 |
中 | ScriptShot 保留为 Segment 的 TypeAlias 一个 Sprint,待前端适配后删除 |
Phase 2 JobRegistry 重命名导致 API 层引用遗漏 |
高 | 使用 IDE 全局重构(PyCharm/Ruff/Rename Symbol),执行后跑全量 pytest |
| Phase 3 DB 字段重命名需要数据迁移 | 中 | Alembic 脚本必须包含 op.alter_column 的 existing_type 和 existing_nullable |
Phase 4 script 迁出 BackgroundTask 后响应时间变长 |
低 | 脚本生成仍立即返回 job_id,前端通过轮询 /tasks/{job_id} 获取结果,接口契约不变 |
| 多 Phase 并行开发导致冲突 | 高 | 严禁并行:必须按 1→2→3→4→5 顺序执行,每个 Phase 合并到主分支后再开始下一个 |
六、作为 GitHub Project Task List 的格式
若导入 GitHub Project,建议按以下结构建 5 个 Milestone:
### Milestone 1: Schema Unification
- [ ] #101 Create `app/schemas/segment.py` with `Segment` model
- [ ] #102 Create `app/schemas/enums.py` with `JobStatus`, `SegmentStatus`, `AvatarCloneStatus`, `KlingTaskStatus`
- [ ] #103 Create `app/schemas/job.py` with `JobParams` Union
- [ ] #104 Remove `ShotUnit` from `app/scheduler/models.py`
- [ ] #105 Remove `ShotTask` from `app/services/kling_video_service.py`
- [ ] #106 Remove `ShotData` from `app/api/v1/video.py`
- [ ] #107 Rename `scene_desc` → `scene` across codebase
- [ ] #108 Migrate all `status` strings to Enums
### Milestone 2: Scheduler De-tasking
- [ ] #201 Rename `TaskRecord` → `JobRecord`
- [ ] #202 Rename `TaskRegistry` → `JobRegistry`
- [ ] #203 Registry auto-deserializes `JobParams` models
- [ ] #204 Replace `dict` changes with `StateChange` dataclass
- [ ] #205 Update all Handlers to return `list[StateChange]`
### Milestone 3: Vendor Firewall
- [ ] #301 API layer: `element_id` → `human_id`
- [ ] #302 DB model: add `provider` field, rename task/element IDs
- [ ] #303 Scheduler model: `kling_task_id` → `provider_task_id`
- [ ] #304 Provider DTOs: `KlingVideoResult`, `KlingImageResult`, etc.
- [ ] #305 Move `KlingPromptBuilder` to Provider layer
- [ ] #306 Alembic migration for avatar table changes
### Milestone 4: Cleanup & Unification
- [ ] #401 Remove legacy compatibility fields (`video_task_id`, `image_task_id`)
- [ ] #402 Fix historical naming (`cache_err`, `RateLimiter` docstrings, etc.)
- [ ] #403 Delete `app/services/dto.py`
- [ ] #404 Migrate `script` task from BackgroundTask to Scheduler
### Milestone 5: CRUD Strong Typing
- [ ] #501 Create `AvatarCreate` / `AvatarUpdate` schemas
- [ ] #502 Type-constrain CRUDBase
- [ ] #503 Refactor `avatar_handler.py` to use `AvatarUpdate` instead of raw dicts
- [ ] #504 Add `lint-semantic` to Makefile / pre-commit
- [ ] #505 Update `AGENTS.md` with Glossary and layer rules
七、相关文档
- 统一异步调度器设计文档
- 数据库设计文档
- AGENTS.md(术语表与分层禁令)