# UI Kit Demo 数据接口规范

| 项目 | 值 |
| --- | --- |
| 文档版本 | 1.0.0 |
| 最后更新 | 2026-10-21 |
| 接口版本 | v1（路径中不显式带版本号，作为演示服务内部版本） |
| Base URL | `http://localhost:3000/api`（`PORT` 可配置） |
| 传输协议 | HTTP/1.1，开发环境为明文 HTTP |
| 数据格式 | `application/json; charset=utf-8` |
| 时间格式 | ISO 8601 UTC，例如 `2026-10-21T08:30:00.000Z` |
| 日期格式 | `YYYY-MM-DD` |
| 金额 | 非负整数，单位：元（CNY），无小数 |
| 字段命名 | `camelCase` |
| 字符编码 | UTF-8 |

机器可读定义：[`openapi.json`](public/openapi.json)（OpenAPI 3.0.3；服务启动后也可访问 `/openapi.json`）。本文档与 `server.js` 的实现保持一致，字段、枚举、状态码以本文为准。

> 约定用语：**必须** 表示客户端不可省略或服务端会强制校验；**应当** 表示推荐做法；**可以** 表示可选能力。

---

## 1. 通用约定

### 1.1 资源与 HTTP 方法

| 方法 | 语义 | 幂等 |
| --- | --- | --- |
| `GET` | 查询资源，不产生副作用 | 是 |
| `POST` | 创建资源或执行批量命令 | 否 |
| `PUT` | 更新当前用户资料（只更新传入字段） | 是 |
| `PATCH` | 部分更新记录 | 是（相同请求体重复提交结果一致） |
| `DELETE` | 删除资源 | 是（重复删除返回 `404 not_found`） |

### 1.2 请求格式

- 带请求体的接口必须发送 `Content-Type: application/json`。
- 请求体必须是 JSON 对象（`{ ... }`），数组、字符串、`null` 等一律返回 `400 bad_request`。
- 服务端忽略未定义的请求字段；客户端应忽略未定义的响应字段。
- 请求体上限为 1 MiB。

### 1.3 成功响应

成功响应直接返回资源或结果对象，不额外包裹 `data` / `code` 字段。分页列表返回 `items` 与分页元数据。创建成功使用 `201`，其余成功使用 `200`。

```json
{
  "record": {
    "id": "C-2026-001",
    "title": "华东区采购中心 采购申请 #001"
  }
}
```

### 1.4 错误响应

所有错误使用统一的 JSON 结构：

```json
{ "error": "validation" }
```

| HTTP 状态 | `error` | 含义 |
| --- | --- | --- |
| `400` | `bad_request` | 请求体不是合法 JSON、不是 JSON 对象，或 `Content-Type` 不是 `application/json` |
| `400` | `captcha` | 验证码错误或已过期 |
| `400` | `email` | 邮箱格式不合法 |
| `400` | `code` | 重置验证码错误或已过期 |
| `400` | `password` | 新密码长度不足 6 位 |
| `400` | `validation` | 字段缺失、枚举值非法、金额/日期不合法、分页参数越界或业务校验失败 |
| `401` | `credentials` | 用户名或密码错误（仅登录接口） |
| `401` | `unauthorized` | 缺少、无效或过期的访问令牌 |
| `404` | `not_found` | 资源不存在，或路由不存在 |
| `500` | `server_error` | 服务端未捕获异常 |

> 错误响应不返回堆栈信息。`400` 的具体字段错误通过 `error` 区分；演示服务不返回字段级明细。
>
> 认证优先于路由匹配：需要认证的请求如果 Token 缺失或失效，统一返回 `401 unauthorized`，不会暴露目标路由是否存在。

### 1.5 认证

除以下公开接口外，所有接口都需要 Bearer Token：

- `GET /api/config`
- `GET /api/captcha`
- `POST /api/login`
- `POST /api/forgot/send`
- `POST /api/forgot/reset`

请求头：

