280 lines
12 KiB
Python
280 lines
12 KiB
Python
"""小程序系统配置 & 页面资源 — 后台管理接口(客服终端 + 管理员)"""
|
||
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}})
|