fix: 强制结单组队分账+按打手俱乐部冻结

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-28 15:03:54 +08:00
parent 4414b74119
commit e91bf6c982
2 changed files with 128 additions and 8 deletions

View File

@@ -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:

View File

@@ -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': '订单不存在'})