文件
hl-api-changelog/changelogs-v2/2026-09/22_8155_需求驳回站内信bizId改为orderId点跳转落空-修复-管理后台.md
T
API Changelog Bot 4e3d3ba2d5
changelog-filename-gate / validate (push) Failing after 2s
docs(changelog): #8155 需求驳回站内信 bizId 由需求行 id 改为 orderId(已部署实测)
REQUIREMENT_REJECTED 事件此前 bizType 固定 ORDER 却把需求行 id 当 bizId 下发,
管理端站内信点「跳转」必然打开一个不存在的订单。后端已改为下发 orderId,
需求行 id 迁到 params(notification_send_log.params_json 实测可反查),
并把 inapp_link_template 前缀从 /order/detail/ 订正为 /order-v2/detail/。

AdminMessageRespVO / AdminMessagePageReqVO 字段结构未变,前端无需改动。
已部署测试服 6af4d93e5 双实例并端到端实测:新产生的 admin_message 行
biz_id 等于 orderId、link 为 /order-v2/detail/2102242483750756354。

覆盖边界:admin_message 是快照表,存量行 biz_id 与 link 仍是旧值、不回刷
(该事件仅在测试环境产生过,order-v3/fleet 未上生产)。

Refs #8155
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 12:00:14 +08:00

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 8155 通知中心: 需求驳回站内信 bizId 改为 orderId,修复管理端「跳转」打开不存在订单 admin wx(GIT) 修复 deployed not_required not_required 本条不改变 AdminMessageRespVO/AdminMessagePageReqVO 的字段结构,只修正 REQUIREMENT_REJECTED 事件写入 admin_message 的 bizId 取值、以及 notification_event_config.inapp_link_template 的路由前缀。gateway_status=not_required:本条不引入任何新路由,受影响的 GET /admin/message/list 与 GET /admin/message/{id} 是既有端点。frontend_status=not_required:前端 jumpToBiz(hl-ui/src/views/notification/MyMessages/index.vue:268-286)只读 row.bizId/row.bizModule/row.bizType/row.peerRole,从不读 row.link,字段名/类型/位置均未变,无需任何前端代码改动。backend_status=deployed:order-v3 与 user-service 均已部署测试服务器至 6af4d93e5(双实例滚动完成),并已在自建夹具上走完「提交用车需求 → 车控驳回 → 站内信产出」全链路,实测读数见正文第八节。⚠️ admin_message 是快照表:bizId 与渲染后的 link 在 createMessage 落库那一刻一次性写入行内(NotificationDispatcher.java:1038 渲染 link、:1042 起调用 AdminMessageService.createMessage、AdminMessageService.java:121-123 写入实体、:541 toVO 透传),不是按需渲染;因此本修复只影响部署生效后新产生的消息,此前已产出的历史站内信 bizId 与 link 都仍是旧值,不做批量回刷——REQUIREMENT_REJECTED 目前只在测试环境触发过(order-v3/fleet 尚未上生产),不涉及生产数据。 2026-09-22 dev-v3

通知中心: 需求驳回站内信 bizId 改为 orderId,修复管理端「跳转」打开不存在订单

存放目录: 二期(order-v3)→ changelogs-v2/2026-09/

服务: hl-order-service-v3(通知发布方)+ hl-user-service(通知分发/站内信收件箱) PR: #8160 Issue: #8155 日期: 2026-09-22 影响范围: 管理后台站内信中心「需求驳回」事件的 bizId 取值与对应跳转链接前缀


⚠️ 关键变化

需求驳回站内信的 bizId 从需求行 ID 改为订单 ID,前端「跳转」按钮从此能正确打开订单详情。

  • 改前:站内信声明 bizType="ORDER",但 bizId 实际写入的是被驳回的需求行 ID(vehicle_requirement/hotel_requirement 主键)。前端按 bizType=ORDER 的契约把 bizId 当 orderId 去查订单,订单查不到,「跳转」必然落空。
  • 改后:bizId 恒等于 order.getOrderId();被驳回的需求行 ID 改经 params 下发,排查能力不丢(落 notification_send_log.params_json)。
  • 前端不需要改任何代码:AdminMessageRespVO 的字段名、类型、位置都没变,变的只是 bizId 这一个既有字段过去写错了、现在写对了。
  • 同批一并修正了该事件 inapp_link_template 的路由前缀(/order/detail/${orderId} → /order-v2/detail/${orderId})。该字段前端当前完全不读,此改动不影响现有跳转行为,只是让 link 本身不再是一个过期契约值。

