Files
along_django/peizhi/xcx_config_views.py

280 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""小程序系统配置 & 页面资源 — 后台管理接口(客服终端 + 管理员)"""
import logging
from django.db import transaction
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, get_ziyuan_miaoshu
from utils.xcx_sys_config import clear_xcx_sys_peizhi_cache, wx_cfg
from yonghu.models import AdminProfile
logger = logging.getLogger(__name__)
# 与弹窗公告等小程序配置页共用权限码
REQUIRED_PERMISSION = '8080a'
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
def _mask_secret(val):
if not val:
return ''
if len(val) <= 8:
return '****'
return val[:4] + '****' + val[-4:]
class XcxSysPeizhiQueryView(APIView):
"""POST /peizhi/xcxsyshq — 获取小程序系统配置"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
ok, err = _authorize_miniapp_config(request)
if not ok:
return err
row = XcxSysPeizhi.objects.first()
data = {
'weixin_appid': row.weixin_appid if row else wx_cfg.WEIXIN_APPID,
'weixin_secret': _mask_secret(row.weixin_secret if row else wx_cfg.WEIXIN_SECRET),
'weixin_official_appid': row.weixin_official_appid if row else wx_cfg.WEIXIN_OFFICIAL_APPID,
'weixin_official_secret': _mask_secret(
row.weixin_official_secret if row else wx_cfg.WEIXIN_OFFICIAL_SECRET
),
'weixin_official_token': row.weixin_official_token if row else wx_cfg.WEIXIN_OFFICIAL_TOKEN,
'weixin_official_encoding_aes_key': _mask_secret(
row.weixin_official_encoding_aes_key if row else wx_cfg.WEIXIN_OFFICIAL_ENCODING_AES_KEY
),
'weixin_template_id': row.weixin_template_id if row else wx_cfg.WEIXIN_TEMPLATE_ID,
'weixin_template_max_per_minute': (
row.weixin_template_max_per_minute if row else wx_cfg.WEIXIN_TEMPLATE_MAX_PER_MINUTE
),
'weixin_template_batch_size': (
row.weixin_template_batch_size if row else wx_cfg.WEIXIN_TEMPLATE_BATCH_SIZE
),
'weixin_broadcast_enabled': (
row.weixin_broadcast_enabled if row else wx_cfg.WEIXIN_BROADCAST_ENABLED
),
'weixin_broadcast_workers': row.weixin_broadcast_workers if row else wx_cfg.WEIXIN_BROADCAST_WORKERS,
'weixin_mchid': row.weixin_mchid if row else wx_cfg.WEIXIN_MCHID,
'weixin_shanghu_miyao': _mask_secret(row.weixin_shanghu_miyao if row else wx_cfg.WEIXIN_SHANGHUMIYAO),
'weixin_notify_url': row.weixin_notify_url if row else wx_cfg.WEIXIN_NOTIFY_URL,
'weixin_cert_path': row.weixin_cert_path if row else wx_cfg.WEIXIN_CERT_PATH,
'weixin_key_path': row.weixin_key_path if row else wx_cfg.WEIXIN_KEY_PATH,
'notify_base_url': row.notify_base_url if row else wx_cfg.NOTIFY_BASE_URL,
'shangpin_shenhe_mode': row.shangpin_shenhe_mode if row else wx_cfg.SHANGPIN_SHENHE_MODE,
'shangjia_paifa_jiage_min': str(
row.shangjia_paifa_jiage_min if row else wx_cfg.SHANGJIA_PAIFA_JIAGE_MIN
),
'shangjia_paifa_jiage_max': str(
row.shangjia_paifa_jiage_max if row else wx_cfg.SHANGJIA_PAIFA_JIAGE_MAX
),
'from_database': bool(row),
}
return Response({'code': 0, 'msg': 'success', 'data': data})
class XcxSysPeizhiUpdateView(APIView):
"""POST /peizhi/xcxsysgx — 更新小程序系统配置"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [JSONParser]
UPDATABLE = frozenset({
'weixin_appid', 'weixin_secret', 'weixin_official_appid', 'weixin_official_secret',
'weixin_official_token', 'weixin_official_encoding_aes_key', 'weixin_template_id',
'weixin_template_max_per_minute', 'weixin_template_batch_size', 'weixin_broadcast_enabled',
'weixin_broadcast_workers', 'weixin_mchid', 'weixin_shanghu_miyao', 'weixin_notify_url',
'weixin_cert_path', 'weixin_key_path', 'notify_base_url', 'shangpin_shenhe_mode',
'shangjia_paifa_jiage_min', 'shangjia_paifa_jiage_max',
})
def post(self, request):
ok, err = _authorize_miniapp_config(request)
if not ok:
return err
payload = request.data.get('config') or request.data
with transaction.atomic():
row, _ = XcxSysPeizhi.objects.get_or_create(id=1)
for key in self.UPDATABLE:
if key in payload and payload[key] is not None:
val = payload[key]
if key.endswith('_secret') or key.endswith('_miyao') or key.endswith('_aes_key'):
if val == '' or '****' in str(val):
continue
if key == 'weixin_broadcast_enabled':
row.weixin_broadcast_enabled = bool(val)
elif key in ('weixin_template_max_per_minute', 'weixin_template_batch_size', 'weixin_broadcast_workers'):
row.__setattr__(key, int(val))
elif key == 'shangpin_shenhe_mode':
mode = int(val)
if mode not in (1, 2):
continue
row.shangpin_shenhe_mode = mode
elif key in ('shangjia_paifa_jiage_min', 'shangjia_paifa_jiage_max'):
from decimal import Decimal, InvalidOperation
try:
amount = Decimal(str(val))
except (InvalidOperation, ValueError, TypeError):
continue
if amount <= 0:
continue
row.__setattr__(key, amount)
else:
row.__setattr__(key, str(val).strip())
row.save()
clear_xcx_sys_peizhi_cache()
row = XcxSysPeizhi.objects.first()
if row and row.shangjia_paifa_jiage_min > row.shangjia_paifa_jiage_max:
row.shangjia_paifa_jiage_min, row.shangjia_paifa_jiage_max = (
row.shangjia_paifa_jiage_max, row.shangjia_paifa_jiage_min,
)
row.save(update_fields=['shangjia_paifa_jiage_min', 'shangjia_paifa_jiage_max'])
clear_xcx_sys_peizhi_cache()
return Response({'code': 0, 'msg': '配置已更新', 'data': None})
class XcxPageZiyuanQueryView(APIView):
"""POST /peizhi/xcxyzycx — 查询页面资源列表"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
ok, err = _authorize_miniapp_config(request)
if not ok:
return err
zu = (request.data.get('ziyuan_zu') or '').strip()
merged = build_page_assets_for_api()
if zu:
groups = {zu: merged.get(zu, {})}
else:
groups = merged
items = []
for gzu, keys in groups.items():
for key, path in keys.items():
items.append({
'ziyuan_zu': gzu,
'ziyuan_key': key,
'ziyuan_path': path,
'miaoshu': get_ziyuan_miaoshu(gzu, key),
})
return Response({'code': 0, 'msg': 'success', 'data': {'list': items, 'groups': groups}})
class XcxPageZiyuanUpdateView(APIView):
"""POST /peizhi/xcxyzgx — 更新页面资源路径"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
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})
with transaction.atomic():
for item in items:
zu = (item.get('ziyuan_zu') or '').strip()
key = (item.get('ziyuan_key') or '').strip()
path = (item.get('ziyuan_path') or '').strip().lstrip('/')
if not zu or not key or not path:
continue
XcxPageZiyuan.objects.update_or_create(
ziyuan_zu=zu, ziyuan_key=key,
defaults={'ziyuan_path': path, 'miaoshu': item.get('miaoshu') or ''},
)
clear_page_ziyuan_cache()
return Response({'code': 0, 'msg': '资源已更新', 'data': None})
class XcxPageZiyuanUploadView(APIView):
"""POST /peizhi/xcxyzsc — 上传页面资源图片"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = (MultiPartParser, FormParser)
def post(self, request):
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})
if not image_file:
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})
default_key = f'{zu}.{key}'
default_path = DEFAULT_ZIYUAN.get(default_key, f'beijing/{zu}/{key}.png')
oss_path = default_path
image_file.seek(0)
full_url = upload_to_oss(image_file, oss_path)
if not full_url:
return Response({'code': 4, 'msg': '上传失败', 'data': None})
with transaction.atomic():
XcxPageZiyuan.objects.update_or_create(
ziyuan_zu=zu, ziyuan_key=key,
defaults={'ziyuan_path': oss_path},
)
clear_page_ziyuan_cache()
return Response({'code': 0, 'msg': '上传成功', 'data': {'ziyuan_path': oss_path}})