# API v2 成本模块接口文档

本文档面向前端，描述 `cost` 成本模块的 API。


## 2026-06-30 前端变更摘要

本次支出明细接口兼容新增“倍数型支出”字段。支出类目接口无变化，按类目汇总接口的响应结构无变化，但汇总金额仍来自 `amount`。

**新增字段（支出明细列表、详情、创建、修改均涉及）：**

| 字段 | 类型 | 位置 | 说明 |
|------|------|------|------|
| `unit_amount` | decimal string / null | request + response | 单价 / 基数金额，如临时工日薪。响应中为字符串，如 `"200.0000"` |
| `quantity` | decimal string / null | request + response | 数量 / 倍数，如 `"3.0000"` |
| `unit_name` | string | request + response | 单位名称，如 `人天`、`小时`、`件`，可为空字符串 |

**兼容规则：**

- 普通支出：继续传 `amount`，不传 `unit_amount`、`quantity` 即可。
- 倍数型支出：传 `unit_amount + quantity`，`amount` 可不传；后端保存时计算 `amount = unit_amount * quantity`。
- 如果同时传 `amount` 和 `unit_amount + quantity`，后端以公式计算结果为准，返回的 `amount` 是计算后的最终金额。
- `unit_amount` 和 `quantity` 必须同时填写或同时为 `null`/不传；只传一个会返回 `400`。
- 将倍数型支出改回普通支出时，PUT 需要同时传：`amount`、`unit_amount: null`、`quantity: null`，`unit_name` 可传空字符串。

---

## 基本约定

- Base URL: `/api/v2`
- 认证：所有接口都需要登录（JWT）。
- 人员身份：后端使用 `request.user.employee` 作为当前员工身份。
- 多商户隔离：所有类目、支出明细只允许访问当前员工所属商户的数据；可通过 `?merchant_id=` 参数跨商户查询（如果权限允许）。
- 时间格式：请求参数中的日期使用 `YYYY-MM-DD`（如 `2026-06-01`），响应中的日期时间使用 ISO 8601。

---

## 1. 支出类目

### 1.1 类目列表

```
GET /api/v2/cost-categories/
```

查询参数：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `merchant_id` | int | 否 | 按商户筛选，不传则使用当前员工的商户 |

成功响应 `200`：

```json
[
  {
    "id": 1,
    "merchant_id": 1,
    "unique_key": "electricity",
    "name": "电费",
    "parent_id": null,
    "description": "每月电费支出",
    "created_at": "2026-06-06T10:00:00Z",
    "updated_at": "2026-06-06T10:00:00Z"
  }
]
```

### 1.2 创建类目

```
POST /api/v2/cost-categories/
```

请求体（JSON）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `unique_key` | string | **是** | 唯一标识键，同商户不可重复，如 `electricity` |
| `name` | string | **是** | 类目名称 |
| `parent_id` | int | 否 | 父类目 ID，用于层级 |
| `description` | string | 否 | 描述 |

请求示例：

```json
{
  "unique_key": "electricity",
  "name": "电费",
  "description": "每月电费支出"
}
```

成功响应 `201`：

```json
{
  "id": 1,
  "merchant_id": 1,
  "unique_key": "electricity",
  "name": "电费",
  "parent_id": null,
  "description": "每月电费支出",
  "created_at": "2026-06-06T10:00:00Z",
  "updated_at": "2026-06-06T10:00:00Z"
}
```

错误响应 `400`：缺少必填字段时返回校验错误。

### 1.3 类目详情

```
GET /api/v2/cost-categories/{category_id}/
```

成功响应 `200`：返回单个类目对象，结构同上。

错误响应 `404`：类目不存在或不属于当前商户。

### 1.4 修改类目

```
PUT /api/v2/cost-categories/{category_id}/
```

请求体（JSON，所有字段可选）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `unique_key` | string | 唯一标识键 |
| `name` | string | 类目名称 |
| `parent_id` | int | 父类目 ID，传 `null` 清除 |
| `description` | string | 描述 |

