fix: 组队强制本俱乐部隔离,修复组队分账冻结幂等键
组队接口一律校验 X-Club-Id=订单=帖;不走跨店互通。分账 biz_id 按队员区分。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -293,6 +293,7 @@ def credit_dashou_order_settle(
|
|||||||
amount,
|
amount,
|
||||||
set_idle_status: bool = False,
|
set_idle_status: bool = False,
|
||||||
increment_chengjiao: bool = True,
|
increment_chengjiao: bool = True,
|
||||||
|
settle_biz_id: str = '',
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
打手订单结算专用入账(P1)。
|
打手订单结算专用入账(P1)。
|
||||||
@@ -300,6 +301,7 @@ def credit_dashou_order_settle(
|
|||||||
- 可提现走 credit_role_balance(未开冻结=全额进 yue,与改造前一致)
|
- 可提现走 credit_role_balance(未开冻结=全额进 yue,与改造前一致)
|
||||||
- zonge / 今日今月收益仍按「全额」累加(统计口径不变)
|
- zonge / 今日今月收益仍按「全额」累加(统计口径不变)
|
||||||
- 成交单量 +1、可选置空闲状态
|
- 成交单量 +1、可选置空闲状态
|
||||||
|
- settle_biz_id:组队多人分账时传入 `{order_id}:team:{uid}`,避免同单幂等撞键
|
||||||
调用方须已在 transaction.atomic() 内;本函数用 F() 更新,勿再对同一 profile save 覆盖余额字段。
|
调用方须已在 transaction.atomic() 内;本函数用 F() 更新,勿再对同一 profile save 覆盖余额字段。
|
||||||
"""
|
"""
|
||||||
from users.models import UserDashou
|
from users.models import UserDashou
|
||||||
@@ -307,6 +309,7 @@ def credit_dashou_order_settle(
|
|||||||
credit = _money(amount)
|
credit = _money(amount)
|
||||||
order_id = str(getattr(order, 'OrderID', None) or getattr(order, 'order_id', '') or '')
|
order_id = str(getattr(order, 'OrderID', None) or getattr(order, 'order_id', '') or '')
|
||||||
cid = order_club_id(order)
|
cid = order_club_id(order)
|
||||||
|
biz_id = (settle_biz_id or order_id or '').strip()
|
||||||
|
|
||||||
if credit <= 0:
|
if credit <= 0:
|
||||||
updates = {}
|
updates = {}
|
||||||
@@ -329,7 +332,7 @@ def credit_dashou_order_settle(
|
|||||||
role=ROLE_DASHOU,
|
role=ROLE_DASHOU,
|
||||||
amount=credit,
|
amount=credit,
|
||||||
source_type=SOURCE_ORDER_SETTLE,
|
source_type=SOURCE_ORDER_SETTLE,
|
||||||
biz_id=order_id,
|
biz_id=biz_id,
|
||||||
profile=dashou_profile,
|
profile=dashou_profile,
|
||||||
order_id=order_id,
|
order_id=order_id,
|
||||||
freeze_reason=f'订单结算 {order_id}',
|
freeze_reason=f'订单结算 {order_id}',
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ def on_order_enter_status_8(order):
|
|||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@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 orders.models import TeamRecruit, TeamMember, Order
|
||||||
from users.models import UserDashou
|
from users.models import UserDashou
|
||||||
from utils.fadan_utils import check_fadan_qiangdan_eligible
|
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()
|
rec = TeamRecruit.objects.select_for_update().filter(id=recruit_id).first()
|
||||||
if not rec:
|
if not rec:
|
||||||
raise ValueError('组队不存在')
|
raise ValueError('组队不存在')
|
||||||
|
# 组队强制本俱乐部:不认跨店互通抢单
|
||||||
|
if require_club_id and (rec.club_id or '') != require_club_id:
|
||||||
|
raise ValueError('仅本俱乐部可加入组队')
|
||||||
if rec.status != TeamRecruit.STATUS_RECRUITING:
|
if rec.status != TeamRecruit.STATUS_RECRUITING:
|
||||||
raise ValueError('招募已结束')
|
raise ValueError('招募已结束')
|
||||||
if int(rec.joined_count or 0) >= int(rec.need_count or 0):
|
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('订单状态不可加入')
|
raise ValueError('订单状态不可加入')
|
||||||
if (getattr(order, 'ClubID', '') or '') != rec.club_id:
|
if (getattr(order, 'ClubID', '') or '') != rec.club_id:
|
||||||
raise ValueError('俱乐部不匹配')
|
raise ValueError('俱乐部不匹配')
|
||||||
|
if require_club_id and (order.ClubID or '') != require_club_id:
|
||||||
|
raise ValueError('仅本俱乐部订单可加入组队')
|
||||||
|
|
||||||
# 忙碌 / 罚单
|
# 忙碌 / 罚单
|
||||||
try:
|
try:
|
||||||
@@ -541,6 +546,7 @@ def settle_team_or_legacy(order, *, set_idle_leader=False) -> bool:
|
|||||||
amount=amt,
|
amount=amt,
|
||||||
set_idle_status=True,
|
set_idle_status=True,
|
||||||
increment_chengjiao=True,
|
increment_chengjiao=True,
|
||||||
|
settle_biz_id=biz_id,
|
||||||
)
|
)
|
||||||
TeamSettleLedger(
|
TeamSettleLedger(
|
||||||
club_id=rec.club_id,
|
club_id=rec.club_id,
|
||||||
|
|||||||
@@ -15,30 +15,45 @@ from orders.services import team_recruit as team_svc
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _uid(request):
|
def _require_club(request):
|
||||||
return getattr(request.user, 'UserUID', '') or ''
|
club = _club(request)
|
||||||
|
if not club:
|
||||||
|
return None, Response({'code': 400, 'msg': '缺少俱乐部'})
|
||||||
|
return club, None
|
||||||
|
|
||||||
|
|
||||||
def _club(request):
|
def _assert_recruit_same_club(rec, club):
|
||||||
return (resolve_club_id_from_request(request) or '').strip()
|
"""组队永不走跨店互通:帖/订单/当前俱乐部必须一致。"""
|
||||||
|
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):
|
class ZuduiKaqiView(APIView):
|
||||||
"""开启组队 POST /zudui/kaqi"""
|
"""开启组队 POST /zudui/kaqi(仅本俱乐部订单,与跨店抢单无关)"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
parser_classes = [JSONParser]
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
uid = _uid(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()
|
oid = str(request.data.get('dingdan_id') or request.data.get('order_id') or '').strip()
|
||||||
if not oid:
|
if not oid:
|
||||||
return Response({'code': 400, 'msg': '缺少订单ID'})
|
return Response({'code': 400, 'msg': '缺少订单ID'})
|
||||||
order = Order.query.filter(OrderID=oid).first()
|
order = Order.query.filter(OrderID=oid).first()
|
||||||
if not order:
|
if not order:
|
||||||
return Response({'code': 404, 'msg': '订单不存在'})
|
return Response({'code': 404, 'msg': '订单不存在'})
|
||||||
club = _club(request)
|
if (order.ClubID or '') != club:
|
||||||
if club and (order.ClubID or '') != club:
|
return Response({'code': 403, 'msg': '仅本俱乐部订单可开启组队'})
|
||||||
return Response({'code': 403, 'msg': '俱乐部不匹配'})
|
|
||||||
try:
|
try:
|
||||||
rec = team_svc.open_recruit(
|
rec = team_svc.open_recruit(
|
||||||
order=order,
|
order=order,
|
||||||
@@ -69,11 +84,18 @@ class ZuduiGaiyaoqiuView(APIView):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
uid = _uid(request)
|
uid = _uid(request)
|
||||||
|
club, err = _require_club(request)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
rid = request.data.get('recruit_id')
|
rid = request.data.get('recruit_id')
|
||||||
try:
|
try:
|
||||||
rid = int(rid)
|
rid = int(rid)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return Response({'code': 400, 'msg': '缺少 recruit_id'})
|
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:
|
try:
|
||||||
kw = {'recruit_id': rid, 'leader_uid': uid}
|
kw = {'recruit_id': rid, 'leader_uid': uid}
|
||||||
if 'need_count' in request.data:
|
if 'need_count' in request.data:
|
||||||
@@ -101,14 +123,18 @@ class ZuduiGuantingView(APIView):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
uid = _uid(request)
|
uid = _uid(request)
|
||||||
|
club, err = _require_club(request)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
rid = request.data.get('recruit_id')
|
rid = request.data.get('recruit_id')
|
||||||
try:
|
try:
|
||||||
rid = int(rid)
|
rid = int(rid)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return Response({'code': 400, 'msg': '缺少 recruit_id'})
|
return Response({'code': 400, 'msg': '缺少 recruit_id'})
|
||||||
rec = TeamRecruit.query.filter(id=rid).first()
|
rec = TeamRecruit.query.filter(id=rid).first()
|
||||||
if not rec:
|
deny = _assert_recruit_same_club(rec, club)
|
||||||
return Response({'code': 404, 'msg': '组队不存在'})
|
if deny:
|
||||||
|
return deny
|
||||||
if rec.leader_uid != uid:
|
if rec.leader_uid != uid:
|
||||||
return Response({'code': 403, 'msg': '仅队长可关停'})
|
return Response({'code': 403, 'msg': '仅队长可关停'})
|
||||||
try:
|
try:
|
||||||
@@ -124,14 +150,14 @@ class ZuduiGuantingView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class ZuduiDatingView(APIView):
|
class ZuduiDatingView(APIView):
|
||||||
"""组队大厅:仅招募中。"""
|
"""组队大厅:仅本俱乐部招募中(不读跨店互通)。"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
parser_classes = [JSONParser]
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
club = _club(request)
|
club, err = _require_club(request)
|
||||||
if not club:
|
if err:
|
||||||
return Response({'code': 400, 'msg': '缺少俱乐部'})
|
return err
|
||||||
page = max(1, int(request.data.get('page') or 1))
|
page = max(1, int(request.data.get('page') or 1))
|
||||||
page_size = min(50, max(1, int(request.data.get('page_size') or 10)))
|
page_size = min(50, max(1, int(request.data.get('page_size') or 10)))
|
||||||
leixing = request.data.get('leixing_id')
|
leixing = request.data.get('leixing_id')
|
||||||
@@ -142,22 +168,27 @@ class ZuduiDatingView(APIView):
|
|||||||
status=TeamRecruit.STATUS_RECRUITING,
|
status=TeamRecruit.STATUS_RECRUITING,
|
||||||
).order_by('-opened_at')
|
).order_by('-opened_at')
|
||||||
|
|
||||||
# 可选:按订单类型过滤
|
# 商品类型必选过滤(与前端大厅一致)
|
||||||
order_ids = None
|
if leixing not in (None, ''):
|
||||||
if leixing not in (None, '') or pingtai not in (None, ''):
|
try:
|
||||||
oq = Order.query.filter(ClubID=club, Status=2)
|
lid = int(leixing)
|
||||||
if leixing not in (None, ''):
|
except (TypeError, ValueError):
|
||||||
try:
|
return Response({'code': 400, 'msg': '商品类型无效'})
|
||||||
oq = oq.filter(ProductTypeID=int(leixing))
|
oq = Order.query.filter(ClubID=club, Status=2, ProductTypeID=lid)
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
if pingtai not in (None, ''):
|
if pingtai not in (None, ''):
|
||||||
try:
|
try:
|
||||||
oq = oq.filter(Platform=int(pingtai))
|
oq = oq.filter(Platform=int(pingtai))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
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__'])
|
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()
|
total = qs.count()
|
||||||
start = (page - 1) * page_size
|
start = (page - 1) * page_size
|
||||||
@@ -187,6 +218,9 @@ class ZuduiXiangqingView(APIView):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
uid = _uid(request)
|
uid = _uid(request)
|
||||||
|
club, err = _require_club(request)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
rid = request.data.get('recruit_id')
|
rid = request.data.get('recruit_id')
|
||||||
oid = str(request.data.get('dingdan_id') or request.data.get('order_id') or '').strip()
|
oid = str(request.data.get('dingdan_id') or request.data.get('order_id') or '').strip()
|
||||||
rec = None
|
rec = None
|
||||||
@@ -196,9 +230,12 @@ class ZuduiXiangqingView(APIView):
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
if not rec and oid:
|
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:
|
if not rec:
|
||||||
return Response({'code': 404, 'msg': '组队不存在'})
|
return Response({'code': 404, 'msg': '组队不存在'})
|
||||||
|
deny = _assert_recruit_same_club(rec, club)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
|
||||||
is_member = TeamMember.query.filter(
|
is_member = TeamMember.query.filter(
|
||||||
recruit_id=rec.id, user_uid=uid, join_status=TeamMember.JOIN_ACTIVE,
|
recruit_id=rec.id, user_uid=uid, join_status=TeamMember.JOIN_ACTIVE,
|
||||||
@@ -218,16 +255,24 @@ class ZuduiRuiduiView(APIView):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
uid = _uid(request)
|
uid = _uid(request)
|
||||||
|
club, err = _require_club(request)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
try:
|
try:
|
||||||
rid = int(request.data.get('recruit_id'))
|
rid = int(request.data.get('recruit_id'))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return Response({'code': 400, 'msg': '缺少 recruit_id'})
|
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:
|
try:
|
||||||
rec = team_svc.join_recruit(
|
rec = team_svc.join_recruit(
|
||||||
recruit_id=rid,
|
recruit_id=rid,
|
||||||
user_uid=uid,
|
user_uid=uid,
|
||||||
game_id=request.data.get('game_id') or '',
|
game_id=request.data.get('game_id') or '',
|
||||||
request=request,
|
request=request,
|
||||||
|
require_club_id=club,
|
||||||
)
|
)
|
||||||
order = Order.query.filter(OrderID=rec.order_id).first()
|
order = Order.query.filter(OrderID=rec.order_id).first()
|
||||||
return Response({
|
return Response({
|
||||||
@@ -248,7 +293,9 @@ class ZuduiWodeView(APIView):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
uid = _uid(request)
|
uid = _uid(request)
|
||||||
club = _club(request)
|
club, err = _require_club(request)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
as_leader = TeamRecruit.query.filter(
|
as_leader = TeamRecruit.query.filter(
|
||||||
club_id=club,
|
club_id=club,
|
||||||
leader_uid=uid,
|
leader_uid=uid,
|
||||||
@@ -263,7 +310,7 @@ class ZuduiWodeView(APIView):
|
|||||||
id__in=member_rids or [0],
|
id__in=member_rids or [0],
|
||||||
club_id=club,
|
club_id=club,
|
||||||
status__in=[TeamRecruit.STATUS_RECRUITING, TeamRecruit.STATUS_LOCKED],
|
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_member = list(as_member_qs) if as_member_qs else []
|
||||||
as_leader_list = list(as_leader)
|
as_leader_list = list(as_leader)
|
||||||
oids = [r.order_id for r in as_leader_list + as_member]
|
oids = [r.order_id for r in as_leader_list + as_member]
|
||||||
|
|||||||
Reference in New Issue
Block a user