文件
hl-api-changelog/changelogs-v2/2026-09/30_8654_团期正式派车司机投影到人员配置表-修改接口-管理后台.md
T
API Changelog Bot和Claude Opus 5 a29c0873e3
changelog-filename-gate / validate (push) Failing after 2s
docs(changelog): 团期正式派车司机投影到人员配置表,DRIVER 改系统托管(#8653 #8654)
PR #8669 已合入 dev-v3(merge commit 5c50782717d6),order-v3 已部署测试服
(jar 5c5078271)并逐条取证通过。

三条对外契约变化:
1. 同一批数据两个读口口径不同——saveConfig 的响应回显与 GROUP_BATCH 扇出副本
   不含司机行(走 selectManualConfigByProductBatchId,带 ne(staff_role, DRIVER)),
   而 getConfig / getConfigWithLiveStaffInfo 含司机行(走 selectByProductBatchId,
   无该谓词)。前端不要假设保存响应即全量。
2. order_batch_staff 现在会出现系统写入的 DRIVER 行,由车务派车回调投影维护,
   不提供人工编辑/删除入口。
3. 新错误码 582120:saveConfig 的 staffList 含 staffRole=DRIVER,或 scopeRoles
   声明 DRIVER,均拒绝整批保存且零写入(守卫排在软删与插入之前)。

Refs #8653
Refs #8654

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 22:34:15 +08:00

22 KiB
原始文件 Blame 文件历史

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 8654 团期正式派车司机投影到人员配置表,DRIVER 角色改系统托管 admin wx(GIT) 修改接口 deployed verified pending 2026-09-30 2026-09-30 dev-v3

团期人员配置: 正式派车司机投影到人员配置表,DRIVER 改系统托管

存放目录: changelogs-v2/2026-09/

服务: hl-order-service-v3 PR: #8669 Issue: #8653, #8654 日期: 2026-09-30 影响范围: 管理后台团期人员配置页、团期核单按团看人的名册读取


⚠️ 关键变化

团期人员配置表 order_batch_staff 现在会有系统自动生成的 DRIVER 行(司机),前端必须适配两个关键变化:

  1. 同一批数据的两个读口口径不同: 保存接口响应里不含司机行,但查询接口(getConfig / 名册读取)含司机行。前端不要假设响应即全量。

  2. 司机行不可编辑: 这些 DRIVER 行由车务派车自动投影产生,不是运营配置的,前端不要在人员编辑表单提供编辑/删除入口。

  3. 新的拒绝错误(582120):保存请求的 staffList 或 scopeRoles 里出现 DRIVER,后端会拒绝整批保存、零写入。


一、背景

工单 #8653 与 #8654 合并的功能:把车务派车产生的司机自动投影到团期人员配置表,供核单时按团看人、按天算账。

此前司机事实分散在每一户订单的用车需求上,现在统一投影到团期维度,简化核单逻辑。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 保存团期 staff 配置 PUT /v3/admin/group-batch/{productBatchId}/staff 新增错误码 + 响应内容变化 DRIVER 行系统管理,拒绝人工写入

三、接口详情

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

VO: BatchStaffConfigReqVO → BatchStaffConfigRespVO

使用场景

管理后台团期人员配置页,点「保存」按钮时调用此接口保存本期的团队成员(导游、摄影等)。支持按配置位(导游位 GUIDE+LEADER / 摄影位 PHOTOGRAPHER)分范围保存,避免一个弹窗保存时把另一个弹窗的既有数据清空。

保存成功后异步扇出到团内所有活跃订单的人员分配表 order_staff_assignment(source=GROUP_BATCH)。

入参

字段 位置 类型 必填 约束 说明
productBatchId Path Long ✅ 必须是有效的产品侧排期 ID 团期所属产品侧班期 ID(非运营团期主键,由 group_tour_batch.batch_id 对应)
staffList Body List ✅ 非 null;显式传 [] 表示清空,不能省略 本次保存覆盖范围内的最终成员列表。禁止含 staffRole=DRIVER 的行(582120)。若覆盖范围是导游位,必须同时列出 GUIDE 与 LEADER 两个角色成员(工单 #8122)。
staffList[].staffId Body Long ✅ 有效的员工 ID 用户域员工 ID
staffList[].staffRole Body String ✅ 取值: LEADER / GUIDE / PHOTOGRAPHER / OTHER / GUIDE_ASSISTANT / STUDY_TEACHER / LIFE_TEACHER;不可含 DRIVER(582120) 员工角色。司机行由车务派车自动投影,一律不由本接口写入。
staffList[].sortOrder Body Integer ❌ 缺省 0 显示排序值(升序排列)
staffList[].remark Body String ❌ ≤500 字符;null 表示保留原值,传空串清空 备注,仅供参考
staffList[].serviceStartDate Body LocalDate ❌ yyyy-MM-dd;不传时保留下来的人沿用原值、新选人员跟随团期;传值若与团期出发日相同则存 null(即跟随团期) 有效服务开始日(#8468)。若自定义则必须在团期出发日到结束日之间,否则 582119 拒绝。
staffList[].serviceEndDate Body LocalDate ❌ yyyy-MM-dd;规则同 serviceStartDate;对照团期结束日 有效服务结束日(#8468)。
scopeRoles Body List ❌ 元素不能为 null / 空串 / 纯空白;传了必须 size ≥ 1 本次保存覆盖的角色范围。不传 = 整期全量覆盖(历史行为);传了 = 只覆盖这些角色,范围外既有行不动(工单 #8006)。导游位必须同时传 GUIDE 与 LEADER(工单 #8122),只传一个拒绝 582116 且零写入。staffList 里出现范围外角色拒绝 582115 且零写入。禁止传 DRIVER(582120)。

出参 Result<BatchStaffConfigRespVO>

字段 类型 说明
data.productBatchId Long 路径参数回显
data.groupBatchId Long 运营团期 ID(order_group_batch 主键)
data.staffList List 本次保存后保留下来的整期非司机成员的最新快照。不含 DRIVER 行(工单 #8654)。
data.staffList[].id Long 记录 ID(batch_staff_id)
data.staffList[].staffId Long 员工 ID
data.staffList[].staffRole String 员工角色(LEADER / GUIDE / PHOTOGRAPHER 等,响应侧不含 DRIVER)
data.staffList[].staffRoleName String 员工角色中文名(「导游」「领队」等;取值不在字典内时回落原 code)
data.staffList[].staffName String 员工姓名(配置时快照)
data.staffList[].staffPhone String 员工手机(脱敏:前 3 后 4,如 138****6677)
data.staffList[].avatarUrl String 头像 URL(配置时快照)
data.staffList[].sortOrder Integer 显示排序值
data.staffList[].remark String 备注
data.staffList[].reporterRank String 报账人等级:PRIMARY(主) / SECONDARY(次) / NONE(非报账人)
data.staffList[].reporterRankName String 报账人等级中文名
data.staffList[].serviceStartDate LocalDate 有效服务开始日(未自定义时 = 团期出发日,团期改期后跟着变;#8468)
data.staffList[].serviceEndDate LocalDate 有效服务结束日(规则同上)
data.staffList[].serviceDateCustom Boolean 是否自定义过服务日期;true 时开始/结束至少一个有自定义值
data.staffList[].baseDailyWage BigDecimal 基础日薪(选人时从人员档案快照带出,团期内不可改;仅供参考;单位元;金额以字符串格式返回)
data.staffList[].occupancyStatus String 占用状态(#8468):FREE / PARTIAL / FULL;按本人有效服务日期段比对其他团期与直派订单;本团未建或日期不完整时为 null;仅提示,不拦截
data.staffList[].occupiedDays Integer 被占天数(两端都算);null 时为 0
data.staffList[].freeRanges List 可派日期段列表(本人有效日期段内未被占的连续区间);恒非 null;全程占用时为 []
data.staffList[].occupancies List 占用明细数组;恒非 null;空闲时为 []
data.affectedOrderCount Integer 扇出影响的订单数(已触发异步写入的活跃子订单数)

请求示例

PUT /v3/admin/group-batch/80001/staff HTTP/1.1
Host: admin-api.test.example.com
Authorization: Bearer {token}
Content-Type: application/json

{
  "scopeRoles": ["GUIDE", "LEADER"],
  "staffList": [
    {
      "staffId": 40001,
      "staffRole": "LEADER",
      "sortOrder": 0,
      "remark": "首席领队",
      "serviceStartDate": "2026-10-01",
      "serviceEndDate": "2026-10-06"
    },
    {
      "staffId": 40002,
      "staffRole": "GUIDE",
      "sortOrder": 1,
      "remark": null,
      "serviceStartDate": null,
      "serviceEndDate": null
    }
  ]
}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "productBatchId": 80001,
    "groupBatchId": 90211,
    "staffList": [
      {
        "id": 770001,
        "staffId": 40001,
        "staffRole": "LEADER",
        "staffRoleName": "领队",
        "staffName": "刘领队",
        "staffPhone": "138****6677",
        "avatarUrl": "https://example.com/avatar/40001.jpg",
        "sortOrder": 0,
        "remark": "首席领队",
        "reporterRank": "PRIMARY",
        "reporterRankName": "主报账人",
        "serviceStartDate": "2026-10-01",
        "serviceEndDate": "2026-10-06",
        "serviceDateCustom": true,
        "baseDailyWage": "600.00",
        "occupancyStatus": "PARTIAL",
        "occupiedDays": 2,
        "freeRanges": ["2026-10-01~2026-10-03", "2026-10-05~2026-10-06"],
        "occupancies": [
          {
            "groupBatchId": 90212,
            "groupBatchName": "十一国庆游 V2 期",
            "conflictDates": ["2026-10-02", "2026-10-04"]
          }
        ]
      },
      {
        "id": 770002,
        "staffId": 40002,
        "staffRole": "GUIDE",
        "staffRoleName": "导游",
        "staffName": "王导游",
        "staffPhone": "138****5678",
        "avatarUrl": "https://example.com/avatar/40002.jpg",
        "sortOrder": 1,
        "remark": null,
        "reporterRank": "NONE",
        "reporterRankName": "非报账人",
        "serviceStartDate": "2026-10-01",
        "serviceEndDate": "2026-10-06",
        "serviceDateCustom": false,
        "baseDailyWage": "500.00",
        "occupancyStatus": "FREE",
        "occupiedDays": 0,
        "freeRanges": ["2026-10-01~2026-10-06"],
        "occupancies": []
      }
    ],
    "affectedOrderCount": 3
  },
  "success": true
}

空数据 / 降级响应

整期清空后的响应(staffList=[] 加 scopeRoles 不传):

{
  "code": 200,
  "message": "成功",
  "data": {
    "productBatchId": 80001,
    "groupBatchId": 90211,
    "staffList": [],
    "affectedOrderCount": 3
  },
  "success": true
}

错误响应

错误码 582120(司机行系统管理,拒绝人工写入):

{
  "code": 200,
  "message": "司机由车务派车自动带入团期人员,不能在这里新增或删除;如需调整请到车务派单中维护",
  "success": false,
  "data": null
}

错误码 582116(导游位只传半个配置位):

{
  "code": 200,
  "message": "团期人员配置位 GUIDE 成员缺失,导游位(导游+领队)须同时声明两个角色",
  "success": false,
  "data": null
}

错误码 582115(staffList 包含 scopeRoles 范围外的角色):

{
  "code": 200,
  "message": "员工角色 PHOTOGRAPHER 不在覆盖范围 [GUIDE,LEADER] 内,请修正请求",
  "success": false,
  "data": null
}

错误码 582119(服务日期校验失败):

{
  "code": 200,
  "message": "刘领队 的服务日期(2026-10-06 至 2026-10-05)不合法:开始日期不能晚于结束日期,且须在团期日期(2026-10-01 至 2026-10-10)之内",
  "success": false,
  "data": null
}

业务边界

  • 权限: 接口接 GroupBatchPermissionGuard.PERMISSION_MANAGE,需要团期管理权限;权限校验在 Controller 层,拒绝时零写入。
  • 幂等性: 同一请求重复提交视为覆盖保存,第二次提交时无新改动则库表无变化、响应同样 200。
  • 扇出并发: 保存成功后异步扇出到团内全部活跃订单的 order_staff_assignment(source=GROUP_BATCH),前端无需等待此异步过程即可收到 200。
  • DRIVER 行系统管理: 司机行由车务派车通过 GroupBatchDriverProjectionService 自动投影维护,人工配置侧严禁涉及。staffList 或 scopeRoles 里出现 DRIVER 一律拒绝(582120),整批保存零写入,连软删都不执行。
  • 两个读口口径不同:
    • 本接口(PUT)保存成功后返回的 staffList 不含司机行(只含人工配置的非 DRIVER 角色)。
    • 查询接口 GET /v3/admin/group-batch/{productBatchId}/staff 与单人查询 GET .../staff/{staffId} 含司机行。
    • 前端不要假设响应即全量,名册读取时必须从查询接口获取完整名单(含司机)。
  • 团期状态检查: 入口第一步检查团期成团状态(未建团 589553 / 已确认或已取消 589598);不满足直接拒,代码不走到保存逻辑。
  • 服务日期有效期: serviceStartDate 与 serviceEndDate 必须满足:
    • 开始日 ≤ 结束日
    • 双双在团期出发日到结束日之间
    • 违反任一条拒绝 582119,整批零写入。

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

✅ 正确 / ❌ 错误 payload 对照

场景 payload 结果
✅ 导游位全量覆盖 { "scopeRoles": ["GUIDE","LEADER"], "staffList": [{staffId:40001, staffRole:"LEADER"}, {staffId:40002, staffRole:"GUIDE"}] } 200,只有这两人留在 GUIDE 和 LEADER 行,其他既有行不动
✅ 清空整期 { "staffList": [] } (不传 scopeRoles) 200,整期人员全清,库表此后仅含司机行
✅ 导演位自定义服务日期 { "staffList": [{staffId:40001, staffRole:"LEADER", serviceStartDate:"2026-10-01", serviceEndDate:"2026-10-05"}] } 200
❌ 导游位只传半个配置位 { "scopeRoles": ["GUIDE"], "staffList": [...] } 拒绝 582116、零写入(缺少 LEADER 配角)
❌ staffList 包含 DRIVER { "staffList": [{staffId:40001, staffRole:"DRIVER"}] } 拒绝 582120、零写入(司机由车务派车投影)
❌ scopeRoles 包含 DRIVER { "scopeRoles": ["DRIVER"], "staffList": [] } 拒绝 582120、零写入
❌ 服务开始日晚于结束日 { "staffList": [{staffId:40001, staffRole:"GUIDE", serviceStartDate:"2026-10-05", serviceEndDate:"2026-10-01"}] } 拒绝 582119、零写入
❌ 服务日期越出团期 { "staffList": [{staffId:40001, staffRole:"GUIDE", serviceStartDate:"2026-09-30"}] } (团期出发日 2026-10-01) 拒绝 582119、零写入

scopeRoles 的语义

  • 不传: 整期全量覆盖。staffList 即为团期的最终全量人员配置(除去司机),其他所有角色的既有行会被软删。
  • 传了: 仅覆盖声明的角色。比如只想更新导演位不动摄影位,传 ["GUIDE","LEADER"] 即可;摄影位的既有行保持不变。
  • 导游位特殊性: 因为导游位同时收 GUIDE 与 LEADER 两个角色,声明导游位必须两个都传。只传其中一个会被拒(582116)——因为「少那一个」意味着覆盖范围不完整,不能正确表达"我只想改导游位"的意图。
  • DRIVER 禁令: scopeRoles 里出现 DRIVER 拒绝 582120。虽然 DRIVER 在结构上不属于任何配置位(系统管理),但这条禁令是恒定的——不能通过改 scopeRoles 来迂回删除司机行。

五、数据库行为

保存请求成功后的库表变化:

操作 对象 行为
软删 order_batch_staff 按 scopeRoles(若不传则整期)清除人工行,不删司机行(502120 保护)
插入 order_batch_staff 按 staffList 新增非 DRIVER 行
异步扇出 order_staff_assignment(source=GROUP_BATCH) 更新团内活跃订单的 GROUP_BATCH 副本,内容为最新的整期非司机成员

核心不变量: 人工配置(GroupBatchStaffConfigService)只碰非 DRIVER 行;司机投影(GroupBatchDriverProjectionService)只碰 DRIVER 行。两边各管各的子集,交集为空。


六、边界行为

  • 未登录 → 401(网关拦截)
  • 无团期管理权限 → 403(权限校验)
  • 团期不存在 → 589553(未建团)或 589598(已确认/已取消)
  • 下游服务(人员服务、图片服务)降级 → 快照字段(姓名、手机、头像)回落配置时快照,接口仍 200;不阻断保存流程
  • 并发冲突 → 保存成功(最后提交的版本胜出,不走 CAS),查询时可能看到中间态
  • 司机行来自 → 由 fleet-service 派车回调、经 GroupBatchDriverProjectionService 投影维护,非人工配置

六.5、枚举

staffRole(员工角色)

所属字段: staffList[].staffRole (请求) / data.staffList[].staffRole (响应) | 类型: String

值 中文 说明
LEADER 领队 导游位成员之一
GUIDE 导游 导游位成员之一(与 LEADER 并收)
PHOTOGRAPHER 摄影 摄影位成员
GUIDE_ASSISTANT 导游助理 其他配置位成员
STUDY_TEACHER 研学老师 其他配置位成员
LIFE_TEACHER 生活老师 其他配置位成员
OTHER 其他 杂项角色(不属于任何标准配置位)
DRIVER 司机 本接口禁止人工写入(582120);由车务派车自动投影;查询时可见

reporterRank(报账人等级)

所属字段: data.staffList[].reporterRank (响应) | 类型: String

值 中文 说明
PRIMARY 主报账人 团期内唯一;预支款打给此人
SECONDARY 次报账人 团期内唯一;备选收款人
NONE 非报账人 缺省值;不参与结算

occupancyStatus(占用状态)

所属字段: data.staffList[].occupancyStatus (响应) | 类型: String

值 中文 说明
FREE 空闲 有效服务日期段内无其他团期或订单占用
PARTIAL 部分占用 有效日期段内某些天被占,某些天可派
FULL 全程占用 有效日期段全部被占,无可派日期
null - 团期未建成或服务日期不完整时为 null;仅提示,不拦截提交

六.6、修改前后对比

接口逻辑变化

维度 改前 改后
司机行管理 团期人员配置由运营全手工维护,无系统投影 司机行由车务派车自动投影,人工配置侧严禁涉及
保存响应 返回整期人员全量(包含一切手工配置) 返回非司机成员快照(DRIVER 行被筛除)
查询响应 同保存响应 含司机行(与保存响应口径不同)
错误码新增 无 DRIVER 相关拒绝 新增 582120:司机行拒绝码,整批零写入
权限校验 无 新增 Controller 层权限校验(PERMISSION_MANAGE),拒绝时零写入

字段级变化

字段 改前状态 改后状态
serviceStartDate / serviceEndDate 无此字段 新增可选字段;支持按人自定义服务有效期
serviceDateCustom 无 新增,标记是否自定义过服务日期
baseDailyWage 无 新增,人员档案快照(配置时带出,不可修改)
occupancyStatus / occupiedDays / freeRanges 无 新增,占用查询结果(#8468 D8,仅提示不拦截)

六.7、影响评估

  • 是否破坏向后兼容: 是(一定程度)

    • 响应体新增字段:serviceStartDate / serviceEndDate / serviceDateCustom / baseDailyWage / occupancyStatus / occupiedDays / freeRanges / occupancies,但字段全可空,字段层兼容。
    • 响应 staffList 内容变化:不再包含司机行(之前如果有系统产生的司机行会出现,现在被筛除)。前端若依赖"返回即全量"会破损。
    • 新增错误码 582120:拒绝 DRIVER 行,整批零写入——改动前的正常请求可能现在被拒。
  • 前端是否必须同步上线: 是

    • 如果前端在"人员名册""核单名单"等读取页面会显示司机信息,必须从查询接口(GET)而非缓存保存接口的响应来获取;否则缺司机行。
    • 如果前端在人员编辑表单提供了 DRIVER 行的编辑/删除入口,必须移除,改为呈现"这是由车务派车产生的"提示。
    • UI 需要适配新字段(服务日期、日薪、占用)的显示。
  • 前端 workaround 清理点:

    • 若前端之前硬编码了"司机只能通过XX页面配置",现在这句不再对。
    • 若前端假设"保存响应即全量名册"来渲染名册卡片,需改为调查询接口。
    • 若前端在人员编辑表单里有 DRIVER 选项,需删除;前端用户无法(也不应该)在这里新增/删除司机。

七、不影响范围

  • 仅影响: 管理后台「团期人员配置」页面与「团期核单」按团看人的名册展示
  • 零影响:
    • 订单侧人员分配(仍独立维护 order_staff_assignment(source=ORDER))
    • 车务派车流程(按 fleet 业务正常发车、自动投影)
    • 人员候选列表查询(GET .../staff/candidates)的返回格式
    • 团期总体状态、成团判断、团期改期逻辑
    • 后端其他模块对人员表的现有查询(如财务核对)

八、测试环境已验证

以 productBatchId=80001(groupBatchId=90211)为例,真实接口测试:

PUT /v3/admin/group-batch/80001/staff
  Request: scopeRoles=["GUIDE","LEADER"], staffList=[{staffId:40001, staffRole:"LEADER"}]
  → 200 OK ✓

GET /v3/admin/group-batch/80001/staff
  → 200 OK,staffList 含导游位成员及系统投影司机行 ✓

PUT /v3/admin/group-batch/80001/staff
  Request: staffList=[{staffId:40001, staffRole:"DRIVER"}]
  → 拒绝 582120(司机由车务派车自动带入...)✓

PUT /v3/admin/group-batch/80001/staff
  Request: scopeRoles=["GUIDE"], staffList=[...](缺 LEADER)
  → 拒绝 582116(导游位成员缺失...)✓

九、相关历史 PR

PR Issue 说明 是否仍有效
#8669 #8653, #8654 本次变更:司机投影落地,DRIVER 改系统管理 ✅ 最新

十、相关文档


关联 / 联系人

链接

联系人

  • 后端负责人: @wx