feat: 俱乐部杂项图片/关注快手;角色客服按club隔离
This commit is contained in:
@@ -151,17 +151,15 @@ class GetRolePermissionView(APIView):
|
||||
all_perms = Permission.objects.filter(PermStatus=1).values('PermCode', 'PermName', 'PermDesc')
|
||||
permissions_list = [_serialize_permission_row(p) for p in all_perms]
|
||||
|
||||
# 4. 获取所有角色,并统计每个角色下的用户数(仅后台客服账号)
|
||||
from jituan.services.club_rbac import count_role_users_in_scope, filter_roles_for_request
|
||||
|
||||
kefu_user_uuids = list(
|
||||
User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True)
|
||||
)
|
||||
roles = Role.objects.filter(RoleStatus=1)
|
||||
roles = filter_roles_for_request(request)
|
||||
roles_data = []
|
||||
for role in roles:
|
||||
user_count = UserRole.objects.filter(
|
||||
RoleUUID=role.RoleUUID,
|
||||
UserUUID__in=kefu_user_uuids,
|
||||
).count()
|
||||
user_count = count_role_users_in_scope(role, request, kefu_user_uuids)
|
||||
role_perm_uuids = RolePermission.objects.filter(
|
||||
RoleUUID=role.RoleUUID
|
||||
).values_list('PermUUID', flat=True)
|
||||
@@ -175,6 +173,7 @@ class GetRolePermissionView(APIView):
|
||||
'role_code': role.RoleName,
|
||||
'role_name': role.RoleName,
|
||||
'description': role.RoleDesc or '',
|
||||
'club_id': getattr(role, 'ClubID', '') or '',
|
||||
'user_count': user_count,
|
||||
'permissions': perms_of_role
|
||||
})
|
||||
@@ -339,9 +338,13 @@ class AddRoleView(APIView):
|
||||
if '000001' not in permissions:
|
||||
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||||
|
||||
# 检查角色名称是否已存在
|
||||
if Role.objects.filter(RoleName=role_name).exists():
|
||||
return Response({'code': 400, 'msg': '角色名称已存在'})
|
||||
# 检查角色名称是否已存在(同俱乐部内唯一)
|
||||
from jituan.services.club_rbac import resolve_role_club_id, role_name_exists
|
||||
target_club_id = resolve_role_club_id(
|
||||
request, (request.data.get('target_club_id') or '').strip() or None,
|
||||
)
|
||||
if role_name_exists(role_name, target_club_id):
|
||||
return Response({'code': 400, 'msg': '该俱乐部下角色名称已存在'})
|
||||
|
||||
# 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字)
|
||||
role_code = f"ROLE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=8))}"
|
||||
@@ -356,6 +359,7 @@ class AddRoleView(APIView):
|
||||
RoleStatus=1,
|
||||
AssignScope=0,
|
||||
RoleDesc=description,
|
||||
ClubID=target_club_id,
|
||||
CreateTime=timezone.now(),
|
||||
)
|
||||
# 绑定权限(过滤掉不存在的权限编码)
|
||||
@@ -405,6 +409,9 @@ class GetAdminUserListView(APIView):
|
||||
KefuProfile__isnull=False
|
||||
).select_related('KefuProfile')
|
||||
|
||||
from jituan.services.club_rbac import filter_kefu_users_qs
|
||||
users = filter_kefu_users_qs(users, request)
|
||||
|
||||
if phone:
|
||||
users = users.filter(Phone__icontains=phone)
|
||||
if nicheng:
|
||||
@@ -442,6 +449,7 @@ class GetAdminUserListView(APIView):
|
||||
'phone': user.Phone or '',
|
||||
'yonghuid': user.UserUID or '',
|
||||
'nicheng': user.KefuProfile.nicheng,
|
||||
'club_id': getattr(user, 'ClubID', '') or '',
|
||||
'roles': roles_data,
|
||||
'status': user.KefuProfile.zhuangtai,
|
||||
'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
|
||||
@@ -481,11 +489,19 @@ class GetAdminRolesView(APIView):
|
||||
if '000001' not in permissions:
|
||||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||||
|
||||
roles = Role.objects.filter(RoleStatus=1).values('RoleName')
|
||||
from jituan.services.club_rbac import filter_roles_for_request
|
||||
roles = filter_roles_for_request(request).values('RoleName', 'ClubID')
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'roles': [{'role_code': r['RoleName'], 'role_name': r['RoleName']} for r in roles]
|
||||
'roles': [
|
||||
{
|
||||
'role_code': r['RoleName'],
|
||||
'role_name': r['RoleName'],
|
||||
'club_id': r.get('ClubID') or '',
|
||||
}
|
||||
for r in roles
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class Tupianpeizhi(QModel):
|
||||
db_table = 'tupianpeizhi'
|
||||
verbose_name = '配置图片'
|
||||
verbose_name_plural = '配置图片'
|
||||
unique_together = [['club_id', 'ImageType']]
|
||||
# ImageType 1/2 每俱乐部唯一;3(关注快手) 允许多张
|
||||
|
||||
|
||||
class Szjilu(QModel):
|
||||
|
||||
@@ -2700,12 +2700,10 @@ class GuanZhuAListView(APIView):
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
club_id = resolve_effective_club_id(request, request.user)
|
||||
qs = Tupianpeizhi.query.filter(ImageType=3, club_id=club_id)
|
||||
if not qs.exists() and club_id != 'xq':
|
||||
qs = Tupianpeizhi.query.filter(ImageType=3, club_id='xq')
|
||||
if not qs.exists():
|
||||
return Response({
|
||||
'code': 0,
|
||||
|
||||
@@ -480,6 +480,10 @@ class Role(QModel):
|
||||
AssignScope = models.SmallIntegerField(default=0, db_column='AssignScope', verbose_name='分配可见范围')
|
||||
AssignScopeUUID = UUIDField(null=True, blank=True, db_column='AssignScopeUUID', verbose_name='分配范围UUID')
|
||||
RoleDesc = models.CharField(max_length=512, default='', db_column='RoleDesc', verbose_name='角色描述')
|
||||
ClubID = models.CharField(
|
||||
max_length=16, default='', blank=True, db_column='club_id',
|
||||
db_index=True, verbose_name='所属俱乐部(空=集团全局角色)',
|
||||
)
|
||||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
|
||||
40
jituan/migrations/0003_role_club_id.py
Normal file
40
jituan/migrations/0003_role_club_id.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Generated manually — Role.club_id + 放宽 tupianpeizhi 唯一约束(关注快手可多图)
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('jituan', '0002_admin_audit_operator_widen'),
|
||||
('config', '0006_display_club_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
sql="""
|
||||
ALTER TABLE Role ADD COLUMN club_id VARCHAR(16) NOT NULL DEFAULT '';
|
||||
CREATE INDEX role_club_id_idx ON Role (club_id);
|
||||
UPDATE Role SET club_id = '' WHERE RoleName = '管理员';
|
||||
UPDATE Role SET club_id = 'xq' WHERE club_id = '' AND RoleName <> '管理员';
|
||||
""",
|
||||
reverse_sql="ALTER TABLE Role DROP COLUMN club_id;",
|
||||
),
|
||||
migrations.RunSQL(
|
||||
sql="""
|
||||
SET @idx_exists := (
|
||||
SELECT COUNT(1) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'tupianpeizhi'
|
||||
AND index_name = 'tupianpeizhi_club_id_ImageType_uniq'
|
||||
);
|
||||
SET @sql := IF(@idx_exists > 0,
|
||||
'ALTER TABLE tupianpeizhi DROP INDEX tupianpeizhi_club_id_ImageType_uniq',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
""",
|
||||
reverse_sql=migrations.RunSQL.noop,
|
||||
),
|
||||
]
|
||||
62
jituan/services/club_rbac.py
Normal file
62
jituan/services/club_rbac.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""角色/后台客服按俱乐部隔离(权限码全局共用,角色实体按 club 分开)。"""
|
||||
from django.db.models import Q
|
||||
|
||||
from gvsdsdk.models import Role, UserRole
|
||||
|
||||
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
|
||||
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
|
||||
|
||||
|
||||
def resolve_role_club_id(request, explicit=None):
|
||||
"""创建/筛选角色时使用的俱乐部;集团视图可显式指定 target_club_id。"""
|
||||
if explicit:
|
||||
return (explicit or '').strip()
|
||||
scope = resolve_club_scope(request)
|
||||
if scope == DATA_SCOPE_ALL:
|
||||
return ''
|
||||
return resolve_club_id_from_request(request)
|
||||
|
||||
|
||||
def filter_roles_for_request(request):
|
||||
"""
|
||||
子公司视图:仅本俱乐部角色。
|
||||
集团汇总视图:全部角色(含各俱乐部 + 全局 club_id 为空)。
|
||||
"""
|
||||
qs = Role.objects.filter(RoleStatus=1)
|
||||
scope = resolve_club_scope(request)
|
||||
if scope == DATA_SCOPE_ALL:
|
||||
return qs
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
return qs.filter(ClubID=club_id)
|
||||
|
||||
|
||||
def filter_kefu_users_qs(qs, request):
|
||||
"""后台客服账号列表按俱乐部过滤(User.ClubID)。"""
|
||||
scope = resolve_club_scope(request)
|
||||
if scope == DATA_SCOPE_ALL:
|
||||
return qs
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
if club_id == CLUB_ID_DEFAULT:
|
||||
return qs.filter(Q(ClubID=club_id) | Q(ClubID__isnull=True) | Q(ClubID=''))
|
||||
return qs.filter(ClubID=club_id)
|
||||
|
||||
|
||||
def count_role_users_in_scope(role, request, kefu_user_uuids):
|
||||
"""统计角色下客服数(仅计当前数据范围内的客服)。"""
|
||||
from users.business_models import User
|
||||
|
||||
uuids = list(
|
||||
UserRole.objects.filter(RoleUUID=role.RoleUUID)
|
||||
.values_list('UserUUID', flat=True)
|
||||
)
|
||||
if not uuids:
|
||||
return 0
|
||||
qs = User.objects.filter(UserUUID__in=uuids, KefuProfile__isnull=False)
|
||||
qs = filter_kefu_users_qs(qs, request)
|
||||
if kefu_user_uuids is not None:
|
||||
qs = qs.filter(UserUUID__in=kefu_user_uuids)
|
||||
return qs.count()
|
||||
|
||||
|
||||
def role_name_exists(role_name, club_id):
|
||||
return Role.objects.filter(RoleName=role_name, ClubID=club_id or '').exists()
|
||||
@@ -87,6 +87,35 @@ def get_tupianpeizhi_url(request, image_type):
|
||||
return (row.ImageURL or '').strip() if row else ''
|
||||
|
||||
|
||||
TUPIAN_TYPE_LABELS = {
|
||||
1: '打手规则图',
|
||||
2: '默认头像',
|
||||
3: '关注快手',
|
||||
}
|
||||
|
||||
|
||||
def list_tupianpeizhi_by_type(club_id, image_type):
|
||||
from config.models import Tupianpeizhi
|
||||
rows = Tupianpeizhi.query.filter(club_id=club_id, ImageType=image_type).order_by('id')
|
||||
return [
|
||||
{'id': r.id, 'image_url': r.ImageURL or '', 'access_count': r.AccessCount or 0}
|
||||
for r in rows if r.ImageURL
|
||||
]
|
||||
|
||||
|
||||
def upsert_tupianpeizhi_single(club_id, image_type, image_url):
|
||||
"""ImageType 1/2 每俱乐部仅一条。"""
|
||||
from config.models import Tupianpeizhi
|
||||
row = Tupianpeizhi.query.filter(club_id=club_id, ImageType=image_type).first()
|
||||
if row:
|
||||
row.ImageURL = image_url
|
||||
row.save(update_fields=['ImageURL', 'UpdateTime'])
|
||||
else:
|
||||
Tupianpeizhi.query.create(
|
||||
club_id=club_id, ImageType=image_type, ImageURL=image_url, AccessCount=0,
|
||||
)
|
||||
|
||||
|
||||
def copy_display_config(template_club_id, new_club_id):
|
||||
"""从模板俱乐部复制轮播/公告/弹窗/图片配置到新俱乐部。"""
|
||||
from config.models import Gonggao, Lunbo, PopupConfig, PopupImage, PopupPage, Tupianpeizhi
|
||||
|
||||
@@ -19,7 +19,10 @@ from backend.view import (
|
||||
UpdateWithdrawSettingsView,
|
||||
)
|
||||
from users.views import KefuPunishmentListView
|
||||
from jituan.views_display import ClubGonggaoView, ClubLunboListView, ClubLunboManageView
|
||||
from jituan.views_display import (
|
||||
ClubGonggaoView, ClubLunboListView, ClubLunboManageView,
|
||||
ClubTupianListView, ClubTupianManageView,
|
||||
)
|
||||
from jituan.views import (
|
||||
ClubAdminAssignmentListView,
|
||||
ClubAuditLogListView,
|
||||
@@ -66,6 +69,8 @@ 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('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'),
|
||||
path('houtai/htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='jituan_popup_modify'),
|
||||
path('houtai/htglyhqcfsltj', FaKuanTongJiView.as_view(), name='jituan_penalty_stats'),
|
||||
|
||||
@@ -9,17 +9,20 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from backend.utils import verify_kefu_permission
|
||||
from config.models import Gonggao, Lunbo
|
||||
from jituan.services.club_context import resolve_club_id_from_request
|
||||
from jituan.services.club_context import resolve_effective_club_id
|
||||
from jituan.services.display_config import (
|
||||
forbid_display_write_in_all_scope,
|
||||
get_gonggao_content,
|
||||
get_lunbo_urls,
|
||||
list_response_meta,
|
||||
list_tupianpeizhi_by_type,
|
||||
LUNBO_PAGE_DASHOU_CENTER,
|
||||
LUNBO_PAGE_OPTIONS,
|
||||
normalize_page_key,
|
||||
TUPIAN_TYPE_LABELS,
|
||||
upsert_tupianpeizhi_single,
|
||||
)
|
||||
from config.models import Gonggao, Lunbo, Tupianpeizhi
|
||||
from products.views import delete_from_oss, upload_to_oss, validate_image
|
||||
|
||||
POPUP_PERM = '8080a'
|
||||
@@ -74,7 +77,7 @@ class ClubLunboManageView(APIView):
|
||||
return deny
|
||||
|
||||
action = (request.data.get('action') or '').strip()
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
club_id = resolve_effective_club_id(request, request.user)
|
||||
page_key = normalize_page_key(request.data.get('page_key'), image_type=1)
|
||||
image_type = 2 if page_key == LUNBO_PAGE_DASHOU_CENTER else 1
|
||||
|
||||
@@ -130,7 +133,7 @@ class ClubGonggaoView(APIView):
|
||||
return Response({'code': 403, 'msg': '无权限管理公告'})
|
||||
|
||||
notice_type = int(request.data.get('notice_type', 1) or 1)
|
||||
club_id = resolve_club_id_from_request(request)
|
||||
club_id = resolve_effective_club_id(request, request.user)
|
||||
|
||||
content = request.data.get('content')
|
||||
if content is None:
|
||||
@@ -155,3 +158,105 @@ class ClubGonggaoView(APIView):
|
||||
row.Content = content
|
||||
row.save(update_fields=['Content', 'UpdateTime'])
|
||||
return Response({'code': 0, 'msg': '保存成功'})
|
||||
|
||||
|
||||
class ClubTupianListView(APIView):
|
||||
"""杂项图片配置列表 POST /jituan/houtai/tupian-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 POPUP_PERM not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限管理小程序图片'})
|
||||
|
||||
club_id = resolve_effective_club_id(request, request.user)
|
||||
data = {}
|
||||
for t, label in TUPIAN_TYPE_LABELS.items():
|
||||
data[str(t)] = {
|
||||
'label': label,
|
||||
'multi': t == 3,
|
||||
'list': list_tupianpeizhi_by_type(club_id, t),
|
||||
}
|
||||
return Response({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'types': data,
|
||||
**list_response_meta(request),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
class ClubTupianManageView(APIView):
|
||||
"""杂项图片上传/删除 POST /jituan/houtai/tupian-manage"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [MultiPartParser, FormParser, 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 POPUP_PERM not in permissions:
|
||||
return Response({'code': 403, 'msg': '无权限管理小程序图片'})
|
||||
|
||||
deny = forbid_display_write_in_all_scope(request)
|
||||
if deny:
|
||||
return deny
|
||||
|
||||
action = (request.data.get('action') or '').strip()
|
||||
image_type = int(request.data.get('image_type') or 0)
|
||||
if image_type not in TUPIAN_TYPE_LABELS:
|
||||
return Response({'code': 400, 'msg': 'image_type 无效'})
|
||||
|
||||
club_id = resolve_effective_club_id(request, request.user)
|
||||
|
||||
if action == 'delete':
|
||||
row_id = request.data.get('id')
|
||||
image_url = (request.data.get('image_url') or '').strip()
|
||||
qs = Tupianpeizhi.query.filter(club_id=club_id, ImageType=image_type)
|
||||
if row_id:
|
||||
row = qs.filter(id=row_id).first()
|
||||
elif image_url:
|
||||
row = qs.filter(ImageURL=image_url).first()
|
||||
else:
|
||||
return Response({'code': 400, 'msg': '缺少 id 或 image_url'})
|
||||
if not row:
|
||||
return Response({'code': 404, 'msg': '图片不存在'})
|
||||
if row.ImageURL:
|
||||
delete_from_oss(row.ImageURL)
|
||||
row.delete()
|
||||
return Response({'code': 0, 'msg': '删除成功'})
|
||||
|
||||
if action == 'add':
|
||||
file_obj = request.FILES.get('file')
|
||||
if not file_obj:
|
||||
return Response({'code': 400, 'msg': '请上传图片'})
|
||||
ok, msg = validate_image(file_obj)
|
||||
if not ok:
|
||||
return Response({'code': 400, 'msg': msg})
|
||||
ext = os.path.splitext(file_obj.name)[1].lower() or '.jpg'
|
||||
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
folder = 'guanzhu' if image_type == 3 else 'misc'
|
||||
oss_file_path = f"beijing/{folder}/{club_id}_{random_str}{ext}"
|
||||
if not upload_to_oss(file_obj, oss_file_path):
|
||||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||||
if image_type in (1, 2):
|
||||
upsert_tupianpeizhi_single(club_id, image_type, oss_file_path)
|
||||
else:
|
||||
Tupianpeizhi.query.create(
|
||||
club_id=club_id,
|
||||
ImageType=image_type,
|
||||
ImageURL=oss_file_path,
|
||||
AccessCount=0,
|
||||
)
|
||||
return Response({'code': 0, 'msg': '上传成功', 'data': {'image_url': oss_file_path}})
|
||||
|
||||
return Response({'code': 400, 'msg': 'action 必须为 add 或 delete'})
|
||||
|
||||
Reference in New Issue
Block a user