```http
Authorization: Bearer <token>
```

Token 为 48 位十六进制字符串，登录成功后签发，有效期 **8 小时**，仅保存在服务端内存中。服务重启后所有 Token 失效。

### 1.6 分页

- `page` 从 `1` 开始，默认 `1`，必须是正整数。
- `pageSize` 默认 `10`，必须是 `1`–`100` 的整数。
- `page` / `pageSize` 不是整数、小于 `1` 或超过上限时返回 `400 validation`，不做静默截断。
- 总数 `total` 是应用筛选后的总数，不是当前页条数。
- 页码超过总页数时返回空 `items`，客户端应回退到最后一页。

### 1.7 过滤与排序

- 字符串搜索 `q` 不区分大小写，匹配 `id`、`title`、`customer`、`owner`、`status` 的拼接文本；空字符串等价于不筛选。
- `status`、`owner` 为精确匹配；非法 `status` 返回 `400 validation`。
- `worksheetId` 必须是已有工作表 ID；未知 ID 返回 `400 validation`，省略时返回全部工作表。
- 非法 `sort` 返回 `400 validation`；省略时使用 `updated_desc`。
- `sort` 允许值：

| 值 | 含义 |
| --- | --- |
| `updated_desc` | 最近更新（默认） |
| `updated_asc` | 最早更新 |
| `amount_desc` | 金额从高到低 |
| `amount_asc` | 金额从低到高 |
| `title_asc` | 标题 A–Z |

### 1.8 枚举

**记录状态 `status`**

| 值 | 含义 |
| --- | --- |
| `pending` | 待审批 |
| `approved` | 已通过 |
| `rejected` | 已驳回 |
| `draft` | 草稿 |

**优先级 `priority`**

| 值 | 含义 |
| --- | --- |
| `high` | 高 |
| `medium` | 中 |
| `low` | 低 |

**动态类型 `activity.type`**

`created`、`approved`、`rejected`、`comment`、`updated`、`status`。

### 1.9 状态码与幂等性

| 操作 | 成功 | 重复提交 | 幂等 |
| --- | --- | --- | --- |
| 查询 | `200` | 返回相同结果 | 是 |
| 创建 | `201` | 生成新记录，不复用 ID | 否 |
| 部分更新 | `200` | 结果一致，并追加一条 `updated` 动态 | 是（状态一致） |
| 删除 | `200` | 第二次返回 `404 not_found` | 是（最终状态一致） |
| 批量更新/删除 | `200` | `updated` / `deleted` 可能为 `0` | 是 |

### 1.10 演示数据与限制

- 数据保存在 Node 进程内存中，**重启服务即重置**。
- 密码为明文存储，仅用于演示，禁止用于生产。
- 重置验证码固定为 `123456`，并在接口响应中返回 `devCode`，仅用于演示。
- 没有限流、审计日志、并发控制、ETag、软删除与回收站。
- 没有 CORS 响应头，默认按同源访问设计。

---

## 2. 接口总览

| 方法 | 路径 | 说明 | 认证 |
| --- | --- | --- | --- |
| GET | `/api/config` | 演示配置与测试账号 | 否 |
| GET | `/api/captcha` | 获取图形验证码 | 否 |
| POST | `/api/login` | 登录并签发 Token | 否 |
| POST | `/api/logout` | 注销当前 Token | 是 |
| POST | `/api/forgot/send` | 发送重置验证码 | 否 |
| POST | `/api/forgot/reset` | 使用验证码重置密码 | 否 |
| GET | `/api/me` | 获取当前用户 | 是 |
| PUT | `/api/me` | 更新当前用户资料 | 是 |
| GET | `/api/worksheets` | 工作表列表与记录数 | 是 |
| GET | `/api/stats` | 首页统计与最近记录 | 是 |
| GET | `/api/records` | 分页查询记录 | 是 |
| POST | `/api/records` | 新建记录 | 是 |
| GET | `/api/records/{id}` | 获取记录详情 | 是 |
| PATCH | `/api/records/{id}` | 更新记录 | 是 |
| DELETE | `/api/records/{id}` | 删除记录 | 是 |
| POST | `/api/records/batch-status` | 批量更新状态 | 是 |
| POST | `/api/records/batch-delete` | 批量删除 | 是 |