成功响应 `200`：返回更新后的类目对象。

### 1.5 删除类目

```
DELETE /api/v2/cost-categories/{category_id}/
```

成功响应 `204`（无响应体）。

错误响应 `404`：类目不存在或不属于当前商户。

---

## 2. 支出明细

### 2.1 支出明细列表

```
GET /api/v2/cost-entries/
```

查询参数：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `merchant_id` | int | 否 | 按商户筛选 |
| `category_id` | int | 否 | 按支出类目筛选 |
| `start` | date | 否 | 发生日期起始，格式 `YYYY-MM-DD` |
| `end` | date | 否 | 发生日期截止，格式 `YYYY-MM-DD` |

默认按 `occurred_at` 降序排列。

成功响应 `200`：

```json
[
  {
    "id": 1,
    "merchant_id": 1,
    "category_id": 1,
    "category_name": "电费",
    "amount": "350.00",
    "unit_amount": null,
    "quantity": null,
    "unit_name": "",
    "occurred_at": "2026-06-01",
    "operator_id": 12,
    "image1": "",
    "image2": "",
    "source_module": "",
    "source_id": "",
    "remarks": "六月份电费",
    "created_at": "2026-06-06T10:00:00Z",
    "updated_at": "2026-06-06T10:00:00Z"
  }
]
```

字段说明：

| 字段 | 类型 | 说明 |
|------|------|------|
| `amount` | string | 最终支出金额 / 统计金额（Decimal 转字符串，前端展示时注意格式化） |
| `unit_amount` | string/null | 单价 / 基数金额；普通支出为 `null` |
| `quantity` | string/null | 数量 / 倍数；普通支出为 `null` |
| `unit_name` | string | 单位名称；普通支出为空字符串 |
| `occurred_at` | string | 发生日期，`YYYY-MM-DD` |
| `operator_id` | int | 经办人 ID，自动从当前登录用户获取 |
| `image1` | string | 凭证图片 URL（七牛云 CDN），无则为空字符串 |
| `image2` | string | 备用凭证图片 URL，无则为空字符串 |
| `source_module` | string | 来源模块名，手工录入时为空 |
| `source_id` | string | 来源记录 ID，手工录入时为空 |

### 2.2 创建支出明细

```
POST /api/v2/cost-entries/
```

Content-Type: `multipart/form-data`（支持图片上传）或 `application/json`

请求体：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `category_id` | int | **是** | 支出类目 ID |
| `amount` | decimal | 条件必填 | 普通支出必填；倍数型支出可不传。最终统计金额，如 `"350.00"` |
| `unit_amount` | decimal | 否 | **新增**。单价 / 基数金额；和 `quantity` 必须同时填写或同时为空 |
| `quantity` | decimal | 否 | **新增**。数量 / 倍数；和 `unit_amount` 必须同时填写或同时为空 |
| `unit_name` | string | 否 | **新增**。单位名称，如 `人天`、`小时`、`件` |
| `occurred_at` | date | **是** | 发生日期，`YYYY-MM-DD` |
| `image1` | file | 否 | 凭证图片 |
| `image2` | file | 否 | 备用凭证图片 |
| `source_module` | string | 否 | 来源模块名 |
| `source_id` | string | 否 | 来源记录 ID |
| `remarks` | string | 否 | 备注 |

请求示例（JSON，普通支出）：

```json
{
  "category_id": 1,
  "amount": "350.00",
  "occurred_at": "2026-06-01",
  "remarks": "六月份电费"
}
```

请求示例（JSON，**新增：倍数型支出**）：

```json
{
  "category_id": 2,
  "unit_amount": "200.00",
  "quantity": "3",
  "unit_name": "人天",
  "occurred_at": "2026-06-01",
  "remarks": "临时工 3 人天"
}
```

倍数型支出成功响应中的 `amount` 会是后端计算后的最终金额，例如 `"600.00"`。

