Files
a_long/shangdian/views.py
2026-06-14 02:38:05 +08:00

3369 lines
139 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.db import models
from utils.oss_utils import upload_to_oss, delete_from_oss
from yonghu.models import UserMain, UserDashou, UserShangjia, UserGuanshi, UserZuzhang
from shangdian.models import Dianpu, YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
from shangpin.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin
from rest_framework.permissions import AllowAny
from django.utils import timezone
from rest_framework_simplejwt.tokens import RefreshToken
from yonghu.models import UserMain
from .models import YonghuPingzheng, Dianpu
import logging
# shangdian/views.py 继续添加
import io
import time
import requests
from django.conf import settings
from django.core.cache import cache
from django.db import transaction
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from .utils import verify_shop_permission
from .models import Dianpu
from django.db.models import F
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': '用户身份信息异常'})
# 先获取 UserMain 对象(不加锁,仅用于校验存在性)
try:
user_main = UserMain.objects.get(yonghuid=user.yonghuid)
except UserMain.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
# 校验店铺是否存在且状态正常(不加锁)
try:
dianpu = Dianpu.objects.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 = UserMain.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.objects.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 = UserMain.objects.get(yonghuid=user.yonghuid)
except UserMain.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
# 1. 查询用户店铺绑定关系
binding = YonghuDianpuBangding.objects.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.objects.filter(user=user_main).exists() or
UserShangjia.objects.filter(user=user_main).exists() or
UserGuanshi.objects.filter(user=user_main).exists() or
UserZuzhang.objects.filter(user=user_main).exists())
def _get_shop_goods(self, dianpu):
"""
获取店铺专属商品
类型、专区、商品均从店铺相关表中获取且需通过平台审核shenhe_zhuangtai=True
"""
# 1. 店铺商品类型:未封禁、已上架、平台审核通过
leixing_queryset = ShangpinLeixingDianpu.objects.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.objects.filter(
dianpu=dianpu,
dianpu_leixing_id__in=leixing_ids,
fengjin_zhuangtai=False,
shangjia_zhuangtai=True
).order_by('-paixu', 'id')
else:
zhuanqu_queryset = ShangpinZhuanqu.objects.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.objects.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.objects.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.objects.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.objects.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.objects.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 : 用户ID7位
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 = UserMain.objects.get(yonghuid=yonghuid)
except UserMain.DoesNotExist:
logger.warning(f"登录失败用户ID不存在 - {yonghuid}")
return Response({'code': 404, 'msg': '账号或密码错误'})
# 4. 通过用户ID + 账号查询凭证表
try:
pingzheng = YonghuPingzheng.objects.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 ShangdianLoginView(APIView):
"""
商家后台登录接口(安全修正版)
POST /shangdian/login
请求参数:
yonghuid : 用户ID7位
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 = UserMain.objects.get(yonghuid=yonghuid)
except UserMain.DoesNotExist:
logger.warning(f"登录失败用户ID不存在 - {yonghuid}")
return Response({'code': 404, 'msg': '用户不存在'})
# 4. 【第二步】通过用户ID + 账号查询凭证表(防止用他人账号登录)
try:
pingzheng = YonghuPingzheng.objects.get(
yonghu=yonghuid, # 关键必须匹配当前用户ID
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. 【第三步】通过用户ID查询绑定关系表获取店铺ID
try:
binding = YonghuDianpuBangding.objects.select_related('dianpu').get(yonghu=user_main)
dianpu = binding.dianpu
except YonghuDianpuBangding.DoesNotExist:
logger.error(f"登录异常:用户 {yonghuid} 未绑定任何店铺")
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关联 UserMain
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})'''
# shangdian/views.py 追加
from django.db.models import Sum
from shangdian.models import DianpuShouzhiMeiriTongji
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.objects.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
import calendar
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 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
})
# 新增导入
from django.http import HttpResponse
from utils.oss_utils import get_oss_client # 使用您已有的 COS 客户端获取方法
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 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
from django.db.models import Count
from rest_framework.views import APIView
from shangpin.models import ShangpinLeixing
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.objects.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.objects.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.objects.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.objects.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.objects.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,
}
})
from django.db import transaction
from utils.oss_utils import delete_from_oss # 引入文件删除工具
# shangdian/views.py
import logging
from django.db import models, transaction
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.objects.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.objects.filter(id=1).first()
shenhe_status = not (audit_config and audit_config.kaiqi_shenhe)
ShangpinLeixingDianpu.objects.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.objects.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.objects.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.objects.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.objects.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.objects.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.objects.filter(id=leixing_id, shenhezhuangtai=1).exists():
return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
ShangpinZhuanqu.objects.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.objects.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.objects.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.objects.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': '删除失败,请稍后重试'})
from django.core.paginator import Paginator
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.objects.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/xgdpsplx
权限: JWT认证 + 店铺验证
"""
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)
elif action == 'add_product':
return self._add_product(dianpu, request.data)
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):
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
# 标题
if 'biaoqian' in data:
product.biaoqian = data['biaoqian']
update_fields.append('biaoqian')
content_changed = True
# 价格
if 'jiage' in data:
product.jiage = data['jiage']
update_fields.append('jiage')
content_changed = True
# 库存
if 'kucun' in data:
product.kucun = data['kucun']
update_fields.append('kucun')
content_changed = True
# 店铺商品类型
if 'dianpu_leixing_id' in data:
# 验证类型属于当前店铺
new_type_id = data['dianpu_leixing_id']
if not ShangpinLeixingDianpu.objects.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 and not ShangpinZhuanqu.objects.filter(id=new_zq_id, dianpu_id=dianpu.id).exists():
return Response({'code': 400, 'msg': '无效的专区'})
product.zhuanqu_id = new_zq_id or 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 = int(data['yaoqiuleixing'])
if yqlx not in [1, 2]:
return Response({'code': 400, 'msg': '抢单要求类型无效'})
product.yaoqiuleixing = yqlx
update_fields.append('yaoqiuleixing')
content_changed = True
# 会员ID
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 = data['yongjin'] or 0
update_fields.append('yongjin')
content_changed = True
# 打手额外分成金额(不能超过商品价格)
if 'ewai_dashou_fencheng' in data:
fencheng = data['ewai_dashou_fencheng'] or 0
current_price = product.jiage
if fencheng > current_price:
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 = data['paixu']
update_fields.append('paixu')
# 上架状态
if 'shangjia_zhuangtai' in data:
product.shangjia_zhuangtai = data['shangjia_zhuangtai']
update_fields.append('shangjia_zhuangtai')
# 商品图片(上传新图则替换旧图)
if 'tupian_url' in data and data['tupian_url'] != product.tupian_url:
# 删除旧图
if product.tupian_url:
delete_from_oss(product.tupian_url)
product.tupian_url = data['tupian_url']
update_fields.append('tupian_url')
content_changed = True
# 规则图片
if 'guize_tupian' in data:
new_rule = data['guize_tupian'] or ''
if new_rule != (product.guize_tupian or ''):
if product.guize_tupian:
delete_from_oss(product.guize_tupian)
product.guize_tupian = new_rule if new_rule else None
update_fields.append('guize_tupian')
content_changed = True
# 检查全局审核模式:若开启且内容有修改,则标记为未审核
if content_changed:
audit_config = DianpuShangpinShenheShezhi.objects.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):
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.objects.get(id=dianpu_leixing_id, dianpu_id=dianpu.id)
except ShangpinLeixingDianpu.DoesNotExist:
return Response({'code': 400, 'msg': '无效的店铺商品类型'})
# 获取映射的公共商品类型
try:
public_type = ShangpinLeixing.objects.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1)
except ShangpinLeixing.DoesNotExist:
return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
# 抢单要求默认会员若用户选择了会员但未指定则用公共类型的会员ID
yaoqiuleixing = data.get('yaoqiuleixing', 1)
if yaoqiuleixing == 1:
huiyuan_id = data.get('huiyuan_id') or public_type.huiyuan_id or None
yongjin = None
else:
huiyuan_id = None
yongjin = data.get('yongjin') or public_type.yongjin or 0
# 打手额外分成金额不能大于价格没传则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.objects.filter(id=1).first()
if audit_config and audit_config.kaiqi_shenhe:
shenhe_status = False
# 创建商品
product = Shangpin.objects.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, # 公共商品类型ID
jieshao=jieshao,
xiadan_xuzhi=data.get('xiadan_xuzhi', ''),
guize_tupian=data.get('guize_tupian') or None,
tupian_url=data.get('tupian_url') or '', # 商品图片必须由前端上传后传入
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': '商品不存在'})
# 删除关联图片OSS
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': '商品已删除'})
from shangpin.models import Huiyuan # 会员模型,根据实际路径调整
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.objects.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
})
from django.db import transaction, IntegrityError
from django.core.paginator import Paginator, EmptyPage
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.objects.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 UserMain.objects.filter(yonghuid=user_uid).exists():
return Response({'code': 404, 'msg': '对应的小程序用户不存在'})
# 检查账号是否已占用
if YonghuPingzheng.objects.filter(zhanghao=zhanghao).exists():
return Response({'code': 400, 'msg': '登录账号已存在'})
if YonghuPingzheng.objects.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.objects.create(
yonghu=user_uid,
zhanghao=zhanghao,
mima=mima,
is_active=True
)
# 创建店铺,关联上级
dianpu = Dianpu.objects.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. 订单列表:支持筛选、分页、返回订单详情及打手图片(含跨平台图片拉取)
"""
from concurrent.futures import ThreadPoolExecutor, as_completed
from django.db.models import Count, Q, Prefetch
from django.core.paginator import Paginator, EmptyPage
# 已存在的验证函数
from dingdan.models import Dingdan, DingdanPingtai, Dashoutupian
from peizhi.models import ClubConfig, Club # 跨平台相关模型,按实际路径调整
from collections import defaultdict
# 如果有 ClubConfig 模型用于获取存储桶域名,确保导入(如没有可忽略,使用 settings 中的 OSS 域名)
try:
from kuaping.models import ClubConfig
except ImportError:
ClubConfig = None
# ================================================================
# 视图1订单统计接口
# ================================================================
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'})
# 2. 验证店铺身份,获取店铺对象
dianpu, error = verify_shop_permission(request, user_id)
if error:
return error
shop_id = dianpu.id
# 3. 获取所有正常的商品类型(审核状态=1
types_qs = ShangpinLeixing.objects.filter(
shenhezhuangtai=1
).order_by('-paixu', 'id')
# 构建类型列表,初始化各状态计数为 0
type_list = []
for t in types_qs:
type_list.append({
'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
}
})
# 4. 获取本店铺所有订单 ID通过平台扩展表
order_ids = list(
DingdanPingtai.objects.filter(dianpu_id=shop_id)
.values_list('dingdan_id', flat=True)
)
total_orders = 0 # 订单总量
global_counts = defaultdict(int) # 全局各状态计数
if order_ids:
# 5. 按商品类型和订单状态聚合统计
agg = (
Dingdan.objects
.filter(dingdan_id__in=order_ids)
.values('leixing_id', 'zhuangtai')
.annotate(cnt=Count('id'))
)
for row in agg:
lid = row['leixing_id'] # 商品类型ID
st = str(row['zhuangtai']) # 状态码 -> 字符串
cnt = row['cnt']
# 累加总量与全局状态
total_orders += cnt
global_counts[st] += cnt
# 填充到对应商品类型
for tp in type_list:
if tp['id'] == lid:
tp['order_count'] += cnt
if st in tp['status_counts']:
tp['status_counts'][st] += cnt
break
# 6. 保证全局统计返回所有8个状态
all_status_keys = ['1', '7', '2', '8', '3', '4', '5', '6']
final_global = {k: global_counts.get(k, 0) for k in all_status_keys}
return Response({
'code': 200,
'msg': '统计成功',
'data': {
'types': type_list,
'status_counts': final_global,
'total_orders': total_orders
}
})
def get_oss_domain():
"""
获取对象存储的域名(用于拼接本地打手图片完整 URL
优先从 ClubConfig 获取,其次从 settings.OSS_URL 获取
"""
if ClubConfig:
my_config = ClubConfig.objects.filter(is_self=1).first()
if my_config and my_config.storage_bucket_domain:
return my_config.storage_bucket_domain.rstrip('/') + '/'
# 降级使用 settings 中的 OSS URL
return getattr(settings, 'OSS_URL', '').rstrip('/') + '/'
def _fetch_partner_images_for_order(order, oss_domain):
"""
根据单个订单获取打手提交的图片列表(完整绝对 URL
处理本地图片 + 跨平台图片(重试拉取对方平台图片)
返回: (images_list, is_punished, punishment_info)
"""
images = []
is_punished = False
punishment_info = {}
# 1. 本地打手图片(我方平台的打手提交)
if order.jiedan_dashou_id:
local_imgs = Dashoutupian.objects.filter(
dingdan_id=order.dingdan_id,
dashou=order.jiedan_dashou_id
).values_list('tupian', flat=True)
for rel_path in local_imgs:
if rel_path:
if rel_path.startswith('http'):
images.append(rel_path)
else:
images.append(oss_domain + rel_path.lstrip('/'))
# 2. 如果订单是跨平台且需要获取对方打手图片
if order.is_cross and order.partner_club_id and order.partner_order_id:
try:
my_club = ClubConfig.objects.filter(is_self=1).first()
if not my_club:
logger.error("无法获取本俱乐部配置")
return images, is_punished, punishment_info
self_club_id = my_club.club_id
# 获取合作关系
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=order.partner_club_id)
partner_domain = club_rel.partner_domain.rstrip('/')
# 构建对方接口URL
url = partner_domain + '/houtai/kpthqtp'
payload = {'order_id': order.partner_order_id, 'club_id': self_club_id}
retries = 2
for attempt in range(retries):
try:
resp = requests.post(url, json=payload, timeout=5)
if resp.status_code == 200:
data = resp.json()
if data.get('code') == 0:
partner_data = data.get('data', {})
partner_imgs = partner_data.get('images', [])
# 对方返回的图片可能是相对路径,拼接对方域名
for img in partner_imgs:
if img.startswith('http'):
images.append(img)
else:
images.append(partner_domain + '/' + img.lstrip('/'))
punishment_info = partner_data.get('punishment', {})
is_punished = bool(punishment_info.get('cfliyou'))
break
else:
logger.warning(f"对方接口HTTP错误: {resp.status_code}")
except Exception as e:
logger.warning(f"请求对方图片失败(重试{attempt+1}: {e}")
except Exception as e:
logger.error(f"获取跨平台图片异常: {e}")
return images, is_punished, punishment_info
def _get_images_for_orders(orders, oss_domain, max_workers=10):
"""
并发获取多个订单的打手图片,返回字典 {dingdan_id: (images, is_punished, punishment_info)}
"""
result = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_order = {
executor.submit(_fetch_partner_images_for_order, order, oss_domain): order.dingdan_id
for order in orders
}
for future in as_completed(future_to_order):
dingdan_id = future_to_order[future]
try:
images, is_punished, pun_info = future.result()
result[dingdan_id] = (images, is_punished, pun_info)
except Exception as e:
logger.error(f"获取订单 {dingdan_id} 图片失败: {e}")
result[dingdan_id] = ([], False, {})
return result
class ShopOrderManageView(APIView):
"""
店铺订单管理与统计接口
URL: /shangdian/ddlbhq
方法: POST
权限: JWT认证 + 店铺身份验证 (verify_shop_permission)
请求参数:
- userId (必填): 当前店铺对应的小程序用户ID
- action (可选): 值为 'statistics' 时返回统计数据;其他任意值或省略均返回订单列表
统计模式 (action='statistics') 返回:
types: 商品类型列表,每个类型包含:
id, jieshao, tupian_url, order_count, status_counts
status_counts: 各状态订单总数 (全局)
total_orders: 订单总量
列表模式 (默认) 支持筛选、分页、搜索,返回:
list: 订单列表,每个订单包含:
dingdan_id, zhuangtai, jine, dashou_fencheng, jiedan_dashou_id, dashou_liuyan,
zhiding_id, shangpin_id, tupian, jieshao, beizhu, nicheng, leixing_id,
create_time, update_time, pingtai_kuozhan (laoban_id, laoban_pingjia, dianpu_shouyi),
dashou_images (绝对URL数组), is_punished, is_fadaned
total: 总条数
page: 当前页码
page_size: 每页条数
"""
permission_classes = [IsAuthenticated]
def post(self, request):
user_id = request.data.get('userId', '').strip()
if not user_id:
return Response({'code': 400, 'msg': '缺少 userId'})
# 验证店铺身份
dianpu, error_response = verify_shop_permission(request, user_id)
if error_response:
return error_response
action = request.data.get('action', '').strip().lower()
if action == 'statistics':
return self._handle_statistics(dianpu)
else:
return self._handle_order_list(request, dianpu)
def _handle_statistics(self, dianpu):
"""统计逻辑:商品类型计数 + 各状态订单数 + 总量"""
shop_id = dianpu.id
# 1. 获取正常商品类型(审核状态=1
types_qs = ShangpinLeixing.objects.filter(shenhezhuangtai=1).order_by('-paixu', 'id')
types_data = []
type_ids = []
for t in types_qs:
types_data.append({
'id': t.id,
'jieshao': t.jieshao or '',
'tupian_url': t.tupian_url or '',
'order_count': 0, # 之后填充
'status_counts': {} # 该类型下各状态计数
})
type_ids.append(t.id)
# 2. 获取本店铺所有订单 ID
order_ids = DingdanPingtai.objects.filter(
dianpu_id=shop_id
).values_list('dingdan_id', flat=True)
# 统计结构
type_status_map = defaultdict(lambda: defaultdict(int))
global_status_counts = defaultdict(int)
total_orders = 0
if order_ids:
# 批量聚合统计
aggregation = Dingdan.objects.filter(
dingdan_id__in=order_ids
).values('leixing_id', 'zhuangtai').annotate(cnt=Count('id'))
for row in aggregation:
lx_id = row['leixing_id']
zt = row['zhuangtai']
cnt = row['cnt']
if lx_id in type_ids:
type_status_map[lx_id][zt] += cnt
global_status_counts[zt] += cnt
total_orders += cnt
# 填充统计数据回类型列表
for t in types_data:
tid = t['id']
t['order_count'] = sum(type_status_map[tid].values())
t['status_counts'] = dict(type_status_map[tid])
return Response({
'code': 200,
'msg': '获取成功',
'data': {
'types': types_data,
'status_counts': dict(global_status_counts),
'total_orders': total_orders
}
})
def _handle_order_list(self, request, dianpu):
"""列表逻辑:筛选 + 分页 + 搜索 + 图片并发获取"""
shop_id = dianpu.id
# ---------- 解析筛选参数 ----------
leixing_id = request.data.get('leixing_id')
zhuangtai = request.data.get('zhuangtai')
dingdan_id = request.data.get('dingdan_id', '').strip()
jiedan_dashou_id = request.data.get('jiedan_dashou_id', '').strip()
nicheng = request.data.get('nicheng', '').strip()
laoban_id = request.data.get('laoban_id', '').strip()
jieshao = request.data.get('jieshao', '').strip()
page = request.data.get('page', 1)
page_size = request.data.get('page_size', 20)
# ---------- 基础查询:店铺的订单(通过扩展表关联) ----------
queryset = DingdanPingtai.objects.filter(
dianpu_id=shop_id
).select_related('dingdan').order_by('-dingdan__create_time')
# 应用筛选
if leixing_id is not None:
queryset = queryset.filter(dingdan__leixing_id=leixing_id)
if zhuangtai is not None:
queryset = queryset.filter(dingdan__zhuangtai=zhuangtai)
if dingdan_id:
queryset = queryset.filter(dingdan__dingdan_id__icontains=dingdan_id)
if jiedan_dashou_id:
queryset = queryset.filter(dingdan__jiedan_dashou_id=jiedan_dashou_id)
if nicheng:
queryset = queryset.filter(dingdan__nicheng__icontains=nicheng)
if laoban_id:
queryset = queryset.filter(laoban_id=laoban_id) # 扩展表字段
if jieshao:
queryset = queryset.filter(dingdan__jieshao__icontains=jieshao)
# ---------- 分页 ----------
paginator = Paginator(queryset, page_size)
try:
page_obj = paginator.page(page)
except EmptyPage:
page_obj = paginator.page(1)
# 收集当前页订单列表(主表 + 扩展表)
orders_with_ext = []
for pt_kuozhan in page_obj:
orders_with_ext.append((pt_kuozhan.dingdan, pt_kuozhan))
# 提取所有订单主表对象,用于并发获取打手图片
dingdan_objs = [order for order, _ in orders_with_ext]
oss_domain = get_oss_domain()
images_map = _get_images_for_orders(dingdan_objs, oss_domain)
# ---------- 组装返回数据 ----------
result_list = []
for order, pt_kuozhan in orders_with_ext:
order_data = {
'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': pt_kuozhan.laoban_id or '',
'laoban_pingjia': pt_kuozhan.laoban_pingjia or '',
'dianpu_shouyi': float(pt_kuozhan.dianpu_shouyi) if pt_kuozhan.dianpu_shouyi else 0.00,
},
# 打手图片(已在后端拼接绝对 URL
'dashou_images': images_map.get(order.dingdan_id, ([], False, {}))[0],
'is_punished': images_map.get(order.dingdan_id, ([], False, {}))[1],
'is_fadaned': False, # 暂不支持罚款状态,可根据需要扩展
}
result_list.append(order_data)
return Response({
'code': 200,
'msg': '获取成功',
'data': {
'list': result_list,
'total': paginator.count,
'page': page_obj.number,
'page_size': page_size
}
})
# ================================================================
# shangdian/views/refund_view.py
# 店铺退款接口 —— 基于跨平台退款接口二改,只处理平台订单,我方派单
# ================================================================
import json
import time
import random
import string
import hashlib
import hmac
import logging
import traceback
from decimal import Decimal
import xmltodict # 解析微信退款回执
import requests
from django.db import transaction
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from shangdian.utils import verify_shop_permission # 店铺身份验证
from dingdan.models import Dingdan, DingdanPingtai, Dashoutupian, Tuikuanjilu
from yonghu.models import UserMain
from shangdian.models import Dianpu
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.objects.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 = UserMain.objects.get(yonghuid=dashou_id)
dashou_profile = dashou_user.dashou_profile
dashou_profile.tuikuanliang += 1
dashou_profile.zhuangtai = 1
dashou_profile.save()
except (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"退款:打手 {dashou_id} 不存在,跳过状态更新")
# 更新退款记录表
self._update_refund_record(order, dashou_id, str(shop_id), reason)
# 打手每日统计(退款动作)
try:
from houtai.utils import update_dashou_daily_by_action
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:
from dingdan.utils import update_daily_payout
update_daily_payout(order.jine)
except Exception as e:
logger.error(f"更新每日支出失败: {e}")
# ========== 新增:商品和店铺每日统计 ==========
try:
from shangdian.utils import update_dianpu_daily_stat
from shangpin.utils import update_shangpin_daily_stat
# 商品每日统计只要有商品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': '退款处理失败,请稍后重试'})
# 7. 跨平台通知如果存在对方平台订单ID异步通知不影响主流程
partner_order_id = order.partner_order_id
partner_club_id = order.partner_club_id
if partner_order_id and partner_club_id:
try:
self._notify_partner_refund(dingdan_id, partner_club_id, partner_order_id, reason, order.jine)
except Exception as e:
logger.error(f"跨平台退款通知异常: {e}")
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.objects.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 _notify_partner_refund(self, dingdan_id, partner_club_id, partner_order_id, reason, amount):
"""
通知对方平台我方已退款。
此方法执行不影响主流程,任何异常仅记录日志。
"""
try:
from dingdan.models import ClubConfig, Club, CrossPlatformOrderData
my_config = ClubConfig.objects.filter(is_self=1).first()
if not my_config:
logger.error("未配置我方俱乐部信息,无法通知对方")
return
self_club_id = my_config.club_id
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
partner_domain = club_rel.partner_domain.rstrip('/')
notify_url = f"{partner_domain}/houtai/dfddtk"
payload = {
'order_id': partner_order_id,
'club_id': self_club_id,
'status': 5,
'reason': reason or '店铺退款'
}
resp = requests.post(notify_url, json=payload, timeout=3)
if resp.status_code == 200:
resp_json = resp.json()
if resp_json.get('code') == 0:
# 更新跨平台扩展表状态
cross_data, _ = CrossPlatformOrderData.objects.get_or_create(
dingdan_id=dingdan_id,
defaults={
'partner_order_id': partner_order_id,
'partner_club_id': partner_club_id,
'partner_order_status': resp_json.get('data', {}).get('partner_status', 0),
'partner_dashou_id': resp_json.get('data', {}).get('partner_dashou_id', ''),
'partner_amount': amount or 0,
}
)
if not _:
cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0)
cross_data.save()
else:
logger.warning(f"对方平台返回错误: {resp_json.get('msg')}")
else:
logger.warning(f"通知对方HTTP错误: {resp.status_code}")
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.objects.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}'})
# ========== 5. 判断是否为跨平台订单有对方订单ID就认为是跨平台 ==========
is_cross = order.is_cross and (order.partner_order_id or order.partner_club_id)
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)
# ============================
# 情况1跨平台订单我方派单给对方平台打手
# ============================
if is_cross:
# 只需更新我方订单状态,打手在对方平台,不处理我方打手余额
order.zhuangtai = 3 # 已完成
order.clkf = str(shop_id) # 处理人记录为店铺ID
order.save()
# 店铺结算时,若涉及跨平台收益,这里是否要更新店铺统计?
# 根据业务需求,可在此处调用 update_dianpu_daily_stat 等
# 但通常跨平台订单的资金已在对方平台流转,店铺仅作为发单方,
# 收益可能已在其他环节计算,此处可根据需要添加统计。
# 通知对方平台(异步,不影响主流程)
if order.partner_club_id and order.partner_order_id:
transaction.on_commit(
lambda: self._notify_partner_order_complete(
order.partner_club_id,
order.partner_order_id,
shop_id
)
)
# ============================
# 情况2本地订单我方打手接单
# ============================
else:
if not dashou_id:
return Response({'code': 400, 'msg': '订单无接单打手,无法结算'})
if dashou_fencheng < 0:
return Response({'code': 400, 'msg': '打手分成金额无效'})
# ---------- 更新打手余额和统计 ----------
try:
dashou_user = UserMain.objects.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 (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过打手更新")
# ---------- 更新订单状态 ----------
order.zhuangtai = 3
order.clkf = str(shop_id)
order.save()
# ---------- 打手每日统计(成交) ----------
try:
from houtai.utils import update_dashou_daily_by_action
update_dashou_daily_by_action(
yonghuid=dashou_id,
amount=dashou_fencheng,
action=2 # 2 表示成交(结算)
)
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# ---------- 店铺收益统计(结算) ----------
# 根据订单上的店铺收益 (从扩展表取) 更新店铺每日统计及余额
# 前提:订单创建时已存储了 dianpu_shouyi 到扩展表
if pingtai_ext.dianpu_shouyi:
try:
from shangpin.utils import update_shangpin_daily_stat
# 商品每日统计只要有商品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, # 2 = 结算
dingdan_jiage=order.jine,
pingtai_shouyi=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成商品ID: {order.shangpin_id}")
from shangdian.utils import update_dianpu_daily_stat
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}")
# ========== 6. 记录操作日志 ==========
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': '服务器内部错误'})
# ==================== 通知对方平台(异步) ====================
def _notify_partner_order_complete(self, partner_club_id, partner_order_id, shop_id):
"""通知对方平台我方订单已完成状态3"""
try:
# 尝试获取对方域名等配置(与退款接口类似)
my_config = ClubConfig.objects.filter(is_self=1).first()
if not my_config:
logger.error("未配置我方俱乐部信息,无法通知对方")
return
self_club_id = my_config.club_id
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
partner_domain = club_rel.partner_domain.rstrip('/')
notify_url = f"{partner_domain}/houtai/dfddtk"
import requests
payload = {
'order_id': partner_order_id,
'club_id': self_club_id,
'status': 3
}
resp = requests.post(notify_url, json=payload, timeout=3)
if resp.status_code == 200:
resp_data = resp.json()
if resp_data.get('code') == 0:
logger.info(f"通知对方订单已完成: {partner_order_id}")
else:
logger.warning(f"对方平台返回错误: {resp_data.get('msg')}")
else:
logger.warning(f"通知对方HTTP错误: {resp.status_code}")
except Exception as e:
logger.error(f"通知对方平台异常: {e}")
# ================================================================
# shangdian/views/transfer_hall_view.py
# 店铺转移大厅接口 —— 严格二改,不添加任何原接口没有的逻辑
# ================================================================
# ================================================================
# shangdian/views/transfer_hall_view.py
# 店铺转移大厅接口 —— 添加本地打手退款统计
# ================================================================
import logging
import threading
from django.core.exceptions import ObjectDoesNotExist
from dingdan.models import Dingdan, DingdanPingtai, OrderDashouHistory
from houtai.utils import update_dashou_daily_by_action # 打手每日统计(退款用)
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.objects.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
partner_club_id = order.partner_club_id
partner_order_id = order.partner_order_id
dashou_fencheng = order.dashou_fencheng or 0 # 用于退款统计
# 是否跨平台:只要有对方信息即认为是跨平台订单
is_cross = bool(partner_club_id or partner_order_id)
try:
with transaction.atomic():
# 锁定订单行
order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
# ---------- 6. 记录打手历史(原接口保留逻辑) ----------
if current_dashou_id:
last_history = OrderDashouHistory.objects.filter(
dingdan_id=dingdan_id
).order_by('-times').first()
next_times = (last_history.times + 1) if last_history else 1
OrderDashouHistory.objects.create(
dingdan_id=dingdan_id,
dashou_id=current_dashou_id,
times=next_times,
operator=str(shop_id)
)
# ---------- 7. 释放本地打手 + 退款统计(仅当非跨平台时处理) ----------
if not is_cross and current_dashou_id:
try:
dashou_profile = UserDashou.objects.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.partner_club_id = None
order.partner_order_id = None
order.jiedan_dashou_id = None
order.clkf = str(shop_id)
# 根据是否有指定打手决定状态
order.zhuangtai = 7 if zhiding_id else 1
order.save()
# ---------- 9. 异步通知对方平台(如果有对方信息) ----------
if is_cross and partner_club_id and partner_order_id:
# 获取我方俱乐部ID
my_club_config = None
try:
from peizhi.models import ClubConfig
my_club_config = ClubConfig.objects.filter(is_self=1).first()
except Exception as e:
logger.error(f"获取俱乐部配置失败: {e}")
if my_club_config:
self_club_id = my_club_config.club_id
def notify():
try:
from dingdan.models import CrossPlatformOrderData
from peizhi.models import Club # 确保导入
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
partner_domain = club_rel.partner_domain.rstrip('/')
notify_url = f"{partner_domain}/houtai/kpttzzydt"
payload = {
'order_id': partner_order_id,
'club_id': self_club_id
}
resp = requests.post(notify_url, json=payload, timeout=3)
if resp.status_code == 200:
resp_json = resp.json()
if resp_json.get('code') == 0:
# 更新跨平台订单数据(原接口逻辑)
try:
cross_data, _ = CrossPlatformOrderData.objects.get_or_create(
dingdan_id=dingdan_id,
defaults={
'partner_order_id': partner_order_id,
'partner_club_id': partner_club_id,
'partner_order_status': resp_json.get('data', {}).get('partner_status', 0),
'partner_dashou_id': '',
'partner_amount': order.jine or 0,
}
)
if not _:
cross_data.partner_order_status = resp_json.get('data', {}).get('partner_status', 0)
cross_data.save(update_fields=['partner_order_status'])
logger.info(f"转移大厅通知对方成功: {dingdan_id}")
except Exception as e:
logger.error(f"更新跨平台订单数据失败: {e}")
else:
logger.warning(f"对方平台返回错误: {resp_json.get('msg')}")
else:
logger.warning(f"对方接口HTTP错误: {resp.status_code}")
except Club.DoesNotExist:
logger.error(f"未找到对方俱乐部配置: partner_club_id={partner_club_id}")
except Exception as e:
logger.error(f"转移大厅通知对方异常: {e}")
threading.Thread(target=notify).start()
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客服罚款
# ================================================================
from dingdan.models import Dingdan, DingdanPingtai, Fadan # 订单、罚款记录
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.objects.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.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists():
return Response({'code': 400, 'msg': '该打手已被罚款过'})
# ---------- 6. 跨平台相关字段 ----------
partner_order_id = order.partner_order_id or ''
partner_club_id = order.partner_club_id or ''
dispatch_type = order.dispatch_type or 0 # 1=我方派单2=对方派单
# 本地创建罚单的通用函数
def create_local_fadan(partner_club=''):
Fadan.objects.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 店铺管理员
partner_club_id=partner_club,
)
# ---------- 7. 判断处罚流向 ----------
# 情况A没有对方订单ID或对方派单 → 本地处罚
if not partner_order_id or dispatch_type == 2:
with transaction.atomic():
create_local_fadan()
logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}")
return Response({'code': 200, 'msg': '罚款已生成'})
# ---------- 8. 情况B有对方订单ID且我方派单 → 跨平台通知对方 ----------
# 获取我方俱乐部ID
try:
my_club = ClubConfig.objects.filter(is_self=1).first()
if not my_club:
return Response({'code': 500, 'msg': '系统配置错误,缺少本俱乐部信息'})
self_club_id = my_club.club_id
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
partner_domain = club_rel.partner_domain
except Club.DoesNotExist:
return Response({'code': 500, 'msg': '未找到对方平台配置,无法通知'})
except Exception as e:
logger.error(f"获取对方平台信息失败: {e}")
return Response({'code': 500, 'msg': '获取对方平台信息失败'})
# 构造通知 payload
url = partner_domain.rstrip('/') + '/houtai/kffkdssq_notify'
payload = {
'order_id': partner_order_id, # 对方平台的订单ID
'club_id': self_club_id, # 我方俱乐部ID
'chufaliyou': chufaliyou,
'fakuanjine': fakuanjine,
'yingxiang_qiangdan': yingxiang_qiangdan,
'shenqingren_shenfen': 4, # 店铺管理员身份
'operator': str(shop_id),
}
# 带重试的通知
success = False
for _ in range(2):
try:
resp = requests.post(url, json=payload, timeout=3)
if resp.status_code == 200 and resp.json().get('code') == 0:
success = True
break
except Exception:
continue
if not success:
return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'})
# 通知成功后,本地也记录一笔(标记为跨平台)
with transaction.atomic():
create_local_fadan(partner_club=partner_club_id)
logger.info(f"跨平台罚款通知成功: 订单 {dingdan_id}")
return Response({'code': 200, 'msg': '罚款已提交至对方平台'})
except Exception as e:
logger.error(f"店铺罚款申请异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '服务器内部错误'})