"""支付相关 API""" from typing import Optional from datetime import datetime from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException, Request, Response 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.core.utils import generate_id from app.crud.order import order_crud from app.models.models import BizWechatPay router = APIRouter(prefix="/api/payment", tags=["支付接口"]) WECHAT_XML_CONTENT_TYPE = "application/xml; charset=utf-8" def _parse_wechat_pay_time(time_end: Optional[str]) -> Optional[datetime]: """解析微信 V2 回调支付时间(yyyyMMddHHmmss)。""" if not time_end: return None try: return datetime.strptime(time_end, "%Y%m%d%H%M%S") except Exception: return None def _to_int_or_none(value) -> Optional[int]: if value in (None, ""): return None try: return int(value) except Exception: return None def _upsert_wechat_pay_record( db: Session, *, order_id: str, out_trade_no: str, total_fee: int, trade_state: Optional[str] = None, trade_state_desc: Optional[str] = None, transaction_id: Optional[str] = None, cash_fee: Optional[int] = None, pay_time: Optional[datetime] = None, operator_id: Optional[str] = None, operator_name: Optional[str] = None, ) -> BizWechatPay: """按商户单号幂等写入/更新微信支付流水。""" record = ( db.query(BizWechatPay) .filter(BizWechatPay.out_trade_no == out_trade_no, BizWechatPay.is_del == 0) .first() ) if record: record.order_id = order_id record.total_fee = total_fee if trade_state: record.trade_state = trade_state if trade_state_desc: record.trade_state_desc = trade_state_desc if transaction_id: record.transaction_id = transaction_id if cash_fee is not None: record.cash_fee = cash_fee if pay_time: record.pay_time = pay_time record.update_at = datetime.now() record.update_by = operator_id return record record = BizWechatPay( id=generate_id(), order_id=order_id, out_trade_no=out_trade_no, transaction_id=transaction_id, total_fee=total_fee, cash_fee=cash_fee, pay_time=pay_time, trade_state=trade_state or "NOTPAY", trade_state_desc=trade_state_desc, create_by=operator_id, create_name=operator_name, update_by=operator_id, ) db.add(record) return record def _wechat_xml_response(return_code: str, return_msg: str, status_code: int = 200) -> Response: xml = ( "" f"" f"" "" ) return Response(content=xml, status_code=status_code, media_type=WECHAT_XML_CONTENT_TYPE) def _wechat_success() -> Response: return _wechat_xml_response("SUCCESS", "OK") def _wechat_fail(msg: str = "FAIL") -> Response: return _wechat_xml_response("FAIL", msg) 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 {} _upsert_wechat_pay_record( db, order_id=str(order.id), out_trade_no=out_trade_no, total_fee=total_amount, trade_state="NOTPAY", trade_state_desc="统一下单成功,待支付", operator_id=current_user.get("user_id"), operator_name=current_user.get("real_name"), ) db.commit() 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") 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 not result.get("success"): return _wechat_fail("签名验证失败") data = result.get("data", {}) out_trade_no = (data.get("out_trade_no") or "").strip() total_fee_raw = data.get("total_fee") # 订单金额(分) transaction_id = (data.get("transaction_id") or "").strip() or None cash_fee = _to_int_or_none(data.get("cash_fee")) pay_time = _parse_wechat_pay_time(data.get("time_end")) trade_state = (data.get("trade_state") or "SUCCESS").strip() or "SUCCESS" trade_state_desc = (data.get("trade_state_desc") or data.get("return_msg") or "支付成功").strip() if not out_trade_no: return _wechat_fail("缺少商户订单号") try: total_fee = int(total_fee_raw) except (TypeError, ValueError): return _wechat_fail("金额格式错误") # 查询订单并更新状态 db = next(get_db()) try: order = order_crud.get_by_order_no(db, out_trade_no) if not order: return _wechat_fail("订单不存在") expected_fee = int(Decimal(str(order.total_amount or 0)) * 100) if total_fee != expected_fee: return _wechat_fail("金额校验失败") # 幂等:已支付订单重复回调,直接返回成功 if getattr(order, "pay_status", None) in ["paid", "refunding", "refunded"]: _upsert_wechat_pay_record( db, order_id=str(order.id), out_trade_no=out_trade_no, total_fee=total_fee, trade_state=trade_state, trade_state_desc=trade_state_desc, transaction_id=transaction_id, cash_fee=cash_fee, pay_time=pay_time, ) db.commit() return _wechat_success() order.paid_amount = Decimal(total_fee) / Decimal("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() _upsert_wechat_pay_record( db, order_id=str(order.id), out_trade_no=out_trade_no, total_fee=total_fee, trade_state=trade_state, trade_state_desc=trade_state_desc, transaction_id=transaction_id, cash_fee=cash_fee, pay_time=pay_time, ) db.commit() return _wechat_success() finally: db.close() except Exception as e: return _wechat_fail(f"处理回调失败: {str(e)}") @router.post("/refund-notify") 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 not result.get("success"): return _wechat_fail("签名验证失败") data = result.get("data", {}) out_trade_no = (data.get("out_trade_no") or "").strip() refund_id = data.get("refund_id") # 退款单号 if not out_trade_no: return _wechat_fail("缺少商户订单号") # 查询订单并更新退款状态 db = next(get_db()) try: order = order_crud.get_by_order_no(db, out_trade_no) if not order: return _wechat_fail("订单不存在") # 幂等:已经是退款态则直接成功返回 if getattr(order, "pay_status", None) == "refunded": return _wechat_success() order_crud.update_refund_status(db, order.id, "refunded", refund_id) return _wechat_success() finally: db.close() except Exception as e: return _wechat_fail(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) })