feat(phase8): 任务 2.06 — EditTemplateService + EditPlanService 服务层 #155
Reference in New Issue
Block a user
Delete Branch "feature/phase8-task206-service-layer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
任务 2.06 — 服务层重构
变更内容
EditTemplateService:封装模板 + 片段配置的完整业务逻辑(名称唯一性校验、软删除、排序等)EditPlanService:封装计划 CRUD、片段管理、状态机流转、生成工作流edit_templates.py和edit_plans.py重构为委托给 Service 层apps/api/app/services/__init__.py统一导出测试
test_edit_template_service.py:33 个测试(CRUD + 片段配置 + 组合查询)test_edit_plan_service.py:44 个测试(CRUD + 状态机 + 片段管理 + 生成工作流)架构模式
代码审查结果 — PR #155(任务 2.06 + 2.16)
结论:✅ 有条件通过(2 个 P2 建议修复,不阻塞合并)
整体评价
PR 包含两部分工作:
服务层设计合理,职责清晰,测试覆盖全面(50+ 用例)。路由层从直接操作 Repository 改为通过 Service 层,符合分层架构原则。PreviewPlayer 组件功能完整,交互体验良好。
P2 建议(建议修复,不阻塞)
P2-1:update_plan 路由中计划不存在时返回 400 而非 404
edit_plans.py中update_plan路由统一 catch ValueError 后返回 400,但svc.update_plan内部get_plan_or_raise抛出的"计划不存在"应返回 404。建议修复:在 service 层定义自定义异常(如
NotFoundError),路由层据此返回正确的 HTTP 状态码。P2-2:EditTemplateService 名称去重使用全量查询
create_template和update_template中通过list_all(skip=0, limit=1000)全量查询后内存遍历做名称去重,性能不佳。建议修复:在 Repository 层增加
find_by_name(name, status)方法,通过数据库查询直接判断。P2-3:delete_plan 缺少事务保护
先删片段再删计划,如果删除计划时出错,片段已被删除,数据不一致。建议用数据库事务包裹。
P3 建议(不阻塞)
create_clip/update_clip的clip_type为 str,未校验枚举有效性亮点
总结:代码质量良好,架构改进方向正确。P2 建议本轮或下轮迭代修复,不阻塞合并。
✅ 审查通过(有条件)
HEAD SHA:
c110baf5注意:
mergeable=False,需要 rebase 后才能合并。总体评价
服务层抽象合理,职责划分清晰。两个 Service 类(EditPlanService + EditTemplateService)封装了完整的 CRUD、状态机流转和渲染流程。路由层从直接调用 Repository 改为委托 Service 层,代码简洁度显著提升。测试覆盖全面(50+ 用例),Stub Repository 设计合理。PreviewPlayer 前端组件实现完整,播放/暂停、进度条拖拽、时间线跳转功能齐全。
发现的问题
[P2-1]
update_plan路由:计划不存在时返回 400 而非 404edit_plans.py中update_plan路由:当
plan_id不存在时,svc.update_plan()内部get_plan_or_raise()抛出ValueError("剪辑计划不存在"),被外层 catch 后返回 HTTP 400。旧代码显式返回 HTTP 404。这是回退。transition_status同理。建议修复: 在 catch 中区分"不存在"和"业务校验失败":
同类问题也存在于
generate_plan路由(can_generate内部也调用get_plan_or_raise)。[P2-2] EditTemplateService 名称去重使用全量查询
edit_template_service.py的create_template和update_template:全量加载所有模板到内存做名称查重,且有
limit=1000上限,超过 1000 个模板时检查不完整。建议: Repository 层应增加
find_by_name(name)方法,返回单个匹配记录。[P2-3]
delete_plan缺少事务保护两步删除操作没有事务包裹。如果第二步失败,片段已删除但计划残留,数据不一致。
建议: 使用
db.begin()/db.commit()包裹,或在 Repository 层提供级联删除。[P3-1]
create_clip未校验clip_type枚举值EditPlanService.create_clip()的clip_type参数类型为str,未校验是否为合法枚举值(如 "video"/"image"/"audio")。非法值会传递到持久化层。建议: 在 Service 层校验或改用枚举类型。
[P3-2] PreviewPlayer 预览浮层无边界检测
PreviewPlayer.tsx的悬浮预览位置计算:仅限制了 Y 轴最小值,未检查右边界和下边界溢出。当素材卡片在屏幕右侧时,预览图可能超出视口。
[P3-3] PR 混合了任务 2.06(服务层)和任务 2.16(PreviewPlayer)的代码
PR 标题标注为任务 2.06,但包含了 PreviewPlayer.tsx(415行)+ PreviewPlayer.css(426行)+ EditingPlanner.tsx 布局调整。建议后续拆分为独立 PR。
审查通过项
can_generate+mark_clips_ready,关注点分离良好get_generation_status聚合查询封装在 Service 层,路由层极简ep-centerclass 正确迁移到父容器结论
✅ 有条件通过 — 2 个 P2 + 3 个 P3,均不影响功能正确性。P2 建议修复但不阻塞合并。需先 rebase 解决冲突。
c110baf533tob7bb572b46代码审查结果:✅ 有条件通过
PR #155 — feat(phase8): AutoClipService 智能选片服务 (任务 2.07)
Commit:
c110baf533db68b8e0acfb4b2cb558522e6743f7文件数: 11 | +2879 / -0
总体评价
服务层设计清晰,职责划分合理。
AutoClipService作为编排层,_score_candidate评分算法简洁可读,_parse_material_requirements对配置解析做了充分的容错处理。抽象仓储接口 (AssetRepository) 与 SQLAlchemy 实现同步新增search_candidates方法,符合端口/适配器架构。测试覆盖全面(347 行),涵盖了主流程、异常路径和评分逻辑。P2 问题不影响基本功能正确性,不阻塞合并,但建议尽快修复。
🔶 P2 — 建议修复
P2-1:同一素材可被重复分配给多个片段
auto_select_assets的循环中,每个片段独立调用search_candidates并选择最高分素材,但没有追踪已分配的素材 ID。如果素材 A 是片段 1 和片段 2 的共同最佳匹配,A 会被分配给两个片段。assigned_asset_ids: set,在候选评分时排除已分配素材,或在search_candidates查询中加exclude_ids参数。P2-2:分类筛选使用 LIKE 查询 JSON 文本,可能误匹配
若
classification_category = "scenic",会同时匹配{"category": "scenic"}和{"notes": "scenic view"}或{"subcategory": "scenic_art"}。JSON_EXTRACT),或在AssetModel上增加独立的classification_category列。P2-3:批量分配缺少事务保护
auto_select_assets循环中对每个 clip 调用self._clip_repo.update(clip),若中途某个 update 失败,部分片段已更新、部分未更新,产生不一致状态。auto_select_assets入口开启事务,所有 clip 更新完成后统一 commit,失败时 rollback。P2-4:对已就绪片段调用
select_for_clip会抛未处理异常mark_ready()要求片段状态为PENDING,若对已是READY/RENDERED的片段调用select_for_clip,会在mark_ready()处抛出ValueError,但select_for_clip没有前置状态检查。_assign_single_clip入口检查 clip 状态,若已是 READY 或以上则跳过或抛出明确的业务异常。🔷 P3 — 建议优化
P3-1:评分算法文档与实现不一致
max(0.0, 1.0 - abs(1.0 - ratio) * 2)是线性衰减。_parse_material_requirements的分类部分文档说"部分匹配按比例得分",但代码对任何不匹配分类都固定给0.3。P3-2:Tags 在内存中过滤可能遗漏候选
search_candidates先在 SQL 中LIMIT 50(按质量分排序),再在内存中过滤 tags。如果满足 tag 条件的素材排在第 51 位之后,会被遗漏。P3-3:
_parse_material_requirements中min_duration的or回退逻辑依赖
0.0为 falsy 的隐式行为(domain 默认值恰为0.0)。虽然当前逻辑恰好正确,但可读性不佳。if getattr(config, "min_duration", 0) > 0 else requirements.get("min_duration")。P3-4:
config_map类型注解过于宽泛dict[str, TemplateClipConfig | None]或定义 Protocol。🧪 测试评价
测试覆盖较好(347 行),覆盖了:计划不存在、无片段、最佳候选分配、无候选、片段不存在、无配置分配、评分各维度、需求解析各场景。
缺失的测试场景:
select_for_clip的异常处理classification_resultLIKE 查询的误匹配场景⚠️ 合并状态
mergeable = False,合并前需要 rebase 到最新 develop。
结论
✅ 有条件通过 — 4 项 P2 + 4 项 P3,均不阻塞合并。建议优先修复 P2-1(重复分配)和 P2-2(LIKE 误匹配),其余可在后续迭代中处理。合并前需 rebase。