"""会员购买支付流程视图。
包含会员购买参数生成、支付回调、失败处理、轮询。
"""
# ==================== 标准库 ====================
import os
import json
import time
import random
import string
import uuid
import hashlib
import requests
import logging
import decimal
from datetime import timedelta
from decimal import Decimal, InvalidOperation, ROUND_CEILING
import defusedxml.ElementTree as ET
# ==================== Django 内置 ====================
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q
from gvsdsdk.fluent import db, func, FQ
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.utils import timezone
# ==================== DRF 框架 ====================
from rest_framework.views import APIView, View
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 utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import format_yuan, quantize_yuan, yuan_to_fen
from backend.utils import (
update_guanshi_daily_by_action,
update_guanshi_xufei_daily,
update_zuzhang_daily_by_action
)
from users.fadan_fenhong_utils import process_fadan_fenhong
# ==================== 各App模型 ====================
from ..models import (
ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
)
from users.models import (
AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
)
from users.business_models import User
from orders.models import CommissionRate
from config.models import Gonggao, Lunbo
from orders.models import Penalty # 用于罚款类型
from jituan.services.club_config import (
get_commission_rate,
get_commission_rate_object,
get_huiyuan_price,
get_huiyuan_fenchong,
)
from jituan.services.club_context import resolve_effective_club_id
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_gsfenhong_club_id
import traceback
# ==================== 日志实例 ====================
logger = logging.getLogger(__name__)
class HuiyuanGoumai(APIView):
"""
会员充值支付参数接口
路径:/shangpin/huiyuangm
请求方式:POST
权限:JWT认证
参数:huiyuanid(会员ID)
返回:支付参数(payParams)和订单ID(dingdanid)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取会员ID参数
huiyuanid = request.data.get('huiyuanid')
is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial'))
if not huiyuanid:
return Response({
'code': 400,
'message': '会员ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 验证用户身份和状态
try:
dashou = request.user.DashouProfile
except UserDashou.DoesNotExist:
return Response({
'code': 400,
'message': '用户不是打手,无法购买会员'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查打手账号状态
if dashou.zhanghaozhuangtai != 1:
return Response({
'code': 400,
'message': '账号状态异常,无法进行购买'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 查询会员信息
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuanid)
except Huiyuan.DoesNotExist:
return Response({
'code': 400,
'message': '会员信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
club_id = resolve_effective_club_id(request, request.user)
from jituan.services.member_recharge import validate_member_purchase
ok, err_msg, club_price, purchase_days = validate_member_purchase(
request.user.UserUID, huiyuanid, club_id, is_trial,
)
if not ok:
return Response({
'code': 400,
'message': err_msg or '不可购买',
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 生成订单ID(前缀CzHY表示会员充值)
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 6. 创建充值记录订单(金额仅用本俱乐部配置)
jine = quantize_yuan(club_price)
# 组合订单说明:会员介绍 + 配置描述
shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}"
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付状态
yonghuid=request.user.UserUID,
jine=jine,
leixing=1, # 充值类型:1=会员
shuoming=shuoming,
huiyuan_id=huiyuanid, # 存储会员ID
club_id=club_id,
is_trial=is_trial,
purchase_days=int(purchase_days),
)
logger.info(
f"创建会员充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, "
f"会员{huiyuanid}, 俱乐部{club_id}, 体验={is_trial}, "
f"金额{jine}元, 天数{purchase_days}")
# 7. 生成微信支付参数
try:
pay_openid, _ = get_payment_openid(request)
if not pay_openid:
from jituan.services.member_recharge import MEMBER_CANCELLED_STATUS
chongzhi_jilu.zhuangtai = MEMBER_CANCELLED_STATUS
chongzhi_jilu.shuoming = f"{shuoming} | 下单失败:openid不存在"[:100]
chongzhi_jilu.save(update_fields=['zhuangtai', 'shuoming'])
return Response({
'code': 400,
'message': '用户openid不存在',
'dingdanid': dingdanid,
}, status=status.HTTP_400_BAD_REQUEST)
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
openid=pay_openid,
pay_type='huiyuan'
)
except Exception as e:
from jituan.services.member_recharge import MEMBER_CANCELLED_STATUS
chongzhi_jilu.zhuangtai = MEMBER_CANCELLED_STATUS
chongzhi_jilu.shuoming = f"{shuoming} | 下单失败:{str(e)[:40]}"[:100]
chongzhi_jilu.save(update_fields=['zhuangtai', 'shuoming'])
logger.error(f"生成会员支付参数失败: {str(e)}")
return Response({
'code': 500,
'message': f'支付参数生成失败: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 8. 返回支付参数和订单ID
return Response({
'code': 200,
'message': '支付参数生成成功',
'payParams': pay_params,
'dingdanid': dingdanid,
'club_id': club_id,
'jine': format_yuan(jine),
'is_trial': is_trial,
'purchase_days': purchase_days,
}, 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):
"""生成微信支付参数(完整版,无省略)"""
# 微信支付配置 - 从settings中获取
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 == 'yajin':
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL
elif pay_type == 'jifen':
PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION
NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL
elif pay_type == 'huiyuan':
PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION
NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL
else:
raise Exception('未知的支付类型')
# 生成随机字符串
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 金额转换(元转分)
total_fee = yuan_to_fen(jine)
# 交易类型(小程序支付使用JSAPI)
trade_type = 'JSAPI'
# 获取客户端IP
user_ip = self.get_client_ip()
# 1. 第一次签名(统一下单接口)
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 # 小程序支付必须传openid
}
# 按照参数名ASCII码从小到大排序
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}'
# MD5签名
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}
"""
# 调用微信统一下单接口(官方地址)
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
timeout=10 # 设置超时
)
# 解析返回的XML
root = ET.fromstring(response.content)
return_code = root.find('return_code').text
result_code = root.find('result_code').text
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
# 获取prepay_id
prepay_id = root.find('prepay_id').text
# 2. 第二次签名(小程序支付参数)
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'
# 生成支付签名(注意:这里key的大小写和字段名与第一次不同)
pay_sign_data = {
'appId': APPID, # 注意:这里是appId,不是appid
'timeStamp': timestamp, # 注意:这里是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, # 对应前端:appId
'timeStamp': timestamp, # 对应前端:timeStamp
'nonceStr': nonce_str, # 对应前端:nonceStr
'package': package, # 对应前端:package
'signType': sign_type, # 对应前端:signType
'paySign': pay_sign # 对应前端:paySign
}
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 err_code is not None:
error_details += f", err_code: {err_code.text}"
if err_code_des is not None:
error_details += f", err_code_des: {err_code_des.text}"
if return_msg is not None:
error_details += f", return_msg: {return_msg.text}"
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')
return ip if ip else '127.0.0.1'
class HuiyuanHuitiao(View):
"""
会员支付回调接口
路径:/shangpin/huiyuanhuitiao
"""
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
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}分")
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
logger.warning(f"会员支付回调状态异常: {return_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', '签名验证失败')
# 查询订单
try:
order = Czjilu.query.get(dingdan_id=out_trade_no)
except Czjilu.DoesNotExist:
logger.error(f"会员订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
if order.leixing != 1:
logger.error(f"订单类型错误: {order.leixing}, 期望1(会员)")
return self.wechat_response('FAIL', '订单类型错误')
if order.zhuangtai != 9:
logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
if order.zhuangtai == 3:
from jituan.services.member_recharge import repair_member_entitlement_if_paid
repair_member_entitlement_if_paid(out_trade_no)
try:
from decimal import Decimal
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')
wechat_amount_yuan = total_fee / 100
order_amount_yuan = float(order.jine)
if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
return self.wechat_response('FAIL', '金额不一致')
from jituan.services.member_recharge import fulfill_member_recharge, MemberRechargeError
try:
fulfill_member_recharge(
out_trade_no,
paid_amount_yuan=wechat_amount_yuan,
source='callback',
)
except MemberRechargeError 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)}")
return self.wechat_response('FAIL', '数据格式错误')
except Exception as e:
logger.error(f"会员支付回调处理异常: {str(e)}", exc_info=True)
return self.wechat_response('FAIL', '处理异常')
# 履约逻辑见 jituan.services.member_recharge(回调与 /huiyuanlx 确认共用)
def verify_signature(self, xml_data):
"""验证微信支付签名(与原代码一致)"""
try:
root = ET.fromstring(xml_data)
sign = root.find('sign').text
if not sign:
logger.error("会员签名字段不存在")
return False
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()
return sign == calculated_sign
except Exception as e:
logger.error(f"会员验证签名异常: {str(e)}")
return False
def wechat_response(self, return_code, return_msg):
"""返回微信回调响应"""
xml_response = f"""
"""
logger.info(f"会员返回微信响应: {return_code} - {return_msg}")
return HttpResponse(xml_response, content_type='application/xml')
# views.py
class HuiyuanShibai(APIView):
"""
会员支付失败处理接口
路径:/shangpin/huiyuanshibai
请求方式:POST
权限:JWT认证
参数:dingdanid(订单ID)
返回:删除结果
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
yonghuid=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单类型是否为会员(1)
if order.leixing != 1:
return Response({
'code': 400,
'message': '订单类型错误,不是会员订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 检查订单状态是否为未支付(9)
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付会员订单: {dingdanid}")
return Response({
'code': 200,
'message': '会员订单已删除'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '会员订单状态不是未支付,无法删除'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"会员支付失败处理异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py
class HuiyuanLunxun(APIView):
"""
会员支付成功轮询接口
路径:/shangpin/huiyuanlx
请求方式:POST
权限:JWT认证
参数:dingdanid(订单ID)
返回:订单状态和会员信息
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
yonghuid=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单类型是否为会员(1)
if order.leixing != 1:
return Response({
'code': 400,
'message': '订单类型错误,不是会员订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 未支付:查微信并幂等履约(与回调互斥,zhuangtai!=9 则跳过)
if order.zhuangtai == 9:
from jituan.services.member_recharge import (
confirm_member_recharge_paid,
MemberRechargeError,
)
try:
confirm_member_recharge_paid(dingdanid, request.user.UserUID)
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
except MemberRechargeError as exc:
logger.info(f"会员确认未就绪 {dingdanid}: {exc}")
return Response({
'code': 400,
'message': str(exc),
}, status=status.HTTP_400_BAD_REQUEST)
if order.zhuangtai != 3:
return Response({
'code': 400,
'message': '会员订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
from jituan.services.member_recharge import repair_member_entitlement_if_paid
repair_member_entitlement_if_paid(dingdanid)
# 5. 查询会员信息
try:
huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id)
# 6. 查询会员购买记录
goumai_record = Huiyuangoumai.query.filter(
yonghu_id=request.user.UserUID,
huiyuan_id=order.huiyuan_id
).first()
if not goumai_record:
return Response({
'code': 400,
'message': '会员购买记录不存在'
}, status=status.HTTP_400_BAD_REQUEST)
# 7. 格式化到期时间
#from django.utils.timezone import localtime
#daoqi_time = localtime(goumai_record.daoqi_time).strftime('%Y-%m-%d %H:%M:%S')
# 直接格式化,不使用时区转换
daoqi_time = goumai_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')
current_user = request.user
dashou = current_user.DashouProfile
logger.info(f"会员轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}, 会员{order.huiyuan_id}")
# 8. 返回会员信息(用于更新前端全局变量)
return Response({
'code': 200,
'message': '支付流程全部完成',
'jifen': dashou.jifen,
'huiyuan': {
'huiyuanid': order.huiyuan_id,
'huiyuanming': huiyuan.jieshao,
'daoqi': daoqi_time
}
}, status=status.HTTP_200_OK)
except Huiyuan.DoesNotExist:
logger.error(f"会员信息不存在: {order.huiyuan_id}")
return Response({
'code': 400,
'message': '会员信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"会员轮询异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py in shangpin app