f1147dced3
- 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>
116 lines
4.7 KiB
Python
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()
|