349 lines
14 KiB
Python
349 lines
14 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, 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 才置 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:
|
||
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)
|