# API 参考手册

后端 API 基于 FastAPI 构建，所有端点均以 `/api/` 为前缀。  
在线文档：启动后访问 `http://localhost:8000/docs`（Swagger UI）或 `/redoc`（ReDoc）。

## 全局响应格式（RNS1.0）

所有 JSON 成功响应（HTTP 2xx）自动包装为统一格式：

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

异常响应格式：

```json
{ "code": 401, "message": "未认证" }
```

规则：
- `code=0` 表示成功，`data` 为业务数据
- `code≠0` 表示错误，`code` 为 HTTP 状态码，`message` 为错误描述
- SSE 流式端点（`text/event-stream`）和非 JSON 响应不包装，直接透传
- 所有小程序 API 响应字段名为 camelCase（如 `userId`、`storeName`）

---

## 1. 管理后台认证 `/api/auth`

### POST `/api/auth/login`
管理后台用户名密码登录。

请求体：
```json
{ "username": "admin", "password": "..." }
```
响应：
```json
{ "access_token": "...", "refresh_token": "...", "token_type": "bearer" }
```

### POST `/api/auth/refresh`
刷新访问令牌。

请求体：
```json
{ "refresh_token": "..." }
```

---

## 2. 小程序认证 `/api/xcx-auth`

小程序用户的完整生命周期：微信登录 → 提交申请 → 管理员审批 → 正式使用。

### POST `/api/xcx-auth/login`
微信登录。用 `wx.login()` 获取的 code 换取 JWT。

请求体：
```json
{ "code": "微信临时登录凭证" }
```
响应：
```json
{
  "access_token": "...",
  "refresh_token": "...",
  "token_type": "bearer",
  "user_status": "pending | approved | rejected | disabled",
  "user_id": 1
}
```
说明：
- 首次登录自动创建 `auth.users` 记录（status=new），前端引导至申请页
- new/pending/rejected 用户获得受限令牌（`limited=True`），仅可访问申请相关端点
- approved 用户获得完整令牌，包含 `site_id` 和 `roles`

### POST `/api/xcx-auth/apply`
提交入驻申请。需受限令牌或完整令牌。

请求体：
```json
{
  "site_code": "AB123",
  "applied_role_text": "助教",
  "phone": "13800138000",
  "employee_number": "E001",
  "nickname": "张三"
}
```
说明：
- `site_code` 格式：2 字母 + 3 数字（如 `AB123`），映射到 `auth.site_code_mapping`
- 后端自动进行人员匹配（`matching.py`），在 ETL 库中查找助教/员工记录

### GET `/api/xcx-auth/status`
查询当前用户状态和申请记录。需受限令牌或完整令牌。

响应：
```json
{
  "user_id": 1,
  "status": "approved",
  "nickname": "张三",
  "applications": [
    {
      "id": 1,
      "site_code": "AB123",
      "applied_role_text": "助教",
      "status": "approved",
      "review_note": null,
      "created_at": "2026-02-25T10:00:00",
      "reviewed_at": "2026-02-25T11:00:00"
    }
  ]
}
```

### GET `/api/xcx-auth/sites`
获取当前用户关联的门店列表。需完整令牌。

### POST `/api/xcx-auth/switch-site`
切换当前门店，返回新的令牌对。需完整令牌。

请求体：
```json
{ "site_id": 2 }
```

### POST `/api/xcx-auth/refresh`
刷新令牌。

请求体：
```json
{ "refresh_token": "..." }
```

### POST `/api/xcx-auth/dev-login`
开发模式 mock 登录（仅 `WX_DEV_MODE=true` 时注册）。

请求体：
```json
{ "openid": "模拟openid", "status": "approved" }
```
说明：
- `status` 可选，为空时保留已有用户当前状态，新用户默认 `new`
- 仅开发/测试环境可用

---

## 3. 任务配置 `/api/tasks`

所有端点需 JWT 认证。

### GET `/api/tasks/registry`
按业务域分组的 ETL 任务列表。

响应示例：
```json
{
  "groups": {
    "会员": [
      {
        "code": "DWD_LOAD_FROM_ODS",
        "name": "ODS → DWD 加载",
        "domain": "会员",
        "layer": "DWD",
        "requires_window": true,
        "is_ods": false,
        "is_dimension": false,
        "default_enabled": true,
        "is_common": true
      }
    ]
  }
}
```

### GET `/api/tasks/dwd-tables`
按业务域分组的 DWD 表定义。

### GET `/api/tasks/flows`
返回 7 种 Flow 定义和 4 种处理模式。

Flow 列表：
| ID | 名称 | 层级 |
|----|------|------|
| `api_ods` | API → ODS | ODS |
| `api_ods_dwd` | API → ODS → DWD | ODS, DWD |
| `api_full` | API → ODS → DWD → DWS → INDEX | ODS, DWD, DWS, INDEX |
| `ods_dwd` | ODS → DWD | DWD |
| `dwd_dws` | DWD → DWS汇总 | DWS |
| `dwd_dws_index` | DWD → DWS → INDEX | DWS, INDEX |
| `dwd_index` | DWD → DWS指数 | INDEX |

处理模式：
| ID | 名称 | 说明 |
|----|------|------|
| `increment_only` | 仅增量处理 | 只处理新增和变更的数据 |
| `verify_only` | 仅校验修复 | 校验现有数据并修复不一致 |
| `increment_verify` | 增量 + 校验修复 | 先增量处理，再校验并修复 |
| `full_window` | 全窗口处理 | 用 API 返回数据的实际时间范围处理全部层 |

### POST `/api/tasks/validate`
验证任务配置并返回 CLI 命令预览。`store_id` 从 JWT 自动注入。

### GET `/api/tasks/sync-check`
对比后端硬编码任务列表与 ETL 真实注册表，返回差异。

---

## 4. 任务执行 `/api/execution`

所有端点需 JWT 认证，`site_id` 从 JWT 提取。

### POST `/api/execution/run`
直接执行任务（不经过队列）。异步启动 ETL CLI 子进程。

请求体：`TaskConfigSchema`（flow、tasks、window 等）

响应：
```json
{ "execution_id": "uuid", "message": "任务已提交执行" }
```

