diff --git a/src/views/project/project/execution/index.vue b/src/views/project/project/execution/index.vue index 6bc3006..fb09d79 100644 --- a/src/views/project/project/execution/index.vue +++ b/src/views/project/project/execution/index.vue @@ -684,10 +684,11 @@ async function confirmDeleteExecution(payload: { name: string; confirmText: stri confirmText: payload.confirmText, reason: payload.reason }); - if (error) return; - window.$message?.success('删除成功'); + // 成功=正常删除;失败=多为打开弹层后对象被并发改状态/删除,错误文案由全局 onError 弹 Toast。 + // 两种情况都关弹层 + 刷新:失败也要让用户离开已失效的弹层、看到最新数据。 deleteDialogVisible.value = false; selectedExecution.value = null; + if (!error) window.$message?.success('删除成功'); // 删执行 → 执行集合 -1,视角 chip + 任务 scope/cross counts 都要刷 await Promise.all([ reloadExecutionData(1), diff --git a/src/views/project/project/execution/modules/task-info-readonly.vue b/src/views/project/project/execution/modules/task-info-readonly.vue index 8613b66..4b7b7c8 100644 --- a/src/views/project/project/execution/modules/task-info-readonly.vue +++ b/src/views/project/project/execution/modules/task-info-readonly.vue @@ -6,6 +6,7 @@ import BusinessFormSection from '@/components/custom/business-form-section.vue'; import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue'; import BusinessUserSelect from '@/components/custom/business-user-select.vue'; import DictSelect from '@/components/custom/dict-select.vue'; +import { SHOW_TASK_PARENT_FIELD } from '../shared'; defineOptions({ name: 'ProjectExecutionTaskInfoReadonly' }); @@ -53,7 +54,7 @@ const parentTaskOptions = computed(() => { - + diff --git a/src/views/project/project/execution/modules/task-operate-dialog.vue b/src/views/project/project/execution/modules/task-operate-dialog.vue index 96255dd..f3a2266 100644 --- a/src/views/project/project/execution/modules/task-operate-dialog.vue +++ b/src/views/project/project/execution/modules/task-operate-dialog.vue @@ -10,6 +10,8 @@ import BusinessFormSection from '@/components/custom/business-form-section.vue'; import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue'; import BusinessUserSelect from '@/components/custom/business-user-select.vue'; import DictSelect from '@/components/custom/dict-select.vue'; +import { SHOW_TASK_PARENT_FIELD } from '../shared'; + defineOptions({ name: 'ProjectExecutionTaskOperateDialog' }); type OperateMode = 'create' | 'edit'; @@ -342,7 +344,7 @@ defineExpose({ /> - + - + diff --git a/src/views/project/project/execution/modules/task-workspace.vue b/src/views/project/project/execution/modules/task-workspace.vue index 93daa1d..b9713aa 100644 --- a/src/views/project/project/execution/modules/task-workspace.vue +++ b/src/views/project/project/execution/modules/task-workspace.vue @@ -696,10 +696,11 @@ async function confirmDeleteTask(payload: { name: string; confirmText: string; r confirmText: payload.confirmText, reason: payload.reason }); - if (error) return; - window.$message?.success('删除成功'); + // 成功=正常删除;失败=多为打开弹层后对象被并发改状态/删除,错误文案由全局 onError 弹 Toast。 + // 两种情况都关弹层 + 刷新列表:失败也要让用户离开已失效的弹层、看到最新数据。 deleteTaskDialogVisible.value = false; deleteTaskTarget.value = null; + if (!error) window.$message?.success('删除成功'); await Promise.all([refreshTableData(), loadTaskStatusBoard()]); } diff --git a/src/views/project/project/execution/shared.ts b/src/views/project/project/execution/shared.ts index 6be35a8..3a062a1 100644 --- a/src/views/project/project/execution/shared.ts +++ b/src/views/project/project/execution/shared.ts @@ -5,6 +5,13 @@ type ExecutionStatusCode = Api.Project.ProjectExecutionStatusCode; type TaskStatusCode = Api.Project.ProjectTaskStatusCode; type ExecutionAssigneeActionType = Api.Project.ExecutionAssigneeActionType; +/** + * 是否在任务界面展示「父任务」相关露出(表格列 / 新建编辑下拉 / 详情只读字段)。 + * 当前业务经执行分层后极少有子任务需求,暂统一隐藏,使任务呈扁平的一级任务列表; + * 底层父子数据与级联完成逻辑保留不动,将来恢复子任务功能改回 true 即可。 + */ +export const SHOW_TASK_PARENT_FIELD = false; + export const executionAssigneeActionNameMap: Record = { join: '加入', inactive: '失效', diff --git a/用户可见错误文案规范.html b/用户可见错误文案规范.html new file mode 100644 index 0000000..f1c04c1 --- /dev/null +++ b/用户可见错误文案规范.html @@ -0,0 +1,589 @@ + + + + + + 用户可见错误文案规范 · cn-rdms + + + + +
+
+

