feat(project): 添加项目完成前置校验功能

- 新增 PROJECT_COMPLETE_PRECONDITION_NOT_MET 错误码用于项目完成校验失败提示
- 将任务附件错误码段从 1_008_007 调整至 1_008_010 避免编号冲突
- 添加 PROJECT_ACTION_COMPLETE 常量用于项目完成操作标识
- 在执行完成时触发 onExecutionCompleted 钩子方法用于后续推送逻辑
- 新增 countNonTerminalByProjectId 方法统计项目下非终态执行/需求/任务数量
- 实现 collectCompletionGaps 和 validateProjectCompletable 方法进行项目完成前置校验
- 在项目状态变更时增加对 complete 操作的特殊校验逻辑
- 添加 ProjectRequirementConstants 接口暴露需求对象类型常量供跨类使用
- 新建 SQL 脚本为项目完成校验查询创建必要的数据库索引
- 补充 ProjectServiceImplTest 测试用例验证项目完成校验功能
This commit is contained in:
2026-06-08 09:59:22 +08:00
parent 622b30733e
commit ab5b00470c
12 changed files with 236 additions and 9 deletions

View File

@@ -117,6 +117,8 @@ public interface ErrorCodeConstants {
ErrorCode PROJECT_MEMBER_BATCH_MANAGER_NOT_ALLOWED = new ErrorCode(1_008_002_036, "批量新增不允许指定为经理,请通过编辑成员调整");
// 批量移出POST /project/project/{id}/members/batch/inactive专用同一请求内 memberId 重复
ErrorCode PROJECT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE = new ErrorCode(1_008_002_037, "请勿在批量移出列表中重复指定同一成员");
// 项目完成前置校验TD-015complete 时子对象(任务/执行/需求)未全终态。{} 由 Service 动态拼全部缺口,只放中文+数字TD-012 脱敏)
ErrorCode PROJECT_COMPLETE_PRECONDITION_NOT_MET = new ErrorCode(1_008_002_038, "项目无法完成,请先处理:{}");
// ========== 执行管理 1-008-003-000 ==========
ErrorCode PROJECT_EXECUTION_NOT_EXISTS = new ErrorCode(1_008_003_000, "执行不存在");
@@ -186,12 +188,12 @@ public interface ErrorCodeConstants {
ErrorCode PROJECT_TASK_WORKLOG_PROGRESS_NOT_MONOTONIC = new ErrorCode(1_008_006_010, "工时进度与日期顺序不一致:早段进度不得高于晚段、晚段进度不得低于早段");
ErrorCode PROJECT_TASK_WORKLOG_DIFFICULTY_INVALID = new ErrorCode(1_008_006_011, "完成难度不在字典范围内");
// ========== 任务 / 工时附件 1_008_007_xxx ==========
ErrorCode PROJECT_TASK_ATTACHMENT_TOO_MANY = new ErrorCode(1_008_007_001, "附件数量不能超过 {} 个");
ErrorCode PROJECT_TASK_ATTACHMENT_URL_INVALID = new ErrorCode(1_008_007_002, "附件地址非法,必须为 http/https URL 且长度不超过 1024");
ErrorCode PROJECT_TASK_ATTACHMENT_NAME_INVALID = new ErrorCode(1_008_007_003, "附件文件名不合法(必填且长度不超过 255");
ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_NOT_ALLOWED = new ErrorCode(1_008_007_004, "附件扩展名【{}】不在允许列表内");
ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_BLOCKED = new ErrorCode(1_008_007_005, "附件类型【{}】被禁止上传");
// ========== 任务 / 工时附件 1_008_010_xxx(原 1_008_007 与下方项目需求段撞号,迁至独立号段;新增错误码域请从 1_008_011 起) ==========
ErrorCode PROJECT_TASK_ATTACHMENT_TOO_MANY = new ErrorCode(1_008_010_001, "附件数量不能超过 {} 个");
ErrorCode PROJECT_TASK_ATTACHMENT_URL_INVALID = new ErrorCode(1_008_010_002, "附件地址非法,必须为 http/https URL 且长度不超过 1024");
ErrorCode PROJECT_TASK_ATTACHMENT_NAME_INVALID = new ErrorCode(1_008_010_003, "附件文件名不合法(必填且长度不超过 255");
ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_NOT_ALLOWED = new ErrorCode(1_008_010_004, "附件扩展名【{}】不在允许列表内");
ErrorCode PROJECT_TASK_ATTACHMENT_TYPE_BLOCKED = new ErrorCode(1_008_010_005, "附件类型【{}】被禁止上传");
// ========== 项目需求 1_008_007_xxx ==========
ErrorCode PROJECT_REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_007_000, "项目需求不存在");