diff --git a/changelogs-v2/2026-05/19_2045_tag-library模块私人标签库管理-新增接口-管理后台.md b/changelogs-v2/2026-05/19_2045_tag-library模块私人标签库管理-新增接口-管理后台.md new file mode 100644 index 0000000..b4da421 --- /dev/null +++ b/changelogs-v2/2026-05/19_2045_tag-library模块私人标签库管理-新增接口-管理后台.md @@ -0,0 +1,500 @@ +# tag-library: 私人标签库管理 6 接口(v5.14 骨架) + +> **存放目录**: `changelogs-v2/2026-05/`(v3 二期) +> **服务**: hl-order-service-v3(端口 8086) +> **PR**: #2045(骨架合并)/ #2240(Gateway 路由修复) +> **Issue**: #2045(v5.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>` + +#### 入参(Query) + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| keyword | Query | String | ❌ | - | 模糊搜索标签名(管理页搜索框) | +| pinnedOnly | Query | Boolean | ❌ | 默认 false | 只返置顶 | + +> userId 从 JWT 自动注入,前端**不要**在 Query 里传 userId。 + +#### 出参 `Result>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| 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` + +#### 入参(Body) + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| tagName | Body | String | ✅ | 非空,长度 ≤ 50 | 标签名 | +| tagColor | Body | String | ❌ | HEX 格式 | 默认 `#5B8FF9` | + +#### 出参 `Result` + +字段同接口 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` + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| 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` + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| 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` + +#### 入参(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` + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| 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://: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 标注)