feat(collab): 新增订单打标签弹窗 tag-picker 聚合接口 + 批量替换标签接口(#3988)

这个提交包含在:
yaosutu 2026-06-18 17:17:09 +08:00
父节点 8ab53749fa
当前提交 2c04507372

查看文件

@ -0,0 +1,274 @@
# 订单打标签弹窗 — 标签选择器聚合 + 批量替换接口(管理后台)
- **日期**2026-06-18
- **端类型**:管理后台
- **Issue**[#3982](https://git.1814.love:8443/wx/HL/issues/3982)
- **PR**[#3988](https://git.1814.love:8443/wx/HL/pulls/3988)
- **Commit**[b8403032e9](https://git.1814.love:8443/wx/HL/commit/b8403032e94f3c73d9e13982619c1cacdebf98ea)
- **服务**hl-order-service-v3端口 8086
- **后端负责人**:腰苏图
---
## ① 接口背景
订单打标签弹窗需要两个新接口支撑:
1. **标签选择器**tag-picker打开弹窗时,聚合返回「当前管理员的标签库全集 + 每条是否已挂本订单 + 订单上已有的游离标签」,供前端渲染勾选状态。
2. **批量全量替换标签**replace-tags弹窗点「确认」时,按入参列表全量替换订单标签diff 软删/新增/改色),同步更新标签库使用计数。
原有 4 个增量操作接口POST /tag 新增单条、DELETE /tag/:id 删单条、PATCH /tag/:id 改单条、GET /tags 查列表)**不变,不影响现有调用**。
---
## ② 变更清单
| 序号 | 变更类型 | 接口 | 说明 |
|------|---------|------|------|
| 1 | 新增接口 | `GET /v3/admin/order/{orderId}/tag-picker` | 打标签弹窗数据(标签库全集 + selected 标记 + 游离标签) |
| 2 | 新增接口 | `PUT /v3/admin/order/{orderId}/tags` | 批量全量替换订单标签 |
---
## ③ 接口详情
### 接口一:标签选择器
| 项目 | 内容 |
|------|------|
| 方法 | GET |
| 路径 | `/v3/admin/order/{orderId}/tag-picker` |
| 接口名 | 打标签弹窗数据(标签库全集 + selected 标记 + 游离标签) |
| 认证 | 需要 JWT`Authorization: Bearer <token>`,userId 从 `X-Admin-Id` 请求头派生,前端不传 |
| 幂等性 | 只读,天然幂等 |
| 限流 | 无特殊限流 |
### 接口二:批量全量替换标签
| 项目 | 内容 |
|------|------|
| 方法 | PUT |
| 路径 | `/v3/admin/order/{orderId}/tags` |
| 接口名 | 批量全量替换订单标签 |
| 认证 | 需要 JWT`Authorization: Bearer <token>` |
| 幂等性 | 非幂等diff 写入,重复调用以最后一次入参为准) |
| 限流 | 无特殊限流 |
---
## ④ 接口入参
### 接口一GET /v3/admin/order/{orderId}/tag-picker
#### 4.1 路径参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| orderId | Long | 是 | 订单 ID |
无请求体。
### 接口二PUT /v3/admin/order/{orderId}/tags
#### 4.1 路径参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| orderId | Long | 是 | 订单 ID |
#### 4.2 请求体字段
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| tags | List | 是(不可为 null | 标签列表;传空数组 [] 表示清空该订单所有标签 |
| tags[].tagName | String | 是 | 标签名,长度 1~50 字 |
| tags[].tagColor | String | 否 | HEX 色值,格式 #RRGGBB(如 #FF6B6B);不传则使用默认色 #5B8FF9 |
> **注意**tags=null 会触发 400 校验失败。清空全部标签请传 {"tags":[]}。
---
## ⑤ 出参字段
### 接口一GET /v3/admin/order/{orderId}/tag-picker
返回类型:`Result<List<TagPickerItemVO>>`
| 字段名 | 类型 | 说明 |
|--------|------|------|
| tagName | String | 标签名 |
| tagColor | String | HEX 色值,如 #5B8FF9 |
| tagScope | String | 作用域,见枚举说明;游离标签为 null |
| isPinned | Boolean | 是否置顶;游离标签为 false |
| sortOrder | Integer | 排序值;游离标签为 null |
| selected | Boolean | 是否已挂在当前订单;true 表示已挂 |
| inLibrary | Boolean | 是否来自标签库;false 表示游离标签(订单上存在但库中已删) |
列表顺序标签库条目优先is_pinned DESC → sort_order ASC → last_used_at DESC,游离标签追加末尾selected=true, inLibrary=false
### 接口二PUT /v3/admin/order/{orderId}/tags
返回类型:`Result<OrderTagListRespVO>`(替换后该订单的全量标签快照)
`OrderTagListRespVO` 字段:
| 字段名 | 类型 | 说明 |
|--------|------|------|
| attached | `List<OrderTagVO>` | 替换后该订单已挂标签列表 |
`OrderTagVO` 字段:
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | LongString 序列化) | tag 主键 |
| orderId | LongString 序列化) | 关联订单 ID |
| tagName | String | 标签名 |
| tagColor | String | 标签色值,如 #FF6B6B |
| creator | String | 创建人姓名(定制师姓名或 SYSTEM |
| createdAt | StringISO-8601 | 创建时间,如 2026-06-18T10:30:00 |
---
## ⑥ 枚举 / 数据字典
### tagScope标签作用域
| 值 | 含义 |
|----|------|
| SYSTEM | 系统标签(全公司共享) |
| PERSONAL | 个人标签(仅创建人可见) |
| null | 游离标签(订单上存在但已从标签库中删除,已无作用域归属) |
---
## ⑦ 错误码
以下为两个新接口涉及的错误码:
| 错误码 | 含义 | 触发场景 |
|--------|------|---------|
| 581401 | 订单不存在 | orderId 在数据库中不存在(两个接口均会触发) |
| 581402 | 标签名为空或超长≤50 字) | tagName 空白或超过 50 字(替换接口) |
| 581404 | tagColor 格式非法(必须 # + 6 位十六进制) | tagColor 传了非 #RRGGBB 格式值(替换接口) |
其他已有错误码不变581403 同名私人标签已存在POST /tag 新增时触发,本次两个新接口不触发)。
---
## ⑧ 示例
### 8.1 典型成功
场景:打开打标签弹窗,标签库有 2 条,订单已挂其中 1 条 + 1 条游离标签。
tag-picker 请求GET /v3/admin/order/1234567890123456789/tag-picker
响应3 条1 条已选库标签、1 条未选库标签、1 条游离标签):
```json
{"code":200,"data":[
{"tagName":"VIP客户","tagColor":"#5B8FF9","tagScope":"SYSTEM","isPinned":true,"sortOrder":1,"selected":true,"inLibrary":true},
{"tagName":"需要接送","tagColor":"#FF6B6B","tagScope":"PERSONAL","isPinned":false,"sortOrder":10,"selected":false,"inLibrary":true},
{"tagName":"旧标签已删","tagColor":"#AAAAAA","tagScope":null,"isPinned":false,"sortOrder":null,"selected":true,"inLibrary":false}
]}
```
批量替换标签请求PUT /v3/admin/order/1234567890123456789/tags
请求体:
```json
{"tags":[{"tagName":"VIP客户","tagColor":"#5B8FF9"},{"tagName":"需要接送","tagColor":"#FF6B6B"}]}
```
响应:
```json
{"code":200,"data":{"attached":[
{"id":"1881234567890000001","orderId":"1234567890123456789","tagName":"VIP客户","tagColor":"#5B8FF9","creator":"张三","createdAt":"2026-06-18T10:30:00"},
{"id":"1881234567890000002","orderId":"1234567890123456789","tagName":"需要接送","tagColor":"#FF6B6B","creator":"张三","createdAt":"2026-06-18T10:30:01"}
]}}
```
### 8.2 边界情况
清空全部标签,请求体 {"tags":[]}, 响应 {"code":200,"data":{"attached":[]}}
tag-picker 返回空列表(标签库为空且订单未挂标签):{"code":200,"data":[]}
### 8.3 业务失败
场景一订单不存在581401
请求GET /v3/admin/order/9999999999999999999/tag-picker
响应:{"code":581401,"msg":"订单不存在"}
场景二标签名为空581402
最小复现请求体:{"tags":[{"tagName":""}]}
响应:{"code":581402,"msg":"标签名为空或超长≤50 字)"}
场景三tagColor 格式非法581404
最小复现请求体:{"tags":[{"tagName":"VIP","tagColor":"red"}]}
响应:{"code":581404,"msg":"tagColor 格式非法(必须 # + 6 位十六进制)"}
场景四tags=null 触发 400
请求体:{"tags":null}
响应:{"code":400,"msg":"tags 不能为 null清空请传空数组 [])"}
---
## ⑨ 业务边界
适用场景:
- GET tag-picker打开打标签弹窗时调用。任意订单状态均可调用。
- PUT tags弹窗点「确认」时调用,全量替换不是追加。任意订单状态均可调用。
不适用场景:
- 两个接口均不做订单状态限制。
- 公司隔离由后端 JWT 校验,前端不传公司 ID。
特殊边界:
- PUT tags 是全量替换语义:后端自动 diff删除不在入参里的旧标签、新增入参里新出现的标签、更新颜色变化的标签
- 游离标签inLibrary=false在 PUT tags 入参中可通过 tagName 匹配保留,也可不传从而删除。
- 标签库使用计数used_count / last_used_atPUT tags 操作后,命中标签库的标签会自动更新使用计数,无需前端额外操作。
- tagColor 不传时,新增标签默认色为 #5B8FF9;已有标签改色时以入参为准。
---
## ⑩ 修改前后对比
本次为纯新增接口,无历史接口修改,无对比项。
原有 4 个增量操作接口完全不变:
| 接口 | 状态 |
|------|------|
| POST /v3/admin/order/{orderId}/tag | 不变 |
| DELETE /v3/admin/order/{orderId}/tag/{tagId} | 不变 |
| PATCH /v3/admin/order/{orderId}/tag/{tagId} | 不变 |
| GET /v3/admin/order/{orderId}/tags | 不变 |
---
## ⑪ 影响评估 / 回滚
- **破坏兼容性**:无(纯新增,不影响现有接口)
- **前端同步上线**:无强制要求,新接口不上线不会影响现有功能
- **回滚方案**:若需回滚,仅需回滚 hl-order-service-v3 至本 PR 前版本;无 DDL 变更,数据库无需操作
---
## ⑫ 注意事项
1. PUT tags 是全量替换,不是追加:每次调用以入参为最终状态全量覆盖,请前端在弹窗「确认」时一次性传完整列表。
2. tags=null 触发 400清空标签请传 {"tags":[]}, 不要传 {"tags":null}。
3. Long 类型 ID 用字符串序列化OrderTagVO.id 和 OrderTagVO.orderId 均为雪花 ID,JSON 序列化为 String,前端 JS 用字符串处理,不要转 Number。
4. tag-picker 中游离标签排在末尾inLibrary=false 的条目追加末尾,前端可据此做分组显示或特殊标注。
5. 颜色默认值tagColor 不传时后端默认 #5B8FF9,前端如需展示颜色预览可读此默认值。
6. 零 DDL本次无数据库结构变更,order-v3 重启后即生效。
---
## ⑬ 关联 / 联系人
- **Issue**https://git.1814.love:8443/wx/HL/issues/3982
- **PR**https://git.1814.love:8443/wx/HL/pulls/3988
- **Commit**https://git.1814.love:8443/wx/HL/commit/b8403032e94f3c73d9e13982619c1cacdebf98ea
- **后端负责人**:腰苏图