138 lines
4.4 KiB
Python
138 lines
4.4 KiB
Python
"""
|
||
支付业务服务
|
||
|
||
职责:Alipay SDK 封装、创建订单、处理支付通知、查询状态
|
||
遵循 BACKEND_DEV.md "薄路由 + 厚服务" 原则
|
||
"""
|
||
from datetime import datetime, timezone, timedelta
|
||
import uuid
|
||
|
||
from alipay import AliPay
|
||
from loguru import logger
|
||
|
||
from app.core.config import settings
|
||
from app.core.security import decode_payment_token
|
||
from app.repositories.orders import create_order, get_order_by_trade_no, update_order_status
|
||
from app.repositories.users import update_user
|
||
|
||
# 支付宝网关地址
|
||
ALIPAY_GATEWAY = "https://openapi.alipay.com/gateway.do"
|
||
ALIPAY_GATEWAY_SANDBOX = "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
|
||
|
||
|
||
def _get_alipay_client() -> AliPay:
|
||
"""延迟初始化 Alipay 客户端"""
|
||
return AliPay(
|
||
appid=settings.ALIPAY_APP_ID,
|
||
app_notify_url=settings.ALIPAY_NOTIFY_URL,
|
||
app_private_key_string=open(settings.ALIPAY_PRIVATE_KEY_PATH).read(),
|
||
alipay_public_key_string=open(settings.ALIPAY_PUBLIC_KEY_PATH).read(),
|
||
sign_type="RSA2",
|
||
debug=settings.ALIPAY_SANDBOX,
|
||
)
|
||
|
||
|
||
def _create_page_pay_url(out_trade_no: str, amount: float, subject: str) -> str | None:
|
||
"""调用 alipay.trade.page.pay,返回支付宝收银台 URL"""
|
||
client = _get_alipay_client()
|
||
order_string = client.api_alipay_trade_page_pay(
|
||
subject=subject,
|
||
out_trade_no=out_trade_no,
|
||
total_amount=amount,
|
||
return_url=settings.ALIPAY_RETURN_URL,
|
||
)
|
||
if not order_string:
|
||
logger.error(f"电脑网站支付下单失败: {out_trade_no}")
|
||
return None
|
||
|
||
gateway = ALIPAY_GATEWAY_SANDBOX if settings.ALIPAY_SANDBOX else ALIPAY_GATEWAY
|
||
pay_url = f"{gateway}?{order_string}"
|
||
logger.info(f"电脑网站支付下单成功: {out_trade_no}")
|
||
return pay_url
|
||
|
||
|
||
def _verify_signature(data: dict, signature: str) -> bool:
|
||
"""验证支付宝异步通知签名"""
|
||
client = _get_alipay_client()
|
||
return client.verify(data, signature)
|
||
|
||
|
||
def create_payment_order(payment_token: str) -> dict:
|
||
"""
|
||
创建支付订单完整流程
|
||
|
||
Returns: {"pay_url": str, "out_trade_no": str, "amount": float}
|
||
Raises: ValueError (token 无效), RuntimeError (API 失败)
|
||
"""
|
||
user_id = decode_payment_token(payment_token)
|
||
if not user_id:
|
||
raise ValueError("付费凭证无效或已过期,请重新登录")
|
||
|
||
out_trade_no = f"VG_{int(datetime.now().timestamp())}_{uuid.uuid4().hex[:8]}"
|
||
amount = settings.PAYMENT_AMOUNT
|
||
|
||
create_order(user_id, out_trade_no, amount)
|
||
|
||
pay_url = _create_page_pay_url(out_trade_no, amount, "IPAgent 会员开通")
|
||
if not pay_url:
|
||
raise RuntimeError("创建支付订单失败,请稍后重试")
|
||
|
||
logger.info(f"用户 {user_id} 创建支付订单: {out_trade_no}")
|
||
|
||
return {"pay_url": pay_url, "out_trade_no": out_trade_no, "amount": amount}
|
||
|
||
|
||
def handle_payment_notify(form_data: dict) -> bool:
|
||
"""
|
||
处理支付宝异步通知完整流程
|
||
|
||
Returns: True=验签通过, False=验签失败
|
||
"""
|
||
data = dict(form_data)
|
||
|
||
signature = data.pop("sign", "")
|
||
data.pop("sign_type", None)
|
||
|
||
if not _verify_signature(data, signature):
|
||
logger.warning(f"支付宝通知验签失败: {data.get('out_trade_no')}")
|
||
return False
|
||
|
||
out_trade_no = data.get("out_trade_no", "")
|
||
trade_status = data.get("trade_status", "")
|
||
trade_no = data.get("trade_no", "")
|
||
|
||
logger.info(f"收到支付宝通知: {out_trade_no}, status={trade_status}, trade_no={trade_no}")
|
||
|
||
if trade_status not in ("TRADE_SUCCESS", "TRADE_FINISHED"):
|
||
return True
|
||
|
||
order = get_order_by_trade_no(out_trade_no)
|
||
if not order:
|
||
logger.warning(f"订单不存在: {out_trade_no}")
|
||
return True
|
||
|
||
if order["status"] == "paid":
|
||
logger.info(f"订单已处理过: {out_trade_no}")
|
||
return True
|
||
|
||
update_order_status(out_trade_no, "paid", trade_no)
|
||
|
||
user_id = order["user_id"]
|
||
expires_at = (datetime.now(timezone.utc) + timedelta(days=settings.PAYMENT_EXPIRE_DAYS)).isoformat()
|
||
update_user(user_id, {
|
||
"is_active": True,
|
||
"role": "user",
|
||
"expires_at": expires_at,
|
||
})
|
||
|
||
logger.success(f"用户 {user_id} 支付成功,已激活,有效期至 {expires_at}")
|
||
return True
|
||
|
||
|
||
def get_order_status(out_trade_no: str) -> str | None:
|
||
"""查询订单支付状态"""
|
||
order = get_order_by_trade_no(out_trade_no)
|
||
if not order:
|
||
return None
|
||
return order["status"]
|