P1 性能优化(避免循环内 N+1 / N 次 DB 查询): - shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组 - product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断 - roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取 - guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete - admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update - jituan 服务层多处合并统计查询、批量预取 map - rank 多处循环 N+1 改批量预取 - backend 多处循环内 count/create 改批量 - config/orders/merchant_ops 多处循环 N+1 改批量预取 其他改动: - users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件 - 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
1167 lines
46 KiB
Python
1167 lines
46 KiB
Python
import hmac
|
||
import threading
|
||
import traceback
|
||
import uuid
|
||
import hashlib
|
||
import xmltodict
|
||
import time
|
||
import logging
|
||
import requests
|
||
import os
|
||
import secrets
|
||
import random
|
||
import string
|
||
from calendar import monthrange
|
||
from decimal import Decimal
|
||
from datetime import date, datetime, timedelta
|
||
from django.utils import timezone
|
||
from django.conf import settings
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.core.paginator import Paginator
|
||
from django.db import models, transaction, IntegrityError
|
||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||
from django.contrib.auth.hashers import make_password
|
||
from django.views.decorators.csrf import csrf_exempt
|
||
from django.utils.decorators import method_decorator
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||
|
||
# 工具类导入
|
||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||
from backend.utils import (
|
||
update_dashou_daily_by_action,
|
||
update_shangjia_daily
|
||
)
|
||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||
from jituan.services.club_penalty import (
|
||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||
)
|
||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||
from shop.utils import update_dianpu_daily_stat
|
||
from products.utils import update_shangpin_daily_stat
|
||
from orders.notice_tasks import dingdan_guangbo
|
||
from ..utils import (
|
||
verify_kefu_permission,
|
||
PERMISSION_TO_SHENFEN,
|
||
SHENFEN_PROFILE_MAP,
|
||
has_fadan_view_permission,
|
||
has_merchant_order_permission,
|
||
check_fadan_permission,
|
||
pick_order_id,
|
||
pick_penalty_create_params,
|
||
write_xiugai_log,
|
||
XIUGAI_LEIXING_DASHOU,
|
||
XIUGAI_LEIXING_GUANSHI,
|
||
XIUGAI_LEIXING_SHANGJIA,
|
||
XIUGAI_LEIXING_ZUZHANG,
|
||
XIUGAI_LEIXING_LABEL,
|
||
)
|
||
|
||
# models 集中导入
|
||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||
## backend
|
||
from backend.models import WithdrawalDailyStats
|
||
|
||
## users
|
||
from users.models import (
|
||
UserGuanshi, UserBoss, UserZuzhang,
|
||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||
)
|
||
from users.business_models import User
|
||
|
||
## orders
|
||
from orders.models import (
|
||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||
OrderPlayerHistory, Order, RefundRecord,
|
||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||
)
|
||
|
||
## shop
|
||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||
|
||
## products
|
||
from products.models import (
|
||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||
)
|
||
|
||
## config
|
||
from config.models import (
|
||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||
TixianQuotaDefault,
|
||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||
)
|
||
|
||
## rank
|
||
from rank.models import (
|
||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||
)
|
||
|
||
# 序列化器
|
||
from ..serializers import PopupPageSerializer
|
||
|
||
# 全局常量、日志对象
|
||
logger = logging.getLogger('houtai')
|
||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||
|
||
class GetOperationLogListView(APIView):
|
||
"""
|
||
后台操作日志列表(xiugaijilu)
|
||
POST /houtai/hqczrz
|
||
权限:000001 超级权限 或 czrz666
|
||
"""
|
||
permission_classes = []
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 401, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '000001' not in permissions and 'czrz666' not in permissions:
|
||
return Response({'code': 403, 'msg': '无权限查看操作日志(需要000001或czrz666)'})
|
||
|
||
yonghuid = request.data.get('yonghuid', '').strip()
|
||
xiugaiid = request.data.get('xiugaiid', '').strip()
|
||
leixing = request.data.get('leixing')
|
||
keyword = request.data.get('keyword', '').strip()
|
||
try:
|
||
page = max(1, int(request.data.get('page', 1)))
|
||
except (ValueError, TypeError):
|
||
page = 1
|
||
try:
|
||
page_size = min(100, max(1, int(request.data.get('page_size', 20))))
|
||
except (ValueError, TypeError):
|
||
page_size = 20
|
||
|
||
qs = Xiugaijilu.query.all().order_by('-CreateTime')
|
||
from jituan.services.admin_audit import filter_xiugaijilu_by_request
|
||
qs = filter_xiugaijilu_by_request(qs, request)
|
||
if yonghuid:
|
||
qs = qs.filter(yonghuid=yonghuid)
|
||
if xiugaiid:
|
||
qs = qs.filter(xiugaiid__icontains=xiugaiid)
|
||
if leixing not in (None, ''):
|
||
try:
|
||
qs = qs.filter(leixing=int(leixing))
|
||
except (ValueError, TypeError):
|
||
pass
|
||
if keyword:
|
||
qs = qs.filter(qitashuoming__icontains=keyword)
|
||
|
||
total = qs.count()
|
||
start = (page - 1) * page_size
|
||
records = qs[start:start + page_size]
|
||
|
||
def _fmt_decimal(val):
|
||
if val is None:
|
||
return None
|
||
return str(val)
|
||
|
||
data_list = []
|
||
for r in records:
|
||
data_list.append({
|
||
'id': r.id,
|
||
'yonghuid': r.yonghuid,
|
||
'xiugaiid': r.xiugaiid,
|
||
'leixing': r.leixing,
|
||
'leixing_label': XIUGAI_LEIXING_LABEL.get(r.leixing, f'类型{r.leixing}'),
|
||
'xiugaitijiao': _fmt_decimal(r.xiugaitijiao),
|
||
'xiugaitijiaoq': _fmt_decimal(r.xiugaitijiaoq),
|
||
'jifen': r.jifen,
|
||
'yjifen': r.yjifen,
|
||
'yajin': _fmt_decimal(r.yajin),
|
||
'yyajin': _fmt_decimal(r.yyajin),
|
||
'shangjiayue': _fmt_decimal(r.shangjiayue),
|
||
'shangjiayueq': _fmt_decimal(r.shangjiayueq),
|
||
'guanshiyue': _fmt_decimal(r.guanshiyue),
|
||
'guanshiyueq': _fmt_decimal(r.guanshiyueq),
|
||
'huiyuants': r.huiyuants,
|
||
'huiyuan_id': r.huiyuan_id,
|
||
'qitashuoming': r.qitashuoming or '',
|
||
'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||
})
|
||
|
||
from jituan.services.club_user_access import list_response_meta
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'list': data_list,
|
||
'total': total,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
**list_response_meta(request),
|
||
},
|
||
})
|
||
|
||
|
||
class GetProductTypeZoneView(APIView):
|
||
"""
|
||
获取商品类型、专区及会员列表
|
||
权限:需要拥有 7007a
|
||
请求:POST /houtai/hthqsplxzq
|
||
参数:{"username": "客服手机号"}
|
||
返回:
|
||
code: 0
|
||
data: {
|
||
type_list: [{id, jieshao, tupian_url, yaoqiuleixing, huiyuan_id, yongjin, shenhezhuangtai, paixu, product_count}, ...],
|
||
zone_list: [{id, mingzi, leixing_id, shenhezhuangtai, paixu}, ...],
|
||
member_list: [{huiyuan_id, jieshao, jiage}, ...]
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '7007a' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||
scope_err = block_non_group_catalog_scope(request)
|
||
if scope_err:
|
||
return Response({'code': 400, 'msg': scope_err})
|
||
|
||
# 查询所有商品类型(含专区数量)
|
||
# 一次性分组聚合所有类型的专区数量,避免循环内 N+1 count
|
||
# 注:ShangpinZhuanqu.leixing_id 是 PositiveIntegerField(非 FK),无法用反向 annotate
|
||
zone_count_rows = ShangpinZhuanqu.query.values('leixing_id').annotate(cnt=Count('id'))
|
||
zone_count_map = {row['leixing_id']: row['cnt'] for row in zone_count_rows}
|
||
|
||
types = ShangpinLeixing.query.all().order_by('-paixu') # paixu 越大越靠前
|
||
type_list = []
|
||
for t in types:
|
||
# 从预聚合的 map 中取专区数量
|
||
zone_count = zone_count_map.get(t.id, 0)
|
||
type_list.append({
|
||
'id': t.id,
|
||
'jieshao': t.jieshao or '',
|
||
'tupian_url': t.tupian_url or '',
|
||
'yaoqiuleixing': t.yaoqiuleixing,
|
||
'huiyuan_id': t.huiyuan_id or '',
|
||
'yongjin': float(t.yongjin) if t.yongjin is not None else None,
|
||
'shenhezhuangtai': t.shenhezhuangtai,
|
||
'paixu': t.paixu,
|
||
'product_count': zone_count,
|
||
})
|
||
|
||
# 查询所有专区
|
||
zones = ShangpinZhuanqu.query.all().order_by('-paixu')
|
||
zone_list = [
|
||
{
|
||
'id': z.id,
|
||
'mingzi': z.mingzi or '',
|
||
'leixing_id': z.leixing_id,
|
||
'shenhezhuangtai': z.shenhezhuangtai,
|
||
'paixu': z.paixu,
|
||
}
|
||
for z in zones
|
||
]
|
||
|
||
# 查询所有会员(只返回 id、介绍、价格)
|
||
members = Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage')
|
||
member_list = [
|
||
{
|
||
'huiyuan_id': m['huiyuan_id'],
|
||
'jieshao': m['jieshao'],
|
||
'jiage': float(m['jiage']),
|
||
}
|
||
for m in members
|
||
]
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'type_list': type_list,
|
||
'zone_list': zone_list,
|
||
'member_list': member_list,
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
class ModifyProductTypeZoneView(APIView):
|
||
"""
|
||
统一修改商品类型/专区(增、删、改)
|
||
权限:需要拥有 7007a
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser, MultiPartParser, FormParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
obj_type = request.data.get('type') # 'leixing' 或 'zhuanqu'
|
||
action = request.data.get('action') # 'add', 'update', 'delete'
|
||
|
||
if not username or not obj_type or not action:
|
||
return Response({'code': 400, 'msg': '缺少必要参数'})
|
||
|
||
# 权限校验
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '7007a' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||
scope_err = block_non_group_catalog_scope(request)
|
||
if scope_err:
|
||
return Response({'code': 400, 'msg': scope_err})
|
||
|
||
if obj_type == 'leixing':
|
||
return self.handle_leixing(request, action)
|
||
elif obj_type == 'zhuanqu':
|
||
return self.handle_zhuanqu(request, action)
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的type参数'})
|
||
|
||
# ---------- 商品类型处理 ----------
|
||
def handle_leixing(self, request, action):
|
||
if action == 'add':
|
||
return self.add_leixing(request)
|
||
elif action == 'update':
|
||
return self.update_leixing(request)
|
||
elif action == 'delete':
|
||
return self.delete_leixing(request)
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的action'})
|
||
|
||
def add_leixing(self, request):
|
||
# 获取并校验字段
|
||
jieshao = request.data.get('jieshao', '').strip()
|
||
if not jieshao:
|
||
return Response({'code': 400, 'msg': '类型名称不能为空'})
|
||
|
||
# 强制转为整数
|
||
try:
|
||
yaoqiuleixing = int(request.data.get('yaoqiuleixing', 0))
|
||
if yaoqiuleixing not in [1, 2]:
|
||
raise ValueError
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'})
|
||
|
||
try:
|
||
shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
|
||
if shenhezhuangtai not in [1, 2]:
|
||
raise ValueError
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
|
||
yongjin = request.data.get('yongjin')
|
||
if yongjin is not None and yongjin != '':
|
||
try:
|
||
yongjin = Decimal(str(yongjin))
|
||
except:
|
||
return Response({'code': 400, 'msg': '佣金金额格式错误'})
|
||
else:
|
||
yongjin = None
|
||
|
||
# 图片校验
|
||
image_file = request.FILES.get('file')
|
||
if not image_file:
|
||
return Response({'code': 400, 'msg': '请上传类型图片'})
|
||
|
||
# 验证图片
|
||
try:
|
||
valid, msg = validate_image(image_file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': msg})
|
||
except ImportError:
|
||
pass
|
||
|
||
# 上传图片
|
||
ext = image_file.name.split('.')[-1].lower()
|
||
new_filename = f"{uuid.uuid4().hex}.{ext}"
|
||
oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}"
|
||
url = upload_to_oss(image_file, oss_path)
|
||
if not url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
|
||
# 创建类型
|
||
try:
|
||
with transaction.atomic():
|
||
leixing = ShangpinLeixing.query.create(
|
||
jieshao=jieshao,
|
||
tupian_url=oss_path,
|
||
yaoqiuleixing=yaoqiuleixing,
|
||
huiyuan_id=huiyuan_id,
|
||
yongjin=yongjin,
|
||
shenhezhuangtai=shenhezhuangtai,
|
||
paixu=0
|
||
)
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'id': leixing.id}})
|
||
except Exception as e:
|
||
logger.error(f"添加类型失败: {e}", exc_info=True)
|
||
delete_from_oss(oss_path)
|
||
return Response({'code': 500, 'msg': '数据库错误'})
|
||
|
||
def update_leixing(self, request):
|
||
type_id = request.data.get('id')
|
||
if not type_id:
|
||
return Response({'code': 400, 'msg': '缺少类型ID'})
|
||
try:
|
||
leixing = ShangpinLeixing.query.get(id=type_id)
|
||
except ShangpinLeixing.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '类型不存在'})
|
||
|
||
changes = {}
|
||
# 名称
|
||
jieshao = request.data.get('jieshao')
|
||
if jieshao is not None and jieshao.strip() != leixing.jieshao:
|
||
changes['jieshao'] = jieshao.strip()
|
||
|
||
# 抢单要求类型(强制转整数)
|
||
yaoqiuleixing = request.data.get('yaoqiuleixing')
|
||
if yaoqiuleixing is not None:
|
||
try:
|
||
new_val = int(yaoqiuleixing)
|
||
if new_val not in [1, 2]:
|
||
raise ValueError
|
||
if new_val != leixing.yaoqiuleixing:
|
||
changes['yaoqiuleixing'] = new_val
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '抢单要求类型无效(必须为1或2)'})
|
||
|
||
# 会员ID
|
||
huiyuan_id = request.data.get('huiyuan_id', '').strip() or None
|
||
if huiyuan_id != leixing.huiyuan_id:
|
||
changes['huiyuan_id'] = huiyuan_id
|
||
|
||
# 佣金金额
|
||
yongjin = request.data.get('yongjin')
|
||
if yongjin is not None:
|
||
if yongjin == '':
|
||
new_val = None
|
||
else:
|
||
try:
|
||
new_val = Decimal(str(yongjin))
|
||
except:
|
||
return Response({'code': 400, 'msg': '佣金金额格式错误'})
|
||
if new_val != leixing.yongjin:
|
||
changes['yongjin'] = new_val
|
||
|
||
# 审核状态
|
||
shenhezhuangtai = request.data.get('shenhezhuangtai')
|
||
if shenhezhuangtai is not None:
|
||
try:
|
||
new_val = int(shenhezhuangtai)
|
||
if new_val not in [1, 2]:
|
||
raise ValueError
|
||
if new_val != leixing.shenhezhuangtai:
|
||
changes['shenhezhuangtai'] = new_val
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
# 图片处理
|
||
image_file = request.FILES.get('file')
|
||
old_image_path = leixing.tupian_url
|
||
if image_file:
|
||
try:
|
||
valid, msg = validate_image(image_file)
|
||
if not valid:
|
||
return Response({'code': 400, 'msg': msg})
|
||
except ImportError:
|
||
pass
|
||
ext = image_file.name.split('.')[-1].lower()
|
||
new_filename = f"{uuid.uuid4().hex}.{ext}"
|
||
oss_path = f"a_long/shangpin/shangpinleixing/{new_filename}"
|
||
url = upload_to_oss(image_file, oss_path)
|
||
if not url:
|
||
return Response({'code': 500, 'msg': '图片上传失败'})
|
||
changes['tupian_url'] = oss_path
|
||
|
||
if not changes and not image_file:
|
||
return Response({'code': 400, 'msg': '未做任何修改'})
|
||
|
||
sync_to_products = request.data.get('sync_to_products') in [True, 'true', '1', 'on']
|
||
try:
|
||
with transaction.atomic():
|
||
for field, value in changes.items():
|
||
setattr(leixing, field, value)
|
||
leixing.save()
|
||
|
||
# 同步商品
|
||
if sync_to_products and ('yaoqiuleixing' in changes or 'huiyuan_id' in changes or 'yongjin' in changes):
|
||
product_updates = {}
|
||
if 'yaoqiuleixing' in changes:
|
||
product_updates['yaoqiuleixing'] = changes['yaoqiuleixing']
|
||
if 'huiyuan_id' in changes:
|
||
product_updates['huiyuan_id'] = changes['huiyuan_id']
|
||
if 'yongjin' in changes:
|
||
product_updates['yongjin'] = changes['yongjin']
|
||
if product_updates:
|
||
Shangpin.query.filter(leixing_id=type_id).update(**product_updates)
|
||
|
||
# 删除旧图片
|
||
if image_file and old_image_path:
|
||
delete_from_oss(old_image_path)
|
||
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
except Exception as e:
|
||
logger.error(f"更新类型失败: {e}", exc_info=True)
|
||
if image_file and 'oss_path' in locals():
|
||
delete_from_oss(oss_path)
|
||
return Response({'code': 500, 'msg': '修改失败'})
|
||
|
||
def delete_leixing(self, request):
|
||
type_id = request.data.get('id')
|
||
if not type_id:
|
||
return Response({'code': 400, 'msg': '缺少类型ID'})
|
||
delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on']
|
||
|
||
try:
|
||
leixing = ShangpinLeixing.query.get(id=type_id)
|
||
except ShangpinLeixing.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '类型不存在'})
|
||
|
||
with transaction.atomic():
|
||
# 删除该类型下的所有专区
|
||
zones = ShangpinZhuanqu.query.filter(leixing_id=type_id)
|
||
if delete_products:
|
||
# 收集 zone_ids 后批量删除,避免循环内 N+1 delete
|
||
zone_ids = list(zones.values_list('id', flat=True))
|
||
if zone_ids:
|
||
Shangpin.query.filter(zhuanqu_id__in=zone_ids).delete()
|
||
ShangpinZhuanqu.query.filter(id__in=zone_ids).delete()
|
||
# 删除直接关联该类型的商品
|
||
Shangpin.query.filter(leixing_id=type_id).delete()
|
||
else:
|
||
zones.delete()
|
||
# 仅清除商品的类型关联
|
||
Shangpin.query.filter(leixing_id=type_id).update(leixing_id=None)
|
||
|
||
leixing.delete()
|
||
|
||
if leixing.tupian_url:
|
||
delete_from_oss(leixing.tupian_url)
|
||
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
# ---------- 商品专区处理 ----------
|
||
def handle_zhuanqu(self, request, action):
|
||
if action == 'add':
|
||
return self.add_zhuanqu(request)
|
||
elif action == 'update':
|
||
return self.update_zhuanqu(request)
|
||
elif action == 'delete':
|
||
return self.delete_zhuanqu(request)
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的action'})
|
||
|
||
def add_zhuanqu(self, request):
|
||
mingzi = request.data.get('mingzi', '').strip()
|
||
leixing_id = request.data.get('leixing_id')
|
||
if not mingzi:
|
||
return Response({'code': 400, 'msg': '专区名称不能为空'})
|
||
if not leixing_id:
|
||
return Response({'code': 400, 'msg': '请选择所属类型'})
|
||
|
||
try:
|
||
leixing_id = int(leixing_id)
|
||
if not ShangpinLeixing.query.filter(id=leixing_id).exists():
|
||
return Response({'code': 404, 'msg': '所属类型不存在'})
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '所属类型ID无效'})
|
||
|
||
try:
|
||
shenhezhuangtai = int(request.data.get('shenhezhuangtai', 1))
|
||
if shenhezhuangtai not in [1, 2]:
|
||
raise ValueError
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
zhuanqu = ShangpinZhuanqu.query.create(
|
||
mingzi=mingzi,
|
||
leixing_id=leixing_id,
|
||
shenhezhuangtai=shenhezhuangtai,
|
||
paixu=0
|
||
)
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'id': zhuanqu.id}})
|
||
except Exception as e:
|
||
logger.error(f"添加专区失败: {e}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '数据库错误'})
|
||
|
||
def update_zhuanqu(self, request):
|
||
zone_id = request.data.get('id')
|
||
if not zone_id:
|
||
return Response({'code': 400, 'msg': '缺少专区ID'})
|
||
try:
|
||
zhuanqu = ShangpinZhuanqu.query.get(id=zone_id)
|
||
except ShangpinZhuanqu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '专区不存在'})
|
||
|
||
changes = {}
|
||
mingzi = request.data.get('mingzi')
|
||
if mingzi is not None and mingzi.strip() != zhuanqu.mingzi:
|
||
changes['mingzi'] = mingzi.strip()
|
||
|
||
leixing_id = request.data.get('leixing_id')
|
||
if leixing_id is not None:
|
||
try:
|
||
new_id = int(leixing_id)
|
||
if not ShangpinLeixing.query.filter(id=new_id).exists():
|
||
return Response({'code': 404, 'msg': '新所属类型不存在'})
|
||
if new_id != zhuanqu.leixing_id:
|
||
changes['leixing_id'] = new_id
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '所属类型ID无效'})
|
||
|
||
shenhezhuangtai = request.data.get('shenhezhuangtai')
|
||
if shenhezhuangtai is not None:
|
||
try:
|
||
new_val = int(shenhezhuangtai)
|
||
if new_val not in [1, 2]:
|
||
raise ValueError
|
||
if new_val != zhuanqu.shenhezhuangtai:
|
||
changes['shenhezhuangtai'] = new_val
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 400, 'msg': '审核状态无效(必须为1或2)'})
|
||
|
||
if not changes:
|
||
return Response({'code': 400, 'msg': '未做任何修改'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
for field, value in changes.items():
|
||
setattr(zhuanqu, field, value)
|
||
zhuanqu.save()
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
except Exception as e:
|
||
logger.error(f"更新专区失败: {e}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '修改失败'})
|
||
|
||
def delete_zhuanqu(self, request):
|
||
zone_id = request.data.get('id')
|
||
if not zone_id:
|
||
return Response({'code': 400, 'msg': '缺少专区ID'})
|
||
delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on']
|
||
|
||
try:
|
||
zhuanqu = ShangpinZhuanqu.query.get(id=zone_id)
|
||
except ShangpinZhuanqu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '专区不存在'})
|
||
|
||
with transaction.atomic():
|
||
if delete_products:
|
||
Shangpin.query.filter(zhuanqu_id=zone_id).delete()
|
||
else:
|
||
Shangpin.query.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None)
|
||
zhuanqu.delete()
|
||
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
|
||
# 继续在 houtai/views_product.py 中添加
|
||
class GetRateView(APIView):
|
||
"""
|
||
获取打手分成费率(平台订单和商家订单)
|
||
权限:需要拥有 7007b
|
||
请求:POST /houtai/hthqfhpz
|
||
参数:{"username": "客服手机号"}
|
||
返回:{"code":0, "data": {"platform_rate": xx, "merchant_rate": xx}}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
if not username:
|
||
return Response({'code': 400, 'msg': '缺少username'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '7007b' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权查看费率'})
|
||
|
||
try:
|
||
platform_rate_obj = CommissionRate.query.get(Platform='1')
|
||
merchant_rate_obj = CommissionRate.query.get(Platform='3')
|
||
except CommissionRate.DoesNotExist as e:
|
||
logger.error(f"费率记录缺失: {e}")
|
||
return Response({'code': 500, 'msg': '费率配置缺失'})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'data': {
|
||
'platform_rate': float(platform_rate_obj.Rate) if platform_rate_obj.Rate else 0,
|
||
'merchant_rate': float(merchant_rate_obj.Rate) if merchant_rate_obj.Rate else 0,
|
||
}
|
||
})
|
||
|
||
|
||
class ModifyRateView(APIView):
|
||
"""
|
||
修改打手分成费率
|
||
权限:需要拥有 7007b
|
||
请求:POST /houtai/htxgfl
|
||
参数:{"username": "客服手机号", "type": "platform" / "merchant", "value": 费率(百分比数字)}
|
||
返回:{"code":0, "msg":"修改成功"}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
username = request.data.get('username', '').strip()
|
||
rate_type = request.data.get('type')
|
||
value = request.data.get('value')
|
||
|
||
if not username or not rate_type or value is None:
|
||
return Response({'code': 400, 'msg': '缺少参数'})
|
||
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions
|
||
if '7007b' not in permissions:
|
||
return Response({'code': 403, 'msg': '您无权修改费率'})
|
||
|
||
try:
|
||
rate_value = Decimal(str(value))
|
||
except:
|
||
return Response({'code': 400, 'msg': '费率值格式错误'})
|
||
|
||
|
||
if rate_type == 'platform':
|
||
Platform = '1'
|
||
elif rate_type == 'merchant':
|
||
Platform = '3'
|
||
else:
|
||
return Response({'code': 400, 'msg': '无效的type参数'})
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
rate_obj = CommissionRate.objects.select_for_update().get(Platform=Platform)
|
||
rate_obj.Rate = rate_value
|
||
rate_obj.save()
|
||
except CommissionRate.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '费率记录不存在'})
|
||
except Exception as e:
|
||
logger.error(f"修改费率失败: {e}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '修改失败'})
|
||
|
||
return Response({'code': 0, 'msg': '修改成功'})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# 权限码(根据你要求,使用 '8080a')
|
||
REQUIRED_PERMISSION = '8080a'
|
||
class PopupNoticeListAPIView(APIView):
|
||
"""
|
||
获取所有弹窗配置(页面 + 弹窗 + 图片)
|
||
URL: POST /houtai/hthqtcxx
|
||
请求参数: { "username": "客服账号" }
|
||
响应格式: { "code": 0, "data": { "pages": [...] } }
|
||
注意:所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段区分
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取前端传递的 username(用于防越权)
|
||
username_frontend = request.data.get('username')
|
||
# 2. 调用公共权限验证方法,返回 (客服对象, 权限列表)
|
||
kefu, permissions = verify_kefu_permission(request, username_frontend)
|
||
if kefu is None:
|
||
# 身份验证失败,返回业务码 403,但 HTTP 状态码仍是 200
|
||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||
|
||
# 3. 检查是否拥有所需权限码 '8080a'
|
||
if REQUIRED_PERMISSION not in permissions:
|
||
return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'})
|
||
|
||
# 4. 查询所有页面,并预加载关联的弹窗和图片(避免N+1查询)
|
||
from jituan.services.display_config import filter_popup_page_by_request, list_response_meta
|
||
|
||
pages = filter_popup_page_by_request(
|
||
PopupPage.query.all().prefetch_related('popups__images'),
|
||
request,
|
||
).order_by('id')
|
||
|
||
# 5. 序列化输出
|
||
serializer = PopupPageSerializer(pages, many=True)
|
||
return Response({
|
||
'code': 0,
|
||
'data': {'pages': serializer.data, **list_response_meta(request)},
|
||
})
|
||
|
||
|
||
class PopupNoticeModifyAPIView(APIView):
|
||
"""
|
||
统一修改接口:支持页面、弹窗、图片的增删改
|
||
URL: POST /houtai/htxgtcxx
|
||
请求中必须包含 action 字段,用于区分操作类型。
|
||
所有响应均使用默认 HTTP 200 状态码,业务成功/失败通过 code 字段体现。
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# ---------- 1. 权限验证 ----------
|
||
username_frontend = request.data.get('username')
|
||
kefu, permissions = verify_kefu_permission(request, username_frontend)
|
||
if kefu is None:
|
||
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||
if REQUIRED_PERMISSION not in permissions:
|
||
return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'})
|
||
|
||
from jituan.services.display_config import forbid_display_write_in_all_scope
|
||
deny = forbid_display_write_in_all_scope(request)
|
||
if deny:
|
||
return deny
|
||
|
||
# ---------- 2. 获取 action 并分发 ----------
|
||
action = request.data.get('action')
|
||
if not action:
|
||
return Response({'code': 400, 'msg': '缺少action参数'})
|
||
|
||
try:
|
||
if action == 'add_page':
|
||
return self.add_page(request)
|
||
elif action == 'update_page':
|
||
return self.update_page(request)
|
||
elif action == 'delete_page':
|
||
return self.delete_page(request)
|
||
elif action == 'add_popup':
|
||
return self.add_popup(request)
|
||
elif action == 'update_popup':
|
||
return self.update_popup(request)
|
||
elif action == 'delete_popup':
|
||
return self.delete_popup(request)
|
||
elif action == 'add_image':
|
||
return self.add_image(request)
|
||
elif action == 'update_image':
|
||
return self.update_image(request)
|
||
elif action == 'delete_image':
|
||
return self.delete_image(request)
|
||
else:
|
||
return Response({'code': 400, 'msg': f'未知的action: {action}'})
|
||
except Exception as e:
|
||
# 捕获未预期的异常,返回服务器错误
|
||
traceback.print_exc()
|
||
return Response({'code': 500, 'msg': f'服务器错误: {str(e)}'})
|
||
|
||
# ==================== 页面相关操作 ====================
|
||
|
||
def add_page(self, request):
|
||
"""添加新页面"""
|
||
from jituan.services.club_context import resolve_club_id_from_request
|
||
from jituan.services.display_config import forbid_display_write_in_all_scope
|
||
|
||
deny = forbid_display_write_in_all_scope(request)
|
||
if deny:
|
||
return deny
|
||
|
||
data = request.data
|
||
# 必填字段校验
|
||
if not data.get('page_key') or not data.get('name'):
|
||
return Response({'code': 400, 'msg': '缺少 page_key 或 name'})
|
||
club_id = resolve_club_id_from_request(request)
|
||
# 唯一性校验(同俱乐部内)
|
||
if PopupPage.query.filter(club_id=club_id, page_key=data['page_key']).exists():
|
||
return Response({'code': 400, 'msg': '页面标识已存在'})
|
||
|
||
page = PopupPage.query.create(
|
||
club_id=club_id,
|
||
page_key=data['page_key'],
|
||
name=data['name'],
|
||
description=data.get('description', ''),
|
||
is_active=data.get('is_active', True),
|
||
ignore_user_mute=data.get('ignore_user_mute', False)
|
||
)
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'id': page.id}})
|
||
|
||
def update_page(self, request):
|
||
"""更新页面信息"""
|
||
page_id = request.data.get('id')
|
||
if not page_id:
|
||
return Response({'code': 400, 'msg': '缺少页面id'})
|
||
try:
|
||
page = PopupPage.query.get(id=page_id)
|
||
except PopupPage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '页面不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, page)
|
||
if deny:
|
||
return deny
|
||
|
||
# 更新允许修改的字段
|
||
page.page_key = request.data.get('page_key', page.page_key)
|
||
page.name = request.data.get('name', page.name)
|
||
page.description = request.data.get('description', page.description)
|
||
page.is_active = request.data.get('is_active', page.is_active)
|
||
page.ignore_user_mute = request.data.get('ignore_user_mute', page.ignore_user_mute)
|
||
page.save()
|
||
return Response({'code': 0, 'msg': '更新成功'})
|
||
|
||
def delete_page(self, request):
|
||
"""删除页面,可级联删除其下所有弹窗和图片"""
|
||
page_id = request.data.get('id')
|
||
cascade = request.data.get('cascade_delete_popups', True) # 默认级联
|
||
if not page_id:
|
||
return Response({'code': 400, 'msg': '缺少页面id'})
|
||
try:
|
||
page = PopupPage.query.get(id=page_id)
|
||
except PopupPage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '页面不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope, forbid_display_write_in_all_scope
|
||
deny = forbid_display_write_in_all_scope(request) or forbid_popup_page_out_of_scope(request, page)
|
||
if deny:
|
||
return deny
|
||
|
||
if cascade:
|
||
# 级联删除:使用事务保证原子性
|
||
with transaction.atomic():
|
||
# 预加载 images 关联,避免循环内 N+1 访问 related manager
|
||
popups = page.popups.prefetch_related('images')
|
||
for popup in popups:
|
||
# 删除弹窗下的所有图片文件
|
||
for img in popup.images.all():
|
||
if img.image_url:
|
||
delete_from_oss(img.image_url)
|
||
popups.delete()
|
||
page.delete()
|
||
else:
|
||
# 非级联删除:如果页面下还有弹窗,则拒绝删除
|
||
if page.popups.exists():
|
||
return Response({'code': 400, 'msg': '该页面下存在弹窗,请先删除弹窗或使用级联删除'})
|
||
page.delete()
|
||
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
# ==================== 弹窗相关操作 ====================
|
||
|
||
def add_popup(self, request):
|
||
"""为指定页面添加新弹窗"""
|
||
data = request.data
|
||
page_id = data.get('page_id')
|
||
popup_id = data.get('popup_id')
|
||
if not page_id or not popup_id:
|
||
return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'})
|
||
try:
|
||
page = PopupPage.query.get(id=page_id)
|
||
except PopupPage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '页面不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, page)
|
||
if deny:
|
||
return deny
|
||
|
||
# 同一页面下 popup_id 必须唯一
|
||
if PopupConfig.query.filter(page=page, popup_id=popup_id).exists():
|
||
return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'})
|
||
|
||
popup = PopupConfig.query.create(
|
||
page=page,
|
||
popup_id=popup_id,
|
||
title=data.get('title', ''),
|
||
description=data.get('description', ''),
|
||
strategy_type=data.get('strategy_type', 'daily'),
|
||
max_count=data.get('max_count', 1),
|
||
reset_interval=data.get('reset_interval', 'day'),
|
||
duration=data.get('duration', 0),
|
||
force_even_muted=data.get('force_even_muted', False),
|
||
sort_order=data.get('sort_order', 0),
|
||
is_active=data.get('is_active', True),
|
||
start_time=data.get('start_time') or None,
|
||
end_time=data.get('end_time') or None
|
||
)
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'id': popup.id}})
|
||
|
||
def update_popup(self, request):
|
||
"""更新弹窗信息"""
|
||
popup_id = request.data.get('id')
|
||
if not popup_id:
|
||
return Response({'code': 400, 'msg': '缺少弹窗id'})
|
||
try:
|
||
popup = PopupConfig.query.get(id=popup_id)
|
||
except PopupConfig.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '弹窗不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, popup.page)
|
||
if deny:
|
||
return deny
|
||
|
||
# 更新弹窗字段
|
||
popup.popup_id = request.data.get('popup_id', popup.popup_id)
|
||
popup.title = request.data.get('title', popup.title)
|
||
popup.description = request.data.get('description', popup.description)
|
||
popup.strategy_type = request.data.get('strategy_type', popup.strategy_type)
|
||
popup.max_count = request.data.get('max_count', popup.max_count)
|
||
popup.reset_interval = request.data.get('reset_interval', popup.reset_interval)
|
||
popup.duration = request.data.get('duration', popup.duration)
|
||
popup.force_even_muted = request.data.get('force_even_muted', popup.force_even_muted)
|
||
popup.sort_order = request.data.get('sort_order', popup.sort_order)
|
||
popup.is_active = request.data.get('is_active', popup.is_active)
|
||
popup.start_time = request.data.get('start_time') or None
|
||
popup.end_time = request.data.get('end_time') or None
|
||
popup.save()
|
||
return Response({'code': 0, 'msg': '更新成功'})
|
||
|
||
def delete_popup(self, request):
|
||
"""删除弹窗,同时删除其下所有图片(包括OSS文件)"""
|
||
popup_id = request.data.get('id')
|
||
if not popup_id:
|
||
return Response({'code': 400, 'msg': '缺少弹窗id'})
|
||
try:
|
||
popup = PopupConfig.query.get(id=popup_id)
|
||
except PopupConfig.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '弹窗不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, popup.page)
|
||
if deny:
|
||
return deny
|
||
|
||
with transaction.atomic():
|
||
for img in popup.images.all():
|
||
if img.image_url:
|
||
delete_from_oss(img.image_url)
|
||
popup.images.all().delete()
|
||
popup.delete()
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
# ==================== 图片相关操作 ====================
|
||
|
||
def _upload_image_file(self, file_obj, page_key):
|
||
"""
|
||
内部方法:将图片上传到OSS指定目录
|
||
目录: a_long/tanchuang/tanchuang/
|
||
文件名: {page_key}_{时间戳}_{uuid8}.{ext}
|
||
返回: 相对路径(例如 a_long/tanchuang/tanchuang/dashouzhongxin_20250419120000_abc12345.jpg)
|
||
"""
|
||
# 验证图片格式和大小
|
||
valid, msg = validate_image(file_obj)
|
||
if not valid:
|
||
raise ValueError(msg)
|
||
|
||
# 生成唯一文件名
|
||
ext = os.path.splitext(file_obj.name)[1].lower()
|
||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
||
unique_id = str(uuid.uuid4())[:8]
|
||
filename = f"{page_key}_{timestamp}_{unique_id}{ext}"
|
||
relative_path = f"a_long/tanchuang/tanchuang/{filename}"
|
||
|
||
# 上传到OSS
|
||
url = upload_to_oss(file_obj, relative_path)
|
||
if not url:
|
||
raise ValueError("图片上传失败")
|
||
return relative_path
|
||
|
||
def add_image(self, request):
|
||
"""为指定弹窗添加图片(支持上传文件)"""
|
||
popup_id = request.data.get('popup_id')
|
||
if not popup_id:
|
||
return Response({'code': 400, 'msg': '缺少 popup_id'})
|
||
try:
|
||
popup = PopupConfig.query.get(id=popup_id)
|
||
except PopupConfig.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '弹窗不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, popup.page)
|
||
if deny:
|
||
return deny
|
||
|
||
# 获取上传的文件
|
||
file_obj = request.FILES.get('file')
|
||
if not file_obj:
|
||
return Response({'code': 400, 'msg': '未提供图片文件'})
|
||
|
||
try:
|
||
page_key = popup.page.page_key
|
||
relative_path = self._upload_image_file(file_obj, page_key)
|
||
except ValueError as e:
|
||
return Response({'code': 400, 'msg': str(e)})
|
||
|
||
# 保存图片记录
|
||
image = PopupImage.query.create(
|
||
popup_config=popup,
|
||
image_url=relative_path,
|
||
text=request.data.get('text', ''),
|
||
sort_order=request.data.get('sort_order', 0)
|
||
)
|
||
return Response({'code': 0, 'msg': '添加成功', 'data': {'id': image.id}})
|
||
|
||
def update_image(self, request):
|
||
"""更新图片信息(可替换图片文件、修改文字、排序)"""
|
||
image_id = request.data.get('id')
|
||
if not image_id:
|
||
return Response({'code': 400, 'msg': '缺少图片id'})
|
||
try:
|
||
image = PopupImage.query.get(id=image_id)
|
||
except PopupImage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '图片不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, image.popup_config.page)
|
||
if deny:
|
||
return deny
|
||
|
||
# 更新文字和排序
|
||
image.text = request.data.get('text', image.text)
|
||
image.sort_order = request.data.get('sort_order', image.sort_order)
|
||
|
||
# 如果提供了新图片文件,则替换
|
||
file_obj = request.FILES.get('file')
|
||
if file_obj:
|
||
try:
|
||
page_key = image.popup_config.page.page_key
|
||
new_path = self._upload_image_file(file_obj, page_key)
|
||
except ValueError as e:
|
||
return Response({'code': 400, 'msg': str(e)})
|
||
# 删除旧图片
|
||
if image.image_url:
|
||
delete_from_oss(image.image_url)
|
||
image.image_url = new_path
|
||
|
||
image.save()
|
||
return Response({'code': 0, 'msg': '更新成功'})
|
||
|
||
def delete_image(self, request):
|
||
"""删除图片(同时删除OSS文件)"""
|
||
image_id = request.data.get('id')
|
||
if not image_id:
|
||
return Response({'code': 400, 'msg': '缺少图片id'})
|
||
try:
|
||
image = PopupImage.query.get(id=image_id)
|
||
except PopupImage.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '图片不存在'})
|
||
|
||
from jituan.services.display_config import forbid_popup_page_out_of_scope
|
||
deny = forbid_popup_page_out_of_scope(request, image.popup_config.page)
|
||
if deny:
|
||
return deny
|
||
|
||
if image.image_url:
|
||
delete_from_oss(image.image_url)
|
||
image.delete()
|
||
return Response({'code': 0, 'msg': '删除成功'})
|
||
|
||
|
||
|