feat: 组队招募(/zudui)+ 结单/自动结算/罚款挂钩

新增 Team 表与接口;进8强制锁、组队分账优先且失败不回退全额队长。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-27 03:12:04 +08:00
parent e9e5ee85e2
commit 7002da6093
14 changed files with 1466 additions and 63 deletions

284
orders/views/zudui.py Normal file
View File

@@ -0,0 +1,284 @@
"""组队 API/zudui/*"""
from __future__ import annotations
import logging
from rest_framework.parsers import JSONParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from jituan.services.club_context import resolve_club_id_from_request
from orders.models import Order, TeamRecruit, TeamMember
from orders.services import team_recruit as team_svc
logger = logging.getLogger(__name__)
def _uid(request):
return getattr(request.user, 'UserUID', '') or ''
def _club(request):
return (resolve_club_id_from_request(request) or '').strip()
class ZuduiKaqiView(APIView):
"""开启组队 POST /zudui/kaqi"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
uid = _uid(request)
oid = str(request.data.get('dingdan_id') or request.data.get('order_id') or '').strip()
if not oid:
return Response({'code': 400, 'msg': '缺少订单ID'})
order = Order.query.filter(OrderID=oid).first()
if not order:
return Response({'code': 404, 'msg': '订单不存在'})
club = _club(request)
if club and (order.ClubID or '') != club:
return Response({'code': 403, 'msg': '俱乐部不匹配'})
try:
rec = team_svc.open_recruit(
order=order,
leader_uid=uid,
need_count=int(request.data.get('need_count') or 1),
share_mode=request.data.get('share_mode') or 'fixed',
per_person_amount=request.data.get('per_person_amount'),
remark=request.data.get('remark') or '',
leader_game_id=request.data.get('leader_game_id') or request.data.get('game_id') or '',
min_deal_rate=request.data.get('min_deal_rate'),
max_fine_rate=request.data.get('max_fine_rate'),
)
return Response({
'code': 0,
'msg': '已开启组队',
'data': team_svc.serialize_recruit_public(rec, order, include_game_ids=True),
})
except ValueError as e:
return Response({'code': 400, 'msg': str(e)})
except Exception as e:
logger.exception('开启组队失败')
return Response({'code': 500, 'msg': '服务器错误'})
class ZuduiGaiyaoqiuView(APIView):
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
uid = _uid(request)
rid = request.data.get('recruit_id')
try:
rid = int(rid)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '缺少 recruit_id'})
try:
kw = {'recruit_id': rid, 'leader_uid': uid}
if 'need_count' in request.data:
kw['need_count'] = request.data.get('need_count')
if 'remark' in request.data:
kw['remark'] = request.data.get('remark')
if 'min_deal_rate' in request.data:
kw['min_deal_rate'] = request.data.get('min_deal_rate')
if 'max_fine_rate' in request.data:
kw['max_fine_rate'] = request.data.get('max_fine_rate')
rec = team_svc.update_recruit(**kw)
order = Order.query.filter(OrderID=rec.order_id).first()
return Response({
'code': 0,
'msg': '已更新',
'data': team_svc.serialize_recruit_public(rec, order, include_game_ids=True),
})
except ValueError as e:
return Response({'code': 400, 'msg': str(e)})
class ZuduiGuantingView(APIView):
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
uid = _uid(request)
rid = request.data.get('recruit_id')
try:
rid = int(rid)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '缺少 recruit_id'})
rec = TeamRecruit.query.filter(id=rid).first()
if not rec:
return Response({'code': 404, 'msg': '组队不存在'})
if rec.leader_uid != uid:
return Response({'code': 403, 'msg': '仅队长可关停'})
try:
rec = team_svc.lock_recruit(rid, actor_uid=uid, reason='manual')
order = Order.query.filter(OrderID=rec.order_id).first()
return Response({
'code': 0,
'msg': '已关停招募',
'data': team_svc.serialize_recruit_public(rec, order, include_game_ids=True),
})
except ValueError as e:
return Response({'code': 400, 'msg': str(e)})
class ZuduiDatingView(APIView):
"""组队大厅:仅招募中。"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
club = _club(request)
if not club:
return Response({'code': 400, 'msg': '缺少俱乐部'})
page = max(1, int(request.data.get('page') or 1))
page_size = min(50, max(1, int(request.data.get('page_size') or 10)))
leixing = request.data.get('leixing_id')
pingtai = request.data.get('fadanpingtai')
qs = TeamRecruit.query.filter(
club_id=club,
status=TeamRecruit.STATUS_RECRUITING,
).order_by('-opened_at')
# 可选:按订单类型过滤
order_ids = None
if leixing not in (None, '') or pingtai not in (None, ''):
oq = Order.query.filter(ClubID=club, Status=2)
if leixing not in (None, ''):
try:
oq = oq.filter(ProductTypeID=int(leixing))
except (TypeError, ValueError):
pass
if pingtai not in (None, ''):
try:
oq = oq.filter(Platform=int(pingtai))
except (TypeError, ValueError):
pass
order_ids = list(oq.values_list('OrderID', flat=True)[:500])
qs = qs.filter(order_id__in=order_ids or ['__none__'])
total = qs.count()
start = (page - 1) * page_size
rows = list(qs[start:start + page_size])
oids = [r.order_id for r in rows]
orders = {o.OrderID: o for o in Order.query.filter(OrderID__in=oids)} if oids else {}
data_list = [
team_svc.serialize_recruit_public(r, orders.get(r.order_id), include_game_ids=False)
for r in rows
]
return Response({
'code': 0,
'msg': 'ok',
'data': {
'list': data_list,
'total': total,
'page': page,
'page_size': page_size,
'has_more': start + len(rows) < total,
},
})
class ZuduiXiangqingView(APIView):
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
uid = _uid(request)
rid = request.data.get('recruit_id')
oid = str(request.data.get('dingdan_id') or request.data.get('order_id') or '').strip()
rec = None
if rid:
try:
rec = TeamRecruit.query.filter(id=int(rid)).first()
except (TypeError, ValueError):
pass
if not rec and oid:
rec = TeamRecruit.query.filter(order_id=oid).order_by('-id').first()
if not rec:
return Response({'code': 404, 'msg': '组队不存在'})
is_member = TeamMember.query.filter(
recruit_id=rec.id, user_uid=uid, join_status=TeamMember.JOIN_ACTIVE,
).exists()
include_game = is_member or rec.leader_uid == uid
order = Order.query.filter(OrderID=rec.order_id).first()
return Response({
'code': 0,
'msg': 'ok',
'data': team_svc.serialize_recruit_public(rec, order, include_game_ids=include_game),
})
class ZuduiRuiduiView(APIView):
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
uid = _uid(request)
try:
rid = int(request.data.get('recruit_id'))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '缺少 recruit_id'})
try:
rec = team_svc.join_recruit(
recruit_id=rid,
user_uid=uid,
game_id=request.data.get('game_id') or '',
request=request,
)
order = Order.query.filter(OrderID=rec.order_id).first()
return Response({
'code': 0,
'msg': '加入成功',
'data': team_svc.serialize_recruit_public(rec, order, include_game_ids=True),
})
except ValueError as e:
return Response({'code': 400, 'msg': str(e)})
except Exception:
logger.exception('加入组队失败')
return Response({'code': 500, 'msg': '服务器错误'})
class ZuduiWodeView(APIView):
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
uid = _uid(request)
club = _club(request)
as_leader = TeamRecruit.query.filter(
club_id=club,
leader_uid=uid,
status__in=[TeamRecruit.STATUS_RECRUITING, TeamRecruit.STATUS_LOCKED],
).order_by('-opened_at')[:50]
member_rids = list(
TeamMember.query.filter(
user_uid=uid, join_status=TeamMember.JOIN_ACTIVE, role=TeamMember.ROLE_MEMBER,
).values_list('recruit_id', flat=True)[:50]
)
as_member_qs = TeamRecruit.query.filter(
id__in=member_rids or [0],
club_id=club,
status__in=[TeamRecruit.STATUS_RECRUITING, TeamRecruit.STATUS_LOCKED],
) if member_rids and club else []
as_member = list(as_member_qs) if as_member_qs else []
as_leader_list = list(as_leader)
oids = [r.order_id for r in as_leader_list + as_member]
orders = {o.OrderID: o for o in Order.query.filter(OrderID__in=oids)} if oids else {}
return Response({
'code': 0,
'msg': 'ok',
'data': {
'as_leader': [
team_svc.serialize_recruit_public(r, orders.get(r.order_id), include_game_ids=True)
for r in as_leader_list
],
'as_member': [
team_svc.serialize_recruit_public(r, orders.get(r.order_id), include_game_ids=True)
for r in as_member
],
},
})