Files
Django/users/fadan_fenhong_utils.py
XingQue 1c67f98866 feat: 罚款分红费率按俱乐部可读可配,接入提现设置
个人利率优先,其次 club_fadan_fenhong_lilv,再回退全局表;组队均摊仍按每张罚单金额×费率。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 19:54:58 +08:00

277 lines
11 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.
# yonghu/fadan_fenhong_utils.py
# 罚款缴纳分红处理公共方法(支持更新罚单中的申请人分红金额)
import logging
from decimal import Decimal
from django.db import transaction
from orders.models import Penalty, PenaltyBonus, PenaltyBonusRate
from users.models import UserShangjia
logger = logging.getLogger(__name__)
# 身份映射字典
# 罚单 ApplicantIdentityshenqingren_shenfen→ fadan_fenhong 表 Identity + fadan_fenhong_lilv 表 shenfen
# 罚单侧1=客服 2=售后 3=管理员 4=店铺 5=商家
# fadan_fenhong 表1=商家 2=客服 3=平台管理者 4=店铺 5=售后
IDENTITY_MAP = {
1: {'fenhong': 2, 'lilv': 1}, # 客服 → 分红记录身份2利率表 shenfen=1
2: {'fenhong': 5, 'lilv': 2}, # 售后 → 分红记录身份5利率表 shenfen=2
3: {'fenhong': 3, 'lilv': 3}, # 管理员
4: {'fenhong': 4, 'lilv': 4}, # 店铺
5: {'fenhong': 1, 'lilv': 5}, # 商家 → 分红记录身份1利率表 shenfen=5钱进 user_shangjia.yue
}
MERCHANT_LILV_SHENFEN = 5
MERCHANT_FENHONG_SHENFEN = 1
def _normalize_club_id(club_id):
cid = (club_id or '').strip()
return cid or 'xq'
def _read_global_fenhong_rate(lilv_shenfen):
try:
global_rate = PenaltyBonusRate.objects.get(Identity=lilv_shenfen)
rate = global_rate.Rate
if rate is not None and Decimal(str(rate)) > 0:
return rate
except PenaltyBonusRate.DoesNotExist:
pass
return None
def _read_club_fenhong_rate(club_id, lilv_shenfen):
"""俱乐部级罚款分红费率club_fadan_fenhong_lilv"""
from jituan.models import ClubFadanFenhongLilv
club_id = _normalize_club_id(club_id)
row = ClubFadanFenhongLilv.query.filter(
club_id=club_id, shenfen=int(lilv_shenfen),
).first()
if row is not None and row.lilv is not None and Decimal(str(row.lilv)) > 0:
return row.lilv
return None
def _get_fenhong_rate(fenhongzhe_id, fenhong_shenfen, lilv_shenfen, club_id='xq'):
"""
获取分红利率:个人单独利率 > 0 优先;否则俱乐部配置;再回退全局 fadan_fenhong_lilv。
"""
club_id = _normalize_club_id(club_id)
try:
individual = PenaltyBonus.objects.get(
BeneficiaryID=fenhongzhe_id,
Identity=fenhong_shenfen,
ClubID=club_id,
IsIndividualRate=True,
)
ind_rate = individual.IndividualRate
if ind_rate is not None and Decimal(str(ind_rate)) > 0:
return ind_rate
except PenaltyBonus.DoesNotExist:
pass
club_rate = _read_club_fenhong_rate(club_id, lilv_shenfen)
if club_rate is not None:
return club_rate
return _read_global_fenhong_rate(lilv_shenfen)
def _calc_bonus_amount(fine_amount, rate):
if fine_amount is None or rate is None:
return Decimal('0.00')
fine = Decimal(str(fine_amount))
r = Decimal(str(rate))
if fine <= 0 or r <= 0:
return Decimal('0.00')
return (fine * r).quantize(Decimal('0.01'))
def lock_penalty_bonus(fadan):
"""
申请/改价/再次申请时锁定应得分红利率与金额,写入罚单供前端展示;
缴纳时按此锁定值入账,不再现场重算。
"""
if getattr(fadan, 'BonusCredited', False):
return fadan
zero_rate = Decimal('0.0000')
zero_amount = Decimal('0.00')
applicant_id = fadan.ApplicantID
src_shenfen = fadan.ApplicantIdentity
if not applicant_id or src_shenfen not in IDENTITY_MAP:
fadan.ApplicantBonusRate = zero_rate
fadan.ApplicantBonusAmount = zero_amount
fadan.save(update_fields=['ApplicantBonusRate', 'ApplicantBonusAmount'])
return fadan
mapped = IDENTITY_MAP[src_shenfen]
club_id = _normalize_club_id(fadan.ClubID)
rate = _get_fenhong_rate(
applicant_id, mapped['fenhong'], mapped['lilv'], club_id=club_id,
)
if rate is None:
fadan.ApplicantBonusRate = zero_rate
fadan.ApplicantBonusAmount = zero_amount
else:
fadan.ApplicantBonusRate = Decimal(str(rate))
fadan.ApplicantBonusAmount = _calc_bonus_amount(fadan.FineAmount, rate)
fadan.save(update_fields=['ApplicantBonusRate', 'ApplicantBonusAmount'])
return fadan
def get_merchant_fenhong_lilv(yonghuid, club_id='xq'):
"""商家申请罚单页预览利率(尚无罚单时)。"""
club_id = _normalize_club_id(club_id)
rate = _get_fenhong_rate(
yonghuid,
MERCHANT_FENHONG_SHENFEN,
MERCHANT_LILV_SHENFEN,
club_id=club_id,
)
return float(rate) if rate is not None else 0.0
def process_fadan_fenhong(fadan_id):
"""
罚款缴纳后的分红入账:优先使用申请时已锁定的利率/金额,不现场重算。
"""
try:
try:
fadan = Penalty.objects.get(id=fadan_id, Status=2)
except Penalty.DoesNotExist:
logger.error(f"罚单 {fadan_id} 不存在或状态不是已缴纳")
return False, "罚单不存在或状态异常"
if fadan.BonusCredited:
logger.info(f"罚单 {fadan_id} 分红已入账,跳过")
return True, "已分红"
shenqing_chufa = fadan.ApplicantID
src_shenfen = fadan.ApplicantIdentity
if not shenqing_chufa:
logger.info(f"罚单 {fadan_id} 无申请处罚者,不进行分红")
return True, "无申请处罚者,无需分红"
if src_shenfen not in IDENTITY_MAP:
logger.error(f"未知的申请处罚者身份: {src_shenfen},罚单 {fadan_id}")
return False, f"未知身份 {src_shenfen}"
mapped = IDENTITY_MAP[src_shenfen]
fenhong_shenfen = mapped['fenhong']
club_id = _normalize_club_id(fadan.ClubID)
locked_amount = Decimal(str(fadan.ApplicantBonusAmount or 0))
locked_rate = fadan.ApplicantBonusRate
# 申请时已锁定:缴纳只按锁定值入账,不因全局利率后续变更而重算
if locked_amount > 0 or (
locked_rate is not None and Decimal(str(locked_rate)) > 0
):
fenhong_jine = locked_amount
rate = locked_rate
else:
# 旧数据兼容:未锁定时按当前利率现场算一次并回写
fakuan_jine = fadan.FineAmount
if fakuan_jine <= 0:
fadan.ApplicantBonusAmount = Decimal('0.00')
fadan.save(update_fields=['ApplicantBonusAmount'])
return True, "罚款金额为0无分红"
rate = _get_fenhong_rate(
shenqing_chufa, fenhong_shenfen, mapped['lilv'], club_id=club_id,
)
if rate is None:
logger.warning(f"罚单 {fadan_id} 未配置分红利率")
fadan.ApplicantBonusAmount = Decimal('0.00')
fadan.save(update_fields=['ApplicantBonusAmount'])
return True, "未配置分红利率,无分红"
fenhong_jine = _calc_bonus_amount(fakuan_jine, rate)
fadan.ApplicantBonusRate = Decimal(str(rate))
fadan.ApplicantBonusAmount = fenhong_jine
fadan.save(update_fields=['ApplicantBonusRate', 'ApplicantBonusAmount'])
if fenhong_jine <= 0:
logger.info(f"罚单 {fadan_id} 锁定分红金额为0无需入账")
fadan.BonusCredited = True
fadan.save(update_fields=['BonusCredited'])
return True, "分红金额为0"
if src_shenfen == 5:
with transaction.atomic():
fadan = Penalty.objects.select_for_update().get(id=fadan_id, Status=2)
if fadan.BonusCredited:
return True, "已分红"
if not UserShangjia.objects.filter(user__UserUID=shenqing_chufa).exists():
logger.error(f"商家 {shenqing_chufa} 扩展表不存在,无法分红入账")
return False, "商家资料不存在,无法分红"
fenhong_record, _created = PenaltyBonus.objects.get_or_create(
BeneficiaryID=shenqing_chufa,
Identity=fenhong_shenfen,
ClubID=club_id,
defaults={
'TotalAmount': Decimal('0.00'),
'WithdrawableAmount': Decimal('0.00'),
'BonusCount': 0,
'IndividualRate': rate,
'IsIndividualRate': False,
},
)
fenhong_record.TotalAmount += fenhong_jine
fenhong_record.BonusCount += 1
fenhong_record.save()
shangjia = UserShangjia.objects.select_for_update().get(user__UserUID=shenqing_chufa)
shangjia.yue += fenhong_jine
shangjia.save(update_fields=['yue'])
logger.info(f"商家 {shenqing_chufa} 余额增加 {fenhong_jine} 元(锁定值)")
fadan.BonusCredited = True
fadan.save(update_fields=['BonusCredited'])
else:
with transaction.atomic():
fadan = Penalty.objects.select_for_update().get(id=fadan_id, Status=2)
if fadan.BonusCredited:
return True, "已分红"
fenhong_record, _created = PenaltyBonus.objects.get_or_create(
BeneficiaryID=shenqing_chufa,
Identity=fenhong_shenfen,
ClubID=club_id,
defaults={
'TotalAmount': Decimal('0.00'),
'WithdrawableAmount': Decimal('0.00'),
'BonusCount': 0,
'IndividualRate': rate,
'IsIndividualRate': False,
},
)
fenhong_record.TotalAmount += fenhong_jine
fenhong_record.WithdrawableAmount += fenhong_jine
fenhong_record.BonusCount += 1
fenhong_record.save()
fadan.BonusCredited = True
fadan.save(update_fields=['BonusCredited'])
logger.info(
f"罚单 {fadan_id} 分红入账成功: 申请者 {shenqing_chufa}, "
f"锁定金额 {fenhong_jine} 元, 锁定利率 {rate}"
)
return True, "分红处理成功"
except Exception as e:
logger.error(f"罚单 {fadan_id} 分红处理异常: {str(e)}", exc_info=True)
return False, f"分红处理异常: {str(e)}"