fix: 罚单申请时锁定分红利率与应得金额,缴纳按锁定值入账

This commit is contained in:
XingQue
2026-06-27 17:46:39 +08:00
parent e2e568bcda
commit d19b6250ae
7 changed files with 228 additions and 104 deletions

View File

@@ -20,89 +20,179 @@ IDENTITY_MAP = {
5: {'fenhong': 1, 'lilv': 5}, # 商家
}
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 _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
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):
"""
罚款缴纳后的分红处理(在回调中调用)
参数:
fadan_id: 罚单 ID
返回:
(success, message)
罚款缴纳后的分红入账:优先使用申请时已锁定的利率/金额,不现场重算。
"""
try:
# 1. 获取罚单对象(必须是已缴纳状态)
try:
fadan = Penalty.objects.get(id=fadan_id, Status=2)
except Penalty.DoesNotExist:
logger.error(f"罚单 {fadan_id} 不存在或状态不是已缴纳")
return False, "罚单不存在或状态异常"
# 防重复分红(微信回调重试时不可二次加钱)
if fadan.ApplicantBonusAmount and Decimal(str(fadan.ApplicantBonusAmount)) > 0:
logger.info(
f"罚单 {fadan_id} 已分红 {fadan.ApplicantBonusAmount} 元,跳过重复处理"
)
if fadan.BonusCredited:
logger.info(f"罚单 {fadan_id} 分红已入账,跳过")
return True, "已分红"
# 2. 获取申请处罚者ID和身份源身份数字
shenqing_chufa = fadan.ApplicantID
src_shenfen = fadan.ApplicantIdentity
if not shenqing_chufa:
logger.info(f"罚单 {fadan_id} 无申请处罚者,不进行分红")
# 即使无申请者也可将分红金额字段设为0默认已是0
return True, "无申请处罚者,无需分红"
# 3. 映射身份数字
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'] # 用于 FadanFenhong.shenfen
lilv_shenfen = mapped['lilv'] # 用于 FadanFenhongLilv.shenfen
fenhong_shenfen = mapped['fenhong']
club_id = _normalize_club_id(fadan.ClubID)
# 4. 获取罚款金额
fakuan_jine = fadan.FineAmount
if fakuan_jine <= 0:
logger.info(f"罚单 {fadan_id} 罚款金额为0不产生分红")
fadan.ApplicantBonusAmount = Decimal('0.00')
fadan.save(update_fields=['ApplicantBonusAmount'])
return True, "罚款金额为0无分红"
locked_amount = Decimal(str(fadan.ApplicantBonusAmount or 0))
locked_rate = fadan.ApplicantBonusRate
# 5. 确定分红利率
rate = _get_fenhong_rate(shenqing_chufa, fenhong_shenfen, lilv_shenfen, club_id=fadan.ClubID)
if rate is None:
logger.warning(f"未找到身份 {src_shenfen} (映射后 fenhong_shenfen={fenhong_shenfen}, lilv_shenfen={lilv_shenfen}) 的分红利率,罚单 {fadan_id} 不进行分红")
fadan.ApplicantBonusAmount = Decimal('0.00')
fadan.save(update_fields=['ApplicantBonusAmount'])
return True, "未配置分红利率,无分红"
if locked_amount > 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'])
# 6. 计算分红金额
fenhong_jine = (Decimal(str(fakuan_jine)) * Decimal(str(rate))).quantize(Decimal('0.01'))
if fenhong_jine <= 0:
logger.info(f"罚单 {fadan_id} 计算分红金额为0无需处理")
fadan.ApplicantBonusAmount = Decimal('0.00')
fadan.save(update_fields=['ApplicantBonusAmount'])
logger.info(f"罚单 {fadan_id} 锁定分红金额为0无需入账")
fadan.BonusCredited = True
fadan.save(update_fields=['BonusCredited'])
return True, "分红金额为0"
club_id = fadan.ClubID or 'xq'
# 7. 根据申请处罚者身份执行不同的分红逻辑
# src_shenfen == 5 代表商家(源身份)
if src_shenfen == 5:
# 商家:分红统计累加 + 余额入账可提现分红字段保持0钱进商家余额
with transaction.atomic():
fadan = Penalty.objects.select_for_update().get(id=fadan_id, Status=2)
if fadan.ApplicantBonusAmount and Decimal(str(fadan.ApplicantBonusAmount)) > 0:
logger.info(f"罚单 {fadan_id} 事务内检测到已分红,跳过")
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(
fenhong_record, _created = PenaltyBonus.objects.get_or_create(
BeneficiaryID=shenqing_chufa,
Identity=fenhong_shenfen,
ClubID=club_id,
@@ -111,33 +201,28 @@ def process_fadan_fenhong(fadan_id):
'WithdrawableAmount': Decimal('0.00'),
'BonusCount': 0,
'IndividualRate': rate,
'is_individual_rate': False
}
'IsIndividualRate': False,
},
)
fenhong_record.TotalAmount += fenhong_jine
# 可提现分红不累加保持0
fenhong_record.BonusCount += 1
fenhong_record.save()
# 直接增加商家余额UserShangjia.yue
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}")
logger.info(f"商家 {shenqing_chufa} 余额增加 {fenhong_jine}(锁定值)")
# 更新罚单中的申请人分红金额
fadan.ApplicantBonusAmount = fenhong_jine
fadan.save(update_fields=['ApplicantBonusAmount'])
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.ApplicantBonusAmount and Decimal(str(fadan.ApplicantBonusAmount)) > 0:
logger.info(f"罚单 {fadan_id} 事务内检测到已分红,跳过")
if fadan.BonusCredited:
return True, "已分红"
fenhong_record, created = PenaltyBonus.objects.get_or_create(
fenhong_record, _created = PenaltyBonus.objects.get_or_create(
BeneficiaryID=shenqing_chufa,
Identity=fenhong_shenfen,
ClubID=club_id,
@@ -146,43 +231,23 @@ def process_fadan_fenhong(fadan_id):
'WithdrawableAmount': Decimal('0.00'),
'BonusCount': 0,
'IndividualRate': rate,
'is_individual_rate': False
}
'IsIndividualRate': False,
},
)
fenhong_record.TotalAmount += fenhong_jine
fenhong_record.WithdrawableAmount += fenhong_jine
fenhong_record.BonusCount += 1
fenhong_record.save()
# 更新罚单中的申请人分红金额
fadan.ApplicantBonusAmount = fenhong_jine
fadan.save(update_fields=['ApplicantBonusAmount'])
fadan.BonusCredited = True
fadan.save(update_fields=['BonusCredited'])
logger.info(f"罚单 {fadan_id} 分红处理成功: 申请者 {shenqing_chufa}, 分红金额 {fenhong_jine} 元, 源身份 {src_shenfen}, 映射后分红身份 {fenhong_shenfen}")
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)}"
def _get_fenhong_rate(fenhongzhe_id, fenhong_shenfen, lilv_shenfen, club_id='xq'):
"""
获取分红利率(优先使用个人单独配置,否则使用全局身份配置)
"""
try:
individual = PenaltyBonus.objects.get(
BeneficiaryID=fenhongzhe_id,
Identity=fenhong_shenfen,
ClubID=club_id,
is_individual_rate=True
)
return individual.IndividualRate
except PenaltyBonus.DoesNotExist:
pass
try:
global_rate = PenaltyBonusRate.objects.get(Identity=lilv_shenfen)
return global_rate.Rate
except PenaltyBonusRate.DoesNotExist:
return None

