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}{k}>')
- 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}{k}>')
+ 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('所有兼容性测试通过!')