""" 支付业务服务 职责: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"]