3369 lines
139 KiB
Python
3369 lines
139 KiB
Python
|
||
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 : 用户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 = 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 : 用户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 = 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': '服务器内部错误'}) |