文件
hl-api-changelog/changelogs-v2/2026-09/12_7530_团期staff去重与报账人扇出限定本团-修改接口-管理后台.md
Mimingguang 10502ccdd3
changelog-filename-gate / validate (push) Failing after 2s
chore(changelog): 补回写漏网 8 条(7510×2/7511/7512 verified+not_required;7513/7530/7531/7535 not_required)
7510/7511/7512 代码早已随供应商关系系列交付(a17a2c2a/4bedc8be),实证后回写 verified 挂 a17a2c2a;
11_7510 为正本 12_7510 的重复副本(canonical_path),标 not_required 消除 pending;
7513/7530/7531/7535 实证前端零改动 not_required。
2026-09-13 09:51:52 +08:00

22 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 7530 团期 staff 配置 staffId 去重(新增 589582)+ 报账人订单副本扇出收窄到本团 admin jw(GIT) 修改接口 deployed verified not_required mmg 2026-09-12 两处变化都需要前端知晓:① staffList 内 staffId 重复将被 589582 拒绝、整批不保存,前端需提交前去重或直接透出文案;② 报账人等级的订单副本同步范围由「全库」收窄为「本团活跃订单」,响应结构与错误码不变。 前端 2026-09-13 闭环 not_required:staff 保存唯一调用方 GroupBatchStaffConfigModal 用 Set+checkbox 构造 staffList 结构无法重复,589582 经 silentError 兜底透文案;reporter-rank 端点前端零调用方,扇出收窄纯后端副作用。零业务代码改动。 2026-09-13 dev-v3

order-v3: 团期 staff 配置 staffId 去重 + 报账人扇出限定本团

服务: hl-order-service-v3 PR: #7571 Issue: #7530


⚠️ 关键变化

🔴 staffList 内 staffId 重复会被拒绝(新增 589582),整批不保存。 同一员工不能在同一团期占两个配置位(比如既当导游又当摄影)。前端需要在提交前自行去重,或把 589582 的文案直接透出给运营。

为什么是拒绝而不是静默去重:重复的两条 item 可以携带不同的 staffRole / sortOrder / remark,静默去重等于服务端替业务随机挑一条,没有正确答案。

🔴 改前这个重复会把该团期的报账人设置入口打成永久 500。 重复的两条会原样写两行,之后调 reporter-rank 时那个「按团期+员工查单行」的查询命中多行,抛出未转译的 TooManyResultsException —— 运营看到的是裸 500,没有可理解的提示,也没有自助恢复手段。本单同时把读取侧改成容错(见下)。

🔴 报账人等级的「订单副本」同步范围由「全库」收窄为「本团活跃订单」。 改前给 A 团设主报账人,会把与 A 团毫无关系的 B、C 团的订单副本一起改掉;改后只动本团。响应结构与错误码不变,前端若此前依赖过跨团联动(不应存在),需自查。


一、背景

团期报账人(PRIMARY 主报账人 / SECONDARY 次报账人)决定团期预支与结算时款项打给谁 —— 财务候选接口就是按 reporterRank 排序并把 PRIMARY 标为默认收款人。

本单修两个互相独立、落在同一批文件的缺陷:

  1. 保存不校验重复:staffList 内两条相同 staffId 会原样落两行,之后报账人设置入口永久 500。
  2. 扇出越界:设置报账人时,团级行的更新已限定本团,但紧接着两次订单副本写入没有任何团期或订单维度条件,会横扫全库所有团期共享来源的副本行。后果是跨团数据污染 —— 被打坏的团里,团级表说乙是主报账人、订单副本说乙是 NONE,两张表长期不一致且不报任何错。团期越多、配主报账人的动作越频繁,被打坏的团越多。

两处不是同一个根因:缺陷 2 与是否重复无关,缺陷 1 即使修完,扇出照样越界。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 团期 staff 全量保存 PUT /v3/admin/group-batch/{productBatchId}/staff 修改 新增错误码 589582:staffList 内 staffId 重复即拒绝,整批不保存
2 设置团期报账人等级 PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank 修改 行为收敛:订单副本同步范围由「全库」收窄为「本团活跃订单」;存量重复行不再 500

三、接口详情

1. 团期 staff 全量保存 PUT /v3/admin/group-batch/{productBatchId}/staff

VO: BatchStaffConfigReqVO

使用场景

hl-ui 管理后台团期详情页「配置人员」,全量覆盖该团期的共享 staff 配置,并异步扇出到团内活跃订单。本单起,请求体内 staffId 重复会被拒绝。

