"""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_openid, _ = get_payment_openid(request)
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. 更新订单状态为已支付(3)
order.zhuangtai = 3
order.save(update_fields=['zhuangtai'])
logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3")
# 8. 更新罚单状态为已缴纳(2)
try:
fadan = None
m = re.search(r'#fadan:(\d+)', order.shuoming or '')
if m:
fadan = Penalty.query.filter(
id=int(m.group(1)),
PenalizedUserID=order.yonghuid,
Status__in=[1, 3],
).first()
if not fadan:
fadan = Penalty.query.filter(
PenalizedUserID=order.yonghuid,
FineAmount=order.jine,
Status__in=[1, 3],
).order_by('-CreateTime').first()
if fadan:
fadan.Status = 2 # 已缴纳
fadan.save(update_fields=['Status'])
logger.info(f"罚单 {fadan.id} 已自动标记为已缴纳")
success, msg = process_fadan_fenhong(fadan.id)
if not success:
logger.error(f"罚单 {fadan.id} 分红处理失败: {msg}")
else:
logger.info(f"罚单 {fadan.id} 分红处理完成: {msg}")
except Exception as e:
logger.error(f"更新罚单状态失败: {str(e)}", exc_info=True)
# 9. 更新收支记录表
try:
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
jine_decimal,
getattr(order, 'club_id', None),
biz_ref=f'cz:{order.dingdan_id}',
)
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元")
except Exception as e:
# 收支记录更新失败不影响主流程,只记录日志
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
# 10. 返回成功响应给微信
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 == 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}元")
# 更新订单状态为已支付
order.zhuangtai = 3
order.save(update_fields=['zhuangtai'])
logger.info(f"订单状态更新为已支付: {out_trade_no}")
# 从临时表获取业务参数并创建审核记录
try:
temp = KaohePayTemp.query.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid)
logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}")
user = User.query.get(UserUID=order.yonghuid)
logger.info(f"获取用户信息成功: {user.UserUID}")
success, msg, record = create_shenhe_jilu_from_temp(user, temp)
if success:
logger.info(f"创建审核记录成功: jilu_id={record.jilu_id}, 订单号: {out_trade_no}")
else:
logger.error(f"创建审核记录失败: {msg}, 订单号: {out_trade_no}")
# 订单已支付但记录创建失败,需要标记订单异常,人工介入
order.zhuangtai = 10 # 10表示支付成功但业务处理失败
order.save(update_fields=['zhuangtai'])
logger.error(f"订单状态标记为异常(10): {out_trade_no}")
except KaohePayTemp.DoesNotExist:
logger.error(f"临时表无记录,订单号: {out_trade_no}, 用户ID: {order.yonghuid}")
# 尝试从attach解析(兼容旧数据)
attach_element = root.find('attach')
if attach_element is not None and attach_element.text:
try:
attach_data = json.loads(attach_element.text)
logger.info(f"从attach解析数据成功: {attach_data}")
# 这里可以保留旧逻辑,但建议只记录日志
except Exception as e:
logger.error(f"解析attach失败: {str(e)}")
except Exception as e:
logger.error(f"处理临时表或创建审核记录异常: {str(e)}", exc_info=True)
# 标记订单异常
order.zhuangtai = 10
order.save(update_fields=['zhuangtai'])
logger.error(f"订单状态标记为异常(10): {out_trade_no}")
# 更新收支记录
try:
jine_decimal = Decimal(str(order.jine))
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
jine_decimal,
getattr(order, 'club_id', None),
biz_ref=f'cz:{order.dingdan_id}',
)
logger.info(f"更新收支记录成功: {jine_decimal}元")
except Exception as e:
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
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 == 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': '订单不存在'})