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:
Neo
2026-02-26 08:03:53 +08:00
parent fafc95e64c
commit b25308c3f4
224 changed files with 17660 additions and 32198 deletions

View File

@@ -5,9 +5,15 @@ FastAPI 依赖注入:从 JWT 提取当前用户信息。
@router.get("/protected")
async def protected_endpoint(user: CurrentUser = Depends(get_current_user)):
print(user.user_id, user.site_id)
# 允许 pending 用户(受限令牌)访问
@router.get("/apply")
async def apply_endpoint(user: CurrentUser = Depends(get_current_user_or_limited)):
if user.limited:
... # 受限逻辑
"""
from dataclasses import dataclass
from dataclasses import dataclass, field
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -24,7 +30,10 @@ class CurrentUser:
"""从 JWT 解析出的当前用户上下文。"""
user_id: int
site_id: int
site_id: int = 0
roles: list[str] = field(default_factory=list)
status: str = "pending"
limited: bool = False
async def get_current_user(
@@ -33,7 +42,7 @@ async def get_current_user(
"""
FastAPI 依赖:从 Authorization header 提取 JWT验证后返回用户信息。
失败时抛出 401。
要求完整令牌(非 limited失败时抛出 401。
"""
token = credentials.credentials
try:
@@ -45,6 +54,14 @@ async def get_current_user(
headers={"WWW-Authenticate": "Bearer"},
)
# 受限令牌不允许通过此依赖
if payload.get("limited"):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="受限令牌无法访问此端点",
headers={"WWW-Authenticate": "Bearer"},
)
user_id_raw = payload.get("sub")
site_id = payload.get("site_id")
@@ -64,4 +81,78 @@ async def get_current_user(
headers={"WWW-Authenticate": "Bearer"},
)
return CurrentUser(user_id=user_id, site_id=site_id)
roles = payload.get("roles", [])
return CurrentUser(
user_id=user_id,
site_id=site_id,
roles=roles,
status="approved",
limited=False,
)
async def get_current_user_or_limited(
credentials: HTTPAuthorizationCredentials = Depends(_bearer_scheme),
) -> CurrentUser:
"""
FastAPI 依赖:允许 pending 用户(受限令牌)访问。
- 受限令牌limited=True返回 CurrentUser(limited=True, roles=[], status="pending")
- 完整令牌:正常返回 CurrentUser
"""
token = credentials.credentials
try:
payload = decode_access_token(token)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的令牌",
headers={"WWW-Authenticate": "Bearer"},
)
user_id_raw = payload.get("sub")
if user_id_raw is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="令牌缺少必要字段",
headers={"WWW-Authenticate": "Bearer"},
)
try:
user_id = int(user_id_raw)
except (TypeError, ValueError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="令牌中 user_id 格式无效",
headers={"WWW-Authenticate": "Bearer"},
)
# 受限令牌pending 用户
if payload.get("limited"):
return CurrentUser(
user_id=user_id,
site_id=0,
roles=[],
status="pending",
limited=True,
)
# 完整令牌:要求 site_id
site_id = payload.get("site_id")
if site_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="令牌缺少必要字段",
headers={"WWW-Authenticate": "Bearer"},
)
roles = payload.get("roles", [])
return CurrentUser(
user_id=user_id,
site_id=site_id,
roles=roles,
status="approved",
limited=False,
)

View File

@@ -27,11 +27,14 @@ def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def create_access_token(user_id: int, site_id: int) -> str:
def create_access_token(
user_id: int, site_id: int, roles: list[str] | None = None
) -> str:
"""
生成 access_token。
payload: sub=user_id, site_id, type=access, exp
payload: sub=user_id, site_id, roles, type=access, exp
roles 参数默认 None保持向后兼容。
"""
expire = datetime.now(timezone.utc) + timedelta(
minutes=config.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
@@ -42,6 +45,8 @@ def create_access_token(user_id: int, site_id: int) -> str:
"type": "access",
"exp": expire,
}
if roles is not None:
payload["roles"] = roles
return jwt.encode(payload, config.JWT_SECRET_KEY, algorithm=config.JWT_ALGORITHM)
@@ -63,15 +68,46 @@ def create_refresh_token(user_id: int, site_id: int) -> str:
return jwt.encode(payload, config.JWT_SECRET_KEY, algorithm=config.JWT_ALGORITHM)
def create_token_pair(user_id: int, site_id: int) -> dict[str, str]:
def create_token_pair(user_id: int, site_id: int, roles: list[str] | None = None) -> dict[str, str]:
"""生成 access_token + refresh_token 令牌对。"""
return {
"access_token": create_access_token(user_id, site_id),
"access_token": create_access_token(user_id, site_id, roles=roles),
"refresh_token": create_refresh_token(user_id, site_id),
"token_type": "bearer",
}
def create_limited_token_pair(user_id: int) -> dict[str, str]:
"""
为 pending 用户签发受限令牌。
payload 不含 site_id 和 roles仅包含 user_id + type + limited=True。
受限令牌仅允许访问申请提交和状态查询端点。
"""
now = datetime.now(timezone.utc)
access_payload = {
"sub": str(user_id),
"type": "access",
"limited": True,
"exp": now + timedelta(minutes=config.JWT_ACCESS_TOKEN_EXPIRE_MINUTES),
}
refresh_payload = {
"sub": str(user_id),
"type": "refresh",
"limited": True,
"exp": now + timedelta(days=config.JWT_REFRESH_TOKEN_EXPIRE_DAYS),
}
return {
"access_token": jwt.encode(
access_payload, config.JWT_SECRET_KEY, algorithm=config.JWT_ALGORITHM
),
"refresh_token": jwt.encode(
refresh_payload, config.JWT_SECRET_KEY, algorithm=config.JWT_ALGORITHM
),
"token_type": "bearer",
}
def decode_token(token: str) -> dict:
"""
解码并验证 JWT 令牌。