From 057e6fcaf811c9eaf6606564eb58b2a8a8cd9ba8 Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 22 Jul 2026 18:58:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=A2=E6=9C=8D=E5=B9=B3=E5=8F=B0=E9=80=80?= =?UTF-8?q?=E6=AC=BE=E5=85=BC=E5=AE=B9=E5=95=86=E6=88=B7/=E8=99=9A?= =?UTF-8?q?=E6=8B=9F=E6=94=AF=E4=BB=98=EF=BC=8C=E6=8C=89=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E6=96=B9=E5=BC=8F=E6=88=96=20DIANDAN=5FPAY?= =?UTF-8?q?=5FMODE=20=E5=88=86=E6=94=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- yonghu/views.py | 164 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 134 insertions(+), 30 deletions(-) diff --git a/yonghu/views.py b/yonghu/views.py index 6082597..4b914f9 100644 --- a/yonghu/views.py +++ b/yonghu/views.py @@ -9127,10 +9127,11 @@ from dingdan.utils import ( import hmac class KefuPlatformRefundView(APIView): """ - 虚拟支付退款接口(代币订单,使用 refund_order 接口) - 业务逻辑与原 KefuPlatformRefundView 完全一致 - 微信 API:POST /xpay/refund_order - 文档:https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/virtual-payment/refund_order.html + 客服平台订单退款(兼容商户支付 / 虚拟支付) + 支付方式判定优先级: + 1) 下单时缓存 diandan_pay_mode_{订单号} + 2) settings.DIANDAN_PAY_MODE(默认 merchant) + 请求:POST /yonghu/kefupttk """ permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] @@ -9178,29 +9179,45 @@ class KefuPlatformRefundView(APIView): if order.zhuangtai not in allowed_statuses: return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST) - # 6. 获取退款所需关键信息 + # 6. 判定本单支付方式 + from django.core.cache import cache + pay_mode = cache.get(f'diandan_pay_mode_{dingdan_id}') + if pay_mode not in ('merchant', 'virtual'): + pay_mode = getattr(settings, 'DIANDAN_PAY_MODE', 'merchant') + if pay_mode not in ('merchant', 'virtual'): + pay_mode = 'merchant' + jiedan_dashou_id = order.jiedan_dashou_id - # 从订单扩展表获取老板 openid(退款API需要) - laoban_openid = None - laoban_id = order.pingtai_kuozhan.laoban_id if hasattr(order, 'pingtai_kuozhan') else None - if laoban_id: - laoban_user = UserMain.objects.filter(yonghuid=laoban_id).first() - if laoban_user: - laoban_openid = getattr(laoban_user, 'openid', None) + wechat_transaction_id = getattr(order, 'wechat_transaction_id', None) - if not laoban_openid: - return Response({'code': 400, 'msg': '未找到用户openid,无法退款'}, status=status.HTTP_400_BAD_REQUEST) + # 7. 按支付方式调用对应退款接口 + if pay_mode == 'virtual': + laoban_openid = None + laoban_id = order.pingtai_kuozhan.laoban_id if hasattr(order, 'pingtai_kuozhan') else None + if laoban_id: + laoban_user = UserMain.objects.filter(yonghuid=laoban_id).first() + if laoban_user: + laoban_openid = getattr(laoban_user, 'openid', None) + if not laoban_openid: + return Response({'code': 400, 'msg': '未找到用户openid,无法退款'}, status=status.HTTP_400_BAD_REQUEST) - # 7. 调用虚拟支付退款接口(refund_order) - refund_result = self.call_virtual_refund( - dingdan_id=order.dingdan_id, - jine=order.jine, - openid=laoban_openid, - wechat_transaction_id=getattr(order, 'wechat_transaction_id', None) - ) + refund_result = self.call_virtual_refund( + dingdan_id=order.dingdan_id, + jine=order.jine, + openid=laoban_openid, + wechat_transaction_id=wechat_transaction_id + ) + else: + refund_result = self.call_wechat_refund( + dingdan_id=order.dingdan_id, + jine=order.jine, + transaction_id=wechat_transaction_id + ) if refund_result['code'] != 0: - logger.error(f"虚拟支付退款失败,订单 {dingdan_id},原因:{refund_result['msg']}") + logger.error( + f"平台退款失败,订单 {dingdan_id},pay_mode={pay_mode},原因:{refund_result['msg']}" + ) return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST) # ========== 以下数据库操作与原逻辑完全一致(全部保留) ========== @@ -9225,6 +9242,9 @@ class KefuPlatformRefundView(APIView): # 10. 更新退款记录表 try: + refund_beizhu = ( + '客服平台退款(虚拟支付)' if pay_mode == 'virtual' else '客服平台退款(商户支付)' + ) refund_record, created = Tuikuanjilu.objects.get_or_create( dingdan_id=dingdan_id, defaults={ @@ -9236,13 +9256,14 @@ class KefuPlatformRefundView(APIView): 'jine': order.jine, 'dashou_fencheng': order.dashou_fencheng or 0, 'jieshao': order.jieshao or '', - 'beizhu': '客服平台退款(虚拟支付)', + 'beizhu': refund_beizhu, 'nicheng': order.nicheng or '', } ) if not created: refund_record.sqzhuangtai = 1 refund_record.chuliid = phone + refund_record.beizhu = refund_beizhu if tuikuan_liyou: refund_record.liyou = tuikuan_liyou refund_record.save() @@ -9304,23 +9325,106 @@ class KefuPlatformRefundView(APIView): except Exception as e: logger.error(f"打手每日统计更新失败: {e}") - logger.info(f"虚拟支付退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}") + logger.info( + f"平台退款成功,订单 {dingdan_id},pay_mode={pay_mode},退款单号 {refund_result.get('refund_id')}" + ) return Response({'code': 0, 'msg': '退款成功'}) except Dingdan.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: - logger.error(f"虚拟支付退款接口异常: {str(e)}", exc_info=True) + logger.error(f"平台退款接口异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - # ==================== 核心退款方法 ==================== + # ==================== 商户微信支付退款 ==================== + def call_wechat_refund(self, dingdan_id, jine, transaction_id=None): + """ + 调用微信支付退款接口 secapi/pay/refund + :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'} + """ + appid = wx_cfg.WEIXIN_APPID + mch_id = wx_cfg.WEIXIN_MCHID + key = wx_cfg.WEIXIN_SHANGHUMIYAO + cert_path = wx_cfg.WEIXIN_CERT_PATH + key_path = wx_cfg.WEIXIN_KEY_PATH + + if not all([appid, mch_id, key, cert_path, key_path]): + missing = [ + k for k, v in { + 'appid': appid, 'mch_id': mch_id, 'key': key, + 'cert_path': cert_path, 'key_path': key_path + }.items() if not v + ] + logger.error(f"微信支付配置缺失: {missing}") + return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'} + + out_refund_no = self.generate_refund_no(dingdan_id) + total_fee = int(float(jine) * 100) + refund_fee = total_fee + + params = { + 'appid': appid, + 'mch_id': mch_id, + 'nonce_str': self.generate_nonce_str(), + 'out_refund_no': out_refund_no, + 'total_fee': total_fee, + 'refund_fee': refund_fee, + 'refund_desc': '客服退款', + } + if transaction_id: + params['transaction_id'] = transaction_id + else: + params['out_trade_no'] = dingdan_id + + sorted_keys = sorted(params.keys()) + stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) + stringSignTemp = f"{stringA}&key={key}" + sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() + params['sign'] = sign + + xml_data = self.dict_to_xml(params) + url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' + + try: + response = requests.post( + url, + data=xml_data, + cert=(cert_path, key_path), + timeout=10 + ) + result = xmltodict.parse(response.content)['xml'] + logger.info(f"微信商户退款响应: {result}") + except Exception as e: + logger.error(f"微信商户退款请求异常: {str(e)}", exc_info=True) + return {'code': 500, 'msg': '微信退款请求异常'} + + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no} + + err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) + logger.warning(f"微信商户退款失败: {err_msg}") + return {'code': 400, 'msg': err_msg} + + def generate_nonce_str(self): + import string + return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + def generate_refund_no(self, dingdan_id): + timestamp = str(int(time.time())) + rand = ''.join(random.choices('0123456789', k=4)) + return f"{dingdan_id}REF{timestamp}{rand}" + + def dict_to_xml(self, data): + xml = [''] + for k, v in data.items(): + xml.append(f'<{k}>{v}') + xml.append('') + return ''.join(xml) + + # ==================== 虚拟支付退款 ==================== def call_virtual_refund(self, dingdan_id, jine, openid, wechat_transaction_id=None): """ 调用虚拟支付退款接口 /xpay/refund_order - :param dingdan_id: 商户订单号(order_id) - :param jine: 退款金额(元) - :param openid: 支付用户的 openid - :param wechat_transaction_id: 微信支付交易单号(wx_order_id),若有则优先使用 :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'} """ uri = "/xpay/refund_order"