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

23 KiB

schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
schema ticket title consumer author change_type backend_status gateway_status frontend_status frontend_owner frontend_ref target_release verified_at status_note updated_at base
hl-changelog/v2 7532 团期出团通知书读写端点(GB-ADM-081 / 082) admin jw(GIT) 新增接口 deployed verified verified mmg 95518b50 2026-09-13 两个新端点。前端必接三件事:① 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 项全绿。 2026-09-13 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 时供前端比对提示「领队 / 用车已变更,是否更新」

请求示例

GET /v3/admin/order/group-batch/2096495107078328322/docs/notice
Authorization: Bearer <admin token>

响应示例

未保存过(草稿):

{
  "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 权限,或请求未经网关鉴权
{
  "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 实时默认值,供比对提示

请求示例

{
  "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
}

响应示例

{
  "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 长度 / 必填不过 按字段提示
{
  "code": 589585,
  "message": "通知书已被他人修改,请刷新后重试",
  "data": null,
  "traceId": null,
  "success": false
}
{
  "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