sa_balance.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """超级管理员余额管理服务"""
  2. import logging
  3. from decimal import Decimal
  4. from sqlalchemy import select, func
  5. from sqlalchemy.ext.asyncio import AsyncSession
  6. from sqlalchemy.exc import IntegrityError
  7. from app.models.monitoring import SuperAdmin, SuperAdminBalanceLog
  8. from app.config import settings
  9. logger = logging.getLogger(__name__)
  10. async def get_balance(db: AsyncSession, sa_id: int) -> Decimal:
  11. """获取超管余额"""
  12. result = await db.execute(
  13. select(SuperAdmin.balance).where(SuperAdmin.id == sa_id)
  14. )
  15. row = result.scalar_one_or_none()
  16. if row is None:
  17. raise ValueError(f"超管 {sa_id} 不存在")
  18. return Decimal(str(row or 0))
  19. async def check_balance(db: AsyncSession, sa_id: int) -> bool:
  20. """检查超管余额是否 > 0"""
  21. balance = await get_balance(db, sa_id)
  22. return balance > 0
  23. async def recharge(
  24. db: AsyncSession,
  25. sa_id: int,
  26. amount: Decimal,
  27. remark: str = "",
  28. ) -> dict:
  29. """充值超管余额。返回 {balance_after, message}"""
  30. if amount <= 0:
  31. raise ValueError("充值金额必须大于 0")
  32. result = await db.execute(
  33. select(SuperAdmin).where(SuperAdmin.id == sa_id).with_for_update()
  34. )
  35. sa = result.scalar_one_or_none()
  36. if not sa:
  37. raise ValueError(f"超管 {sa_id} 不存在")
  38. current = Decimal(str(sa.balance or 0))
  39. balance_after = current + amount
  40. sa.balance = balance_after
  41. # 充值后重置预警标记,允许下次余额下降时再次触发
  42. sa.balance_warning_sent = False
  43. sa.balance_depleted_sent = False
  44. log = SuperAdminBalanceLog(
  45. super_admin_id=sa_id,
  46. change_amount=amount,
  47. balance_after=balance_after,
  48. biz_type="recharge",
  49. remark=remark,
  50. )
  51. db.add(log)
  52. await db.commit()
  53. logger.info("超管 %d 充值 %s 元,余额 %s", sa_id, amount, balance_after)
  54. return {"balance_after": str(balance_after), "message": "充值成功"}
  55. async def deduct(
  56. db: AsyncSession,
  57. sa_id: int,
  58. amount: Decimal,
  59. biz_order_no: str,
  60. ) -> tuple[bool, str]:
  61. """扣减超管余额。返回 (success, reason)。幂等:同一 biz_order_no 不重复扣减。"""
  62. if amount <= 0:
  63. return False, "扣减金额必须大于 0"
  64. # 先检查幂等:是否已存在相同 biz_order_no 的记录
  65. if biz_order_no:
  66. exist = await db.execute(
  67. select(SuperAdminBalanceLog.id).where(
  68. SuperAdminBalanceLog.super_admin_id == sa_id,
  69. SuperAdminBalanceLog.biz_type == "consume",
  70. SuperAdminBalanceLog.biz_order_no == biz_order_no,
  71. )
  72. )
  73. if exist.scalar_one_or_none():
  74. return True, "已扣减(幂等)"
  75. result = await db.execute(
  76. select(SuperAdmin).where(SuperAdmin.id == sa_id).with_for_update()
  77. )
  78. sa = result.scalar_one_or_none()
  79. if not sa:
  80. return False, f"超管 {sa_id} 不存在"
  81. current = Decimal(str(sa.balance or 0))
  82. if current < amount:
  83. logger.warning("超管 %d 余额不足: 当前 %s, 需扣 %s", sa_id, current, amount)
  84. return False, "余额不足"
  85. balance_after = current - amount
  86. sa.balance = balance_after
  87. log = SuperAdminBalanceLog(
  88. super_admin_id=sa_id,
  89. change_amount=-amount,
  90. balance_after=balance_after,
  91. biz_type="consume",
  92. biz_order_no=biz_order_no,
  93. )
  94. db.add(log)
  95. try:
  96. await db.commit()
  97. except IntegrityError:
  98. await db.rollback()
  99. return True, "已扣减(幂等)"
  100. logger.info("超管 %d 扣减 %s 元(订单 %s),余额 %s", sa_id, amount, biz_order_no, balance_after)
  101. return True, "扣减成功"
  102. async def get_balance_logs(
  103. db: AsyncSession,
  104. sa_id: int,
  105. page: int = 1,
  106. size: int = 20,
  107. ) -> dict:
  108. """分页查询余额变动日志"""
  109. count_stmt = select(func.count()).select_from(SuperAdminBalanceLog).where(
  110. SuperAdminBalanceLog.super_admin_id == sa_id
  111. )
  112. total_result = await db.execute(count_stmt)
  113. total = total_result.scalar() or 0
  114. offset = (page - 1) * size
  115. stmt = (
  116. select(SuperAdminBalanceLog)
  117. .where(SuperAdminBalanceLog.super_admin_id == sa_id)
  118. .order_by(SuperAdminBalanceLog.created_at.desc())
  119. .offset(offset)
  120. .limit(size)
  121. )
  122. result = await db.execute(stmt)
  123. logs = result.scalars().all()
  124. items = []
  125. for log in logs:
  126. items.append({
  127. "id": log.id,
  128. "change_amount": str(log.change_amount),
  129. "balance_after": str(log.balance_after),
  130. "biz_type": log.biz_type,
  131. "biz_order_no": log.biz_order_no,
  132. "remark": log.remark,
  133. "created_at": log.created_at.isoformat() if log.created_at else None,
  134. })
  135. return {"total": total, "items": items}
  136. async def get_all_sa_balance(db: AsyncSession) -> list[dict]:
  137. """获取所有超管的余额信息(用于预警检查)"""
  138. result = await db.execute(
  139. select(SuperAdmin).order_by(SuperAdmin.id)
  140. )
  141. sas = result.scalars().all()
  142. return [
  143. {
  144. "id": sa.id,
  145. "username": sa.username,
  146. "remark": sa.remark,
  147. "phone": sa.phone,
  148. "balance": Decimal(str(sa.balance or 0)),
  149. "balance_warning_sent": sa.balance_warning_sent,
  150. "balance_depleted_sent": sa.balance_depleted_sent,
  151. }
  152. for sa in sas
  153. ]
  154. async def get_sa_balance_info(db: AsyncSession, sa_id: int) -> dict:
  155. """获取超管余额详情(余额 + 基本信息)"""
  156. result = await db.execute(
  157. select(SuperAdmin).where(SuperAdmin.id == sa_id)
  158. )
  159. sa = result.scalar_one_or_none()
  160. if not sa:
  161. raise ValueError(f"超管 {sa_id} 不存在")
  162. return {
  163. "id": sa.id,
  164. "username": sa.username,
  165. "nickname": sa.nickname,
  166. "remark": sa.remark,
  167. "phone": sa.phone,
  168. "balance": str(sa.balance or 0),
  169. "balance_warning_sent": sa.balance_warning_sent,
  170. "balance_depleted_sent": sa.balance_depleted_sent,
  171. }