一、背景

缺陷形态

NotificationEventHelper.sendRequirementRejected(order, requirementId, requirementType, reason)(hl-order-service-v3/src/main/java/com/hulalv/shared/notification/NotificationEventHelper.java)改前的调用链:

sendRequirementRejected(...) → sendInternal("REQUIREMENT_REJECTED", order, params, requirementId)
                              → buildAndSend(eventCode, order, params, bizId=requirementId, userId=null)
                              → NotificationEventMessage.bizId = String.valueOf(requirementId)
                                                        .bizType = "ORDER"

而本类同一文件里其余三个事件(sendContractFailed/sendInsuranceFailed/sendPaymentReminder)走的是无 bizId 参数的 send(eventCode, order, params) 重载,该重载内部固定用 order.getOrderId() 作为 bizId——这三个事件从未受本缺陷影响,只有 sendRequirementRejected 在调用点显式传了 requirementId 顶替 bizId。

触发路径

该事件仅在下列 4 个既有写端点判定「打回目标=定制师(CONSULTANT)」时触发,端点本身的请求/响应契约本次未改动:

端点 触发条件
POST /v3/admin/order/{id}/vehicle-requirement/reject(VehicleRequirementAdminController.java:100) 团期管理员打回定制师,恒触发
POST /v3/admin/order/{id}/hotel-requirement/reject(HotelRequirementAdminController.java:96) 团期管理员打回定制师,恒触发
POST /v3/admin/order/{id}/vehicle-requirement/supplier-reject(VehicleRequirementAdminController.java:120) 供应商打回,仅非团期(核心)订单派生目标为 CONSULTANT 时触发;团期订单派生目标为管理员,本期不通知
POST /v3/admin/order/{id}/hotel-requirement/supplier-reject(HotelRequirementAdminController.java:115) 同上

数据落地路径(已核实的完整链路)

NotificationEventHelper.buildAndSend → MQ → NotificationDispatcher.dispatchAdminInApp
  → renderer.render(config.getInappLinkTemplate(), renderParams)      (NotificationDispatcher.java:1038)
  → AdminMessageService.createMessage(adminId, ..., link, bizId, bizType)   (:1042起调用)
  → AdminMessage 实体落库 biz_id / link                                 (AdminMessageService.java:121-123)
  → AdminMessageService.toVO(msg, ...):vo.setLink/setBizId/setBizType  (:541-543)
  → GET /admin/message/list、GET /admin/message/{id} 返回给前端

link 与 bizId 在 createMessage 落库那一刻一次性写入行内,之后读取(无论列表还是详情)都是直接透传已落库的值,不会按当前配置重新渲染——这是下方「六、边界行为」里历史数据不回刷的直接原因。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 站内信分页列表 GET /admin/message/list 返回值语义修正 REQUIREMENT_REJECTED 事件产生的消息行,bizId 由需求行 ID 改为 orderId,link 前缀同步修正;字段结构不变
2 站内信详情 GET /admin/message/{id} 返回值语义修正 同上

三、接口详情

1. 站内信分页列表 GET /admin/message/list

VO: AdminMessagePageReqVO → Result<PageResult<AdminMessageRespVO>>

使用场景

站内信中心列表页拉取当前登录员工的收件箱,系统通知与聊天消息按 createTime 倒序混排返回。REQUIREMENT_REJECTED 属于系统通知(kind=NOTIFY),随全部系统通知混排在本接口结果里,不需要单独的查询参数。

入参

字段 位置 类型 必填 约束 说明
pageNo Query Integer ❌ ≥1,默认 1 页码
pageSize Query Integer ❌ 1~100,默认 20 每页条数
categoryCode Query String ❌ HOUSE/ORDER/SYSTEM 消息分类编码,为空查全部;仅对系统通知有效(聊天行/团队池行该字段为 null)
messageType Query String ❌ ORDER/NORMAL 消息类型,为空查全部;按 kind 派生(ORDER=聊天/NORMAL=系统通知)

