635d127f95
compare.py 透传壳新增后端 harvest(不再依赖客户端 POST /compare/record): - 帧0(客户端首帧不带 trace_id)由 app-server 用 uuid 签发 trace_id、注入转发 body、 回填响应顶层,并按 trace_id 建 running 行(harvest_running) - done 帧(command=done 且 continue=false)更新为 success/failed 终态,并对本次新落成 success 幂等发一次邀请奖(harvest_done + try_reward_on_compare) - /trace/finalize(用户终止/Phase1 未识别,无 done)更新为 cancelled/failed, 不降级已 success(harvest_abort) - 软鉴权 OptionalUser(deps.py 新增 get_current_user_optional):新客户端带 JWT 绑 user_id,老客户端匿名建行、user_id 暂空,由其后续 POST /compare/record 按 trace_id reconcile;新版覆盖够高后可收紧成硬鉴权 - 结构化日志(logging.py 加 trace_id_ctx ContextVar):请求级 trace_id 贯穿 harvest 各阶段 comparison_record 表结构调整(迁移 comparison_record_trace_unique): - 唯一键 (user_id,trace_id) → trace_id 单列(harvest 帧0 建行时 user_id 可能暂缺) - user_id 改可空 repositories/comparison.py 加 harvest_running/harvest_done/harvest_abort; compare_record.py 的 POST /record 降级为灰度期老客户端兼容(按 trace_id 幂等、不降级 success); 补 tests/test_compare_harvest.py,test_compare_proxy/milestone 适配。 文档同步: docs/database/comparison_record.md 更新表结构(唯一键/user_id 可空/status 取值) 与写入模型; docs/api/compare/compare-record-report.md 标注 POST /record 灰度定位。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
3.0 KiB
Python
83 lines
3.0 KiB
Python
"""API 层共享依赖:DB session、当前登录用户。"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Annotated
|
|
|
|
from fastapi import Depends, HTTPException, status
|
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.security import TokenError, decode_token
|
|
from app.db.session import get_db
|
|
from app.models.user import User
|
|
|
|
logger = logging.getLogger("shagua.deps")
|
|
|
|
# auto_error=True → 没传 Authorization header 时直接 403。但我们想 401,
|
|
# 所以手动 auto_error=False + raise HTTPException
|
|
_bearer = HTTPBearer(auto_error=False, scheme_name="Bearer")
|
|
|
|
|
|
def get_current_user(
|
|
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)],
|
|
db: Annotated[Session, Depends(get_db)],
|
|
) -> User:
|
|
"""从 Authorization: Bearer <access_token> 解出当前 user。
|
|
|
|
失败时统一 401,WWW-Authenticate 头让客户端知道要重新登录。
|
|
"""
|
|
if credentials is None or credentials.scheme.lower() != "bearer":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="missing bearer token",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
try:
|
|
payload = decode_token(credentials.credentials, expected_type="access")
|
|
except TokenError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail=str(e),
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
) from e
|
|
|
|
user_id = int(payload["sub"])
|
|
user = db.get(User, user_id)
|
|
if user is None or user.status != "active":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="user not found or disabled",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
return user
|
|
|
|
|
|
def get_current_user_optional(
|
|
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)],
|
|
db: Annotated[Session, Depends(get_db)],
|
|
) -> User | None:
|
|
"""软鉴权:有合法 Bearer 就返回 user,否则(无 header / 无效 token / 用户禁用)一律返回
|
|
None,**不 raise**。
|
|
|
|
比价 step / finalize 灰度期用:新客户端带 JWT → 拿到 user_id 绑定 harvest 行;
|
|
老客户端(不带 JWT)→ None,harvest 行 user_id 暂空,由其后续 /compare/record 上报补齐。
|
|
等新版覆盖率够高,再把这几条端点从软鉴权收紧成硬 get_current_user。
|
|
"""
|
|
if credentials is None or credentials.scheme.lower() != "bearer":
|
|
return None
|
|
try:
|
|
payload = decode_token(credentials.credentials, expected_type="access")
|
|
user = db.get(User, int(payload["sub"]))
|
|
except (TokenError, KeyError, ValueError, TypeError):
|
|
return None
|
|
if user is None or user.status != "active":
|
|
return None
|
|
return user
|
|
|
|
|
|
CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
OptionalUser = Annotated[User | None, Depends(get_current_user_optional)]
|
|
DbSession = Annotated[Session, Depends(get_db)]
|