diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py index 4ecaf27..8b04410 100644 --- a/a_long_dianjing/settings.py +++ b/a_long_dianjing/settings.py @@ -119,8 +119,8 @@ SIMPLE_JWT = { 'LEEWAY': 0, 'AUTH_HEADER_TYPES': ('Bearer',), 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', - 'USER_ID_FIELD': 'yonghuid', - 'USER_ID_CLAIM': 'yonghuid', + 'USER_ID_FIELD': 'UserUID', + 'USER_ID_CLAIM': 'UserUID', 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), 'TOKEN_TYPE_CLAIM': 'token_type', diff --git a/backend/view.py b/backend/view.py index 37f479d..4680e04 100644 --- a/backend/view.py +++ b/backend/view.py @@ -581,7 +581,7 @@ class AddAdminUserView(APIView): # 生成唯一的 yonghuid(7位数字) while True: yonghuid = str(random.randint(1000000, 9999999)) - if not User.query.filter(yonghuid=yonghuid).exists(): + if not User.query.filter(UserUID=yonghuid).exists(): break # 生成唯一的 openid(格式:admin_{时间戳}_{随机6位}) @@ -1064,7 +1064,7 @@ class KefuUpdateDashouView(APIView): # 3. 查询打手 try: - dashou_user = User.query.get(yonghuid=dashou_id) + dashou_user = User.query.get(UserUID=dashou_id) dashou_profile = dashou_user.dashou_profile except User.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}) @@ -1496,7 +1496,7 @@ class KefuGetShangjiaDetailView(APIView): # 查询商家 try: - shangjia = UserShangjia.query.select_related('user').get(user__yonghuid=yonghuid) + shangjia = UserShangjia.query.select_related('user').get(user__UserUID=yonghuid) except UserShangjia.DoesNotExist: return Response({'code': 404, 'msg': '商家不存在'}) @@ -1566,7 +1566,7 @@ class KefuUpdateShangjiaView(APIView): # 使用事务,select_for_update 必须在事务内 with transaction.atomic(): try: - shangjia = UserShangjia.objects.select_for_update().get(user__yonghuid=yonghuid) + shangjia = UserShangjia.objects.select_for_update().get(user__UserUID=yonghuid) except UserShangjia.DoesNotExist: return Response({'code': 404, 'msg': '商家不存在'}) @@ -2968,7 +2968,7 @@ class GetGuanliDetailView(APIView): # 查询用户主表和管事扩展表 try: - user = User.query.select_related('guanshi_profile', 'boss_profile').get(yonghuid=yonghuid) + user = User.query.select_related('guanshi_profile', 'boss_profile').get(UserUID=yonghuid) guanshi = user.guanshi_profile boss = user.boss_profile if hasattr(user, 'boss_profile') else None except User.DoesNotExist: @@ -3015,7 +3015,7 @@ class GetGuanliDetailView(APIView): # 获取多次分红配置(按次数和会员组织) # 查询该管事的所有多次分红记录 - duoci_records = DuociFenhong.query.filter(yonghuid=yonghuid).order_by('cishu') + duoci_records = DuociFenhong.query.filter(UserUID=yonghuid).order_by('cishu') custom_first = {} # 首次分红定制(cishu=1) extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } } @@ -3089,7 +3089,7 @@ class UpdateGuanliView(APIView): # 查询管事对象(不在事务外加锁,事务内加锁) try: - user = User.query.get(yonghuid=yonghuid) + user = User.query.get(UserUID=yonghuid) guanshi = user.guanshi_profile boss = user.boss_profile if hasattr(user, 'boss_profile') else None except User.DoesNotExist: @@ -3100,7 +3100,7 @@ class UpdateGuanliView(APIView): # 开始事务,内部使用 select_for_update with transaction.atomic(): # 重新获取加锁对象 - user = User.objects.select_for_update().get(yonghuid=yonghuid) + user = User.objects.select_for_update().get(UserUID=yonghuid) guanshi = user.guanshi_profile boss = user.boss_profile if hasattr(user, 'boss_profile') else None @@ -3196,7 +3196,7 @@ class UpdateGuanliView(APIView): # 4.2 首次定制分红(cishu=1) if custom_first is not None and isinstance(custom_first, dict): # 获取当前所有首次定制记录 - existing_first = DuociFenhong.query.filter(yonghuid=yonghuid, cishu=1) + existing_first = DuociFenhong.query.filter(UserUID=yonghuid, cishu=1) # 前端传来的定制字典 {会员ID: 金额} for huiyuan_id, amount in custom_first.items(): try: @@ -3205,7 +3205,7 @@ class UpdateGuanliView(APIView): continue if amount <= 0: # 金额为0或负数表示删除定制 - DuociFenhong.query.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() + DuociFenhong.query.filter(UserUID=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() else: DuociFenhong.query.update_or_create( yonghuid=yonghuid, @@ -3256,7 +3256,7 @@ class UpdateGuanliView(APIView): } ) # 删除不在 keep_set 中的额外次数记录 - existing_extra = DuociFenhong.query.filter(yonghuid=yonghuid, cishu__gte=2) + existing_extra = DuociFenhong.query.filter(UserUID=yonghuid, cishu__gte=2) for record in existing_extra: if (record.cishu, record.huiyuan) not in keep_set: record.delete() @@ -3660,7 +3660,7 @@ class GetZuzhangDetailView(APIView): # 查询组长主表、扩展表、老板扩展表(昵称) try: - user = User.query.select_related('zuzhang_profile', 'boss_profile').get(yonghuid=yonghuid) + user = User.query.select_related('zuzhang_profile', 'boss_profile').get(UserUID=yonghuid) zuzhang = user.zuzhang_profile boss = user.boss_profile if hasattr(user, 'boss_profile') else None except User.DoesNotExist: @@ -3706,7 +3706,7 @@ class GetZuzhangDetailView(APIView): m['zuzhangfc'] = str(m['zuzhangfc']) # 获取多次分红配置(按次数和会员组织) - duoci_records = DuociFenhong.query.filter(yonghuid=yonghuid).order_by('cishu') + duoci_records = DuociFenhong.query.filter(UserUID=yonghuid).order_by('cishu') custom_first = {} # 首次分红定制(cishu=1) extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } } @@ -3783,7 +3783,7 @@ class UpdateZuzhangView(APIView): # 查询组长对象 try: - user = User.query.get(yonghuid=yonghuid) + user = User.query.get(UserUID=yonghuid) zuzhang = user.zuzhang_profile boss = user.boss_profile if hasattr(user, 'boss_profile') else None except User.DoesNotExist: @@ -3879,7 +3879,7 @@ class UpdateZuzhangView(APIView): # 4.2 首次定制分红(cishu=1) if custom_first is not None and isinstance(custom_first, dict): - existing_first = DuociFenhong.query.filter(yonghuid=yonghuid, cishu=1) + existing_first = DuociFenhong.query.filter(UserUID=yonghuid, cishu=1) for huiyuan_id, amount in custom_first.items(): try: amount = Decimal(str(amount)) @@ -3887,7 +3887,7 @@ class UpdateZuzhangView(APIView): continue if amount <= 0: # 删除定制(恢复默认) - DuociFenhong.query.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() + DuociFenhong.query.filter(UserUID=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() else: DuociFenhong.query.update_or_create( yonghuid=yonghuid, @@ -3936,7 +3936,7 @@ class UpdateZuzhangView(APIView): } ) # 删除不在 keep_set 中的额外次数记录 - existing_extra = DuociFenhong.query.filter(yonghuid=yonghuid, cishu__gte=2) + existing_extra = DuociFenhong.query.filter(UserUID=yonghuid, cishu__gte=2) for record in existing_extra: if (record.cishu, record.huiyuan) not in keep_set: record.delete() @@ -5026,7 +5026,7 @@ class ShopModifyView(APIView): return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'}) # 检查用户是否存在 - if not User.query.filter(yonghuid=yonghu_id).exists(): + if not User.query.filter(UserUID=yonghu_id).exists(): return Response({'code': 404, 'msg': '用户不存在'}) # 检查凭证是否已存在 @@ -5962,7 +5962,7 @@ class FaKuanChuangJianView(APIView): # 验证用户是否存在对应的扩展表 try: - user = User.query.get(yonghuid=beichufa_id) + user = User.query.get(UserUID=beichufa_id) profile_attr = SHENFEN_PROFILE_MAP.get(shenfen) if not profile_attr or not hasattr(user, profile_attr): return Response({'code': 400, 'msg': '该用户不是所选身份'}) @@ -6162,7 +6162,7 @@ class PunishDashouView(APIView): # 查询打手 try: - dashou_user = User.query.get(yonghuid=dashou_id) + dashou_user = User.query.get(UserUID=dashou_id) dashou = dashou_user.dashou_profile except User.DoesNotExist: return Response({'code': 404, 'msg': '打手用户不存在'}) @@ -6484,7 +6484,7 @@ class CaiwuView(APIView): amount_sum = Czjilu.query.filter( create_time__gte=today_start, create_time__lt=today_end, - yonghuid__in=new_huiyuan_user_ids, + UserUID__in=new_huiyuan_user_ids, leixing=1, zhuangtai=3 ).aggregate(total=Sum('jine')) @@ -7946,7 +7946,7 @@ class KhgglView(APIView): queryset = UserShenheguan.query.select_related('user__dashou_profile') if filter_yonghuid: - queryset = queryset.filter(user__yonghuid=filter_yonghuid) + queryset = queryset.filter(user__UserUID=filter_yonghuid) if filter_zhuangtai is not None: queryset = queryset.filter(zhuangtai=filter_zhuangtai) if filter_shenhe_zhuangtai is not None: @@ -7960,7 +7960,7 @@ class KhgglView(APIView): shenheguan_ids = KaoheguanBankuai.query.filter( bankuai_id__in=bankuai_ids ).values_list('kaoheguan_id', flat=True) - queryset = queryset.filter(user__yonghuid__in=shenheguan_ids) + queryset = queryset.filter(user__UserUID__in=shenheguan_ids) total = queryset.count() start = (page - 1) * page_size @@ -8070,7 +8070,7 @@ class ShgxgsjView(APIView): return Response({'code': 1, 'msg': '缺少用户ID'}, status=400) try: - shenheguan = UserShenheguan.query.select_related('user').get(user__yonghuid=yonghuid) + shenheguan = UserShenheguan.query.select_related('user').get(user__UserUID=yonghuid) except UserShenheguan.DoesNotExist: return Response({'code': 1, 'msg': '审核官不存在'}, status=404) @@ -8189,7 +8189,7 @@ class ZxkfghdsView(APIView): shangjia_ext = old_order.shangjia_kuozhan shangjia_id = shangjia_ext.shangjia_id # 获取商家昵称用于新订单扩展表 - shangjia_user = User.query.get(yonghuid=shangjia_id) + shangjia_user = User.query.get(UserUID=shangjia_id) shangjia_nicheng = shangjia_user.shop_profile.nicheng or f"商家{shangjia_id}" except (ObjectDoesNotExist, UserShangjia.DoesNotExist): logger.error(f"商家信息缺失,订单: {dingdan_id}") @@ -8206,7 +8206,7 @@ class ZxkfghdsView(APIView): dashou_id = old_order.jiedan_dashou_id if dashou_id: try: - dashou_user = User.query.get(yonghuid=dashou_id) + dashou_user = User.query.get(UserUID=dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang += 1 # 退款次数+1 dashou_profile.zhuangtai = 1 # 设为空闲 diff --git a/config/migrations/0001_initial.py b/config/migrations/0001_initial.py index 2f0e170..c091182 100644 --- a/config/migrations/0001_initial.py +++ b/config/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.27 on 2026-06-17 16:31 +# Generated by Django 4.2.27 on 2026-06-17 21:18 from django.db import migrations, models import django.db.models.deletion diff --git a/config/views.py b/config/views.py index 6987fde..43de005 100644 --- a/config/views.py +++ b/config/views.py @@ -11,7 +11,7 @@ from decimal import Decimal, InvalidOperation from django.conf import settings from django.db import transaction, connection -from django.db.models import Q, F, Max, Prefetch +from django.db.models import Q, F, Max, Prefetch from gvsdsdk.fluent import db, func, FQ from django.core.cache import cache from django.core.paginator import Paginator @@ -2191,7 +2191,7 @@ class KehuGetDingdanLianjieView(APIView): # 获取商家昵称 shangjia_nicheng = '' try: - user_main = User.query.get(yonghuid=lianjie_obj.yonghu_id) + user_main = User.query.get(UserUID=lianjie_obj.yonghu_id) shangjia_obj = user_main.shop_profile shangjia_nicheng = shangjia_obj.nicheng except (User.DoesNotExist, UserShangjia.DoesNotExist): @@ -2473,7 +2473,7 @@ class KehuTianxieDingdanView(APIView): # 验证打手是否存在且有效 try: # 先查询用户主表 - dashou_user = User.query.get(yonghuid=zhiding_dashou) + dashou_user = User.query.get(UserUID=zhiding_dashou) logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}") # 通过反向关系获取打手扩展信息 @@ -2571,7 +2571,7 @@ class KehuTianxieDingdanView(APIView): # 获取商家信息用于返回 try: - user_main = User.query.get(yonghuid=lianjie_obj.yonghu_id) + user_main = User.query.get(UserUID=lianjie_obj.yonghu_id) shangjia_obj = user_main.shop_profile shangjia_mingcheng = shangjia_obj.nicheng except: diff --git a/fix_fk.py b/fix_fk.py new file mode 100644 index 0000000..ef7b887 --- /dev/null +++ b/fix_fk.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""修复 yonghu_dianpu_bangding 表的 FK 约束""" +import MySQLdb +from app_secrets import DATABASE_HOST, DATABASE_PORT, DATABASE_USER, DATABASE_PASSWORD, DATABASE_NAME + +conn = MySQLdb.connect( + host=DATABASE_HOST, port=int(DATABASE_PORT), + user=DATABASE_USER, password=DATABASE_PASSWORD, + database=DATABASE_NAME, charset='utf8mb4' +) +cursor = conn.cursor() + +# 1. 查看所有引用 User 表的 FK 约束 +cursor.execute(""" + SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = %s AND REFERENCED_TABLE_NAME = 'User' +""", [DATABASE_NAME]) + +fks = cursor.fetchall() +print("引用 User 表的 FK 约束:") +for fk in fks: + print(f" {fk[0]}: {fk[1]}.{fk[2]} -> {fk[3]}.{fk[4]}") + +# 2. 删除所有引用 User.UserUID 的 FK 约束(类型不兼容的) +for fk in fks: + if fk[4] == 'UserUID': # 引用了 UserUID (varchar) 而不是 UserUUID (binary) + constraint_name = fk[0] + table_name = fk[1] + print(f"\n删除不兼容 FK: {constraint_name} on {table_name}") + try: + cursor.execute(f"ALTER TABLE `{table_name}` DROP FOREIGN KEY `{constraint_name}`") + conn.commit() + print(f" 已删除 {constraint_name}") + except Exception as e: + print(f" 删除失败: {e}") + +# 3. 修改 yonghu_dianpu_bangding.UserUUID 列类型 +print("\n修改 yonghu_dianpu_bangding.UserUUID 列类型...") +try: + cursor.execute("ALTER TABLE yonghu_dianpu_bangding MODIFY COLUMN UserUUID binary(16) NOT NULL") + conn.commit() + print(" 修改成功") +except Exception as e: + print(f" 修改失败: {e}") + +# 4. 重新添加 FK 约束 +print("\n重新添加 FK 约束...") +try: + cursor.execute(""" + ALTER TABLE yonghu_dianpu_bangding + ADD CONSTRAINT yonghu_dianpu_bangding_UserUUID_fk + FOREIGN KEY (UserUUID) REFERENCES User(UserUUID) + """) + conn.commit() + print(" FK 约束添加成功") +except Exception as e: + print(f" FK 约束添加失败: {e}") + +# 5. 验证 +print("\n验证表结构:") +cursor.execute("DESCRIBE yonghu_dianpu_bangding") +for row in cursor.fetchall(): + print(f" {row}") + +conn.close() +print("\n完成!") diff --git a/gvsdsdk/models.py b/gvsdsdk/models.py index ee067d8..f6e5169 100644 --- a/gvsdsdk/models.py +++ b/gvsdsdk/models.py @@ -388,6 +388,15 @@ class User(QModel): def id(self): return self.ID + @property + def shoujihao_renzheng(self): + """兼容旧代码 — 手机号是否已认证""" + return self.PhoneVerified + + @shoujihao_renzheng.setter + def shoujihao_renzheng(self, value): + self.PhoneVerified = bool(value) + def set_password(self, raw_password): self.SetPassword(raw_password) diff --git a/orders/migrations/0001_initial.py b/orders/migrations/0001_initial.py index c05d6bf..3b5e781 100644 --- a/orders/migrations/0001_initial.py +++ b/orders/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.27 on 2026-06-17 16:31 +# Generated by Django 4.2.27 on 2026-06-17 21:18 from django.db import migrations, models import django.db.models.deletion diff --git a/orders/tasks.py b/orders/tasks.py index 3a0ac52..0327518 100644 --- a/orders/tasks.py +++ b/orders/tasks.py @@ -1,208 +1,208 @@ -""" -阿龙电竞 - 订单定时任务 -处理订单超时自动确认等逻辑 -完整版本,包含所有必要的异常处理 -""" - -from celery import shared_task -from django.db import transaction -from django.db.models import F, Q -from django.conf import settings -from django.utils import timezone -from datetime import timedelta -from decimal import Decimal -import logging - -# 导入模型 - 确保路径正确 -from orders.models import Dingdan -from users.models import UserDashou, UserShangjia -from gvsdsdk.models import User -from utils.celery_utils import safe_decimal_operation, log_task_execution, rollback_on_failure -from gvsdsdk.fluent import db, func, FQ - -logger = logging.getLogger(__name__) - - - -@shared_task(bind=True, max_retries=3, default_retry_delay=60) -def process_expired_order(self, dingdan_id): - """ - 处理单个超时订单(订单状态=8,超过48小时未处理) - 【已禁用】ORDER_AUTO_SETTLEMENT_ENABLED=False 且已从 Celery autodiscover 移除 - """ - if not getattr(settings, 'ORDER_AUTO_SETTLEMENT_ENABLED', False): - logger.warning('订单%s自动结算已关闭(ORDER_AUTO_SETTLEMENT_ENABLED=False),拒绝执行', dingdan_id) - return f"订单{dingdan_id}自动结算已禁用,跳过" - - # --- 以下原结算逻辑保留备查 --- - try: - # 使用select_for_update锁定记录,防止并发修改 - with transaction.atomic(): - - # 获取订单,同时锁定记录 - order = Dingdan.objects.select_for_update().get( - dingdan_id=dingdan_id - ) - - # 再次检查订单状态是否为8 - if order.zhuangtai != 8: - logger.info(f"订单{dingdan_id}状态已不是8(当前:{order.zhuangtai}),跳过处理") - return f"订单{dingdan_id}状态已变更,跳过处理" - - # 获取发单平台类型 - fadan_pingtai = order.fadan_pingtai - - # 获取打手ID和分成金额 - dashou_id = order.jiedan_dashou_id - dashou_fencheng = safe_decimal_operation(order.dashou_fencheng) - - # 1. 更新订单状态为3(已完成) - order.zhuangtai = 3 - order.save(update_fields=['zhuangtai']) - - # 2. 更新打手信息(如果存在) - updated_dashou = False - if dashou_id: - try: - # 查询打手用户主表 - dashou_user = User.query.filter(yonghuid=dashou_id).first() - if dashou_user: - # 获取打手扩展表 - dashou_profile = getattr(dashou_user, 'dashou_profile', None) - if dashou_profile: - # 使用F表达式原子更新 - UserDashou.query.filter(pk=dashou_profile.pk).update( - chengjiaozongliang=F('chengjiaozongliang') + 1, - yue=F('yue') + dashou_fencheng, - zonge=F('zonge') + dashou_fencheng, - jinrishouyi=F('jinrishouyi') + dashou_fencheng, - jinyueshouyi=F('jinyueshouyi') + dashou_fencheng - ) - updated_dashou = True - logger.info(f"更新打手{dashou_id}信息成功") - except Exception as e: - logger.error(f"更新打手信息失败,打手ID: {dashou_id}, 错误: {str(e)}") - # 继续执行,不中断流程 - - # 3. 如果是商家发单,更新商家信息 - updated_shangjia = False - if fadan_pingtai == 2: # 商家发单 - try: - # 获取商家扩展信息 - shangjia_kuozhan = getattr(order, 'shangjia_kuozhan', None) - if shangjia_kuozhan: - shangjia_id = shangjia_kuozhan.shangjia_id - if shangjia_id: - # 查询商家用户 - shangjia_user = User.query.filter(yonghuid=shangjia_id).first() - if shangjia_user: - shangjia_profile = getattr(shangjia_user, 'shop_profile', None) - if shangjia_profile: - # 更新商家成交订单数量 - UserShangjia.query.filter(pk=shangjia_profile.pk).update( - chengjiao=F('chengjiao') + 1 - ) - updated_shangjia = True - logger.info(f"更新商家{shangjia_id}信息成功") - except Exception as e: - logger.error(f"更新商家信息失败,订单ID: {dingdan_id}, 错误: {str(e)}") - # 继续执行,不中断流程 - - # 记录任务执行结果 - result_msg = f"订单{dingdan_id}自动结算完成" - if updated_dashou: - result_msg += ",打手信息已更新" - if updated_shangjia: - result_msg += ",商家信息已更新" - - if fadan_pingtai == 1: - pass # 平台收益更新逻辑待完善 - - log_task_execution(f"自动结算订单 {dingdan_id}", True, result_msg) - return result_msg - - except Dingdan.DoesNotExist: - error_msg = f"订单{dingdan_id}不存在" - logger.warning(error_msg) - return error_msg - except Exception as e: - error_msg = f"处理订单{dingdan_id}时发生错误: {str(e)}" - logger.error(error_msg) - - # 重试机制 - if self.request.retries < self.max_retries: - logger.info(f"订单{dingdan_id}处理失败,准备第{self.request.retries + 1}次重试") - raise self.retry(exc=e) - - log_task_execution(f"自动结算订单 {dingdan_id}", False, error_msg) - return error_msg - -@shared_task -@rollback_on_failure("批量检查超时订单") -def check_order_expire_task(): - """ - 批量检查超时订单(补偿机制) - 【已禁用】ORDER_AUTO_SETTLEMENT_ENABLED=False - """ - if not getattr(settings, 'ORDER_AUTO_SETTLEMENT_ENABLED', False): - logger.warning('订单自动结算补偿检查已关闭(ORDER_AUTO_SETTLEMENT_ENABLED=False),拒绝执行') - return {"success": True, "message": "自动结算已禁用", "count": 0} - - # --- 以下原补偿逻辑保留备查 --- - try: - # 计算更宽松的时间点:当前时间减去72小时(48+24小时) - # 这样避免误处理刚变为状态8的订单 - check_time = timezone.now() - timedelta(seconds=settings.ORDER_EXPIRE_SECONDS + 86400) # 48+24=72小时 - - # 查询状态为8且创建时间早于检查时间的订单 - expired_orders = Dingdan.query.filter( - zhuangtai=8, - create_time__lt=check_time - ).values_list('dingdan_id', flat=True) - - expired_count = expired_orders.count() - - if expired_count == 0: - logger.info("补偿检查:无超时订单") - return {"success": True, "message": "无超时订单", "count": 0} - - # 分批处理超时订单(避免一次处理太多) - batch_size = 10 - processed_count = 0 - - for i in range(0, expired_count, batch_size): - batch = expired_orders[i:i+batch_size] - - # 异步处理每个超时订单 - for dingdan_id in batch: - process_expired_order.apply_async( - args=[dingdan_id], - queue='order_tasks', - priority=8 # 补偿任务的优先级低一些 - ) - processed_count += 1 - - result_msg = f"补偿检查:发现{expired_count}个可能超时的订单,已提交处理{processed_count}个" - log_task_execution("批量检查超时订单", True, result_msg) - - return { - "success": True, - "message": result_msg, - "total_count": expired_count, - "processed_count": processed_count - } - - except Exception as e: - error_msg = f"批量检查超时订单失败: {str(e)}" - logger.error(error_msg) - log_task_execution("批量检查超时订单", False, error_msg) - - return { - "success": False, - "message": error_msg, - "error": str(e) - } - - - - +""" +阿龙电竞 - 订单定时任务 +处理订单超时自动确认等逻辑 +完整版本,包含所有必要的异常处理 +""" + +from celery import shared_task +from django.db import transaction +from django.db.models import F, Q +from django.conf import settings +from django.utils import timezone +from datetime import timedelta +from decimal import Decimal +import logging + +# 导入模型 - 确保路径正确 +from orders.models import Dingdan +from users.models import UserDashou, UserShangjia +from gvsdsdk.models import User +from utils.celery_utils import safe_decimal_operation, log_task_execution, rollback_on_failure +from gvsdsdk.fluent import db, func, FQ + +logger = logging.getLogger(__name__) + + + +@shared_task(bind=True, max_retries=3, default_retry_delay=60) +def process_expired_order(self, dingdan_id): + """ + 处理单个超时订单(订单状态=8,超过48小时未处理) + 【已禁用】ORDER_AUTO_SETTLEMENT_ENABLED=False 且已从 Celery autodiscover 移除 + """ + if not getattr(settings, 'ORDER_AUTO_SETTLEMENT_ENABLED', False): + logger.warning('订单%s自动结算已关闭(ORDER_AUTO_SETTLEMENT_ENABLED=False),拒绝执行', dingdan_id) + return f"订单{dingdan_id}自动结算已禁用,跳过" + + # --- 以下原结算逻辑保留备查 --- + try: + # 使用select_for_update锁定记录,防止并发修改 + with transaction.atomic(): + + # 获取订单,同时锁定记录 + order = Dingdan.objects.select_for_update().get( + dingdan_id=dingdan_id + ) + + # 再次检查订单状态是否为8 + if order.zhuangtai != 8: + logger.info(f"订单{dingdan_id}状态已不是8(当前:{order.zhuangtai}),跳过处理") + return f"订单{dingdan_id}状态已变更,跳过处理" + + # 获取发单平台类型 + fadan_pingtai = order.fadan_pingtai + + # 获取打手ID和分成金额 + dashou_id = order.jiedan_dashou_id + dashou_fencheng = safe_decimal_operation(order.dashou_fencheng) + + # 1. 更新订单状态为3(已完成) + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + + # 2. 更新打手信息(如果存在) + updated_dashou = False + if dashou_id: + try: + # 查询打手用户主表 + dashou_user = User.query.filter(UserUID=dashou_id).first() + if dashou_user: + # 获取打手扩展表 + dashou_profile = getattr(dashou_user, 'dashou_profile', None) + if dashou_profile: + # 使用F表达式原子更新 + UserDashou.query.filter(pk=dashou_profile.pk).update( + chengjiaozongliang=F('chengjiaozongliang') + 1, + yue=F('yue') + dashou_fencheng, + zonge=F('zonge') + dashou_fencheng, + jinrishouyi=F('jinrishouyi') + dashou_fencheng, + jinyueshouyi=F('jinyueshouyi') + dashou_fencheng + ) + updated_dashou = True + logger.info(f"更新打手{dashou_id}信息成功") + except Exception as e: + logger.error(f"更新打手信息失败,打手ID: {dashou_id}, 错误: {str(e)}") + # 继续执行,不中断流程 + + # 3. 如果是商家发单,更新商家信息 + updated_shangjia = False + if fadan_pingtai == 2: # 商家发单 + try: + # 获取商家扩展信息 + shangjia_kuozhan = getattr(order, 'shangjia_kuozhan', None) + if shangjia_kuozhan: + shangjia_id = shangjia_kuozhan.shangjia_id + if shangjia_id: + # 查询商家用户 + shangjia_user = User.query.filter(UserUID=shangjia_id).first() + if shangjia_user: + shangjia_profile = getattr(shangjia_user, 'shop_profile', None) + if shangjia_profile: + # 更新商家成交订单数量 + UserShangjia.query.filter(pk=shangjia_profile.pk).update( + chengjiao=F('chengjiao') + 1 + ) + updated_shangjia = True + logger.info(f"更新商家{shangjia_id}信息成功") + except Exception as e: + logger.error(f"更新商家信息失败,订单ID: {dingdan_id}, 错误: {str(e)}") + # 继续执行,不中断流程 + + # 记录任务执行结果 + result_msg = f"订单{dingdan_id}自动结算完成" + if updated_dashou: + result_msg += ",打手信息已更新" + if updated_shangjia: + result_msg += ",商家信息已更新" + + if fadan_pingtai == 1: + pass # 平台收益更新逻辑待完善 + + log_task_execution(f"自动结算订单 {dingdan_id}", True, result_msg) + return result_msg + + except Dingdan.DoesNotExist: + error_msg = f"订单{dingdan_id}不存在" + logger.warning(error_msg) + return error_msg + except Exception as e: + error_msg = f"处理订单{dingdan_id}时发生错误: {str(e)}" + logger.error(error_msg) + + # 重试机制 + if self.request.retries < self.max_retries: + logger.info(f"订单{dingdan_id}处理失败,准备第{self.request.retries + 1}次重试") + raise self.retry(exc=e) + + log_task_execution(f"自动结算订单 {dingdan_id}", False, error_msg) + return error_msg + +@shared_task +@rollback_on_failure("批量检查超时订单") +def check_order_expire_task(): + """ + 批量检查超时订单(补偿机制) + 【已禁用】ORDER_AUTO_SETTLEMENT_ENABLED=False + """ + if not getattr(settings, 'ORDER_AUTO_SETTLEMENT_ENABLED', False): + logger.warning('订单自动结算补偿检查已关闭(ORDER_AUTO_SETTLEMENT_ENABLED=False),拒绝执行') + return {"success": True, "message": "自动结算已禁用", "count": 0} + + # --- 以下原补偿逻辑保留备查 --- + try: + # 计算更宽松的时间点:当前时间减去72小时(48+24小时) + # 这样避免误处理刚变为状态8的订单 + check_time = timezone.now() - timedelta(seconds=settings.ORDER_EXPIRE_SECONDS + 86400) # 48+24=72小时 + + # 查询状态为8且创建时间早于检查时间的订单 + expired_orders = Dingdan.query.filter( + zhuangtai=8, + create_time__lt=check_time + ).values_list('dingdan_id', flat=True) + + expired_count = expired_orders.count() + + if expired_count == 0: + logger.info("补偿检查:无超时订单") + return {"success": True, "message": "无超时订单", "count": 0} + + # 分批处理超时订单(避免一次处理太多) + batch_size = 10 + processed_count = 0 + + for i in range(0, expired_count, batch_size): + batch = expired_orders[i:i+batch_size] + + # 异步处理每个超时订单 + for dingdan_id in batch: + process_expired_order.apply_async( + args=[dingdan_id], + queue='order_tasks', + priority=8 # 补偿任务的优先级低一些 + ) + processed_count += 1 + + result_msg = f"补偿检查:发现{expired_count}个可能超时的订单,已提交处理{processed_count}个" + log_task_execution("批量检查超时订单", True, result_msg) + + return { + "success": True, + "message": result_msg, + "total_count": expired_count, + "processed_count": processed_count + } + + except Exception as e: + error_msg = f"批量检查超时订单失败: {str(e)}" + logger.error(error_msg) + log_task_execution("批量检查超时订单", False, error_msg) + + return { + "success": False, + "message": error_msg, + "error": str(e) + } + + + + diff --git a/orders/utils.py b/orders/utils.py index 1ffd140..bbf3f2f 100644 --- a/orders/utils.py +++ b/orders/utils.py @@ -1,204 +1,204 @@ -import logging -from datetime import date -from decimal import Decimal - -from django.db import transaction -from django.db.models import F - -from config.models import DailyIncomeStat, DailyPayoutStat -from orders.models import Dingdan, DingdanPingtai, DingdanShangjia, Lilubiao -from products.models import Gsfenhong -from users.models import UserDashou, UserGuanshi - -from backend.utils import update_guanshi_daily_by_action - -logger = logging.getLogger(__name__) - - -def get_order_user_id(order): - if order.fadan_pingtai == 1: - try: - ext = DingdanPingtai.objects.get(dingdan=order) - return ext.laoban_id - except DingdanPingtai.DoesNotExist: - return None - elif order.fadan_pingtai == 2: - try: - ext = DingdanShangjia.objects.get(dingdan=order) - return ext.shangjia_id - except DingdanShangjia.DoesNotExist: - return None - return None - - -def update_daily_income(amount): - """ - 原子更新当日收入统计(金额累加,笔数加1)。 - 用于微信支付成功回调。 - - 参数: - amount: Decimal 本次收入金额 - """ - today = date.today() - year, month, day = today.year, today.month, today.day - - with transaction.atomic(): - stat, created = DailyIncomeStat.objects.select_for_update().get_or_create( - date=today, - defaults={ - 'year': year, - 'month': month, - 'day': day, - 'total_amount': amount, - 'total_count': 1 - } - ) - if not created: - # 原子累加 - stat.total_amount = F('total_amount') + amount - stat.total_count = F('total_count') + 1 - stat.save(update_fields=['total_amount', 'total_count']) - - logger.info( - f"每日收入更新: {today}, +{amount}元, 总金额={stat.total_amount if created else stat.total_amount + amount}, 总笔数={stat.total_count if created else stat.total_count + 1}") - - - - - - -def update_daily_payout(amount): - """ - 原子更新当日出款统计(金额累加,笔数加1)。 - 用于提现成功、结算打款等场景。 - - 参数: - amount: Decimal 本次出款金额 - """ - today = date.today() - year, month, day = today.year, today.month, today.day - - with transaction.atomic(): - stat, created = DailyPayoutStat.objects.select_for_update().get_or_create( - date=today, - defaults={ - 'year': year, - 'month': month, - 'day': day, - 'total_amount': amount, - 'total_count': 1 - } - ) - if not created: - # 原子累加 - stat.total_amount = F('total_amount') + amount - stat.total_count = F('total_count') + 1 - stat.save(update_fields=['total_amount', 'total_count']) - - logger.info(f"每日出款更新: {today}, +{amount}元, 总金额={stat.total_amount if created else stat.total_amount + amount}, 总笔数={stat.total_count if created else stat.total_count + 1}") - - - - - - - - -def calc_shangjia_order_fencheng(jine): - """ - 商家发单时计算打手/管事分成。 - 打手优先:打手+管事 > 订单金额时,管事分成为 0。 - """ - jine = Decimal(str(jine)) - try: - lilu_obj = Lilubiao.objects.get(fadanpingtai='3') - rate_dashou = Decimal(str(lilu_obj.lilu)) if lilu_obj.lilu and lilu_obj.lilu > 0 else Decimal('1') - except Lilubiao.DoesNotExist: - rate_dashou = Decimal('1') - - lilu_guanshi_obj = Lilubiao.objects.filter(fadanpingtai='13').first() - rate_guanshi = ( - Decimal(str(lilu_guanshi_obj.lilu)) - if lilu_guanshi_obj and lilu_guanshi_obj.lilu is not None - else Decimal('0') - ) - - dashou_fencheng = (jine * rate_dashou).quantize(Decimal('0.01')) - guanshi_raw = (jine * rate_guanshi).quantize(Decimal('0.01')) - - if dashou_fencheng + guanshi_raw > jine: - guanshi_fencheng = Decimal('0.00') - else: - guanshi_fencheng = guanshi_raw - - return dashou_fencheng, guanshi_fencheng - - -def settle_shangjia_order_guanshi_fenhong(order, dashou_id): - """ - 商家订单结单时结算管事分红(幂等,失败不抛异常阻断主流程)。 - """ - - if getattr(order, 'fadan_pingtai', None) != 2: - return - - guanshi_fencheng = order.guanshi_fencheng or Decimal('0') - if guanshi_fencheng <= 0: - return - - if Gsfenhong.objects.filter(dingdan_id=order.dingdan_id).exists(): - logger.info(f"商家订单管事分红已处理: {order.dingdan_id}") - return - - if not dashou_id: - return - - try: - dashou = UserDashou.objects.select_related('user').get(user__yonghuid=dashou_id) - except UserDashou.DoesNotExist: - logger.info(f"商家订单管事分红跳过: 打手{dashou_id}不存在") - return - - guanshi_id = dashou.yaoqingren - if not guanshi_id: - logger.info(f"商家订单管事分红跳过: 打手{dashou_id}无邀请管事") - return - - try: - with transaction.atomic(): - guanshi = UserGuanshi.objects.select_for_update().get(user__yonghuid=guanshi_id) - UserGuanshi.objects.filter(id=guanshi.id).update( - yue=F('yue') + guanshi_fencheng, - chongzhifenrun=F('chongzhifenrun') + guanshi_fencheng, - ) - - user_main = dashou.user - Gsfenhong.objects.create( - dingdan_id=order.dingdan_id, - guanshi=guanshi_id, - dashouid=dashou_id, - shuoming='商家订单分红', - fenhong=guanshi_fencheng, - avatar=user_main.avatar if user_main else None, - nicheng=dashou.nicheng or '未知打手', - fenhong_leixing=3, - ) - logger.info( - f"商家订单管事分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, " - f"打手{dashou_id}, 金额{guanshi_fencheng}元" - ) - except UserGuanshi.DoesNotExist: - logger.warning(f"商家订单管事分红跳过: 管事{guanshi_id}不存在") - return - except Exception as e: - logger.error(f"商家订单管事分红失败: {e}", exc_info=True) - return - - try: - update_guanshi_daily_by_action( - yonghuid=guanshi_id, - action=3, - amount=guanshi_fencheng, - ) - except Exception as e: - logger.error(f"商家订单管事日统计更新失败: {e}") +import logging +from datetime import date +from decimal import Decimal + +from django.db import transaction +from django.db.models import F + +from config.models import DailyIncomeStat, DailyPayoutStat +from orders.models import Dingdan, DingdanPingtai, DingdanShangjia, Lilubiao +from products.models import Gsfenhong +from users.models import UserDashou, UserGuanshi + +from backend.utils import update_guanshi_daily_by_action + +logger = logging.getLogger(__name__) + + +def get_order_user_id(order): + if order.fadan_pingtai == 1: + try: + ext = DingdanPingtai.objects.get(dingdan=order) + return ext.laoban_id + except DingdanPingtai.DoesNotExist: + return None + elif order.fadan_pingtai == 2: + try: + ext = DingdanShangjia.objects.get(dingdan=order) + return ext.shangjia_id + except DingdanShangjia.DoesNotExist: + return None + return None + + +def update_daily_income(amount): + """ + 原子更新当日收入统计(金额累加,笔数加1)。 + 用于微信支付成功回调。 + + 参数: + amount: Decimal 本次收入金额 + """ + today = date.today() + year, month, day = today.year, today.month, today.day + + with transaction.atomic(): + stat, created = DailyIncomeStat.objects.select_for_update().get_or_create( + date=today, + defaults={ + 'year': year, + 'month': month, + 'day': day, + 'total_amount': amount, + 'total_count': 1 + } + ) + if not created: + # 原子累加 + stat.total_amount = F('total_amount') + amount + stat.total_count = F('total_count') + 1 + stat.save(update_fields=['total_amount', 'total_count']) + + logger.info( + f"每日收入更新: {today}, +{amount}元, 总金额={stat.total_amount if created else stat.total_amount + amount}, 总笔数={stat.total_count if created else stat.total_count + 1}") + + + + + + +def update_daily_payout(amount): + """ + 原子更新当日出款统计(金额累加,笔数加1)。 + 用于提现成功、结算打款等场景。 + + 参数: + amount: Decimal 本次出款金额 + """ + today = date.today() + year, month, day = today.year, today.month, today.day + + with transaction.atomic(): + stat, created = DailyPayoutStat.objects.select_for_update().get_or_create( + date=today, + defaults={ + 'year': year, + 'month': month, + 'day': day, + 'total_amount': amount, + 'total_count': 1 + } + ) + if not created: + # 原子累加 + stat.total_amount = F('total_amount') + amount + stat.total_count = F('total_count') + 1 + stat.save(update_fields=['total_amount', 'total_count']) + + logger.info(f"每日出款更新: {today}, +{amount}元, 总金额={stat.total_amount if created else stat.total_amount + amount}, 总笔数={stat.total_count if created else stat.total_count + 1}") + + + + + + + + +def calc_shangjia_order_fencheng(jine): + """ + 商家发单时计算打手/管事分成。 + 打手优先:打手+管事 > 订单金额时,管事分成为 0。 + """ + jine = Decimal(str(jine)) + try: + lilu_obj = Lilubiao.objects.get(fadanpingtai='3') + rate_dashou = Decimal(str(lilu_obj.lilu)) if lilu_obj.lilu and lilu_obj.lilu > 0 else Decimal('1') + except Lilubiao.DoesNotExist: + rate_dashou = Decimal('1') + + lilu_guanshi_obj = Lilubiao.objects.filter(fadanpingtai='13').first() + rate_guanshi = ( + Decimal(str(lilu_guanshi_obj.lilu)) + if lilu_guanshi_obj and lilu_guanshi_obj.lilu is not None + else Decimal('0') + ) + + dashou_fencheng = (jine * rate_dashou).quantize(Decimal('0.01')) + guanshi_raw = (jine * rate_guanshi).quantize(Decimal('0.01')) + + if dashou_fencheng + guanshi_raw > jine: + guanshi_fencheng = Decimal('0.00') + else: + guanshi_fencheng = guanshi_raw + + return dashou_fencheng, guanshi_fencheng + + +def settle_shangjia_order_guanshi_fenhong(order, dashou_id): + """ + 商家订单结单时结算管事分红(幂等,失败不抛异常阻断主流程)。 + """ + + if getattr(order, 'fadan_pingtai', None) != 2: + return + + guanshi_fencheng = order.guanshi_fencheng or Decimal('0') + if guanshi_fencheng <= 0: + return + + if Gsfenhong.objects.filter(dingdan_id=order.dingdan_id).exists(): + logger.info(f"商家订单管事分红已处理: {order.dingdan_id}") + return + + if not dashou_id: + return + + try: + dashou = UserDashou.objects.select_related('user').get(user__UserUID=dashou_id) + except UserDashou.DoesNotExist: + logger.info(f"商家订单管事分红跳过: 打手{dashou_id}不存在") + return + + guanshi_id = dashou.yaoqingren + if not guanshi_id: + logger.info(f"商家订单管事分红跳过: 打手{dashou_id}无邀请管事") + return + + try: + with transaction.atomic(): + guanshi = UserGuanshi.objects.select_for_update().get(user__UserUID=guanshi_id) + UserGuanshi.objects.filter(id=guanshi.id).update( + yue=F('yue') + guanshi_fencheng, + chongzhifenrun=F('chongzhifenrun') + guanshi_fencheng, + ) + + user_main = dashou.user + Gsfenhong.objects.create( + dingdan_id=order.dingdan_id, + guanshi=guanshi_id, + dashouid=dashou_id, + shuoming='商家订单分红', + fenhong=guanshi_fencheng, + avatar=user_main.avatar if user_main else None, + nicheng=dashou.nicheng or '未知打手', + fenhong_leixing=3, + ) + logger.info( + f"商家订单管事分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, " + f"打手{dashou_id}, 金额{guanshi_fencheng}元" + ) + except UserGuanshi.DoesNotExist: + logger.warning(f"商家订单管事分红跳过: 管事{guanshi_id}不存在") + return + except Exception as e: + logger.error(f"商家订单管事分红失败: {e}", exc_info=True) + return + + try: + update_guanshi_daily_by_action( + yonghuid=guanshi_id, + action=3, + amount=guanshi_fencheng, + ) + except Exception as e: + logger.error(f"商家订单管事日统计更新失败: {e}") diff --git a/orders/views.py b/orders/views.py index 22d3516..c363645 100644 --- a/orders/views.py +++ b/orders/views.py @@ -62,7 +62,7 @@ from orders.models import ( Lilubiao, Pingfen, Tuikuanjilu ) from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai -from users.models import UserDashou, UserShangjia, UserBoss +from users.models import UserDashou, UserShangjia, UserBoss from gvsdsdk.models import User from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao from config.models import ( @@ -241,7 +241,7 @@ class WechatPayNotifyView(APIView): if hasattr(dingdan, 'pingtai_kuozhan'): laoban_id = dingdan.pingtai_kuozhan.laoban_id if laoban_id: - user_main = User.query.filter(yonghuid=laoban_id).first() + user_main = User.query.filter(UserUID=laoban_id).first() if user_main and hasattr(user_main, 'boss_profile'): boss = user_main.boss_profile boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0) @@ -525,7 +525,7 @@ class PaymentVerifyView(APIView): # 更新老板扩展表 laoban_id = dingdan.pingtai_kuozhan.laoban_id if hasattr(dingdan, 'pingtai_kuozhan') else None if laoban_id: - user_main = User.query.filter(yonghuid=laoban_id).first() + user_main = User.query.filter(UserUID=laoban_id).first() if user_main and hasattr(user_main, 'boss_profile'): boss = user_main.boss_profile boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0) @@ -712,7 +712,7 @@ class CreateOrderView(APIView): if zhiding: # 查询主表中是否存在该打手 zhiding_user = User.query.filter( - yonghuid=zhiding, + UserUID=zhiding, #user_type='dashou' ).first() @@ -1349,7 +1349,7 @@ class JiedanView(APIView): # 7. 更新打手扩展表(如果有打手ID和分成) if jiedan_dashou_id and dashou_fencheng > 0: # 查询打手用户 - dashou_user = User.query.filter(yonghuid=jiedan_dashou_id).first() + dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first() if dashou_user: # 查询打手扩展表 @@ -1512,7 +1512,7 @@ class DingdanXiangqingView2(APIView): jiedan_time = '' tijiao_time = '' if jiedan_dashou_id: - dashou_user = User.query.filter(yonghuid=jiedan_dashou_id).first() + dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first() if dashou_user: dashou_touxiang = dashou_user.avatar or '' try: @@ -1661,7 +1661,7 @@ class JiedanView2(APIView): # 9. 更新打手收益与状态 if jiedan_dashou_id and dashou_fencheng > 0: - dashou_user = User.query.filter(yonghuid=jiedan_dashou_id).first() + dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first() if dashou_user: try: dashou_ext = dashou_user.dashou_profile @@ -1916,7 +1916,7 @@ class ShangjiaPaifaView(APIView): zhiding_dashou = None if zhiding_uid: try: - zhiding_user = User.query.get(yonghuid=zhiding_uid) + zhiding_user = User.query.get(UserUID=zhiding_uid) zhiding_dashou = zhiding_user.dashou_profile if zhiding_dashou.zhanghaozhuangtai != 1: return Response({'code': 400, 'msg': '指定打手状态异常'}) @@ -2223,7 +2223,7 @@ class ShangjiaDingdanXiangqingView(APIView): dashou_avatar = '' if dashou_yonghuid: dashou_user = User.query.filter( - yonghuid=dashou_yonghuid + UserUID=dashou_yonghuid ).select_related('dashou_profile').first() if dashou_user: dashou_avatar = dashou_user.avatar or '' @@ -2426,7 +2426,7 @@ class ShangjiaJiesuanView(APIView): # ========== 本地订单正常结算给打手 ========== # 查询打手信息 try: - dashou_user = User.query.get(yonghuid=jiedan_dashou_id) + dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou = dashou_user.dashou_profile except (User.DoesNotExist, UserDashou.DoesNotExist): return Response({ @@ -2769,7 +2769,7 @@ class ShangjiaTuikuanShenqingView(APIView): if dingdan.jiedan_dashou_id: try: dashou_main = User.query.filter( - yonghuid=dingdan.jiedan_dashou_id + UserUID=dingdan.jiedan_dashou_id ).first() if dashou_main and hasattr(dashou_main, 'dashou_profile'): dashou_profile = dashou_main.dashou_profile @@ -2851,7 +2851,7 @@ class ShangjiaChufaShenqingView(APIView): with transaction.atomic(): # 验证打手身份 try: - dashou_user = User.query.get(yonghuid=dashou_id) + dashou_user = User.query.get(UserUID=dashou_id) dashou = dashou_user.dashou_profile except User.DoesNotExist: return Response({ @@ -3030,7 +3030,7 @@ class DashouDingdanHuoquView(APIView): zhiding_info = {} if zhiding_ids: dashou_query = User.query.filter( - yonghuid__in=zhiding_ids + UserUID__in=zhiding_ids ).select_related('dashou_profile').annotate( nicheng=F('dashou_profile__nicheng') ).values('yonghuid', 'avatar', 'nicheng') @@ -3082,7 +3082,7 @@ class DashouDingdanHuoquView(APIView): # 将 yonghuid 字符串转换为 User 的主键 id user_id_map = {} if yonghuid_set: - users = User.query.filter(yonghuid__in=yonghuid_set).values('id', 'yonghuid') + users = User.query.filter(UserUID__in=yonghuid_set).values('id', 'yonghuid') for u in users: user_id_map[u['yonghuid']] = u['id'] @@ -3347,7 +3347,7 @@ def send_group_message( def check_user_permission(identity_type, uid, order): """验证用户身份、账号状态、以及是否属于该订单""" try: - user = User.query.get(yonghuid=uid) + user = User.query.get(UserUID=uid) except User.DoesNotExist: return False, '用户不存在' @@ -4674,7 +4674,7 @@ class AdGengHuanDaShou(APIView): # 8. 验证新打手ID是否存在,并且是打手身份 try: new_dashou_user = User.query.get( - yonghuid=new_dashou_id, + UserUID=new_dashou_id, #user_type='dashou' ) except User.DoesNotExist: @@ -4769,7 +4769,7 @@ class AdGengHuanDaShou(APIView): try: # 查询原打手主表 old_dashou_user = User.query.get( - yonghuid=old_dashou_id, + UserUID=old_dashou_id, #user_type='dashou' ) @@ -4913,7 +4913,7 @@ class AdQiangZhiJieDan(APIView): try: # 查询打手用户主表 dashou_user = User.query.get( - yonghuid=jiedan_dashou_id, + UserUID=jiedan_dashou_id, #user_type='dashou' ) @@ -4956,7 +4956,7 @@ class AdQiangZhiJieDan(APIView): if shangjia_id: # 查询商家用户主表 shangjia_user = User.query.get( - yonghuid=shangjia_id, + UserUID=shangjia_id, #user_type='shop' ) @@ -5089,7 +5089,7 @@ class AdJuJueTuiKuan(APIView): try: # 查询打手用户主表 dashou_user = User.query.get( - yonghuid=jiedan_dashou_id, + UserUID=jiedan_dashou_id, #user_type='dashou' ) @@ -5132,7 +5132,7 @@ class AdJuJueTuiKuan(APIView): if shangjia_id: # 查询商家用户主表 shangjia_user = User.query.get( - yonghuid=shangjia_id, + UserUID=shangjia_id, #user_type='shop' ) @@ -5320,7 +5320,7 @@ class AdTongYiTuiKuanShangJia(APIView): try: # 查询打手用户主表 dashou_user = User.query.get( - yonghuid=jiedan_dashou_id, + UserUID=jiedan_dashou_id, #user_type='dashou' ) @@ -5345,7 +5345,7 @@ class AdTongYiTuiKuanShangJia(APIView): try: # 查询商家用户主表 shangjia_user = User.query.get( - yonghuid=shangjia_id, + UserUID=shangjia_id, #user_type='shop' ) @@ -5529,7 +5529,7 @@ class AdTongYiTuiKuanPingTai(APIView): # 14. 更新打手扩展表 if jiedan_dashou_id: try: - dashou_user = User.query.get(yonghuid=jiedan_dashou_id) + dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.zhuangtai = 1 # 打手状态设为空闲 dashou_profile.tuikuanliang += 1 @@ -5540,7 +5540,7 @@ class AdTongYiTuiKuanPingTai(APIView): # 15. 更新老板扩展表 if laoban_id: try: - laoban_user = User.query.get(yonghuid=laoban_id) + laoban_user = User.query.get(UserUID=laoban_id) laoban_profile = laoban_user.boss_profile laoban_profile.alltui += 1 laoban_profile.zonge -= jine @@ -5865,7 +5865,7 @@ class AdJuJueJieSuan(APIView): # 8. 更新打手扩展表 try: - dashou_user = User.query.get(yonghuid=jiedan_dashou_id) + dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.zhuangtai = 1 # 打手状态设为正常 dashou_profile.tuikuanliang += 1 # 退款订单总量+1 @@ -5947,7 +5947,7 @@ class AdZhuanYiDaTing(APIView): # 如果有接单打手,更新其扩展表 if jiedan_dashou_id: try: - dashou_user = User.query.get(yonghuid=jiedan_dashou_id) + dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.zhuangtai = 1 dashou_profile.tuikuanliang += 1 @@ -6225,7 +6225,7 @@ class ZxsjghdsView(APIView): # 6.2 释放打手 if dashou_id: try: - dashou_user = User.query.get(yonghuid=dashou_id) + dashou_user = User.query.get(UserUID=dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1 dashou_profile.zhuangtai = 1 # 空闲 diff --git a/products/migrations/0001_initial.py b/products/migrations/0001_initial.py index a4e079f..33235d9 100644 --- a/products/migrations/0001_initial.py +++ b/products/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.27 on 2026-06-17 16:31 +# Generated by Django 4.2.27 on 2026-06-17 21:18 from django.db import migrations, models diff --git a/products/migrations/0002_initial.py b/products/migrations/0002_initial.py index de0ec63..4fc360b 100644 --- a/products/migrations/0002_initial.py +++ b/products/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.27 on 2026-06-17 16:31 +# Generated by Django 4.2.27 on 2026-06-17 21:18 from django.db import migrations, models import django.db.models.deletion @@ -9,9 +9,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('products', '0001_initial'), ('rank', '0001_initial'), ('shop', '0001_initial'), + ('products', '0001_initial'), ] operations = [ diff --git a/products/views.py b/products/views.py index 7eba313..4b3af19 100644 --- a/products/views.py +++ b/products/views.py @@ -1,6404 +1,6404 @@ -# ==================== 标准库 ==================== -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 orders.utils import update_daily_income -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 gvsdsdk.models import User - -from orders.models import Lilubiao -from config.models import Szjilu, Gonggao, Lunbo - -from orders.models import Fadan # 用于罚款类型 - -import traceback - -# ==================== 日志实例 ==================== -logger = logging.getLogger(__name__) - - -class ShangpinHuoquView(APIView): - """ - 商品数据获取接口 - 一次性返回商品类型、商品专区、商品列表 - """ - - # 应用节流(限制频率) - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - - def post(self, request): - """ - 处理POST请求,返回所有商品相关数据 - """ - try: - logger.info("收到商品数据获取请求") - - # 1. 查询商品类型(只查询正常状态的类型,并排序) - # 正常运营:只显示 shenhezhuangtai=2 的商品类型 - leixing_queryset = ShangpinLeixing.query.filter( - shenhezhuangtai=1 # 只取正常状态 - ).order_by('-paixu', 'id') # 先按paixu降序,再按id升序 - - # 注意:审核期间使用下面这行代码(先注释掉) - '''leixing_queryset = ShangpinLeixing.query.filter( - shenhezhuangtai=2 # 审核期间:只取审核专用状态 - ).order_by('-paixu', 'id')''' - - shangpinleixing_data = [] - for leixing in leixing_queryset: - shangpinleixing_data.append({ - "id": leixing.id, - "jieshao": leixing.jieshao, - "tupian_url": leixing.tupian_url, - "paixu": leixing.paixu - }) - - # 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 # 🔴【添加】只筛选审核状态为2的专区 - ).order_by('-paixu', 'id') # 先按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 # 🔴【添加】只筛选审核状态为2的商品 - ).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 = { - "tupian": shangpin.tupian_url, - "biaoti": shangpin.biaoqian, - "jiage": shangpin.jiage, - "xiangqing": shangpin.jieshao, - "xiadanxuzhi": shangpin.xiadan_xuzhi, - "guize": shangpin.guize_tupian, - "xiaoliang": shangpin.duiwai_xiaoliang, - "kucun": shangpin.kucun, - } - - 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.dashou_profile - except UserDashou.DoesNotExist: - logger.warning(f"用户{request.user.yonghuid}不是打手,尝试访问打手接口") - return Response({ - 'code': 400, - 'message': '用户不是打手,无法访问此接口' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 检查打手账号状态 - if dashou.zhanghaozhuangtai != 1: - logger.warning(f"打手{request.user.yonghuid}账号状态异常: {dashou.zhanghaozhuangtai}") - return Response({ - 'code': 400, - 'message': '账号状态异常,无法购买会员' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 查询所有会员数据(ORM查询,确保性能) - huiyuan_queryset = Huiyuan.query.all().only( - 'huiyuan_id', # 会员ID - 'jieshao', # 会员名字(注意:jieshao字段实际存储的是会员名字) - 'jtjieshao', # 会员具体介绍 - 'jiage' # 会员价格 - ).order_by('jiage') # 按价格排序 - - # 4. 构建返回数据列表 - huiyuan_list = [] - for huiyuan in huiyuan_queryset: - huiyuan_list.append({ - 'id': huiyuan.huiyuan_id, # 会员ID(对应前端id) - 'mingzi': huiyuan.jieshao, # 会员名字(对应前端mingzi) - 'jiage': float(huiyuan.jiage), # 会员价格(转为浮点数) - 'jieshao': huiyuan.jtjieshao # 会员介绍(对应前端jieshao) - }) - - # 5. 返回数据 - return Response({ - 'code': 200, - 'message': '获取会员列表成功', - 'data': huiyuan_list # 对应前端接收的列表 - }, 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 YajinGoumai(APIView): - """ - 押金充值支付参数接口 - 路径:/shangpin/yajingoumai - 请求方式:POST - 权限:JWT认证 - 参数:jine(金额,1-10000元) - 返回:支付参数(payParams)和订单ID(dingdanid) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 验证用户身份和状态 - try: - dashou = request.user.dashou_profile - except UserDashou.DoesNotExist: - return Response({ - 'code': 400, - 'message': '用户不是打手,无法充值押金' - }, status=status.HTTP_400_BAD_REQUEST) - - if dashou.zhanghaozhuangtai != 1: - return Response({ - 'code': 400, - 'message': '账号状态异常,无法进行充值' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 获取并验证金额参数 - jine = request.data.get('jine') - if not jine: - return Response({ - 'code': 400, - 'message': '请填写充值金额' - }, status=status.HTTP_400_BAD_REQUEST) - - try: - jine = int(jine) - except (ValueError, TypeError): - return Response({ - 'code': 400, - 'message': '金额格式错误,请填写1-10000的整数' - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证金额范围 - if jine < 1 or jine > 10000: - return Response({ - 'code': 400, - 'message': '充值金额必须在1-10000元之间' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 生成订单ID(使用你提供的算法,前缀改为CzYJ) - timestamp_ms = int(time.time() * 1000) - timestamp_ns = int(time.time_ns() // 1000) % 1000000 - random_num = random.randint(0, 99) - - dingdanid = f"CzYJ{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" - - # 4. 创建充值记录订单 - chongzhi_jilu = Czjilu.query.create( - dingdan_id=dingdanid, - zhuangtai=9, # 未支付状态 - yonghuid=request.user.yonghuid, - jine=jine, - leixing=2, # 充值类型:2=押金 - shuoming=settings.YAJIN_PAY_DESCRIPTION - ) - - logger.info(f"创建押金充值订单: 用户{request.user.yonghuid}, 订单{dingdanid}, 金额{jine}元") - - # 5. 生成微信支付参数 - try: - pay_params = self.generate_wechat_pay_params( - dingdanid=dingdanid, - jine=jine, - openid=request.user.openid, # 直接从request.user获取openid - pay_type='yajin' - ) - except Exception as e: - # 支付参数生成失败,删除订单记录 - chongzhi_jilu.delete() - logger.error(f"生成微信支付参数失败: {str(e)}") - return Response({ - 'code': 500, - 'message': f'支付参数生成失败: {str(e)}' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 6. 返回支付参数和订单ID - return Response({ - 'code': 200, - 'message': '支付参数生成成功', - 'payParams': pay_params, # 支付参数(前端接收为payParams) - 'dingdanid': dingdanid # 订单ID(前端接收为dingdanid) - }, 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) - - def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): - """生成微信小程序支付参数""" - # 获取配置 - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO - - # 根据支付类型获取对应的配置 - if pay_type == 'yajin': - PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION - NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL - elif pay_type == 'jifen': - PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION - NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL - elif pay_type == 'huiyuan': - PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION - NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL - else: - raise Exception('未知的支付类型') - - # 生成随机字符串 - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - - # 金额转换(元转分) - total_fee = int(jine * 100) - - # 交易类型(小程序支付使用JSAPI) - trade_type = 'JSAPI' - - # 获取客户端IP - user_ip = self.get_client_ip() - - # 1. 第一次签名(统一下单接口) - sign_data = { - 'appid': APPID, - 'mch_id': MCHID, - 'nonce_str': nonce_str, - 'body': PAY_DESCRIPTION, - 'out_trade_no': dingdanid, - 'total_fee': total_fee, - 'spbill_create_ip': user_ip, - 'notify_url': NOTIFY_URL, - 'trade_type': trade_type, - 'openid': openid # 小程序支付必须传openid - } - - # 按照参数名ASCII码从小到大排序 - sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) - - # 拼接字符串 - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) - sign_string += f'&key={KEY}' - - # MD5签名 - sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - # 构造XML数据 - xml_data = f""" -{APPID} -{PAY_DESCRIPTION} -{MCHID} -{nonce_str} -{NOTIFY_URL} -{openid} -{dingdanid} -{user_ip} -{total_fee} -{trade_type} -{sign} -""" - - # 调用微信统一下单接口(官方地址) - response = requests.post( - 'https://api.mch.weixin.qq.com/pay/unifiedorder', - data=xml_data.encode('utf-8'), - headers={'Content-Type': 'application/xml'}, - timeout=10 # 设置超时 - ) - - # 解析返回的XML - root = ET.fromstring(response.content) - - return_code = root.find('return_code').text - result_code = root.find('result_code').text - - if return_code == 'SUCCESS' and result_code == 'SUCCESS': - # 获取prepay_id - prepay_id = root.find('prepay_id').text - - # 2. 第二次签名(小程序支付参数) - timestamp = str(int(time.time())) - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - package = f'prepay_id={prepay_id}' - sign_type = 'MD5' - - # 生成支付签名(注意:这里key的大小写和字段名与第一次不同) - pay_sign_data = { - 'appId': APPID, # 注意:这里是appId,不是appid - 'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type - } - - sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) - pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) - pay_sign_string += f'&key={KEY}' - pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() - - # 返回前端需要的支付参数 - return { - 'appId': APPID, # 对应前端:appId - 'timeStamp': timestamp, # 对应前端:timeStamp - 'nonceStr': nonce_str, # 对应前端:nonceStr - 'package': package, # 对应前端:package - 'signType': sign_type, # 对应前端:signType - 'paySign': pay_sign # 对应前端:paySign - } - else: - # 获取错误信息 - err_code = root.find('err_code') - err_code_des = root.find('err_code_des') - return_msg = root.find('return_msg') - - error_details = f"return_code: {return_code}" - if err_code is not None: - error_details += f", err_code: {err_code.text}" - if err_code_des is not None: - error_details += f", err_code_des: {err_code_des.text}" - if return_msg is not None: - error_details += f", return_msg: {return_msg.text}" - - raise Exception(f"微信支付下单失败: {error_details}") - - def get_client_ip(self): - """获取客户端IP(从DRF的request对象中获取)""" - request = self.request - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip if ip else '127.0.0.1' - - - - - -class YajinHuitiao(View): - """ - 押金支付回调接口 - 路径:/shangpin/yajinhuitiao - 注意:这个接口不需要JWT认证,由微信服务器调用 - """ - - def post(self, request): - try: - # 1. 解析微信回调XML数据 - request_body = request.body - logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}") - - root = ET.fromstring(request_body) - - # 获取关键字段 - return_code = root.find('return_code').text - result_code = root.find('result_code').text - out_trade_no = root.find('out_trade_no').text # 订单ID - transaction_id = root.find('transaction_id').text # 微信交易号 - total_fee = int(root.find('total_fee').text) # 支付金额(分) - - logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") - - # 2. 验证基本返回状态 - if return_code != 'SUCCESS' or result_code != 'SUCCESS': - logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}") - return self.wechat_response('FAIL', '支付失败') - - # 3. 验证签名(确保是微信发来的) - if not self.verify_signature(request_body): - logger.error(f"签名验证失败: {out_trade_no}") - return self.wechat_response('FAIL', '签名验证失败') - - # 4. 查询订单 - try: - order = Czjilu.query.get(dingdan_id=out_trade_no) - logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") - except Czjilu.DoesNotExist: - logger.error(f"订单不存在: {out_trade_no}") - return self.wechat_response('FAIL', '订单不存在') - - # 5. 检查订单状态是否为未支付(9) - if order.zhuangtai != 9: - logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") - # 已处理过的订单直接返回成功,避免微信重复回调 - return self.wechat_response('SUCCESS', 'OK') - - # 6. 验证金额(微信返回的是分,需要转换) - wechat_amount_yuan = total_fee / 100 # 转换为元 - order_amount_yuan = float(order.jine) # 订单金额(元) - - # 允许1分钱的误差(浮点数比较) - if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: - logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") - return self.wechat_response('FAIL', '金额不一致') - - # 7. 更新订单状态为已支付(3) - order.zhuangtai = 3 - order.save(update_fields=['zhuangtai']) - logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3") - - # 8. 更新打手押金 - try: - # 通过用户ID查找打手(高效查询) - dashou = UserDashou.query.get(user__yonghuid=order.yonghuid) - - # 增加押金(使用Decimal确保精度) - dashou.yajin += Decimal(str(order_amount_yuan)) - dashou.save(update_fields=['yajin']) - - logger.info(f"打手押金更新成功: 用户{order.yonghuid}, 增加{order_amount_yuan}元, 新押金{dashou.yajin}") - - # 8b. 押金分红(仅微信确认到账后;费率 Lilubiao fadanpingtai=12) - try: - - guanshi_id = dashou.yaoqingren - if not guanshi_id: - logger.info(f"押金分红跳过: 打手{order.yonghuid}无邀请管事") - elif Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists(): - logger.info(f"押金分红已处理: 订单{order.dingdan_id}") - else: - lilu_obj = Lilubiao.query.filter(fadanpingtai='12').first() - rate = lilu_obj.lilu if lilu_obj and lilu_obj.lilu is not None else Decimal('0') - if rate <= 0: - logger.info(f"押金分红跳过: 费率未配置或为0") - else: - recharge_amount = Decimal(str(order_amount_yuan)) - fenhong_jine = (recharge_amount * rate).quantize(Decimal('0.01')) - if fenhong_jine > 0: - with transaction.atomic(): - guanshi = UserGuanshi.objects.select_for_update().get( - user__yonghuid=guanshi_id - ) - guanshi.yue += fenhong_jine - guanshi.chongzhifenrun += fenhong_jine - guanshi.save(update_fields=['yue', 'chongzhifenrun']) - - user_main = dashou.user - Gsfenhong.query.create( - dingdan_id=order.dingdan_id, - guanshi=guanshi_id, - dashouid=order.yonghuid, - shuoming='押金分红', - fenhong=fenhong_jine, - avatar=user_main.avatar, - nicheng=dashou.nicheng or '未知打手', - fenhong_leixing=2, - ) - logger.info( - f"押金分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, " - f"打手{order.yonghuid}, 金额{fenhong_jine}元" - ) - try: - update_guanshi_daily_by_action( - yonghuid=guanshi_id, - action=4, - amount=fenhong_jine, - ) - except Exception as e: - logger.error(f"押金分红管事日统计更新失败: {e}") - except UserGuanshi.DoesNotExist: - logger.warning(f"押金分红跳过: 管事{guanshi_id}不存在") - except Exception as e: - logger.error(f"押金分红处理失败: {str(e)}", exc_info=True) - - except UserDashou.DoesNotExist: - # 如果找不到打手,记录错误,但依然返回成功(避免微信重复回调) - logger.error(f"打手信息不存在: 用户{order.yonghuid}") - return self.wechat_response('SUCCESS', 'OK') - except Exception as e: - logger.error(f"更新打手押金失败: {str(e)}", exc_info=True) - return self.wechat_response('FAIL', '更新押金失败') - - # ==== 在返回成功响应之前添加这段代码 ==== - - # 更新收支记录表 - try: - # 获取订单金额(确保是Decimal类型) - jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - - # 查询或创建ID=1的收支记录 - update_daily_income(jine_decimal) - - szjilu, created = Szjilu.query.get_or_create( - id=1, - defaults={ - 'zongsy': Decimal('0.00'), - 'zongls': Decimal('0.00'), - 'zongzc': Decimal('0.00'), - 'jrzc': Decimal('0.00'), - 'jrls': Decimal('0.00') - } - ) - - # 更新三个字段 - szjilu.zongsy += jine_decimal # 总收益 - szjilu.zongls += jine_decimal # 总流水 - szjilu.jrls += jine_decimal # 今日流水 - - szjilu.save() - - logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") - - except Exception as e: - # 收支记录更新失败不影响主流程,只记录日志 - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - - # ==== 添加结束 ==== - - # 9. 返回成功响应给微信 - logger.info(f"押金支付回调处理完成: {out_trade_no}") - return self.wechat_response('SUCCESS', 'OK') - - except ET.ParseError as e: - logger.error(f"XML解析失败: {str(e)}") - return self.wechat_response('FAIL', '数据格式错误') - except Exception as e: - logger.error(f"押金支付回调处理异常: {str(e)}", exc_info=True) - # 返回FAIL让微信重试 - return self.wechat_response('FAIL', '处理异常') - - def verify_signature(self, xml_data): - """验证微信支付签名""" - try: - root = ET.fromstring(xml_data) - sign = root.find('sign').text - - if not sign: - logger.error("签名字段不存在") - return False - - # 获取所有参数并排除sign - params = {} - for child in root: - if child.tag != 'sign' and child.text is not None: - params[child.tag] = child.text - - # 按照参数名ASCII码从小到大排序 - sorted_params = sorted(params.items(), key=lambda x: x[0]) - - # 拼接字符串 - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' - - # 计算签名 - calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - is_valid = sign == calculated_sign - if not is_valid: - logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") - - return is_valid - - except Exception as e: - logger.error(f"验证签名异常: {str(e)}") - return False - - def wechat_response(self, return_code, return_msg): - """返回微信支付回调响应""" - xml_response = f""" - - -""" - - logger.info(f"返回微信响应: {return_code} - {return_msg}") - return HttpResponse(xml_response, content_type='application/xml') - - - - -# views.py -class YajinShibai(APIView): - """ - 押金支付失败处理接口 - 路径:/shangpin/yajinshibai - 请求方式:POST - 权限:JWT认证 - 参数:dingdanid(订单ID) - 返回:删除结果 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取订单ID - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({ - 'code': 400, - 'message': '订单ID不能为空' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 查询订单(确保是当前用户的订单) - try: - order = Czjilu.query.get( - dingdan_id=dingdanid, - yonghuid=request.user.yonghuid - ) - except Czjilu.DoesNotExist: - return Response({ - 'code': 400, - 'message': '订单不存在或不属于当前用户' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 检查订单状态是否为未支付(9) - if order.zhuangtai == 9: - # 删除订单 - order.delete() - logger.info(f"用户{request.user.yonghuid}删除未支付订单: {dingdanid}") - - return Response({ - 'code': 200, - 'message': '订单已删除' - }, status=status.HTTP_200_OK) - else: - return Response({ - 'code': 400, - 'message': '订单状态不是未支付,无法删除' - }, status=status.HTTP_400_BAD_REQUEST) - - except Exception as e: - logger.error(f"押金支付失败处理异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': '服务器内部错误' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -# views.py -class YajinLunxun(APIView): - """ - 押金支付成功轮询接口 - 路径:/shangpin/yajinlunxun - 请求方式:POST - 权限:JWT认证 - 参数:dingdanid(订单ID) - 返回:订单状态和当前押金 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取订单ID - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({ - 'code': 400, - 'message': '订单ID不能为空' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 查询订单(确保是当前用户的订单) - try: - order = Czjilu.query.get( - dingdan_id=dingdanid, - yonghuid=request.user.yonghuid - ) - except Czjilu.DoesNotExist: - return Response({ - 'code': 400, - 'message': '订单不存在或不属于当前用户' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 检查订单状态是否为已支付(3) - if order.zhuangtai != 3: - logger.info(f"订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") - return Response({ - 'code': 400, - 'message': '订单尚未支付完成' - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 获取打手押金(通过反向查询,最快捷的方式) - try: - dashou = request.user.dashou_profile - yajin = float(dashou.yajin) # 转为浮点数方便前端处理 - - logger.info(f"轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}押金为{yajin}元") - - return Response({ - 'code': 200, - 'message': '支付流程全部完成', - 'yajin': yajin # 返回当前押金(前端需要更新全局变量) - }, status=status.HTTP_200_OK) - - except UserDashou.DoesNotExist: - logger.error(f"打手信息不存在: 用户{request.user.yonghuid}") - return Response({ - 'code': 400, - 'message': '打手信息不存在' - }, status=status.HTTP_400_BAD_REQUEST) - - except Exception as e: - logger.error(f"押金轮询异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': '服务器内部错误' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -# views.py -class JifenBuchong(APIView): - """ - 积分补充支付参数接口 - 路径:/shangpin/jifenbc - 请求方式:POST - 权限:JWT认证 - 参数:无(固定5元) - 返回:支付参数(payParams)和订单ID(dingdanid) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 验证用户身份和状态 - try: - dashou = request.user.dashou_profile - except UserDashou.DoesNotExist: - return Response({ - 'code': 400, - 'message': '用户不是打手,无法补充积分' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 检查打手账号状态 - if dashou.zhanghaozhuangtai != 1: - return Response({ - 'code': 400, - 'message': '账号状态异常,无法进行操作' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 检查积分是否已满(满分10分) - if dashou.jifen >= 10: - return Response({ - 'code': 400, - 'message': '积分已满,无需补充' - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 生成订单ID(前缀CzJF表示积分充值) - timestamp_ms = int(time.time() * 1000) - timestamp_ns = int(time.time_ns() // 1000) % 1000000 - random_num = random.randint(0, 99) - - dingdanid = f"CzJF{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" - - # 5. 创建积分充值记录订单(固定5元) - jine = 5.00 # 固定5元 - chongzhi_jilu = Czjilu.query.create( - dingdan_id=dingdanid, - zhuangtai=9, # 未支付状态 - yonghuid=request.user.yonghuid, - jine=jine, - leixing=3, # 充值类型:3=积分 - shuoming=settings.JIFEN_PAY_DESCRIPTION - ) - - logger.info(f"创建积分充值订单: 用户{request.user.yonghuid}, 订单{dingdanid}, 金额{jine}元") - - # 6. 生成微信支付参数(完整版) - try: - pay_params = self.generate_wechat_pay_params( - dingdanid=dingdanid, - jine=jine, - openid=request.user.openid, - pay_type='jifen' - ) - except Exception as e: - # 支付参数生成失败,删除订单记录 - chongzhi_jilu.delete() - logger.error(f"生成积分支付参数失败: {str(e)}") - return Response({ - 'code': 500, - 'message': f'支付参数生成失败: {str(e)}' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 7. 返回支付参数和订单ID - return Response({ - 'code': 200, - 'message': '支付参数生成成功', - 'payParams': pay_params, # 对应前端payParams - 'dingdanid': dingdanid # 对应前端dingdanid - }, 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) - - def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): - """生成微信支付参数(完整版,无省略)""" - # 微信支付配置 - 从settings中获取 - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO - - # 根据支付类型获取对应的配置 - if pay_type == 'yajin': - PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION - NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL - elif pay_type == 'jifen': - PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION - NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL - elif pay_type == 'huiyuan': - PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION - NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL - else: - raise Exception('未知的支付类型') - - # 生成随机字符串 - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - - # 金额转换(元转分) - total_fee = int(jine * 100) - - # 交易类型(小程序支付使用JSAPI) - trade_type = 'JSAPI' - - # 获取客户端IP - user_ip = self.get_client_ip() - - # 1. 第一次签名(统一下单接口) - sign_data = { - 'appid': APPID, - 'mch_id': MCHID, - 'nonce_str': nonce_str, - 'body': PAY_DESCRIPTION, - 'out_trade_no': dingdanid, - 'total_fee': total_fee, - 'spbill_create_ip': user_ip, - 'notify_url': NOTIFY_URL, - 'trade_type': trade_type, - 'openid': openid # 小程序支付必须传openid - } - - # 按照参数名ASCII码从小到大排序 - sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) - - # 拼接字符串 - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) - sign_string += f'&key={KEY}' - - # MD5签名 - sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - # 构造XML数据 - xml_data = f""" -{APPID} -{PAY_DESCRIPTION} -{MCHID} -{nonce_str} -{NOTIFY_URL} -{openid} -{dingdanid} -{user_ip} -{total_fee} -{trade_type} -{sign} -""" - - # 调用微信统一下单接口(官方地址) - response = requests.post( - 'https://api.mch.weixin.qq.com/pay/unifiedorder', - data=xml_data.encode('utf-8'), - headers={'Content-Type': 'application/xml'}, - timeout=10 # 设置超时 - ) - - # 解析返回的XML - root = ET.fromstring(response.content) - - return_code = root.find('return_code').text - result_code = root.find('result_code').text - - if return_code == 'SUCCESS' and result_code == 'SUCCESS': - # 获取prepay_id - prepay_id = root.find('prepay_id').text - - # 2. 第二次签名(小程序支付参数) - timestamp = str(int(time.time())) - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - package = f'prepay_id={prepay_id}' - sign_type = 'MD5' - - # 生成支付签名(注意:这里key的大小写和字段名与第一次不同) - pay_sign_data = { - 'appId': APPID, # 注意:这里是appId,不是appid - 'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type - } - - sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) - pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) - pay_sign_string += f'&key={KEY}' - pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() - - # 返回前端需要的支付参数 - return { - 'appId': APPID, # 对应前端:appId - 'timeStamp': timestamp, # 对应前端:timeStamp - 'nonceStr': nonce_str, # 对应前端:nonceStr - 'package': package, # 对应前端:package - 'signType': sign_type, # 对应前端:signType - 'paySign': pay_sign # 对应前端:paySign - } - else: - # 获取错误信息 - err_code = root.find('err_code') - err_code_des = root.find('err_code_des') - return_msg = root.find('return_msg') - - error_details = f"return_code: {return_code}" - if err_code is not None: - error_details += f", err_code: {err_code.text}" - if err_code_des is not None: - error_details += f", err_code_des: {err_code_des.text}" - if return_msg is not None: - error_details += f", return_msg: {return_msg.text}" - - raise Exception(f"微信支付下单失败: {error_details}") - - def get_client_ip(self): - """获取客户端IP(完整版)""" - request = self.request - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip if ip else '127.0.0.1' - - -# views.py -class JifenHuitiao(View): - """ - 积分支付回调接口 - 路径:/shangpin/jifenhuitiao - 注意:这个接口不需要JWT认证,由微信服务器调用 - """ - - def post(self, request): - try: - # 1. 解析微信回调XML数据 - request_body = request.body - logger.info(f"收到积分支付回调,原始数据: {request_body.decode('utf-8')}") - - root = ET.fromstring(request_body) - - # 获取关键字段 - return_code = root.find('return_code').text - result_code = root.find('result_code').text - out_trade_no = root.find('out_trade_no').text # 订单ID - transaction_id = root.find('transaction_id').text # 微信交易号 - total_fee = int(root.find('total_fee').text) # 支付金额(分) - - logger.info(f"积分回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") - - # 2. 验证基本返回状态 - if return_code != 'SUCCESS' or result_code != 'SUCCESS': - logger.warning(f"积分支付回调状态异常: {return_code}/{result_code}") - return self.wechat_response('FAIL', '支付失败') - - # 3. 验证签名 - if not self.verify_signature(request_body): - logger.error(f"积分签名验证失败: {out_trade_no}") - return self.wechat_response('FAIL', '签名验证失败') - - # 4. 查询订单 - try: - order = Czjilu.query.get(dingdan_id=out_trade_no) - logger.info(f"找到积分订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") - except Czjilu.DoesNotExist: - logger.error(f"积分订单不存在: {out_trade_no}") - return self.wechat_response('FAIL', '订单不存在') - - # 5. 检查订单状态是否为未支付(9) - if order.zhuangtai != 9: - logger.warning(f"积分订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") - return self.wechat_response('SUCCESS', 'OK') - - # 6. 验证金额(固定5元) - wechat_amount_yuan = total_fee / 100 # 转换为元 - order_amount_yuan = float(order.jine) # 订单金额(元) - - # 验证是否是5元(允许1分钱误差) - if abs(wechat_amount_yuan - 5.00) > 0.01: - logger.error(f"积分金额错误: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") - return self.wechat_response('FAIL', '金额错误') - - # ==== 在返回成功响应之前添加这段代码 ==== - - # 更新收支记录表 - try: - # 获取订单金额(确保是Decimal类型) - jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - update_daily_income(jine_decimal) - - # 查询或创建ID=1的收支记录 - szjilu, created = Szjilu.query.get_or_create( - id=1, - defaults={ - 'zongsy': Decimal('0.00'), - 'zongls': Decimal('0.00'), - 'zongzc': Decimal('0.00'), - 'jrzc': Decimal('0.00'), - 'jrls': Decimal('0.00') - } - ) - - # 更新三个字段 - szjilu.zongsy += jine_decimal # 总收益 - szjilu.zongls += jine_decimal # 总流水 - szjilu.jrls += jine_decimal # 今日流水 - - szjilu.save() - - logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") - - except Exception as e: - # 收支记录更新失败不影响主流程,只记录日志 - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - - # ==== 添加结束 ==== - - # 7. 更新订单状态为已支付(3) - order.zhuangtai = 3 - order.save(update_fields=['zhuangtai']) - logger.info(f"积分订单状态更新成功: {order.dingdan_id} -> 3") - - # 8. 更新打手积分 - try: - # 通过用户ID查找打手 - dashou = UserDashou.query.get(user__yonghuid=order.yonghuid) - - # 检查积分是否小于10 - if dashou.jifen < 10: - # 增加5积分,但不超过10 - new_jifen = dashou.jifen + 5 - dashou.jifen = min(new_jifen, 10) # 确保不超过10 - dashou.save(update_fields=['jifen']) - - logger.info(f"打手积分更新成功: 用户{order.yonghuid}, 增加5积分, 新积分{dashou.jifen}") - - - - - else: - logger.warning(f"打手积分已满: 用户{order.yonghuid}, 当前积分{dashou.jifen}") - - except UserDashou.DoesNotExist: - logger.error(f"打手信息不存在: 用户{order.yonghuid}") - return self.wechat_response('SUCCESS', 'OK') - except Exception as e: - logger.error(f"更新打手积分失败: {str(e)}", exc_info=True) - return self.wechat_response('FAIL', '更新积分失败') - - # 9. 返回成功响应给微信 - logger.info(f"积分支付回调处理完成: {out_trade_no}") - return self.wechat_response('SUCCESS', 'OK') - - except ET.ParseError as e: - logger.error(f"积分XML解析失败: {str(e)}") - return self.wechat_response('FAIL', '数据格式错误') - except Exception as e: - logger.error(f"积分支付回调处理异常: {str(e)}", exc_info=True) - return self.wechat_response('FAIL', '处理异常') - - def verify_signature(self, xml_data): - """验证微信支付签名""" - try: - root = ET.fromstring(xml_data) - sign = root.find('sign').text - - if not sign: - logger.error("积分数签名字段不存在") - return False - - # 获取所有参数并排除sign - params = {} - for child in root: - if child.tag != 'sign' and child.text is not None: - params[child.tag] = child.text - - # 按照参数名ASCII码从小到大排序 - sorted_params = sorted(params.items(), key=lambda x: x[0]) - - # 拼接字符串 - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' - - # 计算签名 - calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - is_valid = sign == calculated_sign - if not is_valid: - logger.error(f"积分签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") - - return is_valid - - except Exception as e: - logger.error(f"积分验证签名异常: {str(e)}") - return False - - def wechat_response(self, return_code, return_msg): - """返回微信支付回调响应""" - xml_response = f""" - - -""" - - logger.info(f"积分返回微信响应: {return_code} - {return_msg}") - return HttpResponse(xml_response, content_type='application/xml') - - -# views.py -class JifenShibai(APIView): - """ - 积分支付失败处理接口 - 路径:/shangpin/jifenshibai - 请求方式:POST - 权限:JWT认证 - 参数:dingdanid(订单ID) - 返回:删除结果 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取订单ID - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({ - 'code': 400, - 'message': '订单ID不能为空' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 查询订单(确保是当前用户的订单) - try: - order = Czjilu.query.get( - dingdan_id=dingdanid, - yonghuid=request.user.yonghuid - ) - except Czjilu.DoesNotExist: - return Response({ - 'code': 400, - 'message': '订单不存在或不属于当前用户' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 检查订单类型是否为积分(3) - if order.leixing != 3: - return Response({ - 'code': 400, - 'message': '订单类型错误,不是积分订单' - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 检查订单状态是否为未支付(9) - if order.zhuangtai == 9: - # 删除订单 - order.delete() - logger.info(f"用户{request.user.yonghuid}删除未支付积分订单: {dingdanid}") - - return Response({ - 'code': 200, - 'message': '积分订单已删除' - }, status=status.HTTP_200_OK) - else: - return Response({ - 'code': 400, - 'message': '积分订单状态不是未支付,无法删除' - }, status=status.HTTP_400_BAD_REQUEST) - - except Exception as e: - logger.error(f"积分支付失败处理异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': '服务器内部错误' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -# views.py -class JifenLunxun(APIView): - """ - 积分支付成功轮询接口 - 路径:/shangpin/jifenlunxun - 请求方式:POST - 权限:JWT认证 - 参数:dingdanid(订单ID) - 返回:订单状态和当前积分 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取订单ID - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({ - 'code': 400, - 'message': '订单ID不能为空' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 查询订单(确保是当前用户的订单) - try: - order = Czjilu.query.get( - dingdan_id=dingdanid, - yonghuid=request.user.yonghuid - ) - except Czjilu.DoesNotExist: - return Response({ - 'code': 400, - 'message': '订单不存在或不属于当前用户' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 检查订单类型是否为积分(3) - if order.leixing != 3: - return Response({ - 'code': 400, - 'message': '订单类型错误,不是积分订单' - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 检查订单状态是否为已支付(3) - if order.zhuangtai != 3: - logger.info(f"积分订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") - return Response({ - 'code': 400, - 'message': '积分订单尚未支付完成' - }, status=status.HTTP_400_BAD_REQUEST) - - # 5. 获取打手积分(通过反向查询) - try: - dashou = request.user.dashou_profile - jifen = dashou.jifen # 直接使用整数 - - logger.info(f"积分轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}积分为{jifen}") - - return Response({ - 'code': 200, - 'message': '支付流程全部完成', - 'jifen': jifen # 返回当前积分(前端需要更新全局变量) - }, status=status.HTTP_200_OK) - - except UserDashou.DoesNotExist: - logger.error(f"打手信息不存在: 用户{request.user.yonghuid}") - return Response({ - 'code': 400, - 'message': '打手信息不存在' - }, status=status.HTTP_400_BAD_REQUEST) - - 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 HuiyuanGoumai(APIView): - """ - 会员充值支付参数接口 - 路径:/shangpin/huiyuangm - 请求方式:POST - 权限:JWT认证 - 参数:huiyuanid(会员ID) - 返回:支付参数(payParams)和订单ID(dingdanid) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取会员ID参数 - huiyuanid = request.data.get('huiyuanid') - if not huiyuanid: - return Response({ - 'code': 400, - 'message': '会员ID不能为空' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 验证用户身份和状态 - try: - dashou = request.user.dashou_profile - except UserDashou.DoesNotExist: - return Response({ - 'code': 400, - 'message': '用户不是打手,无法购买会员' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 检查打手账号状态 - if dashou.zhanghaozhuangtai != 1: - return Response({ - 'code': 400, - 'message': '账号状态异常,无法进行购买' - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 查询会员信息 - try: - huiyuan = Huiyuan.query.get(huiyuan_id=huiyuanid) - except Huiyuan.DoesNotExist: - return Response({ - 'code': 400, - 'message': '会员信息不存在' - }, status=status.HTTP_400_BAD_REQUEST) - - # 5. 生成订单ID(前缀CzHY表示会员充值) - timestamp_ms = int(time.time() * 1000) - timestamp_ns = int(time.time_ns() // 1000) % 1000000 - random_num = random.randint(0, 99) - - dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" - - # 6. 创建充值记录订单 - jine = float(huiyuan.jiage) # 会员价格 - # 组合订单说明:会员介绍 + 配置描述 - shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}" - - chongzhi_jilu = Czjilu.query.create( - dingdan_id=dingdanid, - zhuangtai=9, # 未支付状态 - yonghuid=request.user.yonghuid, - jine=jine, - leixing=1, # 充值类型:1=会员 - shuoming=shuoming, - huiyuan_id=huiyuanid # 存储会员ID - ) - - logger.info( - f"创建会员充值订单: 用户{request.user.yonghuid}, 订单{dingdanid}, 会员{huiyuanid}, 金额{jine}元") - - # 7. 生成微信支付参数 - try: - pay_params = self.generate_wechat_pay_params( - dingdanid=dingdanid, - jine=jine, - openid=request.user.openid, - pay_type='huiyuan' - ) - except Exception as e: - # 支付参数生成失败,删除订单记录 - chongzhi_jilu.delete() - logger.error(f"生成会员支付参数失败: {str(e)}") - return Response({ - 'code': 500, - 'message': f'支付参数生成失败: {str(e)}' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 8. 返回支付参数和订单ID - return Response({ - 'code': 200, - 'message': '支付参数生成成功', - 'payParams': pay_params, # 对应前端payParams - 'dingdanid': dingdanid # 对应前端dingdanid - }, 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) - - def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): - """生成微信支付参数(完整版,无省略)""" - # 微信支付配置 - 从settings中获取 - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO - - # 根据支付类型获取对应的配置 - if pay_type == 'yajin': - PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION - NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL - elif pay_type == 'jifen': - PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION - NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL - elif pay_type == 'huiyuan': - PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION - NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL - else: - raise Exception('未知的支付类型') - - # 生成随机字符串 - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - - # 金额转换(元转分) - total_fee = int(jine * 100) - - # 交易类型(小程序支付使用JSAPI) - trade_type = 'JSAPI' - - # 获取客户端IP - user_ip = self.get_client_ip() - - # 1. 第一次签名(统一下单接口) - sign_data = { - 'appid': APPID, - 'mch_id': MCHID, - 'nonce_str': nonce_str, - 'body': PAY_DESCRIPTION, - 'out_trade_no': dingdanid, - 'total_fee': total_fee, - 'spbill_create_ip': user_ip, - 'notify_url': NOTIFY_URL, - 'trade_type': trade_type, - 'openid': openid # 小程序支付必须传openid - } - - # 按照参数名ASCII码从小到大排序 - sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) - - # 拼接字符串 - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) - sign_string += f'&key={KEY}' - - # MD5签名 - sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - # 构造XML数据 - xml_data = f""" -{APPID} -{PAY_DESCRIPTION} -{MCHID} -{nonce_str} -{NOTIFY_URL} -{openid} -{dingdanid} -{user_ip} -{total_fee} -{trade_type} -{sign} -""" - - # 调用微信统一下单接口(官方地址) - response = requests.post( - 'https://api.mch.weixin.qq.com/pay/unifiedorder', - data=xml_data.encode('utf-8'), - headers={'Content-Type': 'application/xml'}, - timeout=10 # 设置超时 - ) - - # 解析返回的XML - root = ET.fromstring(response.content) - - return_code = root.find('return_code').text - result_code = root.find('result_code').text - - if return_code == 'SUCCESS' and result_code == 'SUCCESS': - # 获取prepay_id - prepay_id = root.find('prepay_id').text - - # 2. 第二次签名(小程序支付参数) - timestamp = str(int(time.time())) - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - package = f'prepay_id={prepay_id}' - sign_type = 'MD5' - - # 生成支付签名(注意:这里key的大小写和字段名与第一次不同) - pay_sign_data = { - 'appId': APPID, # 注意:这里是appId,不是appid - 'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type - } - - sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) - pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) - pay_sign_string += f'&key={KEY}' - pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() - - # 返回前端需要的支付参数 - return { - 'appId': APPID, # 对应前端:appId - 'timeStamp': timestamp, # 对应前端:timeStamp - 'nonceStr': nonce_str, # 对应前端:nonceStr - 'package': package, # 对应前端:package - 'signType': sign_type, # 对应前端:signType - 'paySign': pay_sign # 对应前端:paySign - } - else: - # 获取错误信息 - err_code = root.find('err_code') - err_code_des = root.find('err_code_des') - return_msg = root.find('return_msg') - - error_details = f"return_code: {return_code}" - if err_code is not None: - error_details += f", err_code: {err_code.text}" - if err_code_des is not None: - error_details += f", err_code_des: {err_code_des.text}" - if return_msg is not None: - error_details += f", return_msg: {return_msg.text}" - - raise Exception(f"微信支付下单失败: {error_details}") - - def get_client_ip(self): - """获取客户端IP(完整版)""" - request = self.request - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip if ip else '127.0.0.1' - - - - - -class HuiyuanHuitiao(View): - """ - 会员支付回调接口 - 路径:/shangpin/huiyuanhuitiao - """ - - def post(self, request): - try: - # 1. 解析微信回调XML数据 - request_body = request.body - logger.info(f"收到会员支付回调,原始数据: {request_body.decode('utf-8')}") - - root = ET.fromstring(request_body) - - return_code = root.find('return_code').text - result_code = root.find('result_code').text - out_trade_no = root.find('out_trade_no').text - transaction_id = root.find('transaction_id').text - total_fee = int(root.find('total_fee').text) - - logger.info(f"会员回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") - - if return_code != 'SUCCESS' or result_code != 'SUCCESS': - logger.warning(f"会员支付回调状态异常: {return_code}/{result_code}") - return self.wechat_response('FAIL', '支付失败') - - if not self.verify_signature(request_body): - logger.error(f"会员签名验证失败: {out_trade_no}") - return self.wechat_response('FAIL', '签名验证失败') - - # 查询订单 - try: - order = Czjilu.query.get(dingdan_id=out_trade_no) - except Czjilu.DoesNotExist: - logger.error(f"会员订单不存在: {out_trade_no}") - return self.wechat_response('FAIL', '订单不存在') - - if order.leixing != 1: - logger.error(f"订单类型错误: {order.leixing}, 期望1(会员)") - return self.wechat_response('FAIL', '订单类型错误') - - if order.zhuangtai != 9: - logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") - return self.wechat_response('SUCCESS', 'OK') - - # 验证金额 - wechat_amount_yuan = total_fee / 100 - order_amount_yuan = float(order.jine) - if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: - logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") - return self.wechat_response('FAIL', '金额不一致') - - # 查询会员信息 - try: - huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id) - except Huiyuan.DoesNotExist: - logger.error(f"会员信息不存在: {order.huiyuan_id}") - return self.wechat_response('FAIL', '会员信息不存在') - - # ===== 更新收支记录 ===== - try: - jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - - update_daily_income(jine_decimal) - - szjilu, _ = Szjilu.query.get_or_create( - id=1, - defaults={'zongsy': Decimal('0.00'), 'zongls': Decimal('0.00'), 'zongzc': Decimal('0.00'), - 'jrzc': Decimal('0.00'), 'jrls': Decimal('0.00')} - ) - szjilu.zongsy += jine_decimal - szjilu.zongls += jine_decimal - szjilu.jrls += jine_decimal - szjilu.save() - logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") - except Exception as e: - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - - # 更新订单状态 - order.zhuangtai = 3 - order.save(update_fields=['zhuangtai']) - logger.info(f"会员订单状态更新成功: {order.dingdan_id} -> 3") - - # 更新会员购买次数 - huiyuan.goumai_cishu += 1 - huiyuan.save(update_fields=['goumai_cishu']) - - # 处理会员购买记录 - goumai_record, is_new, is_first_buy = self.process_huiyuan_goumai( - order.yonghuid, order.huiyuan_id, huiyuan - ) - - if is_first_buy: - self.add_first_buy_jifen(order.yonghuid) - - # 🆕 新增:处理分红(包含管事和组长) - with transaction.atomic(): - self.process_fenhong(order, huiyuan) - - logger.info(f"会员支付回调处理完成: {out_trade_no}") - return self.wechat_response('SUCCESS', 'OK') - - except ET.ParseError as e: - logger.error(f"会员XML解析失败: {str(e)}") - return self.wechat_response('FAIL', '数据格式错误') - except Exception as e: - logger.error(f"会员支付回调处理异常: {str(e)}", exc_info=True) - return self.wechat_response('FAIL', '处理异常') - - # ---------- 以下为原方法,基本不变,但需要导入新增模型 ---------- - - def process_huiyuan_goumai(self, yonghuid, huiyuan_id, huiyuan): - """处理会员购买记录(与原代码一致)""" - try: - existing_record = Huiyuangoumai.query.filter( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id - ).first() - - is_first_buy_any = False - - if existing_record: - existing_record.daoqi_time += timedelta(days=30) - existing_record.huiyuan_zhuangtai = 1 - existing_record.save() - logger.info(f"会员续费: 用户{yonghuid}, 会员{huiyuan_id}") - return existing_record, False, False - else: - other_huiyuan_count = Huiyuangoumai.query.filter( - yonghu_id=yonghuid - ).exclude(huiyuan_id=huiyuan_id).count() - if other_huiyuan_count == 0: - is_first_buy_any = True - logger.info(f"用户{yonghuid}首次购买任何会员,将获得10积分") - - goumai_record = Huiyuangoumai.query.create( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id, - jieshao=huiyuan.jieshao, - huiyuan_zhuangtai=1, - daoqi_time=timezone.now() + timedelta(days=30) - ) - logger.info(f"新购会员: 用户{yonghuid}, 会员{huiyuan_id}") - return goumai_record, True, is_first_buy_any - except Exception as e: - logger.error(f"处理会员购买记录失败: {str(e)}", exc_info=True) - if "unique" in str(e).lower() or "duplicate" in str(e).lower(): - logger.warning(f"检测到重复创建尝试,已由数据库拦截: 用户{yonghuid}, 会员{huiyuan_id}") - return self.process_huiyuan_goumai(yonghuid, huiyuan_id, huiyuan) - raise - - def add_first_buy_jifen(self, yonghuid): - """首次购买积分奖励(与原代码一致)""" - try: - dashou = UserDashou.query.get(user__yonghuid=yonghuid) - dashou.jifen += 10 - dashou.save(update_fields=['jifen']) - logger.info(f"用户{yonghuid}首次购买会员,积分+10,当前积分: {dashou.jifen}") - except UserDashou.DoesNotExist: - logger.error(f"找不到打手记录,无法增加积分: {yonghuid}") - except Exception as e: - logger.error(f"增加积分失败: {str(e)}") - - # 🆕 新增:统一分红处理函数 - def process_fenhong(self, order, huiyuan): - """ - 处理管事和组长分红(支持多次分红配置) - """ - try: - # 获取打手信息 - dashou = UserDashou.query.get(user__yonghuid=order.yonghuid) - guanshi_id = dashou.yaoqingren - if not guanshi_id: - logger.info(f"打手{order.yonghuid}没有邀请人,无分红") - return - - # 获取管事信息 - try: - guanshi = UserGuanshi.query.get(user__yonghuid=guanshi_id) - except UserGuanshi.DoesNotExist: - logger.warning(f"管事信息不存在: {guanshi_id}") - return - - # 获取组长ID(从管事扩展表的邀请人) - zuzhang_id = guanshi.yaoqingren - - # 获取打手头像和昵称 - user_main = dashou.user - avatar = user_main.avatar - nicheng = dashou.nicheng or "未知打手" - - # 查询该打手针对该管事的已分红次数 - existing_count = Gsfenhong.query.filter( - dashouid=order.yonghuid, - guanshi=guanshi_id - ).count() - cishu = existing_count + 1 # 本次是第几次 - - # 获取订单金额(用于限制分红总额) - order_amount = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - - # 计算管事分红金额 - guanshi_fenhong = self._calc_guanshi_fenhong(guanshi, huiyuan, guanshi_id, cishu) - if guanshi_fenhong is None: - logger.info(f"管事{guanshi_id}本次无分红") - guanshi_fenhong = Decimal('0.00') - - # 计算组长分红金额(如果存在组长) - zuzhang_fenhong = Decimal('0.00') - if zuzhang_id: - # 获取组长信息 - try: - zuzhang = UserZuzhang.query.get(user__yonghuid=zuzhang_id) - except UserZuzhang.DoesNotExist: - logger.warning(f"组长信息不存在: {zuzhang_id}") - zuzhang = None - - zuzhang_fenhong = self._calc_zuzhang_fenhong(zuzhang, huiyuan, zuzhang_id, cishu) - if zuzhang_fenhong is None: - logger.info(f"组长{zuzhang_id}本次无分红") - zuzhang_fenhong = Decimal('0.00') - else: - logger.info(f"管事{guanshi_id}没有组长,无组长分红") - - # 检查分红总额是否超过订单金额(优先保证管事分红) - total_fenhong = guanshi_fenhong + zuzhang_fenhong - if total_fenhong > order_amount: - logger.warning(f"分红总额{total_fenhong}超过订单金额{order_amount},组长分红设为0") - zuzhang_fenhong = Decimal('0.00') - total_fenhong = guanshi_fenhong - if total_fenhong > order_amount: - logger.warning(f"管事分红{guanshi_fenhong}也超过订单金额,强制设为0") - guanshi_fenhong = Decimal('0.00') - total_fenhong = Decimal('0.00') - - # 创建分红记录(先创建主记录,即使金额为0也记录?但为0无意义,所以只有大于0才创建) - if guanshi_fenhong > 0 or zuzhang_fenhong > 0: - # 更新管事扩展表 - if guanshi_fenhong > 0: - guanshi.chongzhifenrun += guanshi_fenhong - guanshi.yue += guanshi_fenhong - guanshi.jinrichongzhi += 1 - guanshi.jinyuechongzhi += 1 - guanshi.save(update_fields=['chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi']) - logger.info(f"管事分红成功: 管事{guanshi_id}, 金额{guanshi_fenhong}") - - # 更新组长扩展表 - if zuzhang_fenhong > 0 and zuzhang: - zuzhang.fenyong_zonge += zuzhang_fenhong - zuzhang.ketixian_jine += zuzhang_fenhong - zuzhang.jinri_fenyong += zuzhang_fenhong - zuzhang.jinyue_fenyong += zuzhang_fenhong - zuzhang.save(update_fields=['fenyong_zonge', 'ketixian_jine', 'jinri_fenyong', 'jinyue_fenyong']) - logger.info(f"组长分红成功: 组长{zuzhang_id}, 金额{zuzhang_fenhong}") - - # 创建分红记录 - Gsfenhong.query.create( - dingdan_id=order.dingdan_id, - guanshi=guanshi_id, - dashouid=order.yonghuid, - shuoming=order.shuoming, - fenhong=guanshi_fenhong, - avatar=avatar, - nicheng=nicheng, - # 新增字段 - zuzhang_id=zuzhang_id if zuzhang_fenhong > 0 else None, - zuzhang_fenhong=zuzhang_fenhong if zuzhang_fenhong > 0 else None, - huiyuan_id=huiyuan.huiyuan_id, - fenhong_leixing=1, - ) - - - - # ========== 管事每日统计(新购) ========== - if guanshi_fenhong > 0 and cishu == 1: - try: - update_guanshi_daily_by_action( - yonghuid=guanshi_id, - action=2, - amount=guanshi_fenhong - ) - logger.info(f"管事每日统计更新成功(新购):管事{guanshi_id},分红{guanshi_fenhong}") - except Exception as e: - logger.error(f"管事每日统计更新失败(新购):{str(e)}") - - # ========== 管事每日统计(续费) ========== - if guanshi_fenhong > 0 and cishu > 1: - try: - update_guanshi_xufei_daily( - yonghuid=guanshi_id, - xufei_jine=guanshi_fenhong - ) - logger.info(f"管事续费统计更新成功:管事{guanshi_id},续费分红{guanshi_fenhong}") - except Exception as e: - logger.error(f"管事续费统计更新失败:{str(e)}") - - # ========== 组长每日统计(分佣收入) ========== - if zuzhang_fenhong > 0 and zuzhang_id: - try: - update_zuzhang_daily_by_action( - yonghuid=zuzhang_id, - action=2, - amount=zuzhang_fenhong - ) - logger.info(f"组长每日统计更新成功:组长{zuzhang_id},分佣{zuzhang_fenhong}") - except Exception as e: - logger.error(f"组长每日统计更新失败:{str(e)}") - - - logger.info(f"分红记录创建成功: 订单{order.dingdan_id}") - else: - logger.info(f"分红金额均为0,不创建记录") - - except Exception as e: - logger.error(f"处理分红失败: {str(e)}", exc_info=True) - # 不抛出异常,避免影响主流程 - - def _calc_guanshi_fenhong(self, guanshi, huiyuan, guanshi_id, cishu): - """计算管事分红金额""" - # 先查多次分红配置表 - try: - config = DuociFenhong.query.get( - huiyuan=huiyuan, - yonghuid=guanshi_id, - cishu=cishu - ) - return Decimal(str(config.guanshi_fenhong)) - except DuociFenhong.DoesNotExist: - pass - - if cishu == 1: - # 第一次分红,取会员表默认金额 - return Decimal(str(huiyuan.guanshifc)) - else: - # 第二次及以上,检查是否开启二次分红 - if guanshi.fenghong_erci_enabled: - return Decimal(str(guanshi.fenghong_erci)) - else: - return None # 无分红 - - def _calc_zuzhang_fenhong(self, zuzhang, huiyuan, zuzhang_id, cishu): - """计算组长分红金额""" - if zuzhang is None: - return None - # 查多次分红配置表 - try: - config = DuociFenhong.query.get( - huiyuan=huiyuan, - yonghuid=zuzhang_id, - cishu=cishu - ) - return Decimal(str(config.zuzhang_fenhong)) - except DuociFenhong.DoesNotExist: - pass - - if cishu == 1: - # 第一次分红,取会员表默认组长分成 - return Decimal(str(huiyuan.zuzhangfc)) - else: - # 第二次及以上,检查组长是否开启额外分红 - if zuzhang.kaioi_ewai_fenhong: - return Decimal(str(zuzhang.ewai_fenhong_jine)) - else: - return None - - # 以下为原辅助方法,保持原样 - def verify_signature(self, xml_data): - """验证微信支付签名(与原代码一致)""" - try: - root = ET.fromstring(xml_data) - sign = root.find('sign').text - if not sign: - logger.error("会员签名字段不存在") - return False - - params = {} - for child in root: - if child.tag != 'sign' and child.text is not None: - params[child.tag] = child.text - - sorted_params = sorted(params.items(), key=lambda x: x[0]) - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' - calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - return sign == calculated_sign - except Exception as e: - logger.error(f"会员验证签名异常: {str(e)}") - return False - - def wechat_response(self, return_code, return_msg): - """返回微信回调响应""" - xml_response = f""" - - -""" - logger.info(f"会员返回微信响应: {return_code} - {return_msg}") - return HttpResponse(xml_response, content_type='application/xml') - - - - - - - - - - - -# views.py -class HuiyuanShibai(APIView): - """ - 会员支付失败处理接口 - 路径:/shangpin/huiyuanshibai - 请求方式:POST - 权限:JWT认证 - 参数:dingdanid(订单ID) - 返回:删除结果 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取订单ID - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({ - 'code': 400, - 'message': '订单ID不能为空' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 查询订单(确保是当前用户的订单) - try: - order = Czjilu.query.get( - dingdan_id=dingdanid, - yonghuid=request.user.yonghuid - ) - except Czjilu.DoesNotExist: - return Response({ - 'code': 400, - 'message': '订单不存在或不属于当前用户' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 检查订单类型是否为会员(1) - if order.leixing != 1: - return Response({ - 'code': 400, - 'message': '订单类型错误,不是会员订单' - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 检查订单状态是否为未支付(9) - if order.zhuangtai == 9: - # 删除订单 - order.delete() - logger.info(f"用户{request.user.yonghuid}删除未支付会员订单: {dingdanid}") - - return Response({ - 'code': 200, - 'message': '会员订单已删除' - }, status=status.HTTP_200_OK) - else: - return Response({ - 'code': 400, - 'message': '会员订单状态不是未支付,无法删除' - }, status=status.HTTP_400_BAD_REQUEST) - - except Exception as e: - logger.error(f"会员支付失败处理异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': '服务器内部错误' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -# views.py -class HuiyuanLunxun(APIView): - """ - 会员支付成功轮询接口 - 路径:/shangpin/huiyuanlx - 请求方式:POST - 权限:JWT认证 - 参数:dingdanid(订单ID) - 返回:订单状态和会员信息 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取订单ID - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({ - 'code': 400, - 'message': '订单ID不能为空' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 查询订单(确保是当前用户的订单) - try: - order = Czjilu.query.get( - dingdan_id=dingdanid, - yonghuid=request.user.yonghuid - ) - except Czjilu.DoesNotExist: - return Response({ - 'code': 400, - 'message': '订单不存在或不属于当前用户' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 检查订单类型是否为会员(1) - if order.leixing != 1: - return Response({ - 'code': 400, - 'message': '订单类型错误,不是会员订单' - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 检查订单状态是否为已支付(3) - if order.zhuangtai != 3: - logger.info(f"会员订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") - return Response({ - 'code': 400, - 'message': '会员订单尚未支付完成' - }, status=status.HTTP_400_BAD_REQUEST) - - # 5. 查询会员信息 - try: - huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id) - - # 6. 查询会员购买记录 - goumai_record = Huiyuangoumai.query.filter( - yonghu_id=request.user.yonghuid, - huiyuan_id=order.huiyuan_id - ).first() - - if not goumai_record: - return Response({ - 'code': 400, - 'message': '会员购买记录不存在' - }, status=status.HTTP_400_BAD_REQUEST) - - # 7. 格式化到期时间 - #from django.utils.timezone import localtime - #daoqi_time = localtime(goumai_record.daoqi_time).strftime('%Y-%m-%d %H:%M:%S') - # 直接格式化,不使用时区转换 - daoqi_time = goumai_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') - current_user = request.user - dashou = current_user.dashou_profile - - logger.info(f"会员轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}, 会员{order.huiyuan_id}") - - # 8. 返回会员信息(用于更新前端全局变量) - return Response({ - 'code': 200, - 'message': '支付流程全部完成', - 'jifen': dashou.jifen, - 'huiyuan': { - 'huiyuanid': order.huiyuan_id, - 'huiyuanming': huiyuan.jieshao, - 'daoqi': daoqi_time - } - }, status=status.HTTP_200_OK) - - except Huiyuan.DoesNotExist: - logger.error(f"会员信息不存在: {order.huiyuan_id}") - return Response({ - 'code': 400, - 'message': '会员信息不存在' - }, status=status.HTTP_400_BAD_REQUEST) - - except Exception as e: - logger.error(f"会员轮询异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': '服务器内部错误' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - -# views.py in shangpin app - -class ShangjiaChongzhi(APIView): - """ - 商家余额充值支付参数接口 - 路径:/shangpin/sjcz - 请求方式:POST - 权限:JWT认证 - 参数:jine(金额,1-10000元) - 返回:支付参数(payParams)和订单ID(dingdanid) - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 验证用户身份和商家状态 - try: - shangjia = request.user.shop_profile - except UserShangjia.DoesNotExist: - logger.warning(f"用户{request.user.yonghuid}尝试充值但不是商家") - return Response({ - 'code': 400, - 'message': '用户不是商家,无法充值' - }, status=status.HTTP_400_BAD_REQUEST) - - # 检查商家状态 - if shangjia.zhuangtai != 1: - logger.warning(f"商家{shangjia.nicheng}状态异常: {shangjia.zhuangtai}") - return Response({ - 'code': 400, - 'message': '商家状态异常,无法进行充值' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 获取并验证金额参数 - jine = request.data.get('jine') - if not jine: - return Response({ - 'code': 400, - 'message': '请填写充值金额' - }, status=status.HTTP_400_BAD_REQUEST) - - try: - jine = float(jine) - except (ValueError, TypeError): - return Response({ - 'code': 400, - 'message': '金额格式错误,请填写有效的数字' - }, status=status.HTTP_400_BAD_REQUEST) - - # 验证金额范围 - if jine < settings.SHANGJIA_CZ_MIN_AMOUNT or jine > settings.SHANGJIA_CZ_MAX_AMOUNT: - return Response({ - 'code': 400, - 'message': f'充值金额必须在{settings.SHANGJIA_CZ_MIN_AMOUNT}-{settings.SHANGJIA_CZ_MAX_AMOUNT}元之间' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 生成订单ID(使用配置的前缀) - timestamp_ms = int(time.time() * 1000) - timestamp_ns = int(time.time_ns() // 1000) % 1000000 - random_num = random.randint(0, 99) - - dingdanid = f"{settings.SHANGJIA_CZ_PREFIX}{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" - - # 4. 创建充值记录订单 - try: - chongzhi_jilu = Czjilu.query.create( - dingdan_id=dingdanid, - zhuangtai=9, # 未支付状态 - yonghuid=request.user.yonghuid, - jine=jine, - leixing=4, # 充值类型:4=商家充值 - shuoming=settings.SHANGJIA_CZ_PAY_DESCRIPTION - ) - logger.info(f"创建商家充值订单: 商家{shangjia.nicheng}, 订单{dingdanid}, 金额{jine}元") - except Exception as e: - logger.error(f"创建订单失败: {str(e)}") - return Response({ - 'code': 500, - 'message': '订单创建失败,请重试' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 5. 生成微信支付参数 - try: - pay_params = self.generate_wechat_pay_params( - dingdanid=dingdanid, - jine=jine, - openid=request.user.openid, - pay_type='shangjia' - ) - except Exception as e: - # 支付参数生成失败,删除订单记录 - chongzhi_jilu.delete() - logger.error(f"生成微信支付参数失败: {str(e)}") - return Response({ - 'code': 500, - 'message': f'支付参数生成失败: {str(e)}' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 6. 返回支付参数和订单ID - logger.info(f"商家{shangjia.nicheng}支付参数生成成功: {dingdanid}") - return Response({ - 'code': 200, - 'message': '支付参数生成成功', - 'payParams': pay_params, - 'dingdanid': dingdanid - }, 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) - - def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): - """生成微信小程序支付参数""" - # 获取配置 - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO - - # 根据支付类型获取对应的配置 - if pay_type == 'shangjia': - PAY_DESCRIPTION = settings.SHANGJIA_CZ_PAY_DESCRIPTION - NOTIFY_URL = settings.SHANGJIA_CZ_PAY_NOTIFY_URL - else: - raise Exception('未知的支付类型') - - # 生成随机字符串 - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - - # 金额转换(元转分) - total_fee = int(Decimal(str(jine)) * 100) - - # 交易类型(小程序支付使用JSAPI) - trade_type = 'JSAPI' - - # 获取客户端IP - user_ip = self.get_client_ip() - - # 1. 第一次签名(统一下单接口) - sign_data = { - 'appid': APPID, - 'mch_id': MCHID, - 'nonce_str': nonce_str, - 'body': PAY_DESCRIPTION, - 'out_trade_no': dingdanid, - 'total_fee': total_fee, - 'spbill_create_ip': user_ip, - 'notify_url': NOTIFY_URL, - 'trade_type': trade_type, - 'openid': openid # 小程序支付必须传openid - } - - # 按照参数名ASCII码从小到大排序 - sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) - - # 拼接字符串 - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) - sign_string += f'&key={KEY}' - - # MD5签名 - sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - # 构造XML数据 - xml_data = f""" -{APPID} -{PAY_DESCRIPTION} -{MCHID} -{nonce_str} -{NOTIFY_URL} -{openid} -{dingdanid} -{user_ip} -{total_fee} -{trade_type} -{sign} -""" - - # 调用微信统一下单接口 - response = requests.post( - 'https://api.mch.weixin.qq.com/pay/unifiedorder', - data=xml_data.encode('utf-8'), - headers={'Content-Type': 'application/xml'}, - timeout=10 - ) - - # 解析返回的XML - root = ET.fromstring(response.content) - - return_code = root.find('return_code').text - result_code = root.find('result_code').text - - if return_code == 'SUCCESS' and result_code == 'SUCCESS': - # 获取prepay_id - prepay_id = root.find('prepay_id').text - - # 2. 第二次签名(小程序支付参数) - timestamp = str(int(time.time())) - nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - package = f'prepay_id={prepay_id}' - sign_type = 'MD5' - - # 生成支付签名 - pay_sign_data = { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type - } - - sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) - pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) - pay_sign_string += f'&key={KEY}' - pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() - - # 返回前端需要的支付参数 - return { - 'appId': APPID, - 'timeStamp': timestamp, - 'nonceStr': nonce_str, - 'package': package, - 'signType': sign_type, - 'paySign': pay_sign - } - else: - # 获取错误信息 - err_code = root.find('err_code') - err_code_des = root.find('err_code_des') - return_msg = root.find('return_msg') - - error_details = f"return_code: {return_code}" - if err_code is not None: - error_details += f", err_code: {err_code.text}" - if err_code_des is not None: - error_details += f", err_code_des: {err_code_des.text}" - if return_msg is not None: - error_details += f", return_msg: {return_msg.text}" - - raise Exception(f"微信支付下单失败: {error_details}") - - def get_client_ip(self): - """获取客户端IP""" - request = self.request - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip if ip else '127.0.0.1' - - - - - -class ShangjiaHuitiao(View): - """ - 商家支付回调接口 - 路径:/shangpin/sjhuitiao - 注意:这个接口不需要JWT认证,由微信服务器调用 - """ - - def post(self, request): - try: - # 1. 解析微信回调XML数据 - request_body = request.body - logger.info(f"收到商家支付回调,原始数据: {request_body.decode('utf-8')}") - - root = ET.fromstring(request_body) - - # 获取关键字段 - return_code = root.find('return_code').text - result_code = root.find('result_code').text - out_trade_no = root.find('out_trade_no').text # 订单ID - transaction_id = root.find('transaction_id').text # 微信交易号 - total_fee = int(root.find('total_fee').text) # 支付金额(分) - - logger.info(f"商家回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") - - # 2. 验证基本返回状态 - if return_code != 'SUCCESS' or result_code != 'SUCCESS': - logger.warning(f"商家支付回调状态异常: {return_code}/{result_code}") - return self.wechat_response('FAIL', '支付失败') - - # 3. 验证签名(确保是微信发来的) - if not self.verify_signature(request_body): - logger.error(f"商家支付签名验证失败: {out_trade_no}") - return self.wechat_response('FAIL', '签名验证失败') - - # 4. 查询订单 - try: - order = Czjilu.query.get(dingdan_id=out_trade_no) - logger.info(f"找到商家订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") - except Czjilu.DoesNotExist: - logger.error(f"商家订单不存在: {out_trade_no}") - return self.wechat_response('FAIL', '订单不存在') - - # 5. 检查订单状态是否为未支付(9) - if order.zhuangtai != 9: - logger.warning(f"商家订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") - # 已处理过的订单直接返回成功,避免微信重复回调 - return self.wechat_response('SUCCESS', 'OK') - - # 6. 验证金额(微信返回的是分,需要转换) - wechat_amount_yuan = Decimal(str(total_fee)) / Decimal('100') # 转换为元 - order_amount_yuan = Decimal(str(order.jine)) # 订单金额(元) - - # 允许1分钱的误差 - if abs(wechat_amount_yuan - order_amount_yuan) > Decimal('0.01'): - logger.error(f"商家订单金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") - return self.wechat_response('FAIL', '金额不一致') - - # ===== 在这里添加收支记录更新代码 ===== - # 更新收支记录表 - try: - # 获取订单金额(确保是Decimal类型) - jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine - - update_daily_income(jine_decimal) - - # 查询或创建ID=1的收支记录 - szjilu, created = Szjilu.query.get_or_create( - id=1, - defaults={ - 'zongsy': Decimal('0.00'), - 'zongls': Decimal('0.00'), - 'zongzc': Decimal('0.00'), - 'jrzc': Decimal('0.00'), - 'jrls': Decimal('0.00') - } - ) - - # 更新三个字段 - szjilu.zongsy += jine_decimal # 总收益 - szjilu.zongls += jine_decimal # 总流水 - szjilu.jrls += jine_decimal # 今日流水 - - szjilu.save() - - logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") - - except Exception as e: - # 收支记录更新失败不影响主流程,只记录日志 - logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) - # ===== 添加结束 ===== - - - - # 7. 使用事务更新订单状态和商家余额 - with transaction.atomic(): - # 更新订单状态为已支付(3) - order.zhuangtai = 3 - order.save(update_fields=['zhuangtai']) - logger.info(f"商家订单状态更新成功: {order.dingdan_id} -> 3") - - # 更新商家余额 - try: - # 通过用户ID查找商家(高效查询) - shangjia = UserShangjia.query.get(user__yonghuid=order.yonghuid) - - # 增加余额 - shangjia.yue += order_amount_yuan - shangjia.save(update_fields=['yue']) - - logger.info(f"商家余额更新成功: 商家{shangjia.nicheng}, 增加{order_amount_yuan}元, 新余额{shangjia.yue}") - - except UserShangjia.DoesNotExist: - # 如果找不到商家,记录错误,但依然返回成功(避免微信重复回调) - logger.error(f"商家信息不存在: 用户{order.yonghuid}") - return self.wechat_response('SUCCESS', 'OK') - except Exception as e: - logger.error(f"更新商家余额失败: {str(e)}", exc_info=True) - # 回滚事务 - raise - - # 8. 返回成功响应给微信 - logger.info(f"商家支付回调处理完成: {out_trade_no}") - return self.wechat_response('SUCCESS', 'OK') - - except ET.ParseError as e: - logger.error(f"XML解析失败: {str(e)}") - return self.wechat_response('FAIL', '数据格式错误') - except Exception as e: - logger.error(f"商家支付回调处理异常: {str(e)}", exc_info=True) - # 返回FAIL让微信重试 - return self.wechat_response('FAIL', '处理异常') - - def verify_signature(self, xml_data): - """验证微信支付签名""" - try: - root = ET.fromstring(xml_data) - sign = root.find('sign').text - - if not sign: - logger.error("签名字段不存在") - return False - - # 获取所有参数并排除sign - params = {} - for child in root: - if child.tag != 'sign' and child.text is not None: - params[child.tag] = child.text - - # 按照参数名ASCII码从小到大排序 - sorted_params = sorted(params.items(), key=lambda x: x[0]) - - # 拼接字符串 - sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' - - # 计算签名 - calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() - - is_valid = sign == calculated_sign - if not is_valid: - logger.error(f"商家支付签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") - - return is_valid - - except Exception as e: - logger.error(f"验证签名异常: {str(e)}") - return False - - def wechat_response(self, return_code, return_msg): - """返回微信支付回调响应""" - xml_response = f""" - - -""" - - logger.info(f"返回微信响应: {return_code} - {return_msg}") - return HttpResponse(xml_response, content_type='application/xml') - - - - -# views.py -class ShangjiaShibai(APIView): - """ - 商家支付失败处理接口 - 路径:/shangpin/sjshibai - 请求方式:POST - 权限:JWT认证 - 参数:dingdanid(订单ID) - 返回:删除结果 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取订单ID - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({ - 'code': 400, - 'message': '订单ID不能为空' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 查询订单(确保是当前用户的订单) - try: - order = Czjilu.query.get( - dingdan_id=dingdanid, - yonghuid=request.user.yonghuid, - leixing=4 # 确保是商家充值订单 - ) - except Czjilu.DoesNotExist: - logger.warning(f"商家订单不存在或不属于当前用户: {dingdanid}, 用户{request.user.yonghuid}") - return Response({ - 'code': 400, - 'message': '订单不存在或不属于当前用户' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 检查订单状态是否为未支付(9) - if order.zhuangtai == 9: - # 删除订单 - order.delete() - logger.info(f"商家{request.user.shop_profile.nicheng}删除未支付订单: {dingdanid}") - - return Response({ - 'code': 200, - 'message': '订单已删除' - }, status=status.HTTP_200_OK) - else: - return Response({ - 'code': 400, - 'message': '订单状态不是未支付,无法删除' - }, status=status.HTTP_400_BAD_REQUEST) - - except UserShangjia.DoesNotExist: - return Response({ - 'code': 400, - 'message': '用户不是商家' - }, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - logger.error(f"商家支付失败处理异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': '服务器内部错误' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - -# views.py -class ShangjiaChenggong(APIView): - """ - 商家支付成功轮询接口 - 路径:/shangpin/sjcg - 请求方式:POST - 权限:JWT认证 - 参数:dingdanid(订单ID) - 返回:订单状态和当前商家余额 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取订单ID - dingdanid = request.data.get('dingdanid') - if not dingdanid: - return Response({ - 'code': 400, - 'message': '订单ID不能为空' - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 查询订单(确保是当前用户的商家订单) - try: - order = Czjilu.query.get( - dingdan_id=dingdanid, - yonghuid=request.user.yonghuid, - leixing=4 # 商家充值类型 - ) - except Czjilu.DoesNotExist: - logger.warning(f"商家订单不存在: {dingdanid}, 用户{request.user.yonghuid}") - return Response({ - 'code': 400, - 'message': '订单不存在或不属于当前用户' - }, status=status.HTTP_400_BAD_REQUEST) - - # 3. 检查订单状态是否为已支付(3) - if order.zhuangtai != 3: - logger.info(f"商家订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") - return Response({ - 'code': 400, - 'message': '订单尚未支付完成' - }, status=status.HTTP_400_BAD_REQUEST) - - # 4. 获取商家余额(通过反向查询) - try: - shangjia = request.user.shop_profile - yue = float(shangjia.yue) # 转为浮点数方便前端处理 - - logger.info(f"商家轮询成功: 订单{dingdanid}已支付,商家{shangjia.nicheng}余额为{yue}元") - - return Response({ - 'code': 200, - 'message': '支付流程全部完成', - 'yue': yue # 返回当前商家余额(前端需要更新全局变量) - }, status=status.HTTP_200_OK) - - except UserShangjia.DoesNotExist: - logger.error(f"商家信息不存在: 用户{request.user.yonghuid}") - return Response({ - 'code': 400, - 'message': '商家信息不存在' - }, status=status.HTTP_400_BAD_REQUEST) - - except Exception as e: - logger.error(f"商家轮询异常: {str(e)}", exc_info=True) - return Response({ - 'code': 500, - 'message': '服务器内部错误' - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -# 管事分红记录查询API - -class GuanshiFenhongListAPI(APIView): - """ - 管事分红记录查询接口 - 权限:需要JWT认证 - 请求方式:POST - 分页参数:page, page_size - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 获取当前管事用户ID - yonghuid = request.user.yonghuid - - # 获取分页参数 - 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('-create_time') \ - .values('dashouid', 'avatar', 'nicheng', 'fenhong', 'create_time')[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']), # 分红金额 - 'create_time': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] 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.yonghuid}") - return Response({ - 'code': 400, - 'message': '缺少账号参数', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 2. 获取当前登录用户 - current_user = request.user - - # 3. 验证用户类型是否为管理员 - if current_user.user_type != 'admin': - logger.warning( - f"权限验证失败:用户 {current_user.yonghuid} 的用户类型为 {current_user.user_type},非管理员") - return Response({ - 'code': 401, - 'message': '无管理员权限', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 4. 验证手机号与传递的账号是否一致 - if current_user.phone != zhanghao: - logger.warning( - f"账号验证失败:用户 {current_user.yonghuid} 的phone={current_user.phone},传递的zhanghao={zhanghao}") - return Response({ - 'code': 401, - 'message': '账号验证失败', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 5. 验证管理员扩展表是否存在 - try: - admin_profile = current_user.admin_profile - logger.info(f"管理员验证通过:用户 {current_user.yonghuid},手机号 {current_user.phone}") - except AdminProfile.DoesNotExist: - logger.error( - f"管理员扩展表不存在:用户 {current_user.yonghuid} 虽然user_type=admin,但没有AdminProfile记录") - return Response({ - 'code': 401, - 'message': '管理员信息不完整', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 6. 查询商品类型数据 - try: - # 使用select_related或prefetch_related优化查询(如果需要关联查询) - # 这里直接查询所有字段,然后提取需要的字段 - shangpinleixing_queryset = ShangpinLeixing.query.all() - - # 转换为前端需要的格式 - shangpinleixing_list = [] - for leixing in shangpinleixing_queryset: - leixing_data = { - 'id': leixing.id, - 'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None, - 'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None, - 'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None, - 'jieshao': leixing.jieshao if leixing.jieshao else '', - 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' - } - shangpinleixing_list.append(leixing_data) - - #logger.info(f"成功查询到 {len(shangpinleixing_list)} 条商品类型数据") - - except Exception as e: - logger.error(f"查询商品类型数据时出错:{str(e)}") - shangpinleixing_list = [] - - # 7. 查询商品专区数据 - try: - # 查询所有商品专区 - zhuanqu_queryset = ShangpinZhuanqu.query.all() - - # 转换为前端需要的格式 - zhuanqu_list = [] - for zhuanqu in zhuanqu_queryset: - zhuanqu_data = { - 'id': zhuanqu.id, - 'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '', - 'leixing_id': zhuanqu.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 ShangpinLeixingFabuView(APIView): - """ - 商品类型发布接口 - 接收图片、类型介绍、抢单要求类型等字段 - 需要管理员权限 - """ - permission_classes = [permissions.IsAuthenticated] - parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 - - def post(self, request): - try: - # === 1. 权限验证 === - # 获取当前用户 - user_main = request.user - - # 1.1 验证前端传递的账号是否匹配(使用phone字段) - zhanghao = request.data.get('zhanghao', '').strip() - if not zhanghao: - return Response( - {'code': 401, 'msg': '未提供账号信息', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 验证账号是否匹配(使用phone字段比较) - if user_main.phone != zhanghao: - return Response( - {'code': 401, 'msg': '账号验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.2 验证是否为管理员(检查user_type字段) - if user_main.user_type != 'admin': - return Response( - {'code': 401, 'msg': '权限不足', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.3 验证管理员扩展表(使用AdminProfile模型) - try: - admin_profile = AdminProfile.query.get(user=user_main) - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'msg': '管理员信息不存在', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # === 2. 接收并验证字段 === - # 2.1 必填字段验证 - jieshao = request.data.get('jieshao', '').strip() - yaoqiuleixing = request.data.get('yaoqiuleixing') - - - if not jieshao: - return Response( - {'code': 400, 'msg': '类型介绍不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - if yaoqiuleixing not in ['1', '2']: - return Response( - {'code': 400, 'msg': '抢单要求类型无效', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2.2 验证图片 - tupian_file = request.FILES.get('file') - if not tupian_file: - - return Response( - {'code': 400, 'msg': '请上传类型图片', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证图片格式和大小 - is_valid, error_msg = validate_image(tupian_file) - if not is_valid: - return Response( - {'code': 400, 'msg': f'图片验证失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2.3 根据抢单要求类型验证对应字段 - huiyuan_id = request.data.get('huiyuan_id', '').strip() - yongjin = request.data.get('yongjin', '').strip() - - - # 转换yaoqiuleixing为整数便于处理 - yaoqiuleixing_int = int(yaoqiuleixing) - - if yaoqiuleixing_int == 1: # 会员抢单 - if not huiyuan_id: - return Response( - {'code': 400, 'msg': '会员抢单必须选择会员', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - # 验证会员ID是否存在 - try: - huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response( - {'code': 400, 'msg': '会员ID不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - # 会员ID为字符串,佣金为空 - yongjin_value = None - huiyuan_id_value = huiyuan_id - else: # yaoqiuleixing_int == 2 佣金抢单 - if not yongjin: - return Response( - {'code': 400, 'msg': '佣金抢单必须填写佣金金额', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - try: - # 验证佣金为有效数字 - yongjin_value = float(yongjin) - if yongjin_value <= 0: - return Response( - {'code': 400, 'msg': '佣金金额必须大于0', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - except ValueError: - return Response( - {'code': 400, 'msg': '佣金金额格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - huiyuan_id_value = None - - # === 3. 创建商品类型记录 === - with transaction.atomic(): - # 3.1 创建商品类型对象 - leixing_obj = ShangpinLeixing( - jieshao=jieshao, - yaoqiuleixing=yaoqiuleixing_int, - huiyuan_id=huiyuan_id_value, - yongjin=yongjin_value, - # tupian_url 先留空,上传成功后再更新 - ) - - # 保存到数据库获取ID - leixing_obj.save() - leixing_id = leixing_obj.id - - # === 4. 处理图片上传 === - try: - # 4.1 生成文件名和路径 - file_extension = os.path.splitext(tupian_file.name)[1].lower() - if not file_extension: - file_extension = '.jpg' - - # 生成唯一文件名:商品类型ID + 随机UUID + 后缀 - unique_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}" - - # 构建OSS文件路径:a_long/leixing/{leixing_id}/{filename} - oss_folder = 'a_long' # 根文件夹 - leixing_folder = 'leixing' # 类型文件夹 - - # 构建完整路径 - oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{unique_filename}" - - # 4.2 上传到OSS - tupian_url = upload_to_oss(tupian_file, oss_file_path) - - if not tupian_url: - # 上传失败,抛出异常触发回滚 - raise Exception("图片上传到OSS失败") - - # 4.3 更新商品类型的图片URL(存储相对路径) - leixing_obj.tupian_url = oss_file_path # 存相对路径,方便拼接 - leixing_obj.save() - - except Exception as oss_error: - # 图片上传失败,事务会回滚,商品类型记录也会被删除 - print(f"OSS上传失败: {oss_error}") - # 手动抛出异常触发事务回滚 - raise Exception(f"图片上传失败: {str(oss_error)}") - - # === 5. 构建返回数据 === - response_data = { - 'id': leixing_id, - 'jieshao': jieshao, - 'yaoqiuleixing': yaoqiuleixing_int, - 'tupian_url': leixing_obj.tupian_url, # 相对路径 - 'huiyuan_id': huiyuan_id_value, - 'yongjin': str(yongjin_value) if yongjin_value else None - } - - # 返回成功响应 - return Response({ - 'code': 0, - 'msg': '商品类型添加成功', - 'data': response_data - }) - - except Exception as e: - # 捕获所有异常,返回500错误 - traceback.print_exc() - return Response({ - 'code': 500, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -class ShangpinZhuanquFabuView(APIView): - """ - 商品专区发布接口 - 接收专区名称、类型ID - 需要管理员权限 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - # === 1. 权限验证 === - user_main = request.user - - # 1.1 验证账号(使用phone字段) - zhanghao = request.data.get('zhanghao', '').strip() - if not zhanghao: - return Response( - {'code': 401, 'msg': '未提供账号信息', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if user_main.phone != zhanghao: - return Response( - {'code': 401, 'msg': '账号验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.2 验证是否为管理员(检查user_type字段) - if user_main.user_type != 'admin': - return Response( - {'code': 401, 'msg': '权限不足', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.3 验证管理员扩展表(使用AdminProfile模型) - try: - admin_profile = AdminProfile.query.get(user=user_main) - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'msg': '管理员信息不存在', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # === 2. 接收并验证字段 === - mingzi = request.data.get('mingzi', '').strip() - leixing_id = request.data.get('leixing_id') - - # 2.1 验证必填字段 - if not mingzi: - return Response( - {'code': 400, 'msg': '专区名称不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - if not leixing_id: - return Response( - {'code': 400, 'msg': '类型ID不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2.2 验证类型ID是否存在 - try: - # 验证商品类型是否存在 - leixing_obj = ShangpinLeixing.query.get(id=leixing_id) - except ShangpinLeixing.DoesNotExist: - return Response( - {'code': 400, 'msg': '商品类型不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2.3 验证名称长度(15汉字以内) - if len(mingzi) > 45: # 数据库字段最大长度 - return Response( - {'code': 400, 'msg': '专区名称过长', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # === 3. 创建商品专区记录 === - zhuanqu_obj = ShangpinZhuanqu( - mingzi=mingzi, - leixing_id=leixing_id - ) - - zhuanqu_obj.save() - - # === 4. 构建返回数据 === - response_data = { - 'id': zhuanqu_obj.id, - 'mingzi': mingzi, - 'leixing_id': leixing_id - } - - # 返回成功响应 - return Response({ - 'code': 0, - 'msg': '商品专区添加成功', - 'data': response_data - }) - - except Exception as e: - traceback.print_exc() - return Response({ - 'code': 500, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -class ShangpinFabuView(APIView): - """ - 商品发布接口 - 接收商品信息、图片等字段 - 需要管理员权限 - """ - permission_classes = [permissions.IsAuthenticated] - parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 - - def post(self, request): - try: - # === 1. 权限验证 === - user_main = request.user - - # 1.1 验证账号(使用phone字段) - zhanghao = request.data.get('zhanghao', '').strip() - if not zhanghao: - return Response( - {'code': 401, 'msg': '未提供账号信息', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if user_main.phone != zhanghao: - return Response( - {'code': 401, 'msg': '账号验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.2 验证是否为管理员(检查user_type字段) - if user_main.user_type != 'admin': - return Response( - {'code': 401, 'msg': '权限不足', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.3 验证管理员扩展表(使用AdminProfile模型) - try: - admin_profile = AdminProfile.query.get(user=user_main) - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'msg': '管理员信息不存在', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # === 2. 接收并验证字段 === - # 2.1 验证必填字段 - biaoti = request.data.get('biaoti', '').strip() - jiage = request.data.get('jiage', '').strip() - kucun = request.data.get('kucun', '').strip() - leixing_id = request.data.get('leixing_id') - zhuanqu_id = request.data.get('zhuanqu_id') - yaoqiuleixing = request.data.get('yaoqiuleixing') - jieshao = request.data.get('jieshao', '').strip() - - # 验证图片 - shangpin_tupian = request.FILES.get('shangpin_tupian') - if not shangpin_tupian: - - return Response( - {'code': 400, 'msg': '请上传商品图片', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证图片格式和大小 - is_valid, error_msg = validate_image(shangpin_tupian) - if not is_valid: - return Response( - {'code': 400, 'msg': f'商品图片验证失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2.2 验证必填字段是否为空 - required_fields = { - 'biaoti': biaoti, - 'jiage': jiage, - 'kucun': kucun, - 'leixing_id': leixing_id, - 'zhuanqu_id': zhuanqu_id, - 'yaoqiuleixing': yaoqiuleixing, - 'jieshao': jieshao - } - - for field_name, field_value in required_fields.items(): - if not field_value: - return Response( - {'code': 400, 'msg': f'{field_name}不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2.3 验证数值字段 - try: - jiage_value = float(jiage) - if jiage_value <= 0: - return Response( - {'code': 400, 'msg': '商品价格必须大于0', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - except ValueError: - return Response( - {'code': 400, 'msg': '商品价格格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - try: - kucun_value = int(kucun) - if kucun_value < 0: - return Response( - {'code': 400, 'msg': '库存不能为负数', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - except ValueError: - return Response( - {'code': 400, 'msg': '库存必须为整数', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证抢单要求类型 - if yaoqiuleixing not in ['1', '2']: - return Response( - {'code': 400, 'msg': '抢单要求类型无效', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # === 3. 验证关联数据 === - # 3.1 验证商品类型 - try: - leixing_obj = ShangpinLeixing.query.get(id=leixing_id) - except ShangpinLeixing.DoesNotExist: - return Response( - {'code': 400, 'msg': '商品类型不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 3.2 根据抢单要求类型验证商品类型 - yaoqiuleixing_int = int(yaoqiuleixing) - - if yaoqiuleixing_int == 1: # 会员抢单 - # 需要验证商品类型的抢单要求类型是否为1 - if leixing_obj.yaoqiuleixing != 1: - return Response( - {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 获取会员ID(从前端或商品类型) - huiyuan_id = request.data.get('huiyuan_id', leixing_obj.huiyuan_id) - if not huiyuan_id: - return Response( - {'code': 400, 'msg': '会员ID不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证会员是否存在 - try: - huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response( - {'code': 400, 'msg': '会员不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 设置会员相关字段 - huiyuan_id_value = huiyuan_id - yongjin_value = None - else: # 佣金抢单 - # 需要验证商品类型的抢单要求类型是否为2 - if leixing_obj.yaoqiuleixing != 2: - return Response( - {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 获取佣金(从前端或商品类型) - yongjin = request.data.get('yongjin', leixing_obj.yongjin) - if not yongjin: - return Response( - {'code': 400, 'msg': '佣金金额不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - try: - yongjin_value = float(yongjin) - if yongjin_value <= 0: - return Response( - {'code': 400, 'msg': '佣金金额必须大于0', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - except ValueError: - return Response( - {'code': 400, 'msg': '佣金金额格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - huiyuan_id_value = None - - # 3.3 验证商品专区 - try: - zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id, leixing_id=leixing_id) - except ShangpinZhuanqu.DoesNotExist: - return Response( - {'code': 400, 'msg': '商品专区不存在或与商品类型不匹配', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # === 4. 创建商品记录 === - with transaction.atomic(): - # 4.1 获取其他字段 - xuzhi = request.data.get('xuzhi', '').strip() - xiaoliang = request.data.get('xiaoliang', '0').strip() - - # 转换销量为整数 - try: - duiwai_xiaoliang_value = int(xiaoliang) if xiaoliang else 0 - except ValueError: - duiwai_xiaoliang_value = 0 - - # 4.2 创建商品对象 - shangpin_obj = Shangpin( - biaoqian=biaoti, - jiage=jiage_value, - kucun=kucun_value, - leixing_id=leixing_id, - zhuanqu_id=zhuanqu_id, - duiwai_xiaoliang=duiwai_xiaoliang_value, - jieshao=jieshao, - xiadan_xuzhi=xuzhi, - yaoqiuleixing=yaoqiuleixing_int, - huiyuan_id=huiyuan_id_value, - yongjin=yongjin_value, - # tupian_url 先留空,上传成功后再更新 - ) - - # 保存到数据库获取ID - shangpin_obj.save() - shangpin_id = shangpin_obj.id - - # === 5. 处理商品图片上传 === - try: - # 5.1 生成文件名和路径 - file_extension = os.path.splitext(shangpin_tupian.name)[1].lower() - if not file_extension: - file_extension = '.jpg' - - # 构建OSS文件路径:a_long/Spcc{shangpin_id}/{filename} - oss_folder = 'a_long' # 根文件夹 - spcc_folder = f"Spcc{shangpin_id}" # 商品图片文件夹 - - # 生成文件名:Spcc{shangpin_id}{file_extension} - unique_filename = f"Spcc{shangpin_id}{file_extension}" - - # 构建完整路径 - oss_file_path = f"{oss_folder}/{spcc_folder}/{unique_filename}" - - # 5.2 上传到OSS - tupian_url = upload_to_oss(shangpin_tupian, oss_file_path) - - if not tupian_url: - # 上传失败,抛出异常触发回滚 - raise Exception("商品图片上传到OSS失败") - - # 5.3 更新商品的图片URL(存储相对路径) - shangpin_obj.tupian_url = oss_file_path # 存相对路径 - shangpin_obj.save() - - except Exception as oss_error: - # 图片上传失败,事务会回滚,商品记录也会被删除 - #print(f"商品图片OSS上传失败: {oss_error}") - # 手动抛出异常触发事务回滚 - raise Exception(f"商品图片上传失败: {str(oss_error)}") - - # === 6. 构建返回数据 === - response_data = { - 'shangpin_id': shangpin_id, - 'biaoti': biaoti, - 'jiage': str(jiage_value), - 'kucun': kucun_value, - 'leixing_id': leixing_id, - 'zhuanqu_id': zhuanqu_id, - 'xiaoliang': duiwai_xiaoliang_value, - 'jieshao': jieshao, - 'xuzhi': xuzhi, - 'yaoqiuleixing': yaoqiuleixing_int, - 'huiyuan_id': huiyuan_id_value, - 'yongjin': str(yongjin_value) if yongjin_value else None, - 'tupian_url': shangpin_obj.tupian_url # 相对路径 - } - - # 返回成功响应 - return Response({ - 'code': 0, - 'msg': '商品发布成功', - 'data': response_data - }) - - except Exception as e: - # 捕获所有异常,返回500错误 - traceback.print_exc() - return Response({ - 'code': 500, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -class GuizeTupianShangchuanView(APIView): - """ - 规则图片上传接口 - 接收商品ID和规则图片 - 需要管理员权限 - """ - permission_classes = [permissions.IsAuthenticated] - parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 - - def post(self, request): - try: - # === 1. 权限验证 === - user_main = request.user - - # 1.1 验证账号(使用phone字段) - zhanghao = request.data.get('zhanghao', '').strip() - if not zhanghao: - return Response( - {'code': 401, 'msg': '未提供账号信息', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if user_main.phone != zhanghao: - return Response( - {'code': 401, 'msg': '账号验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.2 验证是否为管理员(检查user_type字段) - if user_main.user_type != 'admin': - return Response( - {'code': 401, 'msg': '权限不足', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.3 验证管理员扩展表(使用AdminProfile模型) - try: - admin_profile = AdminProfile.query.get(user=user_main) - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'msg': '管理员信息不存在', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # === 2. 接收并验证字段 === - # 2.1 验证商品ID - shangpin_id = request.data.get('shangpin_id') - if not shangpin_id: - return Response( - {'code': 400, 'msg': '商品ID不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2.2 验证规则图片 - guize_tupian = request.FILES.get('guize_tupian') - if not guize_tupian: - return Response( - {'code': 400, 'msg': '请上传规则图片', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证图片格式和大小 - is_valid, error_msg = validate_image(guize_tupian) - if not is_valid: - return Response( - {'code': 400, 'msg': f'规则图片验证失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # === 3. 验证商品是否存在 === - try: - shangpin_obj = Shangpin.query.get(id=shangpin_id) - except Shangpin.DoesNotExist: - return Response( - {'code': 400, 'msg': '商品不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # === 4. 处理规则图片上传 === - try: - # 4.1 生成文件名和路径 - file_extension = os.path.splitext(guize_tupian.name)[1].lower() - if not file_extension: - file_extension = '.jpg' - - # 构建OSS文件路径:a_long/Spgz{shangpin_id}/{filename} - oss_folder = 'a_long' # 根文件夹 - spgz_folder = f"Spgz{shangpin_id}" # 规则图片文件夹 - - # 生成文件名:Spgz{shangpin_id}{file_extension} - unique_filename = f"Spgz{shangpin_id}{file_extension}" - - # 构建完整路径 - oss_file_path = f"{oss_folder}/{spgz_folder}/{unique_filename}" - - # 4.2 上传到OSS - tupian_url = upload_to_oss(guize_tupian, oss_file_path) - - if not tupian_url: - # 上传失败 - return Response({ - 'code': 500, - 'msg': '规则图片上传失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 4.3 更新商品的规则图片URL(存储相对路径) - shangpin_obj.guize_tupian = oss_file_path # 存相对路径 - shangpin_obj.save() - - except Exception as oss_error: - # OSS上传失败 - print(f"规则图片OSS上传失败: {oss_error}") - return Response({ - 'code': 500, - 'msg': f'规则图片上传失败: {str(oss_error)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # === 5. 构建返回数据 === - response_data = { - 'shangpin_id': shangpin_id, - 'guize_tupian_url': oss_file_path # 相对路径 - } - - # 返回成功响应 - return Response({ - 'code': 0, - 'msg': '规则图片上传成功', - 'data': response_data - }) - - except Exception as e: - # 捕获所有异常,返回500错误 - traceback.print_exc() - return Response({ - 'code': 500, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - -class AdHuiyuanHqView(APIView): - """ - 管理员获取会员信息和商品类型接口 - 需要管理员权限 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - # === 1. 权限验证 === - # 获取当前用户 - user_main = request.user - - # 1.1 验证前端传递的账号是否匹配(使用phone字段) - zhanghao = request.data.get('zhanghao', '').strip() - if not zhanghao: - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 验证账号是否匹配(使用phone字段比较) - if user_main.phone != zhanghao: - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.2 验证是否为管理员(检查user_type字段) - if user_main.user_type != 'admin': - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.3 验证管理员扩展表(使用AdminProfile模型) - try: - admin_profile = AdminProfile.query.get(user=user_main) - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # === 2. 查询会员表数据 === - # 查询所有会员数据,按创建时间倒序排列 - huiyuan_queryset = Huiyuan.query.all().order_by('-create_time') - - # 构建会员数据列表 - huiyuan_list = [] - for huiyuan in huiyuan_queryset: - huiyuan_data = { - 'huiyuan_id': huiyuan.huiyuan_id, # 会员ID - 'jieshao': huiyuan.jieshao, # 会员介绍(会员标题) - 'jtjieshao': huiyuan.jtjieshao if huiyuan.jtjieshao else '', # 会员规则介绍(具体介绍) - 'jiage': str(huiyuan.jiage), # 会员价格(转换为字符串,避免Decimal序列化问题) - 'guanshifc': str(huiyuan.guanshifc), # 管事分成(转换为字符串) - 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数(销量) - 'create_time': huiyuan.create_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.create_time else None, - # 创建时间 - 'update_time': huiyuan.update_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.update_time else None - # 更新时间 - } - huiyuan_list.append(huiyuan_data) - - # === 3. 查询商品类型表数据 === - # 筛选抢单要求类型等于数字1的所有数据(yaoqiuleixing=1) - shangpinleixing_queryset = ShangpinLeixing.query.filter( - yaoqiuleixing=1 # 抢单要求类型为1(会员抢单) - ).order_by('id') # 按ID排序 - - # 构建商品类型数据列表 - shangpinleixing_list = [] - for leixing in shangpinleixing_queryset: - # 注意:这里只需要特定的字段 - leixing_data = { - 'id': leixing.id, # 商品类型ID - 'huiyuan_id': leixing.huiyuan_id, # 会员ID(关联字段) - 'jieshao': leixing.jieshao, # 类型介绍(商品类型名称) - 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' # 商品类型图片相对URL - } - shangpinleixing_list.append(leixing_data) - - # === 4. 构建返回数据 === - response_data = { - 'huiyuanList': huiyuan_list, - 'shangpinleixingList': shangpinleixing_list - } - - # 返回成功响应 - return Response({ - 'code': 0, - 'msg': '获取数据成功', - 'data': response_data - }) - - except Exception as e: - # 捕获所有异常,返回500错误 - traceback.print_exc() - return Response({ - 'code': 500, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - -class HuiyuanXiugaiView(APIView): - """ - 会员修改接口 - 需要管理员权限 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - # === 1. 权限验证 === - user_main = request.user - - # 1.1 验证前端传递的账号是否匹配(使用phone字段) - zhanghao = request.data.get('zhanghao', '').strip() - if not zhanghao: - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if user_main.phone != zhanghao: - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.2 验证是否为管理员(检查user_type字段) - if user_main.user_type != 'admin': - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.3 验证管理员扩展表 - try: - admin_profile = AdminProfile.query.get(user=user_main) - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # === 2. 接收并验证必填字段 === - huiyuan_id = request.data.get('huiyuan_id', '').strip() - if not huiyuan_id: - return Response( - {'code': 400, 'msg': '会员ID不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # === 3. 查询会员数据 === - try: - huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response( - {'code': 400, 'msg': '会员不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # === 4. 更新会员信息 === - # 4.1 获取前端传递的字段(只更新有值的字段) - jieshao = request.data.get('jieshao', '').strip() - jtjieshao = request.data.get('jtjieshao', '').strip() - jiage = request.data.get('jiage', '').strip() - guanshifc = request.data.get('guanshifc', '').strip() - - # 4.2 更新字段 - if jieshao: - # 验证名称长度(30个字符以内) - if len(jieshao) > 30: - return Response( - {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - huiyuan_obj.jieshao = jieshao - - if jtjieshao: - # 验证介绍长度(没有长度限制,但做一下基本检查) - if len(jtjieshao) > 2000: - return Response( - {'code': 400, 'msg': '会员介绍过长', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - huiyuan_obj.jtjieshao = jtjieshao - - if jiage: - try: - jiage_value = float(jiage) - if jiage_value < 0: - return Response( - {'code': 400, 'msg': '会员价格不能为负数', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - if jiage_value > 100000: - return Response( - {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - huiyuan_obj.jiage = jiage_value - except ValueError: - return Response( - {'code': 400, 'msg': '会员价格格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - if guanshifc: - try: - guanshifc_value = float(guanshifc) - if guanshifc_value < 0: - return Response( - {'code': 400, 'msg': '管事分成不能为负数', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - # 检查管事分成是否超过会员价格 - if hasattr(huiyuan_obj, 'jiage') and huiyuan_obj.jiage: - if guanshifc_value > float(huiyuan_obj.jiage): - return Response( - {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - huiyuan_obj.guanshifc = guanshifc_value - except ValueError: - return Response( - {'code': 400, 'msg': '管事分成格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 4.3 检查是否有任何字段需要更新 - if not (jieshao or jtjieshao or jiage or guanshifc): - return Response( - {'code': 400, 'msg': '没有需要更新的字段', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 4.4 保存更新 - huiyuan_obj.save() - - # === 5. 构建返回数据 === - response_data = { - 'huiyuan_id': huiyuan_obj.huiyuan_id, - 'jieshao': huiyuan_obj.jieshao, - 'jtjieshao': huiyuan_obj.jtjieshao if huiyuan_obj.jtjieshao else '', - 'jiage': str(huiyuan_obj.jiage), - 'guanshifc': str(huiyuan_obj.guanshifc), - 'goumai_cishu': huiyuan_obj.goumai_cishu, - 'create_time': huiyuan_obj.create_time.strftime( - '%Y-%m-%d %H:%M:%S') if huiyuan_obj.create_time else None, - 'update_time': huiyuan_obj.update_time.strftime( - '%Y-%m-%d %H:%M:%S') if huiyuan_obj.update_time else None - } - - # 返回成功响应 - return Response({ - 'code': 0, - 'msg': '会员信息修改成功', - 'data': response_data - }) - - except Exception as e: - # 捕获所有异常,返回500错误 - traceback.print_exc() - return Response({ - 'code': 500, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - - - - - -class AdHuiyuanFbView(APIView): - """ - 会员发布接口 - 需要管理员权限 - """ - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - # === 1. 权限验证 === - user_main = request.user - - # 1.1 验证前端传递的账号是否匹配 - zhanghao = request.data.get('zhanghao', '').strip() - if not zhanghao: - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if user_main.phone != zhanghao: - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.2 验证是否为管理员 - if user_main.user_type != 'admin': - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 1.3 验证管理员扩展表 - try: - admin_profile = AdminProfile.query.get(user=user_main) - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'msg': '权限验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # === 2. 接收并验证字段 === - jieshao = request.data.get('jieshao', '').strip() - jtjieshao = request.data.get('jtjieshao', '').strip() - jiage = request.data.get('jiage', '').strip() - guanshifc = request.data.get('guanshifc', '').strip() - - # 2.1 验证必填字段 - if not jieshao: - return Response( - {'code': 400, 'msg': '会员名称不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - if not jiage: - return Response( - {'code': 400, 'msg': '会员价格不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - if not guanshifc: - return Response( - {'code': 400, 'msg': '管事分成不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2.2 验证字段长度和格式 - if len(jieshao) > 30: - return Response( - {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - if jtjieshao and len(jtjieshao) > 2000: - return Response( - {'code': 400, 'msg': '会员介绍过长', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2.3 验证价格和分成格式 - try: - jiage_value = float(jiage) - if jiage_value <= 0: - return Response( - {'code': 400, 'msg': '会员价格必须大于0', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - if jiage_value > 100000: - return Response( - {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - except ValueError: - return Response( - {'code': 400, 'msg': '会员价格格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - try: - guanshifc_value = float(guanshifc) - if guanshifc_value < 0: - return Response( - {'code': 400, 'msg': '管事分成不能为负数', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - if guanshifc_value > jiage_value: - return Response( - {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - except ValueError: - return Response( - {'code': 400, 'msg': '管事分成格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # === 3. 生成会员ID === - # 生成6位唯一会员ID,格式:V + 5位随机数字字母 - def generate_huiyuan_id(): - # 使用V开头,后跟5位随机字符(数字和大写字母) - chars = string.digits + string.ascii_uppercase - while True: - random_part = ''.join(random.choices(chars, k=5)) - huiyuan_id = f"V{random_part}" - # 检查是否已存在 - if not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists(): - return huiyuan_id - - huiyuan_id = generate_huiyuan_id() - - # === 4. 创建会员记录 === - with transaction.atomic(): - huiyuan_obj = Huiyuan( - huiyuan_id=huiyuan_id, - jieshao=jieshao, - jtjieshao=jtjieshao if jtjieshao else '', - jiage=jiage_value, - guanshifc=guanshifc_value, - goumai_cishu=0, # 初始购买次数为0 - ) - - huiyuan_obj.save() - - # === 5. 构建返回数据 === - response_data = { - 'huiyuan_id': huiyuan_obj.huiyuan_id, - 'jieshao': huiyuan_obj.jieshao, - 'jtjieshao': huiyuan_obj.jtjieshao, - 'jiage': str(huiyuan_obj.jiage), - 'guanshifc': str(huiyuan_obj.guanshifc), - 'goumai_cishu': huiyuan_obj.goumai_cishu, - 'create_time': huiyuan_obj.create_time.strftime( - '%Y-%m-%d %H:%M:%S') if huiyuan_obj.create_time else None, - 'update_time': huiyuan_obj.update_time.strftime( - '%Y-%m-%d %H:%M:%S') if huiyuan_obj.update_time else None - } - - # 返回成功响应 - return Response({ - 'code': 0, - 'msg': '会员发布成功', - 'data': response_data - }) - - except Exception as e: - # 捕获所有异常,返回500错误 - traceback.print_exc() - return Response({ - 'code': 500, - 'msg': f'系统错误: {str(e)}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -class AdShangpinHuQu(APIView): - """ - 后台商品管理页面数据获取接口 - 路径: /shangpin/adsphq/ - 方法: POST - 权限: JWT认证 + 管理员权限 - 功能: 获取商品管理页面需要的所有数据 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取前端传递的账号 - qianzhanghao = request.data.get('zhanghao', '').strip() - if not qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 2. 从JWT Token获取当前用户 - dangqianyonghu = request.user - - # 3. 验证账号匹配 - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 4. 验证用户类型为管理员 - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 5. 验证管理员扩展表数据 - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 6. 查询公告数据(类型=1的公告) - gonggao_obj = Gonggao.query.filter(leixing=1).first() - shangpingonggao = gonggao_obj.jieshao if gonggao_obj else '' - - # 7. 查询轮播图数据(类型=1的轮播图) - lunbo_objs = Lunbo.query.filter(leixing=1).values('tupian_url') - shangpinlunbo = [item['tupian_url'] for item in lunbo_objs] - - # 8. 查询商品类型数据 - shangpinleixing_data = ShangpinLeixing.query.all().values( - 'id', # 类型ID - 'jieshao', # 类型介绍(类型名称) - 'tupian_url', # 类型图片URL - 'yaoqiuleixing', # 抢单要求类型(1=会员抢单,2=佣金抢单) - 'huiyuan_id', # 会员ID(如果yaoqiuleixing=1) - 'yongjin' # 佣金要求(如果yaoqiuleixing=2) - ) - - # 9. 查询商品专区数据 - shangpinzhuanqu_data = ShangpinZhuanqu.query.all().values( - 'id', # 专区ID - 'mingzi', # 专区名称 - 'leixing_id' # 所属类型ID - ) - - # 10. 查询会员数据 - huiyuan_data = Huiyuan.query.all().values( - 'huiyuan_id', # 会员ID - 'jieshao', # 会员介绍(会员名称) - 'jiage' # 会员价格 - ) - - # 11. 查询商品数据 - shangpin_data = Shangpin.query.all().values( - 'id', # 商品ID - 'biaoqian', # 商品标题/标签 - 'jiage', # 商品价格 - 'leixing_id', # 商品类型ID - 'zhuanqu_id', # 商品专区ID - 'tupian_url', # 商品图片URL - 'duiwai_xiaoliang' # 商品对外销量 - ) - - # 12. 构造返回数据 - response_data = { - 'code': 0, - 'message': '获取成功', - 'data': { - 'shangpingonggao': shangpingonggao, # 商品公告 - 'shangpinlunbo': shangpinlunbo, # 轮播图相对URL数组 - 'shangpinleixing': list(shangpinleixing_data), # 商品类型列表 - 'shangpinzhuanqu': list(shangpinzhuanqu_data), # 商品专区列表 - 'huiyuanliebiao': list(huiyuan_data), # 会员列表 - 'shangpinliebiao': list(shangpin_data), # 商品列表 - } - } - - return Response(response_data, status=status.HTTP_200_OK) - - except Exception as e: - print(f"获取商品管理数据异常: {e}") - return Response( - {'code': 500, 'message': '服务器内部错误'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - -# shangpin/views.py - -class AdGonggaoXiuGai(APIView): - """ - 后台编辑公告接口 - 路径: /shangpin/adggxg/ - 方法: POST - 权限: JWT认证 + 管理员权限 - 功能: 修改公告内容 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 获取前端传递的账号 - qianzhanghao = request.data.get('zhanghao', '').strip() - if not qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 2. 从JWT Token获取当前用户 - dangqianyonghu = request.user - - # 3. 验证账号匹配 - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 4. 验证用户类型为管理员 - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 5. 验证管理员扩展表数据 - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 6. 获取公告内容 - gonggao_neirong = request.data.get('gonggao', '').strip() - - # 7. 验证公告内容 - if not gonggao_neirong: - return Response( - {'code': 400, 'message': '公告内容不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - if len(gonggao_neirong) > 300: - return Response( - {'code': 400, 'message': '公告内容不能超过300字', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 8. 查询公告表,类型为1的公告 - gonggao_obj = Gonggao.query.filter(leixing=1).first() - - # 如果不存在,则创建 - if not gonggao_obj: - gonggao_obj = Gonggao(leixing=1) - - # 9. 更新公告内容 - gonggao_obj.jieshao = gonggao_neirong - gonggao_obj.save() - - # 10. 返回成功响应 - return Response({ - 'code': 0, - 'message': '公告更新成功', - 'data': { - 'gonggao': gonggao_neirong - } - }) - - except Exception as e: - print(f"编辑公告异常: {e}") - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - -class AdZhuanquXiuGai(APIView): - """ - 后台修改专区名称接口 - 路径: /shangpin/adzqxg/ - 方法: POST - 权限: JWT认证 + 管理员权限 - 功能: 修改专区名称 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 1. 权限验证 - qianzhanghao = request.data.get('zhanghao', '').strip() - if not qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - dangqianyonghu = request.user - - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 2. 获取专区ID和新的专区名称 - zhuanqu_id = request.data.get('zhuanqu_id') - mingzi = request.data.get('mingzi', '').strip() - - if not zhuanqu_id: - return Response( - {'code': 400, 'message': '专区ID不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - if not mingzi: - return Response( - {'code': 400, 'message': '专区名称不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - if len(mingzi) > 10: - return Response( - {'code': 400, 'message': '专区名称不能大于10个字', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 3. 查询专区 - try: - zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id) - except ShangpinZhuanqu.DoesNotExist: - return Response( - {'code': 400, 'message': '专区不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 4. 更新专区名称 - zhuanqu_obj.mingzi = mingzi - zhuanqu_obj.save() - - # 5. 返回成功响应 - return Response({ - 'code': 0, - 'message': '专区名称修改成功', - 'data': { - 'id': zhuanqu_obj.id, - 'mingzi': zhuanqu_obj.mingzi, - 'leixing_id': zhuanqu_obj.leixing_id - } - }) - - except Exception as e: - print(f"修改专区名称异常: {e}") - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - - -class AdLunboXiuGai(APIView): - """ - 后台编辑轮播图接口 - 路径: /shangpin/adlunboxg/ - 方法: POST - 权限: JWT认证 + 管理员权限 - 功能: 添加或删除轮播图 - """ - - permission_classes = [IsAuthenticated] - parser_classes = (MultiPartParser, FormParser,JSONParser) - - def post(self, request): - try: - # 1. 权限验证 - qianzhanghao = request.data.get('zhanghao', '').strip() - if not qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - dangqianyonghu = request.user - - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'message': '身份验证失败'}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 2. 获取操作类型 - action = request.data.get('action', '').strip() # 'add' 或 'delete' - - if action not in ['add', 'delete']: - return Response( - {'code': 400, 'message': '操作类型无效', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 3. 处理删除操作 - if action == 'delete': - tupian_url = request.data.get('tupian_url', '').strip() - - if not tupian_url: - return Response( - {'code': 400, 'message': '图片URL不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 查询轮播图记录 - lunbo_obj = Lunbo.query.filter(tupian_url=tupian_url, leixing=1).first() - - if not lunbo_obj: - return Response( - {'code': 400, 'message': '轮播图不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 从OSS删除图片 - delete_success = delete_from_oss(tupian_url) - - if not delete_success: - return Response( - {'code': 500, 'message': 'OSS删除失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 从数据库删除记录 - lunbo_obj.delete() - - return Response({ - 'code': 0, - 'message': '删除成功', - 'data': None - }) - - # 4. 处理添加操作 - else: # action == 'add' - tupian_file = request.FILES.get('file') - - if not tupian_file: - - - return Response( - {'code': 400, 'message': '请上传图片', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证图片 - is_valid, error_msg = validate_image(tupian_file) - if not is_valid: - #print(f"[轮播图上传] 图片验证失败详情: {error_msg}") - # 还可以记录文件大小、名称等信息 - #print(f"[轮播图上传] 问题文件: {tupian_file.name}, Size: {tupian_file.size}") - #print(is_valid) - return Response( - {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 生成文件名和路径 - file_extension = os.path.splitext(tupian_file.name)[1].lower() - if not file_extension: - file_extension = '.jpg' - - # 生成6位随机字符串作为文件名 - random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) - unique_filename = f"{random_str}{file_extension}" - - # 构建OSS文件路径:a_long/lunbo/{filename} - oss_folder = 'a_long' - lunbo_folder = 'lunbo' - - oss_file_path = f"{oss_folder}/{lunbo_folder}/{unique_filename}" - - # 上传到OSS - tupian_url = upload_to_oss(tupian_file, oss_file_path) - - if not tupian_url: - return Response( - {'code': 500, 'message': '图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 创建轮播图记录 - lunbo_obj = Lunbo.query.create( - tupian_url=oss_file_path, # 存相对路径 - leixing=1 - ) - - return Response({ - 'code': 0, - 'message': '添加成功', - 'data': { - 'tupian_url': lunbo_obj.tupian_url - } - }) - - except Exception as e: - #print(f"编辑轮播图异常: {e}") - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - - - -class AdLeixingXiuGai(APIView): - """ - 后台编辑商品类型接口 - 优化版 - 支持:只改图片、只改字段、两者都改 - 拒绝:两者都不改 - """ - - permission_classes = [IsAuthenticated] - parser_classes = (MultiPartParser, FormParser, JSONParser) # 添加JSONParser支持 - - def post(self, request): - try: - # 1. 权限验证(保持不变) - qianzhanghao = request.data.get('zhanghao', '').strip() - if not qianzhanghao: - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - dangqianyonghu = request.user - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - if dangqianyonghu.user_type != 'admin': - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) - - # 2. 获取商品类型ID - leixing_id = request.data.get('leixing_id') - if not leixing_id: - return Response({'code': 400, 'message': '商品类型ID不能为空', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 3. 查询商品类型 - try: - leixing_obj = ShangpinLeixing.query.get(id=leixing_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 400, 'message': '商品类型不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 4. 获取字段并初始化变更标记 - has_changed = False - jieshao = request.data.get('jieshao', '').strip() - yaoqiuleixing = request.data.get('yaoqiuleixing') - huiyuan_id = request.data.get('huiyuan_id', '').strip() - yongjin = request.data.get('yongjin', '').strip() - tupian_url = request.data.get('tupian_url', '').strip() # 原有图片URL - - # 5. 检查非图片字段是否有变更 - if jieshao and jieshao != leixing_obj.jieshao: - leixing_obj.jieshao = jieshao - has_changed = True - - if yaoqiuleixing and int(yaoqiuleixing) != leixing_obj.yaoqiuleixing: - leixing_obj.yaoqiuleixing = int(yaoqiuleixing) - has_changed = True - - # 根据类型清空另一个字段 - if leixing_obj.yaoqiuleixing == 1: - leixing_obj.yongjin = None - if huiyuan_id: - leixing_obj.huiyuan_id = huiyuan_id - # 验证会员存在 - try: - Huiyuan.query.get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - else: # 类型2 - leixing_obj.huiyuan_id = None - if yongjin: - try: - leixing_obj.yongjin = float(yongjin) - if leixing_obj.yongjin <= 0: - return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - except ValueError: - return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # ===== 在现有代码的第5步后面添加 ===== - - # 5.5 检查抢单要求类型不变时,会员ID或佣金是否有变更 - if yaoqiuleixing and int(yaoqiuleixing) == leixing_obj.yaoqiuleixing: - # 类型没有变化,但需要检查对应字段的变化 - - # 情况1:会员抢单 (yaoqiuleixing == 1) - if leixing_obj.yaoqiuleixing == 1: - # 检查会员ID是否变更 - if huiyuan_id and huiyuan_id != leixing_obj.huiyuan_id: - # 验证会员存在 - try: - Huiyuan.query.get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - leixing_obj.huiyuan_id = huiyuan_id - has_changed = True - # print(f"会员ID变更: {leixing_obj.huiyuan_id} -> {huiyuan_id}") - - # 情况2:佣金抢单 (yaoqiuleixing == 2) - elif leixing_obj.yaoqiuleixing == 2: - # 检查佣金是否变更 - if yongjin: - try: - new_yongjin = float(yongjin) - if new_yongjin <= 0: - return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 判断是否有变化(考虑None的情况) - current_yongjin = leixing_obj.yongjin - if (current_yongjin is None and new_yongjin != 0) or \ - (current_yongjin is not None and abs( - float(current_yongjin) - new_yongjin) > 0.001): - leixing_obj.yongjin = new_yongjin - has_changed = True - # print(f"佣金变更: {current_yongjin} -> {new_yongjin}") - - except ValueError: - return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # ===== 添加结束 ===== - - # 6. 处理图片上传(如果有新图片) - tupian_file = request.FILES.get('file') - if tupian_file: - # 验证图片 - is_valid, error_msg = validate_image(tupian_file) - if not is_valid: - return Response({'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST) - - # 删除旧图片(如果存在) - if leixing_obj.tupian_url: - delete_from_oss(leixing_obj.tupian_url) - - # 生成文件名和路径 - file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' - oss_folder = 'a_long' - leixing_folder = 'leixing' - - # 使用原文件名或生成新文件名 - if leixing_obj.tupian_url: - original_filename = leixing_obj.tupian_url.split('/')[-1] - else: - original_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}" - - oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{original_filename}" - - # 上传到OSS - new_tupian_url = upload_to_oss(tupian_file, oss_file_path) - if not new_tupian_url: - return Response({'code': 500, 'message': '图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - leixing_obj.tupian_url = oss_file_path # 存相对路径 - has_changed = True - elif tupian_url and tupian_url != leixing_obj.tupian_url: - # 前端传了新URL(可能是从其他地方选择的) - leixing_obj.tupian_url = tupian_url - has_changed = True - - # 7. 验证是否至少有变更 - if not has_changed: - return Response({ - 'code': 400, - 'message': '没有检测到任何更改', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 8. 保存并返回 - leixing_obj.save() - # 8. 保存商品类型 - leixing_obj.save() - - # 🔴 ===========【新增:同步更新关联商品】=========== - try: - # 1. 准备商品更新字典 - shangpin_update_fields = {} - - # 始终同步抢单要求类型 - shangpin_update_fields['yaoqiuleixing'] = leixing_obj.yaoqiuleixing - - # 根据抢单类型,同步对应字段并清空另一个 - if leixing_obj.yaoqiuleixing == 1: # 会员抢单 - if leixing_obj.huiyuan_id: # 仅当会员ID非空时更新 - shangpin_update_fields['huiyuan_id'] = leixing_obj.huiyuan_id - # 清空佣金字段 - shangpin_update_fields['yongjin'] = None - elif leixing_obj.yaoqiuleixing == 2: # 佣金抢单 - if leixing_obj.yongjin: # 仅当佣金非空时更新 - shangpin_update_fields['yongjin'] = leixing_obj.yongjin - # 清空会员字段 - shangpin_update_fields['huiyuan_id'] = None - - # 2. 执行批量更新(确保只有实际有变化的字段才会执行) - if shangpin_update_fields: - # 查询所有属于该类型的商品(使用filter而非all以提高效率) - affected_count = Shangpin.query.filter( - leixing_id=leixing_obj.id - ).update(**shangpin_update_fields) - - # 可选:记录日志以便追踪 - # logger.info(f"商品类型{leixing_obj.id}修改成功,已同步更新{affected_count}个关联商品") - - except Exception as sync_error: - # 商品同步失败不影响商品类型本身的修改,但记录错误 - # 可根据需求决定是否抛出异常回滚整个事务 - # 当前设计:记录错误但允许商品类型修改成功 - logger.error(f"修改十五") - - # 可在此添加更详细的错误日志记录 - # 🔴 ===========【新增结束】=========== - - - - - - - - response_data = { - 'id': leixing_obj.id, - 'jieshao': leixing_obj.jieshao, - 'tupian_url': leixing_obj.tupian_url, - 'yaoqiuleixing': leixing_obj.yaoqiuleixing, - 'huiyuan_id': leixing_obj.huiyuan_id, - 'yongjin': str(leixing_obj.yongjin) if leixing_obj.yongjin else None - } - - return Response({ - 'code': 0, - 'message': '商品类型修改成功', - 'data': response_data - }) - - except Exception as e: - print(f"编辑商品类型异常: {e}") - return Response({'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - -logger = logging.getLogger(__name__) - - -class AdShangpinNeiRongHuoQu(APIView): - """ - 后台获取商品详情接口 - 路径: /shangpin/adspnrhq/ - 方法: POST - 权限: JWT认证 + 管理员权限 - 功能: 获取指定商品的详细信息 - """ - - permission_classes = [IsAuthenticated] - parser_classes = [JSONParser] - - def post(self, request): - try: - # 1. 获取前端数据 - qianzhanghao = request.data.get('zhanghao', '').strip() - shangpin_id = request.data.get('shangpin_id') - - if not qianzhanghao or not shangpin_id: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 2. 权限验证 - dangqianyonghu = request.user - - # 验证phone字段匹配 - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - logger.warning( - f"账号不匹配: 当前用户phone={getattr(dangqianyonghu, 'phone', '无')}, 前端账号={qianzhanghao}") - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 验证用户类型 - if dangqianyonghu.user_type != 'admin': - logger.warning(f"非管理员访问: user_type={dangqianyonghu.user_type}") - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 3. 验证管理员扩展表 - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - logger.warning(f"管理员扩展表不存在: user_id={dangqianyonghu.id}") - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 4. 查询商品数据 - try: - shangpin_obj = Shangpin.query.get(id=shangpin_id) - except Shangpin.DoesNotExist: - return Response( - {'code': 404, 'message': '商品不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 5. 构建返回数据(与前端字段对应) - response_data = { - 'tupian_url': shangpin_obj.tupian_url or '', # 商品图片相对URL - 'biaoti': shangpin_obj.biaoqian or '', # 商品标题(前端用biaoti,对应biaoqian) - 'jieshao': shangpin_obj.jieshao or '', # 商品介绍 - 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', # 商品价格 - 'kucun': shangpin_obj.kucun, # 商品库存 - 'zhenshixiaoliang': shangpin_obj.zhenshi_xiaoliang, # 商品真实销量 - 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, # 商品对外销量 - 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', # 下单须知 - 'guizetupian_url': shangpin_obj.guize_tupian or '', # 规则图片相对URL - 'yajin': str(shangpin_obj.yajin) if hasattr(shangpin_obj, 'yajin') and shangpin_obj.yajin else None, - # 押金(可能不存在) - 'chuangjianshijian': shangpin_obj.create_time.strftime( - '%Y-%m-%d %H:%M:%S') if shangpin_obj.create_time else '', - 'genggaishijian': shangpin_obj.update_time.strftime( - '%Y-%m-%d %H:%M:%S') if shangpin_obj.update_time else '', - # 额外字段,前端可能不需要但可以保留 - 'leixing_id': shangpin_obj.leixing_id, - 'zhuanqu_id': shangpin_obj.zhuanqu_id, - 'yaoqiuleixing': shangpin_obj.yaoqiuleixing, - 'huiyuan_id': shangpin_obj.huiyuan_id, - 'yongjin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None, - } - - return Response({ - 'code': 0, - 'message': '获取成功', - 'data': response_data - }) - - except Exception as e: - logger.error(f"获取商品详情异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - - - - - - -class AdShangpinXiuGai(APIView): - """ - 后台更改商品数据接口 - 修复版 - 修复:1. 每次上传生成新文件名 2. 解决浏览器缓存问题 - """ - - permission_classes = [IsAuthenticated] - parser_classes = (MultiPartParser, FormParser, JSONParser) - - def post(self, request): - try: - # 1. 权限验证 - qianzhanghao = request.data.get('zhanghao', '').strip() - shangpin_id = request.data.get('shangpin_id') - - if not qianzhanghao or not shangpin_id: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证身份 - dangqianyonghu = request.user - - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 2. 判断操作类型:删除还是修改 - action = request.data.get('action', 'update').strip().lower() - - if action == 'delete': - return self._delete_shangpin(shangpin_id) - else: - return self._update_shangpin(request, shangpin_id) - - except Exception as e: - logger.error(f"更改商品数据异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - def _delete_shangpin(self, shangpin_id): - """删除商品逻辑""" - try: - # 查询商品 - try: - shangpin_obj = Shangpin.query.get(id=shangpin_id) - except Shangpin.DoesNotExist: - return Response( - {'code': 404, 'message': '商品不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 获取图片URL(如果有) - tupian_url = shangpin_obj.tupian_url - guizetupian_url = shangpin_obj.guize_tupian - - # 异步或同步删除OSS图片 - if tupian_url: - try: - delete_from_oss(tupian_url) - except Exception as e: - logger.warning(f"删除商品图片失败: {tupian_url}, 错误: {e}") - - if guizetupian_url: - try: - delete_from_oss(guizetupian_url) - except Exception as e: - logger.warning(f"删除规则图片失败: {guizetupian_url}, 错误: {e}") - - # 删除数据库记录 - shangpin_obj.delete() - - return Response({ - 'code': 0, - 'message': '删除成功', - 'data': { - 'shangpin_id': shangpin_id, - 'deleted': True - } - }) - - except Exception as e: - logger.error(f"删除商品异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '删除失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - def _update_shangpin(self, request, shangpin_id): - """修改商品逻辑 - 修复:每次上传生成新文件名""" - try: - # 查询商品 - try: - shangpin_obj = Shangpin.query.get(id=shangpin_id) - except Shangpin.DoesNotExist: - return Response( - {'code': 404, 'message': '商品不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - has_changes = False - - # 获取原图片URL(如果有) - old_tupian_url = shangpin_obj.tupian_url - - # 3. 处理商品图片上传(如果有新商品图片) - tupian_file = request.FILES.get('tupian_file') - new_tupian_url = None - - if tupian_file: - # 验证图片 - is_valid, error_msg = validate_image(tupian_file) - if not is_valid: - return Response( - {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 生成新文件名:时间戳 + 随机数 + 原扩展名 - timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳 - random_num = random.randint(1000, 9999) - file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' - new_filename = f"sp_{timestamp}_{random_num}{file_extension}" - - # 确定OSS路径 - oss_folder = 'a_long' - shangpin_folder = 'shangpin' - oss_file_path = f"{oss_folder}/{shangpin_folder}/{new_filename}" - - # 删除旧图片(如果存在) - if old_tupian_url: - try: - delete_from_oss(old_tupian_url) - logger.info(f"已删除旧商品图片: {old_tupian_url}") - except Exception as e: - logger.warning(f"删除旧商品图片失败: {old_tupian_url}, 错误: {e}") - # 继续上传新图片,不中断流程 - - # 上传到OSS - new_tupian_url = upload_to_oss(tupian_file, oss_file_path) - - if not new_tupian_url: - return Response( - {'code': 500, 'message': '商品图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 更新数据库中的图片URL - shangpin_obj.tupian_url = oss_file_path # 存相对路径 - has_changes = True - logger.info(f"商品图片更新成功: {oss_file_path}") - - # 4. 处理其他字段(只更新有变化的字段) - # 注意:这里同时处理 request.data(文本字段)和可能有文件的情况 - field_mapping = { - 'biaoti': 'biaoqian', - 'jieshao': 'jieshao', - 'jiage': 'jiage', - 'kucun': 'kucun', - 'duiwaixiaoliang': 'duiwai_xiaoliang', - 'xiadanxuzhi': 'xiadan_xuzhi', - 'yajin': 'yajin', - } - - for front_field, db_field in field_mapping.items(): - # 注意:request.data 可能包含文件和其他字段,但DRF会处理好 - if front_field in request.data: - new_value = request.data.get(front_field) - - # 空字符串处理 - if new_value == '': - new_value = None - - if new_value is not None: - # 获取当前值 - current_value = getattr(shangpin_obj, db_field) - - # 类型转换 - try: - if db_field in ['jiage', 'yajin']: - # 价格和押金 - if new_value: - new_value = Decimal(str(new_value)) - else: - new_value = None - elif db_field in ['kucun', 'duiwai_xiaoliang']: - # 库存和对外销量 - if new_value: - new_value = int(float(new_value)) - else: - new_value = 0 - except (ValueError, TypeError, InvalidOperation) as e: - logger.warning(f"字段{front_field}转换失败: {new_value}, 错误: {e}") - continue - - # 检查是否有变化(处理None值) - if current_value != new_value: - setattr(shangpin_obj, db_field, new_value) - has_changes = True - - # 5. 如果没有检测到任何更改 - if not has_changes: - return Response({ - 'code': 400, - 'message': '没有检测到任何更改', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 6. 保存更改 - shangpin_obj.save() - - # 7. 返回更新后的数据 - response_data = { - 'tupian_url': shangpin_obj.tupian_url or '', - 'biaoti': shangpin_obj.biaoqian or '', - 'jieshao': shangpin_obj.jieshao or '', - 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', - 'kucun': shangpin_obj.kucun, - 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, - 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', - 'yajin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None, - 'genggaishijian': shangpin_obj.update_time.strftime( - '%Y-%m-%d %H:%M:%S') if shangpin_obj.update_time else '', - } - - return Response({ - 'code': 0, - 'message': '更新成功', - 'data': response_data - }) - - except Exception as e: - logger.error(f"更新商品异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '更新失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - -class AdShangpinGuizeXiuGai(APIView): - """ - 后台规则图片更换接口 - 修复版 - 修复:每次上传生成新文件名,避免浏览器缓存 - """ - - permission_classes = [IsAuthenticated] - parser_classes = (MultiPartParser, FormParser, JSONParser) - - def post(self, request): - try: - # 1. 权限验证 - qianzhanghao = request.data.get('zhanghao', '').strip() - shangpin_id = request.data.get('shangpin_id') - action = request.data.get('action', 'update').strip().lower() - - if not qianzhanghao or not shangpin_id: - return Response( - {'code': 400, 'message': '参数不完整', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证身份 - dangqianyonghu = request.user - - if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - if dangqianyonghu.user_type != 'admin': - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - try: - guanliyuan = dangqianyonghu.admin_profile - except AdminProfile.DoesNotExist: - return Response( - {'code': 401, 'message': '身份验证失败', 'data': None}, - status=status.HTTP_401_UNAUTHORIZED - ) - - # 2. 查询商品 - try: - shangpin_obj = Shangpin.query.get(id=shangpin_id) - except Shangpin.DoesNotExist: - return Response( - {'code': 404, 'message': '商品不存在', 'data': None}, - status=status.HTTP_404_NOT_FOUND - ) - - # 3. 处理不同操作 - if action == 'delete': - return self._delete_guize_image(shangpin_obj) - elif action == 'update': - return self._update_guize_image(request, shangpin_obj) - else: - return Response( - {'code': 400, 'message': '操作类型无效', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - except Exception as e: - logger.error(f"规则图片操作异常: {str(e)}", exc_info=True) - return Response( - {'code': 500, 'message': '服务器内部错误', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - def _delete_guize_image(self, shangpin_obj): - """删除规则图片""" - guizetupian_url = shangpin_obj.guize_tupian - - if guizetupian_url: - # 从OSS删除 - try: - delete_success = delete_from_oss(guizetupian_url) - '''if delete_success: - logger.info(f"已删除规则图片: {guizetupian_url}") - else: - logger.warning(f"OSS删除失败,但继续删除数据库记录: {guizetupian_url}")''' - except Exception as e: - logger.warning(f"删除规则图片异常: {e}") - - # 清除数据库记录 - shangpin_obj.guize_tupian = '' - shangpin_obj.save() - - return Response({ - 'code': 0, - 'message': '删除成功', - 'data': { - 'guizetupian_url': '' - } - }) - - def _update_guize_image(self, request, shangpin_obj): - """更新规则图片 - 修复:每次上传生成新文件名""" - guize_file = request.FILES.get('guize_file') - - if not guize_file: - return Response( - {'code': 400, 'message': '请上传规则图片', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 验证图片 - is_valid, error_msg = validate_image(guize_file) - if not is_valid: - return Response( - {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 获取原规则图片URL - old_guizetupian_url = shangpin_obj.guize_tupian - - # 生成新文件名:时间戳 + 随机数 + 原扩展名 - timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳 - random_num = random.randint(1000, 9999) - file_extension = os.path.splitext(guize_file.name)[1].lower() or '.jpg' - new_filename = f"gz_{timestamp}_{random_num}{file_extension}" - - # 确定OSS路径 - oss_folder = 'a_long' - guize_folder = 'guize' # 修改为你的要求:a_long/guize/ - oss_file_path = f"{oss_folder}/{guize_folder}/{new_filename}" - - # 删除旧图片(如果存在) - if old_guizetupian_url: - try: - delete_from_oss(old_guizetupian_url) - #logger.info(f"已删除旧规则图片: {old_guizetupian_url}") - except Exception as e: - logger.warning(f"删除旧规则图片失败: {old_guizetupian_url}, 错误: {e}") - # 继续上传新图片,不中断流程 - - # 上传到OSS - new_guizetupian_url = upload_to_oss(guize_file, oss_file_path) - - if not new_guizetupian_url: - return Response( - {'code': 500, 'message': '规则图片上传失败', 'data': None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 更新数据库(存相对路径) - shangpin_obj.guize_tupian = oss_file_path - shangpin_obj.save() - - #logger.info(f"规则图片更新成功: {oss_file_path}") - - return Response({ - 'code': 0, - 'message': '规则图片更新成功', - 'data': { - 'guizetupian_url': oss_file_path # 返回相对路径 - } - }) - - - - -# views.py -class HuiyuanTypeListView(APIView): - """ - 获取会员类型列表接口 - 路径:/shangpin/adhyjllxcx - 方法:POST - 权限:JWT Token + 管理员验证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - # 🔥 1. 身份验证 - user = request.user - - # 检查是否是管理员 - if not hasattr(user, 'admin_profile'): - return Response({ - 'code': 401, - 'message': '无权限访问', - 'data': None - }, status=status.HTTP_401_UNAUTHORIZED) - - # 🔥 2. 获取所有会员类型 - huiyuan_list = Huiyuan.query.all().order_by('create_time') - - # 🔥 3. 构建返回数据 - huiyuan_data = [] - for huiyuan in huiyuan_list: - huiyuan_data.append({ - 'id': huiyuan.huiyuan_id, # 会员ID - 'jieshao': huiyuan.jieshao, # 会员介绍 - 'jiage': str(huiyuan.jiage), # 价格 - 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数 - 'create_time': huiyuan.create_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.create_time 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, 'admin_profile'): - 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.create_time, - 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.create_time 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, - 'create_time': 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. 获取会员类型统计信息(所有会员类型的当前状态总数) - huiyuan_types = Huiyuan.query.all() - huiyuan_stats = [] - - for huiyuan in huiyuan_types: - # 使用COUNT查询统计 - stats_sql = """ - SELECT COUNT(*) - FROM huiyuangoumai - WHERE huiyuan_id = %s - """ - stats_params = [huiyuan.huiyuan_id] - - if jiluzhuangtai == 1: # 未过期 - stats_sql += " AND daoqi_time > %s" - stats_params.append(now) - else: # 已过期 - stats_sql += " AND daoqi_time <= %s" - stats_params.append(now) - - with connection.cursor() as cursor: - cursor.execute(stats_sql, stats_params) - count = cursor.fetchone()[0] - - huiyuan_stats.append({ - 'id': huiyuan.huiyuan_id, - 'jieshao': huiyuan.jieshao, - 'count': count - }) - - 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.yonghuid).count() - - # 3.3 分红次数(总记录数) - base_queryset = Gsfenhong.query.filter(zuzhang_id=current_user.yonghuid).order_by('-create_time') - 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'))) - - # 时间格式化 - create_time = rec.create_time.strftime('%Y-%m-%d %H:%M:%S') if rec.create_time 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, - 'create_time': create_time, - }) - - # 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(yonghuid__in=guanshi_ids).select_related( - 'boss_profile' - ).only( - 'yonghuid', 'avatar', - 'boss_profile__nickname' - ) - - info = {} - for user in users: - avatar = user.avatar or '' - # 从 boss_profile 获取昵称 - if hasattr(user, 'boss_profile') and user.boss_profile: - nicheng = user.boss_profile.nickname or '' - else: - # 如果没有 boss_profile,使用默认昵称 - nicheng = f"用户{user.yonghuid[-4:]}" - info[user.yonghuid] = {'avatar': avatar, 'nicheng': nicheng} - - # 对于未查询到的用户,提供默认值 - for gid in guanshi_ids: - if gid not in info: - info[gid] = {'avatar': '', 'nicheng': f"用户{gid[-4:]}"} - - return info - - - -# 常量定义(可根据需要调整) -INTEGRAL_PRICE = Decimal('5.00') # 积分购买固定价格(元) - - -class CzhqdyView(APIView): - """ - 获取可用抵扣身份接口 - 路径:/shangpin/czhqdy - 方法:POST - 权限:JWT认证 - 请求参数: - leixing: int # 1=会员,2=押金,3=积分,4=罚款 - huiyuan_id: str # 当 leixing=1 时必传,会员ID - yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元) - fadan_id: int # 当 leixing=4 时必传,罚单ID - 返回: - { - "code": 200, - "msg": "success", - "data": [ - { - "id": 1, # 身份ID:1打手佣金 2管事分红 3组长分红 4打手押金 - "jieshao": "打手佣金抵扣", - "xuyao": "22.22" # 所需抵扣金额(保留两位小数) - }, - ... - ] - } - """ - permission_classes = [permissions.IsAuthenticated] - - # 身份ID与对应扩展表模型、余额字段、费率查询条件的映射 - IDENTITY_CONFIG = [ - { - 'id': 1, - 'name': '打手佣金抵扣', - 'profile_attr': 'dashou_profile', # User 的反向 related_name - 'balance_field': 'yue', - 'rate_key': '5', - 'model': UserDashou, - }, - { - 'id': 2, - 'name': '管事分红抵扣', - 'profile_attr': 'guanshi_profile', - 'balance_field': 'yue', - 'rate_key': '6', - 'model': UserGuanshi, - }, - { - 'id': 3, - 'name': '组长分红抵扣', - 'profile_attr': 'zuzhang_profile', - 'balance_field': 'ketixian_jine', - 'rate_key': '8', - 'model': UserZuzhang, - }, - # 新增打手押金抵扣 - { - 'id': 4, - 'name': '打手押金抵扣', - 'profile_attr': 'dashou_profile', # 与打手佣金共用扩展表 - 'balance_field': 'yajin', # 押金字段 - 'rate_key': '11', # 押金提现费率对应的 fadanpingtai - 'model': UserDashou, - }, - ] - - def post(self, request): - # 获取参数 - leixing = request.data.get('leixing') - huiyuan_id = request.data.get('huiyuan_id') - yajin_jine = request.data.get('yajin_jine') - fadan_id = request.data.get('fadan_id') - - # 参数校验 - if leixing not in [1, 2, 3, 4]: - return Response({'code': 400, 'msg': 'leixing 必须为 1,2,3,4'}, status=status.HTTP_400_BAD_REQUEST) - - if leixing == 1 and not huiyuan_id: - return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST) - - if leixing == 2: - if not yajin_jine: - return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST) - try: - yajin_jine = Decimal(str(yajin_jine)) - if yajin_jine <= 0: - raise ValueError - except: - return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST) - - if leixing == 4 and not fadan_id: - return Response({'code': 400, 'msg': '缴纳罚款时必须提供 fadan_id'}, status=status.HTTP_400_BAD_REQUEST) - - # 获取当前用户 - user = request.user - - # 获取目标价格(需支付的原价) - target_price = None - if leixing == 1: - # 会员 - try: - huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) - target_price = huiyuan.jiage - except Huiyuan.DoesNotExist: - return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND) - elif leixing == 2: - # 押金 - target_price = yajin_jine - elif leixing == 3: - # 积分(固定价格) - target_price = INTEGRAL_PRICE - elif leixing == 4: - # 罚款缴纳 - try: - fadan = Fadan.query.get(id=fadan_id, beichufa_id=user.yonghuid) - target_price = fadan.fakuanjine - except Fadan.DoesNotExist: - return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) - - # 获取所有费率(打手佣金、管事分红、组长分红、打手押金) - rates = {} - for key in ['5', '6', '8', '11']: - try: - rate_obj = Lilubiao.query.get(fadanpingtai=key) - rates[key] = rate_obj.lilu - except Lilubiao.DoesNotExist: - rates[key] = None # 未配置则跳过 - - # 构建可用身份列表 - available_list = [] - - for ident in self.IDENTITY_CONFIG: - # 检查用户是否有该扩展表 - profile = getattr(user, ident['profile_attr'], None) - if not profile: - continue - - # 获取余额 - balance = getattr(profile, ident['balance_field'], Decimal('0.00')) - - # 获取费率 - rate = rates.get(ident['rate_key']) - if rate is None: - continue # 费率未配置,跳过该身份 - - # 计算所需抵扣金额: target_price / (1 - rate) ,向上取整到分 - try: - if rate >= 1: - continue # 防止除零及不合理费率 - required = target_price / (Decimal('1.0') - rate) - # 向上取整到分(保留两位小数,最后一位进位) - required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING) - except Exception: - continue # 计算异常,跳过 - - # 余额是否足够 - if balance >= required: - available_list.append({ - 'id': ident['id'], - 'jieshao': ident['name'], - 'xuyao': str(required), - }) - - return Response({ - 'code': 200, - 'msg': 'success', - 'data': available_list - }) - - - -INTEGRAL_INCREMENT = 5 -MAX_INTEGRAL = 10 - -IDENTITY_CONFIG = { - 1: { - 'name': '打手佣金', - 'model': UserDashou, - 'balance_field': 'yue', - 'status_field': 'zhanghaozhuangtai', - 'rate_key': '5', - }, - 2: { - 'name': '管事分红', - 'model': UserGuanshi, - 'balance_field': 'yue', - 'status_field': 'zhuangtai', - 'rate_key': '6', - }, - 3: { - 'name': '组长分红', - 'model': UserZuzhang, - 'balance_field': 'ketixian_jine', - 'status_field': 'zhuangtai', - 'rate_key': '8', - }, - 4: { - 'name': '打手押金', - 'model': UserDashou, - 'balance_field': 'yajin', - 'status_field': 'zhanghaozhuangtai', - 'rate_key': '11', - }, -} - - -class DsqrgmdhView(APIView): - permission_classes = [permissions.IsAuthenticated] - - def post(self, request): - try: - leixing = request.data.get('leixing') - shenfen_id = request.data.get('shenfen_id') - huiyuan_id = request.data.get('huiyuan_id') - yajin_jine = request.data.get('yajin_jine') - fadan_id = request.data.get('fadan_id') - - logger.info(f"抵扣支付请求: leixing={leixing}, shenfen_id={shenfen_id}, huiyuan_id={huiyuan_id}, yajin_jine={yajin_jine}, fadan_id={fadan_id}") - - if leixing not in [1, 2, 3, 4]: - return Response({'code': 400, 'msg': 'leixing 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST) - if shenfen_id not in [1, 2, 3, 4]: - return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST) - - if leixing == 1 and not huiyuan_id: - return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST) - if leixing == 2: - if not yajin_jine: - return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST) - try: - yajin_jine = Decimal(str(yajin_jine)) - if yajin_jine <= 0: - raise ValueError - if yajin_jine > Decimal('10000'): - return Response({'code': 400, 'msg': '押金金额不能超过10000元'}, status=status.HTTP_400_BAD_REQUEST) - except: - return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST) - - target_price = None - if leixing == 1: - try: - huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) - target_price = huiyuan.jiage - except Huiyuan.DoesNotExist: - return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND) - elif leixing == 2: - target_price = yajin_jine - elif leixing == 3: - target_price = INTEGRAL_PRICE - elif leixing == 4: - if not fadan_id: - return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) - try: - fadan_obj = Fadan.query.get(id=fadan_id, beichufa_id=request.user.yonghuid) - target_price = fadan_obj.fakuanjine - except Fadan.DoesNotExist: - return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) - - identity = IDENTITY_CONFIG.get(shenfen_id) - try: - rate_obj = Lilubiao.query.get(fadanpingtai=identity['rate_key']) - rate = rate_obj.lilu - if rate >= 1: - return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST) - except Lilubiao.DoesNotExist: - return Response({'code': 400, 'msg': f"{identity['name']}费率未配置"}, status=status.HTTP_400_BAD_REQUEST) - - logger.info(f"目标价格={target_price}, 费率={rate}") - - try: - required = target_price / (Decimal('1.0') - rate) - required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING) - logger.info(f"所需抵扣金额={required}") - except Exception as e: - logger.error(f"金额计算错误: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': f'金额计算错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - user = request.user - yonghuid = user.yonghuid - - try: - profile = identity['model'].query.get(user=user) - except identity['model'].DoesNotExist: - return Response({'code': 403, 'msg': f'您不是{identity["name"]}身份'}) - - status_value = getattr(profile, identity['status_field']) - if status_value != 1: - return Response({'code': 403, 'msg': f'{identity["name"]}账号状态异常,无法使用'}) - - balance = getattr(profile, identity['balance_field']) - if balance < required: - return Response({'code': 400, 'msg': f'{identity["name"]}余额不足,需要{required}元,当前{balance}元'}, status=status.HTTP_400_BAD_REQUEST) - - try: - with transaction.atomic(): - locked_profile = identity['model'].objects.select_for_update().get(id=profile.id) - current_balance = getattr(locked_profile, identity['balance_field']) - if current_balance < required: - raise ValueError(f"{identity['name']}余额不足,需要{required}元,当前{current_balance}元") - - setattr(locked_profile, identity['balance_field'], current_balance - required) - locked_profile.save(update_fields=[identity['balance_field']]) - logger.info(f"扣款成功: {identity['name']} 扣除 {required} 元,剩余 {current_balance - required} 元") - - timestamp = str(int(time.time() * 1000))[-12:] - rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4)) - dingdan_id = f"CZ{timestamp}{rand_str}" - - if leixing == 1: - action_desc = f"购买会员 {huiyuan.jieshao}" - elif leixing == 2: - action_desc = f"充值押金 {yajin_jine}元" - elif leixing == 3: - action_desc = "补充积分" - elif leixing == 4: - action_desc = f"缴纳罚款 {target_price}元" - - shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}元" - - czjilu = Czjilu.query.create( - dingdan_id=dingdan_id, - huiyuan_id=huiyuan_id if leixing == 1 else None, - zhuangtai=1, - yonghuid=yonghuid, - jine=required, - leixing=leixing, - shuoming=shuoming, - ) - - response_data = {} - - if leixing == 1: - huiyuan_record, is_first_buy = self._process_member_purchase(yonghuid, huiyuan_id, huiyuan) - response_data['huiyuan'] = { - 'huiyuanid': huiyuan_id, - 'huiyuanming': huiyuan.jieshao, - 'daoqi': huiyuan_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(huiyuan_record, 'daoqi_time') else '', - } - if is_first_buy: - try: - dashou = UserDashou.query.get(user=user) - dashou.jifen += 10 - dashou.save(update_fields=['jifen']) - response_data['jifen'] = dashou.jifen - except UserDashou.DoesNotExist: - pass - - elif leixing == 2: - try: - dashou = UserDashou.objects.select_for_update().get(user=user) - dashou.yajin += yajin_jine - dashou.save(update_fields=['yajin']) - response_data['yajin'] = dashou.yajin - except UserDashou.DoesNotExist: - return Response({'code': 500, 'msg': '打手信息不存在,无法充值押金'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - elif leixing == 4: - if fadan_obj.zhuangtai not in [1, 3]: - return Response({'code': 400, 'msg': '罚单状态不可缴纳'}, status=status.HTTP_400_BAD_REQUEST) - fadan_obj.zhuangtai = 2 - fadan_obj.save(update_fields=['zhuangtai']) - response_data['fadan_id'] = fadan_obj.id - response_data['fadan_status'] = fadan_obj.zhuangtai - - # ========== 修改点2: 使用最新的分红处理方法 ========== - success, msg = process_fadan_fenhong(fadan_obj.id) - if not success: - logger.error(f"罚单 {fadan_obj.id} 分红处理失败: {msg}") - else: - logger.info(f"罚单 {fadan_obj.id} 分红处理成功: {msg}") - - else: # leixing == 3 - try: - dashou = UserDashou.objects.select_for_update().get(user=user) - new_jifen = dashou.jifen + INTEGRAL_INCREMENT - dashou.jifen = min(new_jifen, MAX_INTEGRAL) - dashou.save(update_fields=['jifen']) - response_data['jifen'] = dashou.jifen - except UserDashou.DoesNotExist: - return Response({'code': 500, 'msg': '打手信息不存在,无法补充积分'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - except Exception as e: - logger.error(f"事务处理异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': f'支付处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - final_profile = identity['model'].query.get(user=user) - response_data['balance'] = str(getattr(final_profile, identity['balance_field'])) - - return Response({ - 'code': 200, - 'msg': 'success', - 'data': response_data, - }) - - except Exception as e: - logger.error(f"接口顶层异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': f'系统错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def _process_member_purchase(self, yonghuid, huiyuan_id, huiyuan_obj): - is_first_buy_any = False - try: - record = Huiyuangoumai.query.filter( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id - ).first() - - if record: - record.daoqi_time += timedelta(days=30) - record.huiyuan_zhuangtai = 1 - record.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai']) - else: - other_count = Huiyuangoumai.query.filter(yonghu_id=yonghuid).exclude(huiyuan_id=huiyuan_id).count() - if other_count == 0: - is_first_buy_any = True - record = Huiyuangoumai.query.create( - yonghu_id=yonghuid, - huiyuan_id=huiyuan_id, - jieshao=huiyuan_obj.jieshao, - huiyuan_zhuangtai=1, - daoqi_time=timezone.now() + timedelta(days=30) - ) - return record, is_first_buy_any - except Exception as e: - if 'unique' in str(e).lower() or 'duplicate' in str(e).lower(): - record = Huiyuangoumai.query.get(yonghu_id=yonghuid, huiyuan_id=huiyuan_id) - return record, False +# ==================== 标准库 ==================== +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 orders.utils import update_daily_income +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 gvsdsdk.models import User + +from orders.models import Lilubiao +from config.models import Szjilu, Gonggao, Lunbo + +from orders.models import Fadan # 用于罚款类型 + +import traceback + +# ==================== 日志实例 ==================== +logger = logging.getLogger(__name__) + + +class ShangpinHuoquView(APIView): + """ + 商品数据获取接口 + 一次性返回商品类型、商品专区、商品列表 + """ + + # 应用节流(限制频率) + throttle_classes = [AnonRateThrottle] + permission_classes = [AllowAny] + + def post(self, request): + """ + 处理POST请求,返回所有商品相关数据 + """ + try: + logger.info("收到商品数据获取请求") + + # 1. 查询商品类型(只查询正常状态的类型,并排序) + # 正常运营:只显示 shenhezhuangtai=2 的商品类型 + leixing_queryset = ShangpinLeixing.query.filter( + shenhezhuangtai=1 # 只取正常状态 + ).order_by('-paixu', 'id') # 先按paixu降序,再按id升序 + + # 注意:审核期间使用下面这行代码(先注释掉) + '''leixing_queryset = ShangpinLeixing.query.filter( + shenhezhuangtai=2 # 审核期间:只取审核专用状态 + ).order_by('-paixu', 'id')''' + + shangpinleixing_data = [] + for leixing in leixing_queryset: + shangpinleixing_data.append({ + "id": leixing.id, + "jieshao": leixing.jieshao, + "tupian_url": leixing.tupian_url, + "paixu": leixing.paixu + }) + + # 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 # 🔴【添加】只筛选审核状态为2的专区 + ).order_by('-paixu', 'id') # 先按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 # 🔴【添加】只筛选审核状态为2的商品 + ).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 = { + "tupian": shangpin.tupian_url, + "biaoti": shangpin.biaoqian, + "jiage": shangpin.jiage, + "xiangqing": shangpin.jieshao, + "xiadanxuzhi": shangpin.xiadan_xuzhi, + "guize": shangpin.guize_tupian, + "xiaoliang": shangpin.duiwai_xiaoliang, + "kucun": shangpin.kucun, + } + + 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.dashou_profile + except UserDashou.DoesNotExist: + logger.warning(f"用户{request.user.yonghuid}不是打手,尝试访问打手接口") + return Response({ + 'code': 400, + 'message': '用户不是打手,无法访问此接口' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 检查打手账号状态 + if dashou.zhanghaozhuangtai != 1: + logger.warning(f"打手{request.user.yonghuid}账号状态异常: {dashou.zhanghaozhuangtai}") + return Response({ + 'code': 400, + 'message': '账号状态异常,无法购买会员' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 查询所有会员数据(ORM查询,确保性能) + huiyuan_queryset = Huiyuan.query.all().only( + 'huiyuan_id', # 会员ID + 'jieshao', # 会员名字(注意:jieshao字段实际存储的是会员名字) + 'jtjieshao', # 会员具体介绍 + 'jiage' # 会员价格 + ).order_by('jiage') # 按价格排序 + + # 4. 构建返回数据列表 + huiyuan_list = [] + for huiyuan in huiyuan_queryset: + huiyuan_list.append({ + 'id': huiyuan.huiyuan_id, # 会员ID(对应前端id) + 'mingzi': huiyuan.jieshao, # 会员名字(对应前端mingzi) + 'jiage': float(huiyuan.jiage), # 会员价格(转为浮点数) + 'jieshao': huiyuan.jtjieshao # 会员介绍(对应前端jieshao) + }) + + # 5. 返回数据 + return Response({ + 'code': 200, + 'message': '获取会员列表成功', + 'data': huiyuan_list # 对应前端接收的列表 + }, 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 YajinGoumai(APIView): + """ + 押金充值支付参数接口 + 路径:/shangpin/yajingoumai + 请求方式:POST + 权限:JWT认证 + 参数:jine(金额,1-10000元) + 返回:支付参数(payParams)和订单ID(dingdanid) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 验证用户身份和状态 + try: + dashou = request.user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 400, + 'message': '用户不是打手,无法充值押金' + }, status=status.HTTP_400_BAD_REQUEST) + + if dashou.zhanghaozhuangtai != 1: + return Response({ + 'code': 400, + 'message': '账号状态异常,无法进行充值' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取并验证金额参数 + jine = request.data.get('jine') + if not jine: + return Response({ + 'code': 400, + 'message': '请填写充值金额' + }, status=status.HTTP_400_BAD_REQUEST) + + try: + jine = int(jine) + except (ValueError, TypeError): + return Response({ + 'code': 400, + 'message': '金额格式错误,请填写1-10000的整数' + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证金额范围 + if jine < 1 or jine > 10000: + return Response({ + 'code': 400, + 'message': '充值金额必须在1-10000元之间' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 生成订单ID(使用你提供的算法,前缀改为CzYJ) + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + + dingdanid = f"CzYJ{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + + # 4. 创建充值记录订单 + chongzhi_jilu = Czjilu.query.create( + dingdan_id=dingdanid, + zhuangtai=9, # 未支付状态 + yonghuid=request.user.yonghuid, + jine=jine, + leixing=2, # 充值类型:2=押金 + shuoming=settings.YAJIN_PAY_DESCRIPTION + ) + + logger.info(f"创建押金充值订单: 用户{request.user.yonghuid}, 订单{dingdanid}, 金额{jine}元") + + # 5. 生成微信支付参数 + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=jine, + openid=request.user.openid, # 直接从request.user获取openid + pay_type='yajin' + ) + except Exception as e: + # 支付参数生成失败,删除订单记录 + chongzhi_jilu.delete() + logger.error(f"生成微信支付参数失败: {str(e)}") + return Response({ + 'code': 500, + 'message': f'支付参数生成失败: {str(e)}' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 6. 返回支付参数和订单ID + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'payParams': pay_params, # 支付参数(前端接收为payParams) + 'dingdanid': dingdanid # 订单ID(前端接收为dingdanid) + }, 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) + + def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): + """生成微信小程序支付参数""" + # 获取配置 + APPID = settings.WEIXIN_APPID + MCHID = settings.WEIXIN_MCHID + KEY = settings.WEIXIN_SHANGHUMIYAO + + # 根据支付类型获取对应的配置 + if pay_type == 'yajin': + PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION + NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL + elif pay_type == 'jifen': + PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION + NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL + elif pay_type == 'huiyuan': + PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION + NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL + else: + raise Exception('未知的支付类型') + + # 生成随机字符串 + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + # 金额转换(元转分) + total_fee = int(jine * 100) + + # 交易类型(小程序支付使用JSAPI) + trade_type = 'JSAPI' + + # 获取客户端IP + user_ip = self.get_client_ip() + + # 1. 第一次签名(统一下单接口) + sign_data = { + 'appid': APPID, + 'mch_id': MCHID, + 'nonce_str': nonce_str, + 'body': PAY_DESCRIPTION, + 'out_trade_no': dingdanid, + 'total_fee': total_fee, + 'spbill_create_ip': user_ip, + 'notify_url': NOTIFY_URL, + 'trade_type': trade_type, + 'openid': openid # 小程序支付必须传openid + } + + # 按照参数名ASCII码从小到大排序 + sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) + + # 拼接字符串 + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) + sign_string += f'&key={KEY}' + + # MD5签名 + sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + # 构造XML数据 + xml_data = f""" +{APPID} +{PAY_DESCRIPTION} +{MCHID} +{nonce_str} +{NOTIFY_URL} +{openid} +{dingdanid} +{user_ip} +{total_fee} +{trade_type} +{sign} +""" + + # 调用微信统一下单接口(官方地址) + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 # 设置超时 + ) + + # 解析返回的XML + root = ET.fromstring(response.content) + + return_code = root.find('return_code').text + result_code = root.find('result_code').text + + if return_code == 'SUCCESS' and result_code == 'SUCCESS': + # 获取prepay_id + prepay_id = root.find('prepay_id').text + + # 2. 第二次签名(小程序支付参数) + timestamp = str(int(time.time())) + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + package = f'prepay_id={prepay_id}' + sign_type = 'MD5' + + # 生成支付签名(注意:这里key的大小写和字段名与第一次不同) + pay_sign_data = { + 'appId': APPID, # 注意:这里是appId,不是appid + 'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type + } + + sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) + pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) + pay_sign_string += f'&key={KEY}' + pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() + + # 返回前端需要的支付参数 + return { + 'appId': APPID, # 对应前端:appId + 'timeStamp': timestamp, # 对应前端:timeStamp + 'nonceStr': nonce_str, # 对应前端:nonceStr + 'package': package, # 对应前端:package + 'signType': sign_type, # 对应前端:signType + 'paySign': pay_sign # 对应前端:paySign + } + else: + # 获取错误信息 + err_code = root.find('err_code') + err_code_des = root.find('err_code_des') + return_msg = root.find('return_msg') + + error_details = f"return_code: {return_code}" + if err_code is not None: + error_details += f", err_code: {err_code.text}" + if err_code_des is not None: + error_details += f", err_code_des: {err_code_des.text}" + if return_msg is not None: + error_details += f", return_msg: {return_msg.text}" + + raise Exception(f"微信支付下单失败: {error_details}") + + def get_client_ip(self): + """获取客户端IP(从DRF的request对象中获取)""" + request = self.request + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip if ip else '127.0.0.1' + + + + + +class YajinHuitiao(View): + """ + 押金支付回调接口 + 路径:/shangpin/yajinhuitiao + 注意:这个接口不需要JWT认证,由微信服务器调用 + """ + + def post(self, request): + try: + # 1. 解析微信回调XML数据 + request_body = request.body + logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}") + + root = ET.fromstring(request_body) + + # 获取关键字段 + return_code = root.find('return_code').text + result_code = root.find('result_code').text + out_trade_no = root.find('out_trade_no').text # 订单ID + transaction_id = root.find('transaction_id').text # 微信交易号 + total_fee = int(root.find('total_fee').text) # 支付金额(分) + + logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") + + # 2. 验证基本返回状态 + if return_code != 'SUCCESS' or result_code != 'SUCCESS': + logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}") + return self.wechat_response('FAIL', '支付失败') + + # 3. 验证签名(确保是微信发来的) + if not self.verify_signature(request_body): + logger.error(f"签名验证失败: {out_trade_no}") + return self.wechat_response('FAIL', '签名验证失败') + + # 4. 查询订单 + try: + order = Czjilu.query.get(dingdan_id=out_trade_no) + logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") + except Czjilu.DoesNotExist: + logger.error(f"订单不存在: {out_trade_no}") + return self.wechat_response('FAIL', '订单不存在') + + # 5. 检查订单状态是否为未支付(9) + if order.zhuangtai != 9: + logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") + # 已处理过的订单直接返回成功,避免微信重复回调 + return self.wechat_response('SUCCESS', 'OK') + + # 6. 验证金额(微信返回的是分,需要转换) + wechat_amount_yuan = total_fee / 100 # 转换为元 + order_amount_yuan = float(order.jine) # 订单金额(元) + + # 允许1分钱的误差(浮点数比较) + if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: + logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") + return self.wechat_response('FAIL', '金额不一致') + + # 7. 更新订单状态为已支付(3) + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3") + + # 8. 更新打手押金 + try: + # 通过用户ID查找打手(高效查询) + dashou = UserDashou.query.get(user__UserUID=order.yonghuid) + + # 增加押金(使用Decimal确保精度) + dashou.yajin += Decimal(str(order_amount_yuan)) + dashou.save(update_fields=['yajin']) + + logger.info(f"打手押金更新成功: 用户{order.yonghuid}, 增加{order_amount_yuan}元, 新押金{dashou.yajin}") + + # 8b. 押金分红(仅微信确认到账后;费率 Lilubiao fadanpingtai=12) + try: + + guanshi_id = dashou.yaoqingren + if not guanshi_id: + logger.info(f"押金分红跳过: 打手{order.yonghuid}无邀请管事") + elif Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists(): + logger.info(f"押金分红已处理: 订单{order.dingdan_id}") + else: + lilu_obj = Lilubiao.query.filter(fadanpingtai='12').first() + rate = lilu_obj.lilu if lilu_obj and lilu_obj.lilu is not None else Decimal('0') + if rate <= 0: + logger.info(f"押金分红跳过: 费率未配置或为0") + else: + recharge_amount = Decimal(str(order_amount_yuan)) + fenhong_jine = (recharge_amount * rate).quantize(Decimal('0.01')) + if fenhong_jine > 0: + with transaction.atomic(): + guanshi = UserGuanshi.objects.select_for_update().get( + user__UserUID=guanshi_id + ) + guanshi.yue += fenhong_jine + guanshi.chongzhifenrun += fenhong_jine + guanshi.save(update_fields=['yue', 'chongzhifenrun']) + + user_main = dashou.user + Gsfenhong.query.create( + dingdan_id=order.dingdan_id, + guanshi=guanshi_id, + dashouid=order.yonghuid, + shuoming='押金分红', + fenhong=fenhong_jine, + avatar=user_main.avatar, + nicheng=dashou.nicheng or '未知打手', + fenhong_leixing=2, + ) + logger.info( + f"押金分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, " + f"打手{order.yonghuid}, 金额{fenhong_jine}元" + ) + try: + update_guanshi_daily_by_action( + yonghuid=guanshi_id, + action=4, + amount=fenhong_jine, + ) + except Exception as e: + logger.error(f"押金分红管事日统计更新失败: {e}") + except UserGuanshi.DoesNotExist: + logger.warning(f"押金分红跳过: 管事{guanshi_id}不存在") + except Exception as e: + logger.error(f"押金分红处理失败: {str(e)}", exc_info=True) + + except UserDashou.DoesNotExist: + # 如果找不到打手,记录错误,但依然返回成功(避免微信重复回调) + logger.error(f"打手信息不存在: 用户{order.yonghuid}") + return self.wechat_response('SUCCESS', 'OK') + except Exception as e: + logger.error(f"更新打手押金失败: {str(e)}", exc_info=True) + return self.wechat_response('FAIL', '更新押金失败') + + # ==== 在返回成功响应之前添加这段代码 ==== + + # 更新收支记录表 + try: + # 获取订单金额(确保是Decimal类型) + jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine + + # 查询或创建ID=1的收支记录 + update_daily_income(jine_decimal) + + szjilu, created = Szjilu.query.get_or_create( + id=1, + defaults={ + 'zongsy': Decimal('0.00'), + 'zongls': Decimal('0.00'), + 'zongzc': Decimal('0.00'), + 'jrzc': Decimal('0.00'), + 'jrls': Decimal('0.00') + } + ) + + # 更新三个字段 + szjilu.zongsy += jine_decimal # 总收益 + szjilu.zongls += jine_decimal # 总流水 + szjilu.jrls += jine_decimal # 今日流水 + + szjilu.save() + + logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") + + except Exception as e: + # 收支记录更新失败不影响主流程,只记录日志 + logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) + + # ==== 添加结束 ==== + + # 9. 返回成功响应给微信 + logger.info(f"押金支付回调处理完成: {out_trade_no}") + return self.wechat_response('SUCCESS', 'OK') + + except ET.ParseError as e: + logger.error(f"XML解析失败: {str(e)}") + return self.wechat_response('FAIL', '数据格式错误') + except Exception as e: + logger.error(f"押金支付回调处理异常: {str(e)}", exc_info=True) + # 返回FAIL让微信重试 + return self.wechat_response('FAIL', '处理异常') + + def verify_signature(self, xml_data): + """验证微信支付签名""" + try: + root = ET.fromstring(xml_data) + sign = root.find('sign').text + + if not sign: + logger.error("签名字段不存在") + return False + + # 获取所有参数并排除sign + params = {} + for child in root: + if child.tag != 'sign' and child.text is not None: + params[child.tag] = child.text + + # 按照参数名ASCII码从小到大排序 + sorted_params = sorted(params.items(), key=lambda x: x[0]) + + # 拼接字符串 + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) + sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + + # 计算签名 + calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + is_valid = sign == calculated_sign + if not is_valid: + logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") + + return is_valid + + except Exception as e: + logger.error(f"验证签名异常: {str(e)}") + return False + + def wechat_response(self, return_code, return_msg): + """返回微信支付回调响应""" + xml_response = f""" + + +""" + + logger.info(f"返回微信响应: {return_code} - {return_msg}") + return HttpResponse(xml_response, content_type='application/xml') + + + + +# views.py +class YajinShibai(APIView): + """ + 押金支付失败处理接口 + 路径:/shangpin/yajinshibai + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:删除结果 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.query.get( + dingdan_id=dingdanid, + UserUID=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单状态是否为未支付(9) + if order.zhuangtai == 9: + # 删除订单 + order.delete() + logger.info(f"用户{request.user.yonghuid}删除未支付订单: {dingdanid}") + + return Response({ + 'code': 200, + 'message': '订单已删除' + }, status=status.HTTP_200_OK) + else: + return Response({ + 'code': 400, + 'message': '订单状态不是未支付,无法删除' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"押金支付失败处理异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# views.py +class YajinLunxun(APIView): + """ + 押金支付成功轮询接口 + 路径:/shangpin/yajinlunxun + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:订单状态和当前押金 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.query.get( + dingdan_id=dingdanid, + UserUID=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单状态是否为已支付(3) + if order.zhuangtai != 3: + logger.info(f"订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") + return Response({ + 'code': 400, + 'message': '订单尚未支付完成' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 获取打手押金(通过反向查询,最快捷的方式) + try: + dashou = request.user.dashou_profile + yajin = float(dashou.yajin) # 转为浮点数方便前端处理 + + logger.info(f"轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}押金为{yajin}元") + + return Response({ + 'code': 200, + 'message': '支付流程全部完成', + 'yajin': yajin # 返回当前押金(前端需要更新全局变量) + }, status=status.HTTP_200_OK) + + except UserDashou.DoesNotExist: + logger.error(f"打手信息不存在: 用户{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '打手信息不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"押金轮询异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# views.py +class JifenBuchong(APIView): + """ + 积分补充支付参数接口 + 路径:/shangpin/jifenbc + 请求方式:POST + 权限:JWT认证 + 参数:无(固定5元) + 返回:支付参数(payParams)和订单ID(dingdanid) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 验证用户身份和状态 + try: + dashou = request.user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 400, + 'message': '用户不是打手,无法补充积分' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 检查打手账号状态 + if dashou.zhanghaozhuangtai != 1: + return Response({ + 'code': 400, + 'message': '账号状态异常,无法进行操作' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查积分是否已满(满分10分) + if dashou.jifen >= 10: + return Response({ + 'code': 400, + 'message': '积分已满,无需补充' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 生成订单ID(前缀CzJF表示积分充值) + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + + dingdanid = f"CzJF{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + + # 5. 创建积分充值记录订单(固定5元) + jine = 5.00 # 固定5元 + chongzhi_jilu = Czjilu.query.create( + dingdan_id=dingdanid, + zhuangtai=9, # 未支付状态 + yonghuid=request.user.yonghuid, + jine=jine, + leixing=3, # 充值类型:3=积分 + shuoming=settings.JIFEN_PAY_DESCRIPTION + ) + + logger.info(f"创建积分充值订单: 用户{request.user.yonghuid}, 订单{dingdanid}, 金额{jine}元") + + # 6. 生成微信支付参数(完整版) + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=jine, + openid=request.user.openid, + pay_type='jifen' + ) + except Exception as e: + # 支付参数生成失败,删除订单记录 + chongzhi_jilu.delete() + logger.error(f"生成积分支付参数失败: {str(e)}") + return Response({ + 'code': 500, + 'message': f'支付参数生成失败: {str(e)}' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 7. 返回支付参数和订单ID + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'payParams': pay_params, # 对应前端payParams + 'dingdanid': dingdanid # 对应前端dingdanid + }, 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) + + def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): + """生成微信支付参数(完整版,无省略)""" + # 微信支付配置 - 从settings中获取 + APPID = settings.WEIXIN_APPID + MCHID = settings.WEIXIN_MCHID + KEY = settings.WEIXIN_SHANGHUMIYAO + + # 根据支付类型获取对应的配置 + if pay_type == 'yajin': + PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION + NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL + elif pay_type == 'jifen': + PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION + NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL + elif pay_type == 'huiyuan': + PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION + NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL + else: + raise Exception('未知的支付类型') + + # 生成随机字符串 + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + # 金额转换(元转分) + total_fee = int(jine * 100) + + # 交易类型(小程序支付使用JSAPI) + trade_type = 'JSAPI' + + # 获取客户端IP + user_ip = self.get_client_ip() + + # 1. 第一次签名(统一下单接口) + sign_data = { + 'appid': APPID, + 'mch_id': MCHID, + 'nonce_str': nonce_str, + 'body': PAY_DESCRIPTION, + 'out_trade_no': dingdanid, + 'total_fee': total_fee, + 'spbill_create_ip': user_ip, + 'notify_url': NOTIFY_URL, + 'trade_type': trade_type, + 'openid': openid # 小程序支付必须传openid + } + + # 按照参数名ASCII码从小到大排序 + sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) + + # 拼接字符串 + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) + sign_string += f'&key={KEY}' + + # MD5签名 + sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + # 构造XML数据 + xml_data = f""" +{APPID} +{PAY_DESCRIPTION} +{MCHID} +{nonce_str} +{NOTIFY_URL} +{openid} +{dingdanid} +{user_ip} +{total_fee} +{trade_type} +{sign} +""" + + # 调用微信统一下单接口(官方地址) + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 # 设置超时 + ) + + # 解析返回的XML + root = ET.fromstring(response.content) + + return_code = root.find('return_code').text + result_code = root.find('result_code').text + + if return_code == 'SUCCESS' and result_code == 'SUCCESS': + # 获取prepay_id + prepay_id = root.find('prepay_id').text + + # 2. 第二次签名(小程序支付参数) + timestamp = str(int(time.time())) + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + package = f'prepay_id={prepay_id}' + sign_type = 'MD5' + + # 生成支付签名(注意:这里key的大小写和字段名与第一次不同) + pay_sign_data = { + 'appId': APPID, # 注意:这里是appId,不是appid + 'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type + } + + sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) + pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) + pay_sign_string += f'&key={KEY}' + pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() + + # 返回前端需要的支付参数 + return { + 'appId': APPID, # 对应前端:appId + 'timeStamp': timestamp, # 对应前端:timeStamp + 'nonceStr': nonce_str, # 对应前端:nonceStr + 'package': package, # 对应前端:package + 'signType': sign_type, # 对应前端:signType + 'paySign': pay_sign # 对应前端:paySign + } + else: + # 获取错误信息 + err_code = root.find('err_code') + err_code_des = root.find('err_code_des') + return_msg = root.find('return_msg') + + error_details = f"return_code: {return_code}" + if err_code is not None: + error_details += f", err_code: {err_code.text}" + if err_code_des is not None: + error_details += f", err_code_des: {err_code_des.text}" + if return_msg is not None: + error_details += f", return_msg: {return_msg.text}" + + raise Exception(f"微信支付下单失败: {error_details}") + + def get_client_ip(self): + """获取客户端IP(完整版)""" + request = self.request + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip if ip else '127.0.0.1' + + +# views.py +class JifenHuitiao(View): + """ + 积分支付回调接口 + 路径:/shangpin/jifenhuitiao + 注意:这个接口不需要JWT认证,由微信服务器调用 + """ + + def post(self, request): + try: + # 1. 解析微信回调XML数据 + request_body = request.body + logger.info(f"收到积分支付回调,原始数据: {request_body.decode('utf-8')}") + + root = ET.fromstring(request_body) + + # 获取关键字段 + return_code = root.find('return_code').text + result_code = root.find('result_code').text + out_trade_no = root.find('out_trade_no').text # 订单ID + transaction_id = root.find('transaction_id').text # 微信交易号 + total_fee = int(root.find('total_fee').text) # 支付金额(分) + + logger.info(f"积分回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") + + # 2. 验证基本返回状态 + if return_code != 'SUCCESS' or result_code != 'SUCCESS': + logger.warning(f"积分支付回调状态异常: {return_code}/{result_code}") + return self.wechat_response('FAIL', '支付失败') + + # 3. 验证签名 + if not self.verify_signature(request_body): + logger.error(f"积分签名验证失败: {out_trade_no}") + return self.wechat_response('FAIL', '签名验证失败') + + # 4. 查询订单 + try: + order = Czjilu.query.get(dingdan_id=out_trade_no) + logger.info(f"找到积分订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") + except Czjilu.DoesNotExist: + logger.error(f"积分订单不存在: {out_trade_no}") + return self.wechat_response('FAIL', '订单不存在') + + # 5. 检查订单状态是否为未支付(9) + if order.zhuangtai != 9: + logger.warning(f"积分订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") + return self.wechat_response('SUCCESS', 'OK') + + # 6. 验证金额(固定5元) + wechat_amount_yuan = total_fee / 100 # 转换为元 + order_amount_yuan = float(order.jine) # 订单金额(元) + + # 验证是否是5元(允许1分钱误差) + if abs(wechat_amount_yuan - 5.00) > 0.01: + logger.error(f"积分金额错误: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") + return self.wechat_response('FAIL', '金额错误') + + # ==== 在返回成功响应之前添加这段代码 ==== + + # 更新收支记录表 + try: + # 获取订单金额(确保是Decimal类型) + jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine + update_daily_income(jine_decimal) + + # 查询或创建ID=1的收支记录 + szjilu, created = Szjilu.query.get_or_create( + id=1, + defaults={ + 'zongsy': Decimal('0.00'), + 'zongls': Decimal('0.00'), + 'zongzc': Decimal('0.00'), + 'jrzc': Decimal('0.00'), + 'jrls': Decimal('0.00') + } + ) + + # 更新三个字段 + szjilu.zongsy += jine_decimal # 总收益 + szjilu.zongls += jine_decimal # 总流水 + szjilu.jrls += jine_decimal # 今日流水 + + szjilu.save() + + logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") + + except Exception as e: + # 收支记录更新失败不影响主流程,只记录日志 + logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) + + # ==== 添加结束 ==== + + # 7. 更新订单状态为已支付(3) + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + logger.info(f"积分订单状态更新成功: {order.dingdan_id} -> 3") + + # 8. 更新打手积分 + try: + # 通过用户ID查找打手 + dashou = UserDashou.query.get(user__UserUID=order.yonghuid) + + # 检查积分是否小于10 + if dashou.jifen < 10: + # 增加5积分,但不超过10 + new_jifen = dashou.jifen + 5 + dashou.jifen = min(new_jifen, 10) # 确保不超过10 + dashou.save(update_fields=['jifen']) + + logger.info(f"打手积分更新成功: 用户{order.yonghuid}, 增加5积分, 新积分{dashou.jifen}") + + + + + else: + logger.warning(f"打手积分已满: 用户{order.yonghuid}, 当前积分{dashou.jifen}") + + except UserDashou.DoesNotExist: + logger.error(f"打手信息不存在: 用户{order.yonghuid}") + return self.wechat_response('SUCCESS', 'OK') + except Exception as e: + logger.error(f"更新打手积分失败: {str(e)}", exc_info=True) + return self.wechat_response('FAIL', '更新积分失败') + + # 9. 返回成功响应给微信 + logger.info(f"积分支付回调处理完成: {out_trade_no}") + return self.wechat_response('SUCCESS', 'OK') + + except ET.ParseError as e: + logger.error(f"积分XML解析失败: {str(e)}") + return self.wechat_response('FAIL', '数据格式错误') + except Exception as e: + logger.error(f"积分支付回调处理异常: {str(e)}", exc_info=True) + return self.wechat_response('FAIL', '处理异常') + + def verify_signature(self, xml_data): + """验证微信支付签名""" + try: + root = ET.fromstring(xml_data) + sign = root.find('sign').text + + if not sign: + logger.error("积分数签名字段不存在") + return False + + # 获取所有参数并排除sign + params = {} + for child in root: + if child.tag != 'sign' and child.text is not None: + params[child.tag] = child.text + + # 按照参数名ASCII码从小到大排序 + sorted_params = sorted(params.items(), key=lambda x: x[0]) + + # 拼接字符串 + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) + sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + + # 计算签名 + calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + is_valid = sign == calculated_sign + if not is_valid: + logger.error(f"积分签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") + + return is_valid + + except Exception as e: + logger.error(f"积分验证签名异常: {str(e)}") + return False + + def wechat_response(self, return_code, return_msg): + """返回微信支付回调响应""" + xml_response = f""" + + +""" + + logger.info(f"积分返回微信响应: {return_code} - {return_msg}") + return HttpResponse(xml_response, content_type='application/xml') + + +# views.py +class JifenShibai(APIView): + """ + 积分支付失败处理接口 + 路径:/shangpin/jifenshibai + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:删除结果 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.query.get( + dingdan_id=dingdanid, + UserUID=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单类型是否为积分(3) + if order.leixing != 3: + return Response({ + 'code': 400, + 'message': '订单类型错误,不是积分订单' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 检查订单状态是否为未支付(9) + if order.zhuangtai == 9: + # 删除订单 + order.delete() + logger.info(f"用户{request.user.yonghuid}删除未支付积分订单: {dingdanid}") + + return Response({ + 'code': 200, + 'message': '积分订单已删除' + }, status=status.HTTP_200_OK) + else: + return Response({ + 'code': 400, + 'message': '积分订单状态不是未支付,无法删除' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"积分支付失败处理异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# views.py +class JifenLunxun(APIView): + """ + 积分支付成功轮询接口 + 路径:/shangpin/jifenlunxun + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:订单状态和当前积分 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.query.get( + dingdan_id=dingdanid, + UserUID=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单类型是否为积分(3) + if order.leixing != 3: + return Response({ + 'code': 400, + 'message': '订单类型错误,不是积分订单' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 检查订单状态是否为已支付(3) + if order.zhuangtai != 3: + logger.info(f"积分订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") + return Response({ + 'code': 400, + 'message': '积分订单尚未支付完成' + }, status=status.HTTP_400_BAD_REQUEST) + + # 5. 获取打手积分(通过反向查询) + try: + dashou = request.user.dashou_profile + jifen = dashou.jifen # 直接使用整数 + + logger.info(f"积分轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}积分为{jifen}") + + return Response({ + 'code': 200, + 'message': '支付流程全部完成', + 'jifen': jifen # 返回当前积分(前端需要更新全局变量) + }, status=status.HTTP_200_OK) + + except UserDashou.DoesNotExist: + logger.error(f"打手信息不存在: 用户{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '打手信息不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + 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 HuiyuanGoumai(APIView): + """ + 会员充值支付参数接口 + 路径:/shangpin/huiyuangm + 请求方式:POST + 权限:JWT认证 + 参数:huiyuanid(会员ID) + 返回:支付参数(payParams)和订单ID(dingdanid) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取会员ID参数 + huiyuanid = request.data.get('huiyuanid') + if not huiyuanid: + return Response({ + 'code': 400, + 'message': '会员ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 验证用户身份和状态 + try: + dashou = request.user.dashou_profile + except UserDashou.DoesNotExist: + return Response({ + 'code': 400, + 'message': '用户不是打手,无法购买会员' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查打手账号状态 + if dashou.zhanghaozhuangtai != 1: + return Response({ + 'code': 400, + 'message': '账号状态异常,无法进行购买' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 查询会员信息 + try: + huiyuan = Huiyuan.query.get(huiyuan_id=huiyuanid) + except Huiyuan.DoesNotExist: + return Response({ + 'code': 400, + 'message': '会员信息不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + # 5. 生成订单ID(前缀CzHY表示会员充值) + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + + dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + + # 6. 创建充值记录订单 + jine = float(huiyuan.jiage) # 会员价格 + # 组合订单说明:会员介绍 + 配置描述 + shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}" + + chongzhi_jilu = Czjilu.query.create( + dingdan_id=dingdanid, + zhuangtai=9, # 未支付状态 + yonghuid=request.user.yonghuid, + jine=jine, + leixing=1, # 充值类型:1=会员 + shuoming=shuoming, + huiyuan_id=huiyuanid # 存储会员ID + ) + + logger.info( + f"创建会员充值订单: 用户{request.user.yonghuid}, 订单{dingdanid}, 会员{huiyuanid}, 金额{jine}元") + + # 7. 生成微信支付参数 + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=jine, + openid=request.user.openid, + pay_type='huiyuan' + ) + except Exception as e: + # 支付参数生成失败,删除订单记录 + chongzhi_jilu.delete() + logger.error(f"生成会员支付参数失败: {str(e)}") + return Response({ + 'code': 500, + 'message': f'支付参数生成失败: {str(e)}' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 8. 返回支付参数和订单ID + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'payParams': pay_params, # 对应前端payParams + 'dingdanid': dingdanid # 对应前端dingdanid + }, 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) + + def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): + """生成微信支付参数(完整版,无省略)""" + # 微信支付配置 - 从settings中获取 + APPID = settings.WEIXIN_APPID + MCHID = settings.WEIXIN_MCHID + KEY = settings.WEIXIN_SHANGHUMIYAO + + # 根据支付类型获取对应的配置 + if pay_type == 'yajin': + PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION + NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL + elif pay_type == 'jifen': + PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION + NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL + elif pay_type == 'huiyuan': + PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION + NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL + else: + raise Exception('未知的支付类型') + + # 生成随机字符串 + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + # 金额转换(元转分) + total_fee = int(jine * 100) + + # 交易类型(小程序支付使用JSAPI) + trade_type = 'JSAPI' + + # 获取客户端IP + user_ip = self.get_client_ip() + + # 1. 第一次签名(统一下单接口) + sign_data = { + 'appid': APPID, + 'mch_id': MCHID, + 'nonce_str': nonce_str, + 'body': PAY_DESCRIPTION, + 'out_trade_no': dingdanid, + 'total_fee': total_fee, + 'spbill_create_ip': user_ip, + 'notify_url': NOTIFY_URL, + 'trade_type': trade_type, + 'openid': openid # 小程序支付必须传openid + } + + # 按照参数名ASCII码从小到大排序 + sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) + + # 拼接字符串 + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) + sign_string += f'&key={KEY}' + + # MD5签名 + sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + # 构造XML数据 + xml_data = f""" +{APPID} +{PAY_DESCRIPTION} +{MCHID} +{nonce_str} +{NOTIFY_URL} +{openid} +{dingdanid} +{user_ip} +{total_fee} +{trade_type} +{sign} +""" + + # 调用微信统一下单接口(官方地址) + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 # 设置超时 + ) + + # 解析返回的XML + root = ET.fromstring(response.content) + + return_code = root.find('return_code').text + result_code = root.find('result_code').text + + if return_code == 'SUCCESS' and result_code == 'SUCCESS': + # 获取prepay_id + prepay_id = root.find('prepay_id').text + + # 2. 第二次签名(小程序支付参数) + timestamp = str(int(time.time())) + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + package = f'prepay_id={prepay_id}' + sign_type = 'MD5' + + # 生成支付签名(注意:这里key的大小写和字段名与第一次不同) + pay_sign_data = { + 'appId': APPID, # 注意:这里是appId,不是appid + 'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type + } + + sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) + pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) + pay_sign_string += f'&key={KEY}' + pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() + + # 返回前端需要的支付参数 + return { + 'appId': APPID, # 对应前端:appId + 'timeStamp': timestamp, # 对应前端:timeStamp + 'nonceStr': nonce_str, # 对应前端:nonceStr + 'package': package, # 对应前端:package + 'signType': sign_type, # 对应前端:signType + 'paySign': pay_sign # 对应前端:paySign + } + else: + # 获取错误信息 + err_code = root.find('err_code') + err_code_des = root.find('err_code_des') + return_msg = root.find('return_msg') + + error_details = f"return_code: {return_code}" + if err_code is not None: + error_details += f", err_code: {err_code.text}" + if err_code_des is not None: + error_details += f", err_code_des: {err_code_des.text}" + if return_msg is not None: + error_details += f", return_msg: {return_msg.text}" + + raise Exception(f"微信支付下单失败: {error_details}") + + def get_client_ip(self): + """获取客户端IP(完整版)""" + request = self.request + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip if ip else '127.0.0.1' + + + + + +class HuiyuanHuitiao(View): + """ + 会员支付回调接口 + 路径:/shangpin/huiyuanhuitiao + """ + + def post(self, request): + try: + # 1. 解析微信回调XML数据 + request_body = request.body + logger.info(f"收到会员支付回调,原始数据: {request_body.decode('utf-8')}") + + root = ET.fromstring(request_body) + + return_code = root.find('return_code').text + result_code = root.find('result_code').text + out_trade_no = root.find('out_trade_no').text + transaction_id = root.find('transaction_id').text + total_fee = int(root.find('total_fee').text) + + logger.info(f"会员回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") + + if return_code != 'SUCCESS' or result_code != 'SUCCESS': + logger.warning(f"会员支付回调状态异常: {return_code}/{result_code}") + return self.wechat_response('FAIL', '支付失败') + + if not self.verify_signature(request_body): + logger.error(f"会员签名验证失败: {out_trade_no}") + return self.wechat_response('FAIL', '签名验证失败') + + # 查询订单 + try: + order = Czjilu.query.get(dingdan_id=out_trade_no) + except Czjilu.DoesNotExist: + logger.error(f"会员订单不存在: {out_trade_no}") + return self.wechat_response('FAIL', '订单不存在') + + if order.leixing != 1: + logger.error(f"订单类型错误: {order.leixing}, 期望1(会员)") + return self.wechat_response('FAIL', '订单类型错误') + + if order.zhuangtai != 9: + logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") + return self.wechat_response('SUCCESS', 'OK') + + # 验证金额 + wechat_amount_yuan = total_fee / 100 + order_amount_yuan = float(order.jine) + if abs(wechat_amount_yuan - order_amount_yuan) > 0.01: + logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") + return self.wechat_response('FAIL', '金额不一致') + + # 查询会员信息 + try: + huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id) + except Huiyuan.DoesNotExist: + logger.error(f"会员信息不存在: {order.huiyuan_id}") + return self.wechat_response('FAIL', '会员信息不存在') + + # ===== 更新收支记录 ===== + try: + jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine + + update_daily_income(jine_decimal) + + szjilu, _ = Szjilu.query.get_or_create( + id=1, + defaults={'zongsy': Decimal('0.00'), 'zongls': Decimal('0.00'), 'zongzc': Decimal('0.00'), + 'jrzc': Decimal('0.00'), 'jrls': Decimal('0.00')} + ) + szjilu.zongsy += jine_decimal + szjilu.zongls += jine_decimal + szjilu.jrls += jine_decimal + szjilu.save() + logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") + except Exception as e: + logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) + + # 更新订单状态 + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + logger.info(f"会员订单状态更新成功: {order.dingdan_id} -> 3") + + # 更新会员购买次数 + huiyuan.goumai_cishu += 1 + huiyuan.save(update_fields=['goumai_cishu']) + + # 处理会员购买记录 + goumai_record, is_new, is_first_buy = self.process_huiyuan_goumai( + order.yonghuid, order.huiyuan_id, huiyuan + ) + + if is_first_buy: + self.add_first_buy_jifen(order.yonghuid) + + # 🆕 新增:处理分红(包含管事和组长) + with transaction.atomic(): + self.process_fenhong(order, huiyuan) + + logger.info(f"会员支付回调处理完成: {out_trade_no}") + return self.wechat_response('SUCCESS', 'OK') + + except ET.ParseError as e: + logger.error(f"会员XML解析失败: {str(e)}") + return self.wechat_response('FAIL', '数据格式错误') + except Exception as e: + logger.error(f"会员支付回调处理异常: {str(e)}", exc_info=True) + return self.wechat_response('FAIL', '处理异常') + + # ---------- 以下为原方法,基本不变,但需要导入新增模型 ---------- + + def process_huiyuan_goumai(self, yonghuid, huiyuan_id, huiyuan): + """处理会员购买记录(与原代码一致)""" + try: + existing_record = Huiyuangoumai.query.filter( + yonghu_id=yonghuid, + huiyuan_id=huiyuan_id + ).first() + + is_first_buy_any = False + + if existing_record: + existing_record.daoqi_time += timedelta(days=30) + existing_record.huiyuan_zhuangtai = 1 + existing_record.save() + logger.info(f"会员续费: 用户{yonghuid}, 会员{huiyuan_id}") + return existing_record, False, False + else: + other_huiyuan_count = Huiyuangoumai.query.filter( + yonghu_id=yonghuid + ).exclude(huiyuan_id=huiyuan_id).count() + if other_huiyuan_count == 0: + is_first_buy_any = True + logger.info(f"用户{yonghuid}首次购买任何会员,将获得10积分") + + goumai_record = Huiyuangoumai.query.create( + yonghu_id=yonghuid, + huiyuan_id=huiyuan_id, + jieshao=huiyuan.jieshao, + huiyuan_zhuangtai=1, + daoqi_time=timezone.now() + timedelta(days=30) + ) + logger.info(f"新购会员: 用户{yonghuid}, 会员{huiyuan_id}") + return goumai_record, True, is_first_buy_any + except Exception as e: + logger.error(f"处理会员购买记录失败: {str(e)}", exc_info=True) + if "unique" in str(e).lower() or "duplicate" in str(e).lower(): + logger.warning(f"检测到重复创建尝试,已由数据库拦截: 用户{yonghuid}, 会员{huiyuan_id}") + return self.process_huiyuan_goumai(yonghuid, huiyuan_id, huiyuan) + raise + + def add_first_buy_jifen(self, yonghuid): + """首次购买积分奖励(与原代码一致)""" + try: + dashou = UserDashou.query.get(user__UserUID=yonghuid) + dashou.jifen += 10 + dashou.save(update_fields=['jifen']) + logger.info(f"用户{yonghuid}首次购买会员,积分+10,当前积分: {dashou.jifen}") + except UserDashou.DoesNotExist: + logger.error(f"找不到打手记录,无法增加积分: {yonghuid}") + except Exception as e: + logger.error(f"增加积分失败: {str(e)}") + + # 🆕 新增:统一分红处理函数 + def process_fenhong(self, order, huiyuan): + """ + 处理管事和组长分红(支持多次分红配置) + """ + try: + # 获取打手信息 + dashou = UserDashou.query.get(user__UserUID=order.yonghuid) + guanshi_id = dashou.yaoqingren + if not guanshi_id: + logger.info(f"打手{order.yonghuid}没有邀请人,无分红") + return + + # 获取管事信息 + try: + guanshi = UserGuanshi.query.get(user__UserUID=guanshi_id) + except UserGuanshi.DoesNotExist: + logger.warning(f"管事信息不存在: {guanshi_id}") + return + + # 获取组长ID(从管事扩展表的邀请人) + zuzhang_id = guanshi.yaoqingren + + # 获取打手头像和昵称 + user_main = dashou.user + avatar = user_main.avatar + nicheng = dashou.nicheng or "未知打手" + + # 查询该打手针对该管事的已分红次数 + existing_count = Gsfenhong.query.filter( + dashouid=order.yonghuid, + guanshi=guanshi_id + ).count() + cishu = existing_count + 1 # 本次是第几次 + + # 获取订单金额(用于限制分红总额) + order_amount = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine + + # 计算管事分红金额 + guanshi_fenhong = self._calc_guanshi_fenhong(guanshi, huiyuan, guanshi_id, cishu) + if guanshi_fenhong is None: + logger.info(f"管事{guanshi_id}本次无分红") + guanshi_fenhong = Decimal('0.00') + + # 计算组长分红金额(如果存在组长) + zuzhang_fenhong = Decimal('0.00') + if zuzhang_id: + # 获取组长信息 + try: + zuzhang = UserZuzhang.query.get(user__UserUID=zuzhang_id) + except UserZuzhang.DoesNotExist: + logger.warning(f"组长信息不存在: {zuzhang_id}") + zuzhang = None + + zuzhang_fenhong = self._calc_zuzhang_fenhong(zuzhang, huiyuan, zuzhang_id, cishu) + if zuzhang_fenhong is None: + logger.info(f"组长{zuzhang_id}本次无分红") + zuzhang_fenhong = Decimal('0.00') + else: + logger.info(f"管事{guanshi_id}没有组长,无组长分红") + + # 检查分红总额是否超过订单金额(优先保证管事分红) + total_fenhong = guanshi_fenhong + zuzhang_fenhong + if total_fenhong > order_amount: + logger.warning(f"分红总额{total_fenhong}超过订单金额{order_amount},组长分红设为0") + zuzhang_fenhong = Decimal('0.00') + total_fenhong = guanshi_fenhong + if total_fenhong > order_amount: + logger.warning(f"管事分红{guanshi_fenhong}也超过订单金额,强制设为0") + guanshi_fenhong = Decimal('0.00') + total_fenhong = Decimal('0.00') + + # 创建分红记录(先创建主记录,即使金额为0也记录?但为0无意义,所以只有大于0才创建) + if guanshi_fenhong > 0 or zuzhang_fenhong > 0: + # 更新管事扩展表 + if guanshi_fenhong > 0: + guanshi.chongzhifenrun += guanshi_fenhong + guanshi.yue += guanshi_fenhong + guanshi.jinrichongzhi += 1 + guanshi.jinyuechongzhi += 1 + guanshi.save(update_fields=['chongzhifenrun', 'yue', 'jinrichongzhi', 'jinyuechongzhi']) + logger.info(f"管事分红成功: 管事{guanshi_id}, 金额{guanshi_fenhong}") + + # 更新组长扩展表 + if zuzhang_fenhong > 0 and zuzhang: + zuzhang.fenyong_zonge += zuzhang_fenhong + zuzhang.ketixian_jine += zuzhang_fenhong + zuzhang.jinri_fenyong += zuzhang_fenhong + zuzhang.jinyue_fenyong += zuzhang_fenhong + zuzhang.save(update_fields=['fenyong_zonge', 'ketixian_jine', 'jinri_fenyong', 'jinyue_fenyong']) + logger.info(f"组长分红成功: 组长{zuzhang_id}, 金额{zuzhang_fenhong}") + + # 创建分红记录 + Gsfenhong.query.create( + dingdan_id=order.dingdan_id, + guanshi=guanshi_id, + dashouid=order.yonghuid, + shuoming=order.shuoming, + fenhong=guanshi_fenhong, + avatar=avatar, + nicheng=nicheng, + # 新增字段 + zuzhang_id=zuzhang_id if zuzhang_fenhong > 0 else None, + zuzhang_fenhong=zuzhang_fenhong if zuzhang_fenhong > 0 else None, + huiyuan_id=huiyuan.huiyuan_id, + fenhong_leixing=1, + ) + + + + # ========== 管事每日统计(新购) ========== + if guanshi_fenhong > 0 and cishu == 1: + try: + update_guanshi_daily_by_action( + yonghuid=guanshi_id, + action=2, + amount=guanshi_fenhong + ) + logger.info(f"管事每日统计更新成功(新购):管事{guanshi_id},分红{guanshi_fenhong}") + except Exception as e: + logger.error(f"管事每日统计更新失败(新购):{str(e)}") + + # ========== 管事每日统计(续费) ========== + if guanshi_fenhong > 0 and cishu > 1: + try: + update_guanshi_xufei_daily( + yonghuid=guanshi_id, + xufei_jine=guanshi_fenhong + ) + logger.info(f"管事续费统计更新成功:管事{guanshi_id},续费分红{guanshi_fenhong}") + except Exception as e: + logger.error(f"管事续费统计更新失败:{str(e)}") + + # ========== 组长每日统计(分佣收入) ========== + if zuzhang_fenhong > 0 and zuzhang_id: + try: + update_zuzhang_daily_by_action( + yonghuid=zuzhang_id, + action=2, + amount=zuzhang_fenhong + ) + logger.info(f"组长每日统计更新成功:组长{zuzhang_id},分佣{zuzhang_fenhong}") + except Exception as e: + logger.error(f"组长每日统计更新失败:{str(e)}") + + + logger.info(f"分红记录创建成功: 订单{order.dingdan_id}") + else: + logger.info(f"分红金额均为0,不创建记录") + + except Exception as e: + logger.error(f"处理分红失败: {str(e)}", exc_info=True) + # 不抛出异常,避免影响主流程 + + def _calc_guanshi_fenhong(self, guanshi, huiyuan, guanshi_id, cishu): + """计算管事分红金额""" + # 先查多次分红配置表 + try: + config = DuociFenhong.query.get( + huiyuan=huiyuan, + UserUID=guanshi_id, + cishu=cishu + ) + return Decimal(str(config.guanshi_fenhong)) + except DuociFenhong.DoesNotExist: + pass + + if cishu == 1: + # 第一次分红,取会员表默认金额 + return Decimal(str(huiyuan.guanshifc)) + else: + # 第二次及以上,检查是否开启二次分红 + if guanshi.fenghong_erci_enabled: + return Decimal(str(guanshi.fenghong_erci)) + else: + return None # 无分红 + + def _calc_zuzhang_fenhong(self, zuzhang, huiyuan, zuzhang_id, cishu): + """计算组长分红金额""" + if zuzhang is None: + return None + # 查多次分红配置表 + try: + config = DuociFenhong.query.get( + huiyuan=huiyuan, + UserUID=zuzhang_id, + cishu=cishu + ) + return Decimal(str(config.zuzhang_fenhong)) + except DuociFenhong.DoesNotExist: + pass + + if cishu == 1: + # 第一次分红,取会员表默认组长分成 + return Decimal(str(huiyuan.zuzhangfc)) + else: + # 第二次及以上,检查组长是否开启额外分红 + if zuzhang.kaioi_ewai_fenhong: + return Decimal(str(zuzhang.ewai_fenhong_jine)) + else: + return None + + # 以下为原辅助方法,保持原样 + def verify_signature(self, xml_data): + """验证微信支付签名(与原代码一致)""" + try: + root = ET.fromstring(xml_data) + sign = root.find('sign').text + if not sign: + logger.error("会员签名字段不存在") + return False + + params = {} + for child in root: + if child.tag != 'sign' and child.text is not None: + params[child.tag] = child.text + + sorted_params = sorted(params.items(), key=lambda x: x[0]) + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) + sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + return sign == calculated_sign + except Exception as e: + logger.error(f"会员验证签名异常: {str(e)}") + return False + + def wechat_response(self, return_code, return_msg): + """返回微信回调响应""" + xml_response = f""" + + +""" + logger.info(f"会员返回微信响应: {return_code} - {return_msg}") + return HttpResponse(xml_response, content_type='application/xml') + + + + + + + + + + + +# views.py +class HuiyuanShibai(APIView): + """ + 会员支付失败处理接口 + 路径:/shangpin/huiyuanshibai + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:删除结果 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.query.get( + dingdan_id=dingdanid, + UserUID=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单类型是否为会员(1) + if order.leixing != 1: + return Response({ + 'code': 400, + 'message': '订单类型错误,不是会员订单' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 检查订单状态是否为未支付(9) + if order.zhuangtai == 9: + # 删除订单 + order.delete() + logger.info(f"用户{request.user.yonghuid}删除未支付会员订单: {dingdanid}") + + return Response({ + 'code': 200, + 'message': '会员订单已删除' + }, status=status.HTTP_200_OK) + else: + return Response({ + 'code': 400, + 'message': '会员订单状态不是未支付,无法删除' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"会员支付失败处理异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# views.py +class HuiyuanLunxun(APIView): + """ + 会员支付成功轮询接口 + 路径:/shangpin/huiyuanlx + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:订单状态和会员信息 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.query.get( + dingdan_id=dingdanid, + UserUID=request.user.yonghuid + ) + except Czjilu.DoesNotExist: + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单类型是否为会员(1) + if order.leixing != 1: + return Response({ + 'code': 400, + 'message': '订单类型错误,不是会员订单' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 检查订单状态是否为已支付(3) + if order.zhuangtai != 3: + logger.info(f"会员订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") + return Response({ + 'code': 400, + 'message': '会员订单尚未支付完成' + }, status=status.HTTP_400_BAD_REQUEST) + + # 5. 查询会员信息 + try: + huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id) + + # 6. 查询会员购买记录 + goumai_record = Huiyuangoumai.query.filter( + yonghu_id=request.user.yonghuid, + huiyuan_id=order.huiyuan_id + ).first() + + if not goumai_record: + return Response({ + 'code': 400, + 'message': '会员购买记录不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + # 7. 格式化到期时间 + #from django.utils.timezone import localtime + #daoqi_time = localtime(goumai_record.daoqi_time).strftime('%Y-%m-%d %H:%M:%S') + # 直接格式化,不使用时区转换 + daoqi_time = goumai_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') + current_user = request.user + dashou = current_user.dashou_profile + + logger.info(f"会员轮询成功: 订单{dingdanid}已支付,用户{request.user.yonghuid}, 会员{order.huiyuan_id}") + + # 8. 返回会员信息(用于更新前端全局变量) + return Response({ + 'code': 200, + 'message': '支付流程全部完成', + 'jifen': dashou.jifen, + 'huiyuan': { + 'huiyuanid': order.huiyuan_id, + 'huiyuanming': huiyuan.jieshao, + 'daoqi': daoqi_time + } + }, status=status.HTTP_200_OK) + + except Huiyuan.DoesNotExist: + logger.error(f"会员信息不存在: {order.huiyuan_id}") + return Response({ + 'code': 400, + 'message': '会员信息不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"会员轮询异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +# views.py in shangpin app + +class ShangjiaChongzhi(APIView): + """ + 商家余额充值支付参数接口 + 路径:/shangpin/sjcz + 请求方式:POST + 权限:JWT认证 + 参数:jine(金额,1-10000元) + 返回:支付参数(payParams)和订单ID(dingdanid) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 验证用户身份和商家状态 + try: + shangjia = request.user.shop_profile + except UserShangjia.DoesNotExist: + logger.warning(f"用户{request.user.yonghuid}尝试充值但不是商家") + return Response({ + 'code': 400, + 'message': '用户不是商家,无法充值' + }, status=status.HTTP_400_BAD_REQUEST) + + # 检查商家状态 + if shangjia.zhuangtai != 1: + logger.warning(f"商家{shangjia.nicheng}状态异常: {shangjia.zhuangtai}") + return Response({ + 'code': 400, + 'message': '商家状态异常,无法进行充值' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取并验证金额参数 + jine = request.data.get('jine') + if not jine: + return Response({ + 'code': 400, + 'message': '请填写充值金额' + }, status=status.HTTP_400_BAD_REQUEST) + + try: + jine = float(jine) + except (ValueError, TypeError): + return Response({ + 'code': 400, + 'message': '金额格式错误,请填写有效的数字' + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证金额范围 + if jine < settings.SHANGJIA_CZ_MIN_AMOUNT or jine > settings.SHANGJIA_CZ_MAX_AMOUNT: + return Response({ + 'code': 400, + 'message': f'充值金额必须在{settings.SHANGJIA_CZ_MIN_AMOUNT}-{settings.SHANGJIA_CZ_MAX_AMOUNT}元之间' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 生成订单ID(使用配置的前缀) + timestamp_ms = int(time.time() * 1000) + timestamp_ns = int(time.time_ns() // 1000) % 1000000 + random_num = random.randint(0, 99) + + dingdanid = f"{settings.SHANGJIA_CZ_PREFIX}{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" + + # 4. 创建充值记录订单 + try: + chongzhi_jilu = Czjilu.query.create( + dingdan_id=dingdanid, + zhuangtai=9, # 未支付状态 + yonghuid=request.user.yonghuid, + jine=jine, + leixing=4, # 充值类型:4=商家充值 + shuoming=settings.SHANGJIA_CZ_PAY_DESCRIPTION + ) + logger.info(f"创建商家充值订单: 商家{shangjia.nicheng}, 订单{dingdanid}, 金额{jine}元") + except Exception as e: + logger.error(f"创建订单失败: {str(e)}") + return Response({ + 'code': 500, + 'message': '订单创建失败,请重试' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 5. 生成微信支付参数 + try: + pay_params = self.generate_wechat_pay_params( + dingdanid=dingdanid, + jine=jine, + openid=request.user.openid, + pay_type='shangjia' + ) + except Exception as e: + # 支付参数生成失败,删除订单记录 + chongzhi_jilu.delete() + logger.error(f"生成微信支付参数失败: {str(e)}") + return Response({ + 'code': 500, + 'message': f'支付参数生成失败: {str(e)}' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 6. 返回支付参数和订单ID + logger.info(f"商家{shangjia.nicheng}支付参数生成成功: {dingdanid}") + return Response({ + 'code': 200, + 'message': '支付参数生成成功', + 'payParams': pay_params, + 'dingdanid': dingdanid + }, 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) + + def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): + """生成微信小程序支付参数""" + # 获取配置 + APPID = settings.WEIXIN_APPID + MCHID = settings.WEIXIN_MCHID + KEY = settings.WEIXIN_SHANGHUMIYAO + + # 根据支付类型获取对应的配置 + if pay_type == 'shangjia': + PAY_DESCRIPTION = settings.SHANGJIA_CZ_PAY_DESCRIPTION + NOTIFY_URL = settings.SHANGJIA_CZ_PAY_NOTIFY_URL + else: + raise Exception('未知的支付类型') + + # 生成随机字符串 + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + # 金额转换(元转分) + total_fee = int(Decimal(str(jine)) * 100) + + # 交易类型(小程序支付使用JSAPI) + trade_type = 'JSAPI' + + # 获取客户端IP + user_ip = self.get_client_ip() + + # 1. 第一次签名(统一下单接口) + sign_data = { + 'appid': APPID, + 'mch_id': MCHID, + 'nonce_str': nonce_str, + 'body': PAY_DESCRIPTION, + 'out_trade_no': dingdanid, + 'total_fee': total_fee, + 'spbill_create_ip': user_ip, + 'notify_url': NOTIFY_URL, + 'trade_type': trade_type, + 'openid': openid # 小程序支付必须传openid + } + + # 按照参数名ASCII码从小到大排序 + sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) + + # 拼接字符串 + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) + sign_string += f'&key={KEY}' + + # MD5签名 + sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + # 构造XML数据 + xml_data = f""" +{APPID} +{PAY_DESCRIPTION} +{MCHID} +{nonce_str} +{NOTIFY_URL} +{openid} +{dingdanid} +{user_ip} +{total_fee} +{trade_type} +{sign} +""" + + # 调用微信统一下单接口 + response = requests.post( + 'https://api.mch.weixin.qq.com/pay/unifiedorder', + data=xml_data.encode('utf-8'), + headers={'Content-Type': 'application/xml'}, + timeout=10 + ) + + # 解析返回的XML + root = ET.fromstring(response.content) + + return_code = root.find('return_code').text + result_code = root.find('result_code').text + + if return_code == 'SUCCESS' and result_code == 'SUCCESS': + # 获取prepay_id + prepay_id = root.find('prepay_id').text + + # 2. 第二次签名(小程序支付参数) + timestamp = str(int(time.time())) + nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + package = f'prepay_id={prepay_id}' + sign_type = 'MD5' + + # 生成支付签名 + pay_sign_data = { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type + } + + sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) + pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) + pay_sign_string += f'&key={KEY}' + pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() + + # 返回前端需要的支付参数 + return { + 'appId': APPID, + 'timeStamp': timestamp, + 'nonceStr': nonce_str, + 'package': package, + 'signType': sign_type, + 'paySign': pay_sign + } + else: + # 获取错误信息 + err_code = root.find('err_code') + err_code_des = root.find('err_code_des') + return_msg = root.find('return_msg') + + error_details = f"return_code: {return_code}" + if err_code is not None: + error_details += f", err_code: {err_code.text}" + if err_code_des is not None: + error_details += f", err_code_des: {err_code_des.text}" + if return_msg is not None: + error_details += f", return_msg: {return_msg.text}" + + raise Exception(f"微信支付下单失败: {error_details}") + + def get_client_ip(self): + """获取客户端IP""" + request = self.request + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip if ip else '127.0.0.1' + + + + + +class ShangjiaHuitiao(View): + """ + 商家支付回调接口 + 路径:/shangpin/sjhuitiao + 注意:这个接口不需要JWT认证,由微信服务器调用 + """ + + def post(self, request): + try: + # 1. 解析微信回调XML数据 + request_body = request.body + logger.info(f"收到商家支付回调,原始数据: {request_body.decode('utf-8')}") + + root = ET.fromstring(request_body) + + # 获取关键字段 + return_code = root.find('return_code').text + result_code = root.find('result_code').text + out_trade_no = root.find('out_trade_no').text # 订单ID + transaction_id = root.find('transaction_id').text # 微信交易号 + total_fee = int(root.find('total_fee').text) # 支付金额(分) + + logger.info(f"商家回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分") + + # 2. 验证基本返回状态 + if return_code != 'SUCCESS' or result_code != 'SUCCESS': + logger.warning(f"商家支付回调状态异常: {return_code}/{result_code}") + return self.wechat_response('FAIL', '支付失败') + + # 3. 验证签名(确保是微信发来的) + if not self.verify_signature(request_body): + logger.error(f"商家支付签名验证失败: {out_trade_no}") + return self.wechat_response('FAIL', '签名验证失败') + + # 4. 查询订单 + try: + order = Czjilu.query.get(dingdan_id=out_trade_no) + logger.info(f"找到商家订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") + except Czjilu.DoesNotExist: + logger.error(f"商家订单不存在: {out_trade_no}") + return self.wechat_response('FAIL', '订单不存在') + + # 5. 检查订单状态是否为未支付(9) + if order.zhuangtai != 9: + logger.warning(f"商家订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}") + # 已处理过的订单直接返回成功,避免微信重复回调 + return self.wechat_response('SUCCESS', 'OK') + + # 6. 验证金额(微信返回的是分,需要转换) + wechat_amount_yuan = Decimal(str(total_fee)) / Decimal('100') # 转换为元 + order_amount_yuan = Decimal(str(order.jine)) # 订单金额(元) + + # 允许1分钱的误差 + if abs(wechat_amount_yuan - order_amount_yuan) > Decimal('0.01'): + logger.error(f"商家订单金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元") + return self.wechat_response('FAIL', '金额不一致') + + # ===== 在这里添加收支记录更新代码 ===== + # 更新收支记录表 + try: + # 获取订单金额(确保是Decimal类型) + jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine + + update_daily_income(jine_decimal) + + # 查询或创建ID=1的收支记录 + szjilu, created = Szjilu.query.get_or_create( + id=1, + defaults={ + 'zongsy': Decimal('0.00'), + 'zongls': Decimal('0.00'), + 'zongzc': Decimal('0.00'), + 'jrzc': Decimal('0.00'), + 'jrls': Decimal('0.00') + } + ) + + # 更新三个字段 + szjilu.zongsy += jine_decimal # 总收益 + szjilu.zongls += jine_decimal # 总流水 + szjilu.jrls += jine_decimal # 今日流水 + + szjilu.save() + + logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元") + + except Exception as e: + # 收支记录更新失败不影响主流程,只记录日志 + logger.error(f"更新收支记录失败: {str(e)}", exc_info=True) + # ===== 添加结束 ===== + + + + # 7. 使用事务更新订单状态和商家余额 + with transaction.atomic(): + # 更新订单状态为已支付(3) + order.zhuangtai = 3 + order.save(update_fields=['zhuangtai']) + logger.info(f"商家订单状态更新成功: {order.dingdan_id} -> 3") + + # 更新商家余额 + try: + # 通过用户ID查找商家(高效查询) + shangjia = UserShangjia.query.get(user__UserUID=order.yonghuid) + + # 增加余额 + shangjia.yue += order_amount_yuan + shangjia.save(update_fields=['yue']) + + logger.info(f"商家余额更新成功: 商家{shangjia.nicheng}, 增加{order_amount_yuan}元, 新余额{shangjia.yue}") + + except UserShangjia.DoesNotExist: + # 如果找不到商家,记录错误,但依然返回成功(避免微信重复回调) + logger.error(f"商家信息不存在: 用户{order.yonghuid}") + return self.wechat_response('SUCCESS', 'OK') + except Exception as e: + logger.error(f"更新商家余额失败: {str(e)}", exc_info=True) + # 回滚事务 + raise + + # 8. 返回成功响应给微信 + logger.info(f"商家支付回调处理完成: {out_trade_no}") + return self.wechat_response('SUCCESS', 'OK') + + except ET.ParseError as e: + logger.error(f"XML解析失败: {str(e)}") + return self.wechat_response('FAIL', '数据格式错误') + except Exception as e: + logger.error(f"商家支付回调处理异常: {str(e)}", exc_info=True) + # 返回FAIL让微信重试 + return self.wechat_response('FAIL', '处理异常') + + def verify_signature(self, xml_data): + """验证微信支付签名""" + try: + root = ET.fromstring(xml_data) + sign = root.find('sign').text + + if not sign: + logger.error("签名字段不存在") + return False + + # 获取所有参数并排除sign + params = {} + for child in root: + if child.tag != 'sign' and child.text is not None: + params[child.tag] = child.text + + # 按照参数名ASCII码从小到大排序 + sorted_params = sorted(params.items(), key=lambda x: x[0]) + + # 拼接字符串 + sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) + sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + + # 计算签名 + calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() + + is_valid = sign == calculated_sign + if not is_valid: + logger.error(f"商家支付签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}") + + return is_valid + + except Exception as e: + logger.error(f"验证签名异常: {str(e)}") + return False + + def wechat_response(self, return_code, return_msg): + """返回微信支付回调响应""" + xml_response = f""" + + +""" + + logger.info(f"返回微信响应: {return_code} - {return_msg}") + return HttpResponse(xml_response, content_type='application/xml') + + + + +# views.py +class ShangjiaShibai(APIView): + """ + 商家支付失败处理接口 + 路径:/shangpin/sjshibai + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:删除结果 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的订单) + try: + order = Czjilu.query.get( + dingdan_id=dingdanid, + UserUID=request.user.yonghuid, + leixing=4 # 确保是商家充值订单 + ) + except Czjilu.DoesNotExist: + logger.warning(f"商家订单不存在或不属于当前用户: {dingdanid}, 用户{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单状态是否为未支付(9) + if order.zhuangtai == 9: + # 删除订单 + order.delete() + logger.info(f"商家{request.user.shop_profile.nicheng}删除未支付订单: {dingdanid}") + + return Response({ + 'code': 200, + 'message': '订单已删除' + }, status=status.HTTP_200_OK) + else: + return Response({ + 'code': 400, + 'message': '订单状态不是未支付,无法删除' + }, status=status.HTTP_400_BAD_REQUEST) + + except UserShangjia.DoesNotExist: + return Response({ + 'code': 400, + 'message': '用户不是商家' + }, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error(f"商家支付失败处理异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +# views.py +class ShangjiaChenggong(APIView): + """ + 商家支付成功轮询接口 + 路径:/shangpin/sjcg + 请求方式:POST + 权限:JWT认证 + 参数:dingdanid(订单ID) + 返回:订单状态和当前商家余额 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取订单ID + dingdanid = request.data.get('dingdanid') + if not dingdanid: + return Response({ + 'code': 400, + 'message': '订单ID不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 查询订单(确保是当前用户的商家订单) + try: + order = Czjilu.query.get( + dingdan_id=dingdanid, + UserUID=request.user.yonghuid, + leixing=4 # 商家充值类型 + ) + except Czjilu.DoesNotExist: + logger.warning(f"商家订单不存在: {dingdanid}, 用户{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '订单不存在或不属于当前用户' + }, status=status.HTTP_400_BAD_REQUEST) + + # 3. 检查订单状态是否为已支付(3) + if order.zhuangtai != 3: + logger.info(f"商家订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") + return Response({ + 'code': 400, + 'message': '订单尚未支付完成' + }, status=status.HTTP_400_BAD_REQUEST) + + # 4. 获取商家余额(通过反向查询) + try: + shangjia = request.user.shop_profile + yue = float(shangjia.yue) # 转为浮点数方便前端处理 + + logger.info(f"商家轮询成功: 订单{dingdanid}已支付,商家{shangjia.nicheng}余额为{yue}元") + + return Response({ + 'code': 200, + 'message': '支付流程全部完成', + 'yue': yue # 返回当前商家余额(前端需要更新全局变量) + }, status=status.HTTP_200_OK) + + except UserShangjia.DoesNotExist: + logger.error(f"商家信息不存在: 用户{request.user.yonghuid}") + return Response({ + 'code': 400, + 'message': '商家信息不存在' + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"商家轮询异常: {str(e)}", exc_info=True) + return Response({ + 'code': 500, + 'message': '服务器内部错误' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +# 管事分红记录查询API + +class GuanshiFenhongListAPI(APIView): + """ + 管事分红记录查询接口 + 权限:需要JWT认证 + 请求方式:POST + 分页参数:page, page_size + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 获取当前管事用户ID + yonghuid = request.user.yonghuid + + # 获取分页参数 + 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('-create_time') \ + .values('dashouid', 'avatar', 'nicheng', 'fenhong', 'create_time')[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']), # 分红金额 + 'create_time': item['create_time'].strftime('%Y-%m-%d %H:%M:%S') if item['create_time'] 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.yonghuid}") + return Response({ + 'code': 400, + 'message': '缺少账号参数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 2. 获取当前登录用户 + current_user = request.user + + # 3. 验证用户类型是否为管理员 + if current_user.user_type != 'admin': + logger.warning( + f"权限验证失败:用户 {current_user.yonghuid} 的用户类型为 {current_user.user_type},非管理员") + return Response({ + 'code': 401, + 'message': '无管理员权限', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 4. 验证手机号与传递的账号是否一致 + if current_user.phone != zhanghao: + logger.warning( + f"账号验证失败:用户 {current_user.yonghuid} 的phone={current_user.phone},传递的zhanghao={zhanghao}") + return Response({ + 'code': 401, + 'message': '账号验证失败', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 5. 验证管理员扩展表是否存在 + try: + admin_profile = current_user.admin_profile + logger.info(f"管理员验证通过:用户 {current_user.yonghuid},手机号 {current_user.phone}") + except AdminProfile.DoesNotExist: + logger.error( + f"管理员扩展表不存在:用户 {current_user.yonghuid} 虽然user_type=admin,但没有AdminProfile记录") + return Response({ + 'code': 401, + 'message': '管理员信息不完整', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 6. 查询商品类型数据 + try: + # 使用select_related或prefetch_related优化查询(如果需要关联查询) + # 这里直接查询所有字段,然后提取需要的字段 + shangpinleixing_queryset = ShangpinLeixing.query.all() + + # 转换为前端需要的格式 + shangpinleixing_list = [] + for leixing in shangpinleixing_queryset: + leixing_data = { + 'id': leixing.id, + 'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None, + 'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None, + 'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None, + 'jieshao': leixing.jieshao if leixing.jieshao else '', + 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' + } + shangpinleixing_list.append(leixing_data) + + #logger.info(f"成功查询到 {len(shangpinleixing_list)} 条商品类型数据") + + except Exception as e: + logger.error(f"查询商品类型数据时出错:{str(e)}") + shangpinleixing_list = [] + + # 7. 查询商品专区数据 + try: + # 查询所有商品专区 + zhuanqu_queryset = ShangpinZhuanqu.query.all() + + # 转换为前端需要的格式 + zhuanqu_list = [] + for zhuanqu in zhuanqu_queryset: + zhuanqu_data = { + 'id': zhuanqu.id, + 'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '', + 'leixing_id': zhuanqu.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 ShangpinLeixingFabuView(APIView): + """ + 商品类型发布接口 + 接收图片、类型介绍、抢单要求类型等字段 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 + + def post(self, request): + try: + # === 1. 权限验证 === + # 获取当前用户 + user_main = request.user + + # 1.1 验证前端传递的账号是否匹配(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '未提供账号信息', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 验证账号是否匹配(使用phone字段比较) + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '账号验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限不足', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表(使用AdminProfile模型) + try: + admin_profile = AdminProfile.query.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '管理员信息不存在', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证字段 === + # 2.1 必填字段验证 + jieshao = request.data.get('jieshao', '').strip() + yaoqiuleixing = request.data.get('yaoqiuleixing') + + + if not jieshao: + return Response( + {'code': 400, 'msg': '类型介绍不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if yaoqiuleixing not in ['1', '2']: + return Response( + {'code': 400, 'msg': '抢单要求类型无效', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.2 验证图片 + tupian_file = request.FILES.get('file') + if not tupian_file: + + return Response( + {'code': 400, 'msg': '请上传类型图片', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证图片格式和大小 + is_valid, error_msg = validate_image(tupian_file) + if not is_valid: + return Response( + {'code': 400, 'msg': f'图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.3 根据抢单要求类型验证对应字段 + huiyuan_id = request.data.get('huiyuan_id', '').strip() + yongjin = request.data.get('yongjin', '').strip() + + + # 转换yaoqiuleixing为整数便于处理 + yaoqiuleixing_int = int(yaoqiuleixing) + + if yaoqiuleixing_int == 1: # 会员抢单 + if not huiyuan_id: + return Response( + {'code': 400, 'msg': '会员抢单必须选择会员', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + # 验证会员ID是否存在 + try: + huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response( + {'code': 400, 'msg': '会员ID不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + # 会员ID为字符串,佣金为空 + yongjin_value = None + huiyuan_id_value = huiyuan_id + else: # yaoqiuleixing_int == 2 佣金抢单 + if not yongjin: + return Response( + {'code': 400, 'msg': '佣金抢单必须填写佣金金额', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + try: + # 验证佣金为有效数字 + yongjin_value = float(yongjin) + if yongjin_value <= 0: + return Response( + {'code': 400, 'msg': '佣金金额必须大于0', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '佣金金额格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + huiyuan_id_value = None + + # === 3. 创建商品类型记录 === + with transaction.atomic(): + # 3.1 创建商品类型对象 + leixing_obj = ShangpinLeixing( + jieshao=jieshao, + yaoqiuleixing=yaoqiuleixing_int, + huiyuan_id=huiyuan_id_value, + yongjin=yongjin_value, + # tupian_url 先留空,上传成功后再更新 + ) + + # 保存到数据库获取ID + leixing_obj.save() + leixing_id = leixing_obj.id + + # === 4. 处理图片上传 === + try: + # 4.1 生成文件名和路径 + file_extension = os.path.splitext(tupian_file.name)[1].lower() + if not file_extension: + file_extension = '.jpg' + + # 生成唯一文件名:商品类型ID + 随机UUID + 后缀 + unique_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}" + + # 构建OSS文件路径:a_long/leixing/{leixing_id}/{filename} + oss_folder = 'a_long' # 根文件夹 + leixing_folder = 'leixing' # 类型文件夹 + + # 构建完整路径 + oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{unique_filename}" + + # 4.2 上传到OSS + tupian_url = upload_to_oss(tupian_file, oss_file_path) + + if not tupian_url: + # 上传失败,抛出异常触发回滚 + raise Exception("图片上传到OSS失败") + + # 4.3 更新商品类型的图片URL(存储相对路径) + leixing_obj.tupian_url = oss_file_path # 存相对路径,方便拼接 + leixing_obj.save() + + except Exception as oss_error: + # 图片上传失败,事务会回滚,商品类型记录也会被删除 + print(f"OSS上传失败: {oss_error}") + # 手动抛出异常触发事务回滚 + raise Exception(f"图片上传失败: {str(oss_error)}") + + # === 5. 构建返回数据 === + response_data = { + 'id': leixing_id, + 'jieshao': jieshao, + 'yaoqiuleixing': yaoqiuleixing_int, + 'tupian_url': leixing_obj.tupian_url, # 相对路径 + 'huiyuan_id': huiyuan_id_value, + 'yongjin': str(yongjin_value) if yongjin_value else None + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '商品类型添加成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class ShangpinZhuanquFabuView(APIView): + """ + 商品专区发布接口 + 接收专区名称、类型ID + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # === 1. 权限验证 === + user_main = request.user + + # 1.1 验证账号(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '未提供账号信息', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '账号验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限不足', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表(使用AdminProfile模型) + try: + admin_profile = AdminProfile.query.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '管理员信息不存在', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证字段 === + mingzi = request.data.get('mingzi', '').strip() + leixing_id = request.data.get('leixing_id') + + # 2.1 验证必填字段 + if not mingzi: + return Response( + {'code': 400, 'msg': '专区名称不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not leixing_id: + return Response( + {'code': 400, 'msg': '类型ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.2 验证类型ID是否存在 + try: + # 验证商品类型是否存在 + leixing_obj = ShangpinLeixing.query.get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response( + {'code': 400, 'msg': '商品类型不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.3 验证名称长度(15汉字以内) + if len(mingzi) > 45: # 数据库字段最大长度 + return Response( + {'code': 400, 'msg': '专区名称过长', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 3. 创建商品专区记录 === + zhuanqu_obj = ShangpinZhuanqu( + mingzi=mingzi, + leixing_id=leixing_id + ) + + zhuanqu_obj.save() + + # === 4. 构建返回数据 === + response_data = { + 'id': zhuanqu_obj.id, + 'mingzi': mingzi, + 'leixing_id': leixing_id + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '商品专区添加成功', + 'data': response_data + }) + + except Exception as e: + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class ShangpinFabuView(APIView): + """ + 商品发布接口 + 接收商品信息、图片等字段 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 + + def post(self, request): + try: + # === 1. 权限验证 === + user_main = request.user + + # 1.1 验证账号(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '未提供账号信息', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '账号验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限不足', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表(使用AdminProfile模型) + try: + admin_profile = AdminProfile.query.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '管理员信息不存在', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证字段 === + # 2.1 验证必填字段 + biaoti = request.data.get('biaoti', '').strip() + jiage = request.data.get('jiage', '').strip() + kucun = request.data.get('kucun', '').strip() + leixing_id = request.data.get('leixing_id') + zhuanqu_id = request.data.get('zhuanqu_id') + yaoqiuleixing = request.data.get('yaoqiuleixing') + jieshao = request.data.get('jieshao', '').strip() + + # 验证图片 + shangpin_tupian = request.FILES.get('shangpin_tupian') + if not shangpin_tupian: + + return Response( + {'code': 400, 'msg': '请上传商品图片', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证图片格式和大小 + is_valid, error_msg = validate_image(shangpin_tupian) + if not is_valid: + return Response( + {'code': 400, 'msg': f'商品图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.2 验证必填字段是否为空 + required_fields = { + 'biaoti': biaoti, + 'jiage': jiage, + 'kucun': kucun, + 'leixing_id': leixing_id, + 'zhuanqu_id': zhuanqu_id, + 'yaoqiuleixing': yaoqiuleixing, + 'jieshao': jieshao + } + + for field_name, field_value in required_fields.items(): + if not field_value: + return Response( + {'code': 400, 'msg': f'{field_name}不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.3 验证数值字段 + try: + jiage_value = float(jiage) + if jiage_value <= 0: + return Response( + {'code': 400, 'msg': '商品价格必须大于0', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '商品价格格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + kucun_value = int(kucun) + if kucun_value < 0: + return Response( + {'code': 400, 'msg': '库存不能为负数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '库存必须为整数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证抢单要求类型 + if yaoqiuleixing not in ['1', '2']: + return Response( + {'code': 400, 'msg': '抢单要求类型无效', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 3. 验证关联数据 === + # 3.1 验证商品类型 + try: + leixing_obj = ShangpinLeixing.query.get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response( + {'code': 400, 'msg': '商品类型不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 3.2 根据抢单要求类型验证商品类型 + yaoqiuleixing_int = int(yaoqiuleixing) + + if yaoqiuleixing_int == 1: # 会员抢单 + # 需要验证商品类型的抢单要求类型是否为1 + if leixing_obj.yaoqiuleixing != 1: + return Response( + {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 获取会员ID(从前端或商品类型) + huiyuan_id = request.data.get('huiyuan_id', leixing_obj.huiyuan_id) + if not huiyuan_id: + return Response( + {'code': 400, 'msg': '会员ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证会员是否存在 + try: + huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response( + {'code': 400, 'msg': '会员不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 设置会员相关字段 + huiyuan_id_value = huiyuan_id + yongjin_value = None + else: # 佣金抢单 + # 需要验证商品类型的抢单要求类型是否为2 + if leixing_obj.yaoqiuleixing != 2: + return Response( + {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 获取佣金(从前端或商品类型) + yongjin = request.data.get('yongjin', leixing_obj.yongjin) + if not yongjin: + return Response( + {'code': 400, 'msg': '佣金金额不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + yongjin_value = float(yongjin) + if yongjin_value <= 0: + return Response( + {'code': 400, 'msg': '佣金金额必须大于0', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '佣金金额格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + huiyuan_id_value = None + + # 3.3 验证商品专区 + try: + zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id, leixing_id=leixing_id) + except ShangpinZhuanqu.DoesNotExist: + return Response( + {'code': 400, 'msg': '商品专区不存在或与商品类型不匹配', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 4. 创建商品记录 === + with transaction.atomic(): + # 4.1 获取其他字段 + xuzhi = request.data.get('xuzhi', '').strip() + xiaoliang = request.data.get('xiaoliang', '0').strip() + + # 转换销量为整数 + try: + duiwai_xiaoliang_value = int(xiaoliang) if xiaoliang else 0 + except ValueError: + duiwai_xiaoliang_value = 0 + + # 4.2 创建商品对象 + shangpin_obj = Shangpin( + biaoqian=biaoti, + jiage=jiage_value, + kucun=kucun_value, + leixing_id=leixing_id, + zhuanqu_id=zhuanqu_id, + duiwai_xiaoliang=duiwai_xiaoliang_value, + jieshao=jieshao, + xiadan_xuzhi=xuzhi, + yaoqiuleixing=yaoqiuleixing_int, + huiyuan_id=huiyuan_id_value, + yongjin=yongjin_value, + # tupian_url 先留空,上传成功后再更新 + ) + + # 保存到数据库获取ID + shangpin_obj.save() + shangpin_id = shangpin_obj.id + + # === 5. 处理商品图片上传 === + try: + # 5.1 生成文件名和路径 + file_extension = os.path.splitext(shangpin_tupian.name)[1].lower() + if not file_extension: + file_extension = '.jpg' + + # 构建OSS文件路径:a_long/Spcc{shangpin_id}/{filename} + oss_folder = 'a_long' # 根文件夹 + spcc_folder = f"Spcc{shangpin_id}" # 商品图片文件夹 + + # 生成文件名:Spcc{shangpin_id}{file_extension} + unique_filename = f"Spcc{shangpin_id}{file_extension}" + + # 构建完整路径 + oss_file_path = f"{oss_folder}/{spcc_folder}/{unique_filename}" + + # 5.2 上传到OSS + tupian_url = upload_to_oss(shangpin_tupian, oss_file_path) + + if not tupian_url: + # 上传失败,抛出异常触发回滚 + raise Exception("商品图片上传到OSS失败") + + # 5.3 更新商品的图片URL(存储相对路径) + shangpin_obj.tupian_url = oss_file_path # 存相对路径 + shangpin_obj.save() + + except Exception as oss_error: + # 图片上传失败,事务会回滚,商品记录也会被删除 + #print(f"商品图片OSS上传失败: {oss_error}") + # 手动抛出异常触发事务回滚 + raise Exception(f"商品图片上传失败: {str(oss_error)}") + + # === 6. 构建返回数据 === + response_data = { + 'shangpin_id': shangpin_id, + 'biaoti': biaoti, + 'jiage': str(jiage_value), + 'kucun': kucun_value, + 'leixing_id': leixing_id, + 'zhuanqu_id': zhuanqu_id, + 'xiaoliang': duiwai_xiaoliang_value, + 'jieshao': jieshao, + 'xuzhi': xuzhi, + 'yaoqiuleixing': yaoqiuleixing_int, + 'huiyuan_id': huiyuan_id_value, + 'yongjin': str(yongjin_value) if yongjin_value else None, + 'tupian_url': shangpin_obj.tupian_url # 相对路径 + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '商品发布成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class GuizeTupianShangchuanView(APIView): + """ + 规则图片上传接口 + 接收商品ID和规则图片 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传 + + def post(self, request): + try: + # === 1. 权限验证 === + user_main = request.user + + # 1.1 验证账号(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '未提供账号信息', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '账号验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限不足', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表(使用AdminProfile模型) + try: + admin_profile = AdminProfile.query.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '管理员信息不存在', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证字段 === + # 2.1 验证商品ID + shangpin_id = request.data.get('shangpin_id') + if not shangpin_id: + return Response( + {'code': 400, 'msg': '商品ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.2 验证规则图片 + guize_tupian = request.FILES.get('guize_tupian') + if not guize_tupian: + return Response( + {'code': 400, 'msg': '请上传规则图片', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证图片格式和大小 + is_valid, error_msg = validate_image(guize_tupian) + if not is_valid: + return Response( + {'code': 400, 'msg': f'规则图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 3. 验证商品是否存在 === + try: + shangpin_obj = Shangpin.query.get(id=shangpin_id) + except Shangpin.DoesNotExist: + return Response( + {'code': 400, 'msg': '商品不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 4. 处理规则图片上传 === + try: + # 4.1 生成文件名和路径 + file_extension = os.path.splitext(guize_tupian.name)[1].lower() + if not file_extension: + file_extension = '.jpg' + + # 构建OSS文件路径:a_long/Spgz{shangpin_id}/{filename} + oss_folder = 'a_long' # 根文件夹 + spgz_folder = f"Spgz{shangpin_id}" # 规则图片文件夹 + + # 生成文件名:Spgz{shangpin_id}{file_extension} + unique_filename = f"Spgz{shangpin_id}{file_extension}" + + # 构建完整路径 + oss_file_path = f"{oss_folder}/{spgz_folder}/{unique_filename}" + + # 4.2 上传到OSS + tupian_url = upload_to_oss(guize_tupian, oss_file_path) + + if not tupian_url: + # 上传失败 + return Response({ + 'code': 500, + 'msg': '规则图片上传失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 4.3 更新商品的规则图片URL(存储相对路径) + shangpin_obj.guize_tupian = oss_file_path # 存相对路径 + shangpin_obj.save() + + except Exception as oss_error: + # OSS上传失败 + print(f"规则图片OSS上传失败: {oss_error}") + return Response({ + 'code': 500, + 'msg': f'规则图片上传失败: {str(oss_error)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # === 5. 构建返回数据 === + response_data = { + 'shangpin_id': shangpin_id, + 'guize_tupian_url': oss_file_path # 相对路径 + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '规则图片上传成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +class AdHuiyuanHqView(APIView): + """ + 管理员获取会员信息和商品类型接口 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # === 1. 权限验证 === + # 获取当前用户 + user_main = request.user + + # 1.1 验证前端传递的账号是否匹配(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 验证账号是否匹配(使用phone字段比较) + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表(使用AdminProfile模型) + try: + admin_profile = AdminProfile.query.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 查询会员表数据 === + # 查询所有会员数据,按创建时间倒序排列 + huiyuan_queryset = Huiyuan.query.all().order_by('-create_time') + + # 构建会员数据列表 + huiyuan_list = [] + for huiyuan in huiyuan_queryset: + huiyuan_data = { + 'huiyuan_id': huiyuan.huiyuan_id, # 会员ID + 'jieshao': huiyuan.jieshao, # 会员介绍(会员标题) + 'jtjieshao': huiyuan.jtjieshao if huiyuan.jtjieshao else '', # 会员规则介绍(具体介绍) + 'jiage': str(huiyuan.jiage), # 会员价格(转换为字符串,避免Decimal序列化问题) + 'guanshifc': str(huiyuan.guanshifc), # 管事分成(转换为字符串) + 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数(销量) + 'create_time': huiyuan.create_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.create_time else None, + # 创建时间 + 'update_time': huiyuan.update_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.update_time else None + # 更新时间 + } + huiyuan_list.append(huiyuan_data) + + # === 3. 查询商品类型表数据 === + # 筛选抢单要求类型等于数字1的所有数据(yaoqiuleixing=1) + shangpinleixing_queryset = ShangpinLeixing.query.filter( + yaoqiuleixing=1 # 抢单要求类型为1(会员抢单) + ).order_by('id') # 按ID排序 + + # 构建商品类型数据列表 + shangpinleixing_list = [] + for leixing in shangpinleixing_queryset: + # 注意:这里只需要特定的字段 + leixing_data = { + 'id': leixing.id, # 商品类型ID + 'huiyuan_id': leixing.huiyuan_id, # 会员ID(关联字段) + 'jieshao': leixing.jieshao, # 类型介绍(商品类型名称) + 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' # 商品类型图片相对URL + } + shangpinleixing_list.append(leixing_data) + + # === 4. 构建返回数据 === + response_data = { + 'huiyuanList': huiyuan_list, + 'shangpinleixingList': shangpinleixing_list + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '获取数据成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + +class HuiyuanXiugaiView(APIView): + """ + 会员修改接口 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # === 1. 权限验证 === + user_main = request.user + + # 1.1 验证前端传递的账号是否匹配(使用phone字段) + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员(检查user_type字段) + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表 + try: + admin_profile = AdminProfile.query.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证必填字段 === + huiyuan_id = request.data.get('huiyuan_id', '').strip() + if not huiyuan_id: + return Response( + {'code': 400, 'msg': '会员ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 3. 查询会员数据 === + try: + huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response( + {'code': 400, 'msg': '会员不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 4. 更新会员信息 === + # 4.1 获取前端传递的字段(只更新有值的字段) + jieshao = request.data.get('jieshao', '').strip() + jtjieshao = request.data.get('jtjieshao', '').strip() + jiage = request.data.get('jiage', '').strip() + guanshifc = request.data.get('guanshifc', '').strip() + + # 4.2 更新字段 + if jieshao: + # 验证名称长度(30个字符以内) + if len(jieshao) > 30: + return Response( + {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + huiyuan_obj.jieshao = jieshao + + if jtjieshao: + # 验证介绍长度(没有长度限制,但做一下基本检查) + if len(jtjieshao) > 2000: + return Response( + {'code': 400, 'msg': '会员介绍过长', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + huiyuan_obj.jtjieshao = jtjieshao + + if jiage: + try: + jiage_value = float(jiage) + if jiage_value < 0: + return Response( + {'code': 400, 'msg': '会员价格不能为负数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + if jiage_value > 100000: + return Response( + {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + huiyuan_obj.jiage = jiage_value + except ValueError: + return Response( + {'code': 400, 'msg': '会员价格格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if guanshifc: + try: + guanshifc_value = float(guanshifc) + if guanshifc_value < 0: + return Response( + {'code': 400, 'msg': '管事分成不能为负数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + # 检查管事分成是否超过会员价格 + if hasattr(huiyuan_obj, 'jiage') and huiyuan_obj.jiage: + if guanshifc_value > float(huiyuan_obj.jiage): + return Response( + {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + huiyuan_obj.guanshifc = guanshifc_value + except ValueError: + return Response( + {'code': 400, 'msg': '管事分成格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 4.3 检查是否有任何字段需要更新 + if not (jieshao or jtjieshao or jiage or guanshifc): + return Response( + {'code': 400, 'msg': '没有需要更新的字段', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 4.4 保存更新 + huiyuan_obj.save() + + # === 5. 构建返回数据 === + response_data = { + 'huiyuan_id': huiyuan_obj.huiyuan_id, + 'jieshao': huiyuan_obj.jieshao, + 'jtjieshao': huiyuan_obj.jtjieshao if huiyuan_obj.jtjieshao else '', + 'jiage': str(huiyuan_obj.jiage), + 'guanshifc': str(huiyuan_obj.guanshifc), + 'goumai_cishu': huiyuan_obj.goumai_cishu, + 'create_time': huiyuan_obj.create_time.strftime( + '%Y-%m-%d %H:%M:%S') if huiyuan_obj.create_time else None, + 'update_time': huiyuan_obj.update_time.strftime( + '%Y-%m-%d %H:%M:%S') if huiyuan_obj.update_time else None + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '会员信息修改成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + + + + +class AdHuiyuanFbView(APIView): + """ + 会员发布接口 + 需要管理员权限 + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + # === 1. 权限验证 === + user_main = request.user + + # 1.1 验证前端传递的账号是否匹配 + zhanghao = request.data.get('zhanghao', '').strip() + if not zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if user_main.phone != zhanghao: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.2 验证是否为管理员 + if user_main.user_type != 'admin': + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 1.3 验证管理员扩展表 + try: + admin_profile = AdminProfile.query.get(user=user_main) + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'msg': '权限验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # === 2. 接收并验证字段 === + jieshao = request.data.get('jieshao', '').strip() + jtjieshao = request.data.get('jtjieshao', '').strip() + jiage = request.data.get('jiage', '').strip() + guanshifc = request.data.get('guanshifc', '').strip() + + # 2.1 验证必填字段 + if not jieshao: + return Response( + {'code': 400, 'msg': '会员名称不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not jiage: + return Response( + {'code': 400, 'msg': '会员价格不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not guanshifc: + return Response( + {'code': 400, 'msg': '管事分成不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.2 验证字段长度和格式 + if len(jieshao) > 30: + return Response( + {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if jtjieshao and len(jtjieshao) > 2000: + return Response( + {'code': 400, 'msg': '会员介绍过长', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2.3 验证价格和分成格式 + try: + jiage_value = float(jiage) + if jiage_value <= 0: + return Response( + {'code': 400, 'msg': '会员价格必须大于0', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + if jiage_value > 100000: + return Response( + {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '会员价格格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + guanshifc_value = float(guanshifc) + if guanshifc_value < 0: + return Response( + {'code': 400, 'msg': '管事分成不能为负数', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + if guanshifc_value > jiage_value: + return Response( + {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + except ValueError: + return Response( + {'code': 400, 'msg': '管事分成格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # === 3. 生成会员ID === + # 生成6位唯一会员ID,格式:V + 5位随机数字字母 + def generate_huiyuan_id(): + # 使用V开头,后跟5位随机字符(数字和大写字母) + chars = string.digits + string.ascii_uppercase + while True: + random_part = ''.join(random.choices(chars, k=5)) + huiyuan_id = f"V{random_part}" + # 检查是否已存在 + if not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists(): + return huiyuan_id + + huiyuan_id = generate_huiyuan_id() + + # === 4. 创建会员记录 === + with transaction.atomic(): + huiyuan_obj = Huiyuan( + huiyuan_id=huiyuan_id, + jieshao=jieshao, + jtjieshao=jtjieshao if jtjieshao else '', + jiage=jiage_value, + guanshifc=guanshifc_value, + goumai_cishu=0, # 初始购买次数为0 + ) + + huiyuan_obj.save() + + # === 5. 构建返回数据 === + response_data = { + 'huiyuan_id': huiyuan_obj.huiyuan_id, + 'jieshao': huiyuan_obj.jieshao, + 'jtjieshao': huiyuan_obj.jtjieshao, + 'jiage': str(huiyuan_obj.jiage), + 'guanshifc': str(huiyuan_obj.guanshifc), + 'goumai_cishu': huiyuan_obj.goumai_cishu, + 'create_time': huiyuan_obj.create_time.strftime( + '%Y-%m-%d %H:%M:%S') if huiyuan_obj.create_time else None, + 'update_time': huiyuan_obj.update_time.strftime( + '%Y-%m-%d %H:%M:%S') if huiyuan_obj.update_time else None + } + + # 返回成功响应 + return Response({ + 'code': 0, + 'msg': '会员发布成功', + 'data': response_data + }) + + except Exception as e: + # 捕获所有异常,返回500错误 + traceback.print_exc() + return Response({ + 'code': 500, + 'msg': f'系统错误: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +class AdShangpinHuQu(APIView): + """ + 后台商品管理页面数据获取接口 + 路径: /shangpin/adsphq/ + 方法: POST + 权限: JWT认证 + 管理员权限 + 功能: 获取商品管理页面需要的所有数据 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取前端传递的账号 + qianzhanghao = request.data.get('zhanghao', '').strip() + if not qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 2. 从JWT Token获取当前用户 + dangqianyonghu = request.user + + # 3. 验证账号匹配 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 验证用户类型为管理员 + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 5. 验证管理员扩展表数据 + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 6. 查询公告数据(类型=1的公告) + gonggao_obj = Gonggao.query.filter(leixing=1).first() + shangpingonggao = gonggao_obj.jieshao if gonggao_obj else '' + + # 7. 查询轮播图数据(类型=1的轮播图) + lunbo_objs = Lunbo.query.filter(leixing=1).values('tupian_url') + shangpinlunbo = [item['tupian_url'] for item in lunbo_objs] + + # 8. 查询商品类型数据 + shangpinleixing_data = ShangpinLeixing.query.all().values( + 'id', # 类型ID + 'jieshao', # 类型介绍(类型名称) + 'tupian_url', # 类型图片URL + 'yaoqiuleixing', # 抢单要求类型(1=会员抢单,2=佣金抢单) + 'huiyuan_id', # 会员ID(如果yaoqiuleixing=1) + 'yongjin' # 佣金要求(如果yaoqiuleixing=2) + ) + + # 9. 查询商品专区数据 + shangpinzhuanqu_data = ShangpinZhuanqu.query.all().values( + 'id', # 专区ID + 'mingzi', # 专区名称 + 'leixing_id' # 所属类型ID + ) + + # 10. 查询会员数据 + huiyuan_data = Huiyuan.query.all().values( + 'huiyuan_id', # 会员ID + 'jieshao', # 会员介绍(会员名称) + 'jiage' # 会员价格 + ) + + # 11. 查询商品数据 + shangpin_data = Shangpin.query.all().values( + 'id', # 商品ID + 'biaoqian', # 商品标题/标签 + 'jiage', # 商品价格 + 'leixing_id', # 商品类型ID + 'zhuanqu_id', # 商品专区ID + 'tupian_url', # 商品图片URL + 'duiwai_xiaoliang' # 商品对外销量 + ) + + # 12. 构造返回数据 + response_data = { + 'code': 0, + 'message': '获取成功', + 'data': { + 'shangpingonggao': shangpingonggao, # 商品公告 + 'shangpinlunbo': shangpinlunbo, # 轮播图相对URL数组 + 'shangpinleixing': list(shangpinleixing_data), # 商品类型列表 + 'shangpinzhuanqu': list(shangpinzhuanqu_data), # 商品专区列表 + 'huiyuanliebiao': list(huiyuan_data), # 会员列表 + 'shangpinliebiao': list(shangpin_data), # 商品列表 + } + } + + return Response(response_data, status=status.HTTP_200_OK) + + except Exception as e: + print(f"获取商品管理数据异常: {e}") + return Response( + {'code': 500, 'message': '服务器内部错误'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + +# shangpin/views.py + +class AdGonggaoXiuGai(APIView): + """ + 后台编辑公告接口 + 路径: /shangpin/adggxg/ + 方法: POST + 权限: JWT认证 + 管理员权限 + 功能: 修改公告内容 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 获取前端传递的账号 + qianzhanghao = request.data.get('zhanghao', '').strip() + if not qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 2. 从JWT Token获取当前用户 + dangqianyonghu = request.user + + # 3. 验证账号匹配 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 验证用户类型为管理员 + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 5. 验证管理员扩展表数据 + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 6. 获取公告内容 + gonggao_neirong = request.data.get('gonggao', '').strip() + + # 7. 验证公告内容 + if not gonggao_neirong: + return Response( + {'code': 400, 'message': '公告内容不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if len(gonggao_neirong) > 300: + return Response( + {'code': 400, 'message': '公告内容不能超过300字', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 8. 查询公告表,类型为1的公告 + gonggao_obj = Gonggao.query.filter(leixing=1).first() + + # 如果不存在,则创建 + if not gonggao_obj: + gonggao_obj = Gonggao(leixing=1) + + # 9. 更新公告内容 + gonggao_obj.jieshao = gonggao_neirong + gonggao_obj.save() + + # 10. 返回成功响应 + return Response({ + 'code': 0, + 'message': '公告更新成功', + 'data': { + 'gonggao': gonggao_neirong + } + }) + + except Exception as e: + print(f"编辑公告异常: {e}") + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +class AdZhuanquXiuGai(APIView): + """ + 后台修改专区名称接口 + 路径: /shangpin/adzqxg/ + 方法: POST + 权限: JWT认证 + 管理员权限 + 功能: 修改专区名称 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 1. 权限验证 + qianzhanghao = request.data.get('zhanghao', '').strip() + if not qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + dangqianyonghu = request.user + + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 2. 获取专区ID和新的专区名称 + zhuanqu_id = request.data.get('zhuanqu_id') + mingzi = request.data.get('mingzi', '').strip() + + if not zhuanqu_id: + return Response( + {'code': 400, 'message': '专区ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not mingzi: + return Response( + {'code': 400, 'message': '专区名称不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + if len(mingzi) > 10: + return Response( + {'code': 400, 'message': '专区名称不能大于10个字', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 3. 查询专区 + try: + zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id) + except ShangpinZhuanqu.DoesNotExist: + return Response( + {'code': 400, 'message': '专区不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 4. 更新专区名称 + zhuanqu_obj.mingzi = mingzi + zhuanqu_obj.save() + + # 5. 返回成功响应 + return Response({ + 'code': 0, + 'message': '专区名称修改成功', + 'data': { + 'id': zhuanqu_obj.id, + 'mingzi': zhuanqu_obj.mingzi, + 'leixing_id': zhuanqu_obj.leixing_id + } + }) + + except Exception as e: + print(f"修改专区名称异常: {e}") + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + + +class AdLunboXiuGai(APIView): + """ + 后台编辑轮播图接口 + 路径: /shangpin/adlunboxg/ + 方法: POST + 权限: JWT认证 + 管理员权限 + 功能: 添加或删除轮播图 + """ + + permission_classes = [IsAuthenticated] + parser_classes = (MultiPartParser, FormParser,JSONParser) + + def post(self, request): + try: + # 1. 权限验证 + qianzhanghao = request.data.get('zhanghao', '').strip() + if not qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + dangqianyonghu = request.user + + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'message': '身份验证失败'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 2. 获取操作类型 + action = request.data.get('action', '').strip() # 'add' 或 'delete' + + if action not in ['add', 'delete']: + return Response( + {'code': 400, 'message': '操作类型无效', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 3. 处理删除操作 + if action == 'delete': + tupian_url = request.data.get('tupian_url', '').strip() + + if not tupian_url: + return Response( + {'code': 400, 'message': '图片URL不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 查询轮播图记录 + lunbo_obj = Lunbo.query.filter(tupian_url=tupian_url, leixing=1).first() + + if not lunbo_obj: + return Response( + {'code': 400, 'message': '轮播图不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 从OSS删除图片 + delete_success = delete_from_oss(tupian_url) + + if not delete_success: + return Response( + {'code': 500, 'message': 'OSS删除失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 从数据库删除记录 + lunbo_obj.delete() + + return Response({ + 'code': 0, + 'message': '删除成功', + 'data': None + }) + + # 4. 处理添加操作 + else: # action == 'add' + tupian_file = request.FILES.get('file') + + if not tupian_file: + + + return Response( + {'code': 400, 'message': '请上传图片', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证图片 + is_valid, error_msg = validate_image(tupian_file) + if not is_valid: + #print(f"[轮播图上传] 图片验证失败详情: {error_msg}") + # 还可以记录文件大小、名称等信息 + #print(f"[轮播图上传] 问题文件: {tupian_file.name}, Size: {tupian_file.size}") + #print(is_valid) + return Response( + {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 生成文件名和路径 + file_extension = os.path.splitext(tupian_file.name)[1].lower() + if not file_extension: + file_extension = '.jpg' + + # 生成6位随机字符串作为文件名 + random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + unique_filename = f"{random_str}{file_extension}" + + # 构建OSS文件路径:a_long/lunbo/{filename} + oss_folder = 'a_long' + lunbo_folder = 'lunbo' + + oss_file_path = f"{oss_folder}/{lunbo_folder}/{unique_filename}" + + # 上传到OSS + tupian_url = upload_to_oss(tupian_file, oss_file_path) + + if not tupian_url: + return Response( + {'code': 500, 'message': '图片上传失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 创建轮播图记录 + lunbo_obj = Lunbo.query.create( + tupian_url=oss_file_path, # 存相对路径 + leixing=1 + ) + + return Response({ + 'code': 0, + 'message': '添加成功', + 'data': { + 'tupian_url': lunbo_obj.tupian_url + } + }) + + except Exception as e: + #print(f"编辑轮播图异常: {e}") + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + + + +class AdLeixingXiuGai(APIView): + """ + 后台编辑商品类型接口 - 优化版 + 支持:只改图片、只改字段、两者都改 + 拒绝:两者都不改 + """ + + permission_classes = [IsAuthenticated] + parser_classes = (MultiPartParser, FormParser, JSONParser) # 添加JSONParser支持 + + def post(self, request): + try: + # 1. 权限验证(保持不变) + qianzhanghao = request.data.get('zhanghao', '').strip() + if not qianzhanghao: + return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + dangqianyonghu = request.user + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + if dangqianyonghu.user_type != 'admin': + return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) + + # 2. 获取商品类型ID + leixing_id = request.data.get('leixing_id') + if not leixing_id: + return Response({'code': 400, 'message': '商品类型ID不能为空', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 3. 查询商品类型 + try: + leixing_obj = ShangpinLeixing.query.get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 400, 'message': '商品类型不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 4. 获取字段并初始化变更标记 + has_changed = False + jieshao = request.data.get('jieshao', '').strip() + yaoqiuleixing = request.data.get('yaoqiuleixing') + huiyuan_id = request.data.get('huiyuan_id', '').strip() + yongjin = request.data.get('yongjin', '').strip() + tupian_url = request.data.get('tupian_url', '').strip() # 原有图片URL + + # 5. 检查非图片字段是否有变更 + if jieshao and jieshao != leixing_obj.jieshao: + leixing_obj.jieshao = jieshao + has_changed = True + + if yaoqiuleixing and int(yaoqiuleixing) != leixing_obj.yaoqiuleixing: + leixing_obj.yaoqiuleixing = int(yaoqiuleixing) + has_changed = True + + # 根据类型清空另一个字段 + if leixing_obj.yaoqiuleixing == 1: + leixing_obj.yongjin = None + if huiyuan_id: + leixing_obj.huiyuan_id = huiyuan_id + # 验证会员存在 + try: + Huiyuan.query.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + else: # 类型2 + leixing_obj.huiyuan_id = None + if yongjin: + try: + leixing_obj.yongjin = float(yongjin) + if leixing_obj.yongjin <= 0: + return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + except ValueError: + return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # ===== 在现有代码的第5步后面添加 ===== + + # 5.5 检查抢单要求类型不变时,会员ID或佣金是否有变更 + if yaoqiuleixing and int(yaoqiuleixing) == leixing_obj.yaoqiuleixing: + # 类型没有变化,但需要检查对应字段的变化 + + # 情况1:会员抢单 (yaoqiuleixing == 1) + if leixing_obj.yaoqiuleixing == 1: + # 检查会员ID是否变更 + if huiyuan_id and huiyuan_id != leixing_obj.huiyuan_id: + # 验证会员存在 + try: + Huiyuan.query.get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + leixing_obj.huiyuan_id = huiyuan_id + has_changed = True + # print(f"会员ID变更: {leixing_obj.huiyuan_id} -> {huiyuan_id}") + + # 情况2:佣金抢单 (yaoqiuleixing == 2) + elif leixing_obj.yaoqiuleixing == 2: + # 检查佣金是否变更 + if yongjin: + try: + new_yongjin = float(yongjin) + if new_yongjin <= 0: + return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 判断是否有变化(考虑None的情况) + current_yongjin = leixing_obj.yongjin + if (current_yongjin is None and new_yongjin != 0) or \ + (current_yongjin is not None and abs( + float(current_yongjin) - new_yongjin) > 0.001): + leixing_obj.yongjin = new_yongjin + has_changed = True + # print(f"佣金变更: {current_yongjin} -> {new_yongjin}") + + except ValueError: + return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # ===== 添加结束 ===== + + # 6. 处理图片上传(如果有新图片) + tupian_file = request.FILES.get('file') + if tupian_file: + # 验证图片 + is_valid, error_msg = validate_image(tupian_file) + if not is_valid: + return Response({'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST) + + # 删除旧图片(如果存在) + if leixing_obj.tupian_url: + delete_from_oss(leixing_obj.tupian_url) + + # 生成文件名和路径 + file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' + oss_folder = 'a_long' + leixing_folder = 'leixing' + + # 使用原文件名或生成新文件名 + if leixing_obj.tupian_url: + original_filename = leixing_obj.tupian_url.split('/')[-1] + else: + original_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}" + + oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{original_filename}" + + # 上传到OSS + new_tupian_url = upload_to_oss(tupian_file, oss_file_path) + if not new_tupian_url: + return Response({'code': 500, 'message': '图片上传失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + leixing_obj.tupian_url = oss_file_path # 存相对路径 + has_changed = True + elif tupian_url and tupian_url != leixing_obj.tupian_url: + # 前端传了新URL(可能是从其他地方选择的) + leixing_obj.tupian_url = tupian_url + has_changed = True + + # 7. 验证是否至少有变更 + if not has_changed: + return Response({ + 'code': 400, + 'message': '没有检测到任何更改', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 8. 保存并返回 + leixing_obj.save() + # 8. 保存商品类型 + leixing_obj.save() + + # 🔴 ===========【新增:同步更新关联商品】=========== + try: + # 1. 准备商品更新字典 + shangpin_update_fields = {} + + # 始终同步抢单要求类型 + shangpin_update_fields['yaoqiuleixing'] = leixing_obj.yaoqiuleixing + + # 根据抢单类型,同步对应字段并清空另一个 + if leixing_obj.yaoqiuleixing == 1: # 会员抢单 + if leixing_obj.huiyuan_id: # 仅当会员ID非空时更新 + shangpin_update_fields['huiyuan_id'] = leixing_obj.huiyuan_id + # 清空佣金字段 + shangpin_update_fields['yongjin'] = None + elif leixing_obj.yaoqiuleixing == 2: # 佣金抢单 + if leixing_obj.yongjin: # 仅当佣金非空时更新 + shangpin_update_fields['yongjin'] = leixing_obj.yongjin + # 清空会员字段 + shangpin_update_fields['huiyuan_id'] = None + + # 2. 执行批量更新(确保只有实际有变化的字段才会执行) + if shangpin_update_fields: + # 查询所有属于该类型的商品(使用filter而非all以提高效率) + affected_count = Shangpin.query.filter( + leixing_id=leixing_obj.id + ).update(**shangpin_update_fields) + + # 可选:记录日志以便追踪 + # logger.info(f"商品类型{leixing_obj.id}修改成功,已同步更新{affected_count}个关联商品") + + except Exception as sync_error: + # 商品同步失败不影响商品类型本身的修改,但记录错误 + # 可根据需求决定是否抛出异常回滚整个事务 + # 当前设计:记录错误但允许商品类型修改成功 + logger.error(f"修改十五") + + # 可在此添加更详细的错误日志记录 + # 🔴 ===========【新增结束】=========== + + + + + + + + response_data = { + 'id': leixing_obj.id, + 'jieshao': leixing_obj.jieshao, + 'tupian_url': leixing_obj.tupian_url, + 'yaoqiuleixing': leixing_obj.yaoqiuleixing, + 'huiyuan_id': leixing_obj.huiyuan_id, + 'yongjin': str(leixing_obj.yongjin) if leixing_obj.yongjin else None + } + + return Response({ + 'code': 0, + 'message': '商品类型修改成功', + 'data': response_data + }) + + except Exception as e: + print(f"编辑商品类型异常: {e}") + return Response({'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +logger = logging.getLogger(__name__) + + +class AdShangpinNeiRongHuoQu(APIView): + """ + 后台获取商品详情接口 + 路径: /shangpin/adspnrhq/ + 方法: POST + 权限: JWT认证 + 管理员权限 + 功能: 获取指定商品的详细信息 + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + # 1. 获取前端数据 + qianzhanghao = request.data.get('zhanghao', '').strip() + shangpin_id = request.data.get('shangpin_id') + + if not qianzhanghao or not shangpin_id: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 2. 权限验证 + dangqianyonghu = request.user + + # 验证phone字段匹配 + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + logger.warning( + f"账号不匹配: 当前用户phone={getattr(dangqianyonghu, 'phone', '无')}, 前端账号={qianzhanghao}") + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 验证用户类型 + if dangqianyonghu.user_type != 'admin': + logger.warning(f"非管理员访问: user_type={dangqianyonghu.user_type}") + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 3. 验证管理员扩展表 + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + logger.warning(f"管理员扩展表不存在: user_id={dangqianyonghu.id}") + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 4. 查询商品数据 + try: + shangpin_obj = Shangpin.query.get(id=shangpin_id) + except Shangpin.DoesNotExist: + return Response( + {'code': 404, 'message': '商品不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 5. 构建返回数据(与前端字段对应) + response_data = { + 'tupian_url': shangpin_obj.tupian_url or '', # 商品图片相对URL + 'biaoti': shangpin_obj.biaoqian or '', # 商品标题(前端用biaoti,对应biaoqian) + 'jieshao': shangpin_obj.jieshao or '', # 商品介绍 + 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', # 商品价格 + 'kucun': shangpin_obj.kucun, # 商品库存 + 'zhenshixiaoliang': shangpin_obj.zhenshi_xiaoliang, # 商品真实销量 + 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, # 商品对外销量 + 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', # 下单须知 + 'guizetupian_url': shangpin_obj.guize_tupian or '', # 规则图片相对URL + 'yajin': str(shangpin_obj.yajin) if hasattr(shangpin_obj, 'yajin') and shangpin_obj.yajin else None, + # 押金(可能不存在) + 'chuangjianshijian': shangpin_obj.create_time.strftime( + '%Y-%m-%d %H:%M:%S') if shangpin_obj.create_time else '', + 'genggaishijian': shangpin_obj.update_time.strftime( + '%Y-%m-%d %H:%M:%S') if shangpin_obj.update_time else '', + # 额外字段,前端可能不需要但可以保留 + 'leixing_id': shangpin_obj.leixing_id, + 'zhuanqu_id': shangpin_obj.zhuanqu_id, + 'yaoqiuleixing': shangpin_obj.yaoqiuleixing, + 'huiyuan_id': shangpin_obj.huiyuan_id, + 'yongjin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None, + } + + return Response({ + 'code': 0, + 'message': '获取成功', + 'data': response_data + }) + + except Exception as e: + logger.error(f"获取商品详情异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + + + + + + +class AdShangpinXiuGai(APIView): + """ + 后台更改商品数据接口 - 修复版 + 修复:1. 每次上传生成新文件名 2. 解决浏览器缓存问题 + """ + + permission_classes = [IsAuthenticated] + parser_classes = (MultiPartParser, FormParser, JSONParser) + + def post(self, request): + try: + # 1. 权限验证 + qianzhanghao = request.data.get('zhanghao', '').strip() + shangpin_id = request.data.get('shangpin_id') + + if not qianzhanghao or not shangpin_id: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证身份 + dangqianyonghu = request.user + + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 2. 判断操作类型:删除还是修改 + action = request.data.get('action', 'update').strip().lower() + + if action == 'delete': + return self._delete_shangpin(shangpin_id) + else: + return self._update_shangpin(request, shangpin_id) + + except Exception as e: + logger.error(f"更改商品数据异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + def _delete_shangpin(self, shangpin_id): + """删除商品逻辑""" + try: + # 查询商品 + try: + shangpin_obj = Shangpin.query.get(id=shangpin_id) + except Shangpin.DoesNotExist: + return Response( + {'code': 404, 'message': '商品不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 获取图片URL(如果有) + tupian_url = shangpin_obj.tupian_url + guizetupian_url = shangpin_obj.guize_tupian + + # 异步或同步删除OSS图片 + if tupian_url: + try: + delete_from_oss(tupian_url) + except Exception as e: + logger.warning(f"删除商品图片失败: {tupian_url}, 错误: {e}") + + if guizetupian_url: + try: + delete_from_oss(guizetupian_url) + except Exception as e: + logger.warning(f"删除规则图片失败: {guizetupian_url}, 错误: {e}") + + # 删除数据库记录 + shangpin_obj.delete() + + return Response({ + 'code': 0, + 'message': '删除成功', + 'data': { + 'shangpin_id': shangpin_id, + 'deleted': True + } + }) + + except Exception as e: + logger.error(f"删除商品异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '删除失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + def _update_shangpin(self, request, shangpin_id): + """修改商品逻辑 - 修复:每次上传生成新文件名""" + try: + # 查询商品 + try: + shangpin_obj = Shangpin.query.get(id=shangpin_id) + except Shangpin.DoesNotExist: + return Response( + {'code': 404, 'message': '商品不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + has_changes = False + + # 获取原图片URL(如果有) + old_tupian_url = shangpin_obj.tupian_url + + # 3. 处理商品图片上传(如果有新商品图片) + tupian_file = request.FILES.get('tupian_file') + new_tupian_url = None + + if tupian_file: + # 验证图片 + is_valid, error_msg = validate_image(tupian_file) + if not is_valid: + return Response( + {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 生成新文件名:时间戳 + 随机数 + 原扩展名 + timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳 + random_num = random.randint(1000, 9999) + file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg' + new_filename = f"sp_{timestamp}_{random_num}{file_extension}" + + # 确定OSS路径 + oss_folder = 'a_long' + shangpin_folder = 'shangpin' + oss_file_path = f"{oss_folder}/{shangpin_folder}/{new_filename}" + + # 删除旧图片(如果存在) + if old_tupian_url: + try: + delete_from_oss(old_tupian_url) + logger.info(f"已删除旧商品图片: {old_tupian_url}") + except Exception as e: + logger.warning(f"删除旧商品图片失败: {old_tupian_url}, 错误: {e}") + # 继续上传新图片,不中断流程 + + # 上传到OSS + new_tupian_url = upload_to_oss(tupian_file, oss_file_path) + + if not new_tupian_url: + return Response( + {'code': 500, 'message': '商品图片上传失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 更新数据库中的图片URL + shangpin_obj.tupian_url = oss_file_path # 存相对路径 + has_changes = True + logger.info(f"商品图片更新成功: {oss_file_path}") + + # 4. 处理其他字段(只更新有变化的字段) + # 注意:这里同时处理 request.data(文本字段)和可能有文件的情况 + field_mapping = { + 'biaoti': 'biaoqian', + 'jieshao': 'jieshao', + 'jiage': 'jiage', + 'kucun': 'kucun', + 'duiwaixiaoliang': 'duiwai_xiaoliang', + 'xiadanxuzhi': 'xiadan_xuzhi', + 'yajin': 'yajin', + } + + for front_field, db_field in field_mapping.items(): + # 注意:request.data 可能包含文件和其他字段,但DRF会处理好 + if front_field in request.data: + new_value = request.data.get(front_field) + + # 空字符串处理 + if new_value == '': + new_value = None + + if new_value is not None: + # 获取当前值 + current_value = getattr(shangpin_obj, db_field) + + # 类型转换 + try: + if db_field in ['jiage', 'yajin']: + # 价格和押金 + if new_value: + new_value = Decimal(str(new_value)) + else: + new_value = None + elif db_field in ['kucun', 'duiwai_xiaoliang']: + # 库存和对外销量 + if new_value: + new_value = int(float(new_value)) + else: + new_value = 0 + except (ValueError, TypeError, InvalidOperation) as e: + logger.warning(f"字段{front_field}转换失败: {new_value}, 错误: {e}") + continue + + # 检查是否有变化(处理None值) + if current_value != new_value: + setattr(shangpin_obj, db_field, new_value) + has_changes = True + + # 5. 如果没有检测到任何更改 + if not has_changes: + return Response({ + 'code': 400, + 'message': '没有检测到任何更改', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 6. 保存更改 + shangpin_obj.save() + + # 7. 返回更新后的数据 + response_data = { + 'tupian_url': shangpin_obj.tupian_url or '', + 'biaoti': shangpin_obj.biaoqian or '', + 'jieshao': shangpin_obj.jieshao or '', + 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', + 'kucun': shangpin_obj.kucun, + 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, + 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', + 'yajin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None, + 'genggaishijian': shangpin_obj.update_time.strftime( + '%Y-%m-%d %H:%M:%S') if shangpin_obj.update_time else '', + } + + return Response({ + 'code': 0, + 'message': '更新成功', + 'data': response_data + }) + + except Exception as e: + logger.error(f"更新商品异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '更新失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +class AdShangpinGuizeXiuGai(APIView): + """ + 后台规则图片更换接口 - 修复版 + 修复:每次上传生成新文件名,避免浏览器缓存 + """ + + permission_classes = [IsAuthenticated] + parser_classes = (MultiPartParser, FormParser, JSONParser) + + def post(self, request): + try: + # 1. 权限验证 + qianzhanghao = request.data.get('zhanghao', '').strip() + shangpin_id = request.data.get('shangpin_id') + action = request.data.get('action', 'update').strip().lower() + + if not qianzhanghao or not shangpin_id: + return Response( + {'code': 400, 'message': '参数不完整', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证身份 + dangqianyonghu = request.user + + if not hasattr(dangqianyonghu, 'phone') or str(dangqianyonghu.phone) != qianzhanghao: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + if dangqianyonghu.user_type != 'admin': + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + try: + guanliyuan = dangqianyonghu.admin_profile + except AdminProfile.DoesNotExist: + return Response( + {'code': 401, 'message': '身份验证失败', 'data': None}, + status=status.HTTP_401_UNAUTHORIZED + ) + + # 2. 查询商品 + try: + shangpin_obj = Shangpin.query.get(id=shangpin_id) + except Shangpin.DoesNotExist: + return Response( + {'code': 404, 'message': '商品不存在', 'data': None}, + status=status.HTTP_404_NOT_FOUND + ) + + # 3. 处理不同操作 + if action == 'delete': + return self._delete_guize_image(shangpin_obj) + elif action == 'update': + return self._update_guize_image(request, shangpin_obj) + else: + return Response( + {'code': 400, 'message': '操作类型无效', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + except Exception as e: + logger.error(f"规则图片操作异常: {str(e)}", exc_info=True) + return Response( + {'code': 500, 'message': '服务器内部错误', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + def _delete_guize_image(self, shangpin_obj): + """删除规则图片""" + guizetupian_url = shangpin_obj.guize_tupian + + if guizetupian_url: + # 从OSS删除 + try: + delete_success = delete_from_oss(guizetupian_url) + '''if delete_success: + logger.info(f"已删除规则图片: {guizetupian_url}") + else: + logger.warning(f"OSS删除失败,但继续删除数据库记录: {guizetupian_url}")''' + except Exception as e: + logger.warning(f"删除规则图片异常: {e}") + + # 清除数据库记录 + shangpin_obj.guize_tupian = '' + shangpin_obj.save() + + return Response({ + 'code': 0, + 'message': '删除成功', + 'data': { + 'guizetupian_url': '' + } + }) + + def _update_guize_image(self, request, shangpin_obj): + """更新规则图片 - 修复:每次上传生成新文件名""" + guize_file = request.FILES.get('guize_file') + + if not guize_file: + return Response( + {'code': 400, 'message': '请上传规则图片', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 验证图片 + is_valid, error_msg = validate_image(guize_file) + if not is_valid: + return Response( + {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 获取原规则图片URL + old_guizetupian_url = shangpin_obj.guize_tupian + + # 生成新文件名:时间戳 + 随机数 + 原扩展名 + timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳 + random_num = random.randint(1000, 9999) + file_extension = os.path.splitext(guize_file.name)[1].lower() or '.jpg' + new_filename = f"gz_{timestamp}_{random_num}{file_extension}" + + # 确定OSS路径 + oss_folder = 'a_long' + guize_folder = 'guize' # 修改为你的要求:a_long/guize/ + oss_file_path = f"{oss_folder}/{guize_folder}/{new_filename}" + + # 删除旧图片(如果存在) + if old_guizetupian_url: + try: + delete_from_oss(old_guizetupian_url) + #logger.info(f"已删除旧规则图片: {old_guizetupian_url}") + except Exception as e: + logger.warning(f"删除旧规则图片失败: {old_guizetupian_url}, 错误: {e}") + # 继续上传新图片,不中断流程 + + # 上传到OSS + new_guizetupian_url = upload_to_oss(guize_file, oss_file_path) + + if not new_guizetupian_url: + return Response( + {'code': 500, 'message': '规则图片上传失败', 'data': None}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 更新数据库(存相对路径) + shangpin_obj.guize_tupian = oss_file_path + shangpin_obj.save() + + #logger.info(f"规则图片更新成功: {oss_file_path}") + + return Response({ + 'code': 0, + 'message': '规则图片更新成功', + 'data': { + 'guizetupian_url': oss_file_path # 返回相对路径 + } + }) + + + + +# views.py +class HuiyuanTypeListView(APIView): + """ + 获取会员类型列表接口 + 路径:/shangpin/adhyjllxcx + 方法:POST + 权限:JWT Token + 管理员验证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + # 🔥 1. 身份验证 + user = request.user + + # 检查是否是管理员 + if not hasattr(user, 'admin_profile'): + return Response({ + 'code': 401, + 'message': '无权限访问', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + # 🔥 2. 获取所有会员类型 + huiyuan_list = Huiyuan.query.all().order_by('create_time') + + # 🔥 3. 构建返回数据 + huiyuan_data = [] + for huiyuan in huiyuan_list: + huiyuan_data.append({ + 'id': huiyuan.huiyuan_id, # 会员ID + 'jieshao': huiyuan.jieshao, # 会员介绍 + 'jiage': str(huiyuan.jiage), # 价格 + 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数 + 'create_time': huiyuan.create_time.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.create_time 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, 'admin_profile'): + 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.create_time, + 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.create_time 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, + 'create_time': 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. 获取会员类型统计信息(所有会员类型的当前状态总数) + huiyuan_types = Huiyuan.query.all() + huiyuan_stats = [] + + for huiyuan in huiyuan_types: + # 使用COUNT查询统计 + stats_sql = """ + SELECT COUNT(*) + FROM huiyuangoumai + WHERE huiyuan_id = %s + """ + stats_params = [huiyuan.huiyuan_id] + + if jiluzhuangtai == 1: # 未过期 + stats_sql += " AND daoqi_time > %s" + stats_params.append(now) + else: # 已过期 + stats_sql += " AND daoqi_time <= %s" + stats_params.append(now) + + with connection.cursor() as cursor: + cursor.execute(stats_sql, stats_params) + count = cursor.fetchone()[0] + + huiyuan_stats.append({ + 'id': huiyuan.huiyuan_id, + 'jieshao': huiyuan.jieshao, + 'count': count + }) + + 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.yonghuid).count() + + # 3.3 分红次数(总记录数) + base_queryset = Gsfenhong.query.filter(zuzhang_id=current_user.yonghuid).order_by('-create_time') + 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'))) + + # 时间格式化 + create_time = rec.create_time.strftime('%Y-%m-%d %H:%M:%S') if rec.create_time 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, + 'create_time': create_time, + }) + + # 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( + 'boss_profile' + ).only( + 'yonghuid', 'avatar', + 'boss_profile__nickname' + ) + + info = {} + for user in users: + avatar = user.avatar or '' + # 从 boss_profile 获取昵称 + if hasattr(user, 'boss_profile') and user.boss_profile: + nicheng = user.boss_profile.nickname or '' + else: + # 如果没有 boss_profile,使用默认昵称 + nicheng = f"用户{user.yonghuid[-4:]}" + info[user.yonghuid] = {'avatar': avatar, 'nicheng': nicheng} + + # 对于未查询到的用户,提供默认值 + for gid in guanshi_ids: + if gid not in info: + info[gid] = {'avatar': '', 'nicheng': f"用户{gid[-4:]}"} + + return info + + + +# 常量定义(可根据需要调整) +INTEGRAL_PRICE = Decimal('5.00') # 积分购买固定价格(元) + + +class CzhqdyView(APIView): + """ + 获取可用抵扣身份接口 + 路径:/shangpin/czhqdy + 方法:POST + 权限:JWT认证 + 请求参数: + leixing: int # 1=会员,2=押金,3=积分,4=罚款 + huiyuan_id: str # 当 leixing=1 时必传,会员ID + yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元) + fadan_id: int # 当 leixing=4 时必传,罚单ID + 返回: + { + "code": 200, + "msg": "success", + "data": [ + { + "id": 1, # 身份ID:1打手佣金 2管事分红 3组长分红 4打手押金 + "jieshao": "打手佣金抵扣", + "xuyao": "22.22" # 所需抵扣金额(保留两位小数) + }, + ... + ] + } + """ + permission_classes = [permissions.IsAuthenticated] + + # 身份ID与对应扩展表模型、余额字段、费率查询条件的映射 + IDENTITY_CONFIG = [ + { + 'id': 1, + 'name': '打手佣金抵扣', + 'profile_attr': 'dashou_profile', # User 的反向 related_name + 'balance_field': 'yue', + 'rate_key': '5', + 'model': UserDashou, + }, + { + 'id': 2, + 'name': '管事分红抵扣', + 'profile_attr': 'guanshi_profile', + 'balance_field': 'yue', + 'rate_key': '6', + 'model': UserGuanshi, + }, + { + 'id': 3, + 'name': '组长分红抵扣', + 'profile_attr': 'zuzhang_profile', + 'balance_field': 'ketixian_jine', + 'rate_key': '8', + 'model': UserZuzhang, + }, + # 新增打手押金抵扣 + { + 'id': 4, + 'name': '打手押金抵扣', + 'profile_attr': 'dashou_profile', # 与打手佣金共用扩展表 + 'balance_field': 'yajin', # 押金字段 + 'rate_key': '11', # 押金提现费率对应的 fadanpingtai + 'model': UserDashou, + }, + ] + + def post(self, request): + # 获取参数 + leixing = request.data.get('leixing') + huiyuan_id = request.data.get('huiyuan_id') + yajin_jine = request.data.get('yajin_jine') + fadan_id = request.data.get('fadan_id') + + # 参数校验 + if leixing not in [1, 2, 3, 4]: + return Response({'code': 400, 'msg': 'leixing 必须为 1,2,3,4'}, status=status.HTTP_400_BAD_REQUEST) + + if leixing == 1 and not huiyuan_id: + return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST) + + if leixing == 2: + if not yajin_jine: + return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST) + try: + yajin_jine = Decimal(str(yajin_jine)) + if yajin_jine <= 0: + raise ValueError + except: + return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST) + + if leixing == 4 and not fadan_id: + return Response({'code': 400, 'msg': '缴纳罚款时必须提供 fadan_id'}, status=status.HTTP_400_BAD_REQUEST) + + # 获取当前用户 + user = request.user + + # 获取目标价格(需支付的原价) + target_price = None + if leixing == 1: + # 会员 + try: + huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) + target_price = huiyuan.jiage + except Huiyuan.DoesNotExist: + return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND) + elif leixing == 2: + # 押金 + target_price = yajin_jine + elif leixing == 3: + # 积分(固定价格) + target_price = INTEGRAL_PRICE + elif leixing == 4: + # 罚款缴纳 + try: + fadan = Fadan.query.get(id=fadan_id, beichufa_id=user.yonghuid) + target_price = fadan.fakuanjine + except Fadan.DoesNotExist: + return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) + + # 获取所有费率(打手佣金、管事分红、组长分红、打手押金) + rates = {} + for key in ['5', '6', '8', '11']: + try: + rate_obj = Lilubiao.query.get(fadanpingtai=key) + rates[key] = rate_obj.lilu + except Lilubiao.DoesNotExist: + rates[key] = None # 未配置则跳过 + + # 构建可用身份列表 + available_list = [] + + for ident in self.IDENTITY_CONFIG: + # 检查用户是否有该扩展表 + profile = getattr(user, ident['profile_attr'], None) + if not profile: + continue + + # 获取余额 + balance = getattr(profile, ident['balance_field'], Decimal('0.00')) + + # 获取费率 + rate = rates.get(ident['rate_key']) + if rate is None: + continue # 费率未配置,跳过该身份 + + # 计算所需抵扣金额: target_price / (1 - rate) ,向上取整到分 + try: + if rate >= 1: + continue # 防止除零及不合理费率 + required = target_price / (Decimal('1.0') - rate) + # 向上取整到分(保留两位小数,最后一位进位) + required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING) + except Exception: + continue # 计算异常,跳过 + + # 余额是否足够 + if balance >= required: + available_list.append({ + 'id': ident['id'], + 'jieshao': ident['name'], + 'xuyao': str(required), + }) + + return Response({ + 'code': 200, + 'msg': 'success', + 'data': available_list + }) + + + +INTEGRAL_INCREMENT = 5 +MAX_INTEGRAL = 10 + +IDENTITY_CONFIG = { + 1: { + 'name': '打手佣金', + 'model': UserDashou, + 'balance_field': 'yue', + 'status_field': 'zhanghaozhuangtai', + 'rate_key': '5', + }, + 2: { + 'name': '管事分红', + 'model': UserGuanshi, + 'balance_field': 'yue', + 'status_field': 'zhuangtai', + 'rate_key': '6', + }, + 3: { + 'name': '组长分红', + 'model': UserZuzhang, + 'balance_field': 'ketixian_jine', + 'status_field': 'zhuangtai', + 'rate_key': '8', + }, + 4: { + 'name': '打手押金', + 'model': UserDashou, + 'balance_field': 'yajin', + 'status_field': 'zhanghaozhuangtai', + 'rate_key': '11', + }, +} + + +class DsqrgmdhView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + leixing = request.data.get('leixing') + shenfen_id = request.data.get('shenfen_id') + huiyuan_id = request.data.get('huiyuan_id') + yajin_jine = request.data.get('yajin_jine') + fadan_id = request.data.get('fadan_id') + + logger.info(f"抵扣支付请求: leixing={leixing}, shenfen_id={shenfen_id}, huiyuan_id={huiyuan_id}, yajin_jine={yajin_jine}, fadan_id={fadan_id}") + + if leixing not in [1, 2, 3, 4]: + return Response({'code': 400, 'msg': 'leixing 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST) + if shenfen_id not in [1, 2, 3, 4]: + return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST) + + if leixing == 1 and not huiyuan_id: + return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST) + if leixing == 2: + if not yajin_jine: + return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST) + try: + yajin_jine = Decimal(str(yajin_jine)) + if yajin_jine <= 0: + raise ValueError + if yajin_jine > Decimal('10000'): + return Response({'code': 400, 'msg': '押金金额不能超过10000元'}, status=status.HTTP_400_BAD_REQUEST) + except: + return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST) + + target_price = None + if leixing == 1: + try: + huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) + target_price = huiyuan.jiage + except Huiyuan.DoesNotExist: + return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND) + elif leixing == 2: + target_price = yajin_jine + elif leixing == 3: + target_price = INTEGRAL_PRICE + elif leixing == 4: + if not fadan_id: + return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) + try: + fadan_obj = Fadan.query.get(id=fadan_id, beichufa_id=request.user.yonghuid) + target_price = fadan_obj.fakuanjine + except Fadan.DoesNotExist: + return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) + + identity = IDENTITY_CONFIG.get(shenfen_id) + try: + rate_obj = Lilubiao.query.get(fadanpingtai=identity['rate_key']) + rate = rate_obj.lilu + if rate >= 1: + return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST) + except Lilubiao.DoesNotExist: + return Response({'code': 400, 'msg': f"{identity['name']}费率未配置"}, status=status.HTTP_400_BAD_REQUEST) + + logger.info(f"目标价格={target_price}, 费率={rate}") + + try: + required = target_price / (Decimal('1.0') - rate) + required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING) + logger.info(f"所需抵扣金额={required}") + except Exception as e: + logger.error(f"金额计算错误: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': f'金额计算错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + user = request.user + yonghuid = user.yonghuid + + try: + profile = identity['model'].query.get(user=user) + except identity['model'].DoesNotExist: + return Response({'code': 403, 'msg': f'您不是{identity["name"]}身份'}) + + status_value = getattr(profile, identity['status_field']) + if status_value != 1: + return Response({'code': 403, 'msg': f'{identity["name"]}账号状态异常,无法使用'}) + + balance = getattr(profile, identity['balance_field']) + if balance < required: + return Response({'code': 400, 'msg': f'{identity["name"]}余额不足,需要{required}元,当前{balance}元'}, status=status.HTTP_400_BAD_REQUEST) + + try: + with transaction.atomic(): + locked_profile = identity['model'].objects.select_for_update().get(id=profile.id) + current_balance = getattr(locked_profile, identity['balance_field']) + if current_balance < required: + raise ValueError(f"{identity['name']}余额不足,需要{required}元,当前{current_balance}元") + + setattr(locked_profile, identity['balance_field'], current_balance - required) + locked_profile.save(update_fields=[identity['balance_field']]) + logger.info(f"扣款成功: {identity['name']} 扣除 {required} 元,剩余 {current_balance - required} 元") + + timestamp = str(int(time.time() * 1000))[-12:] + rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4)) + dingdan_id = f"CZ{timestamp}{rand_str}" + + if leixing == 1: + action_desc = f"购买会员 {huiyuan.jieshao}" + elif leixing == 2: + action_desc = f"充值押金 {yajin_jine}元" + elif leixing == 3: + action_desc = "补充积分" + elif leixing == 4: + action_desc = f"缴纳罚款 {target_price}元" + + shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}元" + + czjilu = Czjilu.query.create( + dingdan_id=dingdan_id, + huiyuan_id=huiyuan_id if leixing == 1 else None, + zhuangtai=1, + yonghuid=yonghuid, + jine=required, + leixing=leixing, + shuoming=shuoming, + ) + + response_data = {} + + if leixing == 1: + huiyuan_record, is_first_buy = self._process_member_purchase(yonghuid, huiyuan_id, huiyuan) + response_data['huiyuan'] = { + 'huiyuanid': huiyuan_id, + 'huiyuanming': huiyuan.jieshao, + 'daoqi': huiyuan_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(huiyuan_record, 'daoqi_time') else '', + } + if is_first_buy: + try: + dashou = UserDashou.query.get(user=user) + dashou.jifen += 10 + dashou.save(update_fields=['jifen']) + response_data['jifen'] = dashou.jifen + except UserDashou.DoesNotExist: + pass + + elif leixing == 2: + try: + dashou = UserDashou.objects.select_for_update().get(user=user) + dashou.yajin += yajin_jine + dashou.save(update_fields=['yajin']) + response_data['yajin'] = dashou.yajin + except UserDashou.DoesNotExist: + return Response({'code': 500, 'msg': '打手信息不存在,无法充值押金'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + elif leixing == 4: + if fadan_obj.zhuangtai not in [1, 3]: + return Response({'code': 400, 'msg': '罚单状态不可缴纳'}, status=status.HTTP_400_BAD_REQUEST) + fadan_obj.zhuangtai = 2 + fadan_obj.save(update_fields=['zhuangtai']) + response_data['fadan_id'] = fadan_obj.id + response_data['fadan_status'] = fadan_obj.zhuangtai + + # ========== 修改点2: 使用最新的分红处理方法 ========== + success, msg = process_fadan_fenhong(fadan_obj.id) + if not success: + logger.error(f"罚单 {fadan_obj.id} 分红处理失败: {msg}") + else: + logger.info(f"罚单 {fadan_obj.id} 分红处理成功: {msg}") + + else: # leixing == 3 + try: + dashou = UserDashou.objects.select_for_update().get(user=user) + new_jifen = dashou.jifen + INTEGRAL_INCREMENT + dashou.jifen = min(new_jifen, MAX_INTEGRAL) + dashou.save(update_fields=['jifen']) + response_data['jifen'] = dashou.jifen + except UserDashou.DoesNotExist: + return Response({'code': 500, 'msg': '打手信息不存在,无法补充积分'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + except Exception as e: + logger.error(f"事务处理异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': f'支付处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + final_profile = identity['model'].query.get(user=user) + response_data['balance'] = str(getattr(final_profile, identity['balance_field'])) + + return Response({ + 'code': 200, + 'msg': 'success', + 'data': response_data, + }) + + except Exception as e: + logger.error(f"接口顶层异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': f'系统错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def _process_member_purchase(self, yonghuid, huiyuan_id, huiyuan_obj): + is_first_buy_any = False + try: + record = Huiyuangoumai.query.filter( + yonghu_id=yonghuid, + huiyuan_id=huiyuan_id + ).first() + + if record: + record.daoqi_time += timedelta(days=30) + record.huiyuan_zhuangtai = 1 + record.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai']) + else: + other_count = Huiyuangoumai.query.filter(yonghu_id=yonghuid).exclude(huiyuan_id=huiyuan_id).count() + if other_count == 0: + is_first_buy_any = True + record = Huiyuangoumai.query.create( + yonghu_id=yonghuid, + huiyuan_id=huiyuan_id, + jieshao=huiyuan_obj.jieshao, + huiyuan_zhuangtai=1, + daoqi_time=timezone.now() + timedelta(days=30) + ) + return record, is_first_buy_any + except Exception as e: + if 'unique' in str(e).lower() or 'duplicate' in str(e).lower(): + record = Huiyuangoumai.query.get(yonghu_id=yonghuid, huiyuan_id=huiyuan_id) + return record, False raise \ No newline at end of file diff --git a/rank/migrations/0001_initial.py b/rank/migrations/0001_initial.py index a418056..da2a971 100644 --- a/rank/migrations/0001_initial.py +++ b/rank/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.27 on 2026-06-17 16:31 +# Generated by Django 4.2.27 on 2026-06-17 21:18 from django.conf import settings from django.db import migrations, models diff --git a/rank/utils.py b/rank/utils.py index 4a66d07..b43a30c 100644 --- a/rank/utils.py +++ b/rank/utils.py @@ -1,407 +1,407 @@ -# 标准库 -import time -import random -import logging -from decimal import Decimal -# Django 内置 -from django.db.models import F, Max -# 项目模型 -## 当前dengji应用 -from .models import ( - Chenghao, KaoheCishuFeiyong, ShenheJilu, KaoheJiluFeiyong, - YonghuChenghao, KaoheguanBankuai, DingdanBiaoqian -) -## users应用 -from users.models import UserShenheguan, UserDashou -from gvsdsdk.models import User -## orders应用 -from .models import YonghuChenghao -# 日志对象 -logger = logging.getLogger(__name__) - - - -def check_dashou_biaoqian_required(order_id, dashou_yonghuid): - """ - 检查打手是否满足订单的标签需求。 - - 如果订单没有需求标签,直接放行。 - - 如果有,则要求打手至少拥有其中一个标签。 - 返回 (allowed: bool, message: str) - """ - try: - # 1. 获取订单的需求标签ID集合 - required_ids = set( - DingdanBiaoqian.objects.filter(dingdan_id=order_id) - .values_list('chenghao_id', flat=True) - ) - if not required_ids: - return True, '' - - # 2. 找到打手对应的用户记录 - try: - user = User.objects.get(yonghuid=dashou_yonghuid) - except User.DoesNotExist: - return False, '用户信息异常' - - # 3. 获取打手已拥有的标签ID集合(只取打手类型) - owned_ids = set( - YonghuChenghao.objects.filter( - yonghu=user, - chenghao__leixing='dashou' - ).values_list('chenghao_id', flat=True) - ) - - if not owned_ids: - return False, '您未拥有任何打手标签,无法抢此订单' - - # 4. 交集判断 - if required_ids.intersection(owned_ids): - return True, '' - else: - return False, '您的标签不符合此订单要求,无法抢单' - - except Exception as e: - # 标签校验失败不影响主流程,记录日志并保守放行(也可保守拒绝,按需求选择) - logger.error(f"标签校验异常: {e}", exc_info=True) - # 安全起见,如果校验出错,选择拒绝,避免越权 - return False, '订单标签校验异常,请稍后重试' - - - - - -def get_tag_fee(chenghao, cishu): - """ - 获取标签在指定考核次数下的费用。 - - 若有对应次数的配置,直接返回。 - - 若没有该次数,取该标签下已有最大次数的费用(作为后续次数的价格)。 - - 若该标签完全没有费用记录,返回 0.00(免费)。 - """ - fee_obj = KaoheCishuFeiyong.objects.filter(chenghao=chenghao, cishu=cishu).first() - if fee_obj: - return fee_obj.feiyong - - max_obj = KaoheCishuFeiyong.objects.filter(chenghao=chenghao).order_by('-cishu').first() - if max_obj: - return max_obj.feiyong - - return Decimal('0.00') - - -def generate_unique_jilu_id(): - """ - 生成唯一的审核记录 ID,格式:SH + 时间戳(10位) + 随机数(4位) - 循环检查数据库避免重复,最多尝试 10 次。 - """ - for _ in range(10): - timestamp = str(int(time.time() * 1000))[-10:] - random_part = str(random.randint(1000, 9999)) - jilu_id = f'SH{timestamp}{random_part}' - if not ShenheJilu.objects.filter(jilu_id=jilu_id).exists(): - return jilu_id - raise Exception('无法生成唯一记录ID,请稍后重试') - - -def validate_shenheguan(shenheguan_id, bankuai, fee, applicant_user): - """ - 校验指定考核官是否合法 - - 考核官必须存在且未被禁用 - - 必须属于对应板块 - - 必须处于空闲状态 - - 若费用>0,则要求已认证 - 返回 (valid, error_msg) - """ - if not shenheguan_id: - return True, '' - try: - kg = UserShenheguan.objects.select_related('user').get( - user__yonghuid=shenheguan_id, - zhuangtai=1 - ) - except UserShenheguan.DoesNotExist: - return False, '考核官不存在或已被禁用' - - # 板块权限 - if not KaoheguanBankuai.objects.filter(kaoheguan_id=shenheguan_id, bankuai=bankuai).exists(): - return False, '该考核官不属于此板块' - - # 空闲状态 - if kg.shenhe_zhuangtai != 0: - return False, '该考核官当前忙碌,无法指定' - - # 费用>0需要认证 - if float(fee) > 0 and not kg.is_renzheng: - return False, '费用大于0时只能指定已认证的考核官' - - return True, '' - - -def create_shenhe_jilu(user, tag_id, game_nick, remark, reapply, jilu_id=None, shenheguan_id=''): - """ - 创建或更新审核记录。 - - 首次申请:创建新记录,生成唯一 jilu_id。 - - 再次申请:基于原记录(必须状态为未通过)更新次数、累加总费用、更新备注和游戏昵称, - 保留审核官信息,状态重置为待审核。不创建新记录。 - - 每次申请均创建一条费用明细记录(KaoheJiluFeiyong),记录本次缴费。 - - 若原记录有考核官,则将其状态改为审核中。 - - 新增 shenheguan_id 参数:指定考核官ID,仅在原记录无考核官时可传入。 - 返回 (success, message, record) - """ - try: - # 1. 校验标签存在 - try: - chenghao = Chenghao.objects.get(id=tag_id) - except Chenghao.DoesNotExist: - return False, '标签不存在', None - - # 2. 判断用户是否已拥有该标签 - if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): - return False, '您已拥有该标签,无需重复考核', None - - # 3. 检查是否有进行中的审核记录(待审核、审核中) - has_pending = ShenheJilu.objects.filter( - shenqingren_id=user.yonghuid, - chenghao=chenghao, - zhuangtai__in=[0, 3] - ).exists() - if has_pending: - return False, '您有未完成的考核申请,请等待结果', None - - last_record = None - current_fee = Decimal('0.00') - - # 4. 处理再次申请 - if reapply and jilu_id: - # 查询原记录(未通过状态) - last_record = ShenheJilu.objects.filter( - jilu_id=jilu_id, - shenqingren_id=user.yonghuid, - zhuangtai=2 - ).first() - if not last_record: - return False, '原记录不存在或状态不是未通过', None - - new_cishu = last_record.cishu + 1 - current_fee = get_tag_fee(chenghao, new_cishu) - - # 确定最终状态:有考核官 = 审核中(0),否则待审核(3) - final_kg_id = last_record.shenheguan_id # 保留原来的 - if shenheguan_id and not last_record.shenheguan_id: - final_kg_id = shenheguan_id # 新指定 - - final_zhuangtai = 0 if final_kg_id else 3 # 有考核官就是审核中 - if final_zhuangtai == 3: - final_kg_id = None - - # 原子化更新 - ShenheJilu.objects.filter(pk=last_record.pk).update( - cishu=new_cishu, - jiaofei_jine=F('jiaofei_jine') + current_fee, - dashou_youxi_id=game_nick, - dashou_beizhu=remark, - guize_neirong=chenghao.kaohe_guize or '', - zhuangtai=final_zhuangtai, - shenheguan_id=final_kg_id, - ) - last_record.refresh_from_db() - record = last_record - - # 如果原考核官被替换或清除,释放旧考核官 - if last_record.shenheguan_id != final_kg_id: - if last_record.shenheguan_id: - UserShenheguan.objects.filter( - user__yonghuid=last_record.shenheguan_id - ).update(shenhe_zhuangtai=0) - else: - # 首次申请 - max_cishu = ShenheJilu.objects.filter( - shenqingren_id=user.yonghuid, - chenghao=chenghao, - zhuangtai__in=[1, 2] - ).aggregate(max=Max('cishu'))['max'] or 0 - new_cishu = max_cishu + 1 - current_fee = get_tag_fee(chenghao, new_cishu) - - bankuai = chenghao.bankuai - rule_text = chenghao.kaohe_guize or '' - new_jilu_id = generate_unique_jilu_id() - - initial_zhuangtai = 0 if shenheguan_id else 3 # 指定→审核中,否则待审核 - record = ShenheJilu.objects.create( - jilu_id=new_jilu_id, - shenqingren_id=user.yonghuid, - zhuangtai=initial_zhuangtai, - bankuai=bankuai, - chenghao=chenghao, - cishu=new_cishu, - jiaofei_jine=current_fee, - dashou_youxi_id=game_nick, - dashou_beizhu=remark, - guize_neirong=rule_text, - shenheguan_id=shenheguan_id if shenheguan_id else None, - ) - - # 5. 创建费用明细(防重复) - KaoheJiluFeiyong.objects.get_or_create( - jilu=record, - cishu=record.cishu, - defaults={'jine': current_fee} - ) - - # 6. 更新考核官统计(仅当状态为审核中且有考核官) - if record.zhuangtai == 0 and record.shenheguan_id: - UserShenheguan.objects.filter( - user__yonghuid=record.shenheguan_id - ).update( - shenhe_zongshu=F('shenhe_zongshu') + 1, - shenhe_zhuangtai=1 - ) - - return True, '申请成功', record - - except Exception as e: - logger.error(f"创建审核记录异常: {e}", exc_info=True) - return False, '系统异常', None - - - - - - - - - - - -def create_shenhe_jilu_from_temp(user, temp_data): - """ - 根据临时表数据创建审核记录(专供支付回调使用) - 参数 temp_data 是 KaohePayTemp 对象 - 返回 (success, message, record) - """ - - try: - logger.info(f"开始从临时表创建审核记录: 用户={user.yonghuid}, 订单={temp_data.dingdan_id}") - - chenghao = Chenghao.objects.get(id=temp_data.tag_id) - logger.info(f"获取标签成功: {chenghao.mingcheng}") - - # 检查是否已拥有标签 - if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): - logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}") - return False, '您已拥有该标签,无需重复考核', None - - # 检查是否有进行中的审核 - if ShenheJilu.objects.filter( - shenqingren_id=user.yonghuid, - chenghao=chenghao, - zhuangtai__in=[0, 3] - ).exists(): - logger.warning(f"用户 {user.yonghuid} 有未完成的考核申请") - return False, '您有未完成的考核申请,请等待结果', None - - # 使用临时表中预计算好的次数和费用 - new_cishu = temp_data.cishu - current_fee = temp_data.fee - reapply = temp_data.reapply - jilu_id = temp_data.jilu_id - shenheguan_id = temp_data.shenheguan_id - - logger.info(f"临时表数据: cishu={new_cishu}, fee={current_fee}, reapply={reapply}, jilu_id={jilu_id}, shenheguan_id={shenheguan_id}") - - # 再次申请校验 - if reapply: - if not jilu_id: - logger.error("再次申请但未提供原记录ID") - return False, '再次申请必须提供原记录ID', None - last = ShenheJilu.objects.filter( - jilu_id=jilu_id, - shenqingren_id=user.yonghuid, - zhuangtai=2 - ).first() - if not last: - logger.error(f"原记录不存在或状态不是未通过: jilu_id={jilu_id}") - return False, '原记录不存在或状态不是未通过', None - if new_cishu != last.cishu + 1: - logger.error(f"考核次数不一致: 临时表={new_cishu}, 原记录次数={last.cishu}") - return False, '考核次数不一致', None - logger.info(f"再次申请校验通过,原记录ID: {jilu_id}") - - # 创建或更新审核记录 - if reapply and jilu_id: - # 更新原记录 - final_kg_id = last.shenheguan_id - if shenheguan_id and not last.shenheguan_id: - final_kg_id = shenheguan_id - final_zhuangtai = 0 if final_kg_id else 3 - if final_zhuangtai == 3: - final_kg_id = None - - ShenheJilu.objects.filter(pk=last.pk).update( - cishu=new_cishu, - jiaofei_jine=F('jiaofei_jine') + current_fee, - dashou_youxi_id=temp_data.game_nick, - dashou_beizhu=temp_data.remark, - guize_neirong=chenghao.kaohe_guize or '', - zhuangtai=final_zhuangtai, - shenheguan_id=final_kg_id, - ) - last.refresh_from_db() - record = last - - # 释放旧考核官 - if last.shenheguan_id != final_kg_id and last.shenheguan_id: - UserShenheguan.objects.filter(user__yonghuid=last.shenheguan_id).update(shenhe_zhuangtai=0) - logger.info(f"释放旧考核官: {last.shenheguan_id}") - - logger.info(f"更新审核记录成功: jilu_id={record.jilu_id}, 新次数={new_cishu}") - else: - # 首次申请 - bankuai = chenghao.bankuai - rule_text = chenghao.kaohe_guize or '' - new_jilu_id = generate_unique_jilu_id() - initial_zhuangtai = 0 if shenheguan_id else 3 - - record = ShenheJilu.objects.create( - jilu_id=new_jilu_id, - shenqingren_id=user.yonghuid, - zhuangtai=initial_zhuangtai, - bankuai=bankuai, - chenghao=chenghao, - cishu=new_cishu, - jiaofei_jine=current_fee, - dashou_youxi_id=temp_data.game_nick, - dashou_beizhu=temp_data.remark, - guize_neirong=rule_text, - shenheguan_id=shenheguan_id if shenheguan_id else None, - ) - logger.info(f"创建审核记录成功: jilu_id={new_jilu_id}, 状态={initial_zhuangtai}") - - # 记录费用明细 - fee_detail, created = KaoheJiluFeiyong.objects.get_or_create( - jilu=record, - cishu=record.cishu, - defaults={'jine': current_fee} - ) - if created: - logger.info(f"创建费用明细成功: 记录ID={record.jilu_id}, 金额={current_fee}") - else: - logger.warning(f"费用明细已存在: 记录ID={record.jilu_id}, 次数={record.cishu}") - - # 更新考核官状态 - if record.zhuangtai == 0 and record.shenheguan_id: - UserShenheguan.objects.filter(user__yonghuid=record.shenheguan_id).update( - shenhe_zongshu=F('shenhe_zongshu') + 1, - shenhe_zhuangtai=1 - ) - logger.info(f"更新考核官状态: {record.shenheguan_id}, 审核总数+1, 状态=忙碌") - - logger.info(f"从临时表创建审核记录完成: jilu_id={record.jilu_id}") - return True, '申请成功', record - - except Chenghao.DoesNotExist: - logger.error(f"标签不存在: tag_id={temp_data.tag_id}") - return False, '标签不存在', None - except Exception as e: - logger.error(f"从临时表创建审核记录异常: {str(e)}", exc_info=True) +# 标准库 +import time +import random +import logging +from decimal import Decimal +# Django 内置 +from django.db.models import F, Max +# 项目模型 +## 当前dengji应用 +from .models import ( + Chenghao, KaoheCishuFeiyong, ShenheJilu, KaoheJiluFeiyong, + YonghuChenghao, KaoheguanBankuai, DingdanBiaoqian +) +## users应用 +from users.models import UserShenheguan, UserDashou +from gvsdsdk.models import User +## orders应用 +from .models import YonghuChenghao +# 日志对象 +logger = logging.getLogger(__name__) + + + +def check_dashou_biaoqian_required(order_id, dashou_yonghuid): + """ + 检查打手是否满足订单的标签需求。 + - 如果订单没有需求标签,直接放行。 + - 如果有,则要求打手至少拥有其中一个标签。 + 返回 (allowed: bool, message: str) + """ + try: + # 1. 获取订单的需求标签ID集合 + required_ids = set( + DingdanBiaoqian.objects.filter(dingdan_id=order_id) + .values_list('chenghao_id', flat=True) + ) + if not required_ids: + return True, '' + + # 2. 找到打手对应的用户记录 + try: + user = User.objects.get(UserUID=dashou_yonghuid) + except User.DoesNotExist: + return False, '用户信息异常' + + # 3. 获取打手已拥有的标签ID集合(只取打手类型) + owned_ids = set( + YonghuChenghao.objects.filter( + yonghu=user, + chenghao__leixing='dashou' + ).values_list('chenghao_id', flat=True) + ) + + if not owned_ids: + return False, '您未拥有任何打手标签,无法抢此订单' + + # 4. 交集判断 + if required_ids.intersection(owned_ids): + return True, '' + else: + return False, '您的标签不符合此订单要求,无法抢单' + + except Exception as e: + # 标签校验失败不影响主流程,记录日志并保守放行(也可保守拒绝,按需求选择) + logger.error(f"标签校验异常: {e}", exc_info=True) + # 安全起见,如果校验出错,选择拒绝,避免越权 + return False, '订单标签校验异常,请稍后重试' + + + + + +def get_tag_fee(chenghao, cishu): + """ + 获取标签在指定考核次数下的费用。 + - 若有对应次数的配置,直接返回。 + - 若没有该次数,取该标签下已有最大次数的费用(作为后续次数的价格)。 + - 若该标签完全没有费用记录,返回 0.00(免费)。 + """ + fee_obj = KaoheCishuFeiyong.objects.filter(chenghao=chenghao, cishu=cishu).first() + if fee_obj: + return fee_obj.feiyong + + max_obj = KaoheCishuFeiyong.objects.filter(chenghao=chenghao).order_by('-cishu').first() + if max_obj: + return max_obj.feiyong + + return Decimal('0.00') + + +def generate_unique_jilu_id(): + """ + 生成唯一的审核记录 ID,格式:SH + 时间戳(10位) + 随机数(4位) + 循环检查数据库避免重复,最多尝试 10 次。 + """ + for _ in range(10): + timestamp = str(int(time.time() * 1000))[-10:] + random_part = str(random.randint(1000, 9999)) + jilu_id = f'SH{timestamp}{random_part}' + if not ShenheJilu.objects.filter(jilu_id=jilu_id).exists(): + return jilu_id + raise Exception('无法生成唯一记录ID,请稍后重试') + + +def validate_shenheguan(shenheguan_id, bankuai, fee, applicant_user): + """ + 校验指定考核官是否合法 + - 考核官必须存在且未被禁用 + - 必须属于对应板块 + - 必须处于空闲状态 + - 若费用>0,则要求已认证 + 返回 (valid, error_msg) + """ + if not shenheguan_id: + return True, '' + try: + kg = UserShenheguan.objects.select_related('user').get( + user__UserUID=shenheguan_id, + zhuangtai=1 + ) + except UserShenheguan.DoesNotExist: + return False, '考核官不存在或已被禁用' + + # 板块权限 + if not KaoheguanBankuai.objects.filter(kaoheguan_id=shenheguan_id, bankuai=bankuai).exists(): + return False, '该考核官不属于此板块' + + # 空闲状态 + if kg.shenhe_zhuangtai != 0: + return False, '该考核官当前忙碌,无法指定' + + # 费用>0需要认证 + if float(fee) > 0 and not kg.is_renzheng: + return False, '费用大于0时只能指定已认证的考核官' + + return True, '' + + +def create_shenhe_jilu(user, tag_id, game_nick, remark, reapply, jilu_id=None, shenheguan_id=''): + """ + 创建或更新审核记录。 + - 首次申请:创建新记录,生成唯一 jilu_id。 + - 再次申请:基于原记录(必须状态为未通过)更新次数、累加总费用、更新备注和游戏昵称, + 保留审核官信息,状态重置为待审核。不创建新记录。 + - 每次申请均创建一条费用明细记录(KaoheJiluFeiyong),记录本次缴费。 + - 若原记录有考核官,则将其状态改为审核中。 + - 新增 shenheguan_id 参数:指定考核官ID,仅在原记录无考核官时可传入。 + 返回 (success, message, record) + """ + try: + # 1. 校验标签存在 + try: + chenghao = Chenghao.objects.get(id=tag_id) + except Chenghao.DoesNotExist: + return False, '标签不存在', None + + # 2. 判断用户是否已拥有该标签 + if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): + return False, '您已拥有该标签,无需重复考核', None + + # 3. 检查是否有进行中的审核记录(待审核、审核中) + has_pending = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[0, 3] + ).exists() + if has_pending: + return False, '您有未完成的考核申请,请等待结果', None + + last_record = None + current_fee = Decimal('0.00') + + # 4. 处理再次申请 + if reapply and jilu_id: + # 查询原记录(未通过状态) + last_record = ShenheJilu.objects.filter( + jilu_id=jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=2 + ).first() + if not last_record: + return False, '原记录不存在或状态不是未通过', None + + new_cishu = last_record.cishu + 1 + current_fee = get_tag_fee(chenghao, new_cishu) + + # 确定最终状态:有考核官 = 审核中(0),否则待审核(3) + final_kg_id = last_record.shenheguan_id # 保留原来的 + if shenheguan_id and not last_record.shenheguan_id: + final_kg_id = shenheguan_id # 新指定 + + final_zhuangtai = 0 if final_kg_id else 3 # 有考核官就是审核中 + if final_zhuangtai == 3: + final_kg_id = None + + # 原子化更新 + ShenheJilu.objects.filter(pk=last_record.pk).update( + cishu=new_cishu, + jiaofei_jine=F('jiaofei_jine') + current_fee, + dashou_youxi_id=game_nick, + dashou_beizhu=remark, + guize_neirong=chenghao.kaohe_guize or '', + zhuangtai=final_zhuangtai, + shenheguan_id=final_kg_id, + ) + last_record.refresh_from_db() + record = last_record + + # 如果原考核官被替换或清除,释放旧考核官 + if last_record.shenheguan_id != final_kg_id: + if last_record.shenheguan_id: + UserShenheguan.objects.filter( + user__UserUID=last_record.shenheguan_id + ).update(shenhe_zhuangtai=0) + else: + # 首次申请 + max_cishu = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[1, 2] + ).aggregate(max=Max('cishu'))['max'] or 0 + new_cishu = max_cishu + 1 + current_fee = get_tag_fee(chenghao, new_cishu) + + bankuai = chenghao.bankuai + rule_text = chenghao.kaohe_guize or '' + new_jilu_id = generate_unique_jilu_id() + + initial_zhuangtai = 0 if shenheguan_id else 3 # 指定→审核中,否则待审核 + record = ShenheJilu.objects.create( + jilu_id=new_jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=initial_zhuangtai, + bankuai=bankuai, + chenghao=chenghao, + cishu=new_cishu, + jiaofei_jine=current_fee, + dashou_youxi_id=game_nick, + dashou_beizhu=remark, + guize_neirong=rule_text, + shenheguan_id=shenheguan_id if shenheguan_id else None, + ) + + # 5. 创建费用明细(防重复) + KaoheJiluFeiyong.objects.get_or_create( + jilu=record, + cishu=record.cishu, + defaults={'jine': current_fee} + ) + + # 6. 更新考核官统计(仅当状态为审核中且有考核官) + if record.zhuangtai == 0 and record.shenheguan_id: + UserShenheguan.objects.filter( + user__UserUID=record.shenheguan_id + ).update( + shenhe_zongshu=F('shenhe_zongshu') + 1, + shenhe_zhuangtai=1 + ) + + return True, '申请成功', record + + except Exception as e: + logger.error(f"创建审核记录异常: {e}", exc_info=True) + return False, '系统异常', None + + + + + + + + + + + +def create_shenhe_jilu_from_temp(user, temp_data): + """ + 根据临时表数据创建审核记录(专供支付回调使用) + 参数 temp_data 是 KaohePayTemp 对象 + 返回 (success, message, record) + """ + + try: + logger.info(f"开始从临时表创建审核记录: 用户={user.yonghuid}, 订单={temp_data.dingdan_id}") + + chenghao = Chenghao.objects.get(id=temp_data.tag_id) + logger.info(f"获取标签成功: {chenghao.mingcheng}") + + # 检查是否已拥有标签 + if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): + logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}") + return False, '您已拥有该标签,无需重复考核', None + + # 检查是否有进行中的审核 + if ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + chenghao=chenghao, + zhuangtai__in=[0, 3] + ).exists(): + logger.warning(f"用户 {user.yonghuid} 有未完成的考核申请") + return False, '您有未完成的考核申请,请等待结果', None + + # 使用临时表中预计算好的次数和费用 + new_cishu = temp_data.cishu + current_fee = temp_data.fee + reapply = temp_data.reapply + jilu_id = temp_data.jilu_id + shenheguan_id = temp_data.shenheguan_id + + logger.info(f"临时表数据: cishu={new_cishu}, fee={current_fee}, reapply={reapply}, jilu_id={jilu_id}, shenheguan_id={shenheguan_id}") + + # 再次申请校验 + if reapply: + if not jilu_id: + logger.error("再次申请但未提供原记录ID") + return False, '再次申请必须提供原记录ID', None + last = ShenheJilu.objects.filter( + jilu_id=jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=2 + ).first() + if not last: + logger.error(f"原记录不存在或状态不是未通过: jilu_id={jilu_id}") + return False, '原记录不存在或状态不是未通过', None + if new_cishu != last.cishu + 1: + logger.error(f"考核次数不一致: 临时表={new_cishu}, 原记录次数={last.cishu}") + return False, '考核次数不一致', None + logger.info(f"再次申请校验通过,原记录ID: {jilu_id}") + + # 创建或更新审核记录 + if reapply and jilu_id: + # 更新原记录 + final_kg_id = last.shenheguan_id + if shenheguan_id and not last.shenheguan_id: + final_kg_id = shenheguan_id + final_zhuangtai = 0 if final_kg_id else 3 + if final_zhuangtai == 3: + final_kg_id = None + + ShenheJilu.objects.filter(pk=last.pk).update( + cishu=new_cishu, + jiaofei_jine=F('jiaofei_jine') + current_fee, + dashou_youxi_id=temp_data.game_nick, + dashou_beizhu=temp_data.remark, + guize_neirong=chenghao.kaohe_guize or '', + zhuangtai=final_zhuangtai, + shenheguan_id=final_kg_id, + ) + last.refresh_from_db() + record = last + + # 释放旧考核官 + if last.shenheguan_id != final_kg_id and last.shenheguan_id: + UserShenheguan.objects.filter(user__UserUID=last.shenheguan_id).update(shenhe_zhuangtai=0) + logger.info(f"释放旧考核官: {last.shenheguan_id}") + + logger.info(f"更新审核记录成功: jilu_id={record.jilu_id}, 新次数={new_cishu}") + else: + # 首次申请 + bankuai = chenghao.bankuai + rule_text = chenghao.kaohe_guize or '' + new_jilu_id = generate_unique_jilu_id() + initial_zhuangtai = 0 if shenheguan_id else 3 + + record = ShenheJilu.objects.create( + jilu_id=new_jilu_id, + shenqingren_id=user.yonghuid, + zhuangtai=initial_zhuangtai, + bankuai=bankuai, + chenghao=chenghao, + cishu=new_cishu, + jiaofei_jine=current_fee, + dashou_youxi_id=temp_data.game_nick, + dashou_beizhu=temp_data.remark, + guize_neirong=rule_text, + shenheguan_id=shenheguan_id if shenheguan_id else None, + ) + logger.info(f"创建审核记录成功: jilu_id={new_jilu_id}, 状态={initial_zhuangtai}") + + # 记录费用明细 + fee_detail, created = KaoheJiluFeiyong.objects.get_or_create( + jilu=record, + cishu=record.cishu, + defaults={'jine': current_fee} + ) + if created: + logger.info(f"创建费用明细成功: 记录ID={record.jilu_id}, 金额={current_fee}") + else: + logger.warning(f"费用明细已存在: 记录ID={record.jilu_id}, 次数={record.cishu}") + + # 更新考核官状态 + if record.zhuangtai == 0 and record.shenheguan_id: + UserShenheguan.objects.filter(user__UserUID=record.shenheguan_id).update( + shenhe_zongshu=F('shenhe_zongshu') + 1, + shenhe_zhuangtai=1 + ) + logger.info(f"更新考核官状态: {record.shenheguan_id}, 审核总数+1, 状态=忙碌") + + logger.info(f"从临时表创建审核记录完成: jilu_id={record.jilu_id}") + return True, '申请成功', record + + except Chenghao.DoesNotExist: + logger.error(f"标签不存在: tag_id={temp_data.tag_id}") + return False, '标签不存在', None + except Exception as e: + logger.error(f"从临时表创建审核记录异常: {str(e)}", exc_info=True) return False, '系统异常', None \ No newline at end of file diff --git a/rank/views.py b/rank/views.py index 0b2516a..73a312b 100644 --- a/rank/views.py +++ b/rank/views.py @@ -1,948 +1,948 @@ -import json -import logging -from decimal import Decimal -from django.db import models, transaction -from django.db.models import F -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from .utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan -from .models import ( - YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu, - KaoheguanBankuai, Bankuai, ShenheJilu, KaoheJiluFeiyong -) -from users.models import UserShenheguan, UserDashou -from gvsdsdk.models import User -from products.models import ShangpinLeixing - - -logger = logging.getLogger(__name__) - - -class DashouBiaoqianView(APIView): - """获取当前打手所有称号标签""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - yonghu = request.user - # 查询用户的所有称号关联,一次连表取出 - relations = YonghuChenghao.objects.filter( - yonghu=yonghu - ).select_related('chenghao') - - chenghao_list = [] - for rel in relations: - ch = rel.chenghao - # 解析特效描述 JSON(前端需要对象) - texiao_json = {} - if ch.texiao_miaoshu: - try: - texiao_json = json.loads(ch.texiao_miaoshu) - except json.JSONDecodeError: - texiao_json = {} - chenghao_list.append({ - 'mingcheng': ch.mingcheng, - 'tubiao_url': ch.tubiao_url or '', - 'texiao_json': texiao_json, - }) - - return Response({ - 'code': 0, - 'msg': '成功', - 'data': {'chenghao_list': chenghao_list} - }) - except Exception as e: - logger.error(f"获取打手标签失败: {e}", exc_info=True) - return Response({'code': 1, 'msg': '服务器错误', 'data': None}) - - -class BankuaiBiaoqianView(APIView): - """获取商品类型所属板块的所有标签""" - permission_classes = [IsAuthenticated] - - def post(self, request): - leixing_id = request.data.get('leixing_id') - if not leixing_id: - return Response({'code': 1, 'msg': '缺少leixing_id'}) - - try: - leixing = ShangpinLeixing.objects.select_related('bankuai').get(id=leixing_id) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 2, 'msg': '商品类型不存在'}) - - bankuai = leixing.bankuai - if not bankuai: - return Response({'code': 0, 'data': {'biaoqian_list': []}}) - - # 获取该板块下所有类型的标签(leixing可以为dashou/shangjia等,这里先返回所有) - chenghao_list = Chenghao.objects.filter(bankuai=bankuai) - data = [] - for ch in chenghao_list: - texiao_json = {} - if ch.texiao_miaoshu: - try: - texiao_json = json.loads(ch.texiao_miaoshu) - except: - pass - data.append({ - 'id': ch.id, - 'mingcheng': ch.mingcheng, - 'texiao_json': texiao_json - }) - return Response({'code': 0, 'data': {'biaoqian_list': data}}) - - - - -# 硬编码邀请码 -KAOHEGUAN_INVITE_CODE = 'KHG2026VIP' - -class KaoheguanRegisterView(APIView): - """ - 考核官注册接口 - POST /dengji/khgzc - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - yonghu = request.user - invite_code = request.data.get('inviteCode', '').strip() - - if not invite_code: - return Response({'code': 400, 'msg': '邀请码不能为空'}) - if invite_code != KAOHEGUAN_INVITE_CODE: - return Response({'code': 400, 'msg': '邀请码错误'}) - - kg = UserShenheguan.objects.filter(user=yonghu).first() - if kg: - if kg.zhuangtai == 1: - return Response({'code': 200, 'msg': '您已是考核官', 'data': self._build_data(kg)}) - else: - return Response({'code': 400, 'msg': '账号已被封禁'}) - - with transaction.atomic(): - kg = UserShenheguan.objects.create( - user=yonghu, - zhuangtai=1, - shenhe_zhuangtai=0, - shenhe_zongshu=0, - tongguo_zongshu=0, - yue=0.00, - zonge=0.00 - ) - return Response({'code': 200, 'msg': '注册成功', 'data': self._build_data(kg)}) - - except Exception as e: - logger.error(f"考核官注册失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - def _build_data(self, kg): - bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.yonghuid).select_related('bankuai') - bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs] - return { - 'zhuangtai': kg.zhuangtai, - 'shenhe_zongshu': kg.shenhe_zongshu, - 'tongguo_zongshu': kg.tongguo_zongshu, - 'yue': str(kg.yue), - 'zonge': str(kg.zonge), - 'shenhe_zhuangtai': kg.shenhe_zhuangtai, - 'bankuai_list': bankuai_list, - 'kaoheguanstatus': 1 - } - - - - - -class KaoheguanRefreshView(APIView): - """ - 考核官数据刷新接口 - POST /dengji/khgsx - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - yonghu = request.user - kg = UserShenheguan.objects.filter(user=yonghu).first() - if not kg: - return Response({'code': 400, 'msg': '您还未注册考核官'}) - - if kg.zhuangtai != 1: - return Response({'code': 200, 'data': {'zhuangtai': kg.zhuangtai, 'kaoheguanstatus': 0}}) - - # 查询所属板块 - bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.yonghuid).select_related('bankuai') - bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs] - - # 统计待审核 / 审核中的记录数量 - pending_count = ShenheJilu.objects.filter( - shenheguan_id=yonghu.yonghuid, - zhuangtai=0 # 审核中 - ).count() - - data = { - 'zhuangtai': kg.zhuangtai, - 'shenhe_zongshu': kg.shenhe_zongshu, - 'tongguo_zongshu': kg.tongguo_zongshu, - 'yue': str(kg.yue), - 'zonge': str(kg.zonge), - 'shenhe_zhuangtai': kg.shenhe_zhuangtai, - 'bankuai_list': bankuai_list, - 'kaoheguanstatus': 1, - 'is_renzheng': kg.is_renzheng, # 🆕 是否认证 - 'pending_shenhe_count': pending_count # 🆕 审核中数量 - } - return Response({'code': 200, 'data': data}) - - except Exception as e: - logger.error(f"考核官刷新失败: {e}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - -class ShenheDatingModuleView(APIView): - """审核大厅获取模块列表(板块 + 待审核数量 + 是否关联)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - yonghu = request.user - related_ids = set(KaoheguanBankuai.objects.filter(kaoheguan_id=yonghu.yonghuid) - .values_list('bankuai_id', flat=True)) - - bankuai_list = [] - for bk in Bankuai.objects.all(): - count = ShenheJilu.objects.filter(bankuai=bk, zhuangtai=3, shenheguan_id__isnull=True).count() - bankuai_list.append({ - 'bankuai_id': bk.bankuai_id, - 'mingcheng': bk.mingcheng, - 'count': count, - 'is_related': bk.bankuai_id in related_ids - }) - - return Response({'code': 0, 'data': {'modules': bankuai_list}}) - except Exception as e: - return Response({'code': 500, 'msg': str(e)}, status=500) - -class ShenheDatingDataView(APIView): - """获取审核大厅待审核记录列表""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - data = request.data - bankuai_id = data.get('bankuai_id') - page = int(data.get('page', 1)) - page_size = int(data.get('page_size', 5)) - - query = ShenheJilu.objects.filter(zhuangtai=3, shenheguan_id__isnull=True) - if bankuai_id: - query = query.filter(bankuai_id=bankuai_id) - - total = query.count() - offset = (page - 1) * page_size - jilu_list = query.select_related('bankuai', 'chenghao').order_by('-create_time')[ - offset:offset + page_size] - - records = [] - for jl in jilu_list: - user = User.objects.filter(yonghuid=jl.shenqingren_id).first() - avatar = user.avatar if user else '' - texiao_json = {} - if jl.chenghao and jl.chenghao.texiao_miaoshu: - try: - texiao_json = json.loads(jl.chenghao.texiao_miaoshu) - except: - pass - - # 新增字段:本次考核费用 - kaohe_feiyong = float(jl.jiaofei_jine) if jl.jiaofei_jine is not None else 0.0 - - records.append({ - 'jilu_id': jl.jilu_id, - 'shenqingren_id': jl.shenqingren_id, - 'shenqingren_avatar': avatar, - 'dashou_youxi_id': jl.dashou_youxi_id or '', - 'dashou_beizhu': jl.dashou_beizhu or '', - 'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None, - 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', - 'chenghao_id': jl.chenghao_id, - 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', - 'chenghao_texiao_json': texiao_json, - 'guize_neirong': jl.guize_neirong or '', - 'kaohe_feiyong': kaohe_feiyong, # 新增 - 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', - }) - - has_more = (page * page_size) < total - return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}}) - except Exception as e: - return Response({'code': 500, 'msg': str(e)}, status=500) - -class ShenheQiangdanView(APIView): - """ - 审核官抢单(接审核任务) - 校验:身份、账号封禁、已有进行中审核、记录状态、不能审核自己、板块权限 - 成功后更新考核官扩展表:审核总数+1,审核状态设为“审核中” - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - yonghu = request.user - jilu_id = request.data.get('jilu_id') - if not jilu_id: - return Response({'code': 400, 'msg': '记录ID不能为空'}) - - # 1. 检查考核官身份及账号是否被禁用 - kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() - if not kg: - return Response({'code': 403, 'msg': '您不是考核官或账号已被禁用'}) - - # 2. 检查是否有正在审核中的记录(zhuangtai=0),防止同时审核多个 - ongoing = ShenheJilu.objects.filter( - shenheguan_id=yonghu.yonghuid, zhuangtai=0 - ).exists() - if ongoing: - return Response({'code': 400, 'msg': '您有正在审核中的任务,请完成后再接新任务'}) - - with transaction.atomic(): - # 3. 行锁抢单:仅当状态为待审核(3)且无审核官时才能抢 - jilu = ShenheJilu.objects.select_for_update().filter( - jilu_id=jilu_id, - zhuangtai=3, - shenheguan_id__isnull=True - ).first() - if not jilu: - return Response({'code': 400, 'msg': '该审核任务已被接走、状态变更或不存在'}) - - # 4. 不能审核自己 - if jilu.shenqingren_id == yonghu.yonghuid: - return Response({'code': 400, 'msg': '不能审核自己提交的申请'}) - - # 5. 板块权限:考核官必须属于该板块 - is_related = KaoheguanBankuai.objects.filter( - kaoheguan_id=yonghu.yonghuid, - bankuai=jilu.bankuai - ).exists() - if not is_related: - return Response({'code': 400, 'msg': '您不属于该板块,无法审核'}) - - # 新增:6. 费用>0时,要求考核官已认证 - if jilu.jiaofei_jine and float(jilu.jiaofei_jine) > 0: - if not kg.is_renzheng: - return Response({'code': 400, 'msg': '本考核需要认证考核官接单,您尚未认证'}) - - # 7. 更新审核记录:分配给当前考核官,状态变为审核中 - jilu.zhuangtai = 0 # 审核中 - jilu.shenheguan_id = yonghu.yonghuid - jilu.save() - - # 8. 更新考核官扩展表:审核总数 +1,审核状态置为审核中(1) - UserShenheguan.objects.filter(user=yonghu).update( - shenhe_zongshu=F('shenhe_zongshu') + 1, - shenhe_zhuangtai=1 - ) - - return Response({ - 'code': 0, - 'msg': '抢单成功,请前往游戏内审核,审核完成后请到考核打分页面进行评定。', - 'data': {'bankuai_id': jilu.bankuai.bankuai_id} - }) - - except Exception as e: - logger.error(f"审核官抢单异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) - - - - - - - -class ShenheXiangqingView(APIView): - """获取当前考核官审核中的记录详情(新增待收考核费字段)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - yonghu = request.user - kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() - if not kg: - return Response({'code': 403, 'msg': '您不是考核官或账号异常'}) - - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 10)) - - # 查询当前考核官名下状态为“审核中”的记录 - base_query = ShenheJilu.objects.filter( - shenheguan_id=yonghu.yonghuid, - zhuangtai=0 - ).select_related('bankuai', 'chenghao') - total = base_query.count() - offset = (page - 1) * page_size - jilu_list = base_query[offset:offset + page_size] - - records = [] - for jl in jilu_list: - user = User.objects.filter(yonghuid=jl.shenqingren_id).first() - avatar = user.avatar if user else '' - texiao_json = {} - if jl.chenghao and jl.chenghao.texiao_miaoshu: - try: - texiao_json = json.loads(jl.chenghao.texiao_miaoshu) - except: - pass - - # 是否开启金牌 - is_jinpai = jl.chenghao.kaioi_jinpai if jl.chenghao else False - - # 本次待收考核费(基于记录中的当前次数,从费用明细表查询) - daishou_fei = 0.00 - fee_detail = KaoheJiluFeiyong.objects.filter( - jilu=jl, - cishu=jl.cishu - ).first() - if fee_detail: - daishou_fei = float(fee_detail.jine) - else: - # 兜底:若明细表无记录,尝试用通用费用表计算 - daishou_fei = 0.00 - # 上次失败原因 - last_fail_reason = '' - last_fail = KaoheJujueJilu.objects.filter( - jilu=jl, - cishu=jl.cishu - 1 - ).order_by('-create_time').first() - if last_fail: - last_fail_reason = last_fail.yuanyin - - records.append({ - 'jilu_id': jl.jilu_id, - 'shenqingren_id': jl.shenqingren_id, - 'shenqingren_avatar': avatar, - 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', - 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', - 'chenghao_texiao_json': texiao_json, - 'guize_neirong': jl.guize_neirong or '', - 'dashou_beizhu': jl.dashou_beizhu or '', - 'dashou_youxi_id': jl.dashou_youxi_id or '', - 'shenheguan_youxi_id': jl.shenheguan_youxi_id or '', - 'cishu': jl.cishu, - 'daishou_fei': daishou_fei, # 新增:待收考核费 - 'last_fail_reason': last_fail_reason, - 'is_jinpai': is_jinpai, - 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', - }) - - has_more = (page * page_size) < total - return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}}) - except Exception as e: - return Response({'code': 500, 'msg': str(e)}, status=500) - -class ShenheCaozuoView(APIView): - """ - 考核官执行操作(同意/拒绝/转移/修改昵称),并更新考核官收益 - - 只有“同意”或“拒绝”才增加余额和总额,金额严格从 KaoheJiluFeiyong 表获取 - - 查不到费用则加 0 元,绝不影响流程 - - 转移、修改昵称等操作不涉及收益 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - yonghu = request.user - # 1. 验证考核官身份及账号状态 - kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() - if not kg: - return Response({'code': 403, 'msg': '账号异常,无法操作'}) - - jilu_id = request.data.get('jilu_id') - caozuo = request.data.get('caozuo') # agree / reject / transfer / modify_nick - if not jilu_id or not caozuo: - return Response({'code': 400, 'msg': '缺少必要参数'}) - - with transaction.atomic(): - # 2. 锁定审核记录,确保属于当前考核官且状态为审核中(0) - jilu = ShenheJilu.objects.select_for_update().filter( - jilu_id=jilu_id, - shenheguan_id=yonghu.yonghuid, - zhuangtai=0 - ).first() - if not jilu: - return Response({'code': 400, 'msg': '该记录不存在或不属于您'}) - - # 3. 本次操作产生的收入(仅通过/拒绝时有意义) - daishou_fei = 0.00 - - # 4. 根据操作类型执行对应业务逻辑 - if caozuo == 'agree': - # --- 通过考核 --- - # 查询本次考核应得的费用(基于记录ID和当前次数) - fee_detail = KaoheJiluFeiyong.objects.filter( - jilu=jilu, - cishu=jilu.cishu - ).first() - if fee_detail: - daishou_fei = float(fee_detail.jine) - # 找不到就保持 0,绝不使用任何默认值 - - jilu.zhuangtai = 1 - jilu.save() - - # 授予标签 - if jilu.chenghao: - yh = User.objects.get(yonghuid=jilu.shenqingren_id) - if not YonghuChenghao.objects.filter(yonghu=yh, chenghao=jilu.chenghao).exists(): - YonghuChenghao.objects.create(yonghu=yh, chenghao=jilu.chenghao) - # 如果标签开启了金牌,同步更新打手称号 - if jilu.chenghao.kaioi_jinpai: - dashou_prof = UserDashou.objects.filter(user=yh).first() - if dashou_prof: - dashou_prof.chenghao = '金牌选手' - dashou_prof.save() - - elif caozuo == 'reject': - # --- 拒绝考核 --- - yuanyin = request.data.get('yuanyin', '') - if not yuanyin.strip(): - return Response({'code': 400, 'msg': '请填写拒绝原因'}) - - # 查询本次考核应得的费用(拒绝时同样获得收入) - fee_detail = KaoheJiluFeiyong.objects.filter( - jilu=jilu, - cishu=jilu.cishu - ).first() - if fee_detail: - daishou_fei = float(fee_detail.jine) - # 找不到就保持 0 - - jilu.zhuangtai = 2 - jilu.save() - # 记录拒绝原因 - KaoheJujueJilu.objects.create( - jilu=jilu, - cishu=jilu.cishu, - shenheguan_id=yonghu.yonghuid, - yuanyin=yuanyin - ) - - # 新增:不合格时,如果考核官未认证,则清空考核官ID和游戏昵称 - if not kg.is_renzheng: - jilu.shenheguan_id = None - jilu.shenheguan_youxi_id = '' - jilu.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id']) - - elif caozuo == 'transfer': - # 转移考核官:清空考核官信息,状态回退为待审核 - jilu.shenheguan_id = None - jilu.shenheguan_youxi_id = None - jilu.zhuangtai = 3 - jilu.save() - # 转移不更新收益,直接返回 - - - elif caozuo == 'modify_nick': - # 修改考核官游戏昵称 - youxi_id = request.data.get('youxi_id', '').strip() - if not youxi_id: - return Response({'code': 400, 'msg': '请输入游戏昵称'}) - jilu.shenheguan_youxi_id = youxi_id - jilu.save() - return Response({'code': 0, 'msg': '修改成功'}) - - else: - return Response({'code': 400, 'msg': '无效的操作类型'}) - - # 5. 更新考核官收益(仅“通过”或“拒绝”时执行) - if caozuo in ('transfer'): - update_fields = { - - 'shenhe_zhuangtai': 0, # 设为空闲 - } - UserShenheguan.objects.filter(user=yonghu).update(**update_fields) - if caozuo in ('agree', 'reject'): - update_fields = { - 'yue': F('yue') + Decimal(str(daishou_fei)), - 'zonge': F('zonge') + Decimal(str(daishou_fei)), - 'shenhe_zhuangtai': 0, # 设为空闲 - } - # 只有同意时才增加“通过总数” - if caozuo == 'agree': - update_fields['tongguo_zongshu'] = F('tongguo_zongshu') + 1 - - # 审核总数不再增加 - UserShenheguan.objects.filter(user=yonghu).update(**update_fields) - - return Response({'code': 0, 'msg': '操作成功'}) - - except Exception as e: - logger.error(f"考核操作异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) - - - - -# dengji/views.py -class KaoheJinpaiTagsView(APIView): - """获取考核金牌页面的标签和板块信息""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - # 获取用户已拥有的标签名称集合 - owned_names = set( - YonghuChenghao.objects.filter(yonghu=user, chenghao__leixing='dashou') - .values_list('chenghao__mingcheng', flat=True) - ) - - # 板块列表 - plate_list = [] - for bk in Bankuai.objects.all(): - tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou') - tag_list = [] - for tag in tags: - # 费用列表 - fee_objs = KaoheCishuFeiyong.objects.filter(chenghao=tag).order_by('cishu') - fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)} for f in fee_objs] - - # 若没有记录,补充默认费用 - if not fee_list: - fee_list = [{'cishu': 1, 'feiyong': 5.00}] - - tag_list.append({ - 'id': tag.id, - 'name': tag.mingcheng, - 'texiao_json': json.loads(tag.texiao_miaoshu) if tag.texiao_miaoshu else {}, - 'jinpai': tag.kaioi_jinpai, - 'rule': tag.kaohe_guize or '', - 'fee_list': fee_list, - 'owned': tag.mingcheng in owned_names, - }) - - if tag_list: - plate_list.append({ - 'bankuai_id': bk.bankuai_id, - 'mingcheng': bk.mingcheng, - 'tags': tag_list - }) - - # 新增:返回所有在职考核官信息(用于指定考核官) - kaoheguan_list = [] - for kg in UserShenheguan.objects.filter(zhuangtai=1).select_related('user'): - uid = kg.user.yonghuid - avatar = kg.user.avatar or '' - # 获取考核官的打手昵称(从打手扩展表) - dashou_nick = '' - dashou_prof = UserDashou.objects.filter(user=kg.user).first() - if dashou_prof: - dashou_nick = dashou_prof.nicheng or '' - kaoheguan_list.append({ - 'yonghuid': uid, - 'avatar': avatar, - 'dashou_nick': dashou_nick, - 'is_renzheng': kg.is_renzheng, - 'shenhe_zhuangtai': kg.shenhe_zhuangtai, - }) - - return Response({ - 'code': 0, - 'data': { - 'plate_list': plate_list, - 'my_tags': list(owned_names), - 'kaoheguan_list': kaoheguan_list, # 新增 - } - }) - except Exception as e: - return Response({'code': 500, 'msg': str(e)}, status=500) - - -class KaoheJiluView(APIView): - """获取打手本人的考核记录(支持筛选状态)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - status = request.data.get('status', 'pending') # pending / passed - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 5)) - - if status == 'pending': - # 进行中/未通过:审核中(0)、待审核(3)、未通过(2) - query = ShenheJilu.objects.filter( - shenqingren_id=user.yonghuid, - zhuangtai__in=[0, 2, 3] - ) - else: - query = ShenheJilu.objects.filter( - shenqingren_id=user.yonghuid, - zhuangtai=1 # 已通过 - ) - - total = query.count() - records = query.order_by('-create_time')[(page-1)*page_size : page*page_size] - - data = [] - for jl in records: - # 汇总缴纳总额 - total_fee = KaoheJiluFeiyong.objects.filter(jilu=jl).aggregate( - total=models.Sum('jine') - )['total'] or 0.00 - - # 上次拒绝原因(最近一条) - last_fail = KaoheJujueJilu.objects.filter(jilu=jl).order_by('-create_time').first() - fail_reason = last_fail.yuanyin if last_fail else '' - - # 考核官头像 - kg_user = User.objects.filter(yonghuid=jl.shenheguan_id).first() - kg_avatar = kg_user.avatar if kg_user else '' - - data.append({ - 'jilu_id': jl.jilu_id, - 'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '', - 'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '', - 'zhuangtai': jl.zhuangtai, - 'statusText': dict(ShenheJilu._meta.get_field('zhuangtai').choices).get(jl.zhuangtai, '未知'), - 'statusColor': {0: '#FFA500', 1: '#27ae60', 2: '#e74c3c', 3: '#3498db'}.get(jl.zhuangtai, '#888'), - 'shenheguan_id': jl.shenheguan_id, - 'shenheguan_avatar': kg_avatar, - 'dashou_youxi_id': jl.dashou_youxi_id, - 'dashou_beizhu': jl.dashou_beizhu, - 'cishu': jl.cishu, - 'total_fee': float(total_fee), - 'last_fail_reason': fail_reason, - 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S'), - # 为再次申请预留标签对象数据(前端可能用到) - 'tag_obj': { - 'id': jl.chenghao_id, - 'name': jl.chenghao.mingcheng if jl.chenghao else '', - # 其他字段按需,但申请时会重新拉取 - } if jl.chenghao else None, - }) - - has_more = (page * page_size) < total - return Response({ - 'code': 0, - 'data': {'list': data, 'has_more': has_more, 'total': total} - }) - except Exception as e: - return Response({'code': 500, 'msg': str(e)}, status=500) - - - - -class KaoheFreeApplyView(APIView): - """免费申请考核(首次/再次,费用为0时调用)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - user = request.user - tag_id = request.data.get('tag_id') - game_nick = request.data.get('game_nick', '').strip() - remark = request.data.get('remark', '').strip() - reapply = request.data.get('reapply', False) - jilu_id = request.data.get('jilu_id') if reapply else None - shenheguan_id = request.data.get('shenheguan_id', '').strip() # 新增 - - if not tag_id or not game_nick: - return Response({'code': 400, 'msg': '标签ID和游戏昵称必填'}) - - # 校验费用是否为0 - try: - chenghao = Chenghao.objects.get(id=tag_id) - except Chenghao.DoesNotExist: - return Response({'code': 400, 'msg': '标签不存在'}) - - # 计算本次次数 - if reapply and jilu_id: - last_record = ShenheJilu.objects.filter( - jilu_id=jilu_id, shenqingren_id=user.yonghuid, zhuangtai=2 - ).first() - if not last_record: - return Response({'code': 400, 'msg': '原记录不存在或状态不是未通过'}) - new_cishu = last_record.cishu + 1 - else: - max_cishu = ShenheJilu.objects.filter( - shenqingren_id=user.yonghuid, chenghao=chenghao, zhuangtai__in=[1,2] - ).aggregate(max=models.Max('cishu'))['max'] or 0 - new_cishu = max_cishu + 1 - - fee = get_tag_fee(chenghao, new_cishu) - if fee > 0: - return Response({'code': 400, 'msg': '您有未通过的记录请查看并重考'}) - - # 新增:如果指定了考核官,进行合法性校验 - if shenheguan_id: - valid, err = validate_shenheguan(shenheguan_id, chenghao.bankuai, fee, user) - if not valid: - return Response({'code': 400, 'msg': err}) - - # 调用公共创建逻辑 - success, msg, record = create_shenhe_jilu( - user, tag_id, game_nick, remark, reapply, jilu_id, - shenheguan_id=shenheguan_id # 新增参数 - ) - if success: - return Response({'code': 0, 'msg': msg, 'data': {'jilu_id': record.jilu_id}}) - else: - return Response({'code': 400, 'msg': msg}) - except Exception as e: - return Response({'code': 500, 'msg': str(e)}, status=500) - - - - - - -class KaoheJiluListView(APIView): - """ - 获取当前考核官的历史审核记录 - 路径:POST /dengji/khghqkhjl - 权限:JWT Token (考核官) - 参数: - bankuai_id : int (可选,0或空表示全部) - status : int (可选,0-全部 1-审核中 2-已通过 3-未通过) - page : int (默认1) - page_size : int (默认5) - 返回: - list, has_more, total - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - yonghu = request.user - # 1. 验证考核官身份及账号状态 - kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() - if not kg: - return Response({'code': 403, 'msg': '您不是考核官或账号异常'}) - - # 2. 获取参数 - bankuai_id = request.data.get('bankuai_id') - status = int(request.data.get('status', 0)) # 0全部 1审核中 2已通过 3未通过 - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 5)) - - # 3. 构建查询条件:只查当前考核官审核的记录 - base_query = ShenheJilu.objects.filter(shenheguan_id=yonghu.yonghuid) - - # 板块筛选 - if bankuai_id and int(bankuai_id) > 0: - base_query = base_query.filter(bankuai_id=int(bankuai_id)) - - # 状态筛选(前端传的状态映射到后端zhuangtai字段) - status_map = { - 0: None, # 全部不筛选 - 1: 0, # 审核中 - 2: 1, # 已通过 - 3: 2, # 未通过 - } - if status_map.get(status) is not None: - base_query = base_query.filter(zhuangtai=status_map[status]) - - # 4. 计数与分页 - total = base_query.count() - offset = (page - 1) * page_size - jilu_list = base_query.order_by('-create_time')[offset:offset + page_size] - - # 5. 预取关联数据,减少查询次数 - jilu_list = jilu_list.select_related('bankuai', 'chenghao') - # 为每条记录预取费用明细和拒绝记录(通过 related_name 或手动过滤) - # 由于 KaoheJiluFeiyong 和 KaoheJujueJilu 通过外键指向 ShenheJilu (to_field='jilu_id'), - # 可以使用 prefetch_related - jilu_ids = [j.jilu_id for j in jilu_list] - fee_dict = {} - reject_dict = {} - if jilu_ids: - # 批量获取所有费用明细 - fee_qs = KaoheJiluFeiyong.objects.filter(jilu_id__in=jilu_ids).order_by('cishu') - for fee in fee_qs: - if fee.jilu_id not in fee_dict: - fee_dict[fee.jilu_id] = [] - fee_dict[fee.jilu_id].append({'cishu': fee.cishu, 'jine': float(fee.jine)}) - - # 批量获取所有拒绝记录 - reject_qs = KaoheJujueJilu.objects.filter(jilu_id__in=jilu_ids).order_by('cishu') - for rej in reject_qs: - if rej.jilu_id not in reject_dict: - reject_dict[rej.jilu_id] = [] - reject_dict[rej.jilu_id].append({ - 'cishu': rej.cishu, - 'yuanyin': rej.yuanyin, - 'create_time': rej.create_time.strftime('%Y-%m-%d %H:%M:%S') if rej.create_time else '' - }) - - # 6. 批量获取申请人信息(昵称和头像) - applicant_ids = {j.shenqingren_id for j in jilu_list} - user_map = {} - if applicant_ids: - users = User.objects.filter(yonghuid__in=applicant_ids).select_related('dashou_profile') - for u in users: - avatar = u.avatar or '' - nicheng = '' - try: - # 打手扩展表可能有 nicheng - nicheng = u.dashou_profile.nicheng if u.dashou_profile else '' - except UserDashou.DoesNotExist: - pass - user_map[u.yonghuid] = { - 'avatar': avatar, - 'nicheng': nicheng or f'用户{u.yonghuid}' - } - - # 7. 组装数据 - records = [] - for jl in jilu_list: - user_info = user_map.get(jl.shenqingren_id, {'avatar': '', 'nicheng': '未知'}) - # 标签特效JSON - texiao_json = {} - if jl.chenghao and jl.chenghao.texiao_miaoshu: - try: - texiao_json = json.loads(jl.chenghao.texiao_miaoshu) - except: - pass - - # 汇总总缴纳费用 - fee_items = fee_dict.get(jl.jilu_id, []) - total_fee = sum(item['jine'] for item in fee_items) - - records.append({ - 'jilu_id': jl.jilu_id, - 'shenqingren_id': jl.shenqingren_id, - 'shenqingren_avatar': user_info['avatar'], - 'shenqingren_nicheng': user_info['nicheng'], - 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', - 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', - 'chenghao_texiao_json': texiao_json, - 'zhuangtai': jl.zhuangtai, # 0审核中 1通过 2未通过 3待审核 - 'cishu': jl.cishu, - 'total_fee': total_fee, - 'dashou_youxi_id': jl.dashou_youxi_id or '', - 'dashou_beizhu': jl.dashou_beizhu or '', - 'shenheguan_youxi_id': jl.shenheguan_youxi_id or '', - 'guize_neirong': jl.guize_neirong or '', - 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', - 'update_time': jl.update_time.strftime('%Y-%m-%d %H:%M:%S') if jl.update_time else '', - 'fee_list': fee_items, - 'reject_list': reject_dict.get(jl.jilu_id, []), - }) - - has_more = (page * page_size) < total - return Response({ - 'code': 0, - 'data': { - 'list': records, - 'has_more': has_more, - 'total': total - } - }) - - except Exception as e: - logger.error(f"获取考核记录异常: {str(e)}", exc_info=True) +import json +import logging +from decimal import Decimal +from django.db import models, transaction +from django.db.models import F +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from .utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan +from .models import ( + YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu, + KaoheguanBankuai, Bankuai, ShenheJilu, KaoheJiluFeiyong +) +from users.models import UserShenheguan, UserDashou +from gvsdsdk.models import User +from products.models import ShangpinLeixing + + +logger = logging.getLogger(__name__) + + +class DashouBiaoqianView(APIView): + """获取当前打手所有称号标签""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + # 查询用户的所有称号关联,一次连表取出 + relations = YonghuChenghao.objects.filter( + yonghu=yonghu + ).select_related('chenghao') + + chenghao_list = [] + for rel in relations: + ch = rel.chenghao + # 解析特效描述 JSON(前端需要对象) + texiao_json = {} + if ch.texiao_miaoshu: + try: + texiao_json = json.loads(ch.texiao_miaoshu) + except json.JSONDecodeError: + texiao_json = {} + chenghao_list.append({ + 'mingcheng': ch.mingcheng, + 'tubiao_url': ch.tubiao_url or '', + 'texiao_json': texiao_json, + }) + + return Response({ + 'code': 0, + 'msg': '成功', + 'data': {'chenghao_list': chenghao_list} + }) + except Exception as e: + logger.error(f"获取打手标签失败: {e}", exc_info=True) + return Response({'code': 1, 'msg': '服务器错误', 'data': None}) + + +class BankuaiBiaoqianView(APIView): + """获取商品类型所属板块的所有标签""" + permission_classes = [IsAuthenticated] + + def post(self, request): + leixing_id = request.data.get('leixing_id') + if not leixing_id: + return Response({'code': 1, 'msg': '缺少leixing_id'}) + + try: + leixing = ShangpinLeixing.objects.select_related('bankuai').get(id=leixing_id) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 2, 'msg': '商品类型不存在'}) + + bankuai = leixing.bankuai + if not bankuai: + return Response({'code': 0, 'data': {'biaoqian_list': []}}) + + # 获取该板块下所有类型的标签(leixing可以为dashou/shangjia等,这里先返回所有) + chenghao_list = Chenghao.objects.filter(bankuai=bankuai) + data = [] + for ch in chenghao_list: + texiao_json = {} + if ch.texiao_miaoshu: + try: + texiao_json = json.loads(ch.texiao_miaoshu) + except: + pass + data.append({ + 'id': ch.id, + 'mingcheng': ch.mingcheng, + 'texiao_json': texiao_json + }) + return Response({'code': 0, 'data': {'biaoqian_list': data}}) + + + + +# 硬编码邀请码 +KAOHEGUAN_INVITE_CODE = 'KHG2026VIP' + +class KaoheguanRegisterView(APIView): + """ + 考核官注册接口 + POST /dengji/khgzc + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + invite_code = request.data.get('inviteCode', '').strip() + + if not invite_code: + return Response({'code': 400, 'msg': '邀请码不能为空'}) + if invite_code != KAOHEGUAN_INVITE_CODE: + return Response({'code': 400, 'msg': '邀请码错误'}) + + kg = UserShenheguan.objects.filter(user=yonghu).first() + if kg: + if kg.zhuangtai == 1: + return Response({'code': 200, 'msg': '您已是考核官', 'data': self._build_data(kg)}) + else: + return Response({'code': 400, 'msg': '账号已被封禁'}) + + with transaction.atomic(): + kg = UserShenheguan.objects.create( + user=yonghu, + zhuangtai=1, + shenhe_zhuangtai=0, + shenhe_zongshu=0, + tongguo_zongshu=0, + yue=0.00, + zonge=0.00 + ) + return Response({'code': 200, 'msg': '注册成功', 'data': self._build_data(kg)}) + + except Exception as e: + logger.error(f"考核官注册失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + def _build_data(self, kg): + bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.yonghuid).select_related('bankuai') + bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs] + return { + 'zhuangtai': kg.zhuangtai, + 'shenhe_zongshu': kg.shenhe_zongshu, + 'tongguo_zongshu': kg.tongguo_zongshu, + 'yue': str(kg.yue), + 'zonge': str(kg.zonge), + 'shenhe_zhuangtai': kg.shenhe_zhuangtai, + 'bankuai_list': bankuai_list, + 'kaoheguanstatus': 1 + } + + + + + +class KaoheguanRefreshView(APIView): + """ + 考核官数据刷新接口 + POST /dengji/khgsx + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + kg = UserShenheguan.objects.filter(user=yonghu).first() + if not kg: + return Response({'code': 400, 'msg': '您还未注册考核官'}) + + if kg.zhuangtai != 1: + return Response({'code': 200, 'data': {'zhuangtai': kg.zhuangtai, 'kaoheguanstatus': 0}}) + + # 查询所属板块 + bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.yonghuid).select_related('bankuai') + bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs] + + # 统计待审核 / 审核中的记录数量 + pending_count = ShenheJilu.objects.filter( + shenheguan_id=yonghu.yonghuid, + zhuangtai=0 # 审核中 + ).count() + + data = { + 'zhuangtai': kg.zhuangtai, + 'shenhe_zongshu': kg.shenhe_zongshu, + 'tongguo_zongshu': kg.tongguo_zongshu, + 'yue': str(kg.yue), + 'zonge': str(kg.zonge), + 'shenhe_zhuangtai': kg.shenhe_zhuangtai, + 'bankuai_list': bankuai_list, + 'kaoheguanstatus': 1, + 'is_renzheng': kg.is_renzheng, # 🆕 是否认证 + 'pending_shenhe_count': pending_count # 🆕 审核中数量 + } + return Response({'code': 200, 'data': data}) + + except Exception as e: + logger.error(f"考核官刷新失败: {e}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + +class ShenheDatingModuleView(APIView): + """审核大厅获取模块列表(板块 + 待审核数量 + 是否关联)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + related_ids = set(KaoheguanBankuai.objects.filter(kaoheguan_id=yonghu.yonghuid) + .values_list('bankuai_id', flat=True)) + + bankuai_list = [] + for bk in Bankuai.objects.all(): + count = ShenheJilu.objects.filter(bankuai=bk, zhuangtai=3, shenheguan_id__isnull=True).count() + bankuai_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'count': count, + 'is_related': bk.bankuai_id in related_ids + }) + + return Response({'code': 0, 'data': {'modules': bankuai_list}}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + +class ShenheDatingDataView(APIView): + """获取审核大厅待审核记录列表""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + data = request.data + bankuai_id = data.get('bankuai_id') + page = int(data.get('page', 1)) + page_size = int(data.get('page_size', 5)) + + query = ShenheJilu.objects.filter(zhuangtai=3, shenheguan_id__isnull=True) + if bankuai_id: + query = query.filter(bankuai_id=bankuai_id) + + total = query.count() + offset = (page - 1) * page_size + jilu_list = query.select_related('bankuai', 'chenghao').order_by('-create_time')[ + offset:offset + page_size] + + records = [] + for jl in jilu_list: + user = User.objects.filter(UserUID=jl.shenqingren_id).first() + avatar = user.avatar if user else '' + texiao_json = {} + if jl.chenghao and jl.chenghao.texiao_miaoshu: + try: + texiao_json = json.loads(jl.chenghao.texiao_miaoshu) + except: + pass + + # 新增字段:本次考核费用 + kaohe_feiyong = float(jl.jiaofei_jine) if jl.jiaofei_jine is not None else 0.0 + + records.append({ + 'jilu_id': jl.jilu_id, + 'shenqingren_id': jl.shenqingren_id, + 'shenqingren_avatar': avatar, + 'dashou_youxi_id': jl.dashou_youxi_id or '', + 'dashou_beizhu': jl.dashou_beizhu or '', + 'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None, + 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', + 'chenghao_id': jl.chenghao_id, + 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', + 'chenghao_texiao_json': texiao_json, + 'guize_neirong': jl.guize_neirong or '', + 'kaohe_feiyong': kaohe_feiyong, # 新增 + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', + }) + + has_more = (page * page_size) < total + return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + +class ShenheQiangdanView(APIView): + """ + 审核官抢单(接审核任务) + 校验:身份、账号封禁、已有进行中审核、记录状态、不能审核自己、板块权限 + 成功后更新考核官扩展表:审核总数+1,审核状态设为“审核中” + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + jilu_id = request.data.get('jilu_id') + if not jilu_id: + return Response({'code': 400, 'msg': '记录ID不能为空'}) + + # 1. 检查考核官身份及账号是否被禁用 + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '您不是考核官或账号已被禁用'}) + + # 2. 检查是否有正在审核中的记录(zhuangtai=0),防止同时审核多个 + ongoing = ShenheJilu.objects.filter( + shenheguan_id=yonghu.yonghuid, zhuangtai=0 + ).exists() + if ongoing: + return Response({'code': 400, 'msg': '您有正在审核中的任务,请完成后再接新任务'}) + + with transaction.atomic(): + # 3. 行锁抢单:仅当状态为待审核(3)且无审核官时才能抢 + jilu = ShenheJilu.objects.select_for_update().filter( + jilu_id=jilu_id, + zhuangtai=3, + shenheguan_id__isnull=True + ).first() + if not jilu: + return Response({'code': 400, 'msg': '该审核任务已被接走、状态变更或不存在'}) + + # 4. 不能审核自己 + if jilu.shenqingren_id == yonghu.yonghuid: + return Response({'code': 400, 'msg': '不能审核自己提交的申请'}) + + # 5. 板块权限:考核官必须属于该板块 + is_related = KaoheguanBankuai.objects.filter( + kaoheguan_id=yonghu.yonghuid, + bankuai=jilu.bankuai + ).exists() + if not is_related: + return Response({'code': 400, 'msg': '您不属于该板块,无法审核'}) + + # 新增:6. 费用>0时,要求考核官已认证 + if jilu.jiaofei_jine and float(jilu.jiaofei_jine) > 0: + if not kg.is_renzheng: + return Response({'code': 400, 'msg': '本考核需要认证考核官接单,您尚未认证'}) + + # 7. 更新审核记录:分配给当前考核官,状态变为审核中 + jilu.zhuangtai = 0 # 审核中 + jilu.shenheguan_id = yonghu.yonghuid + jilu.save() + + # 8. 更新考核官扩展表:审核总数 +1,审核状态置为审核中(1) + UserShenheguan.objects.filter(user=yonghu).update( + shenhe_zongshu=F('shenhe_zongshu') + 1, + shenhe_zhuangtai=1 + ) + + return Response({ + 'code': 0, + 'msg': '抢单成功,请前往游戏内审核,审核完成后请到考核打分页面进行评定。', + 'data': {'bankuai_id': jilu.bankuai.bankuai_id} + }) + + except Exception as e: + logger.error(f"审核官抢单异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) + + + + + + + +class ShenheXiangqingView(APIView): + """获取当前考核官审核中的记录详情(新增待收考核费字段)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '您不是考核官或账号异常'}) + + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 10)) + + # 查询当前考核官名下状态为“审核中”的记录 + base_query = ShenheJilu.objects.filter( + shenheguan_id=yonghu.yonghuid, + zhuangtai=0 + ).select_related('bankuai', 'chenghao') + total = base_query.count() + offset = (page - 1) * page_size + jilu_list = base_query[offset:offset + page_size] + + records = [] + for jl in jilu_list: + user = User.objects.filter(UserUID=jl.shenqingren_id).first() + avatar = user.avatar if user else '' + texiao_json = {} + if jl.chenghao and jl.chenghao.texiao_miaoshu: + try: + texiao_json = json.loads(jl.chenghao.texiao_miaoshu) + except: + pass + + # 是否开启金牌 + is_jinpai = jl.chenghao.kaioi_jinpai if jl.chenghao else False + + # 本次待收考核费(基于记录中的当前次数,从费用明细表查询) + daishou_fei = 0.00 + fee_detail = KaoheJiluFeiyong.objects.filter( + jilu=jl, + cishu=jl.cishu + ).first() + if fee_detail: + daishou_fei = float(fee_detail.jine) + else: + # 兜底:若明细表无记录,尝试用通用费用表计算 + daishou_fei = 0.00 + # 上次失败原因 + last_fail_reason = '' + last_fail = KaoheJujueJilu.objects.filter( + jilu=jl, + cishu=jl.cishu - 1 + ).order_by('-create_time').first() + if last_fail: + last_fail_reason = last_fail.yuanyin + + records.append({ + 'jilu_id': jl.jilu_id, + 'shenqingren_id': jl.shenqingren_id, + 'shenqingren_avatar': avatar, + 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', + 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', + 'chenghao_texiao_json': texiao_json, + 'guize_neirong': jl.guize_neirong or '', + 'dashou_beizhu': jl.dashou_beizhu or '', + 'dashou_youxi_id': jl.dashou_youxi_id or '', + 'shenheguan_youxi_id': jl.shenheguan_youxi_id or '', + 'cishu': jl.cishu, + 'daishou_fei': daishou_fei, # 新增:待收考核费 + 'last_fail_reason': last_fail_reason, + 'is_jinpai': is_jinpai, + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', + }) + + has_more = (page * page_size) < total + return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + +class ShenheCaozuoView(APIView): + """ + 考核官执行操作(同意/拒绝/转移/修改昵称),并更新考核官收益 + - 只有“同意”或“拒绝”才增加余额和总额,金额严格从 KaoheJiluFeiyong 表获取 + - 查不到费用则加 0 元,绝不影响流程 + - 转移、修改昵称等操作不涉及收益 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + # 1. 验证考核官身份及账号状态 + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '账号异常,无法操作'}) + + jilu_id = request.data.get('jilu_id') + caozuo = request.data.get('caozuo') # agree / reject / transfer / modify_nick + if not jilu_id or not caozuo: + return Response({'code': 400, 'msg': '缺少必要参数'}) + + with transaction.atomic(): + # 2. 锁定审核记录,确保属于当前考核官且状态为审核中(0) + jilu = ShenheJilu.objects.select_for_update().filter( + jilu_id=jilu_id, + shenheguan_id=yonghu.yonghuid, + zhuangtai=0 + ).first() + if not jilu: + return Response({'code': 400, 'msg': '该记录不存在或不属于您'}) + + # 3. 本次操作产生的收入(仅通过/拒绝时有意义) + daishou_fei = 0.00 + + # 4. 根据操作类型执行对应业务逻辑 + if caozuo == 'agree': + # --- 通过考核 --- + # 查询本次考核应得的费用(基于记录ID和当前次数) + fee_detail = KaoheJiluFeiyong.objects.filter( + jilu=jilu, + cishu=jilu.cishu + ).first() + if fee_detail: + daishou_fei = float(fee_detail.jine) + # 找不到就保持 0,绝不使用任何默认值 + + jilu.zhuangtai = 1 + jilu.save() + + # 授予标签 + if jilu.chenghao: + yh = User.objects.get(UserUID=jilu.shenqingren_id) + if not YonghuChenghao.objects.filter(yonghu=yh, chenghao=jilu.chenghao).exists(): + YonghuChenghao.objects.create(yonghu=yh, chenghao=jilu.chenghao) + # 如果标签开启了金牌,同步更新打手称号 + if jilu.chenghao.kaioi_jinpai: + dashou_prof = UserDashou.objects.filter(user=yh).first() + if dashou_prof: + dashou_prof.chenghao = '金牌选手' + dashou_prof.save() + + elif caozuo == 'reject': + # --- 拒绝考核 --- + yuanyin = request.data.get('yuanyin', '') + if not yuanyin.strip(): + return Response({'code': 400, 'msg': '请填写拒绝原因'}) + + # 查询本次考核应得的费用(拒绝时同样获得收入) + fee_detail = KaoheJiluFeiyong.objects.filter( + jilu=jilu, + cishu=jilu.cishu + ).first() + if fee_detail: + daishou_fei = float(fee_detail.jine) + # 找不到就保持 0 + + jilu.zhuangtai = 2 + jilu.save() + # 记录拒绝原因 + KaoheJujueJilu.objects.create( + jilu=jilu, + cishu=jilu.cishu, + shenheguan_id=yonghu.yonghuid, + yuanyin=yuanyin + ) + + # 新增:不合格时,如果考核官未认证,则清空考核官ID和游戏昵称 + if not kg.is_renzheng: + jilu.shenheguan_id = None + jilu.shenheguan_youxi_id = '' + jilu.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id']) + + elif caozuo == 'transfer': + # 转移考核官:清空考核官信息,状态回退为待审核 + jilu.shenheguan_id = None + jilu.shenheguan_youxi_id = None + jilu.zhuangtai = 3 + jilu.save() + # 转移不更新收益,直接返回 + + + elif caozuo == 'modify_nick': + # 修改考核官游戏昵称 + youxi_id = request.data.get('youxi_id', '').strip() + if not youxi_id: + return Response({'code': 400, 'msg': '请输入游戏昵称'}) + jilu.shenheguan_youxi_id = youxi_id + jilu.save() + return Response({'code': 0, 'msg': '修改成功'}) + + else: + return Response({'code': 400, 'msg': '无效的操作类型'}) + + # 5. 更新考核官收益(仅“通过”或“拒绝”时执行) + if caozuo in ('transfer'): + update_fields = { + + 'shenhe_zhuangtai': 0, # 设为空闲 + } + UserShenheguan.objects.filter(user=yonghu).update(**update_fields) + if caozuo in ('agree', 'reject'): + update_fields = { + 'yue': F('yue') + Decimal(str(daishou_fei)), + 'zonge': F('zonge') + Decimal(str(daishou_fei)), + 'shenhe_zhuangtai': 0, # 设为空闲 + } + # 只有同意时才增加“通过总数” + if caozuo == 'agree': + update_fields['tongguo_zongshu'] = F('tongguo_zongshu') + 1 + + # 审核总数不再增加 + UserShenheguan.objects.filter(user=yonghu).update(**update_fields) + + return Response({'code': 0, 'msg': '操作成功'}) + + except Exception as e: + logger.error(f"考核操作异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) + + + + +# dengji/views.py +class KaoheJinpaiTagsView(APIView): + """获取考核金牌页面的标签和板块信息""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + # 获取用户已拥有的标签名称集合 + owned_names = set( + YonghuChenghao.objects.filter(yonghu=user, chenghao__leixing='dashou') + .values_list('chenghao__mingcheng', flat=True) + ) + + # 板块列表 + plate_list = [] + for bk in Bankuai.objects.all(): + tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou') + tag_list = [] + for tag in tags: + # 费用列表 + fee_objs = KaoheCishuFeiyong.objects.filter(chenghao=tag).order_by('cishu') + fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)} for f in fee_objs] + + # 若没有记录,补充默认费用 + if not fee_list: + fee_list = [{'cishu': 1, 'feiyong': 5.00}] + + tag_list.append({ + 'id': tag.id, + 'name': tag.mingcheng, + 'texiao_json': json.loads(tag.texiao_miaoshu) if tag.texiao_miaoshu else {}, + 'jinpai': tag.kaioi_jinpai, + 'rule': tag.kaohe_guize or '', + 'fee_list': fee_list, + 'owned': tag.mingcheng in owned_names, + }) + + if tag_list: + plate_list.append({ + 'bankuai_id': bk.bankuai_id, + 'mingcheng': bk.mingcheng, + 'tags': tag_list + }) + + # 新增:返回所有在职考核官信息(用于指定考核官) + kaoheguan_list = [] + for kg in UserShenheguan.objects.filter(zhuangtai=1).select_related('user'): + uid = kg.user.yonghuid + avatar = kg.user.avatar or '' + # 获取考核官的打手昵称(从打手扩展表) + dashou_nick = '' + dashou_prof = UserDashou.objects.filter(user=kg.user).first() + if dashou_prof: + dashou_nick = dashou_prof.nicheng or '' + kaoheguan_list.append({ + 'yonghuid': uid, + 'avatar': avatar, + 'dashou_nick': dashou_nick, + 'is_renzheng': kg.is_renzheng, + 'shenhe_zhuangtai': kg.shenhe_zhuangtai, + }) + + return Response({ + 'code': 0, + 'data': { + 'plate_list': plate_list, + 'my_tags': list(owned_names), + 'kaoheguan_list': kaoheguan_list, # 新增 + } + }) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + + +class KaoheJiluView(APIView): + """获取打手本人的考核记录(支持筛选状态)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + status = request.data.get('status', 'pending') # pending / passed + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 5)) + + if status == 'pending': + # 进行中/未通过:审核中(0)、待审核(3)、未通过(2) + query = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + zhuangtai__in=[0, 2, 3] + ) + else: + query = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, + zhuangtai=1 # 已通过 + ) + + total = query.count() + records = query.order_by('-create_time')[(page-1)*page_size : page*page_size] + + data = [] + for jl in records: + # 汇总缴纳总额 + total_fee = KaoheJiluFeiyong.objects.filter(jilu=jl).aggregate( + total=models.Sum('jine') + )['total'] or 0.00 + + # 上次拒绝原因(最近一条) + last_fail = KaoheJujueJilu.objects.filter(jilu=jl).order_by('-create_time').first() + fail_reason = last_fail.yuanyin if last_fail else '' + + # 考核官头像 + kg_user = User.objects.filter(UserUID=jl.shenheguan_id).first() + kg_avatar = kg_user.avatar if kg_user else '' + + data.append({ + 'jilu_id': jl.jilu_id, + 'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '', + 'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '', + 'zhuangtai': jl.zhuangtai, + 'statusText': dict(ShenheJilu._meta.get_field('zhuangtai').choices).get(jl.zhuangtai, '未知'), + 'statusColor': {0: '#FFA500', 1: '#27ae60', 2: '#e74c3c', 3: '#3498db'}.get(jl.zhuangtai, '#888'), + 'shenheguan_id': jl.shenheguan_id, + 'shenheguan_avatar': kg_avatar, + 'dashou_youxi_id': jl.dashou_youxi_id, + 'dashou_beizhu': jl.dashou_beizhu, + 'cishu': jl.cishu, + 'total_fee': float(total_fee), + 'last_fail_reason': fail_reason, + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S'), + # 为再次申请预留标签对象数据(前端可能用到) + 'tag_obj': { + 'id': jl.chenghao_id, + 'name': jl.chenghao.mingcheng if jl.chenghao else '', + # 其他字段按需,但申请时会重新拉取 + } if jl.chenghao else None, + }) + + has_more = (page * page_size) < total + return Response({ + 'code': 0, + 'data': {'list': data, 'has_more': has_more, 'total': total} + }) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + + + + +class KaoheFreeApplyView(APIView): + """免费申请考核(首次/再次,费用为0时调用)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + user = request.user + tag_id = request.data.get('tag_id') + game_nick = request.data.get('game_nick', '').strip() + remark = request.data.get('remark', '').strip() + reapply = request.data.get('reapply', False) + jilu_id = request.data.get('jilu_id') if reapply else None + shenheguan_id = request.data.get('shenheguan_id', '').strip() # 新增 + + if not tag_id or not game_nick: + return Response({'code': 400, 'msg': '标签ID和游戏昵称必填'}) + + # 校验费用是否为0 + try: + chenghao = Chenghao.objects.get(id=tag_id) + except Chenghao.DoesNotExist: + return Response({'code': 400, 'msg': '标签不存在'}) + + # 计算本次次数 + if reapply and jilu_id: + last_record = ShenheJilu.objects.filter( + jilu_id=jilu_id, shenqingren_id=user.yonghuid, zhuangtai=2 + ).first() + if not last_record: + return Response({'code': 400, 'msg': '原记录不存在或状态不是未通过'}) + new_cishu = last_record.cishu + 1 + else: + max_cishu = ShenheJilu.objects.filter( + shenqingren_id=user.yonghuid, chenghao=chenghao, zhuangtai__in=[1,2] + ).aggregate(max=models.Max('cishu'))['max'] or 0 + new_cishu = max_cishu + 1 + + fee = get_tag_fee(chenghao, new_cishu) + if fee > 0: + return Response({'code': 400, 'msg': '您有未通过的记录请查看并重考'}) + + # 新增:如果指定了考核官,进行合法性校验 + if shenheguan_id: + valid, err = validate_shenheguan(shenheguan_id, chenghao.bankuai, fee, user) + if not valid: + return Response({'code': 400, 'msg': err}) + + # 调用公共创建逻辑 + success, msg, record = create_shenhe_jilu( + user, tag_id, game_nick, remark, reapply, jilu_id, + shenheguan_id=shenheguan_id # 新增参数 + ) + if success: + return Response({'code': 0, 'msg': msg, 'data': {'jilu_id': record.jilu_id}}) + else: + return Response({'code': 400, 'msg': msg}) + except Exception as e: + return Response({'code': 500, 'msg': str(e)}, status=500) + + + + + + +class KaoheJiluListView(APIView): + """ + 获取当前考核官的历史审核记录 + 路径:POST /dengji/khghqkhjl + 权限:JWT Token (考核官) + 参数: + bankuai_id : int (可选,0或空表示全部) + status : int (可选,0-全部 1-审核中 2-已通过 3-未通过) + page : int (默认1) + page_size : int (默认5) + 返回: + list, has_more, total + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + yonghu = request.user + # 1. 验证考核官身份及账号状态 + kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first() + if not kg: + return Response({'code': 403, 'msg': '您不是考核官或账号异常'}) + + # 2. 获取参数 + bankuai_id = request.data.get('bankuai_id') + status = int(request.data.get('status', 0)) # 0全部 1审核中 2已通过 3未通过 + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 5)) + + # 3. 构建查询条件:只查当前考核官审核的记录 + base_query = ShenheJilu.objects.filter(shenheguan_id=yonghu.yonghuid) + + # 板块筛选 + if bankuai_id and int(bankuai_id) > 0: + base_query = base_query.filter(bankuai_id=int(bankuai_id)) + + # 状态筛选(前端传的状态映射到后端zhuangtai字段) + status_map = { + 0: None, # 全部不筛选 + 1: 0, # 审核中 + 2: 1, # 已通过 + 3: 2, # 未通过 + } + if status_map.get(status) is not None: + base_query = base_query.filter(zhuangtai=status_map[status]) + + # 4. 计数与分页 + total = base_query.count() + offset = (page - 1) * page_size + jilu_list = base_query.order_by('-create_time')[offset:offset + page_size] + + # 5. 预取关联数据,减少查询次数 + jilu_list = jilu_list.select_related('bankuai', 'chenghao') + # 为每条记录预取费用明细和拒绝记录(通过 related_name 或手动过滤) + # 由于 KaoheJiluFeiyong 和 KaoheJujueJilu 通过外键指向 ShenheJilu (to_field='jilu_id'), + # 可以使用 prefetch_related + jilu_ids = [j.jilu_id for j in jilu_list] + fee_dict = {} + reject_dict = {} + if jilu_ids: + # 批量获取所有费用明细 + fee_qs = KaoheJiluFeiyong.objects.filter(jilu_id__in=jilu_ids).order_by('cishu') + for fee in fee_qs: + if fee.jilu_id not in fee_dict: + fee_dict[fee.jilu_id] = [] + fee_dict[fee.jilu_id].append({'cishu': fee.cishu, 'jine': float(fee.jine)}) + + # 批量获取所有拒绝记录 + reject_qs = KaoheJujueJilu.objects.filter(jilu_id__in=jilu_ids).order_by('cishu') + for rej in reject_qs: + if rej.jilu_id not in reject_dict: + reject_dict[rej.jilu_id] = [] + reject_dict[rej.jilu_id].append({ + 'cishu': rej.cishu, + 'yuanyin': rej.yuanyin, + 'create_time': rej.create_time.strftime('%Y-%m-%d %H:%M:%S') if rej.create_time else '' + }) + + # 6. 批量获取申请人信息(昵称和头像) + applicant_ids = {j.shenqingren_id for j in jilu_list} + user_map = {} + if applicant_ids: + users = User.objects.filter(UserUID__in=applicant_ids).select_related('dashou_profile') + for u in users: + avatar = u.avatar or '' + nicheng = '' + try: + # 打手扩展表可能有 nicheng + nicheng = u.dashou_profile.nicheng if u.dashou_profile else '' + except UserDashou.DoesNotExist: + pass + user_map[u.yonghuid] = { + 'avatar': avatar, + 'nicheng': nicheng or f'用户{u.yonghuid}' + } + + # 7. 组装数据 + records = [] + for jl in jilu_list: + user_info = user_map.get(jl.shenqingren_id, {'avatar': '', 'nicheng': '未知'}) + # 标签特效JSON + texiao_json = {} + if jl.chenghao and jl.chenghao.texiao_miaoshu: + try: + texiao_json = json.loads(jl.chenghao.texiao_miaoshu) + except: + pass + + # 汇总总缴纳费用 + fee_items = fee_dict.get(jl.jilu_id, []) + total_fee = sum(item['jine'] for item in fee_items) + + records.append({ + 'jilu_id': jl.jilu_id, + 'shenqingren_id': jl.shenqingren_id, + 'shenqingren_avatar': user_info['avatar'], + 'shenqingren_nicheng': user_info['nicheng'], + 'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '', + 'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '', + 'chenghao_texiao_json': texiao_json, + 'zhuangtai': jl.zhuangtai, # 0审核中 1通过 2未通过 3待审核 + 'cishu': jl.cishu, + 'total_fee': total_fee, + 'dashou_youxi_id': jl.dashou_youxi_id or '', + 'dashou_beizhu': jl.dashou_beizhu or '', + 'shenheguan_youxi_id': jl.shenheguan_youxi_id or '', + 'guize_neirong': jl.guize_neirong or '', + 'create_time': jl.create_time.strftime('%Y-%m-%d %H:%M:%S') if jl.create_time else '', + 'update_time': jl.update_time.strftime('%Y-%m-%d %H:%M:%S') if jl.update_time else '', + 'fee_list': fee_items, + 'reject_list': reject_dict.get(jl.jilu_id, []), + }) + + has_more = (page * page_size) < total + return Response({ + 'code': 0, + 'data': { + 'list': records, + 'has_more': has_more, + 'total': total + } + }) + + except Exception as e: + logger.error(f"获取考核记录异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) \ No newline at end of file diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py index d40a4da..58a6ce1 100644 --- a/shop/migrations/0001_initial.py +++ b/shop/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.27 on 2026-06-17 16:31 +# Generated by Django 4.2.27 on 2026-06-17 21:18 from django.conf import settings from django.db import migrations, models @@ -131,7 +131,7 @@ class Migration(migrations.Migration): ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('dianpu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.dianpu', verbose_name='关联店铺ID')), - ('yonghu', models.ForeignKey(db_column='UserUUID', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, to_field='UserUID', verbose_name='用户ID')), + ('yonghu', models.ForeignKey(db_column='UserUUID', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户ID')), ], options={ 'verbose_name': '用户店铺绑定关系', diff --git a/shop/models.py b/shop/models.py index a5308de..93fc4eb 100644 --- a/shop/models.py +++ b/shop/models.py @@ -149,7 +149,6 @@ class YonghuDianpuBangding(QModel): id = models.AutoField(primary_key=True, verbose_name='绑定自增ID') yonghu = models.ForeignKey( 'gvsdsdk.User', - to_field='UserUID', on_delete=models.CASCADE, db_column='UserUUID', db_index=True, diff --git a/shop/views.py b/shop/views.py index a1f0889..920de99 100644 --- a/shop/views.py +++ b/shop/views.py @@ -1,3114 +1,3114 @@ -# ==================== 标准库 ==================== -import io -import json -import time -import random -import string -import hmac -import hashlib -import requests -import logging -import traceback -import calendar -import threading -import xmltodict -from decimal import Decimal -from collections import defaultdict -from concurrent.futures import ThreadPoolExecutor, as_completed - -from django.conf import settings -from django.db import models, transaction, IntegrityError -from django.db.models import F, Sum, Count, Q, Prefetch -from gvsdsdk.fluent import db, func, FQ -from django.core.cache import cache -from django.core.paginator import Paginator, EmptyPage -from django.core.exceptions import ObjectDoesNotExist -from django.utils import timezone -from django.http import HttpResponse - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import AllowAny, IsAuthenticated -from rest_framework_simplejwt.tokens import RefreshToken - -from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client - -from .utils import verify_shop_permission -from shop.utils import verify_shop_permission, update_dianpu_daily_stat -from products.utils import update_shangpin_daily_stat -from orders.utils import update_daily_payout -from backend.utils import update_dashou_daily_by_action - -from .models import YonghuPingzheng, Dianpu, DianpuShouzhiMeiriTongji - -from users.models import UserDashou, UserShangjia, UserGuanshi, UserZuzhang -from gvsdsdk.models import User - -from shop.models import YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi - -from products.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan - -from orders.models import ( - Dingdan, DingdanPingtai, Dashoutupian, Tuikuanjilu, - OrderDashouHistory, Fadan -) - -logger = logging.getLogger(__name__) - - -class BindDianpuView(APIView): - """ - 店铺绑定接口 - 接收前端传入的店铺ID,更新用户与店铺的绑定关系 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - dianpu_id = request.data.get('dianpu_id') - if not dianpu_id: - return Response({'code': 400, 'msg': '店铺ID不能为空'}) - - user = request.user - if not hasattr(user, 'yonghuid'): - return Response({'code': 500, 'msg': '用户身份信息异常'}) - - # 先获取 User 对象(不加锁,仅用于校验存在性) - try: - user_main = User.query.get(yonghuid=user.yonghuid) - except User.DoesNotExist: - return Response({'code': 404, 'msg': '用户不存在'}) - - # 校验店铺是否存在且状态正常(不加锁) - try: - dianpu = Dianpu.query.get(id=dianpu_id) - except Dianpu.DoesNotExist: - return Response({'code': 404, 'msg': '店铺不存在'}) - - if dianpu.zhuangtai != 1: - return Response({'code': 200, 'msg': '店铺已被封禁,无法绑定'}) - - # 在事务内部进行加锁操作 - try: - with transaction.atomic(): - # 重新获取加锁的用户对象(确保并发安全) - user_main_locked = User.objects.select_for_update().get(yonghuid=user.yonghuid) - dianpu_locked = Dianpu.objects.select_for_update().get(id=dianpu_id) - - # 再次校验店铺状态(防止事务期间被修改) - if dianpu_locked.zhuangtai != 1: - return Response({'code': 200, 'msg': '店铺已被封禁,无法绑定'}) - - # 处理绑定关系 - try: - existing_binding = YonghuDianpuBangding.objects.select_for_update().get(yonghu=user_main_locked) - old_dianpu = existing_binding.dianpu - if old_dianpu.id == dianpu_locked.id: - return Response({'code': 200, 'msg': '已绑定该店铺'}) - - # 旧店铺绑定人数减1 - old_dianpu.bangding_yonghushu = F('bangding_yonghushu') - 1 - old_dianpu.save(update_fields=['bangding_yonghushu']) - - # 更新绑定关系 - existing_binding.dianpu = dianpu_locked - existing_binding.save() - - except YonghuDianpuBangding.DoesNotExist: - YonghuDianpuBangding.query.create(yonghu=user_main_locked, dianpu=dianpu_locked) - - # 新店铺绑定人数加1 - dianpu_locked.bangding_yonghushu = F('bangding_yonghushu') + 1 - dianpu_locked.save(update_fields=['bangding_yonghushu']) - - logger.info(f"用户 {user.yonghuid} 绑定店铺 {dianpu_id} 成功") - return Response({'code': 200, 'msg': '绑定成功'}) - - except Exception as e: - logger.exception(f"绑定店铺失败: {e}") - return Response({'code': 500, 'msg': '绑定失败,服务器内部错误'}) - - - - - - - -class ShopGoodsView(APIView): - """ - 获取店铺商品数据(类型、专区、商品列表) - 根据用户绑定店铺状态及身份,返回店铺专属或公共商品 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - user = request.user - try: - user_main = User.query.get(yonghuid=user.yonghuid) - except User.DoesNotExist: - return Response({'code': 404, 'msg': '用户不存在'}) - - # 1. 查询用户店铺绑定关系 - binding = YonghuDianpuBangding.query.filter( - yonghu=user_main - ).select_related('dianpu').first() - - # 2. 判断用户是否拥有特殊身份 - has_special = self._has_special_identity(user_main) - - # 3. 根据绑定关系及店铺状态决定数据获取策略 - if binding: - dianpu = binding.dianpu - if dianpu.zhuangtai == 1: - # 店铺正常 -> 返回店铺专属商品(需通过平台审核) - data = self._get_shop_goods(dianpu) - else: - # 店铺被封 -> 返回公共商品,有特殊身份则返回审核状态为1的,否则返回2的 - data = self._get_public_goods(True) - else: - # 无绑定店铺,根据身份决定返回公共商品的审核状态 - data = self._get_public_goods(has_special) - - return Response({'code': 200, 'data': data}) - - def _has_special_identity(self, user_main): - """判断用户是否拥有打手/商家/管事/组长任一身份""" - return (UserDashou.query.filter(user=user_main).exists() or - UserShangjia.query.filter(user=user_main).exists() or - UserGuanshi.query.filter(user=user_main).exists() or - UserZuzhang.query.filter(user=user_main).exists()) - - def _get_shop_goods(self, dianpu): - """ - 获取店铺专属商品 - 类型、专区、商品均从店铺相关表中获取,且需通过平台审核(shenhe_zhuangtai=True) - """ - # 1. 店铺商品类型:未封禁、已上架、平台审核通过 - leixing_queryset = ShangpinLeixingDianpu.query.filter( - dianpu=dianpu, - fengjin_zhuangtai=False, - shangjia_zhuangtai=True, - shenhe_zhuangtai=True # 🔥 必须通过平台审核 - ).select_related('gonggong_leixing').order_by('-paixu', 'id') - - leixing_data = [] - for lx in leixing_queryset: - leixing_data.append({ - 'id': lx.id, # 店铺类型ID,前端归类用 - 'jieshao': lx.jieshao, - 'tupian_url': lx.tupian_url, - 'paixu': lx.paixu - }) - - leixing_ids = [lx['id'] for lx in leixing_data] - - # 2. 专区:根据店铺和店铺类型ID,未封禁、已上架 - if leixing_ids: - zhuanqu_queryset = ShangpinZhuanqu.query.filter( - dianpu=dianpu, - dianpu_leixing_id__in=leixing_ids, - fengjin_zhuangtai=False, - shangjia_zhuangtai=True - ).order_by('-paixu', 'id') - else: - zhuanqu_queryset = ShangpinZhuanqu.query.none() - - zhuanqu_data = [] - for zq in zhuanqu_queryset: - zhuanqu_data.append({ - 'id': zq.id, - 'mingzi': zq.mingzi, - 'leixing_id': zq.dianpu_leixing_id, # 对应店铺类型ID - 'paixu': zq.paixu - }) - - # 3. 商品:根据店铺、店铺类型、专区,未封禁、已上架、平台审核通过、原有审核状态为正常 - zhuanqu_ids = [zq['id'] for zq in zhuanqu_data] - if zhuanqu_ids: - shangpin_queryset = Shangpin.query.filter( - dianpu=dianpu, - dianpu_leixing_id__in=leixing_ids, - zhuanqu_id__in=zhuanqu_ids, - fengjin_zhuangtai=False, - shangjia_zhuangtai=True, - shenhe_zhuangtai=True, # 🔥 平台审核通过 - shenhezhuangtai=1 # 原有审核状态:正常 - ).values( - 'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu', - 'dianpu_leixing_id', 'zhuanqu_id' - ).order_by('-paixu', 'id') - - shangpin_data = list(shangpin_queryset) - # 字段映射:前端统一使用 leixing_id - for sp in shangpin_data: - sp['leixing_id'] = sp.pop('dianpu_leixing_id') - else: - shangpin_data = [] - - return { - 'shangpinleixing': leixing_data, - 'shangpinzhuanqu': zhuanqu_data, - 'shangpinliebiao': shangpin_data - } - - def _get_public_goods(self, use_audit_status_one=True): - """ - 获取公共商品 - 类型、专区、商品均从公共表获取。 - - 必须满足 dianpu 为空且 dianpu_leixing 为空(即纯公共数据)。 - - 审核状态:若 use_audit_status_one=True,则返回 shenhezhuangtai=1 的数据; - 否则返回 shenhezhuangtai=2 的数据(审核专用)。 - """ - target_shenhe = 1 if use_audit_status_one else 2 - - # 1. 公共商品类型:原有审核状态为 target_shenhe - leixing_queryset = ShangpinLeixing.query.filter( - shenhezhuangtai=target_shenhe - ).order_by('-paixu', 'id') - - leixing_data = [] - for lx in leixing_queryset: - leixing_data.append({ - 'id': lx.id, - 'jieshao': lx.jieshao, - 'tupian_url': lx.tupian_url, - 'paixu': lx.paixu - }) - - visible_leixing_ids = [lx['id'] for lx in leixing_data] - - # 2. 专区:dianpu 为空,且 dianpu_leixing 为空,leixing_id 在可见范围内,原有审核状态为 target_shenhe - if visible_leixing_ids: - zhuanqu_queryset = ShangpinZhuanqu.query.filter( - dianpu__isnull=True, - dianpu_leixing__isnull=True, # 🔥 必须为空 - leixing_id__in=visible_leixing_ids, - shenhezhuangtai=target_shenhe - ).order_by('-paixu', 'id') - else: - zhuanqu_queryset = ShangpinZhuanqu.query.none() - - zhuanqu_data = [] - for zq in zhuanqu_queryset: - zhuanqu_data.append({ - 'id': zq.id, - 'mingzi': zq.mingzi, - 'leixing_id': zq.leixing_id, - 'paixu': zq.paixu - }) - - # 3. 商品:dianpu 为空,dianpu_leixing 为空,专区在可见范围内,原有审核状态为 target_shenhe - zhuanqu_ids = [zq['id'] for zq in zhuanqu_data] - if zhuanqu_ids: - shangpin_queryset = Shangpin.query.filter( - dianpu__isnull=True, - dianpu_leixing__isnull=True, # 🔥 必须为空 - leixing_id__in=visible_leixing_ids, - zhuanqu_id__in=zhuanqu_ids, - shenhezhuangtai=target_shenhe - ).values( - 'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu', - 'leixing_id', 'zhuanqu_id' - ).order_by('-paixu', 'id') - shangpin_data = list(shangpin_queryset) - else: - shangpin_data = [] - - return { - 'shangpinleixing': leixing_data, - 'shangpinzhuanqu': zhuanqu_data, - 'shangpinliebiao': shangpin_data - } - - -class ShangdianLoginView(APIView): - """ - 商家后台登录接口(通过凭证直接获取店铺) - POST /shangdian/login - 请求参数: - yonghuid : 用户ID(7位) - zhanghao : 登录账号 - mima : 密码 - 返回数据: - code: 0, data: { - token: "jwt_token", - yonghuid: "1234567", - dianpu_mingcheng: "店铺名称", - dianpu_touxiang: "头像相对URL", - erweima_url: "二维码相对URL" - } - """ - permission_classes = [AllowAny] - - def post(self, request): - # 1. 获取请求参数 - yonghuid = request.data.get('yonghuid', '').strip() - zhanghao = request.data.get('zhanghao', '').strip() - mima = request.data.get('mima', '').strip() - - # 2. 参数完整性校验 - if not yonghuid or not zhanghao or not mima: - return Response({'code': 400, 'msg': '用户ID、账号、密码均不能为空'}) - - # 3. 验证用户主表是否存在 - try: - user_main = User.query.get(yonghuid=yonghuid) - except User.DoesNotExist: - logger.warning(f"登录失败:用户ID不存在 - {yonghuid}") - return Response({'code': 404, 'msg': '账号或密码错误'}) - - # 4. 通过用户ID + 账号查询凭证表 - try: - pingzheng = YonghuPingzheng.query.get( - yonghu=yonghuid, - zhanghao=zhanghao, - is_active=True - ) - except YonghuPingzheng.DoesNotExist: - logger.warning(f"登录失败:账号或密码错误 - 用户ID:{yonghuid}, 账号:{zhanghao}") - return Response({'code': 401, 'msg': '账号或密码错误'}) - - # 5. 校验密码 - if pingzheng.mima != mima: - logger.warning(f"登录失败:密码错误 - 用户ID:{yonghuid}, 账号:{zhanghao}") - return Response({'code': 401, 'msg': '账号或密码错误'}) - - # 6. 【修改点】通过凭证直接获取关联的店铺(利用 OneToOneField 反向关系) - try: - dianpu = pingzheng.dianpu - except Dianpu.DoesNotExist: - logger.error(f"登录异常:凭证 {zhanghao} 未关联任何店铺") - return Response({'code': 403, 'msg': '您尚未关联店铺,请联系管理员'}) - - # 7. 检查店铺状态 - if dianpu.zhuangtai != 1: - logger.warning(f"登录失败:店铺已被封禁 - 店铺ID:{dianpu.id}, 用户ID:{yonghuid}") - return Response({'code': 403, 'msg': '店铺已被封禁,无法登录'}) - - # 8. 生成 JWT Token - refresh = RefreshToken.for_user(user_main) - token = str(refresh.access_token) - - # 9. 更新最后登录时间 - user_main.last_login_time = timezone.now() - user_main.save(update_fields=['last_login_time']) - - # 10. 构造返回数据 - data = { - 'token': token, - 'yonghuid': user_main.yonghuid, - 'dianpu_mingcheng': dianpu.dianpu_mingcheng, - 'dianpu_touxiang': dianpu.dianpu_touxiang or '', - 'erweima_url': dianpu.erweima_url or '' - } - - logger.info(f"商家登录成功:用户ID {yonghuid} - 店铺 {dianpu.dianpu_mingcheng}") - return Response({'code': 0, 'msg': '登录成功', 'data': data}) - - - - -class ShopOverviewView(APIView): - """ - 店铺概览数据接口 - URL: /shangdian/sjdpgl - Method: POST - 权限: JWT认证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 从请求体中获取前端传递的 userId(用于防越权) - frontend_user_id = request.data.get('userId') - if not frontend_user_id: - return Response( - {'code': 400, 'msg': '缺少必要参数 userId'} - ) - - # 调用公共验证方法 - dianpu, error_response = verify_shop_permission(request, frontend_user_id) - if error_response: - return error_response - - # 验证通过,构造返回数据(新增字段,其余保持不变) - data = { - '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 '', - 'create_time': dianpu.create_time.isoformat() if dianpu.create_time else '', - 'update_time': dianpu.update_time.isoformat() if dianpu.update_time else '', - # 新增字段 - 'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng, - 'shouyi_choucheng_feilv': float(dianpu.shouyi_choucheng_feilv) if dianpu.shouyi_choucheng_feilv else 0.0, - 'ketixian_yue': float(dianpu.ketixian_yue), - 'zhengqu_zonge': float(dianpu.zhengqu_zonge), - 'meiri_tixian_xiane': float(dianpu.meiri_tixian_xiane) if dianpu.meiri_tixian_xiane else 0.0, - 'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane, - } - - return Response({ - 'code': 0, - 'msg': 'success', - 'data': data - }) - - - -class QrcodeDownloadView(APIView): - """ - 店铺二维码下载接口(通过后端代理下载,解决跨域限制) - URL: /shangdian/xzewm?dianpu_id=xx - 权限: JWT认证 + 店铺归属验证 - """ - permission_classes = [IsAuthenticated] - - def get(self, request): - # 1. 身份验证与店铺校验 - frontend_user_id = request.GET.get('userId') # 前端通过查询参数传递 userId - if not frontend_user_id: - return Response({'code': 400, 'msg': '缺少必要参数 userId'}, status=400) - - dianpu, error_response = verify_shop_permission(request, frontend_user_id) - if error_response: - return error_response - - # 2. 获取店铺的二维码相对路径 - erweima_relative_url = dianpu.erweima_url - if not erweima_relative_url: - return HttpResponse('店铺暂无二维码', status=404) - - # 3. 拼接 OSS 完整访问地址(使用 settings 中的域名) - oss_domain = getattr(settings, 'COS_DOMAIN', '') - if not oss_domain: - logger.error("COS_DOMAIN 未配置") - return HttpResponse('OSS域名未配置', status=500) - - image_url = oss_domain.rstrip('/') + '/' + erweima_relative_url.lstrip('/') - - # 4. 从腾讯云 COS 获取图片内容 - try: - client = get_oss_client() - bucket = getattr(settings, 'COS_BUCKET', '') - response = client.get_object(Bucket=bucket, Key=erweima_relative_url) - image_data = response['Body'].get_raw_stream().read() - except Exception as e: - logger.exception("从COS获取二维码失败") - return HttpResponse('下载失败,请稍后重试', status=502) - - # 5. 构造下载响应,强制浏览器保存为文件 - filename = f"店铺二维码_{dianpu.dianpu_mingcheng}.png" - response = HttpResponse(image_data, content_type='image/png') - response['Content-Disposition'] = f'attachment; filename="{filename}"' - response['Content-Length'] = str(len(image_data)) - return response - - - - -class DianpuShouzhiTongjiView(APIView): - """ - 店铺每日收支统计接口(支持按日、月、年聚合) - URL: /dianpu/dpsztj - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 身份验证 - frontend_user_id = request.data.get('userId') - if not frontend_user_id: - return Response({'code': 400, 'msg': '缺少 userId'}) - - dianpu, error = verify_shop_permission(request, frontend_user_id) - if error: - return error - - granularity = request.data.get('granularity', 'day') # day / month / year - year = request.data.get('year') - month = request.data.get('month') - - # 查询当前店铺的统计记录 - qs = DianpuShouzhiMeiriTongji.query.filter(dianpu_id=dianpu.id) - - if granularity == 'day': - if not year or not month: - return Response({'code': 400, 'msg': '日颗粒度需要提供年份和月份'}) - try: - year = int(year) - month = int(month) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '年份或月份格式错误'}) - - # 取出该月每一天的数据,并按日排序 - daily_data = qs.filter(nian=year, yue=month).values('ri').annotate( - xiadan_zongliang=Sum('xiadan_zongliang'), - xiadan_zonge=Sum('xiadan_zonge'), - chengjiao_zongliang=Sum('chengjiao_zongliang'), - chengjiao_zonge=Sum('chengjiao_zonge'), - tuikuan_zongliang=Sum('tuikuan_zongliang'), - tuikuan_zonge=Sum('tuikuan_zonge'), - shouyi_zonge=Sum('shouyi_zonge'), - ).order_by('ri') - - # 补全缺失的日期(默认值为0) - days_in_month = calendar.monthrange(year, month)[1] - result = [] - for day in range(1, days_in_month + 1): - found = next((item for item in daily_data if item['ri'] == day), None) - if found: - result.append(found) - else: - result.append({ - 'ri': day, - 'xiadan_zongliang': 0, 'xiadan_zonge': 0, - 'chengjiao_zongliang': 0, 'chengjiao_zonge': 0, - 'tuikuan_zongliang': 0, 'tuikuan_zonge': 0, - 'shouyi_zonge': 0, - }) - return Response({'code': 0, 'data': result, 'type': 'day'}) - - elif granularity == 'month': - if not year: - return Response({'code': 400, 'msg': '月颗粒度需要提供年份'}) - try: - year = int(year) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '年份格式错误'}) - - # 按月份聚合 - monthly_data = qs.filter(nian=year).values('yue').annotate( - xiadan_zongliang=Sum('xiadan_zongliang'), - xiadan_zonge=Sum('xiadan_zonge'), - chengjiao_zongliang=Sum('chengjiao_zongliang'), - chengjiao_zonge=Sum('chengjiao_zonge'), - tuikuan_zongliang=Sum('tuikuan_zongliang'), - tuikuan_zonge=Sum('tuikuan_zonge'), - shouyi_zonge=Sum('shouyi_zonge'), - ).order_by('yue') - - # 补全 1~12 月 - result = [] - for m in range(1, 13): - found = next((item for item in monthly_data if item['yue'] == m), None) - if found: - result.append(found) - else: - result.append({ - 'yue': m, - 'xiadan_zongliang': 0, 'xiadan_zonge': 0, - 'chengjiao_zongliang': 0, 'chengjiao_zonge': 0, - 'tuikuan_zongliang': 0, 'tuikuan_zonge': 0, - 'shouyi_zonge': 0, - }) - return Response({'code': 0, 'data': result, 'type': 'month'}) - - elif granularity == 'year': - # 按年份聚合 - yearly_data = qs.values('nian').annotate( - xiadan_zongliang=Sum('xiadan_zongliang'), - xiadan_zonge=Sum('xiadan_zonge'), - chengjiao_zongliang=Sum('chengjiao_zongliang'), - chengjiao_zonge=Sum('chengjiao_zonge'), - tuikuan_zongliang=Sum('tuikuan_zongliang'), - tuikuan_zonge=Sum('tuikuan_zonge'), - shouyi_zonge=Sum('shouyi_zonge'), - ).order_by('nian') - return Response({'code': 0, 'data': list(yearly_data), 'type': 'year'}) - - else: - return Response({'code': 400, 'msg': '无效的颗粒度'}) - - - -class ShopQrcodeView(APIView): - """ - 店铺专属二维码生成/更新接口 - URL: /shangdian/sdscdpm - Method: POST - 权限: JWT认证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 获取前端传递的 userId - frontend_user_id = request.data.get('userId') - if not frontend_user_id: - return Response( - {'code': 400, 'msg': '缺少必要参数 userId'}, - status=status.HTTP_400_BAD_REQUEST - ) - - # 公共验证 - dianpu, error_response = verify_shop_permission(request, frontend_user_id) - if error_response: - return error_response - - # 获取缓存的微信 access_token - access_token = self._get_wx_access_token() - if not access_token: - return Response( - {'code': 500, 'msg': '获取微信 access_token 失败'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 构造小程序码请求参数 - # 注意:scene 最大32个字符,我们使用店铺ID - scene = str(dianpu.id) - page_path = "pages/index/index" # 前端小程序首页路径 - wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}' - post_data = { - 'scene': scene, - 'page': page_path, - 'width': 430, - 'auto_color': False, - 'line_color': {'r': 0, 'g': 0, 'b': 0}, - } - - # 调用微信接口生成小程序码 - try: - wx_resp = requests.post(wx_url, json=post_data, timeout=10) - except Exception as e: - logger.error(f"请求微信生成二维码接口异常: {e}") - return Response( - {'code': 500, 'msg': '调用微信服务失败'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 判断返回是否为图片 - if wx_resp.status_code != 200 or 'image' not in wx_resp.headers.get('content-type', ''): - error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {} - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信生成二维码失败: {errmsg}") - return Response( - {'code': 500, 'msg': f'生成二维码失败: {errmsg}'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 构造文件名和 COS 路径 - timestamp = int(time.time()) - # 三层文件夹结构: dianpu/shangjiadianpu/dianpuerweima/ - filename = f"dianpu_{dianpu.id}_{timestamp}.png" - oss_path = f"dianpu/shangjiadianpu/dianpuerweima/{filename}" - file_obj = io.BytesIO(wx_resp.content) - - # 上传到 COS - try: - full_url = upload_to_oss(file_obj, oss_path) - except Exception as e: - logger.error(f"上传二维码到 COS 失败: {e}") - return Response( - {'code': 500, 'msg': '图片上传失败'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - if not full_url: - return Response( - {'code': 500, 'msg': '图片上传失败'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # 提取相对路径(根据您的 upload_to_oss 返回值格式调整) - cos_domain = getattr(settings, 'COS_DOMAIN', '') - if cos_domain and full_url.startswith(cos_domain): - relative_path = full_url[len(cos_domain):].lstrip('/') - else: - relative_path = oss_path - - # 删除旧二维码文件(如果存在) - old_relative_path = dianpu.erweima_url - if old_relative_path: - try: - delete_from_oss(old_relative_path) - except Exception as e: - logger.warning(f"删除旧二维码文件失败: {old_relative_path}, 错误: {e}") - - # 更新数据库 - with transaction.atomic(): - dianpu.erweima_url = relative_path - dianpu.save(update_fields=['erweima_url', 'update_time']) - - return Response({ - 'code': 0, - 'msg': 'success', - 'data': {'erweima_url': relative_path} - }) - - def _get_wx_access_token(self): - """ - 获取微信小程序 access_token,带缓存 - 缓存键: wx_mini_access_token - """ - cache_key = 'wx_mini_access_token' - token = cache.get(cache_key) - if token: - return token - - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - if not appid or not secret: - logger.error("微信小程序 AppID 或 Secret 未配置") - return None - - url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}' - try: - resp = requests.get(url, timeout=5) - data = resp.json() - token = data.get('access_token') - expires_in = data.get('expires_in', 7200) - if token: - # 提前200秒过期,防止边界情况 - cache.set(cache_key, token, expires_in - 200) - return token - except Exception as e: - logger.error(f"获取微信 access_token 异常: {e}") - return None - - - - - - -class ShopProductConfigView(APIView): - """ - 获取商品发布所需配置:公共类型、店铺商品类型、专区及各自商品数量 - URL: /shangdian/dphqspxx - 权限: JWT认证 + 店铺归属验证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 身份验证,获取店铺对象 - frontend_user_id = request.data.get('userId') - if not frontend_user_id: - return Response({'code': 400, 'msg': '缺少必要参数 userId'}) - - dianpu, error = verify_shop_permission(request, frontend_user_id) - if error: - return error - - # ---------- 2. 公共商品类型(需审核正常) ---------- - try: - public_types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values( - 'id', 'jieshao', 'tupian_url' - ) - public_types = [{ - 'id': pt['id'], - 'jieshao': pt['jieshao'] or '', - 'tupian_url': pt['tupian_url'] or '', - } for pt in public_types_qs] - except Exception as e: - logger.exception("查询公共商品类型失败") - return Response({'code': 500, 'msg': '服务器错误'}) - - # ---------- 3. 店铺商品类型 ---------- - try: - shop_types = ShangpinLeixingDianpu.query.filter( - dianpu_id=dianpu.id - ).select_related('gonggong_leixing').order_by('-paixu') - except Exception as e: - logger.exception("查询店铺商品类型失败") - return Response({'code': 500, 'msg': '服务器错误'}) - - type_ids = [] - shop_product_types = [] - for t in shop_types: - shop_product_types.append({ - 'id': t.id, - 'jieshao': t.jieshao or '', - 'tupian_url': t.tupian_url or '', - 'paixu': t.paixu, - 'shangjia_zhuangtai': t.shangjia_zhuangtai, - 'fengjin_zhuangtai': t.fengjin_zhuangtai, - 'shenhe_zhuangtai': t.shenhe_zhuangtai, - 'gonggong_leixing_id': t.gonggong_leixing_id, - }) - type_ids.append(t.id) - - # ---------- 4. 各类型商品数量统计 ---------- - type_product_counts = {} - if type_ids: - counts = Shangpin.query.filter( - dianpu_id=dianpu.id, - dianpu_leixing_id__in=type_ids - ).values('dianpu_leixing_id').annotate(cnt=Count('id')) - type_product_counts = {item['dianpu_leixing_id']: item['cnt'] for item in counts} - - # ---------- 5. 专区列表 ---------- - try: - zhuanqu_qs = ShangpinZhuanqu.query.filter( - dianpu_id=dianpu.id - ).order_by('-paixu').values( - 'id', 'mingzi', 'paixu', 'shangjia_zhuangtai', - 'fengjin_zhuangtai', 'dianpu_leixing_id' - ) - zhuanqu_list = [] - zhuanqu_ids = [] - for zq in zhuanqu_qs: - zhuanqu_list.append({ - 'id': zq['id'], - 'mingzi': zq['mingzi'] or '', - 'paixu': zq['paixu'], - 'shangjia_zhuangtai': zq['shangjia_zhuangtai'], - 'fengjin_zhuangtai': zq['fengjin_zhuangtai'], - 'dianpu_leixing_id': zq['dianpu_leixing_id'], - }) - zhuanqu_ids.append(zq['id']) - except Exception as e: - logger.exception("查询专区失败") - return Response({'code': 500, 'msg': '服务器错误'}) - - # ---------- 6. 各专区商品数量统计 ---------- - zhuanqu_product_counts = {} - if zhuanqu_ids: - counts = Shangpin.query.filter( - dianpu_id=dianpu.id, - zhuanqu_id__in=zhuanqu_ids - ).values('zhuanqu_id').annotate(cnt=Count('id')) - zhuanqu_product_counts = {item['zhuanqu_id']: item['cnt'] for item in counts} - - return Response({ - 'code': 0, - 'msg': 'success', - 'data': { - 'public_types': public_types, - 'shop_product_types': shop_product_types, - 'zhuanqu_list': zhuanqu_list, - 'type_product_counts': type_product_counts, - 'zhuanqu_product_counts': zhuanqu_product_counts, - } - }) - - - - -class ShopProductModifyView(APIView): - """店铺商品类型与专区管理(增、改、删)""" - permission_classes = [IsAuthenticated] - - def post(self, request): - try: - frontend_user_id = request.data.get('userId') or request.POST.get('userId') - if not frontend_user_id: - return Response({'code': 400, 'msg': '缺少必要参数 userId'}) - - dianpu, error = verify_shop_permission(request, frontend_user_id) - if error: - return error - - action = request.data.get('action') or request.POST.get('action') - if not action: - return Response({'code': 400, 'msg': '缺少操作类型 action'}) - - handlers = { - 'add_type': self._add_type, - 'update_type': self._update_type, - 'delete_type': self._delete_type, - 'add_zhuanqu': self._add_zhuanqu, - 'update_zhuanqu': self._update_zhuanqu, - 'delete_zhuanqu': self._delete_zhuanqu, - } - handler = handlers.get(action) - if not handler: - return Response({'code': 400, 'msg': '不支持的操作类型'}) - - return handler(dianpu, request) - - except Exception as e: - logger.exception("ShopProductModifyView 全局异常") - return Response({'code': 500, 'msg': '服务器内部错误'}) - - # ---------- 商品类型操作 ---------- - @transaction.atomic - def _add_type(self, dianpu, request): - """添加店铺商品类型""" - try: - jieshao = request.data.get('jieshao') or request.POST.get('jieshao') - gonggong_leixing_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id') - paixu = request.data.get('paixu', 0) or request.POST.get('paixu', 0) - - if not jieshao or not gonggong_leixing_id: - return Response({'code': 400, 'msg': '缺少介绍或公共类型ID'}) - - # 验证公共类型存在且正常 - if not ShangpinLeixing.query.filter(id=gonggong_leixing_id, shenhezhuangtai=1).exists(): - return Response({'code': 400, 'msg': '所选公共商品类型无效'}) - - image_file = request.FILES.get('image') - if not image_file: - return Response({'code': 400, 'msg': '必须上传类型图片'}) - - # 上传图片 - file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png" - full_url = upload_to_oss(image_file, file_path) - if not full_url: - return Response({'code': 500, 'msg': '图片上传失败'}) - - # 提取相对路径 - tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) - - # 审核状态 - audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() - shenhe_status = not (audit_config and audit_config.kaiqi_shenhe) - - ShangpinLeixingDianpu.query.create( - dianpu_id=dianpu.id, - gonggong_leixing_id=gonggong_leixing_id, - jieshao=jieshao, - tupian_url=tupian_url, - paixu=paixu, - shangjia_zhuangtai=True, - fengjin_zhuangtai=False, - shenhe_zhuangtai=shenhe_status, - ) - return Response({'code': 0, 'msg': '商品类型添加成功'}) - except Exception as e: - logger.exception("添加商品类型失败") - return Response({'code': 500, 'msg': '添加失败,请稍后重试'}) - - @transaction.atomic - def _update_type(self, dianpu, request): - """修改店铺商品类型,支持更换图片(先删旧图)""" - type_id = request.data.get('type_id') or request.POST.get('type_id') - if not type_id: - return Response({'code': 400, 'msg': '缺少 type_id'}) - - try: - obj = ShangpinLeixingDianpu.objects.select_for_update().get( - id=type_id, dianpu_id=dianpu.id - ) - except ShangpinLeixingDianpu.DoesNotExist: - return Response({'code': 404, 'msg': '商品类型不存在'}) - except Exception as e: - logger.exception("查询商品类型异常") - return Response({'code': 500, 'msg': '服务器错误'}) - - try: - update_fields = [] - content_changed = False - - # 简介 - if 'jieshao' in request.data or 'jieshao' in request.POST: - obj.jieshao = request.data.get('jieshao') or request.POST.get('jieshao') - update_fields.append('jieshao') - content_changed = True - - # 映射公共类型 - if 'gonggong_leixing_id' in request.data or 'gonggong_leixing_id' in request.POST: - new_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id') - if not ShangpinLeixing.query.filter(id=new_id, shenhezhuangtai=1).exists(): - return Response({'code': 400, 'msg': '公共商品类型无效'}) - obj.gonggong_leixing_id = new_id - update_fields.append('gonggong_leixing_id') - content_changed = True - - # 排序 - if 'paixu' in request.data or 'paixu' in request.POST: - obj.paixu = request.data.get('paixu') or request.POST.get('paixu') - update_fields.append('paixu') - - # 上架状态 - if 'shangjia_zhuangtai' in request.data or 'shangjia_zhuangtai' in request.POST: - val = request.data.get('shangjia_zhuangtai') or request.POST.get('shangjia_zhuangtai') - if isinstance(val, str): - val = val.lower() in ('true', '1', 'yes') - obj.shangjia_zhuangtai = bool(val) - update_fields.append('shangjia_zhuangtai') - - # 图片更换:先删除旧图,再上传新图 - image_file = request.FILES.get('image') - if image_file: - if obj.tupian_url: - delete_from_oss(obj.tupian_url) # 删除旧图片 - file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png" - full_url = upload_to_oss(image_file, file_path) - if not full_url: - return Response({'code': 500, 'msg': '图片上传失败'}) - obj.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) - update_fields.append('tupian_url') - content_changed = True - - # 审核重置 - if content_changed: - audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() - if audit_config and audit_config.kaiqi_shenhe: - obj.shenhe_zhuangtai = False - update_fields.append('shenhe_zhuangtai') - - if update_fields: - obj.save(update_fields=update_fields) - return Response({'code': 0, 'msg': '商品类型更新成功'}) - except Exception as e: - logger.exception("修改商品类型失败") - return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) - - @transaction.atomic - def _delete_type(self, dianpu, request): - """删除店铺商品类型,可选是否同时删除关联商品""" - type_id = request.data.get('id') or request.POST.get('id') - if not type_id: - return Response({'code': 400, 'msg': '缺少类型ID'}) - - try: - obj = ShangpinLeixingDianpu.objects.select_for_update().get( - id=type_id, dianpu_id=dianpu.id - ) - except ShangpinLeixingDianpu.DoesNotExist: - return Response({'code': 404, 'msg': '商品类型不存在'}) - except Exception as e: - logger.exception("查询商品类型异常") - return Response({'code': 500, 'msg': '服务器错误'}) - - try: - delete_products = request.data.get('delete_products') or request.POST.get('delete_products') - delete_products = str(delete_products).lower() in ('true', '1', 'yes') - - if delete_products: - # 删除该类型下所有关联商品(同时清理图片) - products = Shangpin.query.filter( - dianpu_leixing_id=obj.id, dianpu_id=dianpu.id - ) - for p in products: - if p.tupian_url: - delete_from_oss(p.tupian_url) - if p.guize_tupian: - delete_from_oss(p.guize_tupian) - p.delete() - else: - # 仅解除关联,不删商品 - Shangpin.query.filter( - dianpu_leixing_id=obj.id, dianpu_id=dianpu.id - ).update(dianpu_leixing=None) - - # 删除类型自身图片 - if obj.tupian_url: - delete_from_oss(obj.tupian_url) - obj.delete() - return Response({'code': 0, 'msg': '类型删除成功'}) - except Exception as e: - logger.exception("删除商品类型失败") - return Response({'code': 500, 'msg': '删除失败,请稍后重试'}) - - # ---------- 专区操作 ---------- - @transaction.atomic - def _add_zhuanqu(self, dianpu, request): - """添加专区,自动从店铺商品类型获取 leixing_id(公共商品类型ID)""" - try: - mingzi = request.data.get('mingzi') - dianpu_leixing_id = request.data.get('dianpu_leixing_id') - paixu = request.data.get('paixu', 0) - - if not mingzi or not dianpu_leixing_id: - return Response({'code': 400, 'msg': '缺少名称或所属商品类型'}) - - # 获取店铺商品类型,并拿到映射的公共类型ID - try: - shop_type = ShangpinLeixingDianpu.query.get( - id=dianpu_leixing_id, dianpu_id=dianpu.id - ) - leixing_id = shop_type.gonggong_leixing_id # 这就是专区需要的 leixing_id - except ShangpinLeixingDianpu.DoesNotExist: - return Response({'code': 400, 'msg': '所选商品类型无效'}) - - # 验证公共类型有效 - if not ShangpinLeixing.query.filter(id=leixing_id, shenhezhuangtai=1).exists(): - return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) - - ShangpinZhuanqu.query.create( - dianpu_id=dianpu.id, - mingzi=mingzi, - dianpu_leixing_id=dianpu_leixing_id, - leixing_id=leixing_id, # 自动填充 - paixu=paixu, - shangjia_zhuangtai=True, - fengjin_zhuangtai=False, - ) - return Response({'code': 0, 'msg': '专区添加成功'}) - except Exception as e: - logger.exception("添加专区失败") - return Response({'code': 500, 'msg': '添加失败,请稍后重试'}) - - @transaction.atomic - def _update_zhuanqu(self, dianpu, request): - """修改专区,若更换所属商品类型则同步更新 leixing_id""" - zhuanqu_id = request.data.get('zhuanqu_id') - if not zhuanqu_id: - return Response({'code': 400, 'msg': '缺少 zhuanqu_id'}) - - try: - obj = ShangpinZhuanqu.objects.select_for_update().get( - id=zhuanqu_id, dianpu_id=dianpu.id - ) - except ShangpinZhuanqu.DoesNotExist: - return Response({'code': 404, 'msg': '专区不存在'}) - except Exception as e: - logger.exception("查询专区异常") - return Response({'code': 500, 'msg': '服务器错误'}) - - try: - update_fields = [] - - if 'mingzi' in request.data: - obj.mingzi = request.data.get('mingzi') - update_fields.append('mingzi') - - if 'dianpu_leixing_id' in request.data: - new_id = request.data.get('dianpu_leixing_id') - try: - shop_type = ShangpinLeixingDianpu.query.get( - id=new_id, dianpu_id=dianpu.id - ) - obj.dianpu_leixing_id = new_id - obj.leixing_id = shop_type.gonggong_leixing_id # 同步更新公共类型ID - update_fields.extend(['dianpu_leixing_id', 'leixing_id']) - except ShangpinLeixingDianpu.DoesNotExist: - return Response({'code': 400, 'msg': '所选商品类型无效'}) - - if 'paixu' in request.data: - obj.paixu = request.data.get('paixu') - update_fields.append('paixu') - - if 'shangjia_zhuangtai' in request.data: - val = request.data.get('shangjia_zhuangtai') - if isinstance(val, str): - val = val.lower() in ('true', '1', 'yes') - obj.shangjia_zhuangtai = bool(val) - update_fields.append('shangjia_zhuangtai') - - if update_fields: - obj.save(update_fields=update_fields) - return Response({'code': 0, 'msg': '专区更新成功'}) - except Exception as e: - logger.exception("修改专区失败") - return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) - - @transaction.atomic - def _delete_zhuanqu(self, dianpu, request): - """删除专区,可选是否同时删除关联商品""" - zhuanqu_id = request.data.get('id') - if not zhuanqu_id: - return Response({'code': 400, 'msg': '缺少专区ID'}) - - try: - obj = ShangpinZhuanqu.objects.select_for_update().get( - id=zhuanqu_id, dianpu_id=dianpu.id - ) - except ShangpinZhuanqu.DoesNotExist: - return Response({'code': 404, 'msg': '专区不存在'}) - except Exception as e: - logger.exception("查询专区异常") - return Response({'code': 500, 'msg': '服务器错误'}) - - try: - delete_products = request.data.get('delete_products') - delete_products = str(delete_products).lower() in ('true', '1', 'yes') - - if delete_products: - products = Shangpin.query.filter( - zhuanqu_id=obj.id, dianpu_id=dianpu.id - ) - for p in products: - if p.tupian_url: - delete_from_oss(p.tupian_url) - if p.guize_tupian: - delete_from_oss(p.guize_tupian) - p.delete() - else: - Shangpin.query.filter( - zhuanqu_id=obj.id, dianpu_id=dianpu.id - ).update(zhuanqu_id=None) - - obj.delete() - return Response({'code': 0, 'msg': '专区删除成功'}) - except Exception as e: - logger.exception("删除专区失败") - return Response({'code': 500, 'msg': '删除失败,请稍后重试'}) - - - -class ShopProductListView(APIView): - """ - 商品列表接口,支持筛选、分页。 - URL: /shangdian/sdhqspsj - 权限: JWT认证 + 店铺验证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 身份验证,获取店铺对象 - frontend_user_id = request.data.get('userId') - if not frontend_user_id: - return Response({'code': 400, 'msg': '缺少必要参数 userId'}) - - dianpu, error = verify_shop_permission(request, frontend_user_id) - if error: - return error - - # 2. 安全解析分页与筛选参数 - try: - page = int(request.data.get('page', 1)) - page_size = int(request.data.get('page_size', 20)) - except (TypeError, ValueError): - return Response({'code': 400, 'msg': '分页参数格式错误'}) - - keyword = str(request.data.get('keyword', '')).strip() - shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型ID - zhuanqu_id = request.data.get('zhuanqu_id') # 专区ID - - # 布尔型筛选字段 - def parse_bool(val): - if val is None or val == '': - return None - if isinstance(val, bool): - return val - if isinstance(val, str): - return val.lower() in ('true', '1', 'yes') - 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')) - - # 3. 构建查询(只查当前店铺的商品) - products = Shangpin.query.filter(dianpu_id=dianpu.id).select_related( - 'dianpu_leixing' - ).only( - 'id', 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'zhuanqu_id', - 'zhenshi_xiaoliang', 'duiwai_xiaoliang', 'jieshao', 'xiadan_xuzhi', - 'guize_tupian', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', - 'tupian_url', 'ewai_dashou_fencheng', 'paixu', - 'dianpu_leixing_id', 'shangjia_zhuangtai', 'fengjin_zhuangtai', - 'shenhe_zhuangtai', 'create_time', 'update_time' - ) - - # 筛选 - 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 格式错误'}) - - if zhuanqu_id is not None: - try: - products = products.filter(zhuanqu_id=int(zhuanqu_id)) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': 'zhuanqu_id 格式错误'}) - - 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、标题、介绍) - if keyword: - products = products.filter( - models.Q(id__icontains=keyword) | - models.Q(biaoqian__icontains=keyword) | - models.Q(jieshao__icontains=keyword) - ) - - # 4. 分页 - total_count = products.count() - paginator = Paginator(products, page_size) - page_obj = paginator.get_page(page) - - # 5. 构造返回数据 - 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, - 'zhuanqu_id': p.zhuanqu_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, - 'tupian_url': p.tupian_url or '', - 'ewai_dashou_fencheng': p.ewai_dashou_fencheng, - 'paixu': p.paixu, - 'dianpu_leixing_id': p.dianpu_leixing_id, - 'shangjia_zhuangtai': p.shangjia_zhuangtai, - 'fengjin_zhuangtai': p.fengjin_zhuangtai, - 'shenhe_zhuangtai': p.shenhe_zhuangtai, - 'create_time': p.create_time.isoformat() if p.create_time else '', - 'update_time': p.update_time.isoformat() if p.update_time else '', - }) - - return Response({ - 'code': 0, - 'data': { - 'list': product_list, - 'total': total_count, - } - }) - - - -class ShopProductModifyView1(APIView): - """ - 商品统一操作:修改、添加、删除 - URL: /shangdian/xgdpsplx1 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - frontend_user_id = request.data.get('userId') - if not frontend_user_id: - return Response({'code': 400, 'msg': '缺少必要参数 userId'}) - - dianpu, error = verify_shop_permission(request, frontend_user_id) - if error: - return error - - action = request.data.get('action') - if not action: - return Response({'code': 400, 'msg': '缺少操作类型 action'}) - - if action == 'update_product': - return self._update_product(dianpu, request.data, request.FILES) - elif action == 'add_product': - return self._add_product(dianpu, request.data, request.FILES) - elif action == 'delete_product': - return self._delete_product(dianpu, request.data) - else: - return Response({'code': 400, 'msg': '不支持的操作类型'}) - - # ---------- 修改商品 ---------- - @transaction.atomic - def _update_product(self, dianpu, data, files): - product_id = data.get('product_id') - if not product_id: - return Response({'code': 400, 'msg': '缺少 product_id'}) - - try: - product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id) - except Shangpin.DoesNotExist: - return Response({'code': 404, 'msg': '商品不存在'}) - - update_fields = [] - content_changed = False - - # 辅助函数:安全转换为整数,失败则返回原值(让其报错) - def to_int(val): - try: - return int(val) - except (TypeError, ValueError): - return val - - def to_float(val): - try: - return float(val) - except (TypeError, ValueError): - return val - - # 标题 - if 'biaoqian' in data: - product.biaoqian = data['biaoqian'] - update_fields.append('biaoqian') - content_changed = True - - # 价格 - if 'jiage' in data: - product.jiage = to_float(data['jiage']) - update_fields.append('jiage') - content_changed = True - - # 库存 - if 'kucun' in data: - product.kucun = to_int(data['kucun']) - update_fields.append('kucun') - content_changed = True - - # 店铺商品类型 - if 'dianpu_leixing_id' in data: - new_type_id = to_int(data['dianpu_leixing_id']) - if not ShangpinLeixingDianpu.query.filter(id=new_type_id, dianpu_id=dianpu.id).exists(): - return Response({'code': 400, 'msg': '无效的店铺商品类型'}) - product.dianpu_leixing_id = new_type_id - update_fields.append('dianpu_leixing_id') - content_changed = True - - # 专区 - if 'zhuanqu_id' in data: - new_zq_id = data['zhuanqu_id'] - if new_zq_id is not None and new_zq_id != '': - new_zq_id = to_int(new_zq_id) - if not ShangpinZhuanqu.query.filter(id=new_zq_id, dianpu_id=dianpu.id).exists(): - return Response({'code': 400, 'msg': '无效的专区'}) - product.zhuanqu_id = new_zq_id - else: - product.zhuanqu_id = None - update_fields.append('zhuanqu_id') - content_changed = True - - # 介绍 - if 'jieshao' in data: - product.jieshao = data['jieshao'] - update_fields.append('jieshao') - content_changed = True - - # 下单须知 - if 'xiadan_xuzhi' in data: - product.xiadan_xuzhi = data['xiadan_xuzhi'] - update_fields.append('xiadan_xuzhi') - - # ★ 抢单要求类型及会员/佣金自动处理 - if 'yaoqiuleixing' in data: - yqlx = to_int(data['yaoqiuleixing']) - if yqlx not in [1, 2]: - return Response({'code': 400, 'msg': '抢单要求类型无效'}) - product.yaoqiuleixing = yqlx - update_fields.append('yaoqiuleixing') - content_changed = True - - if yqlx == 1: - # 会员抢单:优先使用前端显式传递的非空会员ID - if 'huiyuan_id' in data and data.get('huiyuan_id'): - product.huiyuan_id = data['huiyuan_id'] - update_fields.append('huiyuan_id') - product.yongjin = None - if 'yongjin' not in update_fields: - update_fields.append('yongjin') - else: - # 自动从公共类型获取会员ID,或转为佣金 - try: - shop_type = ShangpinLeixingDianpu.query.get( - id=product.dianpu_leixing_id, dianpu_id=dianpu.id - ) - public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id) - auto_huiyuan_id = public_type.huiyuan_id - if auto_huiyuan_id: - product.huiyuan_id = auto_huiyuan_id - update_fields.append('huiyuan_id') - product.yongjin = None - if 'yongjin' not in update_fields: - update_fields.append('yongjin') - elif public_type.yaoqiuleixing == 2 and public_type.yongjin: - # 无会员,强制转为佣金 - product.yaoqiuleixing = 2 - product.yongjin = public_type.yongjin - product.huiyuan_id = None - # yaoqiuleixing 已在 update_fields 中,需要添加 huiyuan_id 和 yongjin - update_fields.append('huiyuan_id') - update_fields.append('yongjin') - else: - return Response({'code': 400, 'msg': '会员抢单缺少会员ID,公共类型也无会员ID或佣金'}) - except (ShangpinLeixingDianpu.DoesNotExist, ShangpinLeixing.DoesNotExist): - return Response({'code': 400, 'msg': '店铺商品类型无效'}) - elif yqlx == 2: - # 佣金抢单,清除会员ID,设置佣金 - product.huiyuan_id = None - update_fields.append('huiyuan_id') - if 'yongjin' in data: - product.yongjin = to_float(data['yongjin']) or 0 - else: - product.yongjin = 0 - update_fields.append('yongjin') - else: - # 未修改抢单类型,保留原有独立修改逻辑 - if 'huiyuan_id' in data: - product.huiyuan_id = data['huiyuan_id'] or None - update_fields.append('huiyuan_id') - content_changed = True - if 'yongjin' in data: - product.yongjin = to_float(data['yongjin']) or 0 - update_fields.append('yongjin') - content_changed = True - - # 打手额外分成金额 - if 'ewai_dashou_fencheng' in data: - fencheng = to_float(data['ewai_dashou_fencheng']) or 0 - - - if fencheng > (product.jiage or 0): - return Response({'code': 400, 'msg': '分成金额不能超过商品价格'}) - product.ewai_dashou_fencheng = fencheng - update_fields.append('ewai_dashou_fencheng') - content_changed = True - - # 排序 - if 'paixu' in data: - product.paixu = to_int(data['paixu']) - update_fields.append('paixu') - - # 上架状态(保持原逻辑,兼容字符串) - if 'shangjia_zhuangtai' in data: - val = data['shangjia_zhuangtai'] - if isinstance(val, str): - val = val.lower() in ('true', '1', 'yes') - product.shangjia_zhuangtai = bool(val) - update_fields.append('shangjia_zhuangtai') - - # ★ 商品图片真上传 - image_file = files.get('image') - if image_file: - if product.tupian_url: - delete_from_oss(product.tupian_url) - file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png" - full_url = upload_to_oss(image_file, file_path) - if not full_url: - return Response({'code': 500, 'msg': '商品图片上传失败'}) - product.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) - update_fields.append('tupian_url') - content_changed = True - - # ★ 规则图片真上传 - rule_file = files.get('rule_image') - remove_rule = data.get('remove_rule_image', '0') - if rule_file: - if product.guize_tupian: - delete_from_oss(product.guize_tupian) - file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png" - full_url = upload_to_oss(rule_file, file_path) - if full_url: - product.guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) - update_fields.append('guize_tupian') - content_changed = True - elif remove_rule == '1' and product.guize_tupian: - delete_from_oss(product.guize_tupian) - product.guize_tupian = None - update_fields.append('guize_tupian') - content_changed = True - - if content_changed: - audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() - if audit_config and audit_config.kaiqi_shenhe: - product.shenhe_zhuangtai = False - update_fields.append('shenhe_zhuangtai') - - if update_fields: - product.save(update_fields=update_fields + ['update_time']) - return Response({'code': 0, 'msg': '商品修改成功'}) - - # ---------- 添加商品(增强会员/佣金默认逻辑) ---------- - @transaction.atomic - def _add_product(self, dianpu, data, files): - biaoqian = data.get('biaoqian') - jiage = data.get('jiage') - kucun = data.get('kucun') - dianpu_leixing_id = data.get('dianpu_leixing_id') - zhuanqu_id = data.get('zhuanqu_id') - jieshao = data.get('jieshao') - - if not all([biaoqian, jiage is not None, kucun is not None, dianpu_leixing_id, zhuanqu_id, jieshao]): - return Response({'code': 400, 'msg': '必填项缺失'}) - - try: - shop_type = ShangpinLeixingDianpu.query.get(id=dianpu_leixing_id, dianpu_id=dianpu.id) - except ShangpinLeixingDianpu.DoesNotExist: - return Response({'code': 400, 'msg': '无效的店铺商品类型'}) - - try: - public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1) - except ShangpinLeixing.DoesNotExist: - return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) - - # 商品图片上传 - image_file = files.get('image') - if not image_file: - return Response({'code': 400, 'msg': '必须上传商品图片'}) - file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png" - full_url = upload_to_oss(image_file, file_path) - if not full_url: - return Response({'code': 500, 'msg': '商品图片上传失败'}) - tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) - - # 规则图片 - guize_tupian = None - rule_file = files.get('rule_image') - if rule_file: - file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png" - full_url = upload_to_oss(rule_file, file_path) - if full_url: - guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) - - yaoqiuleixing = int(data.get('yaoqiuleixing', 1)) - if yaoqiuleixing == 1: - huiyuan_id = data.get('huiyuan_id') - # 如果前端没传有效的会员ID,尝试从公共类型获取 - if not huiyuan_id: - huiyuan_id = public_type.huiyuan_id - # 仍然没有会员ID,检查公共类型是否可转为佣金 - if not huiyuan_id: - if public_type.yaoqiuleixing == 2 and public_type.yongjin: - # 自动转为佣金抢单 - yaoqiuleixing = 2 - yongjin = public_type.yongjin - huiyuan_id = None - else: - return Response({'code': 400, 'msg': '会员抢单缺少会员ID,且无法自动获取'}) - else: - yongjin = None - else: - huiyuan_id = None - yongjin = data.get('yongjin') or public_type.yongjin or 0 - - ewai_fencheng = data.get('ewai_dashou_fencheng') or 0 - - - try: - ewai_val = float(ewai_fencheng) - jiage_val = float(jiage) - if ewai_val > jiage_val: - return Response({'code': 400, 'msg': '分成金额不能超过商品价格'}) - except ValueError: - # 处理转换失败的情况,比如记录日志或设为默认值 - print("无法转换为数字") - - - - - '''if ewai_fencheng > jiage: - return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})''' - - shenhe_status = True - audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() - if audit_config and audit_config.kaiqi_shenhe: - shenhe_status = False - - product = Shangpin.query.create( - biaoqian=biaoqian, - jiage=jiage, - kucun=kucun, - dianpu_id=dianpu.id, - dianpu_leixing_id=dianpu_leixing_id, - zhuanqu_id=zhuanqu_id, - leixing_id=public_type.id, - jieshao=jieshao, - xiadan_xuzhi=data.get('xiadan_xuzhi', ''), - guize_tupian=guize_tupian, - tupian_url=tupian_url, - yaoqiuleixing=yaoqiuleixing, - huiyuan_id=huiyuan_id, - yongjin=yongjin, - ewai_dashou_fencheng=ewai_fencheng, - kaioi_ewai_dashou_fencheng=True, - paixu=data.get('paixu', 0), - shangjia_zhuangtai=data.get('shangjia_zhuangtai', True), - fengjin_zhuangtai=False, - shenhe_zhuangtai=shenhe_status, - shenhezhuangtai=1, - ) - return Response({'code': 0, 'msg': '商品添加成功', 'product_id': product.id}) - - # ---------- 删除商品(完全不变) ---------- - @transaction.atomic - def _delete_product(self, dianpu, data): - product_id = data.get('product_id') - if not product_id: - return Response({'code': 400, 'msg': '缺少 product_id'}) - - try: - product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id) - except Shangpin.DoesNotExist: - return Response({'code': 404, 'msg': '商品不存在'}) - - if product.tupian_url: - delete_from_oss(product.tupian_url) - if product.guize_tupian: - delete_from_oss(product.guize_tupian) - - product.delete() - return Response({'code': 0, 'msg': '商品已删除'}) - - - -class MemberListView(APIView): - """ - 获取所有会员列表(供商品发布时选择) - URL: /shangdian/hqhy - 权限:JWT认证 + 店铺归属验证 - 说明:前端需传递 userId,接口会进行身份及店铺状态校验 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # ---------- 1. 获取前端传递的用户ID(必须) ---------- - frontend_user_id = request.data.get('userId') - if not frontend_user_id: - return Response({'code': 400, 'msg': '缺少必要参数 userId'}) - - # ---------- 2. 调用公共验证方法(会校验用户身份、店铺状态) ---------- - dianpu, error_response = verify_shop_permission(request, frontend_user_id) - if error_response: - return error_response - - # ---------- 3. 查询所有会员(只返回前端需要的字段) ---------- - try: - members = Huiyuan.query.all().values('huiyuan_id', 'jieshao') - member_list = [ - { - 'huiyuan_id': m['huiyuan_id'], - 'jieshao': m['jieshao'] or '' - } - for m in members - ] - except Exception as e: - logger.exception("获取会员列表失败") - return Response({'code': 500, 'msg': '服务器错误'}) - - # ---------- 4. 返回成功响应 ---------- - return Response({ - 'code': 0, - 'msg': 'success', - 'data': member_list - }) - - - - -class SubShopListView(APIView): - """ - 获取下级店铺列表,同时返回上级分红汇总 - URL: /shangdian/wdxjdp - 方法: POST - 权限: JWT认证 + 上级店铺身份验证 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 身份验证 - 获取上级店铺对象 - user_id = request.data.get('userId') - if not user_id: - return Response({'code': 400, 'msg': '缺少 userId'}) - - parent_dianpu, error = verify_shop_permission(request, user_id) - if error: - return error - # 此时 parent_dianpu 即为当前上级店铺 - - # 2. 读取筛选参数 - dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() - zhuangtai = request.data.get('zhuangtai') - bind_min = request.data.get('bind_min') - bind_max = request.data.get('bind_max') - page = request.data.get('page', 1) - page_size = request.data.get('page_size', 10) - - # 3. 查询下级店铺:yaoqing_dianpu_id 等于当前上级店铺ID - queryset = Dianpu.query.filter(yaoqing_dianpu_id=parent_dianpu.id) - - # 4. 筛选条件 - if dianpu_mingcheng: - queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng) - if zhuangtai is not None: - try: - zhuangtai = int(zhuangtai) - queryset = queryset.filter(zhuangtai=zhuangtai) - except (ValueError, TypeError): - pass - if bind_min is not None: - try: - bind_min = int(bind_min) - queryset = queryset.filter(bangding_yonghushu__gte=bind_min) - except (ValueError, TypeError): - pass - if bind_max is not None: - try: - bind_max = int(bind_max) - queryset = queryset.filter(bangding_yonghushu__lte=bind_max) - except (ValueError, TypeError): - pass - - # 5. 排序:按创建时间倒序 - queryset = queryset.order_by('-create_time') - - # 6. 分页 - try: - paginator = Paginator(queryset, page_size) - page_obj = paginator.page(page) - except EmptyPage: - page_obj = paginator.page(1) - - # 7. 构建列表数据 - shop_list = [] - for shop in page_obj: - shop_list.append({ - 'id': shop.id, - 'dianpu_mingcheng': shop.dianpu_mingcheng, - 'dianpu_touxiang': shop.dianpu_touxiang or '', - 'lianxi_dianhua': shop.lianxi_dianhua or '', - 'zhuangtai': shop.zhuangtai, - 'bangding_yonghushu': shop.bangding_yonghushu, - 'zhengqu_zonge': str(shop.zhengqu_zonge), - 'ketixian_yue': str(shop.ketixian_yue), - 'create_time': shop.create_time.strftime('%Y-%m-%d %H:%M:%S') if shop.create_time else '', - 'update_time': shop.update_time.strftime('%Y-%m-%d %H:%M:%S') if shop.update_time else '', - 'erweima_url': shop.erweima_url or '', - 'fenhong_choucheng_feilv': str(shop.fenhong_choucheng_feilv) if shop.fenhong_choucheng_feilv else '0.0000', - }) - - # 8. 上级分红汇总(从当前店铺主表读取) - shangji_leiji = str(parent_dianpu.shangji_fenhong_leiji) if parent_dianpu.shangji_fenhong_leiji else '0.00' - shangji_ketixian = str(parent_dianpu.shangji_ketixian_fenhong) if parent_dianpu.shangji_ketixian_fenhong else '0.00' - - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': { - 'list': shop_list, - 'total': paginator.count, - 'page': page_obj.number, - 'page_size': page_size, - 'shangji_fenhong_leiji': shangji_leiji, - 'shangji_ketixian_fenhong': shangji_ketixian, - } - }) - - -class SubShopModifyView(APIView): - """ - 添加/修改下级店铺 - URL: /shangdian/xjdpbj - 方法: POST - 权限: JWT认证 + 上级店铺身份验证 - action字段: - - 'add' : 添加新的子店铺 - - 'update': 修改已有子店铺的状态/分红费率 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 身份验证 - user_id = request.data.get('userId') - if not user_id: - return Response({'code': 400, 'msg': '缺少 userId'}) - - parent_dianpu, error = verify_shop_permission(request, user_id) - if error: - return error - - action = request.data.get('action', '').strip().lower() - if action == 'add': - return self._add_subshop(request, parent_dianpu) - elif action == 'update': - return self._update_subshop(request, parent_dianpu) - else: - return Response({'code': 400, 'msg': '未知操作类型,action 应为 add 或 update'}) - - def _add_subshop(self, request, parent_dianpu): - """添加子店铺,需提供用户UID、账号、密码、店铺名称等""" - user_uid = request.data.get('user_uid', '').strip() - zhanghao = request.data.get('zhanghao', '').strip() - mima = request.data.get('mima', '') - dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() - lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip() or None - fenhong_feilv = request.data.get('fenhong_choucheng_feilv', None) - - # 必填校验 - if not all([user_uid, zhanghao, mima, dianpu_mingcheng]): - return Response({'code': 400, 'msg': '必填字段缺失(user_uid, zhanghao, mima, dianpu_mingcheng)'}) - - # 检查用户是否存在 - if not User.query.filter(yonghuid=user_uid).exists(): - return Response({'code': 404, 'msg': '对应的小程序用户不存在'}) - - # 检查账号是否已占用 - if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists(): - return Response({'code': 400, 'msg': '登录账号已存在'}) - if YonghuPingzheng.query.filter(yonghu=user_uid).exists(): - return Response({'code': 400, 'msg': '该用户 ID 已有店铺凭证'}) - - # 费率处理:前端可选,后端默认 0.0500 - if fenhong_feilv is not None: - try: - fenhong_feilv = float(fenhong_feilv) - if fenhong_feilv < 0 or fenhong_feilv > 1: - return Response({'code': 400, 'msg': '分红费率需在 0~1 之间'}) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': '分红费率格式错误'}) - else: - fenhong_feilv = 0.0500 - - try: - with transaction.atomic(): - # 创建凭证 - pingzheng = YonghuPingzheng.query.create( - yonghu=user_uid, - zhanghao=zhanghao, - mima=mima, - is_active=True - ) - # 创建店铺,关联上级 - dianpu = Dianpu.query.create( - pingzheng=pingzheng, - dianpu_mingcheng=dianpu_mingcheng, - lianxi_dianhua=lianxi_dianhua or '', - yaoqing_dianpu_id=parent_dianpu.id, # 关键:绑定上级 - fenhong_choucheng_feilv=fenhong_feilv, - # 其他字段使用默认值 - ) - return Response({ - 'code': 200, - 'msg': '子店铺添加成功', - 'dianpu_id': dianpu.id - }) - except IntegrityError as e: - logger.exception("创建子店铺违反唯一约束") - return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'}) - except Exception as e: - logger.exception("创建子店铺失败") - return Response({'code': 500, 'msg': '创建失败,请稍后重试'}) - - def _update_subshop(self, request, parent_dianpu): - """修改子店铺状态和上级分红费率""" - dianpu_id = request.data.get('dianpu_id') - if not dianpu_id: - return Response({'code': 400, 'msg': '缺少 dianpu_id'}) - - try: - dianpu_id = int(dianpu_id) - except (ValueError, TypeError): - return Response({'code': 400, 'msg': 'dianpu_id 需为数字'}) - - try: - with transaction.atomic(): - sub_dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) - # 权限校验:确保该店铺的上级就是当前店铺 - if sub_dianpu.yaoqing_dianpu_id != parent_dianpu.id: - return Response({'code': 403, 'msg': '无权修改该店铺,非您的下级'}) - - # 更新状态(如果有传) - if 'zhuangtai' in request.data: - zhuangtai = request.data['zhuangtai'] - if zhuangtai not in (0, 1, '0', '1'): - return Response({'code': 400, 'msg': '店铺状态只能为 0(封禁) 或 1(正常)'}) - sub_dianpu.zhuangtai = int(zhuangtai) - - # 更新上级分红费率(如果有传) - if 'fenhong_choucheng_feilv' in request.data: - feilv = request.data['fenhong_choucheng_feilv'] - try: - feilv = float(feilv) - if feilv < 0 or feilv > 1: - return Response({'code': 400, 'msg': '分红费率需在 0~1 之间'}) - sub_dianpu.fenhong_choucheng_feilv = feilv - except (ValueError, TypeError): - return Response({'code': 400, 'msg': '分红费率格式错误'}) - - sub_dianpu.save(update_fields=['zhuangtai', 'fenhong_choucheng_feilv', 'update_time']) - return Response({'code': 200, 'msg': '修改成功'}) - - except Dianpu.DoesNotExist: - return Response({'code': 404, 'msg': '店铺不存在'}) - except Exception as e: - logger.exception("修改下级店铺失败") - return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) - - - - - -# shangdian/views/order_manage_views.py -""" -店铺订单管理接口 -包含: -1. 统计数据:商品类型及订单总数、各状态订单数 -2. 订单列表:支持筛选、分页、返回订单详情及打手图片(含跨平台图片拉取) -""" - - -# ---------- 图片域名 ---------- -""" -店铺订单管理接口 -================= -功能1:获取统计数据(action='statistics') - - 每个商品类型的订单总数(order_count) - - 每个商品类型下各状态的订单数量(status_counts) - - 全局各状态的订单总数(status_counts) - - 店铺订单总量(total_orders) - -功能2:获取订单列表(默认,不带action参数时执行) - - 支持商品类型、订单状态、订单ID、打手ID、游戏昵称、老板ID、订单介绍筛选 - - 支持分页、自定义每页大小 - - 返回打手提交图片(绝对URL)、老板信息、店铺收益等 - -接口地址:POST /shangdian/ddlbhq -权限:JWT认证 + verify_shop_permission 验证店铺身份 -""" - -class ShopOrderStatisticsView(APIView): - """ - 店铺订单统计接口 - URL: POST /shangdian/ddltj - 权限: JWT认证 + 店铺身份验证 - - 请求参数: - userId (必填): 店铺所属用户的ID - - 返回结构: - { - "code": 200, - "msg": "统计成功", - "data": { - "types": [ # 所有有效商品类型 - { - "id": 类型ID, - "jieshao": "类型介绍", - "tupian_url": "类型图片相对路径", - "order_count": 该类型订单总数, - "status_counts": { # 该类型下各状态订单数量 - "1": 数量, - "7": 数量, - "2": 数量, - "8": 数量, - "3": 数量, - "4": 数量, - "5": 数量, - "6": 数量 - } - }, - ... - ], - "status_counts": { # 全局各状态订单总数 - "1": 总数, - "7": 总数, - "2": 总数, - "8": 总数, - "3": 总数, - "4": 总数, - "5": 总数, - "6": 总数 - }, - "total_orders": 订单总量 - } - } - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 身份验证 - user_id = request.data.get('userId', '').strip() - if not user_id: - return Response({'code': 400, 'msg': '缺少 userId'}) - - dianpu, error = verify_shop_permission(request, user_id) - if error: - return error - - shop_id = dianpu.id - - try: - # 2. 获取所有正常状态(审核通过)的商品类型 - types_qs = ShangpinLeixing.query.filter( - shenhezhuangtai=1 - ).order_by('-paixu', 'id') - - type_list = [] - type_id_map = {} - for idx, t in enumerate(types_qs): - item = { - 'id': t.id, - 'jieshao': t.jieshao or '', - 'tupian_url': t.tupian_url or '', - 'order_count': 0, - 'status_counts': { - '1': 0, '7': 0, '2': 0, '8': 0, - '3': 0, '4': 0, '5': 0, '6': 0 - } - } - type_list.append(item) - type_id_map[t.id] = idx - - # 3. 获取本店铺所有订单的主键ID列表(通过平台扩展表) - order_pks = list( - DingdanPingtai.query.filter(dianpu_id=shop_id) - .values_list('dingdan_id', flat=True) # 这里取的是订单主表的主键id - ) - - total_orders = 0 - global_counts = defaultdict(int) - - if order_pks: - # 4. 用主键列表进行聚合统计,按商品类型和订单状态分组 - aggregation = ( - Dingdan.objects - .filter(id__in=order_pks) # 修正点:使用id__in,而不是dingdan_id__in - .values('leixing_id', 'zhuangtai') - .annotate(cnt=Count('id')) - ) - - for row in aggregation: - lid = row['leixing_id'] # 商品类型ID - st = row['zhuangtai'] # 订单状态码 - cnt = row['cnt'] - - # 转为字符串键,方便前端展示 - st_key = str(st) - - # 累加总量 - total_orders += cnt - global_counts[st_key] += cnt - - # 填充到对应商品类型 - if lid in type_id_map: - idx = type_id_map[lid] - type_list[idx]['order_count'] += cnt - if st_key in type_list[idx]['status_counts']: - type_list[idx]['status_counts'][st_key] += cnt - - # 5. 确保全局状态返回所有8个键,即使值为0 - all_status_keys = ['1', '7', '2', '8', '3', '4', '5', '6'] - final_global_counts = { - k: global_counts.get(k, 0) for k in all_status_keys - } - - # 6. 返回统计结果 - return Response({ - 'code': 200, - 'msg': '统计成功', - 'data': { - 'types': type_list, - 'status_counts': final_global_counts, - 'total_orders': total_orders - } - }) - - except Exception as e: - logger.exception("订单统计接口异常") - return Response({ - 'code': 500, - 'msg': '统计失败,服务器内部错误' - }) - -# ================================================================ -# 辅助函数:获取OSS域名(用于拼接本地图片绝对URL) -# ================================================================ -def get_oss_domain(): - """ - 获取对象存储的域名,末尾包含 '/'。 - 返回:字符串,如 'https://example.com/' - """ - return getattr(settings, 'COS_DOMAIN', 'https://xingque999qygwuyq-1404472910.cos.ap-shanghai.myqcloud.com/').rstrip('/') + '/' - - -# ================================================================ -# 辅助函数:获取单个订单的打手提交图片(绝对URL) -# ================================================================ -def _fetch_images(order, oss_domain): - """ - 查询指定订单的打手提交图片记录(Dashoutupian 表), - 将相对路径拼接为完整的绝对 URL 并返回。 - - 参数: - order: Dingdan 订单模型实例 - oss_domain: OSS 域名,末尾含 '/' - - 返回: - (images_list, is_punished, punishment_info) 三元组 - - images_list: 图片绝对 URL 列表 - - is_punished: 暂未使用,默认 False - - punishment_info: 暂未使用,默认空字典 - """ - images = [] - is_punished = False - punishment_info = {} - - # 仅当订单有接单打手ID时才查询打手图片 - if order.jiedan_dashou_id: - # 从打手图片表中查询该订单、该打手的所有图片记录 - image_urls = Dashoutupian.query.filter( - dingdan_id=order.dingdan_id, # 订单ID匹配 - dashou=order.jiedan_dashou_id # 接单打手ID匹配 - ).values_list('tupian', flat=True) # 只取图片URL字段 - - # 遍历所有图片路径 - for img_path in image_urls: - if img_path: - if img_path.startswith('http'): - # 已经是完整URL,直接添加 - images.append(img_path) - else: - # 相对路径,拼接 OSS 域名 - images.append(oss_domain + img_path.lstrip('/')) - - # 跨平台图片拉取逻辑(根据实际业务需求补充,此处保留结构) - # 如果有跨平台订单,需要请求对方平台接口获取打手图片 - return images, is_punished, punishment_info - - -# ================================================================ -# 辅助函数:并发获取多个订单的打手图片 -# ================================================================ -def _batch_images(orders, oss_domain): - """ - 并发调用 _fetch_images,提升批量获取图片的速度。 - - 参数: - orders: Dingdan 订单对象列表 - oss_domain: OSS 域名 - - 返回: - 字典,键为订单ID (dingdan_id),值为 (images_list, is_punished, punishment_info) - """ - result = {} # 存储结果 - # 使用线程池并发执行,max_workers 可根据服务器性能调整 - with ThreadPoolExecutor(max_workers=10) as executor: - # 建立 future 到订单ID的映射 - future_to_oid = { - executor.submit(_fetch_images, order, oss_domain): order.dingdan_id - for order in orders - } - # 等待所有任务完成 - for future in as_completed(future_to_oid): - oid = future_to_oid[future] # 对应的订单ID - try: - # 获取任务返回值 - imgs, pun, pinfo = future.result() - result[oid] = (imgs, pun, pinfo) - except Exception as e: - # 某个订单获取失败,记录日志并赋予空值,保证整体流程不中断 - logger.error(f"获取订单 {oid} 图片失败: {e}") - result[oid] = ([], False, {}) - return result - - -# ================================================================ -# 核心视图类 -# ================================================================ -class ShopOrderManageView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - """ - 统一入口,根据 action 参数分发到统计或列表模块。 - """ - # 1. 从请求中提取店铺用户ID - user_id = request.data.get('userId', '').strip() - if not user_id: - return Response({'code': 400, 'msg': '缺少 userId'}) - - # 2. 验证店铺身份,获取店铺对象 - dianpu, error_response = verify_shop_permission(request, user_id) - if error_response: - return error_response - - # 3. 根据 action 决定调用哪个方法 - action = request.data.get('action', '').strip().lower() - if action == 'statistics': - return self._statistics(dianpu) # 统计 - else: - return self._order_list(request, dianpu) # 列表 - - # ----------------------------------------------------------------- - # 统计实现 (这就是你要求的100个统计字段的详细实现) - # ----------------------------------------------------------------- - def _statistics(self, dianpu): - """ - 统计本店铺的订单数据,输出以下信息: - - types: 商品类型数组,每个元素包含: - id : 类型ID - jieshao : 类型介绍 - tupian_url : 类型图片 - order_count : 【该类型的订单总数】 - status_counts: 该类型下各个状态的订单数量 - - status_counts: 全局各状态订单总数(键为状态码字符串) - - total_orders : 店铺订单总量 - - 所有字段名均与前端保持一致。 - """ - # 获取店铺ID - shop_id = dianpu.id - - # ------------------------------------------------------------ - # 步骤1:获取所有正常的商品类型(审核状态为1) - # ------------------------------------------------------------ - types_query = ShangpinLeixing.query.filter( - shenhezhuangtai=1 # 审核状态为1(正常) - ).order_by('-paixu', 'id') # 按排序权重和ID排序 - - # 构建返回的类型列表,并预初始化各状态计数 - type_list = [] # 最终要返回的类型数组 - type_ids = [] # 所有类型的ID列表,用于后续快速索引 - - # 定义可能出现的所有状态码(1/7已付款,2进行中,8待结算,3已完成,4退款审核,5已退款,6退款失败) - status_keys = ['1', '7', '2', '8', '3', '4', '5', '6'] - - for t in types_query: - # 构造每个类型的基础数据 - type_item = { - 'id': t.id, - 'jieshao': t.jieshao or '', - 'tupian_url': t.tupian_url or '', - 'order_count': 0, # 该类型订单总数,稍后填充 - 'status_counts': {k: 0 for k in status_keys} # 各状态计数初始为0 - } - type_list.append(type_item) - type_ids.append(t.id) # 记录ID - - # ------------------------------------------------------------ - # 步骤2:获取本店铺的所有订单ID(通过平台扩展表) - # ------------------------------------------------------------ - # 从 DingdanPingtai 中过滤出属于当前店铺的订单扩展记录,并提取订单主表ID - order_ids = list( - DingdanPingtai.query.filter( - dianpu_id=shop_id # 店铺ID匹配 - ).values_list('dingdan_id', flat=True) # 只取订单ID字段 - ) - - # 统计变量 - total_orders = 0 # 订单总量 - global_status_counts = defaultdict(int) # 全局各状态计数,键为状态码字符串 - - # ------------------------------------------------------------ - # 步骤3:如果店铺有订单,进行聚合统计 - # ------------------------------------------------------------ - if order_ids: - # 使用 Django 聚合查询:按 leixing_id(商品类型)和 zhuangtai(订单状态)分组计数 - aggregation = Dingdan.query.filter( - dingdan_id__in=order_ids # 从该店铺的订单中查询 - ).values( - 'leixing_id', # 按类型ID分组 - 'zhuangtai' # 按状态分组 - ).annotate( - cnt=Count('id') # 统计每个分组的记录数 - ) - - # 遍历每个聚合结果,累加到对应的类型和全局计数中 - for row in aggregation: - # 提取分组字段 - lid = row['leixing_id'] # 商品类型ID - st = row['zhuangtai'] # 订单状态码 - cnt = row['cnt'] # 该分组下的订单数量 - - # 累加到全局总数 - total_orders += cnt - global_status_counts[str(st)] += cnt - - # 累加到对应商品类型的状态计数中 - if lid in type_ids: - # 在 type_list 中找到对应的类型条目 - for tp in type_list: - if tp['id'] == lid: - # 更新该类型的订单总数 - tp['order_count'] += cnt - # 更新该类型下对应状态的计数(键为字符串) - tp['status_counts'][str(st)] += cnt - break # 找到后跳出循环,因为ID唯一 - - # 将全局状态计数转换为按固定顺序的字典,确保前端展示顺序稳定 - final_global_counts = {k: global_status_counts.get(k, 0) for k in status_keys} - - # ------------------------------------------------------------ - # 步骤4:返回统计结果 - # ------------------------------------------------------------ - return Response({ - 'code': 200, - 'msg': '统计成功', - 'data': { - 'types': type_list, # 商品类型列表(含各类型统计) - 'status_counts': final_global_counts, # 全局各状态总数 - 'total_orders': total_orders # 订单总量 - } - }) - - # ----------------------------------------------------------------- - # 订单列表实现 - # ----------------------------------------------------------------- - def _order_list(self, request, dianpu): - """处理订单列表请求,支持筛选、分页、并发获取图片。""" - shop_id = dianpu.id - - # 解析筛选参数 - leixing_id = request.data.get('leixing_id') - zhuangtai = request.data.get('zhuangtai') - dingdan_id_key = request.data.get('dingdan_id', '').strip() - jiedan_dashou_id_key = request.data.get('jiedan_dashou_id', '').strip() - nicheng_key = request.data.get('nicheng', '').strip() - laoban_id_key = request.data.get('laoban_id', '').strip() - jieshao_key = request.data.get('jieshao', '').strip() - - page = request.data.get('page', 1) - page_size = request.data.get('page_size', 20) - - # 基础查询:从平台扩展表关联订单主表,并按创建时间倒序 - qs = DingdanPingtai.query.filter( - dianpu_id=shop_id - ).select_related('dingdan').order_by('-dingdan__create_time') - - # 应用筛选条件 - if leixing_id is not None: - qs = qs.filter(dingdan__leixing_id=leixing_id) - if zhuangtai is not None: - qs = qs.filter(dingdan__zhuangtai=zhuangtai) - if dingdan_id_key: - qs = qs.filter(dingdan__dingdan_id__icontains=dingdan_id_key) - if jiedan_dashou_id_key: - qs = qs.filter(dingdan__jiedan_dashou_id=jiedan_dashou_id_key) - if nicheng_key: - qs = qs.filter(dingdan__nicheng__icontains=nicheng_key) - if laoban_id_key: - qs = qs.filter(laoban_id=laoban_id_key) - if jieshao_key: - qs = qs.filter(dingdan__jieshao__icontains=jieshao_key) - - # 分页 - paginator = Paginator(qs, page_size) - try: - page_obj = paginator.page(page) - except EmptyPage: - page_obj = paginator.page(1) - - # 获取当前页的所有订单和扩展对象 - orders_with_ext = [(item.dingdan, item) for item in page_obj] - # 提取订单对象列表,用于并发获取图片 - order_objs = [order for order, _ in orders_with_ext] - oss_domain = get_oss_domain() - img_map = _batch_images(order_objs, oss_domain) - - # 构建返回数据 - list_data = [] - for order, ext in orders_with_ext: - imgs, is_pun, _ = img_map.get(order.dingdan_id, ([], False, {})) - list_data.append({ - 'dingdan_id': order.dingdan_id, - 'zhuangtai': order.zhuangtai, - 'jine': float(order.jine) if order.jine else 0.00, - 'dashou_fencheng': float(order.dashou_fencheng) if order.dashou_fencheng else 0.00, - 'jiedan_dashou_id': order.jiedan_dashou_id or '', - 'dashou_liuyan': order.dashou_liuyan or '', - 'zhiding_id': order.zhiding_id or '', - 'shangpin_id': order.shangpin_id or '', - 'tupian': order.tupian or '', - 'jieshao': order.jieshao or '', - 'beizhu': order.beizhu or '', - 'nicheng': order.nicheng or '', - 'leixing_id': order.leixing_id, - 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '', - 'update_time': order.update_time.strftime('%Y-%m-%d %H:%M:%S') if order.update_time else '', - 'pingtai_kuozhan': { - 'laoban_id': ext.laoban_id or '', - 'laoban_pingjia': ext.laoban_pingjia or '', - 'dianpu_shouyi': float(ext.dianpu_shouyi) if ext.dianpu_shouyi else 0.00, - }, - 'dashou_images': imgs, - 'is_punished': is_pun, - 'is_fadaned': False, - }) - - return Response({ - 'code': 200, - 'msg': '列表成功', - 'data': { - 'list': list_data, - 'total': paginator.count, - 'page': page_obj.number, - 'page_size': page_size - } - }) - - - - - - -# ================================================================ -# shangdian/views/refund_view.py -# 店铺退款接口 —— 基于跨平台退款接口二改,只处理平台订单,我方派单 -# ================================================================ - -logger = logging.getLogger(__name__) - - -class ShopRefundView(APIView): - """ - 店铺订单退款接口 - URL: POST /shangdian/tk - 权限: JWT认证 + 店铺身份验证(只处理平台订单,我方派单) - - 请求参数: - userId (必填) : 店铺所属用户的ID - dingdan_id (必填): 订单ID - reason (可选) : 退款理由 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # 1. 获取参数 - user_id = request.data.get('userId', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - reason = request.data.get('reason', '').strip() # 退款理由 - - if not user_id or not dingdan_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # 2. 店铺身份验证 - dianpu, error = verify_shop_permission(request, user_id) - if error: - return error - - shop_id = dianpu.id - - # 3. 查询订单,必须属于本店铺(通过扩展表关联) - try: - # 通过扩展表确认归属,并 select_related 获取订单主表 - pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( - dianpu_id=shop_id, - dingdan__dingdan_id=dingdan_id - ) - order = pingtai_ext.dingdan - except DingdanPingtai.DoesNotExist: - logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") - return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) - - # 4. 订单状态校验(只有这些状态可退款) - allowed_statuses = [1, 7, 2, 8, 4] - if order.zhuangtai not in allowed_statuses: - logger.warning(f"订单状态不允许退款: {order.zhuangtai}") - return Response({'code': 400, 'msg': '订单状态不允许退款'}) - - # 5. 全部视为平台订单(我方派单),调用微信退款 - refund_result = self._call_wechat_refund(order) - if refund_result['code'] != 0: - return Response({'code': 400, 'msg': refund_result['msg']}) - - # 6. 事务内更新订单、打手、记录、统计 - dashou_id = order.jiedan_dashou_id - try: - with transaction.atomic(): - # 更新订单状态为已退款 - order.zhuangtai = 5 - order.clkf = str(shop_id) # 处理人记录为店铺ID - if reason: - order.tkly = reason - order.save() - - # 更新打手统计(退款量+1,状态置为空闲) - if dashou_id: - try: - dashou_user = User.query.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.tuikuanliang += 1 - dashou_profile.zhuangtai = 1 - dashou_profile.save() - except (User.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"退款:打手 {dashou_id} 不存在,跳过状态更新") - - # 更新退款记录表 - self._update_refund_record(order, dashou_id, str(shop_id), reason) - - # 打手每日统计(退款动作) - try: - update_dashou_daily_by_action( - yonghuid=dashou_id, - amount=order.dashou_fencheng or Decimal('0.00'), - action=3 # 3 表示退款 - ) - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - - # 更新平台支出记录 - try: - update_daily_payout(order.jine) - except Exception as e: - logger.error(f"更新每日支出失败: {e}") - - # ========== 新增:商品和店铺每日统计 ========== - try: - # 商品每日统计(只要有商品ID就执行) - if order.shangpin_id: - # 从扩展表获取已计算好的收益值(下单时已存储) - pingtai_shouyi = Decimal('0.00') - dianpu_shouyi = Decimal('0.00') - if hasattr(order, 'pingtai_kuozhan'): - pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - - update_shangpin_daily_stat( - shangpin_id=order.shangpin_id, - caozuo_leixing=3, # 3 = 退款 - dingdan_jiage=order.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") - - # 店铺每日统计(仅当存在店铺ID时执行) - dianpu_id = None - if hasattr(order, 'pingtai_kuozhan'): - dianpu_id = order.pingtai_kuozhan.dianpu_id - if dianpu_id: - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - update_dianpu_daily_stat( - dianpu_id=dianpu_id, - caozuo_leixing=3, # 3 = 退款 - dingdan_jiage=order.jine, - dianpu_shouyi=dianpu_shouyi - ) - logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") - except Exception as e: - logger.error(f"商品/店铺统计更新失败: {e}") - logger.error(traceback.format_exc()) - # 统计失败不影响主流程 - # ============================================= - - except Exception as e: - logger.exception("退款事务执行失败") - return Response({'code': 500, 'msg': '退款处理失败,请稍后重试'}) - - return Response({'code': 200, 'msg': '退款成功'}) - - # ========== 微信退款 ========== - def _call_wechat_refund(self, order): - """调用微信支付退款接口""" - appid = getattr(settings, 'WEIXIN_APPID', '') - mch_id = getattr(settings, 'WEIXIN_MCHID', '') - key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') - cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') - key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') - - if not all([appid, mch_id, key, cert_path, key_path]): - return {'code': 500, 'msg': '微信支付配置不完整'} - - out_refund_no = self._generate_refund_no(order.dingdan_id) - total_fee = int(float(order.jine or 0) * 100) - refund_fee = total_fee - - params = { - 'appid': appid, - 'mch_id': mch_id, - 'nonce_str': self._generate_nonce_str(), - 'out_refund_no': out_refund_no, - 'total_fee': total_fee, - 'refund_fee': refund_fee, - 'refund_desc': '店铺退款', - } - if order.wechat_transaction_id: - params['transaction_id'] = order.wechat_transaction_id - else: - params['out_trade_no'] = order.dingdan_id - - # 签名 - sorted_keys = sorted(params.keys()) - stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) - stringSignTemp = f"{stringA}&key={key}" - sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() - params['sign'] = sign - - xml_data = self._dict_to_xml(params) - url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' - - try: - response = requests.post(url, data=xml_data, cert=(cert_path, key_path), timeout=10) - result = xmltodict.parse(response.content)['xml'] - logger.info(f"微信退款响应: {result}") - except Exception as e: - logger.error(f"微信退款请求异常: {e}", exc_info=True) - return {'code': 500, 'msg': '微信退款请求异常'} - - if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': - return {'code': 0, 'msg': '退款成功'} - else: - err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) - return {'code': 400, 'msg': err_msg} - - # ========== 退款记录 ========== - def _update_refund_record(self, order, dashou_id, chuli_id, reason): - """更新退款记录表""" - try: - refund_record, created = Tuikuanjilu.query.get_or_create( - dingdan_id=order.dingdan_id, - defaults={ - 'dashouid': dashou_id or '', - 'qingqiuid': '', - 'chuliid': chuli_id, - 'liyou': reason or '店铺退款', - 'sqzhuangtai': 1, - 'jine': order.jine or 0, - 'dashou_fencheng': order.dashou_fencheng or 0, - 'jieshao': order.jieshao or '', - 'beizhu': '店铺退款', - 'nicheng': order.nicheng or '', - } - ) - if not created: - refund_record.sqzhuangtai = 1 - refund_record.chuliid = chuli_id - if reason: - refund_record.liyou = reason - refund_record.save() - except Exception as e: - logger.error(f"更新退款记录异常: {e}") - - # ========== 工具方法 ========== - def _generate_nonce_str(self): - return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) - - def _generate_refund_no(self, dingdan_id): - timestamp = str(int(time.time())) - rand = ''.join(random.choices('0123456789', k=4)) - return f"{dingdan_id}REF{timestamp}{rand}" - - def _dict_to_xml(self, data): - xml = [''] - for k, v in data.items(): - xml.append(f'<{k}>{v}') - xml.append('') - return ''.join(xml) - - - - - - -class ShopForceCompleteView(APIView): - """ - 店铺强制结单接口 - URL: POST /shangdian/qzjd - 权限: JWT认证 + 店铺身份验证 - - 请求参数: - userId (必填) : 店铺所属用户的ID - dingdan_id(必填) : 订单ID - - 说明: - - 只处理平台订单(fadan_pingtai == 1) - - 订单必须属于当前店铺 - - 订单状态必须为 8(结算中)才能强制结单 - - 默认视为我方派单,根据是否有对方订单ID判断是否跨平台 - - 本地订单:给打手结算、更新打手余额和统计、更新店铺收益统计 - - 跨平台订单:仅通知对方平台,不处理打手 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # ========== 1. 获取参数 ========== - user_id = request.data.get('userId', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - - if not user_id or not dingdan_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # ========== 2. 店铺身份验证 ========== - dianpu, error = verify_shop_permission(request, user_id) - if error: - return error - - shop_id = dianpu.id - - # ========== 3. 查询订单并验证归属 ========== - try: - # 通过平台扩展表确认订单属于本店铺 - pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( - dianpu_id=shop_id, - dingdan__dingdan_id=dingdan_id - ) - order = pingtai_ext.dingdan - except DingdanPingtai.DoesNotExist: - logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") - return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) - - # ========== 4. 校验订单状态必须为 8(结算中) ========== - if order.zhuangtai != 8: - logger.warning(f"订单 {dingdan_id} 当前状态为 {order.zhuangtai},不是结算中") - return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'}) - - dashou_id = order.jiedan_dashou_id - dashou_fencheng = order.dashou_fencheng or Decimal('0.00') - jine = order.jine or Decimal('0.00') - - try: - with transaction.atomic(): - # 锁定订单行,防止并发 - order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) - - if not dashou_id: - return Response({'code': 400, 'msg': '订单无接单打手,无法结算'}) - if dashou_fencheng < 0: - return Response({'code': 400, 'msg': '打手分成金额无效'}) - - # ---------- 更新打手余额和统计 ---------- - try: - dashou_user = User.query.get(yonghuid=dashou_id) - dashou_profile = dashou_user.dashou_profile - dashou_profile.chengjiaozongliang += 1 - dashou_profile.yue += dashou_fencheng - dashou_profile.zonge += dashou_fencheng - dashou_profile.jinrishouyi += dashou_fencheng - dashou_profile.jinyueshouyi += dashou_fencheng - dashou_profile.zhuangtai = 1 # 空闲 - dashou_profile.save() - except (User.DoesNotExist, ObjectDoesNotExist): - logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过打手更新") - - # ---------- 更新订单状态 ---------- - order.zhuangtai = 3 - order.clkf = str(shop_id) - order.save() - - # ---------- 打手每日统计(成交) ---------- - try: - update_dashou_daily_by_action( - yonghuid=dashou_id, - amount=dashou_fencheng, - action=2 # 2 表示成交(结算) - ) - except Exception as e: - logger.error(f"打手每日统计更新失败: {e}") - - # ---------- 店铺收益统计(结算) ---------- - if pingtai_ext.dianpu_shouyi: - try: - # 商品每日统计(只要有商品ID就执行) - if order.shangpin_id: - # 从扩展表获取已计算好的收益值(下单时已存储) - pingtai_shouyi = Decimal('0.00') - dianpu_shouyi = Decimal('0.00') - if hasattr(order, 'pingtai_kuozhan'): - pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') - dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') - - update_shangpin_daily_stat( - shangpin_id=order.shangpin_id, - caozuo_leixing=2, # 3 = 退款 - dingdan_jiage=order.jine, - pingtai_shouyi=pingtai_shouyi, - dianpu_zongshouyi=dianpu_shouyi - ) - logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") - - - update_dianpu_daily_stat( - dianpu_id=shop_id, - caozuo_leixing=2, # 结算操作 - dingdan_jiage=jine, - dianpu_shouyi=pingtai_ext.dianpu_shouyi, - ) - except Exception as e: - logger.error(f"店铺每日统计更新失败: {e}") - - # ========== 5. 记录操作日志 ========== - logger.info(f"强制结单成功,订单 {dingdan_id},店铺 {shop_id}") - - return Response({'code': 200, 'msg': '强制结单成功'}) - - except Dingdan.DoesNotExist: - logger.warning(f"订单不存在: {dingdan_id}") - return Response({'code': 404, 'msg': '订单不存在'}) - except Exception as e: - logger.error(f"强制结单接口异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - -# ================================================================ -# shangdian/views/transfer_hall_view.py -# 店铺转移大厅接口 —— 严格二改,不添加任何原接口没有的逻辑 -# ================================================================ -# ================================================================ -# shangdian/views/transfer_hall_view.py -# 店铺转移大厅接口 —— 添加本地打手退款统计 -# ================================================================ - -class ShopTransferHallView(APIView): - """ - 店铺转移大厅接口 - URL: POST /shangdian/zydt - 权限: JWT认证 + 店铺身份验证 - - 请求参数: - userId (必填) : 店铺所属用户的ID - dingdan_id(必填) : 订单ID - - 说明: - - 只处理平台订单,我方派单 - - 允许转移的状态:2,8,4 - - 决定是否处理本地打手:有对方订单ID或俱乐部ID(跨平台)则不处理本地打手,只通知对方; - 没有对方信息则处理本地打手(释放+退款统计) - - 转移后清空对方平台信息,订单重回可接单状态 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # ---------- 1. 获取参数 ---------- - user_id = request.data.get('userId', '').strip() - dingdan_id = request.data.get('dingdan_id', '').strip() - - if not user_id or not dingdan_id: - return Response({'code': 400, 'msg': '参数不完整'}) - - # ---------- 2. 店铺身份验证 ---------- - dianpu, error = verify_shop_permission(request, user_id) - if error: - return error - - shop_id = dianpu.id - - # ---------- 3. 验证订单归属 ---------- - try: - pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( - dianpu_id=shop_id, - dingdan__dingdan_id=dingdan_id - ) - order = pingtai_ext.dingdan - except DingdanPingtai.DoesNotExist: - logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") - return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) - - # ---------- 4. 状态校验 ---------- - if order.zhuangtai not in [2, 8, 4]: - return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.zhuangtai}'}) - - # ---------- 5. 提取关键字段 ---------- - current_dashou_id = order.jiedan_dashou_id - zhiding_id = order.zhiding_id - dashou_fencheng = order.dashou_fencheng or 0 # 用于退款统计 - - try: - with transaction.atomic(): - # 锁定订单行 - order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) - - # ---------- 6. 记录打手历史(原接口保留逻辑) ---------- - if current_dashou_id: - last_history = OrderDashouHistory.query.filter( - dingdan_id=dingdan_id - ).order_by('-times').first() - next_times = (last_history.times + 1) if last_history else 1 - OrderDashouHistory.query.create( - dingdan_id=dingdan_id, - dashou_id=current_dashou_id, - times=next_times, - operator=str(shop_id) - ) - - # ---------- 7. 释放本地打手 + 退款统计 ---------- - if current_dashou_id: - try: - dashou_profile = UserDashou.query.get(user__yonghuid=current_dashou_id) - dashou_profile.zhuangtai = 1 # 设为空闲 - dashou_profile.save() - except ObjectDoesNotExist: - logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新") - else: - # 打手释放成功后,记录每日退款统计(转移视为退款) - try: - update_dashou_daily_by_action( - yonghuid=current_dashou_id, - amount=dashou_fencheng, - action=3 # 3 表示退款 - ) - logger.info(f"打手 {current_dashou_id} 转移大厅退款统计成功") - except Exception as e: - logger.error(f"打手每日统计失败: {e}") - - # ---------- 8. 重置订单 ---------- - order.jiedan_dashou_id = None - order.clkf = str(shop_id) - - # 根据是否有指定打手决定状态 - order.zhuangtai = 7 if zhiding_id else 1 - order.save() - - logger.info(f"转移大厅成功,订单 {dingdan_id}") - return Response({'code': 200, 'msg': '转移大厅成功'}) - - except Dingdan.DoesNotExist: - return Response({'code': 404, 'msg': '订单不存在'}) - except Exception as e: - logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True) - return Response({'code': 500, 'msg': '服务器内部错误'}) - - - - - - -# ================================================================ -# shangdian/views/fine_apply_view.py -# 店铺罚款申请接口 —— 二改自 FineApplyView(客服罚款) -# ================================================================ -class ShopFineApplyView(APIView): - """ - 店铺罚款申请接口 - URL: POST /shangdian/fksq - 权限: JWT认证 + 店铺身份验证 - - 请求参数: - userId (必填) : 店铺所属用户的ID(用于身份验证) - dingdan_id (必填) : 订单ID - reason (必填) : 罚款原因 - amount (必填) : 罚款金额(元) - yingxiang_qiangdan (可选) : 是否影响抢单,1=是,0=否,默认1 - - 说明: - - 仅处理平台订单,需属于当前店铺 - - 本地/对方派单处理逻辑与客服接口一致: - 无对方订单ID 或 对方派单 → 本地生成罚单 - 有对方订单ID且我方派单 → 跨平台通知对方,本地也记录(标记对方俱乐部ID) - - 申请人:店铺ID,身份码固定为4(店铺管理员) - - 被处罚人:接单打手,身份码固定为1 - """ - permission_classes = [IsAuthenticated] - - def post(self, request): - # ---------- 1. 提取参数 ---------- - user_id = request.data.get('userId', '').strip() # 店铺用户ID - dingdan_id = request.data.get('dingdan_id', '').strip() - chufaliyou = request.data.get('reason', '').strip() # 罚款原因(对应前端的reason) - fakuanjine = request.data.get('amount', 0) # 罚款金额(对应前端的amount) - yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) # 影响抢单 - - if not all([user_id, dingdan_id, chufaliyou, fakuanjine]): - return Response({'code': 400, 'msg': '参数不完整'}) - - try: - # ---------- 2. 店铺身份验证 ---------- - dianpu, error = verify_shop_permission(request, user_id) - if error: - return error - shop_id = dianpu.id # 店铺ID,作为罚款申请人 - - # ---------- 3. 验证订单归属 ---------- - try: - pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( - dianpu_id=shop_id, - dingdan__dingdan_id=dingdan_id - ) - order = pingtai_ext.dingdan - except DingdanPingtai.DoesNotExist: - logger.warning(f"罚款申请:订单 {dingdan_id} 不属于店铺 {shop_id}") - return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) - - # ---------- 4. 获取打手ID ---------- - dashou_id = order.jiedan_dashou_id - if not dashou_id: - return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) - - # ---------- 5. 重复罚款检查 ---------- - if Fadan.query.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): - return Response({'code': 400, 'msg': '该打手已被罚款过'}) - - # ---------- 6. 创建罚单 ---------- - with transaction.atomic(): - Fadan.query.create( - beichufa_id=dashou_id, - shenqing_chufa=str(shop_id), # 申请人记为店铺ID - shenfen=1, # 被处罚人身份:1 打手 - chufaliyou=chufaliyou, - fakuanjine=fakuanjine, - guanliandingdan_id=dingdan_id, - zhuangtai=1, # 待缴纳 - yingxiang_qiangdan=yingxiang_qiangdan, - shenqingren_shenfen=4, # 申请人身份:4 店铺管理员 - ) - logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}") - return Response({'code': 200, 'msg': '罚款已生成'}) - - except Exception as e: - logger.error(f"店铺罚款申请异常: {traceback.format_exc()}") +# ==================== 标准库 ==================== +import io +import json +import time +import random +import string +import hmac +import hashlib +import requests +import logging +import traceback +import calendar +import threading +import xmltodict +from decimal import Decimal +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor, as_completed + +from django.conf import settings +from django.db import models, transaction, IntegrityError +from django.db.models import F, Sum, Count, Q, Prefetch +from gvsdsdk.fluent import db, func, FQ +from django.core.cache import cache +from django.core.paginator import Paginator, EmptyPage +from django.core.exceptions import ObjectDoesNotExist +from django.utils import timezone +from django.http import HttpResponse + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework_simplejwt.tokens import RefreshToken + +from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client + +from .utils import verify_shop_permission +from shop.utils import verify_shop_permission, update_dianpu_daily_stat +from products.utils import update_shangpin_daily_stat +from orders.utils import update_daily_payout +from backend.utils import update_dashou_daily_by_action + +from .models import YonghuPingzheng, Dianpu, DianpuShouzhiMeiriTongji + +from users.models import UserDashou, UserShangjia, UserGuanshi, UserZuzhang +from gvsdsdk.models import User + +from shop.models import YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi + +from products.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan + +from orders.models import ( + Dingdan, DingdanPingtai, Dashoutupian, Tuikuanjilu, + OrderDashouHistory, Fadan +) + +logger = logging.getLogger(__name__) + + +class BindDianpuView(APIView): + """ + 店铺绑定接口 + 接收前端传入的店铺ID,更新用户与店铺的绑定关系 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + dianpu_id = request.data.get('dianpu_id') + if not dianpu_id: + return Response({'code': 400, 'msg': '店铺ID不能为空'}) + + user = request.user + if not hasattr(user, 'yonghuid'): + return Response({'code': 500, 'msg': '用户身份信息异常'}) + + # 先获取 User 对象(不加锁,仅用于校验存在性) + try: + user_main = User.query.get(UserUID=user.yonghuid) + except User.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + + # 校验店铺是否存在且状态正常(不加锁) + try: + dianpu = Dianpu.query.get(id=dianpu_id) + except Dianpu.DoesNotExist: + return Response({'code': 404, 'msg': '店铺不存在'}) + + if dianpu.zhuangtai != 1: + return Response({'code': 200, 'msg': '店铺已被封禁,无法绑定'}) + + # 在事务内部进行加锁操作 + try: + with transaction.atomic(): + # 重新获取加锁的用户对象(确保并发安全) + user_main_locked = User.objects.select_for_update().get(UserUID=user.yonghuid) + dianpu_locked = Dianpu.objects.select_for_update().get(id=dianpu_id) + + # 再次校验店铺状态(防止事务期间被修改) + if dianpu_locked.zhuangtai != 1: + return Response({'code': 200, 'msg': '店铺已被封禁,无法绑定'}) + + # 处理绑定关系 + try: + existing_binding = YonghuDianpuBangding.objects.select_for_update().get(yonghu=user_main_locked) + old_dianpu = existing_binding.dianpu + if old_dianpu.id == dianpu_locked.id: + return Response({'code': 200, 'msg': '已绑定该店铺'}) + + # 旧店铺绑定人数减1 + old_dianpu.bangding_yonghushu = F('bangding_yonghushu') - 1 + old_dianpu.save(update_fields=['bangding_yonghushu']) + + # 更新绑定关系 + existing_binding.dianpu = dianpu_locked + existing_binding.save() + + except YonghuDianpuBangding.DoesNotExist: + YonghuDianpuBangding.query.create(yonghu=user_main_locked, dianpu=dianpu_locked) + + # 新店铺绑定人数加1 + dianpu_locked.bangding_yonghushu = F('bangding_yonghushu') + 1 + dianpu_locked.save(update_fields=['bangding_yonghushu']) + + logger.info(f"用户 {user.yonghuid} 绑定店铺 {dianpu_id} 成功") + return Response({'code': 200, 'msg': '绑定成功'}) + + except Exception as e: + logger.exception(f"绑定店铺失败: {e}") + return Response({'code': 500, 'msg': '绑定失败,服务器内部错误'}) + + + + + + + +class ShopGoodsView(APIView): + """ + 获取店铺商品数据(类型、专区、商品列表) + 根据用户绑定店铺状态及身份,返回店铺专属或公共商品 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + try: + user_main = User.query.get(UserUUID=user.UserUUID) + except User.DoesNotExist: + return Response({'code': 404, 'msg': '用户不存在'}) + + # 1. 查询用户店铺绑定关系 + binding = YonghuDianpuBangding.query.filter( + yonghu=user_main + ).select_related('dianpu').first() + + # 2. 判断用户是否拥有特殊身份 + has_special = self._has_special_identity(user_main) + + # 3. 根据绑定关系及店铺状态决定数据获取策略 + if binding: + dianpu = binding.dianpu + if dianpu.zhuangtai == 1: + # 店铺正常 -> 返回店铺专属商品(需通过平台审核) + data = self._get_shop_goods(dianpu) + else: + # 店铺被封 -> 返回公共商品,有特殊身份则返回审核状态为1的,否则返回2的 + data = self._get_public_goods(True) + else: + # 无绑定店铺,根据身份决定返回公共商品的审核状态 + data = self._get_public_goods(has_special) + + return Response({'code': 200, 'data': data}) + + def _has_special_identity(self, user_main): + """判断用户是否拥有打手/商家/管事/组长任一身份""" + return (UserDashou.query.filter(user=user_main).exists() or + UserShangjia.query.filter(user=user_main).exists() or + UserGuanshi.query.filter(user=user_main).exists() or + UserZuzhang.query.filter(user=user_main).exists()) + + def _get_shop_goods(self, dianpu): + """ + 获取店铺专属商品 + 类型、专区、商品均从店铺相关表中获取,且需通过平台审核(shenhe_zhuangtai=True) + """ + # 1. 店铺商品类型:未封禁、已上架、平台审核通过 + leixing_queryset = ShangpinLeixingDianpu.query.filter( + dianpu=dianpu, + fengjin_zhuangtai=False, + shangjia_zhuangtai=True, + shenhe_zhuangtai=True # 🔥 必须通过平台审核 + ).select_related('gonggong_leixing').order_by('-paixu', 'id') + + leixing_data = [] + for lx in leixing_queryset: + leixing_data.append({ + 'id': lx.id, # 店铺类型ID,前端归类用 + 'jieshao': lx.jieshao, + 'tupian_url': lx.tupian_url, + 'paixu': lx.paixu + }) + + leixing_ids = [lx['id'] for lx in leixing_data] + + # 2. 专区:根据店铺和店铺类型ID,未封禁、已上架 + if leixing_ids: + zhuanqu_queryset = ShangpinZhuanqu.query.filter( + dianpu=dianpu, + dianpu_leixing_id__in=leixing_ids, + fengjin_zhuangtai=False, + shangjia_zhuangtai=True + ).order_by('-paixu', 'id') + else: + zhuanqu_queryset = ShangpinZhuanqu.query.none() + + zhuanqu_data = [] + for zq in zhuanqu_queryset: + zhuanqu_data.append({ + 'id': zq.id, + 'mingzi': zq.mingzi, + 'leixing_id': zq.dianpu_leixing_id, # 对应店铺类型ID + 'paixu': zq.paixu + }) + + # 3. 商品:根据店铺、店铺类型、专区,未封禁、已上架、平台审核通过、原有审核状态为正常 + zhuanqu_ids = [zq['id'] for zq in zhuanqu_data] + if zhuanqu_ids: + shangpin_queryset = Shangpin.query.filter( + dianpu=dianpu, + dianpu_leixing_id__in=leixing_ids, + zhuanqu_id__in=zhuanqu_ids, + fengjin_zhuangtai=False, + shangjia_zhuangtai=True, + shenhe_zhuangtai=True, # 🔥 平台审核通过 + shenhezhuangtai=1 # 原有审核状态:正常 + ).values( + 'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu', + 'dianpu_leixing_id', 'zhuanqu_id' + ).order_by('-paixu', 'id') + + shangpin_data = list(shangpin_queryset) + # 字段映射:前端统一使用 leixing_id + for sp in shangpin_data: + sp['leixing_id'] = sp.pop('dianpu_leixing_id') + else: + shangpin_data = [] + + return { + 'shangpinleixing': leixing_data, + 'shangpinzhuanqu': zhuanqu_data, + 'shangpinliebiao': shangpin_data + } + + def _get_public_goods(self, use_audit_status_one=True): + """ + 获取公共商品 + 类型、专区、商品均从公共表获取。 + - 必须满足 dianpu 为空且 dianpu_leixing 为空(即纯公共数据)。 + - 审核状态:若 use_audit_status_one=True,则返回 shenhezhuangtai=1 的数据; + 否则返回 shenhezhuangtai=2 的数据(审核专用)。 + """ + target_shenhe = 1 if use_audit_status_one else 2 + + # 1. 公共商品类型:原有审核状态为 target_shenhe + leixing_queryset = ShangpinLeixing.query.filter( + shenhezhuangtai=target_shenhe + ).order_by('-paixu', 'id') + + leixing_data = [] + for lx in leixing_queryset: + leixing_data.append({ + 'id': lx.id, + 'jieshao': lx.jieshao, + 'tupian_url': lx.tupian_url, + 'paixu': lx.paixu + }) + + visible_leixing_ids = [lx['id'] for lx in leixing_data] + + # 2. 专区:dianpu 为空,且 dianpu_leixing 为空,leixing_id 在可见范围内,原有审核状态为 target_shenhe + if visible_leixing_ids: + zhuanqu_queryset = ShangpinZhuanqu.query.filter( + dianpu__isnull=True, + dianpu_leixing__isnull=True, # 🔥 必须为空 + leixing_id__in=visible_leixing_ids, + shenhezhuangtai=target_shenhe + ).order_by('-paixu', 'id') + else: + zhuanqu_queryset = ShangpinZhuanqu.query.none() + + zhuanqu_data = [] + for zq in zhuanqu_queryset: + zhuanqu_data.append({ + 'id': zq.id, + 'mingzi': zq.mingzi, + 'leixing_id': zq.leixing_id, + 'paixu': zq.paixu + }) + + # 3. 商品:dianpu 为空,dianpu_leixing 为空,专区在可见范围内,原有审核状态为 target_shenhe + zhuanqu_ids = [zq['id'] for zq in zhuanqu_data] + if zhuanqu_ids: + shangpin_queryset = Shangpin.query.filter( + dianpu__isnull=True, + dianpu_leixing__isnull=True, # 🔥 必须为空 + leixing_id__in=visible_leixing_ids, + zhuanqu_id__in=zhuanqu_ids, + shenhezhuangtai=target_shenhe + ).values( + 'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu', + 'leixing_id', 'zhuanqu_id' + ).order_by('-paixu', 'id') + shangpin_data = list(shangpin_queryset) + else: + shangpin_data = [] + + return { + 'shangpinleixing': leixing_data, + 'shangpinzhuanqu': zhuanqu_data, + 'shangpinliebiao': shangpin_data + } + + +class ShangdianLoginView(APIView): + """ + 商家后台登录接口(通过凭证直接获取店铺) + POST /shangdian/login + 请求参数: + yonghuid : 用户ID(7位) + zhanghao : 登录账号 + mima : 密码 + 返回数据: + code: 0, data: { + token: "jwt_token", + yonghuid: "1234567", + dianpu_mingcheng: "店铺名称", + dianpu_touxiang: "头像相对URL", + erweima_url: "二维码相对URL" + } + """ + permission_classes = [AllowAny] + + def post(self, request): + # 1. 获取请求参数 + yonghuid = request.data.get('yonghuid', '').strip() + zhanghao = request.data.get('zhanghao', '').strip() + mima = request.data.get('mima', '').strip() + + # 2. 参数完整性校验 + if not yonghuid or not zhanghao or not mima: + return Response({'code': 400, 'msg': '用户ID、账号、密码均不能为空'}) + + # 3. 验证用户主表是否存在 + try: + user_main = User.query.get(UserUID=yonghuid) + except User.DoesNotExist: + logger.warning(f"登录失败:用户ID不存在 - {yonghuid}") + return Response({'code': 404, 'msg': '账号或密码错误'}) + + # 4. 通过用户ID + 账号查询凭证表 + try: + pingzheng = YonghuPingzheng.query.get( + yonghu=yonghuid, + zhanghao=zhanghao, + is_active=True + ) + except YonghuPingzheng.DoesNotExist: + logger.warning(f"登录失败:账号或密码错误 - 用户ID:{yonghuid}, 账号:{zhanghao}") + return Response({'code': 401, 'msg': '账号或密码错误'}) + + # 5. 校验密码 + if pingzheng.mima != mima: + logger.warning(f"登录失败:密码错误 - 用户ID:{yonghuid}, 账号:{zhanghao}") + return Response({'code': 401, 'msg': '账号或密码错误'}) + + # 6. 【修改点】通过凭证直接获取关联的店铺(利用 OneToOneField 反向关系) + try: + dianpu = pingzheng.dianpu + except Dianpu.DoesNotExist: + logger.error(f"登录异常:凭证 {zhanghao} 未关联任何店铺") + return Response({'code': 403, 'msg': '您尚未关联店铺,请联系管理员'}) + + # 7. 检查店铺状态 + if dianpu.zhuangtai != 1: + logger.warning(f"登录失败:店铺已被封禁 - 店铺ID:{dianpu.id}, 用户ID:{yonghuid}") + return Response({'code': 403, 'msg': '店铺已被封禁,无法登录'}) + + # 8. 生成 JWT Token + refresh = RefreshToken.for_user(user_main) + token = str(refresh.access_token) + + # 9. 更新最后登录时间 + user_main.last_login_time = timezone.now() + user_main.save(update_fields=['last_login_time']) + + # 10. 构造返回数据 + data = { + 'token': token, + 'yonghuid': user_main.yonghuid, + 'dianpu_mingcheng': dianpu.dianpu_mingcheng, + 'dianpu_touxiang': dianpu.dianpu_touxiang or '', + 'erweima_url': dianpu.erweima_url or '' + } + + logger.info(f"商家登录成功:用户ID {yonghuid} - 店铺 {dianpu.dianpu_mingcheng}") + return Response({'code': 0, 'msg': '登录成功', 'data': data}) + + + + +class ShopOverviewView(APIView): + """ + 店铺概览数据接口 + URL: /shangdian/sjdpgl + Method: POST + 权限: JWT认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 从请求体中获取前端传递的 userId(用于防越权) + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response( + {'code': 400, 'msg': '缺少必要参数 userId'} + ) + + # 调用公共验证方法 + dianpu, error_response = verify_shop_permission(request, frontend_user_id) + if error_response: + return error_response + + # 验证通过,构造返回数据(新增字段,其余保持不变) + data = { + '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 '', + 'create_time': dianpu.create_time.isoformat() if dianpu.create_time else '', + 'update_time': dianpu.update_time.isoformat() if dianpu.update_time else '', + # 新增字段 + 'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng, + 'shouyi_choucheng_feilv': float(dianpu.shouyi_choucheng_feilv) if dianpu.shouyi_choucheng_feilv else 0.0, + 'ketixian_yue': float(dianpu.ketixian_yue), + 'zhengqu_zonge': float(dianpu.zhengqu_zonge), + 'meiri_tixian_xiane': float(dianpu.meiri_tixian_xiane) if dianpu.meiri_tixian_xiane else 0.0, + 'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane, + } + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': data + }) + + + +class QrcodeDownloadView(APIView): + """ + 店铺二维码下载接口(通过后端代理下载,解决跨域限制) + URL: /shangdian/xzewm?dianpu_id=xx + 权限: JWT认证 + 店铺归属验证 + """ + permission_classes = [IsAuthenticated] + + def get(self, request): + # 1. 身份验证与店铺校验 + frontend_user_id = request.GET.get('userId') # 前端通过查询参数传递 userId + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}, status=400) + + dianpu, error_response = verify_shop_permission(request, frontend_user_id) + if error_response: + return error_response + + # 2. 获取店铺的二维码相对路径 + erweima_relative_url = dianpu.erweima_url + if not erweima_relative_url: + return HttpResponse('店铺暂无二维码', status=404) + + # 3. 拼接 OSS 完整访问地址(使用 settings 中的域名) + oss_domain = getattr(settings, 'COS_DOMAIN', '') + if not oss_domain: + logger.error("COS_DOMAIN 未配置") + return HttpResponse('OSS域名未配置', status=500) + + image_url = oss_domain.rstrip('/') + '/' + erweima_relative_url.lstrip('/') + + # 4. 从腾讯云 COS 获取图片内容 + try: + client = get_oss_client() + bucket = getattr(settings, 'COS_BUCKET', '') + response = client.get_object(Bucket=bucket, Key=erweima_relative_url) + image_data = response['Body'].get_raw_stream().read() + except Exception as e: + logger.exception("从COS获取二维码失败") + return HttpResponse('下载失败,请稍后重试', status=502) + + # 5. 构造下载响应,强制浏览器保存为文件 + filename = f"店铺二维码_{dianpu.dianpu_mingcheng}.png" + response = HttpResponse(image_data, content_type='image/png') + response['Content-Disposition'] = f'attachment; filename="{filename}"' + response['Content-Length'] = str(len(image_data)) + return response + + + + +class DianpuShouzhiTongjiView(APIView): + """ + 店铺每日收支统计接口(支持按日、月、年聚合) + URL: /dianpu/dpsztj + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 身份验证 + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少 userId'}) + + dianpu, error = verify_shop_permission(request, frontend_user_id) + if error: + return error + + granularity = request.data.get('granularity', 'day') # day / month / year + year = request.data.get('year') + month = request.data.get('month') + + # 查询当前店铺的统计记录 + qs = DianpuShouzhiMeiriTongji.query.filter(dianpu_id=dianpu.id) + + if granularity == 'day': + if not year or not month: + return Response({'code': 400, 'msg': '日颗粒度需要提供年份和月份'}) + try: + year = int(year) + month = int(month) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '年份或月份格式错误'}) + + # 取出该月每一天的数据,并按日排序 + daily_data = qs.filter(nian=year, yue=month).values('ri').annotate( + xiadan_zongliang=Sum('xiadan_zongliang'), + xiadan_zonge=Sum('xiadan_zonge'), + chengjiao_zongliang=Sum('chengjiao_zongliang'), + chengjiao_zonge=Sum('chengjiao_zonge'), + tuikuan_zongliang=Sum('tuikuan_zongliang'), + tuikuan_zonge=Sum('tuikuan_zonge'), + shouyi_zonge=Sum('shouyi_zonge'), + ).order_by('ri') + + # 补全缺失的日期(默认值为0) + days_in_month = calendar.monthrange(year, month)[1] + result = [] + for day in range(1, days_in_month + 1): + found = next((item for item in daily_data if item['ri'] == day), None) + if found: + result.append(found) + else: + result.append({ + 'ri': day, + 'xiadan_zongliang': 0, 'xiadan_zonge': 0, + 'chengjiao_zongliang': 0, 'chengjiao_zonge': 0, + 'tuikuan_zongliang': 0, 'tuikuan_zonge': 0, + 'shouyi_zonge': 0, + }) + return Response({'code': 0, 'data': result, 'type': 'day'}) + + elif granularity == 'month': + if not year: + return Response({'code': 400, 'msg': '月颗粒度需要提供年份'}) + try: + year = int(year) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '年份格式错误'}) + + # 按月份聚合 + monthly_data = qs.filter(nian=year).values('yue').annotate( + xiadan_zongliang=Sum('xiadan_zongliang'), + xiadan_zonge=Sum('xiadan_zonge'), + chengjiao_zongliang=Sum('chengjiao_zongliang'), + chengjiao_zonge=Sum('chengjiao_zonge'), + tuikuan_zongliang=Sum('tuikuan_zongliang'), + tuikuan_zonge=Sum('tuikuan_zonge'), + shouyi_zonge=Sum('shouyi_zonge'), + ).order_by('yue') + + # 补全 1~12 月 + result = [] + for m in range(1, 13): + found = next((item for item in monthly_data if item['yue'] == m), None) + if found: + result.append(found) + else: + result.append({ + 'yue': m, + 'xiadan_zongliang': 0, 'xiadan_zonge': 0, + 'chengjiao_zongliang': 0, 'chengjiao_zonge': 0, + 'tuikuan_zongliang': 0, 'tuikuan_zonge': 0, + 'shouyi_zonge': 0, + }) + return Response({'code': 0, 'data': result, 'type': 'month'}) + + elif granularity == 'year': + # 按年份聚合 + yearly_data = qs.values('nian').annotate( + xiadan_zongliang=Sum('xiadan_zongliang'), + xiadan_zonge=Sum('xiadan_zonge'), + chengjiao_zongliang=Sum('chengjiao_zongliang'), + chengjiao_zonge=Sum('chengjiao_zonge'), + tuikuan_zongliang=Sum('tuikuan_zongliang'), + tuikuan_zonge=Sum('tuikuan_zonge'), + shouyi_zonge=Sum('shouyi_zonge'), + ).order_by('nian') + return Response({'code': 0, 'data': list(yearly_data), 'type': 'year'}) + + else: + return Response({'code': 400, 'msg': '无效的颗粒度'}) + + + +class ShopQrcodeView(APIView): + """ + 店铺专属二维码生成/更新接口 + URL: /shangdian/sdscdpm + Method: POST + 权限: JWT认证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 获取前端传递的 userId + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response( + {'code': 400, 'msg': '缺少必要参数 userId'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 公共验证 + dianpu, error_response = verify_shop_permission(request, frontend_user_id) + if error_response: + return error_response + + # 获取缓存的微信 access_token + access_token = self._get_wx_access_token() + if not access_token: + return Response( + {'code': 500, 'msg': '获取微信 access_token 失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 构造小程序码请求参数 + # 注意:scene 最大32个字符,我们使用店铺ID + scene = str(dianpu.id) + page_path = "pages/index/index" # 前端小程序首页路径 + wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}' + post_data = { + 'scene': scene, + 'page': page_path, + 'width': 430, + 'auto_color': False, + 'line_color': {'r': 0, 'g': 0, 'b': 0}, + } + + # 调用微信接口生成小程序码 + try: + wx_resp = requests.post(wx_url, json=post_data, timeout=10) + except Exception as e: + logger.error(f"请求微信生成二维码接口异常: {e}") + return Response( + {'code': 500, 'msg': '调用微信服务失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 判断返回是否为图片 + if wx_resp.status_code != 200 or 'image' not in wx_resp.headers.get('content-type', ''): + error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {} + errmsg = error_info.get('errmsg', '未知错误') + logger.error(f"微信生成二维码失败: {errmsg}") + return Response( + {'code': 500, 'msg': f'生成二维码失败: {errmsg}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 构造文件名和 COS 路径 + timestamp = int(time.time()) + # 三层文件夹结构: dianpu/shangjiadianpu/dianpuerweima/ + filename = f"dianpu_{dianpu.id}_{timestamp}.png" + oss_path = f"dianpu/shangjiadianpu/dianpuerweima/{filename}" + file_obj = io.BytesIO(wx_resp.content) + + # 上传到 COS + try: + full_url = upload_to_oss(file_obj, oss_path) + except Exception as e: + logger.error(f"上传二维码到 COS 失败: {e}") + return Response( + {'code': 500, 'msg': '图片上传失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + if not full_url: + return Response( + {'code': 500, 'msg': '图片上传失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 提取相对路径(根据您的 upload_to_oss 返回值格式调整) + cos_domain = getattr(settings, 'COS_DOMAIN', '') + if cos_domain and full_url.startswith(cos_domain): + relative_path = full_url[len(cos_domain):].lstrip('/') + else: + relative_path = oss_path + + # 删除旧二维码文件(如果存在) + old_relative_path = dianpu.erweima_url + if old_relative_path: + try: + delete_from_oss(old_relative_path) + except Exception as e: + logger.warning(f"删除旧二维码文件失败: {old_relative_path}, 错误: {e}") + + # 更新数据库 + with transaction.atomic(): + dianpu.erweima_url = relative_path + dianpu.save(update_fields=['erweima_url', 'update_time']) + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': {'erweima_url': relative_path} + }) + + def _get_wx_access_token(self): + """ + 获取微信小程序 access_token,带缓存 + 缓存键: wx_mini_access_token + """ + cache_key = 'wx_mini_access_token' + token = cache.get(cache_key) + if token: + return token + + appid = getattr(settings, 'WEIXIN_APPID', '') + secret = getattr(settings, 'WEIXIN_SECRET', '') + if not appid or not secret: + logger.error("微信小程序 AppID 或 Secret 未配置") + return None + + url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}' + try: + resp = requests.get(url, timeout=5) + data = resp.json() + token = data.get('access_token') + expires_in = data.get('expires_in', 7200) + if token: + # 提前200秒过期,防止边界情况 + cache.set(cache_key, token, expires_in - 200) + return token + except Exception as e: + logger.error(f"获取微信 access_token 异常: {e}") + return None + + + + + + +class ShopProductConfigView(APIView): + """ + 获取商品发布所需配置:公共类型、店铺商品类型、专区及各自商品数量 + URL: /shangdian/dphqspxx + 权限: JWT认证 + 店铺归属验证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证,获取店铺对象 + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}) + + dianpu, error = verify_shop_permission(request, frontend_user_id) + if error: + return error + + # ---------- 2. 公共商品类型(需审核正常) ---------- + try: + public_types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values( + 'id', 'jieshao', 'tupian_url' + ) + public_types = [{ + 'id': pt['id'], + 'jieshao': pt['jieshao'] or '', + 'tupian_url': pt['tupian_url'] or '', + } for pt in public_types_qs] + except Exception as e: + logger.exception("查询公共商品类型失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + # ---------- 3. 店铺商品类型 ---------- + try: + shop_types = ShangpinLeixingDianpu.query.filter( + dianpu_id=dianpu.id + ).select_related('gonggong_leixing').order_by('-paixu') + except Exception as e: + logger.exception("查询店铺商品类型失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + type_ids = [] + shop_product_types = [] + for t in shop_types: + shop_product_types.append({ + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'paixu': t.paixu, + 'shangjia_zhuangtai': t.shangjia_zhuangtai, + 'fengjin_zhuangtai': t.fengjin_zhuangtai, + 'shenhe_zhuangtai': t.shenhe_zhuangtai, + 'gonggong_leixing_id': t.gonggong_leixing_id, + }) + type_ids.append(t.id) + + # ---------- 4. 各类型商品数量统计 ---------- + type_product_counts = {} + if type_ids: + counts = Shangpin.query.filter( + dianpu_id=dianpu.id, + dianpu_leixing_id__in=type_ids + ).values('dianpu_leixing_id').annotate(cnt=Count('id')) + type_product_counts = {item['dianpu_leixing_id']: item['cnt'] for item in counts} + + # ---------- 5. 专区列表 ---------- + try: + zhuanqu_qs = ShangpinZhuanqu.query.filter( + dianpu_id=dianpu.id + ).order_by('-paixu').values( + 'id', 'mingzi', 'paixu', 'shangjia_zhuangtai', + 'fengjin_zhuangtai', 'dianpu_leixing_id' + ) + zhuanqu_list = [] + zhuanqu_ids = [] + for zq in zhuanqu_qs: + zhuanqu_list.append({ + 'id': zq['id'], + 'mingzi': zq['mingzi'] or '', + 'paixu': zq['paixu'], + 'shangjia_zhuangtai': zq['shangjia_zhuangtai'], + 'fengjin_zhuangtai': zq['fengjin_zhuangtai'], + 'dianpu_leixing_id': zq['dianpu_leixing_id'], + }) + zhuanqu_ids.append(zq['id']) + except Exception as e: + logger.exception("查询专区失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + # ---------- 6. 各专区商品数量统计 ---------- + zhuanqu_product_counts = {} + if zhuanqu_ids: + counts = Shangpin.query.filter( + dianpu_id=dianpu.id, + zhuanqu_id__in=zhuanqu_ids + ).values('zhuanqu_id').annotate(cnt=Count('id')) + zhuanqu_product_counts = {item['zhuanqu_id']: item['cnt'] for item in counts} + + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'public_types': public_types, + 'shop_product_types': shop_product_types, + 'zhuanqu_list': zhuanqu_list, + 'type_product_counts': type_product_counts, + 'zhuanqu_product_counts': zhuanqu_product_counts, + } + }) + + + + +class ShopProductModifyView(APIView): + """店铺商品类型与专区管理(增、改、删)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + frontend_user_id = request.data.get('userId') or request.POST.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}) + + dianpu, error = verify_shop_permission(request, frontend_user_id) + if error: + return error + + action = request.data.get('action') or request.POST.get('action') + if not action: + return Response({'code': 400, 'msg': '缺少操作类型 action'}) + + handlers = { + 'add_type': self._add_type, + 'update_type': self._update_type, + 'delete_type': self._delete_type, + 'add_zhuanqu': self._add_zhuanqu, + 'update_zhuanqu': self._update_zhuanqu, + 'delete_zhuanqu': self._delete_zhuanqu, + } + handler = handlers.get(action) + if not handler: + return Response({'code': 400, 'msg': '不支持的操作类型'}) + + return handler(dianpu, request) + + except Exception as e: + logger.exception("ShopProductModifyView 全局异常") + return Response({'code': 500, 'msg': '服务器内部错误'}) + + # ---------- 商品类型操作 ---------- + @transaction.atomic + def _add_type(self, dianpu, request): + """添加店铺商品类型""" + try: + jieshao = request.data.get('jieshao') or request.POST.get('jieshao') + gonggong_leixing_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id') + paixu = request.data.get('paixu', 0) or request.POST.get('paixu', 0) + + if not jieshao or not gonggong_leixing_id: + return Response({'code': 400, 'msg': '缺少介绍或公共类型ID'}) + + # 验证公共类型存在且正常 + if not ShangpinLeixing.query.filter(id=gonggong_leixing_id, shenhezhuangtai=1).exists(): + return Response({'code': 400, 'msg': '所选公共商品类型无效'}) + + image_file = request.FILES.get('image') + if not image_file: + return Response({'code': 400, 'msg': '必须上传类型图片'}) + + # 上传图片 + file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(image_file, file_path) + if not full_url: + return Response({'code': 500, 'msg': '图片上传失败'}) + + # 提取相对路径 + tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + + # 审核状态 + audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() + shenhe_status = not (audit_config and audit_config.kaiqi_shenhe) + + ShangpinLeixingDianpu.query.create( + dianpu_id=dianpu.id, + gonggong_leixing_id=gonggong_leixing_id, + jieshao=jieshao, + tupian_url=tupian_url, + paixu=paixu, + shangjia_zhuangtai=True, + fengjin_zhuangtai=False, + shenhe_zhuangtai=shenhe_status, + ) + return Response({'code': 0, 'msg': '商品类型添加成功'}) + except Exception as e: + logger.exception("添加商品类型失败") + return Response({'code': 500, 'msg': '添加失败,请稍后重试'}) + + @transaction.atomic + def _update_type(self, dianpu, request): + """修改店铺商品类型,支持更换图片(先删旧图)""" + type_id = request.data.get('type_id') or request.POST.get('type_id') + if not type_id: + return Response({'code': 400, 'msg': '缺少 type_id'}) + + try: + obj = ShangpinLeixingDianpu.objects.select_for_update().get( + id=type_id, dianpu_id=dianpu.id + ) + except ShangpinLeixingDianpu.DoesNotExist: + return Response({'code': 404, 'msg': '商品类型不存在'}) + except Exception as e: + logger.exception("查询商品类型异常") + return Response({'code': 500, 'msg': '服务器错误'}) + + try: + update_fields = [] + content_changed = False + + # 简介 + if 'jieshao' in request.data or 'jieshao' in request.POST: + obj.jieshao = request.data.get('jieshao') or request.POST.get('jieshao') + update_fields.append('jieshao') + content_changed = True + + # 映射公共类型 + if 'gonggong_leixing_id' in request.data or 'gonggong_leixing_id' in request.POST: + new_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id') + if not ShangpinLeixing.query.filter(id=new_id, shenhezhuangtai=1).exists(): + return Response({'code': 400, 'msg': '公共商品类型无效'}) + obj.gonggong_leixing_id = new_id + update_fields.append('gonggong_leixing_id') + content_changed = True + + # 排序 + if 'paixu' in request.data or 'paixu' in request.POST: + obj.paixu = request.data.get('paixu') or request.POST.get('paixu') + update_fields.append('paixu') + + # 上架状态 + if 'shangjia_zhuangtai' in request.data or 'shangjia_zhuangtai' in request.POST: + val = request.data.get('shangjia_zhuangtai') or request.POST.get('shangjia_zhuangtai') + if isinstance(val, str): + val = val.lower() in ('true', '1', 'yes') + obj.shangjia_zhuangtai = bool(val) + update_fields.append('shangjia_zhuangtai') + + # 图片更换:先删除旧图,再上传新图 + image_file = request.FILES.get('image') + if image_file: + if obj.tupian_url: + delete_from_oss(obj.tupian_url) # 删除旧图片 + file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(image_file, file_path) + if not full_url: + return Response({'code': 500, 'msg': '图片上传失败'}) + obj.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + update_fields.append('tupian_url') + content_changed = True + + # 审核重置 + if content_changed: + audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() + if audit_config and audit_config.kaiqi_shenhe: + obj.shenhe_zhuangtai = False + update_fields.append('shenhe_zhuangtai') + + if update_fields: + obj.save(update_fields=update_fields) + return Response({'code': 0, 'msg': '商品类型更新成功'}) + except Exception as e: + logger.exception("修改商品类型失败") + return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) + + @transaction.atomic + def _delete_type(self, dianpu, request): + """删除店铺商品类型,可选是否同时删除关联商品""" + type_id = request.data.get('id') or request.POST.get('id') + if not type_id: + return Response({'code': 400, 'msg': '缺少类型ID'}) + + try: + obj = ShangpinLeixingDianpu.objects.select_for_update().get( + id=type_id, dianpu_id=dianpu.id + ) + except ShangpinLeixingDianpu.DoesNotExist: + return Response({'code': 404, 'msg': '商品类型不存在'}) + except Exception as e: + logger.exception("查询商品类型异常") + return Response({'code': 500, 'msg': '服务器错误'}) + + try: + delete_products = request.data.get('delete_products') or request.POST.get('delete_products') + delete_products = str(delete_products).lower() in ('true', '1', 'yes') + + if delete_products: + # 删除该类型下所有关联商品(同时清理图片) + products = Shangpin.query.filter( + dianpu_leixing_id=obj.id, dianpu_id=dianpu.id + ) + for p in products: + if p.tupian_url: + delete_from_oss(p.tupian_url) + if p.guize_tupian: + delete_from_oss(p.guize_tupian) + p.delete() + else: + # 仅解除关联,不删商品 + Shangpin.query.filter( + dianpu_leixing_id=obj.id, dianpu_id=dianpu.id + ).update(dianpu_leixing=None) + + # 删除类型自身图片 + if obj.tupian_url: + delete_from_oss(obj.tupian_url) + obj.delete() + return Response({'code': 0, 'msg': '类型删除成功'}) + except Exception as e: + logger.exception("删除商品类型失败") + return Response({'code': 500, 'msg': '删除失败,请稍后重试'}) + + # ---------- 专区操作 ---------- + @transaction.atomic + def _add_zhuanqu(self, dianpu, request): + """添加专区,自动从店铺商品类型获取 leixing_id(公共商品类型ID)""" + try: + mingzi = request.data.get('mingzi') + dianpu_leixing_id = request.data.get('dianpu_leixing_id') + paixu = request.data.get('paixu', 0) + + if not mingzi or not dianpu_leixing_id: + return Response({'code': 400, 'msg': '缺少名称或所属商品类型'}) + + # 获取店铺商品类型,并拿到映射的公共类型ID + try: + shop_type = ShangpinLeixingDianpu.query.get( + id=dianpu_leixing_id, dianpu_id=dianpu.id + ) + leixing_id = shop_type.gonggong_leixing_id # 这就是专区需要的 leixing_id + except ShangpinLeixingDianpu.DoesNotExist: + return Response({'code': 400, 'msg': '所选商品类型无效'}) + + # 验证公共类型有效 + if not ShangpinLeixing.query.filter(id=leixing_id, shenhezhuangtai=1).exists(): + return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) + + ShangpinZhuanqu.query.create( + dianpu_id=dianpu.id, + mingzi=mingzi, + dianpu_leixing_id=dianpu_leixing_id, + leixing_id=leixing_id, # 自动填充 + paixu=paixu, + shangjia_zhuangtai=True, + fengjin_zhuangtai=False, + ) + return Response({'code': 0, 'msg': '专区添加成功'}) + except Exception as e: + logger.exception("添加专区失败") + return Response({'code': 500, 'msg': '添加失败,请稍后重试'}) + + @transaction.atomic + def _update_zhuanqu(self, dianpu, request): + """修改专区,若更换所属商品类型则同步更新 leixing_id""" + zhuanqu_id = request.data.get('zhuanqu_id') + if not zhuanqu_id: + return Response({'code': 400, 'msg': '缺少 zhuanqu_id'}) + + try: + obj = ShangpinZhuanqu.objects.select_for_update().get( + id=zhuanqu_id, dianpu_id=dianpu.id + ) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + except Exception as e: + logger.exception("查询专区异常") + return Response({'code': 500, 'msg': '服务器错误'}) + + try: + update_fields = [] + + if 'mingzi' in request.data: + obj.mingzi = request.data.get('mingzi') + update_fields.append('mingzi') + + if 'dianpu_leixing_id' in request.data: + new_id = request.data.get('dianpu_leixing_id') + try: + shop_type = ShangpinLeixingDianpu.query.get( + id=new_id, dianpu_id=dianpu.id + ) + obj.dianpu_leixing_id = new_id + obj.leixing_id = shop_type.gonggong_leixing_id # 同步更新公共类型ID + update_fields.extend(['dianpu_leixing_id', 'leixing_id']) + except ShangpinLeixingDianpu.DoesNotExist: + return Response({'code': 400, 'msg': '所选商品类型无效'}) + + if 'paixu' in request.data: + obj.paixu = request.data.get('paixu') + update_fields.append('paixu') + + if 'shangjia_zhuangtai' in request.data: + val = request.data.get('shangjia_zhuangtai') + if isinstance(val, str): + val = val.lower() in ('true', '1', 'yes') + obj.shangjia_zhuangtai = bool(val) + update_fields.append('shangjia_zhuangtai') + + if update_fields: + obj.save(update_fields=update_fields) + return Response({'code': 0, 'msg': '专区更新成功'}) + except Exception as e: + logger.exception("修改专区失败") + return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) + + @transaction.atomic + def _delete_zhuanqu(self, dianpu, request): + """删除专区,可选是否同时删除关联商品""" + zhuanqu_id = request.data.get('id') + if not zhuanqu_id: + return Response({'code': 400, 'msg': '缺少专区ID'}) + + try: + obj = ShangpinZhuanqu.objects.select_for_update().get( + id=zhuanqu_id, dianpu_id=dianpu.id + ) + except ShangpinZhuanqu.DoesNotExist: + return Response({'code': 404, 'msg': '专区不存在'}) + except Exception as e: + logger.exception("查询专区异常") + return Response({'code': 500, 'msg': '服务器错误'}) + + try: + delete_products = request.data.get('delete_products') + delete_products = str(delete_products).lower() in ('true', '1', 'yes') + + if delete_products: + products = Shangpin.query.filter( + zhuanqu_id=obj.id, dianpu_id=dianpu.id + ) + for p in products: + if p.tupian_url: + delete_from_oss(p.tupian_url) + if p.guize_tupian: + delete_from_oss(p.guize_tupian) + p.delete() + else: + Shangpin.query.filter( + zhuanqu_id=obj.id, dianpu_id=dianpu.id + ).update(zhuanqu_id=None) + + obj.delete() + return Response({'code': 0, 'msg': '专区删除成功'}) + except Exception as e: + logger.exception("删除专区失败") + return Response({'code': 500, 'msg': '删除失败,请稍后重试'}) + + + +class ShopProductListView(APIView): + """ + 商品列表接口,支持筛选、分页。 + URL: /shangdian/sdhqspsj + 权限: JWT认证 + 店铺验证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证,获取店铺对象 + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}) + + dianpu, error = verify_shop_permission(request, frontend_user_id) + if error: + return error + + # 2. 安全解析分页与筛选参数 + try: + page = int(request.data.get('page', 1)) + page_size = int(request.data.get('page_size', 20)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': '分页参数格式错误'}) + + keyword = str(request.data.get('keyword', '')).strip() + shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型ID + zhuanqu_id = request.data.get('zhuanqu_id') # 专区ID + + # 布尔型筛选字段 + def parse_bool(val): + if val is None or val == '': + return None + if isinstance(val, bool): + return val + if isinstance(val, str): + return val.lower() in ('true', '1', 'yes') + 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')) + + # 3. 构建查询(只查当前店铺的商品) + products = Shangpin.query.filter(dianpu_id=dianpu.id).select_related( + 'dianpu_leixing' + ).only( + 'id', 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'zhuanqu_id', + 'zhenshi_xiaoliang', 'duiwai_xiaoliang', 'jieshao', 'xiadan_xuzhi', + 'guize_tupian', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', + 'tupian_url', 'ewai_dashou_fencheng', 'paixu', + 'dianpu_leixing_id', 'shangjia_zhuangtai', 'fengjin_zhuangtai', + 'shenhe_zhuangtai', 'create_time', 'update_time' + ) + + # 筛选 + 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 格式错误'}) + + if zhuanqu_id is not None: + try: + products = products.filter(zhuanqu_id=int(zhuanqu_id)) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'zhuanqu_id 格式错误'}) + + 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、标题、介绍) + if keyword: + products = products.filter( + models.Q(id__icontains=keyword) | + models.Q(biaoqian__icontains=keyword) | + models.Q(jieshao__icontains=keyword) + ) + + # 4. 分页 + total_count = products.count() + paginator = Paginator(products, page_size) + page_obj = paginator.get_page(page) + + # 5. 构造返回数据 + 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, + 'zhuanqu_id': p.zhuanqu_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, + 'tupian_url': p.tupian_url or '', + 'ewai_dashou_fencheng': p.ewai_dashou_fencheng, + 'paixu': p.paixu, + 'dianpu_leixing_id': p.dianpu_leixing_id, + 'shangjia_zhuangtai': p.shangjia_zhuangtai, + 'fengjin_zhuangtai': p.fengjin_zhuangtai, + 'shenhe_zhuangtai': p.shenhe_zhuangtai, + 'create_time': p.create_time.isoformat() if p.create_time else '', + 'update_time': p.update_time.isoformat() if p.update_time else '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': product_list, + 'total': total_count, + } + }) + + + +class ShopProductModifyView1(APIView): + """ + 商品统一操作:修改、添加、删除 + URL: /shangdian/xgdpsplx1 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}) + + dianpu, error = verify_shop_permission(request, frontend_user_id) + if error: + return error + + action = request.data.get('action') + if not action: + return Response({'code': 400, 'msg': '缺少操作类型 action'}) + + if action == 'update_product': + return self._update_product(dianpu, request.data, request.FILES) + elif action == 'add_product': + return self._add_product(dianpu, request.data, request.FILES) + elif action == 'delete_product': + return self._delete_product(dianpu, request.data) + else: + return Response({'code': 400, 'msg': '不支持的操作类型'}) + + # ---------- 修改商品 ---------- + @transaction.atomic + def _update_product(self, dianpu, data, files): + product_id = data.get('product_id') + if not product_id: + return Response({'code': 400, 'msg': '缺少 product_id'}) + + try: + product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + update_fields = [] + content_changed = False + + # 辅助函数:安全转换为整数,失败则返回原值(让其报错) + def to_int(val): + try: + return int(val) + except (TypeError, ValueError): + return val + + def to_float(val): + try: + return float(val) + except (TypeError, ValueError): + return val + + # 标题 + if 'biaoqian' in data: + product.biaoqian = data['biaoqian'] + update_fields.append('biaoqian') + content_changed = True + + # 价格 + if 'jiage' in data: + product.jiage = to_float(data['jiage']) + update_fields.append('jiage') + content_changed = True + + # 库存 + if 'kucun' in data: + product.kucun = to_int(data['kucun']) + update_fields.append('kucun') + content_changed = True + + # 店铺商品类型 + if 'dianpu_leixing_id' in data: + new_type_id = to_int(data['dianpu_leixing_id']) + if not ShangpinLeixingDianpu.query.filter(id=new_type_id, dianpu_id=dianpu.id).exists(): + return Response({'code': 400, 'msg': '无效的店铺商品类型'}) + product.dianpu_leixing_id = new_type_id + update_fields.append('dianpu_leixing_id') + content_changed = True + + # 专区 + if 'zhuanqu_id' in data: + new_zq_id = data['zhuanqu_id'] + if new_zq_id is not None and new_zq_id != '': + new_zq_id = to_int(new_zq_id) + if not ShangpinZhuanqu.query.filter(id=new_zq_id, dianpu_id=dianpu.id).exists(): + return Response({'code': 400, 'msg': '无效的专区'}) + product.zhuanqu_id = new_zq_id + else: + product.zhuanqu_id = None + update_fields.append('zhuanqu_id') + content_changed = True + + # 介绍 + if 'jieshao' in data: + product.jieshao = data['jieshao'] + update_fields.append('jieshao') + content_changed = True + + # 下单须知 + if 'xiadan_xuzhi' in data: + product.xiadan_xuzhi = data['xiadan_xuzhi'] + update_fields.append('xiadan_xuzhi') + + # ★ 抢单要求类型及会员/佣金自动处理 + if 'yaoqiuleixing' in data: + yqlx = to_int(data['yaoqiuleixing']) + if yqlx not in [1, 2]: + return Response({'code': 400, 'msg': '抢单要求类型无效'}) + product.yaoqiuleixing = yqlx + update_fields.append('yaoqiuleixing') + content_changed = True + + if yqlx == 1: + # 会员抢单:优先使用前端显式传递的非空会员ID + if 'huiyuan_id' in data and data.get('huiyuan_id'): + product.huiyuan_id = data['huiyuan_id'] + update_fields.append('huiyuan_id') + product.yongjin = None + if 'yongjin' not in update_fields: + update_fields.append('yongjin') + else: + # 自动从公共类型获取会员ID,或转为佣金 + try: + shop_type = ShangpinLeixingDianpu.query.get( + id=product.dianpu_leixing_id, dianpu_id=dianpu.id + ) + public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id) + auto_huiyuan_id = public_type.huiyuan_id + if auto_huiyuan_id: + product.huiyuan_id = auto_huiyuan_id + update_fields.append('huiyuan_id') + product.yongjin = None + if 'yongjin' not in update_fields: + update_fields.append('yongjin') + elif public_type.yaoqiuleixing == 2 and public_type.yongjin: + # 无会员,强制转为佣金 + product.yaoqiuleixing = 2 + product.yongjin = public_type.yongjin + product.huiyuan_id = None + # yaoqiuleixing 已在 update_fields 中,需要添加 huiyuan_id 和 yongjin + update_fields.append('huiyuan_id') + update_fields.append('yongjin') + else: + return Response({'code': 400, 'msg': '会员抢单缺少会员ID,公共类型也无会员ID或佣金'}) + except (ShangpinLeixingDianpu.DoesNotExist, ShangpinLeixing.DoesNotExist): + return Response({'code': 400, 'msg': '店铺商品类型无效'}) + elif yqlx == 2: + # 佣金抢单,清除会员ID,设置佣金 + product.huiyuan_id = None + update_fields.append('huiyuan_id') + if 'yongjin' in data: + product.yongjin = to_float(data['yongjin']) or 0 + else: + product.yongjin = 0 + update_fields.append('yongjin') + else: + # 未修改抢单类型,保留原有独立修改逻辑 + if 'huiyuan_id' in data: + product.huiyuan_id = data['huiyuan_id'] or None + update_fields.append('huiyuan_id') + content_changed = True + if 'yongjin' in data: + product.yongjin = to_float(data['yongjin']) or 0 + update_fields.append('yongjin') + content_changed = True + + # 打手额外分成金额 + if 'ewai_dashou_fencheng' in data: + fencheng = to_float(data['ewai_dashou_fencheng']) or 0 + + + if fencheng > (product.jiage or 0): + return Response({'code': 400, 'msg': '分成金额不能超过商品价格'}) + product.ewai_dashou_fencheng = fencheng + update_fields.append('ewai_dashou_fencheng') + content_changed = True + + # 排序 + if 'paixu' in data: + product.paixu = to_int(data['paixu']) + update_fields.append('paixu') + + # 上架状态(保持原逻辑,兼容字符串) + if 'shangjia_zhuangtai' in data: + val = data['shangjia_zhuangtai'] + if isinstance(val, str): + val = val.lower() in ('true', '1', 'yes') + product.shangjia_zhuangtai = bool(val) + update_fields.append('shangjia_zhuangtai') + + # ★ 商品图片真上传 + image_file = files.get('image') + if image_file: + if product.tupian_url: + delete_from_oss(product.tupian_url) + file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(image_file, file_path) + if not full_url: + return Response({'code': 500, 'msg': '商品图片上传失败'}) + product.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + update_fields.append('tupian_url') + content_changed = True + + # ★ 规则图片真上传 + rule_file = files.get('rule_image') + remove_rule = data.get('remove_rule_image', '0') + if rule_file: + if product.guize_tupian: + delete_from_oss(product.guize_tupian) + file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(rule_file, file_path) + if full_url: + product.guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + update_fields.append('guize_tupian') + content_changed = True + elif remove_rule == '1' and product.guize_tupian: + delete_from_oss(product.guize_tupian) + product.guize_tupian = None + update_fields.append('guize_tupian') + content_changed = True + + if content_changed: + audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() + if audit_config and audit_config.kaiqi_shenhe: + product.shenhe_zhuangtai = False + update_fields.append('shenhe_zhuangtai') + + if update_fields: + product.save(update_fields=update_fields + ['update_time']) + return Response({'code': 0, 'msg': '商品修改成功'}) + + # ---------- 添加商品(增强会员/佣金默认逻辑) ---------- + @transaction.atomic + def _add_product(self, dianpu, data, files): + biaoqian = data.get('biaoqian') + jiage = data.get('jiage') + kucun = data.get('kucun') + dianpu_leixing_id = data.get('dianpu_leixing_id') + zhuanqu_id = data.get('zhuanqu_id') + jieshao = data.get('jieshao') + + if not all([biaoqian, jiage is not None, kucun is not None, dianpu_leixing_id, zhuanqu_id, jieshao]): + return Response({'code': 400, 'msg': '必填项缺失'}) + + try: + shop_type = ShangpinLeixingDianpu.query.get(id=dianpu_leixing_id, dianpu_id=dianpu.id) + except ShangpinLeixingDianpu.DoesNotExist: + return Response({'code': 400, 'msg': '无效的店铺商品类型'}) + + try: + public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1) + except ShangpinLeixing.DoesNotExist: + return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) + + # 商品图片上传 + image_file = files.get('image') + if not image_file: + return Response({'code': 400, 'msg': '必须上传商品图片'}) + file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(image_file, file_path) + if not full_url: + return Response({'code': 500, 'msg': '商品图片上传失败'}) + tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + + # 规则图片 + guize_tupian = None + rule_file = files.get('rule_image') + if rule_file: + file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png" + full_url = upload_to_oss(rule_file, file_path) + if full_url: + guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) + + yaoqiuleixing = int(data.get('yaoqiuleixing', 1)) + if yaoqiuleixing == 1: + huiyuan_id = data.get('huiyuan_id') + # 如果前端没传有效的会员ID,尝试从公共类型获取 + if not huiyuan_id: + huiyuan_id = public_type.huiyuan_id + # 仍然没有会员ID,检查公共类型是否可转为佣金 + if not huiyuan_id: + if public_type.yaoqiuleixing == 2 and public_type.yongjin: + # 自动转为佣金抢单 + yaoqiuleixing = 2 + yongjin = public_type.yongjin + huiyuan_id = None + else: + return Response({'code': 400, 'msg': '会员抢单缺少会员ID,且无法自动获取'}) + else: + yongjin = None + else: + huiyuan_id = None + yongjin = data.get('yongjin') or public_type.yongjin or 0 + + ewai_fencheng = data.get('ewai_dashou_fencheng') or 0 + + + try: + ewai_val = float(ewai_fencheng) + jiage_val = float(jiage) + if ewai_val > jiage_val: + return Response({'code': 400, 'msg': '分成金额不能超过商品价格'}) + except ValueError: + # 处理转换失败的情况,比如记录日志或设为默认值 + print("无法转换为数字") + + + + + '''if ewai_fencheng > jiage: + return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})''' + + shenhe_status = True + audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() + if audit_config and audit_config.kaiqi_shenhe: + shenhe_status = False + + product = Shangpin.query.create( + biaoqian=biaoqian, + jiage=jiage, + kucun=kucun, + dianpu_id=dianpu.id, + dianpu_leixing_id=dianpu_leixing_id, + zhuanqu_id=zhuanqu_id, + leixing_id=public_type.id, + jieshao=jieshao, + xiadan_xuzhi=data.get('xiadan_xuzhi', ''), + guize_tupian=guize_tupian, + tupian_url=tupian_url, + yaoqiuleixing=yaoqiuleixing, + huiyuan_id=huiyuan_id, + yongjin=yongjin, + ewai_dashou_fencheng=ewai_fencheng, + kaioi_ewai_dashou_fencheng=True, + paixu=data.get('paixu', 0), + shangjia_zhuangtai=data.get('shangjia_zhuangtai', True), + fengjin_zhuangtai=False, + shenhe_zhuangtai=shenhe_status, + shenhezhuangtai=1, + ) + return Response({'code': 0, 'msg': '商品添加成功', 'product_id': product.id}) + + # ---------- 删除商品(完全不变) ---------- + @transaction.atomic + def _delete_product(self, dianpu, data): + product_id = data.get('product_id') + if not product_id: + return Response({'code': 400, 'msg': '缺少 product_id'}) + + try: + product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id) + except Shangpin.DoesNotExist: + return Response({'code': 404, 'msg': '商品不存在'}) + + if product.tupian_url: + delete_from_oss(product.tupian_url) + if product.guize_tupian: + delete_from_oss(product.guize_tupian) + + product.delete() + return Response({'code': 0, 'msg': '商品已删除'}) + + + +class MemberListView(APIView): + """ + 获取所有会员列表(供商品发布时选择) + URL: /shangdian/hqhy + 权限:JWT认证 + 店铺归属验证 + 说明:前端需传递 userId,接口会进行身份及店铺状态校验 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 1. 获取前端传递的用户ID(必须) ---------- + frontend_user_id = request.data.get('userId') + if not frontend_user_id: + return Response({'code': 400, 'msg': '缺少必要参数 userId'}) + + # ---------- 2. 调用公共验证方法(会校验用户身份、店铺状态) ---------- + dianpu, error_response = verify_shop_permission(request, frontend_user_id) + if error_response: + return error_response + + # ---------- 3. 查询所有会员(只返回前端需要的字段) ---------- + try: + members = Huiyuan.query.all().values('huiyuan_id', 'jieshao') + member_list = [ + { + 'huiyuan_id': m['huiyuan_id'], + 'jieshao': m['jieshao'] or '' + } + for m in members + ] + except Exception as e: + logger.exception("获取会员列表失败") + return Response({'code': 500, 'msg': '服务器错误'}) + + # ---------- 4. 返回成功响应 ---------- + return Response({ + 'code': 0, + 'msg': 'success', + 'data': member_list + }) + + + + +class SubShopListView(APIView): + """ + 获取下级店铺列表,同时返回上级分红汇总 + URL: /shangdian/wdxjdp + 方法: POST + 权限: JWT认证 + 上级店铺身份验证 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证 - 获取上级店铺对象 + user_id = request.data.get('userId') + if not user_id: + return Response({'code': 400, 'msg': '缺少 userId'}) + + parent_dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + # 此时 parent_dianpu 即为当前上级店铺 + + # 2. 读取筛选参数 + dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() + zhuangtai = request.data.get('zhuangtai') + bind_min = request.data.get('bind_min') + bind_max = request.data.get('bind_max') + page = request.data.get('page', 1) + page_size = request.data.get('page_size', 10) + + # 3. 查询下级店铺:yaoqing_dianpu_id 等于当前上级店铺ID + queryset = Dianpu.query.filter(yaoqing_dianpu_id=parent_dianpu.id) + + # 4. 筛选条件 + if dianpu_mingcheng: + queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng) + if zhuangtai is not None: + try: + zhuangtai = int(zhuangtai) + queryset = queryset.filter(zhuangtai=zhuangtai) + except (ValueError, TypeError): + pass + if bind_min is not None: + try: + bind_min = int(bind_min) + queryset = queryset.filter(bangding_yonghushu__gte=bind_min) + except (ValueError, TypeError): + pass + if bind_max is not None: + try: + bind_max = int(bind_max) + queryset = queryset.filter(bangding_yonghushu__lte=bind_max) + except (ValueError, TypeError): + pass + + # 5. 排序:按创建时间倒序 + queryset = queryset.order_by('-create_time') + + # 6. 分页 + try: + paginator = Paginator(queryset, page_size) + page_obj = paginator.page(page) + except EmptyPage: + page_obj = paginator.page(1) + + # 7. 构建列表数据 + shop_list = [] + for shop in page_obj: + shop_list.append({ + 'id': shop.id, + 'dianpu_mingcheng': shop.dianpu_mingcheng, + 'dianpu_touxiang': shop.dianpu_touxiang or '', + 'lianxi_dianhua': shop.lianxi_dianhua or '', + 'zhuangtai': shop.zhuangtai, + 'bangding_yonghushu': shop.bangding_yonghushu, + 'zhengqu_zonge': str(shop.zhengqu_zonge), + 'ketixian_yue': str(shop.ketixian_yue), + 'create_time': shop.create_time.strftime('%Y-%m-%d %H:%M:%S') if shop.create_time else '', + 'update_time': shop.update_time.strftime('%Y-%m-%d %H:%M:%S') if shop.update_time else '', + 'erweima_url': shop.erweima_url or '', + 'fenhong_choucheng_feilv': str(shop.fenhong_choucheng_feilv) if shop.fenhong_choucheng_feilv else '0.0000', + }) + + # 8. 上级分红汇总(从当前店铺主表读取) + shangji_leiji = str(parent_dianpu.shangji_fenhong_leiji) if parent_dianpu.shangji_fenhong_leiji else '0.00' + shangji_ketixian = str(parent_dianpu.shangji_ketixian_fenhong) if parent_dianpu.shangji_ketixian_fenhong else '0.00' + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': shop_list, + 'total': paginator.count, + 'page': page_obj.number, + 'page_size': page_size, + 'shangji_fenhong_leiji': shangji_leiji, + 'shangji_ketixian_fenhong': shangji_ketixian, + } + }) + + +class SubShopModifyView(APIView): + """ + 添加/修改下级店铺 + URL: /shangdian/xjdpbj + 方法: POST + 权限: JWT认证 + 上级店铺身份验证 + action字段: + - 'add' : 添加新的子店铺 + - 'update': 修改已有子店铺的状态/分红费率 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证 + user_id = request.data.get('userId') + if not user_id: + return Response({'code': 400, 'msg': '缺少 userId'}) + + parent_dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + + action = request.data.get('action', '').strip().lower() + if action == 'add': + return self._add_subshop(request, parent_dianpu) + elif action == 'update': + return self._update_subshop(request, parent_dianpu) + else: + return Response({'code': 400, 'msg': '未知操作类型,action 应为 add 或 update'}) + + def _add_subshop(self, request, parent_dianpu): + """添加子店铺,需提供用户UID、账号、密码、店铺名称等""" + user_uid = request.data.get('user_uid', '').strip() + zhanghao = request.data.get('zhanghao', '').strip() + mima = request.data.get('mima', '') + dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() + lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip() or None + fenhong_feilv = request.data.get('fenhong_choucheng_feilv', None) + + # 必填校验 + if not all([user_uid, zhanghao, mima, dianpu_mingcheng]): + return Response({'code': 400, 'msg': '必填字段缺失(user_uid, zhanghao, mima, dianpu_mingcheng)'}) + + # 检查用户是否存在 + if not User.query.filter(UserUID=user_uid).exists(): + return Response({'code': 404, 'msg': '对应的小程序用户不存在'}) + + # 检查账号是否已占用 + if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists(): + return Response({'code': 400, 'msg': '登录账号已存在'}) + if YonghuPingzheng.query.filter(yonghu=user_uid).exists(): + return Response({'code': 400, 'msg': '该用户 ID 已有店铺凭证'}) + + # 费率处理:前端可选,后端默认 0.0500 + if fenhong_feilv is not None: + try: + fenhong_feilv = float(fenhong_feilv) + if fenhong_feilv < 0 or fenhong_feilv > 1: + return Response({'code': 400, 'msg': '分红费率需在 0~1 之间'}) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': '分红费率格式错误'}) + else: + fenhong_feilv = 0.0500 + + try: + with transaction.atomic(): + # 创建凭证 + pingzheng = YonghuPingzheng.query.create( + yonghu=user_uid, + zhanghao=zhanghao, + mima=mima, + is_active=True + ) + # 创建店铺,关联上级 + dianpu = Dianpu.query.create( + pingzheng=pingzheng, + dianpu_mingcheng=dianpu_mingcheng, + lianxi_dianhua=lianxi_dianhua or '', + yaoqing_dianpu_id=parent_dianpu.id, # 关键:绑定上级 + fenhong_choucheng_feilv=fenhong_feilv, + # 其他字段使用默认值 + ) + return Response({ + 'code': 200, + 'msg': '子店铺添加成功', + 'dianpu_id': dianpu.id + }) + except IntegrityError as e: + logger.exception("创建子店铺违反唯一约束") + return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'}) + except Exception as e: + logger.exception("创建子店铺失败") + return Response({'code': 500, 'msg': '创建失败,请稍后重试'}) + + def _update_subshop(self, request, parent_dianpu): + """修改子店铺状态和上级分红费率""" + dianpu_id = request.data.get('dianpu_id') + if not dianpu_id: + return Response({'code': 400, 'msg': '缺少 dianpu_id'}) + + try: + dianpu_id = int(dianpu_id) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': 'dianpu_id 需为数字'}) + + try: + with transaction.atomic(): + sub_dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) + # 权限校验:确保该店铺的上级就是当前店铺 + if sub_dianpu.yaoqing_dianpu_id != parent_dianpu.id: + return Response({'code': 403, 'msg': '无权修改该店铺,非您的下级'}) + + # 更新状态(如果有传) + if 'zhuangtai' in request.data: + zhuangtai = request.data['zhuangtai'] + if zhuangtai not in (0, 1, '0', '1'): + return Response({'code': 400, 'msg': '店铺状态只能为 0(封禁) 或 1(正常)'}) + sub_dianpu.zhuangtai = int(zhuangtai) + + # 更新上级分红费率(如果有传) + if 'fenhong_choucheng_feilv' in request.data: + feilv = request.data['fenhong_choucheng_feilv'] + try: + feilv = float(feilv) + if feilv < 0 or feilv > 1: + return Response({'code': 400, 'msg': '分红费率需在 0~1 之间'}) + sub_dianpu.fenhong_choucheng_feilv = feilv + except (ValueError, TypeError): + return Response({'code': 400, 'msg': '分红费率格式错误'}) + + sub_dianpu.save(update_fields=['zhuangtai', 'fenhong_choucheng_feilv', 'update_time']) + return Response({'code': 200, 'msg': '修改成功'}) + + except Dianpu.DoesNotExist: + return Response({'code': 404, 'msg': '店铺不存在'}) + except Exception as e: + logger.exception("修改下级店铺失败") + return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) + + + + + +# shangdian/views/order_manage_views.py +""" +店铺订单管理接口 +包含: +1. 统计数据:商品类型及订单总数、各状态订单数 +2. 订单列表:支持筛选、分页、返回订单详情及打手图片(含跨平台图片拉取) +""" + + +# ---------- 图片域名 ---------- +""" +店铺订单管理接口 +================= +功能1:获取统计数据(action='statistics') + - 每个商品类型的订单总数(order_count) + - 每个商品类型下各状态的订单数量(status_counts) + - 全局各状态的订单总数(status_counts) + - 店铺订单总量(total_orders) + +功能2:获取订单列表(默认,不带action参数时执行) + - 支持商品类型、订单状态、订单ID、打手ID、游戏昵称、老板ID、订单介绍筛选 + - 支持分页、自定义每页大小 + - 返回打手提交图片(绝对URL)、老板信息、店铺收益等 + +接口地址:POST /shangdian/ddlbhq +权限:JWT认证 + verify_shop_permission 验证店铺身份 +""" + +class ShopOrderStatisticsView(APIView): + """ + 店铺订单统计接口 + URL: POST /shangdian/ddltj + 权限: JWT认证 + 店铺身份验证 + + 请求参数: + userId (必填): 店铺所属用户的ID + + 返回结构: + { + "code": 200, + "msg": "统计成功", + "data": { + "types": [ # 所有有效商品类型 + { + "id": 类型ID, + "jieshao": "类型介绍", + "tupian_url": "类型图片相对路径", + "order_count": 该类型订单总数, + "status_counts": { # 该类型下各状态订单数量 + "1": 数量, + "7": 数量, + "2": 数量, + "8": 数量, + "3": 数量, + "4": 数量, + "5": 数量, + "6": 数量 + } + }, + ... + ], + "status_counts": { # 全局各状态订单总数 + "1": 总数, + "7": 总数, + "2": 总数, + "8": 总数, + "3": 总数, + "4": 总数, + "5": 总数, + "6": 总数 + }, + "total_orders": 订单总量 + } + } + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 身份验证 + user_id = request.data.get('userId', '').strip() + if not user_id: + return Response({'code': 400, 'msg': '缺少 userId'}) + + dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + + shop_id = dianpu.id + + try: + # 2. 获取所有正常状态(审核通过)的商品类型 + types_qs = ShangpinLeixing.query.filter( + shenhezhuangtai=1 + ).order_by('-paixu', 'id') + + type_list = [] + type_id_map = {} + for idx, t in enumerate(types_qs): + item = { + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'order_count': 0, + 'status_counts': { + '1': 0, '7': 0, '2': 0, '8': 0, + '3': 0, '4': 0, '5': 0, '6': 0 + } + } + type_list.append(item) + type_id_map[t.id] = idx + + # 3. 获取本店铺所有订单的主键ID列表(通过平台扩展表) + order_pks = list( + DingdanPingtai.query.filter(dianpu_id=shop_id) + .values_list('dingdan_id', flat=True) # 这里取的是订单主表的主键id + ) + + total_orders = 0 + global_counts = defaultdict(int) + + if order_pks: + # 4. 用主键列表进行聚合统计,按商品类型和订单状态分组 + aggregation = ( + Dingdan.objects + .filter(id__in=order_pks) # 修正点:使用id__in,而不是dingdan_id__in + .values('leixing_id', 'zhuangtai') + .annotate(cnt=Count('id')) + ) + + for row in aggregation: + lid = row['leixing_id'] # 商品类型ID + st = row['zhuangtai'] # 订单状态码 + cnt = row['cnt'] + + # 转为字符串键,方便前端展示 + st_key = str(st) + + # 累加总量 + total_orders += cnt + global_counts[st_key] += cnt + + # 填充到对应商品类型 + if lid in type_id_map: + idx = type_id_map[lid] + type_list[idx]['order_count'] += cnt + if st_key in type_list[idx]['status_counts']: + type_list[idx]['status_counts'][st_key] += cnt + + # 5. 确保全局状态返回所有8个键,即使值为0 + all_status_keys = ['1', '7', '2', '8', '3', '4', '5', '6'] + final_global_counts = { + k: global_counts.get(k, 0) for k in all_status_keys + } + + # 6. 返回统计结果 + return Response({ + 'code': 200, + 'msg': '统计成功', + 'data': { + 'types': type_list, + 'status_counts': final_global_counts, + 'total_orders': total_orders + } + }) + + except Exception as e: + logger.exception("订单统计接口异常") + return Response({ + 'code': 500, + 'msg': '统计失败,服务器内部错误' + }) + +# ================================================================ +# 辅助函数:获取OSS域名(用于拼接本地图片绝对URL) +# ================================================================ +def get_oss_domain(): + """ + 获取对象存储的域名,末尾包含 '/'。 + 返回:字符串,如 'https://example.com/' + """ + return getattr(settings, 'COS_DOMAIN', 'https://xingque999qygwuyq-1404472910.cos.ap-shanghai.myqcloud.com/').rstrip('/') + '/' + + +# ================================================================ +# 辅助函数:获取单个订单的打手提交图片(绝对URL) +# ================================================================ +def _fetch_images(order, oss_domain): + """ + 查询指定订单的打手提交图片记录(Dashoutupian 表), + 将相对路径拼接为完整的绝对 URL 并返回。 + + 参数: + order: Dingdan 订单模型实例 + oss_domain: OSS 域名,末尾含 '/' + + 返回: + (images_list, is_punished, punishment_info) 三元组 + - images_list: 图片绝对 URL 列表 + - is_punished: 暂未使用,默认 False + - punishment_info: 暂未使用,默认空字典 + """ + images = [] + is_punished = False + punishment_info = {} + + # 仅当订单有接单打手ID时才查询打手图片 + if order.jiedan_dashou_id: + # 从打手图片表中查询该订单、该打手的所有图片记录 + image_urls = Dashoutupian.query.filter( + dingdan_id=order.dingdan_id, # 订单ID匹配 + dashou=order.jiedan_dashou_id # 接单打手ID匹配 + ).values_list('tupian', flat=True) # 只取图片URL字段 + + # 遍历所有图片路径 + for img_path in image_urls: + if img_path: + if img_path.startswith('http'): + # 已经是完整URL,直接添加 + images.append(img_path) + else: + # 相对路径,拼接 OSS 域名 + images.append(oss_domain + img_path.lstrip('/')) + + # 跨平台图片拉取逻辑(根据实际业务需求补充,此处保留结构) + # 如果有跨平台订单,需要请求对方平台接口获取打手图片 + return images, is_punished, punishment_info + + +# ================================================================ +# 辅助函数:并发获取多个订单的打手图片 +# ================================================================ +def _batch_images(orders, oss_domain): + """ + 并发调用 _fetch_images,提升批量获取图片的速度。 + + 参数: + orders: Dingdan 订单对象列表 + oss_domain: OSS 域名 + + 返回: + 字典,键为订单ID (dingdan_id),值为 (images_list, is_punished, punishment_info) + """ + result = {} # 存储结果 + # 使用线程池并发执行,max_workers 可根据服务器性能调整 + with ThreadPoolExecutor(max_workers=10) as executor: + # 建立 future 到订单ID的映射 + future_to_oid = { + executor.submit(_fetch_images, order, oss_domain): order.dingdan_id + for order in orders + } + # 等待所有任务完成 + for future in as_completed(future_to_oid): + oid = future_to_oid[future] # 对应的订单ID + try: + # 获取任务返回值 + imgs, pun, pinfo = future.result() + result[oid] = (imgs, pun, pinfo) + except Exception as e: + # 某个订单获取失败,记录日志并赋予空值,保证整体流程不中断 + logger.error(f"获取订单 {oid} 图片失败: {e}") + result[oid] = ([], False, {}) + return result + + +# ================================================================ +# 核心视图类 +# ================================================================ +class ShopOrderManageView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + """ + 统一入口,根据 action 参数分发到统计或列表模块。 + """ + # 1. 从请求中提取店铺用户ID + user_id = request.data.get('userId', '').strip() + if not user_id: + return Response({'code': 400, 'msg': '缺少 userId'}) + + # 2. 验证店铺身份,获取店铺对象 + dianpu, error_response = verify_shop_permission(request, user_id) + if error_response: + return error_response + + # 3. 根据 action 决定调用哪个方法 + action = request.data.get('action', '').strip().lower() + if action == 'statistics': + return self._statistics(dianpu) # 统计 + else: + return self._order_list(request, dianpu) # 列表 + + # ----------------------------------------------------------------- + # 统计实现 (这就是你要求的100个统计字段的详细实现) + # ----------------------------------------------------------------- + def _statistics(self, dianpu): + """ + 统计本店铺的订单数据,输出以下信息: + - types: 商品类型数组,每个元素包含: + id : 类型ID + jieshao : 类型介绍 + tupian_url : 类型图片 + order_count : 【该类型的订单总数】 + status_counts: 该类型下各个状态的订单数量 + - status_counts: 全局各状态订单总数(键为状态码字符串) + - total_orders : 店铺订单总量 + + 所有字段名均与前端保持一致。 + """ + # 获取店铺ID + shop_id = dianpu.id + + # ------------------------------------------------------------ + # 步骤1:获取所有正常的商品类型(审核状态为1) + # ------------------------------------------------------------ + types_query = ShangpinLeixing.query.filter( + shenhezhuangtai=1 # 审核状态为1(正常) + ).order_by('-paixu', 'id') # 按排序权重和ID排序 + + # 构建返回的类型列表,并预初始化各状态计数 + type_list = [] # 最终要返回的类型数组 + type_ids = [] # 所有类型的ID列表,用于后续快速索引 + + # 定义可能出现的所有状态码(1/7已付款,2进行中,8待结算,3已完成,4退款审核,5已退款,6退款失败) + status_keys = ['1', '7', '2', '8', '3', '4', '5', '6'] + + for t in types_query: + # 构造每个类型的基础数据 + type_item = { + 'id': t.id, + 'jieshao': t.jieshao or '', + 'tupian_url': t.tupian_url or '', + 'order_count': 0, # 该类型订单总数,稍后填充 + 'status_counts': {k: 0 for k in status_keys} # 各状态计数初始为0 + } + type_list.append(type_item) + type_ids.append(t.id) # 记录ID + + # ------------------------------------------------------------ + # 步骤2:获取本店铺的所有订单ID(通过平台扩展表) + # ------------------------------------------------------------ + # 从 DingdanPingtai 中过滤出属于当前店铺的订单扩展记录,并提取订单主表ID + order_ids = list( + DingdanPingtai.query.filter( + dianpu_id=shop_id # 店铺ID匹配 + ).values_list('dingdan_id', flat=True) # 只取订单ID字段 + ) + + # 统计变量 + total_orders = 0 # 订单总量 + global_status_counts = defaultdict(int) # 全局各状态计数,键为状态码字符串 + + # ------------------------------------------------------------ + # 步骤3:如果店铺有订单,进行聚合统计 + # ------------------------------------------------------------ + if order_ids: + # 使用 Django 聚合查询:按 leixing_id(商品类型)和 zhuangtai(订单状态)分组计数 + aggregation = Dingdan.query.filter( + dingdan_id__in=order_ids # 从该店铺的订单中查询 + ).values( + 'leixing_id', # 按类型ID分组 + 'zhuangtai' # 按状态分组 + ).annotate( + cnt=Count('id') # 统计每个分组的记录数 + ) + + # 遍历每个聚合结果,累加到对应的类型和全局计数中 + for row in aggregation: + # 提取分组字段 + lid = row['leixing_id'] # 商品类型ID + st = row['zhuangtai'] # 订单状态码 + cnt = row['cnt'] # 该分组下的订单数量 + + # 累加到全局总数 + total_orders += cnt + global_status_counts[str(st)] += cnt + + # 累加到对应商品类型的状态计数中 + if lid in type_ids: + # 在 type_list 中找到对应的类型条目 + for tp in type_list: + if tp['id'] == lid: + # 更新该类型的订单总数 + tp['order_count'] += cnt + # 更新该类型下对应状态的计数(键为字符串) + tp['status_counts'][str(st)] += cnt + break # 找到后跳出循环,因为ID唯一 + + # 将全局状态计数转换为按固定顺序的字典,确保前端展示顺序稳定 + final_global_counts = {k: global_status_counts.get(k, 0) for k in status_keys} + + # ------------------------------------------------------------ + # 步骤4:返回统计结果 + # ------------------------------------------------------------ + return Response({ + 'code': 200, + 'msg': '统计成功', + 'data': { + 'types': type_list, # 商品类型列表(含各类型统计) + 'status_counts': final_global_counts, # 全局各状态总数 + 'total_orders': total_orders # 订单总量 + } + }) + + # ----------------------------------------------------------------- + # 订单列表实现 + # ----------------------------------------------------------------- + def _order_list(self, request, dianpu): + """处理订单列表请求,支持筛选、分页、并发获取图片。""" + shop_id = dianpu.id + + # 解析筛选参数 + leixing_id = request.data.get('leixing_id') + zhuangtai = request.data.get('zhuangtai') + dingdan_id_key = request.data.get('dingdan_id', '').strip() + jiedan_dashou_id_key = request.data.get('jiedan_dashou_id', '').strip() + nicheng_key = request.data.get('nicheng', '').strip() + laoban_id_key = request.data.get('laoban_id', '').strip() + jieshao_key = request.data.get('jieshao', '').strip() + + page = request.data.get('page', 1) + page_size = request.data.get('page_size', 20) + + # 基础查询:从平台扩展表关联订单主表,并按创建时间倒序 + qs = DingdanPingtai.query.filter( + dianpu_id=shop_id + ).select_related('dingdan').order_by('-dingdan__create_time') + + # 应用筛选条件 + if leixing_id is not None: + qs = qs.filter(dingdan__leixing_id=leixing_id) + if zhuangtai is not None: + qs = qs.filter(dingdan__zhuangtai=zhuangtai) + if dingdan_id_key: + qs = qs.filter(dingdan__dingdan_id__icontains=dingdan_id_key) + if jiedan_dashou_id_key: + qs = qs.filter(dingdan__jiedan_dashou_id=jiedan_dashou_id_key) + if nicheng_key: + qs = qs.filter(dingdan__nicheng__icontains=nicheng_key) + if laoban_id_key: + qs = qs.filter(laoban_id=laoban_id_key) + if jieshao_key: + qs = qs.filter(dingdan__jieshao__icontains=jieshao_key) + + # 分页 + paginator = Paginator(qs, page_size) + try: + page_obj = paginator.page(page) + except EmptyPage: + page_obj = paginator.page(1) + + # 获取当前页的所有订单和扩展对象 + orders_with_ext = [(item.dingdan, item) for item in page_obj] + # 提取订单对象列表,用于并发获取图片 + order_objs = [order for order, _ in orders_with_ext] + oss_domain = get_oss_domain() + img_map = _batch_images(order_objs, oss_domain) + + # 构建返回数据 + list_data = [] + for order, ext in orders_with_ext: + imgs, is_pun, _ = img_map.get(order.dingdan_id, ([], False, {})) + list_data.append({ + 'dingdan_id': order.dingdan_id, + 'zhuangtai': order.zhuangtai, + 'jine': float(order.jine) if order.jine else 0.00, + 'dashou_fencheng': float(order.dashou_fencheng) if order.dashou_fencheng else 0.00, + 'jiedan_dashou_id': order.jiedan_dashou_id or '', + 'dashou_liuyan': order.dashou_liuyan or '', + 'zhiding_id': order.zhiding_id or '', + 'shangpin_id': order.shangpin_id or '', + 'tupian': order.tupian or '', + 'jieshao': order.jieshao or '', + 'beizhu': order.beizhu or '', + 'nicheng': order.nicheng or '', + 'leixing_id': order.leixing_id, + 'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '', + 'update_time': order.update_time.strftime('%Y-%m-%d %H:%M:%S') if order.update_time else '', + 'pingtai_kuozhan': { + 'laoban_id': ext.laoban_id or '', + 'laoban_pingjia': ext.laoban_pingjia or '', + 'dianpu_shouyi': float(ext.dianpu_shouyi) if ext.dianpu_shouyi else 0.00, + }, + 'dashou_images': imgs, + 'is_punished': is_pun, + 'is_fadaned': False, + }) + + return Response({ + 'code': 200, + 'msg': '列表成功', + 'data': { + 'list': list_data, + 'total': paginator.count, + 'page': page_obj.number, + 'page_size': page_size + } + }) + + + + + + +# ================================================================ +# shangdian/views/refund_view.py +# 店铺退款接口 —— 基于跨平台退款接口二改,只处理平台订单,我方派单 +# ================================================================ + +logger = logging.getLogger(__name__) + + +class ShopRefundView(APIView): + """ + 店铺订单退款接口 + URL: POST /shangdian/tk + 权限: JWT认证 + 店铺身份验证(只处理平台订单,我方派单) + + 请求参数: + userId (必填) : 店铺所属用户的ID + dingdan_id (必填): 订单ID + reason (可选) : 退款理由 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # 1. 获取参数 + user_id = request.data.get('userId', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + reason = request.data.get('reason', '').strip() # 退款理由 + + if not user_id or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # 2. 店铺身份验证 + dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + + shop_id = dianpu.id + + # 3. 查询订单,必须属于本店铺(通过扩展表关联) + try: + # 通过扩展表确认归属,并 select_related 获取订单主表 + pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( + dianpu_id=shop_id, + dingdan__dingdan_id=dingdan_id + ) + order = pingtai_ext.dingdan + except DingdanPingtai.DoesNotExist: + logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") + return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) + + # 4. 订单状态校验(只有这些状态可退款) + allowed_statuses = [1, 7, 2, 8, 4] + if order.zhuangtai not in allowed_statuses: + logger.warning(f"订单状态不允许退款: {order.zhuangtai}") + return Response({'code': 400, 'msg': '订单状态不允许退款'}) + + # 5. 全部视为平台订单(我方派单),调用微信退款 + refund_result = self._call_wechat_refund(order) + if refund_result['code'] != 0: + return Response({'code': 400, 'msg': refund_result['msg']}) + + # 6. 事务内更新订单、打手、记录、统计 + dashou_id = order.jiedan_dashou_id + try: + with transaction.atomic(): + # 更新订单状态为已退款 + order.zhuangtai = 5 + order.clkf = str(shop_id) # 处理人记录为店铺ID + if reason: + order.tkly = reason + order.save() + + # 更新打手统计(退款量+1,状态置为空闲) + if dashou_id: + try: + dashou_user = User.query.get(UserUID=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.tuikuanliang += 1 + dashou_profile.zhuangtai = 1 + dashou_profile.save() + except (User.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"退款:打手 {dashou_id} 不存在,跳过状态更新") + + # 更新退款记录表 + self._update_refund_record(order, dashou_id, str(shop_id), reason) + + # 打手每日统计(退款动作) + try: + update_dashou_daily_by_action( + yonghuid=dashou_id, + amount=order.dashou_fencheng or Decimal('0.00'), + action=3 # 3 表示退款 + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + # 更新平台支出记录 + try: + update_daily_payout(order.jine) + except Exception as e: + logger.error(f"更新每日支出失败: {e}") + + # ========== 新增:商品和店铺每日统计 ========== + try: + # 商品每日统计(只要有商品ID就执行) + if order.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(order, 'pingtai_kuozhan'): + pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=order.shangpin_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + + # 店铺每日统计(仅当存在店铺ID时执行) + dianpu_id = None + if hasattr(order, 'pingtai_kuozhan'): + dianpu_id = order.pingtai_kuozhan.dianpu_id + if dianpu_id: + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + update_dianpu_daily_stat( + dianpu_id=dianpu_id, + caozuo_leixing=3, # 3 = 退款 + dingdan_jiage=order.jine, + dianpu_shouyi=dianpu_shouyi + ) + logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") + except Exception as e: + logger.error(f"商品/店铺统计更新失败: {e}") + logger.error(traceback.format_exc()) + # 统计失败不影响主流程 + # ============================================= + + except Exception as e: + logger.exception("退款事务执行失败") + return Response({'code': 500, 'msg': '退款处理失败,请稍后重试'}) + + return Response({'code': 200, 'msg': '退款成功'}) + + # ========== 微信退款 ========== + def _call_wechat_refund(self, order): + """调用微信支付退款接口""" + appid = getattr(settings, 'WEIXIN_APPID', '') + mch_id = getattr(settings, 'WEIXIN_MCHID', '') + key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') + cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') + key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') + + if not all([appid, mch_id, key, cert_path, key_path]): + return {'code': 500, 'msg': '微信支付配置不完整'} + + out_refund_no = self._generate_refund_no(order.dingdan_id) + total_fee = int(float(order.jine or 0) * 100) + refund_fee = total_fee + + params = { + 'appid': appid, + 'mch_id': mch_id, + 'nonce_str': self._generate_nonce_str(), + 'out_refund_no': out_refund_no, + 'total_fee': total_fee, + 'refund_fee': refund_fee, + 'refund_desc': '店铺退款', + } + if order.wechat_transaction_id: + params['transaction_id'] = order.wechat_transaction_id + else: + params['out_trade_no'] = order.dingdan_id + + # 签名 + sorted_keys = sorted(params.keys()) + stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) + stringSignTemp = f"{stringA}&key={key}" + sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() + params['sign'] = sign + + xml_data = self._dict_to_xml(params) + url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' + + try: + response = requests.post(url, data=xml_data, cert=(cert_path, key_path), timeout=10) + result = xmltodict.parse(response.content)['xml'] + logger.info(f"微信退款响应: {result}") + except Exception as e: + logger.error(f"微信退款请求异常: {e}", exc_info=True) + return {'code': 500, 'msg': '微信退款请求异常'} + + if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': + return {'code': 0, 'msg': '退款成功'} + else: + err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) + return {'code': 400, 'msg': err_msg} + + # ========== 退款记录 ========== + def _update_refund_record(self, order, dashou_id, chuli_id, reason): + """更新退款记录表""" + try: + refund_record, created = Tuikuanjilu.query.get_or_create( + dingdan_id=order.dingdan_id, + defaults={ + 'dashouid': dashou_id or '', + 'qingqiuid': '', + 'chuliid': chuli_id, + 'liyou': reason or '店铺退款', + 'sqzhuangtai': 1, + 'jine': order.jine or 0, + 'dashou_fencheng': order.dashou_fencheng or 0, + 'jieshao': order.jieshao or '', + 'beizhu': '店铺退款', + 'nicheng': order.nicheng or '', + } + ) + if not created: + refund_record.sqzhuangtai = 1 + refund_record.chuliid = chuli_id + if reason: + refund_record.liyou = reason + refund_record.save() + except Exception as e: + logger.error(f"更新退款记录异常: {e}") + + # ========== 工具方法 ========== + def _generate_nonce_str(self): + return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) + + def _generate_refund_no(self, dingdan_id): + timestamp = str(int(time.time())) + rand = ''.join(random.choices('0123456789', k=4)) + return f"{dingdan_id}REF{timestamp}{rand}" + + def _dict_to_xml(self, data): + xml = [''] + for k, v in data.items(): + xml.append(f'<{k}>{v}') + xml.append('') + return ''.join(xml) + + + + + + +class ShopForceCompleteView(APIView): + """ + 店铺强制结单接口 + URL: POST /shangdian/qzjd + 权限: JWT认证 + 店铺身份验证 + + 请求参数: + userId (必填) : 店铺所属用户的ID + dingdan_id(必填) : 订单ID + + 说明: + - 只处理平台订单(fadan_pingtai == 1) + - 订单必须属于当前店铺 + - 订单状态必须为 8(结算中)才能强制结单 + - 默认视为我方派单,根据是否有对方订单ID判断是否跨平台 + - 本地订单:给打手结算、更新打手余额和统计、更新店铺收益统计 + - 跨平台订单:仅通知对方平台,不处理打手 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ========== 1. 获取参数 ========== + user_id = request.data.get('userId', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not user_id or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # ========== 2. 店铺身份验证 ========== + dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + + shop_id = dianpu.id + + # ========== 3. 查询订单并验证归属 ========== + try: + # 通过平台扩展表确认订单属于本店铺 + pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( + dianpu_id=shop_id, + dingdan__dingdan_id=dingdan_id + ) + order = pingtai_ext.dingdan + except DingdanPingtai.DoesNotExist: + logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") + return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) + + # ========== 4. 校验订单状态必须为 8(结算中) ========== + if order.zhuangtai != 8: + logger.warning(f"订单 {dingdan_id} 当前状态为 {order.zhuangtai},不是结算中") + return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.zhuangtai}'}) + + dashou_id = order.jiedan_dashou_id + dashou_fencheng = order.dashou_fencheng or Decimal('0.00') + jine = order.jine or Decimal('0.00') + + try: + with transaction.atomic(): + # 锁定订单行,防止并发 + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + if not dashou_id: + return Response({'code': 400, 'msg': '订单无接单打手,无法结算'}) + if dashou_fencheng < 0: + return Response({'code': 400, 'msg': '打手分成金额无效'}) + + # ---------- 更新打手余额和统计 ---------- + try: + dashou_user = User.query.get(UserUID=dashou_id) + dashou_profile = dashou_user.dashou_profile + dashou_profile.chengjiaozongliang += 1 + dashou_profile.yue += dashou_fencheng + dashou_profile.zonge += dashou_fencheng + dashou_profile.jinrishouyi += dashou_fencheng + dashou_profile.jinyueshouyi += dashou_fencheng + dashou_profile.zhuangtai = 1 # 空闲 + dashou_profile.save() + except (User.DoesNotExist, ObjectDoesNotExist): + logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过打手更新") + + # ---------- 更新订单状态 ---------- + order.zhuangtai = 3 + order.clkf = str(shop_id) + order.save() + + # ---------- 打手每日统计(成交) ---------- + try: + update_dashou_daily_by_action( + yonghuid=dashou_id, + amount=dashou_fencheng, + action=2 # 2 表示成交(结算) + ) + except Exception as e: + logger.error(f"打手每日统计更新失败: {e}") + + # ---------- 店铺收益统计(结算) ---------- + if pingtai_ext.dianpu_shouyi: + try: + # 商品每日统计(只要有商品ID就执行) + if order.shangpin_id: + # 从扩展表获取已计算好的收益值(下单时已存储) + pingtai_shouyi = Decimal('0.00') + dianpu_shouyi = Decimal('0.00') + if hasattr(order, 'pingtai_kuozhan'): + pingtai_shouyi = order.pingtai_kuozhan.pingtai_shouyi or Decimal('0.00') + dianpu_shouyi = order.pingtai_kuozhan.dianpu_shouyi or Decimal('0.00') + + update_shangpin_daily_stat( + shangpin_id=order.shangpin_id, + caozuo_leixing=2, # 3 = 退款 + dingdan_jiage=order.jine, + pingtai_shouyi=pingtai_shouyi, + dianpu_zongshouyi=dianpu_shouyi + ) + logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") + + + update_dianpu_daily_stat( + dianpu_id=shop_id, + caozuo_leixing=2, # 结算操作 + dingdan_jiage=jine, + dianpu_shouyi=pingtai_ext.dianpu_shouyi, + ) + except Exception as e: + logger.error(f"店铺每日统计更新失败: {e}") + + # ========== 5. 记录操作日志 ========== + logger.info(f"强制结单成功,订单 {dingdan_id},店铺 {shop_id}") + + return Response({'code': 200, 'msg': '强制结单成功'}) + + except Dingdan.DoesNotExist: + logger.warning(f"订单不存在: {dingdan_id}") + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"强制结单接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + +# ================================================================ +# shangdian/views/transfer_hall_view.py +# 店铺转移大厅接口 —— 严格二改,不添加任何原接口没有的逻辑 +# ================================================================ +# ================================================================ +# shangdian/views/transfer_hall_view.py +# 店铺转移大厅接口 —— 添加本地打手退款统计 +# ================================================================ + +class ShopTransferHallView(APIView): + """ + 店铺转移大厅接口 + URL: POST /shangdian/zydt + 权限: JWT认证 + 店铺身份验证 + + 请求参数: + userId (必填) : 店铺所属用户的ID + dingdan_id(必填) : 订单ID + + 说明: + - 只处理平台订单,我方派单 + - 允许转移的状态:2,8,4 + - 决定是否处理本地打手:有对方订单ID或俱乐部ID(跨平台)则不处理本地打手,只通知对方; + 没有对方信息则处理本地打手(释放+退款统计) + - 转移后清空对方平台信息,订单重回可接单状态 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 1. 获取参数 ---------- + user_id = request.data.get('userId', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + + if not user_id or not dingdan_id: + return Response({'code': 400, 'msg': '参数不完整'}) + + # ---------- 2. 店铺身份验证 ---------- + dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + + shop_id = dianpu.id + + # ---------- 3. 验证订单归属 ---------- + try: + pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( + dianpu_id=shop_id, + dingdan__dingdan_id=dingdan_id + ) + order = pingtai_ext.dingdan + except DingdanPingtai.DoesNotExist: + logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") + return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) + + # ---------- 4. 状态校验 ---------- + if order.zhuangtai not in [2, 8, 4]: + return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.zhuangtai}'}) + + # ---------- 5. 提取关键字段 ---------- + current_dashou_id = order.jiedan_dashou_id + zhiding_id = order.zhiding_id + dashou_fencheng = order.dashou_fencheng or 0 # 用于退款统计 + + try: + with transaction.atomic(): + # 锁定订单行 + order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) + + # ---------- 6. 记录打手历史(原接口保留逻辑) ---------- + if current_dashou_id: + last_history = OrderDashouHistory.query.filter( + dingdan_id=dingdan_id + ).order_by('-times').first() + next_times = (last_history.times + 1) if last_history else 1 + OrderDashouHistory.query.create( + dingdan_id=dingdan_id, + dashou_id=current_dashou_id, + times=next_times, + operator=str(shop_id) + ) + + # ---------- 7. 释放本地打手 + 退款统计 ---------- + if current_dashou_id: + try: + dashou_profile = UserDashou.query.get(user__UserUID=current_dashou_id) + dashou_profile.zhuangtai = 1 # 设为空闲 + dashou_profile.save() + except ObjectDoesNotExist: + logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新") + else: + # 打手释放成功后,记录每日退款统计(转移视为退款) + try: + update_dashou_daily_by_action( + yonghuid=current_dashou_id, + amount=dashou_fencheng, + action=3 # 3 表示退款 + ) + logger.info(f"打手 {current_dashou_id} 转移大厅退款统计成功") + except Exception as e: + logger.error(f"打手每日统计失败: {e}") + + # ---------- 8. 重置订单 ---------- + order.jiedan_dashou_id = None + order.clkf = str(shop_id) + + # 根据是否有指定打手决定状态 + order.zhuangtai = 7 if zhiding_id else 1 + order.save() + + logger.info(f"转移大厅成功,订单 {dingdan_id}") + return Response({'code': 200, 'msg': '转移大厅成功'}) + + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}) + except Exception as e: + logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}) + + + + + + +# ================================================================ +# shangdian/views/fine_apply_view.py +# 店铺罚款申请接口 —— 二改自 FineApplyView(客服罚款) +# ================================================================ +class ShopFineApplyView(APIView): + """ + 店铺罚款申请接口 + URL: POST /shangdian/fksq + 权限: JWT认证 + 店铺身份验证 + + 请求参数: + userId (必填) : 店铺所属用户的ID(用于身份验证) + dingdan_id (必填) : 订单ID + reason (必填) : 罚款原因 + amount (必填) : 罚款金额(元) + yingxiang_qiangdan (可选) : 是否影响抢单,1=是,0=否,默认1 + + 说明: + - 仅处理平台订单,需属于当前店铺 + - 本地/对方派单处理逻辑与客服接口一致: + 无对方订单ID 或 对方派单 → 本地生成罚单 + 有对方订单ID且我方派单 → 跨平台通知对方,本地也记录(标记对方俱乐部ID) + - 申请人:店铺ID,身份码固定为4(店铺管理员) + - 被处罚人:接单打手,身份码固定为1 + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + # ---------- 1. 提取参数 ---------- + user_id = request.data.get('userId', '').strip() # 店铺用户ID + dingdan_id = request.data.get('dingdan_id', '').strip() + chufaliyou = request.data.get('reason', '').strip() # 罚款原因(对应前端的reason) + fakuanjine = request.data.get('amount', 0) # 罚款金额(对应前端的amount) + yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) # 影响抢单 + + if not all([user_id, dingdan_id, chufaliyou, fakuanjine]): + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + # ---------- 2. 店铺身份验证 ---------- + dianpu, error = verify_shop_permission(request, user_id) + if error: + return error + shop_id = dianpu.id # 店铺ID,作为罚款申请人 + + # ---------- 3. 验证订单归属 ---------- + try: + pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( + dianpu_id=shop_id, + dingdan__dingdan_id=dingdan_id + ) + order = pingtai_ext.dingdan + except DingdanPingtai.DoesNotExist: + logger.warning(f"罚款申请:订单 {dingdan_id} 不属于店铺 {shop_id}") + return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) + + # ---------- 4. 获取打手ID ---------- + dashou_id = order.jiedan_dashou_id + if not dashou_id: + return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) + + # ---------- 5. 重复罚款检查 ---------- + if Fadan.query.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): + return Response({'code': 400, 'msg': '该打手已被罚款过'}) + + # ---------- 6. 创建罚单 ---------- + with transaction.atomic(): + Fadan.query.create( + beichufa_id=dashou_id, + shenqing_chufa=str(shop_id), # 申请人记为店铺ID + shenfen=1, # 被处罚人身份:1 打手 + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, # 待缴纳 + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=4, # 申请人身份:4 店铺管理员 + ) + logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}") + return Response({'code': 200, 'msg': '罚款已生成'}) + + except Exception as e: + logger.error(f"店铺罚款申请异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '服务器内部错误'}) \ No newline at end of file diff --git a/users/fadan_fenhong_utils.py b/users/fadan_fenhong_utils.py index 3bfb1a6..a854780 100644 --- a/users/fadan_fenhong_utils.py +++ b/users/fadan_fenhong_utils.py @@ -1,166 +1,166 @@ -# yonghu/fadan_fenhong_utils.py -# 罚款缴纳分红处理公共方法(支持更新罚单中的申请人分红金额) - -import logging -from decimal import Decimal -from django.db import transaction - -from orders.models import Fadan, FadanFenhong, FadanFenhongLilv -from users.models import UserShangjia - -logger = logging.getLogger(__name__) - - -# 身份映射字典 -IDENTITY_MAP = { - 1: {'fenhong': 2, 'lilv': 1}, # 客服 - 2: {'fenhong': 5, 'lilv': 2}, # 售后 - 3: {'fenhong': 3, 'lilv': 3}, # 管理员 - 4: {'fenhong': 4, 'lilv': 4}, # 店铺 - 5: {'fenhong': 1, 'lilv': 5}, # 商家 -} - - -def process_fadan_fenhong(fadan_id): - """ - 罚款缴纳后的分红处理(在回调中调用) - 参数: - fadan_id: 罚单 ID - 返回: - (success, message) - """ - try: - # 1. 获取罚单对象(必须是已缴纳状态) - try: - fadan = Fadan.objects.get(id=fadan_id, zhuangtai=2) - except Fadan.DoesNotExist: - logger.error(f"罚单 {fadan_id} 不存在或状态不是已缴纳") - return False, "罚单不存在或状态异常" - - # 2. 获取申请处罚者ID和身份(源身份数字) - shenqing_chufa = fadan.shenqing_chufa - src_shenfen = fadan.shenqingren_shenfen - - if not shenqing_chufa: - logger.info(f"罚单 {fadan_id} 无申请处罚者,不进行分红") - # 即使无申请者,也可将分红金额字段设为0(默认已是0) - return True, "无申请处罚者,无需分红" - - # 3. 映射身份数字 - if src_shenfen not in IDENTITY_MAP: - logger.error(f"未知的申请处罚者身份: {src_shenfen},罚单 {fadan_id}") - return False, f"未知身份 {src_shenfen}" - - mapped = IDENTITY_MAP[src_shenfen] - fenhong_shenfen = mapped['fenhong'] # 用于 FadanFenhong.shenfen - lilv_shenfen = mapped['lilv'] # 用于 FadanFenhongLilv.shenfen - - # 4. 获取罚款金额 - fakuan_jine = fadan.fakuanjine - if fakuan_jine <= 0: - logger.info(f"罚单 {fadan_id} 罚款金额为0,不产生分红") - fadan.shenqingren_fenhong_jine = Decimal('0.00') - fadan.save(update_fields=['shenqingren_fenhong_jine']) - return True, "罚款金额为0,无分红" - - # 5. 确定分红利率 - rate = _get_fenhong_rate(shenqing_chufa, fenhong_shenfen, lilv_shenfen) - if rate is None: - logger.warning(f"未找到身份 {src_shenfen} (映射后 fenhong_shenfen={fenhong_shenfen}, lilv_shenfen={lilv_shenfen}) 的分红利率,罚单 {fadan_id} 不进行分红") - fadan.shenqingren_fenhong_jine = Decimal('0.00') - fadan.save(update_fields=['shenqingren_fenhong_jine']) - return True, "未配置分红利率,无分红" - - # 6. 计算分红金额 - fenhong_jine = (Decimal(str(fakuan_jine)) * Decimal(str(rate))).quantize(Decimal('0.01')) - if fenhong_jine <= 0: - logger.info(f"罚单 {fadan_id} 计算分红金额为0,无需处理") - fadan.shenqingren_fenhong_jine = Decimal('0.00') - fadan.save(update_fields=['shenqingren_fenhong_jine']) - return True, "分红金额为0" - - # 7. 根据申请处罚者身份执行不同的分红逻辑 - # src_shenfen == 5 代表商家(源身份) - if src_shenfen == 5: - # 商家特殊处理:只记录总额,可提现分红永远为0,直接加到商家余额 - with transaction.atomic(): - fenhong_record, created = FadanFenhong.objects.get_or_create( - fenhongzhe_id=shenqing_chufa, - shenfen=fenhong_shenfen, - defaults={ - 'zonge': Decimal('0.00'), - 'ketixian_fenhong': Decimal('0.00'), - 'fenhong_cishu': 0, - 'yingde_lilv': rate, - 'is_individual_rate': False - } - ) - fenhong_record.zonge += fenhong_jine - # 可提现分红不累加,保持0 - fenhong_record.fenhong_cishu += 1 - fenhong_record.save() - - # 直接增加商家余额(UserShangjia.yue) - try: - shangjia = UserShangjia.objects.get(user__yonghuid=shenqing_chufa) - shangjia.yue += fenhong_jine - shangjia.save(update_fields=['yue']) - logger.info(f"商家 {shenqing_chufa} 余额增加 {fenhong_jine} 元") - except UserShangjia.DoesNotExist: - logger.error(f"商家 {shenqing_chufa} 扩展表不存在,无法增加余额") - raise Exception(f"商家 {shenqing_chufa} 扩展表不存在,无法增加余额") - - # 更新罚单中的申请人分红金额 - fadan.shenqingren_fenhong_jine = fenhong_jine - fadan.save(update_fields=['shenqingren_fenhong_jine']) - - else: - # 其他身份(客服、售后、管理员、店铺):同时增加总额和可提现分红 - with transaction.atomic(): - fenhong_record, created = FadanFenhong.objects.get_or_create( - fenhongzhe_id=shenqing_chufa, - shenfen=fenhong_shenfen, - defaults={ - 'zonge': Decimal('0.00'), - 'ketixian_fenhong': Decimal('0.00'), - 'fenhong_cishu': 0, - 'yingde_lilv': rate, - 'is_individual_rate': False - } - ) - fenhong_record.zonge += fenhong_jine - fenhong_record.ketixian_fenhong += fenhong_jine - fenhong_record.fenhong_cishu += 1 - fenhong_record.save() - - # 更新罚单中的申请人分红金额 - fadan.shenqingren_fenhong_jine = fenhong_jine - fadan.save(update_fields=['shenqingren_fenhong_jine']) - - logger.info(f"罚单 {fadan_id} 分红处理成功: 申请者 {shenqing_chufa}, 分红金额 {fenhong_jine} 元, 源身份 {src_shenfen}, 映射后分红身份 {fenhong_shenfen}") - return True, "分红处理成功" - - except Exception as e: - logger.error(f"罚单 {fadan_id} 分红处理异常: {str(e)}", exc_info=True) - return False, f"分红处理异常: {str(e)}" - - -def _get_fenhong_rate(fenhongzhe_id, fenhong_shenfen, lilv_shenfen): - """ - 获取分红利率(优先使用个人单独配置,否则使用全局身份配置) - """ - try: - individual = FadanFenhong.objects.get( - fenhongzhe_id=fenhongzhe_id, - shenfen=fenhong_shenfen, - is_individual_rate=True - ) - return individual.yingde_lilv - except FadanFenhong.DoesNotExist: - pass - - try: - global_rate = FadanFenhongLilv.objects.get(shenfen=lilv_shenfen) - return global_rate.lilv - except FadanFenhongLilv.DoesNotExist: +# yonghu/fadan_fenhong_utils.py +# 罚款缴纳分红处理公共方法(支持更新罚单中的申请人分红金额) + +import logging +from decimal import Decimal +from django.db import transaction + +from orders.models import Fadan, FadanFenhong, FadanFenhongLilv +from users.models import UserShangjia + +logger = logging.getLogger(__name__) + + +# 身份映射字典 +IDENTITY_MAP = { + 1: {'fenhong': 2, 'lilv': 1}, # 客服 + 2: {'fenhong': 5, 'lilv': 2}, # 售后 + 3: {'fenhong': 3, 'lilv': 3}, # 管理员 + 4: {'fenhong': 4, 'lilv': 4}, # 店铺 + 5: {'fenhong': 1, 'lilv': 5}, # 商家 +} + + +def process_fadan_fenhong(fadan_id): + """ + 罚款缴纳后的分红处理(在回调中调用) + 参数: + fadan_id: 罚单 ID + 返回: + (success, message) + """ + try: + # 1. 获取罚单对象(必须是已缴纳状态) + try: + fadan = Fadan.objects.get(id=fadan_id, zhuangtai=2) + except Fadan.DoesNotExist: + logger.error(f"罚单 {fadan_id} 不存在或状态不是已缴纳") + return False, "罚单不存在或状态异常" + + # 2. 获取申请处罚者ID和身份(源身份数字) + shenqing_chufa = fadan.shenqing_chufa + src_shenfen = fadan.shenqingren_shenfen + + if not shenqing_chufa: + logger.info(f"罚单 {fadan_id} 无申请处罚者,不进行分红") + # 即使无申请者,也可将分红金额字段设为0(默认已是0) + return True, "无申请处罚者,无需分红" + + # 3. 映射身份数字 + if src_shenfen not in IDENTITY_MAP: + logger.error(f"未知的申请处罚者身份: {src_shenfen},罚单 {fadan_id}") + return False, f"未知身份 {src_shenfen}" + + mapped = IDENTITY_MAP[src_shenfen] + fenhong_shenfen = mapped['fenhong'] # 用于 FadanFenhong.shenfen + lilv_shenfen = mapped['lilv'] # 用于 FadanFenhongLilv.shenfen + + # 4. 获取罚款金额 + fakuan_jine = fadan.fakuanjine + if fakuan_jine <= 0: + logger.info(f"罚单 {fadan_id} 罚款金额为0,不产生分红") + fadan.shenqingren_fenhong_jine = Decimal('0.00') + fadan.save(update_fields=['shenqingren_fenhong_jine']) + return True, "罚款金额为0,无分红" + + # 5. 确定分红利率 + rate = _get_fenhong_rate(shenqing_chufa, fenhong_shenfen, lilv_shenfen) + if rate is None: + logger.warning(f"未找到身份 {src_shenfen} (映射后 fenhong_shenfen={fenhong_shenfen}, lilv_shenfen={lilv_shenfen}) 的分红利率,罚单 {fadan_id} 不进行分红") + fadan.shenqingren_fenhong_jine = Decimal('0.00') + fadan.save(update_fields=['shenqingren_fenhong_jine']) + return True, "未配置分红利率,无分红" + + # 6. 计算分红金额 + fenhong_jine = (Decimal(str(fakuan_jine)) * Decimal(str(rate))).quantize(Decimal('0.01')) + if fenhong_jine <= 0: + logger.info(f"罚单 {fadan_id} 计算分红金额为0,无需处理") + fadan.shenqingren_fenhong_jine = Decimal('0.00') + fadan.save(update_fields=['shenqingren_fenhong_jine']) + return True, "分红金额为0" + + # 7. 根据申请处罚者身份执行不同的分红逻辑 + # src_shenfen == 5 代表商家(源身份) + if src_shenfen == 5: + # 商家特殊处理:只记录总额,可提现分红永远为0,直接加到商家余额 + with transaction.atomic(): + fenhong_record, created = FadanFenhong.objects.get_or_create( + fenhongzhe_id=shenqing_chufa, + shenfen=fenhong_shenfen, + defaults={ + 'zonge': Decimal('0.00'), + 'ketixian_fenhong': Decimal('0.00'), + 'fenhong_cishu': 0, + 'yingde_lilv': rate, + 'is_individual_rate': False + } + ) + fenhong_record.zonge += fenhong_jine + # 可提现分红不累加,保持0 + fenhong_record.fenhong_cishu += 1 + fenhong_record.save() + + # 直接增加商家余额(UserShangjia.yue) + try: + shangjia = UserShangjia.objects.get(user__UserUID=shenqing_chufa) + shangjia.yue += fenhong_jine + shangjia.save(update_fields=['yue']) + logger.info(f"商家 {shenqing_chufa} 余额增加 {fenhong_jine} 元") + except UserShangjia.DoesNotExist: + logger.error(f"商家 {shenqing_chufa} 扩展表不存在,无法增加余额") + raise Exception(f"商家 {shenqing_chufa} 扩展表不存在,无法增加余额") + + # 更新罚单中的申请人分红金额 + fadan.shenqingren_fenhong_jine = fenhong_jine + fadan.save(update_fields=['shenqingren_fenhong_jine']) + + else: + # 其他身份(客服、售后、管理员、店铺):同时增加总额和可提现分红 + with transaction.atomic(): + fenhong_record, created = FadanFenhong.objects.get_or_create( + fenhongzhe_id=shenqing_chufa, + shenfen=fenhong_shenfen, + defaults={ + 'zonge': Decimal('0.00'), + 'ketixian_fenhong': Decimal('0.00'), + 'fenhong_cishu': 0, + 'yingde_lilv': rate, + 'is_individual_rate': False + } + ) + fenhong_record.zonge += fenhong_jine + fenhong_record.ketixian_fenhong += fenhong_jine + fenhong_record.fenhong_cishu += 1 + fenhong_record.save() + + # 更新罚单中的申请人分红金额 + fadan.shenqingren_fenhong_jine = fenhong_jine + fadan.save(update_fields=['shenqingren_fenhong_jine']) + + logger.info(f"罚单 {fadan_id} 分红处理成功: 申请者 {shenqing_chufa}, 分红金额 {fenhong_jine} 元, 源身份 {src_shenfen}, 映射后分红身份 {fenhong_shenfen}") + return True, "分红处理成功" + + except Exception as e: + logger.error(f"罚单 {fadan_id} 分红处理异常: {str(e)}", exc_info=True) + return False, f"分红处理异常: {str(e)}" + + +def _get_fenhong_rate(fenhongzhe_id, fenhong_shenfen, lilv_shenfen): + """ + 获取分红利率(优先使用个人单独配置,否则使用全局身份配置) + """ + try: + individual = FadanFenhong.objects.get( + fenhongzhe_id=fenhongzhe_id, + shenfen=fenhong_shenfen, + is_individual_rate=True + ) + return individual.yingde_lilv + except FadanFenhong.DoesNotExist: + pass + + try: + global_rate = FadanFenhongLilv.objects.get(shenfen=lilv_shenfen) + return global_rate.lilv + except FadanFenhongLilv.DoesNotExist: return None \ No newline at end of file diff --git a/users/paihang_views.py b/users/paihang_views.py index 3a9dc28..0233565 100644 --- a/users/paihang_views.py +++ b/users/paihang_views.py @@ -1,218 +1,218 @@ -""" -排行榜接口 — 基于 houtai 日统计表聚合查询 -POST /yonghu/phbhqsj -""" -import logging -from datetime import date, timedelta -from decimal import Decimal - -from django.db.models import Sum -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.views import APIView - -from backend.models import ( - DashouRiTongji, - GuanshiRiTongji, - ZuzhangRiTongji, - ShangjiaRiTongji, -) -from users.models import UserDashou, UserShangjia, UserBoss -from gvsdsdk.models import User - -logger = logging.getLogger(__name__) - -MAX_RANK = 50 - -RIQI_OPTIONS = frozenset({'今日', '本周', '本月', '总榜', '昨日', '上周', '上月'}) -SHENFEN_OPTIONS = frozenset({'dashou', 'guanshi', 'zuzhang', 'shangjia'}) - -ROLE_CONFIG = { - 'dashou': { - 'model': DashouRiTongji, - 'sort_field': 'chengjiao_zonge', - 'fields': ('jiedan_zongliang', 'chengjiao_zongliang', 'jiedan_zonge', 'chengjiao_zonge'), - 'int_fields': frozenset({'jiedan_zongliang', 'chengjiao_zongliang'}), - }, - 'guanshi': { - 'model': GuanshiRiTongji, - 'sort_field': 'shouru_zonge', - 'fields': ('yaoqing_dashou_shu', 'chongzhi_dashou_shu', 'shouru_zonge'), - 'int_fields': frozenset({'yaoqing_dashou_shu', 'chongzhi_dashou_shu'}), - }, - 'zuzhang': { - 'model': ZuzhangRiTongji, - 'sort_field': 'shouru_zonge', - 'fields': ('yaoqing_guanshi_shu', 'fenyong_jine', 'shouru_zonge'), - 'int_fields': frozenset({'yaoqing_guanshi_shu'}), - }, - 'shangjia': { - 'model': ShangjiaRiTongji, - 'sort_field': 'jiesuan_jine', - 'fields': ('paifa_dingdan_shu', 'paifa_jine', 'jiesuan_dingdan_shu', 'jiesuan_jine'), - 'int_fields': frozenset({'paifa_dingdan_shu', 'jiesuan_dingdan_shu'}), - }, -} - - -def _resolve_date_range(riqi: str): - """根据前端 riqi 参数返回 (start_date, end_date) 闭区间。""" - today = date.today() - if riqi == '今日': - return today, today - if riqi == '昨日': - d = today - timedelta(days=1) - return d, d - if riqi == '本周': - monday = today - timedelta(days=today.weekday()) - return monday, today - if riqi == '上周': - this_monday = today - timedelta(days=today.weekday()) - last_sunday = this_monday - timedelta(days=1) - last_monday = last_sunday - timedelta(days=6) - return last_monday, last_sunday - if riqi == '本月': - return today.replace(day=1), today - if riqi == '上月': - first_this_month = today.replace(day=1) - last_day_prev = first_this_month - timedelta(days=1) - return last_day_prev.replace(day=1), last_day_prev - return None - - -def _fmt_value(val, is_int: bool): - if val is None: - return 0 if is_int else 0.0 - if is_int: - return int(val) - if isinstance(val, Decimal): - return float(val.quantize(Decimal('0.01'))) - return float(val) - - -class PhbHqsjView(APIView): - """ - 排行榜数据接口 - POST /yonghu/phbhqsj - - 请求参数: - shenfen: dashou | guanshi | zuzhang | shangjia - riqi: 今日 | 本周 | 本月 | 总榜 | 昨日 | 上周 | 上月 - - 响应 data.list[](最多50条,已按各身份排序字段降序): - mingci, yonghuid, nicheng, touxiang + 各身份统计字段 - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - shenfen = (request.data.get('shenfen') or '').strip() - riqi = (request.data.get('riqi') or '').strip() - - if shenfen not in SHENFEN_OPTIONS: - return Response({ - 'code': 400, - 'msg': '参数错误:shenfen 必须为 dashou/guanshi/zuzhang/shangjia', - 'data': {'list': []}, - }) - if riqi not in RIQI_OPTIONS: - return Response({ - 'code': 400, - 'msg': '参数错误:riqi 必须为 今日/本周/本月/总榜/昨日/上周/上月', - 'data': {'list': []}, - }) - - try: - if riqi == '总榜': - rows = self._query_rank_rows(shenfen, all_time=True) - else: - date_range = _resolve_date_range(riqi) - if date_range is None: - return Response({ - 'code': 400, - 'msg': '参数错误:无效的日期范围', - 'data': {'list': []}, - }) - rows = self._query_rank_rows(shenfen, date_range[0], date_range[1]) - nick_map, avatar_map = self._load_user_display(rows, shenfen) - cfg = ROLE_CONFIG[shenfen] - int_fields = cfg['int_fields'] - - result_list = [] - for idx, row in enumerate(rows): - yonghuid = row['yonghuid'] - item = { - 'mingci': idx + 1, - 'yonghuid': yonghuid, - 'nicheng': nick_map.get(yonghuid, '用户'), - 'touxiang': avatar_map.get(yonghuid, '') or '', - } - for field in cfg['fields']: - item[field] = _fmt_value(row.get(field), field in int_fields) - result_list.append(item) - - return Response({ - 'code': 200, - 'msg': '获取成功', - 'data': {'list': result_list}, - }) - except Exception as e: - logger.error('排行榜查询失败 shenfen=%s riqi=%s: %s', shenfen, riqi, e, exc_info=True) - return Response({ - 'code': 500, - 'msg': '服务器内部错误', - 'data': {'list': []}, - }) - - def _query_rank_rows(self, shenfen, start_date=None, end_date=None, all_time=False): - cfg = ROLE_CONFIG[shenfen] - model = cfg['model'] - sort_field = cfg['sort_field'] - fields = cfg['fields'] - annotate_kwargs = {f: Sum(f) for f in fields} - - if all_time: - rows = ( - model.objects - .values('yonghuid') - .annotate(**annotate_kwargs) - .order_by(f'-{sort_field}')[:MAX_RANK] - ) - return list(rows) - - if start_date == end_date: - qs = model.objects.filter(riqi=start_date).order_by(f'-{sort_field}')[:MAX_RANK] - return [ - {'yonghuid': r.yonghuid, **{f: getattr(r, f) for f in fields}} - for r in qs - ] - - rows = ( - model.objects - .filter(riqi__gte=start_date, riqi__lte=end_date) - .values('yonghuid') - .annotate(**annotate_kwargs) - .order_by(f'-{sort_field}')[:MAX_RANK] - ) - return list(rows) - - def _load_user_display(self, rows, shenfen): - yonghuids = [r['yonghuid'] for r in rows if r.get('yonghuid')] - if not yonghuids: - return {}, {} - - users = User.objects.filter(yonghuid__in=yonghuids) - avatar_map = {u.yonghuid: u.avatar or '' for u in users} - - nick_map = {} - if shenfen == 'dashou': - for p in UserDashou.objects.filter(user__yonghuid__in=yonghuids).select_related('user'): - nick_map[p.user.yonghuid] = p.nicheng or '用户' - elif shenfen == 'shangjia': - for p in UserShangjia.objects.filter(user__yonghuid__in=yonghuids).select_related('user'): - nick_map[p.user.yonghuid] = p.nicheng or '用户' - else: - for b in UserBoss.objects.filter(user__yonghuid__in=yonghuids).select_related('user'): - nick_map[b.user.yonghuid] = b.nickname or '用户' - - return nick_map, avatar_map +""" +排行榜接口 — 基于 houtai 日统计表聚合查询 +POST /yonghu/phbhqsj +""" +import logging +from datetime import date, timedelta +from decimal import Decimal + +from django.db.models import Sum +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from backend.models import ( + DashouRiTongji, + GuanshiRiTongji, + ZuzhangRiTongji, + ShangjiaRiTongji, +) +from users.models import UserDashou, UserShangjia, UserBoss +from gvsdsdk.models import User + +logger = logging.getLogger(__name__) + +MAX_RANK = 50 + +RIQI_OPTIONS = frozenset({'今日', '本周', '本月', '总榜', '昨日', '上周', '上月'}) +SHENFEN_OPTIONS = frozenset({'dashou', 'guanshi', 'zuzhang', 'shangjia'}) + +ROLE_CONFIG = { + 'dashou': { + 'model': DashouRiTongji, + 'sort_field': 'chengjiao_zonge', + 'fields': ('jiedan_zongliang', 'chengjiao_zongliang', 'jiedan_zonge', 'chengjiao_zonge'), + 'int_fields': frozenset({'jiedan_zongliang', 'chengjiao_zongliang'}), + }, + 'guanshi': { + 'model': GuanshiRiTongji, + 'sort_field': 'shouru_zonge', + 'fields': ('yaoqing_dashou_shu', 'chongzhi_dashou_shu', 'shouru_zonge'), + 'int_fields': frozenset({'yaoqing_dashou_shu', 'chongzhi_dashou_shu'}), + }, + 'zuzhang': { + 'model': ZuzhangRiTongji, + 'sort_field': 'shouru_zonge', + 'fields': ('yaoqing_guanshi_shu', 'fenyong_jine', 'shouru_zonge'), + 'int_fields': frozenset({'yaoqing_guanshi_shu'}), + }, + 'shangjia': { + 'model': ShangjiaRiTongji, + 'sort_field': 'jiesuan_jine', + 'fields': ('paifa_dingdan_shu', 'paifa_jine', 'jiesuan_dingdan_shu', 'jiesuan_jine'), + 'int_fields': frozenset({'paifa_dingdan_shu', 'jiesuan_dingdan_shu'}), + }, +} + + +def _resolve_date_range(riqi: str): + """根据前端 riqi 参数返回 (start_date, end_date) 闭区间。""" + today = date.today() + if riqi == '今日': + return today, today + if riqi == '昨日': + d = today - timedelta(days=1) + return d, d + if riqi == '本周': + monday = today - timedelta(days=today.weekday()) + return monday, today + if riqi == '上周': + this_monday = today - timedelta(days=today.weekday()) + last_sunday = this_monday - timedelta(days=1) + last_monday = last_sunday - timedelta(days=6) + return last_monday, last_sunday + if riqi == '本月': + return today.replace(day=1), today + if riqi == '上月': + first_this_month = today.replace(day=1) + last_day_prev = first_this_month - timedelta(days=1) + return last_day_prev.replace(day=1), last_day_prev + return None + + +def _fmt_value(val, is_int: bool): + if val is None: + return 0 if is_int else 0.0 + if is_int: + return int(val) + if isinstance(val, Decimal): + return float(val.quantize(Decimal('0.01'))) + return float(val) + + +class PhbHqsjView(APIView): + """ + 排行榜数据接口 + POST /yonghu/phbhqsj + + 请求参数: + shenfen: dashou | guanshi | zuzhang | shangjia + riqi: 今日 | 本周 | 本月 | 总榜 | 昨日 | 上周 | 上月 + + 响应 data.list[](最多50条,已按各身份排序字段降序): + mingci, yonghuid, nicheng, touxiang + 各身份统计字段 + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + shenfen = (request.data.get('shenfen') or '').strip() + riqi = (request.data.get('riqi') or '').strip() + + if shenfen not in SHENFEN_OPTIONS: + return Response({ + 'code': 400, + 'msg': '参数错误:shenfen 必须为 dashou/guanshi/zuzhang/shangjia', + 'data': {'list': []}, + }) + if riqi not in RIQI_OPTIONS: + return Response({ + 'code': 400, + 'msg': '参数错误:riqi 必须为 今日/本周/本月/总榜/昨日/上周/上月', + 'data': {'list': []}, + }) + + try: + if riqi == '总榜': + rows = self._query_rank_rows(shenfen, all_time=True) + else: + date_range = _resolve_date_range(riqi) + if date_range is None: + return Response({ + 'code': 400, + 'msg': '参数错误:无效的日期范围', + 'data': {'list': []}, + }) + rows = self._query_rank_rows(shenfen, date_range[0], date_range[1]) + nick_map, avatar_map = self._load_user_display(rows, shenfen) + cfg = ROLE_CONFIG[shenfen] + int_fields = cfg['int_fields'] + + result_list = [] + for idx, row in enumerate(rows): + yonghuid = row['yonghuid'] + item = { + 'mingci': idx + 1, + 'yonghuid': yonghuid, + 'nicheng': nick_map.get(yonghuid, '用户'), + 'touxiang': avatar_map.get(yonghuid, '') or '', + } + for field in cfg['fields']: + item[field] = _fmt_value(row.get(field), field in int_fields) + result_list.append(item) + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': {'list': result_list}, + }) + except Exception as e: + logger.error('排行榜查询失败 shenfen=%s riqi=%s: %s', shenfen, riqi, e, exc_info=True) + return Response({ + 'code': 500, + 'msg': '服务器内部错误', + 'data': {'list': []}, + }) + + def _query_rank_rows(self, shenfen, start_date=None, end_date=None, all_time=False): + cfg = ROLE_CONFIG[shenfen] + model = cfg['model'] + sort_field = cfg['sort_field'] + fields = cfg['fields'] + annotate_kwargs = {f: Sum(f) for f in fields} + + if all_time: + rows = ( + model.objects + .values('yonghuid') + .annotate(**annotate_kwargs) + .order_by(f'-{sort_field}')[:MAX_RANK] + ) + return list(rows) + + if start_date == end_date: + qs = model.objects.filter(riqi=start_date).order_by(f'-{sort_field}')[:MAX_RANK] + return [ + {'yonghuid': r.yonghuid, **{f: getattr(r, f) for f in fields}} + for r in qs + ] + + rows = ( + model.objects + .filter(riqi__gte=start_date, riqi__lte=end_date) + .values('yonghuid') + .annotate(**annotate_kwargs) + .order_by(f'-{sort_field}')[:MAX_RANK] + ) + return list(rows) + + def _load_user_display(self, rows, shenfen): + yonghuids = [r['yonghuid'] for r in rows if r.get('yonghuid')] + if not yonghuids: + return {}, {} + + users = User.objects.filter(UserUID__in=yonghuids) + avatar_map = {u.yonghuid: u.avatar or '' for u in users} + + nick_map = {} + if shenfen == 'dashou': + for p in UserDashou.objects.filter(user__UserUID__in=yonghuids).select_related('user'): + nick_map[p.user.yonghuid] = p.nicheng or '用户' + elif shenfen == 'shangjia': + for p in UserShangjia.objects.filter(user__UserUID__in=yonghuids).select_related('user'): + nick_map[p.user.yonghuid] = p.nicheng or '用户' + else: + for b in UserBoss.objects.filter(user__UserUID__in=yonghuids).select_related('user'): + nick_map[b.user.yonghuid] = b.nickname or '用户' + + return nick_map, avatar_map diff --git a/users/tixian_shenhe_services.py b/users/tixian_shenhe_services.py index 762becc..cfde88b 100644 --- a/users/tixian_shenhe_services.py +++ b/users/tixian_shenhe_services.py @@ -1,1440 +1,1440 @@ -# yonghu/tixian_shenhe_services.py -""" -自动提现审核流程业务服务层(仅服务于 zddksh 申请接口 与 tixiansq 收款接口) - -流程: - ① zddksh 提交审核:完整资格校验 → 扣款 → 写审核表+提现记录表(状态=1) - 允许多笔并行申请;单笔实际到账(shijidaozhang)不得超过200元 - ② 后台审核通过:校验到账≤200 → 状态 6(待收款) - ③ tixiansq 收款:严格资格+限额 → 到账>200则关单退款(不退手续费),不调微信 - 微信官方 FAIL 关单不退款;SUCCESS 落库;进行中禁止新建打款单 - ④ 打款成功(tixianqr/回调/查单):若收款前已预占则不再累加;未预占的旧单/查单直成则补记 - -状态码(与前端 TX_STATUS 一致): - 1=审核中 2=提现成功 3=提现失败 4=审核成功 5=审核失败 6=待收款 -""" -import decimal -import json -import logging -import random -import time -from datetime import date, timedelta - -import requests -from django.conf import settings -from django.db import transaction -from django.utils import timezone - -from orders.models import Dingdan, DingdanShangjia, Fadan, Lilubiao -from config.models import Szjilu, TixianQuotaDefault - -from utils.fadan_utils import check_fadan_qiangdan_eligible - -from products.models import Huiyuangoumai -from backend.models import TixianRiTongji - -from utils.wechat_v3 import build_authorization - -from .models import ( - TixianAutoRecord, - TixianShenheJilu, - Tixianjilu, - UserDashou, - UserGuanshi, - UserShenheguan, - UserShangjia, - UserZuzhang, -) -from gvsdsdk.models import User - -from orders.utils import update_daily_payout - -from django.db.models import Sum - -from gvsdsdk.fluent import db, func, FQ - -logger = logging.getLogger('yonghu.tixian_shenhe') - -ORDER_STATUS_COMPLETED = 3 -ORDER_STATUS_REFUNDED = 5 - -LEIXING_RATE_KEY = { - 1: '5', - 2: '6', - 3: '8', - 4: '9', - 5: '11', - 6: '10', -} - -DAKUAN_MODE_MANUAL = 1 -DAKUAN_MODE_AUTO = 2 - -# 微信转账单状态(查单接口返回 state 字段) -WX_STATE_SUCCESS = ('SUCCESS',) -WX_STATE_WAIT = ('WAIT_USER_CONFIRM', 'TRANSFERING', 'ACCEPTED', 'PROCESSING') -WX_STATE_FAIL = ('FAIL', 'CLOSED', 'CANCELLED') -WX_STATE_CANCELING = ('CANCELING',) # 撤销中,非终态,禁止换新单号 - -RECONCILE_COMPLETED = 'completed' -RECONCILE_WAIT_CONFIRM = 'wait_confirm' -RECONCILE_ALLOW_NEW = 'allow_new' -RECONCILE_PENDING = 'pending_query' -RECONCILE_AUDIT_CLOSED = 'audit_closed' - -# 单笔实际到账上限(仅收款时校验) -MAX_SINGLE_COLLECT_AMOUNT = decimal.Decimal('200') - -# 查单失败且本地 zhuangtai=0 超过该分钟数,才允许新建打款记录 -PENDING_QUERY_GRACE_MINUTES = 10 - - -def has_dakuan_mode_field(): - """生产库未跑 0032 迁移时模型无此字段,需兼容""" - try: - TixianShenheJilu._meta.get_field('dakuan_mode') - return True - except Exception: - return False - - -def is_auto_dakuan_audit(audit): - """判断是否自动打款审核单;无 dakuan_mode 属性时,有审核记录即视为 zddksh 自动流程""" - if audit is None: - return False - mode = getattr(audit, 'dakuan_mode', None) - if mode is None: - return True - try: - return int(mode) == DAKUAN_MODE_AUTO - except (TypeError, ValueError): - return True - - -def load_audit_dakuan_mode_map(audit_ids): - """批量取审核单打款方式;字段不存在时有关联审核即视为自动(2)""" - return { - aid: meta.get('dakuan_mode', DAKUAN_MODE_AUTO) - for aid, meta in load_audit_meta_map(audit_ids).items() - } - - -def load_audit_meta_map(audit_ids): - """ - 批量取审核表元数据(供后台列表等) - 返回 {审核记录id: {'dakuan_mode': int, 'shenhe_danhao': str}} - """ - if not audit_ids: - return {} - value_fields = ['id', 'shenhe_danhao'] - if has_dakuan_mode_field(): - value_fields.append('dakuan_mode') - result = {} - for row in TixianShenheJilu.query.filter(id__in=audit_ids).values(*value_fields): - result[row['id']] = { - 'shenhe_danhao': row.get('shenhe_danhao') or '', - 'dakuan_mode': row.get('dakuan_mode', DAKUAN_MODE_AUTO), - } - return result - - -def generate_shenhe_danhao(): - """生成唯一审核单号:TXSH + 时间戳后10位 + 4位随机数""" - for _ in range(10): - timestamp = str(int(time.time() * 1000))[-10:] - random_part = str(random.randint(1000, 9999)) - danhao = f'TXSH{timestamp}{random_part}' - if not TixianShenheJilu.query.filter(shenhe_danhao=danhao).exists(): - return danhao - raise RuntimeError('生成审核单号失败,请重试') - - -def get_tixian_feilv(leixing): - rate_key = LEIXING_RATE_KEY.get(leixing) - if not rate_key: - raise ValueError('无效的提现类型') - rate_obj = Lilubiao.query.filter(fadanpingtai=rate_key).first() - if not rate_obj: - raise ValueError(f'提现费率未配置(类型{leixing})') - return decimal.Decimal(str(rate_obj.lilu)) - - -def calc_fee_amounts(jine, feilv): - """申请金额 jine;实际到账 = jine - 手续费""" - shouxufei = (jine * feilv).quantize(decimal.Decimal('0.01')) - shijidaozhang = (jine - shouxufei).quantize(decimal.Decimal('0.01')) - return shouxufei, shijidaozhang - - -def check_dashou_has_valid_huiyuan(yonghuid): - """ - 打手至少持有一个未过期会员(Huiyuangoumai + jiance_shifou_daoqi 同步状态) - 返回 (True, '') 或 (False, 原因) - """ - - records = Huiyuangoumai.query.filter(yonghu_id=yonghuid) - if not records.exists(): - return False, '您暂无会员记录,无法提现' - - for record in records: - is_expired = record.jiance_shifou_daoqi() - if not is_expired and record.huiyuan_zhuangtai == 1: - return True, '' - return False, '您的会员已全部过期,请先续费后再操作' - - -def validate_withdraw_eligibility(user_main, leixing, jine): - """ - 提现申请资格校验(完整基本条件;仅放宽「不限制并行申请笔数」)。 - 与手动提现 TixianShenqingView 对齐。 - """ - yonghuid = user_main.yonghuid - - fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) - if not fadan_ok: - return False, fadan_msg, {} - - if leixing == 1: - try: - dashou = UserDashou.query.get(user=user_main) - except UserDashou.DoesNotExist: - return False, '您不是打手身份,无法提现佣金', {} - if dashou.zhanghaozhuangtai != 1: - return False, '打手账号已被封禁,无法提现', {} - if dashou.zhuangtai != 1: - return False, '有订单进行中,请完成后提现', {} - if dashou.jifen != 10: - return False, '积分不足10分,请补充积分后提现', {} - huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) - if not huiyuan_ok: - return False, huiyuan_msg, {} - if dashou.yue < jine: - return False, f'余额不足,当前余额: {dashou.yue}', {} - return True, '', {'nicheng': dashou.nicheng or ''} - - if leixing == 2: - try: - guanshi = UserGuanshi.query.get(user=user_main) - except UserGuanshi.DoesNotExist: - return False, '您不是管事身份,无法提现分红', {} - if guanshi.zhuangtai != 1: - return False, '管事账号已被封禁,无法提现', {} - if guanshi.yue < jine: - return False, f'余额不足,当前余额: {guanshi.yue}', {} - return True, '', {'nicheng': '管事用户'} - - if leixing == 3: - try: - zuzhang = UserZuzhang.query.get(user=user_main) - except UserZuzhang.DoesNotExist: - return False, '您不是组长身份,无法提现组长分红', {} - if zuzhang.zhuangtai != 1: - return False, '组长账号已被封禁,无法提现', {} - if zuzhang.ketixian_jine < jine: - return False, f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}', {} - return True, '', {'nicheng': ''} - - if leixing == 4: - try: - kaoheguan = UserShenheguan.query.get(user=user_main) - except UserShenheguan.DoesNotExist: - return False, '您不是考核官身份,无法提现分佣', {} - if kaoheguan.zhuangtai != 1: - return False, '考核官账号已被封禁,无法提现', {} - if kaoheguan.yue < jine: - return False, f'考核官可提现金额不足,当前余额: {kaoheguan.yue}', {} - return True, '', {'nicheng': '考核官'} - - if leixing == 5: - try: - dashou = UserDashou.query.get(user=user_main) - except UserDashou.DoesNotExist: - return False, '您不是打手身份,无法提取押金', {} - if dashou.zhanghaozhuangtai != 1: - return False, '打手账号已禁用,无法提取押金', {} - if dashou.zhuangtai != 1: - return False, '您有订单进行中,请完成后提取押金', {} - if dashou.jifen != 10: - return False, '积分不足10分,请补充积分后提取押金', {} - huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) - if not huiyuan_ok: - return False, huiyuan_msg, {} - if dashou.yajin < jine: - return False, f'押金余额不足,当前押金: {dashou.yajin}', {} - unfinished = Dingdan.query.filter( - jiedan_dashou_id=yonghuid, - ).exclude(zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() - if unfinished: - return False, '您还有未完成或未退款的订单,请先处理完毕再提取押金', {} - self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() - if self_fadan: - return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} - return True, '', {'nicheng': dashou.nicheng or ''} - - if leixing == 6: - try: - shangjia = UserShangjia.query.get(user=user_main) - except UserShangjia.DoesNotExist: - return False, '您不是商家身份,无法提现商家余额', {} - if shangjia.zhuangtai != 1: - return False, '商家账号已被禁用,无法提现', {} - if shangjia.yue < jine: - return False, f'商家余额不足,当前余额: {shangjia.yue}', {} - unfinished_orders = DingdanShangjia.query.filter( - shangjia_id=yonghuid, - ).exclude(dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() - if unfinished_orders: - return False, '您还有未完成或未退款的订单,请先处理完毕再提现余额', {} - self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() - if self_fadan: - return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} - applied_fadan = Fadan.query.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() - if applied_fadan: - return False, '您发起的对其他用户的处罚尚未处理完毕,请等待完结', {} - return True, '', {'nicheng': shangjia.nicheng or ''} - - return False, '无效的提现类型', {} - - -def check_shijidaozhang_within_limit(shijidaozhang): - """校验单笔实际到账不超过上限(申请/审核/收款共用)""" - if shijidaozhang > MAX_SINGLE_COLLECT_AMOUNT: - return False, f'单笔实际到账不能超过{MAX_SINGLE_COLLECT_AMOUNT}元,请调整提现金额后重新申请' - return True, '' - - -def validate_collect_amount(audit): - """收款时校验单笔实际到账上限(超限将关单退款)""" - return check_shijidaozhang_within_limit(audit.shijidaozhang) - - -def validate_collect_eligibility(user_main, leixing): - """ - 收款前严格校验(申请时已扣款,不再校余额)。 - 打手佣金/押金:罚单、封禁、接单中、积分、会员、订单等; - 其他身份:账号状态、罚单/订单等。 - """ - yonghuid = user_main.yonghuid - - fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) - if not fadan_ok: - return False, fadan_msg - - if leixing == 1: - try: - dashou = UserDashou.query.get(user=user_main) - except UserDashou.DoesNotExist: - return False, '您不是打手身份,无法收款' - if dashou.zhanghaozhuangtai != 1: - return False, '打手账号已被封禁,无法收款' - if dashou.zhuangtai != 1: - return False, '您有订单进行中,请完成后再收款' - if dashou.jifen != 10: - return False, '积分不足10分,无法收款' - huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) - if not huiyuan_ok: - return False, huiyuan_msg - return True, '' - - if leixing == 5: - try: - dashou = UserDashou.query.get(user=user_main) - except UserDashou.DoesNotExist: - return False, '您不是打手身份,无法收款' - if dashou.zhanghaozhuangtai != 1: - return False, '打手账号已禁用,无法收款' - if dashou.zhuangtai != 1: - return False, '您有订单进行中,请完成后再收款' - if dashou.jifen != 10: - return False, '积分不足10分,无法收款' - huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) - if not huiyuan_ok: - return False, huiyuan_msg - unfinished = Dingdan.query.filter( - jiedan_dashou_id=yonghuid, - ).exclude(zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() - if unfinished: - return False, '您还有未完成或未退款的订单,无法收款' - self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() - if self_fadan: - return False, '您有未处理完毕的自身罚单,无法收款' - return True, '' - - if leixing == 2: - try: - guanshi = UserGuanshi.query.get(user=user_main) - except UserGuanshi.DoesNotExist: - return False, '您不是管事身份,无法收款' - if guanshi.zhuangtai != 1: - return False, '管事账号已被封禁,无法收款' - return True, '' - - if leixing == 6: - try: - shangjia = UserShangjia.query.get(user=user_main) - except UserShangjia.DoesNotExist: - return False, '您不是商家身份,无法收款' - if shangjia.zhuangtai != 1: - return False, '商家账号已被禁用,无法收款' - unfinished_orders = DingdanShangjia.query.filter( - shangjia_id=yonghuid, - ).exclude(dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() - if unfinished_orders: - return False, '您还有未完成或未退款的订单,无法收款' - self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() - if self_fadan: - return False, '您有未处理完毕的自身罚单,无法收款' - applied_fadan = Fadan.query.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() - if applied_fadan: - return False, '您发起的处罚尚未处理完毕,无法收款' - return True, '' - - if leixing == 3: - try: - zuzhang = UserZuzhang.query.get(user=user_main) - except UserZuzhang.DoesNotExist: - return False, '您不是组长身份,无法收款' - if zuzhang.zhuangtai != 1: - return False, '组长账号已被封禁,无法收款' - return True, '' - - if leixing == 4: - try: - kaoheguan = UserShenheguan.query.get(user=user_main) - except UserShenheguan.DoesNotExist: - return False, '您不是考核官身份,无法收款' - if kaoheguan.zhuangtai != 1: - return False, '考核官账号已被封禁,无法收款' - return True, '' - - return False, '无效的提现类型' - - -def _get_personal_daily_quota(leixing, profile): - """打手/管事/组长个人每日限额:额外开启优先,否则默认配置""" - if leixing == 1: - if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0: - return profile.ewai_xiane - return TixianQuotaDefault.query.get(leixing=1).default_quota - if leixing == 2: - if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0: - return profile.ewai_xiane - return TixianQuotaDefault.query.get(leixing=2).default_quota - if leixing == 3: - try: - default_quota = TixianQuotaDefault.query.get(leixing=3).default_quota - except TixianQuotaDefault.DoesNotExist: - default_quota = decimal.Decimal('0.00') - if profile.kaioi_ewai_tixian and profile.ewai_tixian_xiane > 0: - return profile.ewai_tixian_xiane - return default_quota - raise ValueError('无效的个人限额身份') - - -def _get_active_auto_record(shenhe_danhao): - if not shenhe_danhao: - return None - return ( - TixianAutoRecord.query.filter( - shenhe_danhao=shenhe_danhao, - zhuangtai__in=[0, 1], - ) - .order_by('-create_time') - .first() - ) - - -def _get_platform_daily_total(leixing, today=None): - """平台今日已提现总额:只读 houtai.TixianRiTongji(到账金额口径)""" - if today is None: - today = date.today() - try: - return TixianRiTongji.query.get(riqi=today, leixing=leixing).total_amount - except TixianRiTongji.DoesNotExist: - return decimal.Decimal('0.00') - - -def _calc_platform_projected(leixing, shijidaozhang, shenhe_danhao=''): - """ - 平台今日预占后总额(收款前只读预估)。 - 本审核单若已有进行中打款记录,其金额尚未写入统计表,但不应重复加计。 - 返回 (daily_used, projected_total, additional_amount) - """ - daily_used = _get_platform_daily_total(leixing) - active_rec = _get_active_auto_record(shenhe_danhao) - if active_rec: - return daily_used, daily_used, decimal.Decimal('0.00') - additional = shijidaozhang or decimal.Decimal('0.00') - return daily_used, daily_used + additional, additional - - -def _projected_personal_total(personal_today, jine, shenhe_danhao=''): - """个人今日占用预估(申请金额口径)""" - rec = _get_active_auto_record(shenhe_danhao) - already_counted = rec.jine if rec else decimal.Decimal('0.00') - return personal_today - already_counted + jine - - -def _get_platform_daily_limit(leixing): - """ - 平台每日总限额(peizhi.TixianQuotaDefault): - 提现身份1打手→配置4,2管事→5,3组长→6,4考核官→7,5押金→8,6商家→9 - """ - quota_leixing = leixing + 3 - try: - return TixianQuotaDefault.query.get(leixing=quota_leixing).default_quota - except TixianQuotaDefault.DoesNotExist: - logger.warning( - '平台每日总限额未配置 leixing=%s(读取quota_leixing=%s),按0处理禁止新增收款', - leixing, quota_leixing, - ) - return decimal.Decimal('0.00') - - -def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''): - """个人每日限额只读校验(打手/管事/组长)""" - if leixing not in (1, 2, 3): - return True, '', '' - - today = date.today() - if leixing == 1: - try: - profile = UserDashou.query.get(user=user_main) - except UserDashou.DoesNotExist: - return False, '打手信息不存在', 'personal' - if profile.last_tixian_date != today: - personal_today = decimal.Decimal('0.00') - else: - personal_today = profile.jinritixian_jine - elif leixing == 2: - try: - profile = UserGuanshi.query.get(user=user_main) - except UserGuanshi.DoesNotExist: - return False, '管事信息不存在', 'personal' - if profile.last_tixian_date != today: - personal_today = decimal.Decimal('0.00') - else: - personal_today = profile.jinritixian_jine - else: - try: - profile = UserZuzhang.query.get(user=user_main) - except UserZuzhang.DoesNotExist: - return False, '组长信息不存在', 'personal' - if profile.last_tixian_time and profile.last_tixian_time.date() == today: - personal_today = profile.jinri_tixian - else: - personal_today = decimal.Decimal('0.00') - - personal_quota = _get_personal_daily_quota(leixing, profile) - projected_personal = _projected_personal_total(personal_today, jine, shenhe_danhao) - if projected_personal > personal_quota: - return ( - False, - f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}元', - 'personal', - ) - return True, '', '' - - -def _check_platform_quota_readonly(leixing, shijidaozhang, shenhe_danhao=''): - """平台每日总限额只读校验:今日已用 + 本笔到账 > 总限额 则拒绝""" - platform_limit = _get_platform_daily_limit(leixing) - shijidaozhang = shijidaozhang or decimal.Decimal('0.00') - - if platform_limit <= 0 and shijidaozhang > 0: - return False, '今日该角色提现已关闭(总限额为0),暂无法收款', 'platform' - - daily_used, projected, additional = _calc_platform_projected( - leixing, shijidaozhang, shenhe_danhao, - ) - if projected > platform_limit: - logger.warning( - '平台总限额拦截 leixing=%s shenhe_danhao=%s daily_used=%s additional=%s ' - 'projected=%s limit=%s', - leixing, shenhe_danhao, daily_used, additional, projected, platform_limit, - ) - return ( - False, - f'今日该角色提现总额已达上限,今日已提{daily_used}元,本次到账{additional}元,限额{platform_limit}元', - 'platform', - ) - - return True, '', '' - - -def check_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''): - """ - 收款限额只读校验(复用待确认打款单等场景,不写入)。 - 返回 (True, '', '') 或 (False, msg, 'personal'|'platform') - """ - ok, msg, kind = _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao) - if not ok: - return False, msg, kind - return _check_platform_quota_readonly(leixing, shijidaozhang, shenhe_danhao) - - -def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''): - """ - 收款前原子预占每日限额(必须在 transaction.atomic 内调用)。 - 锁定 TixianRiTongji + 用户行,校验通过后立即写入统计,防止高并发超额。 - 本审核单已有进行中/成功打款记录则视为已预占,不重复加计。 - 返回 (True, '', '') 或 (False, msg, 'personal'|'platform') - """ - - today = date.today() - shijidaozhang = shijidaozhang or decimal.Decimal('0.00') - - if _get_active_auto_record(shenhe_danhao): - return True, '', '' - - personal_profile = None - if leixing in (1, 2, 3): - if leixing == 1: - personal_profile = UserDashou.objects.select_for_update().get(user=user_main) - if personal_profile.last_tixian_date != today: - personal_profile.jinritixian_jine = decimal.Decimal('0.00') - personal_profile.last_tixian_date = today - personal_today = personal_profile.jinritixian_jine - elif leixing == 2: - personal_profile = UserGuanshi.objects.select_for_update().get(user=user_main) - if personal_profile.last_tixian_date != today: - personal_profile.jinritixian_jine = decimal.Decimal('0.00') - personal_profile.last_tixian_date = today - personal_today = personal_profile.jinritixian_jine - else: - personal_profile = UserZuzhang.objects.select_for_update().get(user=user_main) - if personal_profile.last_tixian_time and personal_profile.last_tixian_time.date() != today: - personal_profile.jinri_tixian = decimal.Decimal('0.00') - elif not personal_profile.last_tixian_time: - personal_profile.jinri_tixian = decimal.Decimal('0.00') - personal_today = personal_profile.jinri_tixian - - personal_quota = _get_personal_daily_quota(leixing, personal_profile) - if personal_today + jine > personal_quota: - return ( - False, - f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}元', - 'personal', - ) - - platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create( - riqi=today, - leixing=leixing, - defaults={'total_amount': decimal.Decimal('0.00')}, - ) - platform_limit = _get_platform_daily_limit(leixing) - if platform_limit <= 0 and shijidaozhang > 0: - return False, '今日该角色提现已关闭(总限额为0),暂无法收款', 'platform' - if platform_stat.total_amount + shijidaozhang > platform_limit: - return ( - False, - f'今日该角色提现总额已达上限,今日已提{platform_stat.total_amount}元,' - f'本次到账{shijidaozhang}元,限额{platform_limit}元', - 'platform', - ) - - if personal_profile is not None: - if leixing == 1: - personal_profile.jinritixian_jine += jine - personal_profile.save(update_fields=['jinritixian_jine', 'last_tixian_date']) - elif leixing == 2: - personal_profile.jinritixian_jine += jine - personal_profile.save(update_fields=['jinritixian_jine', 'last_tixian_date']) - else: - personal_profile.jinri_tixian += jine - personal_profile.last_tixian_time = timezone.now() - personal_profile.save(update_fields=['jinri_tixian', 'last_tixian_time']) - - platform_stat.total_amount += shijidaozhang - platform_stat.save(update_fields=['total_amount']) - - logger.info( - '收款限额预占成功 yonghuid=%s leixing=%s shenhe_danhao=%s jine=%s shijidaozhang=%s platform_total=%s', - user_main.yonghuid, leixing, shenhe_danhao, jine, shijidaozhang, platform_stat.total_amount, - ) - return True, '', '' - - -def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang): - """微信打款明确失败/用户取消时回滚收款预占的限额""" - today = date.today() - shijidaozhang = shijidaozhang or decimal.Decimal('0.00') - try: - with transaction.atomic(): - if leixing == 1: - dashou = UserDashou.objects.select_for_update().get(user=user_main) - if dashou.last_tixian_date == today and dashou.jinritixian_jine >= jine: - dashou.jinritixian_jine -= jine - dashou.save(update_fields=['jinritixian_jine']) - elif leixing == 2: - guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) - if guanshi.last_tixian_date == today and guanshi.jinritixian_jine >= jine: - guanshi.jinritixian_jine -= jine - guanshi.save(update_fields=['jinritixian_jine']) - elif leixing == 3: - zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) - if zuzhang.jinri_tixian >= jine: - zuzhang.jinri_tixian -= jine - zuzhang.save(update_fields=['jinri_tixian']) - - try: - stat = TixianRiTongji.objects.select_for_update().get( - riqi=today, leixing=leixing, - ) - stat.total_amount = max( - stat.total_amount - shijidaozhang, - decimal.Decimal('0.00'), - ) - stat.save(update_fields=['total_amount']) - except TixianRiTongji.DoesNotExist: - pass - except Exception as e: - logger.error( - '回滚收款限额预占失败 yonghuid=%s leixing=%s err=%s', - user_main.yonghuid, leixing, e, exc_info=True, - ) - return - - logger.info( - '收款限额预占已回滚 yonghuid=%s leixing=%s jine=%s shijidaozhang=%s', - user_main.yonghuid, leixing, jine, shijidaozhang, - ) - - -def release_collect_quota_for_record(auto_record): - """幂等回滚单笔打款对应的收款预占(查单/回调/取消等多路径共用,防重复扣减)""" - if not auto_record.shenhe_danhao: - return - if not _quota_already_reserved_for_record(auto_record): - return - try: - user_main = User.query.get(yonghuid=auto_record.yonghuid) - except User.DoesNotExist: - logger.error('回滚限额预占失败:用户不存在 %s', auto_record.yonghuid) - return - release_collect_quota_reservation( - user_main, - auto_record.leixing, - auto_record.jine, - auto_record.shijidaozhang, - ) - - -def _quota_already_reserved_for_record(auto_record): - """判断本笔打款是否已在收款预占时写入日统计(避免成功时重复累加)""" - if not auto_record.shenhe_danhao: - return False - - today = date.today() - shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00') - stat_total = _get_platform_daily_total(auto_record.leixing, today) - other_sum = TixianAutoRecord.query.filter( - leixing=auto_record.leixing, - zhuangtai__in=[0, 1], - create_time__date=today, - ).exclude(tixian_id=auto_record.tixian_id).aggregate( - total=Sum('shijidaozhang'), - )['total'] or decimal.Decimal('0.00') - return stat_total >= other_sum + shijidaozhang - - -def apply_transfer_success_quota(auto_record): - """ - 打款成功时累加每日限额统计(仅自动审核流程 shenhe_danhao 非空)。 - - houtai.TixianRiTongji.total_amount += 实际到账 - - 用户扩展表个人今日已提 += 申请金额(打手/管事/组长) - 须在 transaction.atomic 内、且打款记录 zhuangtai 由 0→1 时调用一次。 - """ - if not auto_record.shenhe_danhao: - return - today = date.today() - leixing = auto_record.leixing - jine = auto_record.jine - shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00') - - try: - user_main = User.query.get(yonghuid=auto_record.yonghuid) - except User.DoesNotExist: - logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid) - return - - platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create( - riqi=today, - leixing=leixing, - defaults={'total_amount': decimal.Decimal('0.00')}, - ) - platform_stat.total_amount += shijidaozhang - platform_stat.save(update_fields=['total_amount']) - - if leixing == 1: - dashou = UserDashou.objects.select_for_update().get(user=user_main) - if dashou.last_tixian_date != today: - dashou.jinritixian_jine = decimal.Decimal('0.00') - dashou.last_tixian_date = today - dashou.jinritixian_jine += jine - dashou.save(update_fields=['jinritixian_jine', 'last_tixian_date']) - elif leixing == 2: - guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) - if guanshi.last_tixian_date != today: - guanshi.jinritixian_jine = decimal.Decimal('0.00') - guanshi.last_tixian_date = today - guanshi.jinritixian_jine += jine - guanshi.save(update_fields=['jinritixian_jine', 'last_tixian_date']) - elif leixing == 3: - zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) - if zuzhang.last_tixian_time and zuzhang.last_tixian_time.date() != today: - zuzhang.jinri_tixian = decimal.Decimal('0.00') - elif not zuzhang.last_tixian_time: - zuzhang.jinri_tixian = decimal.Decimal('0.00') - zuzhang.jinri_tixian += jine - zuzhang.last_tixian_time = timezone.now() - zuzhang.save(update_fields=['jinri_tixian', 'last_tixian_time']) - - logger.info( - '打款成功已累加限额 bill=%s yonghuid=%s leixing=%s jine=%s shijidaozhang=%s ' - 'platform_total=%s', - auto_record.tixian_id, auto_record.yonghuid, leixing, jine, shijidaozhang, - platform_stat.total_amount, - ) - - -def refund_balance(user_main, leixing, jine): - """审核拒绝时退还可到账金额 shijidaozhang(须在 transaction.atomic 内;手续费已在申请扣款时扣除不退还)""" - if leixing == 1: - dashou = UserDashou.objects.select_for_update().get(user=user_main) - dashou.yue += jine - dashou.save() - elif leixing == 2: - guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) - guanshi.yue += jine - guanshi.save() - elif leixing == 3: - zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) - zuzhang.ketixian_jine += jine - zuzhang.save() - elif leixing == 4: - kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main) - kaoheguan.yue += jine - kaoheguan.save() - elif leixing == 5: - dashou = UserDashou.objects.select_for_update().get(user=user_main) - dashou.yajin += jine - dashou.zhanghaozhuangtai = 1 - dashou.save() - elif leixing == 6: - shangjia = UserShangjia.objects.select_for_update().get(user=user_main) - shangjia.yue += jine - shangjia.save() - else: - raise ValueError('无效的提现类型') - - -def deduct_balance(user_main, leixing, jine): - """扣减申请金额 jine(必须在 transaction.atomic 内调用)""" - if leixing == 1: - dashou = UserDashou.objects.select_for_update().get(user=user_main) - if dashou.yue < jine: - raise ValueError('余额不足') - dashou.yue -= jine - dashou.save() - return dashou.nicheng or '' - if leixing == 2: - guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) - if guanshi.yue < jine: - raise ValueError('余额不足') - guanshi.yue -= jine - guanshi.save() - return '管事用户' - if leixing == 3: - zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) - if zuzhang.ketixian_jine < jine: - raise ValueError('可提现金额不足') - zuzhang.ketixian_jine -= jine - zuzhang.save() - return '' - if leixing == 4: - kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main) - if kaoheguan.yue < jine: - raise ValueError('可提现金额不足') - kaoheguan.yue -= jine - kaoheguan.save() - return '考核官' - if leixing == 5: - dashou = UserDashou.objects.select_for_update().get(user=user_main) - if dashou.yajin < jine: - raise ValueError('押金余额不足') - dashou.yajin -= jine - dashou.zhanghaozhuangtai = 0 - dashou.save() - return dashou.nicheng or '' - if leixing == 6: - shangjia = UserShangjia.objects.select_for_update().get(user=user_main) - if shangjia.yue < jine: - raise ValueError('商家余额不足') - shangjia.yue -= jine - shangjia.save() - return shangjia.nicheng or '' - raise ValueError('无效的提现类型') - - -def get_balance_response_data(user_main, leixing): - data = {} - if leixing == 1: - try: - dashou = UserDashou.query.get(user=user_main) - data['new_yongjin'] = str(dashou.yue.quantize(decimal.Decimal('0.01'))) - except UserDashou.DoesNotExist: - pass - elif leixing == 2: - try: - guanshi = UserGuanshi.query.get(user=user_main) - data['new_fenyong'] = str(guanshi.yue.quantize(decimal.Decimal('0.01'))) - except UserGuanshi.DoesNotExist: - pass - elif leixing == 3: - try: - zuzhang = UserZuzhang.query.get(user=user_main) - data['new_zuzhang_ketixian'] = str(zuzhang.ketixian_jine.quantize(decimal.Decimal('0.01'))) - except UserZuzhang.DoesNotExist: - pass - elif leixing == 4: - try: - kg = UserShenheguan.query.get(user=user_main) - data['new_kaoheguan_yue'] = str(kg.yue.quantize(decimal.Decimal('0.01'))) - except UserShenheguan.DoesNotExist: - pass - return data - - -def sync_audit_and_jilu_status(audit, new_status, **extra_fields): - audit.zhuangtai = new_status - for k, v in extra_fields.items(): - setattr(audit, k, v) - audit.save() - if audit.tixianjilu_id: - update_fields = {'zhuangtai': new_status, 'update_time': timezone.now()} - if 'bhliyou' in extra_fields: - update_fields['bhliyou'] = extra_fields['bhliyou'] - Tixianjilu.query.filter(id=audit.tixianjilu_id).update(**update_fields) - - -def _close_auto_record_failed(auto_record, fail_reason): - """标记打款记录失败并释放收款预占限额""" - if auto_record is None or auto_record.zhuangtai == 1: - return - reason = (fail_reason or '打款失败')[:500] - auto_record.zhuangtai = 2 - auto_record.fail_reason = reason - auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) - release_collect_quota_for_record(auto_record) - - -def close_audit_wechat_failed(audit, auto_record=None, fail_reason='微信转账失败'): - """ - 微信官方终态 FAIL/CANCELLED:审核单关闭(3),不退款(申请时已扣款,手续费不退)。 - 须在 transaction.atomic 内调用。 - """ - _close_auto_record_failed(auto_record, fail_reason) - - audit = TixianShenheJilu.objects.select_for_update().get(pk=audit.pk) - if audit.zhuangtai in (2, 3, 5): - return False - - reason = (fail_reason or '微信转账失败')[:500] - sync_audit_and_jilu_status( - audit, 3, - bhliyou=reason, - fail_reason=reason, - tixian_auto_id=None, - ) - logger.warning( - '微信失败关单(不退款) yonghuid=%s shenhe_danhao=%s reason=%s', - audit.yonghuid, audit.shenhe_danhao, reason, - ) - return True - - -def close_audit_over_limit_refund(audit, auto_record=None, fail_reason='单笔实际到账超过限额'): - """ - 到账超过200元等:审核单关闭(3) + 退还可到账金额 shijidaozhang(手续费不退)。 - 须在 transaction.atomic 内调用。 - """ - reason = (fail_reason or '单笔实际到账超过限额')[:500] - _close_auto_record_failed(auto_record, reason) - - audit = TixianShenheJilu.objects.select_for_update().get(pk=audit.pk) - if audit.zhuangtai in (2, 3, 5): - return False - - user_main = User.objects.select_for_update().get(yonghuid=audit.yonghuid) - refund_balance(user_main, audit.leixing, audit.shijidaozhang) - sync_audit_and_jilu_status( - audit, 3, - bhliyou=reason, - fail_reason=reason, - tixian_auto_id=None, - ) - logger.warning( - '超限关单并退款 yonghuid=%s shenhe_danhao=%s reason=%s', - audit.yonghuid, audit.shenhe_danhao, reason, - ) - return True - - -def query_wechat_transfer_bill(out_bill_no): - """ - 向微信查转账单状态 - GET /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no} - 返回 dict;网络/系统异常返回 None;微信无此单返回 {'_not_found': True} - """ - - url = ( - 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/' - f'transfer-bills/out-bill-no/{out_bill_no}' - ) - try: - auth = build_authorization('GET', url, '') - headers = { - 'Authorization': auth, - 'Accept': 'application/json', - 'User-Agent': 'Mozilla/5.0', - } - resp = requests.get(url, headers=headers, timeout=10) - if resp.status_code == 200: - return resp.json() - if resp.status_code == 404: - return {'_not_found': True} - logger.error( - '微信查单失败 bill=%s HTTP%s body=%s', - out_bill_no, resp.status_code, resp.text[:300], - ) - return None - except (requests.RequestException, OSError, ValueError) as e: - logger.error('微信查单异常 bill=%s err=%s', out_bill_no, e, exc_info=True) - return None - except Exception as e: - logger.error('微信查单未知异常 bill=%s err=%s', out_bill_no, e, exc_info=True) - return None - - -def _parse_package_info(raw): - if not raw: - return None - if isinstance(raw, dict): - return raw - try: - return json.loads(raw) - except (TypeError, ValueError): - return None - - -def _apply_platform_accounting(auto_record): - """平台收支统计(幂等由 mark_transfer_success 外层保证只调一次)""" - - update_daily_payout(auto_record.shijidaozhang) - try: - szjilu = Szjilu.objects.select_for_update().get(id=1) - szjilu.zongsy -= auto_record.shijidaozhang - szjilu.zongzc += auto_record.shijidaozhang - szjilu.jrzc += auto_record.shijidaozhang - szjilu.save() - except Szjilu.DoesNotExist: - Szjilu.query.create( - id=1, - zongsy=-auto_record.shijidaozhang, - zongzc=auto_record.shijidaozhang, - jrzc=auto_record.shijidaozhang, - ) - - -def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounting=True): - """ - 打款成功落库(tixianqr / callback / 查单对账 共用) - select_for_update + zhuangtai==0 才更新并记账,防双写 - 返回 True=本次新标记成功;False=已处理过 - """ - with transaction.atomic(): - ar = TixianAutoRecord.objects.select_for_update().get(pk=auto_record.pk) - if ar.zhuangtai != 0: - return False - - ar.zhuangtai = 1 - update_fields = ['zhuangtai', 'update_time'] - if wechat_transfer_no: - ar.wechat_transfer_no = wechat_transfer_no - update_fields.append('wechat_transfer_no') - ar.save(update_fields=update_fields) - - if ar.shenhe_danhao: - try: - audit = TixianShenheJilu.objects.select_for_update().get( - shenhe_danhao=ar.shenhe_danhao, - ) - if audit.zhuangtai != 2: - sync_audit_and_jilu_status(audit, 2) - except TixianShenheJilu.DoesNotExist: - logger.warning('打款成功但审核记录不存在: %s', ar.shenhe_danhao) - - if ar.shenhe_danhao and not _quota_already_reserved_for_record(ar): - apply_transfer_success_quota(ar) - - if with_accounting: - _apply_platform_accounting(ar) - - return True - - -def _sync_record_from_wx_query(auto_record, wx_data): - """根据微信查单结果修正本地打款记录,返回 (action, payload)""" - if wx_data.get('_not_found'): - age = timezone.now() - auto_record.create_time - if auto_record.zhuangtai == 0 and age < timedelta(minutes=PENDING_QUERY_GRACE_MINUTES): - return RECONCILE_PENDING, { - 'msg': '有收款处理中,请稍后再试', - 'tixian_id': auto_record.tixian_id, - } - if auto_record.zhuangtai == 0: - auto_record.zhuangtai = 2 - auto_record.fail_reason = '微信未找到该打款单,请重新发起收款' - auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) - release_collect_quota_for_record(auto_record) - return RECONCILE_ALLOW_NEW, {} - - state = (wx_data.get('state') or '').upper() - - if state in WX_STATE_SUCCESS: - mark_transfer_success( - auto_record, - wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'), - ) - return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} - - if state in WX_STATE_WAIT: - package = wx_data.get('package_info') or _parse_package_info(auto_record.wechat_package) - if not package: - return RECONCILE_PENDING, { - 'msg': '有收款处理中,请稍后再试', - 'tixian_id': auto_record.tixian_id, - } - if auto_record.zhuangtai == 0: - auto_record.wechat_package = json.dumps(package, ensure_ascii=False) - auto_record.save(update_fields=['wechat_package', 'update_time']) - wx_cfg = settings.WECHAT_PAY_V3_CONFIG - return RECONCILE_WAIT_CONFIRM, { - 'tixian_id': auto_record.tixian_id, - 'package_info': package, - 'shouxufei': str(auto_record.shouxufei), - 'shijidaozhang': str(auto_record.shijidaozhang), - 'current_rate': str(auto_record.feilv), - 'mch_id': wx_cfg.get('MCHID', ''), - 'shenhe_danhao': auto_record.shenhe_danhao or '', - } - - if state in WX_STATE_CANCELING: - return RECONCILE_PENDING, { - 'msg': '收款撤销处理中,请稍后再试', - 'tixian_id': auto_record.tixian_id, - } - - if state in WX_STATE_FAIL: - fail_reason = ( - wx_data.get('fail_reason') or wx_data.get('close_reason') or state - )[:500] - if auto_record.zhuangtai != 1: - auto_record.zhuangtai = 2 - auto_record.fail_reason = fail_reason - auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) - release_collect_quota_for_record(auto_record) - if auto_record.shenhe_danhao: - try: - audit = TixianShenheJilu.query.get(shenhe_danhao=auto_record.shenhe_danhao) - with transaction.atomic(): - close_audit_wechat_failed(audit, None, fail_reason) - except TixianShenheJilu.DoesNotExist: - logger.warning('微信失败但审核记录不存在: %s', auto_record.shenhe_danhao) - return RECONCILE_AUDIT_CLOSED, { - 'msg': '微信已确认转账失败,该笔提现已关闭,请联系客服处理', - } - - if auto_record.zhuangtai == 0: - return RECONCILE_PENDING, { - 'msg': f'收款处理中(微信状态:{state or "未知"}),请稍后再试', - 'tixian_id': auto_record.tixian_id, - } - return RECONCILE_ALLOW_NEW, {} - - -def get_auto_record_for_collect(shenhe_danhao): - """ - 取本笔提现申请应对接微信的那条打款记录: - 优先 zhuangtai=0(进行中),否则取最新一条(可能本地误标失败但微信未终态) - """ - pending = ( - TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao, zhuangtai=0) - .order_by('-create_time') - .first() - ) - if pending: - return pending - return ( - TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao) - .order_by('-create_time') - .first() - ) - - -def reconcile_shenhe_wechat_bills(shenhe_danhao): - """ - P0 防双笔:同一 shenhe_danhao 下所有打款记录逐条问微信 - 任一 SUCCESS / 进行中 → 禁止新建;全部终态失败才 ALLOW_NEW - 返回 (action, payload) - """ - records = list( - TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao).order_by('-create_time') - ) - - if any(r.zhuangtai == 1 for r in records): - try: - audit = TixianShenheJilu.query.get(shenhe_danhao=shenhe_danhao) - if audit.zhuangtai != 2: - sync_audit_and_jilu_status(audit, 2) - except TixianShenheJilu.DoesNotExist: - pass - return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} - - if not records: - return RECONCILE_ALLOW_NEW, {} - - pending_hit = None - for rec in records: - if rec.zhuangtai == 1: - return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} - - wx_data = query_wechat_transfer_bill(rec.tixian_id) - if wx_data is None: - logger.error( - '对账查单不可用 shenhe_danhao=%s bill=%s', - shenhe_danhao, rec.tixian_id, - ) - return RECONCILE_PENDING, { - 'msg': '有收款处理中,系统正在核对微信状态,请稍后再试', - 'tixian_id': rec.tixian_id, - } - - action, payload = _sync_record_from_wx_query(rec, wx_data) - if action == RECONCILE_COMPLETED: - return action, payload - if action == RECONCILE_WAIT_CONFIRM: - return action, payload - if action == RECONCILE_AUDIT_CLOSED: - return action, payload - if action == RECONCILE_PENDING: - if pending_hit is None: - pending_hit = (action, payload) - continue - - if pending_hit: - return pending_hit - return RECONCILE_ALLOW_NEW, {} - - -def resolve_collect_context(yonghuid, tixianjilu_id=None, shenhe_danhao='', shenhe_jilu_id=None): - """ - 从提现记录 ID 进门(P0 收款主路径): - Tixianjilu → TixianShenheJilu,校验归属、状态=6 待收款 - 返回 ({jilu, audit}, err_msg) - """ - if tixianjilu_id is None and not shenhe_danhao: - return None, '缺少提现记录ID' - - jilu = None - if tixianjilu_id is not None: - try: - jilu = Tixianjilu.query.get(id=int(tixianjilu_id), yonghuid=yonghuid) - except (Tixianjilu.DoesNotExist, TypeError, ValueError): - return None, '提现记录不存在或不属于当前用户' - - if jilu.zhuangtai != 6: - return None, f'当前状态不可收款(状态{jilu.zhuangtai}),请等待审核通过' - if not jilu.shenhe_jilu_id or not jilu.shenhe_danhao: - return None, '提现记录未关联审核单,无法收款' - - if shenhe_jilu_id is not None: - try: - if int(shenhe_jilu_id) != jilu.shenhe_jilu_id: - return None, '审核记录ID与提现记录不匹配' - except (TypeError, ValueError): - return None, '审核记录ID格式错误' - - if shenhe_danhao and shenhe_danhao != jilu.shenhe_danhao: - return None, '审核单号与提现记录不匹配' - - shenhe_danhao = jilu.shenhe_danhao - shenhe_jilu_id = jilu.shenhe_jilu_id - - try: - if shenhe_jilu_id: - audit = TixianShenheJilu.query.get( - id=int(shenhe_jilu_id), - shenhe_danhao=shenhe_danhao, - yonghuid=yonghuid, - ) - else: - audit = TixianShenheJilu.query.get( - shenhe_danhao=shenhe_danhao, - yonghuid=yonghuid, - ) - except (TixianShenheJilu.DoesNotExist, TypeError, ValueError): - return None, '审核记录不存在' - - if audit.zhuangtai == 2: - return None, '该提现已完成,请勿重复操作' - if audit.zhuangtai != 6: - return None, f'当前状态不可收款(状态{audit.zhuangtai}),请等待审核通过' - if not is_auto_dakuan_audit(audit): - return None, '该提现单为手动打款,无法使用自动收款' - - if jilu is None: - try: - jilu = Tixianjilu.query.get( - id=audit.tixianjilu_id, - yonghuid=yonghuid, - ) if audit.tixianjilu_id else None - except Tixianjilu.DoesNotExist: - jilu = None - - return {'jilu': jilu, 'audit': audit, 'shenhe_danhao': shenhe_danhao}, '' - - -def handle_post_transfer_failure(tixian_id, shenhe_danhao, err_code='', err_msg=''): - """ - 发起转账 HTTP 非成功时:先查微信再决定是否标失败(P0,禁止盲目标失败导致双笔) - 返回 (action, payload_or_user_msg) - """ - try: - ar = TixianAutoRecord.query.get(tixian_id=tixian_id) - except TixianAutoRecord.DoesNotExist: - return RECONCILE_PENDING, '收款处理中,请稍后再试' - - wx_data = query_wechat_transfer_bill(tixian_id) - if wx_data is None: - logger.error( - '发起转账失败且查单不可用 bill=%s code=%s msg=%s,保持待查单', - tixian_id, err_code, err_msg, - ) - return RECONCILE_PENDING, '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击' - - action, payload = _sync_record_from_wx_query(ar, wx_data) - if action == RECONCILE_ALLOW_NEW: - return RECONCILE_ALLOW_NEW, payload - if action == RECONCILE_WAIT_CONFIRM: - return RECONCILE_WAIT_CONFIRM, payload - if action == RECONCILE_COMPLETED: - return RECONCILE_COMPLETED, payload - return RECONCILE_PENDING, payload.get('msg', '收款处理中,请稍后再试') - - -def get_audit_for_collect(shenhe_danhao, yonghuid, shenhe_jilu_id=None): - """ - 收款前数据库校验(须在 transaction.atomic 内调用;微信查单在事务外单独做) - """ - try: - audit = TixianShenheJilu.objects.select_for_update().get( - shenhe_danhao=shenhe_danhao, - yonghuid=yonghuid, - ) - except TixianShenheJilu.DoesNotExist: - return None, '审核记录不存在' - - if shenhe_jilu_id is not None: - try: - if int(shenhe_jilu_id) != audit.id: - return None, '审核记录ID不匹配' - except (TypeError, ValueError): - return None, '审核记录ID格式错误' - - if audit.zhuangtai == 2: - return None, '该提现已完成,请勿重复操作' - - if audit.zhuangtai != 6: - return None, f'当前状态不可收款(状态{audit.zhuangtai}),请等待审核通过' - - if not is_auto_dakuan_audit(audit): - return None, '该提现单为手动打款,无法使用自动收款' - - return audit, '' - - -def create_audit_application(user_main, leixing, jine): - """ - 提交审核申请:校验 → 扣款 → 写审核表 + 提现记录表 - 不创建打款记录表(TixianAutoRecord 仅在收款时创建) - 允许多笔并行申请(不阻塞待收款/审核中单) - """ - yonghuid = user_main.yonghuid - - ok, msg, extra = validate_withdraw_eligibility(user_main, leixing, jine) - if not ok: - raise ValueError(msg) - - feilv = get_tixian_feilv(leixing) - shouxufei, shijidaozhang = calc_fee_amounts(jine, feilv) - if shijidaozhang <= 0: - raise ValueError('提现金额过低,扣除手续费后无实际到账') - - ok, limit_msg = check_shijidaozhang_within_limit(shijidaozhang) - if not ok: - raise ValueError(limit_msg) - - shenhe_danhao = generate_shenhe_danhao() - nicheng = extra.get('nicheng', '') - - with transaction.atomic(): - nicheng = deduct_balance(user_main, leixing, jine) or nicheng - - audit_kwargs = { - 'shenhe_danhao': shenhe_danhao, - 'yonghuid': yonghuid, - 'leixing': leixing, - 'zhuangtai': 1, - 'shenqing_jine': jine, - 'shouxufei': shouxufei, - 'shijidaozhang': shijidaozhang, - 'feilv': feilv, - } - if has_dakuan_mode_field(): - audit_kwargs['dakuan_mode'] = DAKUAN_MODE_AUTO - audit = TixianShenheJilu.query.create(**audit_kwargs) - - tixian_record = Tixianjilu.query.create( - yonghuid=yonghuid, - avatar=user_main.avatar or '', - phone=user_main.phone or '', - nicheng=nicheng, - leixing=leixing, - zhifu=user_main.zhifu or '', - skzhanghao=user_main.skzhanghao or '', - jine=shijidaozhang, - zhuangtai=1, - fangshi=1, - shenheid=None, - bhliyou='', - shenhe_jilu_id=audit.id, - shenhe_danhao=shenhe_danhao, - ) - - audit.tixianjilu_id = tixian_record.id - audit.save(update_fields=['tixianjilu_id']) - - response_data = get_balance_response_data(user_main, leixing) - response_data.update({ - 'shenhe_danhao': shenhe_danhao, - 'shenhe_jilu_id': audit.id, - 'tixianjilu_id': tixian_record.id, - 'shouxufei': str(shouxufei), - 'shijidaozhang': str(shijidaozhang), - 'current_rate': str(feilv.quantize(decimal.Decimal('0.0001'))), - 'leixing': leixing, - 'zhuangtai': 1, - }) - return response_data +# yonghu/tixian_shenhe_services.py +""" +自动提现审核流程业务服务层(仅服务于 zddksh 申请接口 与 tixiansq 收款接口) + +流程: + ① zddksh 提交审核:完整资格校验 → 扣款 → 写审核表+提现记录表(状态=1) + 允许多笔并行申请;单笔实际到账(shijidaozhang)不得超过200元 + ② 后台审核通过:校验到账≤200 → 状态 6(待收款) + ③ tixiansq 收款:严格资格+限额 → 到账>200则关单退款(不退手续费),不调微信 + 微信官方 FAIL 关单不退款;SUCCESS 落库;进行中禁止新建打款单 + ④ 打款成功(tixianqr/回调/查单):若收款前已预占则不再累加;未预占的旧单/查单直成则补记 + +状态码(与前端 TX_STATUS 一致): + 1=审核中 2=提现成功 3=提现失败 4=审核成功 5=审核失败 6=待收款 +""" +import decimal +import json +import logging +import random +import time +from datetime import date, timedelta + +import requests +from django.conf import settings +from django.db import transaction +from django.utils import timezone + +from orders.models import Dingdan, DingdanShangjia, Fadan, Lilubiao +from config.models import Szjilu, TixianQuotaDefault + +from utils.fadan_utils import check_fadan_qiangdan_eligible + +from products.models import Huiyuangoumai +from backend.models import TixianRiTongji + +from utils.wechat_v3 import build_authorization + +from .models import ( + TixianAutoRecord, + TixianShenheJilu, + Tixianjilu, + UserDashou, + UserGuanshi, + UserShenheguan, + UserShangjia, + UserZuzhang, +) +from gvsdsdk.models import User + +from orders.utils import update_daily_payout + +from django.db.models import Sum + +from gvsdsdk.fluent import db, func, FQ + +logger = logging.getLogger('yonghu.tixian_shenhe') + +ORDER_STATUS_COMPLETED = 3 +ORDER_STATUS_REFUNDED = 5 + +LEIXING_RATE_KEY = { + 1: '5', + 2: '6', + 3: '8', + 4: '9', + 5: '11', + 6: '10', +} + +DAKUAN_MODE_MANUAL = 1 +DAKUAN_MODE_AUTO = 2 + +# 微信转账单状态(查单接口返回 state 字段) +WX_STATE_SUCCESS = ('SUCCESS',) +WX_STATE_WAIT = ('WAIT_USER_CONFIRM', 'TRANSFERING', 'ACCEPTED', 'PROCESSING') +WX_STATE_FAIL = ('FAIL', 'CLOSED', 'CANCELLED') +WX_STATE_CANCELING = ('CANCELING',) # 撤销中,非终态,禁止换新单号 + +RECONCILE_COMPLETED = 'completed' +RECONCILE_WAIT_CONFIRM = 'wait_confirm' +RECONCILE_ALLOW_NEW = 'allow_new' +RECONCILE_PENDING = 'pending_query' +RECONCILE_AUDIT_CLOSED = 'audit_closed' + +# 单笔实际到账上限(仅收款时校验) +MAX_SINGLE_COLLECT_AMOUNT = decimal.Decimal('200') + +# 查单失败且本地 zhuangtai=0 超过该分钟数,才允许新建打款记录 +PENDING_QUERY_GRACE_MINUTES = 10 + + +def has_dakuan_mode_field(): + """生产库未跑 0032 迁移时模型无此字段,需兼容""" + try: + TixianShenheJilu._meta.get_field('dakuan_mode') + return True + except Exception: + return False + + +def is_auto_dakuan_audit(audit): + """判断是否自动打款审核单;无 dakuan_mode 属性时,有审核记录即视为 zddksh 自动流程""" + if audit is None: + return False + mode = getattr(audit, 'dakuan_mode', None) + if mode is None: + return True + try: + return int(mode) == DAKUAN_MODE_AUTO + except (TypeError, ValueError): + return True + + +def load_audit_dakuan_mode_map(audit_ids): + """批量取审核单打款方式;字段不存在时有关联审核即视为自动(2)""" + return { + aid: meta.get('dakuan_mode', DAKUAN_MODE_AUTO) + for aid, meta in load_audit_meta_map(audit_ids).items() + } + + +def load_audit_meta_map(audit_ids): + """ + 批量取审核表元数据(供后台列表等) + 返回 {审核记录id: {'dakuan_mode': int, 'shenhe_danhao': str}} + """ + if not audit_ids: + return {} + value_fields = ['id', 'shenhe_danhao'] + if has_dakuan_mode_field(): + value_fields.append('dakuan_mode') + result = {} + for row in TixianShenheJilu.query.filter(id__in=audit_ids).values(*value_fields): + result[row['id']] = { + 'shenhe_danhao': row.get('shenhe_danhao') or '', + 'dakuan_mode': row.get('dakuan_mode', DAKUAN_MODE_AUTO), + } + return result + + +def generate_shenhe_danhao(): + """生成唯一审核单号:TXSH + 时间戳后10位 + 4位随机数""" + for _ in range(10): + timestamp = str(int(time.time() * 1000))[-10:] + random_part = str(random.randint(1000, 9999)) + danhao = f'TXSH{timestamp}{random_part}' + if not TixianShenheJilu.query.filter(shenhe_danhao=danhao).exists(): + return danhao + raise RuntimeError('生成审核单号失败,请重试') + + +def get_tixian_feilv(leixing): + rate_key = LEIXING_RATE_KEY.get(leixing) + if not rate_key: + raise ValueError('无效的提现类型') + rate_obj = Lilubiao.query.filter(fadanpingtai=rate_key).first() + if not rate_obj: + raise ValueError(f'提现费率未配置(类型{leixing})') + return decimal.Decimal(str(rate_obj.lilu)) + + +def calc_fee_amounts(jine, feilv): + """申请金额 jine;实际到账 = jine - 手续费""" + shouxufei = (jine * feilv).quantize(decimal.Decimal('0.01')) + shijidaozhang = (jine - shouxufei).quantize(decimal.Decimal('0.01')) + return shouxufei, shijidaozhang + + +def check_dashou_has_valid_huiyuan(yonghuid): + """ + 打手至少持有一个未过期会员(Huiyuangoumai + jiance_shifou_daoqi 同步状态) + 返回 (True, '') 或 (False, 原因) + """ + + records = Huiyuangoumai.query.filter(yonghu_id=yonghuid) + if not records.exists(): + return False, '您暂无会员记录,无法提现' + + for record in records: + is_expired = record.jiance_shifou_daoqi() + if not is_expired and record.huiyuan_zhuangtai == 1: + return True, '' + return False, '您的会员已全部过期,请先续费后再操作' + + +def validate_withdraw_eligibility(user_main, leixing, jine): + """ + 提现申请资格校验(完整基本条件;仅放宽「不限制并行申请笔数」)。 + 与手动提现 TixianShenqingView 对齐。 + """ + yonghuid = user_main.yonghuid + + fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) + if not fadan_ok: + return False, fadan_msg, {} + + if leixing == 1: + try: + dashou = UserDashou.query.get(user=user_main) + except UserDashou.DoesNotExist: + return False, '您不是打手身份,无法提现佣金', {} + if dashou.zhanghaozhuangtai != 1: + return False, '打手账号已被封禁,无法提现', {} + if dashou.zhuangtai != 1: + return False, '有订单进行中,请完成后提现', {} + if dashou.jifen != 10: + return False, '积分不足10分,请补充积分后提现', {} + huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) + if not huiyuan_ok: + return False, huiyuan_msg, {} + if dashou.yue < jine: + return False, f'余额不足,当前余额: {dashou.yue}', {} + return True, '', {'nicheng': dashou.nicheng or ''} + + if leixing == 2: + try: + guanshi = UserGuanshi.query.get(user=user_main) + except UserGuanshi.DoesNotExist: + return False, '您不是管事身份,无法提现分红', {} + if guanshi.zhuangtai != 1: + return False, '管事账号已被封禁,无法提现', {} + if guanshi.yue < jine: + return False, f'余额不足,当前余额: {guanshi.yue}', {} + return True, '', {'nicheng': '管事用户'} + + if leixing == 3: + try: + zuzhang = UserZuzhang.query.get(user=user_main) + except UserZuzhang.DoesNotExist: + return False, '您不是组长身份,无法提现组长分红', {} + if zuzhang.zhuangtai != 1: + return False, '组长账号已被封禁,无法提现', {} + if zuzhang.ketixian_jine < jine: + return False, f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}', {} + return True, '', {'nicheng': ''} + + if leixing == 4: + try: + kaoheguan = UserShenheguan.query.get(user=user_main) + except UserShenheguan.DoesNotExist: + return False, '您不是考核官身份,无法提现分佣', {} + if kaoheguan.zhuangtai != 1: + return False, '考核官账号已被封禁,无法提现', {} + if kaoheguan.yue < jine: + return False, f'考核官可提现金额不足,当前余额: {kaoheguan.yue}', {} + return True, '', {'nicheng': '考核官'} + + if leixing == 5: + try: + dashou = UserDashou.query.get(user=user_main) + except UserDashou.DoesNotExist: + return False, '您不是打手身份,无法提取押金', {} + if dashou.zhanghaozhuangtai != 1: + return False, '打手账号已禁用,无法提取押金', {} + if dashou.zhuangtai != 1: + return False, '您有订单进行中,请完成后提取押金', {} + if dashou.jifen != 10: + return False, '积分不足10分,请补充积分后提取押金', {} + huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) + if not huiyuan_ok: + return False, huiyuan_msg, {} + if dashou.yajin < jine: + return False, f'押金余额不足,当前押金: {dashou.yajin}', {} + unfinished = Dingdan.query.filter( + jiedan_dashou_id=yonghuid, + ).exclude(zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() + if unfinished: + return False, '您还有未完成或未退款的订单,请先处理完毕再提取押金', {} + self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if self_fadan: + return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} + return True, '', {'nicheng': dashou.nicheng or ''} + + if leixing == 6: + try: + shangjia = UserShangjia.query.get(user=user_main) + except UserShangjia.DoesNotExist: + return False, '您不是商家身份,无法提现商家余额', {} + if shangjia.zhuangtai != 1: + return False, '商家账号已被禁用,无法提现', {} + if shangjia.yue < jine: + return False, f'商家余额不足,当前余额: {shangjia.yue}', {} + unfinished_orders = DingdanShangjia.query.filter( + shangjia_id=yonghuid, + ).exclude(dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() + if unfinished_orders: + return False, '您还有未完成或未退款的订单,请先处理完毕再提现余额', {} + self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if self_fadan: + return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} + applied_fadan = Fadan.query.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if applied_fadan: + return False, '您发起的对其他用户的处罚尚未处理完毕,请等待完结', {} + return True, '', {'nicheng': shangjia.nicheng or ''} + + return False, '无效的提现类型', {} + + +def check_shijidaozhang_within_limit(shijidaozhang): + """校验单笔实际到账不超过上限(申请/审核/收款共用)""" + if shijidaozhang > MAX_SINGLE_COLLECT_AMOUNT: + return False, f'单笔实际到账不能超过{MAX_SINGLE_COLLECT_AMOUNT}元,请调整提现金额后重新申请' + return True, '' + + +def validate_collect_amount(audit): + """收款时校验单笔实际到账上限(超限将关单退款)""" + return check_shijidaozhang_within_limit(audit.shijidaozhang) + + +def validate_collect_eligibility(user_main, leixing): + """ + 收款前严格校验(申请时已扣款,不再校余额)。 + 打手佣金/押金:罚单、封禁、接单中、积分、会员、订单等; + 其他身份:账号状态、罚单/订单等。 + """ + yonghuid = user_main.yonghuid + + fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) + if not fadan_ok: + return False, fadan_msg + + if leixing == 1: + try: + dashou = UserDashou.query.get(user=user_main) + except UserDashou.DoesNotExist: + return False, '您不是打手身份,无法收款' + if dashou.zhanghaozhuangtai != 1: + return False, '打手账号已被封禁,无法收款' + if dashou.zhuangtai != 1: + return False, '您有订单进行中,请完成后再收款' + if dashou.jifen != 10: + return False, '积分不足10分,无法收款' + huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) + if not huiyuan_ok: + return False, huiyuan_msg + return True, '' + + if leixing == 5: + try: + dashou = UserDashou.query.get(user=user_main) + except UserDashou.DoesNotExist: + return False, '您不是打手身份,无法收款' + if dashou.zhanghaozhuangtai != 1: + return False, '打手账号已禁用,无法收款' + if dashou.zhuangtai != 1: + return False, '您有订单进行中,请完成后再收款' + if dashou.jifen != 10: + return False, '积分不足10分,无法收款' + huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) + if not huiyuan_ok: + return False, huiyuan_msg + unfinished = Dingdan.query.filter( + jiedan_dashou_id=yonghuid, + ).exclude(zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() + if unfinished: + return False, '您还有未完成或未退款的订单,无法收款' + self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if self_fadan: + return False, '您有未处理完毕的自身罚单,无法收款' + return True, '' + + if leixing == 2: + try: + guanshi = UserGuanshi.query.get(user=user_main) + except UserGuanshi.DoesNotExist: + return False, '您不是管事身份,无法收款' + if guanshi.zhuangtai != 1: + return False, '管事账号已被封禁,无法收款' + return True, '' + + if leixing == 6: + try: + shangjia = UserShangjia.query.get(user=user_main) + except UserShangjia.DoesNotExist: + return False, '您不是商家身份,无法收款' + if shangjia.zhuangtai != 1: + return False, '商家账号已被禁用,无法收款' + unfinished_orders = DingdanShangjia.query.filter( + shangjia_id=yonghuid, + ).exclude(dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() + if unfinished_orders: + return False, '您还有未完成或未退款的订单,无法收款' + self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if self_fadan: + return False, '您有未处理完毕的自身罚单,无法收款' + applied_fadan = Fadan.query.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + if applied_fadan: + return False, '您发起的处罚尚未处理完毕,无法收款' + return True, '' + + if leixing == 3: + try: + zuzhang = UserZuzhang.query.get(user=user_main) + except UserZuzhang.DoesNotExist: + return False, '您不是组长身份,无法收款' + if zuzhang.zhuangtai != 1: + return False, '组长账号已被封禁,无法收款' + return True, '' + + if leixing == 4: + try: + kaoheguan = UserShenheguan.query.get(user=user_main) + except UserShenheguan.DoesNotExist: + return False, '您不是考核官身份,无法收款' + if kaoheguan.zhuangtai != 1: + return False, '考核官账号已被封禁,无法收款' + return True, '' + + return False, '无效的提现类型' + + +def _get_personal_daily_quota(leixing, profile): + """打手/管事/组长个人每日限额:额外开启优先,否则默认配置""" + if leixing == 1: + if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0: + return profile.ewai_xiane + return TixianQuotaDefault.query.get(leixing=1).default_quota + if leixing == 2: + if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0: + return profile.ewai_xiane + return TixianQuotaDefault.query.get(leixing=2).default_quota + if leixing == 3: + try: + default_quota = TixianQuotaDefault.query.get(leixing=3).default_quota + except TixianQuotaDefault.DoesNotExist: + default_quota = decimal.Decimal('0.00') + if profile.kaioi_ewai_tixian and profile.ewai_tixian_xiane > 0: + return profile.ewai_tixian_xiane + return default_quota + raise ValueError('无效的个人限额身份') + + +def _get_active_auto_record(shenhe_danhao): + if not shenhe_danhao: + return None + return ( + TixianAutoRecord.query.filter( + shenhe_danhao=shenhe_danhao, + zhuangtai__in=[0, 1], + ) + .order_by('-create_time') + .first() + ) + + +def _get_platform_daily_total(leixing, today=None): + """平台今日已提现总额:只读 houtai.TixianRiTongji(到账金额口径)""" + if today is None: + today = date.today() + try: + return TixianRiTongji.query.get(riqi=today, leixing=leixing).total_amount + except TixianRiTongji.DoesNotExist: + return decimal.Decimal('0.00') + + +def _calc_platform_projected(leixing, shijidaozhang, shenhe_danhao=''): + """ + 平台今日预占后总额(收款前只读预估)。 + 本审核单若已有进行中打款记录,其金额尚未写入统计表,但不应重复加计。 + 返回 (daily_used, projected_total, additional_amount) + """ + daily_used = _get_platform_daily_total(leixing) + active_rec = _get_active_auto_record(shenhe_danhao) + if active_rec: + return daily_used, daily_used, decimal.Decimal('0.00') + additional = shijidaozhang or decimal.Decimal('0.00') + return daily_used, daily_used + additional, additional + + +def _projected_personal_total(personal_today, jine, shenhe_danhao=''): + """个人今日占用预估(申请金额口径)""" + rec = _get_active_auto_record(shenhe_danhao) + already_counted = rec.jine if rec else decimal.Decimal('0.00') + return personal_today - already_counted + jine + + +def _get_platform_daily_limit(leixing): + """ + 平台每日总限额(peizhi.TixianQuotaDefault): + 提现身份1打手→配置4,2管事→5,3组长→6,4考核官→7,5押金→8,6商家→9 + """ + quota_leixing = leixing + 3 + try: + return TixianQuotaDefault.query.get(leixing=quota_leixing).default_quota + except TixianQuotaDefault.DoesNotExist: + logger.warning( + '平台每日总限额未配置 leixing=%s(读取quota_leixing=%s),按0处理禁止新增收款', + leixing, quota_leixing, + ) + return decimal.Decimal('0.00') + + +def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''): + """个人每日限额只读校验(打手/管事/组长)""" + if leixing not in (1, 2, 3): + return True, '', '' + + today = date.today() + if leixing == 1: + try: + profile = UserDashou.query.get(user=user_main) + except UserDashou.DoesNotExist: + return False, '打手信息不存在', 'personal' + if profile.last_tixian_date != today: + personal_today = decimal.Decimal('0.00') + else: + personal_today = profile.jinritixian_jine + elif leixing == 2: + try: + profile = UserGuanshi.query.get(user=user_main) + except UserGuanshi.DoesNotExist: + return False, '管事信息不存在', 'personal' + if profile.last_tixian_date != today: + personal_today = decimal.Decimal('0.00') + else: + personal_today = profile.jinritixian_jine + else: + try: + profile = UserZuzhang.query.get(user=user_main) + except UserZuzhang.DoesNotExist: + return False, '组长信息不存在', 'personal' + if profile.last_tixian_time and profile.last_tixian_time.date() == today: + personal_today = profile.jinri_tixian + else: + personal_today = decimal.Decimal('0.00') + + personal_quota = _get_personal_daily_quota(leixing, profile) + projected_personal = _projected_personal_total(personal_today, jine, shenhe_danhao) + if projected_personal > personal_quota: + return ( + False, + f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}元', + 'personal', + ) + return True, '', '' + + +def _check_platform_quota_readonly(leixing, shijidaozhang, shenhe_danhao=''): + """平台每日总限额只读校验:今日已用 + 本笔到账 > 总限额 则拒绝""" + platform_limit = _get_platform_daily_limit(leixing) + shijidaozhang = shijidaozhang or decimal.Decimal('0.00') + + if platform_limit <= 0 and shijidaozhang > 0: + return False, '今日该角色提现已关闭(总限额为0),暂无法收款', 'platform' + + daily_used, projected, additional = _calc_platform_projected( + leixing, shijidaozhang, shenhe_danhao, + ) + if projected > platform_limit: + logger.warning( + '平台总限额拦截 leixing=%s shenhe_danhao=%s daily_used=%s additional=%s ' + 'projected=%s limit=%s', + leixing, shenhe_danhao, daily_used, additional, projected, platform_limit, + ) + return ( + False, + f'今日该角色提现总额已达上限,今日已提{daily_used}元,本次到账{additional}元,限额{platform_limit}元', + 'platform', + ) + + return True, '', '' + + +def check_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''): + """ + 收款限额只读校验(复用待确认打款单等场景,不写入)。 + 返回 (True, '', '') 或 (False, msg, 'personal'|'platform') + """ + ok, msg, kind = _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao) + if not ok: + return False, msg, kind + return _check_platform_quota_readonly(leixing, shijidaozhang, shenhe_danhao) + + +def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''): + """ + 收款前原子预占每日限额(必须在 transaction.atomic 内调用)。 + 锁定 TixianRiTongji + 用户行,校验通过后立即写入统计,防止高并发超额。 + 本审核单已有进行中/成功打款记录则视为已预占,不重复加计。 + 返回 (True, '', '') 或 (False, msg, 'personal'|'platform') + """ + + today = date.today() + shijidaozhang = shijidaozhang or decimal.Decimal('0.00') + + if _get_active_auto_record(shenhe_danhao): + return True, '', '' + + personal_profile = None + if leixing in (1, 2, 3): + if leixing == 1: + personal_profile = UserDashou.objects.select_for_update().get(user=user_main) + if personal_profile.last_tixian_date != today: + personal_profile.jinritixian_jine = decimal.Decimal('0.00') + personal_profile.last_tixian_date = today + personal_today = personal_profile.jinritixian_jine + elif leixing == 2: + personal_profile = UserGuanshi.objects.select_for_update().get(user=user_main) + if personal_profile.last_tixian_date != today: + personal_profile.jinritixian_jine = decimal.Decimal('0.00') + personal_profile.last_tixian_date = today + personal_today = personal_profile.jinritixian_jine + else: + personal_profile = UserZuzhang.objects.select_for_update().get(user=user_main) + if personal_profile.last_tixian_time and personal_profile.last_tixian_time.date() != today: + personal_profile.jinri_tixian = decimal.Decimal('0.00') + elif not personal_profile.last_tixian_time: + personal_profile.jinri_tixian = decimal.Decimal('0.00') + personal_today = personal_profile.jinri_tixian + + personal_quota = _get_personal_daily_quota(leixing, personal_profile) + if personal_today + jine > personal_quota: + return ( + False, + f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}元', + 'personal', + ) + + platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create( + riqi=today, + leixing=leixing, + defaults={'total_amount': decimal.Decimal('0.00')}, + ) + platform_limit = _get_platform_daily_limit(leixing) + if platform_limit <= 0 and shijidaozhang > 0: + return False, '今日该角色提现已关闭(总限额为0),暂无法收款', 'platform' + if platform_stat.total_amount + shijidaozhang > platform_limit: + return ( + False, + f'今日该角色提现总额已达上限,今日已提{platform_stat.total_amount}元,' + f'本次到账{shijidaozhang}元,限额{platform_limit}元', + 'platform', + ) + + if personal_profile is not None: + if leixing == 1: + personal_profile.jinritixian_jine += jine + personal_profile.save(update_fields=['jinritixian_jine', 'last_tixian_date']) + elif leixing == 2: + personal_profile.jinritixian_jine += jine + personal_profile.save(update_fields=['jinritixian_jine', 'last_tixian_date']) + else: + personal_profile.jinri_tixian += jine + personal_profile.last_tixian_time = timezone.now() + personal_profile.save(update_fields=['jinri_tixian', 'last_tixian_time']) + + platform_stat.total_amount += shijidaozhang + platform_stat.save(update_fields=['total_amount']) + + logger.info( + '收款限额预占成功 yonghuid=%s leixing=%s shenhe_danhao=%s jine=%s shijidaozhang=%s platform_total=%s', + user_main.yonghuid, leixing, shenhe_danhao, jine, shijidaozhang, platform_stat.total_amount, + ) + return True, '', '' + + +def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang): + """微信打款明确失败/用户取消时回滚收款预占的限额""" + today = date.today() + shijidaozhang = shijidaozhang or decimal.Decimal('0.00') + try: + with transaction.atomic(): + if leixing == 1: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + if dashou.last_tixian_date == today and dashou.jinritixian_jine >= jine: + dashou.jinritixian_jine -= jine + dashou.save(update_fields=['jinritixian_jine']) + elif leixing == 2: + guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) + if guanshi.last_tixian_date == today and guanshi.jinritixian_jine >= jine: + guanshi.jinritixian_jine -= jine + guanshi.save(update_fields=['jinritixian_jine']) + elif leixing == 3: + zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) + if zuzhang.jinri_tixian >= jine: + zuzhang.jinri_tixian -= jine + zuzhang.save(update_fields=['jinri_tixian']) + + try: + stat = TixianRiTongji.objects.select_for_update().get( + riqi=today, leixing=leixing, + ) + stat.total_amount = max( + stat.total_amount - shijidaozhang, + decimal.Decimal('0.00'), + ) + stat.save(update_fields=['total_amount']) + except TixianRiTongji.DoesNotExist: + pass + except Exception as e: + logger.error( + '回滚收款限额预占失败 yonghuid=%s leixing=%s err=%s', + user_main.yonghuid, leixing, e, exc_info=True, + ) + return + + logger.info( + '收款限额预占已回滚 yonghuid=%s leixing=%s jine=%s shijidaozhang=%s', + user_main.yonghuid, leixing, jine, shijidaozhang, + ) + + +def release_collect_quota_for_record(auto_record): + """幂等回滚单笔打款对应的收款预占(查单/回调/取消等多路径共用,防重复扣减)""" + if not auto_record.shenhe_danhao: + return + if not _quota_already_reserved_for_record(auto_record): + return + try: + user_main = User.query.get(UserUID=auto_record.yonghuid) + except User.DoesNotExist: + logger.error('回滚限额预占失败:用户不存在 %s', auto_record.yonghuid) + return + release_collect_quota_reservation( + user_main, + auto_record.leixing, + auto_record.jine, + auto_record.shijidaozhang, + ) + + +def _quota_already_reserved_for_record(auto_record): + """判断本笔打款是否已在收款预占时写入日统计(避免成功时重复累加)""" + if not auto_record.shenhe_danhao: + return False + + today = date.today() + shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00') + stat_total = _get_platform_daily_total(auto_record.leixing, today) + other_sum = TixianAutoRecord.query.filter( + leixing=auto_record.leixing, + zhuangtai__in=[0, 1], + create_time__date=today, + ).exclude(tixian_id=auto_record.tixian_id).aggregate( + total=Sum('shijidaozhang'), + )['total'] or decimal.Decimal('0.00') + return stat_total >= other_sum + shijidaozhang + + +def apply_transfer_success_quota(auto_record): + """ + 打款成功时累加每日限额统计(仅自动审核流程 shenhe_danhao 非空)。 + - houtai.TixianRiTongji.total_amount += 实际到账 + - 用户扩展表个人今日已提 += 申请金额(打手/管事/组长) + 须在 transaction.atomic 内、且打款记录 zhuangtai 由 0→1 时调用一次。 + """ + if not auto_record.shenhe_danhao: + return + today = date.today() + leixing = auto_record.leixing + jine = auto_record.jine + shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00') + + try: + user_main = User.query.get(UserUID=auto_record.yonghuid) + except User.DoesNotExist: + logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid) + return + + platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create( + riqi=today, + leixing=leixing, + defaults={'total_amount': decimal.Decimal('0.00')}, + ) + platform_stat.total_amount += shijidaozhang + platform_stat.save(update_fields=['total_amount']) + + if leixing == 1: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + if dashou.last_tixian_date != today: + dashou.jinritixian_jine = decimal.Decimal('0.00') + dashou.last_tixian_date = today + dashou.jinritixian_jine += jine + dashou.save(update_fields=['jinritixian_jine', 'last_tixian_date']) + elif leixing == 2: + guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) + if guanshi.last_tixian_date != today: + guanshi.jinritixian_jine = decimal.Decimal('0.00') + guanshi.last_tixian_date = today + guanshi.jinritixian_jine += jine + guanshi.save(update_fields=['jinritixian_jine', 'last_tixian_date']) + elif leixing == 3: + zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) + if zuzhang.last_tixian_time and zuzhang.last_tixian_time.date() != today: + zuzhang.jinri_tixian = decimal.Decimal('0.00') + elif not zuzhang.last_tixian_time: + zuzhang.jinri_tixian = decimal.Decimal('0.00') + zuzhang.jinri_tixian += jine + zuzhang.last_tixian_time = timezone.now() + zuzhang.save(update_fields=['jinri_tixian', 'last_tixian_time']) + + logger.info( + '打款成功已累加限额 bill=%s yonghuid=%s leixing=%s jine=%s shijidaozhang=%s ' + 'platform_total=%s', + auto_record.tixian_id, auto_record.yonghuid, leixing, jine, shijidaozhang, + platform_stat.total_amount, + ) + + +def refund_balance(user_main, leixing, jine): + """审核拒绝时退还可到账金额 shijidaozhang(须在 transaction.atomic 内;手续费已在申请扣款时扣除不退还)""" + if leixing == 1: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + dashou.yue += jine + dashou.save() + elif leixing == 2: + guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) + guanshi.yue += jine + guanshi.save() + elif leixing == 3: + zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) + zuzhang.ketixian_jine += jine + zuzhang.save() + elif leixing == 4: + kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main) + kaoheguan.yue += jine + kaoheguan.save() + elif leixing == 5: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + dashou.yajin += jine + dashou.zhanghaozhuangtai = 1 + dashou.save() + elif leixing == 6: + shangjia = UserShangjia.objects.select_for_update().get(user=user_main) + shangjia.yue += jine + shangjia.save() + else: + raise ValueError('无效的提现类型') + + +def deduct_balance(user_main, leixing, jine): + """扣减申请金额 jine(必须在 transaction.atomic 内调用)""" + if leixing == 1: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + if dashou.yue < jine: + raise ValueError('余额不足') + dashou.yue -= jine + dashou.save() + return dashou.nicheng or '' + if leixing == 2: + guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) + if guanshi.yue < jine: + raise ValueError('余额不足') + guanshi.yue -= jine + guanshi.save() + return '管事用户' + if leixing == 3: + zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) + if zuzhang.ketixian_jine < jine: + raise ValueError('可提现金额不足') + zuzhang.ketixian_jine -= jine + zuzhang.save() + return '' + if leixing == 4: + kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main) + if kaoheguan.yue < jine: + raise ValueError('可提现金额不足') + kaoheguan.yue -= jine + kaoheguan.save() + return '考核官' + if leixing == 5: + dashou = UserDashou.objects.select_for_update().get(user=user_main) + if dashou.yajin < jine: + raise ValueError('押金余额不足') + dashou.yajin -= jine + dashou.zhanghaozhuangtai = 0 + dashou.save() + return dashou.nicheng or '' + if leixing == 6: + shangjia = UserShangjia.objects.select_for_update().get(user=user_main) + if shangjia.yue < jine: + raise ValueError('商家余额不足') + shangjia.yue -= jine + shangjia.save() + return shangjia.nicheng or '' + raise ValueError('无效的提现类型') + + +def get_balance_response_data(user_main, leixing): + data = {} + if leixing == 1: + try: + dashou = UserDashou.query.get(user=user_main) + data['new_yongjin'] = str(dashou.yue.quantize(decimal.Decimal('0.01'))) + except UserDashou.DoesNotExist: + pass + elif leixing == 2: + try: + guanshi = UserGuanshi.query.get(user=user_main) + data['new_fenyong'] = str(guanshi.yue.quantize(decimal.Decimal('0.01'))) + except UserGuanshi.DoesNotExist: + pass + elif leixing == 3: + try: + zuzhang = UserZuzhang.query.get(user=user_main) + data['new_zuzhang_ketixian'] = str(zuzhang.ketixian_jine.quantize(decimal.Decimal('0.01'))) + except UserZuzhang.DoesNotExist: + pass + elif leixing == 4: + try: + kg = UserShenheguan.query.get(user=user_main) + data['new_kaoheguan_yue'] = str(kg.yue.quantize(decimal.Decimal('0.01'))) + except UserShenheguan.DoesNotExist: + pass + return data + + +def sync_audit_and_jilu_status(audit, new_status, **extra_fields): + audit.zhuangtai = new_status + for k, v in extra_fields.items(): + setattr(audit, k, v) + audit.save() + if audit.tixianjilu_id: + update_fields = {'zhuangtai': new_status, 'update_time': timezone.now()} + if 'bhliyou' in extra_fields: + update_fields['bhliyou'] = extra_fields['bhliyou'] + Tixianjilu.query.filter(id=audit.tixianjilu_id).update(**update_fields) + + +def _close_auto_record_failed(auto_record, fail_reason): + """标记打款记录失败并释放收款预占限额""" + if auto_record is None or auto_record.zhuangtai == 1: + return + reason = (fail_reason or '打款失败')[:500] + auto_record.zhuangtai = 2 + auto_record.fail_reason = reason + auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) + release_collect_quota_for_record(auto_record) + + +def close_audit_wechat_failed(audit, auto_record=None, fail_reason='微信转账失败'): + """ + 微信官方终态 FAIL/CANCELLED:审核单关闭(3),不退款(申请时已扣款,手续费不退)。 + 须在 transaction.atomic 内调用。 + """ + _close_auto_record_failed(auto_record, fail_reason) + + audit = TixianShenheJilu.objects.select_for_update().get(pk=audit.pk) + if audit.zhuangtai in (2, 3, 5): + return False + + reason = (fail_reason or '微信转账失败')[:500] + sync_audit_and_jilu_status( + audit, 3, + bhliyou=reason, + fail_reason=reason, + tixian_auto_id=None, + ) + logger.warning( + '微信失败关单(不退款) yonghuid=%s shenhe_danhao=%s reason=%s', + audit.yonghuid, audit.shenhe_danhao, reason, + ) + return True + + +def close_audit_over_limit_refund(audit, auto_record=None, fail_reason='单笔实际到账超过限额'): + """ + 到账超过200元等:审核单关闭(3) + 退还可到账金额 shijidaozhang(手续费不退)。 + 须在 transaction.atomic 内调用。 + """ + reason = (fail_reason or '单笔实际到账超过限额')[:500] + _close_auto_record_failed(auto_record, reason) + + audit = TixianShenheJilu.objects.select_for_update().get(pk=audit.pk) + if audit.zhuangtai in (2, 3, 5): + return False + + user_main = User.objects.select_for_update().get(UserUID=audit.yonghuid) + refund_balance(user_main, audit.leixing, audit.shijidaozhang) + sync_audit_and_jilu_status( + audit, 3, + bhliyou=reason, + fail_reason=reason, + tixian_auto_id=None, + ) + logger.warning( + '超限关单并退款 yonghuid=%s shenhe_danhao=%s reason=%s', + audit.yonghuid, audit.shenhe_danhao, reason, + ) + return True + + +def query_wechat_transfer_bill(out_bill_no): + """ + 向微信查转账单状态 + GET /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no} + 返回 dict;网络/系统异常返回 None;微信无此单返回 {'_not_found': True} + """ + + url = ( + 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/' + f'transfer-bills/out-bill-no/{out_bill_no}' + ) + try: + auth = build_authorization('GET', url, '') + headers = { + 'Authorization': auth, + 'Accept': 'application/json', + 'User-Agent': 'Mozilla/5.0', + } + resp = requests.get(url, headers=headers, timeout=10) + if resp.status_code == 200: + return resp.json() + if resp.status_code == 404: + return {'_not_found': True} + logger.error( + '微信查单失败 bill=%s HTTP%s body=%s', + out_bill_no, resp.status_code, resp.text[:300], + ) + return None + except (requests.RequestException, OSError, ValueError) as e: + logger.error('微信查单异常 bill=%s err=%s', out_bill_no, e, exc_info=True) + return None + except Exception as e: + logger.error('微信查单未知异常 bill=%s err=%s', out_bill_no, e, exc_info=True) + return None + + +def _parse_package_info(raw): + if not raw: + return None + if isinstance(raw, dict): + return raw + try: + return json.loads(raw) + except (TypeError, ValueError): + return None + + +def _apply_platform_accounting(auto_record): + """平台收支统计(幂等由 mark_transfer_success 外层保证只调一次)""" + + update_daily_payout(auto_record.shijidaozhang) + try: + szjilu = Szjilu.objects.select_for_update().get(id=1) + szjilu.zongsy -= auto_record.shijidaozhang + szjilu.zongzc += auto_record.shijidaozhang + szjilu.jrzc += auto_record.shijidaozhang + szjilu.save() + except Szjilu.DoesNotExist: + Szjilu.query.create( + id=1, + zongsy=-auto_record.shijidaozhang, + zongzc=auto_record.shijidaozhang, + jrzc=auto_record.shijidaozhang, + ) + + +def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounting=True): + """ + 打款成功落库(tixianqr / callback / 查单对账 共用) + select_for_update + zhuangtai==0 才更新并记账,防双写 + 返回 True=本次新标记成功;False=已处理过 + """ + with transaction.atomic(): + ar = TixianAutoRecord.objects.select_for_update().get(pk=auto_record.pk) + if ar.zhuangtai != 0: + return False + + ar.zhuangtai = 1 + update_fields = ['zhuangtai', 'update_time'] + if wechat_transfer_no: + ar.wechat_transfer_no = wechat_transfer_no + update_fields.append('wechat_transfer_no') + ar.save(update_fields=update_fields) + + if ar.shenhe_danhao: + try: + audit = TixianShenheJilu.objects.select_for_update().get( + shenhe_danhao=ar.shenhe_danhao, + ) + if audit.zhuangtai != 2: + sync_audit_and_jilu_status(audit, 2) + except TixianShenheJilu.DoesNotExist: + logger.warning('打款成功但审核记录不存在: %s', ar.shenhe_danhao) + + if ar.shenhe_danhao and not _quota_already_reserved_for_record(ar): + apply_transfer_success_quota(ar) + + if with_accounting: + _apply_platform_accounting(ar) + + return True + + +def _sync_record_from_wx_query(auto_record, wx_data): + """根据微信查单结果修正本地打款记录,返回 (action, payload)""" + if wx_data.get('_not_found'): + age = timezone.now() - auto_record.create_time + if auto_record.zhuangtai == 0 and age < timedelta(minutes=PENDING_QUERY_GRACE_MINUTES): + return RECONCILE_PENDING, { + 'msg': '有收款处理中,请稍后再试', + 'tixian_id': auto_record.tixian_id, + } + if auto_record.zhuangtai == 0: + auto_record.zhuangtai = 2 + auto_record.fail_reason = '微信未找到该打款单,请重新发起收款' + auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) + release_collect_quota_for_record(auto_record) + return RECONCILE_ALLOW_NEW, {} + + state = (wx_data.get('state') or '').upper() + + if state in WX_STATE_SUCCESS: + mark_transfer_success( + auto_record, + wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'), + ) + return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} + + if state in WX_STATE_WAIT: + package = wx_data.get('package_info') or _parse_package_info(auto_record.wechat_package) + if not package: + return RECONCILE_PENDING, { + 'msg': '有收款处理中,请稍后再试', + 'tixian_id': auto_record.tixian_id, + } + if auto_record.zhuangtai == 0: + auto_record.wechat_package = json.dumps(package, ensure_ascii=False) + auto_record.save(update_fields=['wechat_package', 'update_time']) + wx_cfg = settings.WECHAT_PAY_V3_CONFIG + return RECONCILE_WAIT_CONFIRM, { + 'tixian_id': auto_record.tixian_id, + 'package_info': package, + 'shouxufei': str(auto_record.shouxufei), + 'shijidaozhang': str(auto_record.shijidaozhang), + 'current_rate': str(auto_record.feilv), + 'mch_id': wx_cfg.get('MCHID', ''), + 'shenhe_danhao': auto_record.shenhe_danhao or '', + } + + if state in WX_STATE_CANCELING: + return RECONCILE_PENDING, { + 'msg': '收款撤销处理中,请稍后再试', + 'tixian_id': auto_record.tixian_id, + } + + if state in WX_STATE_FAIL: + fail_reason = ( + wx_data.get('fail_reason') or wx_data.get('close_reason') or state + )[:500] + if auto_record.zhuangtai != 1: + auto_record.zhuangtai = 2 + auto_record.fail_reason = fail_reason + auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) + release_collect_quota_for_record(auto_record) + if auto_record.shenhe_danhao: + try: + audit = TixianShenheJilu.query.get(shenhe_danhao=auto_record.shenhe_danhao) + with transaction.atomic(): + close_audit_wechat_failed(audit, None, fail_reason) + except TixianShenheJilu.DoesNotExist: + logger.warning('微信失败但审核记录不存在: %s', auto_record.shenhe_danhao) + return RECONCILE_AUDIT_CLOSED, { + 'msg': '微信已确认转账失败,该笔提现已关闭,请联系客服处理', + } + + if auto_record.zhuangtai == 0: + return RECONCILE_PENDING, { + 'msg': f'收款处理中(微信状态:{state or "未知"}),请稍后再试', + 'tixian_id': auto_record.tixian_id, + } + return RECONCILE_ALLOW_NEW, {} + + +def get_auto_record_for_collect(shenhe_danhao): + """ + 取本笔提现申请应对接微信的那条打款记录: + 优先 zhuangtai=0(进行中),否则取最新一条(可能本地误标失败但微信未终态) + """ + pending = ( + TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao, zhuangtai=0) + .order_by('-create_time') + .first() + ) + if pending: + return pending + return ( + TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao) + .order_by('-create_time') + .first() + ) + + +def reconcile_shenhe_wechat_bills(shenhe_danhao): + """ + P0 防双笔:同一 shenhe_danhao 下所有打款记录逐条问微信 + 任一 SUCCESS / 进行中 → 禁止新建;全部终态失败才 ALLOW_NEW + 返回 (action, payload) + """ + records = list( + TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao).order_by('-create_time') + ) + + if any(r.zhuangtai == 1 for r in records): + try: + audit = TixianShenheJilu.query.get(shenhe_danhao=shenhe_danhao) + if audit.zhuangtai != 2: + sync_audit_and_jilu_status(audit, 2) + except TixianShenheJilu.DoesNotExist: + pass + return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} + + if not records: + return RECONCILE_ALLOW_NEW, {} + + pending_hit = None + for rec in records: + if rec.zhuangtai == 1: + return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} + + wx_data = query_wechat_transfer_bill(rec.tixian_id) + if wx_data is None: + logger.error( + '对账查单不可用 shenhe_danhao=%s bill=%s', + shenhe_danhao, rec.tixian_id, + ) + return RECONCILE_PENDING, { + 'msg': '有收款处理中,系统正在核对微信状态,请稍后再试', + 'tixian_id': rec.tixian_id, + } + + action, payload = _sync_record_from_wx_query(rec, wx_data) + if action == RECONCILE_COMPLETED: + return action, payload + if action == RECONCILE_WAIT_CONFIRM: + return action, payload + if action == RECONCILE_AUDIT_CLOSED: + return action, payload + if action == RECONCILE_PENDING: + if pending_hit is None: + pending_hit = (action, payload) + continue + + if pending_hit: + return pending_hit + return RECONCILE_ALLOW_NEW, {} + + +def resolve_collect_context(yonghuid, tixianjilu_id=None, shenhe_danhao='', shenhe_jilu_id=None): + """ + 从提现记录 ID 进门(P0 收款主路径): + Tixianjilu → TixianShenheJilu,校验归属、状态=6 待收款 + 返回 ({jilu, audit}, err_msg) + """ + if tixianjilu_id is None and not shenhe_danhao: + return None, '缺少提现记录ID' + + jilu = None + if tixianjilu_id is not None: + try: + jilu = Tixianjilu.query.get(id=int(tixianjilu_id), yonghuid=yonghuid) + except (Tixianjilu.DoesNotExist, TypeError, ValueError): + return None, '提现记录不存在或不属于当前用户' + + if jilu.zhuangtai != 6: + return None, f'当前状态不可收款(状态{jilu.zhuangtai}),请等待审核通过' + if not jilu.shenhe_jilu_id or not jilu.shenhe_danhao: + return None, '提现记录未关联审核单,无法收款' + + if shenhe_jilu_id is not None: + try: + if int(shenhe_jilu_id) != jilu.shenhe_jilu_id: + return None, '审核记录ID与提现记录不匹配' + except (TypeError, ValueError): + return None, '审核记录ID格式错误' + + if shenhe_danhao and shenhe_danhao != jilu.shenhe_danhao: + return None, '审核单号与提现记录不匹配' + + shenhe_danhao = jilu.shenhe_danhao + shenhe_jilu_id = jilu.shenhe_jilu_id + + try: + if shenhe_jilu_id: + audit = TixianShenheJilu.query.get( + id=int(shenhe_jilu_id), + shenhe_danhao=shenhe_danhao, + yonghuid=yonghuid, + ) + else: + audit = TixianShenheJilu.query.get( + shenhe_danhao=shenhe_danhao, + UserUID=yonghuid, + ) + except (TixianShenheJilu.DoesNotExist, TypeError, ValueError): + return None, '审核记录不存在' + + if audit.zhuangtai == 2: + return None, '该提现已完成,请勿重复操作' + if audit.zhuangtai != 6: + return None, f'当前状态不可收款(状态{audit.zhuangtai}),请等待审核通过' + if not is_auto_dakuan_audit(audit): + return None, '该提现单为手动打款,无法使用自动收款' + + if jilu is None: + try: + jilu = Tixianjilu.query.get( + id=audit.tixianjilu_id, + UserUID=yonghuid, + ) if audit.tixianjilu_id else None + except Tixianjilu.DoesNotExist: + jilu = None + + return {'jilu': jilu, 'audit': audit, 'shenhe_danhao': shenhe_danhao}, '' + + +def handle_post_transfer_failure(tixian_id, shenhe_danhao, err_code='', err_msg=''): + """ + 发起转账 HTTP 非成功时:先查微信再决定是否标失败(P0,禁止盲目标失败导致双笔) + 返回 (action, payload_or_user_msg) + """ + try: + ar = TixianAutoRecord.query.get(tixian_id=tixian_id) + except TixianAutoRecord.DoesNotExist: + return RECONCILE_PENDING, '收款处理中,请稍后再试' + + wx_data = query_wechat_transfer_bill(tixian_id) + if wx_data is None: + logger.error( + '发起转账失败且查单不可用 bill=%s code=%s msg=%s,保持待查单', + tixian_id, err_code, err_msg, + ) + return RECONCILE_PENDING, '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击' + + action, payload = _sync_record_from_wx_query(ar, wx_data) + if action == RECONCILE_ALLOW_NEW: + return RECONCILE_ALLOW_NEW, payload + if action == RECONCILE_WAIT_CONFIRM: + return RECONCILE_WAIT_CONFIRM, payload + if action == RECONCILE_COMPLETED: + return RECONCILE_COMPLETED, payload + return RECONCILE_PENDING, payload.get('msg', '收款处理中,请稍后再试') + + +def get_audit_for_collect(shenhe_danhao, yonghuid, shenhe_jilu_id=None): + """ + 收款前数据库校验(须在 transaction.atomic 内调用;微信查单在事务外单独做) + """ + try: + audit = TixianShenheJilu.objects.select_for_update().get( + shenhe_danhao=shenhe_danhao, + UserUID=yonghuid, + ) + except TixianShenheJilu.DoesNotExist: + return None, '审核记录不存在' + + if shenhe_jilu_id is not None: + try: + if int(shenhe_jilu_id) != audit.id: + return None, '审核记录ID不匹配' + except (TypeError, ValueError): + return None, '审核记录ID格式错误' + + if audit.zhuangtai == 2: + return None, '该提现已完成,请勿重复操作' + + if audit.zhuangtai != 6: + return None, f'当前状态不可收款(状态{audit.zhuangtai}),请等待审核通过' + + if not is_auto_dakuan_audit(audit): + return None, '该提现单为手动打款,无法使用自动收款' + + return audit, '' + + +def create_audit_application(user_main, leixing, jine): + """ + 提交审核申请:校验 → 扣款 → 写审核表 + 提现记录表 + 不创建打款记录表(TixianAutoRecord 仅在收款时创建) + 允许多笔并行申请(不阻塞待收款/审核中单) + """ + yonghuid = user_main.yonghuid + + ok, msg, extra = validate_withdraw_eligibility(user_main, leixing, jine) + if not ok: + raise ValueError(msg) + + feilv = get_tixian_feilv(leixing) + shouxufei, shijidaozhang = calc_fee_amounts(jine, feilv) + if shijidaozhang <= 0: + raise ValueError('提现金额过低,扣除手续费后无实际到账') + + ok, limit_msg = check_shijidaozhang_within_limit(shijidaozhang) + if not ok: + raise ValueError(limit_msg) + + shenhe_danhao = generate_shenhe_danhao() + nicheng = extra.get('nicheng', '') + + with transaction.atomic(): + nicheng = deduct_balance(user_main, leixing, jine) or nicheng + + audit_kwargs = { + 'shenhe_danhao': shenhe_danhao, + 'yonghuid': yonghuid, + 'leixing': leixing, + 'zhuangtai': 1, + 'shenqing_jine': jine, + 'shouxufei': shouxufei, + 'shijidaozhang': shijidaozhang, + 'feilv': feilv, + } + if has_dakuan_mode_field(): + audit_kwargs['dakuan_mode'] = DAKUAN_MODE_AUTO + audit = TixianShenheJilu.query.create(**audit_kwargs) + + tixian_record = Tixianjilu.query.create( + yonghuid=yonghuid, + avatar=user_main.avatar or '', + phone=user_main.phone or '', + nicheng=nicheng, + leixing=leixing, + zhifu=user_main.zhifu or '', + skzhanghao=user_main.skzhanghao or '', + jine=shijidaozhang, + zhuangtai=1, + fangshi=1, + shenheid=None, + bhliyou='', + shenhe_jilu_id=audit.id, + shenhe_danhao=shenhe_danhao, + ) + + audit.tixianjilu_id = tixian_record.id + audit.save(update_fields=['tixianjilu_id']) + + response_data = get_balance_response_data(user_main, leixing) + response_data.update({ + 'shenhe_danhao': shenhe_danhao, + 'shenhe_jilu_id': audit.id, + 'tixianjilu_id': tixian_record.id, + 'shouxufei': str(shouxufei), + 'shijidaozhang': str(shijidaozhang), + 'current_rate': str(feilv.quantize(decimal.Decimal('0.0001'))), + 'leixing': leixing, + 'zhuangtai': 1, + }) + return response_data diff --git a/users/views.py b/users/views.py index d93e0d9..7de00a6 100644 --- a/users/views.py +++ b/users/views.py @@ -39,8 +39,8 @@ from rest_framework.throttling import AnonRateThrottle from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.authentication import JWTAuthentication -from gvsdsdk.fluent import db, func, FQ - +from gvsdsdk.fluent import db, func, FQ + from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss from utils.fadan_utils import check_fadan_qiangdan_eligible from utils.wechat_v3 import verify_wechat_sign, decrypt_callback_ciphertext @@ -343,7 +343,7 @@ class WechatMiniProgramLoginView(APIView): user_id = timestamp_part + random_part if len(user_id) == 7 and user_id.isdigit(): - if not User.query.filter(yonghuid=user_id).exists(): + if not User.query.filter(UserUID=user_id).exists(): return user_id raise Exception('生成用户ID失败') @@ -827,7 +827,7 @@ class HuoQuYaoQingRenView(APIView): # 3. 查询邀请人主表(头像) try: - inviter_main = User.query.get(yonghuid=yaoqingren_id) + inviter_main = User.query.get(UserUID=yaoqingren_id) touxiang = inviter_main.avatar or '' except User.DoesNotExist: return Response({ @@ -1081,7 +1081,7 @@ class ShangJiaXinXiView(APIView): # ------------------- 3. 从 ShangjiaRiTongji 获取今日统计 ------------------- try: - today_stat = ShangjiaRiTongji.query.get(yonghuid=yonghuid, riqi=today) + today_stat = ShangjiaRiTongji.query.get(UserUID=yonghuid, riqi=today) jinri_paifa_dingdan = today_stat.paifa_dingdan_shu jinri_paifa_jine = float(today_stat.paifa_jine) jinri_tuikuan_dingdan = today_stat.tuikuan_dingdan_shu @@ -1096,7 +1096,7 @@ class ShangJiaXinXiView(APIView): # ------------------- 4. 从 ShangjiaRiTongji 获取本月统计 ------------------- month_stats = ShangjiaRiTongji.query.filter( - yonghuid=yonghuid, + UserUID=yonghuid, riqi__gte=this_month_start, riqi__lte=today ).aggregate( @@ -2161,7 +2161,7 @@ class TixianJiluHuoquViewV2(APIView): limit = 100 offset = (page - 1) * limit - qs = Tixianjilu.query.filter(yonghuid=yonghuid) + qs = Tixianjilu.query.filter(UserUID=yonghuid) if mode == 2: # 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选 if zhuangtai_filter > 0: @@ -3668,7 +3668,7 @@ class AdTongYiChuFa(APIView): # 11.2 查询打手扩展表(只是验证存在性) try: - dashou = UserDashou.query.get(user__yonghuid=chufajilu.dashouid) + dashou = UserDashou.query.get(user__UserUID=chufajilu.dashouid) except UserDashou.DoesNotExist: return Response( {'code': 404, 'message': '被处罚打手不存在', 'data': None}, @@ -3698,7 +3698,7 @@ class AdTongYiChuFa(APIView): # 🔴【新增】11.5 返还打手积分(加5分) if chufajilu.jifen > 0: try: - dashou = UserDashou.query.get(user__yonghuid=chufajilu.dashouid) + dashou = UserDashou.query.get(user__UserUID=chufajilu.dashouid) # 🔴【新增】安全验证:必须积分小于等于5分才允许加分 if dashou.jifen <= 5: # 判断积分是否小于等于5分 @@ -3827,7 +3827,7 @@ class AdckyhxqView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 查询用户主表 - 使用select_related优化查询 - user_main = User.query.filter(yonghuid=uid).first() + user_main = User.query.filter(UserUID=uid).first() if not user_main: return Response({ 'code': 404, @@ -3856,7 +3856,7 @@ class AdckyhxqView(APIView): # 根据用户类型查询扩展信息 if user_type == 1: # 老板 # 使用select_related预取boss_profile - user_main = User.query.filter(yonghuid=uid).select_related('boss_profile').first() + user_main = User.query.filter(UserUID=uid).select_related('boss_profile').first() if user_main.boss_profile: boss_profile = user_main.boss_profile user_info.update({ @@ -3869,7 +3869,7 @@ class AdckyhxqView(APIView): elif user_type == 2: # 打手 # 使用select_related预取dashou_profile - user_main = User.query.filter(yonghuid=uid).select_related('dashou_profile').first() + user_main = User.query.filter(UserUID=uid).select_related('dashou_profile').first() if user_main.dashou_profile: dashou_profile = user_main.dashou_profile @@ -3928,7 +3928,7 @@ class AdckyhxqView(APIView): elif user_type == 3: # 管事 # 使用select_related预取guanshi_profile - user_main = User.query.filter(yonghuid=uid).select_related('guanshi_profile').first() + user_main = User.query.filter(UserUID=uid).select_related('guanshi_profile').first() if user_main.guanshi_profile: guanshi_profile = user_main.guanshi_profile user_info.update({ @@ -3947,7 +3947,7 @@ class AdckyhxqView(APIView): elif user_type == 4: # 商家 # 使用select_related预取shop_profile - user_main = User.query.filter(yonghuid=uid).select_related('shop_profile').first() + user_main = User.query.filter(UserUID=uid).select_related('shop_profile').first() if user_main.shop_profile: shop_profile = user_main.shop_profile user_info.update({ @@ -4061,7 +4061,7 @@ class AdcjxgView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 查询用户主表 - user_main = User.query.filter(yonghuid=uid).first() + user_main = User.query.filter(UserUID=uid).first() if not user_main: return Response({ 'code': 404, @@ -5457,7 +5457,7 @@ class WechatLoginAndDashouRegisterView(APIView): user_id = timestamp_part + random_part if len(user_id) == 7 and user_id.isdigit(): - if not User.query.filter(yonghuid=user_id).exists(): + if not User.query.filter(UserUID=user_id).exists(): return user_id raise Exception('生成用户ID失败') @@ -5730,7 +5730,7 @@ class YonghuTixianShenheXiugaiView(APIView): try: tixian_record = Tixianjilu.objects.select_for_update().get( id=tixian_id, - yonghuid=current_yonghuid # 确保只能修改自己的记录 + UserUID=current_yonghuid # 确保只能修改自己的记录 ) except Tixianjilu.DoesNotExist: return Response({ @@ -6158,7 +6158,7 @@ class ChufaJiluHuoquView(APIView): zhengju_mapping = {} if dingdan_ids and shangjia_ids: # 构建Q对象:dingdan_id在列表中 AND yonghuid在商家ID列表中 - zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=shangjia_ids) + zhengju_q = Q(dingdan_id__in=dingdan_ids, UserUID__in=shangjia_ids) zhengju_queryset = Chufatupian.query.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian') for item in zhengju_queryset: @@ -6353,7 +6353,7 @@ class DashouShensuView(APIView): # 检查是否已经有申诉图片 existing_shensu_tupian = Chufatupian.query.filter( dingdan_id=chufa_record.dingdan_id, - yonghuid=yonghuid + UserUID=yonghuid ).exists() if existing_shensu_tupian: @@ -6600,7 +6600,7 @@ class ShangjiaChufaJiluHuoquView(APIView): shensu_mapping = {} if dingdan_ids and dashou_ids: # 构建Q对象:dingdan_id在列表中 AND yonghuid在打手ID列表中 - shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=dashou_ids) + shensu_q = Q(dingdan_id__in=dingdan_ids, UserUID__in=dashou_ids) shensu_queryset = Chufatupian.query.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian') for item in shensu_queryset: @@ -7064,7 +7064,7 @@ class KefuGetOrderListView(APIView): laoban_ids = [o.pingtai_kuozhan.laoban_id for o in orders_qs if hasattr(o, 'pingtai_kuozhan') and o.pingtai_kuozhan.laoban_id] nickname_map = {} if laoban_ids: - users = User.query.filter(yonghuid__in=laoban_ids).select_related('boss_profile').only( + users = User.query.filter(UserUID__in=laoban_ids).select_related('boss_profile').only( 'yonghuid', 'boss_profile__nickname' ) for user in users: @@ -7233,7 +7233,7 @@ class KefuGetShangjiaOrderListView(APIView): shangjia_ids = [o.shangjia_kuozhan.shangjia_id for o in orders_qs if hasattr(o, 'shangjia_kuozhan') and o.shangjia_kuozhan.shangjia_id] nickname_map = {} if shangjia_ids: - users = User.query.filter(yonghuid__in=shangjia_ids).select_related('shop_profile').only( + users = User.query.filter(UserUID__in=shangjia_ids).select_related('shop_profile').only( 'yonghuid', 'shop_profile__nicheng' ) for user in users: @@ -7339,7 +7339,7 @@ class KefuChangeDashouView(APIView): # 5. 查询新打手 try: - new_dashou_user = User.query.get(yonghuid=new_dashou_id) + new_dashou_user = User.query.get(UserUID=new_dashou_id) except User.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -7401,7 +7401,7 @@ class KefuChangeDashouView(APIView): old_dashou_id = order.jiedan_dashou_id if old_dashou_id: try: - old_dashou_user = User.query.get(yonghuid=old_dashou_id) + old_dashou_user = User.query.get(UserUID=old_dashou_id) old_dashou_profile = old_dashou_user.dashou_profile old_dashou_profile.zhuangtai = 1 # 恢复空闲 old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑) @@ -7577,7 +7577,7 @@ class KefuPlatformRefundView(APIView): jiedan_dashou_id = order.jiedan_dashou_id if jiedan_dashou_id: try: - dashou_user = User.query.get(yonghuid=jiedan_dashou_id) + dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang += 1 dashou_profile.zhuangtai = 1 # 打手状态设为空闲 @@ -7842,7 +7842,7 @@ class KefuRejectRefundView(APIView): # 7. 给打手结算(返还分成) if jiedan_dashou_id: try: - dashou_user = User.query.get(yonghuid=jiedan_dashou_id) + dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile # 增加打手相关统计数据 dashou_profile.chengjiaozongliang += 1 @@ -7862,7 +7862,7 @@ class KefuRejectRefundView(APIView): shangjia_kuozhan = order.shangjia_kuozhan shangjia_id = shangjia_kuozhan.shangjia_id if shangjia_id: - shangjia_user = User.query.get(yonghuid=shangjia_id) + shangjia_user = User.query.get(UserUID=shangjia_id) shangjia_profile = shangjia_user.shop_profile shangjia_profile.chengjiao += 1 shangjia_profile.save() @@ -8019,7 +8019,7 @@ class KefuMerchantRefundView(APIView): # 更新打手 if jiedan_dashou_id: try: - dashou_user = User.query.get(yonghuid=jiedan_dashou_id) + dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang += 1 dashou_profile.zhuangtai = 1 @@ -8029,7 +8029,7 @@ class KefuMerchantRefundView(APIView): # 更新商家 try: - shangjia_user = User.query.get(yonghuid=shangjia_id) + shangjia_user = User.query.get(UserUID=shangjia_id) shangjia_profile = shangjia_user.shop_profile shangjia_profile.tuikuan += 1 shangjia_profile.yue += jine @@ -8156,7 +8156,7 @@ class KefuRejectSettlementView(APIView): # 5.3 更新打手扩展表 try: - dashou_user = User.query.get(yonghuid=jiedan_dashou_id) + dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.zhuangtai = 1 # 打手状态设为空闲 dashou_profile.tuikuanliang += 1 # 退款订单总量+1 @@ -8240,7 +8240,7 @@ class KefuTransferHallView(APIView): if order.jiedan_dashou_id: - UserDashou.query.filter(user__yonghuid=order.jiedan_dashou_id).update(zhuangtai=1) + UserDashou.query.filter(user__UserUID=order.jiedan_dashou_id).update(zhuangtai=1) else: pass @@ -8617,7 +8617,7 @@ class KefuUpdateDashouView(APIView): # 3. 查询打手是否存在 try: - dashou_user = User.query.get(yonghuid=dashou_id, user_type='dashou') + dashou_user = User.query.get(UserUID=dashou_id, user_type='dashou') dashou_profile = dashou_user.dashou_profile except User.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}) @@ -9189,11 +9189,11 @@ class KefuPunishmentDetailView(APIView): shensu_images = [] try: # 商家证据图片:上传者为请求人 (qingqiuid) - zhengju_qs = Chufatupian.query.filter(dingdan_id=dingdan_id, yonghuid=record.qingqiuid) + zhengju_qs = Chufatupian.query.filter(dingdan_id=dingdan_id, UserUID=record.qingqiuid) zhengju_images = [item.tupian for item in zhengju_qs if item.tupian] # 打手申诉图片:上传者为打手 (dashouid) - shensu_qs = Chufatupian.query.filter(dingdan_id=dingdan_id, yonghuid=record.dashouid) + shensu_qs = Chufatupian.query.filter(dingdan_id=dingdan_id, UserUID=record.dashouid) shensu_images = [item.tupian for item in shensu_qs if item.tupian] except Exception as e: logger.error(f"查询图片失败: {e}") @@ -9294,7 +9294,7 @@ class KefuPunishmentActionView(APIView): # 可选验证打手存在 if record.dashouid: try: - dashou = UserDashou.query.get(user__yonghuid=record.dashouid) + dashou = UserDashou.query.get(user__UserUID=record.dashouid) except UserDashou.DoesNotExist: return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST) @@ -9315,7 +9315,7 @@ class KefuPunishmentActionView(APIView): # 返还积分 if record.jifen > 0 and record.dashouid: try: - dashou = UserDashou.query.get(user__yonghuid=record.dashouid) + dashou = UserDashou.query.get(user__UserUID=record.dashouid) if dashou.jifen <= 1000: # 安全限制 dashou.jifen += record.jifen dashou.save() @@ -9410,7 +9410,7 @@ class KefuForceCompleteView(APIView): # 5. 给打手结算 try: - dashou_user = User.query.get(yonghuid=dashou_id) + dashou_user = User.query.get(UserUID=dashou_id) dashou_profile = dashou_user.dashou_profile # 原子更新(避免并发重复加) dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1 @@ -9431,7 +9431,7 @@ class KefuForceCompleteView(APIView): # 通过 DingdanShangjia 获取商家ID shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan' if shangjia_ext and shangjia_ext.shangjia_id: - shangjia_user = User.query.get(yonghuid=shangjia_ext.shangjia_id) + shangjia_user = User.query.get(UserUID=shangjia_ext.shangjia_id) shangjia_profile = shangjia_user.shop_profile shangjia_profile.chengjiao = F('chengjiao') + 1 shangjia_profile.save(update_fields=['chengjiao']) @@ -9874,7 +9874,7 @@ class KefuWithdrawDetailView(APIView): # 查询用户主表 try: - user = User.query.get(yonghuid=record.yonghuid) + user = User.query.get(UserUID=record.yonghuid) except User.DoesNotExist: user = None @@ -10047,7 +10047,7 @@ class KefuWithdrawActionView(APIView): raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态') try: - user = User.objects.select_for_update().get(yonghuid=req_yonghuid) + user = User.objects.select_for_update().get(UserUID=req_yonghuid) except User.DoesNotExist: raise ValueError(f'用户{req_yonghuid}不存在') @@ -10100,7 +10100,7 @@ class KefuWithdrawActionView(APIView): raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程') try: - user = User.query.get(yonghuid=tixian.yonghuid) + user = User.query.get(UserUID=tixian.yonghuid) except User.DoesNotExist: raise ValueError(f'用户{tixian.yonghuid}不存在') @@ -10247,7 +10247,7 @@ class KefuPunishView(APIView): # 5. 查询打手 try: - dashou_user = User.query.get(yonghuid=dashou_id) + dashou_user = User.query.get(UserUID=dashou_id) dashou = dashou_user.dashou_profile except User.DoesNotExist: return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -10329,7 +10329,7 @@ class AdKftjView(APIView): timestamp = str(int(time.time()))[-5:] rand = str(random.randint(0, 99)).zfill(2) uid = timestamp + rand - if len(uid) == 7 and not User.query.filter(yonghuid=uid).exists(): + if len(uid) == 7 and not User.query.filter(UserUID=uid).exists(): return uid raise Exception('无法生成唯一用户ID') @@ -10645,7 +10645,7 @@ class WechatLoginAndGuanshiRegisterView(APIView): random_part = str(random.randint(0, 99)).zfill(2) user_id = timestamp_part + random_part if len(user_id) == 7 and user_id.isdigit(): - if not User.query.filter(yonghuid=user_id).exists(): + if not User.query.filter(UserUID=user_id).exists(): return user_id raise Exception('生成用户ID失败') @@ -11931,7 +11931,7 @@ class FaKuanPayPollView(APIView): if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) - order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) + order = Czjilu.query.get(dingdan_id=dingdanid, UserUID=request.user.yonghuid) if order.zhuangtai == 3: return Response({'code': 200, 'message': '支付成功'}) else: @@ -11954,7 +11954,7 @@ class FaKuanPayFailView(APIView): if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) - order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) + order = Czjilu.query.get(dingdan_id=dingdanid, UserUID=request.user.yonghuid) if order.zhuangtai == 9: # 未支付 order.delete() logger.info(f"用户{request.user.yonghuid}删除未支付订单: {dingdanid}") @@ -12004,7 +12004,7 @@ class KaohePayView(APIView): # 1. 检查是否有未支付的考核订单 """unpaid_order = Czjilu.query.filter( - yonghuid=user.yonghuid, + UserUID=user.yonghuid, leixing=5, zhuangtai=9 ).exists() @@ -12366,10 +12366,10 @@ class KaohePayCallbackView(View): # 从临时表获取业务参数并创建审核记录 try: - temp = KaohePayTemp.query.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid) + temp = KaohePayTemp.query.get(dingdan_id=out_trade_no, UserUID=order.yonghuid) logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}") - user = User.query.get(yonghuid=order.yonghuid) + user = User.query.get(UserUID=order.yonghuid) logger.info(f"获取用户信息成功: {user.yonghuid}") success, msg, record = create_shenhe_jilu_from_temp(user, temp) @@ -12486,7 +12486,7 @@ class KaohePayPollView(APIView): if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) try: - order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) + order = Czjilu.query.get(dingdan_id=dingdanid, UserUID=request.user.yonghuid, leixing=5) if order.zhuangtai == 3: return Response({'code': 200, 'message': '支付成功'}) return Response({'code': 400, 'message': '订单尚未支付'}) @@ -12503,7 +12503,7 @@ class KaohePayFailView(APIView): if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) try: - order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) + order = Czjilu.query.get(dingdan_id=dingdanid, UserUID=request.user.yonghuid, leixing=5) if order.zhuangtai == 9: order.delete() return Response({'code': 200, 'message': '订单已删除'}) @@ -12676,7 +12676,7 @@ class GuanshiContactView(APIView): # 4. 根据邀请人用户ID查询用户主表 try: - inviter_user = User.query.get(yonghuid=inviter_yonghuid) + inviter_user = User.query.get(UserUID=inviter_yonghuid) except User.DoesNotExist: logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}") return Response({ @@ -12855,7 +12855,7 @@ class TixianQueRenAutoView(APIView): try: auto_record = TixianAutoRecord.objects.select_for_update().get( tixian_id=tixian_id, - yonghuid=user_main.yonghuid, + UserUID=user_main.yonghuid, ) except TixianAutoRecord.DoesNotExist: return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND) diff --git a/utils/chat_utils.py b/utils/chat_utils.py index c9e2c2f..129eaaa 100644 --- a/utils/chat_utils.py +++ b/utils/chat_utils.py @@ -1,198 +1,198 @@ -# utils/chat_utils.py -import json -import logging -import requests -from django.conf import settings -from orders.models import Dingdan, DingdanPingtai, DingdanShangjia -from users.models import UserDashou, UserBoss, UserShangjia -from gvsdsdk.models import User - -logger = logging.getLogger('chat_utils') - -# ========== 辅助配置获取 ========== -def _get_self_goeasy_appkey(): - return getattr(settings, 'GOEASY_APPKEY', '') - -def _get_self_goeasy_secret(): - return getattr(settings, 'GOEASY_SECRET', '') - -# ========== 头像拼接 ========== -def _full_local_avatar(relative_url): - if not relative_url: - return '' - if relative_url.startswith('http'): - return relative_url - return relative_url - -# ========== GoEasy 订阅 ========== -def _subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None): - if not appkey: - appkey = _get_self_goeasy_appkey() - if not secret: - secret = _get_self_goeasy_secret() - if not appkey: - logger.error("GoEasy AppKey 未配置") - return False - url = 'https://rest-hangzhou.goeasy.io/v2/im/subscribe-groups' - body = {"appkey": appkey, "userIds": user_ids, "groupIds": group_ids} - headers = {"Content-Type": "application/json"} - if secret: - headers["Authorization"] = f"Bearer {secret}" - try: - resp = requests.post(url, headers=headers, json=body, timeout=10) - if resp.status_code == 200: - return True - logger.error(f"订阅失败: {resp.status_code} {resp.text}") - return False - except Exception as e: - logger.error(f"订阅异常: {e}", exc_info=True) - return False - -# ========== 发送群消息(完整参数) ========== -def _send_group_message(appkey, secret, group_id, sender_id, sender_name, sender_avatar, - message_text, custom_payload=None, group_name=None, order_id=None): - if not appkey: - appkey = _get_self_goeasy_appkey() - if not secret: - secret = _get_self_goeasy_secret() - if not appkey: - logger.error("GoEasy AppKey 未配置") - return False - - url = 'https://rest-hangzhou.goeasy.io/v2/im/message' - - # to.data 包含群聊展示信息及业务字段 - to_data = { - "name": group_name or group_id, - "avatar": sender_avatar or "", - } - if order_id: - to_data["orderId"] = order_id - - payload_str = custom_payload.get("text", message_text) if custom_payload else message_text - - request_body = { - "appkey": appkey, - "senderId": sender_id, - "senderData": {"avatar": sender_avatar, "name": sender_name}, - "to": { - "type": "group", - "id": group_id, - "data": to_data - }, - "type": "text", - "payload": payload_str - } - headers = {"Content-Type": "application/json"} - if secret: - headers["Authorization"] = f"Bearer {secret}" - try: - resp = requests.post(url, headers=headers, json=request_body, timeout=10) - if resp.status_code == 200: - return True - logger.error(f"群聊消息发送失败,状态码:{resp.status_code},响应:{resp.text}") - return False - except Exception as e: - logger.error(f"发送群聊消息异常: {e}", exc_info=True) - return False - -# ========== 核心入口 ========== -def establish_order_chat(dingdan_id): - try: - order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=dingdan_id) - except Dingdan.DoesNotExist: - logger.error(f"订单 {dingdan_id} 不存在") - return False - - dashou_uid = order.jiedan_dashou_id - dashou_goeasy_id = f"Ds{dashou_uid}" - dashou_nickname = f'打手{dashou_uid[:6]}' - dashou_avatar_relative = '' - try: - dashou_user = User.objects.get(yonghuid=dashou_uid) - dashou_avatar_relative = dashou_user.avatar or '' - dashou_profile = UserDashou.objects.get(user=dashou_user) - if dashou_profile.nicheng: - dashou_nickname = dashou_profile.nicheng - except Exception as e: - logger.warning(f"获取打手信息失败: {e}") - dashou_avatar = _full_local_avatar(dashou_avatar_relative) - - appkey = _get_self_goeasy_appkey() - secret = _get_self_goeasy_secret() - if not appkey: - logger.error("GoEasy AppKey 未配置") - return False - - # 本地订单处理 - return _handle_local_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret) - -# ========== 普通订单 / 我方派单 ========== -def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): - group_id = f"group_{order.dingdan_id}" - group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else (order.jieshao or f"订单{order.dingdan_id}") - group_avatar = _full_local_avatar(order.tupian) if order.tupian else _full_local_avatar('') - - partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order) - if not partner_goeasy_id: - return False - - # 订阅双方 - if not _subscribe_users_to_group([dashou_goeasy_id, partner_goeasy_id], [group_id], appkey, secret): - return False - - # 1. 打手发送初始化消息 - init_msg_text = f"订单已接单,内容:{order.jieshao},备注:{order.beizhu},游戏ID:{order.nicheng}" - success1 = _send_group_message( - appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar, - init_msg_text, None, group_name=group_name, order_id=order.dingdan_id - ) - - # 2. 【新增】派单方也发一条消息,确保他也能看到群聊 - partner_msg = f"订单已确认,请开始服务。" - success2 = _send_group_message( - appkey, secret, group_id, partner_goeasy_id, partner_name, partner_avatar, - partner_msg, None, group_name=group_name, order_id=order.dingdan_id - ) - - return success1 and success2 - -def _get_local_partner_info(order): - """本地订单下单方:老板或商家""" - if order.fadan_pingtai == 1: # 老板 - try: - ext = order.pingtai_kuozhan - laoban_id = ext.laoban_id - avatar_full = _full_local_avatar('') - nickname = f'老板{laoban_id[:6]}' - try: - boss_user = User.objects.get(yonghuid=laoban_id) - avatar_full = _full_local_avatar(boss_user.avatar or '') - boss_profile = UserBoss.objects.get(user=boss_user) - if boss_profile.nickname: - nickname = boss_profile.nickname - except Exception: - pass - return f"Boss{laoban_id}", nickname, avatar_full - except Exception as e: - logger.error(f"获取老板信息失败: {e}") - return None, None, None - elif order.fadan_pingtai == 2: # 商家 - try: - ext = order.shangjia_kuozhan - shangjia_id = ext.shangjia_id - nickname = ext.sjnicheng or f'商家{shangjia_id[:6]}' - avatar_full = _full_local_avatar('') - try: - sj_user = User.objects.get(yonghuid=shangjia_id) - avatar_full = _full_local_avatar(sj_user.avatar or '') - except Exception: - pass - return f"Sj{shangjia_id}", nickname, avatar_full - except Exception as e: - logger.error(f"获取商家信息失败: {e}") - return None, None, None - else: - if order.user1_id: - return order.user1_id, "用户", _full_local_avatar('') +# utils/chat_utils.py +import json +import logging +import requests +from django.conf import settings +from orders.models import Dingdan, DingdanPingtai, DingdanShangjia +from users.models import UserDashou, UserBoss, UserShangjia +from gvsdsdk.models import User + +logger = logging.getLogger('chat_utils') + +# ========== 辅助配置获取 ========== +def _get_self_goeasy_appkey(): + return getattr(settings, 'GOEASY_APPKEY', '') + +def _get_self_goeasy_secret(): + return getattr(settings, 'GOEASY_SECRET', '') + +# ========== 头像拼接 ========== +def _full_local_avatar(relative_url): + if not relative_url: + return '' + if relative_url.startswith('http'): + return relative_url + return relative_url + +# ========== GoEasy 订阅 ========== +def _subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None): + if not appkey: + appkey = _get_self_goeasy_appkey() + if not secret: + secret = _get_self_goeasy_secret() + if not appkey: + logger.error("GoEasy AppKey 未配置") + return False + url = 'https://rest-hangzhou.goeasy.io/v2/im/subscribe-groups' + body = {"appkey": appkey, "userIds": user_ids, "groupIds": group_ids} + headers = {"Content-Type": "application/json"} + if secret: + headers["Authorization"] = f"Bearer {secret}" + try: + resp = requests.post(url, headers=headers, json=body, timeout=10) + if resp.status_code == 200: + return True + logger.error(f"订阅失败: {resp.status_code} {resp.text}") + return False + except Exception as e: + logger.error(f"订阅异常: {e}", exc_info=True) + return False + +# ========== 发送群消息(完整参数) ========== +def _send_group_message(appkey, secret, group_id, sender_id, sender_name, sender_avatar, + message_text, custom_payload=None, group_name=None, order_id=None): + if not appkey: + appkey = _get_self_goeasy_appkey() + if not secret: + secret = _get_self_goeasy_secret() + if not appkey: + logger.error("GoEasy AppKey 未配置") + return False + + url = 'https://rest-hangzhou.goeasy.io/v2/im/message' + + # to.data 包含群聊展示信息及业务字段 + to_data = { + "name": group_name or group_id, + "avatar": sender_avatar or "", + } + if order_id: + to_data["orderId"] = order_id + + payload_str = custom_payload.get("text", message_text) if custom_payload else message_text + + request_body = { + "appkey": appkey, + "senderId": sender_id, + "senderData": {"avatar": sender_avatar, "name": sender_name}, + "to": { + "type": "group", + "id": group_id, + "data": to_data + }, + "type": "text", + "payload": payload_str + } + headers = {"Content-Type": "application/json"} + if secret: + headers["Authorization"] = f"Bearer {secret}" + try: + resp = requests.post(url, headers=headers, json=request_body, timeout=10) + if resp.status_code == 200: + return True + logger.error(f"群聊消息发送失败,状态码:{resp.status_code},响应:{resp.text}") + return False + except Exception as e: + logger.error(f"发送群聊消息异常: {e}", exc_info=True) + return False + +# ========== 核心入口 ========== +def establish_order_chat(dingdan_id): + try: + order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + logger.error(f"订单 {dingdan_id} 不存在") + return False + + dashou_uid = order.jiedan_dashou_id + dashou_goeasy_id = f"Ds{dashou_uid}" + dashou_nickname = f'打手{dashou_uid[:6]}' + dashou_avatar_relative = '' + try: + dashou_user = User.objects.get(UserUID=dashou_uid) + dashou_avatar_relative = dashou_user.avatar or '' + dashou_profile = UserDashou.objects.get(user=dashou_user) + if dashou_profile.nicheng: + dashou_nickname = dashou_profile.nicheng + except Exception as e: + logger.warning(f"获取打手信息失败: {e}") + dashou_avatar = _full_local_avatar(dashou_avatar_relative) + + appkey = _get_self_goeasy_appkey() + secret = _get_self_goeasy_secret() + if not appkey: + logger.error("GoEasy AppKey 未配置") + return False + + # 本地订单处理 + return _handle_local_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret) + +# ========== 普通订单 / 我方派单 ========== +def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): + group_id = f"group_{order.dingdan_id}" + group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else (order.jieshao or f"订单{order.dingdan_id}") + group_avatar = _full_local_avatar(order.tupian) if order.tupian else _full_local_avatar('') + + partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order) + if not partner_goeasy_id: + return False + + # 订阅双方 + if not _subscribe_users_to_group([dashou_goeasy_id, partner_goeasy_id], [group_id], appkey, secret): + return False + + # 1. 打手发送初始化消息 + init_msg_text = f"订单已接单,内容:{order.jieshao},备注:{order.beizhu},游戏ID:{order.nicheng}" + success1 = _send_group_message( + appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar, + init_msg_text, None, group_name=group_name, order_id=order.dingdan_id + ) + + # 2. 【新增】派单方也发一条消息,确保他也能看到群聊 + partner_msg = f"订单已确认,请开始服务。" + success2 = _send_group_message( + appkey, secret, group_id, partner_goeasy_id, partner_name, partner_avatar, + partner_msg, None, group_name=group_name, order_id=order.dingdan_id + ) + + return success1 and success2 + +def _get_local_partner_info(order): + """本地订单下单方:老板或商家""" + if order.fadan_pingtai == 1: # 老板 + try: + ext = order.pingtai_kuozhan + laoban_id = ext.laoban_id + avatar_full = _full_local_avatar('') + nickname = f'老板{laoban_id[:6]}' + try: + boss_user = User.objects.get(UserUID=laoban_id) + avatar_full = _full_local_avatar(boss_user.avatar or '') + boss_profile = UserBoss.objects.get(user=boss_user) + if boss_profile.nickname: + nickname = boss_profile.nickname + except Exception: + pass + return f"Boss{laoban_id}", nickname, avatar_full + except Exception as e: + logger.error(f"获取老板信息失败: {e}") + return None, None, None + elif order.fadan_pingtai == 2: # 商家 + try: + ext = order.shangjia_kuozhan + shangjia_id = ext.shangjia_id + nickname = ext.sjnicheng or f'商家{shangjia_id[:6]}' + avatar_full = _full_local_avatar('') + try: + sj_user = User.objects.get(UserUID=shangjia_id) + avatar_full = _full_local_avatar(sj_user.avatar or '') + except Exception: + pass + return f"Sj{shangjia_id}", nickname, avatar_full + except Exception as e: + logger.error(f"获取商家信息失败: {e}") + return None, None, None + else: + if order.user1_id: + return order.user1_id, "用户", _full_local_avatar('') return None, None, None \ No newline at end of file diff --git a/verify_fixes.py b/verify_fixes.py new file mode 100644 index 0000000..38f65c9 --- /dev/null +++ b/verify_fixes.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""验证所有兼容性修复""" +import os; os.environ['DJANGO_SETTINGS_MODULE']='a_long_dianjing.settings' +import django; django.setup() +from gvsdsdk.models import User +from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework_simplejwt.authentication import JWTAuthentication +from unittest.mock import Mock + +u = User.objects.filter(UserName__isnull=False).first() +print(f'测试用户: {u.UserName}, UserUID: {u.UserUID}') + +# 1. JWT 认证 +refresh = RefreshToken.for_user(u) +access = refresh.access_token +print('1. JWT Token 生成成功') + +auth = JWTAuthentication() +request = Mock() +request.META = {'HTTP_AUTHORIZATION': f'Bearer {access}'} +request._request = request +validated_user, _ = auth.authenticate(request) +print(f' JWT 验证成功: {validated_user.UserName}') + +# 2. yonghuid 兼容属性 +print(f'2. yonghuid: {u.yonghuid}') + +# 3. dashou_profile +dp = u.dashou_profile +print(f'3. dashou_profile: {dp}') + +# 4. shoujihao_renzheng +print(f'4. shoujihao_renzheng: {u.shoujihao_renzheng}') + +# 5. user_type +print(f'5. user_type: {u.user_type}') + +# 6. User.query.get(UserUID=...) +u2 = User.query.get(UserUID=u.UserUID) +print(f'6. User.query.get(UserUID=...) OK: {u2.UserName}') + +# 7. user.id +print(f'7. user.id: {u.id}, bool: {bool(u.id)}') + +# 8. has_role +print(f'8. has_role(dashou): {u.has_role("dashou")}') + +print() +print('所有兼容性测试通过!')