计划裁决:邮件路由的读也归运营中心,不去撑大「两端共用读路由」那份名单

全仓测试 route-access.test.ts 钉着一份有意枚举、随 Keycloak spec 审过的名单:哪些读路由被两个端共用。
计划原把邮件的 7 条 GET 标成 design|ops,名单就从八条长到十四条。裁决改路由、不改名单:
①邮箱那一屏按本计划自己的前端说明落在运营中心,设计端没有消费方(测试台读的是通道,不是邮箱账号);
②入站正文装的是客户来往内容 —— ADR-012 已把「设计端能读客户主数据」记为一处有意承担的代价,不再无故加宽;
③要往那份名单里加,得有真实消费方撑着。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
This commit is contained in:
xcs
2026-08-26 01:34:24 +08:00
co-authored by Claude Fable 5
parent 78e36ab73c
commit 43bacffd90
+22 -22
View File
@@ -505,7 +505,8 @@ git commit -m "$(printf '邮件契约:账号入参按 kind 分两形、视图只
// apps/api/src/app/__tests__/mail-routes-registry.test.ts
/**
* 邮件路由的归属:15 条全部进契约,注册表建得起来(字面 / 参数模版互不撞),
* 读标两端、写归运营中心(设计稿 §7.2)。
* 读写都归运营中心(设计稿 §7.2 + 2026-08-26 裁决):邮箱那一屏落在运营中心,设计端没有消费方;
* 而入站正文装的是客户来往内容 —— ADR-012 已把「设计端能读客户主数据」记为一处后果,不再无故加宽。
*/
import { describe, expect, it } from 'vitest';
import * as contracts from '@contracts';
@@ -522,10 +523,9 @@ describe('邮件路由契约', () => {
}
});
it('读标两端,写归 ops', () => {
it('读写都归 ops', () => {
for (const route of MAIL_ROUTES) {
if (route.method === 'GET') expect(route.access).toEqual(['design', 'ops']);
else expect(route.access).toBe('ops');
expect(route.access).toBe('ops');
}
});
@@ -533,7 +533,7 @@ describe('邮件路由契约', () => {
const registry = buildRouteRegistry(routesOf(Object.values(contracts)));
expect(registry.lookup('POST', '/api/gov/mail/accounts/probe')).toBe('ops');
expect(registry.lookup('POST', '/api/gov/mail/accounts/acc-1/probe')).toBe('ops');
expect(registry.lookup('GET', '/api/gov/mail/inbound/m-1/attachments/0')).toEqual(['design', 'ops']);
expect(registry.lookup('GET', '/api/gov/mail/inbound/m-1/attachments/0')).toBe('ops');
});
});
```
@@ -550,7 +550,7 @@ Expected: FAIL(`MAIL_ROUTES` 不存在)。
/**
* 邮件收发的路由契约(设计稿 §7.2;ADR-013)。
*
* 读标两端(设计中心要看通道对上了哪个信箱,运营中心管配置);写归运营中心。
* 读写都归运营中心 —— 邮箱那一屏落在运营中心,设计端没有消费方;入站正文装的是客户来往内容,不无故加宽可读面。
* `/api/gov/*` 的写路由由 app 中间件自动广播 `{kind:'system', id:'gov'}`。
* 探测 / 试发 / 拉取的**厂商失败不是接口错误**:200 的 body 里带原话(脱敏)。
*/
@@ -579,7 +579,7 @@ const listQuery = z.object({
export const listMailAccountsRoute = {
method: 'GET',
path: '/api/gov/mail/accounts',
access: ['design', 'ops'],
access: 'ops',
output: z.object({ accounts: z.array(mailAccountViewSchema) }),
} as const satisfies RouteContract;
@@ -605,7 +605,7 @@ export const probeMailDraftRoute = {
export const getMailAccountRoute = {
method: 'GET',
path: '/api/gov/mail/accounts/:accountId',
access: ['design', 'ops'],
access: 'ops',
params: accountParams,
output: mailAccountViewSchema,
} as const satisfies RouteContract;
@@ -658,7 +658,7 @@ export const sendTestMailRoute = {
export const listMailInboundRoute = {
method: 'GET',
path: '/api/gov/mail/inbound',
access: ['design', 'ops'],
access: 'ops',
query: listQuery,
output: z.object({ mails: z.array(mailInboundSummarySchema) }),
} as const satisfies RouteContract;
@@ -666,7 +666,7 @@ export const listMailInboundRoute = {
export const getMailInboundRoute = {
method: 'GET',
path: '/api/gov/mail/inbound/:mailId',
access: ['design', 'ops'],
access: 'ops',
params: mailParams,
output: mailInboundDetailSchema,
} as const satisfies RouteContract;
@@ -675,7 +675,7 @@ export const getMailInboundRoute = {
export const mailAttachmentRoute = {
method: 'GET',
path: '/api/gov/mail/inbound/:mailId/attachments/:index',
access: ['design', 'ops'],
access: 'ops',
params: z.object({ mailId: z.string().min(1), index: z.coerce.number().int().min(0) }),
output: z.object({ name: z.string(), contentType: z.string(), dataBase64: z.string() }),
} as const satisfies RouteContract;
@@ -684,7 +684,7 @@ export const mailAttachmentRoute = {
export const mailRawRoute = {
method: 'GET',
path: '/api/gov/mail/inbound/:mailId/raw',
access: ['design', 'ops'],
access: 'ops',
params: mailParams,
output: z.object({ name: z.string(), contentType: z.literal('message/rfc822'), dataBase64: z.string() }),
} as const satisfies RouteContract;
@@ -702,7 +702,7 @@ export const redeliverMailInboundRoute = {
export const listMailOutboundRoute = {
method: 'GET',
path: '/api/gov/mail/outbound',
access: ['design', 'ops'],
access: 'ops',
query: listQuery,
output: z.object({ mails: z.array(mailOutboundViewSchema) }),
} as const satisfies RouteContract;
@@ -6518,7 +6518,7 @@ git commit -m "$(printf 'mail.send 工具契约 2.0.0(to 数组 / html / 附件
| 项 | 值 |
|---|---|
| 前缀 | `/api/gov/mail/*`;读标两端 `design\|ops`,写归 `ops` |
| 前缀 | `/api/gov/mail/*`;**读写都归运营中心 `ops`** —— 邮箱那一屏落在运营中心,设计端没有消费方;而入站正文装的是客户来往内容,不无故加宽可读面 |
| 广播 | 写路由成功后自动广播 `{kind:'system', id:'gov'}`;轮询器的新来信 / 拉取出错 / 发信结果广播 `{kind:'system', id:'mail', reason}` —— 前端按 `kind:'system'` 失效 `['gov','mail',*]` |
| 凭证 | 视图只回 `secretBound: boolean`;`secret` 只写不读,PUT 时缺省 = 不改 |
| 厂商失败 | 探测 / 试发 / 立即拉取的**厂商失败不是接口错误**:200 的 body 里 `ok:false` / `state:'failed'` / `error` 带原话(已脱敏) |
@@ -6912,16 +6912,16 @@ curl -s "${H[@]}" "localhost:8787/api/gov/mail/outbound?limit=20" | jq '.mails[]
| `ops` | POST | `/api/gov/mail/inbound/:mailId/redeliver` |
```
在 `| \`design\|ops\` | GET | \`/api/gov/workflows/:id\` |` 之后加 7 行:
在 `| \`ops\` | DELETE | \`/api/gov/access/roles/:role\` |` 那一组的末尾再加 7 行(与上面的写路由并成一组,邮件读写同归 ops):
```markdown
| `design\|ops` | GET | `/api/gov/mail/accounts` |
| `design\|ops` | GET | `/api/gov/mail/accounts/:accountId` |
| `design\|ops` | GET | `/api/gov/mail/inbound` |
| `design\|ops` | GET | `/api/gov/mail/inbound/:mailId` |
| `design\|ops` | GET | `/api/gov/mail/inbound/:mailId/attachments/:index`(可用票据) |
| `design\|ops` | GET | `/api/gov/mail/inbound/:mailId/raw`(可用票据) |
| `design\|ops` | GET | `/api/gov/mail/outbound` |
| `ops` | GET | `/api/gov/mail/accounts` |
| `ops` | GET | `/api/gov/mail/accounts/:accountId` |
| `ops` | GET | `/api/gov/mail/inbound` |
| `ops` | GET | `/api/gov/mail/inbound/:mailId` |
| `ops` | GET | `/api/gov/mail/inbound/:mailId/attachments/:index`(可用票据) |
| `ops` | GET | `/api/gov/mail/inbound/:mailId/raw`(可用票据) |
| `ops` | GET | `/api/gov/mail/outbound` |
```
计数那一行改为:`计数:public 2 + 退役 8 + any 2(+ SSE 1 条字面路由)+ workbench 23 + design 33 + ops 27 + 两端 15 = 契约 110 条(邮件收发 15 条见 docs/API文档-邮件收发.md)。`