文件
hl-api-changelog/changelogs-v2/2026-09/21_7980_司机险年保台账upsert与作废两写口补同名锁新增100503冲突码-内部接口-修改接口-管理后台.md
T
2026-09-21 00:08:16 +08:00

30 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 7980 司机险年保台账 upsert/作废两写口补同一把 @Lock4j name(driver-ins:ledger),新增可重试冲突码 100503;投保写口同键但刻意不互斥(driver-ins:purchase),未新增错误码 internal wx(GIT) 修改接口 deployed not_required not_required PR #8072 已 squash 合并 dev-v3(合并提交 8bf9c520c,#7980 AC-6,工单最后一条验收项)。✅ 已部署:2026-09-20 23:43 hl-order-service-v3 测试服滚动更新两实例,8186/8086 分别 12/13 秒起监听,deploy-backend.sh 报 rolling deploy complete;部署记录行 hl-order-service-v3 <- dev-v3 @ 8bf9c520c,即运行版本就是本次合并提交本身(非祖先链判据,是精确相等)。🔴 backend_status=deployed 但本文档没有做任何功能调用取证:没有实际调过 driver-annual/upsert、driver-annual/{driverId}/invalidate、driver-purchase 三个端点里的任何一个,100503 在测试服上一次都没有触发过——这是「没测」不是「测过不会触发」。理由:三个端点都是 /v3/internal/**,不经网关,唯一合法入口是 fleet 经 Feign 调用,手动触发需要真实写司机年保台账/投保行(台账 upsert 会更新或新插 insurance_order 行,投保会触发保游远程出单 HTTP 与真实扣费),本次评估认为不值得为了取一次 100503 读数去污染真实业务台账,故互斥本身只由代码层面的落库级并发 IT(Lock4jSharedNameConcurrencyIT)与按运行期锁键形状反查的契约用例(Lock4jSharedNameContractTest/Lock4jNameGroupGateTest)覆盖,这些是 PR 正文所载的本地/CI 证据,不是本 changelog 在测试服上做的验证。gateway_status=not_required:三个端点路径/方法零变化,且路径前缀 /v3/internal/,InternalInsuranceController 类注释自陈『不经过认证拦截器(/v3/internal/** 已排除)』,与网关路由无关。frontend_status=not_required,结论+依据:①本组无 C 端——全仓 grep driverInsuranceFeignClient 的 upsertAnnualPolicy/invalidateAnnualPolicy/driverPurchase 三个方法调用点,命中的调用方全部在 hl-fleet-service 且全部挂在 /admin/fleet/drivers 前缀下(DriverInsuranceController.java:33 RequestMapping、:56 purchase 端点、:80 insureAnnual 端点均在此前缀),没有任何 mp/小程序侧调用;②唯一可能把新失败码传导到 hl-ui 的路径是 DriverInsuranceService.java:352-353 那次同步 invalidateAnnualPolicy 调用(购买年险时若旧档案是 MANUAL 年保,出单成功后同步作废旧台账),但已逐行核实:该调用若失败(含本次新增的 100503),被 DriverInsuranceService.java:226 的 catch(AnnualBindingRollbackQueuedException) 捕获,统一重新包装成既有错误码 600206(DriverErrorCode.ANNUAL_BIND_FAILED,:241-242),即便触发的具体原因是 100503,hl-ui 侧看到的仍是它已经在处理的 600206,没有新码需要 hl-ui 适配。mmg 前端复核 2026-09-21:维持 not_required。driver-annual/driver-purchase//v3/internal 在 hl-admin src 全仓零命中,三端点不经网关前端不可达;唯一间接路径(投保成功后同步作废旧 MANUAL 台账失败)已被 fleet 侧重包装为既有 600206,而 600206 前端已显式处理(DriverEditModal.vue:1300/1339 关弹窗并刷新防重复投保、insurance/orders/index.vue:1040 特判收敛、driverInsurancePurchase.js 导出常量并有 spec 覆盖),新增 100503 无触达 hl-ui 的路径,零改动。 2026-09-21 dev-v3

order-v3: 司机险年保台账 upsert/作废两写口补同一把 @Lock4j name,新增可重试冲突码 100503