### GET `/api/execution/queue`
获取当前门店的待执行队列。

### POST `/api/execution/queue`
将任务配置添加到执行队列。

### PUT `/api/execution/queue/reorder`
调整队列中任务的执行顺序。

### DELETE `/api/execution/queue/{task_id}`
从队列中删除待执行任务（仅 pending 状态）。

### POST `/api/execution/{execution_id}/cancel`
取消正在执行的任务。

### GET `/api/execution/history`
执行历史记录（按 `started_at` 降序，默认 50 条，最多 200 条）。

### GET `/api/execution/{execution_id}/logs`
获取指定执行的完整日志。优先从内存缓冲区读取（执行中），否则从数据库读取（已完成）。

---

## 5. 调度管理 `/api/schedules`

所有端点需 JWT 认证。

### GET `/api/schedules`
列出当前门店的所有调度任务。

### POST `/api/schedules`
创建调度任务，自动计算 `next_run_at`。

请求体新增字段（P16）：
- `min_run_interval_value`：最小运行间隔数值（int，默认 0 表示无限制）
- `min_run_interval_unit`：间隔单位（`minutes`/`hours`/`days`，默认 `minutes`）

### PUT `/api/schedules/{schedule_id}`
更新调度任务（部分更新，仅更新请求中提供的字段）。

请求体新增可选字段（P16）：
- `min_run_interval_value`：最小运行间隔数值
- `min_run_interval_unit`：间隔单位

### DELETE `/api/schedules/{schedule_id}`
删除调度任务。

### PATCH `/api/schedules/{schedule_id}/toggle`
切换启用/禁用状态。禁用时 `next_run_at` 置 NULL；启用时重新计算。

### POST `/api/schedules/{schedule_id}/run`
手动触发执行调度任务。

查询参数：
- `force`：是否强制执行（bool，默认 `false`）

行为：
- `force=false`：检查并发状态和最小间隔，不满足条件返回 409
- `force=true`：绕过所有检查，直接入队执行

错误码：
- 409：任务正在执行中（`last_status='running'`）
- 409：最小运行间隔未到（提示距下次可执行的剩余时间）

列表响应新增字段（P16）：
- `min_run_interval_value`：最小运行间隔数值（0 表示无限制）
- `min_run_interval_unit`：间隔单位
- `last_success_at`：最后一次成功执行时间（TIMESTAMPTZ，可为 null）

---

## 6. 数据库查看器 `/api/db`

所有端点需 JWT 认证。使用 ETL 只读连接 + RLS 门店隔离。

### GET `/api/db/schemas`
返回 ETL 数据库中的 Schema 列表。

### GET `/api/db/schemas/{name}/tables`
返回指定 Schema 下所有表的名称和行数统计。

### GET `/api/db/tables/{schema}/{table}/columns`
返回指定表的列定义（列名、数据类型、是否可空、默认值）。

### POST `/api/db/query`
只读 SQL 执行。

安全措施：
- 拦截写操作关键词（INSERT / UPDATE / DELETE / DROP / TRUNCATE）
- 返回行数上限 1000 行
- 查询超时 30 秒
- 连接级 `read_only` 保护

请求体：
```json
{ "sql": "SELECT * FROM dwd.member_info LIMIT 10" }
```

---

## 7. ETL 状态 `/api/etl-status`

### GET `/api/etl-status/cursors`
返回各 ODS 表的最新数据游标（查询 `meta.etl_cursor`）。

### GET `/api/etl-status/recent-runs`
返回最近 50 条任务执行记录。

---

## 8. 环境配置 `/api/env-config`

### GET `/api/env-config`
读取根 `.env` 文件内容（敏感值脱敏显示）。

### PUT `/api/env-config`
更新 `.env` 文件中的配置项。

---

## 9. 运维面板 `/api/ops`

### GET `/api/ops/system`
服务器系统资源概况（CPU、内存、磁盘、启动时间）。

### GET `/api/ops/services`
所有环境（test/prod）的服务运行状态（PID、端口、内存、CPU、运行时长）。

### POST `/api/ops/services/{env}/start`
启动指定环境的后端服务。

### POST `/api/ops/services/{env}/stop`
停止指定环境的后端服务。

### POST `/api/ops/services/{env}/restart`
重启指定环境的后端服务。

### GET `/api/ops/git`
所有环境的 Git 状态（分支、最新提交、是否有本地修改）。

### POST `/api/ops/git/{env}/pull`
对指定环境执行 `git pull --ff-only`。

### POST `/api/ops/git/{env}/sync-deps`
对指定环境执行 `uv sync --all-packages`。

### GET `/api/ops/env-file/{env}`
读取指定环境的 `.env` 文件（敏感值脱敏）。

---

## 10. 其他端点

### GET `/health`
健康检查。返回 `{"status": "ok"}`。

### GET `/api/xcx-test`
MVP 全链路验证端点，从 `test."xcx-test"` 表读取数据。

### GET/POST `/api/wx-callback`
微信消息推送回调。GET 用于签名验证，POST 用于接收消息。

---

## 11. 管理端申请审核 `/api/admin/applications`

### GET `/api/admin/applications`
获取待审核申请列表。需管理后台 JWT。

### POST `/api/admin/applications/{id}/approve`
批准申请。

### POST `/api/admin/applications/{id}/reject`
拒绝申请。

---

## 12. 营业日配置 `/api/business-day`

### GET `/api/business-day/config`
获取营业日分割点配置（`BUSINESS_DAY_START_HOUR`）。

---

## 13. 小程序任务 `/api/xcx/tasks`

所有端点需 JWT（approved 状态）。

### GET `/api/xcx/tasks`
获取当前助教的活跃任务列表。

响应：`TaskListItem[]`

### POST `/api/xcx/tasks/{id}/pin`
置顶任务。

### POST `/api/xcx/tasks/{id}/unpin`
取消置顶。

### POST `/api/xcx/tasks/{id}/abandon`
放弃任务（需填写原因）。

请求体：
```json
{ "reason": "放弃原因" }
```

### POST `/api/xcx/tasks/{id}/restore`
取消放弃，恢复为活跃状态。