本次未改动入参,照源码列出供自包含核对。

出参 Result<PageResult<AdminMessageRespVO>>

PageResult 字段:records(List)、total(int)、page(int)、pageSize(int)。

AdminMessageRespVO 字段(本次仅 bizId/link 两个字段的取值变化,字段结构不变):

字段 类型 说明
messageId String 消息ID(雪花ID,String透传防精度丢失)
categoryCode String 消息分类编码: HOUSE=房务/ORDER=订单/SYSTEM=系统。系统通知为 SYSTEM;聊天消息(kind=CHAT)该字段为 null
title String 消息标题
content String 消息内容
link String 跳转链接。本次修正:REQUIREMENT_REJECTED 新产生的消息此字段前缀由 /order/detail/ 改为 /order-v2/detail/;前端当前不读该字段
bizId String 关联业务ID。本次修正:REQUIREMENT_REJECTED 新产生的消息此字段由需求行 ID 改为订单 ID
bizType String 关联业务类型: ORDER/HOUSE/REFUND
isRead Integer 是否已读(0未读 1已读)
createTime Long 创建时间(时间戳)
messageType String 消息类型(ORDER=订单消息 NORMAL=普通消息)
messageTypeLabel String 消息类型标签(订单消息/普通消息)
kind String 消息特性: NOTIFY=系统通知 / CHAT=聊天消息
senderName String 发件人姓名(仅聊天消息有值)
conversationKey String 会话键(仅聊天消息有值)
teamMessage Boolean 是否车务团队共享消息

请求示例

{ "pageNo": 1, "pageSize": 20, "messageType": "NORMAL" }

响应示例

字段名与类型均照 AdminMessageRespVO 源码;bizId/orderId 取值对齐 NotificationEventHelperTest 单测夹具(baseOrder().orderId = 1001L),用于展示修正后的取值形态:

{
  "code": 200,
  "message": "成功",
  "data": {
    "records": [
      {
        "messageId": "1001",
        "categoryCode": "SYSTEM",
        "title": "需求已驳回",
        "content": "订单 1001 的用车需求已被驳回:车型不合适",
        "link": "/order-v2/detail/1001",
        "bizId": "1001",
        "bizType": "ORDER",
        "isRead": 0,
        "createTime": 1709452800000,
        "messageType": "NORMAL",
        "messageTypeLabel": "普通消息",
        "kind": "NOTIFY",
        "senderName": null,
        "conversationKey": null,
        "teamMessage": false
      }
    ],
    "total": 1,
    "page": 1,
    "pageSize": 20
  },
  "success": true
}

空数据 / 降级响应

{
  "code": 200,
  "message": "成功",
  "data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
  "success": true
}

错误响应

未登录/取不到 adminId 时由 AdminRequestContextUtil.requireAdminId 统一拦截(鉴权异常,非本次改动范围),不返回业务错误码;参数校验失败(如 pageSize>100)由 @Valid 拦截返回标准校验错误结构,本次未改动。

业务边界

  • WHERE 条件含 adminId,仅返回当前登录员工自己的收件箱;VEHICLE_MANAGER 角色会额外混排车务团队共享池行(teamMessage=true),其余角色不受影响。
  • 本次改动不影响分页、排序、已读状态、团队池行的既有行为,只影响 REQUIREMENT_REJECTED 类型行的 bizId/link 取值。
  • 部署生效前已产生的 REQUIREMENT_REJECTED 历史行,bizId/link 仍是旧值,见「六、边界行为」。

2. 站内信详情 GET /admin/message/{id}

VO: Long messageId(PathVariable) → Result<AdminMessageRespVO>

使用场景

列表点开一条消息、顶部铃铛点击、详情页刷新(前端仅持 messageId)时按 ID 拉取单条详情,含完整正文与跳转关联业务信息。纯查询,不改变已读状态。

入参

字段 位置 类型 必填 约束 说明
id Path Long ✅ {id} 消息 ID

