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': '订单不存在'})