"""支付相关 API""" from typing import Optional from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.orm import Session from pydantic import BaseModel, Field from app.core.database import get_db from app.core.auth import get_current_user from app.core.response import success_response, error_response from app.core.wechat_pay import wechat_pay, get_wechat_pay_v2 from app.core.config import settings from app.crud.order import order_crud router = APIRouter(prefix="/api/payment", tags=["支付接口"]) class WechatPayRequest(BaseModel): """微信支付请求""" order_id: str = Field(..., description="订单ID") openid: Optional[str] = Field(None, description="用户OpenID") class WechatPayResponse(BaseModel): """微信支付响应""" success: bool prepay_id: Optional[str] = None pay_sign: Optional[str] = None timestamp: Optional[str] = None nonce_str: Optional[str] = None error: Optional[str] = None @router.post("/wechat/create", response_model=dict) async def create_wechat_pay( request: WechatPayRequest, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """ 创建微信支付订单 返回调起微信支付所需的参数 """ try: # 获取订单信息 order = order_crud.get(db, request.order_id) if not order: return error_response(404, "订单不存在") # 检查订单状态 if getattr(order, "payment_method", None) != "online": return error_response(400, "线下支付订单不支持微信支付") pay_status = getattr(order, "pay_status", None) if pay_status: if pay_status != "pending_payment": return error_response(400, "订单状态不允许支付") elif order.status != "pending_payment": return error_response(400, "订单状态不允许支付") # 检查金额 total_amount = int(float(order.total_amount) * 100) # 元转分(微信支付单位为分) if total_amount <= 0: return error_response(400, "订单金额必须大于0") # 生成商户订单号 out_trade_no = order.order_no # 回调地址 notify_url = settings.WXPAY_NOTIFY_URL # 调用微信支付统一下单 pay_client = get_wechat_pay_v2() result = pay_client.unified_order( out_trade_no=out_trade_no, total_fee=total_amount, body="叁强鲜配-订单支付", openid=request.openid, trade_type="JSAPI" ) if result.get("success"): pp = result.get("pay_params") or {} return success_response({ "prepay_id": result.get("prepay_id"), "order_id": order.id, "order_no": order.order_no, "appId": pp.get("appId"), "timeStamp": pp.get("timeStamp"), "nonceStr": pp.get("nonceStr"), "package": pp.get("package"), "signType": pp.get("signType"), "paySign": pp.get("paySign"), }) else: return error_response(500, result.get("error", "创建支付订单失败")) except Exception as e: return error_response(500, f"创建支付订单失败: {str(e)}") @router.get("/query/{order_no}", response_model=dict) def query_payment_status( order_no: str, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """ 查询支付状态 用于前端轮询查询支付结果 """ try: order = order_crud.get_by_order_no(db, order_no) if not order: return error_response(404, "订单不存在") return success_response({ "order_id": order.id, "order_no": order.order_no, "status": order.status, "is_paid": (getattr(order, "pay_status", None) in ["paid", "refunding", "refunded"]) if getattr(order, "pay_status", None) else (order.status not in ["pending_payment", "cancelled"]) }) except Exception as e: return error_response(500, f"查询失败: {str(e)}") @router.post("/notify", response_model=dict) async def wechat_pay_notify(request: Request): """ 微信支付回调通知 注意:此接口不需要认证 """ try: # 接收微信回调数据 body = await request.body() xml_data = body.decode('utf-8') # 解析回调数据 pay_client = get_wechat_pay_v2() result = pay_client.parse_notify(xml_data) if result.get("success"): data = result.get("data", {}) out_trade_no = data.get("out_trade_no") # 商户订单号 transaction_id = data.get("transaction_id") # 微信订单号 total_fee = data.get("total_fee") # 订单金额(分) # 查询订单并更新状态 db = next(get_db()) try: order = order_crud.get_by_order_no(db, out_trade_no) if order: order.paid_amount = int(total_fee) / 100 if getattr(order, "order_status", None) != "cancelled": order.order_status = "pending_confirm" if getattr(order, "delivery_status", None) in [None, ""]: order.delivery_status = "pending_dispatch" order_crud.sync_pay_fields_from_amounts(order) order.update_at = datetime.now() db.commit() # 返回成功 return success_response({"return_code": "SUCCESS", "return_msg": "OK"}) finally: db.close() else: return error_response(400, "签名验证失败") except Exception as e: return error_response(500, f"处理回调失败: {str(e)}") @router.post("/refund-notify", response_model=dict) async def wechat_refund_notify(request: Request): """ 微信退款回调通知 注意:此接口不需要认证 """ try: body = await request.body() xml_data = body.decode('utf-8') pay_client = get_wechat_pay_v2() result = pay_client.parse_notify(xml_data) if result.get("success"): data = result.get("data", {}) out_trade_no = data.get("out_trade_no") # 商户订单号 refund_id = data.get("refund_id") # 退款单号 # 查询订单并更新退款状态 db = next(get_db()) try: order = order_crud.get_by_order_no(db, out_trade_no) if order: # 更新退款状态 order_crud.update_refund_status(db, order.id, "refunded", refund_id) return success_response({"return_code": "SUCCESS", "return_msg": "OK"}) finally: db.close() else: return error_response(400, "签名验证失败") except Exception as e: return error_response(500, f"处理退款回调失败: {str(e)}") @router.post("/close/{order_no}", response_model=dict) def close_payment( order_no: str, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """ 关闭支付订单 用于订单超时未支付时关闭订单 """ try: order = order_crud.get_by_order_no(db, order_no) if not order: return error_response(404, "订单不存在") pay_status = getattr(order, "pay_status", None) if pay_status: if pay_status != "pending_payment": return error_response(400, "订单状态不允许关闭") elif order.status != "pending_payment": return error_response(400, "订单状态不允许关闭") # 调用微信支付关闭订单 pay_client = get_wechat_pay_v2() out_trade_no = order.order_no result = pay_client.close_order(out_trade_no) if result.get("success"): return success_response({"message": "订单已关闭"}) else: # 即使微信关闭失败,也更新本地订单状态 order_crud.update_status(db, order.id, "cancelled", { "cancel_reason": "支付超时自动关闭" }) return success_response({"message": "订单已关闭(本地)"}) except Exception as e: return error_response(500, f"关闭订单失败: {str(e)}") @router.get("/config", response_model=dict) def get_pay_config( current_user: dict = Depends(get_current_user) ): """ 获取微信支付配置信息 返回支付所需的AppID等配置(不含敏感信息) """ return success_response({ "appId": settings.WXPAY_MINI_APPID, "enabled": bool(settings.WXPAY_MCHID and settings.WXPAY_MINI_APPID) })