"""小程序图标与频道 — 后台管理 API。""" import os from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from backend.utils import verify_kefu_permission from config.models import ClubMiniappIcon, ClubPindao, ClubPindaoImage from jituan.services.club_context import resolve_effective_club_id from jituan.services.display_config import forbid_display_write_in_all_scope, list_response_meta from jituan.services.miniapp_assets import ( ICON_FOLDER, CONFIG_ONLY_DELIVERY_KEYS, MINIAPP_ICON_LABELS, MINIAPP_ICON_META, POSTER_FOLDER, POSTER_ICON_KEYS, default_icon_path, get_miniapp_icons_dict, get_pindao_payload, get_poster_payload, ) from products.views import delete_from_oss, upload_to_oss, validate_image POPUP_PERM = '8080a' PINDAO_FOLDER = 'beijing/pindao' class ClubMiniappIconListView(APIView): """POST /jituan/houtai/miniapp-icon-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) icons_db = { r.icon_key: r for r in ClubMiniappIcon.query.filter(club_id=club_id) } items = [] for key, label in MINIAPP_ICON_META: row = icons_db.get(key) uploaded = bool(row and row.is_uploaded and (row.image_path or '').strip()) if uploaded: image_path = row.image_path.strip() elif key in CONFIG_ONLY_DELIVERY_KEYS: # 未上传的大背景不回默认路径,避免后台预览 404、误以为已配置 image_path = '' else: image_path = (row.image_path if row else '') or default_icon_path(key) items.append({ 'icon_key': key, 'label': label, 'image_path': image_path, 'description': (row.description if row else '') or '', 'is_active': row.is_active if row else True, 'is_uploaded': uploaded, 'id': row.id if row else None, }) return Response({ 'code': 0, 'data': { 'icons': items, 'folder': ICON_FOLDER, **list_response_meta(request), }, }) class ClubMiniappIconManageView(APIView): """POST /jituan/houtai/miniapp-icon-manage action=upload|save_meta""" 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 'upload').strip() club_id = resolve_effective_club_id(request, request.user) raw_key = request.data.get('icon_key') if raw_key is None or raw_key == '': raw_key = request.query_params.get('icon_key') if isinstance(raw_key, (list, tuple)): raw_key = raw_key[0] if raw_key else '' icon_key = str(raw_key or '').strip() if icon_key not in MINIAPP_ICON_LABELS: return Response({ 'code': 400, 'msg': ( f'icon_key 无效: {icon_key or "(空)"}。' '若上传接单池卡背景,请确认 Django 已 pull 到含 grab_card_* 的提交并重启进程' ), }) if action == 'save_meta': description = (request.data.get('description') or '').strip() row, _ = ClubMiniappIcon.query.get_or_create( club_id=club_id, icon_key=icon_key, defaults={ 'label': MINIAPP_ICON_LABELS[icon_key], 'image_path': default_icon_path(icon_key), }, ) row.description = description row.label = MINIAPP_ICON_LABELS[icon_key] row.save(update_fields=['description', 'label', 'UpdateTime']) return Response({'code': 0, 'msg': '保存成功'}) 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 '.png' if ext not in ('.png', '.jpg', '.jpeg', '.webp'): ext = '.png' if icon_key in POSTER_ICON_KEYS: oss_path = f'{POSTER_FOLDER}/{club_id}_{icon_key}{ext}' else: oss_path = f'{ICON_FOLDER}/{icon_key}{ext}' row = ClubMiniappIcon.query.filter(club_id=club_id, icon_key=icon_key).first() if row and row.image_path and row.image_path != oss_path: delete_from_oss(row.image_path) if not upload_to_oss(file_obj, oss_path): return Response({'code': 500, 'msg': '图片上传失败'}) if row: row.image_path = oss_path row.label = MINIAPP_ICON_LABELS[icon_key] row.is_active = True row.is_uploaded = True row.save(update_fields=['image_path', 'label', 'is_active', 'is_uploaded', 'UpdateTime']) else: ClubMiniappIcon.query.create( club_id=club_id, icon_key=icon_key, label=MINIAPP_ICON_LABELS[icon_key], image_path=oss_path, is_active=True, is_uploaded=True, ) return Response({'code': 0, 'msg': '上传成功', 'data': {'image_path': oss_path}}) class ClubPindaoManageView(APIView): """POST /jituan/houtai/pindao-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': '无权限管理频道'}) action = (request.data.get('action') or 'get').strip() club_id = resolve_effective_club_id(request, request.user) if action == 'get': pindao = get_pindao_payload(club_id) images = [ { 'id': img.id, 'image_path': img.image_path, 'sort_order': img.sort_order, } for img in ClubPindaoImage.query.filter(club_id=club_id).order_by('sort_order', 'id') ] return Response({ 'code': 0, 'data': { 'pindao': pindao, 'images': images, 'folder': PINDAO_FOLDER, **list_response_meta(request), }, }) deny = forbid_display_write_in_all_scope(request) if deny: return deny if action == 'save': channel_no = (request.data.get('channel_no') or '').strip() title = (request.data.get('title') or '频道').strip() or '频道' row, _ = ClubPindao.query.get_or_create(club_id=club_id, defaults={'title': title}) row.channel_no = channel_no row.title = title row.is_active = True row.save(update_fields=['channel_no', 'title', 'is_active', 'UpdateTime']) return Response({'code': 0, 'msg': '保存成功'}) if action == 'add_image': 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' import random import string rnd = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) oss_path = f'{PINDAO_FOLDER}/{club_id}_{rnd}{ext}' if not upload_to_oss(file_obj, oss_path): return Response({'code': 500, 'msg': '图片上传失败'}) pindao_row, _ = ClubPindao.query.get_or_create(club_id=club_id, defaults={'title': '频道'}) max_sort = ClubPindaoImage.query.filter(club_id=club_id).order_by('-sort_order').first() sort_order = (max_sort.sort_order + 1) if max_sort else 0 img = ClubPindaoImage.query.create( club_id=club_id, pindao=pindao_row, image_path=oss_path, sort_order=sort_order, ) return Response({'code': 0, 'msg': '上传成功', 'data': {'id': img.id, 'image_path': oss_path}}) if action == 'delete_image': img_id = request.data.get('id') if not img_id: return Response({'code': 400, 'msg': '缺少图片 id'}) img = ClubPindaoImage.query.filter(club_id=club_id, id=img_id).first() if not img: return Response({'code': 404, 'msg': '图片不存在'}) if img.image_path: delete_from_oss(img.image_path) img.delete() return Response({'code': 0, 'msg': '删除成功'}) return Response({'code': 400, 'msg': 'action 无效'})