### GET `/api/xcx/tasks/{id}`
获取任务详情（TASK-2）。返回单个任务的完整信息，含服务记录、备注、AI 分析、维客线索。

响应包含 `customerId` 字段，供前端跳转客户详情/对话页面使用。

---

## 14. 小程序绩效 `/api/xcx/performance`

所有端点需 JWT（approved 状态）。

### GET `/api/xcx/performance`
绩效概览（PERF-1）。返回指定月份的收入档位、DateGroup 分组记录、新客/常客列表。

查询参数：
- `month`：月份（格式 `YYYY-MM`，默认当月）

响应包含：
- `currentTier`：当前档位助教到手单价（`basicRate` = 客户价 - base_deduction，`incentiveRate` = 客户价 × (1 - bonus_deduction_ratio)）
- `nextTier`：下一档到手单价（同上公式，使用下一档抽成参数）
- `upgradeHoursNeeded`：距下一档所需小时数（next_tier.min_hours - total_hours）
- `upgradeBonus`：升档后因抽成降低的节省额
- `incomeItems`：始终 3 项（基础课收入、激励课收入、Top3 销冠奖），金额为 0 时仍显示
- `thisMonthRecords`（DateGroup 分组）、`lastMonthIncome`、`newCustomers`/`regularCustomers`

档位数据来源：`cfg_performance_tier` 配置表（通过 `fdw_queries.get_performance_tiers()` 查询）。

### GET `/api/xcx/performance/records`
绩效明细（PERF-2）。返回指定月份的服务记录明细，按日期分组，支持分页。

查询参数：
- `year`：年份（必填）
- `month`：月份（`1-12`，必填）
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 20，最大 100）
- `coach_id`：目标助教 ID（可选，管理者视角）

权限分流（运行时通过 `get_user_permissions()` 实时检查，非 `require_permission` 中间件）：
- 不带 `coach_id`（查自己）：需 `view_tasks` 权限，`assistant_id` 由当前登录用户绑定决定
- 带 `coach_id`（查他人）：需 `view_board_coach` 权限（`manager`/`head_coach`/`staff`），`assistant_id` 使用传入值；同 site 约束由 `user.site_id` 隐式保证
- 缺少对应权限统一返回 `HTTP 403 权限不足`

对应服务层变更：`performance_service.get_records()` 新增 `assistant_id_override` 参数。

响应字段新增：
- `dateGroups[].records[].isScattered`：散客标记（`member_id ≤ 0` 时为 `true`），前端据此将客户姓名置灰

---

## 15. 小程序备注 `/api/xcx/notes`

所有端点需 JWT（approved 状态）。

### POST `/api/xcx/notes`
创建备注（含星星评分，可选关联任务）。

请求体：
```json
{
  "target_type": "member",
  "target_id": 1,
  "content": "备注内容",
  "task_id": null,
  "rating_service_willingness": 4,
  "rating_revisit_likelihood": 3
}
```

### GET `/api/xcx/notes`
查询某目标的备注列表（按创建时间倒序）。

查询参数：
- `target_type`：目标类型（默认 `member`）
- `target_id`：目标 ID（必填）

### DELETE `/api/xcx/notes/{id}`
删除备注（验证归属后硬删除）。

---

## 16. 小程序配置 `/api/xcx/config`

所有端点需 JWT（approved 状态）。

### GET `/api/xcx/config/skill-types`
项目类型筛选器配置（CONFIG-1）。返回前端筛选器选项列表。

数据源：`app.v_cfg_area_category`（基于 `dws.cfg_area_category` 去重到 category 级别，排除 SPECIAL/OTHER，按 sort_order 排序）。

响应头部自动插入"不限"选项（key=ALL），不存储在数据库中。

响应：`SkillTypeItem[]`
```json
[
  { "key": "ALL", "label": "不限", "emoji": "🔍", "cls": "" },
  { "key": "BILLIARD", "label": "🎱 中式/追分", "emoji": "🎱", "cls": "" },
  { "key": "SNOOKER", "label": "斯诺克", "emoji": "斯", "cls": "" },
  { "key": "MAHJONG", "label": "🀄 麻将/棋牌", "emoji": "🀄", "cls": "" },
  { "key": "KTV", "label": "🎤 团建/K歌", "emoji": "🎤", "cls": "" }
]
```

降级行为：查询失败时返回空数组 `[]`。

---

## 17. 小程序三看板 `/api/xcx/board`

所有端点需 JWT + 对应权限。

### GET `/api/xcx/board/coaches`
助教看板（BOARD-1）。返回助教列表，支持排序×技能×时间三重筛选。

查询参数：
- `sort`：排序维度（`perf_desc`/`perf_asc`/`salary_desc`/`salary_asc`/`sv_desc`/`task_desc`，默认 `perf_desc`）
- `skill`：技能筛选（`ALL`/`BILLIARD`/`SNOOKER`/`MAHJONG`/`KTV`，默认 `ALL`）
- `time`：时间范围（`month`/`quarter`/`last_month`/`last_3m`/`last_quarter`/`last_6m`，默认 `month`）

约束：`time=last_6m` + `sort=sv_desc` 互斥，返回 400。

权限：`view_board_coach`

响应：`CoachBoardResponse`（含 `items` + `dimType`）

### GET `/api/xcx/board/customers`
客户看板（BOARD-2）。返回客户列表，支持维度×项目筛选 + 分页。

查询参数：
- `dimension`：客户维度（`recall`/`potential`/`balance`/`recharge`/`recent`/`spend60`/`freq60`/`loyal`，默认 `recall`）
- `project`：项目筛选（`ALL`/`BILLIARD`/`SNOOKER`/`MAHJONG`/`KTV`，默认 `ALL`）
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 20，最大 100）

权限：`view_board_customer`

响应：`CustomerBoardResponse`（含 `items`/`total`/`page`/`pageSize`）

### GET `/api/xcx/board/finance`
财务看板（BOARD-3）。返回 6 大板块（经营一览/预收资产/营收结构/现金流/支出/助教分析）。

