273 lines
11 KiB
Python
273 lines
11 KiB
Python
"""小程序图标与频道 — 后台管理 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'
|
|
import uuid
|
|
stamp = uuid.uuid4().hex[:12]
|
|
# 路径必须含 club_id + 唯一后缀:否则同名覆盖后 CDN 仍显示旧图,且会串俱乐部
|
|
if icon_key in POSTER_ICON_KEYS:
|
|
oss_path = f'{POSTER_FOLDER}/{club_id}_{icon_key}_{stamp}{ext}'
|
|
else:
|
|
oss_path = f'{ICON_FOLDER}/{club_id}/{icon_key}_{stamp}{ext}'
|
|
|
|
row = ClubMiniappIcon.query.filter(club_id=club_id, icon_key=icon_key).first()
|
|
old_path = (row.image_path or '').strip() if row else ''
|
|
|
|
if hasattr(file_obj, 'seek'):
|
|
try:
|
|
file_obj.seek(0)
|
|
except Exception:
|
|
pass
|
|
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,
|
|
)
|
|
if old_path and old_path != oss_path:
|
|
try:
|
|
delete_from_oss(old_path)
|
|
except Exception:
|
|
pass
|
|
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 无效'})
|