入参字段表

字段 位置 类型 必填 约束 说明
productBatchId Path Long ✅ 产品侧排期 ID 不是运营团期主键 groupBatchId
staffList Body Array ✅ 全量覆盖语义 显式传 [] = 清空;缺字段 / 传 null → 400(行为不变)
staffList[].staffId Body Long ✅ 本单新增约束:列表内不得重复,重复即 589582 员工 ID
staffList[].staffRole Body String ✅ LEADER|GUIDE|DRIVER|PHOTOGRAPHER|OTHER|GUIDE_ASSISTANT|STUDY_TEACHER|LIFE_TEACHER 角色(行为不变)
staffList[].sortOrder Body Integer ❌ 默认 0 展示排序(行为不变)
staffList[].remark Body String ❌ ≤500 备注(行为不变)

出参字段表

字段 类型 说明
staffList Array 结构完全不变(id / staffId / staffRole / staffName / staffPhone / avatarUrl / sortOrder / remark)
affectedOrderCount Integer 语义完全不变,本团活跃订单数

请求示例

{
  "staffList": [
    { "staffId": 1005, "staffRole": "LEADER", "sortOrder": 0 },
    { "staffId": 1008, "staffRole": "LEADER", "sortOrder": 1 }
  ]
}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "staffList": [
      { "id": "2098606570236481538", "staffId": 1005, "staffRole": "LEADER", "staffName": "刘大山", "staffPhone": "138****1005", "avatarUrl": null, "sortOrder": 0, "remark": null },
      { "id": "2098606570240675841", "staffId": 1008, "staffRole": "LEADER", "staffName": "萨仁高娃", "staffPhone": "139****1008", "avatarUrl": null, "sortOrder": 1, "remark": null }
    ],
    "affectedOrderCount": 2
  },
  "success": true
}

空数据 / 降级响应

显式传 {"staffList": []} 仍是清空语义(不会误报 589582):

{
  "code": 200,
  "message": "成功",
  "data": { "staffList": [], "affectedOrderCount": 2 },
  "success": true
}

错误响应

码 符号 触发 本单
589582 BATCH_STAFF_DUPLICATED staffList 内出现重复 staffId 🆕 新增
400 — 缺 staffList 字段 / 传 null 不变
582114 — 角色与人员类型不符 不变
589552 / 589553 — 团期未成团 / 未建团 不变
589507 GROUP_BATCH_PERMISSION_DENIED 无 group-batch:manage 权限 不变
{
  "code": 589582,
  "message": "同一员工在本次团期人员配置中重复出现(staffId=1005),请去重后重试",
  "data": null,
  "traceId": null,
  "success": false
}

文案里的 staffId 是原样的数字字符串(1005),不带千位分隔符,可直接拿去页面上定位那个人。

业务边界

  • 拒绝时零写入、零 Feign:校验在人员反查(Feign)之前,必然失败的请求不会打任何下游、不会软删旧配置、不会插任何行 —— 调用前后 order_batch_staff 逐字节一致(行数、主键、update_time 全等),扇出副本也一格未动。
  • staffId 为 null 的元素跳过不判:@NotNull 已在参数校验层挡住,不在这里重复造错误语义。
  • 同一员工不能在同一团期占两个配置位。若业务确有「一人两位」的诉求,属新需求,不在本单。
  • 存量重复行本单不清理:order_batch_staff 目前没有 (product_batch_id, staff_id) 唯一索引,本单也不加(无 DDL 序号)。存量重复由接口 2 的读取侧兜底,数据订正与建索引列入后续工单。

2. 设置团期报账人等级 PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank

VO: SetReporterRankReqVO

使用场景

hl-ui 管理后台把团期内某位 staff 设为主报账人 / 次报账人 / 无。该等级决定团期预支与结算时款项打给谁。本单起,订单副本的同步范围收窄为本团活跃订单。

入参字段表

字段 位置 类型 必填 约束 说明
productBatchId Path Long ✅ 产品侧排期 ID 入参形态完全不变
staffId Path Long ✅ 员工 ID,须已在该团期已派列表内 入参形态完全不变
reporterRank Body String ✅ PRIMARY / SECONDARY / NONE 入参形态完全不变

出参字段表

字段 类型 说明
data null 响应结构完全不变,成功即 {"code":200,"data":null,"success":true}

请求示例

{
  "reporterRank": "PRIMARY"
}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": null,
  "traceId": null,
  "success": true
}

空数据 / 降级响应

