"""商品/会员/分红 数据查询视图。 原 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 }) # 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': {} }) # 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 # 常量定义(可根据需要调整)