Files
server-core/app/pay/repository.py
T

349 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""支付子应用仓储:充值订单行级操作(不掺 HTTP 逻辑)。"""
from __future__ import annotations
from sqlalchemy import select
from ..infrastructure.repositories import new_id, utcnow_iso
from .models import ComputeRechargeOrder, EventOrder, PaymentBinding, SettlementLog
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()
# ---------------------------------------------------------------------------
# 活动报名订单(课程/沙龙定价支付)
# ---------------------------------------------------------------------------
class EventOrderRepository:
def __init__(self, session):
self.session = session
async def create(self, fields: dict) -> dict:
now = utcnow_iso()
row = EventOrder(
id=new_id("ev"), created_at=now, updated_at=now,
**{k: v for k, v in fields.items() if hasattr(EventOrder, k) and k != "id"},
)
self.session.add(row)
await self.session.commit()
return _to_event_dict(row)
async def get_by_order_no(self, order_no: str) -> dict | None:
row = await self.session.scalar(
select(EventOrder).where(EventOrder.order_no == order_no)
)
return _to_event_dict(row) if row else None
async def find_pending_by_booking(self, booking_id: str, now_iso: str) -> dict | None:
"""同报名未过期 pending 单(继续支付复用,避免重复下单)。"""
row = await self.session.scalar(
select(EventOrder)
.where(
EventOrder.booking_id == booking_id,
EventOrder.status == "pending",
EventOrder.expires_at > now_iso,
)
.order_by(EventOrder.created_at.desc())
.limit(1)
)
return _to_event_dict(row) if row else None
async def _row(self, order_no: str) -> EventOrder | None:
return await self.session.scalar(
select(EventOrder).where(EventOrder.order_no == order_no)
)
async def mark_paid(self, order_no: str, transaction_id: str, payload_json: str) -> bool:
"""pending → paid(幂等排他):仅当仍是 pending 才置 paidFalse=已被处理。"""
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:
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:
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()
def _to_event_dict(o: EventOrder) -> dict:
return {
"id": o.id, "order_no": o.order_no, "booking_id": o.booking_id,
"event_id": o.event_id, "event_title": o.event_title,
"user_id": o.user_id, "username": o.username, "openid": o.openid,
"amount_fen": o.amount_fen, "client_type": o.client_type, "appid": o.appid,
"status": o.status, "prepay_id": o.prepay_id, "transaction_id": o.transaction_id,
"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,
}
# ---------------------------------------------------------------------------
# OPC 收款绑定(微信服务商分账接收方)
# ---------------------------------------------------------------------------
def _to_binding_dict(b: PaymentBinding) -> dict:
return {
"id": b.id, "user_id": b.user_id, "bind_type": b.bind_type,
"openid": b.openid, "real_name": b.real_name, "sub_mchid": b.sub_mchid,
"applyment_id": b.applyment_id, "status": b.status, "detail": b.detail or "",
"applyment_state": b.applyment_state or "", "sign_url": b.sign_url or "",
"account_validation_json": b.account_validation_json or "{}",
"audit_detail_json": b.audit_detail_json or "{}",
"split_allowed": b.split_allowed or "", "split_max_ratio": b.split_max_ratio or 0,
"created_at": b.created_at, "updated_at": b.updated_at,
}
class PaymentBindingRepository:
"""OPC 收款绑定仓储(个人 openid / 商户子商户号双路径)。"""
def __init__(self, session):
self.session = session
async def create(self, fields: dict) -> dict:
now = utcnow_iso()
row = PaymentBinding(
id=new_id("pb"), created_at=now, updated_at=now,
**{k: v for k, v in fields.items() if hasattr(PaymentBinding, k) and k != "id"},
)
self.session.add(row)
await self.session.commit()
return _to_binding_dict(row)
async def get(self, binding_id: str) -> dict | None:
row = await self.session.get(PaymentBinding, binding_id)
return _to_binding_dict(row) if row else None
async def list_by_user(self, user_id: str) -> list[dict]:
rows = (await self.session.scalars(
select(PaymentBinding).where(PaymentBinding.user_id == user_id)
.order_by(PaymentBinding.created_at.desc())
)).all()
return [_to_binding_dict(r) for r in rows]
async def list_all(self, bind_type: str | None = None,
status: str | None = None,
limit: int = 200) -> list[dict]:
"""管理端:列出全部 OPC 收款绑定(可筛 bind_type/status),按更新时间倒序。"""
stmt = select(PaymentBinding)
if bind_type:
stmt = stmt.where(PaymentBinding.bind_type == bind_type)
if status:
stmt = stmt.where(PaymentBinding.status == status)
stmt = stmt.order_by(PaymentBinding.updated_at.desc()).limit(limit)
rows = (await self.session.scalars(stmt)).all()
return [_to_binding_dict(r) for r in rows]
async def get_active(self, user_id: str, bind_type: str | None = None) -> dict | None:
"""取该用户可接收分账的绑定(优先商户绑定→个人绑定;bind_type 指定时仅取该类型)。"""
order = ("merchant", "personal") if bind_type is None else (bind_type,)
for bt in order:
row = await self.session.scalar(
select(PaymentBinding)
.where(
PaymentBinding.user_id == user_id,
PaymentBinding.status == "active",
PaymentBinding.bind_type == bt,
)
.order_by(PaymentBinding.created_at.desc())
.limit(1)
)
if row is not None:
return _to_binding_dict(row)
return None
async def update(self, binding_id: str, fields: dict) -> dict | None:
row = await self.session.get(PaymentBinding, binding_id)
if row is None:
return None
for k, v in fields.items():
if hasattr(PaymentBinding, k):
setattr(row, k, v)
row.updated_at = utcnow_iso()
await self.session.commit()
return _to_binding_dict(row)
async def set_status(self, binding_id: str, status: str, **extra) -> dict | None:
return await self.update(binding_id, {"status": status, **extra})
# ---------------------------------------------------------------------------
# 分账流水镜像(对账)
# ---------------------------------------------------------------------------
def _to_log_dict(l: SettlementLog) -> dict:
return {
"id": l.id, "escrow_id": l.escrow_id, "task_id": l.task_id,
"share_order_no": l.share_order_no, "channel": l.channel,
"amount": l.amount, "commission": l.commission,
"receivers_json": l.receivers_json, "status": l.status, "detail": l.detail,
"created_at": l.created_at, "updated_at": l.updated_at,
}
class SettlementLogRepository:
"""分账流水镜像仓储(业务侧记录真金去向,供对账/审计)。"""
def __init__(self, session):
self.session = session
async def create(self, fields: dict) -> dict:
now = utcnow_iso()
row = SettlementLog(
id=new_id("sl"), created_at=now, updated_at=now,
**{k: v for k, v in fields.items() if hasattr(SettlementLog, k) and k != "id"},
)
self.session.add(row)
await self.session.commit()
return _to_log_dict(row)
async def get_by_share_order_no(self, share_order_no: str) -> dict | None:
row = await self.session.scalar(
select(SettlementLog).where(SettlementLog.share_order_no == share_order_no)
)
return _to_log_dict(row) if row else None
async def set_status(self, log_id: str, status: str, detail: str = "") -> dict | None:
row = await self.session.get(SettlementLog, log_id)
if row is None:
return None
row.status = status
if detail:
row.detail = detail
row.updated_at = utcnow_iso()
await self.session.commit()
return _to_log_dict(row)