| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- """支付相关 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)
- })
|