出参 Result<AdminMessageRespVO>

字段与「1. 站内信分页列表」的 AdminMessageRespVO 表完全一致(同一 VO),此处不重复列出各字段说明;bizId/link 的修正内容同上。

请求示例

GET /admin/message/1001

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "messageId": "1001",
    "categoryCode": "SYSTEM",
    "title": "需求已驳回",
    "content": "订单 1001 的用车需求已被驳回:车型不合适",
    "link": "/order-v2/detail/1001",
    "bizId": "1001",
    "bizType": "ORDER",
    "isRead": 0,
    "createTime": 1709452800000,
    "messageType": "NORMAL",
    "messageTypeLabel": "普通消息",
    "kind": "NOTIFY",
    "senderName": null,
    "conversationKey": null,
    "teamMessage": false
  },
  "success": true
}

空数据 / 降级响应

不存在空数据形态(单条查询查不到直接走下方错误响应);无下游依赖,无降级分支。

错误响应

消息不存在 / 已逻辑删除 / 属于其他员工,三种情况统一返回同一错码(AdminMessageService.getMessageDetail,不区分原因、不泄露他人消息是否存在):

{
  "code": 200401,
  "message": "站内信不存在或无权访问",
  "data": null,
  "success": false
}

错误码定义:UserProfileErrorCode.ADMIN_MESSAGE_NOT_FOUND(hl-user-service/src/main/java/com/hulalv/user/errorcode/UserProfileErrorCode.java:260),HTTP 状态仍为 200,业务码 200401。本次改动未新增/修改该错误码。

业务边界

  • WHERE 条件含 adminId,越权访问一律按不存在处理(不区分"不存在"和"是别人的")。
  • VEHICLE_MANAGER 角色额外放行车务团队池行,其余角色查不到池行,行为不变。
  • 本次改动不影响该接口的鉴权、越权防护、错误码,只影响返回体内 bizId/link 两个字段在 REQUIREMENT_REJECTED 类型行上的取值。

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

本节写的是响应值契约(bizType/bizId 的配对保证),不是入参 payload 规则——本次改动不涉及请求参数。

bizType ⇒ bizId 的配对保证

bizType 声明业务对象类型,bizId 必须是该类型对象的主键;前端据这一对字段决定跳转目标。本次修复后,NotificationEventHelper 发出的全部事件(CONTRACT_FAILED/INSURANCE_FAILED/ORDER_PAYMENT_REMINDER/REQUIREMENT_REJECTED)统一在 buildAndSend 内部写死 bizType="ORDER" 且 bizId=String.valueOf(order.getOrderId()),不再接受调用方传入任意 bizId——从结构上排除"声明 ORDER 却传别的 id"这一类错误。

bizType bizId 语义 前端应有动作
ORDER 订单 ID getOrderReadableRoute(bizId, userStore) → /order-v2/detail/{bizId}(或管家受限变体)
HOUSE / HOUSE_LEAD 参见前端既有路由逻辑(本次未改动) 本次未改动
REFUND 订单 ID(RefundNotificationHelper.java:128-129 本就正确,未改动) 同 ORDER

前端需要知道的取值边界

  • 若 order.getOrderId() 为空,本次修复后该条通知整条不投递(不会出现 bizId 为字面量 "null" 的行);改前会投递一条 bizId="null" 的消息。
  • requirementId 不再出现在 bizId/bizType 里,如需按需求行排查,只能从 notification_send_log.params_json 里取(后端排查用途,前端接口不暴露该字段)。

五、数据库行为

admin_message 表(新产生的 REQUIREMENT_REJECTED 消息,示例 orderId=1001、requirementId=9009)

列 改前 改后
biz_id 9009(需求行 ID) 1001(订单 ID)
biz_type ORDER ORDER(未变)
link /order/detail/1001 /order-v2/detail/1001

notification_send_log.params_json

改前 改后
不含 requirementId 新增 key requirementId(值为空时落空字符串 "",不落字面量 "null")

notification_event_config 表(Flyway V20260922_001)

UPDATE `notification_event_config`
SET `inapp_link_template` = '/order-v2/detail/${orderId}',
    `update_time` = NOW()
