feat: P1-P3 全栈集成 — 数据库基础 + DWS 扩展 + 小程序鉴权 + 工程化体系
## P1 数据库基础 - zqyy_app: 创建 auth/biz schema、FDW 连接 etl_feiqiu - etl_feiqiu: 创建 app schema RLS 视图、商品库存预警表 - 清理 assistant_abolish 残留数据 ## P2 ETL/DWS 扩展 - 新增 DWS 助教订单贡献度表 (dws.assistant_order_contribution) - 新增 assistant_order_contribution_task 任务及 RLS 视图 - member_consumption 增加充值字段、assistant_daily 增加处罚字段 - 更新 ODS/DWD/DWS 任务文档及业务规则文档 - 更新 consistency_checker、flow_runner、task_registry 等核心模块 ## P3 小程序鉴权系统 - 新增 xcx_auth 路由/schema(微信登录 + JWT) - 新增 wechat/role/matching/application 服务层 - zqyy_app 鉴权表迁移 + 角色权限种子数据 - auth/dependencies.py 支持小程序 JWT 鉴权 ## 文档与审计 - 新增 DOCUMENTATION-MAP 文档导航 - 新增 7 份 BD_Manual 数据库变更文档 - 更新 DDL 基线快照(etl_feiqiu 6 schema + zqyy_app auth) - 新增全栈集成审计记录、部署检查清单更新 - 新增 BACKLOG 路线图、FDW→Core 迁移计划 ## Kiro 工程化 - 新增 5 个 Spec(P1/P2/P3/全栈集成/核心业务) - 新增审计自动化脚本(agent_on_stop/build_audit_context/compliance_prescan) - 新增 6 个 Hook(合规检查/会话日志/提交审计等) - 新增 doc-map steering 文件 ## 运维与测试 - 新增 ops 脚本:迁移验证/API 健康检查/ETL 监控/集成报告 - 新增属性测试:test_dws_contribution / test_auth_system - 清理过期 export 报告文件 - 更新 .gitignore 排除规则
This commit is contained in:
1
.kiro/specs/03-miniapp-auth-system/.config.kiro
Normal file
1
.kiro/specs/03-miniapp-auth-system/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"generationMode": "requirements-first"}
|
||||
859
.kiro/specs/03-miniapp-auth-system/design.md
Normal file
859
.kiro/specs/03-miniapp-auth-system/design.md
Normal file
@@ -0,0 +1,859 @@
|
||||
# 设计文档:小程序用户认证系统(miniapp-auth-system)
|
||||
|
||||
## 概述
|
||||
|
||||
本设计在 P1(miniapp-db-foundation)已建立的 `auth` Schema 基础上,实现完整的小程序用户认证链路:
|
||||
|
||||
1. **微信登录**:小程序端发送 `code` → 后端调用微信 `code2Session` → 获取 `openid` → 创建/查找用户 → 签发 JWT
|
||||
2. **用户申请**:新用户填写球房ID + 手机号 + 申请身份 → 系统自动匹配助教/员工 → 管理员审核
|
||||
3. **权限控制**:基于 `user_site_roles` + `role_permissions` 的 RBAC 模型,权限中间件拦截无权请求
|
||||
4. **多店铺支持**:一个用户可关联多个 `site_id`,切换店铺时重新签发 JWT
|
||||
|
||||
**环境变量依赖**:
|
||||
|
||||
| 环境变量 | 用途 | 来源 |
|
||||
|---------|------|------|
|
||||
| `APP_DB_DSN` / `DB_HOST` 等 | 业务库连接 | 根 `.env` |
|
||||
| `PG_DSN` / `ETL_DB_HOST` 等 | ETL 库连接(FDW 匹配) | 根 `.env` |
|
||||
| `JWT_SECRET_KEY` | JWT 签名密钥 | `.env.local` |
|
||||
| `WX_APPID` | 微信小程序 AppID | `.env.local` |
|
||||
| `WX_SECRET` | 微信小程序 AppSecret | `.env.local` |
|
||||
|
||||
**整体认证流程**:
|
||||
|
||||
```
|
||||
小程序端 FastAPI 后端 微信服务器
|
||||
│ │ │
|
||||
│── wx.login() ──► │ │
|
||||
│ 获取 code │ │
|
||||
│ │ │
|
||||
│── POST /api/xcx/login ──► │ │
|
||||
│ {code} │── GET code2Session ──────────► │
|
||||
│ │◄── {openid, session_key} ──── │
|
||||
│ │ │
|
||||
│ │── 查找/创建 auth.users ──► │
|
||||
│ │── 签发 JWT ──► │
|
||||
│◄── {jwt, status} ─────── │ │
|
||||
│ │ │
|
||||
│ [status=pending] │ │
|
||||
│── POST /api/xcx/apply ──► │ │
|
||||
│ {site_code, phone, ...} │── 创建 user_applications ──► │
|
||||
│◄── {application_id} ───── │ │
|
||||
```
|
||||
|
||||
## 架构
|
||||
|
||||
### 分层架构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "小程序端"
|
||||
MP["微信小程序<br/>wx.login() / wx.request()"]
|
||||
end
|
||||
|
||||
subgraph "FastAPI 后端(apps/backend/)"
|
||||
subgraph "路由层"
|
||||
XCX_AUTH["routers/xcx_auth.py<br/>微信登录 + 申请"]
|
||||
XCX_USER["routers/xcx_user.py<br/>用户状态 + 店铺切换"]
|
||||
ADMIN_APP["routers/admin_applications.py<br/>管理端审核"]
|
||||
end
|
||||
|
||||
subgraph "中间件层"
|
||||
PERM_MW["middleware/permission.py<br/>权限中间件"]
|
||||
end
|
||||
|
||||
subgraph "服务层"
|
||||
WX_SVC["services/wechat.py<br/>code2Session 调用"]
|
||||
APP_SVC["services/application.py<br/>申请 CRUD + 审核"]
|
||||
MATCH_SVC["services/matching.py<br/>人员匹配"]
|
||||
ROLE_SVC["services/role.py<br/>角色权限查询"]
|
||||
end
|
||||
|
||||
subgraph "认证层(已有 + 扩展)"
|
||||
JWT["auth/jwt.py<br/>JWT 签发/验证(扩展)"]
|
||||
DEPS["auth/dependencies.py<br/>依赖注入(扩展)"]
|
||||
end
|
||||
|
||||
DB["database.py<br/>数据库连接"]
|
||||
end
|
||||
|
||||
subgraph "数据库"
|
||||
AUTH_SCHEMA["auth Schema<br/>users / applications / roles / ..."]
|
||||
FDW_ETL["fdw_etl Schema<br/>v_dim_assistant / v_dim_staff"]
|
||||
end
|
||||
|
||||
subgraph "外部服务"
|
||||
WX_API["微信 API<br/>code2Session"]
|
||||
end
|
||||
|
||||
MP --> XCX_AUTH
|
||||
MP --> XCX_USER
|
||||
XCX_AUTH --> PERM_MW
|
||||
XCX_USER --> PERM_MW
|
||||
ADMIN_APP --> PERM_MW
|
||||
PERM_MW --> JWT
|
||||
PERM_MW --> DEPS
|
||||
XCX_AUTH --> WX_SVC
|
||||
XCX_AUTH --> APP_SVC
|
||||
ADMIN_APP --> APP_SVC
|
||||
ADMIN_APP --> MATCH_SVC
|
||||
XCX_USER --> ROLE_SVC
|
||||
WX_SVC --> WX_API
|
||||
APP_SVC --> DB
|
||||
MATCH_SVC --> DB
|
||||
ROLE_SVC --> DB
|
||||
DB --> AUTH_SCHEMA
|
||||
DB --> FDW_ETL
|
||||
```
|
||||
|
||||
### 请求处理流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant MP as 小程序
|
||||
participant MW as Permission Middleware
|
||||
participant R as Router
|
||||
participant S as Service
|
||||
participant DB as PostgreSQL
|
||||
|
||||
MP->>R: POST /api/xcx/login {code}
|
||||
R->>S: wechat.code2session(code)
|
||||
S-->>R: openid
|
||||
R->>DB: SELECT FROM auth.users WHERE wx_openid = ?
|
||||
alt 用户不存在
|
||||
R->>DB: INSERT INTO auth.users
|
||||
end
|
||||
R->>R: jwt.create_token_pair(user_id, site_id)
|
||||
R-->>MP: {access_token, refresh_token, status}
|
||||
|
||||
Note over MP,DB: 后续请求(需认证)
|
||||
|
||||
MP->>MW: GET /api/xcx/... (Bearer token)
|
||||
MW->>MW: decode_access_token(token)
|
||||
MW->>DB: SELECT permissions FROM auth.user_site_roles JOIN ...
|
||||
alt 权限不足
|
||||
MW-->>MP: 403 Forbidden
|
||||
else 权限通过
|
||||
MW->>R: 放行
|
||||
R->>S: 业务逻辑
|
||||
S->>DB: 数据操作
|
||||
R-->>MP: 200 OK
|
||||
end
|
||||
```
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 组件 1:微信认证服务(services/wechat.py)
|
||||
|
||||
**职责**:封装微信 `code2Session` API 调用。
|
||||
|
||||
```python
|
||||
import httpx
|
||||
from app.config import get
|
||||
|
||||
WX_APPID: str = get("WX_APPID", "")
|
||||
WX_SECRET: str = get("WX_SECRET", "")
|
||||
CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session"
|
||||
|
||||
async def code2session(code: str) -> dict:
|
||||
"""
|
||||
调用微信 code2Session 接口。
|
||||
|
||||
返回:
|
||||
{"openid": str, "session_key": str, "unionid": str | None}
|
||||
|
||||
异常:
|
||||
WeChatAuthError: 微信接口返回错误码时抛出
|
||||
"""
|
||||
...
|
||||
|
||||
class WeChatAuthError(Exception):
|
||||
"""微信认证错误,包含 errcode 和 errmsg。"""
|
||||
def __init__(self, errcode: int, errmsg: str): ...
|
||||
```
|
||||
|
||||
**设计决策**:
|
||||
- 使用 `httpx.AsyncClient` 异步调用微信 API,与 FastAPI 异步模型一致
|
||||
- `WX_APPID` / `WX_SECRET` 从环境变量读取,缺失时在调用时报错(而非启动时,因为非所有端点都需要微信认证)
|
||||
|
||||
### 组件 2:申请服务(services/application.py)
|
||||
|
||||
**职责**:处理用户申请的创建、查询、审核。
|
||||
|
||||
```python
|
||||
async def create_application(
|
||||
user_id: int,
|
||||
site_code: str,
|
||||
applied_role_text: str,
|
||||
phone: str,
|
||||
employee_number: str | None = None,
|
||||
nickname: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
创建用户申请。
|
||||
|
||||
1. 查找 site_code → site_id 映射
|
||||
2. 检查是否有 pending 申请(有则 409)
|
||||
3. 插入 user_applications 记录
|
||||
4. 更新 users.nickname(如提供)
|
||||
"""
|
||||
...
|
||||
|
||||
async def approve_application(
|
||||
application_id: int,
|
||||
reviewer_id: int,
|
||||
role_id: int,
|
||||
binding: dict | None = None, # {"assistant_id": ..., "staff_id": ..., "binding_type": ...}
|
||||
review_note: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
批准申请。
|
||||
|
||||
1. 检查申请状态为 pending(否则 409)
|
||||
2. 更新 user_applications.status = 'approved'
|
||||
3. 创建 user_site_roles 记录
|
||||
4. 创建 user_assistant_binding 记录(如有 binding)
|
||||
5. 更新 users.status = 'approved'(如果是首次通过)
|
||||
6. 记录 reviewer_id 和 reviewed_at
|
||||
"""
|
||||
...
|
||||
|
||||
async def reject_application(
|
||||
application_id: int,
|
||||
reviewer_id: int,
|
||||
review_note: str,
|
||||
) -> dict:
|
||||
"""
|
||||
拒绝申请。
|
||||
|
||||
1. 检查申请状态为 pending(否则 409)
|
||||
2. 更新 user_applications.status = 'rejected'
|
||||
3. 记录 reviewer_id、review_note、reviewed_at
|
||||
"""
|
||||
...
|
||||
|
||||
async def get_user_applications(user_id: int) -> list[dict]:
|
||||
"""查询用户的所有申请记录。"""
|
||||
...
|
||||
```
|
||||
|
||||
### 组件 3:人员匹配服务(services/matching.py)
|
||||
|
||||
**职责**:根据申请信息在 FDW 外部表中查找候选匹配。
|
||||
|
||||
```python
|
||||
async def find_candidates(
|
||||
site_id: int,
|
||||
phone: str,
|
||||
employee_number: str | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
在助教表和员工表中查找匹配候选。
|
||||
|
||||
查询逻辑:
|
||||
1. fdw_etl.v_dim_assistant: WHERE site_id = ? AND mobile = ?
|
||||
2. fdw_etl.v_dim_staff + v_dim_staff_ex: WHERE site_id = ? AND (mobile = ? OR job_num = ?)
|
||||
3. 合并结果,每条包含 source_type / name / mobile / job_num
|
||||
|
||||
注意:查询 FDW 外部表前需设置 app.current_site_id(RLS 隔离)。
|
||||
但 fdw_etl 中的外部表映射的是 app schema 的 RLS 视图,
|
||||
所以需要在 ETL 库连接上设置 site_id。
|
||||
实际上,我们直接在业务库通过 fdw_etl 查询,
|
||||
FDW 会透传 session 变量到远端。
|
||||
|
||||
返回:
|
||||
[{"source_type": "assistant"|"staff", "id": int, "name": str, "mobile": str, "job_num": str | None}]
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
**设计决策**:
|
||||
- FDW 查询需要在业务库连接上设置 `app.current_site_id`,因为 FDW 外部表映射的是 ETL 库 `app` Schema 的 RLS 视图
|
||||
- 匹配查询使用业务库连接(`get_connection()`),通过 `SET LOCAL app.current_site_id` 设置隔离
|
||||
- 如果 `site_code` 无法映射到 `site_id`,直接返回空列表
|
||||
|
||||
### 组件 4:角色权限服务(services/role.py)
|
||||
|
||||
**职责**:查询用户在指定店铺下的角色和权限。
|
||||
|
||||
```python
|
||||
async def get_user_permissions(user_id: int, site_id: int) -> list[str]:
|
||||
"""
|
||||
获取用户在指定 site_id 下的权限 code 列表。
|
||||
|
||||
SQL: SELECT DISTINCT p.code
|
||||
FROM auth.user_site_roles usr
|
||||
JOIN auth.role_permissions rp ON usr.role_id = rp.role_id
|
||||
JOIN auth.permissions p ON rp.permission_id = p.id
|
||||
WHERE usr.user_id = ? AND usr.site_id = ?
|
||||
"""
|
||||
...
|
||||
|
||||
async def get_user_sites(user_id: int) -> list[dict]:
|
||||
"""
|
||||
获取用户关联的所有店铺及对应角色。
|
||||
|
||||
返回: [{"site_id": int, "site_name": str, "roles": [{"code": str, "name": str}]}]
|
||||
"""
|
||||
...
|
||||
|
||||
async def check_user_has_site_role(user_id: int, site_id: int) -> bool:
|
||||
"""检查用户在指定 site_id 下是否有任何角色绑定。"""
|
||||
...
|
||||
```
|
||||
|
||||
### 组件 5:权限中间件(middleware/permission.py)
|
||||
|
||||
**职责**:基于 JWT 中的 `user_id` + `site_id` 检查用户权限。
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from app.auth.dependencies import get_current_user, CurrentUser
|
||||
|
||||
def require_permission(*permission_codes: str):
|
||||
"""
|
||||
权限装饰器/依赖,用于路由端点。
|
||||
|
||||
用法:
|
||||
@router.get("/finance")
|
||||
async def get_finance(
|
||||
user: CurrentUser = Depends(require_permission("view_board_finance"))
|
||||
):
|
||||
...
|
||||
|
||||
逻辑:
|
||||
1. 从 JWT 提取 user_id + site_id
|
||||
2. 查询 auth.users.status,非 approved 则 403
|
||||
3. 查询 user_site_roles + role_permissions 获取权限列表
|
||||
4. 检查是否包含所需权限,不包含则 403
|
||||
"""
|
||||
...
|
||||
|
||||
def require_approved():
|
||||
"""
|
||||
仅检查用户状态为 approved 的依赖(不检查具体权限)。
|
||||
用于通用的已认证端点。
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
**设计决策**:
|
||||
- 使用 FastAPI 依赖注入模式而非全局中间件,更灵活且可按端点配置
|
||||
- `pending` 用户只能访问申请提交和状态查询端点,其他端点需要 `approved` 状态
|
||||
- 权限检查结果可考虑短期缓存(当前版本不缓存,每次查库)
|
||||
|
||||
### 组件 6:JWT 服务扩展(auth/jwt.py 扩展)
|
||||
|
||||
**职责**:扩展现有 JWT 服务,支持微信登录场景。
|
||||
|
||||
**扩展内容**:
|
||||
|
||||
```python
|
||||
# 新增:创建受限令牌(pending 用户)
|
||||
def create_limited_token_pair(user_id: int) -> dict[str, str]:
|
||||
"""
|
||||
为 pending 用户签发受限令牌。
|
||||
payload 不含 site_id 和 roles,仅包含 user_id + type + limited=True。
|
||||
"""
|
||||
...
|
||||
|
||||
# 扩展:create_access_token payload 增加 roles 字段
|
||||
def create_access_token(user_id: int, site_id: int, roles: list[str] | None = None) -> str:
|
||||
"""
|
||||
生成 access_token。
|
||||
payload: sub=user_id, site_id, roles, type=access, exp
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
**设计决策**:
|
||||
- 保持向后兼容:现有 `create_access_token(user_id, site_id)` 调用不受影响(`roles` 默认 `None`)
|
||||
- `pending` 用户的受限令牌通过 `limited=True` 标记区分,权限中间件据此拦截
|
||||
|
||||
### 组件 7:路由端点
|
||||
|
||||
#### 7.1 小程序认证路由(routers/xcx_auth.py)
|
||||
|
||||
| 方法 | 路径 | 说明 | 认证要求 |
|
||||
|------|------|------|---------|
|
||||
| POST | `/api/xcx/login` | 微信登录 | 无(公开) |
|
||||
| POST | `/api/xcx/apply` | 提交申请 | JWT(含 pending) |
|
||||
| GET | `/api/xcx/me` | 查询自身状态 | JWT(含 pending) |
|
||||
| GET | `/api/xcx/me/sites` | 查询关联店铺 | JWT(approved) |
|
||||
| POST | `/api/xcx/switch-site` | 切换店铺 | JWT(approved) |
|
||||
| POST | `/api/xcx/refresh` | 刷新令牌 | refresh_token |
|
||||
|
||||
#### 7.2 管理端审核路由(routers/admin_applications.py)
|
||||
|
||||
| 方法 | 路径 | 说明 | 认证要求 |
|
||||
|------|------|------|---------|
|
||||
| GET | `/api/admin/applications` | 查询申请列表 | JWT + site_admin/tenant_admin |
|
||||
| GET | `/api/admin/applications/{id}` | 查询申请详情 + 候选匹配 | JWT + site_admin/tenant_admin |
|
||||
| POST | `/api/admin/applications/{id}/approve` | 批准申请 | JWT + site_admin/tenant_admin |
|
||||
| POST | `/api/admin/applications/{id}/reject` | 拒绝申请 | JWT + site_admin/tenant_admin |
|
||||
|
||||
### 组件 8:Pydantic 模型(schemas/xcx_auth.py)
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
import re
|
||||
|
||||
class WxLoginRequest(BaseModel):
|
||||
code: str = Field(..., min_length=1, description="微信临时登录凭证")
|
||||
|
||||
class WxLoginResponse(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
user_status: str # pending / approved / rejected / disabled
|
||||
user_id: int
|
||||
|
||||
class ApplicationRequest(BaseModel):
|
||||
site_code: str = Field(..., pattern=r"^[A-Za-z]{2}\d{3}$", description="球房ID")
|
||||
applied_role_text: str = Field(..., min_length=1, max_length=100)
|
||||
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号")
|
||||
employee_number: str | None = Field(None, max_length=50)
|
||||
nickname: str | None = Field(None, max_length=50)
|
||||
|
||||
class ApplicationResponse(BaseModel):
|
||||
id: int
|
||||
site_code: str
|
||||
applied_role_text: str
|
||||
status: str
|
||||
review_note: str | None = None
|
||||
created_at: str
|
||||
reviewed_at: str | None = None
|
||||
|
||||
class UserStatusResponse(BaseModel):
|
||||
user_id: int
|
||||
status: str
|
||||
nickname: str | None
|
||||
applications: list[ApplicationResponse]
|
||||
|
||||
class SiteInfo(BaseModel):
|
||||
site_id: int
|
||||
site_name: str
|
||||
roles: list[dict]
|
||||
|
||||
class SwitchSiteRequest(BaseModel):
|
||||
site_id: int
|
||||
|
||||
class MatchCandidate(BaseModel):
|
||||
source_type: str # assistant / staff
|
||||
id: int
|
||||
name: str
|
||||
mobile: str | None
|
||||
job_num: str | None
|
||||
|
||||
class ApproveRequest(BaseModel):
|
||||
role_id: int
|
||||
binding: dict | None = None # {"assistant_id": ..., "staff_id": ..., "binding_type": ...}
|
||||
review_note: str | None = None
|
||||
|
||||
class RejectRequest(BaseModel):
|
||||
review_note: str = Field(..., min_length=1)
|
||||
```
|
||||
|
||||
## 数据模型
|
||||
|
||||
### ER 图
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
users {
|
||||
serial id PK
|
||||
varchar wx_openid UK
|
||||
varchar wx_union_id
|
||||
varchar wx_avatar_url
|
||||
varchar nickname
|
||||
varchar phone
|
||||
varchar status "pending/approved/rejected/disabled"
|
||||
timestamptz created_at
|
||||
timestamptz updated_at
|
||||
}
|
||||
|
||||
user_applications {
|
||||
serial id PK
|
||||
int user_id FK
|
||||
varchar site_code
|
||||
int site_id "可空,映射后填入"
|
||||
varchar applied_role_text
|
||||
varchar employee_number "可选"
|
||||
varchar phone
|
||||
varchar status "pending/approved/rejected"
|
||||
int reviewer_id
|
||||
text review_note
|
||||
timestamptz created_at
|
||||
timestamptz reviewed_at
|
||||
}
|
||||
|
||||
site_code_mapping {
|
||||
serial id PK
|
||||
varchar site_code UK "2字母+3数字"
|
||||
bigint site_id UK
|
||||
varchar site_name
|
||||
int tenant_id
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
roles {
|
||||
serial id PK
|
||||
varchar code UK
|
||||
varchar name
|
||||
text description
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
permissions {
|
||||
serial id PK
|
||||
varchar code UK
|
||||
varchar name
|
||||
text description
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
role_permissions {
|
||||
int role_id FK
|
||||
int permission_id FK
|
||||
}
|
||||
|
||||
user_site_roles {
|
||||
serial id PK
|
||||
int user_id FK
|
||||
bigint site_id
|
||||
int role_id FK
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
user_assistant_binding {
|
||||
serial id PK
|
||||
int user_id FK
|
||||
bigint site_id
|
||||
bigint assistant_id "可空"
|
||||
bigint staff_id "可空"
|
||||
varchar binding_type "assistant/staff/manager"
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
users ||--o{ user_applications : "提交申请"
|
||||
users ||--o{ user_site_roles : "店铺角色"
|
||||
users ||--o{ user_assistant_binding : "人员绑定"
|
||||
roles ||--o{ user_site_roles : "角色分配"
|
||||
roles ||--o{ role_permissions : "角色权限"
|
||||
permissions ||--o{ role_permissions : "权限定义"
|
||||
site_code_mapping ||--o{ user_applications : "球房映射"
|
||||
```
|
||||
|
||||
### 表 DDL 概要
|
||||
|
||||
所有表在 `auth` Schema 下,迁移脚本位于 `db/zqyy_app/migrations/`。
|
||||
|
||||
**关键约束**:
|
||||
- `users.wx_openid` UNIQUE — 一个微信用户对应一条记录
|
||||
- `site_code_mapping.site_code` UNIQUE — 球房ID 唯一
|
||||
- `site_code_mapping.site_id` UNIQUE — site_id 唯一映射
|
||||
- `user_site_roles (user_id, site_id, role_id)` UNIQUE — 防止重复分配
|
||||
- `role_permissions (role_id, permission_id)` 联合主键
|
||||
|
||||
**索引**:
|
||||
- `users`: `ix_users_wx_openid` (wx_openid)
|
||||
- `user_applications`: `ix_user_applications_user_id` (user_id), `ix_user_applications_status` (status)
|
||||
- `user_site_roles`: `ix_user_site_roles_user_site` (user_id, site_id)
|
||||
- `site_code_mapping`: `ix_site_code_mapping_site_code` (site_code)
|
||||
|
||||
### 迁移脚本清单
|
||||
|
||||
| 序号 | 文件名 | 内容 |
|
||||
|------|--------|------|
|
||||
| 1 | `YYYY-MM-DD__p3_create_auth_tables.sql` | 创建 users / user_applications / site_code_mapping / roles / permissions / role_permissions / user_site_roles / user_assistant_binding |
|
||||
| 2 | `YYYY-MM-DD__p3_seed_roles_permissions.sql` | 种子数据:权限列表 + 默认角色 + 角色权限映射 |
|
||||
|
||||
|
||||
|
||||
## 正确性属性(Correctness Properties)
|
||||
|
||||
*属性是系统在所有有效执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1:迁移脚本幂等性
|
||||
|
||||
*For any* 本次新增的迁移脚本(DDL + 种子数据),连续执行两次的结果应与执行一次相同——第二次执行不应产生错误,且数据库状态不变。
|
||||
|
||||
**Validates: Requirements 1.9, 2.4, 11.5**
|
||||
|
||||
### Property 2:登录创建/查找用户正确性
|
||||
|
||||
*For any* 有效的微信 `openid`,调用登录逻辑后:若该 `openid` 已存在于 `auth.users` 中,应返回已有用户的 `user_id`;若不存在,应创建新用户(status=`pending`)并返回新 `user_id`。无论哪种情况,返回的 JWT 中 `sub` 应等于该 `user_id`。
|
||||
|
||||
**Validates: Requirements 3.2, 3.3**
|
||||
|
||||
### Property 3:disabled 用户登录拒绝
|
||||
|
||||
*For any* `auth.users` 中 status 为 `disabled` 的用户,通过其 `openid` 登录时应返回 403 错误,不签发 JWT。
|
||||
|
||||
**Validates: Requirements 3.5**
|
||||
|
||||
### Property 4:申请创建正确性
|
||||
|
||||
*For any* 有效的申请数据(合法 `site_code` 格式、11 位手机号、非空 `applied_role_text`),提交申请后 `auth.user_applications` 中应新增一条 status=`pending` 的记录。若 `site_code` 在 `site_code_mapping` 中有映射,记录的 `site_id` 应等于映射值;若无映射,`site_id` 为 NULL 但申请仍成功。若提供了 `nickname`,`auth.users` 中该用户的 `nickname` 应更新。
|
||||
|
||||
**Validates: Requirements 4.1, 4.2, 4.3, 4.4**
|
||||
|
||||
### Property 5:手机号格式验证
|
||||
|
||||
*For any* 非 11 位纯数字的字符串作为 `phone` 提交申请,系统应返回 422 错误,`auth.user_applications` 中不应新增记录。
|
||||
|
||||
**Validates: Requirements 4.5**
|
||||
|
||||
### Property 6:重复申请拒绝
|
||||
|
||||
*For any* 已有一条 status=`pending` 申请的用户,再次提交申请时应返回 409 错误,`auth.user_applications` 中不应新增记录。
|
||||
|
||||
**Validates: Requirements 4.6**
|
||||
|
||||
### Property 7:人员匹配合并正确性
|
||||
|
||||
*For any* 有效的 `site_id` 和 `phone` 组合,匹配服务返回的候选列表应满足:(1) 每条候选的 `source_type` 为 `assistant` 或 `staff`;(2) 助教来源的候选来自 `v_dim_assistant` 表中 `site_id` 和 `mobile` 匹配的记录;(3) 员工来源的候选来自 `v_dim_staff` 表中 `site_id` 和 `mobile`(或 `job_num`)匹配的记录;(4) 列表是两个来源结果的并集,无遗漏。
|
||||
|
||||
**Validates: Requirements 5.1, 5.2, 5.3, 5.4**
|
||||
|
||||
### Property 8:审核操作正确性
|
||||
|
||||
*For any* status=`pending` 的申请:(1) 批准操作后,申请 status 变为 `approved`,`auth.user_site_roles` 中新增角色记录,`auth.users.status` 变为 `approved`,`reviewer_id` 和 `reviewed_at` 非空;(2) 若提供了 binding 信息,`auth.user_assistant_binding` 中新增绑定记录;(3) 拒绝操作后,申请 status 变为 `rejected`,`review_note` 非空,`reviewer_id` 和 `reviewed_at` 非空。
|
||||
|
||||
**Validates: Requirements 6.1, 6.2, 6.3, 6.4, 6.5**
|
||||
|
||||
### Property 9:非 pending 申请审核拒绝
|
||||
|
||||
*For any* status 不是 `pending` 的申请(`approved` / `rejected`),对其执行批准或拒绝操作应返回 409 错误,申请状态不变。
|
||||
|
||||
**Validates: Requirements 6.6**
|
||||
|
||||
### Property 10:用户状态查询完整性
|
||||
|
||||
*For any* 用户,查询自身状态应返回:(1) 用户的 `status` 字段;(2) 该用户所有申请记录的完整列表。若用户 status 为 `approved`,还应返回已关联的店铺列表和对应角色。
|
||||
|
||||
**Validates: Requirements 7.1, 7.2**
|
||||
|
||||
### Property 11:多店铺角色独立分配
|
||||
|
||||
*For any* 用户和多个不同的 `site_id`,系统应允许为该用户在每个 `site_id` 下独立分配不同的角色,且 `auth.user_site_roles` 中的记录互不干扰。
|
||||
|
||||
**Validates: Requirements 8.1**
|
||||
|
||||
### Property 12:店铺切换令牌正确性
|
||||
|
||||
*For any* 拥有多店铺绑定的 approved 用户,切换到目标 `site_id` 后签发的新 JWT 中 `site_id` 应等于目标值,`roles` 应等于该用户在目标 `site_id` 下的角色列表。若用户在目标 `site_id` 下无角色绑定,切换应失败。
|
||||
|
||||
**Validates: Requirements 8.2, 10.4**
|
||||
|
||||
### Property 13:权限中间件拦截正确性
|
||||
|
||||
*For any* 用户、`site_id` 和所需权限 code 的组合:(1) 若用户 status 非 `approved`,返回 403;(2) 若用户在该 `site_id` 下的权限列表不包含所需权限,返回 403;(3) 若用户在该 `site_id` 下拥有所需权限且 status 为 `approved`,放行。
|
||||
|
||||
**Validates: Requirements 8.3, 9.1, 9.2, 9.3**
|
||||
|
||||
### Property 14:JWT payload 结构与状态一致性
|
||||
|
||||
*For any* 通过登录签发的 JWT:(1) 解码后应包含 `sub`(user_id)、`type`、`exp` 字段;(2) 若用户 status 为 `approved`,payload 应包含 `site_id` 和 `roles`;(3) 若用户 status 为 `pending`,payload 应包含 `limited=True`,不含 `site_id` 和 `roles`。
|
||||
|
||||
**Validates: Requirements 10.1, 10.2, 10.3**
|
||||
|
||||
### Property 15:JWT 过期/无效令牌拒绝
|
||||
|
||||
*For any* 过期的 JWT 或被篡改的 JWT 字符串,权限中间件应返回 401 错误,不放行请求。
|
||||
|
||||
**Validates: Requirements 9.4**
|
||||
|
||||
## 错误处理
|
||||
|
||||
### API 错误码规范
|
||||
|
||||
| HTTP 状态码 | 场景 | 响应体 |
|
||||
|------------|------|--------|
|
||||
| 401 | JWT 无效/过期、微信 code2Session 失败 | `{"detail": "具体错误描述"}` |
|
||||
| 403 | 用户 disabled、权限不足、用户未 approved | `{"detail": "具体错误描述"}` |
|
||||
| 404 | 申请不存在 | `{"detail": "申请不存在"}` |
|
||||
| 409 | 重复提交 pending 申请、审核非 pending 申请 | `{"detail": "具体冲突描述"}` |
|
||||
| 422 | 请求体校验失败(手机号格式、site_code 格式等) | Pydantic 标准错误格式 |
|
||||
| 500 | 数据库连接失败、微信 API 超时 | `{"detail": "服务器内部错误"}` |
|
||||
|
||||
### 微信 API 错误处理
|
||||
|
||||
| 微信 errcode | 含义 | 处理方式 |
|
||||
|-------------|------|---------|
|
||||
| 0 | 成功 | 正常流程 |
|
||||
| 40029 | code 无效 | 返回 401,提示"登录凭证无效,请重新登录" |
|
||||
| 45011 | 频率限制 | 返回 429,提示"请求过于频繁" |
|
||||
| 40226 | 高风险用户 | 返回 403,提示"账号存在风险" |
|
||||
| 其他 | 未知错误 | 返回 401,记录日志,提示"微信登录失败" |
|
||||
|
||||
### 数据库错误处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|---------|
|
||||
| 连接失败 | 捕获 `psycopg2.OperationalError`,返回 500 |
|
||||
| 唯一约束冲突(wx_openid) | 并发创建时捕获 `UniqueViolation`,改为查询已有记录 |
|
||||
| 外键约束失败 | 返回 422,提示具体的关联数据不存在 |
|
||||
| FDW 查询失败 | 捕获异常,匹配服务返回空列表,记录日志 |
|
||||
|
||||
### 环境变量缺失处理
|
||||
|
||||
| 变量 | 缺失时行为 |
|
||||
|------|-----------|
|
||||
| `WX_APPID` / `WX_SECRET` | 微信登录端点返回 500,日志记录"微信配置缺失" |
|
||||
| `JWT_SECRET_KEY` | 应用启动时警告(空字符串不安全),JWT 签发/验证使用空密钥(仅开发环境) |
|
||||
| `DB_HOST` 等数据库参数 | 数据库连接失败,返回 500 |
|
||||
|
||||
## 测试策略
|
||||
|
||||
### DDL 测试库落库与文档同步
|
||||
|
||||
DDL 变更必须经过以下流程:
|
||||
|
||||
1. **测试库执行**:在 `test_zqyy_app` 中执行迁移脚本,验证无错误
|
||||
2. **幂等性验证**:连续执行两次,第二次无错误
|
||||
3. **数据库手册更新**:创建/更新 `docs/database/BD_Manual_auth_tables.md`,格式参照现有 `BD_Manual_auth_biz_schemas.md`
|
||||
4. **DDL 基线刷新**:运行 `python scripts/ops/gen_consolidated_ddl.py` 重新生成 `docs/database/ddl/zqyy_app__auth.sql`
|
||||
|
||||
### 小程序认证前端页面
|
||||
|
||||
#### 页面清单
|
||||
|
||||
| 页面 | 路径 | 说明 | H5 原型 |
|
||||
|------|------|------|---------|
|
||||
| login | `pages/login/login` | 微信登录页(自动调用 wx.login) | `docs/h5_ui/pages/login.html` |
|
||||
| apply | `pages/apply/apply` | 申请表单页(球房ID + 手机号 + 身份 + 编号 + 昵称) | `docs/h5_ui/pages/apply.html` |
|
||||
| reviewing | `pages/reviewing/reviewing` | 审核等待页(显示状态 + 申请摘要) | `docs/h5_ui/pages/reviewing.html` |
|
||||
| no-permission | `pages/no-permission/no-permission` | 无权限/已禁用页 | `docs/h5_ui/pages/no-permission.html` |
|
||||
|
||||
#### 认证路由流程
|
||||
|
||||
```
|
||||
app.ts onLaunch()
|
||||
│
|
||||
├── wx.login() → 获取 code
|
||||
│
|
||||
├── POST /api/xcx/login {code}
|
||||
│ │
|
||||
│ ├── 返回 user_status = "approved"
|
||||
│ │ └── 跳转主页(task-list 或 home)
|
||||
│ │
|
||||
│ ├── 返回 user_status = "pending"
|
||||
│ │ ├── 查询 /api/xcx/me → 有 pending 申请
|
||||
│ │ │ └── 跳转 reviewing 页面
|
||||
│ │ └── 查询 /api/xcx/me → 无 pending 申请
|
||||
│ │ └── 跳转 apply 页面
|
||||
│ │
|
||||
│ ├── 返回 user_status = "rejected"
|
||||
│ │ └── 跳转 reviewing 页面(显示拒绝原因 + 重新申请按钮)
|
||||
│ │
|
||||
│ └── 返回 403(disabled)
|
||||
│ └── 跳转 no-permission 页面
|
||||
│
|
||||
└── 登录失败(网络错误等)
|
||||
└── 显示错误提示,提供重试按钮
|
||||
```
|
||||
|
||||
#### app.ts 全局状态管理
|
||||
|
||||
```typescript
|
||||
// globalData 扩展
|
||||
interface IAppOption {
|
||||
globalData: {
|
||||
userInfo?: {
|
||||
userId: number;
|
||||
status: string; // pending / approved / rejected / disabled
|
||||
nickname?: string;
|
||||
};
|
||||
token?: string;
|
||||
refreshToken?: string;
|
||||
currentSiteId?: number;
|
||||
sites?: Array<{ siteId: number; siteName: string; roles: string[] }>;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 请求封装(utils/request.ts)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 统一请求封装:
|
||||
* 1. 自动附加 Authorization: Bearer <token>
|
||||
* 2. 401 时自动尝试 refresh_token 刷新
|
||||
* 3. 刷新失败时跳转 login 页面
|
||||
*/
|
||||
function request(options: RequestOptions): Promise<any> { ... }
|
||||
```
|
||||
|
||||
### 开发模式联调
|
||||
|
||||
#### Mock 登录端点
|
||||
|
||||
后端在 `WX_DEV_MODE=true` 时注册 `POST /api/xcx/dev-login`:
|
||||
|
||||
```python
|
||||
@router.post("/api/xcx/dev-login")
|
||||
async def dev_login(openid: str, status: str = "approved"):
|
||||
"""
|
||||
开发模式 mock 登录。
|
||||
直接根据 openid 查找/创建用户,跳过微信 code2Session。
|
||||
可通过 status 参数模拟不同用户状态。
|
||||
仅在 WX_DEV_MODE=true 时可用。
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
#### 微信开发者工具联调步骤
|
||||
|
||||
联调指南文档位于 `apps/miniprogram/doc/auth-integration-guide.md`,包含:
|
||||
1. 微信开发者工具项目导入配置(appid、不校验合法域名)
|
||||
2. 后端启动命令(`cd apps/backend && uvicorn app.main:app --reload`)
|
||||
3. 小程序请求域名配置(开发环境指向 `http://localhost:8000`)
|
||||
4. 测试流程:登录 → 申请 → 管理端审核 → 重新登录验证
|
||||
5. Mock 模式使用说明
|
||||
|
||||
### 属性测试(Property-Based Testing)
|
||||
|
||||
使用 Python `hypothesis` 框架,测试目录:`tests/`(Monorepo 级属性测试目录)。
|
||||
|
||||
每个属性测试至少运行 100 次迭代。每个测试用注释标注对应的设计属性编号。
|
||||
|
||||
标注格式:`# Feature: miniapp-auth-system, Property N: <属性标题>`
|
||||
|
||||
**属性测试清单**:
|
||||
|
||||
| 属性 | 测试文件 | 测试方法 | 生成器 |
|
||||
|------|---------|---------|--------|
|
||||
| P2 登录创建/查找用户 | `tests/test_auth_system_properties.py` | 生成随机 openid,模拟登录,验证用户创建/查找逻辑 | `hypothesis.strategies.text` 生成 openid |
|
||||
| P4 申请创建正确性 | `tests/test_auth_system_properties.py` | 生成随机合法申请数据,验证申请记录创建 | 自定义 strategy 生成 site_code(2字母+3数字)、phone(11位数字) |
|
||||
| P5 手机号格式验证 | `tests/test_auth_system_properties.py` | 生成随机非法手机号,验证 422 拒绝 | `hypothesis.strategies.text` 过滤非 11 位数字 |
|
||||
| P6 重复申请拒绝 | `tests/test_auth_system_properties.py` | 生成随机用户+申请,提交两次,验证第二次 409 | 复用申请数据生成器 |
|
||||
| P7 人员匹配合并 | `tests/test_auth_system_properties.py` | 生成随机助教/员工数据,验证匹配结果合并 | 自定义 strategy 生成匹配数据 |
|
||||
| P8 审核操作正确性 | `tests/test_auth_system_properties.py` | 生成随机 pending 申请,执行批准/拒绝,验证状态流转 | 自定义 strategy 生成审核数据 |
|
||||
| P9 非 pending 审核拒绝 | `tests/test_auth_system_properties.py` | 生成随机非 pending 申请,验证 409 | `hypothesis.strategies.sampled_from(["approved", "rejected"])` |
|
||||
| P11 多店铺角色独立 | `tests/test_auth_system_properties.py` | 生成随机用户+多个 site_id,验证角色独立分配 | `hypothesis.strategies.lists` 生成 site_id 列表 |
|
||||
| P12 店铺切换令牌 | `tests/test_auth_system_properties.py` | 生成多店铺用户,切换店铺,验证 JWT 内容 | 复用多店铺生成器 |
|
||||
| P13 权限中间件拦截 | `tests/test_auth_system_properties.py` | 生成随机用户+权限组合,验证中间件判断 | 自定义 strategy 生成权限矩阵 |
|
||||
| P14 JWT payload 结构 | `tests/test_auth_system_properties.py` | 生成随机用户(不同 status),签发 JWT,验证 payload | `hypothesis.strategies.sampled_from(["pending", "approved"])` |
|
||||
| P15 JWT 过期/无效拒绝 | `tests/test_auth_system_properties.py` | 生成随机过期/篡改 JWT,验证 401 | 自定义 strategy 生成无效 JWT |
|
||||
|
||||
**注意**:P1(迁移幂等性)、P3(disabled 登录拒绝)、P10(状态查询完整性)作为集成测试在后端测试目录实现,因为它们需要真实数据库环境或涉及具体的数据库状态验证。
|
||||
|
||||
### 单元测试
|
||||
|
||||
单元测试位于 `apps/backend/tests/`,聚焦于:
|
||||
|
||||
- `test_xcx_auth_router.py`:微信登录路由测试(mock 微信 API)
|
||||
- `test_application_service.py`:申请服务的边界情况
|
||||
- `test_matching_service.py`:匹配逻辑的边界情况(空结果、FDW 异常)
|
||||
- `test_permission_middleware.py`:权限中间件的各种组合
|
||||
- `test_jwt_extended.py`:扩展 JWT 的 limited token 逻辑
|
||||
|
||||
### 集成测试
|
||||
|
||||
集成测试通过验证脚本实现,覆盖:
|
||||
|
||||
- 迁移脚本幂等性验证(执行两次无错误)
|
||||
- 种子数据完整性验证(权限和角色数量正确)
|
||||
- 完整认证流程:登录 → 申请 → 审核 → 权限验证
|
||||
196
.kiro/specs/03-miniapp-auth-system/requirements.md
Normal file
196
.kiro/specs/03-miniapp-auth-system/requirements.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 需求文档:小程序用户认证系统(miniapp-auth-system)
|
||||
|
||||
## 简介
|
||||
|
||||
本 SPEC 实现小程序用户认证系统,涵盖微信登录、用户申请审核、人员匹配、多店铺权限管理等完整认证链路。系统基于 P1(miniapp-db-foundation)已建立的 `auth` Schema 和 FDW 映射,在 `test_zqyy_app.auth` 中创建用户、申请、角色、绑定等业务表,并在 FastAPI 后端实现对应的 API 端点和权限中间件。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **Auth_System**:小程序用户认证系统,负责微信登录、用户管理、申请审核、权限控制的完整后端服务
|
||||
- **WeChat_Auth_Service**:微信认证服务模块,负责调用微信 `code2Session` 接口换取 `openid` 和 `session_key`
|
||||
- **Application_Service**:用户申请服务模块,负责处理用户提交的入驻申请、状态流转和审核操作
|
||||
- **Matching_Service**:人员匹配服务模块,负责根据球房ID和手机号/编号在助教表和员工表中查找候选匹配
|
||||
- **Permission_Middleware**:权限中间件,基于用户的 `site_id` + `role` 拦截无权请求
|
||||
- **JWT_Service**:JWT 令牌服务,负责签发和刷新 access_token / refresh_token(已有实现,本 SPEC 扩展)
|
||||
- **site_code**:球房ID,格式为 2 字母 + 3 数字(如 `AB123`),与 `site_id` 一一映射
|
||||
- **site_id**:门店标识符,类型为 `BIGINT`,用于多门店数据隔离
|
||||
- **user_status**:用户状态枚举,取值为 `pending`(审核中)/ `approved`(已通过)/ `rejected`(已拒绝)/ `disabled`(已禁用)
|
||||
- **binding_type**:绑定类型枚举,取值为 `assistant`(助教)/ `staff`(员工)/ `manager`(管理员)
|
||||
- **FDW**:`postgres_fdw` 外部数据包装器,通过 `fdw_etl` Schema 读取 ETL 库数据
|
||||
- **Migration_Script**:存放在 `db/zqyy_app/migrations/` 中的纯 SQL 迁移脚本,以日期前缀命名
|
||||
- **BD_Manual**:数据库手册文档,存放在 `docs/database/` 中,记录表结构变更、兼容性影响、回滚策略和验证 SQL
|
||||
- **DDL_Baseline**:DDL 基线文件,存放在 `docs/database/ddl/` 中,由 `gen_consolidated_ddl.py` 自动生成
|
||||
- **Miniprogram_Auth_Pages**:小程序认证相关前端页面,包括登录页、申请表单页、审核等待页、无权限页
|
||||
- **Dev_Login**:开发模式下的 mock 登录端点,绕过微信 code2Session 调用,用于联调测试
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:认证数据表创建
|
||||
|
||||
**用户故事:** 作为后端开发者,我需要在 `auth` Schema 中创建用户认证相关的数据表,以便支撑完整的认证和权限管理功能。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `users` 表,包含 `id`(SERIAL PK)、`wx_openid`(UNIQUE)、`wx_union_id`、`wx_avatar_url`、`nickname`、`phone`、`status`(默认 `pending`)、`created_at`、`updated_at` 字段
|
||||
2. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `user_applications` 表,包含 `id`(SERIAL PK)、`user_id`(FK → users)、`site_code`、`applied_role_text`、`employee_number`(可选)、`phone`、`status`(默认 `pending`)、`reviewer_id`、`review_note`、`created_at`、`reviewed_at` 字段
|
||||
3. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `site_code_mapping` 表,包含 `id`(SERIAL PK)、`site_code`(UNIQUE,格式 2 字母 + 3 数字)、`site_id`(BIGINT UNIQUE)、`site_name`、`tenant_id`、`created_at` 字段
|
||||
4. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `user_site_roles` 表,包含 `id`(SERIAL PK)、`user_id`(FK → users)、`site_id`(BIGINT)、`role_id`(FK → roles)、`created_at` 字段,并对 `(user_id, site_id, role_id)` 建立唯一约束
|
||||
5. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `user_assistant_binding` 表,包含 `id`(SERIAL PK)、`user_id`(FK → users)、`site_id`(BIGINT)、`assistant_id`(BIGINT,可选)、`staff_id`(BIGINT,可选)、`binding_type`、`created_at` 字段
|
||||
6. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `roles` 表,包含 `id`(SERIAL PK)、`code`(UNIQUE)、`name`、`description`、`created_at` 字段
|
||||
7. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `permissions` 表,包含 `id`(SERIAL PK)、`code`(UNIQUE)、`name`、`description`、`created_at` 字段
|
||||
8. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `role_permissions` 表,包含 `role_id`(FK → roles)、`permission_id`(FK → permissions)字段,并以 `(role_id, permission_id)` 为联合主键
|
||||
9. THE Migration_Script SHALL 使用 `IF NOT EXISTS` / `OR REPLACE` 等幂等语法,确保重复执行不会报错
|
||||
10. THE Migration_Script SHALL 在脚本中包含回滚语句(以注释形式)
|
||||
|
||||
### 需求 2:种子数据预置
|
||||
|
||||
**用户故事:** 作为系统管理员,我需要系统预置固定的权限列表和默认角色,以便审核时可直接分配。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 种子数据脚本执行完成, THE Auth_System SHALL 在 `auth.permissions` 表中插入 5 条固定权限记录:`view_tasks`、`view_board`、`view_board_finance`、`view_board_customer`、`view_board_coach`
|
||||
2. WHEN 种子数据脚本执行完成, THE Auth_System SHALL 在 `auth.roles` 表中插入默认角色(至少包含 `coach`(助教)、`staff`(员工)、`site_admin`(店铺管理员)、`tenant_admin`(租户管理员))
|
||||
3. WHEN 种子数据脚本执行完成, THE Auth_System SHALL 在 `auth.role_permissions` 表中为每个默认角色分配对应的权限组合
|
||||
4. THE 种子数据脚本 SHALL 使用 `ON CONFLICT DO NOTHING` 语法,确保重复执行不会产生重复数据
|
||||
|
||||
### 需求 3:微信登录
|
||||
|
||||
**用户故事:** 作为球房工作人员,我需要通过微信登录小程序,以便快速进入系统。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 小程序端发送微信临时登录凭证(`code`), THE WeChat_Auth_Service SHALL 调用微信 `code2Session` 接口换取 `openid` 和 `session_key`
|
||||
2. WHEN `code2Session` 返回有效 `openid` 且该 `openid` 已存在于 `auth.users` 表中, THE Auth_System SHALL 返回该用户的 JWT 令牌对(access_token + refresh_token)和用户状态信息
|
||||
3. WHEN `code2Session` 返回有效 `openid` 且该 `openid` 不存在于 `auth.users` 表中, THE Auth_System SHALL 创建新用户记录(status 为 `pending`),返回 JWT 令牌对和 `pending` 状态标识
|
||||
4. IF `code2Session` 接口调用失败或返回错误码, THEN THE WeChat_Auth_Service SHALL 返回 HTTP 401 错误,包含具体的错误描述
|
||||
5. WHEN 用户状态为 `disabled`, THE Auth_System SHALL 返回 HTTP 403 错误,拒绝登录
|
||||
|
||||
### 需求 4:用户申请提交
|
||||
|
||||
**用户故事:** 作为球房工作人员,我需要在首次登录后填写申请表单(球房ID、申请身份、手机号、编号、昵称),以便管理员审核我的身份。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 用户提交申请表单(包含 `site_code`、`applied_role_text`、`phone`,可选 `employee_number`), THE Application_Service SHALL 在 `auth.user_applications` 表中创建一条 status 为 `pending` 的申请记录
|
||||
2. WHEN 用户提交的 `site_code` 在 `auth.site_code_mapping` 中存在映射, THE Application_Service SHALL 将申请记录关联到对应的 `site_id`
|
||||
3. WHEN 用户提交的 `site_code` 在 `auth.site_code_mapping` 中不存在映射, THE Application_Service SHALL 仍然接受申请,申请记录中保留 `site_code` 文本,管理端显示"未找到关联信息"
|
||||
4. WHEN 用户提交申请时提供了 `nickname`, THE Auth_System SHALL 更新 `auth.users` 表中该用户的 `nickname` 字段
|
||||
5. IF 用户提交的 `phone` 为空或格式无效(非 11 位数字), THEN THE Application_Service SHALL 返回 HTTP 422 错误,包含具体的校验失败信息
|
||||
6. WHEN 用户已有一条 `pending` 状态的申请, THE Application_Service SHALL 拒绝重复提交,返回 HTTP 409 错误
|
||||
|
||||
### 需求 5:人员匹配
|
||||
|
||||
**用户故事:** 作为系统,我需要根据球房ID和手机号自动建议用户与助教/员工的对应关系,以便管理员快速审核。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 管理员查看某条申请详情时, THE Matching_Service SHALL 根据申请中的 `site_id` 和 `phone` 在 `fdw_etl.v_dim_assistant` 中按 `site_id` + `mobile` 匹配助教记录
|
||||
2. WHEN 管理员查看某条申请详情时, THE Matching_Service SHALL 根据申请中的 `site_id` 和 `phone` 在 `fdw_etl.v_dim_staff` 和 `fdw_etl.v_dim_staff_ex` 中按 `site_id` + `mobile` 匹配员工记录
|
||||
3. WHEN 申请中包含 `employee_number`, THE Matching_Service SHALL 额外按 `job_num` 字段匹配员工记录
|
||||
4. THE Matching_Service SHALL 将助教匹配结果和员工匹配结果合并为统一的候选列表返回,每条候选包含来源类型(`assistant` / `staff`)、姓名、手机号、编号
|
||||
5. WHEN 助教表和员工表均无匹配结果, THE Matching_Service SHALL 返回空候选列表,管理端显示"未找到关联信息"
|
||||
6. WHEN 申请的 `site_code` 无法映射到 `site_id`, THE Matching_Service SHALL 跳过匹配,返回空候选列表
|
||||
|
||||
### 需求 6:申请审核
|
||||
|
||||
**用户故事:** 作为租户管理员,我需要审核用户申请,将用户关联到对应的助教/员工,并分配身份权限。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 管理员批准申请并选择了候选匹配对象, THE Application_Service SHALL 将申请状态更新为 `approved`,在 `auth.user_assistant_binding` 中创建绑定记录,在 `auth.user_site_roles` 中分配角色
|
||||
2. WHEN 管理员批准申请但无候选匹配(手动审核), THE Application_Service SHALL 将申请状态更新为 `approved`,仅在 `auth.user_site_roles` 中分配角色,不创建绑定记录
|
||||
3. WHEN 管理员拒绝申请, THE Application_Service SHALL 将申请状态更新为 `rejected`,记录 `review_note`(拒绝原因)
|
||||
4. WHEN 申请审核通过后, THE Auth_System SHALL 将 `auth.users` 表中该用户的 `status` 更新为 `approved`
|
||||
5. WHEN 审核操作完成, THE Application_Service SHALL 记录 `reviewer_id` 和 `reviewed_at` 时间戳
|
||||
6. IF 审核目标申请的状态不是 `pending`, THEN THE Application_Service SHALL 返回 HTTP 409 错误,拒绝重复审核
|
||||
|
||||
### 需求 7:用户状态查询
|
||||
|
||||
**用户故事:** 作为用户,我需要看到自己的申请状态(审核中/通过/拒绝),以便了解审核进度。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 用户查询自身状态, THE Auth_System SHALL 返回用户的 `status`、所有申请记录列表(含每条申请的 `site_code`、`applied_role_text`、`status`、`review_note`)
|
||||
2. WHEN 用户状态为 `approved`, THE Auth_System SHALL 同时返回用户已关联的店铺列表和对应角色
|
||||
|
||||
### 需求 8:多店铺支持与店铺切换
|
||||
|
||||
**用户故事:** 作为用户,我可以同时属于多个店铺(连锁场景),切换店铺后数据正确隔离。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Auth_System SHALL 允许一个用户通过多次申请关联到多个不同的 `site_id`,每个 `site_id` 独立分配角色
|
||||
2. WHEN 用户切换当前店铺, THE JWT_Service SHALL 签发包含新 `site_id` 的 JWT 令牌对
|
||||
3. WHEN 用户携带某 `site_id` 的 JWT 访问 API, THE Permission_Middleware SHALL 仅允许访问该 `site_id` 下用户拥有权限的资源
|
||||
|
||||
### 需求 9:权限中间件
|
||||
|
||||
**用户故事:** 作为系统,我需要权限中间件正确拦截无权请求,确保数据安全。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 用户携带有效 JWT 访问受保护端点, THE Permission_Middleware SHALL 从 JWT 中提取 `user_id` 和 `site_id`,查询 `auth.user_site_roles` 和 `auth.role_permissions` 获取用户在该店铺下的权限列表
|
||||
2. WHEN 用户的权限列表不包含端点所需的权限 code, THE Permission_Middleware SHALL 返回 HTTP 403 错误
|
||||
3. WHEN 用户的 `status` 不是 `approved`, THE Permission_Middleware SHALL 返回 HTTP 403 错误,拒绝访问受保护端点
|
||||
4. WHEN JWT 令牌过期或无效, THE Permission_Middleware SHALL 返回 HTTP 401 错误
|
||||
|
||||
### 需求 10:JWT 令牌扩展
|
||||
|
||||
**用户故事:** 作为后端开发者,我需要扩展现有 JWT 服务以支持微信登录场景和多店铺切换。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE JWT_Service SHALL 在 JWT payload 中包含 `user_id`(sub)、`site_id`、`roles`(角色 code 列表)、`type`(access/refresh)、`exp` 字段
|
||||
2. WHEN 用户通过微信登录且状态为 `approved`, THE JWT_Service SHALL 使用用户默认店铺(第一个关联的 site_id)签发令牌
|
||||
3. WHEN 用户通过微信登录且状态为 `pending`, THE JWT_Service SHALL 签发不含 `site_id` 和 `roles` 的受限令牌,仅允许访问申请提交和状态查询端点
|
||||
4. WHEN 用户请求切换店铺, THE JWT_Service SHALL 验证用户在目标 `site_id` 下有角色绑定后签发新令牌
|
||||
|
||||
### 需求 11:迁移脚本管理
|
||||
|
||||
**用户故事:** 作为后端开发者,我需要所有数据库变更都有对应的迁移脚本,以便变更可追溯、可重放。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Migration_Script SHALL 将所有认证相关表的 DDL 存放在 `db/zqyy_app/migrations/` 目录中
|
||||
2. THE Migration_Script SHALL 使用日期前缀命名(格式:`YYYY-MM-DD__<描述>.sql`)
|
||||
3. THE Migration_Script SHALL 使用 UTF-8 编码,纯 SQL(非 ORM)
|
||||
4. THE Migration_Script SHALL 在每个脚本中包含回滚语句(以注释形式)
|
||||
5. THE Migration_Script SHALL 使用幂等语法(`IF NOT EXISTS`、`ON CONFLICT DO NOTHING`),确保重复执行不会报错
|
||||
|
||||
### 需求 12:DDL 测试库落库与文档同步
|
||||
|
||||
**用户故事:** 作为后端开发者,我需要所有 DDL 变更在测试库(`test_zqyy_app`)中实际执行验证,并同步更新数据库手册和 DDL 基线,确保文档与实际 Schema 一致。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 迁移脚本编写完成, THE Auth_System SHALL 在 `test_zqyy_app` 测试库中执行迁移脚本,验证无错误
|
||||
2. WHEN 迁移脚本执行成功, THE Auth_System SHALL 创建或更新 `docs/database/BD_Manual_auth_tables.md` 数据库手册,包含变更说明、兼容性影响、回滚策略、验证 SQL(至少 3 条)
|
||||
3. WHEN 迁移脚本执行成功, THE Auth_System SHALL 运行 `python scripts/ops/gen_consolidated_ddl.py` 重新生成 DDL 基线文件 `docs/database/ddl/zqyy_app__auth.sql`
|
||||
4. WHEN 种子数据脚本执行成功, THE Auth_System SHALL 在数据库手册中记录种子数据内容(角色、权限、角色-权限映射)
|
||||
|
||||
### 需求 13:小程序认证前端页面
|
||||
|
||||
**用户故事:** 作为球房工作人员,我需要在小程序中看到登录页、申请表单页、审核状态页,以便完成从微信登录到正式使用的完整流程。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 用户首次打开小程序, THE Auth_System SHALL 展示登录页面,调用 `wx.login()` 获取 code 并发送到后端 `/api/xcx/login`
|
||||
2. WHEN 后端返回 `user_status=pending` 且用户无 pending 申请, THE Auth_System SHALL 跳转到申请表单页面,包含球房ID(`site_code`)、申请身份、手机号、编号(选填)、昵称输入框
|
||||
3. WHEN 用户提交申请表单, THE Auth_System SHALL 调用 `/api/xcx/apply` 提交申请,成功后跳转到审核等待页面
|
||||
4. WHEN 用户状态为 `pending` 且已有 pending 申请, THE Auth_System SHALL 展示审核等待页面,显示"审核中"状态和申请信息摘要
|
||||
5. WHEN 用户状态为 `rejected`, THE Auth_System SHALL 在审核等待页面显示拒绝原因,并提供"重新申请"按钮
|
||||
6. WHEN 用户状态为 `approved`, THE Auth_System SHALL 跳转到小程序主页(任务列表)
|
||||
7. WHEN 用户状态为 `disabled`, THE Auth_System SHALL 展示无权限页面,提示账号已被禁用
|
||||
8. THE Auth_System SHALL 在小程序 `app.ts` 的 `onLaunch` 中实现自动登录逻辑,根据用户状态路由到对应页面
|
||||
9. WHEN 用户拥有多个店铺, THE Auth_System SHALL 在主页提供店铺切换入口
|
||||
|
||||
### 需求 14:前后端联调验证
|
||||
|
||||
**用户故事:** 作为开发者,我需要在微信开发者工具中验证完整的认证流程(登录→申请→审核→进入主页),确保前后端接口对接正确。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Auth_System SHALL 提供联调验证脚本或文档,说明如何在微信开发者工具中测试完整认证流程
|
||||
2. THE Auth_System SHALL 在后端提供开发模式下的 mock 登录端点(`POST /api/xcx/dev-login`),接受任意 openid 直接返回 JWT,绕过微信 code2Session 调用
|
||||
3. WHEN 开发模式启用时, THE Auth_System SHALL 允许通过环境变量 `WX_DEV_MODE=true` 切换到 mock 模式
|
||||
4. THE Auth_System SHALL 在 `apps/miniprogram/doc/` 中提供联调指南文档,包含微信开发者工具配置、后端启动步骤、测试账号说明
|
||||
262
.kiro/specs/03-miniapp-auth-system/tasks.md
Normal file
262
.kiro/specs/03-miniapp-auth-system/tasks.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# 实现计划:小程序用户认证系统(miniapp-auth-system)
|
||||
|
||||
## 概述
|
||||
|
||||
基于已批准的需求和设计文档,将小程序用户认证系统拆分为增量式编码任务。每个任务构建在前一个任务之上,最终完成完整的认证链路。后端使用 Python + FastAPI,数据库使用 PostgreSQL 纯 SQL,属性测试使用 hypothesis。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. 创建认证数据表和种子数据
|
||||
- [x] 1.1 创建迁移脚本 `db/zqyy_app/migrations/YYYY-MM-DD__p3_create_auth_tables.sql`
|
||||
- 在 `auth` Schema 下创建 `users`、`user_applications`、`site_code_mapping`、`roles`、`permissions`、`role_permissions`、`user_site_roles`、`user_assistant_binding` 共 8 张表
|
||||
- 包含所有字段定义、约束、索引、外键
|
||||
- 使用 `IF NOT EXISTS` 幂等语法
|
||||
- 包含回滚语句(注释形式)
|
||||
- _Requirements: 1.1-1.10_
|
||||
|
||||
- [x] 1.2 创建种子数据脚本 `db/zqyy_app/migrations/YYYY-MM-DD__p3_seed_roles_permissions.sql`
|
||||
- 插入 5 条固定权限:`view_tasks`、`view_board`、`view_board_finance`、`view_board_customer`、`view_board_coach`
|
||||
- 插入默认角色:`coach`、`staff`、`site_admin`、`tenant_admin`
|
||||
- 插入角色-权限映射
|
||||
- 使用 `ON CONFLICT DO NOTHING` 幂等语法
|
||||
- _Requirements: 2.1-2.4_
|
||||
|
||||
- [x] 1.3 在测试库执行迁移脚本并验证
|
||||
- 在 `test_zqyy_app` 中执行建表脚本和种子数据脚本
|
||||
- 验证幂等性:连续执行两次无错误
|
||||
- 验证表结构、约束、索引正确
|
||||
- 验证种子数据完整(5 权限、4 角色、角色-权限映射)
|
||||
- _Requirements: 12.1_
|
||||
|
||||
- [x] 1.4 更新数据库手册和 DDL 基线
|
||||
- 创建 `docs/database/BD_Manual_auth_tables.md`,包含变更说明、兼容性影响、回滚策略、验证 SQL(至少 3 条)
|
||||
- 运行 `python scripts/ops/gen_consolidated_ddl.py` 刷新 DDL 基线
|
||||
- 在数据库手册中记录种子数据内容
|
||||
- _Requirements: 12.2, 12.3, 12.4_
|
||||
|
||||
- [x] 1.5 编写迁移脚本幂等性属性测试
|
||||
- **Property 1: 迁移脚本幂等性**
|
||||
- **Validates: Requirements 1.9, 2.4, 11.5**
|
||||
|
||||
- [x] 2. 扩展 JWT 服务和认证依赖
|
||||
- [x] 2.1 扩展 `apps/backend/app/auth/jwt.py`
|
||||
- 新增 `create_limited_token_pair(user_id)` 函数(pending 用户受限令牌)
|
||||
- 扩展 `create_access_token` 支持 `roles` 参数
|
||||
- 保持向后兼容
|
||||
- _Requirements: 10.1, 10.2, 10.3_
|
||||
|
||||
- [x] 2.2 扩展 `apps/backend/app/auth/dependencies.py`
|
||||
- 扩展 `CurrentUser` 数据类,增加 `roles`、`status`、`limited` 字段
|
||||
- 新增 `get_current_user_or_limited` 依赖(允许 pending 用户)
|
||||
- _Requirements: 10.3, 9.1_
|
||||
|
||||
- [x] 2.3 编写 JWT payload 结构属性测试
|
||||
- **Property 14: JWT payload 结构与状态一致性**
|
||||
- **Validates: Requirements 10.1, 10.2, 10.3**
|
||||
|
||||
- [x] 2.4 编写 JWT 过期/无效拒绝属性测试
|
||||
- **Property 15: JWT 过期/无效令牌拒绝**
|
||||
- **Validates: Requirements 9.4**
|
||||
|
||||
- [x] 3. 检查点 - 确保所有测试通过
|
||||
- 确保所有测试通过,如有问题请向用户确认。
|
||||
|
||||
- [x] 4. 实现微信认证服务
|
||||
- [x] 4.1 创建 `apps/backend/app/services/wechat.py`
|
||||
- 实现 `code2session(code)` 异步函数
|
||||
- 使用 `httpx.AsyncClient` 调用微信 API
|
||||
- 从环境变量读取 `WX_APPID` / `WX_SECRET`
|
||||
- 定义 `WeChatAuthError` 异常类
|
||||
- _Requirements: 3.1, 3.4_
|
||||
|
||||
- [x] 4.2 创建 Pydantic 模型 `apps/backend/app/schemas/xcx_auth.py`
|
||||
- 定义 `WxLoginRequest`、`WxLoginResponse`、`ApplicationRequest`、`ApplicationResponse`、`UserStatusResponse`、`SiteInfo`、`SwitchSiteRequest`、`MatchCandidate`、`ApproveRequest`、`RejectRequest`
|
||||
- `site_code` 使用正则校验 `^[A-Za-z]{2}\d{3}$`
|
||||
- `phone` 使用正则校验 `^\d{11}$`
|
||||
- _Requirements: 4.5_
|
||||
|
||||
- [x] 4.3 创建小程序认证路由 `apps/backend/app/routers/xcx_auth.py`
|
||||
- 实现 `POST /api/xcx/login`:微信登录(查找/创建用户 + 签发 JWT)
|
||||
- 实现 `POST /api/xcx/apply`:提交申请
|
||||
- 实现 `GET /api/xcx/me`:查询自身状态
|
||||
- 实现 `GET /api/xcx/me/sites`:查询关联店铺
|
||||
- 实现 `POST /api/xcx/switch-site`:切换店铺
|
||||
- 实现 `POST /api/xcx/refresh`:刷新令牌
|
||||
- 在 `apps/backend/app/main.py` 中注册路由
|
||||
- _Requirements: 3.2, 3.3, 3.5, 4.1-4.6, 7.1, 7.2, 8.2_
|
||||
|
||||
- [x] 4.4 编写登录创建/查找用户属性测试
|
||||
- **Property 2: 登录创建/查找用户正确性**
|
||||
- **Validates: Requirements 3.2, 3.3**
|
||||
|
||||
- [x] 4.5 编写申请创建正确性属性测试
|
||||
- **Property 4: 申请创建正确性**
|
||||
- **Validates: Requirements 4.1, 4.2, 4.3, 4.4**
|
||||
|
||||
- [x] 4.6 编写手机号格式验证属性测试
|
||||
- **Property 5: 手机号格式验证**
|
||||
- **Validates: Requirements 4.5**
|
||||
|
||||
- [x] 4.7 编写重复申请拒绝属性测试
|
||||
- **Property 6: 重复申请拒绝**
|
||||
- **Validates: Requirements 4.6**
|
||||
|
||||
- [x] 5. 实现申请服务和人员匹配
|
||||
- [x] 5.1 创建申请服务 `apps/backend/app/services/application.py`
|
||||
- 实现 `create_application()`:创建申请 + site_code 映射查找
|
||||
- 实现 `approve_application()`:批准 + 创建绑定/角色
|
||||
- 实现 `reject_application()`:拒绝 + 记录原因
|
||||
- 实现 `get_user_applications()`:查询用户申请列表
|
||||
- _Requirements: 4.1-4.4, 6.1-6.6_
|
||||
|
||||
- [x] 5.2 创建人员匹配服务 `apps/backend/app/services/matching.py`
|
||||
- 实现 `find_candidates(site_id, phone, employee_number)`
|
||||
- 通过 FDW 查询 `fdw_etl.v_dim_assistant` 和 `fdw_etl.v_dim_staff` / `v_dim_staff_ex`
|
||||
- 设置 `app.current_site_id` 进行 RLS 隔离
|
||||
- 合并助教和员工匹配结果
|
||||
- _Requirements: 5.1-5.6_
|
||||
|
||||
- [x] 5.3 创建角色权限服务 `apps/backend/app/services/role.py`
|
||||
- 实现 `get_user_permissions(user_id, site_id)`
|
||||
- 实现 `get_user_sites(user_id)`
|
||||
- 实现 `check_user_has_site_role(user_id, site_id)`
|
||||
- _Requirements: 8.1, 9.1_
|
||||
|
||||
- [x] 5.4 编写人员匹配合并属性测试
|
||||
- **Property 7: 人员匹配合并正确性**
|
||||
- **Validates: Requirements 5.1, 5.2, 5.3, 5.4**
|
||||
|
||||
- [x] 5.5 编写审核操作正确性属性测试
|
||||
- **Property 8: 审核操作正确性**
|
||||
- **Validates: Requirements 6.1, 6.2, 6.3, 6.4, 6.5**
|
||||
|
||||
- [x] 5.6 编写非 pending 审核拒绝属性测试
|
||||
- **Property 9: 非 pending 申请审核拒绝**
|
||||
- **Validates: Requirements 6.6**
|
||||
|
||||
- [ ] 6. 检查点 - 确保所有测试通过
|
||||
- 确保所有测试通过,如有问题请向用户确认。
|
||||
|
||||
- [ ] 7. 实现权限中间件和管理端路由
|
||||
- [ ] 7.1 创建权限中间件 `apps/backend/app/middleware/permission.py`
|
||||
- 实现 `require_permission(*permission_codes)` 依赖
|
||||
- 实现 `require_approved()` 依赖
|
||||
- 检查用户 status + 权限列表
|
||||
- _Requirements: 9.1-9.4_
|
||||
|
||||
- [ ] 7.2 创建管理端审核路由 `apps/backend/app/routers/admin_applications.py`
|
||||
- 实现 `GET /api/admin/applications`:查询申请列表
|
||||
- 实现 `GET /api/admin/applications/{id}`:查询申请详情 + 候选匹配
|
||||
- 实现 `POST /api/admin/applications/{id}/approve`:批准申请
|
||||
- 实现 `POST /api/admin/applications/{id}/reject`:拒绝申请
|
||||
- 在 `apps/backend/app/main.py` 中注册路由
|
||||
- _Requirements: 6.1-6.6, 5.1-5.6_
|
||||
|
||||
- [ ] 7.3 编写权限中间件拦截属性测试
|
||||
- **Property 13: 权限中间件拦截正确性**
|
||||
- **Validates: Requirements 8.3, 9.1, 9.2, 9.3**
|
||||
|
||||
- [ ] 7.4 编写多店铺角色独立分配属性测试
|
||||
- **Property 11: 多店铺角色独立分配**
|
||||
- **Validates: Requirements 8.1**
|
||||
|
||||
- [ ] 7.5 编写店铺切换令牌属性测试
|
||||
- **Property 12: 店铺切换令牌正确性**
|
||||
- **Validates: Requirements 8.2, 10.4**
|
||||
|
||||
- [ ] 8. 集成与端到端验证
|
||||
- [ ] 8.1 更新 `apps/backend/app/config.py` 新增微信配置项
|
||||
- 新增 `WX_APPID`、`WX_SECRET`、`WX_DEV_MODE` 配置读取
|
||||
- _Requirements: 3.1, 14.3_
|
||||
|
||||
- [ ] 8.2 更新 `apps/backend/app/main.py` 注册所有新路由
|
||||
- 确保 `xcx_auth` 和 `admin_applications` 路由已注册
|
||||
- 验证无路由冲突
|
||||
- _Requirements: 全部_
|
||||
|
||||
- [ ] 8.3 实现开发模式 mock 登录端点
|
||||
- 在 `routers/xcx_auth.py` 中新增 `POST /api/xcx/dev-login`
|
||||
- 仅在 `WX_DEV_MODE=true` 时注册
|
||||
- 接受 `openid` 和可选 `status` 参数,直接查找/创建用户并返回 JWT
|
||||
- _Requirements: 14.2, 14.3_
|
||||
|
||||
- [ ] 8.4 编写用户状态查询完整性属性测试
|
||||
- **Property 10: 用户状态查询完整性**
|
||||
- **Validates: Requirements 7.1, 7.2**
|
||||
|
||||
- [ ] 8.5 编写 disabled 用户登录拒绝属性测试
|
||||
- **Property 3: disabled 用户登录拒绝**
|
||||
- **Validates: Requirements 3.5**
|
||||
|
||||
- [ ] 9. 小程序认证前端页面
|
||||
- [ ] 9.1 实现请求封装工具 `apps/miniprogram/miniprogram/utils/request.ts`
|
||||
- 统一请求封装:自动附加 Authorization header
|
||||
- 401 时自动尝试 refresh_token 刷新
|
||||
- 刷新失败时跳转 login 页面
|
||||
- 后端 base URL 从配置读取(开发环境 `http://localhost:8000`)
|
||||
- _Requirements: 13.8_
|
||||
|
||||
- [ ] 9.2 实现登录页 `apps/miniprogram/miniprogram/pages/login/`
|
||||
- 调用 `wx.login()` 获取 code
|
||||
- 发送 code 到 `POST /api/xcx/login`
|
||||
- 根据返回的 `user_status` 路由到对应页面
|
||||
- 存储 token 到 globalData 和 Storage
|
||||
- 参考 H5 原型 `docs/h5_ui/pages/login.html`
|
||||
- _Requirements: 13.1, 13.6, 13.7, 13.8_
|
||||
|
||||
- [ ] 9.3 实现申请表单页 `apps/miniprogram/miniprogram/pages/apply/`
|
||||
- 表单字段:球房ID(site_code)、申请身份、手机号、编号(选填)、昵称
|
||||
- 前端校验:site_code 格式(2字母+3数字)、手机号(11位数字)
|
||||
- 提交到 `POST /api/xcx/apply`
|
||||
- 成功后跳转 reviewing 页面
|
||||
- 参考 H5 原型 `docs/h5_ui/pages/apply.html`
|
||||
- _Requirements: 13.2, 13.3_
|
||||
|
||||
- [ ] 9.4 实现审核等待页 `apps/miniprogram/miniprogram/pages/reviewing/`
|
||||
- 显示当前申请状态(审核中/已拒绝)
|
||||
- 显示申请信息摘要(球房ID、申请身份、手机号)
|
||||
- 拒绝时显示拒绝原因 + "重新申请"按钮
|
||||
- 支持下拉刷新查询最新状态
|
||||
- 参考 H5 原型 `docs/h5_ui/pages/reviewing.html`
|
||||
- _Requirements: 13.4, 13.5_
|
||||
|
||||
- [ ] 9.5 实现无权限页 `apps/miniprogram/miniprogram/pages/no-permission/`
|
||||
- 显示账号已禁用提示
|
||||
- 参考 H5 原型 `docs/h5_ui/pages/no-permission.html`
|
||||
- _Requirements: 13.7_
|
||||
|
||||
- [ ] 9.6 更新 `app.ts` 和 `app.json`
|
||||
- 在 `app.json` 中注册新页面(login、apply、reviewing、no-permission)
|
||||
- 在 `app.ts` 的 `onLaunch` 中实现自动登录逻辑
|
||||
- 根据用户状态路由到对应页面
|
||||
- 扩展 globalData 类型定义(token、userInfo、currentSiteId、sites)
|
||||
- _Requirements: 13.8_
|
||||
|
||||
- [ ] 10. 前后端联调验证
|
||||
- [ ] 10.1 编写联调指南文档 `apps/miniprogram/doc/auth-integration-guide.md`
|
||||
- 微信开发者工具项目导入配置说明
|
||||
- 后端启动步骤(含 `WX_DEV_MODE=true` 配置)
|
||||
- 测试流程:mock 登录 → 申请 → 管理端审核 → 重新登录验证
|
||||
- 常见问题排查
|
||||
- _Requirements: 14.1, 14.4_
|
||||
|
||||
- [ ] 10.2 在微信开发者工具中执行联调验证
|
||||
- 验证登录流程:wx.login → 后端 → JWT 返回
|
||||
- 验证申请流程:表单提交 → 后端创建申请 → 审核等待页展示
|
||||
- 验证状态路由:pending/approved/rejected/disabled 各状态正确跳转
|
||||
- 验证 token 刷新:access_token 过期后自动刷新
|
||||
- _Requirements: 14.1_
|
||||
|
||||
- [ ] 11. 最终检查点 - 确保所有测试通过
|
||||
- 确保所有测试通过,如有问题请向用户确认。
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的任务为可选,可跳过以加速 MVP
|
||||
- 每个任务引用了具体的需求编号,确保可追溯
|
||||
- 检查点确保增量验证
|
||||
- 属性测试验证通用正确性属性(hypothesis,最少 100 次迭代)
|
||||
- 单元测试验证具体例子和边界情况
|
||||
- 所有数据库操作在测试库 `test_zqyy_app` 进行
|
||||
- 迁移脚本放在 `db/zqyy_app/migrations/` 目录
|
||||
- 属性测试放在 `tests/` 目录(Monorepo 级)
|
||||
Reference in New Issue
Block a user