From cddd231dc2216d9d41bb4d33a2b55526db8f7c7b Mon Sep 17 00:00:00 2001 From: XingQue Date: Thu, 23 Jul 2026 21:29:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E5=8F=B0=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=95=86=E5=AE=B6=20+=20=E9=BE=99=E5=85=88=E7=94=9F=20lsx=20?= =?UTF-8?q?=E4=BF=B1=E4=B9=90=E9=83=A8/=E9=82=80=E8=AF=B7=E9=93=BE?= =?UTF-8?q?=E7=A7=8D=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持客服按用户UID开通商家(分俱乐部校验);新增 seed_club_lsx、seed_lsx_invite_data。 Co-authored-by: Cursor --- backend/urls.py | 3 +- backend/view.py | 2 + backend/views/kefu.py | 106 ++++++++++++++ jituan/management/commands/seed_club_lsx.py | 66 +++++++++ .../commands/seed_lsx_invite_data.py | 135 ++++++++++++++++++ jituan/urls.py | 2 + 6 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 jituan/management/commands/seed_club_lsx.py create mode 100644 jituan/management/commands/seed_lsx_invite_data.py diff --git a/backend/urls.py b/backend/urls.py index 385f1b0..88fe5c7 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -4,7 +4,7 @@ from django.views.decorators.csrf import csrf_exempt from .view import (GetRolePermissionView, ModifyRolePermissionView, AddRoleView, GetAdminRolesView, GetAdminUserListView, AddAdminUserView, ModifyAdminUserView, KefuGetDashouListView, KefuGetDashouDetailView, KefuUpdateDashouView, KefuGetShangjiaListView, - KefuGetShangjiaDetailView, KefuUpdateShangjiaView, GetProductBaseDataView, GetProductListView, SaveProductOrderView, + KefuGetShangjiaDetailView, KefuUpdateShangjiaView, KefuAddShangjiaView, GetProductBaseDataView, GetProductListView, SaveProductOrderView, AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView, UpdateZhiKanShenheView, UpdateMemberView, GetMemberListView, AddMemberView, GetGuanliListView, GetGuanliDetailView, PromoteGuanshiToZuzhangView, ChangeInviterView, UpdateGuanliView, GetWithdrawSettingsView, UpdateWithdrawSettingsView, GetZuzhangListView, GetZuzhangDetailView, UpdateZuzhangView, GetOperationLogListView, GetProductTypeZoneView, @@ -36,6 +36,7 @@ urlpatterns = [ path('hqsjgllb', KefuGetShangjiaListView.as_view(), name='后台获取商家列表'), path('hthqsjxq', KefuGetShangjiaDetailView.as_view(), name='后台获取商家详情'), path('xgsjxx', KefuUpdateShangjiaView.as_view(), name='后台修改商家数据'), + path('tjsj', KefuAddShangjiaView.as_view(), name='后台添加商家'), path('htsphqjcsx', GetProductBaseDataView.as_view(), name='后台获取商品类型专区'), path('hthqsplb', GetProductListView.as_view(), name='后台获取商品数据'), diff --git a/backend/view.py b/backend/view.py index d0c0803..0ed2f09 100644 --- a/backend/view.py +++ b/backend/view.py @@ -35,6 +35,7 @@ from .views.roles import ( from .views.kefu import ( KefuGetDashouListView, KefuGetDashouDetailView, KefuUpdateDashouView, KefuGetShangjiaListView, KefuGetShangjiaDetailView, KefuUpdateShangjiaView, + KefuAddShangjiaView, KefuChatPermissionsView, DashouGiftHuiyuanSaveView, DashouGiftHuiyuanRemoveView, ) @@ -100,6 +101,7 @@ __all__ = [ 'KefuGetDashouListView', 'KefuGetDashouDetailView', 'KefuUpdateDashouView', 'DashouGiftHuiyuanSaveView', 'DashouGiftHuiyuanRemoveView', 'KefuGetShangjiaListView', 'KefuGetShangjiaDetailView', 'KefuUpdateShangjiaView', + 'KefuAddShangjiaView', 'KefuChatPermissionsView', # products 'GetProductBaseDataView', 'GetProductListView', 'SaveProductOrderView', diff --git a/backend/views/kefu.py b/backend/views/kefu.py index 8abfde9..e758ca9 100644 --- a/backend/views/kefu.py +++ b/backend/views/kefu.py @@ -1110,9 +1110,115 @@ class KefuUpdateShangjiaView(APIView): return Response({'code': 400, 'msg': '未知操作'}) +class KefuAddShangjiaView(APIView): + """ + 后台按用户ID开通商家(分俱乐部,禁止前端自助注册场景使用) + POST /houtai/tjsj 或 /jituan/houtai/tjsj + 参数:username, yonghuid(用户业务ID), nicheng(可选) + 权限:1100a + """ + permission_classes = [] + parser_classes = [JSONParser] + def post(self, request): + username = request.data.get('username', '').strip() + yonghuid = str(request.data.get('yonghuid', '')).strip() + nicheng_in = (request.data.get('nicheng') or '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少username'}) + if not yonghuid: + return Response({'code': 400, 'msg': '请填写用户ID'}) + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '1100a' not in permissions: + return Response({'code': 403, 'msg': '无权限添加商家(需要1100a)'}) + + try: + user = User.query.get(UserUID=yonghuid) + except User.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在,请确认用户ID'}) + + denied = forbid_if_user_out_of_scope(request, user) + if denied: + return denied + + if not getattr(user, 'IsActive', True): + return Response({'code': 400, 'msg': '该账号已被封禁,无法开通商家'}) + + try: + dashou = user.DashouProfile + if int(getattr(dashou, 'zhanghaozhuangtai', 1) or 0) != 1: + return Response({'code': 400, 'msg': '该账号已被封禁,无法开通商家'}) + except UserDashou.DoesNotExist: + pass + except Exception: + pass + + if UserShangjia.query.filter(user=user).exists(): + return Response({'code': 400, 'msg': '该用户已是商家,无需重复添加'}) + + from merchant_ops.services.authz import assert_no_active_staff_when_merchant_register, StaffAuthError + try: + assert_no_active_staff_when_merchant_register(user) + except StaffAuthError as e: + return Response({'code': 403, 'msg': getattr(e, 'msg', None) or str(e)}) + except Exception: + pass + + boss_nickname = '普通商家' + try: + boss = user.BossProfile + if boss.nickname and boss.nickname.strip(): + boss_nickname = boss.nickname.strip() + except UserBoss.DoesNotExist: + pass + + shangjia_nicheng = nicheng_in or boss_nickname + if UserShangjia.query.filter(nicheng=shangjia_nicheng).exists(): + shangjia_nicheng = f"{shangjia_nicheng}{random.randint(1000, 9999)}" + + try: + with transaction.atomic(): + shop = UserShangjia.query.create( + user=user, + nicheng=shangjia_nicheng, + zhuangtai=1, + yue=0.00, + fabu=0, + tuikuan=0, + chengjiao=0, + jinridingdan=0, + jinriliushui=0.00, + jinyuedingdan=0, + jinyueliushui=0.00, + dianhua='', + wechat='', + ) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_SHANGJIA, + qitashuoming=( + f'【后台】添加商家:用户ID={yonghuid},昵称={shop.nicheng},' + f'操作人={kefu.user.Phone}' + ), + ) + except Exception as e: + logger.exception('后台添加商家失败') + return Response({'code': 500, 'msg': f'添加失败:{e}'}) + + return Response({ + 'code': 0, + 'msg': '商家添加成功', + 'data': { + 'yonghuid': yonghuid, + 'nicheng': shop.nicheng, + 'zhuangtai': shop.zhuangtai, + }, + }) class KefuChatPermissionsView(APIView): diff --git a/jituan/management/commands/seed_club_lsx.py b/jituan/management/commands/seed_club_lsx.py new file mode 100644 index 0000000..c562865 --- /dev/null +++ b/jituan/management/commands/seed_club_lsx.py @@ -0,0 +1,66 @@ +"""一键初始化龙先生电竞(lsx)俱乐部(默认不批量给所有客服加任职)。""" +from django.core.management import call_command +from django.core.management.base import BaseCommand + +from jituan.models import Club + + +class Command(BaseCommand): + help = '创建龙先生电竞俱乐部(lsx);任职请单独 seed_admin_assignments --phone ...' + + def add_arguments(self, parser): + parser.add_argument( + '--wx-appid', default='wx7ff90e9d024fcdb8', help='龙先生电竞小程序 appid', + ) + parser.add_argument( + '--assignments', + action='store_true', + help='同时给 --phone 指定客服加 lsx 任职(需配合 --phone)', + ) + parser.add_argument( + '--phone', type=str, default='', help='与 --assignments 联用:客服手机号', + ) + + def handle(self, *args, **options): + wx_appid = (options.get('wx_appid') or '').strip() + club_id = 'lsx' + name = '龙先生电竞' + + if Club.query.filter(club_id=club_id).exists(): + self.stdout.write(self.style.WARNING(f'俱乐部 {club_id} 已存在,跳过 create_club')) + club = Club.query.filter(club_id=club_id).first() + if club and wx_appid and (club.wx_appid or '') != wx_appid: + club.wx_appid = wx_appid + club.save(update_fields=['wx_appid']) + self.stdout.write(f'已更新 {club_id}.wx_appid = {wx_appid}') + else: + self.stdout.write(f'创建俱乐部 {club_id} ...') + call_command( + 'create_club', + club_id, + name=name, + wx_appid=wx_appid, + ) + + if options.get('assignments'): + phone = (options.get('phone') or '').strip() + if not phone: + self.stdout.write(self.style.WARNING( + '未指定 --phone,跳过任职。示例:' + 'python manage.py seed_club_lsx --assignments --phone 你的手机号' + )) + else: + call_command( + 'seed_admin_assignments', + club_id=club_id, + phone=phone, + ) + else: + self.stdout.write( + '未加任职。需要时在后台「数据范围」配置,或:' + 'python manage.py seed_admin_assignments --phone 手机号 --club-id lsx' + ) + + self.stdout.write(self.style.SUCCESS( + f'龙先生电竞 {club_id} 就绪。请在后台「俱乐部配置」检查支付密钥与 wx_appid。' + )) diff --git a/jituan/management/commands/seed_lsx_invite_data.py b/jituan/management/commands/seed_lsx_invite_data.py new file mode 100644 index 0000000..2ec1a1d --- /dev/null +++ b/jituan/management/commands/seed_lsx_invite_data.py @@ -0,0 +1,135 @@ +"""龙先生电竞(lsx)邀请链种子:自动插入组长+管事(含邀请码),无需先登录小程序。""" +from django.core.management import call_command +from django.core.management.base import BaseCommand +from django.db import transaction + +from jituan.models import Club, ClubHuiyuanPrice +from users.business_models import User +from users.models import UserBoss, UserGuanshi, UserZuzhang +from utils.invitationcode_utils import CreateInvitationCode + +CLUB_ID = 'lsx' +WX_APPID = 'wx7ff90e9d024fcdb8' + +# 固定种子账号(可重复执行,幂等;UID/手机避开 xzj 的 9900001/2) +SEED_ZUZHANG = { + 'UserUID': '9910001', + 'OpenID': 'lsx_seed_openid_zuzhang_v1', + 'Phone': '19910000001', + 'UserName': 'lsx_seed_zuzhang', + 'nickname': '龙先生种子组长', +} +SEED_GUANSHI = { + 'UserUID': '9910002', + 'OpenID': 'lsx_seed_openid_guanshi_v1', + 'Phone': '19910000002', + 'UserName': 'lsx_seed_guanshi', + 'nickname': '龙先生种子管事', +} + + +def _ensure_club(): + if Club.query.filter(club_id=CLUB_ID).exists(): + return + call_command( + 'create_club', + CLUB_ID, + name='龙先生电竞', + wx_appid=WX_APPID, + ) + + +def _ensure_user(meta): + user = User.query.filter(UserUID=meta['UserUID']).first() + if not user: + user = User.query.filter(OpenID=meta['OpenID']).first() + if user: + if user.ClubID != CLUB_ID: + user.ClubID = CLUB_ID + user.save(update_fields=['ClubID']) + return user, False + + user = User.query.create( + UserUID=meta['UserUID'], + UserName=meta['UserName'], + OpenID=meta['OpenID'], + Phone=meta['Phone'], + ClubID=CLUB_ID, + ) + UserBoss.query.create(user=user, nickname=meta['nickname']) + return user, True + + +def _ensure_zuzhang(user): + if hasattr(user, 'ZuzhangProfile'): + return user.ZuzhangProfile, False + yaoqingma = CreateInvitationCode(str(user.UserUID)) + z = UserZuzhang.query.create( + user=user, + yaoqingma=yaoqingma, + zhuangtai=1, + ) + return z, True + + +def _ensure_guanshi(user, zuzhang_uid): + if hasattr(user, 'GuanshiProfile'): + return user.GuanshiProfile, False + yaoqingma = CreateInvitationCode(str(user.UserUID)) + g = UserGuanshi.query.create( + user=user, + yaoqingma=yaoqingma, + yaoqingren=zuzhang_uid, + zhuangtai=1, + ) + return g, True + + +class Command(BaseCommand): + help = '插入龙先生电竞种子组长+管事(含邀请码),一条命令即可邀请注册' + + def add_arguments(self, parser): + parser.add_argument( + '--force-new-codes', + action='store_true', + help='已存在时重新生成邀请码(慎用)', + ) + + def handle(self, *args, **options): + with transaction.atomic(): + _ensure_club() + + zz_user, zz_new = _ensure_user(SEED_ZUZHANG) + zz_profile, zz_profile_new = _ensure_zuzhang(zz_user) + if options.get('force_new_codes') and not zz_profile_new: + zz_profile.yaoqingma = CreateInvitationCode(str(zz_user.UserUID)) + zz_profile.save(update_fields=['yaoqingma']) + + gs_user, gs_new = _ensure_user(SEED_GUANSHI) + gs_profile, gs_profile_new = _ensure_guanshi(gs_user, zz_user.UserUID) + if options.get('force_new_codes') and not gs_profile_new: + gs_profile.yaoqingma = CreateInvitationCode(str(gs_user.UserUID)) + gs_profile.save(update_fields=['yaoqingma']) + + price_cnt = ClubHuiyuanPrice.query.filter(club_id=CLUB_ID).count() + + self.stdout.write(self.style.SUCCESS('=' * 60)) + self.stdout.write(self.style.SUCCESS('龙先生电竞 lsx 邀请链种子数据已就绪')) + self.stdout.write(self.style.SUCCESS('=' * 60)) + self.stdout.write(f'club_huiyuan_price 条数: {price_cnt}') + self.stdout.write('') + self.stdout.write('【组长】管事注册用这个邀请码(guanshizhuce):') + self.stdout.write(f' UserUID={zz_user.UserUID} Phone={SEED_ZUZHANG["Phone"]}') + self.stdout.write(f' 组长邀请码 inviteCode = {zz_profile.yaoqingma}') + self.stdout.write('') + self.stdout.write('【管事】打手/接单员注册用这个邀请码(dashouzhuce):') + self.stdout.write(f' UserUID={gs_user.UserUID} Phone={SEED_GUANSHI["Phone"]}') + self.stdout.write(f' 管事邀请码 inviteCode = {gs_profile.yaoqingma}') + self.stdout.write('') + self.stdout.write('商家:请在后台「商家管理 → 添加商家」按用户UID开通(不再用前端邀请码)') + self.stdout.write('') + self.stdout.write('新建: 组长=%s 管事=%s' % ( + '是' if zz_new or zz_profile_new else '否(已存在)', + '是' if gs_new or gs_profile_new else '否(已存在)', + )) + self.stdout.write(self.style.SUCCESS('=' * 60)) diff --git a/jituan/urls.py b/jituan/urls.py index 2fe8c44..0b0902c 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -18,6 +18,7 @@ from backend.view import ( KhgglView, KefuGetDashouListView, KefuGetShangjiaListView, + KefuAddShangjiaView, PopupNoticeListAPIView, PopupNoticeModifyAPIView, ShgxgsjView, @@ -123,6 +124,7 @@ urlpatterns = [ path('houtai/kefuhqdslb', KefuGetDashouListView.as_view(), name='jituan_dashou_list'), path('houtai/hthqgslb', GetGuanliListView.as_view(), name='jituan_guanshi_list'), path('houtai/hqsjgllb', KefuGetShangjiaListView.as_view(), name='jituan_shangjia_list'), + path('houtai/tjsj', KefuAddShangjiaView.as_view(), name='jituan_shangjia_add'), path('houtai/hthqzzlb', GetZuzhangListView.as_view(), name='jituan_zuzhang_list'), path('houtai/lunbo-list', ClubLunboListView.as_view(), name='jituan_lunbo_list'), path('houtai/lunbo-manage', ClubLunboManageView.as_view(), name='jituan_lunbo_manage'),