Files
Django/orders/services/team_recruit.py
XingQue f59ae2416f feat: 打手订单列表组队标记,队友也可看单;详情返回队友昵称
列表附 has_team/角色/人数;组队在队成员纳入订单列表查询。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 03:33:35 +08:00

759 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""组队招募核心服务(新域)。无有效组队时所有挂钩函数立即返回,不影响旧逻辑。"""
from __future__ import annotations
import logging
from decimal import Decimal, ROUND_DOWN
from typing import Optional
from django.db import transaction
from django.db.models import F
from django.utils import timezone
logger = logging.getLogger(__name__)
MONEY = Decimal('0.01')
def _d(v) -> Decimal:
try:
return Decimal(str(v or 0)).quantize(MONEY)
except Exception:
return Decimal('0.00')
def _log(club_id, order_id, recruit_id, actor, action, detail=''):
try:
from orders.models import TeamActionLog
TeamActionLog(
club_id=(club_id or '')[:16],
order_id=(order_id or '')[:32],
recruit_id=recruit_id,
actor_uid=(actor or '')[:32],
action=(action or '')[:32],
detail=(detail or '')[:2000],
).save()
except Exception as e:
logger.warning('TeamActionLog 失败: %s', e)
def calc_display_share(commission: Decimal, mode: str, need_count: int, per_person: Decimal) -> Decimal:
c = _d(commission)
need = max(1, min(3, int(need_count or 1)))
if mode == 'fixed':
return _d(per_person)
# average: C / (1+need)
n = 1 + need
if n <= 0 or c <= 0:
return Decimal('0.00')
return (c / Decimal(n)).quantize(MONEY, rounding=ROUND_DOWN)
def calc_lock_shares(commission: Decimal, mode: str, per_person: Decimal, leader_uid: str, member_uids: list) -> dict:
"""返回 {uid: amount},合计等于 C尾差给队长。member_uids 不含队长。"""
c = _d(commission)
members = [u for u in (member_uids or []) if u and u != leader_uid]
n = 1 + len(members)
out = {}
if n <= 0 or c <= 0:
return {leader_uid: c} if leader_uid else {}
if mode == 'fixed':
p = _d(per_person)
for uid in members:
out[uid] = p
out[leader_uid] = _d(c - p * len(members))
if out[leader_uid] < 0:
out[leader_uid] = Decimal('0.00')
return out
base = (c / Decimal(n)).quantize(MONEY, rounding=ROUND_DOWN)
for uid in members:
out[uid] = base
out[leader_uid] = _d(c - base * len(members))
return out
def get_active_recruit(order_id: str):
from orders.models import TeamRecruit
return TeamRecruit.query.filter(
order_id=order_id,
status__in=[TeamRecruit.STATUS_RECRUITING, TeamRecruit.STATUS_LOCKED],
).first()
def team_should_split(order) -> bool:
"""结单是否走组队分账已锁定或招募中但已有人——进8会强制锁且有在队成员。"""
from orders.models import TeamRecruit, TeamMember
oid = getattr(order, 'OrderID', None) or ''
if not oid:
return False
rec = TeamRecruit.query.filter(
order_id=oid,
status=TeamRecruit.STATUS_LOCKED,
).first()
if not rec:
return False
return TeamMember.query.filter(
recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE,
).exists()
def _set_dashou_busy(uid: str, busy: bool):
from users.models import UserDashou
from users.business_models import User
u = User.query.filter(UserUID=uid).first()
if not u:
return
UserDashou.objects.filter(user=u).update(zhuangtai=0 if busy else 1)
def _subscribe_uids_to_order_group(order_id: str, uids: list):
"""订阅 Ds{uid} 到 group_{order_id}。失败只打日志。"""
try:
from utils.chat_utils import _subscribe_users_to_group, _get_self_goeasy_appkey, _get_self_goeasy_secret
appkey = _get_self_goeasy_appkey()
secret = _get_self_goeasy_secret()
if not appkey:
return False
user_ids = [f'Ds{u}' for u in uids if u]
if not user_ids:
return False
return _subscribe_users_to_group(user_ids, [f'group_{order_id}'], appkey, secret)
except Exception as e:
logger.warning('组队拉群失败 order=%s: %s', order_id, e)
return False
@transaction.atomic
def open_recruit(
*,
order,
leader_uid: str,
need_count: int,
share_mode: str,
per_person_amount=None,
remark: str = '',
leader_game_id: str = '',
min_deal_rate=None,
max_fine_rate=None,
):
from orders.models import TeamRecruit, TeamMember
oid = order.OrderID
club_id = (getattr(order, 'ClubID', None) or '').strip() or 'xq'
if int(order.Status or 0) != 2:
raise ValueError('仅进行中订单可开启组队')
if (order.PlayerID or '') != leader_uid:
raise ValueError('仅接单打手可开启组队')
if TeamRecruit.query.filter(order_id=oid).exclude(
status__in=[TeamRecruit.STATUS_CANCELLED, TeamRecruit.STATUS_VOID, TeamRecruit.STATUS_FINISHED]
).exists():
raise ValueError('本单已有进行中的组队')
need = int(need_count or 0)
if need < 1 or need > 3:
raise ValueError('招募人数须为13')
mode = (share_mode or '').strip().lower()
if mode not in (TeamRecruit.MODE_FIXED, TeamRecruit.MODE_AVERAGE):
raise ValueError('请选择固定值或平均值')
c = _d(order.PlayerCommission)
if c <= 0:
raise ValueError('打手分成无效')
p = _d(per_person_amount)
if mode == TeamRecruit.MODE_FIXED:
if p <= 0:
raise ValueError('请设置固定人均金额')
if p * (1 + need) > c:
raise ValueError('人均×(队长+目标人数)不能超过打手分成')
game_id = (leader_game_id or '').strip()
if not game_id:
raise ValueError('请填写队长游戏ID')
display = calc_display_share(c, mode, need, p)
now = timezone.now()
rec = TeamRecruit(
club_id=club_id,
order_id=oid,
leader_uid=leader_uid,
status=TeamRecruit.STATUS_RECRUITING,
need_count=need,
joined_count=0,
share_mode=mode,
per_person_amount=p if mode == TeamRecruit.MODE_FIXED else Decimal('0.00'),
display_share_amount=display,
commission_snap=c,
order_amount_snap=_d(order.Amount),
remark=(remark or '')[:200],
leader_game_id=game_id[:64],
min_deal_rate=_d(min_deal_rate) if min_deal_rate not in (None, '') else None,
max_fine_rate=_d(max_fine_rate) if max_fine_rate not in (None, '') else None,
opened_at=now,
)
# Decimal null fields
if min_deal_rate in (None, ''):
rec.min_deal_rate = None
if max_fine_rate in (None, ''):
rec.max_fine_rate = None
rec.save()
TeamMember(
club_id=club_id,
recruit_id=rec.id,
order_id=oid,
user_uid=leader_uid,
role=TeamMember.ROLE_LEADER,
game_id=game_id[:64],
share_amount=Decimal('0.00'),
join_status=TeamMember.JOIN_ACTIVE,
joined_at=now,
).save()
_subscribe_uids_to_order_group(oid, [leader_uid])
_log(club_id, oid, rec.id, leader_uid, 'open', f'mode={mode} need={need} display={display}')
return rec
@transaction.atomic
def update_recruit(*, recruit_id: int, leader_uid: str, need_count=None, remark=None,
min_deal_rate='__omit__', max_fine_rate='__omit__'):
from orders.models import TeamRecruit, Order
rec = TeamRecruit.objects.select_for_update().filter(id=recruit_id).first()
if not rec:
raise ValueError('组队不存在')
if rec.leader_uid != leader_uid:
raise ValueError('仅队长可修改')
if rec.status != TeamRecruit.STATUS_RECRUITING:
raise ValueError('仅招募中可修改')
order = Order.query.filter(OrderID=rec.order_id).first()
if not order or int(order.Status or 0) != 2:
raise ValueError('订单非进行中,无法修改招募')
if need_count is not None:
need = int(need_count)
if need < 1 or need > 3:
raise ValueError('招募人数须为13')
if need < int(rec.joined_count or 0):
raise ValueError('目标人数不能小于已入队人数')
if rec.share_mode == TeamRecruit.MODE_FIXED:
p = _d(rec.per_person_amount)
c = _d(order.PlayerCommission) or _d(rec.commission_snap)
if p * (1 + need) > c:
raise ValueError('人均×(队长+目标人数)不能超过打手分成')
rec.need_count = need
if remark is not None:
rec.remark = (remark or '')[:200]
if min_deal_rate != '__omit__':
rec.min_deal_rate = _d(min_deal_rate) if min_deal_rate not in (None, '') else None
if max_fine_rate != '__omit__':
rec.max_fine_rate = _d(max_fine_rate) if max_fine_rate not in (None, '') else None
c = _d(order.PlayerCommission) or _d(rec.commission_snap)
rec.display_share_amount = calc_display_share(
c, rec.share_mode, rec.need_count, rec.per_person_amount,
)
rec.save()
_log(rec.club_id, rec.order_id, rec.id, leader_uid, 'update', f'need={rec.need_count}')
return rec
@transaction.atomic
def lock_recruit(rec_or_id, *, actor_uid='', reason='manual'):
from orders.models import TeamRecruit, TeamMember, Order
rid = rec_or_id if isinstance(rec_or_id, int) else rec_or_id.id
rec = TeamRecruit.objects.select_for_update().filter(id=rid).first()
if not rec:
return None
if rec.status == TeamRecruit.STATUS_LOCKED:
return rec
if rec.status != TeamRecruit.STATUS_RECRUITING:
raise ValueError('当前状态不可锁定')
order = Order.query.filter(OrderID=rec.order_id).first()
c = _d(getattr(order, 'PlayerCommission', None) if order else None) or _d(rec.commission_snap)
members = list(
TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE)
.exclude(role=TeamMember.ROLE_LEADER)
.values_list('user_uid', flat=True)
)
shares = calc_lock_shares(c, rec.share_mode, rec.per_person_amount, rec.leader_uid, list(members))
now = timezone.now()
for uid, amt in shares.items():
TeamMember.objects.filter(
recruit_id=rec.id, user_uid=uid, join_status=TeamMember.JOIN_ACTIVE,
).update(share_amount=amt, UpdateTime=now)
rec.status = TeamRecruit.STATUS_LOCKED
rec.locked_at = now
rec.display_share_amount = shares.get(
next(iter(members), rec.leader_uid),
calc_display_share(c, rec.share_mode, rec.need_count, rec.per_person_amount),
)
# 队友展示用:取任一队友份额,无队友则用队长外均分预览
if members:
rec.display_share_amount = shares.get(members[0], Decimal('0.00'))
rec.save()
_log(rec.club_id, rec.order_id, rec.id, actor_uid, 'lock', reason)
return rec
def on_order_enter_status_8(order):
"""进结算中:招募中强制锁定。不改 eligible。"""
try:
from orders.models import TeamRecruit
oid = getattr(order, 'OrderID', '') or ''
rec = TeamRecruit.query.filter(order_id=oid, status=TeamRecruit.STATUS_RECRUITING).first()
if rec:
lock_recruit(rec.id, actor_uid='system', reason='enter_status_8')
except Exception as e:
logger.error('组队进8锁定失败: %s', e)
@transaction.atomic
def join_recruit(*, recruit_id: int, user_uid: str, game_id: str, request=None, require_club_id=None):
from orders.models import TeamRecruit, TeamMember, Order
from users.models import UserDashou
from utils.fadan_utils import check_fadan_qiangdan_eligible
game_id = (game_id or '').strip()
if not game_id:
raise ValueError('请填写游戏ID')
rec = TeamRecruit.objects.select_for_update().filter(id=recruit_id).first()
if not rec:
raise ValueError('组队不存在')
# 组队强制本俱乐部:不认跨店互通抢单
if require_club_id and (rec.club_id or '') != require_club_id:
raise ValueError('仅本俱乐部可加入组队')
if rec.status != TeamRecruit.STATUS_RECRUITING:
raise ValueError('招募已结束')
if int(rec.joined_count or 0) >= int(rec.need_count or 0):
raise ValueError('人数已满')
if rec.leader_uid == user_uid:
raise ValueError('队长无需加入')
if TeamMember.query.filter(recruit_id=rec.id, user_uid=user_uid, join_status=TeamMember.JOIN_ACTIVE).exists():
raise ValueError('您已在队中')
order = Order.query.filter(OrderID=rec.order_id).first()
if not order or int(order.Status or 0) != 2:
raise ValueError('订单状态不可加入')
if (getattr(order, 'ClubID', '') or '') != rec.club_id:
raise ValueError('俱乐部不匹配')
if require_club_id and (order.ClubID or '') != require_club_id:
raise ValueError('仅本俱乐部订单可加入组队')
# 忙碌 / 罚单
try:
from users.business_models import User
user_obj = User.query.filter(UserUID=user_uid).first()
if not user_obj:
raise ValueError('用户不存在')
profile = UserDashou.query.filter(user=user_obj).first()
if not profile:
raise ValueError('非打手身份')
if int(getattr(profile, 'zhuangtai', 0) or 0) != 1:
raise ValueError('您有订单正在接待中')
if int(getattr(profile, 'zhanghaozhuangtai', 1) or 1) != 1:
raise ValueError('账号状态异常')
except ValueError:
raise
except Exception:
raise ValueError('打手信息异常')
ok, msg = check_fadan_qiangdan_eligible(user_uid, 1)
if not ok:
raise ValueError(msg or '存在未处理罚单')
# 不能同时在其它进行中组队
busy_other = TeamMember.query.filter(
user_uid=user_uid,
join_status=TeamMember.JOIN_ACTIVE,
role=TeamMember.ROLE_MEMBER,
).exclude(order_id=rec.order_id)
if busy_other.exists():
# 仅当对应帖仍 recruiting/locked
from orders.models import TeamRecruit as TR
for m in busy_other[:5]:
r = TR.query.filter(id=m.recruit_id, status__in=[TR.STATUS_RECRUITING, TR.STATUS_LOCKED]).first()
if r:
raise ValueError('您已在其他组队中')
# 可选指标门槛
if rec.min_deal_rate is not None or rec.max_fine_rate is not None:
from jituan.models import DashouDealStat
from jituan.services.deal_stats import serialize_dashou_stat
row = DashouDealStat.query.filter(club_id=rec.club_id, dashou_uid=user_uid).first()
deal_rate = 0.0
fine_rate = 0.0
if row and int(row.order_count or 0) > 0:
s = serialize_dashou_stat(row)
deal_rate = float(s.get('deal_rate') or 0)
fine_rate = float(s.get('fined_rate') or 0)
if rec.min_deal_rate is not None and deal_rate < float(rec.min_deal_rate):
raise ValueError('成交率未达到队长要求')
if rec.max_fine_rate is not None and fine_rate > float(rec.max_fine_rate):
raise ValueError('罚款率超过队长要求')
# 原单会员/押金
if order.GrabRequirement == 1:
from jituan.services.huiyuan_bundle import user_has_huiyuan_access_any, resolve_order_membership_ids
required_ids = resolve_order_membership_ids({
'MembershipID': order.MembershipID,
'GrabRequirement': order.GrabRequirement,
'ProductTypeID': order.ProductTypeID,
})
if not user_has_huiyuan_access_any(user_uid, required_ids, club_id=rec.club_id):
raise ValueError('未开通对应会员或会员已过期')
elif order.GrabRequirement == 2:
yajin_required = order.CommissionReq or 0
if (profile.yajin or 0) < yajin_required:
raise ValueError(f'押金不足,需要{yajin_required}')
now = timezone.now()
TeamMember(
club_id=rec.club_id,
recruit_id=rec.id,
order_id=rec.order_id,
user_uid=user_uid,
role=TeamMember.ROLE_MEMBER,
game_id=game_id[:64],
share_amount=_d(rec.display_share_amount) if rec.share_mode == TeamRecruit.MODE_FIXED else Decimal('0.00'),
join_status=TeamMember.JOIN_ACTIVE,
joined_at=now,
).save()
TeamRecruit.objects.filter(id=rec.id).update(joined_count=F('joined_count') + 1, UpdateTime=now)
rec.refresh_from_db()
_set_dashou_busy(user_uid, True)
_subscribe_uids_to_order_group(rec.order_id, [user_uid, rec.leader_uid])
_log(rec.club_id, rec.order_id, rec.id, user_uid, 'join', game_id)
if int(rec.joined_count or 0) >= int(rec.need_count or 0):
lock_recruit(rec.id, actor_uid=user_uid, reason='full')
rec.refresh_from_db()
return rec
@transaction.atomic
def clear_recruit(*, order_id: str, status: int, actor_uid='system', reason=''):
"""取消/作废清退成员、置空闲、零分账。status=4取消或5作废。"""
from orders.models import TeamRecruit, TeamMember
rec = TeamRecruit.objects.select_for_update().filter(order_id=order_id).exclude(
status__in=[TeamRecruit.STATUS_FINISHED, TeamRecruit.STATUS_CANCELLED, TeamRecruit.STATUS_VOID],
).first()
if not rec:
return None
members = list(
TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE).values_list('user_uid', 'role')
)
now = timezone.now()
TeamMember.objects.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE).update(
join_status=TeamMember.JOIN_CLEARED, UpdateTime=now,
)
for uid, role in members:
if role == TeamMember.ROLE_MEMBER:
_set_dashou_busy(uid, False)
# 队长忙碌由订单主流程管理,此处不强制改队长
rec.status = status
rec.closed_at = now
rec.save(update_fields=['status', 'closed_at', 'UpdateTime'])
_log(rec.club_id, order_id, rec.id, actor_uid, 'clear', f'{reason} ->{status}')
return rec
def on_order_refunded(order):
try:
from orders.models import TeamRecruit
clear_recruit(order_id=order.OrderID, status=TeamRecruit.STATUS_CANCELLED, reason='refund')
except Exception as e:
logger.error('组队退款清退失败: %s', e)
def on_order_change_player(order_id: str, actor_uid='system'):
try:
from orders.models import TeamRecruit
clear_recruit(order_id=order_id, status=TeamRecruit.STATUS_VOID, actor_uid=actor_uid, reason='change_player')
except Exception as e:
logger.error('组队换打手作废失败: %s', e)
@transaction.atomic
def settle_team_or_legacy(order, *, set_idle_leader=False) -> bool:
"""
若应组队分账则多人入账并返回 True否则返回 False 由调用方走原逻辑。
调用方须已在 atomic 且 ideally select_for_update 订单。
"""
from orders.models import TeamRecruit, TeamMember, TeamSettleLedger, Order
from users.business_models import User
from jituan.services.fund_freeze import credit_dashou_order_settle
oid = order.OrderID
if not team_should_split(order):
return False
rec = TeamRecruit.objects.select_for_update().filter(
order_id=oid, status=TeamRecruit.STATUS_LOCKED,
).first()
if not rec:
return False
members = list(
TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE)
)
if not members:
return False
c = _d(order.PlayerCommission)
# 若份额未写齐,按锁定规则重算
if any(_d(m.share_amount) <= 0 and m.role == TeamMember.ROLE_MEMBER for m in members) or \
sum((_d(m.share_amount) for m in members), Decimal('0')) <= 0:
mate_uids = [m.user_uid for m in members if m.role == TeamMember.ROLE_MEMBER]
shares = calc_lock_shares(c, rec.share_mode, rec.per_person_amount, rec.leader_uid, mate_uids)
for m in members:
m.share_amount = shares.get(m.user_uid, Decimal('0.00'))
m.save(update_fields=['share_amount', 'UpdateTime'])
total = sum((_d(m.share_amount) for m in members), Decimal('0.00'))
if total != c:
# 尾差修正到队长
diff = _d(c - total)
for m in members:
if m.role == TeamMember.ROLE_LEADER:
m.share_amount = _d(m.share_amount) + diff
m.save(update_fields=['share_amount', 'UpdateTime'])
break
for m in members:
amt = _d(m.share_amount)
biz_id = f'{oid}:team:{m.user_uid}'
if TeamSettleLedger.query.filter(biz_id=biz_id).exists():
continue
try:
user = User.query.get(UserUID=m.user_uid)
profile = user.DashouProfile
except Exception:
logger.error('组队分账打手不存在 %s %s', oid, m.user_uid)
continue
credit_dashou_order_settle(
order=order,
dashou_profile=profile,
user_id=m.user_uid,
amount=amt,
set_idle_status=True,
increment_chengjiao=True,
settle_biz_id=biz_id,
)
TeamSettleLedger(
club_id=rec.club_id,
order_id=oid,
user_uid=m.user_uid,
amount=amt,
biz_id=biz_id,
).save()
now = timezone.now()
rec.status = TeamRecruit.STATUS_FINISHED
rec.closed_at = now
rec.save(update_fields=['status', 'closed_at', 'UpdateTime'])
_log(rec.club_id, oid, rec.id, 'system', 'settle', f'total={c}')
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]:
"""
若有有效组队(锁定或招募中已有队友),为每人创建罚单并返回列表;否则返回 None 走原逻辑。
"""
from orders.models import TeamRecruit, TeamMember, Penalty
from decimal import Decimal as D
rec = TeamRecruit.query.filter(
order_id=order_id,
status__in=[TeamRecruit.STATUS_RECRUITING, TeamRecruit.STATUS_LOCKED],
).first()
if not rec:
return None
members = list(
TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE)
)
if len(members) < 2:
return None
total = _d(total_amount)
k = len(members)
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 ""}'
created = []
cid = (club_id or rec.club_id or 'xq')[:16]
for i, m in enumerate(members):
p = Penalty(
PenalizedUserID=m.user_uid,
ApplicantID=applicant_id,
Identity=1,
Reason=reason_text,
FineAmount=amounts[i],
RelatedOrderID=order_id,
Status=1,
AffectsGrabbing=affects_grabbing,
ApplicantIdentity=applicant_identity,
ClubID=cid,
)
p.save()
created.append(p)
# 调用方负责 lock_penalty_bonus / 证据图;此处只创建罚单记录
_log(rec.club_id, order_id, rec.id, applicant_id or '', 'penalty_split', f'n={k} total={total}')
return created
def serialize_recruit_public(rec, order=None, *, include_game_ids=False) -> dict:
"""大厅公开字段游戏ID默认不返回。"""
from orders.models import Order, MerchantOrderExt
from jituan.services.order_deal import order_auto_settle_payload
from jituan.models import DashouDealStat
from jituan.services.deal_stats import serialize_dashou_stat
from users.business_models import User
from backend.utils import fmt_datetime
order = order or Order.query.filter(OrderID=rec.order_id).first()
need = int(rec.need_count or 0)
joined = int(rec.joined_count or 0)
data = {
'recruit_id': rec.id,
'order_id': rec.order_id,
'club_id': rec.club_id,
'status': rec.status,
'need_count': need,
'joined_count': joined,
'remain_count': max(0, need - joined),
'share_mode': rec.share_mode,
'display_share_amount': float(_d(rec.display_share_amount)),
'per_person_amount': float(_d(rec.per_person_amount)),
'remark': rec.remark or '',
'leader_uid': rec.leader_uid,
'leader_nicheng': '',
'min_deal_rate': float(rec.min_deal_rate) if rec.min_deal_rate is not None else None,
'max_fine_rate': float(rec.max_fine_rate) if rec.max_fine_rate is not None else None,
'opened_at': fmt_datetime(rec.opened_at) if rec.opened_at else '',
'locked_at': fmt_datetime(rec.locked_at) if rec.locked_at else '',
'fadanpingtai': getattr(order, 'Platform', None) if order else None,
'fadanpingtai_text': '',
'leixing_id': getattr(order, 'ProductTypeID', None) if order else None,
'jieshao': (order.Description or '') if order else '',
'tupian': (order.ImageURL or '') if order else '',
'order_zhuangtai': getattr(order, 'Status', None) if order else None,
'shangjia_nicheng': '',
'leader_deal_stat': None,
}
if order:
data['fadanpingtai_text'] = '平台单' if order.Platform == 1 else ('商家单' if order.Platform == 2 else '')
if order.Platform == 2:
try:
ext = order.shangjia_kuozhan
data['shangjia_nicheng'] = ext.MerchantNickname or ''
except Exception:
pass
# 自动结算字段:仅锁定且订单结算中
if rec.status == 2 and int(order.Status or 0) == 8:
data.update(order_auto_settle_payload(order))
try:
u = User.query.filter(UserUID=rec.leader_uid).select_related('DashouProfile').first()
if u and getattr(u, 'DashouProfile', None):
data['leader_nicheng'] = u.DashouProfile.nicheng or ''
except Exception:
pass
row = DashouDealStat.query.filter(club_id=rec.club_id, dashou_uid=rec.leader_uid).first()
if row and int(row.order_count or 0) > 0:
s = serialize_dashou_stat(row)
data['leader_deal_stat'] = {
'deal_rate': s['deal_rate'],
'fined_rate': s['fined_rate'],
'deal_rate_text': f"{round(s['deal_rate'] * 100, 1)}%",
'fined_rate_text': f"{round(s['fined_rate'] * 100, 1)}%",
}
if include_game_ids:
from orders.models import TeamMember
from users.business_models import User
mems = list(TeamMember.query.filter(recruit_id=rec.id, join_status=1))
uids = [m.user_uid for m in mems]
nick_map = {}
if uids:
for u in User.query.filter(UserUID__in=uids).select_related('DashouProfile'):
try:
nick_map[u.UserUID] = (u.DashouProfile.nicheng or '') if getattr(u, 'DashouProfile', None) else ''
except Exception:
nick_map[u.UserUID] = ''
data['members'] = [
{
'user_uid': m.user_uid,
'role': m.role,
'role_text': '队长' if m.role == 'leader' else '队友',
'nicheng': nick_map.get(m.user_uid, ''),
'game_id': m.game_id,
'share_amount': float(_d(m.share_amount)),
}
for m in mems
]
return data
def batch_team_list_flags(order_ids: list, viewer_uid: str = '') -> dict:
"""订单列表用:{order_id: {has_team, team_status, ...}}不含游戏ID。"""
from orders.models import TeamRecruit, TeamMember
if not order_ids:
return {}
recs = list(
TeamRecruit.query.filter(
order_id__in=list(order_ids),
status__in=[
TeamRecruit.STATUS_RECRUITING,
TeamRecruit.STATUS_LOCKED,
TeamRecruit.STATUS_FINISHED,
],
)
)
if not recs:
return {}
# 一单一帖取最新
best = {}
for r in recs:
prev = best.get(r.order_id)
if not prev or r.id > prev.id:
best[r.order_id] = r
rids = [r.id for r in best.values()]
my_roles = {}
if viewer_uid and rids:
for m in TeamMember.query.filter(
recruit_id__in=rids, user_uid=viewer_uid, join_status=TeamMember.JOIN_ACTIVE,
):
my_roles[m.recruit_id] = m.role
out = {}
status_text = {1: '招募中', 2: '已锁定', 3: '已结束'}
for oid, r in best.items():
role = my_roles.get(r.id) or ('leader' if viewer_uid and r.leader_uid == viewer_uid else '')
out[oid] = {
'has_team': True,
'team_status': r.status,
'team_status_text': status_text.get(r.status, ''),
'team_joined': int(r.joined_count or 0),
'team_need': int(r.need_count or 0),
'team_role': role,
'team_role_text': '队长' if role == 'leader' else ('队友' if role == 'member' else ''),
}
return out