本团没有任何活跃订单(订单全为已完成 / 已取消,或该团零订单)时,仍返 200,团级行照常更新,只是不执行任何订单副本写入:

{
  "code": 200,
  "message": "成功",
  "data": null,
  "traceId": null,
  "success": true
}

错误响应

本单未新增、未调整任何错误码。

码 符号 触发 本单
589508 GROUP_BATCH_REPORTER_NOT_ASSIGNED staffId 不在该团期已派列表 不变
589507 GROUP_BATCH_PERMISSION_DENIED 无 group-batch:manage 权限 不变
589552 / 589553 — 团期未成团 / 未建团 不变
裸 500 — 存量重复行导致 TooManyResultsException ✅ 本单消除
{
  "code": 589508,
  "message": "该员工不是本团期的已派人员,无法设置报账人",
  "data": null,
  "traceId": null,
  "success": false
}

业务边界

  • 报账人唯一性的作用域是「团内」,不是全局:设新主时,只有本团的旧主降级为 NONE;别的团不受任何影响。
  • 跨团同人:同一位员工同时在 A、B 两团时,在 A 团把他设为主报账人,不会改动 B 团里他的副本。
  • source=ORDER 的人员行永不被触碰:本端点只同步团期共享来源(GROUP_BATCH)的副本。
  • 已完成 / 已取消订单的副本保持历史值:活跃订单口径排除这两类,它们的副本不再随团期设置更新,停在历史值。这是已知且接受的口径收窄 —— 已完成订单的人员行本就是归档快照,且改前它反而会被别的团的操作随机改坏。
  • 存量重复行不再 500:读取侧改为「取 batch_staff_id 最小的一行(最早插入的那行)」,返回 200 而不是裸 500。存量重复时另一行的等级保持旧值,由后续的数据订正单一次性清理。
  • 并发双主窗口本单不修:两个管理员同时把甲、乙设为主报账人,仍可能留下双主。这是既有缺口,与本单两个缺陷无因果关系,已列后续工单。

四、契约约束与正确调用方式

  • 提交 staff 配置前请自行去重,或把 589582 的文案直接透出。判断依据是 code === 589582,文案里已含出事的 staffId。
  • 不要把 589582 当成可重试错误:原样重发一定还是 589582,必须先去重。
  • 不要依赖跨团联动:在 A 团设报账人不会、也不应该改动 B 团。若有页面逻辑建立在「改一个团会同步别的团」之上,那个假设改前就是缺陷的表现,不是契约。
  • 团期 staff 列表接口看不到报账人等级:BatchStaffConfigRespVO.BatchStaffItemVO 不含 reporterRank 字段。要读报账人等级请用:
    • 团级:GET /v3/admin/order/group-batch/{groupBatchId}/advance/payee-candidates(注意这里是运营团期主键 groupBatchId,不是 productBatchId)
    • 订单副本:GET /v3/admin/order/{orderId}/staff 的 reporterRank
  • 两个 path 参数的 ID 空间不同:本单两个端点用的是产品侧排期 ID productBatchId;财务候选接口用的是运营团期主键 groupBatchId。不要混用。

五、数据库行为

本单零数据库变更:无新增/修改表、无新增列、无新增索引、无 Flyway 迁移脚本。

表 本单行为
order_batch_staff 读写,无结构变更。写入侧多了一道请求体去重校验(拒绝时零写入);读取侧由「查唯一一条」改为「取 batch_staff_id 最小的一条」,对存量重复行容错
order_staff_assignment 写,无结构变更。两次报账人等级同步的 WHERE 条件各多了一个订单集合限定(order_id IN (本团活跃订单)),由全表收窄为本团
order_main 只读,经既有服务契约取本团活跃订单 ID(排除已完成 / 已取消)

存量数据本单不订正(明确列出,避免误以为已修):

  • order_batch_staff 中已产生的重复行不清理;
  • 因原缺陷被跨团清成 NONE 的 order_staff_assignment 副本不回填。

两项均已列入后续工单(数据订正 → 再加唯一索引,顺序不能反)。


六、边界行为