用户可见错误文案规范

+

来源:技术负债 TD-012 · 落地日期 2026-06-03 · 文档 2026-06-04

+

+ 适用范围:整仓所有"状态机动作 / 状态校验失败"的业务异常,凡 + message + 会被前端直接展示者 +

+

+ 给前端 + toast + 的 + message + 只放用户能看懂的中文;动作 / 状态的内部 code、堆栈等技术细节不进 + message + ,由访问日志承载。本规范是必须遵守的跨模块约定,新功能照做。 +

+
+ +

1 · 核心理念

+

message 面向用户、诊断面向开发,两者分离。

+

+ 前端往往直接把后端业务异常的 + message + 弹给最终用户。如果 + message + 里夹着 + complete + / + status + / + action + 这类内部术语,用户看不懂,会误以为系统异常或数据没保存。 +

+
+
反例(TD-012 的原始案例)
+ 用户点"完成任务",第一次请求已把任务置为已完成;前端重复发了第二次 + complete + 动作,后端返回 + "当前任务状态不支持动作【complete】" + 。用户合理的预期是看到 + "任务已完成,请勿重复提交" + 这类人话。 +
+

+ 因此约定: + + 用户看友好中文( + message + ),开发排查看访问日志( + infra_api_access_log + 里有原始 code、入参、堆栈)。 + + 两条信息流互不污染。 +

+ +

2 · 技术实现

+

+ 整套方案 + 零新表、framework 零改动 + ,纯在 + rdms-project + 域内:一个解析器组件 + 错误码文案模板改造 + service 接入。 +

+ +

+ 2.1 文案解析器 + StatusActionTextResolver +

+

+ 位置: + rdms-project-boot · service/status/StatusActionTextResolver.java + ( + @Component + )。把动作 / 状态的 code 翻成中文展示名,供错误文案使用。 +

+ + + + + + + + + + + + + + + + + + + + +
方法作用查不到 / 空入参
actionName(objectType, actionCode)动作中文名 + 回退原 + actionCode + ,不抛错 +
statusName(objectType, statusCode)状态中文名 + 回退原 + statusCode + ,不抛错 +
+
+
权威源:DB 状态机表,不在代码里硬编码映射
+ 动作名取自 + rdms_object_status_transition.action_name + ,状态名取自 + rdms_object_status_model.status_name + 。运维在状态机表里配新动作 / 新状态,文案自动生效, + 不用改代码发版 + 。 +
+ +

2.2 错误码文案用「{}」占位中文名

+

错误码定义时,文案就留中文名占位,由 service 在抛错前用 resolver 填入。例如:

+
// 个人事项 —— 正面样板
+ErrorCode PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED =
+    new ErrorCode(1_008_008_004, "当前个人事项为「{}」状态,不支持「{}」操作");
+

+ 抛出前: + + exception(PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED, resolver.statusName(...), resolver.actionName(...)) + + —— 占位填的是中文名,不是裸 code。 +

+ +

2.3 已接入面(7 个 service)

+

状态机校验失败抛错时,先经 resolver 翻译再返回的 service:

+
    +
  • + ProductRequirementServiceImpl + (产品需求) +
  • +
  • + PersonalItemServiceImpl + (个人事项) +
  • +
  • + ProjectTaskServiceImpl + (任务) +
  • +
  • + ProductServiceImpl + (产品) +
  • +
  • + ProjectExecutionServiceImpl + (执行) +
  • +
  • + ProjectServiceImpl + (项目) +
  • +
  • + ProjectRequirementServiceImpl + (项目需求) +
  • +