WHERE `event_code` = 'REQUIREMENT_REJECTED';

按 event_code 唯一键定位,最多影响 1 行;只改前缀不改 ${orderId} 占位,重复执行第二次起 affected rows 为 0(幂等)。不改动已合入 dev-v3 的 V20260812_001(Flyway 已冻结迁移不可修改,红线第 ⑫ 条)。


六、边界行为

  • 历史数据不回刷:admin_message.biz_id/link 在 createMessage 落库那一刻写死,之后读取直接透传,不会按当前配置重新渲染。修复部署生效前已产生的 REQUIREMENT_REJECTED 历史站内信,biz_id 仍是需求行 ID、link 仍是 /order/detail/...,两个字段都是旧值,点「跳转」仍会落空。这批数据只存在于测试环境(order-v3/fleet 尚未上生产,该事件在生产从未产生过消息),不做批量回刷。
  • 测试发送端点有意不对齐:POST /admin/notification/config/{id}/test(AdminNotificationController.java:139-140)固定写 bizId="TEST-" + System.currentTimeMillis()、bizType="ORDER",走真实分发路径,会在 admin_message 落一条 biz_id 为 TEST-17... 形态的行,点「跳转」必然落空。本单有意不修(它是管理端调试工具,不代表真实业务对象)。
  • 供应商打回分支不总触发通知:supplier-reject 两个端点仅当打回目标解析为 CONSULTANT(非团期订单)时才触发该事件;团期订单打回目标为管理员,本期不发通知(既有行为,本次未改动)。
  • orderId 为空时整条不投递:order.getOrderId()==null 时直接跳过发送(不产生该条通知),而不是发一条 bizId 为字面量 "null" 的消息。
  • link 字段当前是死字段:前端 jumpToBiz(hl-ui/src/views/notification/MyMessages/index.vue:268-286)只读 bizId/bizModule/bizType/peerRole,全函数不出现 link。link 前缀对齐不改变任何现有跳转行为,只是让该字段不再是一个过期契约值。

六.5、枚举 / 数据字典

bizType(AdminMessageRespVO.bizType)

所属字段: bizId/bizType 成对出现 | 类型: String

值 中文 说明
ORDER 订单 bizId 为订单 ID;本次修复的对象——REQUIREMENT_REJECTED 事件恒为该值
HOUSE 房务 本次未改动
REFUND 退款 RefundNotificationHelper 发出,bizId 本就是订单 ID,未受本次缺陷影响

六.6、修改前后对比

字段级对比

字段 改前 改后
admin_message.biz_id(REQUIREMENT_REJECTED 新消息) 需求行 ID(如 9009) 订单 ID(如 1001)
admin_message.link(REQUIREMENT_REJECTED 新消息) /order/detail/{orderId} /order-v2/detail/{orderId}
notification_send_log.params_json 不含 requirementId 含 requirementId(空值落 "")

行为级对比

行为 改前 改后
前端点「跳转」(REQUIREMENT_REJECTED 类消息) 拿需求行 ID 当 orderId 查订单,查不到,跳转落空 拿正确的 orderId 查订单,正常打开订单详情
order.getOrderId() 为空 仍发送,bizId 落字面量 "null" 直接跳过发送,不产生该条通知

六.7、影响评估

  • 是否破坏向后兼容: 否——AdminMessageRespVO 字段结构未变,仅 REQUIREMENT_REJECTED 一种事件的 bizId/link 取值修正;历史行不追溯改写。
  • 前端是否必须同步上线: 否——字段名/类型/位置均未变,现有读取代码(jumpToBiz 等)无需任何修改即可正确工作。
  • 前端 workaround 清理点: 未查证前端是否针对该缺陷做过规避(例如捕获跳转 404 静默失败、或对 REQUIREMENT_REJECTED 类消息隐藏「跳转」按钮);若存在,现在可以移除,但本次未在 hl-ui 找到此类代码痕迹。

