feat: 投诉仅限本人邀请人+人工客服配置+抢单池商家成交指标
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
26
gongdan/migrations/0003_club_human_kefu.py
Normal file
26
gongdan/migrations/0003_club_human_kefu.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gongdan', '0002_gongdan_target_intervene'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='clubgongdanconfig',
|
||||
name='human_kefu_wechat',
|
||||
field=models.CharField(blank=True, default='', max_length=64, verbose_name='人工客服微信号'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='clubgongdanconfig',
|
||||
name='human_kefu_link',
|
||||
field=models.CharField(blank=True, default='', max_length=512, verbose_name='人工客服链接'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='clubgongdanconfig',
|
||||
name='human_kefu_tip',
|
||||
field=models.CharField(blank=True, default='', max_length=120, verbose_name='人工客服提示文案'),
|
||||
),
|
||||
]
|
||||
@@ -85,6 +85,10 @@ class ClubGongdanConfig(QModel):
|
||||
club_id = models.CharField(max_length=16, unique=True, db_index=True, verbose_name='俱乐部')
|
||||
# 未结清被投诉/牵连时禁止提现
|
||||
complaint_withdraw_block = models.BooleanField(default=False, verbose_name='投诉拦截提现')
|
||||
# 投诉页人工客服(分俱乐部配置;有则前端展示)
|
||||
human_kefu_wechat = models.CharField(max_length=64, blank=True, default='', verbose_name='人工客服微信号')
|
||||
human_kefu_link = models.CharField(max_length=512, blank=True, default='', verbose_name='人工客服链接')
|
||||
human_kefu_tip = models.CharField(max_length=120, blank=True, default='', verbose_name='人工客服提示文案')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
@@ -58,11 +58,25 @@ def ensure_club_gongdan_config(club_id: str, *, withdraw_block: bool = False) ->
|
||||
def get_club_gongdan_config(club_id: str) -> dict:
|
||||
cid = (club_id or '').strip()
|
||||
if not cid:
|
||||
return {'club_id': '', 'complaint_withdraw_block': False}
|
||||
return {
|
||||
'club_id': '',
|
||||
'complaint_withdraw_block': False,
|
||||
'human_kefu_wechat': '',
|
||||
'human_kefu_link': '',
|
||||
'human_kefu_tip': '',
|
||||
'human_kefu_available': False,
|
||||
}
|
||||
row = ClubGongdanConfig.query.filter(club_id=cid).first()
|
||||
wechat = ((row.human_kefu_wechat if row else '') or '').strip()
|
||||
link = ((row.human_kefu_link if row else '') or '').strip()
|
||||
tip = ((row.human_kefu_tip if row else '') or '').strip()
|
||||
return {
|
||||
'club_id': cid,
|
||||
'complaint_withdraw_block': bool(row and row.complaint_withdraw_block),
|
||||
'human_kefu_wechat': wechat,
|
||||
'human_kefu_link': link,
|
||||
'human_kefu_tip': tip or ('添加人工客服微信咨询' if wechat else ('点击联系人工客服' if link else '')),
|
||||
'human_kefu_available': bool(wechat or link),
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +92,107 @@ def set_complaint_withdraw_block(club_id: str, enabled: bool) -> dict:
|
||||
return get_club_gongdan_config(cid)
|
||||
|
||||
|
||||
def update_club_gongdan_config(club_id: str, **fields) -> dict:
|
||||
"""更新俱乐部工单配置(提现拦截 / 人工客服联系方式)。"""
|
||||
cid = (club_id or '').strip()
|
||||
if not cid:
|
||||
raise GongdanError('俱乐部不能为空', 'need_club')
|
||||
row = ensure_club_gongdan_config(cid)
|
||||
dirty = []
|
||||
if 'complaint_withdraw_block' in fields and fields['complaint_withdraw_block'] is not None:
|
||||
val = bool(fields['complaint_withdraw_block'])
|
||||
if row.complaint_withdraw_block != val:
|
||||
row.complaint_withdraw_block = val
|
||||
dirty.append('complaint_withdraw_block')
|
||||
if 'human_kefu_wechat' in fields and fields['human_kefu_wechat'] is not None:
|
||||
val = str(fields['human_kefu_wechat'] or '').strip()[:64]
|
||||
if (row.human_kefu_wechat or '') != val:
|
||||
row.human_kefu_wechat = val
|
||||
dirty.append('human_kefu_wechat')
|
||||
if 'human_kefu_link' in fields and fields['human_kefu_link'] is not None:
|
||||
val = str(fields['human_kefu_link'] or '').strip()[:512]
|
||||
if (row.human_kefu_link or '') != val:
|
||||
row.human_kefu_link = val
|
||||
dirty.append('human_kefu_link')
|
||||
if 'human_kefu_tip' in fields and fields['human_kefu_tip'] is not None:
|
||||
val = str(fields['human_kefu_tip'] or '').strip()[:120]
|
||||
if (row.human_kefu_tip or '') != val:
|
||||
row.human_kefu_tip = val
|
||||
dirty.append('human_kefu_tip')
|
||||
if dirty:
|
||||
dirty.append('UpdateTime')
|
||||
row.save(update_fields=dirty)
|
||||
return get_club_gongdan_config(cid)
|
||||
|
||||
|
||||
def resolve_inviter_complaint_target(yonghuid: str, leixing: int) -> dict:
|
||||
"""
|
||||
管事/组长投诉只能针对本人邀请人,禁止搜索乱选:
|
||||
- 管事投诉:打手的 yaoqingren,且对方须是管事
|
||||
- 组长投诉:管事的 yaoqingren,且对方须是组长
|
||||
无邀请人则不可投诉。
|
||||
"""
|
||||
uid = (yonghuid or '').strip()
|
||||
if not uid:
|
||||
raise GongdanError('请先登录', 'need_login')
|
||||
|
||||
from users.models import UserDashou, UserGuanshi, UserZuzhang
|
||||
|
||||
if leixing == TYPE_GUANSHI:
|
||||
ds = UserDashou.query.filter(user__UserUID=uid).select_related('user').first()
|
||||
if not ds:
|
||||
raise GongdanError('仅打手可投诉自己的管事', 'need_dashou')
|
||||
inviter = (ds.yaoqingren or '').strip()
|
||||
if not inviter:
|
||||
raise GongdanError('你没有邀请人,无法投诉管事', 'no_inviter')
|
||||
gs = UserGuanshi.query.filter(user__UserUID=inviter, zhuangtai=1).select_related('user').first()
|
||||
if not gs:
|
||||
raise GongdanError('你的邀请人不是有效管事,无法投诉', 'inviter_not_guanshi')
|
||||
return {
|
||||
'role': TARGET_ROLE_GUANSHI,
|
||||
'role_label': TARGET_ROLE_LABELS[TARGET_ROLE_GUANSHI],
|
||||
'user_id': inviter,
|
||||
'nicheng': _user_nicheng(inviter),
|
||||
'can_complain': True,
|
||||
}
|
||||
|
||||
if leixing == TYPE_ZUZHANG:
|
||||
gs = UserGuanshi.query.filter(user__UserUID=uid).select_related('user').first()
|
||||
if not gs:
|
||||
raise GongdanError('仅管事可投诉自己的组长', 'need_guanshi')
|
||||
inviter = (gs.yaoqingren or '').strip()
|
||||
if not inviter:
|
||||
raise GongdanError('你没有邀请人,无法投诉组长', 'no_inviter')
|
||||
zz = UserZuzhang.query.filter(user__UserUID=inviter, zhuangtai=1).select_related('user').first()
|
||||
if not zz:
|
||||
raise GongdanError('你的邀请人不是有效组长,无法投诉', 'inviter_not_zuzhang')
|
||||
return {
|
||||
'role': TARGET_ROLE_ZUZHANG,
|
||||
'role_label': TARGET_ROLE_LABELS[TARGET_ROLE_ZUZHANG],
|
||||
'user_id': inviter,
|
||||
'nicheng': _user_nicheng(inviter),
|
||||
'can_complain': True,
|
||||
}
|
||||
|
||||
raise GongdanError('仅管事/组长投诉需要邀请人', 'bad_type')
|
||||
|
||||
|
||||
def peek_inviter_complaint_target(yonghuid: str, leixing: int) -> dict:
|
||||
"""前端展示用:失败时返回 can_complain=False + 原因,不抛错。"""
|
||||
try:
|
||||
return resolve_inviter_complaint_target(yonghuid, leixing)
|
||||
except GongdanError as e:
|
||||
role = TARGET_ROLE_GUANSHI if leixing == TYPE_GUANSHI else TARGET_ROLE_ZUZHANG
|
||||
return {
|
||||
'role': role,
|
||||
'role_label': TARGET_ROLE_LABELS.get(role, ''),
|
||||
'user_id': '',
|
||||
'nicheng': '',
|
||||
'can_complain': False,
|
||||
'reason': str(e),
|
||||
}
|
||||
|
||||
|
||||
def _user_nicheng(uid: str) -> str:
|
||||
"""展示昵称:打手 nicheng → 老板 nickname → 用户{UID}。不把 UID 当昵称展示。"""
|
||||
uid = (uid or '').strip()
|
||||
@@ -103,32 +218,21 @@ def _user_nicheng(uid: str) -> str:
|
||||
return '用户'
|
||||
|
||||
|
||||
def _resolve_target(leixing, guanshi_id=None, target_role=None, target_user_id=None):
|
||||
"""归一化被投诉对象;兼容旧 guanshi_id。管事/组长投诉必须选人。"""
|
||||
def _resolve_target(leixing, guanshi_id=None, target_role=None, target_user_id=None, *, yonghuid=None):
|
||||
"""归一化被投诉对象。管事/组长投诉强制本人邀请人,忽略前端传入的选人。"""
|
||||
role = (target_role or '').strip().lower()
|
||||
tid = (target_user_id or '').strip()
|
||||
gid = (guanshi_id or '').strip() or None
|
||||
|
||||
if leixing == TYPE_GUANSHI:
|
||||
role = TARGET_ROLE_GUANSHI
|
||||
tid = tid or (gid or '')
|
||||
if not tid:
|
||||
raise GongdanError('请选择被投诉的管事', 'need_guanshi')
|
||||
from users.models import UserGuanshi
|
||||
if not UserGuanshi.query.filter(user__UserUID=tid).exists():
|
||||
if not UserGuanshi.objects.filter(user__UserUID=tid).exists():
|
||||
raise GongdanError('管事不存在,请重新选择', 'guanshi_not_found')
|
||||
return role, tid, tid
|
||||
info = resolve_inviter_complaint_target(yonghuid, TYPE_GUANSHI)
|
||||
tid = info['user_id']
|
||||
return TARGET_ROLE_GUANSHI, tid, tid
|
||||
|
||||
if leixing == TYPE_ZUZHANG:
|
||||
role = TARGET_ROLE_ZUZHANG
|
||||
if not tid:
|
||||
raise GongdanError('请选择被投诉的组长', 'need_zuzhang')
|
||||
from users.models import UserZuzhang
|
||||
if not UserZuzhang.query.filter(user__UserUID=tid).exists():
|
||||
if not UserZuzhang.objects.filter(user__UserUID=tid).exists():
|
||||
raise GongdanError('组长不存在,请重新选择', 'zuzhang_not_found')
|
||||
return role, tid, None
|
||||
info = resolve_inviter_complaint_target(yonghuid, TYPE_ZUZHANG)
|
||||
tid = info['user_id']
|
||||
return TARGET_ROLE_ZUZHANG, tid, None
|
||||
|
||||
if role and tid:
|
||||
if role not in TARGET_ROLE_LABELS and role not in ('order', 'recharge', 'penalty'):
|
||||
@@ -388,65 +492,34 @@ def check_complaint_blocks_withdraw(user_main, club_id=None):
|
||||
return True, '', {}
|
||||
|
||||
|
||||
def search_complaint_targets(*, role, keyword='', club_id=None, page=1, page_size=20):
|
||||
"""按昵称/UID 搜管事或组长,供投诉选人(返回 uid 仅供提交,前端展示昵称)。"""
|
||||
def search_complaint_targets(*, role, keyword='', club_id=None, page=1, page_size=20, yonghuid=None):
|
||||
"""已废弃搜索选人:仅返回本人邀请人(防乱投诉)。keyword/club 忽略。"""
|
||||
role = (role or '').strip().lower()
|
||||
keyword = (keyword or '').strip()
|
||||
page = max(int(page or 1), 1)
|
||||
page_size = min(max(int(page_size or 20), 1), 50)
|
||||
cid = (club_id or '').strip()
|
||||
|
||||
from users.models import UserGuanshi, UserZuzhang, UserBoss
|
||||
|
||||
rows = []
|
||||
if role == TARGET_ROLE_GUANSHI:
|
||||
qs = UserGuanshi.query.filter(zhuangtai=1).select_related('user')
|
||||
if cid:
|
||||
qs = qs.filter(user__ClubID=cid)
|
||||
if keyword:
|
||||
boss_uids = list(
|
||||
UserBoss.query.filter(nickname__icontains=keyword).values_list('user__UserUID', flat=True)[:80]
|
||||
)
|
||||
qs = qs.filter(Q(user__UserUID__icontains=keyword) | Q(user__UserUID__in=boss_uids))
|
||||
qs = qs.order_by('-id')
|
||||
total = qs.count()
|
||||
start = (page - 1) * page_size
|
||||
for g in qs[start:start + page_size]:
|
||||
uid = getattr(getattr(g, 'user', None), 'UserUID', None)
|
||||
if not uid:
|
||||
continue
|
||||
rows.append({
|
||||
'user_id': str(uid),
|
||||
'nicheng': _user_nicheng(str(uid)),
|
||||
'role': TARGET_ROLE_GUANSHI,
|
||||
'role_label': TARGET_ROLE_LABELS[TARGET_ROLE_GUANSHI],
|
||||
})
|
||||
leixing = TYPE_GUANSHI
|
||||
elif role == TARGET_ROLE_ZUZHANG:
|
||||
qs = UserZuzhang.query.filter(zhuangtai=1).select_related('user')
|
||||
if cid:
|
||||
qs = qs.filter(user__ClubID=cid)
|
||||
if keyword:
|
||||
boss_uids = list(
|
||||
UserBoss.query.filter(nickname__icontains=keyword).values_list('user__UserUID', flat=True)[:80]
|
||||
)
|
||||
qs = qs.filter(Q(user__UserUID__icontains=keyword) | Q(user__UserUID__in=boss_uids))
|
||||
qs = qs.order_by('-id')
|
||||
total = qs.count()
|
||||
start = (page - 1) * page_size
|
||||
for z in qs[start:start + page_size]:
|
||||
uid = getattr(getattr(z, 'user', None), 'UserUID', None)
|
||||
if not uid:
|
||||
continue
|
||||
rows.append({
|
||||
'user_id': str(uid),
|
||||
'nicheng': _user_nicheng(str(uid)),
|
||||
'role': TARGET_ROLE_ZUZHANG,
|
||||
'role_label': TARGET_ROLE_LABELS[TARGET_ROLE_ZUZHANG],
|
||||
})
|
||||
leixing = TYPE_ZUZHANG
|
||||
else:
|
||||
raise GongdanError('role 须为 guanshi 或 zuzhang', 'bad_role')
|
||||
|
||||
return {'list': rows, 'total': total, 'page': page, 'page_size': page_size, 'role': role}
|
||||
info = peek_inviter_complaint_target(yonghuid, leixing)
|
||||
rows = []
|
||||
if info.get('can_complain') and info.get('user_id'):
|
||||
rows.append({
|
||||
'user_id': info['user_id'],
|
||||
'nicheng': info.get('nicheng') or '',
|
||||
'role': info.get('role') or role,
|
||||
'role_label': info.get('role_label') or '',
|
||||
})
|
||||
return {
|
||||
'list': rows,
|
||||
'total': len(rows),
|
||||
'page': 1,
|
||||
'page_size': 1,
|
||||
'role': role,
|
||||
'can_complain': bool(info.get('can_complain')),
|
||||
'reason': info.get('reason') or '',
|
||||
'invite_only': True,
|
||||
}
|
||||
|
||||
|
||||
def list_gongdan_for_user(yonghuid, *, scope='created', club_id=None, page=1, page_size=20):
|
||||
@@ -544,7 +617,11 @@ def create_gongdan(
|
||||
leixing, order_id, chongzhi_id, fadan_id, guanshi_id or target_user_id,
|
||||
)
|
||||
t_role, t_uid, guanshi_id = _resolve_target(
|
||||
leixing, guanshi_id=guanshi_id, target_role=target_role, target_user_id=target_user_id or guanshi_id,
|
||||
leixing,
|
||||
guanshi_id=guanshi_id,
|
||||
target_role=target_role,
|
||||
target_user_id=target_user_id or guanshi_id,
|
||||
yonghuid=yonghuid,
|
||||
)
|
||||
|
||||
gid = _gen_gongdan_id()
|
||||
|
||||
@@ -12,6 +12,7 @@ from gongdan.views import (
|
||||
GongdanApplyInterveneView,
|
||||
GongdanWithdrawCheckView,
|
||||
GongdanSearchTargetView,
|
||||
GongdanMyInviterTargetView,
|
||||
GongdanMetaView,
|
||||
ChongzhiRecordListView,
|
||||
GongdanAdminListView,
|
||||
@@ -31,6 +32,7 @@ urlpatterns = [
|
||||
path('apply-intervene/', GongdanApplyInterveneView.as_view(), name='申请平台介入'),
|
||||
path('withdraw-block-check/', GongdanWithdrawCheckView.as_view(), name='提现投诉拦截检查'),
|
||||
path('search-target/', GongdanSearchTargetView.as_view(), name='搜索被投诉对象'),
|
||||
path('my-inviter-target/', GongdanMyInviterTargetView.as_view(), name='本人邀请人投诉对象'),
|
||||
path('meta/', GongdanMetaView.as_view(), name='工单枚举'),
|
||||
path('chongzhi-list/', ChongzhiRecordListView.as_view(), name='我的充值记录'),
|
||||
path('admin/list/', GongdanAdminListView.as_view(), name='客服工单列表'),
|
||||
|
||||
@@ -11,6 +11,7 @@ from rest_framework import status
|
||||
|
||||
from gongdan.constants import (
|
||||
MAX_IMAGES, OSS_PREFIX, PERM_VIEW, PERM_HANDLE, TYPE_LABELS, STATUS_LABELS,
|
||||
TYPE_GUANSHI, TYPE_ZUZHANG,
|
||||
)
|
||||
from gongdan.models import Gongdan
|
||||
from gongdan.services import (
|
||||
@@ -29,6 +30,8 @@ from gongdan.services import (
|
||||
search_complaint_targets,
|
||||
get_club_gongdan_config,
|
||||
set_complaint_withdraw_block,
|
||||
update_club_gongdan_config,
|
||||
peek_inviter_complaint_target,
|
||||
)
|
||||
from utils.oss_utils import upload_to_oss, validate_image
|
||||
|
||||
@@ -284,7 +287,7 @@ class GongdanWithdrawCheckView(APIView):
|
||||
|
||||
|
||||
class GongdanSearchTargetView(APIView):
|
||||
"""搜索被投诉管事/组长。POST /gongdan/search-target/"""
|
||||
"""本人邀请人(已禁止全库搜索)。POST /gongdan/search-target/"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
@@ -296,21 +299,48 @@ class GongdanSearchTargetView(APIView):
|
||||
club_id=_resolve_club(request),
|
||||
page=d.get('page') or 1,
|
||||
page_size=d.get('page_size') or 20,
|
||||
yonghuid=request.user.UserUID,
|
||||
)
|
||||
return _ok(data)
|
||||
except GongdanError as e:
|
||||
return _err(str(e))
|
||||
|
||||
|
||||
class GongdanMyInviterTargetView(APIView):
|
||||
"""查询本人可投诉的邀请人。POST /gongdan/my-inviter-target/ body: {leixing:4|6}"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
leixing = int(request.data.get('leixing') or 0)
|
||||
except (TypeError, ValueError):
|
||||
return _err('投诉类型无效')
|
||||
if leixing not in (TYPE_GUANSHI, TYPE_ZUZHANG):
|
||||
return _err('仅管事/组长投诉需要邀请人')
|
||||
return _ok(peek_inviter_complaint_target(request.user.UserUID, leixing))
|
||||
|
||||
|
||||
class GongdanMetaView(APIView):
|
||||
"""类型/状态枚举,供前端展示。GET /gongdan/meta/"""
|
||||
"""类型/状态枚举 + 人工客服联系方式。GET /gongdan/meta/"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
club_id = _resolve_club(request)
|
||||
cfg = get_club_gongdan_config(club_id) if club_id else get_club_gongdan_config('')
|
||||
return _ok({
|
||||
'types': [{'value': k, 'label': v} for k, v in TYPE_LABELS.items()],
|
||||
'statuses': [{'value': k, 'label': v} for k, v in STATUS_LABELS.items()],
|
||||
'max_images': MAX_IMAGES,
|
||||
'human_kefu': {
|
||||
'wechat': cfg.get('human_kefu_wechat') or '',
|
||||
'link': cfg.get('human_kefu_link') or '',
|
||||
'tip': cfg.get('human_kefu_tip') or '',
|
||||
'available': bool(cfg.get('human_kefu_available')),
|
||||
},
|
||||
'inviter_targets': {
|
||||
'guanshi': peek_inviter_complaint_target(request.user.UserUID, TYPE_GUANSHI),
|
||||
'zuzhang': peek_inviter_complaint_target(request.user.UserUID, TYPE_ZUZHANG),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -516,9 +546,9 @@ class GongdanAdminHandleView(APIView):
|
||||
|
||||
class GongdanAdminClubConfigView(APIView):
|
||||
"""
|
||||
当前俱乐部工单开关。
|
||||
当前俱乐部工单开关 + 人工客服联系方式。
|
||||
GET/POST /gongdan/admin/club-config/
|
||||
字段:complaint_withdraw_block(未结清投诉/牵连时禁止提现)
|
||||
字段:complaint_withdraw_block / human_kefu_wechat / human_kefu_link / human_kefu_tip
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@@ -545,15 +575,25 @@ class GongdanAdminClubConfigView(APIView):
|
||||
club_id = self._club_id(request)
|
||||
if not club_id:
|
||||
return _err('请先选择俱乐部')
|
||||
raw = request.data.get('complaint_withdraw_block')
|
||||
if raw is None:
|
||||
return _err('缺少 complaint_withdraw_block')
|
||||
if isinstance(raw, str):
|
||||
enabled = raw.strip().lower() in ('1', 'true', 'yes', 'on')
|
||||
else:
|
||||
enabled = bool(raw)
|
||||
d = request.data or {}
|
||||
patch = {}
|
||||
if 'complaint_withdraw_block' in d:
|
||||
raw = d.get('complaint_withdraw_block')
|
||||
if isinstance(raw, str):
|
||||
patch['complaint_withdraw_block'] = raw.strip().lower() in ('1', 'true', 'yes', 'on')
|
||||
else:
|
||||
patch['complaint_withdraw_block'] = bool(raw)
|
||||
for key in ('human_kefu_wechat', 'human_kefu_link', 'human_kefu_tip'):
|
||||
if key in d:
|
||||
patch[key] = d.get(key)
|
||||
if not patch:
|
||||
return _err('缺少可保存字段')
|
||||
try:
|
||||
data = set_complaint_withdraw_block(club_id, enabled)
|
||||
# 兼容旧前端只传 withdraw 开关
|
||||
if list(patch.keys()) == ['complaint_withdraw_block']:
|
||||
data = set_complaint_withdraw_block(club_id, patch['complaint_withdraw_block'])
|
||||
else:
|
||||
data = update_club_gongdan_config(club_id, **patch)
|
||||
return _ok(data, '已保存')
|
||||
except GongdanError as e:
|
||||
return _err(str(e))
|
||||
|
||||
@@ -206,6 +206,75 @@ def serialize_merchant_stat(row) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def pool_merchant_stat_payload(row) -> dict | None:
|
||||
"""
|
||||
抢单池展示用:无订单样本则不返回;
|
||||
有订单则返回成交率/罚款率;平均成交时长仅有结算样本时返回。
|
||||
"""
|
||||
if not row:
|
||||
return None
|
||||
n = int(row.order_count or 0)
|
||||
if n <= 0:
|
||||
return None
|
||||
full = serialize_merchant_stat(row)
|
||||
out = {
|
||||
'deal_rate': full['deal_rate'],
|
||||
'fine_rate': full['fine_rate'],
|
||||
'deal_rate_text': f"{round(full['deal_rate'] * 100, 1)}%",
|
||||
'fine_rate_text': f"{round(full['fine_rate'] * 100, 1)}%",
|
||||
}
|
||||
sc = int(full.get('settle_duration_sample_count') or 0)
|
||||
if sc > 0 and full.get('avg_deal_hours'):
|
||||
h = float(full['avg_deal_hours'])
|
||||
out['avg_deal_hours'] = h
|
||||
if h < 1:
|
||||
out['avg_deal_hours_text'] = f'{max(1, int(round(h * 60)))}分钟'
|
||||
else:
|
||||
out['avg_deal_hours_text'] = f'{round(h, 1)}小时'
|
||||
return out
|
||||
|
||||
|
||||
def batch_pool_merchant_stats(club_id: str, merchant_uids, order_club_map=None) -> dict:
|
||||
"""
|
||||
批量取商家抢单池指标。
|
||||
order_club_map: {merchant_uid: order_club_id} 优先用订单所属俱乐部查指标。
|
||||
返回 {merchant_uid: payload}
|
||||
"""
|
||||
from jituan.models import MerchantDealStat
|
||||
uids = [str(u).strip() for u in (merchant_uids or []) if u and str(u).strip()]
|
||||
if not uids:
|
||||
return {}
|
||||
default_club = (club_id or '').strip()
|
||||
club_ids = {default_club} if default_club else set()
|
||||
if order_club_map:
|
||||
for cid in order_club_map.values():
|
||||
c = (cid or '').strip()
|
||||
if c:
|
||||
club_ids.add(c)
|
||||
if not club_ids:
|
||||
return {}
|
||||
rows = list(
|
||||
MerchantDealStat.query.filter(
|
||||
club_id__in=list(club_ids),
|
||||
merchant_uid__in=uids,
|
||||
)
|
||||
)
|
||||
by_key = {(r.club_id, r.merchant_uid): r for r in rows}
|
||||
out = {}
|
||||
for uid in uids:
|
||||
prefer = ((order_club_map or {}).get(uid) or default_club or '').strip()
|
||||
row = by_key.get((prefer, uid))
|
||||
if not row:
|
||||
for cid in club_ids:
|
||||
row = by_key.get((cid, uid))
|
||||
if row:
|
||||
break
|
||||
payload = pool_merchant_stat_payload(row)
|
||||
if payload:
|
||||
out[uid] = payload
|
||||
return out
|
||||
|
||||
|
||||
def serialize_dashou_stat(row) -> dict:
|
||||
n = int(row.order_count or 0)
|
||||
c = int(row.completed_count or 0)
|
||||
|
||||
@@ -163,7 +163,7 @@ class DashouDingdanHuoquView(APIView):
|
||||
'AssignedID', 'AssignedResponse', 'AssignedResponseAt', 'AssignedLaterNote',
|
||||
'ProductTypeID', 'ImageURL', 'GrabRequirement',
|
||||
'MembershipID', 'CommissionReq', 'Description', 'Remark', 'CreateTime',
|
||||
'MerchantNickname'
|
||||
'MerchantNickname', 'ClubID',
|
||||
)
|
||||
|
||||
# 10. 转换为列表
|
||||
@@ -308,6 +308,28 @@ class DashouDingdanHuoquView(APIView):
|
||||
|
||||
identity_tag_map = batch_identity_tags(club_id, identity_pairs) if identity_pairs else {}
|
||||
|
||||
# --- 商家成交指标(有样本才返回) ---
|
||||
merchant_stat_map = {}
|
||||
if merchant_uids:
|
||||
try:
|
||||
from jituan.services.deal_stats import batch_pool_merchant_stats
|
||||
order_club_by_merchant = {}
|
||||
for item in dingdan_list:
|
||||
if item['Platform'] != 2:
|
||||
continue
|
||||
oid = item['OrderID']
|
||||
sj = shangjia_id_map.get(oid)
|
||||
if not sj:
|
||||
continue
|
||||
ocid = (item.get('ClubID') or club_id or '').strip()
|
||||
if sj not in order_club_by_merchant and ocid:
|
||||
order_club_by_merchant[sj] = ocid
|
||||
merchant_stat_map = batch_pool_merchant_stats(
|
||||
club_id, merchant_uids, order_club_map=order_club_by_merchant,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"批量商家成交指标失败: {e}", exc_info=True)
|
||||
|
||||
# 12. 格式化返回数据
|
||||
formatted_list = []
|
||||
for order in dingdan_list:
|
||||
@@ -320,7 +342,7 @@ class DashouDingdanHuoquView(APIView):
|
||||
order, yonghuid, club_id=club_id, user_clumber=user_clumber,
|
||||
)
|
||||
)
|
||||
formatted_list.append({
|
||||
row = {
|
||||
'dingdan_id': order['OrderID'],
|
||||
'zhuangtai': order['Status'],
|
||||
'pingtai': order['Platform'],
|
||||
@@ -353,7 +375,10 @@ class DashouDingdanHuoquView(APIView):
|
||||
'shangjia_identity_biaoqian': identity_tag_map.get((sj_uid, SHENFEN_SHANGJIA), []) if sj_uid else [],
|
||||
'zhiding_identity_biaoqian': identity_tag_map.get((zid, SHENFEN_DASHOU), []) if zid else [],
|
||||
'shangjia_youzhi': bool(youzhi_map.get(sj_uid)) if sj_uid else False,
|
||||
})
|
||||
}
|
||||
if sj_uid and sj_uid in merchant_stat_map:
|
||||
row['shangjia_deal_stat'] = merchant_stat_map[sj_uid]
|
||||
formatted_list.append(row)
|
||||
|
||||
has_more = (page * page_size) < total
|
||||
|
||||
|
||||
Reference in New Issue
Block a user