Files
shaguabijia-app-server/app/api/deps.py
T
marco 635d127f95 比价记录改后端 harvest: 透传壳落库替代客户端主动 POST
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>
2026-07-04 01:56:46 +08:00

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)]