Files
cn-rdms-web/用户可见错误文案规范.html

590 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>