30 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 | 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:多次以相同driverIdupsert 只更新同一张活动单,不重复开单。 - 抢锁失败(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 在测试服上一次都没有触发过。
必须两句一起读:
- 这不是"测过没触发",而是没测——三个端点都是
/v3/internal/,不经网关,无法用浏览器/Postman 直接打;唯一合法调用方是 fleet 经 Feign 调,要手动触发就必须让 fleet 真实写一条司机年保台账(upsert/invalidate会改动/插入insurance_order行)或真实调一次保游远程出单接口并产生扣费(purchase),本次评估认为不值得为取一次 100503 读数去污染真实业务数据,故未执行。 - 互斥本身的证据来自 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