diff --git a/backend/view.py b/backend/view.py index 3b85610..23ad6e7 100644 --- a/backend/view.py +++ b/backend/view.py @@ -6658,6 +6658,8 @@ class FaKuanChuangJianView(APIView): ImageURL=relative_url, Purpose=1 ) + from users.fadan_fenhong_utils import lock_penalty_bonus + lock_penalty_bonus(penalty) logger.info(f"罚款创建成功: {penalty.id}") return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': penalty.id}}) @@ -6760,6 +6762,8 @@ class FineApplyView(APIView): ImageURL=relative_url, Purpose=1 ) + from users.fadan_fenhong_utils import lock_penalty_bonus + lock_penalty_bonus(penalty) logger.info(f"客服罚款申请成功: 订单 {OrderID}, 打手 {dashou_id}, 金额 {FineAmount}") return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': penalty.id}}) diff --git a/orders/migrations/0009_penalty_bonus_lock_fields.py b/orders/migrations/0009_penalty_bonus_lock_fields.py new file mode 100644 index 0000000..3ccbd36 --- /dev/null +++ b/orders/migrations/0009_penalty_bonus_lock_fields.py @@ -0,0 +1,31 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0008_order_waibu_penalty_staff'), + ] + + operations = [ + migrations.AddField( + model_name='penalty', + name='ApplicantBonusRate', + field=models.DecimalField( + db_column='shenqingren_fenhong_lilv', + decimal_places=4, + default=0.0, + max_digits=5, + verbose_name='申请人分红锁定利率', + ), + ), + migrations.AddField( + model_name='penalty', + name='BonusCredited', + field=models.BooleanField( + db_column='fenhong_yiruzhang', + default=False, + verbose_name='分红是否已入账', + ), + ), + ] diff --git a/orders/models.py b/orders/models.py index 0d405ff..7c20838 100644 --- a/orders/models.py +++ b/orders/models.py @@ -510,6 +510,14 @@ class Penalty(QModel): max_digits=12, decimal_places=2, default=0.00, db_column='shenqingren_fenhong_jine', verbose_name='申请人分红金额', ) + ApplicantBonusRate = models.DecimalField( + max_digits=5, decimal_places=4, default=0.0000, + db_column='shenqingren_fenhong_lilv', verbose_name='申请人分红锁定利率', + ) + BonusCredited = models.BooleanField( + default=False, + db_column='fenhong_yiruzhang', verbose_name='分红是否已入账', + ) ClubID = models.CharField( max_length=16, default='xq', db_index=True, db_column='club_id', verbose_name='所属俱乐部', diff --git a/orders/views.py b/orders/views.py index 6659feb..1af629e 100644 --- a/orders/views.py +++ b/orders/views.py @@ -64,6 +64,8 @@ from jituan.services.club_config import get_commission_rate, get_commission_rate from jituan.services.club_context import resolve_club_id_from_request from jituan.services.club_user import get_payment_openid from jituan.services.club_penalty import resolve_penalty_club_id +from users.fadan_fenhong_utils import lock_penalty_bonus +from users.fadan_fenhong_utils import lock_penalty_bonus from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai from users.models import UserDashou, UserShangjia, UserBoss from users.business_models import User @@ -2293,25 +2295,21 @@ class ShangjiaDingdanXiangqingView(APIView): 'shensuliyou': fadan_obj.AppealReason or '', 'tongyi_chufaliyou': fadan_obj.ApproveReason or '', 'bohuiliyou': fadan_obj.RejectReason or '', + 'yingde_fenhong': float(fadan_obj.ApplicantBonusAmount or 0), + 'fenhong_lilv': float(fadan_obj.ApplicantBonusRate or 0), + 'fenhong_yiruzhang': bool(fadan_obj.BonusCredited), 'CreateTime': ct, 'create_time': ct, 'UpdateTime': ut, 'update_time': ut, } - # 7. 商家分红利率(申请页预览):个人记录优先,否则全局 fadan_fenhong_lilv shenfen=5 + # 7. 商家分红利率(申请页预览):与缴纳后分红同一套规则 fenhong_lilv = 0.0 try: - last_fenhong = PenaltyBonus.query.filter( - BeneficiaryID=yonghuid, - Identity=1, # fadan_fenhong 表:1=商家 - ).order_by('-CreateTime').first() - if last_fenhong and last_fenhong.IndividualRate: - fenhong_lilv = float(last_fenhong.IndividualRate) - else: - global_rate = PenaltyBonusRate.query.filter(Identity=5).first() - if global_rate and global_rate.Rate: - fenhong_lilv = float(global_rate.Rate) + from users.fadan_fenhong_utils import get_merchant_fenhong_lilv + order_club_id = getattr(order, 'ClubID', None) or 'xq' + fenhong_lilv = get_merchant_fenhong_lilv(yonghuid, club_id=order_club_id) except Exception as e: logger.warning(f'获取分红利率失败: {e}') @@ -6531,7 +6529,7 @@ class ShangjiaFakuanApplyView(APIView): staff_kefu_id = getattr(staff_member, 'staff_user_id', None) or None with transaction.atomic(): - Penalty.query.create( + fadan = Penalty.query.create( PenalizedUserID=dashou_id, ApplicantID=shangjia_id, # 申请人为当前商家 Identity=1, # 被打者身份:1=打手 @@ -6544,6 +6542,7 @@ class ShangjiaFakuanApplyView(APIView): ClubID=resolve_penalty_club_id(order=order, penalized_user_id=dashou_id), ShopStaffUserID=staff_kefu_id, ) + lock_penalty_bonus(fadan) logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}罚款成功,金额{fakuanjine}") return Response({'code': 0, 'msg': '罚款申请已提交'}) @@ -6647,6 +6646,7 @@ class ShangjiaFakuaiXiugaiView(APIView): fadan.ProcessorID = processor_id fadan.ProcessorIdentity = 5 # 小程序商家自行处理 fadan.save() + lock_penalty_bonus(fadan) return Response({'code': 0, 'msg': '罚款修改成功'}) def _cancel_penalty(self, fadan, liyou, processor_id=None): @@ -6685,6 +6685,7 @@ class ShangjiaFakuaiXiugaiView(APIView): fadan.ProcessorID = processor_id fadan.ProcessorIdentity = 5 fadan.save() + lock_penalty_bonus(fadan) logger.info(f"商家重新申请罚单{fadan.id},金额={jine}") return Response({'code': 0, 'msg': '已重新申请罚款'}) diff --git a/shop/views.py b/shop/views.py index 085dca9..d632b99 100644 --- a/shop/views.py +++ b/shop/views.py @@ -3096,7 +3096,7 @@ class ShopFineApplyView(APIView): # ---------- 6. 创建罚单 ---------- with transaction.atomic(): - Penalty.query.create( + penalty = Penalty.query.create( PenalizedUserID=dashou_id, ApplicantID=str(shop_id), # 申请人记为店铺ID Identity=1, # 被处罚人身份:1 打手 @@ -3107,6 +3107,8 @@ class ShopFineApplyView(APIView): AffectsGrabbing=yingxiang_qiangdan, ApplicantIdentity=4, # 申请人身份:4 店铺管理员 ) + from users.fadan_fenhong_utils import lock_penalty_bonus + lock_penalty_bonus(penalty) logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}") return Response({'code': 200, 'msg': '罚款已生成'}) diff --git a/users/fadan_fenhong_utils.py b/users/fadan_fenhong_utils.py index 1f68e04..37cf9b4 100644 --- a/users/fadan_fenhong_utils.py +++ b/users/fadan_fenhong_utils.py @@ -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 \ No newline at end of file diff --git a/users/views.py b/users/views.py index dc4436a..cc3fd7e 100644 --- a/users/views.py +++ b/users/views.py @@ -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'])