Files
Django/backend/views/shops.py
XingQue 6515568ec0 feat: 只看审核按俱乐部隔离,商品管理支持俱乐部视图
未绑店出货开关按 club_id 配置;后台商品列表可在具体小程序下管理并切换审核展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 03:20:03 +08:00

1045 lines
46 KiB
Python
Raw Permalink 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 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,
has_perm_code,
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']
SHOP_MANAGE_PERMS = ('999a', '999b', '999c', '999d', '999e')
class ShopListView(APIView):
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': '身份验证失败,请检查登录状态'})
# 2. 检查是否拥有所需权限999a~999e 任意一个)
if not has_perm_code(permissions, *SHOP_MANAGE_PERMS):
return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'})
# 3. 解析请求参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip()
zhuangtai = request.data.get('zhuangtai')
lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip()
user_id = request.data.get('user_id', '').strip()
ketixian_yue_min = request.data.get('ketixian_yue_min')
ketixian_yue_max = request.data.get('ketixian_yue_max')
zhengqu_zonge_min = request.data.get('zhengqu_zonge_min')
zhengqu_zonge_max = request.data.get('zhengqu_zonge_max')
# 4. 构建查询集(先查店铺,预加载关联凭证;按俱乐部隔离)
from jituan.services.club_context import filter_club_char_field
queryset = Dianpu.query.select_related('pingzheng').all()
queryset = filter_club_char_field(queryset, request, field='club_id')
if dianpu_mingcheng:
queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng)
if zhuangtai is not None:
queryset = queryset.filter(zhuangtai=zhuangtai)
if lianxi_dianhua:
queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua)
if user_id:
queryset = queryset.filter(pingzheng__yonghu=user_id)
# 金额范围过滤
if ketixian_yue_min is not None:
queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min)
if ketixian_yue_max is not None:
queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max)
if zhengqu_zonge_min is not None:
queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min)
if zhengqu_zonge_max is not None:
queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max)
# 5. 分页
paginator = Paginator(queryset, page_size)
page_obj = paginator.get_page(page)
# 6. 构造返回数据
shop_list = []
for dianpu in page_obj:
pingzheng = dianpu.pingzheng
shop_list.append({
'id': dianpu.id,
'dianpu_mingcheng': dianpu.dianpu_mingcheng,
'dianpu_touxiang': dianpu.dianpu_touxiang or '',
'lianxi_dianhua': dianpu.lianxi_dianhua or '',
'weixinhao': dianpu.weixinhao or '',
'zhuangtai': dianpu.zhuangtai,
'bangding_yonghushu': dianpu.bangding_yonghushu,
'erweima_url': dianpu.erweima_url or '',
'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng,
'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0,
'ketixian_yue': dianpu.ketixian_yue,
'zhengqu_zonge': dianpu.zhengqu_zonge,
'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0,
'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane,
'pingzheng__yonghu': pingzheng.yonghu, # 用户ID
'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号
'club_id': getattr(dianpu, 'club_id', '') or '',
})
# 获取默认配置ID=1不存在则创建
default_config, _ = DianpuMorenPeizhi.query.get_or_create(id=1, defaults={
'moren_choucheng_feilv': 0.1,
'moren_tixian_xiane': 1000.00
})
return Response({
'code': 0,
'msg': 'success',
'data': {
'list': shop_list,
'total': paginator.count,
'default_config': {
'moren_choucheng_feilv': default_config.moren_choucheng_feilv,
'moren_tixian_xiane': default_config.moren_tixian_xiane,
}
}
})
class ShopModifyView(APIView):
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': '身份验证失败,请检查登录状态'})
action = request.data.get('action', '')
# 2. 根据 action 分派任务
if action == 'update_dianpu':
return self._update_dianpu(request, permissions)
elif action == 'add_dianpu':
return self._add_dianpu(request, permissions)
elif action == 'update_default':
return self._update_default(request, permissions)
else:
return Response({'code': 400, 'msg': '未知操作类型'})
def _update_dianpu(self, request, permissions):
"""修改店铺信息,精确权限控制"""
dianpu_id = request.data.get('dianpu_id')
if not dianpu_id:
return Response({'code': 400, 'msg': '缺少店铺ID'})
try:
with transaction.atomic():
dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id)
# 基础信息修改 (999e)
if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']):
if not has_perm_code(permissions, '999e'):
return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'})
if 'dianpu_mingcheng' in request.data:
dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng']
if 'lianxi_dianhua' in request.data:
dianpu.lianxi_dianhua = request.data['lianxi_dianhua']
if 'weixinhao' in request.data:
dianpu.weixinhao = request.data['weixinhao']
# 店铺状态 (999a)
if 'zhuangtai' in request.data:
if not has_perm_code(permissions, '999a'):
return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'})
new_status = int(request.data['zhuangtai'])
if new_status not in (0, 1):
return Response({'code': 400, 'msg': '店铺状态无效'})
dianpu.zhuangtai = new_status
# 可提现余额 (999b)
if 'ketixian_yue' in request.data:
if not has_perm_code(permissions, '999b'):
return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'})
dianpu.ketixian_yue = request.data['ketixian_yue']
# 提现限额相关 (999c)
if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data:
if not has_perm_code(permissions, '999c'):
return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'})
if 'kaiqi_meiri_xiane' in request.data:
dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane']
if 'meiri_tixian_xiane' in request.data:
dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane']
# 抽成相关 (999d)
if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data:
if not has_perm_code(permissions, '999d'):
return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'})
if 'kaiqi_shouyi_choucheng' in request.data:
dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng']
if 'shouyi_choucheng_feilv' in request.data:
dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv']
dianpu.save() # Django 会自动判断更新哪些字段
return Response({'code': 0, 'msg': '店铺信息更新成功'})
except Dianpu.DoesNotExist:
return Response({'code': 404, 'msg': '店铺不存在'})
except Exception as e:
logger.exception("修改店铺失败")
return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
def _add_dianpu(self, request, permissions):
"""添加店铺,需要权限 999b"""
if not has_perm_code(permissions, '999b'):
return Response({'code': 403, 'msg': '无权限添加店铺(999b)'})
yonghu_id = request.data.get('yonghu_id')
zhanghao = request.data.get('zhanghao')
mima = request.data.get('mima')
dianpu_mingcheng = request.data.get('dianpu_mingcheng')
if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]):
return Response({'code': 400, 'msg': '必填字段缺失用户ID、账号、密码、店铺名称'})
# 检查用户是否存在
if not User.query.filter(UserUID=yonghu_id).exists():
return Response({'code': 404, 'msg': '用户不存在'})
# 检查凭证是否已存在
if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists():
return Response({'code': 400, 'msg': '登录账号已存在'})
if YonghuPingzheng.query.filter(yonghu=yonghu_id).exists():
return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'})
try:
with transaction.atomic():
pingzheng = YonghuPingzheng.query.create(
yonghu=yonghu_id,
zhanghao=zhanghao,
mima=mima,
is_active=True
)
from jituan.services.club_write import resolve_club_id_for_write
dianpu = Dianpu.query.create(
pingzheng=pingzheng,
dianpu_mingcheng=dianpu_mingcheng,
lianxi_dianhua=request.data.get('lianxi_dianhua', ''),
weixinhao=request.data.get('weixinhao', ''),
kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False),
shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0),
kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False),
meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0),
club_id=resolve_club_id_for_write(request),
)
return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id})
except IntegrityError as e:
logger.error("创建店铺违反唯一性约束: %s", e)
return Response({'code': 400, 'msg': '数据冲突请检查账号或用户ID'})
except Exception as e:
logger.exception("创建店铺失败")
return Response({'code': 500, 'msg': '创建失败,请稍后重试'})
def _update_default(self, request, permissions):
"""修改默认配置,按修改内容校验权限"""
feilv = request.data.get('moren_choucheng_feilv')
xiane = request.data.get('moren_tixian_xiane')
modify_commission = feilv is not None
modify_withdraw = xiane is not None
if modify_commission and not has_perm_code(permissions, '999d'):
return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'})
if modify_withdraw and not has_perm_code(permissions, '999c'):
return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'})
try:
config, _ = DianpuMorenPeizhi.query.get_or_create(id=1)
if modify_commission:
config.moren_choucheng_feilv = feilv
if modify_withdraw:
config.moren_tixian_xiane = xiane
config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) +
(['moren_tixian_xiane'] if modify_withdraw else []))
return Response({'code': 0, 'msg': '默认配置更新成功'})
except Exception as e:
logger.exception("更新默认配置失败")
return Response({'code': 500, 'msg': '更新失败'})
# ==================== 1. 公共商品类型 + 审核模式 ====================
class ShopPublicTypeAndAuditView(APIView):
"""
返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关
前端请求路径:/houtai/htdphqsplx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# ---------- 身份与权限校验 ----------
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
# 拥有 1199ab/1199abc/1199abd 任一个即可访问
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'})
# ---------- 查询公共商品类型(仅正常审核状态的) ----------
try:
# shenhezhuangtai = 1 表示正常 (根据模型默认值)
types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values(
'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin'
)
type_list = []
for t in types_qs:
type_list.append({
'id': t['id'],
'jieshao': t['jieshao'] or '',
'tupian_url': t['tupian_url'] or '',
'yaoqiuleixing': t['yaoqiuleixing'],
'huiyuan_id': t['huiyuan_id'],
'yongjin': t['yongjin'],
})
except Exception as e:
logger.exception("查询公共商品类型失败")
return Response({'code': 500, 'msg': '服务器错误,请稍后重试'})
# ---------- 获取本俱乐部审核模式配置 ----------
from jituan.services.club_write import resolve_club_id_for_write
from shop.services.shenhe_config import get_or_create_shenhe_config
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import resolve_club_scope
if resolve_club_scope(request) == DATA_SCOPE_ALL:
# 集团汇总:不自动创建,返回默认值提示前端切到具体俱乐部
return Response({
'code': 0,
'data': {
'public_types': type_list,
'kaiqi_shenhe': False,
'zhi_kan_shenhe': True,
'club_scoped': False,
'msg': '请切换到具体俱乐部查看/修改审核展示开关',
}
})
club_id = resolve_club_id_for_write(request)
audit_config = get_or_create_shenhe_config(club_id)
kaiqi_shenhe = audit_config.kaiqi_shenhe
zhi_kan_shenhe = bool(getattr(audit_config, 'zhi_kan_shenhe', True))
return Response({
'code': 0,
'data': {
'public_types': type_list,
'kaiqi_shenhe': kaiqi_shenhe,
'zhi_kan_shenhe': zhi_kan_shenhe,
'club_id': club_id,
'club_scoped': True,
}
})
# ==================== 2. 店铺列表(支持搜索、筛选、分页) ====================
class ShopListForProductView(APIView):
"""
返回店铺列表,用于商品管理页面选择店铺。
支持关键词搜索店铺ID/名称/用户ID、店铺状态筛选、分页。
前端请求路径:/houtai/htdphqdpys
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问店铺列表'})
# ---------- 安全解析分页参数 ----------
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数格式错误'})
# 搜索关键词字符串ORM 自动防注入)
keyword = str(request.data.get('keyword', '')).strip()
# 店铺状态筛选1正常0封禁不传则全部
zhuangtai = request.data.get('zhuangtai')
# ---------- 构建查询集关联凭证表获取用户ID按俱乐部隔离 ----------
from jituan.services.club_context import filter_club_char_field
shops = Dianpu.query.select_related('pingzheng').all()
shops = filter_club_char_field(shops, request, field='club_id')
# 状态筛选:只接受 0 或 1
if zhuangtai is not None:
try:
zhuangtai = int(zhuangtai)
if zhuangtai in [0, 1]:
shops = shops.filter(zhuangtai=zhuangtai)
else:
return Response({'code': 400, 'msg': '状态参数只能为0或1'})
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '状态参数必须为整数'})
# 关键词搜索店铺ID/名称/用户ID
if keyword:
shops = shops.filter(
models.Q(id__icontains=keyword) |
models.Q(dianpu_mingcheng__icontains=keyword) |
models.Q(pingzheng__yonghu__icontains=keyword)
)
# ---------- 分页 ----------
paginator = Paginator(shops, page_size)
page_obj = paginator.get_page(page)
shop_list = []
for shop in page_obj:
shop_list.append({
'id': shop.id,
'dianpu_mingcheng': shop.dianpu_mingcheng,
'zhuangtai': shop.zhuangtai,
'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '',
'lianxi_dianhua': shop.lianxi_dianhua or '',
'weixinhao': shop.weixinhao or '',
})
return Response({
'code': 0,
'data': {
'list': shop_list,
'total': paginator.count
}
})
# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ====================
class ShopProductTypeMappingView(APIView):
"""
返回店铺商品类型映射ShangpinLeixingDianpu列表。
支持店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。
每次请求同时返回“待审核总数”shenhe_zhuangtai=False 的记录数)。
前端请求路径:/houtai/htdphqyssplx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问商品类型映射'})
# ---------- 安全解析参数 ----------
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数格式错误'})
keyword = str(request.data.get('keyword', '')).strip()
shop_id = request.data.get('shop_id')
public_type_id = request.data.get('public_type_id')
# 布尔型筛选字段处理None 表示不筛选,否则转为布尔值
def parse_bool(val):
if val is None:
return None
return bool(val)
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
# ---------- 查询(连表店铺和公共类型) ----------
mappings = ShangpinLeixingDianpu.query.select_related('dianpu', 'gonggong_leixing').all()
# 筛选条件:均为参数化,完全防注入
if shop_id is not None:
mappings = mappings.filter(dianpu_id=int(shop_id))
if public_type_id is not None:
mappings = mappings.filter(gonggong_leixing_id=int(public_type_id))
if shangjia is not None:
mappings = mappings.filter(shangjia_zhuangtai=shangjia)
if fengjin is not None:
mappings = mappings.filter(fengjin_zhuangtai=fengjin)
if shenhe is not None:
mappings = mappings.filter(shenhe_zhuangtai=shenhe)
# 关键字搜索介绍、ID、店铺名、公共类型名
if keyword:
mappings = mappings.filter(
models.Q(jieshao__icontains=keyword) |
models.Q(id__icontains=keyword) |
models.Q(dianpu__dianpu_mingcheng__icontains=keyword) |
models.Q(gonggong_leixing__jieshao__icontains=keyword)
)
# ---------- 待审核总数(不受分页影响) ----------
pending_total = mappings.filter(shenhe_zhuangtai=False).count()
# ---------- 分页 ----------
paginator = Paginator(mappings, page_size)
page_obj = paginator.get_page(page)
mapping_list = []
for m in page_obj:
mapping_list.append({
'id': m.id,
'dianpu_id': m.dianpu_id,
'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '',
'gonggong_leixing_id': m.gonggong_leixing_id,
'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '',
'jieshao': m.jieshao,
'tupian_url': m.tupian_url or '',
'paixu': m.paixu,
'shangjia_zhuangtai': m.shangjia_zhuangtai,
'fengjin_zhuangtai': m.fengjin_zhuangtai,
'shenhe_zhuangtai': m.shenhe_zhuangtai,
})
return Response({
'code': 0,
'data': {
'list': mapping_list,
'total': paginator.count,
'pending_total': pending_total
}
})
# ==================== 统一修改接口(已修正事务) ====================
class ShopProductModifyView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
user_perms = permissions
action = request.data.get('action')
if action == 'update_audit_mode':
return self._update_audit_mode(request, user_perms)
elif action == 'update_zhi_kan_shenhe':
return self._update_zhi_kan_shenhe(request, user_perms)
elif action == 'update_mapping_status':
return self._update_mapping_status(request, user_perms)
else:
return Response({'code': 400, 'msg': '未知操作'})
@transaction.atomic # ✅ 事务修复点
def _update_audit_mode(self, request, user_perms):
if not has_perm_code(user_perms, '1199ab'):
return Response({'code': 403, 'msg': '无权限修改审核模式'})
kaiqi = request.data.get('kaiqi_shenhe')
if kaiqi is None:
return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'})
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import resolve_club_scope
from jituan.services.club_write import resolve_club_id_for_write
from shop.services.shenhe_config import get_or_create_shenhe_config
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return Response({'code': 400, 'msg': '请先切换到具体俱乐部再修改审核模式'})
try:
club_id = resolve_club_id_for_write(request)
config = get_or_create_shenhe_config(club_id)
config = DianpuShangpinShenheShezhi.objects.select_for_update().get(pk=config.pk)
config.kaiqi_shenhe = bool(kaiqi)
config.save(update_fields=['kaiqi_shenhe'])
return Response({'code': 0, 'msg': '审核模式已更新', 'data': {'club_id': club_id}})
except Exception as e:
logger.exception("更新审核模式失败")
return Response({'code': 500, 'msg': '服务器错误'})
@transaction.atomic
def _update_zhi_kan_shenhe(self, request, user_perms):
"""点单端:未绑店用户是否按身份只看审核专用商品(按俱乐部)。"""
if not has_perm_code(user_perms, '1199ab'):
return Response({'code': 403, 'msg': '无权限修改只看审核开关'})
val = request.data.get('zhi_kan_shenhe')
if val is None:
return Response({'code': 400, 'msg': '缺少参数 zhi_kan_shenhe'})
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import resolve_club_scope
from jituan.services.club_write import resolve_club_id_for_write
from shop.services.shenhe_config import get_or_create_shenhe_config
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return Response({'code': 400, 'msg': '请先切换到具体俱乐部再修改只看审核开关'})
try:
club_id = resolve_club_id_for_write(request)
config = get_or_create_shenhe_config(club_id)
config = DianpuShangpinShenheShezhi.objects.select_for_update().get(pk=config.pk)
config.zhi_kan_shenhe = bool(val)
config.save(update_fields=['zhi_kan_shenhe'])
return Response({'code': 0, 'msg': '只看审核开关已更新', 'data': {'club_id': club_id}})
except Exception:
logger.exception("更新只看审核开关失败")
return Response({'code': 500, 'msg': '服务器错误'})
@transaction.atomic
def _update_mapping_status(self, request, user_perms):
mapping_id = request.data.get('mapping_id')
if not mapping_id:
return Response({'code': 400, 'msg': '缺少 mapping_id'})
try:
mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id))
except (ShangpinLeixingDianpu.DoesNotExist, ValueError):
return Response({'code': 404, 'msg': '映射不存在'})
fengjin = request.data.get('fengjin_zhuangtai')
shangjia = request.data.get('shangjia_zhuangtai')
shenhe = request.data.get('shenhe_zhuangtai')
# 权限判断
if fengjin is not None:
if fengjin:
if not has_perm_code(user_perms, '1199abc'):
return Response({'code': 403, 'msg': '无权限封禁'})
else:
if not has_perm_code(user_perms, '1199abd'):
return Response({'code': 403, 'msg': '无权限解封'})
mapping.fengjin_zhuangtai = bool(fengjin)
if shangjia is not None:
if not shangjia:
if not has_perm_code(user_perms, '1199abc'):
return Response({'code': 403, 'msg': '无权限下架'})
else:
if not has_perm_code(user_perms, '1199abd'):
return Response({'code': 403, 'msg': '无权限上架'})
mapping.shangjia_zhuangtai = bool(shangjia)
if shenhe is not None:
if shenhe:
if not has_perm_code(user_perms, '1199abd'):
return Response({'code': 403, 'msg': '无权限审核通过'})
mapping.shenhe_zhuangtai = bool(shenhe)
mapping.save()
return Response({'code': 0, 'msg': '修改成功'})
# 商品管理相关权限组(拥有任意一个即可查看列表)
PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe']
class ProductListView(APIView):
"""
商品列表接口,支持多条件筛选与分页。
请求路径: /houtai/hqhtdpsplx
权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# ----- 身份与权限验证 -----
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not has_perm_code(permissions, *PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无商品管理权限'})
# ----- 安全解析分页与筛选参数 -----
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 15))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数格式错误'})
keyword = str(request.data.get('keyword', '')).strip()
shop_id = request.data.get('shop_id') # 店铺ID (可选)
shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选)
public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选)
# 布尔型状态筛选
def parse_bool(val):
if val is None:
return None
return bool(val)
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
# ----- 构建查询(预加载店铺和店铺商品类型) -----
products = Shangpin.query.select_related('dianpu', 'dianpu_leixing').all()
# 按店铺筛选
if shop_id is not None:
try:
products = products.filter(dianpu_id=int(shop_id))
except (ValueError, TypeError):
return Response({'code': 400, 'msg': 'shop_id 格式错误'})
# 按店铺商品类型映射筛选
if shop_product_type_id is not None:
try:
products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
except (ValueError, TypeError):
return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
# 按公共商品类型筛选leixing_id 存储的是公共类型的ID
if public_type_id is not None:
try:
products = products.filter(leixing_id=int(public_type_id))
except (ValueError, TypeError):
return Response({'code': 400, 'msg': 'public_type_id 格式错误'})
# 平台审核状态筛选 (BooleanField)
if shenhe is not None:
products = products.filter(shenhe_zhuangtai=shenhe)
# 上架状态筛选
if shangjia is not None:
products = products.filter(shangjia_zhuangtai=shangjia)
# 封禁状态筛选
if fengjin is not None:
products = products.filter(fengjin_zhuangtai=fengjin)
# 关键词搜索商品ID或商品标题搜索具有模糊性使用icontains
if keyword:
# 尝试纯数字可能是商品ID使用精确匹配与标题模糊搜索
products = products.filter(
models.Q(id__icontains=keyword) |
models.Q(biaoqian__icontains=keyword)
)
# ----- 获取待审核总数(平台审核未通过的数量) -----
pending_total = products.filter(shenhe_zhuangtai=False).count()
# ----- 分页 -----
paginator = Paginator(products, page_size)
page_obj = paginator.get_page(page)
# ----- 构造返回数据批量获取公共类型名称避免N+1 -----
# 提取所有公共商品类型ID
type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None}
public_type_map = {}
if type_ids:
public_types = ShangpinLeixing.query.filter(id__in=type_ids).values('id', 'jieshao')
public_type_map = {t['id']: t['jieshao'] for t in public_types}
product_list = []
for p in page_obj:
product_list.append({
'id': p.id,
'biaoqian': p.biaoqian or '',
'jiage': p.jiage,
'kucun': p.kucun,
'leixing_id': p.leixing_id,
'public_type_name': public_type_map.get(p.leixing_id, ''),
'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
'duiwai_xiaoliang': p.duiwai_xiaoliang,
'jieshao': p.jieshao or '',
'xiadan_xuzhi': p.xiadan_xuzhi or '',
'guize_tupian': p.guize_tupian or '',
'yaoqiuleixing': p.yaoqiuleixing,
'huiyuan_id': p.huiyuan_id or '',
'yongjin': p.yongjin,
'kaioi_ewai_PlayerCommission': p.kaioi_ewai_dashou_fencheng,
'ewai_PlayerCommission': p.ewai_dashou_fencheng,
'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '',
'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '',
'paixu': p.paixu,
'dianpu_id': p.dianpu_id,
'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '',
'dianpu_leixing_id': p.dianpu_leixing_id,
'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '',
'shangjia_zhuangtai': p.shangjia_zhuangtai,
'fengjin_zhuangtai': p.fengjin_zhuangtai,
'shenhe_zhuangtai': p.shenhe_zhuangtai,
})
return Response({
'code': 0,
'data': {
'list': product_list,
'total': paginator.count,
'pending_total': pending_total, # 待审核商品总数
}
})
class ProductModifyView(APIView):
"""
商品修改接口,根据修改内容细粒度鉴权。
请求路径: /houtai/htxgdpspsj
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
action = request.data.get('action', '')
if action != 'update_product':
return Response({'code': 400, 'msg': '未知操作'})
return self._update_product(request, permissions)
@transaction.atomic
def _update_product(self, request, permissions):
"""执行商品修改,严格校验权限"""
product_id = request.data.get('product_id')
if not product_id:
return Response({'code': 400, 'msg': '缺少商品ID'})
try:
# 行级锁防止并发修改
product = Shangpin.objects.select_for_update().get(id=int(product_id))
except (Shangpin.DoesNotExist, ValueError):
return Response({'code': 404, 'msg': '商品不存在'})
# 需要修改的字段
fields_to_update = {}
# 将要修改的状态标志用于后续权限判断
status_changes = {
'shangjia': request.data.get('shangjia_zhuangtai'),
'fengjin': request.data.get('fengjin_zhuangtai'),
'shenhe': request.data.get('shenhe_zhuangtai'),
}
# ---------- 权限分类检查 ----------
# 1. 封禁/下架操作 (消极操作) -> 1199abc
if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']):
if not has_perm_code(permissions, '1199abc'):
return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'})
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True:
if not has_perm_code(permissions, '1199abc'):
return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'})
# 2. 解封/上架/过审 (积极操作) -> 1199abd
if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']):
if not has_perm_code(permissions, '1199abd'):
return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'})
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False:
if not has_perm_code(permissions, '1199abd'):
return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'})
# 3. 修改其他数据(非状态字段)-> 1199abe
# 除了上架、封禁、审核之外的字段都属于“其他数据”
other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username',
'shangjia_zhuangtai', 'fengjin_zhuangtai',
'shenhe_zhuangtai']]
if other_fields and not has_perm_code(permissions, '1199abe'):
return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'})
# ---------- 应用修改 ----------
# 允许修改的字段白名单(防止注入)
# 前端参数名 → 模型字段名映射
field_name_map = {
'kaioi_ewai_PlayerCommission': 'kaioi_ewai_dashou_fencheng',
'ewai_PlayerCommission': 'ewai_dashou_fencheng',
}
allowed_fields = [
'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang',
'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin',
'kaioi_ewai_PlayerCommission', 'ewai_PlayerCommission',
'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai'
]
update_fields = []
for field in allowed_fields:
value = request.data.get(field)
if value is not None:
model_field = field_name_map.get(field, field)
# 简单类型校验(根据字段类型)
if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_PlayerCommission', 'paixu']:
try:
value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value)
except (ValueError, TypeError):
return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'})
setattr(product, model_field, value)
update_fields.append(model_field)
if update_fields:
product.save(update_fields=update_fields)
return Response({'code': 0, 'msg': '商品修改成功'})
class ShopCopyCatalogView(APIView):
"""
复制店铺商品目录(类型 + 专区 + 商品,含 OSS 图片独立拷贝)。
请求路径: /houtai/htfzdpspsj
权限: 店铺管理权限 999a~999e 任一
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
if not has_perm_code(permissions, *SHOP_MANAGE_PERMS):
return Response({'code': 403, 'msg': '您没有权限执行店铺商品复制操作'})
source_user_uid = str(request.data.get('source_user_uid', '')).strip()
target_user_uid = str(request.data.get('target_user_uid', '')).strip()
copy_mode = str(request.data.get('copy_mode', 'append')).strip().lower()
if not source_user_uid or not target_user_uid:
return Response({'code': 400, 'msg': '请填写源店铺与目标店铺的用户ID'})
from backend.services.shop_copy_service import copy_shop_catalog, ShopCopyError
try:
result = copy_shop_catalog(source_user_uid, target_user_uid, copy_mode)
return Response({
'code': 0,
'msg': (
f'复制成功:商品类型 {result["copied_types"]} 个,'
f'专区 {result["copied_zones"]} 个,商品 {result["copied_products"]}'
),
'data': result,
})
except ShopCopyError as e:
return Response({'code': e.code, 'msg': e.message})
except Exception:
logger.exception('复制店铺商品失败')
return Response({'code': 500, 'msg': '复制失败,请稍后重试'})