场景 行为
staffList 内两条相同 staffId(staffRole 不同) code=589582,整批不保存,零 Feign、零写入
staffList 内两条相同 staffId(完全相同) 同上,code=589582
staffList 为 [] 200,清空语义,不误报 589582
缺 staffList 字段 400「staff 配置列表不能缺失;确要清空请显式传空数组 []」
staffId 互不相同 200,结构与 affectedOrderCount 与改前一致
库里已有同一员工的两行(存量脏数据),调 reporter-rank 200(改前是裸 500),更新的是 batch_staff_id 较小的那一行
A 团设主报账人,B 团有同等级的人 B 团团级与订单副本一格不动(改前 B 团副本会被清成 NONE)
同一员工同时在 A、B 两团,在 A 团设为主报账人 B 团里他的副本保持原值(改前会被一并置为 PRIMARY)
本团零活跃订单,设报账人 200,团级行照常更新,零副本写入
本团有已完成 / 已取消订单 它们的副本保持历史值,不再被更新(已知且接受)
订单里 source=ORDER 的人员行 永不被触碰

六.6、修改前后对比

维度 改前 改后
PUT .../staff 传重复 staffId 200,原样写两行 589582,整批不保存
重复写入后再调 reporter-rank 裸 500(TooManyResultsException,非业务码),该团期入口永久不可用 200,取最早插入的那一行
reporter-rank 的订单副本同步范围 全库所有团期共享来源的副本 本团活跃订单
在 A 团设主报账人对 B 团的影响 B 团同等级副本被清成 NONE、同人副本被置为 PRIMARY(与 B 团团级表长期不一致,且不报错) 零影响
本团零活跃订单时 仍执行全库更新 零副本写入,团级行照常更新
已完成 / 已取消订单的副本 会被(别的团的操作随机)改动 保持历史值,不再被更新
source=ORDER 的人员行 不受影响 不受影响(不变)
两个端点的路径 / 方法 / 请求体结构 — 完全不变
两个端点的响应体结构 — 完全不变
reporter-rank 的错误码 — 无新增、无调整

六.7、影响评估

  • 兼容性:
    • 端点 1 新增一个失败分支。原先能通过的「同一人配两个位」的请求会从 200 变成 589582。本次调查未发现前端存在该姿势,但归属方是 hl-ui,请 mmg 自查。其余请求形态行为完全不变。
    • 端点 2 响应结构与错误码完全不变,只是副作用范围收窄。正常使用方无感知。
  • 需要前端动的:
    1. 提交 staff 配置前去重,或把 589582 文案透出;
    2. 自查是否有依赖跨团联动的逻辑(不应存在)。
  • 数据影响:本单只止血,不订正存量。已被打坏的副本需要等后续的数据订正单,在那之前团级表与订单副本可能仍不一致。
  • 性能:reporter-rank 每次多一条本团活跃订单的主键查询(本服务内 DB 查询,非 Feign);换来的是两条 UPDATE 由���表扫描收窄为按订单集合命中,代价远小于改前。单团扇出量通常 <20 个订单,IN 列表不会膨胀到需要分批。
  • 不影响任何其它服务:本单只改 order-v3,不动网关路由、不动 hl-common、不动其它微服务。

七、不影响范围

  • GET /v3/admin/group-batch/{productBatchId}/staff(查配置)与 /staff/candidates(候选列表)零改动。
  • GET /v3/admin/order/group-batch/{groupBatchId}/advance/payee-candidates(财务候选)零改动 —— 它是本单验收时的观测出口,本身未被修改。
  • GET /v3/admin/order/{orderId}/staff(订单人员)零改动 —— 同上,仅作为观测出口。
  • source=ORDER / source=FLEET 的人员行零影响(实测 FLEET 524 行 / 2 个主报账人在全程各次动作后数字未变)。
  • 权限口径零改动:两个写口仍是 group-batch:manage,拒绝时零写入。
  • 零改动:Controller 方法体、Entity、Flyway、网关配置、hl-common、hl-mp-service、hl-fleet-service、小程序端。
  • 既有错误码零调整:589507 / 589508 / 589552 / 589553 / 582114 的号码、文案、触发条件全部不变。

八、测试环境已验证

部署:hl-order-service-v3 @ fix/7530-staff-dedupe-fanout / 4bc6f4855(已合并 dev-v3 = d9ed89e55),双实例滚动重启完成(8086 / 8186)。全部实测经真实网关 api.test.1814.love:9443 + Bearer 鉴权。

造数用了 3 个既有团期(未新建团期、未下新订单):A 团 2 个活跃订单、B 团 2 个活跃订单 + 1 个已取消订单、C 团 零订单。员工取资源域真人:甲 1005 / 乙 1007 / 丙 1008 / 丁 1009。测试后现场已完全恢复(两张表的活跃行都回到测前的 0 行,3 条手工夹具行已硬删)。

去重(端点 1)

