From 871144b53b271987b5196fb22c961cfea3a16ebe Mon Sep 17 00:00:00 2001 From: XingQue Date: Mon, 27 Jul 2026 08:18:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=84=E9=98=9F=E7=BB=93=E5=8D=95?= =?UTF-8?q?=E5=90=8E=E7=BD=9A=E6=AC=BE=E4=BB=8D=E6=8C=89=E9=98=9F=E5=91=98?= =?UTF-8?q?=E5=9D=87=E6=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 商家常在结单后申请罚款,招募已是结束态;现含已结束组队,并从结算流水兜底还原队员。 Co-authored-by: Cursor --- orders/services/team_recruit.py | 72 +++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/orders/services/team_recruit.py b/orders/services/team_recruit.py index a9eab1d..8fa9549 100644 --- a/orders/services/team_recruit.py +++ b/orders/services/team_recruit.py @@ -741,35 +741,75 @@ def settle_team_or_legacy(order, *, set_idle_leader=False) -> bool: return True -def maybe_split_penalties(*, order_id: str, total_amount, reason: str, applicant_id, applicant_identity, - affects_grabbing=1, club_id='', extra_fields=None) -> Optional[list]: +def _team_members_for_penalty_split(order_id: str): """ - 若有有效组队(锁定或招募中已有队友),为每人创建罚单并返回列表;否则返回 None 走原逻辑。 + 取本单应均摊罚款的组队成员。 + 含招募中/已锁定/已结束(商家多在结单后申请罚款;结单会把招募置为已结束)。 + 取消/作废的组队不均摊。 """ - from orders.models import TeamRecruit, TeamMember, Penalty - from decimal import Decimal as D + from orders.models import TeamRecruit, TeamMember, TeamSettleLedger - rec = TeamRecruit.query.filter( - order_id=order_id, - status__in=[TeamRecruit.STATUS_RECRUITING, TeamRecruit.STATUS_LOCKED], - ).first() + oid = (order_id or '').strip() + if not oid: + return None, [] + rec = ( + TeamRecruit.query.filter( + order_id=oid, + status__in=[ + TeamRecruit.STATUS_RECRUITING, + TeamRecruit.STATUS_LOCKED, + TeamRecruit.STATUS_FINISHED, + ], + ) + .order_by('-id') + .first() + ) if not rec: - return None + return None, [] members = list( TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE) ) - if len(members) < 2: + # 结单后偶发清退:用同单结算流水还原参与人 + if len(members) < 2 and int(rec.status or 0) == TeamRecruit.STATUS_FINISHED: + ledger_uids = list( + TeamSettleLedger.query.filter(order_id=oid) + .values_list('user_uid', flat=True) + .distinct() + ) + if len(ledger_uids) >= 2: + members = list(TeamMember.query.filter(recruit_id=rec.id, user_uid__in=ledger_uids)) + if len(members) < 2: + # 无成员行时,用流水 UID 构造轻量对象供均摊 + class _M: + def __init__(self, uid, role): + self.user_uid = uid + self.role = role + leader = rec.leader_uid or '' + members = [ + _M(u, TeamMember.ROLE_LEADER if u == leader else TeamMember.ROLE_MEMBER) + for u in ledger_uids + ] + return rec, members + + +def maybe_split_penalties(*, order_id: str, total_amount, reason: str, applicant_id, applicant_identity, + affects_grabbing=1, club_id='', extra_fields=None) -> Optional[list]: + """ + 若有组队且至少 2 人在队(含结单后已结束),为每人创建罚单并返回列表;否则返回 None 走原逻辑。 + """ + from orders.models import Penalty + from decimal import Decimal as D + + rec, members = _team_members_for_penalty_split(order_id) + if not rec or len(members) < 2: return None total = _d(total_amount) k = len(members) + # 队长优先拿尾差 + members.sort(key=lambda m: 0 if getattr(m, 'role', '') == 'leader' else 1) base = (total / D(k)).quantize(MONEY, rounding=ROUND_DOWN) amounts = [base] * k - amounts[0] = _d(total - base * (k - 1)) # 尾差给列表第一人(队长优先排前面) - - # 队长优先 - members.sort(key=lambda m: 0 if m.role == TeamMember.ROLE_LEADER else 1) - amounts = [base] * k amounts[0] = _d(total - base * (k - 1)) reason_text = f'【组队均摊】{reason or ""}'