View File

@@ -11741,7 +11741,10 @@ class ShangjiaCufaTongjiView(APIView):
shensuzhong_jine=Sum('FineAmount', filter=Q(Status=3)),
yibohui=Count('id', filter=Q(Status=4)),
yibohui_jine=Sum('FineAmount', filter=Q(Status=4)),
shijidaozhang_jine=Sum('ApplicantBonusAmount', filter=Q(Status=2)),
shijidaozhang_jine=Sum(
'ApplicantBonusAmount',
filter=Q(Status=2, BonusCredited=True),
),
)
jifen_agg = PenaltyRecord.query.filter(ApplicantID=yonghuid).aggregate(
@@ -11875,6 +11878,8 @@ class ShangjiaFakuanLieBiaoView(APIView):
'shensuliyou': r.AppealReason or '',
'bohuiliyou': r.RejectReason or '',
'applicant_bonus_amount': str(r.ApplicantBonusAmount or 0),
'applicant_bonus_rate': str(r.ApplicantBonusRate or 0),
'bonus_credited': bool(r.BonusCredited),
'shop_staff_user_id': r.ShopStaffUserID or '',
'staff_display_name': staff_name_map.get(r.ShopStaffUserID or '', ''),
'zhengju_tupian': zhengju_map.get(rid, []),
@@ -12006,7 +12011,7 @@ class FaKuanPayView(APIView):
yonghuid=request.user.UserUID,
jine=jine,
leixing=4,
shuoming=settings.FAKUAN_PAY_DESCRIPTION,
shuoming=f'{settings.FAKUAN_PAY_DESCRIPTION}#fadan:{fadan.id}',
club_id=resolve_club_id_for_write(request, request.user),
)
@@ -12225,12 +12230,20 @@ class FaKuanHuitiaoView(View):
# 8. 更新罚单状态为已缴纳2
try:
# 根据订单中的用户ID和金额匹配待缴纳的罚单
fadan = Penalty.query.filter(
PenalizedUserID=order.yonghuid,
FineAmount=order.jine,
Status__in=[1, 3]
).first()
fadan = None
m = re.search(r'#fadan:(\d+)', order.shuoming or '')
if m:
fadan = Penalty.query.filter(
id=int(m.group(1)),
PenalizedUserID=order.yonghuid,
Status__in=[1, 3],
).first()
if not fadan:
fadan = Penalty.query.filter(
PenalizedUserID=order.yonghuid,
FineAmount=order.jine,
Status__in=[1, 3],
).order_by('-CreateTime').first()
if fadan:
fadan.Status = 2 # 已缴纳
fadan.save(update_fields=['Status'])