文件
hl-api-changelog/changelogs-v2/2026-09/24_8292_订单标签四个写端点补订单归属守卫,与读面口径对齐-修改接口-管理后台.md
T
2026-09-24 10:23:01 +08:00

18 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 8292 订单标签四个写端点补订单归属守卫,与读面口径对齐 admin wx(GIT) 修改接口 deployed not_required not_required 接口路径、请求体、成功响应结构全未变,变的是失败分支:新增 581064 无权修改该订单。无新增路由,故 gateway_status=not_required。测试服实测(网关 https://api.test.1814.love,部署 ab630a3f8):test_admin(CUSTOMIZER,非归属) POST 他人订单标签 → 581064 且复查无残留;test_admin 对自己订单读/原样全量回写 → 200;wx(SUPER_ADMIN) 对他人订单读/原样回写 → 200;admin(当前角色 GROUP_BATCH_MANAGER)写他人订单 → 581008(#8154 的 Controller 角色守卫先拒)。边缘变化:deleteTag/patchTag 传不存在的 orderId 时错误码由 581410/581411 变为 581401。frontend_status=pending:hl-ui 现按 err.message 透传,581064 走通用 toast 即可,无需改码。 前端 2026-09-24 核验 not_required:四写端点前端只消费 PUT /tags(replaceOrderTags 全量替换,POST/DELETE/PATCH 增删改无封装调用),TagPickerModal catch 纯透 err.message 无按码分支;581064/581410/581411/581401/581433 前端零引用,新增 581064 走通用 toast 透后端原文即达标;581008/581045 是 orderAccess 路由隔离角色守卫与标签业务无关;581410/581411 变形只影响前端无调用的 DELETE/PATCH。零改动。 2026-09-24 dev-v3

订单标签: 四个写端点补订单归属守卫,与读面口径对齐

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

服务: hl-order-service-v3(端口 8086) PR: #8307 Issue: #8292 日期: 2026-09-24 影响范围: 订单详情页「标签」区四个写操作的权限口径;接口路径、请求体、成功响应结构均未变


⚠️ 关键变化(本版与上版行为不同,必读)

这四个写接口现在会拒绝「非本单归属人」的调用,失败时返回新错误码 581064(无权修改该订单)。

  • 本次变了什么:四个写端点补上了订单归属校验。
  • 前端以前以为的:除团期管理员外谁都能写(确实是 #8290 之前的行为)。
  • 实际现在是什么:与读面(#8290 已收紧的 GET .../tags、GET .../tag-picker)同一套放行集合——读得到的单才写得动。

前端以前只会从这四个接口收到 581008(团期管理员)与 581045(房务),现在会新增收到 581064。


一、背景

#8170 AC-18(PR #8290)给 GET /v3/admin/order/{orderId}/tags、GET .../tag-picker 与 GET .../itinerary-document 补上了 OrderViewGuard.assertOrderReadable,读面收紧后同一批数据上的四个写端点仍零归属校验,形成「不能看,但能改」——任意后台角色拿到 orderId 就能增、能改、能删他人订单的标签,其中 PUT .../tags 是全量替换,会清掉不在入参里的标签。

口径由 wx 于 2026-09-24 定案:写面与读面读写对称。本单是 #8170 挂账的「存量治理」单。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 增订单标签 POST /v3/admin/order/{orderId}/tag 失败分支新增 非归属人 → 581064
2 删订单标签 DELETE /v3/admin/order/{orderId}/tag/{tagId} 失败分支新增 非归属人 → 581064;orderId 不存在时由 581410/581411 改为 581401
3 改订单标签 PATCH /v3/admin/order/{orderId}/tag/{tagId} 失败分支新增 非归属人 → 581064;533 原有「非标签创建人 → 581433」不变
4 全量替换订单标签 PUT /v3/admin/order/{orderId}/tags 失败分支新增 非归属人 → 581064

四个接口的请求体、成功响应体、路径参数全部未变。


三、接口详情

1. 增订单标签 POST /v3/admin/order/{orderId}/tag

VO: TagAddReqVO → Result<OrderTagVO>

使用场景

订单详情页标签区点击「打标签」后提交。creator / createdAt 由后端从 JWT 派生,前端不传。

入参

字段 位置 类型 必填 约束 说明
orderId Path String ✅ - 订单 ID
tagName Body String ✅ trim 后非空,同订单内不重名 标签名
tagColor Body String ❌ # + 6 位十六进制 不传默认 #5B8FF9

出参 Result<OrderTagVO>

字段 类型 说明
tagId String 雪花 ID,字符串返回防精度丢失
orderId String 订单 ID
tagName String 标签名
tagColor String 色值

请求示例

{ "tagName": "重点客户", "tagColor": "#FF6B6B" }

响应示例

{ "code": 200, "message": "成功", "data": { "tagId": "2100...", "orderId": "9199...", "tagName": "重点客户", "tagColor": "#FF6B6B" }, "success": true }

空数据 / 降级响应

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

写接口无列表与降级语义:成功恒返 200 + 业务结果,失败恒返业务码,不存在「空集合」形态; 无下游 Feign/消息依赖,故也没有降级分支。前端按 code == 200 判成功、否则 toast message 即可。

错误响应

{ "code": 581064, "message": "无权修改该订单", "success": false, "data": null }

业务边界

  • 鉴权:必须是本单定制师,或 ADMIN / SUPER_ADMIN / 车务管理员;团期管理员被 581008 拒(只读承诺 #8154);房务管理员与房务组长 581045。
  • 空值:tagName trim 后为空 → 581402;颜色格式非法 → 581404。
  • 幂等/并发:同订单同名标签 → 581403(不做幂等 upsert)。
  • 失败零写入:归属守卫排在插入之前,被拒时一行都不会写。

2. 删订单标签 DELETE /v3/admin/order/{orderId}/tag/{tagId}

VO: 无请求体 → Result<Boolean>

使用场景

订单详情页标签区删除某个已挂标签(软删)。

入参

字段 位置 类型 必填 约束 说明
orderId Path String ✅ - 订单 ID
tagId Path String ✅ - order_tag.tag_id

出参 Result<Boolean>

字段 类型 说明
data Boolean true = 删除成功

请求示例

DELETE /v3/admin/order/9199000000000000002/tag/2100123456789012345
Authorization: Bearer <admin-token>

响应示例

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

空数据 / 降级响应

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

写接口无列表与降级语义:成功恒返 200 + 业务结果,失败恒返业务码,不存在「空集合」形态; 无下游 Feign/消息依赖,故也没有降级分支。前端按 code == 200 判成功、否则 toast message 即可。

错误响应

{ "code": 581401, "message": "订单不存在", "success": false, "data": null }

业务边界

  • 鉴权:同接口 1。标签级没有创建人限制(任何有权者都可删该单的标签),权限完全由订单归属决定。
  • 顺序:先取订单 → 判空(581401)→ 归属守卫 → 才按 tagId 取标签。越权请求不再能从「标签不存在 581410」与「标签不属于该订单 581411」的差别推断某个 tagId 是否存在。
  • 失败零写入:守卫在任何标签表访问之前。

3. 改订单标签 PATCH /v3/admin/order/{orderId}/tag/{tagId}

VO: TagPatchReqVO → Result<Boolean>

使用场景

订单详情页标签区编辑某个已挂标签的名称或颜色(PATCH 语义,只改传入字段)。

入参

字段 位置 类型 必填 约束 说明
orderId Path String ✅ - 订单 ID
tagId Path String ✅ - order_tag.tag_id
tagName Body String ❌ trim 后非空 不传则不改
tagColor Body String ❌ # + 6 位十六进制 不传则不改

出参 Result<Boolean>

字段 类型 说明
data Boolean true = 修改成功

请求示例

{ "tagName": "高净值", "tagColor": "#FF6B6B" }

响应示例

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

空数据 / 降级响应

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

写接口无列表与降级语义:成功恒返 200 + 业务结果,失败恒返业务码,不存在「空集合」形态; 无下游 Feign/消息依赖,故也没有降级分支。前端按 code == 200 判成功、否则 toast message 即可。

错误响应

{ "code": 581433, "message": "无权修改该标签(非创建人)", "success": false, "data": null }

业务边界

  • 两道互不替代的校验:① 订单归属(581064,本单新增);② 标签级创建人(操作人 ≠ order_tag.created_by → 581433,既有行为未变)。
  • 581433 的文案本单由「非创建人且非主管」订正为「非创建人」:实现里从来没有「主管」判定分支,原文案是空承诺。
  • 顺序:先取订单 → 判空(581401)→ 归属守卫 → 才按 tagId 取标签。
  • 操作人上下文缺失时第二道校验整体放行(既有 fail-open),故它不是归属防线;归属防线是第一道。

4. 全量替换订单标签 PUT /v3/admin/order/{orderId}/tags

VO: ReplaceOrderTagsReqVO → Result<OrderTagListRespVO>

使用场景

打标签弹窗点「确定」后整批提交(前端先 GET .../tag-picker 再提交全量列表)。

入参

字段 位置 类型 必填 约束 说明
orderId Path String ✅ - 订单 ID
tags[].tagName Body String ✅ trim 后非空 标签名(按名去重,保留首个)
tags[].tagColor Body String ❌ # + 6 位十六进制 不传默认 #5B8FF9

出参 Result<OrderTagListRespVO>

字段 类型 说明
attached Array 替换后的全量标签快照(元素同 OrderTagVO)

请求示例

{ "tags": [ { "tagName": "重点客户", "tagColor": "#FF6B6B" } ] }

响应示例

{ "code": 200, "message": "成功", "data": { "attached": [ { "tagId": "2100...", "orderId": "9199...", "tagName": "重点客户", "tagColor": "#FF6B6B" } ] }, "success": true }

空数据 / 降级响应

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

写接口无列表与降级语义:成功恒返 200 + 业务结果,失败恒返业务码,不存在「空集合」形态; 无下游 Feign/消息依赖,故也没有降级分支。前端按 code == 200 判成功、否则 toast message 即可。

错误响应

{ "code": 581064, "message": "无权修改该订单", "success": false, "data": null }

业务边界

  • 这是四个写端点里后果最重的一个:全量替换按 diff 清掉不在入参里的标签。传空 tags 数组 = 一次性清空该单全部标签。
  • 鉴权:同接口 1;归属守卫排在 diff 之前(防御性,不做无谓写、不依赖事务回滚兜底)。
  • 事务:@Transactional(rollbackFor = Exception.class),diff 失败整笔回滚。
  • 失败零写入:被拒时 diff 一步都不执行。

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

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

✅ 正确 / ❌ 错误 payload 对照

场景 payload
✅ 只改名不改色 { "tagName": "高净值" }
✅ 只改色不改名 { "tagColor": "#FF6B6B" }
✅ 全量替换为空标签 { "tags": [] }(有权者:清空该单标签)
❌ 全量替换传 null { "tags": null } → 参数校验失败
❌ 颜色少 # 或非 6 位 { "tagColor": "FF6B6B" } → 581404
❌ 非归属人写他人订单 任意上述 payload → 581064

切换状态时的必要动作

无字段互斥关系;PATCH 是「只改传入字段」语义,不需要先读回再整对象提交。若要整批调整,用 PUT .../tags 传全量列表(空数组即清空)。


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

表 order_tag(继承 BaseDO,含 deleted_at 逻辑删除列)。

操作 SQL 效果
POST 增标签 INSERT INTO order_tag (tag_id, order_id, tag_name, tag_color, creator, created_by, ...)
DELETE 删标签 软删 UPDATE order_tag SET deleted_at = NOW() WHERE tag_id = ?(BaseDO.deletedAt 带 @TableLogic(value = "NULL", delval = "NOW()"))
PATCH 改标签 UPDATE order_tag SET tag_name = ?, tag_color = ? ...(仅传入字段)
PUT 全量替换 差集软删 + 新增 INSERT + 颜色变更 UPDATE;命中 user_tag_library 的标签会 touchUsage 累加 used_count 并刷新 last_used_at

读面一致性:所有 selectByOrderId 查询被 @TableLogic 自动附加 deleted_at IS NULL,被软删的标签即刻从接口结果消失,但行仍留在库里(无业务恢复入口)。


六、边界行为

  • 未登录 → 401(网关拦截)
  • orderId 不存在 → POST / PUT 返 581401;DELETE / PATCH 本单起也返 581401(此前会走到 581410/581411)
  • tagId 不存在 → 581410;tagId 不属于该 orderId → 581411(DELETE)/ 581434(PATCH)
  • 同订单同名标签 → 581403(新增时)
  • 团期管理员 → 581008(#8154 的角色级只读,Controller 层先拒,与 groupBatchId 无关)
  • 房务管理员 / 房务组长 → 581045
  • 老数据兼容:存量标签的 created_by 若为空,PATCH 的第二道校验仍按操作人比对(既有行为,本单未改)

六.5、枚举 / 数据字典

本节不适用:四个接口无枚举入参或返回值(标签类型字段已由 #3941 移除)。


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

字段级对比

字段 改前 改后
请求体字段 — 无变化
响应体字段 — 无变化
失败错误码 581008(团期管理员)/ 581045(房务) 新增 581064(非归属人);581008 / 581045 保持
DELETE/PATCH 的 orderId 不存在 581410 / 581411(先查标签) 581401 订单不存在(先查订单)

行为级对比

行为 改前 改后
非归属角色写他人订单标签(POST / DELETE / PATCH / PUT) 放行 581064
非归属角色全量替换他人订单标签(PUT 空数组) 放行,会清空他人标签 581064,零写入
本单定制师写自己订单标签 放行 放行(不变)
ADMIN / SUPER_ADMIN / 车务管理员 放行 放行(不变)
团期管理员 581008 581008(不变)
房务管理员 / 房务组长 放行 581045

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

  • 是否破坏向后兼容:对合法调用方否(有权角色行为零变化);对越权调用是有意的行为收紧。
  • 前端是否必须同步上线:否。接口路径与结构未变,581064 走通用 err.message toast 即可;但要知悉该码的语义(是写权限,不是登录态问题)。
  • 前端 workaround 清理点:无。hl-ui 现按 err.message 透传,无按码分支可清理。

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

  • 仅影响:订单详情页「标签」区的四个写操作(增 / 删 / 改 / 全量替换)。
  • 零影响:
    • 标签的三个读接口(GET .../tags、GET .../tag-picker、GET .../itinerary-document)——本单未改
    • 创单链路写入标签(OrderCreateTransactionExecutor 同事务内 batchCreate,无「他人订单」概念)
    • 订单详情 / 行程 / 财务等其它读写面
    • user_tag_library 私人标签库的全部接口
    • 存量数据(不迁移;软删语义未变)
    • 其它服务(本改动不跨服务,无 Feign/事件/MQ 契约变化)

八、测试环境已验证

真实网关调用(https://api.test.1814.love,测试服部署 ★ab630a3f8):

GET  /v3/admin/order/9199000000000000002/tags   as CUSTOMIZER(非归属) → 581008 无权查看此订单 ✓
POST /v3/admin/order/9199000000000000002/tag    as CUSTOMIZER(非归属) → 581064 无权修改该订单 ✓
GET  /v3/admin/order/9199000000000000002/tags   as SUPER_ADMIN        → 200 复查确认无残留标签 ✓
PUT  /v3/admin/order/9199000000000000002/tags   as SUPER_ADMIN(原样回写)→ 200 ✓
GET  /v3/admin/order/2096701868536188930/tags   as CUSTOMIZER(本单定制师)→ 200 ✓
PUT  /v3/admin/order/2096701868536188930/tags   as CUSTOMIZER(本单定制师)→ 200 ✓
POST /v3/admin/order/9199000000000000002/tag    as GROUP_BATCH_MANAGER → 581008(#8154 角色守卫先拒) ✓

单元与门禁:定向 Tests run 129 / Failures 0 / Errors 0;ArchTest Tests run 26 / Failures 0 / Errors 0。


九、相关历史 PR(纠错 / 功能演进时必写)

PR Issue 说明 是否仍有效
#8290 #8170 标签读面补 assertOrderReadable,本单就是它显影出的写面缺口 ✅ 有效
本 PR #8307 #8292 标签写面补归属守卫 + 新写面码 581064 + 581433 文案订正 ✅ 最新

十、相关文档

关联 / 联系人

链接

联系人

  • 后端负责人: @wx