From 1429b2d113a6f3a82ae49308d8c24b08415e77fe Mon Sep 17 00:00:00 2001 From: aaron <> Date: Mon, 27 Jan 2025 23:57:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=80=80=E6=AC=BE=E7=94=B3?= =?UTF-8?q?=E8=AF=B7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/merchant_order.py | 51 ++++++++++++++++- app/api/endpoints/wechat.py | 87 ++++++++++++++++++++++++++++- app/core/wechat.py | 69 +++++++++++++++++++++++ 3 files changed, 204 insertions(+), 3 deletions(-) diff --git a/app/api/endpoints/merchant_order.py b/app/api/endpoints/merchant_order.py index f382593..aa15bba 100644 --- a/app/api/endpoints/merchant_order.py +++ b/app/api/endpoints/merchant_order.py @@ -19,7 +19,8 @@ from datetime import datetime, timezone from app.models.merchant import MerchantDB from app.models.point import PointRecordDB from app.core.account import AccountManager - +from app.core.wechat import WeChatClient +from pydantic import BaseModel router = APIRouter() @router.post("", response_model=ResponseModel) @@ -364,4 +365,50 @@ async def calculate_order_price( return success_response(data={ "original_price": float(product.sale_price), "final_amount": product.sale_price - }) \ No newline at end of file + }) + +class RefundRequest(BaseModel): + """退款请求""" + order_id: str + +@router.post("/refund/approve", response_model=ResponseModel) +async def refund_merchant_order( + request: RefundRequest, + db: Session = Depends(get_db), + admin_user: UserDB = Depends(get_admin_user) +): + """ + 审核通过退款申请(管理员接口) + + - 检查订单是否处于退款中状态 + - 调用微信支付退款接口 + - 退款状态由微信支付回调更新 + """ + try: + # 查询订单 + order = db.query(MerchantOrderDB).filter( + MerchantOrderDB.order_id == request.order_id, + MerchantOrderDB.status == MerchantOrderStatus.REFUNDING, # 只能退款申请中的订单 + MerchantOrderDB.pay_status == True # 已支付的订单 + ).first() + + if not order: + return error_response(code=404, message="订单不存在或状态不正确") + + # 调用微信支付退款 + wechat = WeChatClient() + try: + await wechat.apply_refund( + order_id=order.order_id, + total_amount=int(float(order.pay_amount) * 100), # 转换为分 + reason="用户申请退款" + ) + except Exception as e: + return error_response(code=500, message=f"申请退款失败: {str(e)}") + + db.commit() + return success_response(message="退款申请成功") + + except Exception as e: + db.rollback() + return error_response(code=500, message=f"处理退款失败: {str(e)}") \ No newline at end of file diff --git a/app/api/endpoints/wechat.py b/app/api/endpoints/wechat.py index c89eee3..6b1f8ce 100644 --- a/app/api/endpoints/wechat.py +++ b/app/api/endpoints/wechat.py @@ -18,6 +18,8 @@ from app.models.merchant_order import MerchantOrderDB, MerchantOrderStatus from app.models.merchant_pay_order import MerchantPayOrderDB, MerchantPayOrderStatus import enum from app.core.point_manager import PointManager +from app.core.point_manager import PointRecordType + router = APIRouter() class PhoneNumberRequest(BaseModel): @@ -271,4 +273,87 @@ async def payment_notify( except Exception as e: print(f"处理支付回调失败: {str(e)}") db.rollback() - return error_response(code=500, message=f"处理支付回调失败: {str(e)}") \ No newline at end of file + return error_response(code=500, message=f"处理支付回调失败: {str(e)}") + +@router.post("/refund/notify") +async def refund_notify( + request: Request, + db: Session = Depends(get_db) +): + """微信支付退款回调通知""" + try: + # 初始化微信支付客户端 + wechat = WeChatClient() + + data = await wechat.verify_payment_notify(request) + if not data: + print(f"退款回调数据验证失败: {data}") + return error_response(code=400, message="回调数据验证失败") + + print(f"退款回调数据验证成功: {data}") + + # 获取退款信息 + out_trade_no = data.get("out_trade_no") # 订单号 + refund_status = data.get("refund_status") + success_time = data.get("success_time") + + if not all([out_trade_no, refund_status]) or refund_status != "SUCCESS": + return error_response(code=400, message="缺少必要的退款信息或退款未成功") + + # 根据订单类型处理退款 + if out_trade_no.startswith('M'): # 商家商品订单 + order = db.query(MerchantOrderDB).filter( + MerchantOrderDB.order_id == out_trade_no, + MerchantOrderDB.status == MerchantOrderStatus.REFUNDING + ).first() + + if not order: + return error_response(code=404, message="订单不存在或状态不正确") + + # 更新订单状态 + order.status = MerchantOrderStatus.REFUNDED + + # 扣除积分 + points = int(float(order.pay_amount) * 10) # 按照支付金额计算积分 + point_manager = PointManager(db) + point_manager.deduct_points( + order.user_id, + points, + PointRecordType.CONSUME_RETURN, + f"订单退款,扣除{settings.POINT_ALIAS}", + order.order_id + ) + + elif out_trade_no.startswith('P'): # 商家在线买单 + order = db.query(MerchantPayOrderDB).filter( + MerchantPayOrderDB.order_id == out_trade_no, + MerchantPayOrderDB.status == MerchantPayOrderStatus.REFUNDING + ).first() + + if not order: + return error_response(code=404, message="订单不存在或状态不正确") + + # 更新订单状态 + order.status = MerchantPayOrderStatus.REFUNDED + + # 扣除积分 + points = int(float(order.amount) * 10) # 按照支付金额计算积分 + point_manager = PointManager(db) + point_manager.deduct_points( + order.user_id, + points, + PointRecordType.CONSUME_RETURN, + f"订单退款,扣除{settings.POINT_ALIAS}", + order.order_id + ) + + else: + return error_response(code=400, message="不支持的订单类型") + + db.commit() + return success_response(message="退款处理成功") + + except Exception as e: + print(f"处理退款回调失败: {str(e)}") + db.rollback() + return error_response(code=500, message=f"处理退款回调失败: {str(e)}") \ No newline at end of file diff --git a/app/core/wechat.py b/app/core/wechat.py index 1cf1ba3..06397a6 100644 --- a/app/core/wechat.py +++ b/app/core/wechat.py @@ -363,5 +363,74 @@ class WeChatClient: import traceback print("详细错误信息:", traceback.format_exc()) raise Exception(f"处理支付回调失败: {str(e)}") + + async def apply_refund( + self, + order_id: str, + total_amount: int, + reason: str = "用户申请退款" + ) -> dict: + """ + 申请退款 + + Args: + order_id: 订单号(同时作为退款单号) + total_amount: 退款金额(分) + reason: 退款原因 + + Returns: + dict: 退款结果 + """ + url_path = "/v3/refund/domestic/refunds" + api_url = f"https://api.mch.weixin.qq.com{url_path}" + + # 构建请求数据 + body = { + "out_trade_no": order_id, # 原订单号 + "out_refund_no": order_id, # 退款单号使用原订单号 + "reason": reason, + "notify_url": f"{settings.API_BASE_URL}/api/wechat/refund/notify", + "amount": { + "refund": total_amount, # 退款金额 + "total": total_amount, # 原订单金额 + "currency": "CNY" + } + } + + # 生成签名 + nonce_str = str(uuid.uuid4()).replace('-', '') + timestamp = str(int(time.time())) + signature = self.sign_message("POST", url_path, body) + + # 构建认证头 + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': ( + f'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{self.mch_id}",' + f'nonce_str="{nonce_str}",' + f'timestamp="{timestamp}",' + f'serial_no="{self.cert_serial_no}",' + f'signature="{signature}"' + ) + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(api_url, json=body, headers=headers) as response: + result = await response.json() + + if response.status != 200: + raise Exception(f"退款申请失败: {result.get('message')}") + + # 验证响应签名 + if not self.verify_response(response.headers, await response.read()): + raise Exception("响应签名验证失败") + + return result + + except Exception as e: + raise Exception(f"申请退款失败: {str(e)}") \ No newline at end of file