From 8e7481e25d9abd4f304e78924e17eb75473cf489 Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 29 Jul 2026 00:50:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=AE=A1=E4=BA=8B/=E7=BB=84=E9=95=BF?= =?UTF-8?q?=E5=88=86=E7=BA=A2=E5=85=A5=E8=B4=A6=E6=8E=A5=E5=85=A5=E8=B5=84?= =?UTF-8?q?=E9=87=91=E5=86=BB=E7=BB=93=E7=BD=91=E5=85=B3=EF=BC=88=E4=BC=9A?= =?UTF-8?q?=E5=91=98/=E5=95=86=E5=AE=B6=E5=8D=95/=E6=8A=BC=E9=87=91?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- jituan/services/czjilu_fulfill.py | 18 +++++- jituan/services/fund_freeze.py | 83 +++++++++++++++++++++++++--- jituan/services/fund_freeze_admin.py | 3 +- jituan/services/member_recharge.py | 50 ++++++++++++----- orders/utils.py | 16 +++++- rank/reward_services.py | 37 +++++++++++-- 6 files changed, 173 insertions(+), 34 deletions(-) diff --git a/jituan/services/czjilu_fulfill.py b/jituan/services/czjilu_fulfill.py index c063c41..ec9d5e6 100644 --- a/jituan/services/czjilu_fulfill.py +++ b/jituan/services/czjilu_fulfill.py @@ -5,6 +5,7 @@ import logging from decimal import Decimal from django.db import transaction +from django.db.models import F from backend.utils import update_guanshi_daily_by_action from jituan.constants import CLUB_ID_DEFAULT @@ -152,9 +153,20 @@ def _apply_yajin_fenhong(order, dashou, amount): if fenhong_jine <= 0: return guanshi = UserGuanshi.objects.select_for_update().get(user__UserUID=guanshi_id) - guanshi.yue += fenhong_jine - guanshi.chongzhifenrun += fenhong_jine - guanshi.save(update_fields=['yue', 'chongzhifenrun']) + from jituan.services.fund_freeze import credit_guanshi_fenhong + dingdan_id = str(order.dingdan_id or '') + result = credit_guanshi_fenhong( + user_id=str(guanshi_id), + amount=fenhong_jine, + biz_id=f'{dingdan_id}:guanshi:yajin', + profile=guanshi, + czjilu_id=dingdan_id, + freeze_reason=f'押金分红 {dingdan_id}', + ) + if not result.get('idempotent'): + UserGuanshi.objects.filter(pk=guanshi.pk).update( + chongzhifenrun=F('chongzhifenrun') + fenhong_jine, + ) user_main = dashou.user Gsfenhong.query.create( dingdan_id=order.dingdan_id, diff --git a/jituan/services/fund_freeze.py b/jituan/services/fund_freeze.py index ea1087b..94aea54 100644 --- a/jituan/services/fund_freeze.py +++ b/jituan/services/fund_freeze.py @@ -90,16 +90,28 @@ def calc_freeze_amount(credit: Decimal, cfg: ClubFundFreezeConfig) -> Decimal: return frozen -def _source_allowed(cfg: ClubFundFreezeConfig, source_type: str) -> bool: +def _source_allowed(cfg: ClubFundFreezeConfig, source_type: str, role: str = '') -> bool: scope = cfg.source_scope if not scope: return True if isinstance(scope, str): - return source_type == scope - try: - return source_type in list(scope) - except TypeError: + items = [scope] + else: + try: + items = list(scope) + except TypeError: + return True + if source_type in items: return True + # 管事/组长几乎没有 order_settle 入账;历史配置只勾了「订单结算」时仍允许分红冻结 + role = (role or '').strip().lower() + if ( + role in (ROLE_GUANSHI, ROLE_ZUZHANG, ROLE_SHENHEGUAN) + and source_type == SOURCE_FENHONG + and set(items) == {'order_settle'} + ): + return True + return False def _config_snapshot(cfg: ClubFundFreezeConfig) -> Dict[str, Any]: @@ -195,7 +207,7 @@ def credit_role_balance( cfg = get_freeze_config(cid, role) freeze_on = bool( - cfg and cfg.enabled and _source_allowed(cfg, source_type) and biz_id + cfg and cfg.enabled and _source_allowed(cfg, source_type, role=role) and biz_id ) skip_reason = '' if not freeze_on: @@ -203,7 +215,7 @@ def credit_role_balance( skip_reason = 'no_config' elif not cfg.enabled: skip_reason = 'disabled' - elif not _source_allowed(cfg, source_type): + elif not _source_allowed(cfg, source_type, role=role): skip_reason = f'source_not_in_scope:{source_type}' elif not biz_id: skip_reason = 'empty_biz_id' @@ -306,13 +318,68 @@ def _load_profile(role: str, user_id: str): def split_preview(club_id: Optional[str], role: str, amount, source_type: str = SOURCE_ORDER_SETTLE) -> Tuple[Decimal, Decimal]: """只算不写库:后台试算用。""" credit = _money(amount) + role = (role or '').strip().lower() cfg = get_freeze_config(club_id, role) - if not cfg or not cfg.enabled or not _source_allowed(cfg, source_type): + if not cfg or not cfg.enabled or not _source_allowed(cfg, source_type, role=role): return credit, Decimal('0.00') frozen = calc_freeze_amount(credit, cfg) return _money(credit - frozen), frozen +def credit_guanshi_fenhong( + *, + user_id: str, + amount, + biz_id: str, + profile=None, + order_id: str = '', + czjilu_id: str = '', + club_id: Optional[str] = None, + freeze_reason: str = '', +) -> Dict[str, Any]: + """管事分红入账:未开冻结=全额进 yue;开启则按配置拆冻结池。调用方须在 atomic 内。""" + cid = settle_freeze_club_id(user_id=str(user_id or '')) if not club_id else _club(club_id) + return credit_role_balance( + club_id=cid, + user_id=str(user_id), + role=ROLE_GUANSHI, + amount=amount, + source_type=SOURCE_FENHONG, + biz_id=biz_id, + profile=profile, + order_id=order_id or '', + czjilu_id=czjilu_id or '', + freeze_reason=freeze_reason or f'分红 {biz_id}', + ) + + +def credit_zuzhang_fenhong( + *, + user_id: str, + amount, + biz_id: str, + profile=None, + order_id: str = '', + czjilu_id: str = '', + club_id: Optional[str] = None, + freeze_reason: str = '', +) -> Dict[str, Any]: + """组长分红入账:未开冻结=全额进 ketixian_jine。调用方须在 atomic 内。""" + cid = settle_freeze_club_id(user_id=str(user_id or '')) if not club_id else _club(club_id) + return credit_role_balance( + club_id=cid, + user_id=str(user_id), + role=ROLE_ZUZHANG, + amount=amount, + source_type=SOURCE_FENHONG, + biz_id=biz_id, + profile=profile, + order_id=order_id or '', + czjilu_id=czjilu_id or '', + freeze_reason=freeze_reason or f'分红 {biz_id}', + ) + + def settle_freeze_club_id(order=None, user_id: str = '') -> str: """ 结算冻结规则按「入账人所属俱乐部」,不按订单俱乐部。 diff --git a/jituan/services/fund_freeze_admin.py b/jituan/services/fund_freeze_admin.py index d2b19bc..7948081 100644 --- a/jituan/services/fund_freeze_admin.py +++ b/jituan/services/fund_freeze_admin.py @@ -137,7 +137,8 @@ def save_config(request, data: dict, operator: str = '') -> Tuple[Optional[dict] source_scope = data.get('source_scope') if source_scope is None: - source_scope = ['order_settle'] + # 默认同时覆盖订单结算与分红(管事/组长主要靠分红入账) + source_scope = ['order_settle', 'fenhong'] if isinstance(source_scope, str): try: source_scope = json.loads(source_scope) diff --git a/jituan/services/member_recharge.py b/jituan/services/member_recharge.py index 5d7b3e0..7b6d289 100644 --- a/jituan/services/member_recharge.py +++ b/jituan/services/member_recharge.py @@ -23,6 +23,7 @@ from decimal import Decimal import defusedxml.ElementTree as ET import requests from django.db import transaction +from django.db.models import F from django.utils import timezone from backend.utils import ( @@ -538,23 +539,44 @@ def _apply_member_fenhong(order, huiyuan, formal_cishu=None, trial_guanshi_fc=No return user_main = dashou.user + dingdan_id = str(order.dingdan_id or '') + from jituan.services.fund_freeze import credit_guanshi_fenhong, credit_zuzhang_fenhong + if guanshi_fenhong > 0: - guanshi.chongzhifenrun += guanshi_fenhong - guanshi.yue += guanshi_fenhong - guanshi.jinrichongzhi += 1 - guanshi.jinyuechongzhi += 1 - guanshi.save(update_fields=[ - 'chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi', - ]) + result = credit_guanshi_fenhong( + user_id=str(guanshi_id), + amount=guanshi_fenhong, + biz_id=f'{dingdan_id}:guanshi:member', + profile=guanshi, + czjilu_id=dingdan_id, + freeze_reason=f'会员充值分红 {dingdan_id}', + ) + if result.get('idempotent'): + logger.warning('会员管事分红幂等跳过 dingdan=%s guanshi=%s', dingdan_id, guanshi_id) + else: + type(guanshi).query.filter(pk=guanshi.pk).update( + chongzhifenrun=F('chongzhifenrun') + guanshi_fenhong, + jinrichongzhi=F('jinrichongzhi') + 1, + jinyuechongzhi=F('jinyuechongzhi') + 1, + ) if zuzhang_fenhong > 0 and zuzhang: - zuzhang.fenyong_zonge += zuzhang_fenhong - zuzhang.ketixian_jine += zuzhang_fenhong - zuzhang.jinri_fenyong += zuzhang_fenhong - zuzhang.jinyue_fenyong += zuzhang_fenhong - zuzhang.save(update_fields=[ - 'fenyong_zonge', 'ketixian_jine', 'jinri_fenyong', 'jinyue_fenyong', - ]) + result = credit_zuzhang_fenhong( + user_id=str(zuzhang_id), + amount=zuzhang_fenhong, + biz_id=f'{dingdan_id}:zuzhang:member', + profile=zuzhang, + czjilu_id=dingdan_id, + freeze_reason=f'会员充值分红 {dingdan_id}', + ) + if result.get('idempotent'): + logger.warning('会员组长分红幂等跳过 dingdan=%s zuzhang=%s', dingdan_id, zuzhang_id) + else: + type(zuzhang).query.filter(pk=zuzhang.pk).update( + fenyong_zonge=F('fenyong_zonge') + zuzhang_fenhong, + jinri_fenyong=F('jinri_fenyong') + zuzhang_fenhong, + jinyue_fenyong=F('jinyue_fenyong') + zuzhang_fenhong, + ) Gsfenhong.query.create( dingdan_id=order.dingdan_id, diff --git a/orders/utils.py b/orders/utils.py index b2673de..ed2c7e2 100644 --- a/orders/utils.py +++ b/orders/utils.py @@ -182,10 +182,20 @@ def settle_shangjia_order_guanshi_fenhong(order, dashou_id): try: with transaction.atomic(): guanshi = UserGuanshi.objects.select_for_update().get(user__UserUID=guanshi_id) - UserGuanshi.objects.filter(id=guanshi.id).update( - yue=F('yue') + guanshi_fencheng, - chongzhifenrun=F('chongzhifenrun') + guanshi_fencheng, + from jituan.services.fund_freeze import credit_guanshi_fenhong + order_id = str(getattr(order, 'OrderID', '') or '') + result = credit_guanshi_fenhong( + user_id=str(guanshi_id), + amount=guanshi_fencheng, + biz_id=f'{order_id}:guanshi:merchant', + profile=guanshi, + order_id=order_id, + freeze_reason=f'商家订单分红 {order_id}', ) + if not result.get('idempotent'): + UserGuanshi.objects.filter(id=guanshi.id).update( + chongzhifenrun=F('chongzhifenrun') + guanshi_fencheng, + ) user_main = dashou.user Gsfenhong.objects.create( diff --git a/rank/reward_services.py b/rank/reward_services.py index 7575708..31ac0e0 100644 --- a/rank/reward_services.py +++ b/rank/reward_services.py @@ -946,7 +946,7 @@ def claim_reward(claim_id: int, user, request_shenfen: str = '') -> dict: amount = claim.reward_amount _write_claim_log(claim, uid, RankRewardClaimLog.ACTION_START, amount, '开始入账') - _credit_balance(claim.shenfen, uid, amount) + _credit_balance(claim.shenfen, uid, amount, biz_id=f'rank_claim:{claim.id}') claim.status = RankRewardClaim.STATUS_CLAIMED claim.claimed_at = timezone.now() @@ -962,17 +962,44 @@ def claim_reward(claim_id: int, user, request_shenfen: str = '') -> dict: } -def _credit_balance(shenfen: str, yonghuid: str, amount: Decimal): +def _credit_balance(shenfen: str, yonghuid: str, amount: Decimal, biz_id: str = ''): amount = Decimal(str(amount)) + biz = (biz_id or '').strip() or f'rank:{shenfen}:{yonghuid}:{amount}' if shenfen == 'dashou': + from jituan.services.fund_freeze import ( + credit_role_balance, settle_freeze_club_id, ROLE_DASHOU, SOURCE_FENHONG, + ) p = UserDashou.objects.select_for_update().get(user__UserUID=yonghuid) - UserDashou.objects.filter(pk=p.pk).update(yue=F('yue') + amount) + credit_role_balance( + club_id=settle_freeze_club_id(user_id=str(yonghuid)), + user_id=str(yonghuid), + role=ROLE_DASHOU, + amount=amount, + source_type=SOURCE_FENHONG, + biz_id=biz, + profile=p, + freeze_reason='排行榜奖励', + ) elif shenfen == 'guanshi': + from jituan.services.fund_freeze import credit_guanshi_fenhong p = UserGuanshi.objects.select_for_update().get(user__UserUID=yonghuid) - UserGuanshi.objects.filter(pk=p.pk).update(yue=F('yue') + amount) + credit_guanshi_fenhong( + user_id=str(yonghuid), + amount=amount, + biz_id=biz, + profile=p, + freeze_reason='排行榜奖励', + ) elif shenfen == 'zuzhang': + from jituan.services.fund_freeze import credit_zuzhang_fenhong p = UserZuzhang.objects.select_for_update().get(user__UserUID=yonghuid) - UserZuzhang.objects.filter(pk=p.pk).update(ketixian_jine=F('ketixian_jine') + amount) + credit_zuzhang_fenhong( + user_id=str(yonghuid), + amount=amount, + biz_id=biz, + profile=p, + freeze_reason='排行榜奖励', + ) elif shenfen == 'shangjia': p = UserShangjia.objects.select_for_update().get(user__UserUID=yonghuid) UserShangjia.objects.filter(pk=p.pk).update(yue=F('yue') + amount)