存放目录:

  • 一期(v2,无 order-v3 标签的工单)→ changelogs/{YYYY-MM}/
  • 二期(v3,order-v3 标签的工单)→ changelogs-v2/{YYYY-MM}/

服务: hl-order-service-v3(消费方为 hl-fleet-service,经 Feign 调入,均在 fleet /admin/fleet/drivers/** 之下,无 C 端) PR: #8072 Issue: #7980(AC-6,最后一条验收项) 日期: 2026-09-20 影响范围: /v3/internal/insurance/driver-annual/upsert、/v3/internal/insurance/driver-annual/{driverId}/invalidate 两个 internal 端点新增可重试冲突码;/v3/internal/insurance/driver-purchase 端点锁的 Redis 键名变化但外部行为、错误码集合均未变化


⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)

  • InsuranceManageService.upsertDriverAnnualPolicy、InsuranceManageService.invalidateDriverAnnualPolicy、InsuranceCreateService.purchaseForDriver 三个方法写着逐字相同的锁键 'driver-ins:' + driverId(三种不同的 SpEL 写法,字面上不容易看出是同一组),却都没写 name——lock4j 缺省 name 时用「类全限定名+方法名」拼进 Redis 键,于是三处此前各持一把互不相干的锁。
  • 🔴 本次的结论与工单原表不同:不是把三处并成一把锁,而是拆成两个域:
    • upsertDriverAnnualPolicy 与 invalidateDriverAnnualPolicy 补同一个显式 name = "driver-ins:ledger"——这两者是真缺陷,必须互斥:upsert 第 2 步 updateById(existing) 是全行盲写(existing 带着查询时的 status=INSURED 一起写回),InsuranceOrder 无 @Version、insurance_order 表无唯一索引;upsert 与 invalidate 交错执行会把刚作废的单静默复活成 INSURED 且不补任何状态流水——这个终态任何合法串行序都产不出来,打破的是"调用返回后该司机无活动 MANUAL 年险单"这条后置条件,而 fleet 正据此把司机档案的 insurance_type 切走。
    • purchaseForDriver 补的是另一个显式 name = "driver-ins:purchase",自成一域、刻意不与台账两口互斥:它不写台账行,只在重叠校验里读台账行;而 upsert 本身不做保障期重叠校验(年保台账记的是司机在外面已买好的年险,系统没有立场拒绝登记),所以"年单与按天单保障期重叠"这个终态在合法串行序 purchase → upsert 下本来就可达,把 purchase 并进同一把锁消不掉它,只会把"upsert 并发成功"换成"upsert 抢锁超时拿 100503 后重试、终态一模一样",白白给一条 MQ 驱动的写口加一个失败码。
  • 🔴 purchaseForDriver 没有新增任何可能返回的错误码:它的锁语义与改前完全等价(同司机投保动作仍然自我串行化,因为默认 name 对单方法本来就够用),唯一变化是它在 Redis 里的键字符串从「类全名+方法名」变成显式的 driver-ins:purchase。不要把「三个方法都补了 name」读成「三个端点都新增了 100503」——只有 upsert/invalidate 这两口之间此前不互斥、本次补上后才有了新的失败路径。
  • 本组无 C 端:三个端点都在 /v3/internal/insurance/**,只有 hl-fleet-service 经 Feign 调入,且 fleet 侧全部挂在 /admin/fleet/drivers/** 之下(详见 status_note 与「七、不影响范围」)。

一、背景

DriverInsuranceLockConstants(本次新建)把设计意图写死成两条互相独立的判据:① Lock4jSharedNameContractTest 按运行期锁键形状反查全类,钉死三个方法成员与"台账两口同名、投保口异名"这条关系;② Lock4jNameGroupGateTest 的 SPLIT_BY_DESIGN 条目钉死这一串键上允许出现的 name 集合恰好是 {driver-ins:ledger, driver-ins:purchase}——第四个方法只要漏写 name,集合里就会冒出空串,门禁当场变红。这两条是 PR 正文所载的本地/CI 证据(Lock4jSharedNameConcurrencyIT 4 例、Lock4jNameGroupGateTest 7 例、Lock4jSharedNameContractTest 5 例),本 changelog 未在测试服上重跑,仅作背景引用。

PR 正文另确认了两条会动摇初始判断但不改变最终结论的事实:① fleet 侧另有一把按司机的锁 fleet:insurance-task:lifecycle:{driverId}(DriverAnnualLedgerOutboxEffectService.java:42-49、DriverService.java:640),台账 upsert/invalidate 在 fleet 侧本来就是串行触发的——但 order-v3 是数据的主人,/v3/internal/insurance/driver-annual/** 是对外契约端点,不能把不变量寄在调用方的自觉上;②测试服日志分母是 0(该组两个方法在采样窗口内一次都没被调用过),据此不能得出"没有告警=没有缺陷"的结论。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 司机年保台账 upsert(内部) POST /v3/internal/insurance/driver-annual/upsert 新增可能返回的错误码 抢锁超时返 100503,与 §2 共用 driver-ins:ledger 锁
2 司机年保台账作废(内部) POST /v3/internal/insurance/driver-annual/{driverId}/invalidate 新增可能返回的错误码 与 §1 共用 driver-ins:ledger 锁
3 司机险投保(内部) POST /v3/internal/insurance/driver-purchase 无外部可观察变化 锁语义与改前完全等价,仅 Redis 键名从默认值改为显式 driver-ins:purchase,未新增错误码

三、接口详情

1. 司机年保台账 upsert(内部) POST /v3/internal/insurance/driver-annual/upsert

VO: DriverAnnualPolicyUpsertDTO → Long

使用场景

fleet 车务在司机档案页录入/编辑「annual(年保)」保险类型时,afterCommit 联动调本端点把年保信息写进 order-v3 的台账:按 (bizType=DRIVER, bizId=driverId, source=MANUAL, status=INSURED) 查活动单,有则更新保单号/保费/起止/投保人,无则插入新单。本次改动不涉及入参/出参字段,只新增一条「被 §2 作废端点占用同一把锁」的失败分支。

入参

字段 位置 类型 必填 约束 说明
driverId Body Long ✅ @NotNull 司机 ID,落 InsuranceOrder.bizId(bizType=DRIVER)
driverName Body String - - 司机姓名,落 policyHolderName 投保人留痕
policyNo Body String ✅ @NotBlank 司机自有年保单真实保单号,落 extPolicyNo
annualPremium Body BigDecimal ✅ @DecimalMin(0, inclusive=false) 年保险费(元),必须大于 0,落 totalPremium
startDate Body LocalDate ✅ 不得晚于 endDate 年保起始日,跨字段校验在提供方(非法返 540225)
endDate Body LocalDate ✅ - 年保结束日
adminId Body Long - fleet 透传,可空=系统 操作管理员 ID,审计留痕用

出参 Result<Long>

字段 类型 说明
data Long(雪花 ID,超出 JS 安全整数范围时 JSON 序列化为字符串,本项目全局 Jackson 策略) 台账单 ID(insuranceOrderId),fleet 回写 fleet_driver.insurance_order_id 用

请求示例

{
  "driverId": "1934567890123456789",
  "driverName": "周师傅",
  "policyNo": "PICC-2026-77721",
  "annualPremium": "3800.00",
  "startDate": "2026-01-01",
  "endDate": "2026-12-31",
  "adminId": 1
}

响应示例

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

空数据 / 降级响应

本接口无「空数据」概念(成功恒返回台账单 ID,有则更新既有单号,无则插入新单号)。无降级路径。

错误响应

{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false }
code 触发条件
400 参数校验失败(driverId/policyNo/annualPremium/startDate/endDate 缺失或格式非法)
540225 年保起始日晚于结束日
100503(本次新增) 抢锁等待超过 3 秒(acquireTimeout,本组 @Lock4j 未显式设置,取 lock4j-core 注解默认值 3000ms;本次另一写口——§2 作废端点——正持有 driver-ins:ledger 锁),可重试

业务边界

  • 天然幂等,无 @Idempotent:多次以相同 driverId upsert 只更新同一张活动单,不重复开单。
  • 抢锁失败(100503)时该次请求未进入方法体,零写入——不会出现"读到旧快照后半途更新"的中间态。
  • 纯本地写库,事务内无同步 Feign/MQ(区别于 §3 投保端点持锁期间含远程出单 HTTP)。
  • 历史多张残留(脏数据)时只更新 createTime 最新一张,其余由 §2 全量兜底清理,本次未改此行为。
  • 老数据兼容:无字段变化。

2. 司机年保台账作废(内部) POST /v3/internal/insurance/driver-annual/{driverId}/invalidate

VO: 无 ReqVO(纯路径参数,无请求体) → Void

使用场景

fleet 车务把司机档案的保险类型从「annual(年保)」切走时,afterCommit 联动调本端点把该司机全部 MANUAL 活动年险单置 CANCELLED(历史脏数据多张残留时全量兜底清理,正常路径只有一张)。无活动单时幂等返回成功。本次改动不涉及入参/出参,只新增一条「被 §1 upsert 端点占用同一把锁」的失败分支。

入参

字段 位置 类型 必填 约束 说明
driverId Path Long ✅ - 司机 ID

出参 Result<Void>

字段 类型 说明
data null 无返回数据

请求示例

POST /v3/internal/insurance/driver-annual/1934567890123456789/invalidate

(跨服务 Feign 调用,无请求体。)

响应示例

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

空数据 / 降级响应

该司机无活动 MANUAL 年险单时同样返回 200 成功(幂等短路,不报错,覆盖"保险类型反复切换"与"从未录入 annual"两种场景)。无降级路径。

错误响应

{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false }
code 触发条件
100503(本次新增) 抢锁等待超过 3 秒(acquireTimeout 默认 3000ms;§1 upsert 端点正持有 driver-ins:ledger 锁),可重试

(本端点自身无其他业务校验分支——无活动单不算错误,是幂等成功。)

业务边界

  • 天然幂等,无 @Idempotent:重复触发/从未开单均不报错。
  • 抢锁失败(100503)时该次请求未进入方法体,零写入——这正是本次要补的缺口:改前 upsert 可能用陈旧快照把刚被本方法置 CANCELLED 的单全行盲写回 INSURED 且不留流水,改后二者不再能交错执行。
  • 不调保游 CancelIns(手工单无外部单号),纯本地翻态,与 cancel(按保单 PK 退保,走保游)是两条不同路径。
  • 老数据兼容:无字段变化。

3. 司机险投保(内部) POST /v3/internal/insurance/driver-purchase

VO: DriverPurchaseInsuranceRequest → DriverInsurancePolicyDTO

使用场景

fleet 车管在司机档案页点「投保」(或详情页「直接投保全年保险」),复用订单侧 purchase 内核出单,落 bizType=DRIVER + bizId=driverId + orderId=NULL。本次改动只是把它自身的 @Lock4j 锁键从"类全名+方法名"改成显式字符串 driver-ins:purchase——它此前对单方法自身就已经生效自互斥(lock4j 缺省 name 对单方法足够用),外部可观察行为、错误码集合、并发语义全部零变化,本节仅为完整性列出,不代表本接口新增了任何契约。

入参

字段 位置 类型 必填 约束 说明
driverId Body Long ✅ @NotNull 司机 ID,落 InsuranceOrder.bizId
planId Body Long ✅ @NotNull,usageCategory 须为 DRIVER/BOTH 司机专用保险计划 ID
coverageStartDate Body LocalDate ✅ @NotNull 保障开始日期
coverageEndDate Body LocalDate ✅ @NotNull 保障结束日期
insuredPerson Body InsuredPersonItemDTO ✅ @NotNull @Valid 被保人(单对象,司机险一人一单)
insuredPerson.name Body String ✅ @NotNull 姓名
insuredPerson.idCardType Body String ✅ @NotNull,ID_CARD/PASSPORT/OTHER 证件类型
insuredPerson.idCardNo Body String ✅ @NotNull 证件号码(明文,内网 Feign 传输)
insuredPerson.phone Body String - - 手机号
insuredPerson.gender Body String - 1=男/2=女/0=未知 性别编码
insuredPerson.birthday Body LocalDate - 可空,引擎从身份证派生 出生日期
orderId Body Long - 仅形状对齐,必传 null 司机投保不关联订单
remark Body String - - 备注

出参 Result<DriverInsurancePolicyDTO>

字段 类型 说明
insuranceOrderId Long(雪花,超出 JS 安全整数范围时序列化为字符串) 保单 ID
policyNo String 本系统保单号(同 extPolicyNo)
extPolicyNo String 保游网外部保单号(异步出单受理时为空,回调后回填)
extOrderNo String 保游网外部订单号
totalPremium BigDecimal(序列化为字符串) 保单总保费
status String 保单状态:PENDING/INSURING/INSURED/CANCELLED/FAILED
statusLabel String 保单状态中文标签
source String 保单来源:BAOYOU/MANUAL
policyPdfUrl String 保单 PDF 地址(未生成/MANUAL 单为空)
coverageStartDate LocalDate 保障起期
coverageEndDate LocalDate 保障止期
insuredPersons List<InsuredPersonBrief> 被保人列表(司机险一人一单,通常单元素)
insuredPersons[].name String 被保人姓名
insuredPersons[].idCardNo String 证件号(脱敏,保前 3 尾 4)

请求示例

{
  "driverId": "1934567890123456789",
  "planId": 300,
  "coverageStartDate": "2026-06-01",
  "coverageEndDate": "2027-05-31",
  "insuredPerson": {
    "name": "周师傅",
    "idCardType": "ID_CARD",
    "idCardNo": "150102198001011234",
    "phone": "13800138000"
  },
  "orderId": null,
  "remark": "司机险投保: 周师傅"
}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "insuranceOrderId": "1934567890200888",
    "policyNo": "BY-2026-0001",
    "extPolicyNo": "",
    "extOrderNo": "EXT20260601001",
    "totalPremium": "88.00",
    "status": "INSURING",
    "statusLabel": "出单中",
    "source": "BAOYOU",
    "policyPdfUrl": "",
    "coverageStartDate": "2026-06-01",
    "coverageEndDate": "2027-05-31",
    "insuredPersons": [ { "name": "周师傅", "idCardNo": "150**********1234" } ]
  },
  "success": true
}

空数据 / 降级响应

本接口无「空数据」概念,无降级路径。

错误响应

{ "code": 540032, "message": "该司机该保障期已有生效保单", "data": null, "success": false }
code 触发条件
540005 保险计划不存在
540031 该保险计划未标注为司机可用(usageCategory 非 DRIVER/BOTH)
540032 该司机该保障期已有生效保单(锁内区间重叠互斥,含 MANUAL 年保台账单参与判定)
100502 5 秒幂等窗口内重复提交(键=driverId+保障起止)

🔴 本端点没有新增 100503:以上错误码集合与改动前完全一致,本次改动不改变这张表的任何一行。

业务边界

  • 幂等:@Idempotent(keyPrefix=insurance:driver-purchase,键=driverId+保障起止)5 秒窗口,仅作完全相同请求误重提防抖,不承担区间重叠去重职责(区间重叠由锁内 540032 校验负责)。
  • 🔴 与 §1/§2 台账两口键虽同为 'driver-ins:' + driverId,但刻意不互斥:本方法在重叠校验里只读 MANUAL 台账单(含未提交完成的除外),不写台账行;upsert 不做重叠校验,所以"年单与按天单重叠"这个终态在合法串行序 purchase → upsert 下本来就可达,并锁消不掉它。
  • 持锁期间包含保游远程出单 HTTP(区别于 §1/§2 纯本地写库),这也是不把台账写口并进本锁的现实理由之一——台账是毫秒级操作,排在一笔外部 HTTP 后面等锁纯属浪费。
  • 老数据兼容:无字段变化。

四、契约约束与正确调用方式(接口类必写)

本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。

并发冲突(100503)只在 §1/§2 之间

场景 响应
✅ 单个 upsert 或 invalidate 请求,无并发 200 + 正常业务结果
✅ 同司机 upsert 与 invalidate 先后到达,后者在 3 秒内轮到锁 两个都 200(排队等待,非立即失败)
❌ 同司机 upsert ∥ invalidate 并发,后到方等待超过 3 秒未抢到锁 { "code": 100503, "message": "资源被占用,请稍后重试", "success": false },HTTP 状态码仍是 200
✅ 同司机 §3 投保 与 §1/§2 台账写口并发 不受影响、不会产生 100503——两者不在同一把锁上,§3 的重叠拒绝(540032)与本次改动无关
✅ 不同 driverId 并发 互不影响,各自独立加锁

调用方(fleet)必须做的事:判断响应体 code === 100503(不是判 HTTP 状态码),命中时按各自的失败处理策略重试(Outbox/MQ 驱动的 §1/§2 调用方本就有重投机制,见「六.6」)。

触发概率评估(结构性推理,非测试服实测)

fleet 侧对台账 upsert/invalidate 的两个真实调用点(DriverAnnualLedgerOutboxEffectService.java:97/113/126)本身已被 fleet 侧的 fleet:insurance-task:lifecycle:{driverId} 锁串行化(同一司机的多个事件不会并发发起 Feign 调用),真实竞争概率接近 0;且持锁段是纯本地写库、无 HTTP,正常耗时上界远小于 acquireTimeout 默认的 3000ms 超时阈值。不构成类似 #8050 的重试风暴。这是结构性推理,本文档未在测试服上实测验证这个判断。


五、数据库行为(涉及写操作时必写)

本次不改变任何一次成功写操作实际写入的行数或内容——三个端点各自原有的写入逻辑(台账 upsert/CANCELLED 翻态/投保出单)逐字节不变。变化的只是"谁能在同一时刻对同一 driverId 执行台账写入":

维度 改动前 改动后
upsert ∥ invalidate 是否互斥 否(各自持独立锁,可交错执行) 是(同一把锁,串行执行)
交错执行的后果(改前可复现,PR 变异证明) 刚作废的单被 upsert 陈旧快照全行盲写复活成 INSURED,且不留状态流水(行=INSURED,末条流水 new_status=CANCELLED,两者矛盾) 该终态不再可达
purchase 与台账两口的关系 各自独立锁,互不阻塞 仍然互不阻塞(刻意维持,非缺陷)
抢锁失败时是否有部分写入(upsert/invalidate) N/A(此前不会因锁而失败) 否,零写入

六、边界行为

  • 司机不存在等前置校验:本三端点自身不做司机存在性校验(由 fleet 侧在调用前保证),保持改前行为
  • upsert 起止日期反转 → 540225
  • purchase 计划不存在/非司机可用/保障期重叠 → 540005/540031/540032,与本次改动无关,行为不变
  • invalidate 无活动单 → 幂等返回成功,不是错误
  • 抢锁超时(本次新增,仅 upsert/invalidate 之间)→ 100503,HTTP 200,可重试,零写入
  • purchase 端点不会因本次改动新增任何失败分支
  • 老数据兼容:本次不涉及字段增删,存量台账数据无需迁移

六.5、枚举 / 数据字典(接口出现枚举时必写)

保单状态(status,出现于 §3 出参)

所属字段: DriverInsurancePolicyDTO.status | 类型: String(本次未变化,随改动一并列出便于自包含联调)

值 中文 说明
PENDING 待出单 -
INSURING 出单中 异步受理期,extPolicyNo 可能为空
INSURED 已承保 -
CANCELLED 已退保 -
FAILED 投保失败 -

保单来源(source,出现于 §3 出参)

所属字段: DriverInsurancePolicyDTO.source | 类型: String

值 中文 说明
BAOYOU 保游网出单 有电子保单,可下载
MANUAL 手工录入(司机年保档案联动) §1/§2 两个端点操作的正是这类单;无保游网电子保单,前端应隐藏下载/重传按钮

六.6、修改前后对比(修改/删除类接口必写,新增跳过)

字段级对比

字段 改前 改后
(无) 三个接口的请求字段、响应字段逐字节不变 同左

行为级对比

行为 改前 改后
upsert ∥ invalidate 并发 各持独立锁,可交错执行,可复活已作废的单且不留流水 同一把锁 driver-ins:ledger,串行执行
purchase 与台账两口 各自独立锁,互不阻塞 刻意维持互不阻塞(driver-ins:purchase 自成一域),非本次要修的问题
抢锁失败的响应(仅 upsert/invalidate) 不存在这条路径 路径真实存在(返回 100503,HTTP 200,业务失败,可重试);已部署测试服但未做任何功能调用取证,无实测触发数据
InsuranceManageService 两处 javadoc(改前断言"与 purchaseForDriver 同键互斥") 断言为假(三处均无 name 时逐字相同的键也不构成互斥) 订正为准确描述:与台账另一口同名互斥,与 purchase 刻意不互斥
请求/响应字段、既有业务错误码(540005/540031/540032/540225) 不变 不变

六.7、影响评估(修改/删除类必写)

  • 是否破坏向后兼容: 否——三个接口的请求体/响应体字段、既有错误码全部零增删;仅 upsert/invalidate 之间新增一条此前不存在的失败路径(100503),purchase 端点零变化。
  • 前端是否必须同步上线: 不适用——本组无 C 端、无 hl-ui 直接触达路径;唯一间接路径(§3 投保后同步作废旧 MANUAL 台账失败)已被 fleet 侧既有错误码 600206 吸收,hl-ui 无需为本次改动新增任何处理逻辑。
  • 前端 workaround 清理点: 无。

七、不影响范围(显式声明, 帮前端/QA 缩小排查面)

  • 仅影响: driver-annual/upsert、driver-annual/{driverId}/invalidate 两个端点之间的并发失败分支
  • 零影响:
    • driver-purchase 端点的请求/响应字段、错误码集合、并发语义(外部可观察行为零变化)
    • InternalInsuranceController 其余端点(自动投保、保险列表/覆盖查询、退保、分享保单、下载保单、司机保单分页/详情/按服务日查询等)
    • fleet 侧 fleet:insurance-task:lifecycle:{driverId} 锁及其覆盖的档案编辑/出单绑定/状态回调路径——那是另一个服务里的另一把锁,本次未改动
    • fleet 侧 DriverInsuranceController(/admin/fleet/drivers/**)的入参/出参/错误码——车管投保页面看到的契约零变化
    • hl-gateway 路由配置——/v3/internal/** 本就不对公网暴露,本次未新增任何路由

八、测试环境已验证

部署(已完成)

  • deploy-backend.sh 已执行,hl-order-service-v3 滚动更新两实例,8186 端口 12 秒起监听、8086 端口 13 秒起监听,脚本报告 rolling deploy complete。
  • 部署记录行:hl-order-service-v3 <- dev-v3 @ 8bf9c520c——运行版本就是本次合并提交本身(精确相等,非祖先链判据)。

🔴 没有做任何功能调用取证

本文档没有调用过 driver-annual/upsert、driver-annual/{driverId}/invalidate、driver-purchase 三个端点中的任何一个;100503 在测试服上一次都没有触发过。

必须两句一起读:

  1. 这不是"测过没触发",而是没测——三个端点都是 /v3/internal/,不经网关,无法用浏览器/Postman 直接打;唯一合法调用方是 fleet 经 Feign 调,要手动触发就必须让 fleet 真实写一条司机年保台账(upsert/invalidate 会改动/插入 insurance_order 行)或真实调一次保游远程出单接口并产生扣费(purchase),本次评估认为不值得为取一次 100503 读数去污染真实业务数据,故未执行。
  2. 互斥本身的证据来自 PR 正文所载的代码层面证据(非测试服):Lock4jSharedNameConcurrencyIT(4 例落库级并发 IT,含变异证明——摘掉共用 name 后复现"刚作废的单被复活成 INSURED 且流水终点仍是 CANCELLED"这一矛盾态)与按运行期锁键形状反查的契约用例 Lock4jSharedNameContractTest/Lock4jNameGroupGateTest(钉死"台账两口同名、投保口异名"这条关系,防止日后有人漏写或错写 name)。这些是本地/CI 证据,不是本 changelog 在测试服上独立验证的结果。

十、相关文档

  • 关联 Issue: wx/HL#7980(AC-6,最后一条验收项)
  • 关联 PR: wx/HL#8072
  • 前序三份(同工单不同 AC,均为 backend_status=pending 待部署):changelogs-v2/2026-09/20_7980_配房工作台七个写口补齐需求级互斥新增100503可重试冲突码-修改接口-管理后台.md(AC-5,#8069)、changelogs-v2/2026-09/20_7980_出行人删改与发票申请三组写口补同名锁新增100503冲突码触达C端-修改接口-管理后台.md(AC-7,#8049)、changelogs-v2/2026-09/20_7980_产品库存扣减恢复两写口补同名锁新增100503冲突码-修改接口-管理后台.md(AC-8,#8047)

关联 / 联系人

链接

联系人

  • 后端负责人: @wx