feat: 俱乐部订单单向互通抢单(集团配置 + 接单池/抢单校验)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
35
jituan/migrations/0016_club_order_grab_link.py
Normal file
35
jituan/migrations/0016_club_order_grab_link.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Generated manually for ClubOrderGrabLink
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('jituan', '0015_club_recharge_page_copy'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClubOrderGrabLink',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('grabber_club_id', models.CharField(db_index=True, max_length=16, verbose_name='抢单方俱乐部')),
|
||||||
|
('order_club_id', models.CharField(db_index=True, max_length=16, verbose_name='被抢订单所属俱乐部')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '俱乐部订单互通抢单',
|
||||||
|
'db_table': 'club_order_grab_link',
|
||||||
|
'unique_together': {('grabber_club_id', 'order_club_id')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clubordergrablink',
|
||||||
|
index=models.Index(fields=['grabber_club_id'], name='idx_cogl_grabber'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clubordergrablink',
|
||||||
|
index=models.Index(fields=['order_club_id'], name='idx_cogl_order'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -444,3 +444,23 @@ class KefuMenuPage(QModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'kefu_menu_page'
|
db_table = 'kefu_menu_page'
|
||||||
ordering = ['sort_order', 'page_id']
|
ordering = ['sort_order', 'page_id']
|
||||||
|
|
||||||
|
|
||||||
|
class ClubOrderGrabLink(QModel):
|
||||||
|
"""
|
||||||
|
俱乐部订单单向互通抢单。
|
||||||
|
grabber_club_id 的打手可抢 order_club_id 的待接单;反向需另建一条。
|
||||||
|
关停:删除记录即可(两字段,无开关列)。
|
||||||
|
"""
|
||||||
|
grabber_club_id = models.CharField(max_length=16, db_index=True, verbose_name='抢单方俱乐部')
|
||||||
|
order_club_id = models.CharField(max_length=16, db_index=True, verbose_name='被抢订单所属俱乐部')
|
||||||
|
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'club_order_grab_link'
|
||||||
|
verbose_name = '俱乐部订单互通抢单'
|
||||||
|
unique_together = [['grabber_club_id', 'order_club_id']]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['grabber_club_id'], name='idx_cogl_grabber'),
|
||||||
|
models.Index(fields=['order_club_id'], name='idx_cogl_order'),
|
||||||
|
]
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ KEFU_MENU_ROW_DEFS = [
|
|||||||
'require_group_manage': True},
|
'require_group_manage': True},
|
||||||
{'page_id': 'admin.club-config', 'name': '俱乐部密钥配置', 'path': '/admin/club-config', 'parent_id': 'admin', 'sort_order': 73,
|
{'page_id': 'admin.club-config', 'name': '俱乐部密钥配置', 'path': '/admin/club-config', 'parent_id': 'admin', 'sort_order': 73,
|
||||||
'require_group_manage': True},
|
'require_group_manage': True},
|
||||||
# require_super 兜底(旧 DB 无 require_admin_user_manage 列时仍可显示);运行时仍按 page_id=admin.user 走用户管理闸门
|
{'page_id': 'admin.order-grab-share', 'name': '订单互通抢单', 'path': '/admin/order-grab-share', 'parent_id': 'admin', 'sort_order': 76,
|
||||||
|
'require_group_manage': True},
|
||||||
{'page_id': 'admin.user', 'name': '后台用户', 'path': '/admin/user', 'parent_id': 'admin', 'sort_order': 74,
|
{'page_id': 'admin.user', 'name': '后台用户', 'path': '/admin/user', 'parent_id': 'admin', 'sort_order': 74,
|
||||||
'require_super': True, 'require_admin_user_manage': True},
|
'require_super': True, 'require_admin_user_manage': True},
|
||||||
{'page_id': 'admin.operation-log', 'name': '操作日志', 'path': '/admin/operation-log', 'parent_id': 'admin', 'sort_order': 75,
|
{'page_id': 'admin.operation-log', 'name': '操作日志', 'path': '/admin/operation-log', 'parent_id': 'admin', 'sort_order': 75,
|
||||||
|
|||||||
90
jituan/services/order_grab_share.py
Normal file
90
jituan/services/order_grab_share.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""俱乐部订单单向互通抢单:grabber 可看/抢 order 俱乐部的待接单。"""
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from jituan.constants import CLUB_ID_DEFAULT
|
||||||
|
from jituan.models import Club, ClubOrderGrabLink
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_club_id(club_id):
|
||||||
|
return (club_id or '').strip() or CLUB_ID_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def order_club_ids_for_grabber(grabber_club_id):
|
||||||
|
"""
|
||||||
|
打手所属俱乐部可见的订单 ClubID 集合(含本店 + 已配置可抢的对方店)。
|
||||||
|
"""
|
||||||
|
gc = normalize_club_id(grabber_club_id)
|
||||||
|
ids = {gc}
|
||||||
|
for oc in ClubOrderGrabLink.query.filter(
|
||||||
|
grabber_club_id=gc,
|
||||||
|
).values_list('order_club_id', flat=True):
|
||||||
|
oc = (oc or '').strip()
|
||||||
|
if oc:
|
||||||
|
ids.add(oc)
|
||||||
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
def order_club_q_for_grabber(grabber_club_id):
|
||||||
|
"""用于 Order 查询的 Q:本店 + 互通店;本店含默认俱乐部时兼容空 ClubID 旧单。"""
|
||||||
|
allowed = order_club_ids_for_grabber(grabber_club_id)
|
||||||
|
q = Q(ClubID__in=list(allowed))
|
||||||
|
if CLUB_ID_DEFAULT in allowed:
|
||||||
|
q |= Q(ClubID__isnull=True) | Q(ClubID='')
|
||||||
|
return q
|
||||||
|
|
||||||
|
|
||||||
|
def can_grabber_take_order_club(grabber_club_id, order_club_id):
|
||||||
|
gc = normalize_club_id(grabber_club_id)
|
||||||
|
oc = normalize_club_id(order_club_id)
|
||||||
|
if gc == oc:
|
||||||
|
return True
|
||||||
|
return ClubOrderGrabLink.query.filter(
|
||||||
|
grabber_club_id=gc, order_club_id=oc,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def list_grab_links():
|
||||||
|
name_map = {c.club_id: c.name for c in Club.query.filter(status=1)}
|
||||||
|
rows = list(ClubOrderGrabLink.query.order_by('grabber_club_id', 'order_club_id'))
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'id': r.id,
|
||||||
|
'grabber_club_id': r.grabber_club_id,
|
||||||
|
'grabber_club_name': name_map.get(r.grabber_club_id, r.grabber_club_id),
|
||||||
|
'order_club_id': r.order_club_id,
|
||||||
|
'order_club_name': name_map.get(r.order_club_id, r.order_club_id),
|
||||||
|
'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||||||
|
}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def add_grab_link(grabber_club_id, order_club_id):
|
||||||
|
gc = (grabber_club_id or '').strip()
|
||||||
|
oc = (order_club_id or '').strip()
|
||||||
|
if not gc or not oc:
|
||||||
|
return None, '请选择抢单方与被抢订单俱乐部'
|
||||||
|
if gc == oc:
|
||||||
|
return None, '不能配置本店抢本店(本店订单默认可见)'
|
||||||
|
if not Club.query.filter(club_id=gc, status=1).exists():
|
||||||
|
return None, f'抢单方俱乐部不存在: {gc}'
|
||||||
|
if not Club.query.filter(club_id=oc, status=1).exists():
|
||||||
|
return None, f'订单方俱乐部不存在: {oc}'
|
||||||
|
if ClubOrderGrabLink.query.filter(grabber_club_id=gc, order_club_id=oc).exists():
|
||||||
|
return None, '该互通规则已存在'
|
||||||
|
row = ClubOrderGrabLink.query.create(
|
||||||
|
grabber_club_id=gc,
|
||||||
|
order_club_id=oc,
|
||||||
|
)
|
||||||
|
return {'id': row.id, 'grabber_club_id': gc, 'order_club_id': oc}, None
|
||||||
|
|
||||||
|
|
||||||
|
def delete_grab_link(link_id):
|
||||||
|
try:
|
||||||
|
link_id = int(link_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False, '无效的规则 id'
|
||||||
|
deleted, _ = ClubOrderGrabLink.objects.filter(id=link_id).delete()
|
||||||
|
if not deleted:
|
||||||
|
return False, '规则不存在'
|
||||||
|
return True, None
|
||||||
@@ -100,6 +100,7 @@ from jituan.views_recharge_page_copy import (
|
|||||||
RechargePageCopyAdminSaveView,
|
RechargePageCopyAdminSaveView,
|
||||||
RechargePageCopyView,
|
RechargePageCopyView,
|
||||||
)
|
)
|
||||||
|
from jituan.views_order_grab_share import ClubOrderGrabLinkManageView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('auth/wechat-login', ClubWechatLoginView.as_view(), name='jituan_wechat_login'),
|
path('auth/wechat-login', ClubWechatLoginView.as_view(), name='jituan_wechat_login'),
|
||||||
@@ -110,6 +111,7 @@ urlpatterns = [
|
|||||||
path('auth/perm-debug', ClubPermDebugView.as_view(), name='jituan_perm_debug'),
|
path('auth/perm-debug', ClubPermDebugView.as_view(), name='jituan_perm_debug'),
|
||||||
path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'),
|
path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'),
|
||||||
path('houtai/club-manage', ClubManageView.as_view(), name='jituan_club_manage'),
|
path('houtai/club-manage', ClubManageView.as_view(), name='jituan_club_manage'),
|
||||||
|
path('houtai/order-grab-link', ClubOrderGrabLinkManageView.as_view(), name='jituan_order_grab_link'),
|
||||||
path('houtai/payment-channel-manage', ClubPaymentChannelManageView.as_view(), name='jituan_payment_channel_manage'),
|
path('houtai/payment-channel-manage', ClubPaymentChannelManageView.as_view(), name='jituan_payment_channel_manage'),
|
||||||
path('houtai/club-cert-upload', ClubCertUploadView.as_view(), name='jituan_club_cert_upload'),
|
path('houtai/club-cert-upload', ClubCertUploadView.as_view(), name='jituan_club_cert_upload'),
|
||||||
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),
|
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),
|
||||||
|
|||||||
50
jituan/views_order_grab_share.py
Normal file
50
jituan/views_order_grab_share.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"""集团后台:俱乐部订单单向互通抢单配置。"""
|
||||||
|
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 backend.utils import verify_kefu_permission
|
||||||
|
from jituan.services.admin_context import can_manage_admin_assignments
|
||||||
|
from jituan.services.order_grab_share import add_grab_link, delete_grab_link, list_grab_links
|
||||||
|
|
||||||
|
|
||||||
|
class ClubOrderGrabLinkManageView(APIView):
|
||||||
|
"""
|
||||||
|
POST /jituan/houtai/order-grab-link
|
||||||
|
action: list | add | delete
|
||||||
|
仅集团权限(can_manage_admin_assignments)可操作。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = (request.data.get('username') or request.data.get('phone') or '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 400, 'msg': '缺少username'})
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if not can_manage_admin_assignments(request.user, permissions):
|
||||||
|
return Response({'code': 403, 'msg': '仅集团权限可配置订单互通'})
|
||||||
|
|
||||||
|
action = (request.data.get('action') or 'list').strip()
|
||||||
|
if action == 'list':
|
||||||
|
return Response({'code': 0, 'msg': 'ok', 'data': {'list': list_grab_links()}})
|
||||||
|
|
||||||
|
if action == 'add':
|
||||||
|
data, err = add_grab_link(
|
||||||
|
request.data.get('grabber_club_id'),
|
||||||
|
request.data.get('order_club_id'),
|
||||||
|
)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 400, 'msg': err})
|
||||||
|
return Response({'code': 0, 'msg': '已添加', 'data': data})
|
||||||
|
|
||||||
|
if action == 'delete':
|
||||||
|
ok, err = delete_grab_link(request.data.get('id'))
|
||||||
|
if not ok:
|
||||||
|
return Response({'code': 400, 'msg': err or '删除失败'})
|
||||||
|
return Response({'code': 0, 'msg': '已删除'})
|
||||||
|
|
||||||
|
return Response({'code': 400, 'msg': '无效 action'})
|
||||||
@@ -124,8 +124,9 @@ class DashouDingdanHuoquView(APIView):
|
|||||||
yonghuid, club_id=club_id,
|
yonghuid, club_id=club_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. 构建基础查询条件
|
# 2. 构建基础查询条件:本店待抢单 + 集团配置的单向互通店
|
||||||
base_query = Q(Status__in=[1, 7])
|
from jituan.services.order_grab_share import order_club_q_for_grabber
|
||||||
|
base_query = Q(Status__in=[1, 7]) & order_club_q_for_grabber(club_id)
|
||||||
if leixing_id is not None:
|
if leixing_id is not None:
|
||||||
base_query &= Q(ProductTypeID=leixing_id)
|
base_query &= Q(ProductTypeID=leixing_id)
|
||||||
|
|
||||||
@@ -469,6 +470,13 @@ class QiangdanView(APIView):
|
|||||||
return Response({'code': 400, 'msg': order_result})
|
return Response({'code': 400, 'msg': order_result})
|
||||||
order = order_result
|
order = order_result
|
||||||
|
|
||||||
|
# 2.1 俱乐部互通:只能抢本店或已授权可抢的对方店订单
|
||||||
|
from jituan.services.club_context import resolve_effective_club_id
|
||||||
|
from jituan.services.order_grab_share import can_grabber_take_order_club
|
||||||
|
grabber_club = resolve_effective_club_id(request, request.user)
|
||||||
|
if not can_grabber_take_order_club(grabber_club, getattr(order, 'ClubID', None)):
|
||||||
|
return Response({'code': 403, 'msg': '无权抢该俱乐部订单'})
|
||||||
|
|
||||||
validation_result = self._validate_order_requirements(order, dashou_profile, request.user.UserUID)
|
validation_result = self._validate_order_requirements(order, dashou_profile, request.user.UserUID)
|
||||||
if not validation_result['success']:
|
if not validation_result['success']:
|
||||||
return Response({'code': 400, 'msg': validation_result['message']})
|
return Response({'code': 400, 'msg': validation_result['message']})
|
||||||
|
|||||||
Reference in New Issue
Block a user