成功响应 `201`：返回创建的支出明细对象。

错误响应 `400`：校验失败。
错误响应 `404`：`category_id` 不存在或不属于当前商户。

### 2.3 支出明细详情

```
GET /api/v2/cost-entries/{entry_id}/
```

成功响应 `200`：返回单个支出明细对象，含 `category_name`。

错误响应 `404`：明细不存在或不属于当前商户。

### 2.4 修改支出明细

```
PUT /api/v2/cost-entries/{entry_id}/
```

Content-Type: `multipart/form-data` 或 `application/json`

请求体（所有字段可选）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `category_id` | int | 切换类目 |
| `amount` | decimal/null | 普通支出金额；倍数型支出若公式字段存在，会被后端公式结果覆盖 |
| `unit_amount` | decimal/null | **新增**。单价 / 基数金额；传 `null` 可清除公式字段 |
| `quantity` | decimal/null | **新增**。数量 / 倍数；传 `null` 可清除公式字段 |
| `unit_name` | string | **新增**。单位名称；可传空字符串清空 |
| `occurred_at` | date | 发生日期 |
| `image1` | file | 凭证图片 |
| `image2` | file | 备用凭证图片 |
| `source_module` | string | 来源模块名 |
| `source_id` | string | 来源记录 ID |
| `remarks` | string | 备注 |

成功响应 `200`：返回更新后的支出明细对象。

修改倍数型支出数量示例：

```json
{
  "quantity": "4"
}
```

后端会按已有 `unit_amount * quantity` 重算并返回新的 `amount`。

将倍数型支出改回普通支出示例：

```json
{
  "amount": "550.00",
  "unit_amount": null,
  "quantity": null,
  "unit_name": ""
}
```

### 2.5 删除支出明细

```
DELETE /api/v2/cost-entries/{entry_id}/
```

成功响应 `204`。

错误响应 `404`：明细不存在或不属于当前商户。

---

## 3. 按类目汇总

```
GET /api/v2/cost-summary/by-category/
```

查询参数：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `merchant_id` | int | 否 | 按商户筛选 |
| `start` | date | 否 | 发生日期起始 |
| `end` | date | 否 | 发生日期截止 |

按 `total_amount` 降序排列。

成功响应 `200`：

```json
{
  "merchant_id": 1,
  "start_date": "2026-06-01",
  "end_date": "2026-06-30",
  "results": [
    {
      "category_id": 1,
      "category_name": "电费",
      "category_key": "electricity",
      "total_amount": "500.00",
      "entry_count": 2
    },
    {
      "category_id": 2,
      "category_name": "水费",
      "category_key": "water",
      "total_amount": "80.00",
      "entry_count": 1
    }
  ]
}
```

字段说明：

| 字段 | 类型 | 说明 |
|------|------|------|
| `total_amount` | string | 该类别在查询时间段内的总金额 |
| `entry_count` | integer | 该类别在查询时间段内的记录数 |
| `category_key` | string | 类目唯一键，可用于前端做图表的 key 映射 |

---

## 快速参考

| Method | URL | 说明 |
|--------|-----|------|
| GET | `/api/v2/cost-categories/` | 类目列表 |
| POST | `/api/v2/cost-categories/` | 创建类目 |
| GET | `/api/v2/cost-categories/{id}/` | 类目详情 |
| PUT | `/api/v2/cost-categories/{id}/` | 修改类目 |
| DELETE | `/api/v2/cost-categories/{id}/` | 删除类目 |
| GET | `/api/v2/cost-entries/` | 支出明细列表 |
| POST | `/api/v2/cost-entries/` | 创建支出明细 |
| GET | `/api/v2/cost-entries/{id}/` | 支出明细详情 |
| PUT | `/api/v2/cost-entries/{id}/` | 修改支出明细 |
| DELETE | `/api/v2/cost-entries/{id}/` | 删除支出明细 |
| GET | `/api/v2/cost-summary/by-category/` | 按类目汇总 |