From 454f5abe90c5755b2adb03bb07cd96d577342c21 Mon Sep 17 00:00:00 2001 From: XingQue Date: Mon, 27 Jul 2026 03:25:52 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=84=E9=98=9F=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E6=9C=AC=E4=BF=B1=E4=B9=90=E9=83=A8=E9=9A=94=E7=A6=BB=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BB=84=E9=98=9F=E5=88=86=E8=B4=A6=E5=86=BB?= =?UTF-8?q?=E7=BB=93=E5=B9=82=E7=AD=89=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 组队接口一律校验 X-Club-Id=订单=帖;不走跨店互通。分账 biz_id 按队员区分。 Co-authored-by: Cursor --- jituan/services/fund_freeze.py | 5 +- orders/services/team_recruit.py | 8 ++- orders/views/zudui.py | 101 +++++++++++++++++++++++--------- 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/jituan/services/fund_freeze.py b/jituan/services/fund_freeze.py index d8f9f85..36d6f28 100644 --- a/jituan/services/fund_freeze.py +++ b/jituan/services/fund_freeze.py @@ -293,6 +293,7 @@ def credit_dashou_order_settle( amount, set_idle_status: bool = False, increment_chengjiao: bool = True, + settle_biz_id: str = '', ) -> Dict[str, Any]: """ 打手订单结算专用入账(P1)。 @@ -300,6 +301,7 @@ def credit_dashou_order_settle( - 可提现走 credit_role_balance(未开冻结=全额进 yue,与改造前一致) - zonge / 今日今月收益仍按「全额」累加(统计口径不变) - 成交单量 +1、可选置空闲状态 + - settle_biz_id:组队多人分账时传入 `{order_id}:team:{uid}`,避免同单幂等撞键 调用方须已在 transaction.atomic() 内;本函数用 F() 更新,勿再对同一 profile save 覆盖余额字段。 """ from users.models import UserDashou @@ -307,6 +309,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) + biz_id = (settle_biz_id or order_id or '').strip() if credit <= 0: updates = {} @@ -329,7 +332,7 @@ def credit_dashou_order_settle( role=ROLE_DASHOU, amount=credit, source_type=SOURCE_ORDER_SETTLE, - biz_id=order_id, + biz_id=biz_id, profile=dashou_profile, order_id=order_id, freeze_reason=f'订单结算 {order_id}', diff --git a/orders/services/team_recruit.py b/orders/services/team_recruit.py index 991a5e5..a01e78b 100644 --- a/orders/services/team_recruit.py +++ b/orders/services/team_recruit.py @@ -315,7 +315,7 @@ def on_order_enter_status_8(order): @transaction.atomic -def join_recruit(*, recruit_id: int, user_uid: str, game_id: str, request=None): +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 @@ -327,6 +327,9 @@ def join_recruit(*, recruit_id: int, user_uid: str, game_id: str, request=None): 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): @@ -341,6 +344,8 @@ def join_recruit(*, recruit_id: int, user_uid: str, game_id: str, request=None): 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: @@ -541,6 +546,7 @@ def settle_team_or_legacy(order, *, set_idle_leader=False) -> bool: amount=amt, set_idle_status=True, increment_chengjiao=True, + settle_biz_id=biz_id, ) TeamSettleLedger( club_id=rec.club_id, diff --git a/orders/views/zudui.py b/orders/views/zudui.py index a0e46ff..2d4084d 100644 --- a/orders/views/zudui.py +++ b/orders/views/zudui.py @@ -15,30 +15,45 @@ from orders.services import team_recruit as team_svc logger = logging.getLogger(__name__) -def _uid(request): - return getattr(request.user, 'UserUID', '') or '' +def _require_club(request): + club = _club(request) + if not club: + return None, Response({'code': 400, 'msg': '缺少俱乐部'}) + return club, None -def _club(request): - return (resolve_club_id_from_request(request) or '').strip() +def _assert_recruit_same_club(rec, club): + """组队永不走跨店互通:帖/订单/当前俱乐部必须一致。""" + if not rec: + return Response({'code': 404, 'msg': '组队不存在'}) + if (rec.club_id or '') != club: + return Response({'code': 403, 'msg': '仅本俱乐部可操作组队'}) + order = Order.query.filter(OrderID=rec.order_id).first() + if not order: + return Response({'code': 404, 'msg': '订单不存在'}) + if (order.ClubID or '') != club: + return Response({'code': 403, 'msg': '订单不属于本俱乐部'}) + return None class ZuduiKaqiView(APIView): - """开启组队 POST /zudui/kaqi""" + """开启组队 POST /zudui/kaqi(仅本俱乐部订单,与跨店抢单无关)""" permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): uid = _uid(request) + club, err = _require_club(request) + if err: + return err 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': '俱乐部不匹配'}) + if (order.ClubID or '') != club: + return Response({'code': 403, 'msg': '仅本俱乐部订单可开启组队'}) try: rec = team_svc.open_recruit( order=order, @@ -69,11 +84,18 @@ class ZuduiGaiyaoqiuView(APIView): def post(self, request): uid = _uid(request) + club, err = _require_club(request) + if err: + return err 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() + deny = _assert_recruit_same_club(rec, club) + if deny: + return deny try: kw = {'recruit_id': rid, 'leader_uid': uid} if 'need_count' in request.data: @@ -101,14 +123,18 @@ class ZuduiGuantingView(APIView): def post(self, request): uid = _uid(request) + club, err = _require_club(request) + if err: + return err 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': '组队不存在'}) + deny = _assert_recruit_same_club(rec, club) + if deny: + return deny if rec.leader_uid != uid: return Response({'code': 403, 'msg': '仅队长可关停'}) try: @@ -124,14 +150,14 @@ class ZuduiGuantingView(APIView): class ZuduiDatingView(APIView): - """组队大厅:仅招募中。""" + """组队大厅:仅本俱乐部招募中(不读跨店互通)。""" permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): - club = _club(request) - if not club: - return Response({'code': 400, 'msg': '缺少俱乐部'}) + club, err = _require_club(request) + if err: + return err 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') @@ -142,22 +168,27 @@ class ZuduiDatingView(APIView): 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 leixing not in (None, ''): + try: + lid = int(leixing) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '商品类型无效'}) + oq = Order.query.filter(ClubID=club, Status=2, ProductTypeID=lid) 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]) + order_ids = list(oq.values_list('OrderID', flat=True)[:800]) qs = qs.filter(order_id__in=order_ids or ['__none__']) + elif pingtai not in (None, ''): + try: + oq = Order.query.filter(ClubID=club, Status=2, Platform=int(pingtai)) + order_ids = list(oq.values_list('OrderID', flat=True)[:800]) + qs = qs.filter(order_id__in=order_ids or ['__none__']) + except (TypeError, ValueError): + pass total = qs.count() start = (page - 1) * page_size @@ -187,6 +218,9 @@ class ZuduiXiangqingView(APIView): def post(self, request): uid = _uid(request) + club, err = _require_club(request) + if err: + return err rid = request.data.get('recruit_id') oid = str(request.data.get('dingdan_id') or request.data.get('order_id') or '').strip() rec = None @@ -196,9 +230,12 @@ class ZuduiXiangqingView(APIView): except (TypeError, ValueError): pass if not rec and oid: - rec = TeamRecruit.query.filter(order_id=oid).order_by('-id').first() + rec = TeamRecruit.query.filter(order_id=oid, club_id=club).order_by('-id').first() if not rec: return Response({'code': 404, 'msg': '组队不存在'}) + deny = _assert_recruit_same_club(rec, club) + if deny: + return deny is_member = TeamMember.query.filter( recruit_id=rec.id, user_uid=uid, join_status=TeamMember.JOIN_ACTIVE, @@ -218,16 +255,24 @@ class ZuduiRuiduiView(APIView): def post(self, request): uid = _uid(request) + club, err = _require_club(request) + if err: + return err try: rid = int(request.data.get('recruit_id')) except (TypeError, ValueError): return Response({'code': 400, 'msg': '缺少 recruit_id'}) + rec0 = TeamRecruit.query.filter(id=rid).first() + deny = _assert_recruit_same_club(rec0, club) + if deny: + return deny try: rec = team_svc.join_recruit( recruit_id=rid, user_uid=uid, game_id=request.data.get('game_id') or '', request=request, + require_club_id=club, ) order = Order.query.filter(OrderID=rec.order_id).first() return Response({ @@ -248,7 +293,9 @@ class ZuduiWodeView(APIView): def post(self, request): uid = _uid(request) - club = _club(request) + club, err = _require_club(request) + if err: + return err as_leader = TeamRecruit.query.filter( club_id=club, leader_uid=uid, @@ -263,7 +310,7 @@ class ZuduiWodeView(APIView): id__in=member_rids or [0], club_id=club, status__in=[TeamRecruit.STATUS_RECRUITING, TeamRecruit.STATUS_LOCKED], - ) if member_rids and club else [] + ) if member_rids 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]