---

## 3. 认证与账号

### 3.1 获取演示配置

`GET /api/config`

公开接口，返回测试账号与演示验证码，便于前端展示提示。生产环境不应暴露此接口。

**响应 `200`**

```json
{
  "demo": true,
  "credentials": { "username": "admin", "password": "123456" },
  "forgotCode": "123456"
}
```

---

### 3.2 获取图形验证码

`GET /api/captcha`

**响应 `200`**

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | string | 验证码 ID，24 位十六进制，5 分钟内有效 |
| `svg` | string | 可直接嵌入页面的 SVG 字符串，宽 120、高 40 |

```json
{
  "id": "6f2c1a9b8d7e4f3a2b1c0d9e",
  "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"120\" height=\"40\">...</svg>"
}
```

验证码一次性使用：登录时无论成功与否，校验后立即作废。

---

### 3.3 登录

`POST /api/login`

**请求体**

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `username` | string | 是 | 用户名 |
| `password` | string | 是 | 密码 |
| `captchaId` | string | 是 | `/api/captcha` 返回的 `id` |
| `captcha` | string | 是 | 用户输入的 4 位字符，不区分大小写 |

```json
{
  "username": "admin",
  "password": "123456",
  "captchaId": "6f2c1a9b8d7e4f3a2b1c0d9e",
  "captcha": "A7K2"
}
```

**响应 `200`**

```json
{
  "token": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822c",
  "user": {
    "id": "u1",
    "username": "admin",
    "name": "王敏",
    "email": "admin@example.com",
    "role": "管理员",
    "avatar": "王"
  }
}
```

**错误**

| 状态 | error | 场景 |
| --- | --- | --- |
| `400` | `validation` | 缺少用户名、密码或验证码 ID |
| `400` | `captcha` | 验证码不存在、过期或输入错误 |
| `401` | `credentials` | 用户名或密码错误 |

---

### 3.4 注销

`POST /api/logout`

需要认证。删除当前 Token，重复注销返回 `401 unauthorized`。

**响应 `200`**

```json
{ "ok": true }
```

---

### 3.5 发送重置验证码

`POST /api/forgot/send`

**请求体**

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `email` | string | 是 | 注册邮箱，需符合基本邮箱格式 |

**响应 `200`**

```json
{ "ok": true, "devCode": "123456" }
```

`devCode` 仅用于演示；生产环境应删除该字段并通过邮件发送验证码。验证码 10 分钟内有效，同一邮箱再次发送会覆盖旧验证码。为不暴露邮箱是否存在，无论邮箱是否已注册都返回相同结果。

**错误**：`400 email`。

---

### 3.6 重置密码

`POST /api/forgot/reset`

**请求体**

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `email` | string | 是 | 注册邮箱 |
| `code` | string | 是 | 6 位验证码 |
| `password` | string | 是 | 新密码，至少 6 位 |

**响应 `200`**

```json
{ "ok": true }
```

**错误**：`400 validation`（缺少必填字段）、`400 code`（验证码错误/过期）、`400 password`（长度不足）。

> 演示服务不会检查邮箱是否已注册；未注册邮箱也会返回成功，避免账号枚举。

---

### 3.7 当前用户

`GET /api/me`

**响应 `200`**

```json
{
  "user": {
    "id": "u1",
    "username": "admin",
    "name": "王敏",
    "email": "admin@example.com",
    "role": "管理员",
    "avatar": "王"
  }
}
```

---

### 3.8 更新当前用户

`PUT /api/me`

**请求体**（字段均可选，至少传一个）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `name` | string | 姓名，去除首尾空格后不能为空，最长 40 |
| `email` | string | 邮箱，需符合基本邮箱格式，最长 120 |

