## 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 排除规则
32 KiB
设计文档:小程序用户认证系统(miniapp-auth-system)
概述
本设计在 P1(miniapp-db-foundation)已建立的 auth Schema 基础上,实现完整的小程序用户认证链路:
- 微信登录:小程序端发送
code→ 后端调用微信code2Session→ 获取openid→ 创建/查找用户 → 签发 JWT - 用户申请:新用户填写球房ID + 手机号 + 申请身份 → 系统自动匹配助教/员工 → 管理员审核
- 权限控制:基于
user_site_roles+role_permissions的 RBAC 模型,权限中间件拦截无权请求 - 多店铺支持:一个用户可关联多个
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} ───── │ │
架构
分层架构
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
请求处理流程
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 调用。
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)
职责:处理用户申请的创建、查询、审核。
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 外部表中查找候选匹配。
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 库appSchema 的 RLS 视图 - 匹配查询使用业务库连接(
get_connection()),通过SET LOCAL app.current_site_id设置隔离 - 如果
site_code无法映射到site_id,直接返回空列表
组件 4:角色权限服务(services/role.py)
职责:查询用户在指定店铺下的角色和权限。
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 检查用户权限。
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 服务,支持微信登录场景。
扩展内容:
# 新增:创建受限令牌(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)
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 图
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_openidUNIQUE — 一个微信用户对应一条记录site_code_mapping.site_codeUNIQUE — 球房ID 唯一site_code_mapping.site_idUNIQUE — 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 变更必须经过以下流程:
- 测试库执行:在
test_zqyy_app中执行迁移脚本,验证无错误 - 幂等性验证:连续执行两次,第二次无错误
- 数据库手册更新:创建/更新
docs/database/BD_Manual_auth_tables.md,格式参照现有BD_Manual_auth_biz_schemas.md - 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 全局状态管理
// 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)
/**
* 统一请求封装:
* 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:
@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,包含:
- 微信开发者工具项目导入配置(appid、不校验合法域名)
- 后端启动命令(
cd apps/backend && uvicorn app.main:app --reload) - 小程序请求域名配置(开发环境指向
http://localhost:8000) - 测试流程:登录 → 申请 → 管理端审核 → 重新登录验证
- 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 逻辑
集成测试
集成测试通过验证脚本实现,覆盖:
- 迁移脚本幂等性验证(执行两次无错误)
- 种子数据完整性验证(权限和角色数量正确)
- 完整认证流程:登录 → 申请 → 审核 → 权限验证