payment.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. """支付相关 API"""
  2. from typing import Optional
  3. from datetime import datetime
  4. from fastapi import APIRouter, Depends, HTTPException, Request
  5. from sqlalchemy.orm import Session
  6. from pydantic import BaseModel, Field
  7. from app.core.database import get_db
  8. from app.core.auth import get_current_user
  9. from app.core.response import success_response, error_response
  10. from app.core.wechat_pay import wechat_pay, get_wechat_pay_v2
  11. from app.core.config import settings
  12. from app.crud.order import order_crud
  13. router = APIRouter(prefix="/api/payment", tags=["支付接口"])
  14. class WechatPayRequest(BaseModel):
  15. """微信支付请求"""
  16. order_id: str = Field(..., description="订单ID")
  17. openid: Optional[str] = Field(None, description="用户OpenID")
  18. class WechatPayResponse(BaseModel):
  19. """微信支付响应"""
  20. success: bool
  21. prepay_id: Optional[str] = None
  22. pay_sign: Optional[str] = None
  23. timestamp: Optional[str] = None
  24. nonce_str: Optional[str] = None
  25. error: Optional[str] = None
  26. @router.post("/wechat/create", response_model=dict)
  27. async def create_wechat_pay(
  28. request: WechatPayRequest,
  29. db: Session = Depends(get_db),
  30. current_user: dict = Depends(get_current_user)
  31. ):
  32. """
  33. 创建微信支付订单
  34. 返回调起微信支付所需的参数
  35. """
  36. try:
  37. # 获取订单信息
  38. order = order_crud.get(db, request.order_id)
  39. if not order:
  40. return error_response(404, "订单不存在")
  41. # 检查订单状态
  42. if getattr(order, "payment_method", None) != "online":
  43. return error_response(400, "线下支付订单不支持微信支付")
  44. pay_status = getattr(order, "pay_status", None)
  45. if pay_status:
  46. if pay_status != "pending_payment":
  47. return error_response(400, "订单状态不允许支付")
  48. elif order.status != "pending_payment":
  49. return error_response(400, "订单状态不允许支付")
  50. # 检查金额
  51. total_amount = int(float(order.total_amount) * 100) # 元转分(微信支付单位为分)
  52. if total_amount <= 0:
  53. return error_response(400, "订单金额必须大于0")
  54. # 生成商户订单号
  55. out_trade_no = order.order_no
  56. # 回调地址
  57. notify_url = settings.WXPAY_NOTIFY_URL
  58. # 调用微信支付统一下单
  59. pay_client = get_wechat_pay_v2()
  60. result = pay_client.unified_order(
  61. out_trade_no=out_trade_no,
  62. total_fee=total_amount,
  63. body="叁强鲜配-订单支付",
  64. openid=request.openid,
  65. trade_type="JSAPI"
  66. )
  67. if result.get("success"):
  68. pp = result.get("pay_params") or {}
  69. return success_response({
  70. "prepay_id": result.get("prepay_id"),
  71. "order_id": order.id,
  72. "order_no": order.order_no,
  73. "appId": pp.get("appId"),
  74. "timeStamp": pp.get("timeStamp"),
  75. "nonceStr": pp.get("nonceStr"),
  76. "package": pp.get("package"),
  77. "signType": pp.get("signType"),
  78. "paySign": pp.get("paySign"),
  79. })
  80. else:
  81. return error_response(500, result.get("error", "创建支付订单失败"))
  82. except Exception as e:
  83. return error_response(500, f"创建支付订单失败: {str(e)}")
  84. @router.get("/query/{order_no}", response_model=dict)
  85. def query_payment_status(
  86. order_no: str,
  87. db: Session = Depends(get_db),
  88. current_user: dict = Depends(get_current_user)
  89. ):
  90. """
  91. 查询支付状态
  92. 用于前端轮询查询支付结果
  93. """
  94. try:
  95. order = order_crud.get_by_order_no(db, order_no)
  96. if not order:
  97. return error_response(404, "订单不存在")
  98. return success_response({
  99. "order_id": order.id,
  100. "order_no": order.order_no,
  101. "status": order.status,
  102. "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"])
  103. })
  104. except Exception as e:
  105. return error_response(500, f"查询失败: {str(e)}")
  106. @router.post("/notify", response_model=dict)
  107. async def wechat_pay_notify(request: Request):
  108. """
  109. 微信支付回调通知
  110. 注意:此接口不需要认证
  111. """
  112. try:
  113. # 接收微信回调数据
  114. body = await request.body()
  115. xml_data = body.decode('utf-8')
  116. # 解析回调数据
  117. pay_client = get_wechat_pay_v2()
  118. result = pay_client.parse_notify(xml_data)
  119. if result.get("success"):
  120. data = result.get("data", {})
  121. out_trade_no = data.get("out_trade_no") # 商户订单号
  122. transaction_id = data.get("transaction_id") # 微信订单号
  123. total_fee = data.get("total_fee") # 订单金额(分)
  124. # 查询订单并更新状态
  125. db = next(get_db())
  126. try:
  127. order = order_crud.get_by_order_no(db, out_trade_no)
  128. if order:
  129. order.paid_amount = int(total_fee) / 100
  130. if getattr(order, "order_status", None) != "cancelled":
  131. order.order_status = "pending_confirm"
  132. if getattr(order, "delivery_status", None) in [None, ""]:
  133. order.delivery_status = "pending_dispatch"
  134. order_crud.sync_pay_fields_from_amounts(order)
  135. order.update_at = datetime.now()
  136. db.commit()
  137. # 返回成功
  138. return success_response({"return_code": "SUCCESS", "return_msg": "OK"})
  139. finally:
  140. db.close()
  141. else:
  142. return error_response(400, "签名验证失败")
  143. except Exception as e:
  144. return error_response(500, f"处理回调失败: {str(e)}")
  145. @router.post("/refund-notify", response_model=dict)
  146. async def wechat_refund_notify(request: Request):
  147. """
  148. 微信退款回调通知
  149. 注意:此接口不需要认证
  150. """
  151. try:
  152. body = await request.body()
  153. xml_data = body.decode('utf-8')
  154. pay_client = get_wechat_pay_v2()
  155. result = pay_client.parse_notify(xml_data)
  156. if result.get("success"):
  157. data = result.get("data", {})
  158. out_trade_no = data.get("out_trade_no") # 商户订单号
  159. refund_id = data.get("refund_id") # 退款单号
  160. # 查询订单并更新退款状态
  161. db = next(get_db())
  162. try:
  163. order = order_crud.get_by_order_no(db, out_trade_no)
  164. if order:
  165. # 更新退款状态
  166. order_crud.update_refund_status(db, order.id, "refunded", refund_id)
  167. return success_response({"return_code": "SUCCESS", "return_msg": "OK"})
  168. finally:
  169. db.close()
  170. else:
  171. return error_response(400, "签名验证失败")
  172. except Exception as e:
  173. return error_response(500, f"处理退款回调失败: {str(e)}")
  174. @router.post("/close/{order_no}", response_model=dict)
  175. def close_payment(
  176. order_no: str,
  177. db: Session = Depends(get_db),
  178. current_user: dict = Depends(get_current_user)
  179. ):
  180. """
  181. 关闭支付订单
  182. 用于订单超时未支付时关闭订单
  183. """
  184. try:
  185. order = order_crud.get_by_order_no(db, order_no)
  186. if not order:
  187. return error_response(404, "订单不存在")
  188. pay_status = getattr(order, "pay_status", None)
  189. if pay_status:
  190. if pay_status != "pending_payment":
  191. return error_response(400, "订单状态不允许关闭")
  192. elif order.status != "pending_payment":
  193. return error_response(400, "订单状态不允许关闭")
  194. # 调用微信支付关闭订单
  195. pay_client = get_wechat_pay_v2()
  196. out_trade_no = order.order_no
  197. result = pay_client.close_order(out_trade_no)
  198. if result.get("success"):
  199. return success_response({"message": "订单已关闭"})
  200. else:
  201. # 即使微信关闭失败,也更新本地订单状态
  202. order_crud.update_status(db, order.id, "cancelled", {
  203. "cancel_reason": "支付超时自动关闭"
  204. })
  205. return success_response({"message": "订单已关闭(本地)"})
  206. except Exception as e:
  207. return error_response(500, f"关闭订单失败: {str(e)}")
  208. @router.get("/config", response_model=dict)
  209. def get_pay_config(
  210. current_user: dict = Depends(get_current_user)
  211. ):
  212. """
  213. 获取微信支付配置信息
  214. 返回支付所需的AppID等配置(不含敏感信息)
  215. """
  216. return success_response({
  217. "appId": settings.WXPAY_MINI_APPID,
  218. "enabled": bool(settings.WXPAY_MCHID and settings.WXPAY_MINI_APPID)
  219. })