```json
{ "name": "王敏", "email": "wangmin@example.com" }
```

**响应 `200`**：与 `GET /api/me` 相同，返回更新后的 `user`。

**错误**：`400 validation`。

---

## 4. 工作表与统计

### 4.1 工作表列表

`GET /api/worksheets`

**响应 `200`**

```json
{
  "worksheets": [
    {
      "id": "customer",
      "name": "客户管理",
      "nameEn": "Customers",
      "icon": "task-folder-solid",
      "color": "#2196f3",
      "count": 128
    }
  ]
}
```

`count` 为当前进程内该工作表的记录总数，随新增/删除实时变化。

---

### 4.2 首页统计

`GET /api/stats`

**响应 `200`**

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `total` | integer | 全部记录数 |
| `pending` | integer | 待审批数 |
| `approved` | integer | 已通过数 |
| `amount` | integer | 全部记录金额合计（元） |
| `byWorksheet` | object | `{ [worksheetId]: count }` |
| `recent` | Record[] | 按 `updatedAt` 倒序的前 6 条记录 |

```json
{
  "total": 228,
  "pending": 56,
  "approved": 62,
  "amount": 11003000,
  "byWorksheet": { "customer": 128, "order": 64, "project": 36 },
  "recent": []
}
```

---

## 5. 记录

### 5.1 查询记录

`GET /api/records`

**查询参数**

| 参数 | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| `worksheetId` | string | 空 | 工作表 ID；为空时返回全部工作表，未知 ID 返回 `400` |
| `q` | string | 空 | 关键词搜索，不区分大小写 |
| `status` | enum | 空 | 记录状态，非法值返回 `400` |
| `owner` | string | 空 | 负责人精确匹配 |
| `sort` | enum | `updated_desc` | 排序方式，非法值返回 `400` |
| `page` | integer | `1` | 页码，正整数 |
| `pageSize` | integer | `10` | 每页条数，1–100 的整数 |

**响应 `200`**

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `items` | Record[] | 当前页记录 |
| `total` | integer | 筛选后的总条数 |
| `page` | integer | 当前页 |
| `pageSize` | integer | 实际每页条数 |
| `owners` | string[] | 当前工作表下的负责人列表（不受 `q` / `status` 影响） |
| `statuses` | string[] | 固定状态枚举 |
| `worksheet` | Worksheet \| null | 当前工作表元数据 |

```json
{
  "items": [
    {
      "id": "C-2026-001",
      "worksheetId": "customer",
      "title": "华东区采购中心 采购申请 #001",
      "customer": "华东区采购中心",
      "owner": "王敏",
      "amount": 48200,
      "status": "pending",
      "priority": "high",
      "phone": "13800000001",
      "email": "contact1@example.com",
      "date": "2026-09-01",
      "note": "请在本周五前完成审批，交付地址详见附件。",
      "createdAt": "2026-09-01T09:00:00.000Z",
      "updatedAt": "2026-09-03T10:00:00.000Z",
      "activity": []
    }
  ],
  "total": 128,
  "page": 1,
  "pageSize": 10,
  "owners": ["王敏", "李强", "赵蕾"],
  "statuses": ["pending", "approved", "rejected", "draft"],
  "worksheet": {
    "id": "customer",
    "name": "客户管理",
    "nameEn": "Customers",
    "icon": "task-folder-solid",
    "color": "#2196f3",
    "count": 128
  }
}
```

**错误**：`400 validation`（`worksheetId`、`status`、`sort` 非法，或分页参数不是合法整数/越界）。

