# 游玩项目（活动）+ 备品（物资）新增「是否收费」字段

- **端类型**：管理后台
- **变更类型**：修改接口
- **日期**：2026-06-01
- **Issue**：[#3331](https://git.1814.love:8443/wx/HL/issues/3331)
- **PR**：[#3334](https://git.1814.love:8443/wx/HL/pulls/3334)
- **Commit**：[5da05a3](https://git.1814.love:8443/wx/HL/commit/5da05a324813cf8109d35c521c5e7bb9719627be)
- **后端负责人**：腰苏图（yst@1814.love）

---

## ① 接口背景

游玩项目（活动）和备品（物资）两个资源域同步新增「是否收费」属性，用于在管理后台创建/编辑时标记该资源是否需要游客付费。该字段同步透出到各自的列表和详情接口，便于运营人员在列表页快速识别收费/免费资源。字典来源 `sys_yes_no`，后端已配置，前端无需额外配置。

---

## ② 变更清单

### 游玩项目（ActivityController `/admin/activity`）

| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 创建游玩项目 | POST | `/admin/activity/item` | 入参新增 `isCharged`（Integer，默认 0） |
| 更新游玩项目 | PUT | `/admin/activity/item/{activityId}` | 入参新增 `isCharged`（Integer，可选） |
| 游玩项目详情 | GET | `/admin/activity/item/{activityId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 游玩项目列表 | GET | `/admin/activity/items` | 出参新增 `isCharged` + `isChargedLabel` |

### 备品（SuppliesController `/admin/supplies`）

| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 创建备品 | POST | `/admin/supplies/item` | 入参新增 `isCharged`（Integer，默认 0） |
| 更新备品 | PUT | `/admin/supplies/item/{suppliesId}` | 入参新增 `isCharged`（Integer，可选） |
| 备品详情 | GET | `/admin/supplies/item/{suppliesId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 备品列表 | GET | `/admin/supplies/items` | 出参新增 `isCharged` + `isChargedLabel` |

所有变更均为**向后兼容新增字段**，不涉及字段改名或删除。

---

## ③ 接口详情

| 项目 | 说明 |
|------|------|
| **认证方式** | 管理后台 JWT Token，请求头 `Authorization: Bearer {token}` |
| **幂等性** | POST 创建非幂等；PUT 更新幂等（相同 activityId / suppliesId 可重复调用） |
| **限流** | 无单独限流配置，走网关全局限流 |
| **权限** | 管理员角色，普通前端用户无访问权限 |

---

## ④ 接口入参

### 4.1 路径参数 / Query 参数

**PUT `/admin/activity/item/{activityId}`**

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| activityId | Long | 是 | 游玩项目 ID（路径参数） |

**PUT `/admin/supplies/item/{suppliesId}`**

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| suppliesId | Long | 是 | 备品 ID（路径参数） |

**GET `/admin/activity/items`** 和 **GET `/admin/supplies/items`**（列表，原有 Query 参数不变，无新增查询参数）

### 4.2 请求体字段（新增字段）

**POST `/admin/activity/item`**（`ActivityCreateRequest`，新增字段）

| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | 0 | 是否收费，1=是 0=否 |

**PUT `/admin/activity/item/{activityId}`**（`ActivityUpdateRequest`，新增字段）

| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | null（不传则不修改） | 是否收费，1=是 0=否 |

**POST `/admin/supplies/item`**（`SuppliesCreateRequest`，新增字段）

| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | 0 | 是否收费，1=是 0=否 |

**PUT `/admin/supplies/item/{suppliesId}`**（`SuppliesUpdateRequest`，新增字段）

| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | null（不传则不修改） | 是否收费，1=是 0=否 |

---

## ⑤ 出参字段

### GET `/admin/activity/item/{activityId}` 详情（`ActivityVO`，新增字段）

| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费，1=是 0=否 | 否（DB 默认 0） |
| isChargedLabel | String | 是否收费中文文本，取自字典 sys_yes_no（"是"/"否"） | 是（字典缺失时为 null） |

### GET `/admin/activity/items` 列表（`ActivityListVO`，新增字段）

| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费，1=是 0=否 | 否（DB 默认 0） |
| isChargedLabel | String | 是否收费中文文本，取自字典 sys_yes_no（"是"/"否"） | 是（字典缺失时为 null） |

### GET `/admin/supplies/item/{suppliesId}` 详情（`SuppliesVO`，新增字段）

| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费，1=是 0=否 | 否（DB 默认 0） |
| isChargedLabel | String | 是否收费中文文本，取自字典 sys_yes_no（"是"/"否"） | 是（字典缺失时为 null） |

### GET `/admin/supplies/items` 列表（`SuppliesListVO`，新增字段）

| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费，1=是 0=否 | 否（DB 默认 0） |
| isChargedLabel | String | 是否收费中文文本，取自字典 sys_yes_no（"是"/"否"） | 是（字典缺失时为 null） |

---

## ⑥ 枚举 / 数据字典

### sys_yes_no 字典（后端已配置，前端无需维护）

| 字典值（isCharged） | 中文标签（isChargedLabel） | 说明 |
|--------------------|-----------------------------|------|
| 1 | 是 | 该资源收费 |
| 0 | 否 | 该资源免费 |

> `isChargedLabel` 由后端查字典自动翻译，前端可直接展示。若字典配置缺失（不正常情况），该字段返回 null。

---

## ⑦ 错误码

| 错误码 | HTTP 状态 | 场景 |
|--------|-----------|------|
| 400 | 400 | `isCharged` 传入非 0/1 的值（如 2、-1），Bean Validation 失败，msg="是否收费取值范围为0或1" |
| 401 | 401 | 未登录或 Token 失效 |
| 403 | 403 | 无管理员权限 |
| 404 | 404 | 资源 ID 不存在或已删除 |

---

## ⑧ 示例

### 8.1 典型成功 — 创建收费游玩项目

**请求**
```
POST /admin/activity/item
Authorization: Bearer {admin_token}
Content-Type: application/json

{
  "name": "高原骑马体验",
  "categoryCode": "OUTDOOR",
  "isCharged": 1
}
```

**响应**
```json
{
  "code": 200,
  "msg": "成功",
  "data": {
    "activityId": "1929000000000001",
    "name": "高原骑马体验",
    "categoryCode": "OUTDOOR",
    "isCharged": 1,
    "isChargedLabel": "是",
    "status": 0
  }
}
```

### 8.2 边界情况 — 创建备品时不传 isCharged（默认 0）

**请求**
```
POST /admin/supplies/item
Authorization: Bearer {admin_token}
Content-Type: application/json

{
  "name": "登山杖",
  "categoryCode": "OUTDOOR_GEAR",
  "billingType": "PER_ITEM",
  "basePrice": 25.00
}
```

**响应**（isCharged 默认为 0）
```json
{
  "code": 200,
  "msg": "成功",
  "data": {
    "suppliesId": "1929000000000002",
    "name": "登山杖",
    "billingType": "PER_ITEM",
    "isCharged": 0,
    "isChargedLabel": "否",
    "status": 0
  }
}
```

### 8.3 业务失败 — isCharged 传入越界值

**请求**
```
PUT /admin/activity/item/1929000000000001
Authorization: Bearer {admin_token}
Content-Type: application/json

{
  "name": "高原骑马体验",
  "isCharged": 2
}
```

**响应**（400 参数校验失败）
```json
{
  "code": 400,
  "msg": "是否收费取值范围为0或1",
  "data": null
}
```

---

## ⑨ 业务边界

**适用场景**：
- 创建/编辑游玩项目或备品时，可设置是否收费标记（不传默认不收费）
- 列表和详情页均展示该字段，供运营人员快速识别资源收费属性

**不适用场景**：
- 该字段仅用于标记属性，不影响产品定价逻辑（定价由费用项管理）
- 不用于权限控制，仅作展示属性

**特殊边界**：
- 历史存量游玩项目和备品（PR #3334 上线前创建的）`isCharged` 默认为 0，`isChargedLabel` 返回 "否"
- PUT 更新时若不传 `isCharged` 字段（null），该字段值保持不变（不会被清零），区别于传 `0`（明确设置为否）
- `isChargedLabel` 为 null 时属于字典配置异常，非正常情况

---

## ⑩ 修改前后对比

### 字段级对比

**入参（POST 创建 / PUT 更新）—— 游玩项目和备品相同**

| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增，Integer，可选，默认 0（Create），null 不修改（Update） |

**出参（详情 + 列表）—— 游玩项目和备品相同**

| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增，Integer，1=是/0=否 |
| isChargedLabel | 不存在 | ✨ 新增，String，字典翻译文本，可能为 null |

### 行为级对比

| 行为 | 变更前 | 变更后 |
|------|--------|--------|
| 创建游玩项目 / 备品 | 无收费标记 | 可设置 isCharged，默认 0 |
| 列表展示 | 无收费信息 | 新增 isCharged + isChargedLabel |
| 详情展示 | 无收费信息 | 新增 isCharged + isChargedLabel |

---

## ⑪ 影响评估 / 回滚

**破坏兼容性**：无，新增字段均为可选，现有调用代码无需修改即可正常运行。

**前端同步上线**：
- 创建/编辑表单：可选接入 `isCharged` 开关/选择器，不接入则默认提交 0（不收费）
- 列表/详情页：可选展示 `isChargedLabel`，不展示也不影响功能

**回滚方案**：
- 若需回滚，分别执行以下 DDL 并回退服务部署：
  - `ALTER TABLE activity DROP COLUMN is_charged;`
  - `ALTER TABLE supplies_item DROP COLUMN is_charged;`
- 前端零改动时回滚对前端无感

---

## ⑫ 注意事项

1. `isChargedLabel` 依赖字典 `sys_yes_no` 配置，后端已配置好，前端**不需要**维护字典表
2. 列表接口返回 `isChargedLabel` 纯粹为展示便利，前端也可用 `isCharged` 值自行映射（1→"是"，0→"否"）
3. PUT 更新时 `isCharged` 不传（null）表示不修改，区别于传 `0`（明确设置为否）
4. 游玩项目和备品两个域的字段命名、类型、字典完全一致，调用逻辑相同
5. 历史数据库存量记录 `is_charged` 列已通过 DDL 添加默认值 0，无历史数据问题

---

## ⑬ 关联 / 联系人

- **Issue**：[https://git.1814.love:8443/wx/HL/issues/3331](https://git.1814.love:8443/wx/HL/issues/3331)
- **PR**：[https://git.1814.love:8443/wx/HL/pulls/3334](https://git.1814.love:8443/wx/HL/pulls/3334)
- **Commit**：[https://git.1814.love:8443/wx/HL/commit/5da05a324813cf8109d35c521c5e7bb9719627be](https://git.1814.love:8443/wx/HL/commit/5da05a324813cf8109d35c521c5e7bb9719627be)
- **后端负责人**：腰苏图（企微 / yst@1814.love）