PUT /v3/admin/group-batch/2096494989130260482/staff
body: {"staffList":[{"staffId":1005,"staffRole":"LEADER"},{"staffId":1008,"staffRole":"LEADER"},{"staffId":1005,"staffRole":"GUIDE"}]}
-> {"code":589582,"message":"同一员工在本次团期人员配置中重复出现(staffId=1005),请去重后重试"}
   调用前后 order_batch_staff 逐字节一致(行数 / 主键 / update_time 全等);扇出副本 update_time 也未变

正常路径与边界:staffId 互不相同 → 200,affectedOrderCount=2,结构与改前一致;{} → 400「staff 配置列表不能缺失」;{"staffList":[]} → 200 清空语义,不误报 589582。

存量重复行不再 500(端点 2)

手工插入第二行同 (团期, 1005),batch_staff_id=9200000000000000001(大于既有行 2098605826007609346):

PUT /v3/admin/group-batch/2096494989130260482/staff/1005/reporter-rank  body:{"reporterRank":"SECONDARY"}
-> {"code":200,"message":"成功","success":true}          ← 改前这里是 TooManyResultsException 裸 500

2098605826007609346  1005  SECONDARY   ← 被更新的是 batch_staff_id 较小的那一行
9200000000000000001  1005  NONE        ← 重复行保持旧值

两团隔离(核心)

初始态:A 团 丙1008=PRIMARY,B 团 乙1007=PRIMARY(团级与两团各自的活跃订单副本均已就位)。核心动作:

PUT /v3/admin/group-batch/2096494989130260482/staff/1005/reporter-rank  body:{"reporterRank":"PRIMARY"}
-> {"code":200,"success":true}
观测出口 结果
A 团 payee-candidates 甲1005 PRIMARY(isDefault:true)、丙1008 NONE ✅
A 团每一个活跃订单 GET /v3/admin/order/{id}/staff 甲1005 PRIMARY、丙1008 NONE ✅(2/2 单)
B 团 payee-candidates 乙1007 仍 PRIMARY、甲1005 仍 NONE ✅
B 团每一个活跃订单 乙1007 仍 PRIMARY、甲1005 仍 NONE ✅(2/2 单,改前此处会变成 NONE)
A 团订单里 source=ORDER 的丁1009 仍 PRIMARY,未被触碰 ✅
B 团已取消订单上的副本 仍 PRIMARY(保持历史值,已知且接受) ✅

零活跃订单(且是对扇出收窄的第二条、更强的证明)

C 团全团零订单,且甲1005 此刻在 A、B 两团都有副本。若扇出仍是全局的,在一个零订单的团里把甲设为主报账人会把全库所有甲的副本刷成 PRIMARY、并把所有别人的 PRIMARY 清成 NONE:

PUT /v3/admin/group-batch/2089667254789484545/staff/1005/reporter-rank  body:{"reporterRank":"PRIMARY"}
-> {"code":200,"success":true}
   团级行正确更新为 PRIMARY
   全库团期共享副本:动作前后 COUNT(*)=9、MAX(update_time)=10:55:02 完全不变 → 零副本写入 ✅

返回 200 而不是 SQL 语法错误,也证明空集合确实没有进入 IN 条件。

本地全量单测

mvn -o -pl hl-order-service-v3 -am test → Tests run: 9951, Failures: 0, Errors: 13, Skipped: 49 (数字对账:dev-v3 基线 9942 + 本单新增 9 例 = 9951)。 13 个 Errors 全部是 Testcontainers 找不到 Docker(跑全量前停了本机 colima 腾内存),与本单零交集。 门禁:RedLineArchTest 12/12、MapperBoundaryArchTest 26/26、ErrorCodeUniquenessGuardTest 3/3 全绿。


十、相关文档

  • Issue:https://git.1814.love:8443/wx/HL/issues/7530
  • PR:https://git.1814.love:8443/wx/HL/pulls/7571
  • 新增错误码:GroupBatchErrorCode.BATCH_STAFF_DUPLICATED = 589582(团期段 589500-589599)
  • 报账人等级枚举与「团期内唯一」口径:hl-order-service-v3/src/main/java/com/hulalv/order/assignment/enums/ReporterRank.java
  • 团级报账人观测出口:GET /v3/admin/order/group-batch/{groupBatchId}/advance/payee-candidates
  • 订单副本观测出口:GET /v3/admin/order/{orderId}/staff

关联 / 联系人

  • 后端:jw
  • 前端(hl-ui 管理后台):mmg