查询参数：
- `time`：时间范围（`month`/`lastMonth`/`week`/`lastWeek`/`quarter3`/`quarter`/`lastQuarter`/`half6`，默认 `month`）
- `area`：区域筛选（`all`/`hall`/`hallA`/`hallB`/`hallC`/`mahjong`/`teamBuilding`，默认 `all`）
- `compare`：环比开关（0=关闭/1=开启，默认 0）

约束：`area≠all` 时 `recharge` 板块为 null；`compare=0` 时响应不含环比字段。

权限：`view_board_finance`

响应：`FinanceBoardResponse`（含 `overview`/`recharge`/`revenue`/`cashflow`/`expense`/`coachAnalysis`）

---

## 17A. 小程序助教详情 `/api/xcx/coaches`

所有端点需 JWT（approved 状态）+ `view_board_coach` 权限。

### GET `/api/xcx/coaches/{coach_id}/banner`
助教 banner 轻量信息。仅返回 `id` / `name` / `level` / `storeName`，用于 `coach-service-records` 等只需 banner 数据的页面首屏快速加载。

比 `/{coach_id}` 详情快一个数量级。

权限：`view_board_coach`

响应：`CoachBannerResponse`
```json
{ "id": 123, "name": "张三", "level": "金牌", "storeName": "朗朗桌球（总店）" }
```

### GET `/api/xcx/coaches/{coach_id}`
助教详情（COACH-1）。返回助教基础信息、绩效、收入、档位、任务分组、TOP 客户、近期服务、历史月份、备注。

权限：`view_board_coach`（2026-03-27 权限改造 W4：助教详情跟助教看板走）

响应：`CoachDetailResponse`

字段说明：

- `storeName`：助教所在门店名（跟随被查看助教所在门店，供小程序 banner 展示）
- `performance`：`PerformanceMetrics`，与任务页 `PerformanceSummary` 同源（来自 `monthly_summary` 实时值）。字段从 6 扩展到 25：
  - 核心字段：`totalHours`、`totalIncome`、`totalCustomers`、`monthLabel`、`tierNodes`、`basicHours`、`bonusHours`、`currentTier`（数组下标 0-based）、`nextTierHours`、`tierCompleted`、`bonusMoney`、`incomeTrend`、`incomeTrendDir`、`prevMonth`、`currentTierLabel`
  - 详情专属扩展：`customerBalance`、`tasksCompleted`
  - 兼容旧字段（前端渐进适配）：`monthlyHours`、`monthlySalary`
- `taskStats`：`CoachTaskStats`，当月任务完成统计（按 `task_type` 分类计数，数据源 `coach_tasks` 表）
  - `callback`：`follow_up_visit` 完成数
  - `recall`：`high_priority_recall` + `priority_recall` 完成数
- `topCustomers[].isScattered`：散客标识（`member_id ≤ 0`），前端据此将客户姓名置灰
- `serviceRecords[].isScattered`：同上，作用于近期服务记录

---

## 17B. 小程序客户详情 `/api/xcx/customers`

所有端点需 JWT（approved 状态）+ `view_board_customer` 权限。

### GET `/api/xcx/customers/{customer_id}`
客户详情（CUST-1）。返回客户基础信息、Banner 概览、AI 洞察、助教任务、心动助教、维客线索、消费记录、备注。

权限：`view_board_customer`（2026-03-27 权限改造 W4：客户详情跟客户看板走）

响应：`CustomerDetailResponse`

字段说明（本次新增）：

- `consumptionRecords[].foodDetail`：自定义食品类目名称（`string | null`）。为空时前端降级展示「食品酒水」
- `notes[].creatorName`：备注创建者姓名
- `notes[].creatorRole`：备注创建者角色

---

## 18. 小程序 CHAT `/api/xcx/chat`

所有端点需 JWT（approved 状态）。替代原 `xcx_ai_chat`（`/api/ai/*`），统一迁移到 `/api/xcx/chat/*` 路径。

### GET `/api/xcx/chat/history`
对话历史列表（CHAT-1）。返回当前用户的对话列表，按最后消息时间倒序。

查询参数：
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 20，最大 100）

响应：`ChatHistoryResponse`（含 `items`/`total`/`page`/`pageSize`）

每条对话包含：`id`（对话 ID）、`title`（对话标题）、`customerName`（关联客户姓名，可选）、`lastMessage`（最后消息摘要）、`timestamp`（最后消息时间）、`unreadCount`（未读数）。

### GET `/api/xcx/chat/messages`
通过上下文查询消息（CHAT-2b）。根据 `contextType` + `contextId` 自动查找或创建对话。

查询参数：
- `contextType`：上下文类型（`task`/`customer`/`coach`/`general`，必填）
- `contextId`：上下文 ID（必填）
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 50，最大 100）

对话复用规则：`task` 始终复用（无时限）；`customer`/`coach` ≤ 3 天复用、> 3 天新建；`general` 始终新建。

响应：`ChatMessagesResponse`（含 `chatId`/`items`/`total`/`page`/`pageSize`）

### GET `/api/xcx/chat/{chat_id}/messages`
通过 chatId 查询消息（CHAT-2a）。消息按 `created_at` 正序。

路径参数：
- `chat_id`：对话 ID

查询参数：
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 50，最大 100）

响应：`ChatMessagesResponse`

每条消息包含：`id`、`role`（`user`/`assistant`）、`content`、`createdAt`、`referenceCard`（可选，含 `type`/`title`/`summary`/`data` 键值对）。

### POST `/api/xcx/chat/{chat_id}/messages`
发送消息并获取同步 AI 回复（CHAT-3）。chatId 归属验证：不属于当前用户返回 403。

请求体：
```json
{ "content": "消息内容" }
```

响应：`SendMessageResponse`（含 `userMessage` 和 `aiReply`，各含 `id`/`content`/`createdAt`）

AI 失败降级：用户消息仍保存，`aiReply.content` 返回错误提示，HTTP 200。

### POST `/api/xcx/chat/stream`
SSE 流式对话端点（CHAT-4）。chatId 归属验证在流开始前完成。

请求体：
```json
{ "chatId": 1, "content": "消息内容" }
```

响应：`text/event-stream`（不经过 ResponseWrapper 包装）

