文件
hl-api-changelog/changelogs-v2/2026-09/20_7987_团期时间线新增开窗与重配两类事件-修改接口-管理后台.md
T

24 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 7987 团期时间线新增开窗与重配两类事件,事件类型枚举扩展 admin wx(GIT) 修改接口 deployed verified not_required mmg hl-ui gateway_status=verified 的依据:2026-09-20 18:22-18:46 经网关 https://api.test.1814.love:9443 实测(账号 cw_test_7444)。GET /v3/admin/order/group-batch/{id}/status-logs 在两个批次上分别取到 BATCH_VEHICLE_REQUIREMENT_REOPENED(开窗,extra 含 windowMinutes/blockedStage/expiresAt/scopeGroupCodes/scopeDates 五项齐全)与 BATCH_VEHICLE_DISPATCH_RECONFIGURED(重配),eventType/eventTypeName/changeType=DATA/extra 各字段逐一对照一致。⚠️ 本次实测发现并已订正一处契约偏差:重配事件的 operatorId 原写字符串 SYSTEM,实测为 null(与库里其它 SYSTEM 类事件惯例一致),判为文档写错而非实现错,已在字段表/字段值/JSON 示例/注意事项共 5 处改完。前端若按原文档判空会得到相反预期,故这条订正与发布同一批。 前端实证翻 not_required(mmg 2026-09-20):前端无自建 eventType 映射表,StatusLogsPanel 渲染 `eventTypeName || eventType || '事件'`(后端直供中文名+原码兜底),operatorType=SYSTEM 时 operatorId=null/operatorName「系统」是该面板注释钉住的既有口径,两类新事件与 operatorId 订正均自动正常显示,零改动。 【上一轮原注】backend_status=deployed 的判据(2026-09-20 18:35 复核):hl-order-service-v3 测试服部署点 e179e09bd(2026-09-20 17:55:02 发布),`git merge-base --is-ancestor ab92377c7 e179e09bd` = true,故本篇端点的代码确已在测试服运行的字节里。⚠️ 这是一次**时点读数**:测试服由多会话共用,随时可能被滚到别的提交;origin/dev-v3 在本次复核时已前进到 f56692a51,落后的是部署点不是本篇。 gateway_status 保持 pending——本会话未对本篇端点做任何真实网关调用,正文示例值的来源已在各小节逐处标注(单元测试字面量 / @ApiModelProperty example 声明),不是抓包。待网关复验后再置 verified,**不得为了让门禁变绿改这个位**。 2026-09-20 dev-v3

order-v3: 团期时间线新增开窗与重配两类事件

存放目录: 二期(v3) → changelogs-v2/{YYYY-MM}/

服务: hl-order-v3 (端口 8021) PR: #8039 Issue: #7987 日期: 2026-09-20 影响范围: 管理后台 - 团期详情页 - 状态流水/时间线


⚠️ 关键变化

后端在团期时间线(group_batch_status_log 表)新增两种事件记录:

  • BATCH_VEHICLE_REQUIREMENT_REOPENED(开窗事件)—— 管理员显式开启受控重配窗口时写入
  • BATCH_VEHICLE_DISPATCH_RECONFIGURED(重配事件)—— fleet 回调确认重配生效时写入

前端必须新增这两个 event_type 值的渲染分支,否则这两类事件在管理后台团期时间线上将无法正常显示其标签名(见下文「③ 前端渲染缺口」)。


一、背景

#7987 实施"受控重配窗口"机制:管理员可在团期进入待出发后仍显式开一个时间限制的窗口,允许车务在此期间重新排车。此前这两件事的事实只落在需求行的三个字段(reconfigure_reason / reconfigure_opened_by / reconfigure_opened_at),但运营打开团期详情页时看不到这些列,无法追溯窗口何时开过。

本次变更补上团期时间线的留痕,让管理员在团期详情页就能看到「谁在何时以什么原因开了重配窗口」与「窗口内换车是否生效」。


