fix(projects): 针对技术负债去优化代码
This commit is contained in:
@@ -684,10 +684,11 @@ async function confirmDeleteExecution(payload: { name: string; confirmText: stri
|
|||||||
confirmText: payload.confirmText,
|
confirmText: payload.confirmText,
|
||||||
reason: payload.reason
|
reason: payload.reason
|
||||||
});
|
});
|
||||||
if (error) return;
|
// 成功=正常删除;失败=多为打开弹层后对象被并发改状态/删除,错误文案由全局 onError 弹 Toast。
|
||||||
window.$message?.success('删除成功');
|
// 两种情况都关弹层 + 刷新:失败也要让用户离开已失效的弹层、看到最新数据。
|
||||||
deleteDialogVisible.value = false;
|
deleteDialogVisible.value = false;
|
||||||
selectedExecution.value = null;
|
selectedExecution.value = null;
|
||||||
|
if (!error) window.$message?.success('删除成功');
|
||||||
// 删执行 → 执行集合 -1,视角 chip + 任务 scope/cross counts 都要刷
|
// 删执行 → 执行集合 -1,视角 chip + 任务 scope/cross counts 都要刷
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
reloadExecutionData(1),
|
reloadExecutionData(1),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
|||||||
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
|
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
|
||||||
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
||||||
import DictSelect from '@/components/custom/dict-select.vue';
|
import DictSelect from '@/components/custom/dict-select.vue';
|
||||||
|
import { SHOW_TASK_PARENT_FIELD } from '../shared';
|
||||||
|
|
||||||
defineOptions({ name: 'ProjectExecutionTaskInfoReadonly' });
|
defineOptions({ name: 'ProjectExecutionTaskInfoReadonly' });
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ const parentTaskOptions = computed(() => {
|
|||||||
<ElFormItem label="任务类型">
|
<ElFormItem label="任务类型">
|
||||||
<DictSelect :model-value="taskType" :dict-code="RDMS_TASK_ITEM_TYPE_DICT_CODE" disabled placeholder="--" />
|
<DictSelect :model-value="taskType" :dict-code="RDMS_TASK_ITEM_TYPE_DICT_CODE" disabled placeholder="--" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="父任务">
|
<ElFormItem v-if="SHOW_TASK_PARENT_FIELD" label="父任务">
|
||||||
<ElSelect :model-value="parentTaskId" disabled clearable filterable class="w-full" placeholder="无">
|
<ElSelect :model-value="parentTaskId" disabled clearable filterable class="w-full" placeholder="无">
|
||||||
<ElOption v-for="item in parentTaskOptions" :key="item.id" :label="item.taskTitle" :value="item.id" />
|
<ElOption v-for="item in parentTaskOptions" :key="item.id" :label="item.taskTitle" :value="item.id" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
|||||||
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
|
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
|
||||||
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
||||||
import DictSelect from '@/components/custom/dict-select.vue';
|
import DictSelect from '@/components/custom/dict-select.vue';
|
||||||
|
import { SHOW_TASK_PARENT_FIELD } from '../shared';
|
||||||
|
|
||||||
defineOptions({ name: 'ProjectExecutionTaskOperateDialog' });
|
defineOptions({ name: 'ProjectExecutionTaskOperateDialog' });
|
||||||
|
|
||||||
type OperateMode = 'create' | 'edit';
|
type OperateMode = 'create' | 'edit';
|
||||||
@@ -342,7 +344,7 @@ defineExpose({
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="父任务">
|
<ElFormItem v-if="SHOW_TASK_PARENT_FIELD" label="父任务">
|
||||||
<ElSelect v-model="model.parentTaskId" clearable filterable class="w-full" placeholder="请选择父任务">
|
<ElSelect v-model="model.parentTaskId" clearable filterable class="w-full" placeholder="请选择父任务">
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="item in selectableParentTasks"
|
v-for="item in selectableParentTasks"
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import type { PaginationProps } from 'element-plus';
|
|||||||
import { RDMS_REQ_PRIORITY_DICT_CODE } from '@/constants/dict';
|
import { RDMS_REQ_PRIORITY_DICT_CODE } from '@/constants/dict';
|
||||||
import { useAuthStore } from '@/store/modules/auth';
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
import DictTag from '@/components/custom/dict-tag.vue';
|
import DictTag from '@/components/custom/dict-tag.vue';
|
||||||
import { formatDate, formatDateRange, getTaskStatusName, getTaskStatusTagType } from '../shared';
|
import {
|
||||||
|
SHOW_TASK_PARENT_FIELD,
|
||||||
|
formatDate,
|
||||||
|
formatDateRange,
|
||||||
|
getTaskStatusName,
|
||||||
|
getTaskStatusTagType
|
||||||
|
} from '../shared';
|
||||||
import { useTaskActions } from '../composables/use-task-actions';
|
import { useTaskActions } from '../composables/use-task-actions';
|
||||||
|
|
||||||
defineOptions({ name: 'ProjectExecutionTaskTableView' });
|
defineOptions({ name: 'ProjectExecutionTaskTableView' });
|
||||||
@@ -141,7 +147,12 @@ function handleSizeChange(pageSize: number) {
|
|||||||
<ElTableColumn v-if="!crossExecutionMode" label="负责人" min-width="120" show-overflow-tooltip>
|
<ElTableColumn v-if="!crossExecutionMode" label="负责人" min-width="120" show-overflow-tooltip>
|
||||||
<template #default="{ row }">{{ row.ownerNickname || row.ownerId || '--' }}</template>
|
<template #default="{ row }">{{ row.ownerNickname || row.ownerId || '--' }}</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn v-if="!crossExecutionMode" label="父任务" min-width="140" show-overflow-tooltip>
|
<ElTableColumn
|
||||||
|
v-if="!crossExecutionMode && SHOW_TASK_PARENT_FIELD"
|
||||||
|
label="父任务"
|
||||||
|
min-width="140"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
<template #default="{ row }">{{ getParentTaskLabel(row.parentTaskId) }}</template>
|
<template #default="{ row }">{{ getParentTaskLabel(row.parentTaskId) }}</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="进度" width="160">
|
<ElTableColumn label="进度" width="160">
|
||||||
|
|||||||
@@ -696,10 +696,11 @@ async function confirmDeleteTask(payload: { name: string; confirmText: string; r
|
|||||||
confirmText: payload.confirmText,
|
confirmText: payload.confirmText,
|
||||||
reason: payload.reason
|
reason: payload.reason
|
||||||
});
|
});
|
||||||
if (error) return;
|
// 成功=正常删除;失败=多为打开弹层后对象被并发改状态/删除,错误文案由全局 onError 弹 Toast。
|
||||||
window.$message?.success('删除成功');
|
// 两种情况都关弹层 + 刷新列表:失败也要让用户离开已失效的弹层、看到最新数据。
|
||||||
deleteTaskDialogVisible.value = false;
|
deleteTaskDialogVisible.value = false;
|
||||||
deleteTaskTarget.value = null;
|
deleteTaskTarget.value = null;
|
||||||
|
if (!error) window.$message?.success('删除成功');
|
||||||
await Promise.all([refreshTableData(), loadTaskStatusBoard()]);
|
await Promise.all([refreshTableData(), loadTaskStatusBoard()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ type ExecutionStatusCode = Api.Project.ProjectExecutionStatusCode;
|
|||||||
type TaskStatusCode = Api.Project.ProjectTaskStatusCode;
|
type TaskStatusCode = Api.Project.ProjectTaskStatusCode;
|
||||||
type ExecutionAssigneeActionType = Api.Project.ExecutionAssigneeActionType;
|
type ExecutionAssigneeActionType = Api.Project.ExecutionAssigneeActionType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否在任务界面展示「父任务」相关露出(表格列 / 新建编辑下拉 / 详情只读字段)。
|
||||||
|
* 当前业务经执行分层后极少有子任务需求,暂统一隐藏,使任务呈扁平的一级任务列表;
|
||||||
|
* 底层父子数据与级联完成逻辑保留不动,将来恢复子任务功能改回 true 即可。
|
||||||
|
*/
|
||||||
|
export const SHOW_TASK_PARENT_FIELD = false;
|
||||||
|
|
||||||
export const executionAssigneeActionNameMap: Record<ExecutionAssigneeActionType, string> = {
|
export const executionAssigneeActionNameMap: Record<ExecutionAssigneeActionType, string> = {
|
||||||
join: '加入',
|
join: '加入',
|
||||||
inactive: '失效',
|
inactive: '失效',
|
||||||
|
|||||||
589
用户可见错误文案规范.html
Normal file
589
用户可见错误文案规范.html
Normal file
@@ -0,0 +1,589 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
<title>用户可见错误文案规范 · cn-rdms</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--fg: #1f2328;
|
||||||
|
--fg-muted: #57606a;
|
||||||
|
--bg: #ffffff;
|
||||||
|
--bg-soft: #f6f8fa;
|
||||||
|
--border: #d0d7de;
|
||||||
|
--border-soft: #e7ecf2;
|
||||||
|
--accent: #0969da;
|
||||||
|
--accent-soft: #ddf4ff;
|
||||||
|
--purple: #8250df;
|
||||||
|
--purple-soft: #fbefff;
|
||||||
|
--warn: #9a6700;
|
||||||
|
--warn-soft: #fff8c5;
|
||||||
|
--danger: #cf222e;
|
||||||
|
--danger-soft: #ffebe9;
|
||||||
|
--ok: #1a7f37;
|
||||||
|
--ok-soft: #dafbe1;
|
||||||
|
--code-bg: #f6f8fa;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Hiragino Sans GB',
|
||||||
|
'Source Han Sans CN', 'Noto Sans CJK SC', Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.75;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 240px 1fr;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
aside.sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
align-self: start;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--bg-soft);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding: 24px 18px;
|
||||||
|
}
|
||||||
|
aside.sidebar .sidebar-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--fg-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
aside.sidebar nav ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
aside.sidebar nav li {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
aside.sidebar nav a {
|
||||||
|
color: var(--fg);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
display: block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
aside.sidebar nav a:hover {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 48px 32px 96px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header.doc-header {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
header.doc-header h1 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
header.doc-header .meta {
|
||||||
|
color: var(--fg-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
header.doc-header .lead {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--fg);
|
||||||
|
margin: 12px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 44px 0 12px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
border-left: 5px solid var(--accent);
|
||||||
|
font-size: 21px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 26px 0 8px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: 'JetBrains Mono', Menlo, Consolas, 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
font-family: 'JetBrains Mono', Menlo, Consolas, 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: var(--bg-soft);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
tr:nth-child(2n) td {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout {
|
||||||
|
border-left: 4px solid var(--accent);
|
||||||
|
background: var(--accent-soft);
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 14px 0;
|
||||||
|
}
|
||||||
|
.callout.warn {
|
||||||
|
border-left-color: var(--warn);
|
||||||
|
background: var(--warn-soft);
|
||||||
|
}
|
||||||
|
.callout.danger {
|
||||||
|
border-left-color: var(--danger);
|
||||||
|
background: var(--danger-soft);
|
||||||
|
}
|
||||||
|
.callout.ok {
|
||||||
|
border-left-color: var(--ok);
|
||||||
|
background: var(--ok-soft);
|
||||||
|
}
|
||||||
|
.callout .title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bad {
|
||||||
|
color: var(--danger);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.good {
|
||||||
|
color: var(--ok);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
footer.doc-footer {
|
||||||
|
margin-top: 56px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
color: var(--fg-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-title">目录</div>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#理念">1 · 核心理念</a></li>
|
||||||
|
<li><a href="#实现">2 · 技术实现</a></li>
|
||||||
|
<li><a href="#决策">3 · 关键设计决策</a></li>
|
||||||
|
<li><a href="#清单">4 · 新功能落地清单</a></li>
|
||||||
|
<li><a href="#缺口">5 · 信息泄漏类红线</a></li>
|
||||||
|
<li><a href="#关联">6 · 关联文档</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<div class="wrap">
|
||||||
|
<header class="doc-header">
|
||||||
|
<h1>用户可见错误文案规范</h1>
|
||||||
|
<p class="meta">来源:技术负债 TD-012 · 落地日期 2026-06-03 · 文档 2026-06-04</p>
|
||||||
|
<p class="meta">
|
||||||
|
适用范围:整仓所有"状态机动作 / 状态校验失败"的业务异常,凡
|
||||||
|
<code>message</code>
|
||||||
|
会被前端直接展示者
|
||||||
|
</p>
|
||||||
|
<p class="lead">
|
||||||
|
给前端
|
||||||
|
<code>toast</code>
|
||||||
|
的
|
||||||
|
<code>message</code>
|
||||||
|
只放用户能看懂的中文;动作 / 状态的内部 code、堆栈等技术细节不进
|
||||||
|
<code>message</code>
|
||||||
|
,由访问日志承载。本规范是必须遵守的跨模块约定,新功能照做。
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<h2 id="理念">1 · 核心理念</h2>
|
||||||
|
<p><strong>message 面向用户、诊断面向开发,两者分离。</strong></p>
|
||||||
|
<p>
|
||||||
|
前端往往直接把后端业务异常的
|
||||||
|
<code>message</code>
|
||||||
|
弹给最终用户。如果
|
||||||
|
<code>message</code>
|
||||||
|
里夹着
|
||||||
|
<code>complete</code>
|
||||||
|
/
|
||||||
|
<code>status</code>
|
||||||
|
/
|
||||||
|
<code>action</code>
|
||||||
|
这类内部术语,用户看不懂,会误以为系统异常或数据没保存。
|
||||||
|
</p>
|
||||||
|
<div class="callout danger">
|
||||||
|
<div class="title">反例(TD-012 的原始案例)</div>
|
||||||
|
用户点"完成任务",第一次请求已把任务置为已完成;前端重复发了第二次
|
||||||
|
<code>complete</code>
|
||||||
|
动作,后端返回
|
||||||
|
<span class="bad">"当前任务状态不支持动作【complete】"</span>
|
||||||
|
。用户合理的预期是看到
|
||||||
|
<span class="good">"任务已完成,请勿重复提交"</span>
|
||||||
|
这类人话。
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
因此约定:
|
||||||
|
<strong>
|
||||||
|
用户看友好中文(
|
||||||
|
<code>message</code>
|
||||||
|
),开发排查看访问日志(
|
||||||
|
<code>infra_api_access_log</code>
|
||||||
|
里有原始 code、入参、堆栈)。
|
||||||
|
</strong>
|
||||||
|
两条信息流互不污染。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="实现">2 · 技术实现</h2>
|
||||||
|
<p>
|
||||||
|
整套方案
|
||||||
|
<strong>零新表、framework 零改动</strong>
|
||||||
|
,纯在
|
||||||
|
<code>rdms-project</code>
|
||||||
|
域内:一个解析器组件 + 错误码文案模板改造 + service 接入。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
2.1 文案解析器
|
||||||
|
<code>StatusActionTextResolver</code>
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
位置:
|
||||||
|
<code>rdms-project-boot · service/status/StatusActionTextResolver.java</code>
|
||||||
|
(
|
||||||
|
<code>@Component</code>
|
||||||
|
)。把动作 / 状态的 code 翻成中文展示名,供错误文案使用。
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>方法</th>
|
||||||
|
<th>作用</th>
|
||||||
|
<th>查不到 / 空入参</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>actionName(objectType, actionCode)</code></td>
|
||||||
|
<td>动作中文名</td>
|
||||||
|
<td>
|
||||||
|
回退原
|
||||||
|
<code>actionCode</code>
|
||||||
|
,不抛错
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>statusName(objectType, statusCode)</code></td>
|
||||||
|
<td>状态中文名</td>
|
||||||
|
<td>
|
||||||
|
回退原
|
||||||
|
<code>statusCode</code>
|
||||||
|
,不抛错
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="callout purple">
|
||||||
|
<div class="title">权威源:DB 状态机表,不在代码里硬编码映射</div>
|
||||||
|
动作名取自
|
||||||
|
<code>rdms_object_status_transition.action_name</code>
|
||||||
|
,状态名取自
|
||||||
|
<code>rdms_object_status_model.status_name</code>
|
||||||
|
。运维在状态机表里配新动作 / 新状态,文案自动生效,
|
||||||
|
<strong>不用改代码发版</strong>
|
||||||
|
。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>2.2 错误码文案用「{}」占位中文名</h3>
|
||||||
|
<p>错误码定义时,文案就留中文名占位,由 service 在抛错前用 resolver 填入。例如:</p>
|
||||||
|
<pre><code>// 个人事项 —— 正面样板
|
||||||
|
ErrorCode PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED =
|
||||||
|
new ErrorCode(1_008_008_004, "当前个人事项为「{}」状态,不支持「{}」操作");</code></pre>
|
||||||
|
<p>
|
||||||
|
抛出前:
|
||||||
|
<code>
|
||||||
|
exception(PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED, resolver.statusName(...), resolver.actionName(...))
|
||||||
|
</code>
|
||||||
|
—— 占位填的是中文名,不是裸 code。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>2.3 已接入面(7 个 service)</h3>
|
||||||
|
<p>状态机校验失败抛错时,先经 resolver 翻译再返回的 service:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>ProductRequirementServiceImpl</code>
|
||||||
|
(产品需求)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>PersonalItemServiceImpl</code>
|
||||||
|
(个人事项)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>ProjectTaskServiceImpl</code>
|
||||||
|
(任务)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>ProductServiceImpl</code>
|
||||||
|
(产品)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>ProjectExecutionServiceImpl</code>
|
||||||
|
(执行)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>ProjectServiceImpl</code>
|
||||||
|
(项目)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>ProjectRequirementServiceImpl</code>
|
||||||
|
(项目需求)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="决策">3 · 关键设计决策</h2>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<strong>不硬编码映射,跟 DB 状态机走。</strong>
|
||||||
|
中文名是状态机表里运维可配的数据,不复刻成 Java 常量 / Enum;加新动作、新状态不需要改代码(呼应仓库"字典 /
|
||||||
|
状态值不复刻成 Java 常量"的纪律)。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>解析器只翻译、绝不抛错。</strong>
|
||||||
|
空入参或查不到一律回退原 code。它是文案美化层,不能反过来变成新的故障点——翻译失败也要让原始业务异常正常返回。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>轻量、可回滚。</strong>
|
||||||
|
纯加一个 Component + 改错误码文案模板 + service 接入,不新表、不动 framework、复用现有错误码体系,因此
|
||||||
|
<strong>无演示库补丁、前端零改动</strong>
|
||||||
|
。
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2 id="清单">4 · 新功能落地清单</h2>
|
||||||
|
<div class="callout ok">
|
||||||
|
<div class="title">凡新增"状态机动作 / 状态校验"且 message 会被前端展示,照此四步</div>
|
||||||
|
</div>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
service 注入
|
||||||
|
<code>StatusActionTextResolver</code>
|
||||||
|
。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
错误码文案写成「{}」占位中文名(参考
|
||||||
|
<code>PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED</code>
|
||||||
|
),
|
||||||
|
<span class="bad">不要把 code 直接嵌进文案</span>
|
||||||
|
。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
抛错前用
|
||||||
|
<code>actionName / statusName</code>
|
||||||
|
把 code 翻成中文名再填占位。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
新对象类型在
|
||||||
|
<code>rdms_object_status_model</code>
|
||||||
|
/
|
||||||
|
<code>rdms_object_status_transition</code>
|
||||||
|
配好
|
||||||
|
<code>status_name</code>
|
||||||
|
/
|
||||||
|
<code>action_name</code>
|
||||||
|
,resolver 即自动生效。
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2 id="缺口">5 · 信息泄漏类红线</h2>
|
||||||
|
<p>
|
||||||
|
除"状态机动作 / 状态翻中文"外,凡
|
||||||
|
<code>message</code>
|
||||||
|
会被前端直接展示,
|
||||||
|
<strong>以下技术 token 一律不得出现在 message 里</strong>
|
||||||
|
,只能进日志(
|
||||||
|
<code>log.warn</code>
|
||||||
|
/
|
||||||
|
<code>infra_api_access_log</code>
|
||||||
|
):
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>禁止外泄</th>
|
||||||
|
<th>反例</th>
|
||||||
|
<th>正确做法</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>数据库表名 / 列名</td>
|
||||||
|
<td>
|
||||||
|
"未在
|
||||||
|
<code>system_role</code>
|
||||||
|
找到"
|
||||||
|
</td>
|
||||||
|
<td>"…未配置,请联系管理员"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>权限码 / 内部标记</td>
|
||||||
|
<td>
|
||||||
|
"操作权限【
|
||||||
|
<code>project:project:update</code>
|
||||||
|
】"、"【
|
||||||
|
<code>member</code>
|
||||||
|
】"
|
||||||
|
</td>
|
||||||
|
<td>"您没有此项操作权限,请联系管理员"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>动作 / 状态 code</td>
|
||||||
|
<td>
|
||||||
|
"不支持动作【
|
||||||
|
<code>complete</code>
|
||||||
|
】"
|
||||||
|
</td>
|
||||||
|
<td>resolver 翻中文名(见 §2)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>类名 / 字段名 / 堆栈</td>
|
||||||
|
<td>
|
||||||
|
"
|
||||||
|
<code>NullPointerException at ...</code>
|
||||||
|
"
|
||||||
|
</td>
|
||||||
|
<td>友好提示,异常进日志</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="callout ok">
|
||||||
|
<div class="title">2026-06-04 · B 类存量整改已落地</div>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>加班申请</strong>
|
||||||
|
:
|
||||||
|
<code>OvertimeApplicationServiceImpl</code>
|
||||||
|
已注入 resolver,文案对齐其它域「当前加班申请为「{}」状态,不支持「{}」操作」。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>操作权限不足</strong>
|
||||||
|
:
|
||||||
|
<code>Project/ProductObjectPermissionService</code>
|
||||||
|
已去占位、权限码改
|
||||||
|
<code>log.warn</code>
|
||||||
|
;错误码文案改"您没有该项目/产品的此项操作权限,请联系管理员"。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>表名外泄</strong>
|
||||||
|
:
|
||||||
|
<code>PRODUCT/PROJECT_INTERNAL_ROLE_NOT_CONFIGURED</code>
|
||||||
|
两条已去掉
|
||||||
|
<code>system_role</code>
|
||||||
|
。
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="callout ok">
|
||||||
|
<div class="title">2026-06-04 · 加班申请 service 层单测已补</div>
|
||||||
|
新增
|
||||||
|
<code>OvertimeApplicationServiceImplTest</code>
|
||||||
|
(2 用例,
|
||||||
|
<code>mvn test</code>
|
||||||
|
通过):验证状态机「动作不允许 / 缺原因」抛错时,
|
||||||
|
<code>message</code>
|
||||||
|
填的是
|
||||||
|
<code>StatusActionTextResolver</code>
|
||||||
|
翻出的中文名、不外泄英文动作 / 状态 code。属 Mockito 单测(mock resolver),覆盖的是 service 层「填中文名而非裸
|
||||||
|
code」这条契约;resolver 自身的 DB 翻译由
|
||||||
|
<code>StatusActionTextResolverTest</code>
|
||||||
|
覆盖。真实 DB 状态机表是否配齐
|
||||||
|
<code>status_name</code>
|
||||||
|
/
|
||||||
|
<code>action_name</code>
|
||||||
|
的端到端校验仍依赖运行时,不在单测范围。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="关联">6 · 关联文档</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="./对象状态能力落地规范.md">对象状态能力落地规范.md</a>
|
||||||
|
—— 状态机模型与流转设计,本规范的中文名权威源即来自这两张表。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../debt/技术负债台账.html">技术负债台账 · TD-012</a>
|
||||||
|
—— 本规范的需求来源条目。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
技术诊断承载:
|
||||||
|
<code>infra_api_access_log</code>
|
||||||
|
(访问日志,留原始 code / 入参 / 堆栈)。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>CLAUDE.md · 接口语义</code>
|
||||||
|
章节留有指向本文档的红线指针。
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<footer class="doc-footer">cn-rdms · 跨模块约定 · 用户可见错误文案规范</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user