3114 lines
127 KiB
Python
3114 lines
127 KiB
Python
# ==================== 标准库 ====================
|
||
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 = ['<xml>']
|
||
for k, v in data.items():
|
||
xml.append(f'<{k}>{v}</{k}>')
|
||
xml.append('</xml>')
|
||
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()}")
|
||
return Response({'code': 500, 'msg': '服务器内部错误'}) |