二、变更接口清单

既存接口(#7304 落地)返回值扩展:

# 接口 方法 路径 变更类型 说明
1 团期状态流水查询 GET /v3/admin/order/group-batch/{id}/status-logs 响应新增 event_type 值 新增两种事件类型

三、接口详情

1. 团期状态流水查询 GET /v3/admin/order/group-batch/{id}/status-logs

VO: GroupBatchStatusLogItemVO (既存响应 VO,新增 eventTypeName 映射值)

使用场景

管理后台团期详情页的状态流水/时间线面板,用于向运营展示该团期的历史状态变化与重要操作记录。本次变更使该时间线新增两类事件的可见性。

入参

字段 位置 类型 必填 约束 说明
id Path Long ✅ - 团期主订单 ID

出参 Result<List<GroupBatchStatusLogItemVO>>

字段 类型 说明
logId String 流水记录 ID
groupBatchId Long 团期主订单 ID
changeType String 变更类型:STATUS(状态流转) / DATA(数据变更)
eventType String 事件类型代码(如 BATCH_VEHICLE_REQUIREMENT_REOPENED)
eventTypeName String 事件类型中文名(新增两种:团车受控重开·已开配车窗口 / 团车受控重配·车辆安排已变更)
fromStatus String 状态前值代码(如 PENDING_DEPARTURE)
fromStatusName String 状态前值中文名
toStatus String 状态后值代码
toStatusName String 状态后值中文名
content String 事件文本内容(前端直接渲染)
reason String 事件原因/备注(可为 null)
operatorType String 操作人类型:ADMIN / SYSTEM / MQ
operatorId String 操作人 ID。⚠️ 2026-09-20 网关实测订正:operatorType=SYSTEM 时本字段是 null,不是字符串 "SYSTEM"(与库里其它 SYSTEM 类事件如 BATCH_VEHICLE_REQUIREMENT_DONE 的惯例一致)。前端判空请按 null 处理
operatorName String 操作人名称
extra Map<String, Object> 结构化附加数据,字段随事件类型变化(见下文"新增事件详解")
changedAt String 事件发生时刻(ISO 8601)

新增事件类型详解

BATCH_VEHICLE_REQUIREMENT_REOPENED 事件

  • eventType: "BATCH_VEHICLE_REQUIREMENT_REOPENED"
  • eventTypeName: "团车受控重开·已开配车窗口"
  • changeType: "DATA" (状态内数据变更,fromStatus = toStatus)
  • operatorType: "ADMIN"
  • operatorId: 开窗管理员 ID
  • operatorName: 开窗管理员真名
  • content: "开启受控重配窗口:允许车务在 X 分钟内重新安排本团车辆(授权 N 个乘车分组 / M 个行程日,2026-09-20 10:30:00 前有效)。此前团期已进入「待出发」,正常不可再配车。"
  • reason: 管理员必填的开窗原因(如"团期特殊变更")
  • extra: 结构化数据
    {
      "requirementId": "需求行主键(字符串)",
      "requirementVersion": 需求行版本号(整数),
      "blockedStage": "PENDING_DEPARTURE",
      "windowMinutes": 120,
      "expiresAt": "2026-09-20T12:30:00",
      "scopeGroupCodes": ["乘车分组编号数组"],
      "scopeDates": ["2026-09-21", "2026-09-22"]
    }
    

BATCH_VEHICLE_DISPATCH_RECONFIGURED 事件

  • eventType: "BATCH_VEHICLE_DISPATCH_RECONFIGURED"
  • eventTypeName: "团车受控重配·车辆安排已变更"
  • changeType: "DATA" (状态内数据变更,fromStatus = toStatus)
  • operatorType: "SYSTEM"
  • operatorId: null(⚠️ 2026-09-20 网关实测订正:原写 "SYSTEM",实测为 null)
  • operatorName: "系统"
  • content: "本团车辆安排已被重新排定:车务在受控重配窗口内完成换车,新的配车计划(版本 20260920_v1)已生效,原计划作废。开窗授权人:张三;具体经办人见车务配车记录。"
  • reason: 开窗时管理员填写的原因(沿用窗口的 reason,非重配操作人的备注)
  • extra: 结构化数据
    {
      "requirementId": "需求行主键(字符串)",
      "requirementVersion": 需求行版本号(整数),
      "planVersion": "fleet 配车计划版本号(字符串)",
      "reconfigureOpenedBy": "开窗管理员 ID(与前一条事件的 operatorId 对应)",
      "reconfigureOpenedAt": "2026-09-20T10:15:30"
    }
    

请求示例

GET /v3/admin/order/group-batch/8000001/status-logs

响应示例

{
  "code": 200,
  "message": "成功",
  "data": [
    {
      "logId": "12345",
      "groupBatchId": "8000001",
      "changeType": "DATA",
      "eventType": "BATCH_VEHICLE_REQUIREMENT_REOPENED",
      "eventTypeName": "团车受控重开·已开配车窗口",
      "fromStatus": "PENDING_DEPARTURE",
      "fromStatusName": "待出发",
      "toStatus": "PENDING_DEPARTURE",
      "toStatusName": "待出发",
      "content": "开启受控重配窗口:允许车务在 120 分钟内重新安排本团车辆(授权 2 个乘车分组 / 3 个行程日,2026-09-20T12:30:00 前有效)。此前团期已进入「待出发」,正常不可再配车。",
      "reason": "临时增加3人,需调整座位分组与用车方案",
      "operatorType": "ADMIN",
      "operatorId": "1001",
      "operatorName": "李四",
      "extra": {
        "requirementId": "7000001",
        "requirementVersion": 2,
        "blockedStage": "PENDING_DEPARTURE",
        "windowMinutes": 120,
        "expiresAt": "2026-09-20T12:30:00",
        "scopeGroupCodes": ["GRP001", "GRP002"],
        "scopeDates": ["2026-09-21", "2026-09-22", "2026-09-23"]
      },
      "changedAt": "2026-09-20T10:30:00"
    },
    {
      "logId": "12346",
      "groupBatchId": "8000001",
      "changeType": "DATA",
      "eventType": "BATCH_VEHICLE_DISPATCH_RECONFIGURED",
      "eventTypeName": "团车受控重配·车辆安排已变更",
      "fromStatus": "PENDING_DEPARTURE",
      "fromStatusName": "待出发",
      "toStatus": "PENDING_DEPARTURE",
      "toStatusName": "待出发",
      "content": "本团车辆安排已被重新排定:车务在受控重配窗口内完成换车,新的配车计划(版本 20260920_v1)已生效,原计划作废。开窗授权人:李四;具体经办人见车务配车记录。",
      "reason": "临时增加3人,需调整座位分组与用车方案",
      "operatorType": "SYSTEM",
      "operatorId": null,
      "operatorName": "系统",
      "extra": {
        "requirementId": "7000001",
        "requirementVersion": 2,
        "planVersion": "20260920_v1",
        "reconfigureOpenedBy": "1001",
        "reconfigureOpenedAt": "2026-09-20T10:30:00"
      },
      "changedAt": "2026-09-20T11:45:00"
    }
  ],
  "success": true
}

空数据 / 降级响应

{
  "code": 200,
  "data": [],
  "success": true
}

错误响应

{
  "code": 589500,
  "message": "团期不存在",
  "success": false,
  "data": null
}

业务边界

  • 接口返回的时间线按 changedAt 升序排列(从最早到最新)
  • 两个新事件都属于 DATA 类(状态内数据变更),不涉及团期主状态 batch_status 变化
  • 库里若有历史脏值或未知 event_type,后端 eventTypeLabel 返回 null,前端需兜底显示原始 code 值

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

本节说明后端接受请求、返回响应的规则。

✅ 正确 / ❌ 错误请求对照

场景 请求示例 结果
✅ 查询既存团期的时间线 GET /v3/admin/order/group-batch/8000001/status-logs 200 + 时间线列表
✅ 无流水时返回空列表 GET /v3/admin/order/group-batch/9999999/status-logs(不存在团期) 200 + data: []
❌ 团期 ID 非数字 GET /v3/admin/order/group-batch/abc/status-logs 400

字段约束与兼容性规则

  • eventType 兼容性:库里可能存在本版本未定义的历史 event_type 值 → 后端 eventTypeLabel 返回 null → 前端应兜底显示原始 code 值,而不是报错
  • extra 字段:根据 eventType 不同而变化(见下文详解);前端需根据 eventType 判断应读取哪些 extra 字段,勿假设所有事件的 extra 结构相同
  • operatorType 非 ADMIN 时:⚠️ 2026-09-20 网关实测订正:operatorId 是 null、operatorName 是 "系统"——两个字段形态不同,不要当成同一回事(原文把它们合并描述成「可能不是真实人名」,会让前端以为 operatorId 也有值) → 前端应按类型判断如何渲染,勿硬编码为"管理员"
  • reason 字段:可为 null(仅新增事件强制填写,既有历史事件可能无此字段) → 前端应做 null check

六.5、枚举:新增事件类型

eventType(GroupBatchLogEventType)

所属字段: GroupBatchStatusLogItemVO.eventType | 类型: String | 枚举类: com.hulalv.order.groupbatch.enums.GroupBatchLogEventType

新增两个枚举值(共有 N+2 个值):

值 中文名 说明 changeType 何时写入
BATCH_VEHICLE_REQUIREMENT_REOPENED 团车受控重开·已开配车窗口 管理员显式开启受控重配窗口,允许车务在时间限制内重新排车 DATA 调用 POST /v3/admin/order/group-batch/{id}/vehicle-requirement/reopen 时
BATCH_VEHICLE_DISPATCH_RECONFIGURED 团车受控重配·车辆安排已变更 fleet 就绪回调确认重配生效,新车辆计划已应用,原计划作废 DATA fleet Outbox 回调解除受控重开阻断时

operatorType(操作人类型)

所属字段: GroupBatchStatusLogItemVO.operatorType | 类型: String

既有三种值,新增事件涉及的分别是:

值 中文名 说明 示例事件
ADMIN 管理员 真实的管理后台登录用户 BATCH_VEHICLE_REQUIREMENT_REOPENED
SYSTEM 系统 无 HTTP 请求上下文的自动操作(MQ / 定时任务 / 回调) BATCH_VEHICLE_DISPATCH_RECONFIGURED
MQ MQ 事件 MQ 消费触发(如支付/退款回调) 既有其他事件

四、前端需求与缺口说明

① eventTypeLabel 映射(前端渲染分支)

后端实现(GroupBatchStatusLogService.java:237-248):

private String eventTypeLabel(String value) {
    if (value == null) {
        return null;
    }
    for (GroupBatchLogEventType type : GroupBatchLogEventType.values()) {
        if (type.getValue().equals(value)) {
            return type.getLabel();
        }
    }
    return null;  // 未知值返回 null
}

当前状况:

  • 后端遍历枚举查找 event_type,找到则返回 label,否则返回 null
  • 前端收到的 eventTypeName 字段值为 null 时,该行事件在时间线上显示为空白(既不报错、也不显示原始值,这是现状)

前端必须新增的映射:

// 示例伪码
const eventTypeLabels = {
  "BATCH_VEHICLE_REQUIREMENT_REOPENED": "团车受控重开·已开配车窗口",
  "BATCH_VEHICLE_DISPATCH_RECONFIGURED": "团车受控重配·车辆安排已变更",
  // ... 既有其他 event_type ...
};

不加分支的后果:这两类事件在管理后台团期时间线上无法显示其事件名称,运营只看得到时刻/操作人/备注,但看不出"这是什么事件",影响运营追溯。

② 重配事件的操作人恒为 SYSTEM(不能拿 extra 字段顶替)

源码事实(GroupBatchService.java:1089-1118,recordReconfigureTrail 方法):

// 就绪回调中,无 HTTP 请求上下文,operatorResolver.resolve() 返回 SYSTEM
Operator operator = operatorResolver.resolve();  // → Operator.system()
groupBatchStatusLogService.recordDataChange(groupBatchId,
        GroupBatchLogEventType.BATCH_VEHICLE_DISPATCH_RECONFIGURED,
        batch == null ? null : GroupBatchStatus.fromCode(batch.getBatchStatus()),
        content, window.reason(), extra);

重配事件的字段值:

  • operatorType: "SYSTEM"
  • operatorId: null(⚠️ 2026-09-20 网关实测订正:原写 "SYSTEM",实测为 null)
  • operatorName: "系统"
  • extra.reconfigureOpenedBy: 开窗管理员 ID(不是重配操作人)
  • extra.reconfigureOpenedAt: 开窗时刻(不是重配时刻,重配时刻在 changedAt)

这不是 bug,是设计:

  • 重配的真实经办人(谁在车务后台点的"重配"按钮)只存在于 fleet 侧的 fleet_group_dispatch.updated_by
  • order-v3 是接收者,fleet 回调进来时没有经办人载荷
  • extra.reconfigureOpenedBy 答的是"谁授权开的窗"(过去的管理动作),与"谁换的车"(当前的车务动作)是两个不同的人、不同的时刻

前端若把 extra.reconfigureOpenedBy 显示成"经办人",就是把一个错误的责任人写到审计页面上。

  • ✅ 正确做法:operatorName 显示"系统",content 或 extra.reconfigureOpenedBy 里可补充说明"开窗人是谁"
  • ❌ 错误做法:拿 extra.reconfigureOpenedBy 冒充 operatorId/operatorName 展示成经办人

③ 开窗事件的操作人是真实人(与重配事件不同)

源码事实(GroupVehicleRequirementService.java:2305-2347,recordReopenTrail 方法):

public GroupVehicleReopenRespVO doReopen(Long groupBatchId, 
                                         GroupVehicleRequirementReopenReqVO req,
                                         String operatorId) {
    // ... 业务逻辑 ...
    recordReopenTrail(groupBatchId, batch, active, req, windowMinutes, expiresAt);
}

private void recordReopenTrail(...) {
    // recordDataChange 内部调 operatorResolver.resolve(),因为这是 HTTP 请求上下文
    // 会从 AdminAuthInterceptor 注入的 AdminContext 里取当前登录管理员
    groupBatchStatusLogService.recordDataChange(groupBatchId,
            GroupBatchLogEventType.BATCH_VEHICLE_REQUIREMENT_REOPENED,
            GroupBatchStatus.fromCode(batch.getBatchStatus()), content, req.getReason(), extra);
}

开窗事件的操作人:

  • operatorType: "ADMIN"(不是 SYSTEM)
  • operatorId: 调用 reopen 端点的管理员 ID(真实人)
  • operatorName: 管理员真名

两个事件的操作人不同的原因:

  • 开窗:用户通过管理后台 HTTP 端点主动触发 → 上下文有 AdminContext → operatorResolver 返回真实人
  • 重配:fleet 异步回调 → 无 HTTP 请求上下文 → operatorResolver 返回 SYSTEM

前端不能混淆:别以为所有时间线事件的 operatorName 都是真实人,重配这条特殊。


五、数据库行为

新增行落在 group_batch_status_log 表:

事件 写入时机 from_status to_status change_type 示例
BATCH_VEHICLE_REQUIREMENT_REOPENED 调用 POST /v3/admin/order/group-batch/{id}/vehicle-requirement/reopen 时 PENDING_DEPARTURE PENDING_DEPARTURE DATA 管理员张三在 2026-09-20 10:30 开窗
BATCH_VEHICLE_DISPATCH_RECONFIGURED fleet 就绪回调解除受控重开阻断时 PENDING_DEPARTURE PENDING_DEPARTURE DATA fleet 确认换车后在 2026-09-20 11:45 生效

关键:两个事件都不改变团期 batch_status,from_status 与 to_status 相同。


六、边界行为

  • 库里存在历史脏值 → 后端 eventTypeLabel 返回 null → 前端时间线该行显示空白或兜底处理
  • 并发写入 → 开窗事务与重配事务独立,都跟随各自的外层事务(不丢失)
  • 幂等性 → 开窗已有生效窗口时幂等返回既有令牌(不重复写库);普通首配回调、常规刷新回调若无受控重开阻断时 → CAS 0 行返回 → 不落本事件(常态)

六.6、修改前后对比

接口行为对比

行为 改前 改后
时间线中开窗操作的可见性 三个字段存库但页面不展示 新增一条 DATA 类事件,时间线上可见
时间线中重配操作的可见性 无记录 新增一条 DATA 类事件,标明"车已换、谁开的窗、老计划作废"
事件类型枚举 共 N 种 新增 2 种,共 N+2 种
前端事件渲染 既有 N 种 event_type 的映射 需新增 BATCH_VEHICLE_REQUIREMENT_REOPENED / BATCH_VEHICLE_DISPATCH_RECONFIGURED 两条映射

六.7、影响评估

  • 是否破坏向后兼容: 否。既存接口返回值扩展,老 event_type 不变
  • 前端是否必须同步上线: 是。不新增渲染映射会导致新增两类事件在时间线上显示为空白
  • 前端 workaround 清理点:
    • 若前端已有"event_type 未知时显示原始 code"的兜底逻辑,可临时显示 BATCH_VEHICLE_REQUIREMENT_REOPENED 等原始值
    • 但正式上线必须使用中文标签,不能长期显示原始值(运营不认识)

七、不影响范围

  • 仅影响: 管理后台 - 团期详情页 - 状态流水/时间线面板(新增两类事件的渲染)
  • 零影响:
    • 团期主状态推进(batch_status 机制不变)
    • 用车需求状态机(需求行读写逻辑不变)
    • 订单详情、C 端行程页面(无此数据源)
    • 旧团期数据(存量团期的时间线不会补充新事件)

八、测试环境已验证

后端逻辑验证(提交 ab92377c7):

POST /v3/admin/order/group-batch/8000001/vehicle-requirement/reopen
  请求体: { "scopeGroupCodes": ["GRP001"], "scopeDates": ["2026-09-21"], "reason": "test reason" }
  → 200, group_batch_status_log 新增 1 行 BATCH_VEHICLE_REQUIREMENT_REOPENED ✓

fleet 就绪回调(模拟)
  request: {"groupBatchId": 8000001, "requirementId": 7000001, "requirementVersion": 2, "planVersion": "v1"}
  → 200, group_batch_status_log 新增 1 行 BATCH_VEHICLE_DISPATCH_RECONFIGURED ✓

GET /v3/admin/order/group-batch/8000001/status-logs
  → 200, 返回值中 eventTypeName 正确映射(后端已验证) ✓

前端渲染验证:**待前端补充映射后联调**

九、相关历史 PR

PR Issue 说明 是否仍有效
#7986 #7986 受控重配窗口核心机制(团车需求侧) ✅ 有效
#8039 #7987 本 PR - 补团期时间线留痕 ✅ 最新

十、相关文档


关联 / 联系人

链接

联系人

  • 后端负责人: @wx
  • 前端对接人: @mmg(hl-ui 仓库,需新增 eventTypeLabel 映射)