feat: 俱乐部杂项图片/关注快手;角色客服按club隔离

This commit is contained in:
XingQue
2026-06-25 02:43:40 +08:00
parent 0c4b28ea45
commit 7a2723fef7
9 changed files with 280 additions and 21 deletions

View 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,
),
]

View 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()

View File

@@ -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

View File

@@ -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'),

View File

@@ -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'})