sa_balance.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. """超级管理员余额管理路由"""
  2. import logging
  3. from decimal import Decimal
  4. from fastapi import APIRouter, Depends, HTTPException, Query
  5. from pydantic import BaseModel
  6. from sqlalchemy.ext.asyncio import AsyncSession
  7. from app.database import get_db
  8. from app.services import sa_balance as svc
  9. logger = logging.getLogger(__name__)
  10. router = APIRouter(prefix="/api/sa-balance", tags=["超管余额"])
  11. public_router = APIRouter(prefix="/api/public/sa-balance", tags=["超管余额公开接口"])
  12. # ---------- 请求/响应 Schema ----------
  13. class RechargeRequest(BaseModel):
  14. sa_id: int
  15. amount: float
  16. remark: str = ""
  17. class DeductRequest(BaseModel):
  18. sa_id: int
  19. amount: float
  20. biz_order_no: str
  21. # ---------- 内部控制面板接口 ----------
  22. @router.post("/recharge", summary="给超管充值")
  23. async def handle_recharge(
  24. payload: RechargeRequest,
  25. db: AsyncSession = Depends(get_db),
  26. ):
  27. try:
  28. result = await svc.recharge(db, payload.sa_id, Decimal(str(payload.amount)), payload.remark)
  29. return {"code": 200, "data": result}
  30. except ValueError as e:
  31. raise HTTPException(status_code=400, detail=str(e))
  32. @router.get("/list", summary="所有超管余额列表")
  33. async def handle_list_sa_balance(
  34. db: AsyncSession = Depends(get_db),
  35. ):
  36. items = await svc.get_all_sa_balance(db)
  37. return {"code": 200, "data": items}
  38. @router.get("/{sa_id}", summary="查询超管余额详情")
  39. async def handle_get_sa_balance(
  40. sa_id: int,
  41. db: AsyncSession = Depends(get_db),
  42. ):
  43. try:
  44. info = await svc.get_sa_balance_info(db, sa_id)
  45. return {"code": 200, "data": info}
  46. except ValueError as e:
  47. raise HTTPException(status_code=404, detail=str(e))
  48. @router.get("/{sa_id}/logs", summary="查询超管余额变动日志")
  49. async def handle_get_sa_balance_logs(
  50. sa_id: int,
  51. page: int = Query(1, ge=1),
  52. size: int = Query(20, ge=1, le=100),
  53. db: AsyncSession = Depends(get_db),
  54. ):
  55. result = await svc.get_balance_logs(db, sa_id, page, size)
  56. return {"code": 200, "data": result}
  57. # ---------- AIGC-Space 调用的公开接口 ----------
  58. @public_router.post("/deduct", summary="扣减超管余额(AIGC-Space 调用)")
  59. async def handle_deduct(
  60. payload: DeductRequest,
  61. db: AsyncSession = Depends(get_db),
  62. ):
  63. try:
  64. ok, reason = await svc.deduct(db, payload.sa_id, Decimal(str(payload.amount)), payload.biz_order_no)
  65. return {"code": 200, "success": ok, "reason": reason}
  66. except Exception as e:
  67. # 扣减异常(DB 错误等),写入补偿记录,后台任务会重试
  68. logger.exception("超管扣减异常,写入补偿记录: sa_id=%d, amount=%s, order=%s",
  69. payload.sa_id, payload.amount, payload.biz_order_no)
  70. try:
  71. from app.services.compensation_service import record_pending
  72. await record_pending(
  73. db,
  74. target_type="sa",
  75. target_id=payload.sa_id,
  76. amount=Decimal(str(payload.amount)),
  77. biz_order_no=payload.biz_order_no,
  78. error_msg=str(e),
  79. )
  80. # 返回 success=True,告诉调用方"已接收,补偿任务会处理"
  81. return {"code": 200, "success": True, "reason": "已记录待补偿", "compensated": True}
  82. except Exception as record_err:
  83. logger.error("补偿记录写入失败: %s", record_err)
  84. return {"code": 200, "success": False, "reason": f"扣减异常且补偿记录失败: {e}"}
  85. class RecordPendingRequest(BaseModel):
  86. sa_id: int
  87. amount: float
  88. biz_order_no: str
  89. error_msg: str = ""
  90. @public_router.post("/record-pending", summary="记录待补偿扣减(AIGC-Space 网络失败时调用)")
  91. async def handle_record_pending(
  92. payload: RecordPendingRequest,
  93. db: AsyncSession = Depends(get_db),
  94. ):
  95. """当 AIGC-Space 调用 /deduct 失败(网络超时等)时,调用此端点记录待补偿"""
  96. try:
  97. from app.services.compensation_service import record_pending
  98. await record_pending(
  99. db,
  100. target_type="sa",
  101. target_id=payload.sa_id,
  102. amount=Decimal(str(payload.amount)),
  103. biz_order_no=payload.biz_order_no,
  104. error_msg=payload.error_msg,
  105. )
  106. return {"code": 200, "success": True, "reason": "已记录待补偿"}
  107. except Exception as e:
  108. logger.error("记录待补偿失败: %s", e)
  109. return {"code": 200, "success": False, "reason": str(e)}
  110. @public_router.get("/{sa_id}/status", summary="查询超管余额状态(AIGC-Space 调用)")
  111. async def handle_sa_balance_status(
  112. sa_id: int,
  113. db: AsyncSession = Depends(get_db),
  114. ):
  115. try:
  116. balance = await svc.get_balance(db, sa_id)
  117. return {
  118. "code": 200,
  119. "data": {
  120. "sa_id": sa_id,
  121. "balance": str(balance),
  122. "is_sufficient": balance > 0,
  123. },
  124. }
  125. except ValueError as e:
  126. raise HTTPException(status_code=404, detail=str(e))