**示例**

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "http://localhost:3000/api/records?worksheetId=customer&status=pending&sort=amount_desc&page=2&pageSize=20"
```

---

### 5.2 新建记录

`POST /api/records`

**请求体**

| 字段 | 类型 | 必填 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `worksheetId` | string | 是 | — | 必须是有效工作表 ID |
| `title` | string | 是 | — | 最长 120 |
| `customer` | string | 否 | `""` | 最长 120 |
| `owner` | string | 否 | 当前用户名 | 最长 40 |
| `amount` | integer | 否 | `0` | 非负整数，单位元；小数/负数返回 `400 validation` |
| `status` | enum | 否 | `draft` | 非法值回退为 `draft` |
| `priority` | enum | 否 | `medium` | 非法值回退为 `medium` |
| `phone` | string | 否 | `""` | 最长 30 |
| `email` | string | 否 | `""` | 最长 120 |
| `date` | string | 否 | 当天 | 必须是真实存在的 `YYYY-MM-DD` 日期 |
| `note` | string | 否 | `""` | 最长 1000 |

`title` 会去除首尾空格，去除后不能为空。`id` 由服务端生成，格式为 `<工作表前缀>-2026-<三位序号>`，例如 `C-2026-129`。

**响应 `201`**

```json
{ "record": { "id": "C-2026-129", "...": "完整记录字段" } }
```

**错误**：`400 validation`（工作表不存在、标题为空、金额或日期不合法）。

---

### 5.3 获取记录详情

`GET /api/records/{id}`

**响应 `200`**

```json
{ "record": { "id": "C-2026-001", "...": "完整记录字段" } }
```

**错误**：`404 not_found`。

---

### 5.4 更新记录

`PATCH /api/records/{id}`

部分更新。仅处理以下字段，未传字段保持不变，未知字段忽略。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `title` | string | 去除首尾空格后不能为空；最长 1000 |
| `customer` | string | 最长 1000 |
| `owner` | string | 最长 1000 |
| `amount` | integer | 非负整数；小数/负数返回 `400 validation` |
| `status` | enum | 仅接受合法状态，非法值忽略 |
| `priority` | enum | 仅接受合法优先级，非法值忽略 |
| `phone` | string | 最长 1000 |
| `email` | string | 最长 1000 |
| `date` | string | 必须是真实存在的 `YYYY-MM-DD` 日期 |
| `note` | string | 最长 1000 |

成功后自动追加一条 `activity`（`type: "updated"`），并刷新 `updatedAt`。

**响应 `200`**

```json
{ "record": { "id": "C-2026-001", "status": "approved", "...": "..." } }
```

**错误**：`400 validation`（标题为空白、金额/日期不合法）、`404 not_found`。

> 演示服务没有乐观锁；生产实现应校验邮箱、状态流转，并引入 `version` 或 `If-Match`。

---

### 5.5 删除记录

`DELETE /api/records/{id}`

**响应 `200`**

```json
{ "ok": true }
```

**错误**：`404 not_found`。

---

### 5.6 批量更新状态

`POST /api/records/batch-status`

**请求体**

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `ids` | string[] | 是 | 非空记录 ID 列表 |
| `status` | enum | 是 | 目标状态 |

```json
{ "ids": ["C-2026-001", "C-2026-002"], "status": "approved" }
```

**响应 `200`**

```json
{ "ok": true, "updated": 2 }
```

`updated` 为实际匹配并更新的记录数；不存在的 ID 会被忽略。

**错误**：`400 validation`（状态非法，或 `ids` 不是非空字符串数组）。

---

### 5.7 批量删除

`POST /api/records/batch-delete`

**请求体**

```json
{ "ids": ["C-2026-001", "C-2026-002"] }
```

**响应 `200`**

```json
{ "ok": true, "deleted": 2 }
```

`deleted` 为实际删除数；不存在的 ID 会被忽略。`ids` 必须是非空字符串数组，否则返回 `400 validation`。

---

## 6. 数据模型

### 6.1 User

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | string | 用户 ID |
| `username` | string | 登录名 |
| `name` | string | 显示名 |
| `email` | string | 邮箱 |
| `role` | string | `管理员` 或 `成员` |
| `avatar` | string | 头像占位字符，取姓名首字 |

### 6.2 Worksheet

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | string | 工作表 ID |
| `name` | string | 中文名 |
| `nameEn` | string | 英文名 |
| `icon` | string | 图标类名，对应 `themes/icons/mdfont.css` |
| `color` | string | 主题色，十六进制 |
| `count` | integer | 记录数 |

### 6.3 Record

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | string | 记录编号 |
| `worksheetId` | string | 所属工作表 |
| `title` | string | 标题 |
| `customer` | string | 客户/供应商 |
| `owner` | string | 负责人 |
| `amount` | integer | 金额，单位元 |
| `status` | enum | 记录状态 |
| `priority` | enum | 优先级 |
| `phone` | string | 联系电话 |
| `email` | string | 联系邮箱 |
| `date` | string | 业务日期 `YYYY-MM-DD` |
| `note` | string | 备注 |
| `createdAt` | string | 创建时间 ISO 8601 |
| `updatedAt` | string | 更新时间 ISO 8601 |
| `activity` | Activity[] | 动态列表，按时间正序存储 |

### 6.4 Activity

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | string | UUID |
| `type` | enum | `created` / `approved` / `rejected` / `comment` / `updated` / `status` |
| `user` | string | 操作人显示名 |
| `at` | string | 操作时间 ISO 8601 |
| `text` | string | 服务端生成的中文描述，仅作辅助展示 |

---

## 7. 调用示例

### 7.1 登录

```bash
BASE=http://localhost:3000/api

