docs(tag-library): 新增 v5.14 私人标签库 6 接口契约(管理后台)

- GET    /v3/admin/tag-library              查我的标签库(keyword/pinnedOnly)
- POST   /v3/admin/tag-library              新增预设(tagName/tagColor)
- PUT    /v3/admin/tag-library/{id}         修改预设(重命名/换色)
- DELETE /v3/admin/tag-library/{id}         软删预设
- PUT    /v3/admin/tag-library/sort         批量排序(拖拽)
- PUT    /v3/admin/tag-library/{id}/pin     置顶/取消置顶 toggle

错误码段位 581420-581429(hl-order-service-v3 拥有)。
当前 ServiceImpl 为 Mock 实现,契约本次发布后不再变。
关联 PR:wx/HL#2045(骨架)+ wx/HL#2240(Gateway 路由修复)
这个提交包含在:
yaosutu 2026-05-19 14:32:04 +08:00
父节点 a47588b9a4
当前提交 0bbcdd23e7

查看文件

@ -0,0 +1,500 @@
# tag-library: 私人标签库管理 6 接口v5.14 骨架)
> **存放目录**: `changelogs-v2/2026-05/`v3 二期)
> **服务**: hl-order-service-v3端口 8086
> **PR**: #2045(骨架合并)/ #2240Gateway 路由修复)
> **Issue**: #2045v5.14 §14 章节实现)
> **日期**: 2026-05-19
> **影响范围**: 管理后台「我的标签库」管理页 + 创建订单时的标签下拉
---
## ⚠️ 关键变化(首次发布无)
本次为首次发布;非纠错变更。
⚠️ **当前实现状态**6 个接口已上线,**ServiceImpl 为 Mock 实现**(返回硬编码样例数据),真实 DB 业务在跟进中。**接口契约本次发布后不再变**,前端可按契约对接联调。
---
## 一、背景
v5.14 引入「私人标签库」概念:每个管理员账号有自己独立的标签预设库,可在订单创建/编辑页打标签时从下拉中选取(取代以前自由文本输入)。
**模型隔离**
- userId 从 JWT 自动派生,前端**不传**操作人
- 仅本人可读/写/排序/置顶/删除自己的库
- 删除标签库预设**不**影响已挂订单上的历史 order_tag 实例(保留快照)
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|----------|
| 1 | 查我的标签库 | GET | `/v3/admin/tag-library` | 新增 |
| 2 | 新增标签库预设 | POST | `/v3/admin/tag-library` | 新增 |
| 3 | 修改标签库预设(重命名/换色) | PUT | `/v3/admin/tag-library/{id}` | 新增 |
| 4 | 删除标签库预设(软删) | DELETE | `/v3/admin/tag-library/{id}` | 新增 |
| 5 | 批量排序标签库(拖拽) | PUT | `/v3/admin/tag-library/sort` | 新增 |
| 6 | 置顶/取消置顶toggle | PUT | `/v3/admin/tag-library/{id}/pin` | 新增 |
---
## 三、接口详情
### 1. 查我的标签库 `GET /v3/admin/tag-library`
**VO**: `TagLibraryListReqVO``Result<List<TagLibraryItemVO>>`
#### 入参Query
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| keyword | Query | String | ❌ | - | 模糊搜索标签名(管理页搜索框) |
| pinnedOnly | Query | Boolean | ❌ | 默认 false | 只返置顶 |
> userId 从 JWT 自动注入,前端**不要**在 Query 里传 userId。
#### 出参 `Result<List<TagLibraryItemVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 标签库行 ID雪花,前端按字符串处理避免精度丢失 |
| tagName | String | 标签名 |
| tagColor | String | HEX 色值(如 `#5B8FF9` |
| sortOrder | Integer | 排序值 |
| isPinned | Boolean | 是否置顶 |
| usedCount | Integer | 使用次数 |
| lastUsedAt | LocalDateTime | 最近使用时间ISO 8601,可能为 null |
**列表排序**`isPinned DESC, sortOrder ASC, lastUsedAt DESC`(后端固定,前端不要前端再排)
#### 请求示例
```
GET /v3/admin/tag-library?keyword=高&pinnedOnly=false
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": [
{
"id": 2050000000000001001,
"tagName": "高净值客户",
"tagColor": "#FF6B6B",
"sortOrder": 1,
"isPinned": true,
"usedCount": 12,
"lastUsedAt": "2026-05-15T14:32:10"
},
{
"id": 2050000000000001002,
"tagName": "高频复购",
"tagColor": "#5B8FF9",
"sortOrder": 2,
"isPinned": false,
"usedCount": 5,
"lastUsedAt": "2026-05-10T09:15:00"
}
],
"success": true
}
```
#### 空数据响应(首次访问无库)
```json
{
"code": 200,
"message": "成功",
"data": [],
"success": true
}
```
---
### 2. 新增标签库预设 `POST /v3/admin/tag-library`
**VO**: `TagLibraryCreateReqVO``Result<TagLibraryItemVO>`
#### 入参Body
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| tagName | Body | String | ✅ | 非空,长度 ≤ 50 | 标签名 |
| tagColor | Body | String | ❌ | HEX 格式 | 默认 `#5B8FF9` |
#### 出参 `Result<TagLibraryItemVO>`
字段同接口 1 出参(返回新建行完整字段,`sortOrder` 追加到末尾,`isPinned=false``usedCount=0``lastUsedAt=null`)。
#### 请求示例
```json
{
"tagName": "VIP",
"tagColor": "#FFD700"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": 2050000000000001005,
"tagName": "VIP",
"tagColor": "#FFD700",
"sortOrder": 6,
"isPinned": false,
"usedCount": 0,
"lastUsedAt": null
},
"success": true
}
```
#### 错误响应(重名)
```json
{
"code": 581421,
"message": "标签名已存在",
"success": false,
"data": null
}
```
---
### 3. 修改标签库预设 `PUT /v3/admin/tag-library/{id}`
**VO**: `TagLibraryUpdateReqVO``Result<TagLibraryItemVO>`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 标签库行 ID |
| tagName | Body | String | ❌ | 长度 ≤ 50 | 不传则不改 |
| tagColor | Body | String | ❌ | HEX | 不传则不改 |
> 两字段都不传 → 等同空操作(不报错)。
#### 出参
返回修改后的完整 `TagLibraryItemVO`(字段同接口 1
#### 请求示例
```json
{
"tagName": "VIP-Gold",
"tagColor": "#FFA500"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": 2050000000000001005,
"tagName": "VIP-Gold",
"tagColor": "#FFA500",
"sortOrder": 6,
"isPinned": false,
"usedCount": 0,
"lastUsedAt": null
},
"success": true
}
```
#### 错误响应(非本人标签)
```json
{
"code": 581423,
"message": "无权操作该标签",
"success": false,
"data": null
}
```
---
### 4. 删除标签库预设 `DELETE /v3/admin/tag-library/{id}`
**VO**: - → `Result<Boolean>`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 标签库行 ID |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| data | Boolean | true=删除成功 |
#### 请求示例
```
DELETE /v3/admin/tag-library/2050000000000001005
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": true,
"success": true
}
```
#### 错误响应id 不存在)
```json
{
"code": 581424,
"message": "标签不存在或已删除",
"success": false,
"data": null
}
```
---
### 5. 批量排序标签库 `PUT /v3/admin/tag-library/sort`
**VO**: `TagLibrarySortReqVO``Result<Boolean>`
#### 入参Body
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| items | Body | Array | ✅ | 非空 | 批量排序映射 |
| items[].id | - | Long | ✅ | - | 标签库行 ID |
| items[].sortOrder | - | Integer | ✅ | - | 新排序值 |
> 仅会更新本人库的行;若 `items` 含非本人 id,**整批拒绝**581425,不部分成功。
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| data | Boolean | true=全量成功 |
#### 请求示例
```json
{
"items": [
{ "id": 2050000000000001001, "sortOrder": 1 },
{ "id": 2050000000000001002, "sortOrder": 2 },
{ "id": 2050000000000001005, "sortOrder": 3 }
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": true,
"success": true
}
```
#### 错误响应(混入他人 id
```json
{
"code": 581425,
"message": "排序列表含非本人标签 ID",
"success": false,
"data": null
}
```
---
### 6. 置顶/取消置顶 `PUT /v3/admin/tag-library/{id}/pin`
**VO**: `TagLibraryPinReqVO``Result<Boolean>`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 标签库行 ID |
| pinned | Body | Boolean | ✅ | - | `true`=置顶 / `false`=取消置顶 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| data | Boolean | true=操作成功 |
#### 请求示例
```json
{
"pinned": true
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": true,
"success": true
}
```
#### 错误响应(非本人)
```json
{
"code": 581423,
"message": "无权操作该标签",
"success": false,
"data": null
}
```
---
## 四、契约约束与正确调用方式
### userId 派生规则
| 场景 | payload |
|------|---------|
| ✅ 调用任一接口 | 仅传业务字段,不传 userId后端从 JWT 取) |
| ❌ Body / Query 传 userId | 后端忽略(不报错但无效) |
### 排序接口幂等性
| 场景 | 行为 |
|------|------|
| ✅ 整列重新提交全量 items | 全量覆盖排序 |
| ✅ 提交部分 items | 仅更新提交的行,其他行 sort_order 保持 |
| ❌ items 含他人 id | 581425 整批拒绝(不部分成功) |
### tagColor 兜底
- POST 不传 `tagColor` → 写入 `#5B8FF9`(深蓝默认)
- PUT 不传 `tagColor` → 不改原值
---
## 五、数据库行为
| 前端动作 | 数据库影响 |
|----------|-----------|
| POST 新建 | `tag_library` 表插入一行,`user_id` 取 JWT,`sort_order = MAX(sort_order)+1``is_pinned=false``used_count=0` |
| PUT 修改 | 仅更新 `tag_name` / `tag_color`;不传字段不动 |
| DELETE | 软删(`deleted=1`),不物理删;不级联清理已挂订单的历史 `order_tag` |
| PUT sort | 批量 UPDATE `sort_order`,事务包裹(全成功或全回滚) |
| PUT pin | 仅更新 `is_pinned``updated_at` 同步刷新 |
**软删与订单标签的关系**
| 时间线 | 状态 |
|--------|------|
| T0管理员将"VIP"标签挂到订单 A | `order_tag` 表插入快照tag_name="VIP", tag_color="#FFD700" |
| T1管理员从标签库删除"VIP"预设 | `tag_library.deleted=1``order_tag` 表**不变** |
| T2再次打开订单 A | 仍显示"VIP"标签(快照),但下拉里不再出现 |
---
## 六、边界行为
- 未登录 → 401网关拦截
- 列表为空(首次访问/全部删完)→ `Result.success([])`,**不**返 404 / 不返 581420
- DELETE/PUT 操作不存在的 id → 581424
- DELETE/PUT 操作他人 id → 581423
- 排序提交空 items 数组 → 400`@NotEmpty`
- 新增标签名同用户已存在 → 581421
- 新增标签名超长 / 空 / 全空格 → 400 / 581422
---
## 七、不影响范围
- **仅影响**: 管理后台「我的标签库」管理页 + 订单页打标签下拉
- **零影响**:
- C 端小程序(无 /mp 接口)
- 订单创建 / 编辑接口(订单上的 `order_tag` 写入是另外的接口链路,不在本批)
- 历史已挂订单的标签(快照不动)
- 其他管理员的标签库(用户隔离)
---
## 八、测试环境已验证
⚠️ **当前实现为 Mock**ServiceImpl 返回硬编码样例数据,未走 DB。前端可按本契约对接联调;联调通过后后端会接入真实 DB**契约不再变**)。
```
GET /v3/admin/tag-library → 200 + 5 条 Mock 样例 ✓
POST /v3/admin/tag-library → 200 + 新增样例回显 ✓
PUT /v3/admin/tag-library/{id} → 200 + 修改样例回显 ✓
DELETE /v3/admin/tag-library/{id} → 200 + true ✓
PUT /v3/admin/tag-library/sort → 200 + true ✓
PUT /v3/admin/tag-library/{id}/pin → 200 + true ✓
```
Swagger UI`http://<host>:8086/doc.html` → 找「[admin] 私人标签库管理v5.14)」分组。
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #2045 | - | 首发6 接口空骨架 + Mock ServiceImpl + 错误码段位 581420-581429 | ✅ 有效 |
| #2240 | - | 修复 Gateway 缺 `/v3/admin/tag-library/**` 路由404 修复) | ✅ 有效 |
---
## 十、错误码
| 错误码 | 含义 | 触发场景 |
|--------|------|---------|
| 400 | 参数校验失败 | tagName 空 / 超长 / items 空 |
| 401 | 未登录 | 无 JWT 或 JWT 过期 |
| 581420 | 当前用户暂无标签库 | 保留位(当前实现不抛此错,空库返 `[]` |
| 581421 | 标签名已存在 | POST 时同用户已有同名标签 |
| 581422 | 标签名格式非法或长度超过 50 字符 | 含特殊字符 / 长度 > 50兜底校验 |
| 581423 | 无权操作该标签 | PUT/DELETE/PIN 操作他人的 id |
| 581424 | 标签不存在或已删除 | id 不存在 / 已软删 |
| 581425 | 排序列表含非本人标签 ID | sort 接口 items 混入他人 id |
---
## 十一、相关文档
- 关联 PR骨架[wx/HL#2045](https://git.1814.love:8443/wx/HL/pulls/2045)
- 关联 PR路由修复[wx/HL#2240](https://git.1814.love:8443/wx/HL/pulls/2240)
- 后续:真实 DB 业务实现 PR 合并后将追加一条 follow-up changelog契约不变,仅去掉 Mock 标注)