SSE 事件类型：
- `event: message` — `data: {"token": "文本片段"}`
- `event: done` — `data: {"messageId": 123, "createdAt": "ISO8601"}`
- `event: error` — `data: {"message": "错误描述"}`

---

## 15. 维客线索 `/api/member-retention-clue`

替代原 `member-birthday` 端点，提供维客线索管理能力。

---

## 19. 租户管理后台认证 `/api/tenant/auth`

租户管理员独立认证体系，与小程序认证完全隔离。使用用户名+密码登录，JWT `aud=tenant-admin`。

### POST `/api/tenant/auth/login`
租户管理员登录。

请求体：
```json
{ "username": "admin01", "password": "..." }
```
响应：
```json
{ "access_token": "...", "refresh_token": "...", "token_type": "bearer" }
```
错误码：
- 401：用户名或密码错误（统一消息，不区分）
- 403：账号已被禁用（`is_active=false`）

### POST `/api/tenant/auth/refresh`
刷新租户管理员令牌。验证 `aud=tenant-admin` + `type=refresh`。

请求体：
```json
{ "refresh_token": "..." }
```
响应：同登录响应格式。

错误码：
- 401：无效的刷新令牌 / 令牌类型不匹配

---

## 20. 租户用户审核与管理 `/api/tenant`

所有端点需租户管理员 JWT（`aud=tenant-admin`），数据自动按 `managed_site_ids` 隔离。

### GET `/api/tenant/my-sites`
当前管理员管辖的店铺列表（用于前端筛选下拉）。

响应：
```json
[
  { "siteId": 1, "siteName": "XX球房", "siteCode": "LLQ001" }
]
```

### GET `/api/tenant/roles`
小程序可用角色列表（从 `auth.roles` 动态读取，排除 `tenant_admin`/`site_admin` 管理类角色）。

响应：`RoleItem[]`（含 `id`/`code`/`name`/`description`）

### GET `/api/tenant/site-staff`
按角色 + 门店查询人员候选列表。直连 ETL 库底层表，手动 `site_id` 过滤（FDW 视图 RLS 跨库不生效）。

查询参数：
- `role`：角色 code（`coach` → 查 `dwd.dim_assistant`，其他 → 查 `dwd.dim_staff`，必填）
- `site_id`：店铺 site_id（与 `site_code` 二选一）
- `site_code`：店铺编号（与 `site_id` 二选一）

响应：`StaffCandidate[]`（含 `id`/`identityLabel`/`name`/`mobile`/`entryTime`/`source`）

说明：
- coach 角色的 `identityLabel` 从 `dws.cfg_assistant_level_price` 配置表读取中文等级名，回退为数字
- 非 coach 角色的 `identityLabel` 使用 `dim_staff.job` 字段（职位名称）

### GET `/api/tenant/applications`
申请列表，支持状态筛选、门店筛选和分页。排除 `cancelled` 状态申请。

查询参数：
- `status`：按状态筛选（`pending`/`approved`/`rejected`，可选）
- `site_id`：按门店筛选（可选）
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 20，最大 100）

隔离策略：
- `tenant_admin`：按 `tenant_id` 过滤，覆盖该租户下所有店铺（不受 JWT `managed_site_ids` 限制）
- `site_admin`：按 `managed_site_ids` 精确过滤

响应：`{ items, total, page, pageSize }`

说明：`phone` 字段取自 `auth.user_applications.phone`（申请时填写，NOT NULL），非 `auth.users.phone`。

### GET `/api/tenant/applications/{id}/match-suggestions`
关联匹配建议。通过 `site_code_mapping` 解析 `site_id`，在 `v_dim_assistant`（`scd2_is_current=1`）和 `v_dim_staff` + `v_dim_staff_ex` 中按 phone 匹配。

响应：`MatchSuggestion[]`（含 `assistantId`/`staffId`/`name`/`number`/`sourceTable`）

### POST `/api/tenant/applications/{id}/approve`
审核通过。事务内多表写入：`users.status='approved'` → `user_site_roles` → `user_assistant_binding` → `user_applications.status='approved'`。

请求体：
```json
{ "role": "coach", "assistantId": 123, "staffId": null }
```
错误码：
- 409：该申请已被处理（非 pending 状态）

### POST `/api/tenant/applications/{id}/reject`
审核拒绝。

请求体：
```json
{ "reason": "拒绝原因" }
```
错误码：
- 409：该申请已被处理

### GET `/api/tenant/users`
已通过审核用户列表，支持角色筛选、关键词搜索和分页。

查询参数：
- `role`：按角色筛选（可选）
- `keyword`：关键词搜索（姓名/手机号，可选）
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 20，最大 100）

响应：`{ items, total, page, pageSize }`

### PATCH `/api/tenant/users/{id}`
编辑用户角色/门店/状态。

请求体（部分更新）：
```json
{ "role": "staff", "siteId": 2, "status": "disabled" }
```
错误码：
- 403：目标门店不在管辖范围内
- 404：用户不存在

### PUT `/api/tenant/users/{id}/binding`
更新用户助教/员工绑定。

请求体：
```json
{ "assistantId": 456, "staffId": null }
```

---

## 21. 租户 Excel 上传 `/api/tenant/excel`

所有端点需租户管理员 JWT。支持 4 种模板：财务支出（expense）、团购收入（platform_income）、助教奖罚（salary_adj）、充值业绩归属（recharge_commission）。

### POST `/api/tenant/excel/upload`
上传 Excel 文件，执行格式校验 → 人员匹配 → 冲突检测。

请求：`multipart/form-data`
- `file`：Excel 文件（.xlsx/.xls）
- `upload_type`：模板类型
- `site_id`：门店 ID

响应：
```json
{
  "errors": [],
  "warnings": [{ "rowIndex": 2, "column": "助教姓名", "message": "未匹配到助教/员工：张三" }],
  "passedRows": [...],
  "uploadId": 1,
  "conflicts": [{ "rowIndex": 3, "fieldDiffs": [{ "field": "金额", "oldValue": "100.00", "newValue": "200.00" }] }]
}
```
错误码：
- 400：无效的模板类型 / 无效的 Excel 文件 / 文件内容为空 / 解析失败

