Files
Django/products/views/huiyuan_payment.py

687 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""会员购买支付流程视图。
包含会员购买参数生成、支付回调、失败处理、轮询。
"""
# ==================== 标准库 ====================
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"""<xml>
<appid>{APPID}</appid>
<body>{PAY_DESCRIPTION}</body>
<mch_id>{MCHID}</mch_id>
<nonce_str>{nonce_str}</nonce_str>
<notify_url>{NOTIFY_URL}</notify_url>
<openid>{openid}</openid>
<out_trade_no>{dingdanid}</out_trade_no>
<spbill_create_ip>{user_ip}</spbill_create_ip>
<total_fee>{total_fee}</total_fee>
<trade_type>{trade_type}</trade_type>
<sign>{sign}</sign>
</xml>"""
# 调用微信统一下单接口(官方地址)
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"""<xml>
<return_code><![CDATA[{return_code}]]></return_code>
<return_msg><![CDATA[{return_msg}]]></return_msg>
</xml>"""
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