From e91bf6c9821cafd5257b07ef3bd6d823c0bdfa4d Mon Sep 17 00:00:00 2001 From: XingQue Date: Tue, 28 Jul 2026 15:03:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BC=BA=E5=88=B6=E7=BB=93=E5=8D=95?= =?UTF-8?q?=E7=BB=84=E9=98=9F=E5=88=86=E8=B4=A6+=E6=8C=89=E6=89=93?= =?UTF-8?q?=E6=89=8B=E4=BF=B1=E4=B9=90=E9=83=A8=E5=86=BB=E7=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- jituan/services/fund_freeze.py | 64 ++++++++++++++++++++++++++++-- users/views/kefu_orders.py | 72 +++++++++++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/jituan/services/fund_freeze.py b/jituan/services/fund_freeze.py index 36d6f28..ea1087b 100644 --- a/jituan/services/fund_freeze.py +++ b/jituan/services/fund_freeze.py @@ -197,8 +197,32 @@ def credit_role_balance( freeze_on = bool( cfg and cfg.enabled and _source_allowed(cfg, source_type) and biz_id ) + skip_reason = '' + if not freeze_on: + if not cfg: + skip_reason = 'no_config' + elif not cfg.enabled: + skip_reason = 'disabled' + elif not _source_allowed(cfg, source_type): + skip_reason = f'source_not_in_scope:{source_type}' + elif not biz_id: + skip_reason = 'empty_biz_id' + else: + skip_reason = 'unknown' + logger.info( + 'fund_freeze skip club=%s role=%s source=%s biz=%s user=%s amount=%s reason=%s', + cid, role, source_type, biz_id, user_id, credit, skip_reason, + ) + frozen = calc_freeze_amount(credit, cfg) if freeze_on else Decimal('0.00') available = _money(credit - frozen) + if freeze_on and frozen <= 0: + skip_reason = 'calc_zero' + logger.info( + 'fund_freeze calc_zero club=%s role=%s source=%s biz=%s amount=%s mode=%s rate=%s', + cid, role, source_type, biz_id, credit, + getattr(cfg, 'freeze_mode', None), getattr(cfg, 'freeze_rate', None), + ) # 幂等:已存在同 biz 冻结单则只补可提现差额不重复冻(极端重入) if freeze_on and frozen > 0: @@ -216,6 +240,7 @@ def credit_role_balance( 'ledger_id': existed.id, 'skipped_freeze': True, 'idempotent': True, + 'skip_reason': 'idempotent', } _add_withdrawable(role, profile, available, also_zonge=also_credit_zonge) @@ -245,12 +270,19 @@ def credit_role_balance( next_eligible_at=next_at, ) ledger_id = ledger.id + logger.info( + 'fund_freeze applied club=%s role=%s source=%s biz=%s user=%s ' + 'credit=%s available=%s frozen=%s ledger=%s', + cid, role, source_type, biz_id, user_id, credit, available, frozen, ledger_id, + ) return { 'available': available, 'frozen': frozen, 'ledger_id': ledger_id, 'skipped_freeze': frozen <= 0, + 'skip_reason': skip_reason or None, + 'club_id': cid, } @@ -281,8 +313,33 @@ def split_preview(club_id: Optional[str], role: str, amount, source_type: str = return _money(credit - frozen), frozen -def order_club_id(order) -> str: - return _club(getattr(order, 'ClubID', None) or getattr(order, 'club_id', None)) +def settle_freeze_club_id(order=None, user_id: str = '') -> str: + """ + 结算冻结规则按「入账人所属俱乐部」,不按订单俱乐部。 + + 跨店抢单、跨平台商家发单时,订单 ClubID 可能是对方店; + 分成进的是接单打手钱包,必须用打手 User.ClubID 取冻结配置。 + 仅当打手无归属时,才回退订单 ClubID / 默认俱乐部。 + """ + uid = (user_id or '').strip() + if uid: + try: + from users.business_models import User + u = User.query.filter(UserUID=uid).only('ClubID').first() + if u and getattr(u, 'ClubID', None) and str(u.ClubID).strip(): + return _club(u.ClubID) + except Exception as e: + logger.warning('settle_freeze_club_id user ClubID failed uid=%s: %s', uid, e) + if order is not None: + cid = getattr(order, 'ClubID', None) or getattr(order, 'club_id', None) + if cid and str(cid).strip(): + return _club(cid) + return _club(None) + + +def order_club_id(order, user_id: str = '') -> str: + """兼容旧名:实际按打手所属俱乐部解析冻结配置俱乐部。""" + return settle_freeze_club_id(order, user_id=user_id) def credit_dashou_order_settle( @@ -299,6 +356,7 @@ def credit_dashou_order_settle( 打手订单结算专用入账(P1)。 - 可提现走 credit_role_balance(未开冻结=全额进 yue,与改造前一致) + - 冻结配置按打手所属俱乐部(跨店单不按订单 ClubID) - zonge / 今日今月收益仍按「全额」累加(统计口径不变) - 成交单量 +1、可选置空闲状态 - settle_biz_id:组队多人分账时传入 `{order_id}:team:{uid}`,避免同单幂等撞键 @@ -308,7 +366,7 @@ def credit_dashou_order_settle( credit = _money(amount) order_id = str(getattr(order, 'OrderID', None) or getattr(order, 'order_id', '') or '') - cid = order_club_id(order) + cid = settle_freeze_club_id(order, user_id=str(user_id or '')) biz_id = (settle_biz_id or order_id or '').strip() if credit <= 0: diff --git a/users/views/kefu_orders.py b/users/views/kefu_orders.py index ffbd226..8f3cd6e 100644 --- a/users/views/kefu_orders.py +++ b/users/views/kefu_orders.py @@ -1803,19 +1803,81 @@ class KefuForceCompleteView(APIView): if not dashou_fencheng or dashou_fencheng <= 0: return Response({'code': 400, 'msg': '打手分成金额无效'}) - # 5. 给打手结算 + # 5. 给打手结算(组队分账优先;均走冻结入账网关) try: dashou_user = User.query.get(UserUID=dashou_id) dashou_profile = dashou_user.DashouProfile + except (User.DoesNotExist, AttributeError): + return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'}) + + freeze_result = { + 'available': str(dashou_fencheng), + 'frozen': '0.00', + 'skipped_freeze': True, + 'team_settled': False, + } + team_done = False + try: + from orders.services.team_recruit import ( + settle_team_or_legacy, get_active_recruit, lock_recruit, team_should_split, + ) + from orders.models import TeamRecruit, TeamSettleLedger + rec = get_active_recruit(order.OrderID) + if rec and rec.status == TeamRecruit.STATUS_RECRUITING: + lock_recruit(rec.id, actor_uid='system', reason='force_settle_lock') + team_done = settle_team_or_legacy(order, set_idle_leader=True) + except Exception as e: + logger.error('强制结单组队分账异常 %s: %s', dingdan_id, e, exc_info=True) + from orders.models import TeamSettleLedger + # 必须抛出让 atomic 回滚,禁止 return 提交半成品入账 + if TeamSettleLedger.query.filter(order_id=dingdan_id).exists(): + raise RuntimeError(f'组队分账异常且已有入账记录: {e}') from e + try: + from orders.services.team_recruit import team_should_split, get_active_recruit + if get_active_recruit(dingdan_id) or team_should_split(order): + raise RuntimeError(f'组队分账失败: {e}') from e + except RuntimeError: + raise + except Exception: + pass + team_done = False + + if team_done: + freeze_result['team_settled'] = True + freeze_result['skipped_freeze'] = False + logger.info('强制结单组队分账完成 order=%s', dingdan_id) + else: + try: + from orders.services.team_recruit import get_active_recruit + if get_active_recruit(dingdan_id): + raise RuntimeError('存在有效组队但分账未完成,已阻止全额打给队长') + except RuntimeError: + raise + except Exception: + pass from jituan.services.fund_freeze import credit_dashou_order_settle - credit_dashou_order_settle( + settle_ret = credit_dashou_order_settle( order=order, dashou_profile=dashou_profile, user_id=dashou_id, amount=dashou_fencheng, ) - except (User.DoesNotExist, AttributeError): - return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'}) + freeze_result = { + 'available': str(settle_ret.get('available') or 0), + 'frozen': str(settle_ret.get('frozen') or 0), + 'skipped_freeze': bool(settle_ret.get('skipped_freeze')), + 'skip_reason': settle_ret.get('skip_reason'), + 'club_id': settle_ret.get('club_id'), + 'ledger_id': settle_ret.get('ledger_id'), + 'team_settled': False, + } + logger.info( + '强制结单打手入账 order=%s dashou=%s available=%s frozen=%s skip=%s reason=%s club=%s', + dingdan_id, dashou_id, + freeze_result['available'], freeze_result['frozen'], + freeze_result['skipped_freeze'], freeze_result.get('skip_reason'), + freeze_result.get('club_id'), + ) # 6. 如果是商家发单,更新商家成交订单数量 if order.Platform == 2: @@ -1910,7 +1972,7 @@ class KefuForceCompleteView(APIView): kefu.zongchuli = F('zongchuli') + 1 kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli']) - return Response({'code': 0, 'msg': '强制结单成功'}) + return Response({'code': 0, 'msg': '强制结单成功', 'data': freeze_result}) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'})