From c991aa99c0701b4965c76ce27b5bedf4727c1e4a Mon Sep 17 00:00:00 2001 From: XingQue Date: Sat, 27 Jun 2026 01:38:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B0=8F=E7=A8=8B=E5=BA=8F=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=8E=A5=E5=8F=A3=E6=94=B9=E7=94=A8=E5=AE=A2=E6=9C=8D?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=A0=A1=E9=AA=8C=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?401=E8=B8=A2=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- peizhi/xcx_config_views.py | 91 +++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/peizhi/xcx_config_views.py b/peizhi/xcx_config_views.py index 08de07f..0834c5c 100644 --- a/peizhi/xcx_config_views.py +++ b/peizhi/xcx_config_views.py @@ -1,12 +1,13 @@ -"""小程序系统配置 & 页面资源 — 后台管理接口""" +"""小程序系统配置 & 页面资源 — 后台管理接口(客服终端 + 管理员)""" import logging from django.db import transaction -from rest_framework import permissions, status +from rest_framework import permissions from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.response import Response from rest_framework.views import APIView +from houtai.utils import verify_kefu_permission from peizhi.models import XcxSysPeizhi, XcxPageZiyuan from utils.oss_utils import upload_to_oss, validate_image from utils.page_ziyuan_service import DEFAULT_ZIYUAN, build_page_assets_for_api, clear_page_ziyuan_cache @@ -15,17 +16,53 @@ from yonghu.models import AdminProfile logger = logging.getLogger(__name__) +# 与弹窗公告等小程序配置页共用权限码 +REQUIRED_PERMISSION = '8080a' -def _verify_admin(request, zhanghao): - if not zhanghao: - return False, Response({'code': 1, 'msg': '账号不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) - user_main = request.user - if user_main.phone != zhanghao or user_main.user_type != 'admin': - return False, Response({'code': 1, 'msg': '无权限', 'data': None}, status=status.HTTP_401_UNAUTHORIZED) - try: - user_main.admin_profile - except AdminProfile.DoesNotExist: - return False, Response({'code': 1, 'msg': '无权限', 'data': None}, status=status.HTTP_401_UNAUTHORIZED) + +def _frontend_account(request): + raw = ( + request.data.get('username') + or request.data.get('phone') + or request.data.get('zhanghao') + or '' + ) + return str(raw).strip() + + +def _authorize_miniapp_config(request): + """ + 客服终端:verify_kefu_permission + 8080a + 管理员账号:user_type=admin 且 phone 匹配(兼容旧后台) + 失败一律 HTTP 200 + code,避免前端 401 误踢登录 + """ + account = _frontend_account(request) + if not account: + return False, Response({'code': 1, 'msg': '账号不能为空', 'data': None}) + + user = request.user + if user.user_type == 'admin' and user.phone == account: + try: + user.admin_profile + return True, None + except AdminProfile.DoesNotExist: + pass + + kefu, perms_or_resp = verify_kefu_permission(request, account) + if kefu is None: + if isinstance(perms_or_resp, Response): + body = perms_or_resp.data if hasattr(perms_or_resp, 'data') else {} + code = body.get('code', 403) + msg = body.get('msg', '身份验证失败') + return False, Response({'code': code, 'msg': msg, 'data': None}) + return False, Response({'code': 403, 'msg': '身份验证失败', 'data': None}) + + if REQUIRED_PERMISSION not in perms_or_resp: + return False, Response({ + 'code': 403, + 'msg': '您没有权限访问小程序配置(需权限 8080a)', + 'data': None, + }) return True, None @@ -38,13 +75,13 @@ def _mask_secret(val): class XcxSysPeizhiQueryView(APIView): - """POST /peizhi/xcxsyshq — 管理员获取小程序系统配置""" + """POST /peizhi/xcxsyshq — 获取小程序系统配置""" permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] def post(self, request): - ok, err = _verify_admin(request, request.data.get('zhanghao')) + ok, err = _authorize_miniapp_config(request) if not ok: return err row = XcxSysPeizhi.objects.first() @@ -82,7 +119,7 @@ class XcxSysPeizhiQueryView(APIView): class XcxSysPeizhiUpdateView(APIView): - """POST /peizhi/xcxsysgx — 管理员更新小程序系统配置""" + """POST /peizhi/xcxsysgx — 更新小程序系统配置""" permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] @@ -96,7 +133,7 @@ class XcxSysPeizhiUpdateView(APIView): }) def post(self, request): - ok, err = _verify_admin(request, request.data.get('zhanghao')) + ok, err = _authorize_miniapp_config(request) if not ok: return err payload = request.data.get('config') or request.data @@ -120,13 +157,13 @@ class XcxSysPeizhiUpdateView(APIView): class XcxPageZiyuanQueryView(APIView): - """POST /peizhi/xcxyzycx — 管理员查询页面资源列表""" + """POST /peizhi/xcxyzycx — 查询页面资源列表""" permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] def post(self, request): - ok, err = _verify_admin(request, request.data.get('zhanghao')) + ok, err = _authorize_miniapp_config(request) if not ok: return err zu = (request.data.get('ziyuan_zu') or '').strip() @@ -148,18 +185,18 @@ class XcxPageZiyuanQueryView(APIView): class XcxPageZiyuanUpdateView(APIView): - """POST /peizhi/xcxyzgx — 管理员更新页面资源路径""" + """POST /peizhi/xcxyzgx — 更新页面资源路径""" permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] def post(self, request): - ok, err = _verify_admin(request, request.data.get('zhanghao')) + ok, err = _authorize_miniapp_config(request) if not ok: return err items = request.data.get('items') or [] if not isinstance(items, list) or not items: - return Response({'code': 1, 'msg': 'items 不能为空', 'data': None}, status=400) + return Response({'code': 1, 'msg': 'items 不能为空', 'data': None}) with transaction.atomic(): for item in items: zu = (item.get('ziyuan_zu') or '').strip() @@ -176,25 +213,25 @@ class XcxPageZiyuanUpdateView(APIView): class XcxPageZiyuanUploadView(APIView): - """POST /peizhi/xcxyzsc — 管理员上传页面资源图片""" + """POST /peizhi/xcxyzsc — 上传页面资源图片""" permission_classes = [permissions.IsAuthenticated] parser_classes = (MultiPartParser, FormParser) def post(self, request): - ok, err = _verify_admin(request, request.data.get('zhanghao')) + ok, err = _authorize_miniapp_config(request) if not ok: return err zu = (request.data.get('ziyuan_zu') or '').strip() key = (request.data.get('ziyuan_key') or '').strip() image_file = request.FILES.get('image') if not zu or not key: - return Response({'code': 1, 'msg': 'ziyuan_zu/ziyuan_key 必填', 'data': None}, status=400) + return Response({'code': 1, 'msg': 'ziyuan_zu/ziyuan_key 必填', 'data': None}) if not image_file: - return Response({'code': 2, 'msg': '请选择图片', 'data': None}, status=400) + return Response({'code': 2, 'msg': '请选择图片', 'data': None}) is_valid, error_msg = validate_image(image_file) if not is_valid: - return Response({'code': 3, 'msg': error_msg, 'data': None}, status=400) + return Response({'code': 3, 'msg': error_msg, 'data': None}) default_key = f'{zu}.{key}' default_path = DEFAULT_ZIYUAN.get(default_key, f'beijing/{zu}/{key}.png') @@ -202,7 +239,7 @@ class XcxPageZiyuanUploadView(APIView): image_file.seek(0) full_url = upload_to_oss(image_file, oss_path) if not full_url: - return Response({'code': 4, 'msg': '上传失败', 'data': None}, status=500) + return Response({'code': 4, 'msg': '上传失败', 'data': None}) with transaction.atomic(): XcxPageZiyuan.objects.update_or_create(