### POST `/api/tenant/excel/confirm`
确认写入。单事务写入目标表，失败回滚整批。

请求体：
```json
{
  "uploadId": 1,
  "resolutions": [
    { "rowIndex": 3, "action": "replace" },
    { "rowIndex": 5, "action": "keep" }
  ]
}
```
响应：
```json
{ "message": "写入成功", "inserted": 10, "updated": 2, "resolved": 3 }
```
错误码：
- 404：上传记录不存在
- 409：该上传批次已被处理

### GET `/api/tenant/excel/logs`
上传记录列表，分页，按 `managed_site_ids` 隔离。

查询参数：
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 20，最大 100）

响应：`{ items, total, page, pageSize }`

### GET `/api/tenant/excel/template/{type}`
下载空白 Excel 模板文件（含表头和格式说明）。

路径参数：
- `type`：模板类型（`expense`/`platform_income`/`salary_adj`/`recharge_commission`）

响应：`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`

---

## 22. 租户维客线索管理 `/api/tenant`

所有端点需租户管理员 JWT。线索操作先校验 `site_id` 是否在管辖范围内，不在则返回 404（避免泄露线索存在性）。

### GET `/api/tenant/customers/search`
客户搜索。在管辖门店范围内搜索 `v_dim_member`（nickname 模糊匹配 OR mobile 精确匹配，`scd2_is_current=1`）。手机号脱敏返回。

查询参数：
- `keyword`：搜索关键词（必填，最少 1 字符）
- `site_id`：指定门店 ID 筛选（可选）

响应：
```json
{
  "items": [
    { "memberId": 1, "nickname": "张三", "mobileMasked": "138****1234", "siteId": 1, "siteName": "XX球房" }
  ]
}
```

### GET `/api/tenant/customers/{member_id}/clues`
该客户在管辖门店范围内的全部线索，支持 source 和 is_hidden 筛选。

查询参数：
- `source`：按来源筛选（`manual`/`ai_consumption`/`ai_note`，可选）
- `is_hidden`：按隐藏状态筛选（可选）

响应：`{ items: ClueListItem[] }`

### PATCH `/api/tenant/clues/{id}`
编辑线索 category/summary/detail。

请求体：
```json
{ "category": "消费习惯", "summary": "每周消费 2-3 次", "detail": "偏好周末下午时段" }
```
错误码：
- 404：线索不存在（或不在管辖范围）
- 422：无效的线索大类 / 摘要为空或超长

### DELETE `/api/tenant/clues/{id}`
物理删除线索。

错误码：
- 404：线索不存在

### PATCH `/api/tenant/clues/{id}/visibility`
切换线索 is_hidden 状态。

请求体：
```json
{ "isHidden": true }
```

---

## 23. 租户店铺管理员 CRUD `/api/tenant/site-admins`

所有端点需租户管理员 JWT（`aud=tenant-admin`），且 `admin_type` 必须为 `tenant_admin`。店铺管理员（`admin_type='site_admin'`）无权访问。

### GET `/api/tenant/site-admins`
店铺管理员列表。

响应：`SiteAdmin[]`（含 `id`/`username`/`displayName`/`managedSiteIds`/`isActive`/`createdAt`）

### POST `/api/tenant/site-admins`
创建店铺管理员。`admin_type` 自动设为 `site_admin`，`tenant_id` 继承当前租户管理员。

请求体：
```json
{
  "username": "LL0001zhangsan",
  "password": "...",
  "displayName": "张三",
  "managedSiteIds": [1]
}
```

- 409：用户名已存在

### PATCH `/api/tenant/site-admins/{id}`
编辑店铺管理员（displayName / managedSiteIds / isActive）。

- 404：管理员不存在或不属于当前租户
- 422：至少需要提供一个修改字段

### DELETE `/api/tenant/site-admins/{id}`
软删除店铺管理员（`deleted_at` 设为当前时间）。

- 404：管理员不存在
- 409：管理员已被删除

### POST `/api/tenant/site-admins/{id}/reset-password`
重置店铺管理员密码。

请求体：
```json
{ "newPassword": "..." }
```

- 404：管理员不存在

---

## 24. 管理端租户管理员 CRUD `/api/admin`

所有端点需管理后台 JWT（`admin_users` 表认证）。

### GET `/api/admin/tenant-admins`
管理员列表，支持分页和关键词搜索。

查询参数：
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 20，最大 100）
- `keyword`：关键词搜索（用户名/显示名称，可选）
- `include_inactive`：是否包含已禁用管理员（bool，默认 `false`）

响应：`{ items, total, page, pageSize }`

每条记录新增 `tenant_name` 字段（来自 `biz.tenants` JOIN）。

### POST `/api/admin/tenant-admins`
创建租户管理员。密码 bcrypt 哈希存储。`tenant_id` 从 `biz.tenants` 选择，`managed_site_ids` 从 `biz.sites` 选择。

请求体：
```json
{
  "username": "admin01",
  "password": "...",
  "displayName": "管理员01",
  "tenantId": 1,
  "managedSiteIds": [1, 2, 3]
}
```
响应（201）：
```json
{ "id": 1, "createdAt": "2026-03-20T10:00:00" }
```
错误码：
- 409：用户名已存在

### PATCH `/api/admin/tenant-admins/{id}`
编辑租户管理员（username / display_name / managed_site_ids / is_active）。

请求体（部分更新）：
```json
{ "username": "newname", "displayName": "新名称", "managedSiteIds": [1, 2], "isActive": false }
```
错误码：
- 404：租户管理员不存在
- 409：用户名已存在（修改 username 时冲突）
- 422：至少需要提供一个修改字段

### DELETE `/api/admin/tenant-admins/{id}`
软删除租户管理员（`is_active=false`）。

错误码：
- 404：租户管理员不存在
- 409：管理员已被禁用

### POST `/api/admin/tenant-admins/{id}/reset-password`
重置租户管理员密码。

请求体：
```json
{ "newPassword": "..." }
```
错误码：
- 404：租户管理员不存在

### WebSocket `/ws/logs/{execution_id}`
实时日志推送。连接后自动接收指定执行的日志流。

---

