Files
gaokao-volunteer-system/docs/plans/T6-admin-mvp.md

384 lines
16 KiB
Markdown
Raw Normal View History

2026-06-13 14:49:58 +08:00
# T6 管理后台 MVP — 详细设计
**状态**: v2.1 实施设计
**最后更新**: 2026-06-12
**关联文档**: [IMPLEMENTATION_PLAN_v2.md](../IMPLEMENTATION_PLAN_v2.md), [TECH_ARCHITECTURE.md](../TECH_ARCHITECTURE.md)
**关联任务**: T6.1 FastAPI 后端基础 / T6.2 仪表盘 / T6.3 用户管理 / T6.4 订单管理 / T6.5 案例管理 / T6.6 简易 WebUI / T6.7 Docker 部署
---
## 1. 设计目标
### 1.1 总目标
为运营人员提供一个最小可用的 Web 管理后台,承接订单/用户/案例/审核报告的核心 CRUD + 统计展示,覆盖 2026 高考季6/25-7/5的运营需求。
### 1.2 非目标
- ❌ 复杂 RBAC 权限体系MVP 用 admin 单一角色,后续 T8 扩展)
- ❌ 重前端Vue/React—— 走 ECharts + 纯 HTML/JSv2.1 不引前端框架)
- ❌ 高并发/集群部署 —— 用户基数小(目标 100+ 考生),单机 SQLite 足够
- ❌ 完整 OpenAPI 兼容性 —— 自动 Swagger 文档即可
### 1.3 关键原则
| 原则 | 说明 |
| ------------------- | --------------------------------------------------------------------- |
| **本地优先** | 默认本地 SQLite云端可选 |
| **零侵入宿主** | 不依赖外部子项目,仅调用本项目 `data/orders/` 已实现层 |
| **测试优先** | 每个路由有 test client 测试;覆盖率目标 >= 70% |
| **最小风险闭环** | T6.1 只做骨架(启动+路由+JWT+Swagger业务路由在 T6.2-T6.5 增量实现 |
| **明文/加密分字段** | API 层明文接收,落盘前调用 T4.1 加密层 |
---
## 2. 技术选型v2.1 决策)
| 维度 | 选择 | 理由 |
| ----------- | ---------------------------- | ------------------------------------------------- |
| Web 框架 | **FastAPI 0.133** | 自动 OpenAPI、依赖注入、类型驱动 |
| ASGI 服务器 | **uvicorn 0.41** | 标配 FastAPI 部署 |
| 认证 | **PyJWT 2.13 (HS256)** | 标准 JWT密钥从环境变量读取 |
| ORM | **不使用 SQLAlchemy** | 保持项目"零运行时第三方"约束;用 stdlib `sqlite3` |
| Pydantic | **Pydantic v2.13** | FastAPI 强依赖;用 BaseModel 做请求/响应模型 |
| 密码哈希 | stdlib `hashlib.pbkdf2_hmac` | 不引 bcrypt/argon2MVP 简化) |
| 测试 | pytest + httpx TestClient | httpx 0.28 已装 |
**为什么不用 SQLAlchemy**:项目 README/requirements-dev.txt 明确"运行时本项目为纯标准库实现,无第三方运行时依赖"。T4.1 已用 stdlib `sqlite3` + 自写 dataclass。引入 SQLAlchemy 会破坏这个约束。DAO 层用 `sqlite3` + `dataclass` 即可覆盖 CRUD。
---
## 3. 模块结构
```
admin/
├── __init__.py # 模块导出
├── app.py # FastAPI app 工厂 (create_app)
├── config.py # 配置加载(环境变量 + .env 兜底)
├── db.py # SQLite 连接管理 + DAO 工厂
├── auth.py # JWT 签发/校验/依赖
├── password.py # PBKDF2 哈希
├── routes/
│ ├── __init__.py
│ ├── health.py # GET /health (公开)
│ ├── auth.py # POST /api/auth/login, GET /api/auth/me
│ ├── orders.py # T6.4 订单管理(列表/详情/录单/PATCH/导出)
│ ├── stats.py # GET /api/stats/orders (骨架,T6.2 完善)
│ └── meta.py # GET /api/meta (province 列等)
└── tests/
├── __init__.py
├── conftest.py # FastAPI TestClient + 内存 SQLite fixture
├── test_app.py # app 启动 + Swagger 可达
├── test_auth.py # JWT 签发/校验 + 401 路径
├── test_routes_health.py
├── test_routes_auth.py
└── test_routes_orders.py
```
### 3.1 顶层路由v2.1 MVP 阶段)
| 方法 | 路径 | 鉴权 | 说明 | 阶段 |
| ----- | -------------------- | ---- | ------------------------------ | ---- |
| GET | `/health` | 公开 | 健康检查DB + 版本) | T6.1 |
| GET | `/docs` | 公开 | Swagger UIFastAPI 自动) | T6.1 |
| GET | `/openapi.json` | 公开 | OpenAPI Schema | T6.1 |
| POST | `/api/auth/login` | 公开 | 用户名+密码 → JWT | T6.1 |
| GET | `/api/auth/me` | JWT | 当前用户信息 | T6.1 |
| GET | `/api/orders` | JWT | 订单列表(真实 DAO + 脱敏) | T6.4 |
| GET | `/api/orders/export` | JWT | CSV 导出(默认脱敏) | T6.4 |
| GET | `/api/orders/{id}` | JWT | 订单详情 + 状态历史 | T6.4 |
| POST | `/api/orders` | JWT | 手工录单external_id 可空) | T6.4 |
| PATCH | `/api/orders/{id}` | JWT | 业务字段更新 / 状态流转 / 退款 | T6.4 |
| GET | `/api/stats/orders` | JWT | 订单统计(占位,空对象) | T6.1 |
| GET | `/api/meta` | JWT | 元数据(省份列等) | T6.1 |
> T6.2 / T6.3 / T6.4 已分别落地仪表盘、用户管理与订单管理;后续剩余案例 CRUD、登录页 + 列表页等增量任务。
---
## 4. 数据模型
### 4.1 复用 T4.1
- `data/orders/models.py::Order` — 订单 dataclass
- `data/orders/schema.py::apply_schema` — DDL
- `data/orders/crypto.py::encrypt/decrypt/hash_for_index` — 字段加密
- `data/orders/state_machine.py::OrderStatus` — 6 态枚举
### 4.2 新增admin_users 表T6.1 引入)
```sql
CREATE TABLE IF NOT EXISTS admin_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL, -- 存储格式: <salt_hex>$<pbkdf2_hash_hex>
role TEXT NOT NULL DEFAULT 'admin', -- 后续 T8 RBAC 扩展
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
last_login_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_admin_users_username ON admin_users(username);
```
### 4.3 决策点
- **只允许单 admin?** ❌ — 支持多账户(运营多人轮班)
- **密码存储?** PBKDF2-HMAC-SHA256, 200k 迭代, 16B salt → 64B hash
- **默认账户?** 启动时若 `admin_users` 为空,自动创建 `admin/admin123`,并在日志 WARN 提示修改
---
## 5. JWT 设计
### 5.1 Token 结构HS256
```json
{
"sub": "admin:1", // 用户标识
"username": "admin",
"role": "admin",
"iat": 1718172800,
"exp": 1718176400 // 默认 1 小时
}
```
### 5.2 配置
| 环境变量 | 默认值 | 说明 |
| -------------------- | ---------------------------- | ------------------- |
| `GAOKAO_JWT_SECRET` | dev-only 占位(启动时 WARN | HS256 密钥(>=32B |
| `GAOKAO_JWT_EXP_MIN` | 60 | 过期时间(分钟) |
| `GAOKAO_DB_PATH` | `data/orders/admin.db` | SQLite 路径 |
| `GAOKAO_ENV` | `dev` | dev/prod 标识 |
> **安全警告**:生产部署必须显式设置 `GAOKAO_JWT_SECRET` 为高熵随机值。MVP 阶段在 `dev` 环境用占位即可,但启动日志必须明显提示。
### 5.3 中间件行为
- 请求带 `Authorization: Bearer <token>` → 解析 token,设置 `request.state.user`
- 无 token / 过期 / 签名错 → 返回 401 `WWW-Authenticate: Bearer`
- 受保护路由通过 `Depends(get_current_user)` 获取当前用户
- 公开路由health, login, docs, openapi走白名单
---
## 6. Swagger / OpenAPI
### 6.1 自动生成
FastAPI 自动生成 `/docs` (Swagger UI) 和 `/openapi.json`。无需手写。
### 6.2 元信息增强
```python
app = FastAPI(
title="高考志愿填报管理后台 API",
version="0.1.0",
description="""管理后台 MVP API。
认证:POST /api/auth/login → Bearer JWT。
详细字段:data/orders/models.py""",
contact={"name": "Hermes Agent"},
license_info={"name": "MIT"},
)
```
### 6.3 标签分组
- `health` — 健康检查
- `auth` — 认证
- `orders` — 订单
- `stats` — 统计
---
## 7. 测试策略TDD
### 7.1 测试框架
- `pytest` + `fastapi.testclient.TestClient`(内部用 httpxdev 依赖需显式安装)
- fixture内存 SQLite`sqlite3.connect(':memory:')`+ 自动建表 + 自动建 admin 账户
- 每个测试函数独立 DB避免状态污染
### 7.2 必测项
| 项 | 验证 |
| ------------------------------- | --------------------------- |
| `GET /health` | 200 + `{"status":"ok"}` |
| `GET /docs` | 200 HTML |
| `GET /openapi.json` | 200 JSON, 含 `paths` |
| `POST /api/auth/login` 正确凭证 | 200 + JWT |
| `POST /api/auth/login` 错凭证 | 401 |
| `GET /api/auth/me` 无 token | 401 |
| `GET /api/auth/me` 有效 token | 200 + 用户信息 |
| `GET /api/orders` 无 token | 401 |
| `GET /api/orders` 有效 token | 200 + `[]` (MVP 阶段无数据) |
| `GET /api/orders/{id}` 不存在 | 404 |
| JWT 过期 | 401 |
| JWT 签名错 | 401 |
| `pytest --cov admin` | 覆盖率 >= 70% |
### 7.3 覆盖率门槛
参考 v2.1 实施计划:核心包覆盖率 >= 70%。T6.1 完成后跑:
```bash
cd /home/long/project/gaokao-volunteer-system
python3 -m pytest admin/tests/ --cov=admin --cov-report=term-missing
```
---
## 8. 部署
### 8.1 MVP 本地启动
```bash
# 1. 安装依赖(一次性)
pip3 install --user --break-system-packages fastapi uvicorn pyjwt httpx pydantic
# 2. 启动服务
export GAOKAO_JWT_SECRET="$(python3 -c 'import secrets;print(secrets.token_hex(32))')"
python3 -m admin.app --port 8000
# 3. 访问
open http://localhost:8000/docs
```
### 8.2 T6.7 Docker 阶段(不在 T6.1 范围)
T6.7 阶段出 Dockerfile + docker-compose.yml。
---
## 9. 风险与缓解
| 风险 | 概率 | 影响 | 缓解 |
| -------------------------- | ---- | ---- | -------------------------------------------- |
| FastAPI 升级破坏兼容 | 低 | 中 | 锁版本fastapi==0.133.1, pydantic==2.13.4 |
| 默认 admin 账户泄露 | 中 | 高 | 启动日志 WARN + README 强调 prod 必须改密 |
| JWT secret 弱 | 中 | 高 | dev 占位 + 启动日志断言长度 |
| SQLite 并发写冲突 | 低 | 低 | MVP 单机 + 启用 WAL |
| 与 T4.1 dataclass 耦合过紧 | 低 | 中 | DAO 层明确接口边界 |
---
## 10. T6.2 仪表盘(一站式数据统计)
### 10.1 设计目标
为运营人员提供一个最小可用的仪表盘端点:返回订单数 / 用户数 / 收入三项汇总
卡片,叠加 6 态分布 / 来源分布 / 服务版本分布三个柱状图数据源,并给出
今日 / 7 天 / 30 天三个窗口的趋势序列日粒度0 填充)。
### 10.2 端点
| 方法 | 路径 | 鉴权 | 说明 |
| ---- | ---------------------- | ---- | --------------------------------------------------- |
| GET | `/api/stats/dashboard` | JWT | 一站式仪表盘 payload |
| GET | `/api/stats/orders` | JWT | 订单维度统计(沿用 T6.1 stub 字段名,接入真实 SQL |
### 10.3 响应契约
```json
{
"summary": {
"total_orders": 6, "total_revenue_cents": 100000, "total_users": 1,
"orders_today": 3, "orders_7d": 4, "orders_30d": 5,
"revenue_today_cents": 20000, "revenue_7d_cents": 70000, "revenue_30d_cents": 100000
},
"by_status": {"pending": 0, "paid": 1, "serving": 1, "delivered": 0, "completed": 1, "refunded": 1},
"by_source": {"xianyu": 0, "wechat": 0, "web": 0, "school": 0},
"by_service_version":{"audit": 0, "basic": 0, "standard": 0, "premium": 0},
"trends": {
"today": [{"date": "2026-06-12", "orders": 3, "revenue_cents": 20000}],
"7d": [{"date": "2026-06-06", "orders": 0, "revenue_cents": 0}, ... 共 7 个点 ...],
"30d": [{"date": "2026-05-14", "orders": 0, "revenue_cents": 0}, ... 共 30 个点 ...]
},
"generated_at": "2026-06-12T16:30:00+00:00"
}
```
### 10.4 口径约定(必须文档化)
- **收入 (revenue_cents)** = 所有 **非 pending 且非 refunded** 订单的
`amount_cents` 累计值。
- `pending`:未付款,不计入有效收入
- `refunded`:已退款,从累计收入中扣除
- `paid / serving / delivered / completed`:四态均计入
- **趋势桶粒度** = 日UTC`YYYY-MM-DD`)。`substr(created_at, 1, 10)`
ISO8601 字符串前缀等价。
- **"今日"** = 服务器当前 UTC 日;**7d / 30d** = 含今日回溯 7 / 30 个完整日。
- **0 填充**:窗口内的"无订单日"也要返回 0 点,前端拿到的是稠密序列。
- **数据源隔离**
- `orders` / `order_status_history``GAOKAO_ORDERS_DB_PATH`(默认 `data/orders.db`
- `admin_users``GAOKAO_DB_PATH`(默认 `data/orders/admin.db`
- **不读 PII**:统计路径只触碰 `amount_cents` / `status` / `source` /
`service_version` / `created_at`,不进入加密层。
### 10.5 模块分层
- `admin/stats.py`:纯函数 SQL 聚合层(`build_dashboard_payload` /
`compute_summary` / `compute_by_*` / `compute_trends` / `generate_day_series`
- 可单测:纯函数 + 可注入 `today` 参数
- 与路由层解耦:业务测试不依赖 FastAPI
- `admin/routes/stats.py`FastAPI 路由层
- 仅鉴权 + 响应包装
- `DashboardResponse` / `OrderStatsResponse` 两个 pydantic 模型
### 10.6 配置新增
- `Settings.orders_db_path``GAOKAO_ORDERS_DB_PATH`,默认 `data/orders.db`
-`data.orders.*` 共享同一 DB
-`data.channel_sync.webhook_server` 已有的同名环境变量对齐
### 10.7 测试覆盖11 用例)
`admin/tests/test_routes_stats_dashboard.py`
- 鉴权(无 token 401 / 有 token 200
- 空库形状契约summary / by\_\* / trends 三层结构稳定
- 趋势序列1 / 7 / 30 个点,按日期严格升序
- 0 填充点:包含完整三字段(`date` / `orders` / `revenue_cents`
- 真实数据窗口边界45 天前的订单被 30d 窗口排除,但计入 total
- 收入口径pending / refunded 不计入 revenue
- 0 填充:范围内无订单的日也返回 0 点
- 兼容层:`/api/stats/orders` 字段名不变,`_stub` 标记已移除
### 10.8 T6.2 DoD
- [x] `admin/stats.py` 纯函数聚合层落地
- [x] `/api/stats/dashboard` + `/api/stats/orders` 双端点接入
- [x] 11 个 dashboard 测试 + 1 个真实数据兼容测试,全部通过
- [x] admin 全量 87/87 测试通过repo 全量 412/412 测试通过
- [x] 新增代码 ruff check 通过
- [x] 文档同步:本文档新增 §10、README 增量更新、CHANGELOG 记录
---
## 11. 完成标准DoD
T6.1 完成的硬标准:
- [x] 设计文档(本文件)
- [x] `admin/` 模块代码app/config/db/auth/password/routes
- [x] 测试套件通过(覆盖率 >= 70%
- [x] `python3 -m admin.app` 启动成功
- [x] `http://localhost:8000/docs` 可访问
- [x] `http://localhost:8000/openapi.json` 返回合法 OpenAPI schema
- [x] `POST /api/auth/login` + `GET /api/auth/me` 端到端通
- [x] 默认 admin 账户创建,WARN 日志输出
- [x] requirements-dev.txt 或新 requirements-admin.txt 列出依赖
- [x] AGENTS.md / README.md 增量更新T6.1 章节)
- [x] git commit + push 三仓库
---
## 12. 后续任务衔接
- [x] **T6.2** (仪表盘)`/api/stats/dashboard` + `/api/stats/orders` 已落地(见 §10
- [x] **T6.3** (用户管理)`/api/admin/users` 列表 / 详情 / 脱敏 / 搜索已落地
- [x] **T6.4** (订单管理)`/api/orders` 已补齐列表 / 详情 / POST / PATCH / export状态变化统一走 `state_machine`
- **T6.5** (案例管理):新增 `data/cases/` 表 + `/api/cases` CRUD
- **T6.6** (WebUI)HTML 模板 + 静态托管
- **T6.7** (Docker)Dockerfile + compose