18 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 | 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。
- 空值:
tagNametrim 后为空 → 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.messagetoast 即可;但要知悉该码的语义(是写权限,不是登录态问题)。 - 前端 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 文案订正 | ✅ 最新 |
十、相关文档
- 关联 Issue: wx/HL#8292
- 关联 PR: wx/HL#8307
- 后续计划: 无
关联 / 联系人
链接
联系人
- 后端负责人: @wx