# 1. 获取验证码，记录 id，并从 svg 中识别 4 位字符
curl -s "$BASE/captcha"

# 2. 登录（把 CAPTCHA_ID / CODE 替换为实际值）
TOKEN=$(curl -s -X POST "$BASE/login" \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"123456","captchaId":"CAPTCHA_ID","captcha":"CODE"}' \
  | node -pe 'JSON.parse(require("fs").readFileSync(0)).token')
```

### 7.2 查询、创建、更新

```bash
# 列表
curl -s -H "Authorization: Bearer $TOKEN" \
  "$BASE/records?worksheetId=order&pageSize=5"

# 新建
curl -s -X POST "$BASE/records" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"worksheetId":"order","title":"演示订单","amount":12800,"status":"pending"}'

# 更新
curl -s -X PATCH "$BASE/records/O-2026-001" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"status":"approved","note":"已确认"}'

# 批量
curl -s -X POST "$BASE/records/batch-status" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"ids":["O-2026-001"],"status":"approved"}'
```

### 7.3 找回密码

```bash
curl -s -X POST "$BASE/forgot/send" \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com"}'

curl -s -X POST "$BASE/forgot/reset" \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","code":"123456","password":"newpass123"}'
```

---

## 8. 版本与兼容策略

- 当前为演示 API 的 `1.0.0`，路径不带版本号。
- 新增可选字段、新增枚举值、新增接口视为向后兼容。
- 删除字段、修改字段类型、修改错误码语义视为破坏性变更，需要新增路径版本（如 `/api/v2`）。
- 客户端应忽略未知字段，服务端应忽略未知请求字段。

## 9. 生产化改造清单

按优先级排序：

1. 使用 `bcrypt`/`argon2` 哈希密码，禁止明文与默认账号。
2. Token 改为短时 JWT + 刷新令牌，并支持服务端吊销与会话列表。
3. 验证码 / 登录 / 找回密码接口增加 IP 与账号维度限流、防重放与审计。
4. 重置验证码通过邮件发送，响应中移除 `devCode`，并限制尝试次数。
5. 使用数据库与事务，加入软删除、版本号/乐观锁、创建人/修改人。
6. 增加字段级校验、统一错误明细（如 `details: [{ field, message }]`）和请求 ID。
7. 启用 HTTPS、CORS 白名单、CSRF 防护（若使用 Cookie）与安全响应头。
8. 增加 OpenAPI 契约测试，保证文档、mock 与真实服务一致。
