Files
meijiaka-zy/docs/semantic-refactoring-plan.md
T

21 KiB
Raw Blame History

后端语义治理与架构重构计划

范围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

P0shot/segment/scene 的三重命名 + 四处重复定义

  • 症状ScriptShotSchema)、ShotDataAPI)、ShotTaskService)、ShotUnit(Scheduler)四者并存,字段名 scene/scene_desctype/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_idKling 主体 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

P1task / 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_idimage_task_idcache_err 等 Celery 时代残留;script 任务仍走 BackgroundTask,其他任务走 Scheduler
  • 风险:双轨运行导致统一监控、重试、日志无法覆盖全部任务类型
  • 关键文件app/scheduler/handlers/video_handler.py, app/scheduler/handlers/image_handler.py, app/api/v1/tasks.py

P2CRUD 层裸字典更新

  • 症状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)  │
│  术语: 仅使用底层技术术语                                │
└─────────────────────────────────────────────────────────┘

核心禁令

  1. elementomnikling_task_id供应商术语禁止出现在 Layer 3 以上
  2. shot 禁止出现在 Layer 3 以上(Kling 术语,业务层统一叫 segment
  3. task 禁止出现在 Layer 4Scheduler 内部统一叫 job
  4. dict[str, Any] 禁止出现在跨层传递的接口中

三、重构阶段(Phase 1-5

每个 Phase 独立成组,建议按顺序执行。每个 Phase 完成后必须跑通 pytestmake lint


Phase 1Schema 统一 + 状态机 Enum 化

目标:建立"单一真相源",消除 shot/segment/scene 的四重定义 预估工时3-4 天 影响面:全项目基础类型

Task 1.1:新建统一术语 Schema

  • 新建 app/schemas/segment.py
    class 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, failed
    • SegmentStatus: pending, submitted, completed, failed
    • AvatarCloneStatus: pending, voice_processing, voice_failed, element_pending, element_processing, element_failed, succeed
    • KlingTaskStatus: submitted, processing, succeed, failed(仅限 Provider 层使用)
  • 新建 app/schemas/job.py,定义 JobParams Union
    • VideoJobParams(含 segments: list[Segment]
    • ImageJobParams
    • SubtitleJobParams
    • CopyJobParams
    • AvatarCloneJobParams

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.pyTaskRecord.status 类型改为 JobStatus
  • app/services/kling_video_service.pyVideoGenerationJob.status 类型改为 JobStatus
  • app/models/avatar.pyAvatar.status 类型改为 AvatarCloneStatus
  • app/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 2Scheduler 层全面"去 task 化"

目标:消除 task 的五重语义混用,建立 Job 专属语义域 预估工时3-4 天 影响面app/scheduler/ 目录及引用方

Task 2.1:核心模型与 Registry 重命名

  • app/scheduler/models.pyTaskRecordJobRecord
  • app/scheduler/registry.pyTaskRegistryJobRegistry
    • 所有 task_id 参数/字段 → job_id
    • 所有 task_type 参数/字段 → job_type
    • running_task_idsrunning_job_ids
  • app/scheduler/engine.pyAsyncEngine 中所有 taskjob

Task 2.2Registry 承担全部序列化职责

  • JobRegistry.get() 中统一完成 JSON 反序列化
    • 解析 params 字段时,根据 job_type 路由到正确的 JobParams Pydantic 模型
    • 保证 Handler 拿到的 job.params 永远是强类型对象
  • 删除 video_handler.pyimage_handler.py 中所有的 isinstance(shots, str): json.loads 逻辑
  • 删除 _download_and_upload 中的手动 JSON 类型判断

Task 2.3StateChange 取代裸字典

  • 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.pytick() 返回类型改为 list[StateChange]
  • 修改所有 Handlerchanges.append({"task_id": ..., "field": ...})changes.append(StateChange(job_id=..., field=..., value=...))

Task 2.4API 层适配

  • app/api/v1/tasks.py 中:内部变量名 task_id 在引用 Scheduler 时改为 job_idAPI 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/ 返回为空(仅 StateChange dataclass 内部可保留)
  • pytest 通过

Phase 3:建立"供应商防火墙"Adapter 层隔离)

目标:将 Kling/Volc 术语彻底下压到 Provider 层,业务层以上使用纯业务语义 预估工时4-5 天 影响面API Schema、DB 模型、Scheduler 模型、Provider 层

Task 3.1API 层删除 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_idhuman_id
    • VideoParams 中的 shotssegments

Task 3.2:DB 模型增加供应商抽象

  • app/models/avatar.py
    • element_id: Mapped[int | None]provider_element_id: Mapped[int | None]
    • voice_task_idprovider_voice_job_id
    • element_task_idprovider_element_job_id
    • 新增 provider: Mapped[str] = mapped_column(default="kling")(为未来多供应商做准备)
  • 生成 Alembic 迁移脚本(字段重命名 + 新增字段)

Task 3.3Scheduler 模型供应商抽象

  • app/scheduler/models.pyPhase 2 后的 JobRecordSegment):
    • kling_task_idprovider_task_id
    • 如需追溯供应商,增加 provider: str = "kling"

Task 3.4Provider 返回值模型化

  • 新建 app/ai/providers/kling_dto.py
    • KlingVideoResult
    • KlingImageResult
    • KlingVoiceResult
    • KlingElementResult
  • 修改 app/ai/providers/klingai_provider.py
    • 所有返回裸 dict[str, Any] 的方法改为返回对应的 Kling*Result
    • 状态字段类型改为 KlingTaskStatus

Task 3.5Prompt 语法迁移到 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.6Service 层适配器映射

  • 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 entryregistry entry
    • cache_errregistry_err
    • Failed to update cacheFailed 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 的逻辑迁移至此
    • 继承 AsyncHandlername = "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 5CRUD 层强类型化

目标:消灭 CRUD 层的裸字典更新 预估工时2 天 影响面CRUD Base、Avatar CRUD、Scheduler Handler

Task 5.1CRUD Base 类型约束

  • app/crud/base.py
    • obj_in: dict[str, Any]obj_in: CreateSchemaType | UpdateSchemaType
    • 保留 dict 仅作为 update 的 fallback,但所有业务调用方优先使用 Schema

Task 5.2Avatar 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.3Handler 调用方改造

  • 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.yamlMakefile 中增加 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 保留为 SegmentTypeAlias 一个 Sprint,待前端适配后删除
Phase 2 JobRegistry 重命名导致 API 层引用遗漏 使用 IDE 全局重构(PyCharm/Ruff/Rename Symbol),执行后跑全量 pytest
Phase 3 DB 字段重命名需要数据迁移 Alembic 脚本必须包含 op.alter_columnexisting_typeexisting_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

七、相关文档