Files
Neo-ZQYY/.kiro/specs/03-miniapp-auth-system/design.md
Neo b25308c3f4 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 排除规则
2026-02-26 08:03:53 +08:00

32 KiB
Raw Blame History

设计文档小程序用户认证系统miniapp-auth-system

概述

本设计在 P1miniapp-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} ───── │                                │

架构

分层架构

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_idRLS 隔离)。
    但 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

职责:查询用户在指定店铺下的角色和权限。

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 状态
  • 权限检查结果可考虑短期缓存(当前版本不缓存,每次查库)

组件 6JWT 服务扩展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 查询关联店铺 JWTapproved
POST /api/xcx/switch-site 切换店铺 JWTapproved
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

组件 8Pydantic 模型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_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 3disabled 用户登录拒绝

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_codesite_code_mapping 中有映射,记录的 site_id 应等于映射值;若无映射,site_id 为 NULL 但申请仍成功。若提供了 nicknameauth.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_idphone 组合,匹配服务返回的候选列表应满足:(1) 每条候选的 source_typeassistantstaff(2) 助教来源的候选来自 v_dim_assistant 表中 site_idmobile 匹配的记录;(3) 员工来源的候选来自 v_dim_staff 表中 site_idmobile(或 job_num)匹配的记录;(4) 列表是两个来源结果的并集,无遗漏。

Validates: Requirements 5.1, 5.2, 5.3, 5.4

Property 8审核操作正确性

For any status=pending 的申请:(1) 批准操作后,申请 status 变为 approvedauth.user_site_roles 中新增角色记录,auth.users.status 变为 approvedreviewer_idreviewed_at 非空;(2) 若提供了 binding 信息,auth.user_assistant_binding 中新增绑定记录;(3) 拒绝操作后,申请 status 变为 rejectedreview_note 非空,reviewer_idreviewed_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 14JWT payload 结构与状态一致性

For any 通过登录签发的 JWT(1) 解码后应包含 subuser_idtypeexp 字段;(2) 若用户 status 为 approvedpayload 应包含 site_idroles(3) 若用户 status 为 pendingpayload 应包含 limited=True,不含 site_idroles

Validates: Requirements 10.1, 10.2, 10.3

Property 15JWT 过期/无效令牌拒绝

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 页面(显示拒绝原因 + 重新申请按钮)
  │     │
  │     └── 返回 403disabled
  │           └── 跳转 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,包含:

  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_code2字母+3数字、phone11位数字
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迁移幂等性、P3disabled 登录拒绝、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 逻辑

集成测试

集成测试通过验证脚本实现,覆盖:

  • 迁移脚本幂等性验证(执行两次无错误)
  • 种子数据完整性验证(权限和角色数量正确)
  • 完整认证流程:登录 → 申请 → 审核 → 权限验证