## 25. 注册体系管理 `/api/admin`（admin_registry）

管理「连接器 → 租户 → 店铺」三级注册体系。所有端点需管理后台 JWT。

### GET `/api/admin/tenants`
所有活跃租户列表（含连接器名称）。

响应：
```json
[
  {
    "id": 1,
    "tenant_id": 12345,
    "tenant_name": "朗朗桌球",
    "connector_name": "飞球",
    "is_active": true
  }
]
```

### GET `/api/admin/tenants/{tenant_id}/sites`
指定租户下所有活跃店铺。

路径参数：
- `tenant_id`：租户 ID（`biz.tenants.id`）

响应：
```json
[
  {
    "id": 1,
    "site_id": 67890,
    "site_name": "XX球房",
    "site_code": "LLQ001",
    "site_label": "朗朗桌球XX店",
    "is_active": true
  }
]
```

### PUT `/api/admin/sites/{site_id}/site-code`
设置/修改店铺简写ID。事务内执行：旧 code 标记退役 → 新 code 插入历史 → 更新 sites.site_code。

路径参数：
- `site_id`：店铺 ID（`biz.sites.id`）

请求体：
```json
{ "new_code": "LLQ002" }
```

响应：
```json
{
  "site_id": 1,
  "old_code": "LLQ001",
  "new_code": "LLQ002",
  "history_cleaned": false
}
```

校验规则：
- 格式：6 位字符，3+3 模式，统一大写存储
- 全局唯一：`biz.sites.site_code` + `biz.site_code_history.site_code` 均不可重复

错误码：
- 400：格式不合法
- 409：简写ID 已被占用

### GET `/api/admin/sites/{site_id}/site-code-history`
查看店铺简写ID 变更历史。

路径参数：
- `site_id`：店铺 ID（`biz.sites.id`）

响应：
```json
[
  {
    "id": 1,
    "site_code": "LLQ001",
    "is_current": false,
    "created_at": "2026-03-20T10:00:00",
    "retired_at": "2026-03-22T15:30:00"
  },
  {
    "id": 2,
    "site_code": "LLQ002",
    "is_current": true,
    "created_at": "2026-03-22T15:30:00",
    "retired_at": null
  }
]
```

### POST `/api/admin/sites/sync`
手动触发店铺信息同步。通过 FDW 读取 ETL 库 `dwd.dim_site`（`scd2_is_current=1`），对比 `biz.sites`，新增/更新店铺信息。

响应：
```json
{ "inserted": 3, "updated": 1 }
```

---

## 26. AI 监控后台 `/api/admin/ai`

所有端点需管理后台 JWT（`Depends(require_admin)`）。P15 新增，13 个端点。

### GET `/api/admin/ai/dashboard`
AI 运行总览。返回今日统计、7 天趋势、App 分布、Token 预算、告警列表、App 健康状态。

查询参数：
- `site_id`：门店筛选（可选）

### GET `/api/admin/ai/trigger-jobs`
调度任务分页列表 + 今日去重统计。

查询参数：
- `event_type`、`status`、`site_id`、`date_from`、`date_to`：筛选（均可选）
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 20）

### GET `/api/admin/ai/trigger-jobs/{job_id}`
调度任务详情（含 payload、error_message）。

### POST `/api/admin/ai/trigger-jobs/{job_id}/retry`
手动重跑：创建新 trigger_job（`is_forced=true`），异步执行。

### GET `/api/admin/ai/run-logs`
调用记录分页列表。

查询参数：
- `app_type`、`status`、`trigger_type`、`site_id`、`date_from`、`date_to`：筛选（均可选）
- `page`、`page_size`：分页

### GET `/api/admin/ai/run-logs/{log_id}`
调用记录详情（含完整 prompt/response/error，不脱敏）。

### POST `/api/admin/ai/cache/invalidate`
批量缓存失效。

请求体：
```json
{ "site_id": 123, "app_type": "app3_clue", "member_id": 456 }
```
`site_id` 必填，`app_type` 和 `member_id` 可选。

### GET `/api/admin/ai/budget`
Token 预算使用情况（日/月已用量、上限、百分比）。

### POST `/api/admin/ai/batch-run`
创建批量执行请求，返回预估（不立即执行）。

请求体：
```json
{ "app_types": ["app3_clue", "app7_customer_analysis"], "member_ids": [1, 2, 3], "site_id": 123 }
```
响应：`{ batch_id, estimated_calls, estimated_tokens }`

### POST `/api/admin/ai/batch-run/confirm`
确认批量执行（`batch_id` 有效期 10 分钟）。

请求体：
```json
{ "batch_id": "uuid" }
```
错误码：
- 400：batch_id 无效或已过期
- 409：batch_id 已确认

### GET `/api/admin/ai/alerts`
告警列表（`ai_run_logs` WHERE status IN ('failed','timeout','circuit_open')）。

查询参数：
- `alert_status`：告警状态筛选（可选）
- `site_id`：门店筛选（可选）
- `page`、`page_size`：分页

### POST `/api/admin/ai/alerts/{log_id}/ack`
确认告警：`alert_status → acknowledged`。

### POST `/api/admin/ai/alerts/{log_id}/ignore`
忽略告警：`alert_status → ignored`。

---

## 27. 开发调试全链路日志 `/api/admin/dev-trace`

所有端点需管理后台 JWT + admin 角色（`Depends(require_admin)`），非 admin 返回 403。

DevTrace 模块提供开发阶段的全链路请求追踪能力，覆盖 HTTP 请求、SSE 流式响应、WebSocket 连接、后台 Job 执行、异常/错误、数据库连接生命周期和中间件层。日志以 JSON Lines 格式写入本地文件系统，仅用于开发调试，不影响生产环境。

### GET `/api/admin/dev-trace/dates`
返回有日志数据的日期列表。

响应：
```json
{ "dates": ["2026-03-23", "2026-03-22", "2026-03-21"] }
```

### GET `/api/admin/dev-trace/requests`
按条件分页查询请求列表。

