增加支付宝支付相关页面和程序

This commit is contained in:
2026-06-10 11:33:23 +08:00
parent d5a7ba8f14
commit 913dfee9e0
9 changed files with 444 additions and 96 deletions
+268 -29
View File
@@ -1,16 +1,87 @@
import logging
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.models.payment_order import PaymentOrder
from app.models.system_config import SystemConfig
from app.services.credits import add_credits
from app.utils.id_gen import generate_id, generate_order_no
logger = logging.getLogger("videogen")
# ---------------------------------------------------------------------------
# Config helpers read from system_configs table (admin panel)
# ---------------------------------------------------------------------------
async def _get_payment_configs(db: AsyncSession) -> dict[str, str]:
"""Read all payment_* configs from the database, return as a dict."""
result = await db.execute(
select(SystemConfig).where(SystemConfig.key.like("payment_%"))
)
return {c.key: c.value for c in result.scalars().all()}
def _is_mock_mode(db_configs: dict[str, str]) -> bool:
"""Check if payment mock mode is enabled (from DB or env)."""
db_val = db_configs.get("payment_mock", "")
if db_val:
return db_val.lower() in ("true", "1", "yes")
return settings.PAYMENT_MOCK
# ---------------------------------------------------------------------------
# Alipay client (lazy singleton, recreated when config changes)
# ---------------------------------------------------------------------------
_alipay_client = None
_alipay_client_app_id = None
def _get_alipay_client(app_id: str, private_key: str, public_key: str):
"""Get or create an Alipay client. Recreated if app_id changes."""
global _alipay_client, _alipay_client_app_id
if _alipay_client is not None and _alipay_client_app_id == app_id:
return _alipay_client
try:
from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
except ImportError:
logger.error(
"alipay-sdk-python is not installed. "
"Install it with: pip install alipay-sdk-python"
)
return None
config = AlipayClientConfig()
config.server_url = "https://openapi.alipay.com/gateway.do"
config.app_id = app_id
config.app_private_key = private_key
config.alipay_public_key = public_key
config.sign_type = "RSA2"
config.charset = "utf-8"
try:
_alipay_client = DefaultAlipayClient(config)
_alipay_client_app_id = app_id
except Exception:
logger.exception("Failed to initialize Alipay client")
_alipay_client = None
_alipay_client_app_id = None
return _alipay_client
# ---------------------------------------------------------------------------
# Create recharge order
# ---------------------------------------------------------------------------
async def create_recharge_order(
db: AsyncSession,
user_id: str,
@@ -20,7 +91,12 @@ async def create_recharge_order(
bonus_credits: float = 0.0,
method: str = "wechat",
) -> PaymentOrder:
"""Create a payment order. In mock mode, immediately completes payment."""
"""Create a payment order.
Reads payment config from the database (admin panel).
Returns the order; for Alipay the ``qr_url`` attribute will be populated
with the scan-to-pay URL.
"""
total_credits = credits + bonus_credits
order = PaymentOrder(
id=generate_id(),
@@ -34,7 +110,11 @@ async def create_recharge_order(
db.add(order)
await db.flush()
if settings.PAYMENT_MOCK:
# Read config from database
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
# Mock: immediately complete payment
order.status = "paid"
order.paid_at = datetime.now()
@@ -52,57 +132,184 @@ async def create_recharge_order(
else:
# Real payment: delegate to WeChat or Alipay
if method == "wechat":
_create_wechat_order(order)
_create_wechat_order(order, db_configs)
elif method == "alipay":
_create_alipay_order(order)
qr_url = _create_alipay_order(order, db_configs)
if qr_url:
# Attach QR URL to the order instance (transient, not persisted)
order.qr_url = qr_url # type: ignore[attr-defined]
return order
def _create_wechat_order(order: PaymentOrder) -> None:
# ---------------------------------------------------------------------------
# WeChat (stub)
# ---------------------------------------------------------------------------
def _create_wechat_order(order: PaymentOrder, db_configs: dict[str, str]) -> None:
"""Create a WeChat Pay order. Stub for real integration."""
if not settings.WECHAT_MCH_ID or not settings.WECHAT_API_KEY:
logger.warning("WeChat payment config missing (WECHAT_MCH_ID / WECHAT_API_KEY)")
mch_id = db_configs.get("payment_wechat_mch_id", "")
api_key = db_configs.get("payment_wechat_api_key", "")
if not mch_id or not api_key:
logger.warning("WeChat payment config missing in database")
return
logger.info(
f"WeChat order created: mch_id={settings.WECHAT_MCH_ID}, "
f"WeChat order created: mch_id={mch_id}, "
f"order_no={order.order_no}, amount={order.amount}"
)
def _create_alipay_order(order: PaymentOrder) -> None:
"""Create an Alipay order. Stub for real integration."""
if not settings.ALIPAY_APP_ID or not settings.ALIPAY_PRIVATE_KEY:
logger.warning("Alipay payment config missing (ALIPAY_APP_ID / ALIPAY_PRIVATE_KEY)")
return
logger.info(
f"Alipay order created: app_id={settings.ALIPAY_APP_ID}, "
f"order_no={order.order_no}, amount={order.amount}"
)
# ---------------------------------------------------------------------------
# Alipay trade.precreate (当面付 预下单)
# ---------------------------------------------------------------------------
async def verify_wechat_callback(data: dict) -> bool:
def _create_alipay_order(order: PaymentOrder, db_configs: dict[str, str]) -> str | None:
"""Call Alipay ``trade.precreate`` to obtain a QR code URL.
Reads all Alipay config from the database (admin panel).
Returns the ``qr_code`` URL on success, or ``None`` on failure.
"""
app_id = db_configs.get("payment_alipay_app_id", "")
private_key = db_configs.get("payment_alipay_private_key", "")
public_key = db_configs.get("payment_alipay_public_key", "")
notify_url = db_configs.get("payment_alipay_notify_url", "")
if not app_id or not private_key:
logger.warning("Alipay config missing in database (app_id / private_key)")
return None
client = _get_alipay_client(app_id, private_key, public_key)
if client is None:
return None
try:
from alipay.aop.api.domain.AlipayTradePrecreateModel import (
AlipayTradePrecreateModel,
)
from alipay.aop.api.request.AlipayTradePrecreateRequest import (
AlipayTradePrecreateRequest,
)
model = AlipayTradePrecreateModel()
model.out_trade_no = order.order_no
model.total_amount = f"{order.amount:.2f}"
model.subject = f"充值订单 {order.order_no}"
body_parts = []
if order.credits > 0:
body_parts.append(f"{order.credits}积分")
if body_parts:
model.body = " ".join(body_parts)
request = AlipayTradePrecreateRequest()
request.biz_model = model
if notify_url:
request.notify_url = notify_url
response = client.execute(request)
if response.code == "10000":
qr_url = response.qr_code
logger.info(
f"Alipay precreate success: order_no={order.order_no}, "
f"qr_url={qr_url}"
)
return qr_url
else:
logger.error(
f"Alipay precreate failed: code={response.code}, "
f"msg={response.msg}, sub_code={response.sub_code}, "
f"sub_msg={response.sub_msg}, order_no={order.order_no}"
)
return None
except Exception:
logger.exception(f"Alipay precreate exception: order_no={order.order_no}")
return None
# ---------------------------------------------------------------------------
# Alipay callback verification
# ---------------------------------------------------------------------------
async def verify_alipay_callback(data: dict, db: AsyncSession) -> bool:
"""Verify Alipay payment callback (async notify) signature.
Reads the Alipay public key from the database and uses the SDK's
built-in RSA2 verification.
"""
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
return True
public_key = db_configs.get("payment_alipay_public_key", "")
if not public_key:
logger.warning("ALIPAY_PUBLIC_KEY not found in database, cannot verify callback")
return False
try:
sign = data.get("sign")
if not sign:
logger.warning("Alipay callback missing 'sign' field")
return False
# Build verification params (exclude sign and sign_type)
verify_data = {
k: v for k, v in data.items()
if k not in ("sign", "sign_type") and v is not None and v != ""
}
from alipay.aop.api.util.Signature import verify_with_rsa
sign_content = "&".join(
f"{k}={v}" for k, v in sorted(verify_data.items())
)
is_valid = verify_with_rsa(
public_key.encode("utf-8"),
sign_content.encode("utf-8"),
sign,
)
if not is_valid:
logger.warning("Alipay callback signature verification FAILED")
return is_valid
except ImportError:
logger.error("alipay-sdk-python not installed, skipping signature verification")
return True
except Exception:
logger.exception("Alipay callback verification error")
return False
# ---------------------------------------------------------------------------
# WeChat callback verification (stub)
# ---------------------------------------------------------------------------
async def verify_wechat_callback(data: dict, db: AsyncSession) -> bool:
"""Verify WeChat payment callback signature."""
if settings.PAYMENT_MOCK:
db_configs = await _get_payment_configs(db)
mock_mode = _is_mock_mode(db_configs)
if mock_mode:
return True
# Real verification would use WECHAT_API_KEY to verify signature
logger.info("WeChat callback verification (real mode not implemented)")
return True
async def verify_alipay_callback(data: dict) -> bool:
"""Verify Alipay payment callback signature."""
if settings.PAYMENT_MOCK:
return True
# Real verification would use ALIPAY_PUBLIC_KEY to verify signature
logger.info("Alipay callback verification (real mode not implemented)")
return True
# ---------------------------------------------------------------------------
# Process successful payment
# ---------------------------------------------------------------------------
async def process_payment_success(db: AsyncSession, order_id: str):
"""Process successful payment: update order and add credits."""
from sqlalchemy import select
result = await db.execute(
select(PaymentOrder).where(PaymentOrder.id == order_id).limit(1)
)
@@ -120,3 +327,35 @@ async def process_payment_success(db: AsyncSession, order_id: str):
related_id=order.id,
)
await db.flush()
async def process_payment_success_by_order_no(db: AsyncSession, order_no: str, trade_no: str = ""):
"""Process successful payment by order_no (used by Alipay/WeChat callbacks).
Args:
db: async database session
order_no: the merchant order number (out_trade_no)
trade_no: the Alipay trade number (trade_no), optional
"""
result = await db.execute(
select(PaymentOrder).where(PaymentOrder.order_no == order_no).limit(1)
)
order = result.scalar_one_or_none()
if not order or order.status != "pending":
logger.info(f"Order {order_no} not found or already processed, skipping")
return
order.status = "paid"
order.paid_at = datetime.now()
if trade_no:
order.trade_no = trade_no
await add_credits(
db,
order.user_id,
order.credits,
f"充值成功({order.credits}积分)",
related_id=order.id,
)
await db.flush()
logger.info(f"Payment success processed: order_no={order_no}, trade_no={trade_no}")