diff --git a/users/views/pay.py b/users/views/pay.py index b9e333b..b87fd9d 100644 --- a/users/views/pay.py +++ b/users/views/pay.py @@ -1,1044 +1,1039 @@ -"""users.views.pay - auto-generated by split script.""" -import os -import sys -import json -import re -import time -import uuid -import random -import string -import requests -import hashlib -import traceback -import decimal -import jwt -import ipaddress -import xmltodict -from datetime import datetime, timedelta -from decimal import Decimal -import defusedxml.ElementTree as ET - -from django.conf import settings -from django.db import models, transaction, IntegrityError, DatabaseError, connection -from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch -from django.core.cache import cache -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.core.exceptions import ObjectDoesNotExist -from django.shortcuts import get_object_or_404 -from django.utils import timezone -from django.http import HttpResponse -from django.views import View -from django.views.decorators.csrf import csrf_exempt -from django.contrib.auth.hashers import make_password - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import permissions, status -from rest_framework.permissions import AllowAny, IsAuthenticated -from rest_framework.parsers import MultiPartParser, FormParser, JSONParser -from rest_framework.throttling import AnonRateThrottle -from rest_framework_simplejwt.tokens import RefreshToken -from rest_framework_simplejwt.authentication import JWTAuthentication - -from gvsdsdk.fluent import db, func, FQ - -from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss -from utils.money import yuan_to_fen -from utils.fadan_utils import check_fadan_qiangdan_eligible -from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode -from users.fadan_fenhong_utils import process_fadan_fenhong - -from orders.utils import ( - update_daily_payout, - settle_shangjia_order_guanshi_fenhong -) -from backend.utils import ( - update_dashou_daily_by_action, update_guanshi_daily_by_action, - update_shangjia_daily, update_zuzhang_daily_by_action, - verify_kefu_permission -) -from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan - -from ..models import ( - UserBoss, UserDashou, UserShangjia, UserGuanshi, - UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser, - TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu, - RankingRecord -) -from users.business_models import User -from ..tixian_shenhe_services import ( - load_audit_meta_map, refund_balance, sync_audit_and_jilu_status, - mark_transfer_success, release_collect_quota_for_record, - close_audit_wechat_failed, query_wechat_transfer_bill, - check_shijidaozhang_within_limit, - check_dashou_trial_blocks_commission_withdraw, - WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING, -) -from ..tixian_shenhe_views import process_audit_collect - -from orders.models import ( - Order, PlatformOrderExt, MerchantOrderExt, CommissionRate, - PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage, - Penalty, PenaltyAppealImage -) -from products.models import ( - ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu -) -from config.models import Qunpeizhi -from backend.models import MerchantDailyStats -from rank.models import ( - KaohePayTemp, Chenghao, ShenheJilu, - KaoheCishuFeiyong, YonghuChenghao -) -from users.models import UserShenheguan -from products.utils import update_shangpin_daily_stat -from shop.utils import update_dianpu_daily_stat -from rank.utils import create_shenhe_jilu_from_temp - -import logging - -logger = logging.getLogger(__name__) - -class FaKuanPayView(APIView): - """ - 罚款微信支付下单接口(完全照抄 YajinGoumai) - 路径:POST /yonghu/fkjn - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - fadan_id = request.data.get('fadan_id') - if not fadan_id: - return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) - - try: - fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.UserUID) - except Penalty.DoesNotExist: - return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND) - - if fadan.Status not in [1, 3]: - return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST) - - jine = float(fadan.FineAmount) - - timestamp_ms = int(time.time() * 1000) - timestamp_ns = int(time.time_ns() // 1000) % 1000000 - random_num = random.randint(0, 99) - dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" - - from jituan.services.club_penalty import penalty_cz_club_id - from jituan.services.club_user import get_payment_openid - from products.czjilu_types import CZJILU_LEIXING_FAKUAN, penalty_cz_shuoming - - chongzhi_jilu = Czjilu.query.create( - dingdan_id=dingdanid, - zhuangtai=9, - yonghuid=request.user.UserUID, - jine=jine, - leixing=CZJILU_LEIXING_FAKUAN, - shuoming=penalty_cz_shuoming(fadan.id), - club_id=penalty_cz_club_id(fadan), - ) - - # 生成支付参数(支持支付宝/微信) - pay_method = (request.data.get('pay_method') or request.data.get('payMethod') or 'wechat').lower() - if pay_method == 'alipay': - try: - from jituan.services.alipay_pay import generate_alipay_czjilu_pay_params - pay_params = generate_alipay_czjilu_pay_params( - dingdanid=dingdanid, - jine=jine, - subject=settings.FAKUAN_PAY_DESCRIPTION, - club_id=penalty_cz_club_id(fadan), - ) - except Exception as e: - chongzhi_jilu.delete() - logger.error(f"生成支付宝支付参数失败: {str(e)}") - return Response({ - 'code': 500, - 'message': f'支付参数生成失败: {str(e)}' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - else: - pay_club = penalty_cz_club_id(fadan) - pay_openid, _ = get_payment_openid(request, club_id=pay_club) - if not pay_openid: - chongzhi_jilu.delete() - return Response({ - 'code': 400, - 'message': '当前小程序未绑定微信openid,请退出后重新登录再支付', - }, status=status.HTTP_400_BAD_REQUEST) - try: - pay_params = self.generate_wechat_pay_params( - dingdanid=dingdanid, - jine=jine, - openid=pay_openid, - pay_type='fakuan' - ) - except Exception as e: - chongzhi_jilu.delete() - logger.error(f"生成微信支付参数失败: {str(e)}") - return Response({ - 'code': 500, - 'message': f'支付参数生成失败: {str(e)}' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - return Response({ - 'code': 200, - 'message': '支付参数生成成功', - 'payParams': pay_params, - 'dingdanid': dingdanid - }, status=status.HTTP_200_OK) - - except Exception as e: - logger.error(f"罚款支付下单接口异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': '服务器内部错误,请稍后重试' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): - from jituan.services.club_write import resolve_club_id_for_write - from jituan.services.wechat_pay import get_wechat_v2_config - pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user)) - APPID = pay_cfg['appid'] - MCHID = pay_cfg['mch_id'] - KEY = pay_cfg['key'] - - if pay_type == 'fakuan': - PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION - NOTIFY_URL = settings.FAKUAN_PAY_NOTIFY_URL # 回调地址在这里 - else: - raise Exception('未知的支付类型') - - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - total_fee = yuan_to_fen(jine) - trade_type = 'JSAPI' - user_ip = self.get_client_ip() - - # 第一次签名 - sign_data = { - 'appid': APPID, - 'mch_id': MCHID, - 'nonce_str': nonce_str, - 'body': PAY_DESCRIPTION, - 'out_trade_no': dingdanid, - 'total_fee': total_fee, - 'spbill_create_ip': user_ip, - 'notify_url': NOTIFY_URL, # 关键:把回调地址传入 - 'trade_type': trade_type, - 'openid': openid - } - - sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) - sign_string += f'&key={KEY}' - sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - # 构建 XML,这里必须包含 notify_url - xml_data = f""" -{APPID} -{PAY_DESCRIPTION} -{MCHID} -{nonce_str} -{NOTIFY_URL} -{openid} -{dingdanid} -{user_ip} -{total_fee} -{trade_type} -{sign} -""" - - response = requests.post( - 'https://api.mch.weixin.qq.com/pay/unifiedorder', - data=xml_data.encode('utf-8'), - headers={'Content-Type': 'application/xml'}, - timeout=10 - ) - - logger.info(f"罚款微信支付响应: {response.text}") - - root = ET.fromstring(response.content) - - return_code_node = root.find('return_code') - if return_code_node is None or return_code_node.text is None: - logger.error(f"微信返回缺少return_code,原始: {response.text}") - raise Exception('微信支付接口异常,缺少return_code') - return_code = return_code_node.text - - result_code_node = root.find('result_code') - result_code = result_code_node.text if result_code_node is not None else None - - if return_code == 'SUCCESS' and result_code == 'SUCCESS': - prepay_id_node = root.find('prepay_id') - if prepay_id_node is None or prepay_id_node.text is None: - logger.error(f"统一下单成功但无prepay_id,原始: {response.text}") - raise Exception('微信支付接口异常,缺少prepay_id') - prepay_id = prepay_id_node.text - timestamp = str(int(time.time())) - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - package = f'prepay_id={prepay_id}' - sign_type = 'MD5' - - pay_sign_data = { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type - } - - sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) - pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) - pay_sign_string += f'&key={KEY}' - pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() - - return { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type, - 'paySign': pay_sign - } - else: - err_code = root.find('err_code') - err_code_des = root.find('err_code_des') - return_msg = root.find('return_msg') - error_details = f"return_code: {return_code}" - if result_code: - error_details += f", result_code: {result_code}" - if return_msg is not None and return_msg.text: - error_details += f", return_msg: {return_msg.text}" - if err_code is not None and err_code.text: - error_details += f", err_code: {err_code.text}" - if err_code_des is not None and err_code_des.text: - error_details += f", err_code_des: {err_code_des.text}" - raise Exception(f"微信支付下单失败: {error_details}") - - def get_client_ip(self): - request = self.request - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip if ip else '127.0.0.1' - - -class FaKuanHuitiaoView(View): - - """ - 罚款支付回调接口(严格借鉴 YajinHuitiao 接口,参数一个不少) - 路径:POST /yonghu/fk_huitiao - """ - def post(self, request): - try: - # 1. 解析微信回调XML数据 - request_body = request.body - logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}") - - root = ET.fromstring(request_body) - - # 获取关键字段 - return_code = root.find('return_code').text - result_code = root.find('result_code').text - out_trade_no = root.find('out_trade_no').text # 订单ID - transaction_id = root.find('transaction_id').text # 微信交易号 - total_fee = int(root.find('total_fee').text) # 支付金额(分) - - logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") - - # 2. 验证基本返回状态 - if return_code != 'SUCCESS' or result_code != 'SUCCESS': - logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}") - return self.wechat_response('FAIL', '支付失败') - - # 3. 验证签名(确保是微信发来的) - from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature - notify_club = club_id_from_czjilu(out_trade_no) - if not verify_wechat_v2_signature(request_body, notify_club): - logger.error(f"签名验证失败: {out_trade_no}") - return self.wechat_response('FAIL', '签名验证失败') - - # 4. 查询订单 - try: - order = Czjilu.query.get(dingdan_id=out_trade_no) - logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") - except Czjilu.DoesNotExist: - logger.error(f"订单不存在: {out_trade_no}") - return self.wechat_response('FAIL', '订单不存在') - - from products.czjilu_types import is_penalty_czjilu - if not is_penalty_czjilu(order): - logger.error(f"非罚款支付订单误入罚款回调: {out_trade_no}, leixing={order.leixing}") - return self.wechat_response('FAIL', '订单类型错误') - - # 5. 检查订单状态是否为未支付(9) - if order.zhuangtai != 9: - logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") - try: - from jituan.services.szjilu_accounting import repair_wechat_income_if_missing - repair_wechat_income_if_missing( - Decimal(str(order.jine)), - getattr(order, 'club_id', None), - biz_ref=f'cz:{out_trade_no}', - ) - except Exception as exc: - logger.error('罚款回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) - return self.wechat_response('SUCCESS', 'OK') - - # 6. 验证金额(微信返回的是分,需要转换) - wechat_amount_yuan = total_fee / 100 # 转换为元 - order_amount_yuan = float(order.jine) # 订单金额(元) - - # 允许1分钱的误差(浮点数比较) - if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: - logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") - return self.wechat_response('FAIL', '金额不一致') - - # 7. 调用统一履约函数(事务 + 行锁 + 幂等 + 罚单状态 + 分红 + 收支记录) - from jituan.services.czjilu_fulfill import fulfill_fakuan, CzjiluFulfillError - try: - fulfill_fakuan(out_trade_no, paid_amount_yuan=wechat_amount_yuan) - except CzjiluFulfillError as exc: - logger.error('罚款回调履约失败 %s: %s', out_trade_no, exc) - return self.wechat_response('FAIL', str(exc)) - - # 8. 返回成功响应给微信 - logger.info(f"罚款支付回调处理完成: {out_trade_no}") - return self.wechat_response('SUCCESS', 'OK') - - except ET.ParseError as e: - logger.error(f"XML解析失败: {str(e)}") - return self.wechat_response('FAIL', '数据格式错误') - except Exception as e: - logger.error(f"罚款支付回调处理异常: {str(e)}", exc_info=True) - # 返回FAIL让微信重试 - return self.wechat_response('FAIL', '处理异常') - - def verify_signature(self, xml_data): - """验证微信支付签名(完全照搬 YajinHuitiao 的 verify_signature)""" - try: - root = ET.fromstring(xml_data) - sign = root.find('sign').text - - if not sign: - logger.error("签名字段不存在") - return False - - # 获取所有参数并排除sign - params = {} - for child in root: - if child.tag != 'sign' and child.text is not None: - params[child.tag] = child.text - - # 按照参数名ASCII码从小到大排序 - sorted_params = sorted(params.items(), key=lambda x: x[0]) - - # 拼接字符串 - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' - - # 计算签名 - calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - is_valid = sign == calculated_sign - if not is_valid: - logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") - - return is_valid - - except Exception as e: - logger.error(f"验证签名异常: {str(e)}") - return False - - def wechat_response(self, return_code, return_msg): - """返回微信支付回调响应(完全照搬 YajinHuitiao 的 wechat_response)""" - xml_response = f""" - - -""" - - logger.info(f"返回微信响应: {return_code} - {return_msg}") - return HttpResponse(xml_response, content_type='application/xml') - - -class FaKuanPayPollView(APIView): - """罚款支付轮询接口(借鉴 YajinLunxun)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({'code': 400, 'message': '订单ID不能为空'}) - - order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID) - # 订单未支付时,尝试补偿履约(回调延迟/失败时通过轮询兜底) - if order.zhuangtai == 9: - from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError - try: - confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=6) - order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID) - except CzjiluFulfillError as exc: - logger.info(f"罚款确认未就绪 {dingdanid}: {exc}") - return Response({'code': 400, 'message': '订单尚未支付'}) - - if order.zhuangtai == 3: - return Response({'code': 200, 'message': '支付成功'}) - else: - return Response({'code': 400, 'message': '订单尚未支付'}) - except Czjilu.DoesNotExist: - return Response({'code': 400, 'message': '订单不存在'}) - except Exception as e: - logger.error(f"轮询异常: {str(e)}") - return Response({'code': 500, 'message': '服务器内部错误'}) - - -class FaKuanPayFailView(APIView): - """罚款支付失败处理接口(借鉴 YajinShibai)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({'code': 400, 'message': '订单ID不能为空'}) - - order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID) - if order.zhuangtai == 9: # 未支付 - order.delete() - logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}") - return Response({'code': 200, 'message': '订单已删除'}) - else: - return Response({'code': 400, 'message': '订单状态不是未支付,无法删除'}) - except Czjilu.DoesNotExist: - return Response({'code': 400, 'message': '订单不存在'}) - except Exception as e: - logger.error(f"支付失败处理异常: {str(e)}") - return Response({'code': 500, 'message': '服务器内部错误'}) - - -class KaohePayView(APIView): - """ - 考核支付下单接口 - 路径:POST /yonghu/khzf - 参数: - tag_id: 标签ID - game_nick: 游戏昵称 - remark: 备注 - reapply: 是否再次申请 (true/false) - jilu_id: 再次申请时的原记录ID(reapply=true时必传) - shenheguan_id: 指定考核官ID(可选) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - tag_id = request.data.get('tag_id') - game_nick = request.data.get('game_nick', '').strip() - remark = request.data.get('remark', '').strip() - reapply = request.data.get('reapply', False) - jilu_id = request.data.get('jilu_id') if reapply else None - shenheguan_id = request.data.get('shenheguan_id', '').strip() - - logger.info(f"用户 {user.UserUID} 发起考核支付申请,标签ID: {tag_id}, 再次申请: {reapply}") - - if not tag_id or not game_nick: - logger.warning(f"用户 {user.UserUID} 参数不完整: tag_id={tag_id}, game_nick={game_nick}") - return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'}) - - # 1. 检查是否有未支付的考核订单 - """unpaid_order = Czjilu.query.filter( - UserUID=user.UserUID, - leixing=5, - zhuangtai=9 - ).exists() - if unpaid_order: - logger.warning(f"用户 {user.UserUID} 存在未支付的考核订单") - return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})""" - - # 2. 验证标签 - try: - chenghao = Chenghao.query.get(id=tag_id) - logger.info(f"标签验证通过: {chenghao.mingcheng}") - except Chenghao.DoesNotExist: - logger.error(f"标签不存在: tag_id={tag_id}") - return Response({'code': 404, 'message': '标签不存在'}) - - # 3. 检查用户是否已拥有该标签 - if YonghuChenghao.query.filter(yonghu=user, chenghao=chenghao).exists(): - logger.warning(f"用户 {user.UserUID} 已拥有标签 {chenghao.mingcheng}") - return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'}) - - # 4. 检查是否有未完成的申请 - if ShenheJilu.query.filter( - shenqingren_id=user.UserUID, - chenghao=chenghao, - zhuangtai__in=[0, 3] # 审核中、待审核 - ).exists(): - logger.warning(f"用户 {user.UserUID} 有未完成的考核申请") - return Response({'code': 400, 'message': '您有未完成的考核申请,请等待结果'}) - - # 5. 计算考核次数和费用 - if reapply and jilu_id: - last = ShenheJilu.query.filter( - jilu_id=jilu_id, - shenqingren_id=user.UserUID, - zhuangtai=2 - ).first() - if not last: - logger.warning(f"用户 {user.UserUID} 再次申请失败:原记录不存在或状态不是未通过,jilu_id={jilu_id}") - return Response({'code': 400, 'message': '原记录不存在或状态不是未通过'}) - new_cishu = last.cishu + 1 - logger.info(f"再次申请,原记录ID: {jilu_id},新次数: {new_cishu}") - else: - max_cishu = ShenheJilu.query.filter( - shenqingren_id=user.UserUID, - chenghao=chenghao, - zhuangtai__in=[1, 2] - ).aggregate(max=models.Max('cishu'))['max'] or 0 - new_cishu = max_cishu + 1 - logger.info(f"首次申请,计算次数: {new_cishu}") - - # 6. 获取本次费用 - fee = get_tag_fee(chenghao, new_cishu) - logger.info(f"本次考核费用: {fee}元,次数: {new_cishu}") - - if fee <= 0: - logger.warning(f"费用为0,应使用免费申请接口,tag_id={tag_id}, cishu={new_cishu}") - return Response({'code': 400, 'message': '本次费用为0,请直接使用免费申请接口'}) - - # 7. 生成订单ID - timestamp_ms = int(time.time() * 1000) - timestamp_ns = int(time.time_ns() // 1000) % 1000000 - random_num = random.randint(0, 99) - dingdanid = f"KH{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" - logger.info(f"生成订单ID: {dingdanid}") - - # 8. 创建支付订单(Czjilu) - from jituan.services.club_write import resolve_club_id_for_write - order = Czjilu.query.create( - dingdan_id=dingdanid, - zhuangtai=9, # 未支付 - yonghuid=user.UserUID, - jine=float(fee), - leixing=5, # 5-考核支付 - shuoming=settings.KAOHE_PAY_DESCRIPTION, - club_id=resolve_club_id_for_write(request, user), - ) - logger.info(f"创建支付订单成功: {dingdanid}") - - # 9. 保存考核支付临时上下文(用于回调时取业务参数) - try: - KaohePayTemp.query.create( - dingdan_id=dingdanid, - yonghuid=user.UserUID, - tag_id=tag_id, - game_nick=game_nick, - remark=remark, - reapply=reapply, - jilu_id=jilu_id if reapply else None, - shenheguan_id=shenheguan_id, - cishu=new_cishu, - fee=fee, - ) - logger.info(f"保存临时上下文成功: {dingdanid}") - except Exception as e: - logger.error(f"保存临时上下文失败: {str(e)}", exc_info=True) - # 临时表保存失败应该回滚订单 - order.delete() - return Response({'code': 500, 'message': '系统异常,请稍后重试'}) - - # 10. 构造 attach 参数(只传订单ID,确保不超127字节) - attach_str = json.dumps({'oid': dingdanid}, ensure_ascii=False) - logger.info(f"attach内容: {attach_str},长度: {len(attach_str.encode('utf-8'))}字节") - - # 11. 生成支付参数(支持支付宝/微信) - pay_method = (request.data.get('pay_method') or request.data.get('payMethod') or 'wechat').lower() - if pay_method == 'alipay': - try: - from jituan.services.alipay_pay import generate_alipay_czjilu_pay_params - pay_params = generate_alipay_czjilu_pay_params( - dingdanid=dingdanid, - jine=float(fee), - subject=settings.KAOHE_PAY_DESCRIPTION, - club_id=resolve_club_id_for_write(request, user), - ) - logger.info(f"支付宝支付参数生成成功: {dingdanid}") - except Exception as e: - order.delete() - try: - KaohePayTemp.query.filter(dingdan_id=dingdanid).delete() - except Exception as del_e: - logger.error(f"删除临时表记录失败: {str(del_e)}") - logger.error(f"生成支付宝支付参数失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'}) - else: - try: - pay_params = self.generate_wechat_pay_params( - dingdanid=dingdanid, - jine=float(fee), - openid=user.OpenID, - attach=attach_str, - pay_type='kaohe' - ) - logger.info(f"微信支付参数生成成功: {dingdanid}") - except Exception as e: - order.delete() # 下单失败,删除订单记录 - # 同时删除临时表记录 - try: - KaohePayTemp.query.filter(dingdan_id=dingdanid).delete() - except Exception as del_e: - logger.error(f"删除临时表记录失败: {str(del_e)}") - logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True) - return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'}) - - return Response({ - 'code': 200, - 'message': '支付参数生成成功', - 'payParams': pay_params, - 'dingdanid': dingdanid - }) - - except Exception as e: - logger.error(f"考核支付下单异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'message': '服务器内部错误'}) - - def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type): - """生成微信JSAPI支付参数""" - from jituan.services.club_write import resolve_club_id_for_write - from jituan.services.wechat_pay import get_wechat_v2_config - pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user)) - APPID = pay_cfg['appid'] - MCHID = pay_cfg['mch_id'] - KEY = pay_cfg['key'] - - if pay_type == 'kaohe': - PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION - NOTIFY_URL = settings.KAOHE_PAY_NOTIFY_URL - else: - raise Exception('未知支付类型') - - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - total_fee = yuan_to_fen(jine) - trade_type = 'JSAPI' - user_ip = self.get_client_ip() - - logger.info(f"微信支付下单参数: out_trade_no={dingdanid}, total_fee={total_fee}, openid={openid}, ip={user_ip}") - - # 第一次签名(统一下单) - sign_data = { - 'appid': APPID, - 'mch_id': MCHID, - 'nonce_str': nonce_str, - 'body': PAY_DESCRIPTION, - 'out_trade_no': dingdanid, - 'total_fee': total_fee, - 'spbill_create_ip': user_ip, - 'notify_url': NOTIFY_URL, - 'trade_type': trade_type, - 'openid': openid, - 'attach': attach, - } - - sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) - sign_string += f'&key={KEY}' - sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - # 构建XML请求体 - xml_data = f""" -{APPID} -{PAY_DESCRIPTION} -{MCHID} -{nonce_str} -{NOTIFY_URL} -{openid} -{dingdanid} -{user_ip} -{total_fee} -{trade_type} - -{sign} -""" - - logger.info(f"微信支付请求XML: {xml_data[:500]}...") # 只打印前500字符 - - response = requests.post( - 'https://api.mch.weixin.qq.com/pay/unifiedorder', - data=xml_data.encode('utf-8'), - headers={'Content-Type': 'application/xml'}, - timeout=10 - ) - - logger.info(f"微信支付响应: {response.text}") - - root = ET.fromstring(response.content) - - # 安全获取return_code - return_code_node = root.find('return_code') - if return_code_node is None or return_code_node.text is None: - logger.error(f"微信返回缺少return_code节点,原始内容: {response.text}") - raise Exception('微信支付接口异常,缺少return_code') - return_code = return_code_node.text - - # 安全获取result_code - result_code_node = root.find('result_code') - if result_code_node is None or result_code_node.text is None: - logger.error(f"微信返回缺少result_code节点,原始内容: {response.text}") - raise Exception('微信支付接口异常,缺少result_code') - result_code = result_code_node.text - - if return_code == 'SUCCESS' and result_code == 'SUCCESS': - prepay_id_node = root.find('prepay_id') - if prepay_id_node is None or prepay_id_node.text is None: - logger.error(f"统一下单成功但未返回prepay_id,原始内容: {response.text}") - raise Exception('微信支付接口异常,缺少prepay_id') - prepay_id = prepay_id_node.text - - timestamp = str(int(time.time())) - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - package = f'prepay_id={prepay_id}' - sign_type = 'MD5' - - # 第二次签名(前端调起支付用) - pay_sign_data = { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type, - } - sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) - pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) - pay_sign_string += f'&key={KEY}' - pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() - - logger.info(f"微信支付预下单成功,prepay_id={prepay_id}") - - return { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type, - 'paySign': pay_sign, - } - else: - # 安全获取错误信息 - err_code_node = root.find('err_code') - err_code = err_code_node.text if err_code_node is not None else '未知错误码' - err_code_des_node = root.find('err_code_des') - err_code_des = err_code_des_node.text if err_code_des_node is not None else '未知错误描述' - error_details = f"return_code: {return_code}, result_code: {result_code}, err_code: {err_code}, err_code_des: {err_code_des}" - logger.error(f"微信支付下单失败: {error_details}") - raise Exception(f"微信支付下单失败: {error_details}") - - def get_client_ip(self): - """获取客户端IP""" - request = self.request - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR', '127.0.0.1') - return ip if ip else '127.0.0.1' - - -class KaohePayCallbackView(View): - """ - 考核支付回调接口 - 路径:/yonghu/kh_huitiao - """ - - def post(self, request): - try: - request_body = request.body - logger.info(f"收到考核支付回调: {request_body.decode('utf-8')}") - - root = ET.fromstring(request_body) - - # 安全获取return_code - return_code_node = root.find('return_code') - if return_code_node is None or return_code_node.text is None: - logger.error(f"微信回调XML缺少return_code节点,原始内容: {request_body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - return_code = return_code_node.text - - # 安全获取result_code - result_code_node = root.find('result_code') - if result_code_node is None or result_code_node.text is None: - logger.error(f"微信回调XML缺少result_code节点,原始内容: {request_body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - result_code = result_code_node.text - - # 安全获取out_trade_no - out_trade_no_node = root.find('out_trade_no') - if out_trade_no_node is None or out_trade_no_node.text is None: - logger.error(f"微信回调XML缺少out_trade_no节点,原始内容: {request_body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - out_trade_no = out_trade_no_node.text - - # 安全获取transaction_id - transaction_id_node = root.find('transaction_id') - if transaction_id_node is None or transaction_id_node.text is None: - logger.error(f"微信回调XML缺少transaction_id节点,原始内容: {request_body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - transaction_id = transaction_id_node.text - - # 安全获取total_fee - total_fee_node = root.find('total_fee') - if total_fee_node is None or total_fee_node.text is None: - logger.error(f"微信回调XML缺少total_fee节点,原始内容: {request_body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - total_fee = int(total_fee_node.text) - - logger.info( - f"回调参数解析成功: out_trade_no={out_trade_no}, transaction_id={transaction_id}, total_fee={total_fee}") - - if return_code != 'SUCCESS' or result_code != 'SUCCESS': - logger.warning(f"支付状态异常: return_code={return_code}, result_code={result_code}") - return self.wechat_response('FAIL', '支付失败') - - # 验证签名 - from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature - notify_club = club_id_from_czjilu(out_trade_no) - if not verify_wechat_v2_signature(request_body, notify_club): - logger.error(f"签名验证失败,订单号: {out_trade_no}") - return self.wechat_response('FAIL', '签名验证失败') - logger.info(f"签名验证成功,订单号: {out_trade_no}") - - # 查找订单 - try: - order = Czjilu.query.get(dingdan_id=out_trade_no, leixing=5) - logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.zhuangtai}") - except Czjilu.DoesNotExist: - logger.error(f"考核订单不存在: {out_trade_no}") - return self.wechat_response('FAIL', '订单不存在') - - if order.zhuangtai != 9: - logger.warning(f"订单状态非未支付: {order.zhuangtai}, 订单号: {out_trade_no}") - if order.zhuangtai == 3: - logger.info(f"订单已支付,补试收支记账: {out_trade_no}") - try: - from jituan.services.szjilu_accounting import repair_wechat_income_if_missing - repair_wechat_income_if_missing( - Decimal(str(order.jine)), - getattr(order, 'club_id', None), - biz_ref=f'cz:{out_trade_no}', - ) - except Exception as exc: - logger.error('考核回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) - return self.wechat_response('SUCCESS', 'OK') - return self.wechat_response('FAIL', '订单状态异常') - - # 验证金额 - wechat_yuan = total_fee / 100 - if abs(wechat_yuan - float(order.jine)) > 0.01: - logger.error(f"金额不一致: 订单金额={order.jine}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}") - return self.wechat_response('FAIL', '金额不一致') - logger.info(f"金额验证通过: {wechat_yuan}元") - - # 调用统一履约函数(事务 + 行锁 + 幂等 + 审核记录 + 收支记录) - from jituan.services.czjilu_fulfill import fulfill_kaohe, CzjiluFulfillError - try: - fulfill_kaohe(out_trade_no, paid_amount_yuan=wechat_yuan) - except CzjiluFulfillError as exc: - logger.error('考核回调履约失败 %s: %s', out_trade_no, exc) - return self.wechat_response('FAIL', str(exc)) - - logger.info(f"考核支付回调处理完成,订单号: {out_trade_no}") - return self.wechat_response('SUCCESS', 'OK') - - except ET.ParseError as e: - logger.error(f"XML解析失败: {str(e)}, 原始数据: {request.body.decode('utf-8')}") - return self.wechat_response('FAIL', '数据格式错误') - except Exception as e: - logger.error(f"考核支付回调异常: {str(e)}", exc_info=True) - return self.wechat_response('FAIL', '处理异常') - - def verify_signature(self, xml_data): - """验证微信支付签名""" - try: - root = ET.fromstring(xml_data) - sign_node = root.find('sign') - if sign_node is None or sign_node.text is None: - logger.error("微信回调XML缺少sign节点") - return False - sign = sign_node.text - - params = {} - for child in root: - if child.tag != 'sign' and child.text is not None: - params[child.tag] = child.text - - sorted_params = sorted(params.items(), key=lambda x: x[0]) - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' - calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - result = sign == calculated_sign - if not result: - logger.error(f"签名验证失败: 原签名={sign}, 计算签名={calculated_sign}") - return result - except Exception as e: - logger.error(f"签名验证异常: {str(e)}", exc_info=True) - return False - - def wechat_response(self, return_code, return_msg): - """构造微信回调响应XML""" - xml = f""" - - -""" - logger.info(f"返回微信响应: {xml}") - return HttpResponse(xml, content_type='application/xml') - - -class KaohePayPollView(APIView): - """考核支付轮询""" - permission_classes = [IsAuthenticated] - - def post(self, request): - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({'code': 400, 'message': '订单ID不能为空'}) - try: - order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5) - # 订单未支付时,尝试补偿履约(回调延迟/失败时通过轮询兜底) - if order.zhuangtai == 9: - from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError - try: - confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=5) - order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5) - except CzjiluFulfillError as exc: - logger.info(f"考核确认未就绪 {dingdanid}: {exc}") - return Response({'code': 400, 'message': '订单尚未支付'}) - - if order.zhuangtai == 3: - return Response({'code': 200, 'message': '支付成功'}) - return Response({'code': 400, 'message': '订单尚未支付'}) - except Czjilu.DoesNotExist: - return Response({'code': 400, 'message': '订单不存在'}) - - -class KaohePayFailView(APIView): - """考核支付失败处理""" - permission_classes = [IsAuthenticated] - - def post(self, request): - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({'code': 400, 'message': '订单ID不能为空'}) - try: - order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5) - if order.zhuangtai == 9: - order.delete() - return Response({'code': 200, 'message': '订单已删除'}) - return Response({'code': 400, 'message': '订单状态无法删除'}) - except Czjilu.DoesNotExist: - return Response({'code': 400, 'message': '订单不存在'}) +"""users.views.pay - auto-generated by split script.""" +import os +import sys +import json +import re +import time +import uuid +import random +import string +import requests +import hashlib +import traceback +import decimal +import jwt +import ipaddress +import xmltodict +from datetime import datetime, timedelta +from decimal import Decimal +import defusedxml.ElementTree as ET + +from django.conf import settings +from django.db import models, transaction, IntegrityError, DatabaseError, connection +from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch +from django.core.cache import cache +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from django.http import HttpResponse +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.hashers import make_password + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import permissions, status +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.parsers import MultiPartParser, FormParser, JSONParser +from rest_framework.throttling import AnonRateThrottle +from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework_simplejwt.authentication import JWTAuthentication + +from gvsdsdk.fluent import db, func, FQ + +from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss +from utils.money import yuan_to_fen +from utils.fadan_utils import check_fadan_qiangdan_eligible +from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode +from users.fadan_fenhong_utils import process_fadan_fenhong + +from orders.utils import ( + update_daily_payout, + settle_shangjia_order_guanshi_fenhong +) +from backend.utils import ( + update_dashou_daily_by_action, update_guanshi_daily_by_action, + update_shangjia_daily, update_zuzhang_daily_by_action, + verify_kefu_permission +) +from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan + +from ..models import ( + UserBoss, UserDashou, UserShangjia, UserGuanshi, + UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser, + TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu, + RankingRecord +) +from users.business_models import User +from ..tixian_shenhe_services import ( + load_audit_meta_map, refund_balance, sync_audit_and_jilu_status, + mark_transfer_success, release_collect_quota_for_record, + close_audit_wechat_failed, query_wechat_transfer_bill, + check_shijidaozhang_within_limit, + check_dashou_trial_blocks_commission_withdraw, + WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING, +) +from ..tixian_shenhe_views import process_audit_collect + +from orders.models import ( + Order, PlatformOrderExt, MerchantOrderExt, CommissionRate, + PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage, + Penalty, PenaltyAppealImage +) +from products.models import ( + ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu +) +from config.models import Qunpeizhi +from backend.models import MerchantDailyStats +from rank.models import ( + KaohePayTemp, Chenghao, ShenheJilu, + KaoheCishuFeiyong, YonghuChenghao +) +from users.models import UserShenheguan +from products.utils import update_shangpin_daily_stat +from shop.utils import update_dianpu_daily_stat +from rank.utils import create_shenhe_jilu_from_temp + +import logging + +logger = logging.getLogger(__name__) + +class FaKuanPayView(APIView): + """ + 罚款微信支付下单接口(完全照抄 YajinGoumai) + 路径:POST /yonghu/fkjn + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + fadan_id = request.data.get('fadan_id') + if not fadan_id: + return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) + + try: + fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.UserUID) + except Penalty.DoesNotExist: + return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND) + + if fadan.Status not in [1, 3]: + return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST) + + jine = float(fadan.FineAmount) + + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + + from jituan.services.club_penalty import penalty_cz_club_id + from jituan.services.club_user import get_payment_openid + from products.czjilu_types import CZJILU_LEIXING_FAKUAN, penalty_cz_shuoming + + chongzhi_jilu = Czjilu.query.create( + dingdan_id=dingdanid, + zhuangtai=9, + yonghuid=request.user.UserUID, + jine=jine, + leixing=CZJILU_LEIXING_FAKUAN, + shuoming=penalty_cz_shuoming(fadan.id), + club_id=penalty_cz_club_id(fadan), + ) + + # 生成支付参数(支持支付宝/微信) + # 支付必须用「当前小程序/打手写入俱乐部」的 AppID+openid; + # 与罚单 ClubID(申请人俱乐部)混用会导致部分人 openid/appid 不一致而下单失败。 + from jituan.services.club_write import resolve_club_id_for_write + pay_club = resolve_club_id_for_write(request, request.user) + pay_method = (request.data.get('pay_method') or request.data.get('payMethod') or 'wechat').lower() + if pay_method == 'alipay': + try: + from jituan.services.alipay_pay import generate_alipay_czjilu_pay_params + pay_params = generate_alipay_czjilu_pay_params( + dingdanid=dingdanid, + jine=jine, + subject=settings.FAKUAN_PAY_DESCRIPTION, + club_id=pay_club, + ) + except Exception as e: + chongzhi_jilu.delete() + logger.error(f"生成支付宝支付参数失败: {str(e)}") + return Response({ + 'code': 500, + 'message': f'支付参数生成失败: {str(e)}', + 'msg': f'支付参数生成失败: {str(e)}', + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + else: + pay_openid, pay_club = get_payment_openid(request, club_id=pay_club) + if not pay_openid: + chongzhi_jilu.delete() + return Response({ + 'code': 400, + 'message': '当前小程序未绑定微信openid,请退出后重新登录再支付', + 'msg': '当前小程序未绑定微信openid,请退出后重新登录再支付', + }, status=status.HTTP_400_BAD_REQUEST) + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=jine, + openid=pay_openid, + pay_type='fakuan', + club_id=pay_club, + ) + except Exception as e: + chongzhi_jilu.delete() + logger.error(f"生成微信支付参数失败: {str(e)}") + return Response({ + 'code': 500, + 'message': f'支付参数生成失败: {str(e)}', + 'msg': f'支付参数生成失败: {str(e)}', + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'msg': '支付参数生成成功', + 'payParams': pay_params, + 'dingdanid': dingdanid + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"罚款支付下单接口异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误,请稍后重试', + 'msg': '服务器内部错误,请稍后重试', + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type, club_id=None): + from jituan.services.club_write import resolve_club_id_for_write + from jituan.services.wechat_pay import ( + get_wechat_v2_config, + log_wechat_v2_pay_diag, + parse_wechat_v2_unifiedorder_response, + ) + pay_club = club_id or resolve_club_id_for_write(self.request, self.request.user) + pay_cfg = get_wechat_v2_config(pay_club) + uid = getattr(self.request.user, 'UserUID', '') + log_wechat_v2_pay_diag( + pay_cfg, scene='fakuan', openid=openid, order_hint=f'{dingdanid} uid={uid}', + ) + APPID = pay_cfg['appid'] + MCHID = pay_cfg['mch_id'] + KEY = pay_cfg['key'] + if not APPID or not MCHID or not KEY: + raise Exception( + f'俱乐部{pay_club}微信支付配置不完整(appid/mch_id/密钥),请联系管理员' + ) + + if pay_type == 'fakuan': + PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION + NOTIFY_URL = settings.FAKUAN_PAY_NOTIFY_URL # 回调地址在这里 + else: + raise Exception('未知的支付类型') + + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + total_fee = yuan_to_fen(jine) + trade_type = 'JSAPI' + user_ip = self.get_client_ip() + + # 第一次签名 + sign_data = { + 'appid': APPID, + 'mch_id': MCHID, + 'nonce_str': nonce_str, + 'body': PAY_DESCRIPTION, + 'out_trade_no': dingdanid, + 'total_fee': total_fee, + 'spbill_create_ip': user_ip, + 'notify_url': NOTIFY_URL, # 关键:把回调地址传入 + 'trade_type': trade_type, + 'openid': openid + } + + sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) + sign_string += f'&key={KEY}' + sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + # 构建 XML,这里必须包含 notify_url + xml_data = f""" +{APPID} +{PAY_DESCRIPTION} +{MCHID} +{nonce_str} +{NOTIFY_URL} +{openid} +{dingdanid} +{user_ip} +{total_fee} +{trade_type} +{sign} +""" + + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 + ) + + logger.info(f"罚款微信支付响应: {response.text}") + + parsed = parse_wechat_v2_unifiedorder_response( + response.content, log_prefix=f'FAKUAN_UNIFIED club={pay_club}', + ) + prepay_id = parsed['prepay_id'] + timestamp = str(int(time.time())) + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + package = f'prepay_id={prepay_id}' + sign_type = 'MD5' + + pay_sign_data = { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type + } + + sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) + pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) + pay_sign_string += f'&key={KEY}' + pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() + + return { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type, + 'paySign': pay_sign + } + + def get_client_ip(self): + request = self.request + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip if ip else '127.0.0.1' + + +class FaKuanHuitiaoView(View): + + """ + 罚款支付回调接口(严格借鉴 YajinHuitiao 接口,参数一个不少) + 路径:POST /yonghu/fk_huitiao + """ + def post(self, request): + try: + # 1. 解析微信回调XML数据 + request_body = request.body + logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}") + + root = ET.fromstring(request_body) + + # 获取关键字段 + return_code = root.find('return_code').text + result_code = root.find('result_code').text + out_trade_no = root.find('out_trade_no').text # 订单ID + transaction_id = root.find('transaction_id').text # 微信交易号 + total_fee = int(root.find('total_fee').text) # 支付金额(分) + + logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") + + # 2. 验证基本返回状态 + if return_code != 'SUCCESS' or result_code != 'SUCCESS': + logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}") + return self.wechat_response('FAIL', '支付失败') + + # 3. 验证签名(确保是微信发来的) + from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature + notify_club = club_id_from_czjilu(out_trade_no) + if not verify_wechat_v2_signature(request_body, notify_club): + logger.error(f"签名验证失败: {out_trade_no}") + return self.wechat_response('FAIL', '签名验证失败') + + # 4. 查询订单 + try: + order = Czjilu.query.get(dingdan_id=out_trade_no) + logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") + except Czjilu.DoesNotExist: + logger.error(f"订单不存在: {out_trade_no}") + return self.wechat_response('FAIL', '订单不存在') + + from products.czjilu_types import is_penalty_czjilu + if not is_penalty_czjilu(order): + logger.error(f"非罚款支付订单误入罚款回调: {out_trade_no}, leixing={order.leixing}") + return self.wechat_response('FAIL', '订单类型错误') + + # 5. 检查订单状态是否为未支付(9) + if order.zhuangtai != 9: + logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None), + biz_ref=f'cz:{out_trade_no}', + ) + except Exception as exc: + logger.error('罚款回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) + return self.wechat_response('SUCCESS', 'OK') + + # 6. 验证金额(微信返回的是分,需要转换) + wechat_amount_yuan = total_fee / 100 # 转换为元 + order_amount_yuan = float(order.jine) # 订单金额(元) + + # 允许1分钱的误差(浮点数比较) + if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: + logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") + return self.wechat_response('FAIL', '金额不一致') + + # 7. 调用统一履约函数(事务 + 行锁 + 幂等 + 罚单状态 + 分红 + 收支记录) + from jituan.services.czjilu_fulfill import fulfill_fakuan, CzjiluFulfillError + try: + fulfill_fakuan(out_trade_no, paid_amount_yuan=wechat_amount_yuan) + except CzjiluFulfillError as exc: + logger.error('罚款回调履约失败 %s: %s', out_trade_no, exc) + return self.wechat_response('FAIL', str(exc)) + + # 8. 返回成功响应给微信 + logger.info(f"罚款支付回调处理完成: {out_trade_no}") + return self.wechat_response('SUCCESS', 'OK') + + except ET.ParseError as e: + logger.error(f"XML解析失败: {str(e)}") + return self.wechat_response('FAIL', '数据格式错误') + except Exception as e: + logger.error(f"罚款支付回调处理异常: {str(e)}", exc_info=True) + # 返回FAIL让微信重试 + return self.wechat_response('FAIL', '处理异常') + + def verify_signature(self, xml_data): + """验证微信支付签名(完全照搬 YajinHuitiao 的 verify_signature)""" + try: + root = ET.fromstring(xml_data) + sign = root.find('sign').text + + if not sign: + logger.error("签名字段不存在") + return False + + # 获取所有参数并排除sign + params = {} + for child in root: + if child.tag != 'sign' and child.text is not None: + params[child.tag] = child.text + + # 按照参数名ASCII码从小到大排序 + sorted_params = sorted(params.items(), key=lambda x: x[0]) + + # 拼接字符串 + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) + sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + + # 计算签名 + calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + is_valid = sign == calculated_sign + if not is_valid: + logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") + + return is_valid + + except Exception as e: + logger.error(f"验证签名异常: {str(e)}") + return False + + def wechat_response(self, return_code, return_msg): + """返回微信支付回调响应(完全照搬 YajinHuitiao 的 wechat_response)""" + xml_response = f""" + + +""" + + logger.info(f"返回微信响应: {return_code} - {return_msg}") + return HttpResponse(xml_response, content_type='application/xml') + + +class FaKuanPayPollView(APIView): + """罚款支付轮询接口(借鉴 YajinLunxun)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 400, 'message': '订单ID不能为空'}) + + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID) + # 订单未支付时,尝试补偿履约(回调延迟/失败时通过轮询兜底) + if order.zhuangtai == 9: + from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError + try: + confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=6) + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID) + except CzjiluFulfillError as exc: + logger.info(f"罚款确认未就绪 {dingdanid}: {exc}") + return Response({'code': 400, 'message': '订单尚未支付'}) + + if order.zhuangtai == 3: + return Response({'code': 200, 'message': '支付成功'}) + else: + return Response({'code': 400, 'message': '订单尚未支付'}) + except Czjilu.DoesNotExist: + return Response({'code': 400, 'message': '订单不存在'}) + except Exception as e: + logger.error(f"轮询异常: {str(e)}") + return Response({'code': 500, 'message': '服务器内部错误'}) + + +class FaKuanPayFailView(APIView): + """罚款支付失败处理接口(借鉴 YajinShibai)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 400, 'message': '订单ID不能为空'}) + + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID) + if order.zhuangtai == 9: # 未支付 + order.delete() + logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}") + return Response({'code': 200, 'message': '订单已删除'}) + else: + return Response({'code': 400, 'message': '订单状态不是未支付,无法删除'}) + except Czjilu.DoesNotExist: + return Response({'code': 400, 'message': '订单不存在'}) + except Exception as e: + logger.error(f"支付失败处理异常: {str(e)}") + return Response({'code': 500, 'message': '服务器内部错误'}) + + +class KaohePayView(APIView): + """ + 考核支付下单接口 + 路径:POST /yonghu/khzf + 参数: + tag_id: 标签ID + game_nick: 游戏昵称 + remark: 备注 + reapply: 是否再次申请 (true/false) + jilu_id: 再次申请时的原记录ID(reapply=true时必传) + shenheguan_id: 指定考核官ID(可选) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + tag_id = request.data.get('tag_id') + game_nick = request.data.get('game_nick', '').strip() + remark = request.data.get('remark', '').strip() + reapply = request.data.get('reapply', False) + jilu_id = request.data.get('jilu_id') if reapply else None + shenheguan_id = request.data.get('shenheguan_id', '').strip() + + logger.info(f"用户 {user.UserUID} 发起考核支付申请,标签ID: {tag_id}, 再次申请: {reapply}") + + if not tag_id or not game_nick: + logger.warning(f"用户 {user.UserUID} 参数不完整: tag_id={tag_id}, game_nick={game_nick}") + return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'}) + + # 1. 检查是否有未支付的考核订单 + """unpaid_order = Czjilu.query.filter( + UserUID=user.UserUID, + leixing=5, + zhuangtai=9 + ).exists() + if unpaid_order: + logger.warning(f"用户 {user.UserUID} 存在未支付的考核订单") + return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})""" + + # 2. 验证标签 + try: + chenghao = Chenghao.query.get(id=tag_id) + logger.info(f"标签验证通过: {chenghao.mingcheng}") + except Chenghao.DoesNotExist: + logger.error(f"标签不存在: tag_id={tag_id}") + return Response({'code': 404, 'message': '标签不存在'}) + + # 3. 检查用户是否已拥有该标签 + if YonghuChenghao.query.filter(yonghu=user, chenghao=chenghao).exists(): + logger.warning(f"用户 {user.UserUID} 已拥有标签 {chenghao.mingcheng}") + return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'}) + + # 4. 检查是否有未完成的申请 + if ShenheJilu.query.filter( + shenqingren_id=user.UserUID, + chenghao=chenghao, + zhuangtai__in=[0, 3] # 审核中、待审核 + ).exists(): + logger.warning(f"用户 {user.UserUID} 有未完成的考核申请") + return Response({'code': 400, 'message': '您有未完成的考核申请,请等待结果'}) + + # 5. 计算考核次数和费用 + if reapply and jilu_id: + last = ShenheJilu.query.filter( + jilu_id=jilu_id, + shenqingren_id=user.UserUID, + zhuangtai=2 + ).first() + if not last: + logger.warning(f"用户 {user.UserUID} 再次申请失败:原记录不存在或状态不是未通过,jilu_id={jilu_id}") + return Response({'code': 400, 'message': '原记录不存在或状态不是未通过'}) + new_cishu = last.cishu + 1 + logger.info(f"再次申请,原记录ID: {jilu_id},新次数: {new_cishu}") + else: + max_cishu = ShenheJilu.query.filter( + shenqingren_id=user.UserUID, + chenghao=chenghao, + zhuangtai__in=[1, 2] + ).aggregate(max=models.Max('cishu'))['max'] or 0 + new_cishu = max_cishu + 1 + logger.info(f"首次申请,计算次数: {new_cishu}") + + # 6. 获取本次费用 + fee = get_tag_fee(chenghao, new_cishu) + logger.info(f"本次考核费用: {fee}元,次数: {new_cishu}") + + if fee <= 0: + logger.warning(f"费用为0,应使用免费申请接口,tag_id={tag_id}, cishu={new_cishu}") + return Response({'code': 400, 'message': '本次费用为0,请直接使用免费申请接口'}) + + # 7. 生成订单ID + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + dingdanid = f"KH{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + logger.info(f"生成订单ID: {dingdanid}") + + # 8. 创建支付订单(Czjilu) + from jituan.services.club_write import resolve_club_id_for_write + order = Czjilu.query.create( + dingdan_id=dingdanid, + zhuangtai=9, # 未支付 + yonghuid=user.UserUID, + jine=float(fee), + leixing=5, # 5-考核支付 + shuoming=settings.KAOHE_PAY_DESCRIPTION, + club_id=resolve_club_id_for_write(request, user), + ) + logger.info(f"创建支付订单成功: {dingdanid}") + + # 9. 保存考核支付临时上下文(用于回调时取业务参数) + try: + KaohePayTemp.query.create( + dingdan_id=dingdanid, + yonghuid=user.UserUID, + tag_id=tag_id, + game_nick=game_nick, + remark=remark, + reapply=reapply, + jilu_id=jilu_id if reapply else None, + shenheguan_id=shenheguan_id, + cishu=new_cishu, + fee=fee, + ) + logger.info(f"保存临时上下文成功: {dingdanid}") + except Exception as e: + logger.error(f"保存临时上下文失败: {str(e)}", exc_info=True) + # 临时表保存失败应该回滚订单 + order.delete() + return Response({'code': 500, 'message': '系统异常,请稍后重试'}) + + # 10. 构造 attach 参数(只传订单ID,确保不超127字节) + attach_str = json.dumps({'oid': dingdanid}, ensure_ascii=False) + logger.info(f"attach内容: {attach_str},长度: {len(attach_str.encode('utf-8'))}字节") + + # 11. 生成支付参数(支持支付宝/微信) + pay_method = (request.data.get('pay_method') or request.data.get('payMethod') or 'wechat').lower() + if pay_method == 'alipay': + try: + from jituan.services.alipay_pay import generate_alipay_czjilu_pay_params + pay_params = generate_alipay_czjilu_pay_params( + dingdanid=dingdanid, + jine=float(fee), + subject=settings.KAOHE_PAY_DESCRIPTION, + club_id=resolve_club_id_for_write(request, user), + ) + logger.info(f"支付宝支付参数生成成功: {dingdanid}") + except Exception as e: + order.delete() + try: + KaohePayTemp.query.filter(dingdan_id=dingdanid).delete() + except Exception as del_e: + logger.error(f"删除临时表记录失败: {str(del_e)}") + logger.error(f"生成支付宝支付参数失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'}) + else: + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=float(fee), + openid=user.OpenID, + attach=attach_str, + pay_type='kaohe' + ) + logger.info(f"微信支付参数生成成功: {dingdanid}") + except Exception as e: + order.delete() # 下单失败,删除订单记录 + # 同时删除临时表记录 + try: + KaohePayTemp.query.filter(dingdan_id=dingdanid).delete() + except Exception as del_e: + logger.error(f"删除临时表记录失败: {str(del_e)}") + logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'}) + + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'payParams': pay_params, + 'dingdanid': dingdanid + }) + + except Exception as e: + logger.error(f"考核支付下单异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'message': '服务器内部错误'}) + + def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type): + """生成微信JSAPI支付参数""" + from jituan.services.club_write import resolve_club_id_for_write + from jituan.services.wechat_pay import get_wechat_v2_config + pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user)) + APPID = pay_cfg['appid'] + MCHID = pay_cfg['mch_id'] + KEY = pay_cfg['key'] + + if pay_type == 'kaohe': + PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION + NOTIFY_URL = settings.KAOHE_PAY_NOTIFY_URL + else: + raise Exception('未知支付类型') + + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + total_fee = yuan_to_fen(jine) + trade_type = 'JSAPI' + user_ip = self.get_client_ip() + + logger.info(f"微信支付下单参数: out_trade_no={dingdanid}, total_fee={total_fee}, openid={openid}, ip={user_ip}") + + # 第一次签名(统一下单) + sign_data = { + 'appid': APPID, + 'mch_id': MCHID, + 'nonce_str': nonce_str, + 'body': PAY_DESCRIPTION, + 'out_trade_no': dingdanid, + 'total_fee': total_fee, + 'spbill_create_ip': user_ip, + 'notify_url': NOTIFY_URL, + 'trade_type': trade_type, + 'openid': openid, + 'attach': attach, + } + + sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) + sign_string += f'&key={KEY}' + sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + # 构建XML请求体 + xml_data = f""" +{APPID} +{PAY_DESCRIPTION} +{MCHID} +{nonce_str} +{NOTIFY_URL} +{openid} +{dingdanid} +{user_ip} +{total_fee} +{trade_type} + +{sign} +""" + + logger.info(f"微信支付请求XML: {xml_data[:500]}...") # 只打印前500字符 + + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 + ) + + logger.info(f"微信支付响应: {response.text}") + + root = ET.fromstring(response.content) + + # 安全获取return_code + return_code_node = root.find('return_code') + if return_code_node is None or return_code_node.text is None: + logger.error(f"微信返回缺少return_code节点,原始内容: {response.text}") + raise Exception('微信支付接口异常,缺少return_code') + return_code = return_code_node.text + + # 安全获取result_code + result_code_node = root.find('result_code') + if result_code_node is None or result_code_node.text is None: + logger.error(f"微信返回缺少result_code节点,原始内容: {response.text}") + raise Exception('微信支付接口异常,缺少result_code') + result_code = result_code_node.text + + if return_code == 'SUCCESS' and result_code == 'SUCCESS': + prepay_id_node = root.find('prepay_id') + if prepay_id_node is None or prepay_id_node.text is None: + logger.error(f"统一下单成功但未返回prepay_id,原始内容: {response.text}") + raise Exception('微信支付接口异常,缺少prepay_id') + prepay_id = prepay_id_node.text + + timestamp = str(int(time.time())) + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + package = f'prepay_id={prepay_id}' + sign_type = 'MD5' + + # 第二次签名(前端调起支付用) + pay_sign_data = { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type, + } + sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) + pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) + pay_sign_string += f'&key={KEY}' + pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() + + logger.info(f"微信支付预下单成功,prepay_id={prepay_id}") + + return { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type, + 'paySign': pay_sign, + } + else: + # 安全获取错误信息 + err_code_node = root.find('err_code') + err_code = err_code_node.text if err_code_node is not None else '未知错误码' + err_code_des_node = root.find('err_code_des') + err_code_des = err_code_des_node.text if err_code_des_node is not None else '未知错误描述' + error_details = f"return_code: {return_code}, result_code: {result_code}, err_code: {err_code}, err_code_des: {err_code_des}" + logger.error(f"微信支付下单失败: {error_details}") + raise Exception(f"微信支付下单失败: {error_details}") + + def get_client_ip(self): + """获取客户端IP""" + request = self.request + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR', '127.0.0.1') + return ip if ip else '127.0.0.1' + + +class KaohePayCallbackView(View): + """ + 考核支付回调接口 + 路径:/yonghu/kh_huitiao + """ + + def post(self, request): + try: + request_body = request.body + logger.info(f"收到考核支付回调: {request_body.decode('utf-8')}") + + root = ET.fromstring(request_body) + + # 安全获取return_code + return_code_node = root.find('return_code') + if return_code_node is None or return_code_node.text is None: + logger.error(f"微信回调XML缺少return_code节点,原始内容: {request_body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + return_code = return_code_node.text + + # 安全获取result_code + result_code_node = root.find('result_code') + if result_code_node is None or result_code_node.text is None: + logger.error(f"微信回调XML缺少result_code节点,原始内容: {request_body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + result_code = result_code_node.text + + # 安全获取out_trade_no + out_trade_no_node = root.find('out_trade_no') + if out_trade_no_node is None or out_trade_no_node.text is None: + logger.error(f"微信回调XML缺少out_trade_no节点,原始内容: {request_body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + out_trade_no = out_trade_no_node.text + + # 安全获取transaction_id + transaction_id_node = root.find('transaction_id') + if transaction_id_node is None or transaction_id_node.text is None: + logger.error(f"微信回调XML缺少transaction_id节点,原始内容: {request_body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + transaction_id = transaction_id_node.text + + # 安全获取total_fee + total_fee_node = root.find('total_fee') + if total_fee_node is None or total_fee_node.text is None: + logger.error(f"微信回调XML缺少total_fee节点,原始内容: {request_body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + total_fee = int(total_fee_node.text) + + logger.info( + f"回调参数解析成功: out_trade_no={out_trade_no}, transaction_id={transaction_id}, total_fee={total_fee}") + + if return_code != 'SUCCESS' or result_code != 'SUCCESS': + logger.warning(f"支付状态异常: return_code={return_code}, result_code={result_code}") + return self.wechat_response('FAIL', '支付失败') + + # 验证签名 + from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature + notify_club = club_id_from_czjilu(out_trade_no) + if not verify_wechat_v2_signature(request_body, notify_club): + logger.error(f"签名验证失败,订单号: {out_trade_no}") + return self.wechat_response('FAIL', '签名验证失败') + logger.info(f"签名验证成功,订单号: {out_trade_no}") + + # 查找订单 + try: + order = Czjilu.query.get(dingdan_id=out_trade_no, leixing=5) + logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.zhuangtai}") + except Czjilu.DoesNotExist: + logger.error(f"考核订单不存在: {out_trade_no}") + return self.wechat_response('FAIL', '订单不存在') + + if order.zhuangtai != 9: + logger.warning(f"订单状态非未支付: {order.zhuangtai}, 订单号: {out_trade_no}") + if order.zhuangtai == 3: + logger.info(f"订单已支付,补试收支记账: {out_trade_no}") + try: + from jituan.services.szjilu_accounting import repair_wechat_income_if_missing + repair_wechat_income_if_missing( + Decimal(str(order.jine)), + getattr(order, 'club_id', None), + biz_ref=f'cz:{out_trade_no}', + ) + except Exception as exc: + logger.error('考核回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True) + return self.wechat_response('SUCCESS', 'OK') + return self.wechat_response('FAIL', '订单状态异常') + + # 验证金额 + wechat_yuan = total_fee / 100 + if abs(wechat_yuan - float(order.jine)) > 0.01: + logger.error(f"金额不一致: 订单金额={order.jine}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}") + return self.wechat_response('FAIL', '金额不一致') + logger.info(f"金额验证通过: {wechat_yuan}元") + + # 调用统一履约函数(事务 + 行锁 + 幂等 + 审核记录 + 收支记录) + from jituan.services.czjilu_fulfill import fulfill_kaohe, CzjiluFulfillError + try: + fulfill_kaohe(out_trade_no, paid_amount_yuan=wechat_yuan) + except CzjiluFulfillError as exc: + logger.error('考核回调履约失败 %s: %s', out_trade_no, exc) + return self.wechat_response('FAIL', str(exc)) + + logger.info(f"考核支付回调处理完成,订单号: {out_trade_no}") + return self.wechat_response('SUCCESS', 'OK') + + except ET.ParseError as e: + logger.error(f"XML解析失败: {str(e)}, 原始数据: {request.body.decode('utf-8')}") + return self.wechat_response('FAIL', '数据格式错误') + except Exception as e: + logger.error(f"考核支付回调异常: {str(e)}", exc_info=True) + return self.wechat_response('FAIL', '处理异常') + + def verify_signature(self, xml_data): + """验证微信支付签名""" + try: + root = ET.fromstring(xml_data) + sign_node = root.find('sign') + if sign_node is None or sign_node.text is None: + logger.error("微信回调XML缺少sign节点") + return False + sign = sign_node.text + + params = {} + for child in root: + if child.tag != 'sign' and child.text is not None: + params[child.tag] = child.text + + sorted_params = sorted(params.items(), key=lambda x: x[0]) + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) + sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + result = sign == calculated_sign + if not result: + logger.error(f"签名验证失败: 原签名={sign}, 计算签名={calculated_sign}") + return result + except Exception as e: + logger.error(f"签名验证异常: {str(e)}", exc_info=True) + return False + + def wechat_response(self, return_code, return_msg): + """构造微信回调响应XML""" + xml = f""" + + +""" + logger.info(f"返回微信响应: {xml}") + return HttpResponse(xml, content_type='application/xml') + + +class KaohePayPollView(APIView): + """考核支付轮询""" + permission_classes = [IsAuthenticated] + + def post(self, request): + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 400, 'message': '订单ID不能为空'}) + try: + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5) + # 订单未支付时,尝试补偿履约(回调延迟/失败时通过轮询兜底) + if order.zhuangtai == 9: + from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError + try: + confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=5) + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5) + except CzjiluFulfillError as exc: + logger.info(f"考核确认未就绪 {dingdanid}: {exc}") + return Response({'code': 400, 'message': '订单尚未支付'}) + + if order.zhuangtai == 3: + return Response({'code': 200, 'message': '支付成功'}) + return Response({'code': 400, 'message': '订单尚未支付'}) + except Czjilu.DoesNotExist: + return Response({'code': 400, 'message': '订单不存在'}) + + +class KaohePayFailView(APIView): + """考核支付失败处理""" + permission_classes = [IsAuthenticated] + + def post(self, request): + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({'code': 400, 'message': '订单ID不能为空'}) + try: + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5) + if order.zhuangtai == 9: + order.delete() + return Response({'code': 200, 'message': '订单已删除'}) + return Response({'code': 400, 'message': '订单状态无法删除'}) + except Czjilu.DoesNotExist: + return Response({'code': 400, 'message': '订单不存在'})