七、不影响范围

  • 仅影响: REQUIREMENT_REJECTED 事件写入 admin_message 的 biz_id/link 取值,及其对应的 notification_event_config.inapp_link_template 前缀。
  • 零影响:
    • AdminMessageRespVO/AdminMessagePageReqVO 的字段结构(名称/类型/位置)
    • GET /admin/message/list、GET /admin/message/{id} 之外的全部站内信接口(未读总数、分类未读汇总、标记已读、全部已读、分类已读、删除)——入参/出参/错误码均未改动
    • 4 个触发该通知的写端点(两个 .../reject、两个 .../supplier-reject)自身的入参、出参、错误码——这些端点本身代码本次未改动,只是它们触发的通知副作用取值变了
    • 本类其余 3 个事件(CONTRACT_FAILED/INSURANCE_FAILED/ORDER_PAYMENT_REMINDER)——这 3 个事件改前就已经用 order.getOrderId() 作为 bizId(走的是无 bizId 参数的 send() 重载),未受本次缺陷影响;本次改动只是把这个既有正确取值从「各方法各自传入」统一收敛到 buildAndSend 内部写死,行为不变
    • RefundNotificationHelper 发出的退款类事件(bizId=orderId 本就正确),未改动
    • C 端 user_message 收件箱(dispatchInApp/InternalMpMessageController):REQUIREMENT_REJECTED 不带 userId,不投递 C 端,与本次改动无关

八、测试环境已验证

部署:hl-order-service-v3 与 hl-user-service 均已部署到 dev-v3 的 6af4d93e5,双实例滚动完成(order-v3 8086/8186、user-service 8081/8181,Nacos namespaceId=test 四个实例均 healthy:true)。deploy-status.sh 复核:两服务 COMMIT=6af4d93e5 BEHIND=0/N STATE=ok,部署时间 2026-09-22 11:35:43 / 11:36:25。

Flyway 迁移(hl-user-service-8181.log):

11:36:32.835 [main] INFO o.f.core.internal.command.DbMigrate - Migrating schema `hl_user_service` to version "20260922.001 - fix requirement rejected inapp link prefix"
11:36:32.859 [main] INFO o.f.core.internal.command.DbMigrate - Successfully applied 1 migration to schema `hl_user_service`, now at version v20260922.001 (execution time 00:00.035s)

端到端实测(自建夹具:orderId=2102242483750756354 / orderNo=HL20260922114400396 / 需求行 id=2102243384951558146)——提交用车需求 → 车控驳回 → 站内信产出。新产生的 admin_message 行:

字段 实测值
biz_type ORDER
biz_id 2102242483750756354(= orderId,不是需求行 id 2102243384951558146)
link /order-v2/detail/2102242483750756354
title 您提交的用车需求被驳回
create_time 2026-09-22 11:47:51

分发日志(hl-user-service-8181.log):

11:47:51.310 c.h.u.n.NotificationDispatcher - 分发通知事件: eventCode=REQUIREMENT_REJECTED, userId=null, bizId=2102242483750756354
11:47:51.325 c.h.user.service.AdminMessageService - 创建管理端站内信: adminId=1001, categoryCode=SYSTEM, eventCode=REQUIREMENT_REJECTED, title=您提交的用车需求被驳回

需求行 ID 仍可反查:同一次事件的 notification_send_log.params_json 实测落库,含 "requirementId":"2102243384951558146",与夹具需求行 id 一致——需求行 ID 从 bizId 换到 params 后没有丢失(但它不在站内信接口的响应里,详见四、契约约束)。

事件配置:notification_event_config.inapp_link_template 实测为 /order-v2/detail/${orderId}(字面量,Flyway placeholder-replacement: false),占位符在 NotificationDispatcher 运行时渲染。

定向单测(dev-v3 @ 6af4d93e5):

测试类 读数
NotificationEventHelperTest Tests run: 10, Failures: 0, Errors: 0, Skipped: 0
RequirementServiceTest Tests run: 321, Failures: 0, Errors: 0, Skipped: 0
LayerEnforcementTest Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
RedLineArchTest Tests run: 12, Failures: 0, Errors: 0, Skipped: 0

合计 Tests run: 348, Failures: 0, Errors: 0, Skipped: 0,BUILD SUCCESS。


十、相关文档

关联 / 联系人

链接

联系人

  • 后端负责人: @wx