import logging from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession logger = logging.getLogger("payment") from app.dependencies import get_db, get_current_user from app.models.user import User from app.models.payment_order import PaymentOrder from app.models.recharge_package import RechargePackage from app.schemas.payment import RechargeRequest, PaymentOrderOut from app.services.payment import ( create_recharge_order, verify_wechat_callback, verify_alipay_callback, process_payment_success_by_order_no, process_refund, _get_payment_configs, _close_alipay_order, _get_order_expire_seconds, ) router = APIRouter(prefix="/payments", tags=["payments"]) @router.get("/methods") async def get_payment_methods( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Return which payment methods are enabled (from admin config).""" from app.services.payment import _get_payment_configs configs = await _get_payment_configs(db) return { "alipay": configs.get("payment_alipay_enabled", "").lower() == "true", "wechat": configs.get("payment_wechat_enabled", "").lower() == "true", } @router.post("/recharge", response_model=PaymentOrderOut) async def recharge( req: RechargeRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): if req.method not in ("wechat", "alipay"): raise HTTPException(status_code=400, detail="不支持的支付方式") # Check if the selected payment method is enabled in admin config from app.services.payment import _get_payment_configs, _is_mock_mode configs = await _get_payment_configs(db) if not _is_mock_mode(configs): enabled_key = f"payment_{req.method}_enabled" if configs.get(enabled_key, "").lower() != "true": raise HTTPException(status_code=400, detail="该支付方式未启用") result = await db.execute( select(RechargePackage).where( RechargePackage.id == req.plan, RechargePackage.is_active == True, ) .limit(1) ) pkg = result.scalar_one_or_none() if not pkg: raise HTTPException(status_code=400, detail="无效的套餐") try: order = await create_recharge_order( db, current_user.id, credits=pkg.credits, price=pkg.price, label=pkg.name, bonus_credits=pkg.bonus_credits, method=req.method, ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) return order @router.post("/wechat/callback") async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)): # 读取微信支付回调数据 body_bytes = await request.body() body_str = body_bytes.decode("utf-8") # 获取配置 from app.services.payment import _get_payment_configs, _is_mock_mode, _get_wechat_client db_configs = await _get_payment_configs(db) # 检查 mock 模式 if _is_mock_mode(db_configs): try: import json data = json.loads(body_str) if body_str else {} order_no = data.get("out_trade_no") if order_no: await process_payment_success_by_order_no(db, order_no) logger.info(f"Mock WeChat callback processed: order_no={order_no}") return {"code": "SUCCESS", "message": "OK"} except Exception as e: logger.exception(f"Mock WeChat callback error: {e}") return {"code": "SUCCESS", "message": "OK"} # 微信要求即使处理失败也返回成功 # 真实模式:使用 wechatpayv3 SDK 验证回调并解析数据 try: mch_id = db_configs.get("payment_wechat_mch_id", "") private_key = db_configs.get("payment_wechat_private_key", "") cert_serial_no = db_configs.get("payment_wechat_cert_serial_no", "") api_v3_key = db_configs.get("payment_wechat_api_v3_key", "") gateway = db_configs.get("payment_wechat_gateway", "") client = _get_wechat_client(mch_id, private_key, cert_serial_no, api_v3_key, gateway) if not client: logger.error("WeChat client not initialized for callback") return {"code": "SUCCESS", "message": "OK"} # 从请求头获取必要信息 headers = dict(request.headers) timestamp = headers.get("Wechatpay-Timestamp", "") nonce = headers.get("Wechatpay-Nonce", "") signature = headers.get("Wechatpay-Signature", "") serial_no = headers.get("Wechatpay-Serial", "") # 验证签名 is_verified = client.verify( timestamp=timestamp, nonce=nonce, body=body_str, signature=signature, serial_no=serial_no ) if not is_verified: logger.warning("WeChat callback signature verification failed") raise HTTPException(status_code=400, detail="签名验证失败") # 解密回调数据 decrypted_data = client.decrypt(body_str) if not decrypted_data: logger.error("WeChat callback decryption failed") raise HTTPException(status_code=400, detail="数据解密失败") # 处理支付成功回调 if decrypted_data.get("event_type") == "TRANSACTION.SUCCESS": resource = decrypted_data.get("resource", {}) order_no = resource.get("out_trade_no", "") transaction_id = resource.get("transaction_id", "") amount_info = resource.get("amount", {}) total_amount = amount_info.get("total", 0) / 100 # 转换为元 if order_no: await process_payment_success_by_order_no(db, order_no, transaction_id, total_amount) logger.info( f"WeChat callback processed: order_no={order_no}, " f"transaction_id={transaction_id}, amount={total_amount}" ) return {"code": "SUCCESS", "message": "OK"} except Exception as e: logger.exception(f"WeChat callback processing error: {e}") # 微信支付要求即使处理失败也返回成功,避免重复回调 return {"code": "SUCCESS", "message": "OK"} @router.post("/alipay/callback") async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)): form_data = await request.form() data = dict(form_data) logger.info( f"ALIPAY_CALLBACK order_no={data.get('out_trade_no')} " f"data={data}" ) # Verify signature first if not await verify_alipay_callback(data, db): raise HTTPException(status_code=400, detail="签名验证失败") # Check trade_status – only "TRADE_SUCCESS" and "TRADE_FINISHED" mean paid trade_status = data.get("trade_status", "") if trade_status not in ("TRADE_SUCCESS", "TRADE_FINISHED"): logger.info(f"Alipay callback trade_status={trade_status}, ignoring") return "success" order_no = data.get("out_trade_no") trade_no = data.get("trade_no", "") total_amount_str = data.get("total_amount", "") total_amount = float(total_amount_str) if total_amount_str else None if order_no: await process_payment_success_by_order_no(db, order_no, trade_no, total_amount) return "success" @router.get("/orders", response_model=list[PaymentOrderOut]) async def list_orders( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): # Auto-expire stale pending orders before returning from app.services.payment import _check_and_expire_order result = await db.execute( select(PaymentOrder) .where(PaymentOrder.user_id == current_user.id) .order_by(PaymentOrder.created_at.desc()) ) orders = result.scalars().all() for o in orders: await _check_and_expire_order(db, o) return orders @router.get("/orders/{order_no}", response_model=PaymentOrderOut) async def get_order( order_no: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): from app.services.payment import _check_and_expire_order result = await db.execute( select(PaymentOrder) .where( PaymentOrder.order_no == order_no, PaymentOrder.user_id == current_user.id, ) .limit(1) ) order = result.scalar_one_or_none() if not order: raise HTTPException(status_code=404, detail="订单不存在") # Auto-expire if needed await _check_and_expire_order(db, order) return order @router.post("/orders/{order_no}/cancel") async def cancel_order( order_no: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Cancel a pending order. Only the order owner can cancel, only if still pending.""" result = await db.execute( select(PaymentOrder).where( PaymentOrder.order_no == order_no, PaymentOrder.user_id == current_user.id, ).limit(1) ) order = result.scalar_one_or_none() if not order: raise HTTPException(status_code=404, detail="订单不存在") if order.status != "pending": raise HTTPException(status_code=400, detail=f"订单状态为{order.status},无法取消") # If it's an Alipay or WeChat order, call close API first db_configs = await _get_payment_configs(db) if order.payment_method == "alipay": try: await _close_alipay_order(db, order, db_configs) except Exception as e: logger.exception(f"Failed to close Alipay order {order_no}: {e}") elif order.payment_method == "wechat": try: from app.services.payment import _close_wechat_order await _close_wechat_order(db, order, db_configs) except Exception as e: logger.exception(f"Failed to close WeChat order {order_no}: {e}") order.status = "cancelled" await db.flush() logger.info( f"ORDER_CANCELLED order_no={order_no} user={current_user.id} amount={order.amount}" ) return {"ok": True}