# 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<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 标注）
