"""商家充值与充值抵扣视图。
包含商家充值支付参数/回调/失败/成功轮询,以及充值打手抵押查询与抵扣接口。
含模块级常量 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. 生成微信支付参数
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"""
{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'
# 生成支付签名
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 = Decimal(str(total_fee)) / Decimal('100') # 转换为元
order_amount_yuan = Decimal(str(order.jine)) # 订单金额(元)
# 允许1分钱的误差
if abs(wechat_amount_yuan - order_amount_yuan) > Decimal('0.01'):
logger.error(f"商家订单金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
return self.wechat_response('FAIL', '金额不一致')
# ===== 在这里添加收支记录更新代码 =====
# 更新收支记录表
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)
# ===== 添加结束 =====
# 7. 使用事务更新订单状态和商家余额
with transaction.atomic():
# 更新订单状态为已支付(3)
order.zhuangtai = 3
order.save(update_fields=['zhuangtai'])
logger.info(f"商家订单状态更新成功: {order.dingdan_id} -> 3")
# 更新商家余额
try:
# 通过用户ID查找商家(高效查询)
shangjia = UserShangjia.query.get(user__UserUID=order.yonghuid)
# 增加余额
shangjia.yue += order_amount_yuan
shangjia.save(update_fields=['yue'])
logger.info(f"商家余额更新成功: 商家{shangjia.nicheng}, 增加{order_amount_yuan}元, 新余额{shangjia.yue}")
except UserShangjia.DoesNotExist:
# 如果找不到商家,记录错误,但依然返回成功(避免微信重复回调)
logger.error(f"商家信息不存在: 用户{order.yonghuid}")
return self.wechat_response('SUCCESS', 'OK')
except Exception as e:
logger.error(f"更新商家余额失败: {str(e)}", exc_info=True)
# 回滚事务
raise
# 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"""
"""
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. 检查订单状态是否为已支付(3)
if order.zhuangtai != 3:
logger.info(f"商家订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
return Response({
'code': 400,
'message': '订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 获取商家余额(通过反向查询)
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, # 身份ID:1打手佣金 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)