From 9575edcfc4299ea139ba207e7e656b61516c03e4 Mon Sep 17 00:00:00 2001 From: XingQue Date: Thu, 25 Jun 2026 15:35:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=AC=E5=91=8A=E6=8C=89=E9=A1=B5?= =?UTF-8?q?=E3=80=81=E8=BA=AB=E4=BB=BD=E6=A0=87=E7=AD=BE=E3=80=81=E6=8B=BC?= =?UTF-8?q?=E5=A4=9A=E5=A4=9A=E8=AE=A2=E5=8D=95=E5=8F=B7=E3=80=81=E6=B4=BE?= =?UTF-8?q?=E5=8D=95=E4=B8=8E=E6=8A=A2=E5=8D=95=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/view.py | 28 +- config/migrations/0008_gonggao_page_key.py | 34 +++ config/models.py | 7 +- config/views.py | 2 +- jituan/management/commands/seed_kefu_menu.py | 2 + jituan/migrations/0005_identity_tag.py | 56 ++++ jituan/models.py | 33 +++ jituan/services/display_config.py | 8 +- jituan/services/identity_tag.py | 110 ++++++++ jituan/urls.py | 13 + jituan/views_display.py | 13 +- jituan/views_identity_tag.py | 241 ++++++++++++++++++ .../0008_order_waibu_penalty_staff.py | 27 ++ orders/models.py | 8 + orders/views.py | 62 ++++- users/migrations/0004_user_shangjia_youzhi.py | 16 ++ users/models.py | 1 + 17 files changed, 646 insertions(+), 15 deletions(-) create mode 100644 config/migrations/0008_gonggao_page_key.py create mode 100644 jituan/migrations/0005_identity_tag.py create mode 100644 jituan/services/identity_tag.py create mode 100644 jituan/views_identity_tag.py create mode 100644 orders/migrations/0008_order_waibu_penalty_staff.py create mode 100644 users/migrations/0004_user_shangjia_youzhi.py diff --git a/backend/view.py b/backend/view.py index cb1df07..14a384b 100644 --- a/backend/view.py +++ b/backend/view.py @@ -1574,6 +1574,7 @@ class KefuGetShangjiaDetailView(APIView): 'jinriliushui': str(shangjia.jinriliushui), 'jinyuedingdan': shangjia.jinyuedingdan, 'jinyueliushui': str(shangjia.jinyueliushui), + 'is_youzhi_shangjia': bool(shangjia.is_youzhi_shangjia), 'CreateTime': shangjia.CreateTime.isoformat() if shangjia.CreateTime else None, 'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '', } @@ -1607,7 +1608,7 @@ class KefuUpdateShangjiaView(APIView): return Response({'code': 401, 'msg': '缺少username'}) if not yonghuid: return Response({'code': 400, 'msg': '缺少商家ID'}) - if action not in ('add_balance', 'sub_balance', 'ban', 'unban'): + if action not in ('add_balance', 'sub_balance', 'ban', 'unban', 'set_youzhi'): return Response({'code': 400, 'msg': 'action参数无效'}) # 权限校验 @@ -1616,7 +1617,7 @@ class KefuUpdateShangjiaView(APIView): return permissions # 操作权限校验 - if action in ('add_balance', 'ban', 'unban') and '1100a' not in permissions: + if action in ('add_balance', 'ban', 'unban', 'set_youzhi') and '1100a' not in permissions: return Response({'code': 403, 'msg': '无权限执行此操作(需要1100a)'}) if action == 'sub_balance' and '1100b' not in permissions: return Response({'code': 403, 'msg': '无权限减少商家余额(需要1100b)'}) @@ -1717,6 +1718,29 @@ class KefuUpdateShangjiaView(APIView): ) return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}}) + if action == 'set_youzhi': + if '1100a' not in permissions: + return Response({'code': 403, 'msg': '无权限设置优质商家(需要1100a)'}) + is_youzhi = bool(request.data.get('is_youzhi_shangjia', False)) + shangjia.is_youzhi_shangjia = is_youzhi + shangjia.save(update_fields=['is_youzhi_shangjia']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_SHANGJIA, + qitashuoming=( + f'【后台】设置优质商家:{is_youzhi},' + f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' + ), + ) + return Response({ + 'code': 0, + 'msg': '优质商家设置成功', + 'data': {'is_youzhi_shangjia': is_youzhi}, + }) + + return Response({'code': 400, 'msg': '未知操作'}) + diff --git a/config/migrations/0008_gonggao_page_key.py b/config/migrations/0008_gonggao_page_key.py new file mode 100644 index 0000000..31711ea --- /dev/null +++ b/config/migrations/0008_gonggao_page_key.py @@ -0,0 +1,34 @@ +from django.db import migrations, models + + +def backfill_gonggao_page_key(apps, schema_editor): + Gonggao = apps.get_model('config', 'Gonggao') + for row in Gonggao.objects.filter(page_key='').only('id'): + row.page_key = 'order_pool' + row.save(update_fields=['page_key']) + for row in Gonggao.objects.filter(page_key__isnull=True).only('id'): + row.page_key = 'order_pool' + row.save(update_fields=['page_key']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0007_platform_income_log'), + ] + + operations = [ + migrations.AddField( + model_name='gonggao', + name='page_key', + field=models.CharField( + db_index=True, default='order_pool', max_length=32, + verbose_name='页面标识(order_pool/accept_order/merchant_home)', + ), + ), + migrations.RunPython(backfill_gonggao_page_key, migrations.RunPython.noop), + migrations.AlterUniqueTogether( + name='gonggao', + unique_together={('club_id', 'page_key')}, + ), + ] diff --git a/config/models.py b/config/models.py index a924d81..d77d12c 100644 --- a/config/models.py +++ b/config/models.py @@ -24,15 +24,20 @@ class Lunbo(QModel): class Gonggao(QModel): club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID') + page_key = models.CharField( + max_length=32, default='order_pool', db_index=True, + verbose_name='页面标识(order_pool/accept_order/merchant_home)', + ) NoticeType = models.IntegerField(null=True, blank=True, verbose_name='公告类型') CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间') Content = models.TextField(null=True, blank=True, verbose_name='公告介绍') + class Meta: db_table = 'gonggao' verbose_name = '公告' verbose_name_plural = '公告' - # 不要索引!数据量太小 + unique_together = [['club_id', 'page_key']] class Qunpeizhi(QModel): GroupType = models.IntegerField(null=True, blank=True, verbose_name='群类型:1.打手 2.管事') diff --git a/config/views.py b/config/views.py index 0f86432..b69a977 100644 --- a/config/views.py +++ b/config/views.py @@ -138,7 +138,7 @@ class ShangpinGonggaoView(APIView): from jituan.services.display_config import get_gonggao_content, get_lunbo_urls, normalize_page_key page_key = normalize_page_key(request.data.get('page_key'), image_type=1) - gonggao_content = get_gonggao_content(request, notice_type=1) + gonggao_content = get_gonggao_content(request, notice_type=1, page_key=page_key) lunbo_urls = get_lunbo_urls(request, page_key=page_key, image_type=1) response_data = { "shangpingonggao": gonggao_content, diff --git a/jituan/management/commands/seed_kefu_menu.py b/jituan/management/commands/seed_kefu_menu.py index 4ca85ba..f48c49d 100644 --- a/jituan/management/commands/seed_kefu_menu.py +++ b/jituan/management/commands/seed_kefu_menu.py @@ -45,6 +45,8 @@ MENU_ROWS = [ 'perm_codes': ['1100a', '1100b']}, {'page_id': 'user.zuzhang', 'name': '组长管理', 'path': '/user/zuzhang', 'parent_id': 'user', 'sort_order': 54, 'perm_codes': ['6600a', '6600b', '6600c', '6600d', '6600e']}, + {'page_id': 'user.identity-tag', 'name': '身份装饰标签', 'path': '/user/identity-tag', 'parent_id': 'user', 'sort_order': 55, + 'perm_codes': ['sfbq666']}, # 提现 {'page_id': 'withdraw', 'name': '提现管理', 'path': '', 'parent_id': '', 'sort_order': 60}, {'page_id': 'withdraw.audit', 'name': '提现审核', 'path': '/withdraw/audit', 'parent_id': 'withdraw', 'sort_order': 61, diff --git a/jituan/migrations/0005_identity_tag.py b/jituan/migrations/0005_identity_tag.py new file mode 100644 index 0000000..ed77b56 --- /dev/null +++ b/jituan/migrations/0005_identity_tag.py @@ -0,0 +1,56 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('jituan', '0004_kefu_menu_page'), + ] + + operations = [ + migrations.CreateModel( + name='IdentityTag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=16)), + ('name', models.CharField(max_length=64, verbose_name='标签名')), + ('color', models.CharField(default='#000000', max_length=32, verbose_name='主色')), + ('flash_enabled', models.BooleanField(default=False, verbose_name='闪光特效')), + ('texiao_json', models.TextField(blank=True, default='', verbose_name='特效JSON')), + ('sort_order', models.IntegerField(default=0)), + ('status', models.IntegerField(default=1, verbose_name='1启用0停用')), + ('CreateTime', models.DateTimeField(auto_now_add=True)), + ('UpdateTime', models.DateTimeField(auto_now=True)), + ], + options={ + 'db_table': 'identity_tag', + 'ordering': ['sort_order', 'id'], + }, + ), + migrations.CreateModel( + name='IdentityTagBind', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=16)), + ('yonghuid', models.CharField(db_index=True, max_length=7)), + ('shenfen', models.IntegerField(verbose_name='1打手2管事3商家4组长')), + ('CreateTime', models.DateTimeField(auto_now_add=True)), + ('tag', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='binds', to='jituan.identitytag', + )), + ], + options={ + 'db_table': 'identity_tag_bind', + }, + ), + migrations.AddIndex( + model_name='identitytagbind', + index=models.Index(fields=['club_id', 'yonghuid', 'shenfen'], name='identity_tag_bind_lookup'), + ), + migrations.AlterUniqueTogether( + name='identitytagbind', + unique_together={('club_id', 'yonghuid', 'shenfen', 'tag')}, + ), + ] diff --git a/jituan/models.py b/jituan/models.py index d171688..a2ed461 100644 --- a/jituan/models.py +++ b/jituan/models.py @@ -157,6 +157,39 @@ class ClubFadanFenhongLilv(QModel): unique_together = [['club_id', 'shenfen']] +class IdentityTag(QModel): + """身份装饰标签定义(与考核称号、订单需求标签分离)。""" + club_id = models.CharField(max_length=16, db_index=True) + name = models.CharField(max_length=64, verbose_name='标签名') + color = models.CharField(max_length=32, default='#000000', verbose_name='主色') + flash_enabled = models.BooleanField(default=False, verbose_name='闪光特效') + texiao_json = models.TextField(blank=True, default='', verbose_name='特效JSON') + sort_order = models.IntegerField(default=0) + status = models.IntegerField(default=1, verbose_name='1启用0停用') + CreateTime = models.DateTimeField(auto_now_add=True) + UpdateTime = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'identity_tag' + ordering = ['sort_order', 'id'] + + +class IdentityTagBind(QModel): + """用户身份与装饰标签绑定。""" + club_id = models.CharField(max_length=16, db_index=True) + yonghuid = models.CharField(max_length=7, db_index=True) + shenfen = models.IntegerField(verbose_name='1打手2管事3商家4组长') + tag = models.ForeignKey(IdentityTag, on_delete=models.CASCADE, related_name='binds') + CreateTime = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'identity_tag_bind' + unique_together = [['club_id', 'yonghuid', 'shenfen', 'tag']] + indexes = [ + models.Index(fields=['club_id', 'yonghuid', 'shenfen']), + ] + + class KefuMenuPage(QModel): """客服后台左侧菜单页(固定 page_id,与前端路由对应)。""" page_id = models.CharField(max_length=64, primary_key=True, verbose_name='页面ID') diff --git a/jituan/services/display_config.py b/jituan/services/display_config.py index 341673e..8b1e92d 100644 --- a/jituan/services/display_config.py +++ b/jituan/services/display_config.py @@ -58,10 +58,13 @@ def _display_club_id(request): return resolve_effective_club_id(request, None) -def get_gonggao_content(request, notice_type=1): +def get_gonggao_content(request, notice_type=1, page_key=None): from config.models import Gonggao club_id = _display_club_id(request) - obj = Gonggao.query.filter(club_id=club_id, NoticeType=notice_type).first() + pk = normalize_page_key(page_key, image_type=1) if page_key else LUNBO_PAGE_ORDER_POOL + obj = Gonggao.query.filter(club_id=club_id, page_key=pk).first() + if not obj and notice_type: + obj = Gonggao.query.filter(club_id=club_id, NoticeType=notice_type).first() return obj.Content if obj and obj.Content else '' @@ -131,6 +134,7 @@ def copy_display_config(template_club_id, new_club_id): for row in Gonggao.query.filter(club_id=template_club_id): Gonggao.query.create( club_id=new_club_id, + page_key=getattr(row, 'page_key', None) or 'order_pool', NoticeType=row.NoticeType, Content=row.Content, ) diff --git a/jituan/services/identity_tag.py b/jituan/services/identity_tag.py new file mode 100644 index 0000000..231d741 --- /dev/null +++ b/jituan/services/identity_tag.py @@ -0,0 +1,110 @@ +"""身份装饰标签查询与组装(与考核称号、订单需求标签分离)。""" +import json + +from jituan.models import IdentityTag, IdentityTagBind + +SHENFEN_DASHOU = 1 +SHENFEN_GUANSHI = 2 +SHENFEN_SHANGJIA = 3 +SHENFEN_ZUZHANG = 4 + + +def _tag_texiao(tag): + if tag.texiao_json: + try: + return json.loads(tag.texiao_json) + except (TypeError, ValueError, json.JSONDecodeError): + pass + return { + 'color': tag.color or '#000000', + 'flash': bool(tag.flash_enabled), + } + + +def format_tag_item(tag): + return { + 'id': tag.id, + 'mingcheng': tag.name, + 'texiao_json': _tag_texiao(tag), + } + + +def get_identity_tags_for_user(club_id, yonghuid, shenfen): + """单用户单身份装饰标签列表。""" + binds = ( + IdentityTagBind.query.filter( + club_id=club_id, yonghuid=yonghuid, shenfen=shenfen, + ) + .select_related('tag') + .order_by('tag__sort_order', 'tag__id') + ) + result = [] + for b in binds: + tag = b.tag + if tag.status != 1: + continue + result.append(format_tag_item(tag)) + return result + + +def batch_identity_tags(club_id, yonghuid_shenfen_pairs): + """ + 批量查询:[(yonghuid, shenfen), ...] -> {(yonghuid, shenfen): [tags]} + """ + if not yonghuid_shenfen_pairs: + return {} + yonghuids = {p[0] for p in yonghuid_shenfen_pairs} + shenfens = {p[1] for p in yonghuid_shenfen_pairs} + binds = ( + IdentityTagBind.query.filter( + club_id=club_id, yonghuid__in=yonghuids, shenfen__in=shenfens, + ) + .select_related('tag') + .order_by('tag__sort_order', 'tag__id') + ) + out = {} + pair_set = set(yonghuid_shenfen_pairs) + for b in binds: + key = (b.yonghuid, b.shenfen) + if key not in pair_set: + continue + if b.tag.status != 1: + continue + out.setdefault(key, []).append(format_tag_item(b.tag)) + return out + + +def get_my_identity_tags(user, club_id): + """当前用户各已激活身份的装饰标签。""" + uid = user.UserUID + data = {'dashou': [], 'guanshi': [], 'zuzhang': [], 'shangjia': []} + + try: + dp = user.DashouProfile + if dp.zhanghaozhuangtai == 1: + data['dashou'] = get_identity_tags_for_user(club_id, uid, SHENFEN_DASHOU) + except Exception: + pass + + try: + gp = user.GuanshiProfile + if gp.zhuangtai == 1: + data['guanshi'] = get_identity_tags_for_user(club_id, uid, SHENFEN_GUANSHI) + except Exception: + pass + + try: + zp = user.ZuzhangProfile + if zp.zhuangtai == 1: + data['zuzhang'] = get_identity_tags_for_user(club_id, uid, SHENFEN_ZUZHANG) + except Exception: + pass + + try: + sp = user.ShopProfile + if sp.zhuangtai == 1: + data['shangjia'] = get_identity_tags_for_user(club_id, uid, SHENFEN_SHANGJIA) + except Exception: + pass + + return data diff --git a/jituan/urls.py b/jituan/urls.py index a6a7e33..4345aa1 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -43,6 +43,14 @@ from jituan.views import ( ClubWechatLoginView, ) +from jituan.views_identity_tag import ( + IdentityTagBindListView, + IdentityTagBindView, + IdentityTagListView, + IdentityTagManageView, + IdentityTagMyTagsView, +) + urlpatterns = [ path('auth/wechat-login', ClubWechatLoginView.as_view(), name='jituan_wechat_login'), path('auth/kefu-login', ClubKefuLoginView.as_view(), name='jituan_kefu_login'), @@ -71,6 +79,11 @@ urlpatterns = [ path('houtai/lunbo-list', ClubLunboListView.as_view(), name='jituan_lunbo_list'), path('houtai/lunbo-manage', ClubLunboManageView.as_view(), name='jituan_lunbo_manage'), path('houtai/gonggao', ClubGonggaoView.as_view(), name='jituan_gonggao'), + path('identity-tag/my-tags', IdentityTagMyTagsView.as_view(), name='jituan_identity_my_tags'), + path('houtai/identity-tag/list', IdentityTagListView.as_view(), name='jituan_identity_tag_list'), + path('houtai/identity-tag/manage', IdentityTagManageView.as_view(), name='jituan_identity_tag_manage'), + path('houtai/identity-tag/bind-list', IdentityTagBindListView.as_view(), name='jituan_identity_tag_bind_list'), + path('houtai/identity-tag/bind', IdentityTagBindView.as_view(), name='jituan_identity_tag_bind'), path('houtai/tupian-list', ClubTupianListView.as_view(), name='jituan_tupian_list'), path('houtai/tupian-manage', ClubTupianManageView.as_view(), name='jituan_tupian_manage'), path('houtai/hthqtcxx', PopupNoticeListAPIView.as_view(), name='jituan_popup_list'), diff --git a/jituan/views_display.py b/jituan/views_display.py index 3e17985..72d4b73 100644 --- a/jituan/views_display.py +++ b/jituan/views_display.py @@ -133,6 +133,7 @@ class ClubGonggaoView(APIView): return Response({'code': 403, 'msg': '无权限管理公告'}) notice_type = int(request.data.get('notice_type', 1) or 1) + page_key = normalize_page_key(request.data.get('page_key'), image_type=1) club_id = resolve_effective_club_id(request, request.user) content = request.data.get('content') @@ -140,8 +141,9 @@ class ClubGonggaoView(APIView): return Response({ 'code': 0, 'data': { - 'content': get_gonggao_content(request, notice_type=notice_type), + 'content': get_gonggao_content(request, notice_type=notice_type, page_key=page_key), 'notice_type': notice_type, + 'page_key': page_key, **list_response_meta(request), }, }) @@ -152,12 +154,13 @@ class ClubGonggaoView(APIView): row, _ = Gonggao.query.get_or_create( club_id=club_id, - NoticeType=notice_type, - defaults={'Content': content}, + page_key=page_key, + defaults={'NoticeType': notice_type, 'Content': content}, ) row.Content = content - row.save(update_fields=['Content', 'UpdateTime']) - return Response({'code': 0, 'msg': '保存成功'}) + row.NoticeType = notice_type + row.save(update_fields=['Content', 'NoticeType', 'UpdateTime']) + return Response({'code': 0, 'msg': '保存成功', 'data': {'page_key': page_key}}) class ClubTupianListView(APIView): diff --git a/jituan/views_identity_tag.py b/jituan/views_identity_tag.py new file mode 100644 index 0000000..c99abb8 --- /dev/null +++ b/jituan/views_identity_tag.py @@ -0,0 +1,241 @@ +"""身份装饰标签:后台管理 + 小程序 my-tags。""" +import json + +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.models import IdentityTag, IdentityTagBind +from jituan.services.club_context import resolve_effective_club_id +from jituan.services.display_config import list_response_meta +from jituan.services.identity_tag import ( + SHENFEN_DASHOU, + SHENFEN_GUANSHI, + SHENFEN_SHANGJIA, + SHENFEN_ZUZHANG, + format_tag_item, + get_my_identity_tags, +) +from users.business_models import User + +IDENTITY_TAG_PERM = 'sfbq666' + +SHENFEN_LABELS = { + SHENFEN_DASHOU: '打手', + SHENFEN_GUANSHI: '管事', + SHENFEN_SHANGJIA: '商家', + SHENFEN_ZUZHANG: '组长', +} + + +def _build_texiao_json(color, flash_enabled): + return json.dumps({ + 'color': color or '#000000', + 'flash': bool(flash_enabled), + }, ensure_ascii=False) + + +class IdentityTagMyTagsView(APIView): + """小程序:当前用户各身份装饰标签 POST /jituan/identity-tag/my-tags""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + user = request.user + club_id = resolve_effective_club_id(request, user) + data = get_my_identity_tags(user, club_id) + return Response({'code': 200, 'msg': '成功', 'data': data}) + + +class IdentityTagListView(APIView): + """后台:标签定义列表 POST /jituan/houtai/identity-tag/list""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = (request.data.get('phone') or request.data.get('username') or '').strip() + if not phone: + return Response({'code': 401, 'msg': '缺少客服账号'}) + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return permissions + if IDENTITY_TAG_PERM not in permissions: + return Response({'code': 403, 'msg': '无权限管理身份装饰标签'}) + + club_id = resolve_effective_club_id(request, request.user) + rows = IdentityTag.query.filter(club_id=club_id).order_by('sort_order', 'id') + return Response({ + 'code': 0, + 'data': { + 'list': [ + { + 'id': r.id, + 'name': r.name, + 'color': r.color, + 'flash_enabled': r.flash_enabled, + 'sort_order': r.sort_order, + 'status': r.status, + } + for r in rows + ], + **list_response_meta(request), + }, + }) + + +class IdentityTagManageView(APIView): + """后台:新增/编辑/删除标签 POST /jituan/houtai/identity-tag/manage""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = (request.data.get('phone') or request.data.get('username') or '').strip() + if not phone: + return Response({'code': 401, 'msg': '缺少客服账号'}) + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return permissions + if IDENTITY_TAG_PERM not in permissions: + return Response({'code': 403, 'msg': '无权限管理身份装饰标签'}) + + club_id = resolve_effective_club_id(request, request.user) + action = (request.data.get('action') or '').strip() + + if action == 'delete': + tag_id = request.data.get('id') + if not tag_id: + return Response({'code': 400, 'msg': '缺少标签ID'}) + IdentityTag.query.filter(id=tag_id, club_id=club_id).delete() + return Response({'code': 0, 'msg': '删除成功'}) + + name = (request.data.get('name') or '').strip() + if not name: + return Response({'code': 400, 'msg': '标签名不能为空'}) + color = (request.data.get('color') or '#000000').strip() + flash_enabled = bool(request.data.get('flash_enabled', False)) + sort_order = int(request.data.get('sort_order', 0) or 0) + status = int(request.data.get('status', 1) or 1) + texiao_json = _build_texiao_json(color, flash_enabled) + + tag_id = request.data.get('id') + if tag_id: + row = IdentityTag.query.filter(id=tag_id, club_id=club_id).first() + if not row: + return Response({'code': 404, 'msg': '标签不存在'}) + row.name = name + row.color = color + row.flash_enabled = flash_enabled + row.sort_order = sort_order + row.status = status + row.texiao_json = texiao_json + row.save() + return Response({'code': 0, 'msg': '更新成功', 'data': {'id': row.id}}) + + row = IdentityTag.query.create( + club_id=club_id, + name=name, + color=color, + flash_enabled=flash_enabled, + sort_order=sort_order, + status=status, + texiao_json=texiao_json, + ) + return Response({'code': 0, 'msg': '创建成功', 'data': {'id': row.id}}) + + +class IdentityTagBindListView(APIView): + """后台:绑定列表 POST /jituan/houtai/identity-tag/bind-list""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = (request.data.get('phone') or request.data.get('username') or '').strip() + if not phone: + return Response({'code': 401, 'msg': '缺少客服账号'}) + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return permissions + if IDENTITY_TAG_PERM not in permissions: + return Response({'code': 403, 'msg': '无权限管理身份装饰标签'}) + + club_id = resolve_effective_club_id(request, request.user) + keyword = (request.data.get('keyword') or '').strip() + shenfen = request.data.get('shenfen') + page = max(1, int(request.data.get('page', 1) or 1)) + page_size = min(50, max(1, int(request.data.get('page_size', 20) or 20))) + + qs = IdentityTagBind.query.filter(club_id=club_id).select_related('tag') + if shenfen is not None and str(shenfen) != '': + qs = qs.filter(shenfen=int(shenfen)) + if keyword: + qs = qs.filter(yonghuid__icontains=keyword) + + total = qs.count() + rows = qs.order_by('-id')[(page - 1) * page_size:page * page_size] + items = [] + for b in rows: + items.append({ + 'id': b.id, + 'yonghuid': b.yonghuid, + 'shenfen': b.shenfen, + 'shenfen_label': SHENFEN_LABELS.get(b.shenfen, str(b.shenfen)), + 'tag_id': b.tag_id, + 'tag_name': b.tag.name if b.tag else '', + }) + return Response({ + 'code': 0, + 'data': { + 'list': items, + 'total': total, + 'page': page, + 'page_size': page_size, + **list_response_meta(request), + }, + }) + + +class IdentityTagBindView(APIView): + """后台:绑定/解绑 POST /jituan/houtai/identity-tag/bind""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + phone = (request.data.get('phone') or request.data.get('username') or '').strip() + if not phone: + return Response({'code': 401, 'msg': '缺少客服账号'}) + kefu, permissions = verify_kefu_permission(request, phone) + if kefu is None: + return permissions + if IDENTITY_TAG_PERM not in permissions: + return Response({'code': 403, 'msg': '无权限管理身份装饰标签'}) + + club_id = resolve_effective_club_id(request, request.user) + action = (request.data.get('action') or 'bind').strip() + + if action == 'unbind': + bind_id = request.data.get('id') + if not bind_id: + return Response({'code': 400, 'msg': '缺少绑定ID'}) + IdentityTagBind.query.filter(id=bind_id, club_id=club_id).delete() + return Response({'code': 0, 'msg': '解绑成功'}) + + yonghuid = (request.data.get('yonghuid') or '').strip() + tag_id = request.data.get('tag_id') + shenfen = request.data.get('shenfen') + if not yonghuid or not tag_id or shenfen is None: + return Response({'code': 400, 'msg': '参数不完整'}) + if not User.query.filter(UserUID=yonghuid).exists(): + return Response({'code': 404, 'msg': '用户不存在'}) + tag = IdentityTag.query.filter(id=tag_id, club_id=club_id, status=1).first() + if not tag: + return Response({'code': 404, 'msg': '标签不存在'}) + + IdentityTagBind.query.get_or_create( + club_id=club_id, + yonghuid=yonghuid, + shenfen=int(shenfen), + tag=tag, + ) + return Response({'code': 0, 'msg': '绑定成功'}) diff --git a/orders/migrations/0008_order_waibu_penalty_staff.py b/orders/migrations/0008_order_waibu_penalty_staff.py new file mode 100644 index 0000000..3ca6f78 --- /dev/null +++ b/orders/migrations/0008_order_waibu_penalty_staff.py @@ -0,0 +1,27 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0007_penaltyrecord_clubid'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='ExternalOrderID', + field=models.CharField( + blank=True, db_column='waibu_dingdan_id', db_index=True, + max_length=64, null=True, verbose_name='外部平台订单号', + ), + ), + migrations.AddField( + model_name='penalty', + name='ShopStaffUserID', + field=models.CharField( + blank=True, db_column='dianpu_kefu_id', max_length=20, + null=True, verbose_name='店铺客服用户ID', + ), + ), + ] diff --git a/orders/models.py b/orders/models.py index 507851d..2da4e66 100644 --- a/orders/models.py +++ b/orders/models.py @@ -166,6 +166,10 @@ class Order(QModel): max_length=64, blank=True, null=True, db_index=True, db_column='wechat_transaction_id', verbose_name='微信支付订单号', ) + ExternalOrderID = models.CharField( + max_length=64, blank=True, null=True, db_index=True, + db_column='waibu_dingdan_id', verbose_name='外部平台订单号', + ) class Meta: db_table = 'dingdan' @@ -510,6 +514,10 @@ class Penalty(QModel): max_length=16, default='xq', db_index=True, db_column='club_id', verbose_name='所属俱乐部', ) + ShopStaffUserID = models.CharField( + max_length=20, blank=True, null=True, + db_column='dianpu_kefu_id', verbose_name='店铺客服用户ID', + ) class Meta: db_table = 'fadan' diff --git a/orders/views.py b/orders/views.py index 589adbf..0d191da 100644 --- a/orders/views.py +++ b/orders/views.py @@ -1831,6 +1831,7 @@ class ShangjiaPaifaView(APIView): label_id = data.get('labelId') # 整数或 None commission_enabled = data.get('commissionEnabled', False) commission_value_raw = data.get('commissionValue') + waibu_dingdan_id_raw = data.get('waibuDingdanId', '') # ----- 2. 字符串字段安全处理 ----- dingdan_jieshao = str(dingdan_jieshao_raw).strip() @@ -1838,6 +1839,7 @@ class ShangjiaPaifaView(APIView): laoban_name = str(laoban_name_raw).strip() zhiding_uid = str(zhiding_uid_raw).strip() commission_value_str = str(commission_value_raw).strip() if commission_value_raw is not None else '' + waibu_dingdan_id = str(waibu_dingdan_id_raw).strip()[:64] if waibu_dingdan_id_raw else '' # ----- 3. 必填校验 ----- if not all([shangpin_type_id, dingdan_jieshao, laoban_name, jiage_str]): @@ -1961,6 +1963,7 @@ class ShangjiaPaifaView(APIView): ImageURL=request.user.Avatar, User1ID=f"Sj{request.user.UserUID}", ClubID=club_id, + ExternalOrderID=waibu_dingdan_id or None, ) logger.info(f"订单主表创建: {dingdan.OrderID}") @@ -2057,8 +2060,9 @@ class ShangjiaDingdanHuoquView(APIView): # 🆕 商品类型筛选(兼容 leixing_id / ProductTypeID) leixing_id = pick_leixing_id(data) - # 🆕 模糊搜索关键字(支持订单ID、订单介绍、游戏昵称、打手ID) + # 🆕 模糊搜索关键字(支持订单ID、订单介绍、游戏昵称、打手ID、拼多多订单号) keyword = data.get('keyword', '').strip() + waibu_dingdan_id = str(data.get('waibuDingdanId') or '').strip() except (ValueError, TypeError): return Response({ 'code': 2, @@ -2087,8 +2091,10 @@ class ShangjiaDingdanHuoquView(APIView): if leixing_id is not None: base_qs = base_qs.filter(Order__ProductTypeID=leixing_id) - # 7. 模糊搜索(支持订单ID、订单介绍、游戏昵称、打手ID) - if keyword: + # 7. 模糊搜索(支持订单ID、订单介绍、游戏昵称、打手ID、拼多多订单号) + if waibu_dingdan_id: + base_qs = base_qs.filter(Order__ExternalOrderID__exact=waibu_dingdan_id) + elif keyword: # 订单ID精确匹配(dingdan_id有唯一索引,先用精确匹配) dingdan_id_match = Q(Order__OrderID__exact=keyword) # 订单介绍模糊匹配 @@ -2097,9 +2103,11 @@ class ShangjiaDingdanHuoquView(APIView): nicheng_match = Q(Order__Nickname__icontains=keyword) # 打手ID精确匹配 dashou_match = Q(Order__PlayerID__exact=keyword) + # 拼多多订单号精确匹配 + waibu_match = Q(Order__ExternalOrderID__exact=keyword) base_qs = base_qs.filter( - dingdan_id_match | jieshao_match | nicheng_match | dashou_match + dingdan_id_match | jieshao_match | nicheng_match | dashou_match | waibu_match ) # 8. 计算待结算订单数量(状态为8) @@ -2317,6 +2325,9 @@ class ShangjiaDingdanXiangqingView(APIView): # 新增:更换打手后的新订单ID 'xin_dingdan_id': xin_dingdan_id, + + # 拼多多订单号(商家/客服可见) + 'waibu_dingdan_id': order.ExternalOrderID or '', } try: @@ -3148,12 +3159,43 @@ class DashouDingdanHuoquView(APIView): }) shangjia_tags_map[oid] = sj_tags + # --- 身份装饰标签 + 优质商家 --- + from jituan.services.identity_tag import ( + SHENFEN_DASHOU, + SHENFEN_SHANGJIA, + batch_identity_tags, + ) + from users.models import UserShangjia + + identity_pairs = [] + youzhi_map = {} + merchant_uids = set(shangjia_id_map.values()) + if merchant_uids: + for sj in UserShangjia.query.filter( + user__UserUID__in=merchant_uids + ).select_related('user').values('user__UserUID', 'is_youzhi_shangjia'): + youzhi_map[sj['user__UserUID']] = bool(sj['is_youzhi_shangjia']) + + for item in dingdan_list: + oid = item['OrderID'] + zhid = item.get('AssignedID') + if zhid: + identity_pairs.append((zhid, SHENFEN_DASHOU)) + if item['Platform'] == 2 and oid in shangjia_id_map: + sj_uid = shangjia_id_map[oid] + if sj_uid: + identity_pairs.append((sj_uid, SHENFEN_SHANGJIA)) + + club_id = resolve_club_id_from_request(request) + identity_tag_map = batch_identity_tags(club_id, identity_pairs) if identity_pairs else {} + # 12. 格式化返回数据 formatted_list = [] for order in dingdan_list: oid = order['OrderID'] zid = order.get('AssignedID') info = zhiding_info.get(zid, {}) + sj_uid = shangjia_id_map.get(oid) if order['Platform'] == 2 else None formatted_list.append({ 'dingdan_id': order['OrderID'], 'zhuangtai': order['Status'], @@ -3176,6 +3218,10 @@ class DashouDingdanHuoquView(APIView): 'xuqiu_biaoqian': xuqiu_tags_map.get(oid, []), 'dashou_biaoqian': dashou_tags_map.get(oid, []), 'shangjia_biaoqian': shangjia_tags_map.get(oid, []), + # 身份装饰标签 + 优质商家 + '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, }) has_more = (page * page_size) < total @@ -6215,6 +6261,7 @@ class ZxsjghdsView(APIView): jieshao = old_order.Description nicheng = old_order.Nickname beizhu = old_order.Remark + waibu_dingdan_id = old_order.ExternalOrderID user1_id = old_order.User1ID tupian = old_order.ImageURL fadan_pingtai = old_order.Platform @@ -6303,6 +6350,7 @@ class ZxsjghdsView(APIView): Remark=beizhu, User1ID=user1_id, # 商家标识 ImageURL=tupian, + ExternalOrderID=waibu_dingdan_id, # 不继承:zhiding_id, jiedan_dashou_id, dashou_liuyan 等 ) @@ -6418,6 +6466,11 @@ class ShangjiaFakuanApplyView(APIView): return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST) # ========== 6. 本地创建罚单 ========== + staff_kefu_id = None + staff_member = getattr(request, '_staff_member', None) + if staff_member is not None: + staff_kefu_id = getattr(staff_member, 'staff_user_id', None) or None + with transaction.atomic(): Penalty.query.create( PenalizedUserID=dashou_id, @@ -6430,6 +6483,7 @@ class ShangjiaFakuanApplyView(APIView): AffectsGrabbing=yingxiang_qiangdan, ApplicantIdentity=5, # 申请人是商家 ClubID=resolve_penalty_club_id(order=order, penalized_user_id=dashou_id), + ShopStaffUserID=staff_kefu_id, ) logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}罚款成功,金额{fakuanjine}") return Response({'code': 0, 'msg': '罚款申请已提交'}) diff --git a/users/migrations/0004_user_shangjia_youzhi.py b/users/migrations/0004_user_shangjia_youzhi.py new file mode 100644 index 0000000..7cb5b37 --- /dev/null +++ b/users/migrations/0004_user_shangjia_youzhi.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0003_tixian_club_id'), + ] + + operations = [ + migrations.AddField( + model_name='usershangjia', + name='is_youzhi_shangjia', + field=models.BooleanField(default=False, verbose_name='优质商家'), + ), + ] diff --git a/users/models.py b/users/models.py index 5d92131..82088dd 100644 --- a/users/models.py +++ b/users/models.py @@ -167,6 +167,7 @@ class UserShangjia(QModel): jinriliushui = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='商家今日流水') jinyuedingdan = models.IntegerField(default=0, verbose_name='商家今月订单总量') jinyueliushui = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='商家今月流水') + is_youzhi_shangjia = models.BooleanField(default=False, verbose_name='优质商家') CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: