商品与专区、店铺增加 club_id(历史默认 xq);列表按俱乐部过滤;工单列表返回待办统计;新增 hqshouji/rzwlsjh 手机号查询与认证接口。 Co-authored-by: Cursor <cursoragent@cursor.com>
376 lines
15 KiB
Python
376 lines
15 KiB
Python
"""shop.views.shop_member_views - 会员列表与下级店铺管理视图."""
|
||
# ==================== 标准库 ====================
|
||
import io
|
||
import json
|
||
import time
|
||
import random
|
||
import string
|
||
import hmac
|
||
import hashlib
|
||
import requests
|
||
import logging
|
||
import traceback
|
||
import calendar
|
||
import threading
|
||
import xmltodict
|
||
from decimal import Decimal
|
||
from collections import defaultdict
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
|
||
from django.conf import settings
|
||
from django.db import models, transaction, IntegrityError
|
||
from django.db.models import F, Sum, Count, Q, Prefetch
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
from django.core.cache import cache
|
||
from django.core.paginator import Paginator, EmptyPage
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.utils import timezone
|
||
from django.http import HttpResponse
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||
from rest_framework_simplejwt.tokens import RefreshToken
|
||
|
||
from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client
|
||
from utils.money import yuan_to_fen
|
||
|
||
from ..utils import verify_shop_permission
|
||
from shop.utils import verify_shop_permission, update_dianpu_daily_stat
|
||
from products.utils import update_shangpin_daily_stat
|
||
from orders.utils import update_daily_payout
|
||
from backend.utils import update_dashou_daily_by_action
|
||
|
||
from ..models import YonghuPingzheng, Dianpu, DianpuShouzhiMeiriTongji
|
||
|
||
from users.models import UserDashou, UserShangjia, UserGuanshi, UserZuzhang
|
||
from users.business_models import User
|
||
|
||
from shop.models import YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||
|
||
from products.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan
|
||
|
||
from orders.models import (
|
||
Order, PlatformOrderExt, PlayerDeliveryImage, RefundRecord,
|
||
OrderPlayerHistory, Penalty
|
||
)
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class MemberListView(APIView):
|
||
"""
|
||
获取所有会员列表(供商品发布时选择)
|
||
URL: /shangdian/hqhy
|
||
权限:JWT认证 + 店铺归属验证
|
||
说明:前端需传递 userId,接口会进行身份及店铺状态校验
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# ---------- 1. 获取前端传递的用户ID(必须) ----------
|
||
frontend_user_id = request.data.get('userId')
|
||
if not frontend_user_id:
|
||
return Response({'code': 400, 'msg': '缺少必要参数 userId'})
|
||
|
||
# ---------- 2. 调用公共验证方法(会校验用户身份、店铺状态) ----------
|
||
dianpu, error_response = verify_shop_permission(request, frontend_user_id)
|
||
if error_response:
|
||
return error_response
|
||
|
||
# ---------- 3. 查询所有会员(只返回前端需要的字段) ----------
|
||
try:
|
||
members = Huiyuan.query.all().values('huiyuan_id', 'jieshao')
|
||
member_list = [
|
||
{
|
||
'huiyuan_id': m['huiyuan_id'],
|
||
'jieshao': m['jieshao'] or ''
|
||
}
|
||
for m in members
|
||
]
|
||
except Exception as e:
|
||
logger.exception("获取会员列表失败")
|
||
return Response({'code': 500, 'msg': '服务器错误'})
|
||
|
||
# ---------- 4. 返回成功响应 ----------
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'success',
|
||
'data': member_list
|
||
})
|
||
|
||
|
||
|
||
|
||
class SubShopListView(APIView):
|
||
"""
|
||
获取下级店铺列表,同时返回上级分红汇总
|
||
URL: /shangdian/wdxjdp
|
||
方法: POST
|
||
权限: JWT认证 + 上级店铺身份验证
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 身份验证 - 获取上级店铺对象
|
||
user_id = request.data.get('userId')
|
||
if not user_id:
|
||
return Response({'code': 400, 'msg': '缺少 userId'})
|
||
|
||
parent_dianpu, error = verify_shop_permission(request, user_id)
|
||
if error:
|
||
return error
|
||
# 此时 parent_dianpu 即为当前上级店铺
|
||
|
||
# 2. 读取筛选参数
|
||
dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip()
|
||
zhuangtai = request.data.get('zhuangtai')
|
||
bind_min = request.data.get('bind_min')
|
||
bind_max = request.data.get('bind_max')
|
||
page = request.data.get('page', 1)
|
||
page_size = request.data.get('page_size', 10)
|
||
|
||
# 3. 查询下级店铺:yaoqing_dianpu_id 等于当前上级店铺ID
|
||
queryset = Dianpu.query.filter(yaoqing_dianpu_id=parent_dianpu.id)
|
||
|
||
# 4. 筛选条件
|
||
if dianpu_mingcheng:
|
||
queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng)
|
||
if zhuangtai is not None:
|
||
try:
|
||
zhuangtai = int(zhuangtai)
|
||
queryset = queryset.filter(zhuangtai=zhuangtai)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
if bind_min is not None:
|
||
try:
|
||
bind_min = int(bind_min)
|
||
queryset = queryset.filter(bangding_yonghushu__gte=bind_min)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
if bind_max is not None:
|
||
try:
|
||
bind_max = int(bind_max)
|
||
queryset = queryset.filter(bangding_yonghushu__lte=bind_max)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 5. 排序:按创建时间倒序
|
||
queryset = queryset.order_by('-CreateTime')
|
||
|
||
# 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),
|
||
'CreateTime': shop.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if shop.CreateTime else '',
|
||
'UpdateTime': shop.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if shop.UpdateTime else '',
|
||
'erweima_url': shop.erweima_url or '',
|
||
'fenhong_choucheng_feilv': str(shop.fenhong_choucheng_feilv) if shop.fenhong_choucheng_feilv else '0.0000',
|
||
})
|
||
|
||
# 8. 上级分红汇总(从当前店铺主表读取)
|
||
shangji_leiji = str(parent_dianpu.shangji_fenhong_leiji) if parent_dianpu.shangji_fenhong_leiji else '0.00'
|
||
shangji_ketixian = str(parent_dianpu.shangji_ketixian_fenhong) if parent_dianpu.shangji_ketixian_fenhong else '0.00'
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'list': shop_list,
|
||
'total': paginator.count,
|
||
'page': page_obj.number,
|
||
'page_size': page_size,
|
||
'shangji_fenhong_leiji': shangji_leiji,
|
||
'shangji_ketixian_fenhong': shangji_ketixian,
|
||
}
|
||
})
|
||
|
||
|
||
class SubShopModifyView(APIView):
|
||
"""
|
||
添加/修改下级店铺
|
||
URL: /shangdian/xjdpbj
|
||
方法: POST
|
||
权限: JWT认证 + 上级店铺身份验证
|
||
action字段:
|
||
- 'add' : 添加新的子店铺
|
||
- 'update': 修改已有子店铺的状态/分红费率
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 身份验证
|
||
user_id = request.data.get('userId')
|
||
if not user_id:
|
||
return Response({'code': 400, 'msg': '缺少 userId'})
|
||
|
||
parent_dianpu, error = verify_shop_permission(request, user_id)
|
||
if error:
|
||
return error
|
||
|
||
action = request.data.get('action', '').strip().lower()
|
||
if action == 'add':
|
||
return self._add_subshop(request, parent_dianpu)
|
||
elif action == 'update':
|
||
return self._update_subshop(request, parent_dianpu)
|
||
else:
|
||
return Response({'code': 400, 'msg': '未知操作类型,action 应为 add 或 update'})
|
||
|
||
def _add_subshop(self, request, parent_dianpu):
|
||
"""添加子店铺,需提供用户UID、账号、密码、店铺名称等"""
|
||
user_uid = request.data.get('user_uid', '').strip()
|
||
zhanghao = request.data.get('zhanghao', '').strip()
|
||
mima = request.data.get('mima', '')
|
||
dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip()
|
||
lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip() or None
|
||
fenhong_feilv = request.data.get('fenhong_choucheng_feilv', None)
|
||
|
||
# 必填校验
|
||
if not all([user_uid, zhanghao, mima, dianpu_mingcheng]):
|
||
return Response({'code': 400, 'msg': '必填字段缺失(user_uid, zhanghao, mima, dianpu_mingcheng)'})
|
||
|
||
# 检查用户是否存在
|
||
if not User.query.filter(UserUID=user_uid).exists():
|
||
return Response({'code': 404, 'msg': '对应的小程序用户不存在'})
|
||
|
||
# 检查账号是否已占用
|
||
if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists():
|
||
return Response({'code': 400, 'msg': '登录账号已存在'})
|
||
if YonghuPingzheng.query.filter(yonghu=user_uid).exists():
|
||
return Response({'code': 400, 'msg': '该用户 ID 已有店铺凭证'})
|
||
|
||
# 费率处理:前端可选,后端默认 0.0500
|
||
if fenhong_feilv is not None:
|
||
try:
|
||
fenhong_feilv = float(fenhong_feilv)
|
||
if fenhong_feilv < 0 or fenhong_feilv > 1:
|
||
return Response({'code': 400, 'msg': '分红费率需在 0~1 之间'})
|
||
except (ValueError, TypeError):
|
||
return Response({'code': 400, 'msg': '分红费率格式错误'})
|
||
else:
|
||
fenhong_feilv = 0.0500
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
# 创建凭证
|
||
pingzheng = YonghuPingzheng.query.create(
|
||
yonghu=user_uid,
|
||
zhanghao=zhanghao,
|
||
mima=mima,
|
||
is_active=True
|
||
)
|
||
# 创建店铺,关联上级(俱乐部与上级一致)
|
||
dianpu = Dianpu.query.create(
|
||
pingzheng=pingzheng,
|
||
dianpu_mingcheng=dianpu_mingcheng,
|
||
lianxi_dianhua=lianxi_dianhua or '',
|
||
yaoqing_dianpu_id=parent_dianpu.id, # 关键:绑定上级
|
||
fenhong_choucheng_feilv=fenhong_feilv,
|
||
club_id=getattr(parent_dianpu, 'club_id', None) or 'xq',
|
||
)
|
||
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', 'UpdateTime'])
|
||
return Response({'code': 200, 'msg': '修改成功'})
|
||
|
||
except Dianpu.DoesNotExist:
|
||
return Response({'code': 404, 'msg': '店铺不存在'})
|
||
except Exception as e:
|
||
logger.exception("修改下级店铺失败")
|
||
return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
|
||
|
||
|
||
|
||
|
||
|
||
# shangdian/views/order_manage_views.py
|
||
"""
|
||
店铺订单管理接口
|
||
包含:
|
||
1. 统计数据:商品类型及订单总数、各状态订单数
|
||
2. 订单列表:支持筛选、分页、返回订单详情及打手图片(含跨平台图片拉取)
|
||
"""
|
||
|
||
|
||
# ---------- 图片域名 ----------
|
||
"""
|
||
店铺订单管理接口
|
||
=================
|
||
功能1:获取统计数据(action='statistics')
|
||
- 每个商品类型的订单总数(order_count)
|
||
- 每个商品类型下各状态的订单数量(status_counts)
|
||
- 全局各状态的订单总数(status_counts)
|
||
- 店铺订单总量(total_orders)
|
||
|
||
功能2:获取订单列表(默认,不带action参数时执行)
|
||
- 支持商品类型、订单状态、订单ID、打手ID、游戏昵称、老板ID、订单介绍筛选
|
||
- 支持分页、自定义每页大小
|
||
- 返回打手提交图片(绝对URL)、老板信息、店铺收益等
|
||
|
||
接口地址:POST /shangdian/ddlbhq
|
||
权限:JWT认证 + verify_shop_permission 验证店铺身份
|
||
"""
|
||
|