From 1584c8ee80dca648272e53dbff1f89ab3bf82794 Mon Sep 17 00:00:00 2001 From: jw Date: Sat, 12 Sep 2026 11:55:44 +0800 Subject: [PATCH] =?UTF-8?q?docs(order-v3):=20#7532=20=E5=9B=A2=E6=9C=9F?= =?UTF-8?q?=E5=87=BA=E5=9B=A2=E9=80=9A=E7=9F=A5=E4=B9=A6=E8=AF=BB=E5=86=99?= =?UTF-8?q?=E7=AB=AF=E7=82=B9=EF=BC=88GB-ADM-081=20/=20082=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增两个管理后台端点: GET / PUT /v3/admin/order/group-batch/{groupBatchId}/docs/notice 读端点在未保存过时按团期数据即时拼草稿返回(不落库),写端点整体覆盖保存到新表 group_batch_notice(Flyway V20260912_140)。 前端必接三件事: 1. expectedVersion 必填、首次传 0、收到 589585 必须【重读再提交】——直接重试一定还是 589585; 2. releasable=false 时置灰「打印 / 存 PDF」(阶段门只卡下发不卡编辑,招募期可起草); 3. 正文是纯文本语义,后端不转义也不反转义,按纯文本渲染、不要 v-html, 也不要在前端 escape/unescape 后再提交。 另一条部署事实:新权限码 group-batch:docs 在 user-service 有 10 分钟 Redis 缓存, 上线后 ADMIN 最多 10 分钟才生效,这段窗口的 589507 不是漏配。 当前授权范围 ADMIN / SUPER_ADMIN,FINANCE 未授。 原型出入三处(contacts 单串 vs 两个输入框、完成编辑未发请求、service/bring 未接控件) 已在正文列出,口径由 mmg 对齐。 Co-Authored-By: Claude Opus 5 (1M context) --- ...›¢期出团通知书读写端点-新增接口-管理后台.md | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 changelogs-v2/2026-09/12_7532_团期出团通知书读写端点-新增接口-管理后台.md diff --git a/changelogs-v2/2026-09/12_7532_团期出团通知书读写端点-新增接口-管理后台.md b/changelogs-v2/2026-09/12_7532_团期出团通知书读写端点-新增接口-管理后台.md new file mode 100644 index 00000000..1ca1fbcc --- /dev/null +++ b/changelogs-v2/2026-09/12_7532_团期出团通知书读写端点-新增接口-管理后台.md @@ -0,0 +1,412 @@ +--- +schema: "hl-changelog/v2" +ticket: "7532" +title: "团期出团通知书读写端点(GB-ADM-081 / 082)" +consumer: "admin" +author: "jw(GIT)" +change_type: "新增接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "mmg" +frontend_ref: "" +target_release: "" +verified_at: "2026-09-12" +status_note: "两个新端点。前端必接三件事:① expectedVersion 必填、首次传 0、收到 589585 必须重读再提交(不能直接重试);② releasable=false 时置灰「打印 / 存 PDF」;③ 正文是纯文本语义,后端不转义也不反转义,前端按纯文本渲染。另:新权限码 group-batch:docs 在 user-service 有 10 分钟缓存,上线后最多 10 分钟才对 ADMIN 生效。" +updated_at: "2026-09-12" +base: "dev-v3" +--- + +# order-v3: 团期出团通知书读写端点(GB-ADM-081 / 082) + +**服务**: hl-order-service-v3 +**PR**: #7577 +**Issue**: #7532 + +--- + +## ⚠️ 关键变化 + +🔴 **`expectedVersion` 是必填的乐观锁版本,收到 `589585` 必须重读再提交。** 直接原样重试一定还是 `589585`。首次保存传 `0`,此后取上一次读/写响应里的 `version`。 + +🔴 **正文是纯文本语义:后端不做 HTML 转义,也不做反转义。** 请按纯文本渲染(`textContent` / Vue 的 `{{ }}`),**不要** `v-html`。之所以不在后端转义:那只约束单次请求,挡不住「读出 → 只改集合时间 → 其他字段原样提交」这种**普通编辑**——`A&B` 会逐轮变成 `A&B`、`A&B`,用户第一次读回就看到编码串。 + +🔴 **`releasable=false` 时请置灰「打印 / 存 PDF」。** 阶段门只卡**下发**不卡编辑:招募期就可以起草和保存,但不该发给客人。 + +🔴 **保存是整体覆盖,不是增量。** 9 个内容字段必须每次带齐,服务端不做字段级合并。编辑前先 GET 读回全量。 + +🔴 **新权限码 `group-batch:docs` 有最多 10 分钟的生效延迟。** 权限码在 user-service 有 10 分钟 Redis 缓存,上线后 ADMIN 可能先看到 `589507`、约 10 分钟后自动变正常。**这不是漏配**,请勿在这段窗口内报障。当前授权范围:`ADMIN` / `SUPER_ADMIN`(`FINANCE` 未授)。 + +--- + +## 一、背景 + +出团前运营要给客人发一份「出团通知书」:集合时间地点、领队是谁、坐什么车、含哪些服务、要带什么、出事找谁。 + +改前这份东西**完全在线下 Word 里做**,团期系统里一个字段都没有,每次出团都要人肉从四五个页面抄数据,抄错就是客人第二天站错地方。 + +本单给它加了读、写两个端点。读端点在**未保存过**时按团期现有数据即时拼一份草稿返回(不落库),运营在草稿上改完再保存。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 读出团通知书 | GET | `/v3/admin/order/group-batch/{groupBatchId}/docs/notice` | 新增 | 未保存过时返回按团期数据拼的草稿,不落库 | +| 2 | 保存出团通知书 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/docs/notice` | 新增 | 整体覆盖保存,`version` 乐观锁 | + +--- + +## 三、接口详情 + +### 1. 读出团通知书 `GET /v3/admin/order/group-batch/{groupBatchId}/docs/notice` + +**VO**: `GroupBatchNoticeRespVO` + +#### 使用场景 + +团期详情 → 底部操作条「打印 / 导出 ▾」→「生成出团通知书」→ 全屏浮层。打开浮层时调一次。 + +从未保存过 → 返回按团期现状拼的**草稿**(`saved=false`);已保存过 → 返回库内内容(`saved=true`)。两种情况都附带 `defaults` 实时默认值。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期主订单 ID | **是运营团期主键 `order_group_batch.group_batch_id`**,不是产品侧排期 ID `productBatchId` | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| title | String | 通知书标题 | +| greeting | String | 问候语 | +| meetTime | String | 集合时间。**展示文案,不是时间类型**,服务端不解析格式 | +| meetPlace | String | 集合地点。**无结构化来源,默认恒为空串**,由运营填 | +| leader | String | 领队姓名与介绍。默认值来自团期人员配置的导游位(`GUIDE` 与 `LEADER` 并收),按 `sortOrder` 升序、`、` 连接 | +| bus | String | 车辆信息(车型 + 车牌)。默认值来自团内子订单的**行程用车**快照,已去重 | +| contacts | String | 联系方式。默认值 = 首位领队姓名 + **脱敏**手机号 | +| service | String | 服务包含说明。**无结构化来源,默认恒为空串** | +| bring | String | 携带物品清单。**无结构化来源,默认恒为空串** | +| saved | Boolean | `false` = 本次是即时生成的草稿、尚未落库;`true` = 取自库内 | +| releasable | Boolean | 是否允许下发给客户。服务端计算,**客户端传值忽略**。`false` 时请置灰「打印 / 存 PDF」 | +| version | Integer | 乐观锁版本。`saved=false` 时恒为 `0`。**保存请求的 `expectedVersion` 取自这里** | +| updateTime | String | 最后保存时间,ISO-8601 **秒精度**(`2026-09-12T11:36:51`)。`saved=false` 时为 `null` | +| defaults | Object | **实时默认值快照**,含上面 9 个内容字段。`saved=false` 时与正文完全一致;`saved=true` 时供前端比对提示「领队 / 用车已变更,是否更新」 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2096495107078328322/docs/notice +Authorization: Bearer +``` + +#### 响应示例 + +未保存过(草稿): + +```json +{ + "code": 200, + "message": "成功", + "data": { + "title": "测试小蒙马-多档-固定金额 · 出团通知书", + "greeting": "亲爱的团友,欢迎参加本次行程!", + "meetTime": "2026-12-20 08:30", + "meetPlace": "", + "leader": "刘大山、萨仁高娃", + "bus": "mpv 蒙A-G8888", + "contacts": "刘大山 138****1005", + "service": "", + "bring": "", + "saved": false, + "releasable": false, + "version": 0, + "updateTime": null, + "defaults": { + "title": "测试小蒙马-多档-固定金额 · 出团通知书", + "greeting": "亲爱的团友,欢迎参加本次行程!", + "meetTime": "2026-12-20 08:30", + "meetPlace": "", + "leader": "刘大山、萨仁高娃", + "bus": "mpv 蒙A-G8888", + "contacts": "刘大山 138****1005", + "service": "", + "bring": "" + } + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +**默认值是「尽力而为」,任何一个来源取不到都退化为空串,接口照常 200、不报错**: + +- 团期未配人员 → `leader` / `contacts` 为空串 +- 团期无子订单 / 未配车 → `bus` 为空串 +- 团期出发日为空 → `meetTime` 为空串(**不会拼出 `null 08:30` 这种串**) +- 某一户的用车快照数据异常 → **只跳过那一户**,`bus` 用其余户拼出来;整张通知书照常返回 + +九个内容字段**永远是字符串,不会是 `null`**,前端不必判空指针。 + +#### 错误响应 + +| 码 | 符号 | 触发 | +|----|------|------| +| 589500 | `GROUP_BATCH_NOT_FOUND` | 团期不存在 / 已软删除 | +| 589507 | `GROUP_BATCH_PERMISSION_DENIED` | 无 `group-batch:docs` 权限,或请求未经网关鉴权 | + +```json +{ + "code": 589507, + "message": "无操作权限(非团期管理员 / 非本定制师名下)", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- **读接口不落库**:`saved=false` 的草稿只是即时拼出来的,不调这个接口再调保存,库里不会有任何行。 +- **保存后正文就冻结了**:之后换了领队、改了配车,已保存的 `leader` / `bus` **不会自动刷新**。这是对客定稿文档应有的行为——自动变更比不变更更危险(客人手里的版本会和系统里的对不上)。想提示用户,就拿 `defaults` 和正文逐字段比对。 +- **`groupBatchId` 不是 `productBatchId`**:本接口用运营团期主键;团期人员配置那组接口(`/v3/admin/group-batch/{productBatchId}/staff`)用的是产品侧排期 ID,两者不是同一个 ID 空间。 +- **通知书里没有出行人名单**,且不会有——对客可分发文档不带出行人信息。 + +--- + +### 2. 保存出团通知书 `PUT /v3/admin/order/group-batch/{groupBatchId}/docs/notice` + +**VO**: `GroupBatchNoticeSaveReqVO` + +#### 使用场景 + +通知书浮层「完成编辑」按钮。**整体覆盖**保存:9 个内容字段每次带齐。 + +> 原型里「完成编辑」目前只是 `setEditing(false)`、不发请求,需要 mmg 把本接口挂上去。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 团期主订单 ID | 同上 | +| title | Body | String | ✅ | `@NotBlank`,≤128 | 通知书标题,**不可为空串** | +| greeting | Body | String | ✅ | `@NotNull`,≤512 | 问候语,允许空串 | +| meetTime | Body | String | ✅ | `@NotNull`,≤64 | 集合时间。**服务端不解析时间格式、不与出发日做强校验**,随便填什么文案都行 | +| meetPlace | Body | String | ✅ | `@NotNull`,≤256 | 集合地点,允许空串 | +| leader | Body | String | ✅ | `@NotNull`,≤256 | 领队姓名与介绍。多领队在同一串内并列,**接口不拆结构** | +| bus | Body | String | ✅ | `@NotNull`,≤256 | 车辆信息,允许空串 | +| contacts | Body | String | ✅ | `@NotNull`,≤256 | 联系方式。**对客文档,手机号请按脱敏形态填写** | +| service | Body | String | ✅ | `@NotNull`,≤1024 | 服务包含说明,允许空串 | +| bring | Body | String | ✅ | `@NotNull`,≤1024 | 携带物品清单。**只写物品名,禁写任何客户证件号码**,命中即 589587 | +| expectedVersion | Body | Integer | ✅ | `@NotNull`,≥0 | 乐观锁版本。取自读接口的 `version`;**首次保存传 `0`** | + +> 原型把 `contacts` 拆成 `leaderPhone` / `managerPhone` 两个输入框,本接口是**单个 `contacts` 串**,口径请 mmg 对齐。 +> 原型「三 · 随身携带」「四 · 已含服务」目前渲染的是写死 `
  • ` 列表、未接输入控件,需要补控件才能编辑 `service` / `bring`。 + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| (同读接口全部字段) | — | 结构与读接口**完全一致** | +| saved | Boolean | 保存成功后恒为 `true` | +| version | Integer | **写入后的新版本**。下次保存把它当 `expectedVersion` 传回来 | +| updateTime | String | 本次写入时间,ISO-8601 秒精度 | +| defaults | Object | 实时默认值,供比对提示 | + +#### 请求示例 + +```json +{ + "title": "呼伦贝尔亲子研学 6 日 · 出团通知书", + "greeting": "亲爱的团友,欢迎参加本次行程!", + "meetTime": "2026-12-20 09:30", + "meetPlace": "海拉尔东山国际机场 T1 到达厅 3 号门", + "leader": "刘大山(10 年草原线经验)、萨仁高娃", + "bus": "mpv 蒙A-G8888", + "contacts": "领队 刘先生 138****1005 / 24 小时应急 400-888-8888", + "service": "含 5 早 8 正餐、5 晚住宿、全程用车、景区门票、旅游意外险", + "bring": "防晒霜、驱蚊液、外套(早晚温差 15℃)、常用药品", + "expectedVersion": 0 +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "title": "呼伦贝尔亲子研学 6 日 · 出团通知书", + "greeting": "亲爱的团友,欢迎参加本次行程!", + "meetTime": "2026-12-20 09:30", + "meetPlace": "海拉尔东山国际机场 T1 到达厅 3 号门", + "leader": "刘大山(10 年草原线经验)、萨仁高娃", + "bus": "mpv 蒙A-G8888", + "contacts": "领队 刘先生 138****1005 / 24 小时应急 400-888-8888", + "service": "含 5 早 8 正餐、5 晚住宿、全程用车、景区门票、旅游意外险", + "bring": "防晒霜、驱蚊液、外套(早晚温差 15℃)、常用药品", + "saved": true, + "releasable": false, + "version": 1, + "updateTime": "2026-09-12T11:36:51", + "defaults": { "leader": "刘大山、萨仁高娃", "bus": "mpv 蒙A-G8888", "...": "其余同读接口" } + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +内容字段允许全部传空串(除 `title` 必须非空)。此时保存成功,读回来就是九个空串,`saved=true`。**不存在「保存了但没生效」的降级分支**——要么 200 落库,要么返业务错误码且**零写入**。 + +#### 错误响应 + +| 码 | 符号 | 触发 | 前端该怎么办 | +|----|------|------|---| +| 589500 | `GROUP_BATCH_NOT_FOUND` | 团期不存在 / 已软删除 | 提示并关闭浮层 | +| 589507 | `GROUP_BATCH_PERMISSION_DENIED` | 无 `group-batch:docs` 权限 | 隐藏入口 | +| 589585 | `NOTICE_CONCURRENT_MODIFICATION` | `expectedVersion` 与库内不符,或并发首次保存撞唯一键。**零写入** | **重新 GET 读回全量再让用户提交**;直接重试一定还是 589585 | +| 589587 | `NOTICE_CONTENT_FORBIDDEN` | 任一字段命中证件号形态(连续 ≥15 位数字 / 18 位身份证形态) | 提示用户删掉证件号;响应**不回显**命中的那串号码 | +| 400 | — | `@Valid` 长度 / 必填不过 | 按字段提示 | + +```json +{ + "code": 589585, + "message": "通知书已被他人修改,请刷新后重试", + "data": null, + "traceId": null, + "success": false +} +``` + +```json +{ + "code": 589587, + "message": "通知书正文不可包含客户证件号码,请删除后重试", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- **保存不卡阶段**:任意团期状态都能保存,包括招募中。阶段门只管**下发**(`releasable`)。 +- **`releasable` 客户端传值忽略**:它不在请求体里,服务端按团期状态算。 +- **同一秒内的并发保存也能判出冲突**:锁用的是 `version` 整数,不是时间戳。两名运营同时点保存,后一个必然拿到 589585 而不是静默覆盖前一个。 +- **重复提交同一份内容不会产生第二份数据**:一个团期恒一行(唯一键),且带 `expectedVersion`,重放第二次必然 CAS 失败。 +- **证件号校验只看形态不看语义**:连续 15 位以上数字(银行卡形态)同样会被拦。正常文案里的日期 `2026-12-20`、脱敏手机 `138****1005`、座位数 `33 座`**不会**误伤。 + +--- + +## 四、契约约束与正确调用方式 + +- **编辑流程固定是:GET 读全量 → 用户改 → PUT 带 `expectedVersion` 全量提交 → 用响应里的新 `version` 更新本地状态。** 跳过第一步直接 PUT 会因为拿不到正确的 `version` 而失败。 +- **`expectedVersion` 首次传 `0`,之后一律用上一次响应的 `version`**,不要自己 +1。 +- **收到 589585 不要重试,要重读。** 重试是原样再发一次,版本还是旧的。 +- **正文按纯文本渲染**,不要 `v-html`,也不要在前端做 `escape` / `unescape` 加工后再提交——那会把用户原文改掉。 +- **`releasable=false` 时置灰「打印 / 存 PDF」**,但**不要**同时禁掉编辑与保存——招募期起草是允许的。 +- **`defaults` 只用来做提示,不要自动覆盖用户正文。** 想做「一键用最新默认值」按钮的话,把 `defaults` 填进表单让用户确认后再提交。 +- **两个 ID 别混用**:本接口是 `groupBatchId`(运营团期主键);团期人员配置接口是 `productBatchId`(产品侧排期 ID)。 +- **`updateTime` 是秒精度**,两个端点形态一致,可直接 `new Date(...)` 解析。 + +--- + +## 五、数据库行为 + +**新建表 `group_batch_notice`**(Flyway `V20260912_140__create_group_batch_notice.sql`,团期 : 通知书 = 1 : 1): + +| 列 | 类型 | 说明 | +|---|---|---| +| `notice_id` | BIGINT PK | 雪花 ID | +| `group_batch_id` | BIGINT | **唯一索引 `uk_group_batch_id`**,并发首写由它兜住 | +| `title` / `greeting` / `meet_time` / `meet_place` / `leader` / `bus` / `contacts` / `service_included` / `bring` | VARCHAR | 九个正文列,`NOT NULL DEFAULT ''`。注意 `service` 在库里叫 `service_included`(避开 MySQL 易混词),**API 字段名仍是 `service`** | +| `version` | INT | 乐观锁版本,首次保存写入 `1`,此后每次 +1 | +| `create_time` / `update_time` / `created_by` / `updated_by` / `deleted_at` | — | 标准审计列 | + +**写行为**: +- 读接口 `GET` **完全不写库**(草稿是内存里拼的)。 +- 写接口 `PUT` 只写 `group_batch_notice` 一张表:无行则 `INSERT`(`version=1`),有行则按 `version` CAS `UPDATE`。**CAS 命中 0 行即抛 589585,零写入。** +- **`order_group_batch` 零改动**:不加列、不改列、不推进 `batch_status`、不写状态流水。 +- **不发任何消息 / 通知**。 + +**另一条 Flyway 在 hl-user-service**:`V20260912_140__add_group_batch_docs_permission.sql`,注册权限码 `group-batch:docs` 并授予 `ADMIN` / `SUPER_ADMIN`(`INSERT IGNORE`,可安全重跑)。**这两条迁移必须同批部署**——否则端点对 ADMIN 不可用。 + +--- + +## 六、边界行为 + +| 场景 | 行为 | +|---|---| +| 从未保存过,GET | 200,`saved=false`、`version=0`、`updateTime=null`,正文 = 默认值 | +| 团期未配人员 | `leader` / `contacts` 空串,接口正常 200 | +| 团期无子订单 / 未配车 | `bus` 空串,接口正常 200 | +| 团期出发日为空 | `meetTime` 空串,**不会出现 `null 08:30`** | +| 某户用车快照数据异常 | **只跳过那一户**,`bus` 用其余户拼;读接口照常 200 | +| 团期状态是枚举外的历史脏值 | `releasable=false`,接口照常 200(不会因此 500) | +| 首次保存传 `expectedVersion=0` | 200,`version=1` | +| 表里无行但传了非 0 的 `expectedVersion` | 589585(客户端手上是已失效状态),零写入 | +| 两人同时首次保存 | 后到的撞唯一键 → 589585,零写入 | +| 两人同时改已存在的通知书 | 后提交的 CAS 命中 0 行 → 589585,零写入 | +| 正文含 `&` 或尖括号文字 | **原样存、原样返**,多轮读写往返逐字不变 | +| `bring` 里写了身份证号 | 589587,**整份不保存** | +| `contacts` 里写了银行卡号 | 589587,同上 | +| 招募中的团期保存 | **允许**,200;只是 `releasable=false` | + +--- + +## 七、不影响范围 + +- **`order_group_batch` 表零改动**,团期状态机、阶段推进、状态流水全部不受影响。 +- **团期人员配置那组接口(`/v3/admin/group-batch/{productBatchId}/staff*`)零改动** —— 本单只读它,未改其行为。 +- **用车需求 / 派车相关接口零改动** —— 本单只读用车快照。 +- **既有权限码零改动**:`group-batch:view` / `manage` / `export` / `list` / `finance:*` / `demand:confirm` / `contract:issue` 的号码、语义、授权关系全不变;本单只**追加**一个新码。 +- **既有错误码零改动**。 +- **网关路由零改动**(`/v3/admin/**` 通配已覆盖)。 +- **小程序端(mp 域)零影响**;其它微服务除 user-service 的一条权限种子外零改动。 +- **不发任何通知消息**。 + +--- + +## 八、测试环境已验证 + +**部署**:`hl-order-service-v3` + `hl-user-service` 同 @ `feat/7532-group-batch-notice`(已合并 dev-v3 = `86ff9d115`),均双实例滚动重启完成。**全部实测经真实网关 `api.test.1814.love:9443` + Bearer 鉴权**。 + +Flyway 实测:`flyway_schema_history` → `20260912.140 / create group batch notice / success=1 / 57ms`(order-v3)、`20260912.140 / add group batch docs permission / success=1 / 8ms`(user-service)。 + +| 场景 | 结果 | +|---|---| +| 未保存过 GET | 200,`saved=false` / `version=0` / `updateTime=null`;`leader="刘大山、萨仁高娃"`、`bus="mpv 蒙A-G8888"`、`contacts="刘大山 138****1005"`(**响应全文无明文手机号**) | +| 保存(`expectedVersion=0`) | 200,`saved=true` / `version=1`;紧接着 GET 逐字段相同 | +| 再用 `expectedVersion=0` 提交 | **589585**,随后 GET 内容与第一次保存完全一致、`version` 仍为 1(零写入) | +| `bring` 传 18 位身份证形态 | **589587**,随后 GET `bring` 未变、`version` 未递增(零写入) | +| `releasable` 逐状态 | `RECRUITING`/`MATERIAL_PREPARING` → `false`;`PENDING_DEPARTURE`/`TRAVELLING`/`SETTLED` → `true` | +| **读写往返恒等(两轮)** | 正文 `亲爱的团友,欢迎参加本次行程!A&B 与 <未加密>`,GET→原样 PUT→GET **连做两轮**,四次读到的正文**逐字相等**,从未出现 `&`;`version` 1→2→3 证明两次保存都真落了库。库内原文核对一致 | +| 权限门 | 种子生效后:`ADMIN`/`SUPER_ADMIN` → 200;`FINANCE`/`CUSTOMIZER` → 589507。阳性对照:同一 FINANCE token 调 `group-batch:view` 的端点 → 200 | + +**权限码缓存窗口实测**:种子落库后 `ADMIN` 先返 589507,**约 10 分钟后**自动翻成 200(user-service `PermissionService` 的角色权限码 Redis 缓存 TTL 为 10 分钟)。上线后请勿把这段窗口当成漏配。 + +**本地全量**:`mvn -o -pl hl-order-service-v3 -am test` → **`Tests run: 9982, Failures: 0, Errors: 13, Skipped: 49`**(对账:基线 9958 + 本单 24 例)。13 个 Errors 全部是 Testcontainers 找不到 Docker,与本单零交集。门禁 `RedLineArchTest` 12/12、`MapperBoundaryArchTest` 26/26、`LayerEnforcementTest` 5/5、`ErrorCodeUniquenessGuardTest` 3/3 全绿。 + +测试环境造数已全部回收:通知书行已删、临时配的团期人员已清空、临时挂靠的订单归属已还原、团期状态已还原。 + +--- + +## 十、相关文档 + +- Issue:https://git.1814.love:8443/wx/HL/issues/7532 +- PR:https://git.1814.love:8443/wx/HL/pulls/7577 +- 契约卡片(已更新为「已落地」并记下三处偏离):`docs/group/团期模块接口文档-v2.0.html` GB-ADM-081 / GB-ADM-082 +- 新表:`hl-order-service-v3/src/main/resources/db/migration/V20260912_140__create_group_batch_notice.sql` +- 权限种子:`hl-user-service/src/main/resources/db/migration/V20260912_140__add_group_batch_docs_permission.sql` + +--- + +## 关联 / 联系人 + +- 后端:jw +- 前端(hl-ui 管理后台):mmg