FinBuddy_Web 计费事务安全与支付流程

· FinBuddy_Web

今日进展

  1. 计费事务原子性修复:feature_billing_service.py 的 Step 5-7(余额扣除 + 日用量更新 + 流水写入)改用 begin_nested() savepoint 包裹,确保扣除和写入在同一事务中完成
  2. session_id 隐私脱敏:日志中只记录 session_id 前 8 字符,避免泄露完整会话标识
  3. frozen_reason 获取方式修正:从 hasattr 改为 getattr,更 Pythonic
  4. 版本比较日志增强:package_service.pyis_newer_versionmeets_min_version 在 parse 失败时增加 debug 日志
  5. 支付前端流程:新增 payment.ts 服务、更新 Packages.vue 套餐购买页面、更新类型定义
  6. Auth 模块更新:认证 schemas 和 service 修改
  7. LLM 代理服务更新:proxy_service.py 修改
  8. 环境变量更新:dev/staging/prod 三套 .env 同步更新

关键代码/伪代码

计费事务原子性 — savepoint 方案

# 之前:余额扣除和流水写入不在同一事务
# 并发场景下可能出现:扣了余额但没写流水
# 严重时导致用户余额凭空减少

# 改造前(有 bug)
deducted_account = await self.account_repo.deduct_balance(user_id, actual_fc)
# 如果这里异常,余额扣了但流水没写
tx = Transaction(user_id=user_id, ...)
self.session.add(tx)
await self.session.commit()

# 改造后(savepoint 保证原子性)
# 使用 begin_nested() 而非 rollback()
# 回滚 savepoint 不影响外层 session 状态
async with self.session.begin_nested():
    deducted_account = await self.account_repo.deduct_balance(user_id, actual_fc)
    if deducted_account is None:
        raise InsufficientBalanceException(...)

    # 刷新 ORM 对象,确保读到最新数据库值
    await self.session.refresh(deducted_account)

    # 更新日用量/月用量
    deducted_account.daily_used += actual_fc
    deducted_account.monthly_used += actual_fc

    # 创建交易流水
    tx = Transaction(
        user_id=user_id,
        tx_type="consume",
        feature=feature_code,
        amount=-actual_fc,
        balance_after=deducted_account.balance,
        ...
    )
    self.session.add(tx)
    # savepoint 结束时自动 commit
    # 如果任何一步失败,整个 savepoint 回滚

session_id 隐私脱敏

# 之前:完整 session_id 写入日志
logger.info("bill_feature_start",
    user_id=user_id,
    feature_code=feature_code)  # session_id 可能在其他地方泄露

# 改造后:只记录前 8 字符
safe_session_id = session_id[:8] if session_id else None
logger.info("bill_feature_start",
    user_id=user_id,
    feature_code=feature_code,
    session_id_prefix=safe_session_id)  # 足够追踪,不泄露完整标识

遇到的问题

  • 计费事务不一致:线上发现偶尔有"扣了余额没写流水"的情况,查了半天发现是并发请求时 deduct_balance 和流水写入不在同一事务。savepoint 方案解决了这个问题,而且不会破坏外层事务状态
  • begin_nested() vs rollback():一开始想用 rollback 处理失败场景,但 rollback 会把整个 session 置为无效状态。savepoint 回滚只影响嵌套部分,外层 session 不受影响
  • 支付前端联调:payment.tsPackages.vue 需要和后端支付接口对齐,目前还在联调中

明日计划

  • 支付流程端到端测试:从选套餐到支付到 FC 到账的完整链路
  • 计费事务压力测试:模拟并发扣费场景,验证 savepoint 方案的可靠性
  • Auth 模块改动验证:确保登录/注册/Token 刷新流程正常