Files
Pine f1147dced3 feat(pay): 微信支付V3子应用与算力充值
- app/pay 独立支付子应用(config/wxpay/models/repository/service/routers),便于后期拆分微服务
- 充值订单表 0026_compute_recharge;套餐支持折扣(discount)与上架开关(enabled),DB system_configs 优先、env 兜底
- 统一小程序支付:桌面端出小程序码→扫码进小程序确认页按 openid 发起 JSAPI→回调幂等到账(引擎 adjust_user_quota+镜像回写)
- 内嵌 wechatpayv3(含 async_),响应验签失败降级告警;新增 aiofiles 依赖

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 22:29:44 +08:00

116 lines
4.7 KiB
Python

# -*- coding: utf-8 -*-
"""支付子应用仓储:充值订单行级操作(不掺 HTTP 逻辑)。"""
from __future__ import annotations
from sqlalchemy import select
from ..infrastructure.repositories import new_id, utcnow_iso
from .models import ComputeRechargeOrder
def _to_dict(o: ComputeRechargeOrder) -> dict:
return {
"id": o.id, "order_no": o.order_no, "user_id": o.user_id, "username": o.username,
"engine_user_id": o.engine_user_id, "package_id": o.package_id,
"amount_fen": o.amount_fen, "quota_micro": o.quota_micro,
"bonus_quota_micro": o.bonus_quota_micro, "client_type": o.client_type,
"appid": o.appid, "status": o.status, "prepay_id": o.prepay_id,
"transaction_id": o.transaction_id, "code_url": o.code_url,
"expires_at": o.expires_at, "paid_at": o.paid_at, "credited_at": o.credited_at,
"notify_payload": o.notify_payload, "created_at": o.created_at, "updated_at": o.updated_at,
}
class ComputeRechargeOrderRepository:
def __init__(self, session):
self.session = session
async def create(self, fields: dict) -> dict:
now = utcnow_iso()
row = ComputeRechargeOrder(
id=new_id("cr"), created_at=now, updated_at=now,
**{k: v for k, v in fields.items() if hasattr(ComputeRechargeOrder, k) and k != "id"},
)
self.session.add(row)
await self.session.commit()
return _to_dict(row)
async def get_by_order_no(self, order_no: str) -> dict | None:
row = await self.session.scalar(
select(ComputeRechargeOrder).where(ComputeRechargeOrder.order_no == order_no)
)
return _to_dict(row) if row else None
async def find_pending_same_amount(self, user_id: str, amount_fen: int, now_iso: str) -> dict | None:
"""同用户同金额未过期 pending 单(下单防重复扫码复用)。"""
row = await self.session.scalar(
select(ComputeRechargeOrder)
.where(
ComputeRechargeOrder.user_id == user_id,
ComputeRechargeOrder.amount_fen == amount_fen,
ComputeRechargeOrder.status == "pending",
ComputeRechargeOrder.expires_at > now_iso,
)
.order_by(ComputeRechargeOrder.created_at.desc())
.limit(1)
)
return _to_dict(row) if row else None
async def list_by_user(self, user_id: str, limit: int = 50) -> list[dict]:
rows = (await self.session.scalars(
select(ComputeRechargeOrder)
.where(ComputeRechargeOrder.user_id == user_id)
.order_by(ComputeRechargeOrder.created_at.desc())
.limit(limit)
)).all()
return [_to_dict(r) for r in rows]
async def _row(self, order_no: str) -> ComputeRechargeOrder | None:
return await self.session.scalar(
select(ComputeRechargeOrder).where(ComputeRechargeOrder.order_no == order_no)
)
async def mark_paid(self, order_no: str, transaction_id: str, payload_json: str) -> bool:
"""pending → paid(幂等排他):仅当仍是 pending 才置 paid;False=已被处理(回调重放/并发)。"""
row = await self._row(order_no)
if row is None or row.status != "pending":
return False
row.status = "paid"
row.transaction_id = transaction_id or row.transaction_id
row.paid_at = utcnow_iso()
row.notify_payload = payload_json
row.updated_at = utcnow_iso()
await self.session.commit()
return True
async def mark_credited(self, order_no: str) -> None:
"""paid → credited:引擎到账完成。"""
row = await self._row(order_no)
if row is None:
return
row.status = "credited"
row.credited_at = utcnow_iso()
row.updated_at = utcnow_iso()
await self.session.commit()
async def set_prepay(self, order_no: str, appid: str, prepay_id: str) -> None:
"""记录 JSAPI 预支付信息(pending 态,小程序扫码拉起支付时回填)。"""
row = await self._row(order_no)
if row is None:
return
row.appid = appid or row.appid
row.prepay_id = prepay_id or row.prepay_id
row.updated_at = utcnow_iso()
await self.session.commit()
async def mark_status(self, order_no: str, status: str, payload_json: str = "") -> None:
"""置 closed / failed 等非终态迁移。"""
row = await self._row(order_no)
if row is None:
return
row.status = status
if payload_json:
row.notify_payload = payload_json
row.updated_at = utcnow_iso()
await self.session.commit()