# API 契约 — 接口协议

## 统一响应格式

所有接口统一返回 JSON，结构固定：

```json
{"code": 0, "data": {...}, "msg": ""}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | int | `0` 成功，非0为错误码 |
| `data` | any\|null | 成功时的载荷，失败时为 `null` |
| `msg` | string | 错误描述，成功时为空串 `""` |

PHP 端实现：
```php
$this->success($data);          // code=0
$this->error(401, '请先登录');  // code=401, data=null
```

**Ajax 请求认证失败时**（`X-Requested-With: XMLHttpRequest`）返回 `code=401` 的 JSON，不 302 跳转。
**普通页面请求未登录时** 302 跳转 `/login/index`，不返回 JSON。

---

## 错误码约定

| code | 含义 |
|------|------|
| `0` | 成功（保留，不得用于错误） |
| `1` | 通用业务错误（参数缺失、数据不存在等） |
| `2` | 数据验证失败（如两次密码不一致） |
| `3` | 策略不满足（如密码强度不够） |
| `401` | 未登录 |
| `403` | 无权限 |

业务错误统一用 `code=1`，只有需要前端分支处理时才用具体错误码（2、3...）。

---

## Grid 接口协议（DevExtreme CustomStore）

### load — `GET /{module}/grid`

DevExtreme 自动附带的查询参数：

| 参数 | 类型 | 说明 |
|------|------|------|
| `skip` | int | 跳过的行数（分页偏移） |
| `take` | int | 本页条数 |
| `requireTotalCount` | bool | 为 true 时需要返回 totalCount |
| `filter` | JSON | DevExtreme 过滤表达式（见下方） |
| `sort` | JSON | 排序数组 `[{"selector":"字段","desc":false}]` |
| `group` | JSON | 分组配置（已开启 `grouping: true`） |

响应 data 结构：

```json
{
  "code": 0,
  "data": {
    "data": [...],
    "totalCount": 100
  },
  "msg": ""
}
```

### save — `POST /{module}/save`

请求 body（JSON）：

```json
// 新增（无 key）
{"key": null, "values": {"field1": "value1", ...}}

// 更新（有 key）
{"key": 1, "values": {"field1": "new_value", ...}}
```

响应 data：

```json
// 新增成功
{"key": 新生成的主键值}

// 更新成功
{"key": 传入的主键值}
```

### remove — `POST /{module}/remove`

请求 body（JSON）：

```json
{"key": 1}
```

响应 data：`null`

### export — `POST /{module}/export`

请求格式：**表单 POST（非 JSON body）**，三个字段均为 JSON 字符串：

| 字段 | 类型 | 说明 |
|------|------|------|
| `columns` | JSON string | `[{"dataField":"字段名","caption":"列头"},...]` |
| `keys` | JSON string（可选） | 导出已选时传，主键数组 `[1,2,3]` |
| `filter` | JSON string（可选） | 无 keys 时用，DevExtreme filter 表达式 |

响应：直接输出 Excel 文件（不是 JSON），前端会触发浏览器下载。

---

## DevExtreme Filter 表达式格式

`DxFilter::toSql()` 支持以下格式（自动转成带参数化的 SQL 片段）：

```js
// 单条件（三元素）
["admin_name", "=", "张三"]
["admin_name", "contains", "张"]
["admin_status", "<>", null]        // → IS NOT NULL

// 简写（二元素，默认 "="）
["admin_status", 1]

// 取反
["!", ["admin_status", 0]]

// 组合（and/or 可混用，可嵌套）
[["admin_name","contains","张"], "and", ["admin_status","=",1]]
[["f1","=","v1"], "or", ["f2","=","v2"], "or", ["f3","=","v3"]]
```

支持的操作符：
`=` `<>` `>` `>=` `<` `<=` `startswith` `endswith` `contains` `notcontains` `between` `anyof` `noneof` `isblank` `isnotblank`

---

## 分页接口（非 Grid 场景）

`Model::paging()` 用于非 Grid 的自定义分页查询，返回：

```php
[
    'page'    => 1,      // 当前页
    'pagesize'=> 50,     // 每页条数
    'total'   => 200,    // 总页数
    'records' => 10000,  // 总记录数
    'limit'   => 50,     // 本页实际条数
]
```

---

## URL 路由格式

```
/{模块名}/{方法名}
/{模块名}/{方法名}/k/{键}/{值}/{键}/{值}   // 键值传参
/{模块名}/{方法名}/v/{值1}/{值2}            // 位置传参
```

请求参数合并优先级：GET → POST body（JSON）→ URL `/k/` 键值段
参数自动按方法形参名绑定，不需要在 Action 里手动读 `$_GET/$_POST`。

---

## 异常与错误页

- 抛出 `MvcException` → 被 `ExceptionHandler` 捕获
- `DEBUG` 常量为 true（开发环境）：输出异常详情（Monokai 暗色风格）
- `DEBUG` 为 false（生产环境）：记录 `error_log`，页面显示通用错误提示

Action 内业务校验失败时，**用 `$this->error(code, msg)` 而不是抛异常**（异常用于不可预期的错误）。
