Files
Django/products/views/chongzhi_payment.py

1137 lines
49 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.
"""商家充值与充值抵扣视图。
包含商家充值支付参数/回调/失败/成功轮询,以及充值打手抵押查询与抵扣接口。
含模块级常量 INTEGRAL_PRICE / INTEGRAL_INCREMENT / MAX_INTEGRAL / IDENTITY_CONFIG。
"""
# ==================== 标准库 ====================
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 ShangjiaChongzhi(APIView):
"""
商家余额充值支付参数接口
路径:/shangpin/sjcz
请求方式POST
权限JWT认证
参数jine金额1-10000元
返回:支付参数(payParams)和订单ID(dingdanid)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 验证用户身份和商家状态
try:
shangjia = request.user.ShopProfile
except UserShangjia.DoesNotExist:
logger.warning(f"用户{request.user.UserUID}尝试充值但不是商家")
return Response({
'code': 400,
'message': '用户不是商家,无法充值'
}, status=status.HTTP_400_BAD_REQUEST)
# 检查商家状态
if shangjia.zhuangtai != 1:
logger.warning(f"商家{shangjia.nicheng}状态异常: {shangjia.zhuangtai}")
return Response({
'code': 400,
'message': '商家状态异常,无法进行充值'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取并验证金额参数
jine = request.data.get('jine')
if not jine:
return Response({
'code': 400,
'message': '请填写充值金额'
}, status=status.HTTP_400_BAD_REQUEST)
try:
jine = quantize_yuan(jine)
except (ValueError, TypeError, InvalidOperation):
return Response({
'code': 400,
'message': '金额格式错误,请填写有效的数字'
}, status=status.HTTP_400_BAD_REQUEST)
# 验证金额范围
if jine < settings.SHANGJIA_CZ_MIN_AMOUNT or jine > settings.SHANGJIA_CZ_MAX_AMOUNT:
return Response({
'code': 400,
'message': f'充值金额必须在{settings.SHANGJIA_CZ_MIN_AMOUNT}-{settings.SHANGJIA_CZ_MAX_AMOUNT}元之间'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 生成订单ID使用配置的前缀
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"{settings.SHANGJIA_CZ_PREFIX}{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 4. 创建充值记录订单
try:
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付状态
yonghuid=request.user.UserUID,
jine=jine,
leixing=4, # 充值类型4=商家充值
shuoming=settings.SHANGJIA_CZ_PAY_DESCRIPTION,
club_id=resolve_club_id_for_write(request, request.user),
)
logger.info(f"创建商家充值订单: 商家{shangjia.nicheng}, 订单{dingdanid}, 金额{jine}")
except Exception as e:
logger.error(f"创建订单失败: {str(e)}")
return Response({
'code': 500,
'message': '订单创建失败,请重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 5. 生成支付参数(支持支付宝/微信)
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.SHANGJIA_CZ_PAY_DESCRIPTION,
club_id=resolve_club_id_for_write(request, request.user),
)
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:
try:
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)
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
openid=pay_openid,
pay_type='shangjia'
)
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)
# 6. 返回支付参数和订单ID
logger.info(f"商家{shangjia.nicheng}支付参数生成成功: {dingdanid}")
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 == 'shangjia':
PAY_DESCRIPTION = settings.SHANGJIA_CZ_PAY_DESCRIPTION
NOTIFY_URL = settings.SHANGJIA_CZ_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'
# 生成支付签名
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 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 ShangjiaHuitiao(View):
"""
商家支付回调接口
路径:/shangpin/sjhuitiao
注意这个接口不需要JWT认证由微信服务器调用
"""
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_merchant_recharge_czjilu
if not is_merchant_recharge_czjilu(order):
logger.error(
'非商家充值订单误入商家回调: %s, leixing=%s, shuoming=%s',
out_trade_no, order.leixing, order.shuoming,
)
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:{order.dingdan_id}',
)
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_shangjia, CzjiluFulfillError
try:
fulfill_shangjia(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):
"""验证微信支付签名"""
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):
"""返回微信支付回调响应"""
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 ShangjiaShibai(APIView):
"""
商家支付失败处理接口
路径:/shangpin/sjshibai
请求方式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,
leixing=4 # 确保是商家充值订单
)
except Czjilu.DoesNotExist:
logger.warning(f"商家订单不存在或不属于当前用户: {dingdanid}, 用户{request.user.UserUID}")
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单状态是否为未支付9
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"商家{request.user.ShopProfile.nicheng}删除未支付订单: {dingdanid}")
return Response({
'code': 200,
'message': '订单已删除'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '订单状态不是未支付,无法删除'
}, status=status.HTTP_400_BAD_REQUEST)
except UserShangjia.DoesNotExist:
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 ShangjiaChenggong(APIView):
"""
商家支付成功轮询接口
路径:/shangpin/sjcg
请求方式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,
leixing=4 # 商家充值类型
)
except Czjilu.DoesNotExist:
logger.warning(f"商家订单不存在: {dingdanid}, 用户{request.user.UserUID}")
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 订单未支付时,尝试补偿履约(回调延迟/失败时通过轮询兜底)
if order.zhuangtai == 9:
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
try:
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=4)
order = Czjilu.query.get(
dingdan_id=dingdanid,
yonghuid=request.user.UserUID,
leixing=4
)
except CzjiluFulfillError as exc:
logger.info(f"商家充值确认未就绪 {dingdanid}: {exc}")
return Response({
'code': 400,
'message': '订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 检查订单状态是否为已支付3
if order.zhuangtai != 3:
logger.info(f"商家订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
return Response({
'code': 400,
'message': '订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 获取商家余额(通过反向查询)
try:
shangjia = request.user.ShopProfile
yue = float(shangjia.yue) # 转为浮点数方便前端处理
logger.info(f"商家轮询成功: 订单{dingdanid}已支付,商家{shangjia.nicheng}余额为{yue}")
return Response({
'code': 200,
'message': '支付流程全部完成',
'yue': yue # 返回当前商家余额(前端需要更新全局变量)
}, status=status.HTTP_200_OK)
except UserShangjia.DoesNotExist:
logger.error(f"商家信息不存在: 用户{request.user.UserUID}")
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)
# 管事分红记录查询API
INTEGRAL_PRICE = Decimal('5.00') # 积分购买固定价格(元)
class CzhqdyView(APIView):
"""
获取可用抵扣身份接口
路径:/shangpin/czhqdy
方法POST
权限JWT认证
请求参数:
leixing: int # 1=会员2=押金3=积分4=罚款
huiyuan_id: str # 当 leixing=1 时必传会员ID
yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元)
fadan_id: int # 当 leixing=4 时必传罚单ID
返回:
{
"code": 200,
"msg": "success",
"data": [
{
"id": 1, # 身份ID1打手佣金 2管事分红 3组长分红 4打手押金
"jieshao": "打手佣金抵扣",
"xuyao": "22.22" # 所需抵扣金额(保留两位小数)
},
...
]
}
"""
permission_classes = [permissions.IsAuthenticated]
# 身份ID与对应扩展表模型、余额字段、费率查询条件的映射
IDENTITY_CONFIG = [
{
'id': 1,
'name': '打手佣金抵扣',
'profile_attr': 'DashouProfile', # User 的反向 related_name
'balance_field': 'yue',
'rate_key': '5',
'model': UserDashou,
},
{
'id': 2,
'name': '管事分红抵扣',
'profile_attr': 'GuanshiProfile',
'balance_field': 'yue',
'rate_key': '6',
'model': UserGuanshi,
},
{
'id': 3,
'name': '组长分红抵扣',
'profile_attr': 'ZuzhangProfile',
'balance_field': 'ketixian_jine',
'rate_key': '8',
'model': UserZuzhang,
},
# 新增打手押金抵扣
{
'id': 4,
'name': '打手押金抵扣',
'profile_attr': 'DashouProfile', # 与打手佣金共用扩展表
'balance_field': 'yajin', # 押金字段
'rate_key': '11', # 押金提现费率对应的 Platform
'model': UserDashou,
},
]
def post(self, request):
# 获取参数
leixing = request.data.get('leixing')
huiyuan_id = request.data.get('huiyuan_id')
yajin_jine = request.data.get('yajin_jine')
fadan_id = request.data.get('fadan_id')
is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial'))
# 参数校验
if leixing not in [1, 2, 3, 4]:
return Response({'code': 400, 'msg': 'leixing 必须为 1,2,3,4'}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 1 and not huiyuan_id:
return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 1 and is_trial:
return Response({
'code': 400,
'msg': '体验会员仅支持微信支付,不可用佣金、分红或押金抵扣',
}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 2:
if not yajin_jine:
return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
try:
yajin_jine = Decimal(str(yajin_jine))
if yajin_jine <= 0:
raise ValueError
except:
return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 4 and not fadan_id:
return Response({'code': 400, 'msg': '缴纳罚款时必须提供 fadan_id'}, status=status.HTTP_400_BAD_REQUEST)
# 获取当前用户
user = request.user
# 获取目标价格(需支付的原价)
target_price = None
if leixing == 1:
# 会员
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
club_id = resolve_effective_club_id(request, request.user)
target_price = get_huiyuan_price(club_id, huiyuan_id, huiyuan)
except Huiyuan.DoesNotExist:
return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
elif leixing == 2:
# 押金
target_price = yajin_jine
elif leixing == 3:
# 积分(固定价格)
target_price = INTEGRAL_PRICE
elif leixing == 4:
# 罚款缴纳
try:
fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=user.UserUID)
target_price = fadan.FineAmount
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
# 获取所有费率(打手佣金、管事分红、组长分红、打手押金)
rates = {}
club_id = resolve_effective_club_id(request, request.user)
for key in ['5', '6', '8', '11']:
rates[key] = get_commission_rate(club_id, key)
# 构建可用身份列表
available_list = []
for ident in self.IDENTITY_CONFIG:
# 检查用户是否有该扩展表
profile = getattr(user, ident['profile_attr'], None)
if not profile:
continue
# 获取余额
balance = getattr(profile, ident['balance_field'], Decimal('0.00'))
# 获取费率
rate = rates.get(ident['rate_key'])
if rate is None:
continue # 费率未配置,跳过该身份
# 计算所需抵扣金额: target_price / (1 - rate) ,向上取整到分
try:
if rate >= 1:
continue # 防止除零及不合理费率
required = target_price / (Decimal('1.0') - rate)
# 向上取整到分(保留两位小数,最后一位进位)
required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING)
except Exception:
continue # 计算异常,跳过
# 余额是否足够
if balance >= required:
available_list.append({
'id': ident['id'],
'jieshao': ident['name'],
'xuyao': str(required),
})
return Response({
'code': 200,
'msg': 'success',
'data': available_list
})
INTEGRAL_INCREMENT = 5
MAX_INTEGRAL = 10
IDENTITY_CONFIG = {
1: {
'name': '打手佣金',
'model': UserDashou,
'balance_field': 'yue',
'status_field': 'zhanghaozhuangtai',
'rate_key': '5',
},
2: {
'name': '管事分红',
'model': UserGuanshi,
'balance_field': 'yue',
'status_field': 'zhuangtai',
'rate_key': '6',
},
3: {
'name': '组长分红',
'model': UserZuzhang,
'balance_field': 'ketixian_jine',
'status_field': 'zhuangtai',
'rate_key': '8',
},
4: {
'name': '打手押金',
'model': UserDashou,
'balance_field': 'yajin',
'status_field': 'zhanghaozhuangtai',
'rate_key': '11',
},
}
class DsqrgmdhView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
from jituan.services.member_recharge import MemberRechargeError
try:
leixing = request.data.get('leixing')
shenfen_id = request.data.get('shenfen_id')
huiyuan_id = request.data.get('huiyuan_id')
yajin_jine = request.data.get('yajin_jine')
fadan_id = request.data.get('fadan_id')
is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial'))
logger.info(f"抵扣支付请求: leixing={leixing}, shenfen_id={shenfen_id}, huiyuan_id={huiyuan_id}, yajin_jine={yajin_jine}, fadan_id={fadan_id}, is_trial={is_trial}")
if leixing not in [1, 2, 3, 4]:
return Response({'code': 400, 'msg': 'leixing 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
if shenfen_id not in [1, 2, 3, 4]:
return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 1 and is_trial:
return Response({
'code': 400,
'msg': '体验会员仅支持微信支付,不可用佣金、分红或押金抵扣',
}, status=status.HTTP_400_BAD_REQUEST)
if leixing == 2:
if not yajin_jine:
return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
try:
yajin_jine = Decimal(str(yajin_jine))
if yajin_jine <= 0:
raise ValueError
if yajin_jine > Decimal('10000'):
return Response({'code': 400, 'msg': '押金金额不能超过10000元'}, status=status.HTTP_400_BAD_REQUEST)
except:
return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
target_price = None
club_id = resolve_effective_club_id(request, request.user)
if leixing == 1:
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
target_price = get_huiyuan_price(club_id, huiyuan_id, huiyuan)
except Huiyuan.DoesNotExist:
return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
elif leixing == 2:
target_price = yajin_jine
elif leixing == 3:
target_price = INTEGRAL_PRICE
elif leixing == 4:
if not fadan_id:
return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST)
try:
fadan_obj = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.UserUID)
target_price = fadan_obj.FineAmount
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
identity = IDENTITY_CONFIG.get(shenfen_id)
from jituan.services.club_config import get_commission_rate
try:
rate = get_commission_rate(club_id, identity['rate_key'])
if rate >= 1:
return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST)
except Exception:
return Response({'code': 400, 'msg': f"{identity['name']}费率未配置"}, status=status.HTTP_400_BAD_REQUEST)
logger.info(f"目标价格={target_price}, 费率={rate}")
try:
required = target_price / (Decimal('1.0') - rate)
required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING)
logger.info(f"所需抵扣金额={required}")
except Exception as e:
logger.error(f"金额计算错误: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': f'金额计算错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
user = request.user
yonghuid = user.UserUID
try:
profile = identity['model'].query.get(user=user)
except identity['model'].DoesNotExist:
return Response({'code': 403, 'msg': f'您不是{identity["name"]}身份'})
status_value = getattr(profile, identity['status_field'])
if status_value != 1:
return Response({'code': 403, 'msg': f'{identity["name"]}账号状态异常,无法使用'})
balance = getattr(profile, identity['balance_field'])
if balance < required:
return Response({'code': 400, 'msg': f'{identity["name"]}余额不足,需要{required}元,当前{balance}'}, status=status.HTTP_400_BAD_REQUEST)
try:
fulfill_error = None
with transaction.atomic():
locked_profile = identity['model'].objects.select_for_update().get(id=profile.id)
current_balance = getattr(locked_profile, identity['balance_field'])
if current_balance < required:
raise ValueError(f"{identity['name']}余额不足,需要{required}元,当前{current_balance}")
setattr(locked_profile, identity['balance_field'], current_balance - required)
locked_profile.save(update_fields=[identity['balance_field']])
logger.info(f"扣款成功: {identity['name']} 扣除 {required} 元,剩余 {current_balance - required}")
timestamp = str(int(time.time() * 1000))[-12:]
rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
dingdan_id = f"CZ{timestamp}{rand_str}"
if leixing == 1:
action_desc = f"购买会员 {huiyuan.jieshao}"
elif leixing == 2:
action_desc = f"充值押金 {yajin_jine}"
elif leixing == 3:
action_desc = "补充积分"
elif leixing == 4:
action_desc = f"缴纳罚款 {target_price}"
shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}"
cz_leixing = leixing
cz_club_id = club_id
from jituan.services.member_recharge import MEMBER_PENDING_STATUS, MEMBER_FAILED_STATUS, MEMBER_PAID_STATUS
cz_zhuangtai = MEMBER_PENDING_STATUS
if leixing == 4:
from jituan.services.club_penalty import penalty_cz_club_id
from products.czjilu_types import (
CZJILU_LEIXING_FAKUAN,
CZJILU_PAID_STATUS,
PENALTY_FADAN_TAG,
)
cz_leixing = CZJILU_LEIXING_FAKUAN
cz_club_id = penalty_cz_club_id(fadan_obj)
cz_zhuangtai = CZJILU_PAID_STATUS
shuoming = f"{shuoming}{PENALTY_FADAN_TAG}{fadan_obj.id}"
member_purchase_days = 0
czjilu_kwargs = {
'dingdan_id': dingdan_id,
'zhuangtai': cz_zhuangtai,
'yonghuid': yonghuid,
'jine': required,
'leixing': cz_leixing,
'shuoming': shuoming,
'club_id': cz_club_id,
}
if leixing == 1:
from jituan.services.member_recharge import club_huiyuan_sellable
_, _, member_purchase_days = club_huiyuan_sellable(
club_id, huiyuan_id, is_trial=False,
)
czjilu_kwargs.update({
'huiyuan_id': huiyuan_id,
'is_trial': False,
'purchase_days': member_purchase_days,
})
czjilu = Czjilu.query.create(**czjilu_kwargs)
response_data = {'dingdan_id': dingdan_id}
fulfill_error = None
if leixing == 1:
from jituan.services.member_recharge import (
fulfill_member_balance_purchase,
validate_member_purchase,
)
try:
ok, err_msg, _, _ = validate_member_purchase(
yonghuid, huiyuan_id, club_id, is_trial=False,
)
if not ok:
raise MemberRechargeError(err_msg or '会员不可购买', 'purchase_not_allowed')
huiyuan_record, new_jifen = fulfill_member_balance_purchase(
czjilu, huiyuan, club_id=club_id,
)
response_data['huiyuan'] = {
'huiyuanid': huiyuan_id,
'huiyuanming': huiyuan.jieshao,
'daoqi': huiyuan_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(huiyuan_record, 'daoqi_time') else '',
}
response_data['jifen'] = new_jifen
except MemberRechargeError as exc:
setattr(locked_profile, identity['balance_field'], current_balance)
locked_profile.save(update_fields=[identity['balance_field']])
czjilu.zhuangtai = MEMBER_FAILED_STATUS
czjilu.shuoming = f"{shuoming} | 履约失败:{str(exc)[:40]}"[:100]
czjilu.save(update_fields=['zhuangtai', 'shuoming'])
fulfill_error = exc
elif leixing == 2:
try:
dashou = UserDashou.objects.select_for_update().get(user=user)
dashou.yajin += yajin_jine
dashou.save(update_fields=['yajin'])
czjilu.zhuangtai = MEMBER_PAID_STATUS
czjilu.save(update_fields=['zhuangtai'])
response_data['yajin'] = dashou.yajin
except UserDashou.DoesNotExist:
setattr(locked_profile, identity['balance_field'], current_balance)
locked_profile.save(update_fields=[identity['balance_field']])
czjilu.zhuangtai = MEMBER_FAILED_STATUS
czjilu.shuoming = f"{shuoming} | 履约失败:打手不存在"[:100]
czjilu.save(update_fields=['zhuangtai', 'shuoming'])
fulfill_error = MemberRechargeError('打手信息不存在,无法充值押金', 'dashou_not_found')
elif leixing == 4:
if fadan_obj.Status not in [1, 3]:
return Response({'code': 400, 'msg': '罚单状态不可缴纳'}, status=status.HTTP_400_BAD_REQUEST)
fadan_obj.Status = 2
fadan_obj.save(update_fields=['Status'])
response_data['fadan_id'] = fadan_obj.id
response_data['fadan_status'] = fadan_obj.Status
# ========== 修改点2: 使用最新的分红处理方法 ==========
success, msg = process_fadan_fenhong(fadan_obj.id)
if not success:
logger.error(f"罚单 {fadan_obj.id} 分红处理失败: {msg}")
else:
logger.info(f"罚单 {fadan_obj.id} 分红处理成功: {msg}")
else: # leixing == 3
try:
dashou = UserDashou.objects.select_for_update().get(user=user)
new_jifen = dashou.jifen + INTEGRAL_INCREMENT
dashou.jifen = min(new_jifen, MAX_INTEGRAL)
dashou.save(update_fields=['jifen'])
czjilu.zhuangtai = MEMBER_PAID_STATUS
czjilu.save(update_fields=['zhuangtai'])
response_data['jifen'] = dashou.jifen
except UserDashou.DoesNotExist:
setattr(locked_profile, identity['balance_field'], current_balance)
locked_profile.save(update_fields=[identity['balance_field']])
czjilu.zhuangtai = MEMBER_FAILED_STATUS
czjilu.shuoming = f"{shuoming} | 履约失败:打手不存在"[:100]
czjilu.save(update_fields=['zhuangtai', 'shuoming'])
fulfill_error = MemberRechargeError('打手信息不存在,无法补充积分', 'dashou_not_found')
except Exception as e:
logger.error(f"事务处理异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': f'支付处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if fulfill_error:
logger.warning('余额抵扣履约失败 dingdan=%s err=%s', dingdan_id, fulfill_error)
return Response({
'code': 400,
'msg': str(fulfill_error),
'data': response_data,
}, status=status.HTTP_400_BAD_REQUEST)
final_profile = identity['model'].query.get(user=user)
response_data['balance'] = str(getattr(final_profile, identity['balance_field']))
return Response({
'code': 200,
'msg': 'success',
'data': response_data,
})
except Exception as e:
logger.error(f"接口顶层异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': f'系统错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)