+ +

3 · 关键设计决策

+
    +
  1. + 不硬编码映射,跟 DB 状态机走。 + 中文名是状态机表里运维可配的数据,不复刻成 Java 常量 / Enum;加新动作、新状态不需要改代码(呼应仓库"字典 / + 状态值不复刻成 Java 常量"的纪律)。 +
  2. +
  3. + 解析器只翻译、绝不抛错。 + 空入参或查不到一律回退原 code。它是文案美化层,不能反过来变成新的故障点——翻译失败也要让原始业务异常正常返回。 +
  4. +
  5. + 轻量、可回滚。 + 纯加一个 Component + 改错误码文案模板 + service 接入,不新表、不动 framework、复用现有错误码体系,因此 + 无演示库补丁、前端零改动 + 。 +
  6. +
+ +

4 · 新功能落地清单

+
+
凡新增"状态机动作 / 状态校验"且 message 会被前端展示,照此四步
+
+
    +
  1. + service 注入 + StatusActionTextResolver + 。 +
  2. +
  3. + 错误码文案写成「{}」占位中文名(参考 + PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED + ), + 不要把 code 直接嵌进文案 + 。 +
  4. +
  5. + 抛错前用 + actionName / statusName + 把 code 翻成中文名再填占位。 +
  6. +
  7. + 新对象类型在 + rdms_object_status_model + / + rdms_object_status_transition + 配好 + status_name + / + action_name + ,resolver 即自动生效。 +
  8. +
+ +

5 · 信息泄漏类红线

+

+ 除"状态机动作 / 状态翻中文"外,凡 + message + 会被前端直接展示, + 以下技术 token 一律不得出现在 message 里 + ,只能进日志( + log.warn + / + infra_api_access_log + ): +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
禁止外泄反例正确做法
数据库表名 / 列名 + "未在 + system_role + 找到" + "…未配置,请联系管理员"
权限码 / 内部标记 + "操作权限【 + project:project:update + 】"、"【 + member + 】" + "您没有此项操作权限,请联系管理员"
动作 / 状态 code + "不支持动作【 + complete + 】" + resolver 翻中文名(见 §2)
类名 / 字段名 / 堆栈 + " + NullPointerException at ... + " + 友好提示,异常进日志
+
+
2026-06-04 · B 类存量整改已落地
+
    +
  • + 加班申请 + : + OvertimeApplicationServiceImpl + 已注入 resolver,文案对齐其它域「当前加班申请为「{}」状态,不支持「{}」操作」。 +
  • +
  • + 操作权限不足 + : + Project/ProductObjectPermissionService + 已去占位、权限码改 + log.warn + ;错误码文案改"您没有该项目/产品的此项操作权限,请联系管理员"。 +
  • +
  • + 表名外泄 + : + PRODUCT/PROJECT_INTERNAL_ROLE_NOT_CONFIGURED + 两条已去掉 + system_role + 。 +
  • +
+
+
+
2026-06-04 · 加班申请 service 层单测已补
+ 新增 + OvertimeApplicationServiceImplTest + (2 用例, + mvn test + 通过):验证状态机「动作不允许 / 缺原因」抛错时, + message + 填的是 + StatusActionTextResolver + 翻出的中文名、不外泄英文动作 / 状态 code。属 Mockito 单测(mock resolver),覆盖的是 service 层「填中文名而非裸 + code」这条契约;resolver 自身的 DB 翻译由 + StatusActionTextResolverTest + 覆盖。真实 DB 状态机表是否配齐 + status_name + / + action_name + 的端到端校验仍依赖运行时,不在单测范围。 +
+ +

6 · 关联文档

+
    +
  • + 对象状态能力落地规范.md + —— 状态机模型与流转设计,本规范的中文名权威源即来自这两张表。 +
  • +
  • + 技术负债台账 · TD-012 + —— 本规范的需求来源条目。 +
  • +
  • + 技术诊断承载: + infra_api_access_log + (访问日志,留原始 code / 入参 / 堆栈)。 +
  • +
  • + CLAUDE.md · 接口语义 + 章节留有指向本文档的红线指针。 +
  • +
+ + +
+ +