| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- """支付相关 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 = (
- "<xml>"
- f"<return_code><![CDATA[{return_code}]]></return_code>"
- f"<return_msg><![CDATA[{return_msg}]]></return_msg>"
- "</xml>"
- )
- 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)
- })
|