"""商家充值与充值抵扣视图。 包含商家充值支付参数/回调/失败/成功轮询,以及充值打手抵押查询与抵扣接口。 含模块级常量 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)