refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构

- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
fawney19
2026-04-03 16:26:16 +08:00
parent 8f26e1a31f
commit 1d9c77522a
868 changed files with 1735 additions and 2433 deletions

View File

@@ -0,0 +1,3 @@
from src.services.payment.service import PaymentService
__all__ = ["PaymentService"]

View File

@@ -0,0 +1,23 @@
from __future__ import annotations
from src.services.payment.gateway.alipay import AlipayGateway
from src.services.payment.gateway.base import PaymentGateway
from src.services.payment.gateway.manual import ManualGateway
from src.services.payment.gateway.wechat import WeChatGateway
_GATEWAYS: dict[str, PaymentGateway] = {
"alipay": AlipayGateway(),
"wechat": WeChatGateway(),
"manual": ManualGateway(),
}
def get_payment_gateway(payment_method: str) -> PaymentGateway:
key = (payment_method or "").strip().lower()
gateway = _GATEWAYS.get(key)
if gateway is None:
raise ValueError(f"unsupported payment_method: {payment_method}")
return gateway
__all__ = ["PaymentGateway", "get_payment_gateway"]

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
from typing import Any
from src.services.payment.gateway.base import PaymentGateway
class AlipayGateway(PaymentGateway):
payment_method = "alipay"
display_name = "支付宝"
def create_checkout_payload(self, *, order: Any) -> dict[str, Any]:
gateway_order_id = getattr(order, "gateway_order_id", None) or f"ali_{order.order_no}"
return {
"gateway": self.payment_method,
"display_name": self.display_name,
"gateway_order_id": gateway_order_id,
"payment_url": f"/pay/mock/alipay/{order.order_no}",
"qr_code": f"mock://alipay/{order.order_no}",
"expires_at": getattr(order, "expires_at", None),
}

View File

@@ -0,0 +1,69 @@
from __future__ import annotations
import hashlib
import hmac
import json
from abc import ABC, abstractmethod
from typing import Any
class PaymentGateway(ABC):
"""支付网关抽象。
当前阶段只提供统一结构和占位返回,便于后续接入真实 SDK。
"""
payment_method: str
display_name: str
@abstractmethod
def create_checkout_payload(self, *, order: Any) -> dict[str, Any]:
"""为前端返回统一的支付指引结构。"""
@staticmethod
def build_callback_signature(
*,
payload: dict[str, Any] | None,
callback_secret: str | None,
) -> str | None:
if payload is None:
return None
if not callback_secret:
return None
canonical = json.dumps(
payload,
sort_keys=True,
ensure_ascii=False,
separators=(",", ":"),
default=str,
)
return hmac.new(
callback_secret.encode("utf-8"),
canonical.encode("utf-8"),
hashlib.sha256,
).hexdigest()
def verify_callback_payload(
self,
*,
payload: dict[str, Any] | None,
callback_signature: str | None = None,
callback_secret: str | None = None,
) -> bool:
"""校验回调。
默认使用 HMAC-SHA256 对 payload 进行签名校验。
真实接入时可由各支付渠道覆写该方法使用官方 SDK 验签。
"""
expected_signature = self.build_callback_signature(
payload=payload,
callback_secret=callback_secret,
)
if expected_signature is None:
return False
provided = (callback_signature or "").strip()
if not provided:
return False
if provided.lower().startswith("sha256="):
provided = provided.split("=", 1)[1]
return hmac.compare_digest(provided.lower(), expected_signature.lower())

View File

@@ -0,0 +1,22 @@
from __future__ import annotations
from typing import Any
from src.services.payment.gateway.base import PaymentGateway
class ManualGateway(PaymentGateway):
payment_method = "manual"
display_name = "人工打款"
def create_checkout_payload(self, *, order: Any) -> dict[str, Any]:
gateway_order_id = getattr(order, "gateway_order_id", None) or f"manual_{order.order_no}"
return {
"gateway": self.payment_method,
"display_name": self.display_name,
"gateway_order_id": gateway_order_id,
"payment_url": None,
"qr_code": None,
"instructions": "请线下确认到账后由管理员处理",
"expires_at": getattr(order, "expires_at", None),
}

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
from typing import Any
from src.services.payment.gateway.base import PaymentGateway
class WeChatGateway(PaymentGateway):
payment_method = "wechat"
display_name = "微信支付"
def create_checkout_payload(self, *, order: Any) -> dict[str, Any]:
gateway_order_id = getattr(order, "gateway_order_id", None) or f"wx_{order.order_no}"
return {
"gateway": self.payment_method,
"display_name": self.display_name,
"gateway_order_id": gateway_order_id,
"payment_url": f"/pay/mock/wechat/{order.order_no}",
"qr_code": f"mock://wechat/{order.order_no}",
"expires_at": getattr(order, "expires_at", None),
}

