1156 lines
45 KiB
Python
1156 lines
45 KiB
Python
"""商品/会员/分红 数据查询视图。
|
|
|
|
原 products/views.py 查询类视图聚合:商品数据获取、商品详情、打手会员列表、
|
|
管事分红记录、管理员全量数据获取、会员类型/记录查询、组长分红记录。
|
|
"""
|
|
# ==================== 标准库 ====================
|
|
import os
|
|
import json
|
|
import time
|
|
import random
|
|
import string
|
|
import uuid
|
|
import hashlib
|
|
import requests
|
|
import logging
|
|
import decimal
|
|
from datetime import timedelta
|
|
from decimal import Decimal, InvalidOperation, ROUND_CEILING
|
|
import defusedxml.ElementTree as ET
|
|
|
|
# ==================== Django 内置 ====================
|
|
from django.conf import settings
|
|
from django.db import transaction, connection
|
|
from django.db.models import Q
|
|
from gvsdsdk.fluent import db, func, FQ
|
|
from django.shortcuts import render, get_object_or_404
|
|
from django.http import HttpResponse
|
|
from django.utils import timezone
|
|
|
|
# ==================== DRF 框架 ====================
|
|
from rest_framework.views import APIView, View
|
|
from rest_framework.response import Response
|
|
from rest_framework import permissions, status
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
|
from rest_framework.throttling import AnonRateThrottle
|
|
|
|
# ==================== 项目工具函数 ====================
|
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
|
from utils.money import format_yuan, quantize_yuan, yuan_to_fen
|
|
from backend.utils import (
|
|
update_guanshi_daily_by_action,
|
|
update_guanshi_xufei_daily,
|
|
update_zuzhang_daily_by_action
|
|
)
|
|
from users.fadan_fenhong_utils import process_fadan_fenhong
|
|
|
|
# ==================== 各App模型 ====================
|
|
from ..models import (
|
|
ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
|
|
Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
|
|
)
|
|
from users.models import (
|
|
AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
|
|
)
|
|
from users.business_models import User
|
|
|
|
from orders.models import CommissionRate
|
|
from config.models import Gonggao, Lunbo
|
|
|
|
from orders.models import Penalty # 用于罚款类型
|
|
from jituan.services.club_config import (
|
|
get_commission_rate,
|
|
get_commission_rate_object,
|
|
get_huiyuan_price,
|
|
get_huiyuan_fenchong,
|
|
)
|
|
from jituan.services.club_context import resolve_effective_club_id
|
|
from jituan.services.club_write import resolve_club_id_for_write
|
|
from jituan.services.club_user import get_payment_openid
|
|
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
|
from jituan.constants import CLUB_ID_DEFAULT
|
|
|
|
import traceback
|
|
|
|
# ==================== 日志实例 ====================
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ShangpinHuoquView(APIView):
|
|
"""
|
|
商品数据获取接口
|
|
一次性返回商品类型、商品专区、商品列表
|
|
"""
|
|
|
|
# 应用节流(限制频率)
|
|
throttle_classes = [AnonRateThrottle]
|
|
permission_classes = [AllowAny]
|
|
|
|
def post(self, request):
|
|
"""
|
|
处理POST请求,返回所有商品相关数据
|
|
"""
|
|
try:
|
|
logger.info("收到商品数据获取请求")
|
|
|
|
from jituan.services.club_context import resolve_effective_club_id
|
|
from jituan.services.club_shangpin_leixing import build_club_leixing_list_for_api
|
|
|
|
club_id = resolve_effective_club_id(request, getattr(request, 'user', None))
|
|
leixing_rows = build_club_leixing_list_for_api(club_id)
|
|
shangpinleixing_data = [
|
|
{
|
|
'id': row['id'],
|
|
'jieshao': row['jieshao'],
|
|
'tupian_url': row['tupian_url'],
|
|
'paixu': row['paixu'],
|
|
}
|
|
for row in leixing_rows
|
|
]
|
|
|
|
# 2. 查询商品专区(只查询可见类型下的专区,并排序)
|
|
# 先获取可见的类型ID列表
|
|
visible_leixing_ids = [leixing['id'] for leixing in shangpinleixing_data]
|
|
|
|
if visible_leixing_ids:
|
|
zhuanqu_queryset = ShangpinZhuanqu.query.filter(
|
|
leixing_id__in=visible_leixing_ids,
|
|
shenhezhuangtai=1,
|
|
club_id=club_id or CLUB_ID_DEFAULT,
|
|
).order_by('-paixu', 'id')
|
|
else:
|
|
zhuanqu_queryset = ShangpinZhuanqu.query.none()
|
|
|
|
shangpinzhuanqu_data = []
|
|
for zhuanqu in zhuanqu_queryset:
|
|
shangpinzhuanqu_data.append({
|
|
"id": zhuanqu.id,
|
|
"mingzi": zhuanqu.mingzi,
|
|
"leixing_id": zhuanqu.leixing_id,
|
|
"paixu": zhuanqu.paixu,
|
|
"tupian_url": getattr(zhuanqu, 'tupian_url', None) or '',
|
|
})
|
|
|
|
# 3. 查询商品列表(只查询可见专区下的商品,并排序)
|
|
# 先获取可见的专区ID列表
|
|
visible_zhuanqu_ids = [zhuanqu['id'] for zhuanqu in shangpinzhuanqu_data]
|
|
|
|
if visible_zhuanqu_ids:
|
|
# 注意:这里使用values()只获取指定字段,提高查询效率
|
|
shangpin_queryset = Shangpin.query.filter(
|
|
zhuanqu_id__in=visible_zhuanqu_ids,
|
|
shenhezhuangtai=1,
|
|
club_id=club_id or CLUB_ID_DEFAULT,
|
|
).values(
|
|
'id', 'biaoqian', 'jiage', 'leixing_id',
|
|
'zhuanqu_id', 'tupian_url', 'duiwai_xiaoliang', 'paixu'
|
|
).order_by('-paixu', 'id') # 先按paixu降序,再按id升序
|
|
|
|
# 将QuerySet转换为列表
|
|
shangpinliebiao_data = list(shangpin_queryset)
|
|
else:
|
|
shangpinliebiao_data = []
|
|
|
|
# 4. 构建返回数据
|
|
response_data = {
|
|
"shangpinleixing": shangpinleixing_data,
|
|
"shangpinzhuanqu": shangpinzhuanqu_data,
|
|
"shangpinliebiao": shangpinliebiao_data
|
|
}
|
|
|
|
logger.info(f"返回数据:类型{len(shangpinleixing_data)}个,"
|
|
f"专区{len(shangpinzhuanqu_data)}个,"
|
|
f"商品{len(shangpinliebiao_data)}个")
|
|
|
|
# 5. 返回响应
|
|
return Response(response_data, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
# 记录异常
|
|
logger.error(f"获取商品数据时发生错误:{str(e)}", exc_info=True)
|
|
|
|
# 返回错误响应
|
|
error_data = {
|
|
"error": "服务器内部错误",
|
|
"detail": "获取商品数据失败,请稍后重试"
|
|
}
|
|
return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
|
|
|
|
|
|
class ShangpinXiangqingHuoquView(APIView):
|
|
"""
|
|
商品详情接口 - 极简版
|
|
假设所有字段都存在,直接返回
|
|
"""
|
|
|
|
throttle_classes = [AnonRateThrottle]
|
|
permission_classes = [AllowAny]
|
|
|
|
def post(self, request):
|
|
try:
|
|
shangpin_id = request.data.get('shangpin_id')
|
|
|
|
if not shangpin_id:
|
|
return Response({
|
|
"code": 1,
|
|
"msg": "商品ID不能为空",
|
|
"data": None
|
|
}, status=400)
|
|
|
|
# 直接查询,数据量小,性能没问题
|
|
shangpin = Shangpin.query.filter(id=shangpin_id).first()
|
|
|
|
if not shangpin:
|
|
return Response({
|
|
"code": 2,
|
|
"msg": "商品不存在",
|
|
"data": None
|
|
}, status=404)
|
|
|
|
# 直接构建数据,不处理空值
|
|
data = {
|
|
"id": shangpin.id,
|
|
"tupian": shangpin.tupian_url,
|
|
"tupian_url": shangpin.tupian_url,
|
|
"biaoti": shangpin.biaoqian,
|
|
"biaoqian": shangpin.biaoqian,
|
|
"jiage": shangpin.jiage,
|
|
"jieshao": shangpin.jieshao or '',
|
|
"xiangqing": shangpin.jieshao,
|
|
"xiadanxuzhi": shangpin.xiadan_xuzhi,
|
|
"guize": shangpin.guize_tupian,
|
|
"duiwai_xiaoliang": shangpin.duiwai_xiaoliang or 0,
|
|
"xiaoliang": shangpin.duiwai_xiaoliang,
|
|
"kucun": shangpin.kucun,
|
|
"leixing_id": shangpin.leixing_id,
|
|
"zhuanqu_id": shangpin.zhuanqu_id,
|
|
"shi_zhuanpan": bool(getattr(shangpin, 'shi_zhuanpan', False)),
|
|
}
|
|
|
|
return Response({
|
|
"code": 0,
|
|
"msg": "success",
|
|
"data": data
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"接口异常: {e}")
|
|
return Response({
|
|
"code": 500,
|
|
"msg": "服务器错误",
|
|
"data": None
|
|
}, status=500)
|
|
|
|
|
|
|
|
|
|
|
|
class DashouHuiyuanList(APIView):
|
|
"""
|
|
获取打手会员商品列表
|
|
路径:/shangpin/dshyhq
|
|
请求方式:POST
|
|
权限:JWT认证
|
|
返回:会员商品列表,字段对应前端需求
|
|
"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
# 1. 检查用户是否是打手(通过反向查询)
|
|
try:
|
|
dashou = request.user.DashouProfile
|
|
except UserDashou.DoesNotExist:
|
|
logger.warning(f"用户{request.user.UserUID}不是打手,尝试访问打手接口")
|
|
return Response({
|
|
'code': 400,
|
|
'message': '用户不是打手,无法访问此接口'
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# 2. 检查打手账号状态
|
|
if dashou.zhanghaozhuangtai != 1:
|
|
logger.warning(f"打手{request.user.UserUID}账号状态异常: {dashou.zhanghaozhuangtai}")
|
|
return Response({
|
|
'code': 400,
|
|
'message': '账号状态异常,无法购买会员'
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# 3. 仅返回本俱乐部可售会员(club_huiyuan_price 已配置且启用)
|
|
from jituan.constants import CLUB_ID_DEFAULT
|
|
from jituan.models import ClubHuiyuanPrice
|
|
from decimal import Decimal as _Decimal
|
|
|
|
club_id = resolve_effective_club_id(request, request.user) or CLUB_ID_DEFAULT
|
|
huiyuan_queryset = list(Huiyuan.query.all().only(
|
|
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
|
|
).order_by('jiage'))
|
|
|
|
yonghuid = request.user.UserUID
|
|
_huiyuan_ids = [h.huiyuan_id for h in huiyuan_queryset]
|
|
|
|
# 批量预取 1:本俱乐部所有 ClubHuiyuanPrice(启用),按 huiyuan_id 索引
|
|
_club_price_map = {
|
|
cp.huiyuan_id: cp
|
|
for cp in ClubHuiyuanPrice.query.filter(
|
|
club_id=club_id,
|
|
huiyuan_id__in=_huiyuan_ids,
|
|
is_enabled=True,
|
|
)
|
|
} if _huiyuan_ids else {}
|
|
|
|
from jituan.services.member_recharge import can_purchase_trial
|
|
|
|
def _local_sellable(cp_row, is_trial):
|
|
"""单行 ClubHuiyuanPrice 计算 sellable/price/days(避免重复查询)。"""
|
|
if not cp_row:
|
|
return False, _Decimal('0'), 0
|
|
if is_trial:
|
|
if not cp_row.trial_enabled:
|
|
return False, _Decimal('0'), 0
|
|
price = _Decimal(str(cp_row.trial_price or 0))
|
|
days = int(cp_row.trial_days or 0)
|
|
if price <= 0 or days <= 0:
|
|
return False, _Decimal('0'), 0
|
|
return True, price, days
|
|
price = _Decimal(str(cp_row.jiage or 0))
|
|
days = int(cp_row.formal_days or 30)
|
|
if price <= 0:
|
|
return False, _Decimal('0'), 0
|
|
return True, price, days
|
|
|
|
def _local_can_trial(huiyuan_id):
|
|
"""与下单/履约共用 can_purchase_trial。"""
|
|
ok, _ = can_purchase_trial(yonghuid, huiyuan_id, club_id)
|
|
return ok
|
|
|
|
huiyuan_list = []
|
|
for huiyuan in huiyuan_queryset:
|
|
cp_row = _club_price_map.get(huiyuan.huiyuan_id)
|
|
sellable, club_price, formal_days = _local_sellable(cp_row, is_trial=False)
|
|
if not sellable or not club_price:
|
|
continue
|
|
trial_ok = _local_can_trial(huiyuan.huiyuan_id)
|
|
trial_sellable, trial_price, trial_days = _local_sellable(cp_row, is_trial=True)
|
|
# 远程新增:card_image / card_bg / is_bundle 字段
|
|
card_image = (cp_row.card_image or '').strip() if cp_row else ''
|
|
card_bg = (cp_row.card_bg or '').strip() if cp_row else ''
|
|
is_bundle = bool(getattr(huiyuan, 'is_bundle', False))
|
|
# 总会员(is_bundle)禁用体验会员
|
|
if is_bundle:
|
|
trial_sellable = False
|
|
trial_price = _Decimal('0')
|
|
trial_days = 0
|
|
trial_ok = False
|
|
huiyuan_list.append({
|
|
'id': huiyuan.huiyuan_id,
|
|
'mingzi': huiyuan.jieshao,
|
|
'jiage': format_yuan(club_price),
|
|
'formal_days': formal_days,
|
|
'jieshao': huiyuan.jtjieshao,
|
|
'is_bundle': is_bundle,
|
|
'trial_enabled': bool(trial_sellable),
|
|
'trial_price': format_yuan(trial_price) if trial_sellable else '0.00',
|
|
'trial_days': trial_days if trial_sellable else 0,
|
|
'can_buy_trial': bool(trial_ok and trial_sellable),
|
|
'card_image': card_image,
|
|
'card_bg': card_bg,
|
|
})
|
|
|
|
# 5. 返回数据
|
|
return Response({
|
|
'code': 200,
|
|
'message': '获取会员列表成功',
|
|
'data': huiyuan_list,
|
|
'club_id': club_id,
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"获取会员列表接口异常: {str(e)}", exc_info=True)
|
|
return Response({
|
|
'code': 500,
|
|
'message': '服务器内部错误,请稍后重试'
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GuanshiFenhongListAPI(APIView):
|
|
"""
|
|
管事分红记录查询接口
|
|
权限:需要JWT认证
|
|
请求方式:POST
|
|
分页参数:page, page_size
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
# 获取当前管事用户ID
|
|
yonghuid = request.user.UserUID
|
|
|
|
# 获取分页参数
|
|
page = int(request.data.get('page', 1))
|
|
page_size = int(request.data.get('page_size', 50))
|
|
|
|
if page < 1:
|
|
page = 1
|
|
if page_size < 1 or page_size > 100: # 限制最大100条
|
|
page_size = 50
|
|
|
|
# 计算偏移量
|
|
offset = (page - 1) * page_size
|
|
|
|
# 查询条件:管事ID匹配
|
|
query = Q(guanshi=yonghuid)
|
|
|
|
# 先查询总数(使用分页查询优化)
|
|
|
|
# 查询总记录数
|
|
zong_tiaoshu = Gsfenhong.query.filter(query).count()
|
|
|
|
# 查询去重后的打手数量
|
|
chongzhi_dashou_shuliang = Gsfenhong.query.filter(query).values('dashouid').distinct().count()
|
|
|
|
# 查询当前页数据,按创建时间倒序排列
|
|
fenhong_list = Gsfenhong.query.filter(query) \
|
|
.order_by('-CreateTime') \
|
|
.values('dashouid', 'avatar', 'nicheng', 'fenhong', 'CreateTime')[offset:offset + page_size]
|
|
|
|
# 格式化数据
|
|
formatted_list = []
|
|
for item in fenhong_list:
|
|
formatted_list.append({
|
|
'yonghuid': item['dashouid'], # 打手ID
|
|
'avatar': item['avatar'] or '', # 打手头像
|
|
'nicheng': item['nicheng'] or '未知用户', # 打手昵称
|
|
'fenhong': str(item['fenhong']), # 分红金额
|
|
'CreateTime': item['CreateTime'].strftime('%Y-%m-%d %H:%M:%S') if item['CreateTime'] else ''
|
|
})
|
|
|
|
# 返回数据
|
|
return Response({
|
|
'code': 200,
|
|
'msg': '成功',
|
|
'data': {
|
|
'list': formatted_list,
|
|
'total': zong_tiaoshu, # 总记录数
|
|
'chongzhi_dashou_shuliang': chongzhi_dashou_shuliang, # 充值打手数量(去重)
|
|
'page': page,
|
|
'page_size': page_size,
|
|
'has_more': len(formatted_list) == page_size # 是否还有更多数据
|
|
}
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"查询管事分红记录失败: {str(e)}", exc_info=True)
|
|
return Response({
|
|
'code': 500,
|
|
'msg': f'服务器错误: {str(e)}',
|
|
'data': {}
|
|
})
|
|
|
|
|
|
FENHONG_LEIXING_LABELS = {
|
|
1: '会员分红',
|
|
2: '押金分红',
|
|
3: '订单分佣',
|
|
}
|
|
|
|
|
|
class GuanshiFenhongListV2API(APIView):
|
|
"""
|
|
管事分红明细 v2(旧 fenhonghq 不动)。
|
|
POST /shangpin/guanshi-fenhong-v2
|
|
- 列表带类型/说明/订单号/双时间字段
|
|
- 可选 fenhong_leixing 筛选;分项统计始终按管事全量汇总
|
|
"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
from django.db.models import Sum, Count
|
|
|
|
yonghuid = request.user.UserUID
|
|
page = int(request.data.get('page', 1) or 1)
|
|
page_size = int(request.data.get('page_size', 50) or 50)
|
|
if page < 1:
|
|
page = 1
|
|
if page_size < 1 or page_size > 100:
|
|
page_size = 50
|
|
|
|
leixing_raw = request.data.get('fenhong_leixing', None)
|
|
leixing_filter = None
|
|
if leixing_raw is not None and str(leixing_raw).strip() != '':
|
|
try:
|
|
leixing_filter = int(leixing_raw)
|
|
except (TypeError, ValueError):
|
|
return Response({'code': 400, 'msg': 'fenhong_leixing 无效', 'data': {}})
|
|
if leixing_filter not in FENHONG_LEIXING_LABELS:
|
|
return Response({'code': 400, 'msg': 'fenhong_leixing 无效', 'data': {}})
|
|
|
|
base_qs = Gsfenhong.objects.filter(guanshi=yonghuid)
|
|
|
|
# 分项统计:始终全量(不被列表筛选缩小)
|
|
amount_by = {'1': '0.00', '2': '0.00', '3': '0.00'}
|
|
count_by = {'1': 0, '2': 0, '3': 0}
|
|
total_amount = Decimal('0.00')
|
|
for row in base_qs.values('fenhong_leixing').annotate(
|
|
amount=Sum('fenhong'), cnt=Count('id'),
|
|
):
|
|
key = str(int(row.get('fenhong_leixing') or 1))
|
|
amt = Decimal(str(row.get('amount') or 0)).quantize(Decimal('0.01'))
|
|
cnt = int(row.get('cnt') or 0)
|
|
if key in amount_by:
|
|
amount_by[key] = str(amt)
|
|
count_by[key] = cnt
|
|
total_amount += amt
|
|
|
|
list_qs = base_qs
|
|
if leixing_filter is not None:
|
|
list_qs = list_qs.filter(fenhong_leixing=leixing_filter)
|
|
|
|
zong_tiaoshu = list_qs.count()
|
|
chongzhi_dashou_shuliang = base_qs.values('dashouid').distinct().count()
|
|
offset = (page - 1) * page_size
|
|
rows = list(
|
|
list_qs.order_by('-CreateTime')
|
|
.values(
|
|
'dingdan_id', 'dashouid', 'avatar', 'nicheng', 'fenhong',
|
|
'shuoming', 'fenhong_leixing', 'CreateTime',
|
|
)[offset:offset + page_size]
|
|
)
|
|
|
|
formatted_list = []
|
|
for item in rows:
|
|
leixing = int(item.get('fenhong_leixing') or 1)
|
|
ts = ''
|
|
if item.get('CreateTime'):
|
|
ts = item['CreateTime'].strftime('%Y-%m-%d %H:%M:%S')
|
|
formatted_list.append({
|
|
'yonghuid': item.get('dashouid') or '',
|
|
'avatar': item.get('avatar') or '',
|
|
'nicheng': item.get('nicheng') or '未知用户',
|
|
'fenhong': str(item.get('fenhong') or '0'),
|
|
'CreateTime': ts,
|
|
'create_time': ts,
|
|
'fenhong_leixing': leixing,
|
|
'leixing_label': FENHONG_LEIXING_LABELS.get(leixing, '分红'),
|
|
'shuoming': (item.get('shuoming') or '').strip(),
|
|
'dingdan_id': item.get('dingdan_id') or '',
|
|
})
|
|
|
|
return Response({
|
|
'code': 200,
|
|
'msg': '成功',
|
|
'data': {
|
|
'list': formatted_list,
|
|
'total': zong_tiaoshu,
|
|
'chongzhi_dashou_shuliang': chongzhi_dashou_shuliang,
|
|
'page': page,
|
|
'page_size': page_size,
|
|
'has_more': len(formatted_list) == page_size,
|
|
'total_amount': str(total_amount.quantize(Decimal('0.01'))),
|
|
'amount_by_leixing': amount_by,
|
|
'count_by_leixing': count_by,
|
|
},
|
|
})
|
|
except Exception as e:
|
|
logger.error('查询管事分红记录 v2 失败: %s', e, exc_info=True)
|
|
return Response({
|
|
'code': 500,
|
|
'msg': f'服务器错误: {str(e)}',
|
|
'data': {},
|
|
})
|
|
|
|
|
|
# views.py - 商品模块视图
|
|
class AdGetAllDataView(APIView):
|
|
"""
|
|
获取商品类型、商品专区、会员数据的接口
|
|
接口地址:/shangpin/adhqlx
|
|
请求方法:POST
|
|
权限要求:JWT Token认证 + 管理员权限
|
|
"""
|
|
|
|
# 需要JWT Token认证
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
"""
|
|
处理POST请求,返回商品相关数据
|
|
"""
|
|
try:
|
|
# 1. 获取请求数据
|
|
zhanghao = request.data.get('zhanghao')
|
|
|
|
if not zhanghao:
|
|
logger.warning(f"管理员接口:未接收到账号参数,请求用户:{request.user.UserUID}")
|
|
return Response({
|
|
'code': 400,
|
|
'message': '缺少账号参数',
|
|
'data': None
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# 2. 获取当前登录用户
|
|
current_user = request.user
|
|
|
|
# 3. 验证用户类型是否为管理员
|
|
if current_user.UserType != 'admin':
|
|
logger.warning(
|
|
f"权限验证失败:用户 {current_user.UserUID} 的用户类型为 {current_user.UserType},非管理员")
|
|
return Response({
|
|
'code': 401,
|
|
'message': '无管理员权限',
|
|
'data': None
|
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
|
|
|
# 4. 验证手机号与传递的账号是否一致
|
|
if current_user.Phone != zhanghao:
|
|
logger.warning(
|
|
f"账号验证失败:用户 {current_user.UserUID} 的phone={current_user.Phone},传递的zhanghao={zhanghao}")
|
|
return Response({
|
|
'code': 401,
|
|
'message': '账号验证失败',
|
|
'data': None
|
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
|
|
|
# 5. 验证管理员扩展表是否存在
|
|
try:
|
|
admin_profile = current_user.AdminProfile
|
|
logger.info(f"管理员验证通过:用户 {current_user.UserUID},手机号 {current_user.Phone}")
|
|
except AdminProfile.DoesNotExist:
|
|
logger.error(
|
|
f"管理员扩展表不存在:用户 {current_user.UserUID} 虽然user_type=admin,但没有AdminProfile记录")
|
|
return Response({
|
|
'code': 401,
|
|
'message': '管理员信息不完整',
|
|
'data': None
|
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
|
|
|
# 6. 查询商品类型数据
|
|
try:
|
|
# 优化:使用 .values() 直接提取字段,避免模型实例化开销(字典表 <10 条)
|
|
shangpinleixing_rows = ShangpinLeixing.query.all().values(
|
|
'id', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'jieshao', 'tupian_url'
|
|
)
|
|
|
|
# 转换为前端需要的格式
|
|
shangpinleixing_list = []
|
|
for row in shangpinleixing_rows:
|
|
leixing_data = {
|
|
'id': row['id'],
|
|
'yaoqiuleixing': row['yaoqiuleixing'],
|
|
'huiyuan_id': row['huiyuan_id'] or None,
|
|
'yongjin': str(row['yongjin']) if row['yongjin'] is not None else None,
|
|
'jieshao': row['jieshao'] or '',
|
|
'tupian_url': row['tupian_url'] or ''
|
|
}
|
|
shangpinleixing_list.append(leixing_data)
|
|
|
|
#logger.info(f"成功查询到 {len(shangpinleixing_list)} 条商品类型数据")
|
|
|
|
except Exception as e:
|
|
logger.error(f"查询商品类型数据时出错:{str(e)}")
|
|
shangpinleixing_list = []
|
|
|
|
# 7. 查询商品专区数据
|
|
try:
|
|
# 优化:使用 .values() 直接提取字段(字典表 <50 条)
|
|
zhuanqu_rows = ShangpinZhuanqu.query.all().values('id', 'mingzi', 'leixing_id')
|
|
|
|
# 转换为前端需要的格式
|
|
zhuanqu_list = []
|
|
for row in zhuanqu_rows:
|
|
zhuanqu_data = {
|
|
'id': row['id'],
|
|
'mingzi': row['mingzi'] or '',
|
|
'leixing_id': row['leixing_id']
|
|
}
|
|
zhuanqu_list.append(zhuanqu_data)
|
|
|
|
# logger.info(f"成功查询到 {len(zhuanqu_list)} 条商品专区数据")
|
|
|
|
except Exception as e:
|
|
logger.error(f"查询商品专区数据时出错:{str(e)}")
|
|
zhuanqu_list = []
|
|
|
|
# 8. 查询会员数据
|
|
try:
|
|
# 查询所有会员
|
|
huiyuan_queryset = Huiyuan.query.all()
|
|
|
|
# 转换为前端需要的格式
|
|
huiyuan_list = []
|
|
for huiyuan in huiyuan_queryset:
|
|
huiyuan_data = {
|
|
'huiyuan_id': huiyuan.huiyuan_id,
|
|
'jieshao': huiyuan.jieshao,
|
|
'jiage': str(huiyuan.jiage) # Decimal转为字符串
|
|
}
|
|
huiyuan_list.append(huiyuan_data)
|
|
|
|
# logger.info(f"成功查询到 {len(huiyuan_list)} 条会员数据")
|
|
|
|
except Exception as e:
|
|
logger.error(f"查询会员数据时出错:{str(e)}")
|
|
huiyuan_list = []
|
|
|
|
# 9. 检查会员列表是否为空
|
|
if not huiyuan_list:
|
|
logger.warning("会员列表为空,可能会影响商品发布功能")
|
|
|
|
# 10. 组织返回数据
|
|
response_data = {
|
|
'code': 0,
|
|
'message': '获取数据成功',
|
|
'data': {
|
|
'shangpinleixingList': shangpinleixing_list,
|
|
'zhuanquList': zhuanqu_list,
|
|
'huiyuanList': huiyuan_list
|
|
}
|
|
}
|
|
|
|
return Response(response_data, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
# 捕获未预期的异常
|
|
logger.error(f"接口处理异常:{str(e)}", exc_info=True)
|
|
return Response({
|
|
'code': 500,
|
|
'message': f'服务器内部错误:{str(e)}',
|
|
'data': None
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
# views.py - shangpin模块
|
|
|
|
|
|
class HuiyuanTypeListView(APIView):
|
|
"""
|
|
获取会员类型列表接口
|
|
路径:/shangpin/adhyjllxcx
|
|
方法:POST
|
|
权限:JWT Token + 管理员验证
|
|
"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
# 🔥 1. 身份验证
|
|
user = request.user
|
|
|
|
# 检查是否是管理员
|
|
if not hasattr(user, 'AdminProfile'):
|
|
return Response({
|
|
'code': 401,
|
|
'message': '无权限访问',
|
|
'data': None
|
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
|
|
|
# 🔥 2. 获取所有会员类型
|
|
# 优化:使用 .values() 直接提取字段,避免模型实例化开销
|
|
huiyuan_rows = Huiyuan.query.all().order_by('CreateTime').values(
|
|
'huiyuan_id', 'jieshao', 'jiage', 'goumai_cishu', 'CreateTime'
|
|
)
|
|
|
|
# 🔥 3. 构建返回数据
|
|
huiyuan_data = []
|
|
for row in huiyuan_rows:
|
|
huiyuan_data.append({
|
|
'id': row['huiyuan_id'], # 会员ID
|
|
'jieshao': row['jieshao'], # 会员介绍
|
|
'jiage': str(row['jiage']), # 价格
|
|
'goumai_cishu': row['goumai_cishu'], # 购买次数
|
|
'CreateTime': row['CreateTime'].strftime('%Y-%m-%d %H:%M:%S') if row['CreateTime'] else None
|
|
})
|
|
|
|
return Response({
|
|
'code': 0,
|
|
'message': '获取成功',
|
|
'data': huiyuan_data
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
print(f"获取会员类型列表失败: {str(e)}")
|
|
return Response({
|
|
'code': 500,
|
|
'message': '系统错误',
|
|
'data': None
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
|
|
class HuiyuanRecordListView(APIView):
|
|
"""
|
|
获取会员记录列表接口
|
|
路径:/shangpin/adhyjlcxhq
|
|
方法:POST
|
|
权限:JWT Token + 管理员验证
|
|
参数:
|
|
- zhanghao: 管理员账号
|
|
- huiyuan_type: 会员类型ID
|
|
- jiluzhuangtai: 记录状态(1=未过期, 2=已过期)
|
|
- page: 页码
|
|
- pagesize: 每页条数
|
|
"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
try:
|
|
# 🔥 1. 身份验证
|
|
user = request.user
|
|
|
|
# 检查是否是管理员
|
|
if not hasattr(user, 'AdminProfile'):
|
|
return Response({
|
|
'code': 401,
|
|
'message': '无权限访问',
|
|
'data': None
|
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
|
|
|
# 🔥 2. 获取参数
|
|
zhanghao = request.data.get('zhanghao', '').strip()
|
|
huiyuan_type = request.data.get('huiyuan_type', '').strip()
|
|
jiluzhuangtai = request.data.get('jiluzhuangtai', 1) # 默认未过期
|
|
page = int(request.data.get('page', 1))
|
|
pagesize = int(request.data.get('pagesize', 5))
|
|
|
|
# 🔥 3. 参数验证
|
|
if not zhanghao:
|
|
return Response({
|
|
'code': 400,
|
|
'message': '管理员账号不能为空',
|
|
'data': None
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if not huiyuan_type:
|
|
return Response({
|
|
'code': 400,
|
|
'message': '会员类型不能为空',
|
|
'data': None
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# 🔥 4. 检查会员类型是否存在
|
|
huiyuan_obj = Huiyuan.query.filter(huiyuan_id=huiyuan_type).first()
|
|
if not huiyuan_obj:
|
|
return Response({
|
|
'code': 404,
|
|
'message': '会员类型不存在',
|
|
'data': None
|
|
}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
# 🔥 5. 构建基础查询(一次性获取所有符合条件的记录ID)
|
|
now = timezone.now()
|
|
|
|
# 🔥 使用原生SQL优化查询(避免N+1问题)
|
|
with connection.cursor() as cursor:
|
|
# 先查询符合条件的总记录数
|
|
count_sql = """
|
|
SELECT COUNT(*)
|
|
FROM huiyuangoumai hg
|
|
WHERE hg.huiyuan_id = %s
|
|
"""
|
|
params = [huiyuan_type]
|
|
|
|
if jiluzhuangtai == 1: # 未过期
|
|
count_sql += " AND hg.daoqi_time > %s"
|
|
params.append(now)
|
|
else: # 已过期
|
|
count_sql += " AND hg.daoqi_time <= %s"
|
|
params.append(now)
|
|
|
|
cursor.execute(count_sql, params)
|
|
total_count = cursor.fetchone()[0]
|
|
|
|
# 🔥 6. 分页查询(使用窗口函数提高性能)
|
|
offset = (page - 1) * pagesize
|
|
|
|
record_sql = """
|
|
SELECT
|
|
hg.id,
|
|
hg.yonghu_id,
|
|
hg.huiyuan_id,
|
|
hg.jieshao,
|
|
hg.daoqi_time,
|
|
hg.CreateTime,
|
|
um.avatar,
|
|
ud.nicheng
|
|
FROM huiyuangoumai hg
|
|
LEFT JOIN user_main um ON hg.yonghu_id = um.yonghuid
|
|
LEFT JOIN user_dashou ud ON um.id = ud.user_id
|
|
WHERE hg.huiyuan_id = %s
|
|
"""
|
|
|
|
record_params = [huiyuan_type]
|
|
|
|
if jiluzhuangtai == 1: # 未过期
|
|
record_sql += " AND hg.daoqi_time > %s"
|
|
record_params.append(now)
|
|
else: # 已过期
|
|
record_sql += " AND hg.daoqi_time <= %s"
|
|
record_params.append(now)
|
|
|
|
record_sql += " ORDER BY hg.CreateTime DESC LIMIT %s OFFSET %s"
|
|
record_params.extend([pagesize, offset])
|
|
|
|
cursor.execute(record_sql, record_params)
|
|
records = cursor.fetchall()
|
|
|
|
# 🔥 7. 处理查询结果
|
|
record_list = []
|
|
for record in records:
|
|
record_list.append({
|
|
'id': record[0],
|
|
'yonghuid': record[1], # 用户ID
|
|
'huiyuan_id': record[2],
|
|
'jieshao': record[3],
|
|
'daoqi_time': record[4].strftime('%Y-%m-%d %H:%M:%S') if record[4] else None,
|
|
'CreateTime': record[5].strftime('%Y-%m-%d %H:%M:%S') if record[5] else None,
|
|
'touxiang': record[6], # 头像相对URL
|
|
'nicheng': record[7] or record[1], # 昵称或用户ID
|
|
'yonghu_type': 2 # 🔥 默认打手类型(实际应该根据user_type判断)
|
|
})
|
|
|
|
# 🔥 8. 判断是否还有更多数据
|
|
has_more = (page * pagesize) < total_count
|
|
|
|
# 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数)
|
|
# 优化:原实现为 N+1 查询(外层遍历 Huiyuan 全表 + 内层每条记录执行一次 COUNT SQL)
|
|
# 改为单条 LEFT JOIN + GROUP BY 聚合,将 N+1 次查询降为 1 次
|
|
# 注意:Huiyuangoumai.huiyuan_id 是 CharField 而非 ForeignKey,无法用 ORM annotate
|
|
stats_sql = """
|
|
SELECT
|
|
h.huiyuan_id,
|
|
h.jieshao,
|
|
COUNT(hg.id) AS count
|
|
FROM huiyuan h
|
|
LEFT JOIN huiyuangoumai hg
|
|
ON hg.huiyuan_id = h.huiyuan_id
|
|
"""
|
|
stats_params = []
|
|
if jiluzhuangtai == 1: # 未过期
|
|
# 条件放在 ON 子句而非 WHERE,确保无购买记录的会员类型仍返回 count=0
|
|
stats_sql += " AND hg.daoqi_time > %s"
|
|
stats_params.append(now)
|
|
else: # 已过期
|
|
stats_sql += " AND hg.daoqi_time <= %s"
|
|
stats_params.append(now)
|
|
stats_sql += " GROUP BY h.huiyuan_id, h.jieshao"
|
|
|
|
with connection.cursor() as cursor:
|
|
cursor.execute(stats_sql, stats_params)
|
|
stats_rows = cursor.fetchall()
|
|
|
|
huiyuan_stats = [
|
|
{
|
|
'id': row[0],
|
|
'jieshao': row[1],
|
|
'count': row[2]
|
|
}
|
|
for row in stats_rows
|
|
]
|
|
|
|
return Response({
|
|
'code': 0,
|
|
'message': '获取成功',
|
|
'data': {
|
|
'list': record_list,
|
|
'total': total_count,
|
|
'current_page': page,
|
|
'page_size': pagesize,
|
|
'has_more': has_more,
|
|
'huiyuan_stats': huiyuan_stats, # 🔥 所有会员类型的统计
|
|
'current_huiyuan': {
|
|
'id': huiyuan_obj.huiyuan_id,
|
|
'jieshao': huiyuan_obj.jieshao,
|
|
'count': total_count
|
|
}
|
|
}
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
except ValueError as e:
|
|
print(f"参数错误: {str(e)}")
|
|
return Response({
|
|
'code': 400,
|
|
'message': '参数错误',
|
|
'data': None
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
except Exception as e:
|
|
print(f"获取会员记录失败: {str(e)}")
|
|
traceback.print_exc()
|
|
return Response({
|
|
'code': 500,
|
|
'message': '系统错误',
|
|
'data': None
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ZuzhangFenhongView(APIView):
|
|
"""
|
|
组长分红记录接口
|
|
路径:/shangpin/zuzhangfenhong
|
|
方法:POST
|
|
权限:JWT认证,仅限组长身份
|
|
请求参数:
|
|
{
|
|
"page": 1, # 页码,默认1
|
|
"page_size": 5 # 每页条数,默认5
|
|
}
|
|
返回:
|
|
{
|
|
"code": 200,
|
|
"msg": "success",
|
|
"data": {
|
|
"fenhong_zonge": "1234.56", # 分红总额
|
|
"yaoqing_guanshi": 10, # 邀请管事数
|
|
"fenhong_cishu": 50, # 分红次数
|
|
"total": 50, # 总记录数
|
|
"has_more": false, # 是否有下一页
|
|
"list": [...] # 记录列表
|
|
}
|
|
}
|
|
"""
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
current_user = request.user
|
|
|
|
# 1. 验证组长身份
|
|
try:
|
|
zuzhang = UserZuzhang.query.get(user=current_user)
|
|
except UserZuzhang.DoesNotExist:
|
|
return Response({
|
|
'code': 403,
|
|
'msg': '当前用户不是组长身份',
|
|
'data': None
|
|
}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
# 2. 请求参数
|
|
page = int(request.data.get('page', 1))
|
|
page_size = int(request.data.get('page_size', 5))
|
|
page = max(page, 1)
|
|
page_size = max(page_size, 1)
|
|
|
|
# 3. 统计数据
|
|
# 3.1 分红总额(从组长扩展表)
|
|
fenhong_zonge = zuzhang.fenyong_zonge
|
|
|
|
# 3.2 邀请管事数(统计邀请人等于当前用户ID的管事记录)
|
|
yaoqing_guanshi = UserGuanshi.query.filter(yaoqingren=current_user.UserUID).count()
|
|
|
|
# 3.3 分红次数(总记录数)
|
|
base_queryset = Gsfenhong.query.filter(zuzhang_id=current_user.UserUID).order_by('-CreateTime')
|
|
fenhong_cishu = base_queryset.count()
|
|
|
|
# 4. 分页查询
|
|
start = (page - 1) * page_size
|
|
end = start + page_size
|
|
records = base_queryset[start:end]
|
|
|
|
# 5. 批量获取管事用户信息(只查 boss_profile 获取昵称)
|
|
guanshi_ids = list(set(rec.guanshi for rec in records if rec.guanshi))
|
|
guanshi_info_map = self._get_guanshi_info(guanshi_ids)
|
|
|
|
# 6. 组装列表数据
|
|
list_data = []
|
|
for rec in records:
|
|
# 打手信息(直接从记录中取快照)
|
|
dashou_avatar = rec.avatar or ''
|
|
dashou_nicheng = rec.nicheng or ''
|
|
dashou_yonghuid = rec.dashouid or ''
|
|
|
|
# 管事信息
|
|
guanshi_id = rec.guanshi
|
|
guanshi_info = guanshi_info_map.get(guanshi_id, {'avatar': '', 'nicheng': ''})
|
|
guanshi_avatar = guanshi_info['avatar']
|
|
guanshi_nicheng = guanshi_info['nicheng']
|
|
|
|
# 组长分红金额
|
|
fenhong = rec.zuzhang_fenhong
|
|
if fenhong is None:
|
|
fenhong = decimal.Decimal('0.00')
|
|
fenhong_str = str(fenhong.quantize(decimal.Decimal('0.00')))
|
|
|
|
# 时间格式化
|
|
CreateTime = rec.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if rec.CreateTime else ''
|
|
|
|
list_data.append({
|
|
'dashou_avatar': dashou_avatar,
|
|
'dashou_nicheng': dashou_nicheng,
|
|
'dashou_yonghuid': dashou_yonghuid,
|
|
'guanshi_avatar': guanshi_avatar,
|
|
'guanshi_nicheng': guanshi_nicheng,
|
|
'guanshi_yonghuid': guanshi_id,
|
|
'fenhong': fenhong_str,
|
|
'CreateTime': CreateTime,
|
|
})
|
|
|
|
# 7. 是否有更多数据
|
|
has_more = (end < fenhong_cishu)
|
|
|
|
return Response({
|
|
'code': 200,
|
|
'msg': 'success',
|
|
'data': {
|
|
'fenhong_zonge': str(fenhong_zonge.quantize(decimal.Decimal('0.00'))),
|
|
'yaoqing_guanshi': yaoqing_guanshi,
|
|
'fenhong_cishu': fenhong_cishu,
|
|
'total': fenhong_cishu,
|
|
'has_more': has_more,
|
|
'list': list_data,
|
|
}
|
|
})
|
|
|
|
def _get_guanshi_info(self, guanshi_ids):
|
|
"""
|
|
批量获取管事用户的头像和昵称
|
|
昵称从老板扩展表(boss_profile)获取,头像从主表获取
|
|
返回字典:{用户ID: {'avatar': 相对路径, 'nicheng': 昵称}}
|
|
"""
|
|
if not guanshi_ids:
|
|
return {}
|
|
|
|
# 查询用户主表,并预加载 boss_profile
|
|
users = User.query.filter(UserUID__in=guanshi_ids).select_related(
|
|
'BossProfile'
|
|
).only(
|
|
'UserUID', 'Avatar',
|
|
'BossProfile__nickname'
|
|
)
|
|
|
|
info = {}
|
|
for user in users:
|
|
avatar = user.Avatar or ''
|
|
# 从 boss_profile 获取昵称
|
|
if hasattr(user, 'BossProfile') and user.BossProfile:
|
|
nicheng = user.BossProfile.nickname or ''
|
|
else:
|
|
# 如果没有 boss_profile,使用默认昵称
|
|
nicheng = f"用户{user.UserUID[-4:]}"
|
|
info[user.UserUID] = {'avatar': avatar, 'nicheng': nicheng}
|
|
|
|
# 对于未查询到的用户,提供默认值
|
|
for gid in guanshi_ids:
|
|
if gid not in info:
|
|
info[gid] = {'avatar': '', 'nicheng': f"用户{gid[-4:]}"}
|
|
|
|
return info
|
|
|
|
|
|
|
|
# 常量定义(可根据需要调整)
|
|
|
|
|