查询参数：
- `date`：日期筛选（格式 `YYYY-MM-DD`，必填）
- `start_time`：开始时间（`HH:MM`，可选）
- `end_time`：结束时间（`HH:MM`，可选）
- `trace_type`：追踪类型筛选（`http`/`sse`/`ws`/`job`，可选）
- `method`：HTTP 方法筛选（`GET`/`POST`/`PUT`/`DELETE`，可选）
- `path_contains`：路径关键词模糊匹配（可选）
- `status_code`：HTTP 状态码精确匹配（可选）
- `min_duration`：最小耗时（毫秒，可选）
- `has_error`：是否包含错误（bool，可选）
- `span_type`：包含指定 span 类型的记录（可选）
- `page`：页码（默认 1）
- `page_size`：每页条数（默认 50）

响应：`{ items, total, page, pageSize }`

### GET `/api/admin/dev-trace/request/{request_id}`
返回指定 request_id 的完整 trace 记录（含所有 spans）。

路径参数：
- `request_id`：请求 ID

响应包含完整字段：`request_id`、`trace_type`、`timestamp`、`method`、`path`、`status_code`、`total_duration_ms`、`user_id`、`site_id`、`db_query_count`、`db_total_ms`、`error`、`spans`（span 数组，每个含 `span_type`/`module`/`function`/`description_zh`/`duration_ms`/`params`/`result_summary`/`extra`）。

错误码：
- 404：指定 request_id 不存在

### POST `/api/admin/dev-trace/cleanup`
按日期范围手动清理日志。

请求体：
```json
{ "start_date": "2026-03-20", "end_date": "2026-03-21" }
```

响应：
```json
{ "deleted_dates": ["2026-03-20", "2026-03-21"], "deleted_count": 2 }
```

### GET `/api/admin/dev-trace/settings`
返回当前 trace 设置。

响应：
```json
{
  "enabled": true,
  "log_dir": "export/dev-trace-logs",
  "retention_days": 7,
  "log_sql": true,
  "log_params": true,
  "coverage_scan_interval_minutes": 60
}
```

### PUT `/api/admin/dev-trace/settings`
更新运行时设置（不需重启，即时生效）。

请求体（部分更新）：
```json
{ "enabled": false, "retention_days": 14, "log_sql": false }
```

响应：更新后的完整设置对象（同 GET 响应格式）。

### GET `/api/admin/dev-trace/coverage`
返回最近一次覆盖率扫描结果。

响应：
```json
{
  "scanned_at": "2026-03-23T10:00:00",
  "route": { "total": 30, "covered": 28, "uncovered": ["func_a", "func_b"] },
  "service": { "total": 45, "covered": 40, "uncovered": ["svc_x", "svc_y", "svc_z", "svc_w", "svc_v"] },
  "job": { "total": 4, "covered": 4, "uncovered": [] },
  "sse": { "total": 1, "covered": 1, "uncovered": [] },
  "ws": { "total": 1, "covered": 1, "uncovered": [] }
}
```

### POST `/api/admin/dev-trace/coverage/scan`
手动触发覆盖率扫描。

响应：扫描完成后的覆盖率结果（同 GET 响应格式）。

---

## 28. 数据库健康监控 `/api/admin/db-health`

所有端点需管理后台 JWT。

### GET `/api/admin/db-health`
返回 4 个数据库的健康状态（etl_feiqiu / test_etl_feiqiu / zqyy_app / test_zqyy_app）。

对每个库执行诊断 SQL：`pg_stat_activity`（连接池）、`pg_database_size()`（大小）、慢查询统计。连接失败时返回 `status: "disconnected"`，其余字段为 null。即使所有库都连接失败，仍返回 HTTP 200。

响应：
```json
[
  {
    "db_name": "zqyy_app",
    "status": "connected",
    "active_connections": 5,
    "idle_connections": 3,
    "db_size_mb": 128.45,
    "slow_query_count": 0
  },
  {
    "db_name": "etl_feiqiu",
    "status": "disconnected",
    "active_connections": null,
    "idle_connections": null,
    "db_size_mb": null,
    "slow_query_count": null
  }
]
```

---

## 29. 触发器统一视图 `/api/admin/triggers`

所有端点需管理后台 JWT。

### GET `/api/admin/triggers/unified`
聚合三张表的触发器数据，返回统一格式列表。

数据源：
- `biz.trigger_jobs`（业务触发器）→ `source="biz"`
- `biz.ai_trigger_jobs`（AI 事件链，最近 100 条）→ `source="ai"`
- `public.scheduled_tasks`（ETL 调度）→ `source="etl"`

某数据源查询失败时记录日志，返回其他数据源数据。

响应：
```json
[
  {
    "id": 1,
    "name": "daily_sync",
    "source": "biz",
    "trigger_condition": "cron",
    "status": "active",
    "last_run_at": "2026-03-25T03:00:00",
    "next_run_at": "2026-03-26T03:00:00",
    "last_error": null
  }
]
```

---

## 30. 触发器配置编辑 `/api/trigger-jobs`

在已有的 `/api/trigger-jobs` 路由模块中新增 PATCH 端点。需管理后台 JWT。

### PATCH `/api/trigger-jobs/{id}/config`
编辑触发器的 `cron_expression` 或 `interval_seconds`。

仅 merge 请求中非 null 的字段到 `trigger_config` JSONB，不覆盖其他已有字段。更新后重新计算 `next_run_at`。

请求体（部分更新，至少提供一个字段）：
```json
{ "cron_expression": "0 3 * * 1", "interval_seconds": null }
```

校验规则：
- 空请求体（两个字段均为 null）→ 422
- `cron_expression` 格式无效（非 5 字段 / 超范围值）→ 422
- `interval_seconds < 1` → 422
- `job_id` 不存在 → 404

响应：更新后的完整 `TriggerJobItem`。

---

## 错误码约定

| HTTP 状态码 | 含义 |
|-------------|------|
| 200 | 成功 |
| 201 | 创建成功 |
| 400 | 请求参数错误 / SQL 执行错误 |
| 401 | 未认证 / 令牌无效 / 受限令牌 |
| 404 | 资源不存在 |
| 408 | 查询超时 |
| 409 | 状态冲突（如删除非 pending 任务） |
| 422 | 请求体验证失败 |
| 500 | 服务器内部错误 |