View File

@@ -0,0 +1,478 @@
from __future__ import annotations
import hashlib
import json
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Any
from uuid import uuid4
from sqlalchemy.orm import Session
from src.models.database import PaymentCallback, PaymentOrder, User, Wallet
from src.services.billing.precision import to_money_decimal
from src.services.payment.gateway import get_payment_gateway
from src.services.wallet import WalletService
class PaymentService:
"""支付订单与回调处理服务。
当前实现目标:
- 打通充值订单创建
- 打通支付回调幂等到账
- 真实网关签名/SDK 留给后续渠道适配层
"""
@staticmethod
def _build_order_no() -> str:
ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S%f")
return f"po_{ts}_{uuid4().hex[:12]}"
@staticmethod
def _build_payload_hash(payload: dict[str, Any] | None) -> str | None:
if payload is None:
return None
encoded = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str).encode(
"utf-8"
)
return hashlib.sha256(encoded).hexdigest()
@classmethod
def create_recharge_order(
cls,
db: Session,
*,
user: User,
amount_usd: Decimal | float | int | str,
payment_method: str,
pay_amount: Decimal | float | int | str | None = None,
pay_currency: str | None = None,
exchange_rate: Decimal | float | int | str | None = None,
expires_in_minutes: int = 30,
gateway_order_id: str | None = None,
gateway_response: dict[str, Any] | None = None,
) -> PaymentOrder:
amount = to_money_decimal(amount_usd)
if amount <= Decimal("0"):
raise ValueError("recharge amount must be positive")
if not payment_method:
raise ValueError("payment_method is required")
if payment_method == "admin_manual":
raise ValueError("admin_manual is reserved for admin recharge")
gateway = get_payment_gateway(payment_method)
wallet = WalletService.get_or_create_wallet(db, user=user)
if wallet is None:
raise ValueError("wallet not available")
if wallet.status != "active":
raise ValueError("wallet is not active")
now = datetime.now(timezone.utc)
order = PaymentOrder(
order_no=cls._build_order_no(),
wallet_id=wallet.id,
user_id=user.id,
amount_usd=amount,
pay_amount=to_money_decimal(pay_amount) if pay_amount is not None else None,
pay_currency=pay_currency,
exchange_rate=to_money_decimal(exchange_rate) if exchange_rate is not None else None,
refunded_amount_usd=Decimal("0"),
refundable_amount_usd=Decimal("0"),
payment_method=payment_method,
gateway_order_id=gateway_order_id,
gateway_response=gateway_response,
status="pending",
expires_at=now + timedelta(minutes=max(expires_in_minutes, 1)),
)
db.add(order)
db.flush()
checkout = gateway.create_checkout_payload(order=order)
order.gateway_order_id = order.gateway_order_id or checkout.get("gateway_order_id")
order.gateway_response = gateway_response if gateway_response is not None else checkout
return order
@classmethod
def refresh_order_status(cls, order: PaymentOrder | None) -> bool:
if order is None:
return False
if order.status != "pending":
return False
now = datetime.now(timezone.utc)
if order.expires_at is not None and order.expires_at < now:
order.status = "expired"
return True
return False
@staticmethod
def get_order(
db: Session,
*,
order_id: str | None = None,
order_no: str | None = None,
gateway_order_id: str | None = None,
) -> PaymentOrder | None:
if order_id:
return db.query(PaymentOrder).filter(PaymentOrder.id == order_id).first()
if order_no:
return db.query(PaymentOrder).filter(PaymentOrder.order_no == order_no).first()
if gateway_order_id:
return (
db.query(PaymentOrder)
.filter(PaymentOrder.gateway_order_id == gateway_order_id)
.first()
)
return None
@classmethod
def list_user_orders(
cls,
db: Session,
*,
user_id: str,
limit: int,
offset: int,
) -> tuple[list[PaymentOrder], int, bool]:
expired_count = cls.expire_overdue_pending_orders(db, user_id=user_id)
q = db.query(PaymentOrder).filter(PaymentOrder.user_id == user_id)
total = q.count()
items = q.order_by(PaymentOrder.created_at.desc()).offset(offset).limit(limit).all()
return items, total, expired_count > 0
@classmethod
def list_orders(
cls,
db: Session,
*,
status: str | None = None,
payment_method: str | None = None,
limit: int = 50,
offset: int = 0,
) -> tuple[list[PaymentOrder], int, bool]:
expired_count = 0
if status in {None, "pending", "expired"}:
expired_count = cls.expire_overdue_pending_orders(
db,
payment_method=payment_method,
)
q = db.query(PaymentOrder)
if status:
q = q.filter(PaymentOrder.status == status)
if payment_method:
q = q.filter(PaymentOrder.payment_method == payment_method)
total = q.count()
items = q.order_by(PaymentOrder.created_at.desc()).offset(offset).limit(limit).all()
return items, total, expired_count > 0
@staticmethod
def expire_overdue_pending_orders(
db: Session,
*,
user_id: str | None = None,
payment_method: str | None = None,
) -> int:
now = datetime.now(timezone.utc)
q = db.query(PaymentOrder).filter(
PaymentOrder.status == "pending",
PaymentOrder.expires_at.isnot(None),
PaymentOrder.expires_at < now,
)
if user_id:
q = q.filter(PaymentOrder.user_id == user_id)
if payment_method:
q = q.filter(PaymentOrder.payment_method == payment_method)
return int(q.update({PaymentOrder.status: "expired"}, synchronize_session=False) or 0)
@staticmethod
def list_callbacks(
db: Session,
*,
payment_method: str | None = None,
limit: int = 50,
offset: int = 0,
) -> tuple[list[PaymentCallback], int]:
q = db.query(PaymentCallback)
if payment_method:
q = q.filter(PaymentCallback.payment_method == payment_method)
total = q.count()
items = q.order_by(PaymentCallback.created_at.desc()).offset(offset).limit(limit).all()
return items, total
@staticmethod
def get_user_order(
db: Session,
*,
user_id: str,
order_id: str,
) -> PaymentOrder | None:
return (
db.query(PaymentOrder)
.filter(PaymentOrder.id == order_id, PaymentOrder.user_id == user_id)
.first()
)
@classmethod
def fail_order(
cls,
db: Session,
*,
order: PaymentOrder,
reason: str | None = None,
) -> PaymentOrder:
locked_order = (
db.query(PaymentOrder)
.filter(PaymentOrder.id == order.id)
.with_for_update()
.one_or_none()
)
if locked_order is None:
raise ValueError("payment order not found")
if locked_order.status == "credited":
raise ValueError("credited order cannot be failed")
locked_order.status = "failed"
payload = dict(locked_order.gateway_response or {})
if reason:
payload["failure_reason"] = reason
payload["failed_at"] = datetime.now(timezone.utc).isoformat()
locked_order.gateway_response = payload
return locked_order
@classmethod
def expire_order(
cls,
db: Session,
*,
order: PaymentOrder,
reason: str | None = None,
) -> tuple[PaymentOrder, bool]:
locked_order = (
db.query(PaymentOrder)
.filter(PaymentOrder.id == order.id)
.with_for_update()
.one_or_none()
)
if locked_order is None:
raise ValueError("payment order not found")
if locked_order.status == "credited":
raise ValueError("credited order cannot be expired")
if locked_order.status == "expired":
return locked_order, False
if locked_order.status != "pending":
raise ValueError(f"only pending order can be expired: {locked_order.status}")
locked_order.status = "expired"
payload = dict(locked_order.gateway_response or {})
if reason:
payload["expire_reason"] = reason
payload["expired_at"] = datetime.now(timezone.utc).isoformat()
locked_order.gateway_response = payload
return locked_order, True
@classmethod
def log_callback(
cls,
db: Session,
*,
payment_method: str,
callback_key: str,
order_no: str | None = None,
gateway_order_id: str | None = None,
payload: dict[str, Any] | None = None,
signature_valid: bool = False,
status: str = "received",
payment_order: PaymentOrder | None = None,
error_message: str | None = None,
) -> tuple[PaymentCallback, bool]:
existing = (
db.query(PaymentCallback).filter(PaymentCallback.callback_key == callback_key).first()
)
if existing is not None:
return existing, False
callback = PaymentCallback(
payment_order_id=payment_order.id if payment_order else None,
payment_method=payment_method,
callback_key=callback_key,
order_no=order_no,
gateway_order_id=gateway_order_id,
payload_hash=cls._build_payload_hash(payload),
signature_valid=signature_valid,
status=status,
payload=payload,
error_message=error_message,
)
db.add(callback)
db.flush()
return callback, True
@classmethod
def credit_order(
cls,
db: Session,
*,
order: PaymentOrder,
gateway_order_id: str | None = None,
gateway_response: dict[str, Any] | None = None,
pay_amount: Decimal | float | int | str | None = None,
pay_currency: str | None = None,
exchange_rate: Decimal | float | int | str | None = None,
) -> tuple[PaymentOrder, bool]:
locked_order = (
db.query(PaymentOrder)
.filter(PaymentOrder.id == order.id)
.with_for_update()
.one_or_none()
)
if locked_order is None:
raise ValueError("payment order not found")
if locked_order.status == "credited":
return locked_order, False
if locked_order.status in {"failed", "expired", "refunded"}:
raise ValueError(f"payment order is not creditable: {locked_order.status}")
now = datetime.now(timezone.utc)
if locked_order.expires_at is not None and locked_order.expires_at < now:
locked_order.status = "expired"
raise ValueError("payment order expired")
wallet = db.query(Wallet).filter(Wallet.id == locked_order.wallet_id).first()
if wallet is None:
raise ValueError("wallet not found")
if wallet.status != "active":
raise ValueError("wallet is not active")
if gateway_order_id:
locked_order.gateway_order_id = gateway_order_id
if gateway_response is not None:
locked_order.gateway_response = gateway_response
if pay_amount is not None:
locked_order.pay_amount = to_money_decimal(pay_amount)
if pay_currency is not None:
locked_order.pay_currency = pay_currency
if exchange_rate is not None:
locked_order.exchange_rate = to_money_decimal(exchange_rate)
locked_order.status = "paid"
locked_order.paid_at = locked_order.paid_at or now
locked_order.refundable_amount_usd = to_money_decimal(locked_order.amount_usd)
WalletService.create_wallet_transaction(
db,
wallet=wallet,
category="recharge",
reason_code="topup_gateway",
amount=locked_order.amount_usd,
balance_type="recharge",
link_type="payment_order",
link_id=locked_order.id,
description=f"充值到账({locked_order.payment_method})",
)
locked_order.status = "credited"
locked_order.credited_at = now
return locked_order, True
@classmethod
def handle_callback(
cls,
db: Session,
*,
payment_method: str,
callback_key: str,
payload: dict[str, Any] | None,
callback_signature: str | None,
callback_secret: str | None,
order_no: str | None = None,
gateway_order_id: str | None = None,
amount_usd: Decimal | float | int | str | None = None,
pay_amount: Decimal | float | int | str | None = None,
pay_currency: str | None = None,
exchange_rate: Decimal | float | int | str | None = None,
) -> dict[str, Any]:
gateway = get_payment_gateway(payment_method)
verified = gateway.verify_callback_payload(
payload=payload,
callback_signature=callback_signature,
callback_secret=callback_secret,
)
callback, created = cls.log_callback(
db,
payment_method=payment_method,
callback_key=callback_key,
order_no=order_no,
gateway_order_id=gateway_order_id,
payload=payload,
signature_valid=verified,
)
if not created and callback.status == "processed":
return {
"ok": True,
"duplicate": True,
"credited": False,
"order_id": callback.payment_order_id,
}
if not verified:
callback.status = "failed"
callback.error_message = "invalid callback signature"
callback.processed_at = datetime.now(timezone.utc)
return {"ok": False, "duplicate": not created, "error": callback.error_message}
order = cls.get_order(
db,
order_no=order_no or callback.order_no,
gateway_order_id=gateway_order_id or callback.gateway_order_id,
)
if order is None:
callback.status = "failed"
callback.error_message = "payment order not found"
callback.processed_at = datetime.now(timezone.utc)
return {"ok": False, "duplicate": not created, "error": callback.error_message}
callback.payment_order_id = order.id
callback.order_no = order.order_no
callback.gateway_order_id = gateway_order_id or order.gateway_order_id
if amount_usd is None:
callback.status = "failed"
callback.error_message = "callback amount is required"
callback.processed_at = datetime.now(timezone.utc)
return {"ok": False, "duplicate": not created, "error": callback.error_message}
expected = to_money_decimal(order.amount_usd)
actual = to_money_decimal(amount_usd)
if actual != expected:
callback.status = "failed"
callback.error_message = "callback amount mismatch"
callback.processed_at = datetime.now(timezone.utc)
return {"ok": False, "duplicate": not created, "error": callback.error_message}
try:
updated_order, credited = cls.credit_order(
db,
order=order,
gateway_order_id=gateway_order_id,
gateway_response=payload,
pay_amount=pay_amount,
pay_currency=pay_currency,
exchange_rate=exchange_rate,
)
except ValueError as exc:
callback.status = "failed"
callback.error_message = str(exc)
callback.processed_at = datetime.now(timezone.utc)
return {"ok": False, "duplicate": not created, "error": callback.error_message}
callback.status = "processed"
callback.error_message = None
callback.processed_at = datetime.now(timezone.utc)
return {
"ok": True,
"duplicate": not created,
"credited": credited,
"order_id": updated_order.id,
"order_no": updated_order.order_no,
"status": updated_order.status,
"wallet_id": updated_order.wallet_id,
}