文件
hl-api-changelog/changelogs-v2/2026-09/12_7532_团期出团通知书读写端点-新增接口-管理后台.md
2026-09-13 10:20:40 +08:00

413 行
23 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
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: "verified"
frontend_owner: "mmg"
frontend_ref: "95518b50"
target_release: ""
verified_at: "2026-09-13"
status_note: "两个新端点。前端必接三件事:① expectedVersion 必填、首次传 0、收到 589585 必须重读再提交(不能直接重试);② releasable=false 时置灰「打印 / 存 PDF」;③ 正文是纯文本语义,后端不转义也不反转义,前端按纯文本渲染。另:新权限码 group-batch:docs 在 user-service 有 10 分钟缓存,上线后最多 10 分钟才对 ADMIN 生效。 前端 2026-09-13 已交付(hl-admin 95518b50):新建 groupBatchNotice.js(V3 baseURL 置空+雪花串透传)+7 例定向单测;新建 GroupBatchNoticeModal 全屏浮层左表单右纯文本预览,详情页「更多操作」常挂入口。落实红线:expectedVersion 首次传 0/此后取响应 version 禁自增;589585 重新 GET 读回再提交禁原样重试;releasable=false 只置灰打印/存PDF编辑保存可用;正文纯文本禁 v-html;589587 透后端 message;contacts 单串不拆。单测 7/7+checkpoint 13 项全绿。"
updated_at: "2026-09-13"
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 <admin token>
```
#### 响应示例
未保存过(草稿):
```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 对齐。
> 原型「三 · 随身携带」「四 · 已含服务」目前渲染的是写死 `<li>` 列表、未接输入控件,需要补控件才能编辑 `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 **连做两轮**,四次读到的正文**逐字相等**,从未出现 `&amp;`;`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