merge: 合并 origin/main 视图拆分 (c5ae305)

This commit is contained in:
XingQue committed 2026-07-05 16:17:51 +08:00
commit 3b91fb09ea
44 files changed
+32751 -188157

No files matched your search

-2
View File
@@ -1,2 +0,0 @@
# 3. 在项目的 `__init__.py` 文件(与manage.py同级)中,添加两行代码
# 文件路径:a_long_dianjing/__init__.py
+57 -41
View File
@@ -188,57 +188,73 @@ class KhgglView(APIView):
end = start + page_size
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
from collections import defaultdict
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
# ---------- 预计算每个板块的打手数量 ----------
bankuai_dashou_count_map = {}
all_banquai = Bankuai.query.all()
for bk in all_banquai:
tag_ids = Chenghao.query.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True)
count = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(
chenghao_id__in=tag_ids,
yonghu__DashouProfile__isnull=False,
),
request,
).values('yonghu').distinct().count()
bankuai_dashou_count_map[bk.bankuai_id] = count
# ---------- 优化:板块打手数量(1 次 annotate 替代 2N 次循环查询)----------
# 原逻辑:每板块查 dashou 标签 ID,再统计关联的不同用户数(要求有 DashouProfile)
bankuai_dashou_qs = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(
chenghao__leixing='dashou',
yonghu__DashouProfile__isnull=False,
),
request,
)
bankuai_dashou_count_map = {
item['chenghao__bankuai_id']: item['user_count']
for item in bankuai_dashou_qs.values('chenghao__bankuai_id').annotate(
user_count=Count('yonghu', distinct=True)
)
}
# ---------- 新增:板块标签统计 ----------
bankuai_tags_data = []
for bk in all_banquai:
tags = Chenghao.query.filter(bankuai=bk, leixing='dashou')
tag_list = []
for tag in tags:
user_count = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(chenghao=tag),
request,
).values('yonghu').distinct().count()
tag_list.append({
'id': tag.id,
'name': tag.mingcheng,
'user_count': user_count,
})
bankuai_tags_data.append({
# ---------- 优化:板块标签统计(1 次 annotate 替代 N*M 次循环查询)----------
# 原逻辑:每板块每标签分别统计关联的不同用户数(不要求 DashouProfile)
tag_counts_qs = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(chenghao__leixing='dashou'),
request,
).values(
'chenghao_id', 'chenghao__bankuai_id', 'chenghao__mingcheng'
).annotate(user_count=Count('yonghu', distinct=True))
tags_by_bankuai = defaultdict(list)
for item in tag_counts_qs:
tags_by_bankuai[item['chenghao__bankuai_id']].append({
'id': item['chenghao_id'],
'name': item['chenghao__mingcheng'],
'user_count': item['user_count'],
})
all_banquai = Bankuai.query.all()
bankuai_tags_data = [
{
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'tags': tag_list,
})
'tags': tags_by_bankuai.get(bk.bankuai_id, []),
}
for bk in all_banquai
]
# ---------- 优化:审核官板块关联(1 次批量查询替代 P 次循环查询)----------
shenheguan_user_uids = [sg.user.UserUID for sg in shenheguan_list]
all_kgs = KaoheguanBankuai.query.filter(
kaoheguan_id__in=shenheguan_user_uids
).select_related('bankuai')
kg_map = defaultdict(list)
for kg in all_kgs:
kg_map[kg.kaoheguan_id].append(kg)
# 构造返回数据(审核官列表)
data_list = []
for sg in shenheguan_list:
user = sg.user
# 获取该审核官关联的板块
bk_qs = KaoheguanBankuai.query.filter(kaoheguan_id=user.UserUID).select_related('bankuai')
bankuai_info = []
for bk in bk_qs:
bid = bk.bankuai.bankuai_id
bankuai_info.append({
'bankuai_id': bid,
'mingcheng': bk.bankuai.mingcheng,
'dashou_count': bankuai_dashou_count_map.get(bid, 0)
})
bankuai_info = [
{
'bankuai_id': kg.bankuai.bankuai_id,
'mingcheng': kg.bankuai.mingcheng,
'dashou_count': bankuai_dashou_count_map.get(kg.bankuai.bankuai_id, 0),
}
for kg in kg_map.get(user.UserUID, [])
]
dashou_nick = user.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else ''
-7101
View File
File diff suppressed because it is too large. Load diff
+87
View File
@@ -0,0 +1,87 @@
"""orders.views package aggregate entry - re-export all submodule symbols.
Preserves external `from orders.views import X` for orders/urls.py and merchant_ops.
"""
from .payment import (
PaymentFailView,
WechatPayNotifyView,
PaymentVerifyView,
CreateOrderView,
)
from .boss_orders import (
DingdanHuoquView,
DingdanXiangqingView,
JiedanView,
DingdanXiangqingView2,
JiedanView2,
)
from .merchant import (
ShangpinLeixingView,
ShangjiaPaifaView,
ShangjiaDingdanHuoquView,
ShangjiaShujuTongjiView,
ShangjiaDingdanXiangqingView,
ShangjiaJiesuanView,
ShangjiaChexiaoView,
ShangjiaTuikuanShenqingView,
ShangjiaChufaShenqingView,
)
from .chat import (
DaiLiQunLiaoXiaoXiView,
ShangjiaLianjieLiuYanZhuanFaView,
LianTongDuiHuaZhunBeiView,
LianTongPeiDuiDingDanLieBiaoView,
LianTongDingDanZhuangTaiView,
LianTongDingDanZhuangTaiPiLiangView,
)
from .dashou import (
DashouDingdanHuoquView,
ShangpinLeixingHuoquView,
QiangdanView,
DashouDingdanHuoquView1,
DashouCOSZhengshuView,
ShangjiaCOSZhengshuView,
DashouTijiaoView,
DashouDingdanXiangqingView,
DashouHuoquLeixingView,
DashouXiugaiView,
)
from .admin import (
AdGetDingdanXiangQing,
AdGengHuanDaShou,
AdQiangZhiJieDan,
AdJuJueTuiKuan,
AdTongYiTuiKuanShangJia,
AdTongYiTuiKuanPingTai,
AdJuJueJieSuan,
AdZhuanYiDaTing,
AdQuXiaoZhiDing,
ZhiDingChaxunView,
ZhiDingHuiFuView,
ZxsjghdsView,
)
from .merchant_penalty import (
ShangjiaFakuanApplyView,
ShangjiaFakuaiXiugaiView,
)
__all__ = [
"PaymentFailView", "WechatPayNotifyView", "PaymentVerifyView", "CreateOrderView",
"DingdanHuoquView", "DingdanXiangqingView", "JiedanView",
"DingdanXiangqingView2", "JiedanView2",
"ShangpinLeixingView", "ShangjiaPaifaView", "ShangjiaDingdanHuoquView",
"ShangjiaShujuTongjiView", "ShangjiaDingdanXiangqingView", "ShangjiaJiesuanView",
"ShangjiaChexiaoView", "ShangjiaTuikuanShenqingView", "ShangjiaChufaShenqingView",
"DaiLiQunLiaoXiaoXiView", "ShangjiaLianjieLiuYanZhuanFaView",
"LianTongDuiHuaZhunBeiView", "LianTongPeiDuiDingDanLieBiaoView",
"LianTongDingDanZhuangTaiView", "LianTongDingDanZhuangTaiPiLiangView",
"DashouDingdanHuoquView", "ShangpinLeixingHuoquView", "QiangdanView",
"DashouDingdanHuoquView1", "DashouCOSZhengshuView", "ShangjiaCOSZhengshuView",
"DashouTijiaoView", "DashouDingdanXiangqingView", "DashouHuoquLeixingView",
"DashouXiugaiView",
"AdGetDingdanXiangQing", "AdGengHuanDaShou", "AdQiangZhiJieDan",
"AdJuJueTuiKuan", "AdTongYiTuiKuanShangJia", "AdTongYiTuiKuanPingTai",
"AdJuJueJieSuan", "AdZhuanYiDaTing", "AdQuXiaoZhiDing",
"ZhiDingChaxunView", "ZhiDingHuiFuView", "ZxsjghdsView",
"ShangjiaFakuanApplyView", "ShangjiaFakuaiXiugaiView",
]
File diff suppressed because it is too large. Load diff
+821
View File
@@ -0,0 +1,821 @@
"""orders.views.boss_orders - auto-generated by split script."""
# ==================== 标准库 ====================
import hmac
import os
import json
import time
import random
import string
import traceback
import threading
import requests
import hashlib
import xml.etree.ElementTree as ET
import xmltodict
import logging
from decimal import Decimal
from django.conf import settings
from django.db import models, transaction
from gvsdsdk.fluent import db, func, FQ
from django.db.models import F, Q, OuterRef, Subquery
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.authentication import JWTAuthentication
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sts.v20180813 import sts_client, models
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.money import yuan_to_fen
from utils.chat_utils import (
_send_group_message, _subscribe_users_to_group, establish_order_chat,
prepare_order_group_chat, resolve_pair_group_id,
parse_pair_from_group_id, resolve_pair_from_order, check_pair_chat_permission,
query_pair_orders, order_to_chat_dict,
)
from utils.fadan_utils import check_fadan_qiangdan_eligible
from orders.utils import (
calc_shangjia_order_fencheng,
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from backend.utils import update_shangjia_daily, update_dashou_daily_by_action, pick_leixing_id, datetime_aliases, fmt_datetime
from rank.services import record_dashou_biaoxian
from rank.utils import check_dashou_biaoqian_required
from orders.notice_tasks import dingdan_guangbo
from ..models import (
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate,
CommissionRate, PlayerRating, RefundRecord,
)
from jituan.services.club_config import get_commission_rate, get_commission_rate_object
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_penalty_club_id, resolve_penalty_record_club_id
from users.fadan_fenhong_utils import lock_penalty_bonus
from users.fadan_fenhong_utils import lock_penalty_bonus
from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from users.models import UserDashou, UserShangjia, UserBoss
from users.business_models import User
from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
from config.models import (
ShangjiaLianjie
)
logger = logging.getLogger(__name__)
class DingdanHuoquView(APIView):
"""订单获取接口"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 获取当前登录用户
try:
current_user = request.user
yonghuid = current_user.UserUID
except Exception as e:
return Response({
'code': 1,
'msg': '用户认证失败',
'data': None
})
# 获取前端参数
try:
data = request.data
zhuangtai_list = data.get('zhuangtai_list', [])
page = int(data.get('page', 1))
page_size = int(data.get('page_size', 10))
except Exception as e:
return Response({
'code': 2,
'msg': '参数格式错误',
'data': None
})
# 参数验证
if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0:
return Response({
'code': 3,
'msg': '状态列表不能为空',
'data': None
})
if page < 1:
page = 1
if page_size <= 0:
page_size = 10
try:
# 查询扩展表,获取用户的所有订单ID
pingtai_orders = PlatformOrderExt.query.filter(BossID=yonghuid)
pingtai_order_ids = list(pingtai_orders.values_list('Order__OrderID', flat=True))
if not pingtai_order_ids:
return Response({
'code': 0,
'msg': '成功',
'data': {
'list': [],
'has_more': False,
'total': 0
}
})
# 查询主表
status_query = Q()
if zhuangtai_list != [1, 2, 3, 4, 5, 6, 7, 8]:
status_query = Q(Status__in=zhuangtai_list)
query = Q(OrderID__in=pingtai_order_ids) & status_query
orders = Order.query.filter(query).order_by('-CreateTime')
#total_orders = orders.count()
# 手动分页逻辑
start_index = (page - 1) * page_size
end_index = start_index + page_size
# 获取当前页的数据
current_page_orders = orders[start_index:end_index]
# 构建返回数据
order_list = []
for order in current_page_orders:
order_data = {
'dingdanId': order.OrderID,
'zhuangtai': order.Status,
'jine': float(order.Amount) if order.Amount else 0.0,
'tupian': order.ImageURL or '',
'jieshao': order.Description or '',
'beizhu': order.Remark or '',
'nicheng': order.Nickname or '',
'chuangjianshijian': fmt_datetime(order.CreateTime),
'create_time': fmt_datetime(order.CreateTime),
}
order_list.append(order_data)
# 判断是否还有更多数据
current_page_count = len(current_page_orders)
has_more = current_page_count == page_size
return Response({
'code': 0,
'msg': '成功',
'data': {
'list': order_list,
'has_more': has_more,
#'total': total_orders
}
})
except Exception as e:
return Response({
'code': 4,
'msg': '服务器内部错误',
'data': None
})
class DingdanXiangqingView(APIView):
"""订单详情接口"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 获取当前登录用户
try:
current_user = request.user
yonghuid = current_user.UserUID
except Exception as e:
return Response({
'code': 1,
'msg': '用户认证失败',
'data': None
})
# 获取前端参数
try:
data = request.data
dingdan_id = data.get('dingdanId', '')
except Exception as e:
return Response({
'code': 2,
'msg': '参数格式错误',
'data': None
})
# 参数验证
if not dingdan_id:
return Response({
'code': 3,
'msg': '订单ID不能为空',
'data': None
})
try:
# 1. 验证订单是否属于当前用户
# 查询平台订单扩展表,检查laoban_id是否等于当前用户
pingtai_kuozhan = PlatformOrderExt.query.filter(
BossID=yonghuid,
Order__OrderID=dingdan_id
).first()
if not pingtai_kuozhan:
# 如果不是平台订单,检查商家订单
shangjia_kuozhan = MerchantOrderExt.query.filter(
MerchantID=yonghuid,
Order__OrderID=dingdan_id
).first()
if not shangjia_kuozhan:
return Response({
'code': 4,
'msg': '订单不存在或无权限查看',
'data': None
})
# 2. 查询订单主表
dingdan = Order.query.filter(OrderID=dingdan_id).first()
if not dingdan:
return Response({
'code': 5,
'msg': '订单不存在',
'data': None
})
# 3. 获取订单信息
zhuangtai = dingdan.Status or 0
zhiding_id = dingdan.AssignedID or ''
jiedan_dashou_id = dingdan.PlayerID or ''
dashou_liuyan = dingdan.PlayerRemark or ''
# 4. 查询打手交付图片
dashoutupian_records = PlayerDeliveryImage.query.filter(
OrderID=dingdan_id
).order_by('CreateTime')[:9] # 最多取9条
dashoujiaofu_list = []
for record in dashoutupian_records:
if record.ImageURL:
dashoujiaofu_list.append(record.ImageURL)
# 5. 构建返回数据(字段名与前端对应)
response_data = {
'zhuangtai': zhuangtai, # 订单状态
'zhiding': zhiding_id, # 指定打手ID
'jiedan_dashou_id': jiedan_dashou_id, # 服务打手ID
'dashouliuyan': dashou_liuyan, # 打手留言
'dashoujiaofu': dashoujiaofu_list # 打手交付图片列表
}
# 6. 返回成功响应
return Response({
'code': 0,
'msg': '成功',
'data': response_data
})
except Exception as e:
print(f"订单详情查询失败: {str(e)}")
return Response({
'code': 6,
'msg': '服务器内部错误',
'data': None
})
class JiedanView(APIView):
"""结单接口"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 获取当前登录用户
try:
current_user = request.user
yonghuid = current_user.UserUID
except Exception as e:
return Response({
'code': 1,
'msg': '用户认证失败',
'data': None
})
# 获取前端参数
try:
data = request.data
dingdan_id = data.get('dingdanId', '')
pingfen = int(data.get('pingfen', 0)) # 评分,默认0
liuyan = data.get('liuyan', '') # 留言,默认空
except Exception as e:
return Response({
'code': 2,
'msg': '参数格式错误',
'data': None
})
# 参数验证
if not dingdan_id:
return Response({
'code': 3,
'msg': '订单ID不能为空',
'data': None
})
# 验证评分范围
if pingfen < 0 or pingfen > 5:
return Response({
'code': 4,
'msg': '评分必须在0-5分之间',
'data': None
})
# 使用事务确保数据一致性
try:
with transaction.atomic():
# 1. 验证订单是否存在且属于当前用户
pingtai_kuozhan = PlatformOrderExt.query.filter(
BossID=yonghuid,
Order__OrderID=dingdan_id
).select_related('Order').first()
if not pingtai_kuozhan:
return Response({
'code': 5,
'msg': '订单不存在或无权限操作',
'data': None
})
dingdan = pingtai_kuozhan.Order
# 2. 验证订单状态(必须是结算中 - 状态8)
if dingdan.Status != 8:
return Response({
'code': 6,
'msg': '订单不在结算状态,无法结单',
'data': None
})
# 3. 获取订单相关数据
dashou_fencheng = dingdan.PlayerCommission or Decimal('0.00') # 打手分成
jiedan_dashou_id = dingdan.PlayerID or '' # 接单打手ID
shangpin_id = dingdan.ProductID # 商品ID
# 4. 更新平台扩展表的老板评价(如果有留言)
if liuyan:
pingtai_kuozhan.BossReview = liuyan
pingtai_kuozhan.save()
# 5. 如果有评分,更新评分表
if pingfen > 0 and jiedan_dashou_id:
# 使用filter查询,更健壮
pingfen_records = PlayerRating.query.filter(PlayerID=jiedan_dashou_id)
if pingfen_records.exists():
# 已有记录,更新
pingfen_record = pingfen_records.first()
pingfen_record.TotalScore += Decimal(str(pingfen))
pingfen_record.RatingCount += 1
pingfen_record.save()
else:
# 新记录,创建
PlayerRating.query.create(
PlayerID=jiedan_dashou_id,
TotalScore=Decimal(str(pingfen)),
RatingCount=1
)
# 6. 更新商品真实销量(如果有商品ID)
if shangpin_id:
shangpin_records = Shangpin.query.filter(id=shangpin_id)
if shangpin_records.exists():
shangpin = shangpin_records.first()
# 检查字段是否存在
if hasattr(shangpin, 'zhenshi_xiaoliang'):
shangpin.zhenshi_xiaoliang = (shangpin.zhenshi_xiaoliang or 0) + 1
shangpin.save()
# 7. 更新打手扩展表(如果有打手ID和分成)
if jiedan_dashou_id and dashou_fencheng > 0:
# 查询打手用户
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first()
if dashou_user:
# 查询打手扩展表
dashou_kuozhan = UserDashou.query.filter(user=dashou_user).first()
if dashou_kuozhan:
# 更新四个金额字段
dashou_kuozhan.zonge = (dashou_kuozhan.zonge or Decimal('0.00')) + dashou_fencheng
dashou_kuozhan.yue = (dashou_kuozhan.yue or Decimal('0.00')) + dashou_fencheng
dashou_kuozhan.jinrishouyi = (dashou_kuozhan.jinrishouyi or Decimal(
'0.00')) + dashou_fencheng
dashou_kuozhan.jinyueshouyi = (dashou_kuozhan.jinyueshouyi or Decimal(
'0.00')) + dashou_fencheng
# 成交总量加1
dashou_kuozhan.chengjiaozongliang = (dashou_kuozhan.chengjiaozongliang or 0) + 1
# 状态改为数字1
dashou_kuozhan.zhuangtai = 1
dashou_kuozhan.save()
# 8. 更新订单状态为已完成(状态3)
dingdan.Status = 3
dingdan.save()
# 🔥 新增:调用打手每日统计(成交)
try:
update_dashou_daily_by_action(
yonghuid=jiedan_dashou_id,
amount=dashou_fencheng,
action=2 # 2 表示成交(结算)
)
logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 不影响主流程
# ========== 新增:商品和店铺每日统计 ==========
try:
# 商品每日统计(只要有商品ID就执行)
if dingdan.ProductID:
# 从扩展表获取已计算好的收益值(下单时已存储)
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(dingdan, 'pingtai_kuozhan'):
pingtai_shouyi = dingdan.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_shangpin_daily_stat(
ProductID=dingdan.ProductID,
caozuo_leixing=2, # 2 = 结单
dingdan_jiage=dingdan.Amount,
PlatformIncome=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成,商品ID: {dingdan.ProductID}")
# 店铺每日统计(仅当存在店铺ID时执行)
# ========== 修改为 ==========
# 店铺每日统计(包含上级分红)
dianpu_id = None
shangji_id = None
shangji_fen = Decimal('0.00')
if hasattr(dingdan, 'pingtai_kuozhan'):
dianpu_id = dingdan.pingtai_kuozhan.ShopID
shangji_id = dingdan.pingtai_kuozhan.ParentShopID
shangji_fen = dingdan.pingtai_kuozhan.ParentBonus or Decimal('0.00')
if dianpu_id:
dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_dianpu_daily_stat(
ShopID=dianpu_id,
caozuo_leixing=2, # 结算
dingdan_jiage=dingdan.Amount,
ShopIncome=dianpu_shouyi,
shangji_ShopID=shangji_id, # 新增:上级店铺ID
ParentBonus=shangji_fen # 新增:该单给上级的分红
)
logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}, 上级ID: {shangji_id}, 上级分红: {shangji_fen}")
'''dianpu_id = None
if hasattr(dingdan, 'pingtai_kuozhan'):
dianpu_id = dingdan.pingtai_kuozhan.ShopID
if dianpu_id:
dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_dianpu_daily_stat(
ShopID=dianpu_id,
caozuo_leixing=2, # 2 = 结单
dingdan_jiage=dingdan.Amount,
ShopIncome=dianpu_shouyi
)
logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")'''
except Exception as e:
logger.error(f"商品/店铺统计更新失败: {e}")
logger.error(traceback.format_exc())
# 统计失败不影响主流程
# =============================================
# 9. 返回成功响应
return Response({
'code': 0,
'msg': '结单成功',
'data': None
})
except Exception as e:
# 事务会自动回滚
return Response({
'code': 7,
'msg': '结单失败,服务器内部错误',
'data': None
})
class DingdanXiangqingView2(APIView):
"""订单详情接口(老板端)- 返回完整评价信息"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghuid = request.user.UserUID
except Exception:
return Response({'code': 1, 'msg': '用户认证失败', 'data': None})
dingdan_id = request.data.get('dingdanId', '')
if not dingdan_id:
return Response({'code': 2, 'msg': '订单ID不能为空', 'data': None})
try:
# 权限验证
pingtai_ext = PlatformOrderExt.query.filter(
BossID=yonghuid,
Order__OrderID=dingdan_id
).select_related('Order').first()
if not pingtai_ext:
return Response({'code': 3, 'msg': '订单不存在或无权限查看', 'data': None})
order = pingtai_ext.Order
zhuangtai = order.Status or 0
jiedan_dashou_id = order.PlayerID or ''
dashou_liuyan = order.PlayerRemark or ''
UpdateTime = order.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if order.UpdateTime else ''
# 交付图片
dashoujiaofu_list = list(
PlayerDeliveryImage.query.filter(OrderID=dingdan_id)
.order_by('CreateTime')[:9]
.values_list('ImageURL', flat=True)
)
# 打手信息
dashou_nicheng = ''
dashou_touxiang = ''
jiedan_time = ''
tijiao_time = ''
if jiedan_dashou_id:
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first()
if dashou_user:
dashou_touxiang = dashou_user.Avatar or ''
try:
dashou_nicheng = dashou_user.DashouProfile.nicheng or ''
except UserDashou.DoesNotExist:
pass
biaoxian = DashouBiaoxian.query.filter(
OrderID=dingdan_id,
dashou_id=jiedan_dashou_id
).first()
if biaoxian:
if biaoxian.jiedan_time:
jiedan_time = biaoxian.jiedan_time.strftime('%Y-%m-%d %H:%M:%S')
if biaoxian.tijiao_time:
tijiao_time = biaoxian.tijiao_time.strftime('%Y-%m-%d %H:%M:%S')
# 老板自己的评价(如果已经结算过,可能已有评价)
laoban_pingjia = pingtai_ext.BossReview or ''
shangxian_sudu = None
fuwu_taidu = None
fanche_cishu = None
if jiedan_dashou_id and zhuangtai in [3, 4, 5, 6]: # 已完成或退款相关状态才可能有评价
biaoxian = DashouBiaoxian.query.filter(
OrderID=dingdan_id,
dashou_id=jiedan_dashou_id
).first()
if biaoxian:
shangxian_sudu = biaoxian.shangxian_sudu
fuwu_taidu = biaoxian.fuwu_taidu
fanche_cishu = biaoxian.fanche_cishu
data = {
'zhuangtai': zhuangtai,
'dashou': jiedan_dashou_id,
'dashou_nicheng': dashou_nicheng,
'dashou_touxiang': dashou_touxiang,
'dashouliuyan': dashou_liuyan,
'dashoujiaofu': dashoujiaofu_list,
'UpdateTime': UpdateTime,
'jiedan_time': jiedan_time,
'tijiao_time': tijiao_time,
'laoban_pingjia': laoban_pingjia,
'shangxian_sudu': shangxian_sudu, # 1-5 或 null
'fuwu_taidu': fuwu_taidu, # 1-5 或 null
'fanche_cishu': fanche_cishu # int 或 null
}
return Response({'code': 0, 'msg': '成功', 'data': data})
except Exception as e:
logger.error(f"订单详情查询异常: {e}", exc_info=True)
return Response({'code': 4, 'msg': '服务器内部错误', 'data': None})
class JiedanView2(APIView):
"""结单接口(老板端)- 接收全新评价字段并写入表现表"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 用户认证
try:
current_user = request.user
yonghuid = current_user.UserUID
except Exception:
return Response({'code': 1, 'msg': '用户认证失败', 'data': None})
# 2. 获取参数(字段名与前端完全一致)
try:
data = request.data
dingdan_id = data.get('dingdanId', '')
fanche_cishu = data.get('fanche_cishu') # 前端传的字符串或数字,后端转为整数
shangxian_sudu = data.get('shangxian_sudu')
fuwu_taidu = data.get('fuwu_taidu')
liuyan = data.get('liuyan', '')
except Exception:
return Response({'code': 2, 'msg': '参数格式错误', 'data': None})
# 参数校验
if not dingdan_id:
return Response({'code': 3, 'msg': '订单ID不能为空', 'data': None})
# 转换并校验必填评价字段
try:
fanche_cishu = int(fanche_cishu) if fanche_cishu is not None else None
shangxian_sudu = int(shangxian_sudu) if shangxian_sudu is not None else None
fuwu_taidu = int(fuwu_taidu) if fuwu_taidu is not None else None
except (ValueError, TypeError):
return Response({'code': 4, 'msg': '评价参数必须为整数', 'data': None})
if fanche_cishu is None:
return Response({'code': 5, 'msg': '翻车次数不能为空', 'data': None})
if not (1 <= shangxian_sudu <= 5 and 1 <= fuwu_taidu <= 5):
return Response({'code': 6, 'msg': '评价分数必须在1-5之间', 'data': None})
# 使用事务保证数据一致性
try:
with transaction.atomic():
# 3. 权限验证:订单属于当前老板
pingtai_ext = PlatformOrderExt.query.filter(
BossID=yonghuid,
Order__OrderID=dingdan_id
).select_related('Order').first()
if not pingtai_ext:
return Response({'code': 7, 'msg': '订单不存在或无权限操作', 'data': None})
order = pingtai_ext.Order
# 4. 状态校验:必须是结算中(状态8)
if order.Status != 8:
return Response({'code': 8, 'msg': '订单不在结算状态', 'data': None})
# 5. 提取订单关键数据
dashou_fencheng = order.PlayerCommission or Decimal('0.00')
jiedan_dashou_id = order.PlayerID or ''
shangpin_id = order.ProductID
# 6. 保存老板评价(兼容旧逻辑)
if liuyan:
pingtai_ext.BossReview = liuyan
pingtai_ext.save()
# 7. 更新评分表(用服务态度分作为综合评分,保留原有统计)
if jiedan_dashou_id and fuwu_taidu:
pingfen_records = PlayerRating.query.filter(PlayerID=jiedan_dashou_id)
if pingfen_records.exists():
pf = pingfen_records.first()
pf.TotalScore += Decimal(str(fuwu_taidu))
pf.RatingCount += 1
pf.save()
else:
PlayerRating.query.create(
PlayerID=jiedan_dashou_id,
TotalScore=Decimal(str(fuwu_taidu)),
RatingCount=1
)
# 8. 更新商品真实销量
if shangpin_id:
shangpin_qs = Shangpin.query.filter(id=shangpin_id)
if shangpin_qs.exists():
sp = shangpin_qs.first()
if hasattr(sp, 'zhenshi_xiaoliang'):
sp.zhenshi_xiaoliang = (sp.zhenshi_xiaoliang or 0) + 1
sp.save()
# 9. 更新打手收益与状态
if jiedan_dashou_id and dashou_fencheng > 0:
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first()
if dashou_user:
try:
dashou_ext = dashou_user.DashouProfile
dashou_ext.zonge = (dashou_ext.zonge or Decimal('0.00')) + dashou_fencheng
dashou_ext.yue = (dashou_ext.yue or Decimal('0.00')) + dashou_fencheng
dashou_ext.jinrishouyi = (dashou_ext.jinrishouyi or Decimal('0.00')) + dashou_fencheng
dashou_ext.jinyueshouyi = (dashou_ext.jinyueshouyi or Decimal('0.00')) + dashou_fencheng
dashou_ext.chengjiaozongliang = (dashou_ext.chengjiaozongliang or 0) + 1
dashou_ext.zhuangtai = 1
dashou_ext.save()
except UserDashou.DoesNotExist:
pass
# 10. 更新订单状态为已完成
order.Status = 3
order.save()
# 11. 打手每日统计(成交)
try:
update_dashou_daily_by_action(
UserID=jiedan_dashou_id,
amount=dashou_fencheng,
action=2 # 2=结算/成交
)
except Exception as e:
logger.error(f"打手每日统计失败: {e}")
# 12. 商品/店铺每日统计
try:
if shangpin_id:
# 从扩展表取已计算收益
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(order, 'pingtai_kuozhan'):
pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_shangpin_daily_stat(
ProductID=shangpin_id,
caozuo_leixing=2,
dingdan_jiage=order.Amount,
PlatformIncome=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
# 店铺统计(含上级分红)
if hasattr(order, 'pingtai_kuozhan'):
dianpu_id = order.pingtai_kuozhan.ShopID
shangji_id = order.pingtai_kuozhan.ParentShopID
shangji_fen = order.pingtai_kuozhan.ParentBonus or Decimal('0.00')
if dianpu_id:
update_dianpu_daily_stat(
ShopID=dianpu_id,
caozuo_leixing=2,
dingdan_jiage=order.Amount,
ShopIncome=dianpu_shouyi,
shangji_ShopID=shangji_id,
ParentBonus=shangji_fen
)
except Exception as e:
logger.error(f"商品/店铺统计失败: {e}")
# 14. 🆕 写入打手表现表(结算评价)
if jiedan_dashou_id:
record_dashou_biaoxian(
dingdan_id=dingdan_id,
dashou_id=jiedan_dashou_id,
xingwei=3, # 3=结算
fanche_RatingCount=fanche_cishu,
shangxian_sudu=shangxian_sudu,
fuwu_taidu=fuwu_taidu
# bankuai 暂不传入,后续可从订单商品关联
)
# 15. 返回成功
return Response({'code': 0, 'msg': '结单成功', 'data': None})
except Exception as e:
logger.error(f"结单事务异常: {e}", exc_info=True)
return Response({'code': 9, 'msg': '服务器内部错误,结单失败', 'data': None})
+541
View File
@@ -0,0 +1,541 @@
"""orders.views.chat - auto-generated by split script."""
# ==================== 标准库 ====================
import hmac
import os
import json
import time
import random
import string
import traceback
import threading
import requests
import hashlib
import xml.etree.ElementTree as ET
import xmltodict
import logging
from decimal import Decimal
from django.conf import settings
from django.db import models, transaction
from gvsdsdk.fluent import db, func, FQ
from django.db.models import F, Q, OuterRef, Subquery
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.authentication import JWTAuthentication
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sts.v20180813 import sts_client, models
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.money import yuan_to_fen
from utils.chat_utils import (
_send_group_message, _subscribe_users_to_group, establish_order_chat,
prepare_order_group_chat, resolve_pair_group_id,
parse_pair_from_group_id, resolve_pair_from_order, check_pair_chat_permission,
query_pair_orders, order_to_chat_dict,
)
from utils.fadan_utils import check_fadan_qiangdan_eligible
from orders.utils import (
calc_shangjia_order_fencheng,
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from backend.utils import update_shangjia_daily, update_dashou_daily_by_action, pick_leixing_id, datetime_aliases, fmt_datetime
from rank.services import record_dashou_biaoxian
from rank.utils import check_dashou_biaoqian_required
from orders.notice_tasks import dingdan_guangbo
from ..models import (
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate,
CommissionRate, PlayerRating, RefundRecord,
)
from jituan.services.club_config import get_commission_rate, get_commission_rate_object
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_penalty_club_id, resolve_penalty_record_club_id
from users.fadan_fenhong_utils import lock_penalty_bonus
from users.fadan_fenhong_utils import lock_penalty_bonus
from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from users.models import UserDashou, UserShangjia, UserBoss
from users.business_models import User
from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
from config.models import (
ShangjiaLianjie
)
logger = logging.getLogger(__name__)
def get_self_goeasy_appkey():
try:
return getattr(settings, 'GOEASY_APPKEY', None)
except Exception:
return None
def get_self_goeasy_secret():
try:
return getattr(settings, 'GOEASY_SECRET', None)
except Exception:
return None
def get_self_storage_domain():
try:
return getattr(settings, 'COS_DOMAIN', '')
except Exception:
return ''
def full_avatar(relative_url, bucket_domain=None):
if not relative_url:
return ''
if relative_url.startswith('http'):
return relative_url
domain = bucket_domain or get_self_storage_domain()
if domain:
domain = domain.rstrip('/') + '/'
return domain + relative_url.lstrip('/')
return relative_url
# ----------------- GoEasy API 封装 -----------------
def subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None):
"""订阅用户到群组,确保能收发该群消息"""
if not appkey:
appkey = get_self_goeasy_appkey()
if not secret:
secret = get_self_goeasy_secret()
if not appkey:
logger.error("订阅失败:GoEasy AppKey 未配置")
return False
url = 'https://rest-hangzhou.goeasy.io/v2/im/subscribe-groups'
body = {"appkey": appkey, "userIds": user_ids, "groupIds": group_ids}
headers = {"Content-Type": "application/json"}
if secret:
headers["Authorization"] = f"Bearer {secret}"
try:
r = requests.post(url, headers=headers, json=body, timeout=10)
if r.status_code == 200:
logger.info(f"订阅成功:用户 {user_ids} -> 群 {group_ids}")
return True
logger.error(f"订阅失败:{r.status_code} {r.text}")
return False
except Exception as e:
logger.exception("订阅异常")
return False
def send_group_message(
appkey, secret, group_id, sender_id, sender_name, sender_avatar,
message_text='', custom_payload=None, group_name='', order_id=''
):
"""
通过 GoEasy REST API 发送群消息。
message_text: 普通文本内容
custom_payload: 自定义消息体(如订单卡片),此时 type 设为 'order'
"""
url = 'https://rest-hangzhou.goeasy.io/v2/im/message'
to_data = {
"name": group_name or group_id,
"avatar": sender_avatar or '',
"orderId": order_id,
}
msg_type = 'order' if custom_payload else 'text'
payload = custom_payload if custom_payload else message_text
body = {
"appkey": appkey,
"senderId": sender_id,
"senderData": {"avatar": sender_avatar, "name": sender_name},
"to": {"type": "group", "id": group_id, "data": to_data},
"type": msg_type,
"payload": payload
}
headers = {"Content-Type": "application/json"}
if secret:
headers["Authorization"] = f"Bearer {secret}"
for attempt in range(3): # 最多重试3次
try:
r = requests.post(url, headers=headers, json=body, timeout=10)
if r.status_code == 200:
logger.info(f"消息发送成功:sender={sender_id}, group={group_id}, type={msg_type}")
return True
logger.warning(f"消息发送失败 (尝试 {attempt+1}/3):状态码 {r.status_code}, 响应 {r.text}")
except Exception as e:
logger.warning(f"消息发送异常 (尝试 {attempt+1}/3):{e}")
if attempt < 2:
time.sleep(1)
logger.error(f"消息最终发送失败:sender={sender_id}, group={group_id}")
return False
# ----------------- 权限校验 -----------------
def check_user_permission(identity_type, uid, order):
"""验证用户身份、账号状态、以及是否属于该订单"""
try:
user = User.query.get(UserUID=uid)
except User.DoesNotExist:
return False, '用户不存在'
if identity_type == 'PlayerID':
try:
dashou = UserDashou.query.get(user=user)
if dashou.zhanghaozhuangtai != 1 :
return False, '打手账号已被封禁或状态异常'
except UserDashou.DoesNotExist:
return False, '该用户不是打手'
if order.PlayerID != uid:
return False, '你不是该订单的接单打手'
elif identity_type == 'shangjia':
try:
shop = UserShangjia.query.get(user=user)
if shop.zhuangtai != 1:
return False, '商家账号已被禁用'
except UserShangjia.DoesNotExist:
return False, '该用户不是商家'
try:
if order.shangjia_kuozhan.MerchantID != uid:
return False, '你不是该订单的发布商家'
except MerchantOrderExt.DoesNotExist:
return False, '订单缺少商家信息'
elif identity_type == 'boss':
try:
UserBoss.query.get(user=user) # 老板暂无封禁字段,仅需存在
except UserBoss.DoesNotExist:
return False, '该用户不是老板'
try:
if order.pingtai_kuozhan.BossID != uid:
return False, '你不是该订单的老板'
except PlatformOrderExt.DoesNotExist:
return False, '订单缺少平台信息'
else:
return False, '不支持的身份类型'
return True, ''
# ----------------- 主视图 -----------------
class DaiLiQunLiaoXiaoXiView(APIView):
"""
代理发送群聊消息
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
# ===== 1. 参数解析 =====
order_id = request.data.get('orderId')
identity_type = request.data.get('identityType') # dashou / shangjia / boss
sender_name = request.data.get('senderName', '')
sender_avatar_raw = request.data.get('senderAvatar', '')
message_type = request.data.get('messageType', 'text')
message_text = request.data.get('text', '')
order_payload = request.data.get('orderPayload', None)
logger.info(f"[群聊代理] 收到请求: orderId={order_id}, identityType={identity_type}, "
f"messageType={message_type}, text={message_text[:20]}...")
if not order_id or not identity_type:
return Response({'code': 400, 'msg': '缺少 orderId 或 identityType'})
if identity_type not in ('PlayerID', 'shangjia', 'boss'):
return Response({'code': 400, 'msg': 'identityType 无效'})
# ===== 2. 构造 senderId =====
uid = request.user.UserUID
prefix_map = {'PlayerID': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'}
sender_id = prefix_map[identity_type] + uid
logger.info(f"[群聊代理] 登录用户 UserID={uid}, 构造 senderId={sender_id}")
# ===== 3. 查询订单 =====
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=order_id)
except Order.DoesNotExist:
logger.warning(f"[群聊代理] 订单 {order_id} 不存在")
return Response({'code': 404, 'msg': '订单不存在'})
# ===== 4. 权限校验 =====
valid, err_msg = check_user_permission(identity_type, uid, order)
if not valid:
logger.warning(f"[群聊代理] 权限校验失败: {err_msg}, uid={uid}, identity={identity_type}")
return Response({'code': 403, 'msg': err_msg})
# ===== 5. 我方 GoEasy 配置 =====
self_appkey = get_self_goeasy_appkey()
self_secret = get_self_goeasy_secret()
if not self_appkey:
logger.error("[群聊代理] 我方 GoEasy AppKey 未配置")
return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'})
# ===== 6. 公共群信息(新单走配对群,与文赫一致) =====
real_group_id = resolve_pair_group_id(order) or f"group_{order.OrderID}"
group_name = (order.Description[:20] + '…') if order.Description and len(order.Description) > 20 else f"订单{order.OrderID}"
sender_avatar = full_avatar(sender_avatar_raw) if sender_avatar_raw else full_avatar('')
# ===== 7. 订阅发送者到群 =====
if not subscribe_users_to_group([sender_id], [real_group_id], self_appkey, self_secret):
logger.error("[群聊代理] 订阅群组失败")
return Response({'code': 500, 'msg': '订阅群组失败'})
custom_payload = order_payload if message_type == 'order' else None
text_content = message_text
# ---------- 向我方群发送消息 ----------
local_ok = send_group_message(
appkey=self_appkey,
secret=self_secret,
group_id=real_group_id,
sender_id=sender_id,
sender_name=sender_name,
sender_avatar=sender_avatar,
message_text=text_content,
custom_payload=custom_payload,
group_name=group_name,
order_id=order.OrderID,
)
if not local_ok:
return Response({'code': 500, 'msg': '消息发送失败'})
return Response({'code': 200, 'msg': '发送成功'})
class ShangjiaLianjieLiuYanZhuanFaView(APIView):
"""
商家链接留言转发接口
路径:POST /dingdan/sjljlbxxzf
固定以 B{商家ID} 身份发送群消息。
"""
permission_classes = []
def post(self, request):
token = request.data.get('token', '').strip()
dingdan_id = str(request.data.get('dingdan_id') or request.data.get('dingdanId') or '').strip()
text = (request.data.get('text') or '').strip()
if not text or (not token and not dingdan_id):
return Response({'code': 400, 'msg': '缺少参数'})
if token:
try:
lianjie = ShangjiaLianjie.query.get(LinkToken=token)
except ShangjiaLianjie.DoesNotExist:
return Response({'code': 404, 'msg': '链接不存在或已失效'})
else:
try:
lianjie = ShangjiaLianjie.query.get(OrderID=dingdan_id)
except ShangjiaLianjie.DoesNotExist:
return Response({'code': 404, 'msg': '订单链接不存在或已失效'})
if not lianjie.OrderID:
return Response({'code': 400, 'msg': '链接缺少订单ID'})
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=lianjie.OrderID)
except Order.DoesNotExist:
return Response({'code': 500, 'msg': '订单数据异常'})
shangjia_uid = lianjie.UserID
sender_id = f"B{shangjia_uid}"
sender_name = "老板"
sender_avatar = ''
appkey = get_self_goeasy_appkey()
secret = get_self_goeasy_secret()
if not appkey:
return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'})
group_id = resolve_pair_group_id(order) or f"group_{order.OrderID}"
group_name = (order.Description[:20] + '…') if order.Description and len(order.Description) > 20 else f"订单{order.OrderID}"
if not _subscribe_users_to_group([sender_id], [group_id], appkey, secret):
return Response({'code': 500, 'msg': '订阅群组失败'})
local_ok = _send_group_message(
appkey=appkey,
secret=secret,
group_id=group_id,
sender_id=sender_id,
sender_name=sender_name,
sender_avatar=sender_avatar,
message_text=text,
custom_payload=None,
group_name=group_name,
order_id=order.OrderID,
)
if not local_ok:
return Response({'code': 500, 'msg': '消息发送失败'})
return Response({'code': 200, 'msg': '发送成功'})
class LianTongDuiHuaZhunBeiView(APIView):
"""进入订单群聊前准备:返回配对 groupId 及双方资料,并订阅 GoEasy 群(不影响抢单校验)"""
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
IDENTITY_MAP = {
'dashou': 'PlayerID',
'shangjia': 'shangjia',
'normal': 'boss',
'boss': 'boss',
}
def post(self, request):
dingdan_id = request.data.get('dingdan_id') or request.data.get('orderId')
identity_type = request.data.get('identityType') or ''
if not dingdan_id:
return Response({'code': 400, 'msg': '缺少 dingdan_id'})
backend_identity = self.IDENTITY_MAP.get(identity_type, identity_type)
if backend_identity not in ('PlayerID', 'shangjia', 'boss'):
return Response({'code': 400, 'msg': 'identityType 无效'})
uid = request.user.UserUID
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
valid, err_msg = check_user_permission(backend_identity, uid, order)
if not valid:
return Response({'code': 403, 'msg': err_msg})
if backend_identity == 'PlayerID' and not order.PlayerID:
return Response({'code': 400, 'msg': '订单尚未接单,暂无法进入群聊'})
data, subscribed = prepare_order_group_chat(order, backend_identity, uid, subscribe=True)
if not subscribed:
logger.warning(f'ltdhzb 订阅群失败 order={dingdan_id} uid={uid}')
return Response({'code': 0, 'msg': 'ok', 'data': data})
class LianTongPeiDuiDingDanLieBiaoView(APIView):
"""聊天页:获取打手与商家/老板之间的历史订单列表(可搜索)"""
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
IDENTITY_MAP = {
'dashou': 'PlayerID',
'shangjia': 'shangjia',
'normal': 'boss',
'boss': 'boss',
}
def post(self, request):
group_id = request.data.get('groupId') or request.data.get('group_id')
dingdan_id = request.data.get('dingdan_id') or request.data.get('orderId')
keyword = (request.data.get('keyword') or '').strip()
identity_type = request.data.get('identityType') or ''
backend_identity = self.IDENTITY_MAP.get(identity_type, identity_type)
if backend_identity not in ('PlayerID', 'shangjia', 'boss'):
return Response({'code': 400, 'msg': 'identityType 无效'})
dashou_uid, partner_uid, partner_role = parse_pair_from_group_id(group_id)
if not dashou_uid and dingdan_id:
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
dashou_uid, partner_uid, partner_role = resolve_pair_from_order(order)
if not dashou_uid or not partner_uid:
return Response({'code': 400, 'msg': '无法解析配对信息,请确认订单已接单'})
uid = request.user.UserUID
valid, err_msg = check_pair_chat_permission(backend_identity, uid, dashou_uid, partner_uid, partner_role)
if not valid:
return Response({'code': 403, 'msg': err_msg})
orders = query_pair_orders(dashou_uid, partner_uid, partner_role, keyword=keyword, limit=40)
latest = orders[0] if orders else None
return Response({'code': 0, 'msg': 'ok', 'data': {'list': orders, 'latest': latest}})
class LianTongDingDanZhuangTaiView(APIView):
"""聊天页:刷新单个订单状态"""
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
IDENTITY_MAP = LianTongPeiDuiDingDanLieBiaoView.IDENTITY_MAP
def post(self, request):
dingdan_id = request.data.get('dingdan_id') or request.data.get('orderId')
identity_type = request.data.get('identityType') or ''
if not dingdan_id:
return Response({'code': 400, 'msg': '缺少 dingdan_id'})
backend_identity = self.IDENTITY_MAP.get(identity_type, identity_type)
if backend_identity not in ('PlayerID', 'shangjia', 'boss'):
return Response({'code': 400, 'msg': 'identityType 无效'})
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
valid, err_msg = check_user_permission(backend_identity, request.user.UserUID, order)
if not valid:
return Response({'code': 403, 'msg': err_msg})
data = order_to_chat_dict(order)
return Response({'code': 0, 'msg': 'ok', 'data': data})
class LianTongDingDanZhuangTaiPiLiangView(APIView):
"""消息列表:批量刷新订单状态"""
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
order_ids = request.data.get('order_ids') or []
if not isinstance(order_ids, list):
return Response({'code': 400, 'msg': 'order_ids 格式错误'})
uid = request.user.UserUID
result = {}
for oid in order_ids[:20]:
oid = str(oid).strip()
if not oid:
continue
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=oid)
except Order.DoesNotExist:
continue
for identity in ('PlayerID', 'shangjia', 'boss'):
valid, _ = check_user_permission(identity, uid, order)
if valid:
d = order_to_chat_dict(order)
result[oid] = {
'jieshao': d['jieshao'],
'jine': d['jine'],
'zhuangtai': d['zhuangtai'],
'zhuangtaiText': d['zhuangtaiText'],
}
break
return Response({'code': 0, 'msg': 'ok', 'data': result})
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+378
View File
@@ -0,0 +1,378 @@
"""orders.views.merchant_penalty - auto-generated by split script."""
# ==================== 标准库 ====================
import hmac
import os
import json
import time
import random
import string
import traceback
import threading
import requests
import hashlib
import xml.etree.ElementTree as ET
import xmltodict
import logging
from decimal import Decimal
from django.conf import settings
from django.db import models, transaction
from gvsdsdk.fluent import db, func, FQ
from django.db.models import F, Q, OuterRef, Subquery
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.authentication import JWTAuthentication
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sts.v20180813 import sts_client, models
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.money import yuan_to_fen
from utils.chat_utils import (
_send_group_message, _subscribe_users_to_group, establish_order_chat,
prepare_order_group_chat, resolve_pair_group_id,
parse_pair_from_group_id, resolve_pair_from_order, check_pair_chat_permission,
query_pair_orders, order_to_chat_dict,
)
from utils.fadan_utils import check_fadan_qiangdan_eligible
from orders.utils import (
calc_shangjia_order_fencheng,
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from backend.utils import update_shangjia_daily, update_dashou_daily_by_action, pick_leixing_id, datetime_aliases, fmt_datetime
from rank.services import record_dashou_biaoxian
from rank.utils import check_dashou_biaoqian_required
from orders.notice_tasks import dingdan_guangbo
from ..models import (
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate,
CommissionRate, PlayerRating, RefundRecord,
)
from jituan.services.club_config import get_commission_rate, get_commission_rate_object
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_penalty_club_id, resolve_penalty_record_club_id
from users.fadan_fenhong_utils import lock_penalty_bonus
from users.fadan_fenhong_utils import lock_penalty_bonus
from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from users.models import UserDashou, UserShangjia, UserBoss
from users.business_models import User
from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
from config.models import (
ShangjiaLianjie
)
logger = logging.getLogger(__name__)
# ===================== 视图:商家罚款申请 =====================
# 文件路径:dingdan/views.py 或单独文件,根据项目结构放置
class ShangjiaFakuanApplyView(APIView):
"""
商家罚款申请接口
路径:POST /dingdan/sjfksq
权限:JWT认证的商家用户
参数:
dingdan_id - 订单ID (必填)
chufa_liyou - 罚款原因 (必填)
fakuanjine - 罚款金额,元 (必填)
yingxiang_qiangdan - 是否影响抢单 1是 0否 (必填)
说明:
1. 自动从订单中获取被打手ID,防止商家篡改目标。
2. 同一订单同一打手不允许重复罚款。
3. 跨平台订单若为我方派单,则通知对方平台处罚;否则本地生成罚单。
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# ========== 1. 身份验证 ==========
current_user = request.user
try:
shangjia_id = current_user.UserUID # 商家的用户ID
except AttributeError:
return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# ========== 2. 参数提取与校验 ==========
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
chufa_liyou = request.data.get('chufa_liyou', '').strip()
fakuanjine = request.data.get('fakuanjine', 0)
yingxiang_qiangdan = int(request.data.get('yingxiang_qiangdan', 1))
evidence_urls = request.data.get('evidence_urls') or []
if not dingdan_id or not chufa_liyou or not fakuanjine:
return Response({'code': 2, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
if not isinstance(evidence_urls, list):
return Response({'code': 2, 'msg': '证据图片格式错误'}, status=status.HTTP_400_BAD_REQUEST)
evidence_urls = [str(u).strip() for u in evidence_urls if str(u).strip()]
if len(evidence_urls) > 9:
return Response({'code': 2, 'msg': '证据图片最多9张'}, status=status.HTTP_400_BAD_REQUEST)
if len(evidence_urls) < 1:
return Response({'code': 2, 'msg': '请至少上传1张证据图片'}, status=status.HTTP_400_BAD_REQUEST)
try:
fakuanjine = float(fakuanjine)
if fakuanjine <= 0:
raise ValueError
except (ValueError, TypeError):
return Response({'code': 2, 'msg': '罚款金额格式错误'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 3. 获取订单,校验归属商家 ==========
try:
# 关联查询商家扩展表,确保订单属于当前商家
shangjia_kuozhan = MerchantOrderExt.query.select_related('Order').get(
Order__OrderID=dingdan_id,
MerchantID=shangjia_id
)
order = shangjia_kuozhan.Order
except MerchantOrderExt.DoesNotExist:
return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN)
# ========== 4. 获取被打手ID ==========
dashou_id = order.PlayerID
if not dashou_id:
return Response({'code': 4, 'msg': '该订单暂无接单打手,无法罚款'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 5. 重复罚款检查 ==========
if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=dingdan_id).exists():
return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 6. 本地创建罚单 ==========
staff_kefu_id = None
staff_member = getattr(request, '_staff_member', None)
if staff_member is not None:
staff_kefu_id = getattr(staff_member, 'staff_user_id', None) or None
from utils.penalty_status import PENALTY_PLATFORM_AUDIT
with transaction.atomic():
fadan = Penalty.query.create(
PenalizedUserID=dashou_id,
ApplicantID=shangjia_id, # 申请人为当前商家
Identity=1, # 被打者身份:1=打手
Reason=chufa_liyou,
FineAmount=fakuanjine,
RelatedOrderID=dingdan_id,
Status=PENALTY_PLATFORM_AUDIT, # 平台审核中,通过后变为待缴纳
AffectsGrabbing=yingxiang_qiangdan,
ApplicantIdentity=5, # 申请人是商家
ClubID=resolve_penalty_club_id(
order=order,
penalized_user_id=dashou_id,
request=request,
applicant_id=shangjia_id,
user=current_user,
),
ShopStaffUserID=staff_kefu_id,
)
for url in evidence_urls:
PenaltyAppealImage.query.create(
Penalty=fadan,
PenalizedUserID=dashou_id,
ImageURL=url,
Purpose=1,
)
lock_penalty_bonus(fadan)
logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}提交罚款申请(平台审核),金额{fakuanjine}")
return Response({'code': 0, 'msg': '罚款申请已提交,等待平台审核', 'data': {'fadan_id': fadan.id}})
class ShangjiaFakuaiXiugaiView(APIView):
"""
商家端统一修改接口:罚款修改/撤销/再次申请,订单退款撤销/修改理由
URL: /dingdan/sjfkxgycx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
# 验证商家身份
try:
shangjia = user.ShopProfile
except UserShangjia.DoesNotExist:
return Response({'code': 403, 'msg': '您不是商家'}, status=status.HTTP_403_FORBIDDEN)
if shangjia.zhuangtai != 1:
return Response({'code': 403, 'msg': '商家状态异常'}, status=status.HTTP_403_FORBIDDEN)
operation = request.data.get('operation')
dingdan_id = request.data.get('dingdan_id') or request.data.get('OrderID')
if not dingdan_id:
return Response({'code': 400, 'msg': '缺少订单ID'}, status=status.HTTP_400_BAD_REQUEST)
# 验证订单属于该商家
try:
dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
if not hasattr(dingdan, 'shangjia_kuozhan') or dingdan.shangjia_kuozhan.MerchantID != user.UserUID:
return Response({'code': 403, 'msg': '无权操作此订单'}, status=status.HTTP_403_FORBIDDEN)
# 处理订单相关操作
if operation == 'cancel_refund':
return self._cancel_refund(dingdan, request.data.get('liyou', ''))
elif operation == 'modify_refund_reason':
return self._modify_refund_reason(dingdan, request.data.get('liyou', ''))
# 处理罚款相关操作
fadan_id = request.data.get('fadan_id')
if not fadan_id:
return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST)
try:
fadan = Penalty.query.get(id=fadan_id)
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND)
if fadan.RelatedOrderID != dingdan_id:
return Response({'code': 403, 'msg': '罚单与订单不匹配'}, status=status.HTTP_403_FORBIDDEN)
if operation == 'modify_penalty':
return self._modify_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine'), user.UserUID)
elif operation == 'cancel_penalty':
return self._cancel_penalty(fadan, request.data.get('liyou', ''), user.UserUID)
elif operation == 'resubmit_penalty':
return self._resubmit_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine'), user.UserUID)
elif operation == 'manage_penalty_evidence':
return self._manage_penalty_evidence(fadan, request.data)
else:
return Response({'code': 400, 'msg': '无效的操作类型'}, status=status.HTTP_400_BAD_REQUEST)
def _cancel_refund(self, dingdan, liyou):
"""撤销退款申请(状态4 → 状态8,清空退款理由)"""
if dingdan.Status != 4:
return Response({'code': 400, 'msg': '只有退款中的订单可以撤销退款申请'})
with transaction.atomic():
dingdan.Status = 8
dingdan.RefundReason = '' # 清空退款理由
dingdan.save()
# 记录撤销理由可存入日志(如需要可另建日志表,此处简单处理)
logger.info(f"商家撤销退款订单 {dingdan.OrderID},理由: {liyou}")
return Response({'code': 0, 'msg': '已撤销退款申请'})
def _modify_refund_reason(self, dingdan, liyou):
"""修改退款理由(仅状态4时)"""
if dingdan.Status != 4:
return Response({'code': 400, 'msg': '只有退款中的订单可以修改退款理由'})
if not liyou.strip():
return Response({'code': 400, 'msg': '退款理由不能为空'})
dingdan.RefundReason = liyou[:500]
dingdan.save()
return Response({'code': 0, 'msg': '退款理由修改成功'})
def _modify_penalty(self, fadan, liyou, jine_str, processor_id=None):
"""修改罚款(待缴纳 / 申诉中 / 平台审核中)"""
from utils.penalty_status import PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT
if fadan.Status not in (PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT):
return Response({'code': 400, 'msg': '当前罚单状态不可修改'})
if not liyou.strip():
return Response({'code': 400, 'msg': '罚款理由不能为空'})
try:
jine = Decimal(jine_str)
if jine <= 0:
raise ValueError
except:
return Response({'code': 400, 'msg': '罚款金额必须为正数'})
with transaction.atomic():
fadan.Reason = liyou[:500]
fadan.FineAmount = jine
if processor_id:
fadan.ProcessorID = processor_id
fadan.ProcessorIdentity = 5 # 小程序商家自行处理
fadan.save()
lock_penalty_bonus(fadan)
return Response({'code': 0, 'msg': '罚款修改成功'})
def _cancel_penalty(self, fadan, liyou, processor_id=None):
"""撤销罚款(待缴纳 / 申诉中 / 平台审核中 → 已驳回)"""
from utils.penalty_status import PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT, PENALTY_REJECTED
if fadan.Status not in (PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT):
return Response({'code': 400, 'msg': '当前罚单状态不可撤销'})
if not liyou.strip():
return Response({'code': 400, 'msg': '驳回理由不能为空'})
with transaction.atomic():
fadan.Status = PENALTY_REJECTED # 已驳回
fadan.RejectReason = liyou[:500]
if processor_id:
fadan.ProcessorID = processor_id
fadan.ProcessorIdentity = 5 # 小程序商家撤销=商家作为处理人
fadan.save()
return Response({'code': 0, 'msg': '罚款已撤销(驳回)'})
def _resubmit_penalty(self, fadan, liyou, jine_str, processor_id=None):
"""再次申请罚款(已驳回 → 平台审核中)"""
from utils.penalty_status import PENALTY_REJECTED, PENALTY_PLATFORM_AUDIT
if fadan.Status != PENALTY_REJECTED:
return Response({'code': 400, 'msg': '只有已驳回的罚单可再次申请'})
if not liyou.strip():
return Response({'code': 400, 'msg': '罚款理由不能为空'})
try:
jine = Decimal(jine_str)
if jine <= 0:
raise ValueError
except:
return Response({'code': 400, 'msg': '罚款金额必须为正数'})
with transaction.atomic():
fadan.Status = PENALTY_PLATFORM_AUDIT
fadan.Reason = liyou[:500]
fadan.FineAmount = jine
fadan.RejectReason = '' # 清空驳回理由
if processor_id:
fadan.ProcessorID = processor_id
fadan.ProcessorIdentity = 5
fadan.save()
lock_penalty_bonus(fadan)
logger.info(f"商家重新申请罚单{fadan.id},金额={jine}")
return Response({'code': 0, 'msg': '已重新申请罚款'})
def _manage_penalty_evidence(self, fadan, data):
"""管理罚单证据图片(Purpose=1),与申诉图片严格隔离"""
from utils.penalty_status import PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT
if fadan.Status not in (PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT):
return Response({'code': 400, 'msg': '当前罚单状态不可修改证据图片'})
add_urls = data.get('add_urls') or []
remove_ids = data.get('remove_ids') or []
if not add_urls and not remove_ids:
return Response({'code': 400, 'msg': '未提供图片变更'})
with transaction.atomic():
if remove_ids:
PenaltyAppealImage.query.filter(
Penalty=fadan, Purpose=1, id__in=remove_ids
).delete()
existing_count = PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=1).count()
if existing_count + len(add_urls) > 9:
return Response({'code': 400, 'msg': '证据图片最多9张'})
for url in add_urls:
url = str(url).strip()
if not url:
continue
PenaltyAppealImage.query.create(
Penalty=fadan,
PenalizedUserID=fadan.PenalizedUserID,
ImageURL=url,
Purpose=1,
)
logger.info(
f"罚单{fadan.id}证据图变更:新增{len(add_urls)}张,删除{len(remove_ids)}张"
)
return Response({'code': 0, 'msg': '证据图片已更新'})
File diff suppressed because it is too large. Load diff
-6105
View File
File diff suppressed because it is too large. Load diff
+108
View File
@@ -0,0 +1,108 @@
"""
商品应用视图聚合模块(products/views/__init__.py)
本模块原为 products/views.py(6105 行单文件),已按功能组拆分到 products/views/ 子包。
本文件作为聚合 re-export 入口,重新导出所有视图类与公共符号。
外部引用(products/urls.py、jituan/views_miniapp_assets.py、jituan/views_display.py)保持不变。
功能组拆分:
- product_query 商品/会员/分红 数据查询
- yajin_payment 押金支付流程
- jifen_payment 积分支付流程
- huiyuan_payment 会员购买支付流程
- chongzhi_payment 商家充值与充值抵扣(含 INTEGRAL_* / IDENTITY_CONFIG 常量)
- shangpin_admin 商品发布与管理后台
"""
import logging
# 各功能组视图
from .product_query import (
ShangpinHuoquView,
ShangpinXiangqingHuoquView,
DashouHuiyuanList,
GuanshiFenhongListAPI,
AdGetAllDataView,
HuiyuanTypeListView,
HuiyuanRecordListView,
ZuzhangFenhongView,
)
from .yajin_payment import (
YajinGoumai,
YajinHuitiao,
YajinShibai,
YajinLunxun,
)
from .jifen_payment import (
JifenBuchong,
JifenHuitiao,
JifenShibai,
JifenLunxun,
)
from .huiyuan_payment import (
HuiyuanGoumai,
HuiyuanHuitiao,
HuiyuanShibai,
HuiyuanLunxun,
)
from .chongzhi_payment import (
ShangjiaChongzhi,
ShangjiaHuitiao,
ShangjiaShibai,
ShangjiaChenggong,
CzhqdyView,
DsqrgmdhView,
INTEGRAL_PRICE,
INTEGRAL_INCREMENT,
MAX_INTEGRAL,
IDENTITY_CONFIG,
)
from .shangpin_admin import (
ShangpinLeixingFabuView,
ShangpinZhuanquFabuView,
ShangpinFabuView,
GuizeTupianShangchuanView,
AdHuiyuanHqView,
HuiyuanXiugaiView,
AdHuiyuanFbView,
AdShangpinHuQu,
AdGonggaoXiuGai,
AdZhuanquXiuGai,
AdLunboXiuGai,
AdLeixingXiuGai,
AdShangpinNeiRongHuoQu,
AdShangpinXiuGai,
AdShangpinGuizeXiuGai,
)
# 重新导出原文件从 utils.oss_utils 引入的公共符号,保持外部引用不变
# (jituan/views_miniapp_assets.py、jituan/views_display.py 依赖)
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
# 模块级符号(保持向后兼容)
logger = logging.getLogger(__name__)
__all__ = [
# product_query
'ShangpinHuoquView', 'ShangpinXiangqingHuoquView', 'DashouHuiyuanList',
'GuanshiFenhongListAPI', 'AdGetAllDataView',
'HuiyuanTypeListView', 'HuiyuanRecordListView', 'ZuzhangFenhongView',
# yajin_payment
'YajinGoumai', 'YajinHuitiao', 'YajinShibai', 'YajinLunxun',
# jifen_payment
'JifenBuchong', 'JifenHuitiao', 'JifenShibai', 'JifenLunxun',
# huiyuan_payment
'HuiyuanGoumai', 'HuiyuanHuitiao', 'HuiyuanShibai', 'HuiyuanLunxun',
# chongzhi_payment
'ShangjiaChongzhi', 'ShangjiaHuitiao', 'ShangjiaShibai', 'ShangjiaChenggong',
'CzhqdyView', 'DsqrgmdhView',
'INTEGRAL_PRICE', 'INTEGRAL_INCREMENT', 'MAX_INTEGRAL', 'IDENTITY_CONFIG',
# shangpin_admin
'ShangpinLeixingFabuView', 'ShangpinZhuanquFabuView', 'ShangpinFabuView',
'GuizeTupianShangchuanView',
'AdHuiyuanHqView', 'HuiyuanXiugaiView', 'AdHuiyuanFbView', 'AdShangpinHuQu',
'AdGonggaoXiuGai', 'AdZhuanquXiuGai', 'AdLunboXiuGai', 'AdLeixingXiuGai',
'AdShangpinNeiRongHuoQu', 'AdShangpinXiuGai', 'AdShangpinGuizeXiuGai',
# 公共符号(向后兼容)
'validate_image', 'upload_to_oss', 'delete_from_oss',
'logger',
]
File diff suppressed because it is too large. Load diff
+677
View File
@@ -0,0 +1,677 @@
"""会员购买支付流程视图。
包含会员购买参数生成、支付回调、失败处理、轮询。
"""
# ==================== 标准库 ====================
import os
import json
import time
import random
import string
import uuid
import hashlib
import requests
import logging
import decimal
from datetime import timedelta
from decimal import Decimal, InvalidOperation, ROUND_CEILING
import defusedxml.ElementTree as ET
# ==================== Django 内置 ====================
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q
from gvsdsdk.fluent import db, func, FQ
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.utils import timezone
# ==================== DRF 框架 ====================
from rest_framework.views import APIView, View
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
# ==================== 项目工具函数 ====================
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import format_yuan, quantize_yuan, yuan_to_fen
from backend.utils import (
update_guanshi_daily_by_action,
update_guanshi_xufei_daily,
update_zuzhang_daily_by_action
)
from users.fadan_fenhong_utils import process_fadan_fenhong
# ==================== 各App模型 ====================
from ..models import (
ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
)
from users.models import (
AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
)
from users.business_models import User
from orders.models import CommissionRate
from config.models import Gonggao, Lunbo
from orders.models import Penalty # 用于罚款类型
from jituan.services.club_config import (
get_commission_rate,
get_commission_rate_object,
get_huiyuan_price,
get_huiyuan_fenchong,
)
from jituan.services.club_context import resolve_effective_club_id
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_gsfenhong_club_id
import traceback
# ==================== 日志实例 ====================
logger = logging.getLogger(__name__)
class HuiyuanGoumai(APIView):
"""
会员充值支付参数接口
路径:/shangpin/huiyuangm
请求方式:POST
权限:JWT认证
参数:huiyuanid(会员ID)
返回:支付参数(payParams)和订单ID(dingdanid)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取会员ID参数
huiyuanid = request.data.get('huiyuanid')
is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial'))
if not huiyuanid:
return Response({
'code': 400,
'message': '会员ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 验证用户身份和状态
try:
dashou = request.user.DashouProfile
except UserDashou.DoesNotExist:
return Response({
'code': 400,
'message': '用户不是打手,无法购买会员'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查打手账号状态
if dashou.zhanghaozhuangtai != 1:
return Response({
'code': 400,
'message': '账号状态异常,无法进行购买'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 查询会员信息
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuanid)
except Huiyuan.DoesNotExist:
return Response({
'code': 400,
'message': '会员信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
club_id = resolve_effective_club_id(request, request.user)
from jituan.services.member_recharge import validate_member_purchase
ok, err_msg, club_price, purchase_days = validate_member_purchase(
request.user.UserUID, huiyuanid, club_id, is_trial,
)
if not ok:
return Response({
'code': 400,
'message': err_msg or '不可购买',
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 生成订单ID(前缀CzHY表示会员充值)
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 6. 创建充值记录订单(金额仅用本俱乐部配置)
jine = quantize_yuan(club_price)
# 组合订单说明:会员介绍 + 配置描述
shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}"
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付状态
yonghuid=request.user.UserUID,
jine=jine,
leixing=1, # 充值类型:1=会员
shuoming=shuoming,
huiyuan_id=huiyuanid, # 存储会员ID
club_id=club_id,
is_trial=is_trial,
purchase_days=int(purchase_days),
)
logger.info(
f"创建会员充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, "
f"会员{huiyuanid}, 俱乐部{club_id}, 体验={is_trial}, "
f"金额{jine}元, 天数{purchase_days}")
# 7. 生成微信支付参数
try:
pay_openid, _ = get_payment_openid(request)
if not pay_openid:
chongzhi_jilu.delete()
return Response({'code': 400, 'message': '用户openid不存在'}, status=status.HTTP_400_BAD_REQUEST)
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
openid=pay_openid,
pay_type='huiyuan'
)
except Exception as e:
# 支付参数生成失败,删除订单记录
chongzhi_jilu.delete()
logger.error(f"生成会员支付参数失败: {str(e)}")
return Response({
'code': 500,
'message': f'支付参数生成失败: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 8. 返回支付参数和订单ID
return Response({
'code': 200,
'message': '支付参数生成成功',
'payParams': pay_params,
'dingdanid': dingdanid,
'club_id': club_id,
'jine': format_yuan(jine),
'is_trial': is_trial,
'purchase_days': purchase_days,
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"会员充值接口异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误,请稍后重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信支付参数(完整版,无省略)"""
# 微信支付配置 - 从settings中获取
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.wechat_pay import get_wechat_v2_config
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user))
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL
elif pay_type == 'jifen':
PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION
NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL
elif pay_type == 'huiyuan':
PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION
NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL
else:
raise Exception('未知的支付类型')
# 生成随机字符串
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 金额转换(元转分)
total_fee = yuan_to_fen(jine)
# 交易类型(小程序支付使用JSAPI)
trade_type = 'JSAPI'
# 获取客户端IP
user_ip = self.get_client_ip()
# 1. 第一次签名(统一下单接口)
sign_data = {
'appid': APPID,
'mch_id': MCHID,
'nonce_str': nonce_str,
'body': PAY_DESCRIPTION,
'out_trade_no': dingdanid,
'total_fee': total_fee,
'spbill_create_ip': user_ip,
'notify_url': NOTIFY_URL,
'trade_type': trade_type,
'openid': openid # 小程序支付必须传openid
}
# 按照参数名ASCII码从小到大排序
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
sign_string += f'&key={KEY}'
# MD5签名
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
# 构造XML数据
xml_data = f"""<xml>
<appid>{APPID}</appid>
<body>{PAY_DESCRIPTION}</body>
<mch_id>{MCHID}</mch_id>
<nonce_str>{nonce_str}</nonce_str>
<notify_url>{NOTIFY_URL}</notify_url>
<openid>{openid}</openid>
<out_trade_no>{dingdanid}</out_trade_no>
<spbill_create_ip>{user_ip}</spbill_create_ip>
<total_fee>{total_fee}</total_fee>
<trade_type>{trade_type}</trade_type>
<sign>{sign}</sign>
</xml>"""
# 调用微信统一下单接口(官方地址)
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
timeout=10 # 设置超时
)
# 解析返回的XML
root = ET.fromstring(response.content)
return_code = root.find('return_code').text
result_code = root.find('result_code').text
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
# 获取prepay_id
prepay_id = root.find('prepay_id').text
# 2. 第二次签名(小程序支付参数)
timestamp = str(int(time.time()))
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
package = f'prepay_id={prepay_id}'
sign_type = 'MD5'
# 生成支付签名(注意:这里key的大小写和字段名与第一次不同)
pay_sign_data = {
'appId': APPID, # 注意:这里是appId,不是appid
'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp
'nonceStr': nonce_str,
'package': package,
'signType': sign_type
}
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
pay_sign_string += f'&key={KEY}'
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
# 返回前端需要的支付参数
return {
'appId': APPID, # 对应前端:appId
'timeStamp': timestamp, # 对应前端:timeStamp
'nonceStr': nonce_str, # 对应前端:nonceStr
'package': package, # 对应前端:package
'signType': sign_type, # 对应前端:signType
'paySign': pay_sign # 对应前端:paySign
}
else:
# 获取错误信息
err_code = root.find('err_code')
err_code_des = root.find('err_code_des')
return_msg = root.find('return_msg')
error_details = f"return_code: {return_code}"
if err_code is not None:
error_details += f", err_code: {err_code.text}"
if err_code_des is not None:
error_details += f", err_code_des: {err_code_des.text}"
if return_msg is not None:
error_details += f", return_msg: {return_msg.text}"
raise Exception(f"微信支付下单失败: {error_details}")
def get_client_ip(self):
"""获取客户端IP(完整版)"""
request = self.request
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip if ip else '127.0.0.1'
class HuiyuanHuitiao(View):
"""
会员支付回调接口
路径:/shangpin/huiyuanhuitiao
"""
def post(self, request):
try:
# 1. 解析微信回调XML数据
request_body = request.body
logger.info(f"收到会员支付回调,原始数据: {request_body.decode('utf-8')}")
root = ET.fromstring(request_body)
return_code = root.find('return_code').text
result_code = root.find('result_code').text
out_trade_no = root.find('out_trade_no').text
transaction_id = root.find('transaction_id').text
total_fee = int(root.find('total_fee').text)
logger.info(f"会员回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分")
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
logger.warning(f"会员支付回调状态异常: {return_code}/{result_code}")
return self.wechat_response('FAIL', '支付失败')
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
notify_club = club_id_from_czjilu(out_trade_no)
if not verify_wechat_v2_signature(request_body, notify_club):
logger.error(f"会员签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
# 查询订单
try:
order = Czjilu.query.get(dingdan_id=out_trade_no)
except Czjilu.DoesNotExist:
logger.error(f"会员订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
if order.leixing != 1:
logger.error(f"订单类型错误: {order.leixing}, 期望1(会员)")
return self.wechat_response('FAIL', '订单类型错误')
if order.zhuangtai != 9:
logger.warning(f"会员订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
if order.zhuangtai == 3:
from jituan.services.member_recharge import repair_member_entitlement_if_paid
repair_member_entitlement_if_paid(out_trade_no)
try:
from decimal import Decimal
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
Decimal(str(order.jine)),
getattr(order, 'club_id', None),
biz_ref=f'cz:{out_trade_no}',
)
except Exception as exc:
logger.error('会员回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True)
return self.wechat_response('SUCCESS', 'OK')
wechat_amount_yuan = total_fee / 100
order_amount_yuan = float(order.jine)
if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
logger.error(f"会员金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
return self.wechat_response('FAIL', '金额不一致')
from jituan.services.member_recharge import fulfill_member_recharge, MemberRechargeError
try:
fulfill_member_recharge(
out_trade_no,
paid_amount_yuan=wechat_amount_yuan,
source='callback',
)
except MemberRechargeError as exc:
logger.error('会员回调履约失败 %s: %s', out_trade_no, exc)
return self.wechat_response('FAIL', str(exc))
logger.info(f"会员支付回调处理完成: {out_trade_no}")
return self.wechat_response('SUCCESS', 'OK')
except ET.ParseError as e:
logger.error(f"会员XML解析失败: {str(e)}")
return self.wechat_response('FAIL', '数据格式错误')
except Exception as e:
logger.error(f"会员支付回调处理异常: {str(e)}", exc_info=True)
return self.wechat_response('FAIL', '处理异常')
# 履约逻辑见 jituan.services.member_recharge(回调与 /huiyuanlx 确认共用)
def verify_signature(self, xml_data):
"""验证微信支付签名(与原代码一致)"""
try:
root = ET.fromstring(xml_data)
sign = root.find('sign').text
if not sign:
logger.error("会员签名字段不存在")
return False
params = {}
for child in root:
if child.tag != 'sign' and child.text is not None:
params[child.tag] = child.text
sorted_params = sorted(params.items(), key=lambda x: x[0])
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
return sign == calculated_sign
except Exception as e:
logger.error(f"会员验证签名异常: {str(e)}")
return False
def wechat_response(self, return_code, return_msg):
"""返回微信回调响应"""
xml_response = f"""<xml>
<return_code><![CDATA[{return_code}]]></return_code>
<return_msg><![CDATA[{return_msg}]]></return_msg>
</xml>"""
logger.info(f"会员返回微信响应: {return_code} - {return_msg}")
return HttpResponse(xml_response, content_type='application/xml')
# views.py
class HuiyuanShibai(APIView):
"""
会员支付失败处理接口
路径:/shangpin/huiyuanshibai
请求方式:POST
权限:JWT认证
参数:dingdanid(订单ID)
返回:删除结果
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
yonghuid=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单类型是否为会员(1)
if order.leixing != 1:
return Response({
'code': 400,
'message': '订单类型错误,不是会员订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 检查订单状态是否为未支付(9)
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付会员订单: {dingdanid}")
return Response({
'code': 200,
'message': '会员订单已删除'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '会员订单状态不是未支付,无法删除'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"会员支付失败处理异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py
class HuiyuanLunxun(APIView):
"""
会员支付成功轮询接口
路径:/shangpin/huiyuanlx
请求方式:POST
权限:JWT认证
参数:dingdanid(订单ID)
返回:订单状态和会员信息
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
yonghuid=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单类型是否为会员(1)
if order.leixing != 1:
return Response({
'code': 400,
'message': '订单类型错误,不是会员订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 未支付:查微信并幂等履约(与回调互斥,zhuangtai!=9 则跳过)
if order.zhuangtai == 9:
from jituan.services.member_recharge import (
confirm_member_recharge_paid,
MemberRechargeError,
)
try:
confirm_member_recharge_paid(dingdanid, request.user.UserUID)
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
except MemberRechargeError as exc:
logger.info(f"会员确认未就绪 {dingdanid}: {exc}")
return Response({
'code': 400,
'message': str(exc),
}, status=status.HTTP_400_BAD_REQUEST)
if order.zhuangtai != 3:
return Response({
'code': 400,
'message': '会员订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
from jituan.services.member_recharge import repair_member_entitlement_if_paid
repair_member_entitlement_if_paid(dingdanid)
# 5. 查询会员信息
try:
huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id)
# 6. 查询会员购买记录
goumai_record = Huiyuangoumai.query.filter(
yonghu_id=request.user.UserUID,
huiyuan_id=order.huiyuan_id
).first()
if not goumai_record:
return Response({
'code': 400,
'message': '会员购买记录不存在'
}, status=status.HTTP_400_BAD_REQUEST)
# 7. 格式化到期时间
#from django.utils.timezone import localtime
#daoqi_time = localtime(goumai_record.daoqi_time).strftime('%Y-%m-%d %H:%M:%S')
# 直接格式化,不使用时区转换
daoqi_time = goumai_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')
current_user = request.user
dashou = current_user.DashouProfile
logger.info(f"会员轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}, 会员{order.huiyuan_id}")
# 8. 返回会员信息(用于更新前端全局变量)
return Response({
'code': 200,
'message': '支付流程全部完成',
'jifen': dashou.jifen,
'huiyuan': {
'huiyuanid': order.huiyuan_id,
'huiyuanming': huiyuan.jieshao,
'daoqi': daoqi_time
}
}, status=status.HTTP_200_OK)
except Huiyuan.DoesNotExist:
logger.error(f"会员信息不存在: {order.huiyuan_id}")
return Response({
'code': 400,
'message': '会员信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"会员轮询异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py in shangpin app
+607
View File
@@ -0,0 +1,607 @@
"""积分支付流程视图。
包含积分补充、充值回调、失败处理、轮询。
"""
# ==================== 标准库 ====================
import os
import json
import time
import random
import string
import uuid
import hashlib
import requests
import logging
import decimal
from datetime import timedelta
from decimal import Decimal, InvalidOperation, ROUND_CEILING
import defusedxml.ElementTree as ET
# ==================== Django 内置 ====================
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q
from gvsdsdk.fluent import db, func, FQ
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.utils import timezone
# ==================== DRF 框架 ====================
from rest_framework.views import APIView, View
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
# ==================== 项目工具函数 ====================
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import format_yuan, quantize_yuan, yuan_to_fen
from backend.utils import (
update_guanshi_daily_by_action,
update_guanshi_xufei_daily,
update_zuzhang_daily_by_action
)
from users.fadan_fenhong_utils import process_fadan_fenhong
# ==================== 各App模型 ====================
from ..models import (
ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
)
from users.models import (
AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
)
from users.business_models import User
from orders.models import CommissionRate
from config.models import Gonggao, Lunbo
from orders.models import Penalty # 用于罚款类型
from jituan.services.club_config import (
get_commission_rate,
get_commission_rate_object,
get_huiyuan_price,
get_huiyuan_fenchong,
)
from jituan.services.club_context import resolve_effective_club_id
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_gsfenhong_club_id
import traceback
# ==================== 日志实例 ====================
logger = logging.getLogger(__name__)
class JifenBuchong(APIView):
"""
积分补充支付参数接口
路径:/shangpin/jifenbc
请求方式:POST
权限:JWT认证
参数:无(固定5元)
返回:支付参数(payParams)和订单ID(dingdanid)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 验证用户身份和状态
try:
dashou = request.user.DashouProfile
except UserDashou.DoesNotExist:
return Response({
'code': 400,
'message': '用户不是打手,无法补充积分'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 检查打手账号状态
if dashou.zhanghaozhuangtai != 1:
return Response({
'code': 400,
'message': '账号状态异常,无法进行操作'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查积分是否已满(满分10分)
if dashou.jifen >= 10:
return Response({
'code': 400,
'message': '积分已满,无需补充'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 生成订单ID(前缀CzJF表示积分充值)
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"CzJF{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 5. 创建积分充值记录订单(固定5元)
jine = 5.00 # 固定5元
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付状态
yonghuid=request.user.UserUID,
jine=jine,
leixing=3, # 充值类型:3=积分
shuoming=settings.JIFEN_PAY_DESCRIPTION,
club_id=resolve_club_id_for_write(request, request.user),
)
logger.info(f"创建积分充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}元")
# 6. 生成微信支付参数(完整版)
try:
pay_openid, _ = get_payment_openid(request)
if not pay_openid:
chongzhi_jilu.delete()
return Response({'code': 400, 'message': '用户openid不存在'}, status=status.HTTP_400_BAD_REQUEST)
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
openid=pay_openid,
pay_type='jifen'
)
except Exception as e:
# 支付参数生成失败,删除订单记录
chongzhi_jilu.delete()
logger.error(f"生成积分支付参数失败: {str(e)}")
return Response({
'code': 500,
'message': f'支付参数生成失败: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 7. 返回支付参数和订单ID
return Response({
'code': 200,
'message': '支付参数生成成功',
'payParams': pay_params, # 对应前端payParams
'dingdanid': dingdanid # 对应前端dingdanid
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"积分充值接口异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误,请稍后重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信支付参数(完整版,无省略)"""
# 微信支付配置 - 从settings中获取
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.wechat_pay import get_wechat_v2_config
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user))
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL
elif pay_type == 'jifen':
PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION
NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL
elif pay_type == 'huiyuan':
PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION
NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL
else:
raise Exception('未知的支付类型')
# 生成随机字符串
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 金额转换(元转分)
total_fee = yuan_to_fen(jine)
# 交易类型(小程序支付使用JSAPI)
trade_type = 'JSAPI'
# 获取客户端IP
user_ip = self.get_client_ip()
# 1. 第一次签名(统一下单接口)
sign_data = {
'appid': APPID,
'mch_id': MCHID,
'nonce_str': nonce_str,
'body': PAY_DESCRIPTION,
'out_trade_no': dingdanid,
'total_fee': total_fee,
'spbill_create_ip': user_ip,
'notify_url': NOTIFY_URL,
'trade_type': trade_type,
'openid': openid # 小程序支付必须传openid
}
# 按照参数名ASCII码从小到大排序
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
sign_string += f'&key={KEY}'
# MD5签名
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
# 构造XML数据
xml_data = f"""<xml>
<appid>{APPID}</appid>
<body>{PAY_DESCRIPTION}</body>
<mch_id>{MCHID}</mch_id>
<nonce_str>{nonce_str}</nonce_str>
<notify_url>{NOTIFY_URL}</notify_url>
<openid>{openid}</openid>
<out_trade_no>{dingdanid}</out_trade_no>
<spbill_create_ip>{user_ip}</spbill_create_ip>
<total_fee>{total_fee}</total_fee>
<trade_type>{trade_type}</trade_type>
<sign>{sign}</sign>
</xml>"""
# 调用微信统一下单接口(官方地址)
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
timeout=10 # 设置超时
)
# 解析返回的XML
root = ET.fromstring(response.content)
return_code = root.find('return_code').text
result_code = root.find('result_code').text
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
# 获取prepay_id
prepay_id = root.find('prepay_id').text
# 2. 第二次签名(小程序支付参数)
timestamp = str(int(time.time()))
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
package = f'prepay_id={prepay_id}'
sign_type = 'MD5'
# 生成支付签名(注意:这里key的大小写和字段名与第一次不同)
pay_sign_data = {
'appId': APPID, # 注意:这里是appId,不是appid
'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp
'nonceStr': nonce_str,
'package': package,
'signType': sign_type
}
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
pay_sign_string += f'&key={KEY}'
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
# 返回前端需要的支付参数
return {
'appId': APPID, # 对应前端:appId
'timeStamp': timestamp, # 对应前端:timeStamp
'nonceStr': nonce_str, # 对应前端:nonceStr
'package': package, # 对应前端:package
'signType': sign_type, # 对应前端:signType
'paySign': pay_sign # 对应前端:paySign
}
else:
# 获取错误信息
err_code = root.find('err_code')
err_code_des = root.find('err_code_des')
return_msg = root.find('return_msg')
error_details = f"return_code: {return_code}"
if err_code is not None:
error_details += f", err_code: {err_code.text}"
if err_code_des is not None:
error_details += f", err_code_des: {err_code_des.text}"
if return_msg is not None:
error_details += f", return_msg: {return_msg.text}"
raise Exception(f"微信支付下单失败: {error_details}")
def get_client_ip(self):
"""获取客户端IP(完整版)"""
request = self.request
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip if ip else '127.0.0.1'
# views.py
class JifenHuitiao(View):
"""
积分支付回调接口
路径:/shangpin/jifenhuitiao
注意:这个接口不需要JWT认证,由微信服务器调用
"""
def post(self, request):
try:
# 1. 解析微信回调XML数据
request_body = request.body
logger.info(f"收到积分支付回调,原始数据: {request_body.decode('utf-8')}")
root = ET.fromstring(request_body)
# 获取关键字段
return_code = root.find('return_code').text
result_code = root.find('result_code').text
out_trade_no = root.find('out_trade_no').text # 订单ID
transaction_id = root.find('transaction_id').text # 微信交易号
total_fee = int(root.find('total_fee').text) # 支付金额(分)
logger.info(f"积分回调数据: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分")
# 2. 验证基本返回状态
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
logger.warning(f"积分支付回调状态异常: {return_code}/{result_code}")
return self.wechat_response('FAIL', '支付失败')
# 3. 验证签名
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
notify_club = club_id_from_czjilu(out_trade_no)
if not verify_wechat_v2_signature(request_body, notify_club):
logger.error(f"积分签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
# 4. 查询订单
try:
order = Czjilu.query.get(dingdan_id=out_trade_no)
logger.info(f"找到积分订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
except Czjilu.DoesNotExist:
logger.error(f"积分订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
# 5. 检查订单状态是否为未支付(9)
if order.zhuangtai != 9:
logger.warning(f"积分订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
Decimal(str(order.jine)),
getattr(order, 'club_id', None),
biz_ref=f'cz:{out_trade_no}',
)
except Exception as exc:
logger.error('积分回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True)
return self.wechat_response('SUCCESS', 'OK')
# 6. 验证金额(固定5元)
wechat_amount_yuan = total_fee / 100 # 转换为元
order_amount_yuan = float(order.jine) # 订单金额(元)
# 验证是否是5元(允许1分钱误差)
if abs(wechat_amount_yuan - 5.00) > 0.01:
logger.error(f"积分金额错误: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
return self.wechat_response('FAIL', '金额错误')
from jituan.services.czjilu_fulfill import fulfill_jifen_recharge, CzjiluFulfillError
try:
fulfill_jifen_recharge(out_trade_no, paid_amount_yuan=wechat_amount_yuan)
except CzjiluFulfillError as exc:
logger.error('积分回调履约失败 %s: %s', out_trade_no, exc)
return self.wechat_response('FAIL', str(exc))
logger.info(f"积分支付回调处理完成: {out_trade_no}")
return self.wechat_response('SUCCESS', 'OK')
except ET.ParseError as e:
logger.error(f"积分XML解析失败: {str(e)}")
return self.wechat_response('FAIL', '数据格式错误')
except Exception as e:
logger.error(f"积分支付回调处理异常: {str(e)}", exc_info=True)
return self.wechat_response('FAIL', '处理异常')
def verify_signature(self, xml_data):
"""验证微信支付签名"""
try:
root = ET.fromstring(xml_data)
sign = root.find('sign').text
if not sign:
logger.error("积分数签名字段不存在")
return False
# 获取所有参数并排除sign
params = {}
for child in root:
if child.tag != 'sign' and child.text is not None:
params[child.tag] = child.text
# 按照参数名ASCII码从小到大排序
sorted_params = sorted(params.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
# 计算签名
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
is_valid = sign == calculated_sign
if not is_valid:
logger.error(f"积分签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
return is_valid
except Exception as e:
logger.error(f"积分验证签名异常: {str(e)}")
return False
def wechat_response(self, return_code, return_msg):
"""返回微信支付回调响应"""
xml_response = f"""<xml>
<return_code><![CDATA[{return_code}]]></return_code>
<return_msg><![CDATA[{return_msg}]]></return_msg>
</xml>"""
logger.info(f"积分返回微信响应: {return_code} - {return_msg}")
return HttpResponse(xml_response, content_type='application/xml')
# views.py
class JifenShibai(APIView):
"""
积分支付失败处理接口
路径:/shangpin/jifenshibai
请求方式:POST
权限:JWT认证
参数:dingdanid(订单ID)
返回:删除结果
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
yonghuid=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单类型是否为积分(3)
if order.leixing != 3:
return Response({
'code': 400,
'message': '订单类型错误,不是积分订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 检查订单状态是否为未支付(9)
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付积分订单: {dingdanid}")
return Response({
'code': 200,
'message': '积分订单已删除'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '积分订单状态不是未支付,无法删除'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"积分支付失败处理异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py
class JifenLunxun(APIView):
"""
积分支付成功轮询接口
路径:/shangpin/jifenlunxun
请求方式:POST
权限:JWT认证
参数:dingdanid(订单ID)
返回:订单状态和当前积分
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
yonghuid=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单类型是否为积分(3)
if order.leixing != 3:
return Response({
'code': 400,
'message': '订单类型错误,不是积分订单'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 未支付则查微信并幂等履约
if order.zhuangtai == 9:
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
try:
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=3)
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
except CzjiluFulfillError as exc:
logger.info(f"积分确认未就绪 {dingdanid}: {exc}")
return Response({
'code': 400,
'message': str(exc),
}, status=status.HTTP_400_BAD_REQUEST)
if order.zhuangtai != 3:
logger.info(f"积分订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
return Response({
'code': 400,
'message': '积分订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 获取打手积分(通过反向查询)
try:
dashou = request.user.DashouProfile
jifen = dashou.jifen # 直接使用整数
logger.info(f"积分轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}积分为{jifen}")
return Response({
'code': 200,
'message': '支付流程全部完成',
'jifen': jifen # 返回当前积分(前端需要更新全局变量)
}, status=status.HTTP_200_OK)
except UserDashou.DoesNotExist:
logger.error(f"打手信息不存在: 用户{request.user.UserUID}")
return Response({
'code': 400,
'message': '打手信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"积分轮询异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+971
View File
@@ -0,0 +1,971 @@
"""商品/会员/分红 数据查询视图。
原 products/views.py 查询类视图聚合:商品数据获取、商品详情、打手会员列表、
管事分红记录、管理员全量数据获取、会员类型/记录查询、组长分红记录。
"""
# ==================== 标准库 ====================
import os
import json
import time
import random
import string
import uuid
import hashlib
import requests
import logging
import decimal
from datetime import timedelta
from decimal import Decimal, InvalidOperation, ROUND_CEILING
import defusedxml.ElementTree as ET
# ==================== Django 内置 ====================
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q
from gvsdsdk.fluent import db, func, FQ
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.utils import timezone
# ==================== DRF 框架 ====================
from rest_framework.views import APIView, View
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
# ==================== 项目工具函数 ====================
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import format_yuan, quantize_yuan, yuan_to_fen
from backend.utils import (
update_guanshi_daily_by_action,
update_guanshi_xufei_daily,
update_zuzhang_daily_by_action
)
from users.fadan_fenhong_utils import process_fadan_fenhong
# ==================== 各App模型 ====================
from ..models import (
ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
)
from users.models import (
AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
)
from users.business_models import User
from orders.models import CommissionRate
from config.models import Gonggao, Lunbo
from orders.models import Penalty # 用于罚款类型
from jituan.services.club_config import (
get_commission_rate,
get_commission_rate_object,
get_huiyuan_price,
get_huiyuan_fenchong,
)
from jituan.services.club_context import resolve_effective_club_id
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_gsfenhong_club_id
import traceback
# ==================== 日志实例 ====================
logger = logging.getLogger(__name__)
class ShangpinHuoquView(APIView):
"""
商品数据获取接口
一次性返回商品类型、商品专区、商品列表
"""
# 应用节流(限制频率)
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
def post(self, request):
"""
处理POST请求,返回所有商品相关数据
"""
try:
logger.info("收到商品数据获取请求")
from jituan.services.club_context import resolve_effective_club_id
from jituan.services.club_shangpin_leixing import build_club_leixing_list_for_api
club_id = resolve_effective_club_id(request, getattr(request, 'user', None))
leixing_rows = build_club_leixing_list_for_api(club_id)
shangpinleixing_data = [
{
'id': row['id'],
'jieshao': row['jieshao'],
'tupian_url': row['tupian_url'],
'paixu': row['paixu'],
}
for row in leixing_rows
]
# 2. 查询商品专区(只查询可见类型下的专区,并排序)
# 先获取可见的类型ID列表
visible_leixing_ids = [leixing['id'] for leixing in shangpinleixing_data]
if visible_leixing_ids:
zhuanqu_queryset = ShangpinZhuanqu.query.filter(
leixing_id__in=visible_leixing_ids,
shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的专区
).order_by('-paixu', 'id') # 先按paixu降序,再按id升序
else:
zhuanqu_queryset = ShangpinZhuanqu.query.none()
shangpinzhuanqu_data = []
for zhuanqu in zhuanqu_queryset:
shangpinzhuanqu_data.append({
"id": zhuanqu.id,
"mingzi": zhuanqu.mingzi,
"leixing_id": zhuanqu.leixing_id,
"paixu": zhuanqu.paixu
})
# 3. 查询商品列表(只查询可见专区下的商品,并排序)
# 先获取可见的专区ID列表
visible_zhuanqu_ids = [zhuanqu['id'] for zhuanqu in shangpinzhuanqu_data]
if visible_zhuanqu_ids:
# 注意:这里使用values()只获取指定字段,提高查询效率
shangpin_queryset = Shangpin.query.filter(
zhuanqu_id__in=visible_zhuanqu_ids,
shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的商品
).values(
'id', 'biaoqian', 'jiage', 'leixing_id',
'zhuanqu_id', 'tupian_url', 'duiwai_xiaoliang', 'paixu'
).order_by('-paixu', 'id') # 先按paixu降序,再按id升序
# 将QuerySet转换为列表
shangpinliebiao_data = list(shangpin_queryset)
else:
shangpinliebiao_data = []
# 4. 构建返回数据
response_data = {
"shangpinleixing": shangpinleixing_data,
"shangpinzhuanqu": shangpinzhuanqu_data,
"shangpinliebiao": shangpinliebiao_data
}
logger.info(f"返回数据:类型{len(shangpinleixing_data)}个,"
f"专区{len(shangpinzhuanqu_data)}个,"
f"商品{len(shangpinliebiao_data)}个")
# 5. 返回响应
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
# 记录异常
logger.error(f"获取商品数据时发生错误:{str(e)}", exc_info=True)
# 返回错误响应
error_data = {
"error": "服务器内部错误",
"detail": "获取商品数据失败,请稍后重试"
}
return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ShangpinXiangqingHuoquView(APIView):
"""
商品详情接口 - 极简版
假设所有字段都存在,直接返回
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
def post(self, request):
try:
shangpin_id = request.data.get('shangpin_id')
if not shangpin_id:
return Response({
"code": 1,
"msg": "商品ID不能为空",
"data": None
}, status=400)
# 直接查询,数据量小,性能没问题
shangpin = Shangpin.query.filter(id=shangpin_id).first()
if not shangpin:
return Response({
"code": 2,
"msg": "商品不存在",
"data": None
}, status=404)
# 直接构建数据,不处理空值
data = {
"tupian": shangpin.tupian_url,
"biaoti": shangpin.biaoqian,
"jiage": shangpin.jiage,
"xiangqing": shangpin.jieshao,
"xiadanxuzhi": shangpin.xiadan_xuzhi,
"guize": shangpin.guize_tupian,
"xiaoliang": shangpin.duiwai_xiaoliang,
"kucun": shangpin.kucun,
}
return Response({
"code": 0,
"msg": "success",
"data": data
})
except Exception as e:
logger.error(f"接口异常: {e}")
return Response({
"code": 500,
"msg": "服务器错误",
"data": None
}, status=500)
class DashouHuiyuanList(APIView):
"""
获取打手会员商品列表
路径:/shangpin/dshyhq
请求方式:POST
权限:JWT认证
返回:会员商品列表,字段对应前端需求
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 检查用户是否是打手(通过反向查询)
try:
dashou = request.user.DashouProfile
except UserDashou.DoesNotExist:
logger.warning(f"用户{request.user.UserUID}不是打手,尝试访问打手接口")
return Response({
'code': 400,
'message': '用户不是打手,无法访问此接口'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 检查打手账号状态
if dashou.zhanghaozhuangtai != 1:
logger.warning(f"打手{request.user.UserUID}账号状态异常: {dashou.zhanghaozhuangtai}")
return Response({
'code': 400,
'message': '账号状态异常,无法购买会员'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 仅返回本俱乐部可售会员(club_huiyuan_price 已配置且启用)
from jituan.services.member_recharge import club_huiyuan_sellable, can_purchase_trial
club_id = resolve_effective_club_id(request, request.user)
huiyuan_queryset = Huiyuan.query.all().only(
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
).order_by('jiage')
huiyuan_list = []
yonghuid = request.user.UserUID
for huiyuan in huiyuan_queryset:
sellable, club_price, formal_days = club_huiyuan_sellable(
club_id, huiyuan.huiyuan_id, is_trial=False,
)
if not sellable or not club_price:
continue
trial_ok, _ = can_purchase_trial(yonghuid, huiyuan.huiyuan_id, club_id)
trial_sellable, trial_price, trial_days = club_huiyuan_sellable(
club_id, huiyuan.huiyuan_id, is_trial=True,
)
huiyuan_list.append({
'id': huiyuan.huiyuan_id,
'mingzi': huiyuan.jieshao,
'jiage': format_yuan(club_price),
'formal_days': formal_days,
'jieshao': huiyuan.jtjieshao,
'trial_enabled': bool(trial_sellable),
'trial_price': format_yuan(trial_price) if trial_sellable else '0.00',
'trial_days': trial_days if trial_sellable else 0,
'can_buy_trial': bool(trial_ok and trial_sellable),
})
# 5. 返回数据
return Response({
'code': 200,
'message': '获取会员列表成功',
'data': huiyuan_list,
'club_id': club_id,
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"获取会员列表接口异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误,请稍后重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class GuanshiFenhongListAPI(APIView):
"""
管事分红记录查询接口
权限:需要JWT认证
请求方式:POST
分页参数:page, page_size
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 获取当前管事用户ID
yonghuid = request.user.UserUID
# 获取分页参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 50))
if page < 1:
page = 1
if page_size < 1 or page_size > 100: # 限制最大100条
page_size = 50
# 计算偏移量
offset = (page - 1) * page_size
# 查询条件:管事ID匹配
query = Q(guanshi=yonghuid)
# 先查询总数(使用分页查询优化)
# 查询总记录数
zong_tiaoshu = Gsfenhong.query.filter(query).count()
# 查询去重后的打手数量
chongzhi_dashou_shuliang = Gsfenhong.query.filter(query).values('dashouid').distinct().count()
# 查询当前页数据,按创建时间倒序排列
fenhong_list = Gsfenhong.query.filter(query) \
.order_by('-CreateTime') \
.values('dashouid', 'avatar', 'nicheng', 'fenhong', 'CreateTime')[offset:offset + page_size]
# 格式化数据
formatted_list = []
for item in fenhong_list:
formatted_list.append({
'yonghuid': item['dashouid'], # 打手ID
'avatar': item['avatar'] or '', # 打手头像
'nicheng': item['nicheng'] or '未知用户', # 打手昵称
'fenhong': str(item['fenhong']), # 分红金额
'CreateTime': item['CreateTime'].strftime('%Y-%m-%d %H:%M:%S') if item['CreateTime'] else ''
})
# 返回数据
return Response({
'code': 200,
'msg': '成功',
'data': {
'list': formatted_list,
'total': zong_tiaoshu, # 总记录数
'chongzhi_dashou_shuliang': chongzhi_dashou_shuliang, # 充值打手数量(去重)
'page': page,
'page_size': page_size,
'has_more': len(formatted_list) == page_size # 是否还有更多数据
}
})
except Exception as e:
logger.error(f"查询管事分红记录失败: {str(e)}", exc_info=True)
return Response({
'code': 500,
'msg': f'服务器错误: {str(e)}',
'data': {}
})
# views.py - 商品模块视图
class AdGetAllDataView(APIView):
"""
获取商品类型、商品专区、会员数据的接口
接口地址:/shangpin/adhqlx
请求方法:POST
权限要求:JWT Token认证 + 管理员权限
"""
# 需要JWT Token认证
permission_classes = [IsAuthenticated]
def post(self, request):
"""
处理POST请求,返回商品相关数据
"""
try:
# 1. 获取请求数据
zhanghao = request.data.get('zhanghao')
if not zhanghao:
logger.warning(f"管理员接口:未接收到账号参数,请求用户:{request.user.UserUID}")
return Response({
'code': 400,
'message': '缺少账号参数',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取当前登录用户
current_user = request.user
# 3. 验证用户类型是否为管理员
if current_user.UserType != 'admin':
logger.warning(
f"权限验证失败:用户 {current_user.UserUID} 的用户类型为 {current_user.UserType},非管理员")
return Response({
'code': 401,
'message': '无管理员权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 4. 验证手机号与传递的账号是否一致
if current_user.Phone != zhanghao:
logger.warning(
f"账号验证失败:用户 {current_user.UserUID} 的phone={current_user.Phone},传递的zhanghao={zhanghao}")
return Response({
'code': 401,
'message': '账号验证失败',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 5. 验证管理员扩展表是否存在
try:
admin_profile = current_user.AdminProfile
logger.info(f"管理员验证通过:用户 {current_user.UserUID},手机号 {current_user.Phone}")
except AdminProfile.DoesNotExist:
logger.error(
f"管理员扩展表不存在:用户 {current_user.UserUID} 虽然user_type=admin,但没有AdminProfile记录")
return Response({
'code': 401,
'message': '管理员信息不完整',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 6. 查询商品类型数据
try:
# 使用select_related或prefetch_related优化查询(如果需要关联查询)
# 这里直接查询所有字段,然后提取需要的字段
shangpinleixing_queryset = ShangpinLeixing.query.all()
# 转换为前端需要的格式
shangpinleixing_list = []
for leixing in shangpinleixing_queryset:
leixing_data = {
'id': leixing.id,
'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None,
'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None,
'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None,
'jieshao': leixing.jieshao if leixing.jieshao else '',
'tupian_url': leixing.tupian_url if leixing.tupian_url else ''
}
shangpinleixing_list.append(leixing_data)
#logger.info(f"成功查询到 {len(shangpinleixing_list)} 条商品类型数据")
except Exception as e:
logger.error(f"查询商品类型数据时出错:{str(e)}")
shangpinleixing_list = []
# 7. 查询商品专区数据
try:
# 查询所有商品专区
zhuanqu_queryset = ShangpinZhuanqu.query.all()
# 转换为前端需要的格式
zhuanqu_list = []
for zhuanqu in zhuanqu_queryset:
zhuanqu_data = {
'id': zhuanqu.id,
'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '',
'leixing_id': zhuanqu.leixing_id
}
zhuanqu_list.append(zhuanqu_data)
# logger.info(f"成功查询到 {len(zhuanqu_list)} 条商品专区数据")
except Exception as e:
logger.error(f"查询商品专区数据时出错:{str(e)}")
zhuanqu_list = []
# 8. 查询会员数据
try:
# 查询所有会员
huiyuan_queryset = Huiyuan.query.all()
# 转换为前端需要的格式
huiyuan_list = []
for huiyuan in huiyuan_queryset:
huiyuan_data = {
'huiyuan_id': huiyuan.huiyuan_id,
'jieshao': huiyuan.jieshao,
'jiage': str(huiyuan.jiage) # Decimal转为字符串
}
huiyuan_list.append(huiyuan_data)
# logger.info(f"成功查询到 {len(huiyuan_list)} 条会员数据")
except Exception as e:
logger.error(f"查询会员数据时出错:{str(e)}")
huiyuan_list = []
# 9. 检查会员列表是否为空
if not huiyuan_list:
logger.warning("会员列表为空,可能会影响商品发布功能")
# 10. 组织返回数据
response_data = {
'code': 0,
'message': '获取数据成功',
'data': {
'shangpinleixingList': shangpinleixing_list,
'zhuanquList': zhuanqu_list,
'huiyuanList': huiyuan_list
}
}
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
# 捕获未预期的异常
logger.error(f"接口处理异常:{str(e)}", exc_info=True)
return Response({
'code': 500,
'message': f'服务器内部错误:{str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py - shangpin模块
class HuiyuanTypeListView(APIView):
"""
获取会员类型列表接口
路径:/shangpin/adhyjllxcx
方法:POST
权限:JWT Token + 管理员验证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 🔥 1. 身份验证
user = request.user
# 检查是否是管理员
if not hasattr(user, 'AdminProfile'):
return Response({
'code': 401,
'message': '无权限访问',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 🔥 2. 获取所有会员类型
huiyuan_list = Huiyuan.query.all().order_by('CreateTime')
# 🔥 3. 构建返回数据
huiyuan_data = []
for huiyuan in huiyuan_list:
huiyuan_data.append({
'id': huiyuan.huiyuan_id, # 会员ID
'jieshao': huiyuan.jieshao, # 会员介绍
'jiage': str(huiyuan.jiage), # 价格
'goumai_cishu': huiyuan.goumai_cishu, # 购买次数
'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None
})
return Response({
'code': 0,
'message': '获取成功',
'data': huiyuan_data
}, status=status.HTTP_200_OK)
except Exception as e:
print(f"获取会员类型列表失败: {str(e)}")
return Response({
'code': 500,
'message': '系统错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class HuiyuanRecordListView(APIView):
"""
获取会员记录列表接口
路径:/shangpin/adhyjlcxhq
方法:POST
权限:JWT Token + 管理员验证
参数:
- zhanghao: 管理员账号
- huiyuan_type: 会员类型ID
- jiluzhuangtai: 记录状态(1=未过期, 2=已过期)
- page: 页码
- pagesize: 每页条数
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 🔥 1. 身份验证
user = request.user
# 检查是否是管理员
if not hasattr(user, 'AdminProfile'):
return Response({
'code': 401,
'message': '无权限访问',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 🔥 2. 获取参数
zhanghao = request.data.get('zhanghao', '').strip()
huiyuan_type = request.data.get('huiyuan_type', '').strip()
jiluzhuangtai = request.data.get('jiluzhuangtai', 1) # 默认未过期
page = int(request.data.get('page', 1))
pagesize = int(request.data.get('pagesize', 5))
# 🔥 3. 参数验证
if not zhanghao:
return Response({
'code': 400,
'message': '管理员账号不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if not huiyuan_type:
return Response({
'code': 400,
'message': '会员类型不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 🔥 4. 检查会员类型是否存在
huiyuan_obj = Huiyuan.query.filter(huiyuan_id=huiyuan_type).first()
if not huiyuan_obj:
return Response({
'code': 404,
'message': '会员类型不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 🔥 5. 构建基础查询(一次性获取所有符合条件的记录ID)
now = timezone.now()
# 🔥 使用原生SQL优化查询(避免N+1问题)
with connection.cursor() as cursor:
# 先查询符合条件的总记录数
count_sql = """
SELECT COUNT(*)
FROM huiyuangoumai hg
WHERE hg.huiyuan_id = %s
"""
params = [huiyuan_type]
if jiluzhuangtai == 1: # 未过期
count_sql += " AND hg.daoqi_time > %s"
params.append(now)
else: # 已过期
count_sql += " AND hg.daoqi_time <= %s"
params.append(now)
cursor.execute(count_sql, params)
total_count = cursor.fetchone()[0]
# 🔥 6. 分页查询(使用窗口函数提高性能)
offset = (page - 1) * pagesize
record_sql = """
SELECT
hg.id,
hg.yonghu_id,
hg.huiyuan_id,
hg.jieshao,
hg.daoqi_time,
hg.CreateTime,
um.avatar,
ud.nicheng
FROM huiyuangoumai hg
LEFT JOIN user_main um ON hg.yonghu_id = um.yonghuid
LEFT JOIN user_dashou ud ON um.id = ud.user_id
WHERE hg.huiyuan_id = %s
"""
record_params = [huiyuan_type]
if jiluzhuangtai == 1: # 未过期
record_sql += " AND hg.daoqi_time > %s"
record_params.append(now)
else: # 已过期
record_sql += " AND hg.daoqi_time <= %s"
record_params.append(now)
record_sql += " ORDER BY hg.CreateTime DESC LIMIT %s OFFSET %s"
record_params.extend([pagesize, offset])
cursor.execute(record_sql, record_params)
records = cursor.fetchall()
# 🔥 7. 处理查询结果
record_list = []
for record in records:
record_list.append({
'id': record[0],
'yonghuid': record[1], # 用户ID
'huiyuan_id': record[2],
'jieshao': record[3],
'daoqi_time': record[4].strftime('%Y-%m-%d %H:%M:%S') if record[4] else None,
'CreateTime': record[5].strftime('%Y-%m-%d %H:%M:%S') if record[5] else None,
'touxiang': record[6], # 头像相对URL
'nicheng': record[7] or record[1], # 昵称或用户ID
'yonghu_type': 2 # 🔥 默认打手类型(实际应该根据user_type判断)
})
# 🔥 8. 判断是否还有更多数据
has_more = (page * pagesize) < total_count
# 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数)
huiyuan_types = Huiyuan.query.all()
huiyuan_stats = []
for huiyuan in huiyuan_types:
# 使用COUNT查询统计
stats_sql = """
SELECT COUNT(*)
FROM huiyuangoumai
WHERE huiyuan_id = %s
"""
stats_params = [huiyuan.huiyuan_id]
if jiluzhuangtai == 1: # 未过期
stats_sql += " AND daoqi_time > %s"
stats_params.append(now)
else: # 已过期
stats_sql += " AND daoqi_time <= %s"
stats_params.append(now)
with connection.cursor() as cursor:
cursor.execute(stats_sql, stats_params)
count = cursor.fetchone()[0]
huiyuan_stats.append({
'id': huiyuan.huiyuan_id,
'jieshao': huiyuan.jieshao,
'count': count
})
return Response({
'code': 0,
'message': '获取成功',
'data': {
'list': record_list,
'total': total_count,
'current_page': page,
'page_size': pagesize,
'has_more': has_more,
'huiyuan_stats': huiyuan_stats, # 🔥 所有会员类型的统计
'current_huiyuan': {
'id': huiyuan_obj.huiyuan_id,
'jieshao': huiyuan_obj.jieshao,
'count': total_count
}
}
}, status=status.HTTP_200_OK)
except ValueError as e:
print(f"参数错误: {str(e)}")
return Response({
'code': 400,
'message': '参数错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
print(f"获取会员记录失败: {str(e)}")
traceback.print_exc()
return Response({
'code': 500,
'message': '系统错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ZuzhangFenhongView(APIView):
"""
组长分红记录接口
路径:/shangpin/zuzhangfenhong
方法:POST
权限:JWT认证,仅限组长身份
请求参数:
{
"page": 1, # 页码,默认1
"page_size": 5 # 每页条数,默认5
}
返回:
{
"code": 200,
"msg": "success",
"data": {
"fenhong_zonge": "1234.56", # 分红总额
"yaoqing_guanshi": 10, # 邀请管事数
"fenhong_cishu": 50, # 分红次数
"total": 50, # 总记录数
"has_more": false, # 是否有下一页
"list": [...] # 记录列表
}
}
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
current_user = request.user
# 1. 验证组长身份
try:
zuzhang = UserZuzhang.query.get(user=current_user)
except UserZuzhang.DoesNotExist:
return Response({
'code': 403,
'msg': '当前用户不是组长身份',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 2. 请求参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 5))
page = max(page, 1)
page_size = max(page_size, 1)
# 3. 统计数据
# 3.1 分红总额(从组长扩展表)
fenhong_zonge = zuzhang.fenyong_zonge
# 3.2 邀请管事数(统计邀请人等于当前用户ID的管事记录)
yaoqing_guanshi = UserGuanshi.query.filter(yaoqingren=current_user.UserUID).count()
# 3.3 分红次数(总记录数)
base_queryset = Gsfenhong.query.filter(zuzhang_id=current_user.UserUID).order_by('-CreateTime')
fenhong_cishu = base_queryset.count()
# 4. 分页查询
start = (page - 1) * page_size
end = start + page_size
records = base_queryset[start:end]
# 5. 批量获取管事用户信息(只查 boss_profile 获取昵称)
guanshi_ids = list(set(rec.guanshi for rec in records if rec.guanshi))
guanshi_info_map = self._get_guanshi_info(guanshi_ids)
# 6. 组装列表数据
list_data = []
for rec in records:
# 打手信息(直接从记录中取快照)
dashou_avatar = rec.avatar or ''
dashou_nicheng = rec.nicheng or ''
dashou_yonghuid = rec.dashouid or ''
# 管事信息
guanshi_id = rec.guanshi
guanshi_info = guanshi_info_map.get(guanshi_id, {'avatar': '', 'nicheng': ''})
guanshi_avatar = guanshi_info['avatar']
guanshi_nicheng = guanshi_info['nicheng']
# 组长分红金额
fenhong = rec.zuzhang_fenhong
if fenhong is None:
fenhong = decimal.Decimal('0.00')
fenhong_str = str(fenhong.quantize(decimal.Decimal('0.00')))
# 时间格式化
CreateTime = rec.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if rec.CreateTime else ''
list_data.append({
'dashou_avatar': dashou_avatar,
'dashou_nicheng': dashou_nicheng,
'dashou_yonghuid': dashou_yonghuid,
'guanshi_avatar': guanshi_avatar,
'guanshi_nicheng': guanshi_nicheng,
'guanshi_yonghuid': guanshi_id,
'fenhong': fenhong_str,
'CreateTime': CreateTime,
})
# 7. 是否有更多数据
has_more = (end < fenhong_cishu)
return Response({
'code': 200,
'msg': 'success',
'data': {
'fenhong_zonge': str(fenhong_zonge.quantize(decimal.Decimal('0.00'))),
'yaoqing_guanshi': yaoqing_guanshi,
'fenhong_cishu': fenhong_cishu,
'total': fenhong_cishu,
'has_more': has_more,
'list': list_data,
}
})
def _get_guanshi_info(self, guanshi_ids):
"""
批量获取管事用户的头像和昵称
昵称从老板扩展表(boss_profile)获取,头像从主表获取
返回字典:{用户ID: {'avatar': 相对路径, 'nicheng': 昵称}}
"""
if not guanshi_ids:
return {}
# 查询用户主表,并预加载 boss_profile
users = User.query.filter(UserUID__in=guanshi_ids).select_related(
'BossProfile'
).only(
'UserUID', 'Avatar',
'BossProfile__nickname'
)
info = {}
for user in users:
avatar = user.Avatar or ''
# 从 boss_profile 获取昵称
if hasattr(user, 'BossProfile') and user.BossProfile:
nicheng = user.BossProfile.nickname or ''
else:
# 如果没有 boss_profile,使用默认昵称
nicheng = f"用户{user.UserUID[-4:]}"
info[user.UserUID] = {'avatar': avatar, 'nicheng': nicheng}
# 对于未查询到的用户,提供默认值
for gid in guanshi_ids:
if gid not in info:
info[gid] = {'avatar': '', 'nicheng': f"用户{gid[-4:]}"}
return info
# 常量定义(可根据需要调整)
File diff suppressed because it is too large. Load diff
+613
View File
@@ -0,0 +1,613 @@
"""押金支付流程视图。
包含押金购买的参数生成、支付回调、失败处理、轮询。
"""
# ==================== 标准库 ====================
import os
import json
import time
import random
import string
import uuid
import hashlib
import requests
import logging
import decimal
from datetime import timedelta
from decimal import Decimal, InvalidOperation, ROUND_CEILING
import defusedxml.ElementTree as ET
# ==================== Django 内置 ====================
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q
from gvsdsdk.fluent import db, func, FQ
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.utils import timezone
# ==================== DRF 框架 ====================
from rest_framework.views import APIView, View
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
# ==================== 项目工具函数 ====================
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import format_yuan, quantize_yuan, yuan_to_fen
from backend.utils import (
update_guanshi_daily_by_action,
update_guanshi_xufei_daily,
update_zuzhang_daily_by_action
)
from users.fadan_fenhong_utils import process_fadan_fenhong
# ==================== 各App模型 ====================
from ..models import (
ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan,
Czjilu, Gsfenhong, Huiyuangoumai, DuociFenhong
)
from users.models import (
AdminProfile, UserDashou, UserGuanshi, UserZuzhang, UserShangjia
)
from users.business_models import User
from orders.models import CommissionRate
from config.models import Gonggao, Lunbo
from orders.models import Penalty # 用于罚款类型
from jituan.services.club_config import (
get_commission_rate,
get_commission_rate_object,
get_huiyuan_price,
get_huiyuan_fenchong,
)
from jituan.services.club_context import resolve_effective_club_id
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_gsfenhong_club_id
import traceback
# ==================== 日志实例 ====================
logger = logging.getLogger(__name__)
class YajinGoumai(APIView):
"""
押金充值支付参数接口
路径:/shangpin/yajingoumai
请求方式:POST
权限:JWT认证
参数:jine(金额,1-10000元)
返回:支付参数(payParams)和订单ID(dingdanid)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 验证用户身份和状态
try:
dashou = request.user.DashouProfile
except UserDashou.DoesNotExist:
return Response({
'code': 400,
'message': '用户不是打手,无法充值押金'
}, status=status.HTTP_400_BAD_REQUEST)
if dashou.zhanghaozhuangtai != 1:
return Response({
'code': 400,
'message': '账号状态异常,无法进行充值'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取并验证金额参数
jine = request.data.get('jine')
if not jine:
return Response({
'code': 400,
'message': '请填写充值金额'
}, status=status.HTTP_400_BAD_REQUEST)
try:
jine = int(jine)
except (ValueError, TypeError):
return Response({
'code': 400,
'message': '金额格式错误,请填写1-10000的整数'
}, status=status.HTTP_400_BAD_REQUEST)
# 验证金额范围
if jine < 1 or jine > 10000:
return Response({
'code': 400,
'message': '充值金额必须在1-10000元之间'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 生成订单ID(使用你提供的算法,前缀改为CzYJ)
timestamp_ms = int(time.time() * 1000)
timestamp_ns = int(time.time_ns() // 1000) % 1000000
random_num = random.randint(0, 99)
dingdanid = f"CzYJ{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 4. 创建充值记录订单
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付状态
yonghuid=request.user.UserUID,
jine=jine,
leixing=2, # 充值类型:2=押金
shuoming=settings.YAJIN_PAY_DESCRIPTION,
club_id=resolve_club_id_for_write(request, request.user),
)
logger.info(f"创建押金充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}元")
# 5. 生成微信支付参数
try:
pay_openid, _ = get_payment_openid(request)
if not pay_openid:
chongzhi_jilu.delete()
return Response({'code': 400, 'message': '用户openid不存在'}, status=status.HTTP_400_BAD_REQUEST)
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=jine,
openid=pay_openid,
pay_type='yajin'
)
except Exception as e:
# 支付参数生成失败,删除订单记录
chongzhi_jilu.delete()
logger.error(f"生成微信支付参数失败: {str(e)}")
return Response({
'code': 500,
'message': f'支付参数生成失败: {str(e)}'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 6. 返回支付参数和订单ID
return Response({
'code': 200,
'message': '支付参数生成成功',
'payParams': pay_params, # 支付参数(前端接收为payParams)
'dingdanid': dingdanid # 订单ID(前端接收为dingdanid)
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"押金充值接口异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误,请稍后重试'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信小程序支付参数"""
# 获取配置
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.wechat_pay import get_wechat_v2_config
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, self.request.user))
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
PAY_DESCRIPTION = settings.YAJIN_PAY_DESCRIPTION
NOTIFY_URL = settings.YAJIN_PAY_NOTIFY_URL
elif pay_type == 'jifen':
PAY_DESCRIPTION = settings.JIFEN_PAY_DESCRIPTION
NOTIFY_URL = settings.JIFEN_PAY_NOTIFY_URL
elif pay_type == 'huiyuan':
PAY_DESCRIPTION = settings.HUIYUAN_PAY_DESCRIPTION
NOTIFY_URL = settings.HUIYUAN_PAY_NOTIFY_URL
else:
raise Exception('未知的支付类型')
# 生成随机字符串
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 金额转换(元转分)
total_fee = yuan_to_fen(jine)
# 交易类型(小程序支付使用JSAPI)
trade_type = 'JSAPI'
# 获取客户端IP
user_ip = self.get_client_ip()
# 1. 第一次签名(统一下单接口)
sign_data = {
'appid': APPID,
'mch_id': MCHID,
'nonce_str': nonce_str,
'body': PAY_DESCRIPTION,
'out_trade_no': dingdanid,
'total_fee': total_fee,
'spbill_create_ip': user_ip,
'notify_url': NOTIFY_URL,
'trade_type': trade_type,
'openid': openid # 小程序支付必须传openid
}
# 按照参数名ASCII码从小到大排序
sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value])
sign_string += f'&key={KEY}'
# MD5签名
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
# 构造XML数据
xml_data = f"""<xml>
<appid>{APPID}</appid>
<body>{PAY_DESCRIPTION}</body>
<mch_id>{MCHID}</mch_id>
<nonce_str>{nonce_str}</nonce_str>
<notify_url>{NOTIFY_URL}</notify_url>
<openid>{openid}</openid>
<out_trade_no>{dingdanid}</out_trade_no>
<spbill_create_ip>{user_ip}</spbill_create_ip>
<total_fee>{total_fee}</total_fee>
<trade_type>{trade_type}</trade_type>
<sign>{sign}</sign>
</xml>"""
# 调用微信统一下单接口(官方地址)
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
timeout=10 # 设置超时
)
# 解析返回的XML
root = ET.fromstring(response.content)
return_code = root.find('return_code').text
result_code = root.find('result_code').text
if return_code == 'SUCCESS' and result_code == 'SUCCESS':
# 获取prepay_id
prepay_id = root.find('prepay_id').text
# 2. 第二次签名(小程序支付参数)
timestamp = str(int(time.time()))
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
package = f'prepay_id={prepay_id}'
sign_type = 'MD5'
# 生成支付签名(注意:这里key的大小写和字段名与第一次不同)
pay_sign_data = {
'appId': APPID, # 注意:这里是appId,不是appid
'timeStamp': timestamp, # 注意:这里是timeStamp,不是timestamp
'nonceStr': nonce_str,
'package': package,
'signType': sign_type
}
sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0])
pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data])
pay_sign_string += f'&key={KEY}'
pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
# 返回前端需要的支付参数
return {
'appId': APPID, # 对应前端:appId
'timeStamp': timestamp, # 对应前端:timeStamp
'nonceStr': nonce_str, # 对应前端:nonceStr
'package': package, # 对应前端:package
'signType': sign_type, # 对应前端:signType
'paySign': pay_sign # 对应前端:paySign
}
else:
# 获取错误信息
err_code = root.find('err_code')
err_code_des = root.find('err_code_des')
return_msg = root.find('return_msg')
error_details = f"return_code: {return_code}"
if err_code is not None:
error_details += f", err_code: {err_code.text}"
if err_code_des is not None:
error_details += f", err_code_des: {err_code_des.text}"
if return_msg is not None:
error_details += f", return_msg: {return_msg.text}"
raise Exception(f"微信支付下单失败: {error_details}")
def get_client_ip(self):
"""获取客户端IP(从DRF的request对象中获取)"""
request = self.request
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip if ip else '127.0.0.1'
class YajinHuitiao(View):
"""
押金支付回调接口
路径:/shangpin/yajinhuitiao
注意:这个接口不需要JWT认证,由微信服务器调用
"""
def post(self, request):
try:
# 1. 解析微信回调XML数据
request_body = request.body
logger.info(f"收到微信支付回调,原始数据: {request_body.decode('utf-8')}")
root = ET.fromstring(request_body)
# 获取关键字段
return_code = root.find('return_code').text
result_code = root.find('result_code').text
out_trade_no = root.find('out_trade_no').text # 订单ID
transaction_id = root.find('transaction_id').text # 微信交易号
total_fee = int(root.find('total_fee').text) # 支付金额(分)
logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}分")
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
notify_club = club_id_from_czjilu(out_trade_no)
# 2. 验证基本返回状态
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}")
return self.wechat_response('FAIL', '支付失败')
# 3. 验证签名(确保是微信发来的)
if not verify_wechat_v2_signature(request_body, notify_club):
logger.error(f"签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
# 4. 查询订单
try:
order = Czjilu.query.get(dingdan_id=out_trade_no)
logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}")
except Czjilu.DoesNotExist:
logger.error(f"订单不存在: {out_trade_no}")
return self.wechat_response('FAIL', '订单不存在')
# 5. 检查订单状态是否为未支付(9)
if order.zhuangtai != 9:
logger.warning(f"订单状态不是未支付: {order.dingdan_id}, 当前状态: {order.zhuangtai}")
try:
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
repair_wechat_income_if_missing(
Decimal(str(order.jine)),
getattr(order, 'club_id', None),
biz_ref=f'cz:{out_trade_no}',
)
except Exception as exc:
logger.error('押金回调补记收支失败 %s: %s', out_trade_no, exc, exc_info=True)
return self.wechat_response('SUCCESS', 'OK')
# 6. 验证金额(微信返回的是分,需要转换)
wechat_amount_yuan = total_fee / 100 # 转换为元
order_amount_yuan = float(order.jine) # 订单金额(元)
# 允许1分钱的误差(浮点数比较)
if abs(wechat_amount_yuan - order_amount_yuan) > 0.01:
logger.error(f"金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
return self.wechat_response('FAIL', '金额不一致')
from jituan.services.czjilu_fulfill import fulfill_yajin_recharge, CzjiluFulfillError
try:
fulfill_yajin_recharge(out_trade_no, paid_amount_yuan=wechat_amount_yuan)
except CzjiluFulfillError as exc:
logger.error('押金回调履约失败 %s: %s', out_trade_no, exc)
return self.wechat_response('FAIL', str(exc))
logger.info(f"押金支付回调处理完成: {out_trade_no}")
return self.wechat_response('SUCCESS', 'OK')
except ET.ParseError as e:
logger.error(f"XML解析失败: {str(e)}")
return self.wechat_response('FAIL', '数据格式错误')
except Exception as e:
logger.error(f"押金支付回调处理异常: {str(e)}", exc_info=True)
# 返回FAIL让微信重试
return self.wechat_response('FAIL', '处理异常')
def verify_signature(self, xml_data):
"""验证微信支付签名"""
try:
root = ET.fromstring(xml_data)
sign = root.find('sign').text
if not sign:
logger.error("签名字段不存在")
return False
# 获取所有参数并排除sign
params = {}
for child in root:
if child.tag != 'sign' and child.text is not None:
params[child.tag] = child.text
# 按照参数名ASCII码从小到大排序
sorted_params = sorted(params.items(), key=lambda x: x[0])
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}'
# 计算签名
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
is_valid = sign == calculated_sign
if not is_valid:
logger.error(f"签名验证失败: 计算签名{calculated_sign}, 收到签名{sign}")
return is_valid
except Exception as e:
logger.error(f"验证签名异常: {str(e)}")
return False
def wechat_response(self, return_code, return_msg):
"""返回微信支付回调响应"""
xml_response = f"""<xml>
<return_code><![CDATA[{return_code}]]></return_code>
<return_msg><![CDATA[{return_msg}]]></return_msg>
</xml>"""
logger.info(f"返回微信响应: {return_code} - {return_msg}")
return HttpResponse(xml_response, content_type='application/xml')
# views.py
class YajinShibai(APIView):
"""
押金支付失败处理接口
路径:/shangpin/yajinshibai
请求方式:POST
权限:JWT认证
参数:dingdanid(订单ID)
返回:删除结果
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
yonghuid=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 检查订单状态是否为未支付(9)
if order.zhuangtai == 9:
# 删除订单
order.delete()
logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}")
return Response({
'code': 200,
'message': '订单已删除'
}, status=status.HTTP_200_OK)
else:
return Response({
'code': 400,
'message': '订单状态不是未支付,无法删除'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"押金支付失败处理异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py
class YajinLunxun(APIView):
"""
押金支付成功轮询接口
路径:/shangpin/yajinlunxun
请求方式:POST
权限:JWT认证
参数:dingdanid(订单ID)
返回:订单状态和当前押金
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取订单ID
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({
'code': 400,
'message': '订单ID不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 查询订单(确保是当前用户的订单)
try:
order = Czjilu.query.get(
dingdan_id=dingdanid,
yonghuid=request.user.UserUID
)
except Czjilu.DoesNotExist:
return Response({
'code': 400,
'message': '订单不存在或不属于当前用户'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 未支付则查微信并幂等履约
if order.zhuangtai == 9:
from jituan.services.czjilu_fulfill import confirm_czjilu_paid, CzjiluFulfillError
try:
confirm_czjilu_paid(dingdanid, request.user.UserUID, expected_leixing=2)
order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
except CzjiluFulfillError as exc:
logger.info(f"押金确认未就绪 {dingdanid}: {exc}")
return Response({
'code': 400,
'message': str(exc),
}, status=status.HTTP_400_BAD_REQUEST)
if order.zhuangtai != 3:
logger.info(f"订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
return Response({
'code': 400,
'message': '订单尚未支付完成'
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 获取打手押金(通过反向查询,最快捷的方式)
try:
dashou = request.user.DashouProfile
yajin = float(dashou.yajin) # 转为浮点数方便前端处理
logger.info(f"轮询成功: 订单{dingdanid}已支付,用户{request.user.UserUID}押金为{yajin}元")
return Response({
'code': 200,
'message': '支付流程全部完成',
'yajin': yajin # 返回当前押金(前端需要更新全局变量)
}, status=status.HTTP_200_OK)
except UserDashou.DoesNotExist:
logger.error(f"打手信息不存在: 用户{request.user.UserUID}")
return Response({
'code': 400,
'message': '打手信息不存在'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"押金轮询异常: {str(e)}", exc_info=True)
return Response({
'code': 500,
'message': '服务器内部错误'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py
-957
View File
@@ -1,957 +0,0 @@
import json
import logging
from decimal import Decimal
from django.db import models, transaction
from django.db.models import F
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from .models import (
YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu,
KaoheguanBankuai, Bankuai, ShenheJilu, KaoheJiluFeiyong
)
from users.models import UserShenheguan, UserDashou
from users.business_models import User
from products.models import ShangpinLeixing
logger = logging.getLogger(__name__)
class DashouBiaoqianView(APIView):
"""获取当前打手所有称号标签"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
# 查询用户的所有称号关联,一次连表取出
relations = YonghuChenghao.objects.filter(
yonghu=yonghu
).select_related('chenghao')
chenghao_list = []
for rel in relations:
ch = rel.chenghao
# 解析特效描述 JSON(前端需要对象)
texiao_json = {}
if ch.texiao_miaoshu:
try:
texiao_json = json.loads(ch.texiao_miaoshu)
except json.JSONDecodeError:
texiao_json = {}
chenghao_list.append({
'mingcheng': ch.mingcheng,
'tubiao_url': ch.tubiao_url or '',
'texiao_json': texiao_json,
})
return Response({
'code': 0,
'msg': '成功',
'data': {'chenghao_list': chenghao_list}
})
except Exception as e:
logger.error(f"获取打手标签失败: {e}", exc_info=True)
return Response({'code': 1, 'msg': '服务器错误', 'data': None})
class BankuaiBiaoqianView(APIView):
"""获取商品类型所属板块的所有标签"""
permission_classes = [IsAuthenticated]
def post(self, request):
leixing_id = request.data.get('leixing_id') or request.data.get('ProductTypeID')
if not leixing_id:
return Response({'code': 1, 'msg': '缺少leixing_id'})
try:
leixing = ShangpinLeixing.objects.select_related('bankuai').get(id=leixing_id)
except ShangpinLeixing.DoesNotExist:
return Response({'code': 2, 'msg': '商品类型不存在'})
bankuai = leixing.bankuai
if not bankuai:
return Response({'code': 0, 'data': {'biaoqian_list': []}})
# 获取该板块下所有类型的标签(leixing可以为dashou/shangjia等,这里先返回所有)
chenghao_list = Chenghao.objects.filter(bankuai=bankuai, leixing='dashou')
data = []
from jituan.services.club_context import resolve_effective_club_id
from jituan.services.club_kaohe_chenghao import filter_chenghao_ids_for_pool
club_id = resolve_effective_club_id(request, request.user)
allowed_ids = set(
filter_chenghao_ids_for_pool(club_id, list(chenghao_list.values_list('id', flat=True)))
)
for ch in chenghao_list:
if ch.id not in allowed_ids:
continue
texiao_json = {}
if ch.texiao_miaoshu:
try:
texiao_json = json.loads(ch.texiao_miaoshu)
except:
pass
data.append({
'id': ch.id,
'mingcheng': ch.mingcheng,
'texiao_json': texiao_json
})
return Response({'code': 0, 'data': {'biaoqian_list': data}})
# 硬编码邀请码
KAOHEGUAN_INVITE_CODE = 'KHG2026VIP'
class KaoheguanRegisterView(APIView):
"""
考核官注册接口
POST /dengji/khgzc
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
invite_code = request.data.get('inviteCode', '').strip()
if not invite_code:
return Response({'code': 400, 'msg': '邀请码不能为空'})
if invite_code != KAOHEGUAN_INVITE_CODE:
return Response({'code': 400, 'msg': '邀请码错误'})
kg = UserShenheguan.objects.filter(user=yonghu).first()
if kg:
if kg.zhuangtai == 1:
return Response({'code': 200, 'msg': '您已是考核官', 'data': self._build_data(kg)})
else:
return Response({'code': 400, 'msg': '账号已被封禁'})
with transaction.atomic():
kg = UserShenheguan.objects.create(
user=yonghu,
zhuangtai=1,
shenhe_zhuangtai=0,
shenhe_zongshu=0,
tongguo_zongshu=0,
yue=0.00,
zonge=0.00
)
return Response({'code': 200, 'msg': '注册成功', 'data': self._build_data(kg)})
except Exception as e:
logger.error(f"考核官注册失败: {e}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})
def _build_data(self, kg):
bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.UserUID).select_related('bankuai')
bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs]
return {
'zhuangtai': kg.zhuangtai,
'shenhe_zongshu': kg.shenhe_zongshu,
'tongguo_zongshu': kg.tongguo_zongshu,
'yue': str(kg.yue),
'zonge': str(kg.zonge),
'shenhe_zhuangtai': kg.shenhe_zhuangtai,
'bankuai_list': bankuai_list,
'kaoheguanstatus': 1
}
class KaoheguanRefreshView(APIView):
"""
考核官数据刷新接口
POST /dengji/khgsx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
kg = UserShenheguan.objects.filter(user=yonghu).first()
if not kg:
return Response({'code': 400, 'msg': '您还未注册考核官'})
if kg.zhuangtai != 1:
return Response({'code': 200, 'data': {'zhuangtai': kg.zhuangtai, 'kaoheguanstatus': 0}})
# 查询所属板块
bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.UserUID).select_related('bankuai')
bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs]
# 统计待审核 / 审核中的记录数量
pending_count = ShenheJilu.objects.filter(
shenheguan_id=yonghu.UserUID,
zhuangtai=0 # 审核中
).count()
data = {
'zhuangtai': kg.zhuangtai,
'shenhe_zongshu': kg.shenhe_zongshu,
'tongguo_zongshu': kg.tongguo_zongshu,
'yue': str(kg.yue),
'zonge': str(kg.zonge),
'shenhe_zhuangtai': kg.shenhe_zhuangtai,
'bankuai_list': bankuai_list,
'kaoheguanstatus': 1,
'is_renzheng': kg.is_renzheng, # 🆕 是否认证
'pending_shenhe_count': pending_count # 🆕 审核中数量
}
return Response({'code': 200, 'data': data})
except Exception as e:
logger.error(f"考核官刷新失败: {e}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})
class ShenheDatingModuleView(APIView):
"""审核大厅获取模块列表(板块 + 待审核数量 + 是否关联)"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
related_ids = set(KaoheguanBankuai.objects.filter(kaoheguan_id=yonghu.UserUID)
.values_list('bankuai_id', flat=True))
bankuai_list = []
for bk in Bankuai.objects.all():
count = ShenheJilu.objects.filter(bankuai=bk, zhuangtai=3, shenheguan_id__isnull=True).count()
bankuai_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'count': count,
'is_related': bk.bankuai_id in related_ids
})
return Response({'code': 0, 'data': {'modules': bankuai_list}})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
class ShenheDatingDataView(APIView):
"""获取审核大厅待审核记录列表"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
data = request.data
bankuai_id = data.get('bankuai_id')
page = int(data.get('page', 1))
page_size = int(data.get('page_size', 5))
query = ShenheJilu.objects.filter(zhuangtai=3, shenheguan_id__isnull=True)
if bankuai_id:
query = query.filter(bankuai_id=bankuai_id)
total = query.count()
offset = (page - 1) * page_size
jilu_list = query.select_related('bankuai', 'chenghao').order_by('-CreateTime')[
offset:offset + page_size]
records = []
for jl in jilu_list:
user = User.objects.filter(UserUID=jl.shenqingren_id).first()
avatar = user.Avatar if user else ''
texiao_json = {}
if jl.chenghao and jl.chenghao.texiao_miaoshu:
try:
texiao_json = json.loads(jl.chenghao.texiao_miaoshu)
except:
pass
# 新增字段:本次考核费用
kaohe_feiyong = float(jl.jiaofei_jine) if jl.jiaofei_jine is not None else 0.0
records.append({
'jilu_id': jl.jilu_id,
'shenqingren_id': jl.shenqingren_id,
'shenqingren_avatar': avatar,
'dashou_youxi_id': jl.dashou_youxi_id or '',
'dashou_beizhu': jl.dashou_beizhu or '',
'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None,
'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '',
'chenghao_id': jl.chenghao_id,
'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '',
'chenghao_texiao_json': texiao_json,
'guize_neirong': jl.guize_neirong or '',
'kaohe_feiyong': kaohe_feiyong, # 新增
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
})
has_more = (page * page_size) < total
return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
class ShenheQiangdanView(APIView):
"""
审核官抢单(接审核任务)
校验:身份、账号封禁、已有进行中审核、记录状态、不能审核自己、板块权限
成功后更新考核官扩展表:审核总数+1,审核状态设为“审核中”
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
jilu_id = request.data.get('jilu_id')
if not jilu_id:
return Response({'code': 400, 'msg': '记录ID不能为空'})
# 1. 检查考核官身份及账号是否被禁用
kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first()
if not kg:
return Response({'code': 403, 'msg': '您不是考核官或账号已被禁用'})
# 2. 检查是否有正在审核中的记录(zhuangtai=0),防止同时审核多个
ongoing = ShenheJilu.objects.filter(
shenheguan_id=yonghu.UserUID, zhuangtai=0
).exists()
if ongoing:
return Response({'code': 400, 'msg': '您有正在审核中的任务,请完成后再接新任务'})
with transaction.atomic():
# 3. 行锁抢单:仅当状态为待审核(3)且无审核官时才能抢
jilu = ShenheJilu.objects.select_for_update().filter(
jilu_id=jilu_id,
zhuangtai=3,
shenheguan_id__isnull=True
).first()
if not jilu:
return Response({'code': 400, 'msg': '该审核任务已被接走、状态变更或不存在'})
# 4. 不能审核自己
if jilu.shenqingren_id == yonghu.UserUID:
return Response({'code': 400, 'msg': '不能审核自己提交的申请'})
# 5. 板块权限:考核官必须属于该板块
is_related = KaoheguanBankuai.objects.filter(
kaoheguan_id=yonghu.UserUID,
bankuai=jilu.bankuai
).exists()
if not is_related:
return Response({'code': 400, 'msg': '您不属于该板块,无法审核'})
# 新增:6. 费用>0时,要求考核官已认证
if jilu.jiaofei_jine and float(jilu.jiaofei_jine) > 0:
if not kg.is_renzheng:
return Response({'code': 400, 'msg': '本考核需要认证考核官接单,您尚未认证'})
# 7. 更新审核记录:分配给当前考核官,状态变为审核中
jilu.zhuangtai = 0 # 审核中
jilu.shenheguan_id = yonghu.UserUID
jilu.save()
# 8. 更新考核官扩展表:审核总数 +1,审核状态置为审核中(1)
UserShenheguan.objects.filter(user=yonghu).update(
shenhe_zongshu=F('shenhe_zongshu') + 1,
shenhe_zhuangtai=1
)
return Response({
'code': 0,
'msg': '抢单成功,请前往游戏内审核,审核完成后请到考核打分页面进行评定。',
'data': {'bankuai_id': jilu.bankuai.bankuai_id}
})
except Exception as e:
logger.error(f"审核官抢单异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
class ShenheXiangqingView(APIView):
"""获取当前考核官审核中的记录详情(新增待收考核费字段)"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first()
if not kg:
return Response({'code': 403, 'msg': '您不是考核官或账号异常'})
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
# 查询当前考核官名下状态为“审核中”的记录
base_query = ShenheJilu.objects.filter(
shenheguan_id=yonghu.UserUID,
zhuangtai=0
).select_related('bankuai', 'chenghao')
total = base_query.count()
offset = (page - 1) * page_size
jilu_list = base_query[offset:offset + page_size]
records = []
for jl in jilu_list:
user = User.objects.filter(UserUID=jl.shenqingren_id).first()
avatar = user.Avatar if user else ''
texiao_json = {}
if jl.chenghao and jl.chenghao.texiao_miaoshu:
try:
texiao_json = json.loads(jl.chenghao.texiao_miaoshu)
except:
pass
# 是否开启金牌
is_jinpai = jl.chenghao.kaioi_jinpai if jl.chenghao else False
# 本次待收考核费(基于记录中的当前次数,从费用明细表查询)
daishou_fei = 0.00
fee_detail = KaoheJiluFeiyong.objects.filter(
jilu=jl,
cishu=jl.cishu
).first()
if fee_detail:
daishou_fei = float(fee_detail.jine)
else:
# 兜底:若明细表无记录,尝试用通用费用表计算
daishou_fei = 0.00
# 上次失败原因
last_fail_reason = ''
last_fail = KaoheJujueJilu.objects.filter(
jilu=jl,
cishu=jl.cishu - 1
).order_by('-CreateTime').first()
if last_fail:
last_fail_reason = last_fail.yuanyin
records.append({
'jilu_id': jl.jilu_id,
'shenqingren_id': jl.shenqingren_id,
'shenqingren_avatar': avatar,
'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '',
'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '',
'chenghao_texiao_json': texiao_json,
'guize_neirong': jl.guize_neirong or '',
'dashou_beizhu': jl.dashou_beizhu or '',
'dashou_youxi_id': jl.dashou_youxi_id or '',
'shenheguan_youxi_id': jl.shenheguan_youxi_id or '',
'cishu': jl.cishu,
'daishou_fei': daishou_fei, # 新增:待收考核费
'last_fail_reason': last_fail_reason,
'is_jinpai': is_jinpai,
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
})
has_more = (page * page_size) < total
return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
class ShenheCaozuoView(APIView):
"""
考核官执行操作(同意/拒绝/转移/修改昵称),并更新考核官收益
- 只有“同意”或“拒绝”才增加余额和总额,金额严格从 KaoheJiluFeiyong 表获取
- 查不到费用则加 0 元,绝不影响流程
- 转移、修改昵称等操作不涉及收益
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
# 1. 验证考核官身份及账号状态
kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first()
if not kg:
return Response({'code': 403, 'msg': '账号异常,无法操作'})
jilu_id = request.data.get('jilu_id')
caozuo = request.data.get('caozuo') # agree / reject / transfer / modify_nick
if not jilu_id or not caozuo:
return Response({'code': 400, 'msg': '缺少必要参数'})
with transaction.atomic():
# 2. 锁定审核记录,确保属于当前考核官且状态为审核中(0)
jilu = ShenheJilu.objects.select_for_update().filter(
jilu_id=jilu_id,
shenheguan_id=yonghu.UserUID,
zhuangtai=0
).first()
if not jilu:
return Response({'code': 400, 'msg': '该记录不存在或不属于您'})
# 3. 本次操作产生的收入(仅通过/拒绝时有意义)
daishou_fei = 0.00
# 4. 根据操作类型执行对应业务逻辑
if caozuo == 'agree':
# --- 通过考核 ---
# 查询本次考核应得的费用(基于记录ID和当前次数)
fee_detail = KaoheJiluFeiyong.objects.filter(
jilu=jilu,
cishu=jilu.cishu
).first()
if fee_detail:
daishou_fei = float(fee_detail.jine)
# 找不到就保持 0,绝不使用任何默认值
jilu.zhuangtai = 1
jilu.save()
# 授予标签
if jilu.chenghao:
yh = User.objects.get(UserUID=jilu.shenqingren_id)
if not YonghuChenghao.objects.filter(yonghu=yh, chenghao=jilu.chenghao).exists():
YonghuChenghao.objects.create(yonghu=yh, chenghao=jilu.chenghao)
# 如果标签开启了金牌,同步更新打手称号
if jilu.chenghao.kaioi_jinpai:
dashou_prof = UserDashou.objects.filter(user=yh).first()
if dashou_prof:
dashou_prof.chenghao = '金牌选手'
dashou_prof.save()
elif caozuo == 'reject':
# --- 拒绝考核 ---
yuanyin = request.data.get('yuanyin', '')
if not yuanyin.strip():
return Response({'code': 400, 'msg': '请填写拒绝原因'})
# 查询本次考核应得的费用(拒绝时同样获得收入)
fee_detail = KaoheJiluFeiyong.objects.filter(
jilu=jilu,
cishu=jilu.cishu
).first()
if fee_detail:
daishou_fei = float(fee_detail.jine)
# 找不到就保持 0
jilu.zhuangtai = 2
jilu.save()
# 记录拒绝原因
KaoheJujueJilu.objects.create(
jilu=jilu,
cishu=jilu.cishu,
shenheguan_id=yonghu.UserUID,
yuanyin=yuanyin
)
# 新增:不合格时,如果考核官未认证,则清空考核官ID和游戏昵称
if not kg.is_renzheng:
jilu.shenheguan_id = None
jilu.shenheguan_youxi_id = ''
jilu.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id'])
elif caozuo == 'transfer':
# 转移考核官:清空考核官信息,状态回退为待审核
jilu.shenheguan_id = None
jilu.shenheguan_youxi_id = None
jilu.zhuangtai = 3
jilu.save()
# 转移不更新收益,直接返回
elif caozuo == 'modify_nick':
# 修改考核官游戏昵称
youxi_id = request.data.get('youxi_id', '').strip()
if not youxi_id:
return Response({'code': 400, 'msg': '请输入游戏昵称'})
jilu.shenheguan_youxi_id = youxi_id
jilu.save()
return Response({'code': 0, 'msg': '修改成功'})
else:
return Response({'code': 400, 'msg': '无效的操作类型'})
# 5. 更新考核官收益(仅“通过”或“拒绝”时执行)
if caozuo in ('transfer'):
update_fields = {
'shenhe_zhuangtai': 0, # 设为空闲
}
UserShenheguan.objects.filter(user=yonghu).update(**update_fields)
if caozuo in ('agree', 'reject'):
update_fields = {
'yue': F('yue') + Decimal(str(daishou_fei)),
'zonge': F('zonge') + Decimal(str(daishou_fei)),
'shenhe_zhuangtai': 0, # 设为空闲
}
# 只有同意时才增加“通过总数”
if caozuo == 'agree':
update_fields['tongguo_zongshu'] = F('tongguo_zongshu') + 1
# 审核总数不再增加
UserShenheguan.objects.filter(user=yonghu).update(**update_fields)
return Response({'code': 0, 'msg': '操作成功'})
except Exception as e:
logger.error(f"考核操作异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
# dengji/views.py
class KaoheJinpaiTagsView(APIView):
"""获取考核金牌页面的标签和板块信息"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
# 获取用户已拥有的标签名称集合
owned_names = set(
YonghuChenghao.objects.filter(yonghu=user, chenghao__leixing='dashou')
.values_list('chenghao__mingcheng', flat=True)
)
# 板块列表
plate_list = []
for bk in Bankuai.objects.all():
tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou')
tag_list = []
for tag in tags:
# 费用列表
fee_objs = KaoheCishuFeiyong.objects.filter(chenghao=tag).order_by('cishu')
fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)} for f in fee_objs]
# 若没有记录,补充默认费用
if not fee_list:
fee_list = [{'cishu': 1, 'feiyong': 5.00}]
tag_list.append({
'id': tag.id,
'name': tag.mingcheng,
'texiao_json': json.loads(tag.texiao_miaoshu) if tag.texiao_miaoshu else {},
'jinpai': tag.kaioi_jinpai,
'rule': tag.kaohe_guize or '',
'fee_list': fee_list,
'owned': tag.mingcheng in owned_names,
})
if tag_list:
plate_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'tags': tag_list
})
# 新增:返回所有在职考核官信息(用于指定考核官)
kaoheguan_list = []
for kg in UserShenheguan.objects.filter(zhuangtai=1).select_related('user'):
uid = kg.user.UserUID
avatar = kg.user.Avatar or ''
# 获取考核官的打手昵称(从打手扩展表)
dashou_nick = ''
dashou_prof = UserDashou.objects.filter(user=kg.user).first()
if dashou_prof:
dashou_nick = dashou_prof.nicheng or ''
kaoheguan_list.append({
'yonghuid': uid,
'avatar': avatar,
'dashou_nick': dashou_nick,
'is_renzheng': kg.is_renzheng,
'shenhe_zhuangtai': kg.shenhe_zhuangtai,
})
return Response({
'code': 0,
'data': {
'plate_list': plate_list,
'my_tags': list(owned_names),
'kaoheguan_list': kaoheguan_list, # 新增
}
})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
class KaoheJiluView(APIView):
"""获取打手本人的考核记录(支持筛选状态)"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
status = request.data.get('status', 'pending') # pending / passed
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 5))
if status == 'pending':
# 进行中/未通过:审核中(0)、待审核(3)、未通过(2)
query = ShenheJilu.objects.filter(
shenqingren_id=user.UserUID,
zhuangtai__in=[0, 2, 3]
)
else:
query = ShenheJilu.objects.filter(
shenqingren_id=user.UserUID,
zhuangtai=1 # 已通过
)
total = query.count()
records = query.order_by('-CreateTime')[(page-1)*page_size : page*page_size]
data = []
for jl in records:
# 汇总缴纳总额
total_fee = KaoheJiluFeiyong.objects.filter(jilu=jl).aggregate(
total=models.Sum('jine')
)['total'] or 0.00
# 上次拒绝原因(最近一条)
last_fail = KaoheJujueJilu.objects.filter(jilu=jl).order_by('-CreateTime').first()
fail_reason = last_fail.yuanyin if last_fail else ''
# 考核官头像
kg_user = User.objects.filter(UserUID=jl.shenheguan_id).first()
kg_avatar = kg_user.Avatar if kg_user else ''
data.append({
'jilu_id': jl.jilu_id,
'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '',
'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '',
'zhuangtai': jl.zhuangtai,
'statusText': dict(ShenheJilu._meta.get_field('zhuangtai').choices).get(jl.zhuangtai, '未知'),
'statusColor': {0: '#FFA500', 1: '#27ae60', 2: '#e74c3c', 3: '#3498db'}.get(jl.zhuangtai, '#888'),
'shenheguan_id': jl.shenheguan_id,
'shenheguan_avatar': kg_avatar,
'dashou_youxi_id': jl.dashou_youxi_id,
'dashou_beizhu': jl.dashou_beizhu,
'cishu': jl.cishu,
'total_fee': float(total_fee),
'last_fail_reason': fail_reason,
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
# 为再次申请预留标签对象数据(前端可能用到)
'tag_obj': {
'id': jl.chenghao_id,
'name': jl.chenghao.mingcheng if jl.chenghao else '',
# 其他字段按需,但申请时会重新拉取
} if jl.chenghao else None,
})
has_more = (page * page_size) < total
return Response({
'code': 0,
'data': {'list': data, 'has_more': has_more, 'total': total}
})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
class KaoheFreeApplyView(APIView):
"""免费申请考核(首次/再次,费用为0时调用)"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
tag_id = request.data.get('tag_id')
game_nick = request.data.get('game_nick', '').strip()
remark = request.data.get('remark', '').strip()
reapply = request.data.get('reapply', False)
jilu_id = request.data.get('jilu_id') if reapply else None
shenheguan_id = request.data.get('shenheguan_id', '').strip() # 新增
if not tag_id or not game_nick:
return Response({'code': 400, 'msg': '标签ID和游戏昵称必填'})
# 校验费用是否为0
try:
chenghao = Chenghao.objects.get(id=tag_id)
except Chenghao.DoesNotExist:
return Response({'code': 400, 'msg': '标签不存在'})
# 计算本次次数
if reapply and jilu_id:
last_record = ShenheJilu.objects.filter(
jilu_id=jilu_id, shenqingren_id=user.UserUID, zhuangtai=2
).first()
if not last_record:
return Response({'code': 400, 'msg': '原记录不存在或状态不是未通过'})
new_cishu = last_record.cishu + 1
else:
max_cishu = ShenheJilu.objects.filter(
shenqingren_id=user.UserUID, chenghao=chenghao, zhuangtai__in=[1,2]
).aggregate(max=models.Max('cishu'))['max'] or 0
new_cishu = max_cishu + 1
fee = get_tag_fee(chenghao, new_cishu)
if fee > 0:
return Response({'code': 400, 'msg': '您有未通过的记录请查看并重考'})
# 新增:如果指定了考核官,进行合法性校验
if shenheguan_id:
valid, err = validate_shenheguan(shenheguan_id, chenghao.bankuai, fee, user)
if not valid:
return Response({'code': 400, 'msg': err})
# 调用公共创建逻辑
success, msg, record = create_shenhe_jilu(
user, tag_id, game_nick, remark, reapply, jilu_id,
shenheguan_id=shenheguan_id # 新增参数
)
if success:
return Response({'code': 0, 'msg': msg, 'data': {'jilu_id': record.jilu_id}})
else:
return Response({'code': 400, 'msg': msg})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
class KaoheJiluListView(APIView):
"""
获取当前考核官的历史审核记录
路径:POST /dengji/khghqkhjl
权限:JWT Token (考核官)
参数:
bankuai_id : int (可选,0或空表示全部)
status : int (可选,0-全部 1-审核中 2-已通过 3-未通过)
page : int (默认1)
page_size : int (默认5)
返回:
list, has_more, total
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
# 1. 验证考核官身份及账号状态
kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first()
if not kg:
return Response({'code': 403, 'msg': '您不是考核官或账号异常'})
# 2. 获取参数
bankuai_id = request.data.get('bankuai_id')
status = int(request.data.get('status', 0)) # 0全部 1审核中 2已通过 3未通过
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 5))
# 3. 构建查询条件:只查当前考核官审核的记录
base_query = ShenheJilu.objects.filter(shenheguan_id=yonghu.UserUID)
# 板块筛选
if bankuai_id and int(bankuai_id) > 0:
base_query = base_query.filter(bankuai_id=int(bankuai_id))
# 状态筛选(前端传的状态映射到后端zhuangtai字段)
status_map = {
0: None, # 全部不筛选
1: 0, # 审核中
2: 1, # 已通过
3: 2, # 未通过
}
if status_map.get(status) is not None:
base_query = base_query.filter(zhuangtai=status_map[status])
# 4. 计数与分页
total = base_query.count()
offset = (page - 1) * page_size
jilu_list = base_query.order_by('-CreateTime')[offset:offset + page_size]
# 5. 预取关联数据,减少查询次数
jilu_list = jilu_list.select_related('bankuai', 'chenghao')
# 为每条记录预取费用明细和拒绝记录(通过 related_name 或手动过滤)
# 由于 KaoheJiluFeiyong 和 KaoheJujueJilu 通过外键指向 ShenheJilu (to_field='jilu_id'),
# 可以使用 prefetch_related
jilu_ids = [j.jilu_id for j in jilu_list]
fee_dict = {}
reject_dict = {}
if jilu_ids:
# 批量获取所有费用明细
fee_qs = KaoheJiluFeiyong.objects.filter(jilu_id__in=jilu_ids).order_by('cishu')
for fee in fee_qs:
if fee.jilu_id not in fee_dict:
fee_dict[fee.jilu_id] = []
fee_dict[fee.jilu_id].append({'cishu': fee.cishu, 'jine': float(fee.jine)})
# 批量获取所有拒绝记录
reject_qs = KaoheJujueJilu.objects.filter(jilu_id__in=jilu_ids).order_by('cishu')
for rej in reject_qs:
if rej.jilu_id not in reject_dict:
reject_dict[rej.jilu_id] = []
reject_dict[rej.jilu_id].append({
'cishu': rej.cishu,
'yuanyin': rej.yuanyin,
'CreateTime': rej.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if rej.CreateTime else ''
})
# 6. 批量获取申请人信息(昵称和头像)
applicant_ids = {j.shenqingren_id for j in jilu_list}
user_map = {}
if applicant_ids:
users = User.objects.filter(UserUID__in=applicant_ids).select_related('DashouProfile')
for u in users:
avatar = u.Avatar or ''
nicheng = ''
try:
# 打手扩展表可能有 nicheng
nicheng = u.DashouProfile.nicheng if getattr(u, 'DashouProfile', None) else ''
except UserDashou.DoesNotExist:
pass
user_map[u.UserUID] = {
'avatar': avatar,
'nicheng': nicheng or f'用户{u.UserUID}'
}
# 7. 组装数据
records = []
for jl in jilu_list:
user_info = user_map.get(jl.shenqingren_id, {'avatar': '', 'nicheng': '未知'})
# 标签特效JSON
texiao_json = {}
if jl.chenghao and jl.chenghao.texiao_miaoshu:
try:
texiao_json = json.loads(jl.chenghao.texiao_miaoshu)
except:
pass
# 汇总总缴纳费用
fee_items = fee_dict.get(jl.jilu_id, [])
total_fee = sum(item['jine'] for item in fee_items)
records.append({
'jilu_id': jl.jilu_id,
'shenqingren_id': jl.shenqingren_id,
'shenqingren_avatar': user_info['avatar'],
'shenqingren_nicheng': user_info['nicheng'],
'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '',
'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '',
'chenghao_texiao_json': texiao_json,
'zhuangtai': jl.zhuangtai, # 0审核中 1通过 2未通过 3待审核
'cishu': jl.cishu,
'total_fee': total_fee,
'dashou_youxi_id': jl.dashou_youxi_id or '',
'dashou_beizhu': jl.dashou_beizhu or '',
'shenheguan_youxi_id': jl.shenheguan_youxi_id or '',
'guize_neirong': jl.guize_neirong or '',
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
'UpdateTime': jl.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.UpdateTime else '',
'fee_list': fee_items,
'reject_list': reject_dict.get(jl.jilu_id, []),
})
has_more = (page * page_size) < total
return Response({
'code': 0,
'data': {
'list': records,
'has_more': has_more,
'total': total
}
})
except Exception as e:
logger.error(f"获取考核记录异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
+30
View File
@@ -0,0 +1,30 @@
"""rank.views package aggregate entry - re-export all submodule symbols.
Preserves external `from rank.views import X` for rank/urls.py.
"""
from .biaoqian import (
DashouBiaoqianView,
BankuaiBiaoqianView,
KaoheJinpaiTagsView,
)
from .kaoheguan import (
KAOHEGUAN_INVITE_CODE,
KaoheguanRegisterView,
KaoheguanRefreshView,
KaoheFreeApplyView,
)
from .shenhe import (
ShenheDatingModuleView,
ShenheDatingDataView,
ShenheQiangdanView,
ShenheXiangqingView,
ShenheCaozuoView,
)
from .kaohe_jilu import (
KaoheJiluView,
KaoheJiluListView,
)
+179
View File
@@ -0,0 +1,179 @@
"""rank.views.biaoqian - auto-generated by split script."""
import json
import logging
from decimal import Decimal
from django.db import models, transaction
from django.db.models import F
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from ..utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu,
KaoheguanBankuai, Bankuai, ShenheJilu, KaoheJiluFeiyong
)
from users.models import UserShenheguan, UserDashou
from users.business_models import User
from products.models import ShangpinLeixing
logger = logging.getLogger(__name__)
class DashouBiaoqianView(APIView):
"""获取当前打手所有称号标签"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
# 查询用户的所有称号关联,一次连表取出
relations = YonghuChenghao.objects.filter(
yonghu=yonghu
).select_related('chenghao')
chenghao_list = []
for rel in relations:
ch = rel.chenghao
# 解析特效描述 JSON(前端需要对象)
texiao_json = {}
if ch.texiao_miaoshu:
try:
texiao_json = json.loads(ch.texiao_miaoshu)
except json.JSONDecodeError:
texiao_json = {}
chenghao_list.append({
'mingcheng': ch.mingcheng,
'tubiao_url': ch.tubiao_url or '',
'texiao_json': texiao_json,
})
return Response({
'code': 0,
'msg': '成功',
'data': {'chenghao_list': chenghao_list}
})
except Exception as e:
logger.error(f"获取打手标签失败: {e}", exc_info=True)
return Response({'code': 1, 'msg': '服务器错误', 'data': None})
class BankuaiBiaoqianView(APIView):
"""获取商品类型所属板块的所有标签"""
permission_classes = [IsAuthenticated]
def post(self, request):
leixing_id = request.data.get('leixing_id') or request.data.get('ProductTypeID')
if not leixing_id:
return Response({'code': 1, 'msg': '缺少leixing_id'})
try:
leixing = ShangpinLeixing.objects.select_related('bankuai').get(id=leixing_id)
except ShangpinLeixing.DoesNotExist:
return Response({'code': 2, 'msg': '商品类型不存在'})
bankuai = leixing.bankuai
if not bankuai:
return Response({'code': 0, 'data': {'biaoqian_list': []}})
# 获取该板块下所有类型的标签(leixing可以为dashou/shangjia等,这里先返回所有)
chenghao_list = Chenghao.objects.filter(bankuai=bankuai, leixing='dashou')
data = []
from jituan.services.club_context import resolve_effective_club_id
from jituan.services.club_kaohe_chenghao import filter_chenghao_ids_for_pool
club_id = resolve_effective_club_id(request, request.user)
allowed_ids = set(
filter_chenghao_ids_for_pool(club_id, list(chenghao_list.values_list('id', flat=True)))
)
for ch in chenghao_list:
if ch.id not in allowed_ids:
continue
texiao_json = {}
if ch.texiao_miaoshu:
try:
texiao_json = json.loads(ch.texiao_miaoshu)
except:
pass
data.append({
'id': ch.id,
'mingcheng': ch.mingcheng,
'texiao_json': texiao_json
})
return Response({'code': 0, 'data': {'biaoqian_list': data}})
class KaoheJinpaiTagsView(APIView):
"""获取考核金牌页面的标签和板块信息"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
# 获取用户已拥有的标签名称集合
owned_names = set(
YonghuChenghao.objects.filter(yonghu=user, chenghao__leixing='dashou')
.values_list('chenghao__mingcheng', flat=True)
)
# 板块列表
plate_list = []
for bk in Bankuai.objects.all():
tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou')
tag_list = []
for tag in tags:
# 费用列表
fee_objs = KaoheCishuFeiyong.objects.filter(chenghao=tag).order_by('cishu')
fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)} for f in fee_objs]
# 若没有记录,补充默认费用
if not fee_list:
fee_list = [{'cishu': 1, 'feiyong': 5.00}]
tag_list.append({
'id': tag.id,
'name': tag.mingcheng,
'texiao_json': json.loads(tag.texiao_miaoshu) if tag.texiao_miaoshu else {},
'jinpai': tag.kaioi_jinpai,
'rule': tag.kaohe_guize or '',
'fee_list': fee_list,
'owned': tag.mingcheng in owned_names,
})
if tag_list:
plate_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'tags': tag_list
})
# 新增:返回所有在职考核官信息(用于指定考核官)
kaoheguan_list = []
for kg in UserShenheguan.objects.filter(zhuangtai=1).select_related('user'):
uid = kg.user.UserUID
avatar = kg.user.Avatar or ''
# 获取考核官的打手昵称(从打手扩展表)
dashou_nick = ''
dashou_prof = UserDashou.objects.filter(user=kg.user).first()
if dashou_prof:
dashou_nick = dashou_prof.nicheng or ''
kaoheguan_list.append({
'yonghuid': uid,
'avatar': avatar,
'dashou_nick': dashou_nick,
'is_renzheng': kg.is_renzheng,
'shenhe_zhuangtai': kg.shenhe_zhuangtai,
})
return Response({
'code': 0,
'data': {
'plate_list': plate_list,
'my_tags': list(owned_names),
'kaoheguan_list': kaoheguan_list, # 新增
}
})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
+243
View File
@@ -0,0 +1,243 @@
"""rank.views.kaohe_jilu - auto-generated by split script."""
import json
import logging
from decimal import Decimal
from django.db import models, transaction
from django.db.models import F
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from ..utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu,
KaoheguanBankuai, Bankuai, ShenheJilu, KaoheJiluFeiyong
)
from users.models import UserShenheguan, UserDashou
from users.business_models import User
from products.models import ShangpinLeixing
logger = logging.getLogger(__name__)
class KaoheJiluView(APIView):
"""获取打手本人的考核记录(支持筛选状态)"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
status = request.data.get('status', 'pending') # pending / passed
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 5))
if status == 'pending':
# 进行中/未通过:审核中(0)、待审核(3)、未通过(2)
query = ShenheJilu.objects.filter(
shenqingren_id=user.UserUID,
zhuangtai__in=[0, 2, 3]
)
else:
query = ShenheJilu.objects.filter(
shenqingren_id=user.UserUID,
zhuangtai=1 # 已通过
)
total = query.count()
records = query.order_by('-CreateTime')[(page-1)*page_size : page*page_size]
data = []
for jl in records:
# 汇总缴纳总额
total_fee = KaoheJiluFeiyong.objects.filter(jilu=jl).aggregate(
total=models.Sum('jine')
)['total'] or 0.00
# 上次拒绝原因(最近一条)
last_fail = KaoheJujueJilu.objects.filter(jilu=jl).order_by('-CreateTime').first()
fail_reason = last_fail.yuanyin if last_fail else ''
# 考核官头像
kg_user = User.objects.filter(UserUID=jl.shenheguan_id).first()
kg_avatar = kg_user.Avatar if kg_user else ''
data.append({
'jilu_id': jl.jilu_id,
'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '',
'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '',
'zhuangtai': jl.zhuangtai,
'statusText': dict(ShenheJilu._meta.get_field('zhuangtai').choices).get(jl.zhuangtai, '未知'),
'statusColor': {0: '#FFA500', 1: '#27ae60', 2: '#e74c3c', 3: '#3498db'}.get(jl.zhuangtai, '#888'),
'shenheguan_id': jl.shenheguan_id,
'shenheguan_avatar': kg_avatar,
'dashou_youxi_id': jl.dashou_youxi_id,
'dashou_beizhu': jl.dashou_beizhu,
'cishu': jl.cishu,
'total_fee': float(total_fee),
'last_fail_reason': fail_reason,
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
# 为再次申请预留标签对象数据(前端可能用到)
'tag_obj': {
'id': jl.chenghao_id,
'name': jl.chenghao.mingcheng if jl.chenghao else '',
# 其他字段按需,但申请时会重新拉取
} if jl.chenghao else None,
})
has_more = (page * page_size) < total
return Response({
'code': 0,
'data': {'list': data, 'has_more': has_more, 'total': total}
})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
class KaoheJiluListView(APIView):
"""
获取当前考核官的历史审核记录
路径:POST /dengji/khghqkhjl
权限:JWT Token (考核官)
参数:
bankuai_id : int (可选,0或空表示全部)
status : int (可选,0-全部 1-审核中 2-已通过 3-未通过)
page : int (默认1)
page_size : int (默认5)
返回:
list, has_more, total
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
# 1. 验证考核官身份及账号状态
kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first()
if not kg:
return Response({'code': 403, 'msg': '您不是考核官或账号异常'})
# 2. 获取参数
bankuai_id = request.data.get('bankuai_id')
status = int(request.data.get('status', 0)) # 0全部 1审核中 2已通过 3未通过
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 5))
# 3. 构建查询条件:只查当前考核官审核的记录
base_query = ShenheJilu.objects.filter(shenheguan_id=yonghu.UserUID)
# 板块筛选
if bankuai_id and int(bankuai_id) > 0:
base_query = base_query.filter(bankuai_id=int(bankuai_id))
# 状态筛选(前端传的状态映射到后端zhuangtai字段)
status_map = {
0: None, # 全部不筛选
1: 0, # 审核中
2: 1, # 已通过
3: 2, # 未通过
}
if status_map.get(status) is not None:
base_query = base_query.filter(zhuangtai=status_map[status])
# 4. 计数与分页
total = base_query.count()
offset = (page - 1) * page_size
jilu_list = base_query.order_by('-CreateTime')[offset:offset + page_size]
# 5. 预取关联数据,减少查询次数
jilu_list = jilu_list.select_related('bankuai', 'chenghao')
# 为每条记录预取费用明细和拒绝记录(通过 related_name 或手动过滤)
# 由于 KaoheJiluFeiyong 和 KaoheJujueJilu 通过外键指向 ShenheJilu (to_field='jilu_id'),
# 可以使用 prefetch_related
jilu_ids = [j.jilu_id for j in jilu_list]
fee_dict = {}
reject_dict = {}
if jilu_ids:
# 批量获取所有费用明细
fee_qs = KaoheJiluFeiyong.objects.filter(jilu_id__in=jilu_ids).order_by('cishu')
for fee in fee_qs:
if fee.jilu_id not in fee_dict:
fee_dict[fee.jilu_id] = []
fee_dict[fee.jilu_id].append({'cishu': fee.cishu, 'jine': float(fee.jine)})
# 批量获取所有拒绝记录
reject_qs = KaoheJujueJilu.objects.filter(jilu_id__in=jilu_ids).order_by('cishu')
for rej in reject_qs:
if rej.jilu_id not in reject_dict:
reject_dict[rej.jilu_id] = []
reject_dict[rej.jilu_id].append({
'cishu': rej.cishu,
'yuanyin': rej.yuanyin,
'CreateTime': rej.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if rej.CreateTime else ''
})
# 6. 批量获取申请人信息(昵称和头像)
applicant_ids = {j.shenqingren_id for j in jilu_list}
user_map = {}
if applicant_ids:
users = User.objects.filter(UserUID__in=applicant_ids).select_related('DashouProfile')
for u in users:
avatar = u.Avatar or ''
nicheng = ''
try:
# 打手扩展表可能有 nicheng
nicheng = u.DashouProfile.nicheng if getattr(u, 'DashouProfile', None) else ''
except UserDashou.DoesNotExist:
pass
user_map[u.UserUID] = {
'avatar': avatar,
'nicheng': nicheng or f'用户{u.UserUID}'
}
# 7. 组装数据
records = []
for jl in jilu_list:
user_info = user_map.get(jl.shenqingren_id, {'avatar': '', 'nicheng': '未知'})
# 标签特效JSON
texiao_json = {}
if jl.chenghao and jl.chenghao.texiao_miaoshu:
try:
texiao_json = json.loads(jl.chenghao.texiao_miaoshu)
except:
pass
# 汇总总缴纳费用
fee_items = fee_dict.get(jl.jilu_id, [])
total_fee = sum(item['jine'] for item in fee_items)
records.append({
'jilu_id': jl.jilu_id,
'shenqingren_id': jl.shenqingren_id,
'shenqingren_avatar': user_info['avatar'],
'shenqingren_nicheng': user_info['nicheng'],
'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '',
'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '',
'chenghao_texiao_json': texiao_json,
'zhuangtai': jl.zhuangtai, # 0审核中 1通过 2未通过 3待审核
'cishu': jl.cishu,
'total_fee': total_fee,
'dashou_youxi_id': jl.dashou_youxi_id or '',
'dashou_beizhu': jl.dashou_beizhu or '',
'shenheguan_youxi_id': jl.shenheguan_youxi_id or '',
'guize_neirong': jl.guize_neirong or '',
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
'UpdateTime': jl.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.UpdateTime else '',
'fee_list': fee_items,
'reject_list': reject_dict.get(jl.jilu_id, []),
})
has_more = (page * page_size) < total
return Response({
'code': 0,
'data': {
'list': records,
'has_more': has_more,
'total': total
}
})
except Exception as e:
logger.error(f"获取考核记录异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
+186
View File
@@ -0,0 +1,186 @@
"""rank.views.kaoheguan - auto-generated by split script."""
import json
import logging
from decimal import Decimal
from django.db import models, transaction
from django.db.models import F
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from ..utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu,
KaoheguanBankuai, Bankuai, ShenheJilu, KaoheJiluFeiyong
)
from users.models import UserShenheguan, UserDashou
from users.business_models import User
from products.models import ShangpinLeixing
logger = logging.getLogger(__name__)
KAOHEGUAN_INVITE_CODE = 'KHG2026VIP'
class KaoheguanRegisterView(APIView):
"""
考核官注册接口
POST /dengji/khgzc
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
invite_code = request.data.get('inviteCode', '').strip()
if not invite_code:
return Response({'code': 400, 'msg': '邀请码不能为空'})
if invite_code != KAOHEGUAN_INVITE_CODE:
return Response({'code': 400, 'msg': '邀请码错误'})
kg = UserShenheguan.objects.filter(user=yonghu).first()
if kg:
if kg.zhuangtai == 1:
return Response({'code': 200, 'msg': '您已是考核官', 'data': self._build_data(kg)})
else:
return Response({'code': 400, 'msg': '账号已被封禁'})
with transaction.atomic():
kg = UserShenheguan.objects.create(
user=yonghu,
zhuangtai=1,
shenhe_zhuangtai=0,
shenhe_zongshu=0,
tongguo_zongshu=0,
yue=0.00,
zonge=0.00
)
return Response({'code': 200, 'msg': '注册成功', 'data': self._build_data(kg)})
except Exception as e:
logger.error(f"考核官注册失败: {e}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})
def _build_data(self, kg):
bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.UserUID).select_related('bankuai')
bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs]
return {
'zhuangtai': kg.zhuangtai,
'shenhe_zongshu': kg.shenhe_zongshu,
'tongguo_zongshu': kg.tongguo_zongshu,
'yue': str(kg.yue),
'zonge': str(kg.zonge),
'shenhe_zhuangtai': kg.shenhe_zhuangtai,
'bankuai_list': bankuai_list,
'kaoheguanstatus': 1
}
class KaoheguanRefreshView(APIView):
"""
考核官数据刷新接口
POST /dengji/khgsx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
kg = UserShenheguan.objects.filter(user=yonghu).first()
if not kg:
return Response({'code': 400, 'msg': '您还未注册考核官'})
if kg.zhuangtai != 1:
return Response({'code': 200, 'data': {'zhuangtai': kg.zhuangtai, 'kaoheguanstatus': 0}})
# 查询所属板块
bankuai_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=kg.user.UserUID).select_related('bankuai')
bankuai_list = [{'bankuai_id': b.bankuai.bankuai_id, 'mingcheng': b.bankuai.mingcheng} for b in bankuai_qs]
# 统计待审核 / 审核中的记录数量
pending_count = ShenheJilu.objects.filter(
shenheguan_id=yonghu.UserUID,
zhuangtai=0 # 审核中
).count()
data = {
'zhuangtai': kg.zhuangtai,
'shenhe_zongshu': kg.shenhe_zongshu,
'tongguo_zongshu': kg.tongguo_zongshu,
'yue': str(kg.yue),
'zonge': str(kg.zonge),
'shenhe_zhuangtai': kg.shenhe_zhuangtai,
'bankuai_list': bankuai_list,
'kaoheguanstatus': 1,
'is_renzheng': kg.is_renzheng, # 🆕 是否认证
'pending_shenhe_count': pending_count # 🆕 审核中数量
}
return Response({'code': 200, 'data': data})
except Exception as e:
logger.error(f"考核官刷新失败: {e}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})
class KaoheFreeApplyView(APIView):
"""免费申请考核(首次/再次,费用为0时调用)"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
tag_id = request.data.get('tag_id')
game_nick = request.data.get('game_nick', '').strip()
remark = request.data.get('remark', '').strip()
reapply = request.data.get('reapply', False)
jilu_id = request.data.get('jilu_id') if reapply else None
shenheguan_id = request.data.get('shenheguan_id', '').strip() # 新增
if not tag_id or not game_nick:
return Response({'code': 400, 'msg': '标签ID和游戏昵称必填'})
# 校验费用是否为0
try:
chenghao = Chenghao.objects.get(id=tag_id)
except Chenghao.DoesNotExist:
return Response({'code': 400, 'msg': '标签不存在'})
# 计算本次次数
if reapply and jilu_id:
last_record = ShenheJilu.objects.filter(
jilu_id=jilu_id, shenqingren_id=user.UserUID, zhuangtai=2
).first()
if not last_record:
return Response({'code': 400, 'msg': '原记录不存在或状态不是未通过'})
new_cishu = last_record.cishu + 1
else:
max_cishu = ShenheJilu.objects.filter(
shenqingren_id=user.UserUID, chenghao=chenghao, zhuangtai__in=[1,2]
).aggregate(max=models.Max('cishu'))['max'] or 0
new_cishu = max_cishu + 1
fee = get_tag_fee(chenghao, new_cishu)
if fee > 0:
return Response({'code': 400, 'msg': '您有未通过的记录请查看并重考'})
# 新增:如果指定了考核官,进行合法性校验
if shenheguan_id:
valid, err = validate_shenheguan(shenheguan_id, chenghao.bankuai, fee, user)
if not valid:
return Response({'code': 400, 'msg': err})
# 调用公共创建逻辑
success, msg, record = create_shenhe_jilu(
user, tag_id, game_nick, remark, reapply, jilu_id,
shenheguan_id=shenheguan_id # 新增参数
)
if success:
return Response({'code': 0, 'msg': msg, 'data': {'jilu_id': record.jilu_id}})
else:
return Response({'code': 400, 'msg': msg})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
+401
View File
@@ -0,0 +1,401 @@
"""rank.views.shenhe - auto-generated by split script."""
import json
import logging
from decimal import Decimal
from django.db import models, transaction
from django.db.models import F
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from ..utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
YonghuChenghao, Chenghao, KaoheCishuFeiyong, KaoheJujueJilu,
KaoheguanBankuai, Bankuai, ShenheJilu, KaoheJiluFeiyong
)
from users.models import UserShenheguan, UserDashou
from users.business_models import User
from products.models import ShangpinLeixing
logger = logging.getLogger(__name__)
class ShenheDatingModuleView(APIView):
"""审核大厅获取模块列表(板块 + 待审核数量 + 是否关联)"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
related_ids = set(KaoheguanBankuai.objects.filter(kaoheguan_id=yonghu.UserUID)
.values_list('bankuai_id', flat=True))
bankuai_list = []
for bk in Bankuai.objects.all():
count = ShenheJilu.objects.filter(bankuai=bk, zhuangtai=3, shenheguan_id__isnull=True).count()
bankuai_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'count': count,
'is_related': bk.bankuai_id in related_ids
})
return Response({'code': 0, 'data': {'modules': bankuai_list}})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
class ShenheDatingDataView(APIView):
"""获取审核大厅待审核记录列表"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
data = request.data
bankuai_id = data.get('bankuai_id')
page = int(data.get('page', 1))
page_size = int(data.get('page_size', 5))
query = ShenheJilu.objects.filter(zhuangtai=3, shenheguan_id__isnull=True)
if bankuai_id:
query = query.filter(bankuai_id=bankuai_id)
total = query.count()
offset = (page - 1) * page_size
jilu_list = query.select_related('bankuai', 'chenghao').order_by('-CreateTime')[
offset:offset + page_size]
records = []
for jl in jilu_list:
user = User.objects.filter(UserUID=jl.shenqingren_id).first()
avatar = user.Avatar if user else ''
texiao_json = {}
if jl.chenghao and jl.chenghao.texiao_miaoshu:
try:
texiao_json = json.loads(jl.chenghao.texiao_miaoshu)
except:
pass
# 新增字段:本次考核费用
kaohe_feiyong = float(jl.jiaofei_jine) if jl.jiaofei_jine is not None else 0.0
records.append({
'jilu_id': jl.jilu_id,
'shenqingren_id': jl.shenqingren_id,
'shenqingren_avatar': avatar,
'dashou_youxi_id': jl.dashou_youxi_id or '',
'dashou_beizhu': jl.dashou_beizhu or '',
'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None,
'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '',
'chenghao_id': jl.chenghao_id,
'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '',
'chenghao_texiao_json': texiao_json,
'guize_neirong': jl.guize_neirong or '',
'kaohe_feiyong': kaohe_feiyong, # 新增
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
})
has_more = (page * page_size) < total
return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
class ShenheQiangdanView(APIView):
"""
审核官抢单(接审核任务)
校验:身份、账号封禁、已有进行中审核、记录状态、不能审核自己、板块权限
成功后更新考核官扩展表:审核总数+1,审核状态设为“审核中”
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
jilu_id = request.data.get('jilu_id')
if not jilu_id:
return Response({'code': 400, 'msg': '记录ID不能为空'})
# 1. 检查考核官身份及账号是否被禁用
kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first()
if not kg:
return Response({'code': 403, 'msg': '您不是考核官或账号已被禁用'})
# 2. 检查是否有正在审核中的记录(zhuangtai=0),防止同时审核多个
ongoing = ShenheJilu.objects.filter(
shenheguan_id=yonghu.UserUID, zhuangtai=0
).exists()
if ongoing:
return Response({'code': 400, 'msg': '您有正在审核中的任务,请完成后再接新任务'})
with transaction.atomic():
# 3. 行锁抢单:仅当状态为待审核(3)且无审核官时才能抢
jilu = ShenheJilu.objects.select_for_update().filter(
jilu_id=jilu_id,
zhuangtai=3,
shenheguan_id__isnull=True
).first()
if not jilu:
return Response({'code': 400, 'msg': '该审核任务已被接走、状态变更或不存在'})
# 4. 不能审核自己
if jilu.shenqingren_id == yonghu.UserUID:
return Response({'code': 400, 'msg': '不能审核自己提交的申请'})
# 5. 板块权限:考核官必须属于该板块
is_related = KaoheguanBankuai.objects.filter(
kaoheguan_id=yonghu.UserUID,
bankuai=jilu.bankuai
).exists()
if not is_related:
return Response({'code': 400, 'msg': '您不属于该板块,无法审核'})
# 新增:6. 费用>0时,要求考核官已认证
if jilu.jiaofei_jine and float(jilu.jiaofei_jine) > 0:
if not kg.is_renzheng:
return Response({'code': 400, 'msg': '本考核需要认证考核官接单,您尚未认证'})
# 7. 更新审核记录:分配给当前考核官,状态变为审核中
jilu.zhuangtai = 0 # 审核中
jilu.shenheguan_id = yonghu.UserUID
jilu.save()
# 8. 更新考核官扩展表:审核总数 +1,审核状态置为审核中(1)
UserShenheguan.objects.filter(user=yonghu).update(
shenhe_zongshu=F('shenhe_zongshu') + 1,
shenhe_zhuangtai=1
)
return Response({
'code': 0,
'msg': '抢单成功,请前往游戏内审核,审核完成后请到考核打分页面进行评定。',
'data': {'bankuai_id': jilu.bankuai.bankuai_id}
})
except Exception as e:
logger.error(f"审核官抢单异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
class ShenheXiangqingView(APIView):
"""获取当前考核官审核中的记录详情(新增待收考核费字段)"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first()
if not kg:
return Response({'code': 403, 'msg': '您不是考核官或账号异常'})
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
# 查询当前考核官名下状态为“审核中”的记录
base_query = ShenheJilu.objects.filter(
shenheguan_id=yonghu.UserUID,
zhuangtai=0
).select_related('bankuai', 'chenghao')
total = base_query.count()
offset = (page - 1) * page_size
jilu_list = base_query[offset:offset + page_size]
records = []
for jl in jilu_list:
user = User.objects.filter(UserUID=jl.shenqingren_id).first()
avatar = user.Avatar if user else ''
texiao_json = {}
if jl.chenghao and jl.chenghao.texiao_miaoshu:
try:
texiao_json = json.loads(jl.chenghao.texiao_miaoshu)
except:
pass
# 是否开启金牌
is_jinpai = jl.chenghao.kaioi_jinpai if jl.chenghao else False
# 本次待收考核费(基于记录中的当前次数,从费用明细表查询)
daishou_fei = 0.00
fee_detail = KaoheJiluFeiyong.objects.filter(
jilu=jl,
cishu=jl.cishu
).first()
if fee_detail:
daishou_fei = float(fee_detail.jine)
else:
# 兜底:若明细表无记录,尝试用通用费用表计算
daishou_fei = 0.00
# 上次失败原因
last_fail_reason = ''
last_fail = KaoheJujueJilu.objects.filter(
jilu=jl,
cishu=jl.cishu - 1
).order_by('-CreateTime').first()
if last_fail:
last_fail_reason = last_fail.yuanyin
records.append({
'jilu_id': jl.jilu_id,
'shenqingren_id': jl.shenqingren_id,
'shenqingren_avatar': avatar,
'bankuai_mingcheng': jl.bankuai.mingcheng if jl.bankuai else '',
'chenghao_mingcheng': jl.chenghao.mingcheng if jl.chenghao else '',
'chenghao_texiao_json': texiao_json,
'guize_neirong': jl.guize_neirong or '',
'dashou_beizhu': jl.dashou_beizhu or '',
'dashou_youxi_id': jl.dashou_youxi_id or '',
'shenheguan_youxi_id': jl.shenheguan_youxi_id or '',
'cishu': jl.cishu,
'daishou_fei': daishou_fei, # 新增:待收考核费
'last_fail_reason': last_fail_reason,
'is_jinpai': is_jinpai,
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
})
has_more = (page * page_size) < total
return Response({'code': 0, 'data': {'list': records, 'has_more': has_more, 'total': total}})
except Exception as e:
return Response({'code': 500, 'msg': str(e)}, status=500)
class ShenheCaozuoView(APIView):
"""
考核官执行操作(同意/拒绝/转移/修改昵称),并更新考核官收益
- 只有“同意”或“拒绝”才增加余额和总额,金额严格从 KaoheJiluFeiyong 表获取
- 查不到费用则加 0 元,绝不影响流程
- 转移、修改昵称等操作不涉及收益
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghu = request.user
# 1. 验证考核官身份及账号状态
kg = UserShenheguan.objects.filter(user=yonghu, zhuangtai=1).first()
if not kg:
return Response({'code': 403, 'msg': '账号异常,无法操作'})
jilu_id = request.data.get('jilu_id')
caozuo = request.data.get('caozuo') # agree / reject / transfer / modify_nick
if not jilu_id or not caozuo:
return Response({'code': 400, 'msg': '缺少必要参数'})
with transaction.atomic():
# 2. 锁定审核记录,确保属于当前考核官且状态为审核中(0)
jilu = ShenheJilu.objects.select_for_update().filter(
jilu_id=jilu_id,
shenheguan_id=yonghu.UserUID,
zhuangtai=0
).first()
if not jilu:
return Response({'code': 400, 'msg': '该记录不存在或不属于您'})
# 3. 本次操作产生的收入(仅通过/拒绝时有意义)
daishou_fei = 0.00
# 4. 根据操作类型执行对应业务逻辑
if caozuo == 'agree':
# --- 通过考核 ---
# 查询本次考核应得的费用(基于记录ID和当前次数)
fee_detail = KaoheJiluFeiyong.objects.filter(
jilu=jilu,
cishu=jilu.cishu
).first()
if fee_detail:
daishou_fei = float(fee_detail.jine)
# 找不到就保持 0,绝不使用任何默认值
jilu.zhuangtai = 1
jilu.save()
# 授予标签
if jilu.chenghao:
yh = User.objects.get(UserUID=jilu.shenqingren_id)
if not YonghuChenghao.objects.filter(yonghu=yh, chenghao=jilu.chenghao).exists():
YonghuChenghao.objects.create(yonghu=yh, chenghao=jilu.chenghao)
# 如果标签开启了金牌,同步更新打手称号
if jilu.chenghao.kaioi_jinpai:
dashou_prof = UserDashou.objects.filter(user=yh).first()
if dashou_prof:
dashou_prof.chenghao = '金牌选手'
dashou_prof.save()
elif caozuo == 'reject':
# --- 拒绝考核 ---
yuanyin = request.data.get('yuanyin', '')
if not yuanyin.strip():
return Response({'code': 400, 'msg': '请填写拒绝原因'})
# 查询本次考核应得的费用(拒绝时同样获得收入)
fee_detail = KaoheJiluFeiyong.objects.filter(
jilu=jilu,
cishu=jilu.cishu
).first()
if fee_detail:
daishou_fei = float(fee_detail.jine)
# 找不到就保持 0
jilu.zhuangtai = 2
jilu.save()
# 记录拒绝原因
KaoheJujueJilu.objects.create(
jilu=jilu,
cishu=jilu.cishu,
shenheguan_id=yonghu.UserUID,
yuanyin=yuanyin
)
# 新增:不合格时,如果考核官未认证,则清空考核官ID和游戏昵称
if not kg.is_renzheng:
jilu.shenheguan_id = None
jilu.shenheguan_youxi_id = ''
jilu.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id'])
elif caozuo == 'transfer':
# 转移考核官:清空考核官信息,状态回退为待审核
jilu.shenheguan_id = None
jilu.shenheguan_youxi_id = None
jilu.zhuangtai = 3
jilu.save()
# 转移不更新收益,直接返回
elif caozuo == 'modify_nick':
# 修改考核官游戏昵称
youxi_id = request.data.get('youxi_id', '').strip()
if not youxi_id:
return Response({'code': 400, 'msg': '请输入游戏昵称'})
jilu.shenheguan_youxi_id = youxi_id
jilu.save()
return Response({'code': 0, 'msg': '修改成功'})
else:
return Response({'code': 400, 'msg': '无效的操作类型'})
# 5. 更新考核官收益(仅“通过”或“拒绝”时执行)
if caozuo in ('transfer'):
update_fields = {
'shenhe_zhuangtai': 0, # 设为空闲
}
UserShenheguan.objects.filter(user=yonghu).update(**update_fields)
if caozuo in ('agree', 'reject'):
update_fields = {
'yue': F('yue') + Decimal(str(daishou_fei)),
'zonge': F('zonge') + Decimal(str(daishou_fei)),
'shenhe_zhuangtai': 0, # 设为空闲
}
# 只有同意时才增加“通过总数”
if caozuo == 'agree':
update_fields['tongguo_zongshu'] = F('tongguo_zongshu') + 1
# 审核总数不再增加
UserShenheguan.objects.filter(user=yonghu).update(**update_fields)
return Response({'code': 0, 'msg': '操作成功'})
except Exception as e:
logger.error(f"考核操作异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
-112
View File
@@ -1,112 +0,0 @@
-- 重命名 create_time → CreateTime, update_time → UpdateTime
-- 生成时间: 自动生成
ALTER TABLE `abnormal_user_log` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `admin_profile` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `bankuai` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `chenghao` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `chufajilu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `chufajilu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `chufatupian` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `chufatupian` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `czjilu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `czjilu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `daily_income_stat` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `daily_income_stat` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `daily_payout_stat` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `daily_payout_stat` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `dashou_biaoxian` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `dashoutupian` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `dashoutupian` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `dianpu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `dianpu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `dianpu_moren_peizhi` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `dianpu_moren_peizhi` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `dianpu_shangpin_shenhe_shezhi` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `dianpu_shangpin_shenhe_shezhi` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `dianpu_shouzhi_meiri_tongji` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `dianpu_shouzhi_meiri_tongji` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `dingdan` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `dingdan` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `dingdan_biaoqian` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `dingdan_peihu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `dingdan_peihu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `duoci_fenhong` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `duoci_fenhong` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `fadan` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `fadan` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `fadan_fenhong` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `fadan_fenhong` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `fadan_fenhong_lilv` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `fadan_fenhong_lilv` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `fadan_shensu_tupian` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `gonggao` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `gonggao` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `gsfenhong` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `gsfenhong` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `huiyuan` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `huiyuan` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `huiyuangoumai` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `huiyuangoumai` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `jingyan_jilu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `jingyan_jilu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `kaohe_jujue_jilu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `kaohe_pay_temp` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `liaotian` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `liaotian` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `lilubiao` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `lilubiao` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `lunbo` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `lunbo` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `order_dashou_history` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `pingfenbiao` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `pingfenbiao` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `qunpeizhi` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `qunpeizhi` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `role_permission` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `shangjia_lianjie` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `shangjia_lianjie` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `shangjia_moban` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `shangjia_moban` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `shangpin` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `shangpin` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `shangpin_meiri_tongji` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `shangpin_meiri_tongji` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `shenhe_jilu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `shenhe_jilu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `szjilu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `szjilu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `tixian_auto_record` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `tixian_auto_record` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `tixian_quota_default` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `tixian_quota_default` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `tixian_shenhe_jilu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `tixian_shenhe_jilu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `tixianjilu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `tixianjilu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `tuikuanjilu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `tuikuanjilu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `tupianpeizhi` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `tupianpeizhi` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `user_boss` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `user_dashou` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `user_guanshi` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `user_kefu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `user_kefu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `user_role` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `user_role` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `user_sensitive_data` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `user_sensitive_data` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `user_shangjia` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `user_shenheguan` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `user_shenheguan` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `user_zuzhang` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `user_zuzhang` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `xiugaijilu` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `xiugaijilu` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `yonghu_dianpu_bangding` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `yonghu_dianpu_bangding` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
ALTER TABLE `yonghu_pingzheng` CHANGE COLUMN `create_time` `CreateTime` datetime(6) NOT NULL;
ALTER TABLE `yonghu_pingzheng` CHANGE COLUMN `update_time` `UpdateTime` datetime(6) NOT NULL;
-- 完成
-3124
View File
File diff suppressed because it is too large. Load diff
+51
View File
@@ -0,0 +1,51 @@
"""shop.views package aggregate entry - re-export all submodule symbols.
Preserves external `from shop.views import X` for shop/urls.py.
"""
from .shop_base_views import (
BindDianpuView,
ShopGoodsView,
ShangdianLoginView,
ShopOverviewView,
QrcodeDownloadView,
DianpuShouzhiTongjiView,
ShopQrcodeView,
)
from .shop_product_views import (
ShopProductConfigView,
ShopProductModifyView,
ShopProductListView,
ShopProductModifyView1,
)
from .shop_member_views import (
MemberListView,
SubShopListView,
SubShopModifyView,
)
from .shop_order_views import (
ShopOrderStatisticsView,
ShopOrderManageView,
)
from .shop_refund_views import (
ShopRefundView,
ShopForceCompleteView,
ShopTransferHallView,
ShopFineApplyView,
)
__all__ = [
# shop_base_views
"BindDianpuView", "ShopGoodsView", "ShangdianLoginView",
"ShopOverviewView", "QrcodeDownloadView", "DianpuShouzhiTongjiView",
"ShopQrcodeView",
# shop_product_views
"ShopProductConfigView", "ShopProductModifyView",
"ShopProductListView", "ShopProductModifyView1",
# shop_member_views
"MemberListView", "SubShopListView", "SubShopModifyView",
# shop_order_views
"ShopOrderStatisticsView", "ShopOrderManageView",
# shop_refund_views
"ShopRefundView", "ShopForceCompleteView",
"ShopTransferHallView", "ShopFineApplyView",
]
+771
View File
@@ -0,0 +1,771 @@
"""shop.views.shop_base_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 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, 'UserUID'):
return Response({'code': 500, 'msg': '用户身份信息异常'})
# 先获取 User 对象(不加锁,仅用于校验存在性)
try:
user_main = User.query.get(UserUID=user.UserUID)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
# 校验店铺是否存在且状态正常(不加锁)
try:
dianpu = Dianpu.query.get(id=dianpu_id)
except Dianpu.DoesNotExist:
return Response({'code': 404, 'msg': '店铺不存在'})
if dianpu.zhuangtai != 1:
return Response({'code': 200, 'msg': '店铺已被封禁,无法绑定'})
# 在事务内部进行加锁操作
try:
with transaction.atomic():
# 重新获取加锁的用户对象(确保并发安全)
user_main_locked = User.objects.select_for_update().get(UserUID=user.UserUID)
dianpu_locked = Dianpu.objects.select_for_update().get(id=dianpu_id)
# 再次校验店铺状态(防止事务期间被修改)
if dianpu_locked.zhuangtai != 1:
return Response({'code': 200, 'msg': '店铺已被封禁,无法绑定'})
# 处理绑定关系
try:
existing_binding = YonghuDianpuBangding.objects.select_for_update().get(yonghu=user_main_locked)
old_dianpu = existing_binding.dianpu
if old_dianpu.id == dianpu_locked.id:
return Response({'code': 200, 'msg': '已绑定该店铺'})
# 旧店铺绑定人数减1
old_dianpu.bangding_yonghushu = F('bangding_yonghushu') - 1
old_dianpu.save(update_fields=['bangding_yonghushu'])
# 更新绑定关系
existing_binding.dianpu = dianpu_locked
existing_binding.save()
except YonghuDianpuBangding.DoesNotExist:
YonghuDianpuBangding.query.create(yonghu=user_main_locked, dianpu=dianpu_locked)
# 新店铺绑定人数加1
dianpu_locked.bangding_yonghushu = F('bangding_yonghushu') + 1
dianpu_locked.save(update_fields=['bangding_yonghushu'])
logger.info(f"用户 {user.UserUID} 绑定店铺 {dianpu_id} 成功")
return Response({'code': 200, 'msg': '绑定成功'})
except Exception as e:
logger.exception(f"绑定店铺失败: {e}")
return Response({'code': 500, 'msg': '绑定失败,服务器内部错误'})
class ShopGoodsView(APIView):
"""
获取店铺商品数据(类型、专区、商品列表)
根据用户绑定店铺状态及身份,返回店铺专属或公共商品
"""
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
try:
user_main = User.query.get(UserUUID=user.UserUUID)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
# 1. 查询用户店铺绑定关系
binding = YonghuDianpuBangding.query.filter(
yonghu=user_main
).select_related('dianpu').first()
# 2. 判断用户是否拥有特殊身份
has_special = self._has_special_identity(user_main)
# 3. 根据绑定关系及店铺状态决定数据获取策略
if binding:
dianpu = binding.dianpu
if dianpu.zhuangtai == 1:
# 店铺正常 -> 返回店铺专属商品(需通过平台审核)
data = self._get_shop_goods(dianpu)
else:
# 店铺被封 -> 返回公共商品,有特殊身份则返回审核状态为1的,否则返回2的
data = self._get_public_goods(True)
else:
# 无绑定店铺,根据身份决定返回公共商品的审核状态
data = self._get_public_goods(has_special)
return Response({'code': 200, 'data': data})
def _has_special_identity(self, user_main):
"""判断用户是否拥有打手/商家/管事/组长任一身份"""
return (UserDashou.query.filter(user=user_main).exists() or
UserShangjia.query.filter(user=user_main).exists() or
UserGuanshi.query.filter(user=user_main).exists() or
UserZuzhang.query.filter(user=user_main).exists())
def _get_shop_goods(self, dianpu):
"""
获取店铺专属商品
类型、专区、商品均从店铺相关表中获取,且需通过平台审核(shenhe_zhuangtai=True)
"""
# 1. 店铺商品类型:未封禁、已上架、平台审核通过
leixing_queryset = ShangpinLeixingDianpu.query.filter(
dianpu=dianpu,
fengjin_zhuangtai=False,
shangjia_zhuangtai=True,
shenhe_zhuangtai=True # 🔥 必须通过平台审核
).select_related('gonggong_leixing').order_by('-paixu', 'id')
leixing_data = []
for lx in leixing_queryset:
leixing_data.append({
'id': lx.id, # 店铺类型ID,前端归类用
'jieshao': lx.jieshao,
'tupian_url': lx.tupian_url,
'paixu': lx.paixu
})
leixing_ids = [lx['id'] for lx in leixing_data]
# 2. 专区:根据店铺和店铺类型ID,未封禁、已上架
if leixing_ids:
zhuanqu_queryset = ShangpinZhuanqu.query.filter(
dianpu=dianpu,
dianpu_leixing_id__in=leixing_ids,
fengjin_zhuangtai=False,
shangjia_zhuangtai=True
).order_by('-paixu', 'id')
else:
zhuanqu_queryset = ShangpinZhuanqu.query.none()
zhuanqu_data = []
for zq in zhuanqu_queryset:
zhuanqu_data.append({
'id': zq.id,
'mingzi': zq.mingzi,
'leixing_id': zq.dianpu_leixing_id, # 对应店铺类型ID
'paixu': zq.paixu
})
# 3. 商品:根据店铺、店铺类型、专区,未封禁、已上架、平台审核通过、原有审核状态为正常
zhuanqu_ids = [zq['id'] for zq in zhuanqu_data]
if zhuanqu_ids:
shangpin_queryset = Shangpin.query.filter(
dianpu=dianpu,
dianpu_leixing_id__in=leixing_ids,
zhuanqu_id__in=zhuanqu_ids,
fengjin_zhuangtai=False,
shangjia_zhuangtai=True,
shenhe_zhuangtai=True, # 🔥 平台审核通过
shenhezhuangtai=1 # 原有审核状态:正常
).values(
'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu',
'dianpu_leixing_id', 'zhuanqu_id'
).order_by('-paixu', 'id')
shangpin_data = list(shangpin_queryset)
# 字段映射:前端统一使用 leixing_id
for sp in shangpin_data:
sp['leixing_id'] = sp.pop('dianpu_leixing_id')
else:
shangpin_data = []
return {
'shangpinleixing': leixing_data,
'shangpinzhuanqu': zhuanqu_data,
'shangpinliebiao': shangpin_data
}
def _get_public_goods(self, use_audit_status_one=True):
"""
获取公共商品
类型、专区、商品均从公共表获取。
- 必须满足 dianpu 为空且 dianpu_leixing 为空(即纯公共数据)。
- 审核状态:若 use_audit_status_one=True,则返回 shenhezhuangtai=1 的数据;
否则返回 shenhezhuangtai=2 的数据(审核专用)。
"""
target_shenhe = 1 if use_audit_status_one else 2
# 1. 公共商品类型:原有审核状态为 target_shenhe
leixing_queryset = ShangpinLeixing.query.filter(
shenhezhuangtai=target_shenhe
).order_by('-paixu', 'id')
leixing_data = []
for lx in leixing_queryset:
leixing_data.append({
'id': lx.id,
'jieshao': lx.jieshao,
'tupian_url': lx.tupian_url,
'paixu': lx.paixu
})
visible_leixing_ids = [lx['id'] for lx in leixing_data]
# 2. 专区:dianpu 为空,且 dianpu_leixing 为空,leixing_id 在可见范围内,原有审核状态为 target_shenhe
if visible_leixing_ids:
zhuanqu_queryset = ShangpinZhuanqu.query.filter(
dianpu__isnull=True,
dianpu_leixing__isnull=True, # 🔥 必须为空
leixing_id__in=visible_leixing_ids,
shenhezhuangtai=target_shenhe
).order_by('-paixu', 'id')
else:
zhuanqu_queryset = ShangpinZhuanqu.query.none()
zhuanqu_data = []
for zq in zhuanqu_queryset:
zhuanqu_data.append({
'id': zq.id,
'mingzi': zq.mingzi,
'leixing_id': zq.leixing_id,
'paixu': zq.paixu
})
# 3. 商品:dianpu 为空,dianpu_leixing 为空,专区在可见范围内,原有审核状态为 target_shenhe
zhuanqu_ids = [zq['id'] for zq in zhuanqu_data]
if zhuanqu_ids:
shangpin_queryset = Shangpin.query.filter(
dianpu__isnull=True,
dianpu_leixing__isnull=True, # 🔥 必须为空
leixing_id__in=visible_leixing_ids,
zhuanqu_id__in=zhuanqu_ids,
shenhezhuangtai=target_shenhe
).values(
'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu',
'leixing_id', 'zhuanqu_id'
).order_by('-paixu', 'id')
shangpin_data = list(shangpin_queryset)
else:
shangpin_data = []
return {
'shangpinleixing': leixing_data,
'shangpinzhuanqu': zhuanqu_data,
'shangpinliebiao': shangpin_data
}
class ShangdianLoginView(APIView):
"""
商家后台登录接口(通过凭证直接获取店铺)
POST /shangdian/login
请求参数:
yonghuid : 用户ID(7位)
zhanghao : 登录账号
mima : 密码
返回数据:
code: 0, data: {
token: "jwt_token",
yonghuid: "1234567",
dianpu_mingcheng: "店铺名称",
dianpu_touxiang: "头像相对URL",
erweima_url: "二维码相对URL"
}
"""
permission_classes = [AllowAny]
def post(self, request):
# 1. 获取请求参数
yonghuid = request.data.get('yonghuid', '').strip()
zhanghao = request.data.get('zhanghao', '').strip()
mima = request.data.get('mima', '').strip()
# 2. 参数完整性校验
if not yonghuid or not zhanghao or not mima:
return Response({'code': 400, 'msg': '用户ID、账号、密码均不能为空'})
# 3. 验证用户主表是否存在
try:
user_main = User.query.get(UserUID=yonghuid)
except User.DoesNotExist:
logger.warning(f"登录失败:用户ID不存在 - {yonghuid}")
return Response({'code': 404, 'msg': '账号或密码错误'})
# 4. 通过用户ID + 账号查询凭证表
try:
pingzheng = YonghuPingzheng.query.get(
yonghu=yonghuid,
zhanghao=zhanghao,
is_active=True
)
except YonghuPingzheng.DoesNotExist:
logger.warning(f"登录失败:账号或密码错误 - 用户ID:{yonghuid}, 账号:{zhanghao}")
return Response({'code': 401, 'msg': '账号或密码错误'})
# 5. 校验密码
if pingzheng.mima != mima:
logger.warning(f"登录失败:密码错误 - 用户ID:{yonghuid}, 账号:{zhanghao}")
return Response({'code': 401, 'msg': '账号或密码错误'})
# 6. 【修改点】通过凭证直接获取关联的店铺(利用 OneToOneField 反向关系)
try:
dianpu = pingzheng.dianpu
except Dianpu.DoesNotExist:
logger.error(f"登录异常:凭证 {zhanghao} 未关联任何店铺")
return Response({'code': 403, 'msg': '您尚未关联店铺,请联系管理员'})
# 7. 检查店铺状态
if dianpu.zhuangtai != 1:
logger.warning(f"登录失败:店铺已被封禁 - 店铺ID:{dianpu.id}, 用户ID:{yonghuid}")
return Response({'code': 403, 'msg': '店铺已被封禁,无法登录'})
# 8. 生成 JWT Token
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
# 9. 更新最后登录时间
user_main.UserLastLoginDate = timezone.now()
user_main.save(update_fields=['UserLastLoginDate'])
# 10. 构造返回数据
data = {
'token': token,
'yonghuid': user_main.UserUID,
'dianpu_mingcheng': dianpu.dianpu_mingcheng,
'dianpu_touxiang': dianpu.dianpu_touxiang or '',
'erweima_url': dianpu.erweima_url or ''
}
logger.info(f"商家登录成功:用户ID {yonghuid} - 店铺 {dianpu.dianpu_mingcheng}")
return Response({'code': 0, 'msg': '登录成功', 'data': data})
class ShopOverviewView(APIView):
"""
店铺概览数据接口
URL: /shangdian/sjdpgl
Method: POST
权限: JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 从请求体中获取前端传递的 userId(用于防越权)
frontend_user_id = request.data.get('userId')
if not frontend_user_id:
return Response(
{'code': 400, 'msg': '缺少必要参数 userId'}
)
# 调用公共验证方法
dianpu, error_response = verify_shop_permission(request, frontend_user_id)
if error_response:
return error_response
# 验证通过,构造返回数据(新增字段,其余保持不变)
data = {
'id': dianpu.id,
'dianpu_mingcheng': dianpu.dianpu_mingcheng,
'dianpu_touxiang': dianpu.dianpu_touxiang or '',
'lianxi_dianhua': dianpu.lianxi_dianhua or '',
'weixinhao': dianpu.weixinhao or '',
'zhuangtai': dianpu.zhuangtai,
'bangding_yonghushu': dianpu.bangding_yonghushu,
'erweima_url': dianpu.erweima_url or '',
'CreateTime': dianpu.CreateTime.isoformat() if dianpu.CreateTime else '',
'UpdateTime': dianpu.UpdateTime.isoformat() if dianpu.UpdateTime else '',
# 新增字段
'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng,
'shouyi_choucheng_feilv': float(dianpu.shouyi_choucheng_feilv) if dianpu.shouyi_choucheng_feilv else 0.0,
'ketixian_yue': float(dianpu.ketixian_yue),
'zhengqu_zonge': float(dianpu.zhengqu_zonge),
'meiri_tixian_xiane': float(dianpu.meiri_tixian_xiane) if dianpu.meiri_tixian_xiane else 0.0,
'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane,
}
return Response({
'code': 0,
'msg': 'success',
'data': data
})
class QrcodeDownloadView(APIView):
"""
店铺二维码下载接口(通过后端代理下载,解决跨域限制)
URL: /shangdian/xzewm?dianpu_id=xx
权限: JWT认证 + 店铺归属验证
"""
permission_classes = [IsAuthenticated]
def get(self, request):
# 1. 身份验证与店铺校验
frontend_user_id = request.GET.get('userId') # 前端通过查询参数传递 userId
if not frontend_user_id:
return Response({'code': 400, 'msg': '缺少必要参数 userId'}, status=400)
dianpu, error_response = verify_shop_permission(request, frontend_user_id)
if error_response:
return error_response
# 2. 获取店铺的二维码相对路径
erweima_relative_url = dianpu.erweima_url
if not erweima_relative_url:
return HttpResponse('店铺暂无二维码', status=404)
# 3. 拼接 OSS 完整访问地址(使用 settings 中的域名)
oss_domain = getattr(settings, 'COS_DOMAIN', '')
if not oss_domain:
logger.error("COS_DOMAIN 未配置")
return HttpResponse('OSS域名未配置', status=500)
image_url = oss_domain.rstrip('/') + '/' + erweima_relative_url.lstrip('/')
# 4. 从腾讯云 COS 获取图片内容
try:
client = get_oss_client()
bucket = getattr(settings, 'COS_BUCKET', '')
response = client.get_object(Bucket=bucket, Key=erweima_relative_url)
image_data = response['Body'].get_raw_stream().read()
except Exception as e:
logger.exception("从COS获取二维码失败")
return HttpResponse('下载失败,请稍后重试', status=502)
# 5. 构造下载响应,强制浏览器保存为文件
filename = f"店铺二维码_{dianpu.dianpu_mingcheng}.png"
response = HttpResponse(image_data, content_type='image/png')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
response['Content-Length'] = str(len(image_data))
return response
class DianpuShouzhiTongjiView(APIView):
"""
店铺每日收支统计接口(支持按日、月、年聚合)
URL: /dianpu/dpsztj
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 身份验证
frontend_user_id = request.data.get('userId')
if not frontend_user_id:
return Response({'code': 400, 'msg': '缺少 userId'})
dianpu, error = verify_shop_permission(request, frontend_user_id)
if error:
return error
granularity = request.data.get('granularity', 'day') # day / month / year
year = request.data.get('year')
month = request.data.get('month')
# 查询当前店铺的统计记录
qs = DianpuShouzhiMeiriTongji.query.filter(dianpu_id=dianpu.id)
if granularity == 'day':
if not year or not month:
return Response({'code': 400, 'msg': '日颗粒度需要提供年份和月份'})
try:
year = int(year)
month = int(month)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '年份或月份格式错误'})
# 取出该月每一天的数据,并按日排序
daily_data = qs.filter(nian=year, yue=month).values('ri').annotate(
xiadan_zongliang=Sum('xiadan_zongliang'),
xiadan_zonge=Sum('xiadan_zonge'),
chengjiao_zongliang=Sum('chengjiao_zongliang'),
chengjiao_zonge=Sum('chengjiao_zonge'),
tuikuan_zongliang=Sum('tuikuan_zongliang'),
tuikuan_zonge=Sum('tuikuan_zonge'),
shouyi_zonge=Sum('shouyi_zonge'),
).order_by('ri')
# 补全缺失的日期(默认值为0)
days_in_month = calendar.monthrange(year, month)[1]
result = []
for day in range(1, days_in_month + 1):
found = next((item for item in daily_data if item['ri'] == day), None)
if found:
result.append(found)
else:
result.append({
'ri': day,
'xiadan_zongliang': 0, 'xiadan_zonge': 0,
'chengjiao_zongliang': 0, 'chengjiao_zonge': 0,
'tuikuan_zongliang': 0, 'tuikuan_zonge': 0,
'shouyi_zonge': 0,
})
return Response({'code': 0, 'data': result, 'type': 'day'})
elif granularity == 'month':
if not year:
return Response({'code': 400, 'msg': '月颗粒度需要提供年份'})
try:
year = int(year)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '年份格式错误'})
# 按月份聚合
monthly_data = qs.filter(nian=year).values('yue').annotate(
xiadan_zongliang=Sum('xiadan_zongliang'),
xiadan_zonge=Sum('xiadan_zonge'),
chengjiao_zongliang=Sum('chengjiao_zongliang'),
chengjiao_zonge=Sum('chengjiao_zonge'),
tuikuan_zongliang=Sum('tuikuan_zongliang'),
tuikuan_zonge=Sum('tuikuan_zonge'),
shouyi_zonge=Sum('shouyi_zonge'),
).order_by('yue')
# 补全 1~12 月
result = []
for m in range(1, 13):
found = next((item for item in monthly_data if item['yue'] == m), None)
if found:
result.append(found)
else:
result.append({
'yue': m,
'xiadan_zongliang': 0, 'xiadan_zonge': 0,
'chengjiao_zongliang': 0, 'chengjiao_zonge': 0,
'tuikuan_zongliang': 0, 'tuikuan_zonge': 0,
'shouyi_zonge': 0,
})
return Response({'code': 0, 'data': result, 'type': 'month'})
elif granularity == 'year':
# 按年份聚合
yearly_data = qs.values('nian').annotate(
xiadan_zongliang=Sum('xiadan_zongliang'),
xiadan_zonge=Sum('xiadan_zonge'),
chengjiao_zongliang=Sum('chengjiao_zongliang'),
chengjiao_zonge=Sum('chengjiao_zonge'),
tuikuan_zongliang=Sum('tuikuan_zongliang'),
tuikuan_zonge=Sum('tuikuan_zonge'),
shouyi_zonge=Sum('shouyi_zonge'),
).order_by('nian')
return Response({'code': 0, 'data': list(yearly_data), 'type': 'year'})
else:
return Response({'code': 400, 'msg': '无效的颗粒度'})
class ShopQrcodeView(APIView):
"""
店铺专属二维码生成/更新接口
URL: /shangdian/sdscdpm
Method: POST
权限: JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 获取前端传递的 userId
frontend_user_id = request.data.get('userId')
if not frontend_user_id:
return Response(
{'code': 400, 'msg': '缺少必要参数 userId'},
status=status.HTTP_400_BAD_REQUEST
)
# 公共验证
dianpu, error_response = verify_shop_permission(request, frontend_user_id)
if error_response:
return error_response
# 获取缓存的微信 access_token
access_token = self._get_wx_access_token()
if not access_token:
return Response(
{'code': 500, 'msg': '获取微信 access_token 失败'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 构造小程序码请求参数
# 注意:scene 最大32个字符,我们使用店铺ID
scene = str(dianpu.id)
page_path = "pages/index/index" # 前端小程序首页路径
wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}'
post_data = {
'scene': scene,
'page': page_path,
'width': 430,
'auto_color': False,
'line_color': {'r': 0, 'g': 0, 'b': 0},
}
# 调用微信接口生成小程序码
try:
wx_resp = requests.post(wx_url, json=post_data, timeout=10)
except Exception as e:
logger.error(f"请求微信生成二维码接口异常: {e}")
return Response(
{'code': 500, 'msg': '调用微信服务失败'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 判断返回是否为图片
if wx_resp.status_code != 200 or 'image' not in wx_resp.headers.get('content-type', ''):
error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {}
errmsg = error_info.get('errmsg', '未知错误')
logger.error(f"微信生成二维码失败: {errmsg}")
return Response(
{'code': 500, 'msg': f'生成二维码失败: {errmsg}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 构造文件名和 COS 路径
timestamp = int(time.time())
# 三层文件夹结构: dianpu/shangjiadianpu/dianpuerweima/
filename = f"dianpu_{dianpu.id}_{timestamp}.png"
oss_path = f"dianpu/shangjiadianpu/dianpuerweima/{filename}"
file_obj = io.BytesIO(wx_resp.content)
# 上传到 COS
try:
full_url = upload_to_oss(file_obj, oss_path)
except Exception as e:
logger.error(f"上传二维码到 COS 失败: {e}")
return Response(
{'code': 500, 'msg': '图片上传失败'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if not full_url:
return Response(
{'code': 500, 'msg': '图片上传失败'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 提取相对路径(根据您的 upload_to_oss 返回值格式调整)
cos_domain = getattr(settings, 'COS_DOMAIN', '')
if cos_domain and full_url.startswith(cos_domain):
relative_path = full_url[len(cos_domain):].lstrip('/')
else:
relative_path = oss_path
# 删除旧二维码文件(如果存在)
old_relative_path = dianpu.erweima_url
if old_relative_path:
try:
delete_from_oss(old_relative_path)
except Exception as e:
logger.warning(f"删除旧二维码文件失败: {old_relative_path}, 错误: {e}")
# 更新数据库
with transaction.atomic():
dianpu.erweima_url = relative_path
dianpu.save(update_fields=['erweima_url', 'UpdateTime'])
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
+375
View File
@@ -0,0 +1,375 @@
"""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,
# 其他字段使用默认值
)
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 验证店铺身份
"""
+531
View File
@@ -0,0 +1,531 @@
"""shop.views.shop_order_views - 店铺订单统计与管理视图(含 OSS 图片批量辅助函数)."""
# ==================== 标准库 ====================
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 ShopOrderStatisticsView(APIView):
"""
店铺订单统计接口
URL: POST /shangdian/ddltj
权限: JWT认证 + 店铺身份验证
请求参数:
userId (必填): 店铺所属用户的ID
返回结构:
{
"code": 200,
"msg": "统计成功",
"data": {
"types": [ # 所有有效商品类型
{
"id": 类型ID,
"jieshao": "类型介绍",
"tupian_url": "类型图片相对路径",
"order_count": 该类型订单总数,
"status_counts": { # 该类型下各状态订单数量
"1": 数量,
"7": 数量,
"2": 数量,
"8": 数量,
"3": 数量,
"4": 数量,
"5": 数量,
"6": 数量
}
},
...
],
"status_counts": { # 全局各状态订单总数
"1": 总数,
"7": 总数,
"2": 总数,
"8": 总数,
"3": 总数,
"4": 总数,
"5": 总数,
"6": 总数
},
"total_orders": 订单总量
}
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 身份验证
user_id = request.data.get('userId', '').strip()
if not user_id:
return Response({'code': 400, 'msg': '缺少 userId'})
dianpu, error = verify_shop_permission(request, user_id)
if error:
return error
shop_id = dianpu.id
try:
# 2. 获取所有正常状态(审核通过)的商品类型
types_qs = ShangpinLeixing.query.filter(
shenhezhuangtai=1
).order_by('-paixu', 'id')
type_list = []
type_id_map = {}
for idx, t in enumerate(types_qs):
item = {
'id': t.id,
'jieshao': t.jieshao or '',
'tupian_url': t.tupian_url or '',
'order_count': 0,
'status_counts': {
'1': 0, '7': 0, '2': 0, '8': 0,
'3': 0, '4': 0, '5': 0, '6': 0
}
}
type_list.append(item)
type_id_map[t.id] = idx
# 3. 获取本店铺所有订单的主键ID列表(通过平台扩展表)
order_pks = list(
PlatformOrderExt.query.filter(dianpu_id=shop_id)
.values_list('Order_id', flat=True) # 这里取的是订单主表的主键id
)
total_orders = 0
global_counts = defaultdict(int)
if order_pks:
# 4. 用主键列表进行聚合统计,按商品类型和订单状态分组
aggregation = (
Order.objects
.filter(id__in=order_pks) # 修正点:使用id__in,而不是dingdan_id__in
.values('ProductTypeID', 'Status')
.annotate(cnt=Count('id'))
)
for row in aggregation:
lid = row['ProductTypeID'] # 商品类型ID
st = row['Status'] # 订单状态码
cnt = row['cnt']
# 转为字符串键,方便前端展示
st_key = str(st)
# 累加总量
total_orders += cnt
global_counts[st_key] += cnt
# 填充到对应商品类型
if lid in type_id_map:
idx = type_id_map[lid]
type_list[idx]['order_count'] += cnt
if st_key in type_list[idx]['status_counts']:
type_list[idx]['status_counts'][st_key] += cnt
# 5. 确保全局状态返回所有8个键,即使值为0
all_status_keys = ['1', '7', '2', '8', '3', '4', '5', '6']
final_global_counts = {
k: global_counts.get(k, 0) for k in all_status_keys
}
# 6. 返回统计结果
return Response({
'code': 200,
'msg': '统计成功',
'data': {
'types': type_list,
'status_counts': final_global_counts,
'total_orders': total_orders
}
})
except Exception as e:
logger.exception("订单统计接口异常")
return Response({
'code': 500,
'msg': '统计失败,服务器内部错误'
})
# ================================================================
# 辅助函数:获取OSS域名(用于拼接本地图片绝对URL)
# ================================================================
def get_oss_domain():
"""
获取对象存储的域名,末尾包含 '/'。
返回:字符串,如 'https://example.com/'
"""
return getattr(settings, 'COS_DOMAIN', 'https://xingque999qygwuyq-1404472910.cos.ap-shanghai.myqcloud.com/').rstrip('/') + '/'
# ================================================================
# 辅助函数:获取单个订单的打手提交图片(绝对URL)
# ================================================================
def _fetch_images(order, oss_domain):
"""
查询指定订单的打手提交图片记录(Dashoutupian 表),
将相对路径拼接为完整的绝对 URL 并返回。
参数:
order: Dingdan 订单模型实例
oss_domain: OSS 域名,末尾含 '/'
返回:
(images_list, is_punished, punishment_info) 三元组
- images_list: 图片绝对 URL 列表
- is_punished: 暂未使用,默认 False
- punishment_info: 暂未使用,默认空字典
"""
images = []
is_punished = False
punishment_info = {}
# 仅当订单有接单打手ID时才查询打手图片
if order.PlayerID:
# 从打手图片表中查询该订单、该打手的所有图片记录
image_urls = PlayerDeliveryImage.query.filter(
OrderID=order.OrderID, # 订单ID匹配
PlayerID=order.PlayerID # 接单打手ID匹配
).values_list('ImageURL', flat=True) # 只取图片URL字段
# 遍历所有图片路径
for img_path in image_urls:
if img_path:
if img_path.startswith('http'):
# 已经是完整URL,直接添加
images.append(img_path)
else:
# 相对路径,拼接 OSS 域名
images.append(oss_domain + img_path.lstrip('/'))
# 跨平台图片拉取逻辑(根据实际业务需求补充,此处保留结构)
# 如果有跨平台订单,需要请求对方平台接口获取打手图片
return images, is_punished, punishment_info
# ================================================================
# 辅助函数:并发获取多个订单的打手图片
# ================================================================
def _batch_images(orders, oss_domain):
"""
并发调用 _fetch_images,提升批量获取图片的速度。
参数:
orders: Dingdan 订单对象列表
oss_domain: OSS 域名
返回:
字典,键为订单ID (dingdan_id),值为 (images_list, is_punished, punishment_info)
"""
result = {} # 存储结果
# 使用线程池并发执行,max_workers 可根据服务器性能调整
with ThreadPoolExecutor(max_workers=10) as executor:
# 建立 future 到订单ID的映射
future_to_oid = {
executor.submit(_fetch_images, order, oss_domain): order.OrderID
for order in orders
}
# 等待所有任务完成
for future in as_completed(future_to_oid):
oid = future_to_oid[future] # 对应的订单ID
try:
# 获取任务返回值
imgs, pun, pinfo = future.result()
result[oid] = (imgs, pun, pinfo)
except Exception as e:
# 某个订单获取失败,记录日志并赋予空值,保证整体流程不中断
logger.error(f"获取订单 {oid} 图片失败: {e}")
result[oid] = ([], False, {})
return result
# ================================================================
# 核心视图类
# ================================================================
class ShopOrderManageView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
"""
统一入口,根据 action 参数分发到统计或列表模块。
"""
# 1. 从请求中提取店铺用户ID
user_id = request.data.get('userId', '').strip()
if not user_id:
return Response({'code': 400, 'msg': '缺少 userId'})
# 2. 验证店铺身份,获取店铺对象
dianpu, error_response = verify_shop_permission(request, user_id)
if error_response:
return error_response
# 3. 根据 action 决定调用哪个方法
action = request.data.get('action', '').strip().lower()
if action == 'statistics':
return self._statistics(dianpu) # 统计
else:
return self._order_list(request, dianpu) # 列表
# -----------------------------------------------------------------
# 统计实现 (这就是你要求的100个统计字段的详细实现)
# -----------------------------------------------------------------
def _statistics(self, dianpu):
"""
统计本店铺的订单数据,输出以下信息:
- types: 商品类型数组,每个元素包含:
id : 类型ID
jieshao : 类型介绍
tupian_url : 类型图片
order_count : 【该类型的订单总数】
status_counts: 该类型下各个状态的订单数量
- status_counts: 全局各状态订单总数(键为状态码字符串)
- total_orders : 店铺订单总量
所有字段名均与前端保持一致。
"""
# 获取店铺ID
shop_id = dianpu.id
# ------------------------------------------------------------
# 步骤1:获取所有正常的商品类型(审核状态为1)
# ------------------------------------------------------------
types_query = ShangpinLeixing.query.filter(
shenhezhuangtai=1 # 审核状态为1(正常)
).order_by('-paixu', 'id') # 按排序权重和ID排序
# 构建返回的类型列表,并预初始化各状态计数
type_list = [] # 最终要返回的类型数组
type_ids = [] # 所有类型的ID列表,用于后续快速索引
# 定义可能出现的所有状态码(1/7已付款,2进行中,8待结算,3已完成,4退款审核,5已退款,6退款失败)
status_keys = ['1', '7', '2', '8', '3', '4', '5', '6']
for t in types_query:
# 构造每个类型的基础数据
type_item = {
'id': t.id,
'jieshao': t.jieshao or '',
'tupian_url': t.tupian_url or '',
'order_count': 0, # 该类型订单总数,稍后填充
'status_counts': {k: 0 for k in status_keys} # 各状态计数初始为0
}
type_list.append(type_item)
type_ids.append(t.id) # 记录ID
# ------------------------------------------------------------
# 步骤2:获取本店铺的所有订单ID(通过平台扩展表)
# ------------------------------------------------------------
# 从 DingdanPingtai 中过滤出属于当前店铺的订单扩展记录,并提取订单主表ID
order_ids = list(
PlatformOrderExt.query.filter(
dianpu_id=shop_id # 店铺ID匹配
).values_list('Order_id', flat=True) # 只取订单ID字段
)
# 统计变量
total_orders = 0 # 订单总量
global_status_counts = defaultdict(int) # 全局各状态计数,键为状态码字符串
# ------------------------------------------------------------
# 步骤3:如果店铺有订单,进行聚合统计
# ------------------------------------------------------------
if order_ids:
# 使用 Django 聚合查询:按 leixing_id(商品类型)和 zhuangtai(订单状态)分组计数
aggregation = Order.query.filter(
OrderID__in=order_ids # 从该店铺的订单中查询
).values(
'ProductTypeID', # 按类型ID分组
'Status' # 按状态分组
).annotate(
cnt=Count('id') # 统计每个分组的记录数
)
# 遍历每个聚合结果,累加到对应的类型和全局计数中
for row in aggregation:
# 提取分组字段
lid = row['ProductTypeID'] # 商品类型ID
st = row['Status'] # 订单状态码
cnt = row['cnt'] # 该分组下的订单数量
# 累加到全局总数
total_orders += cnt
global_status_counts[str(st)] += cnt
# 累加到对应商品类型的状态计数中
if lid in type_ids:
# 在 type_list 中找到对应的类型条目
for tp in type_list:
if tp['id'] == lid:
# 更新该类型的订单总数
tp['order_count'] += cnt
# 更新该类型下对应状态的计数(键为字符串)
tp['status_counts'][str(st)] += cnt
break # 找到后跳出循环,因为ID唯一
# 将全局状态计数转换为按固定顺序的字典,确保前端展示顺序稳定
final_global_counts = {k: global_status_counts.get(k, 0) for k in status_keys}
# ------------------------------------------------------------
# 步骤4:返回统计结果
# ------------------------------------------------------------
return Response({
'code': 200,
'msg': '统计成功',
'data': {
'types': type_list, # 商品类型列表(含各类型统计)
'status_counts': final_global_counts, # 全局各状态总数
'total_orders': total_orders # 订单总量
}
})
# -----------------------------------------------------------------
# 订单列表实现
# -----------------------------------------------------------------
def _order_list(self, request, dianpu):
"""处理订单列表请求,支持筛选、分页、并发获取图片。"""
shop_id = dianpu.id
# 解析筛选参数
leixing_id = request.data.get('leixing_id')
zhuangtai = request.data.get('zhuangtai')
dingdan_id_key = request.data.get('dingdan_id', '').strip()
jiedan_dashou_id_key = request.data.get('jiedan_dashou_id', '').strip()
nicheng_key = request.data.get('nicheng', '').strip()
laoban_id_key = request.data.get('laoban_id', '').strip()
jieshao_key = request.data.get('jieshao', '').strip()
page = request.data.get('page', 1)
page_size = request.data.get('page_size', 20)
# 基础查询:从平台扩展表关联订单主表,并按创建时间倒序
qs = PlatformOrderExt.query.filter(
dianpu_id=shop_id
).select_related('Order').order_by('-Order__CreateTime')
# 应用筛选条件
if leixing_id is not None:
qs = qs.filter(Order__ProductTypeID=leixing_id)
if zhuangtai is not None:
qs = qs.filter(Order__Status=zhuangtai)
if dingdan_id_key:
qs = qs.filter(Order__OrderID__icontains=dingdan_id_key)
if jiedan_dashou_id_key:
qs = qs.filter(Order__PlayerID=jiedan_dashou_id_key)
if nicheng_key:
qs = qs.filter(Order__Nickname__icontains=nicheng_key)
if laoban_id_key:
qs = qs.filter(BossID=laoban_id_key)
if jieshao_key:
qs = qs.filter(Order__Description__icontains=jieshao_key)
# 分页
paginator = Paginator(qs, page_size)
try:
page_obj = paginator.page(page)
except EmptyPage:
page_obj = paginator.page(1)
# 获取当前页的所有订单和扩展对象
orders_with_ext = [(item.Order, item) for item in page_obj]
# 提取订单对象列表,用于并发获取图片
order_objs = [order for order, _ in orders_with_ext]
oss_domain = get_oss_domain()
img_map = _batch_images(order_objs, oss_domain)
# 构建返回数据
list_data = []
for order, ext in orders_with_ext:
imgs, is_pun, _ = img_map.get(order.OrderID, ([], False, {}))
list_data.append({
'dingdan_id': order.OrderID,
'zhuangtai': order.Status,
'jine': float(order.Amount) if order.Amount else 0.00,
'dashou_fencheng': float(order.PlayerCommission) if order.PlayerCommission else 0.00,
'jiedan_dashou_id': order.PlayerID or '',
'dashou_liuyan': order.PlayerRemark or '',
'zhiding_id': order.AssignedID or '',
'shangpin_id': order.ProductID or '',
'tupian': order.ImageURL or '',
'jieshao': order.Description or '',
'beizhu': order.Remark or '',
'nicheng': order.Nickname or '',
'leixing_id': order.ProductTypeID,
'CreateTime': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
'UpdateTime': order.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if order.UpdateTime else '',
'pingtai_kuozhan': {
'laoban_id': ext.BossID or '',
'laoban_pingjia': ext.BossReview or '',
'dianpu_shouyi': float(ext.ShopIncome) if ext.ShopIncome else 0.00,
},
'dashou_images': imgs,
'is_punished': is_pun,
'is_fadaned': False,
})
return Response({
'code': 200,
'msg': '列表成功',
'data': {
'list': list_data,
'total': paginator.count,
'page': page_obj.number,
'page_size': page_size
}
})
File diff suppressed because it is too large. Load diff
+660
View File
@@ -0,0 +1,660 @@
"""shop.views.shop_refund_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 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 = PlatformOrderExt.query.select_related('Order').get(
ShopID=shop_id,
Order__OrderID=dingdan_id
)
order = pingtai_ext.Order
except PlatformOrderExt.DoesNotExist:
logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在")
return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'})
# 4. 订单状态校验(只有这些状态可退款)
allowed_statuses = [1, 7, 2, 8, 4]
if order.Status not in allowed_statuses:
logger.warning(f"订单状态不允许退款: {order.Status}")
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.PlayerID
try:
with transaction.atomic():
# 更新订单状态为已退款
order.Status = 5
order.AssignedCS = str(shop_id) # 处理人记录为店铺ID
if reason:
order.RefundReason = reason
order.save()
# 更新打手统计(退款量+1,状态置为空闲)
if dashou_id:
try:
dashou_user = User.query.get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.tuikuanliang += 1
dashou_profile.zhuangtai = 1
dashou_profile.save()
except (User.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"退款:打手 {dashou_id} 不存在,跳过状态更新")
# 更新退款记录表
self._update_refund_record(order, dashou_id, str(shop_id), reason)
# 打手每日统计(退款动作)
try:
update_dashou_daily_by_action(
yonghuid=dashou_id,
amount=order.PlayerCommission or Decimal('0.00'),
action=3 # 3 表示退款
)
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 更新平台支出记录
try:
update_daily_payout(order.Amount, getattr(order, 'ClubID', None))
except Exception as e:
logger.error(f"更新每日支出失败: {e}")
# ========== 新增:商品和店铺每日统计 ==========
try:
# 商品每日统计(只要有商品ID就执行)
if order.ProductID:
# 从扩展表获取已计算好的收益值(下单时已存储)
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(order, 'pingtai_kuozhan'):
pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_shangpin_daily_stat(
shangpin_id=order.ProductID,
caozuo_leixing=3, # 3 = 退款
dingdan_jiage=order.Amount,
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.ShopIncome or Decimal('0.00')
update_dianpu_daily_stat(
dianpu_id=dianpu_id,
caozuo_leixing=3, # 3 = 退款
dingdan_jiage=order.Amount,
dianpu_shouyi=dianpu_shouyi
)
logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
except Exception as e:
logger.error(f"商品/店铺统计更新失败: {e}")
logger.error(traceback.format_exc())
# 统计失败不影响主流程
# =============================================
except Exception as e:
logger.exception("退款事务执行失败")
return Response({'code': 500, 'msg': '退款处理失败,请稍后重试'})
return Response({'code': 200, 'msg': '退款成功'})
# ========== 微信退款 ==========
def _call_wechat_refund(self, order):
"""调用微信支付退款接口"""
appid = getattr(settings, 'WEIXIN_APPID', '')
mch_id = getattr(settings, 'WEIXIN_MCHID', '')
key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '')
key_path = getattr(settings, 'WEIXIN_KEY_PATH', '')
if not all([appid, mch_id, key, cert_path, key_path]):
return {'code': 500, 'msg': '微信支付配置不完整'}
out_refund_no = self._generate_refund_no(order.OrderID)
total_fee = yuan_to_fen(order.Amount or 0)
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.WechatTransactionID:
params['transaction_id'] = order.WechatTransactionID
else:
params['out_trade_no'] = order.OrderID
# 签名
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 = RefundRecord.query.get_or_create(
OrderID=order.OrderID,
defaults={
'PlayerID': dashou_id or '',
'ApplicantID': '',
'ProcessorID': chuli_id,
'Reason': reason or '店铺退款',
'ApplyStatus': 1,
'Amount': order.Amount or 0,
'PlayerCommission': order.PlayerCommission or 0,
'Description': order.Description or '',
'Remark': '店铺退款',
'Nickname': order.Nickname or '',
}
)
if not created:
refund_record.ApplyStatus = 1
refund_record.ProcessorID = chuli_id
if reason:
refund_record.Reason = reason
refund_record.save()
except Exception as e:
logger.error(f"更新退款记录异常: {e}")
# ========== 工具方法 ==========
def _generate_nonce_str(self):
return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
def _generate_refund_no(self, dingdan_id):
timestamp = str(int(time.time()))
rand = ''.join(random.choices('0123456789', k=4))
return f"{dingdan_id}REF{timestamp}{rand}"
def _dict_to_xml(self, data):
xml = ['<xml>']
for k, v in data.items():
xml.append(f'<{k}>{v}</{k}>')
xml.append('</xml>')
return ''.join(xml)
class ShopForceCompleteView(APIView):
"""
店铺强制结单接口
URL: POST /shangdian/qzjd
权限: JWT认证 + 店铺身份验证
请求参数:
userId (必填) : 店铺所属用户的ID
dingdan_id(必填) : 订单ID
说明:
- 只处理平台订单(fadan_pingtai == 1)
- 订单必须属于当前店铺
- 订单状态必须为 8(结算中)才能强制结单
- 默认视为我方派单,根据是否有对方订单ID判断是否跨平台
- 本地订单:给打手结算、更新打手余额和统计、更新店铺收益统计
- 跨平台订单:仅通知对方平台,不处理打手
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# ========== 1. 获取参数 ==========
user_id = request.data.get('userId', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
if not user_id or not dingdan_id:
return Response({'code': 400, 'msg': '参数不完整'})
# ========== 2. 店铺身份验证 ==========
dianpu, error = verify_shop_permission(request, user_id)
if error:
return error
shop_id = dianpu.id
# ========== 3. 查询订单并验证归属 ==========
try:
# 通过平台扩展表确认订单属于本店铺
pingtai_ext = PlatformOrderExt.query.select_related('Order').get(
ShopID=shop_id,
Order__OrderID=dingdan_id
)
order = pingtai_ext.Order
except PlatformOrderExt.DoesNotExist:
logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在")
return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'})
# ========== 4. 校验订单状态必须为 8(结算中) ==========
if order.Status != 8:
logger.warning(f"订单 {dingdan_id} 当前状态为 {order.Status},不是结算中")
return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})
dashou_id = order.PlayerID
dashou_fencheng = order.PlayerCommission or Decimal('0.00')
jine = order.Amount or Decimal('0.00')
try:
with transaction.atomic():
# 锁定订单行,防止并发
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
if not dashou_id:
return Response({'code': 400, 'msg': '订单无接单打手,无法结算'})
if dashou_fencheng < 0:
return Response({'code': 400, 'msg': '打手分成金额无效'})
# ---------- 更新打手余额和统计 ----------
try:
dashou_user = User.query.get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.chengjiaozongliang += 1
dashou_profile.yue += dashou_fencheng
dashou_profile.zonge += dashou_fencheng
dashou_profile.jinrishouyi += dashou_fencheng
dashou_profile.jinyueshouyi += dashou_fencheng
dashou_profile.zhuangtai = 1 # 空闲
dashou_profile.save()
except (User.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过打手更新")
# ---------- 更新订单状态 ----------
order.Status = 3
order.AssignedCS = str(shop_id)
order.save()
# ---------- 打手每日统计(成交) ----------
try:
update_dashou_daily_by_action(
yonghuid=dashou_id,
amount=dashou_fencheng,
action=2 # 2 表示成交(结算)
)
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# ---------- 店铺收益统计(结算) ----------
if pingtai_ext.ShopIncome:
try:
# 商品每日统计(只要有商品ID就执行)
if order.ProductID:
# 从扩展表获取已计算好的收益值(下单时已存储)
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(order, 'pingtai_kuozhan'):
pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_shangpin_daily_stat(
shangpin_id=order.ProductID,
caozuo_leixing=2, # 3 = 退款
dingdan_jiage=order.Amount,
pingtai_shouyi=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}")
update_dianpu_daily_stat(
dianpu_id=shop_id,
caozuo_leixing=2, # 结算操作
dingdan_jiage=jine,
dianpu_shouyi=pingtai_ext.ShopIncome,
)
except Exception as e:
logger.error(f"店铺每日统计更新失败: {e}")
# ========== 5. 记录操作日志 ==========
logger.info(f"强制结单成功,订单 {dingdan_id},店铺 {shop_id}")
return Response({'code': 200, 'msg': '强制结单成功'})
except Order.DoesNotExist:
logger.warning(f"订单不存在: {dingdan_id}")
return Response({'code': 404, 'msg': '订单不存在'})
except Exception as e:
logger.error(f"强制结单接口异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})
# ================================================================
# shangdian/views/transfer_hall_view.py
# 店铺转移大厅接口 —— 严格二改,不添加任何原接口没有的逻辑
# ================================================================
# ================================================================
# shangdian/views/transfer_hall_view.py
# 店铺转移大厅接口 —— 添加本地打手退款统计
# ================================================================
class ShopTransferHallView(APIView):
"""
店铺转移大厅接口
URL: POST /shangdian/zydt
权限: JWT认证 + 店铺身份验证
请求参数:
userId (必填) : 店铺所属用户的ID
dingdan_id(必填) : 订单ID
说明:
- 只处理平台订单,我方派单
- 允许转移的状态:2,8,4
- 决定是否处理本地打手:有对方订单ID或俱乐部ID(跨平台)则不处理本地打手,只通知对方;
没有对方信息则处理本地打手(释放+退款统计)
- 转移后清空对方平台信息,订单重回可接单状态
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# ---------- 1. 获取参数 ----------
user_id = request.data.get('userId', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
if not user_id or not dingdan_id:
return Response({'code': 400, 'msg': '参数不完整'})
# ---------- 2. 店铺身份验证 ----------
dianpu, error = verify_shop_permission(request, user_id)
if error:
return error
shop_id = dianpu.id
# ---------- 3. 验证订单归属 ----------
try:
pingtai_ext = PlatformOrderExt.query.select_related('Order').get(
ShopID=shop_id,
Order__OrderID=dingdan_id
)
order = pingtai_ext.Order
except PlatformOrderExt.DoesNotExist:
logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在")
return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'})
# ---------- 4. 状态校验 ----------
if order.Status not in [2, 8, 4]:
return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.Status}'})
# ---------- 5. 提取关键字段 ----------
current_dashou_id = order.PlayerID
zhiding_id = order.zhiding_id
dashou_fencheng = order.PlayerCommission or 0 # 用于退款统计
try:
with transaction.atomic():
# 锁定订单行
order = Order.objects.select_for_update().get(OrderID=dingdan_id)
# ---------- 6. 记录打手历史(原接口保留逻辑) ----------
if current_dashou_id:
last_history = OrderPlayerHistory.query.filter(
dingdan_id=dingdan_id
).order_by('-times').first()
next_times = (last_history.times + 1) if last_history else 1
OrderPlayerHistory.query.create(
dingdan_id=dingdan_id,
dashou_id=current_dashou_id,
times=next_times,
operator=str(shop_id)
)
# ---------- 7. 释放本地打手 + 退款统计 ----------
if current_dashou_id:
try:
dashou_profile = UserDashou.query.get(user__UserUID=current_dashou_id)
dashou_profile.zhuangtai = 1 # 设为空闲
dashou_profile.save()
except ObjectDoesNotExist:
logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新")
else:
# 打手释放成功后,记录每日退款统计(转移视为退款)
try:
update_dashou_daily_by_action(
yonghuid=current_dashou_id,
amount=dashou_fencheng,
action=3 # 3 表示退款
)
logger.info(f"打手 {current_dashou_id} 转移大厅退款统计成功")
except Exception as e:
logger.error(f"打手每日统计失败: {e}")
# ---------- 8. 重置订单 ----------
order.jiedan_dashou_id = None
order.clkf = str(shop_id)
# 根据是否有指定打手决定状态
order.zhuangtai = 7 if zhiding_id else 1
order.save()
logger.info(f"转移大厅成功,订单 {dingdan_id}")
return Response({'code': 200, 'msg': '转移大厅成功'})
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
except Exception as e:
logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})
# ================================================================
# shangdian/views/fine_apply_view.py
# 店铺罚款申请接口 —— 二改自 FineApplyView(客服罚款)
# ================================================================
class ShopFineApplyView(APIView):
"""
店铺罚款申请接口
URL: POST /shangdian/fksq
权限: JWT认证 + 店铺身份验证
请求参数:
userId (必填) : 店铺所属用户的ID(用于身份验证)
dingdan_id (必填) : 订单ID
reason (必填) : 罚款原因
amount (必填) : 罚款金额(元)
yingxiang_qiangdan (可选) : 是否影响抢单,1=是,0=否,默认1
说明:
- 仅处理平台订单,需属于当前店铺
- 本地/对方派单处理逻辑与客服接口一致:
无对方订单ID 或 对方派单 → 本地生成罚单
有对方订单ID且我方派单 → 跨平台通知对方,本地也记录(标记对方俱乐部ID)
- 申请人:店铺ID,身份码固定为4(店铺管理员)
- 被处罚人:接单打手,身份码固定为1
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# ---------- 1. 提取参数 ----------
user_id = request.data.get('userId', '').strip() # 店铺用户ID
dingdan_id = request.data.get('dingdan_id', '').strip()
chufaliyou = request.data.get('reason', '').strip() # 罚款原因(对应前端的reason)
fakuanjine = request.data.get('amount', 0) # 罚款金额(对应前端的amount)
yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) # 影响抢单
if not all([user_id, dingdan_id, chufaliyou, fakuanjine]):
return Response({'code': 400, 'msg': '参数不完整'})
try:
# ---------- 2. 店铺身份验证 ----------
dianpu, error = verify_shop_permission(request, user_id)
if error:
return error
shop_id = dianpu.id # 店铺ID,作为罚款申请人
# ---------- 3. 验证订单归属 ----------
try:
pingtai_ext = PlatformOrderExt.query.select_related('Order').get(
ShopID=shop_id,
Order__OrderID=dingdan_id
)
order = pingtai_ext.Order
except PlatformOrderExt.DoesNotExist:
logger.warning(f"罚款申请:订单 {dingdan_id} 不属于店铺 {shop_id}")
return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'})
# ---------- 4. 获取打手ID ----------
dashou_id = order.PlayerID
if not dashou_id:
return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
# ---------- 5. 重复罚款检查 ----------
if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=dingdan_id).exists():
return Response({'code': 400, 'msg': '该打手已被罚款过'})
# ---------- 6. 创建罚单 ----------
from jituan.services.club_penalty import resolve_penalty_club_id
with transaction.atomic():
penalty = Penalty.query.create(
PenalizedUserID=dashou_id,
ApplicantID=str(shop_id), # 申请人记为店铺ID
Identity=1, # 被处罚人身份:1 打手
Reason=chufaliyou,
FineAmount=fakuanjine,
RelatedOrderID=dingdan_id,
Status=1, # 待缴纳
AffectsGrabbing=yingxiang_qiangdan,
ApplicantIdentity=4, # 申请人身份:4 店铺管理员
ClubID=resolve_penalty_club_id(
order=order,
request=request,
user=request.user,
applicant_id=str(shop_id),
),
)
from users.fadan_fenhong_utils import lock_penalty_bonus
lock_penalty_bonus(penalty)
logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}")
return Response({'code': 200, 'msg': '罚款已生成'})
except Exception as e:
logger.error(f"店铺罚款申请异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '服务器内部错误'})
-13458
View File
File diff suppressed because it is too large. Load diff
+128
View File
@@ -0,0 +1,128 @@
"""users.views package aggregate entry - re-export all submodule symbols.
Preserves external `from users.views import X` for users/urls.py and jituan/urls.py.
"""
from .auth import (
DashouZhuceView,
GuanshiRegisterView,
GuanshiYaoqingDashouListView,
WechatLoginAndDashouRegisterView,
WechatLoginAndGuanshiRegisterView,
WechatMiniProgramLoginView,
WeixinOfficialCallbackView,
ZuzhangXinxiView,
ZuzhangZhuceView,
)
from .user_info import (
BossBiaoshiView,
DashouGengxinView,
DashouJianquanView,
DashouXinxiAPIView,
DashouZiliaoHuoquView,
GuanshiXinxiView,
HuoQuYaoQingRenView,
LaobanJianquanView,
ShangJiaXinXiView,
ShangjiaNichengGengxinView,
UserInfoUpdateView,
YaoqingmaView,
ZaixianZhuangtaiAPIView,
huoquKehuduanIP,
huoquYonghuDaili,
)
from .shangjia import (
DaShouFaKuanLieBiaoView,
FaKuanJiFenTongJiView,
FaKuanShenSuView,
ShangjiaCufaTongjiView,
ShangjiaFakuanLieBiaoView,
ShangjiaZhuceView,
)
from .tixian import (
FORBIDDEN_STATUS_FOR_WITHDRAW,
GuanshiContactView,
ORDER_STATUS_COMPLETED,
ORDER_STATUS_FAIL,
ORDER_STATUS_REFUNDED,
ShoukuanXinxiShangchuanView,
TixianAssetView,
TixianCallbackV3View,
TixianJiluHuoquViewV2,
TixianQueRenAutoView,
TixianShenqingV3View,
TixianShenqingView,
TixianXinxiHuoquView,
YonghuTixianShenheXiugaiView,
generate_tixian_id,
)
from .admin import (
AdGetOrderList,
AdGetOrderTypes,
AdGuanLiYongHu,
AdKfglView,
AdKftjView,
AdKfxgView,
AdTongYiChuFa,
AdYaoQingDaShou,
AdcjxgView,
AdckyhxqView,
AddtxshView,
AdminFinancialDataView,
AdminLoginView,
AdtixianqkView,
CfGuanLi,
PaihangbangGuanliQueryView,
PaihangbangRiqiliebiaoView,
admin_required,
)
from .chufa import (
ChufaJiluHuoquView,
DashouPermission,
DashouShensuView,
ShangjiaChufaJiluHuoquView,
)
from .kefu import (
KefuCancelDesignationView,
KefuChangeDashouView,
KefuForceCompleteView,
KefuGetDashouDetailView,
KefuGetDashouListView,
KefuGetOrderDetailView,
KefuGetOrderListView,
KefuGetOrderTypesView,
KefuGetShangjiaOrderListView,
KefuLoginView,
KefuMerchantRefundView,
KefuPlatformRefundView,
KefuPunishView,
KefuPunishmentActionView,
KefuPunishmentDetailView,
KefuPunishmentListView,
KefuRecoverOrderView,
KefuRejectRefundView,
KefuRejectSettlementView,
KefuStatsView,
KefuTransferHallView,
KefuUpdateDashouView,
KefuWithdrawActionView,
KefuWithdrawDetailView,
KefuWithdrawListView,
)
from .pay import (
FaKuanHuitiaoView,
FaKuanPayFailView,
FaKuanPayPollView,
FaKuanPayView,
KaohePayCallbackView,
KaohePayFailView,
KaohePayPollView,
KaohePayView,
)
+2890
View File
File diff suppressed because it is too large. Load diff
+1936
View File
File diff suppressed because it is too large. Load diff
+830
View File
@@ -0,0 +1,830 @@
"""users.views.chufa - auto-generated by split script."""
import os
import sys
import json
import re
import time
import uuid
import random
import string
import requests
import hashlib
import traceback
import decimal
import jwt
import ipaddress
import xmltodict
from datetime import datetime, timedelta
from decimal import Decimal
import defusedxml.ElementTree as ET
from django.conf import settings
from django.db import models, transaction, IntegrityError, DatabaseError, connection
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
from django.core.cache import cache
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.http import HttpResponse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.hashers import make_password
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.authentication import JWTAuthentication
from gvsdsdk.fluent import db, func, FQ
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import yuan_to_fen
from utils.fadan_utils import check_fadan_qiangdan_eligible
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from users.fadan_fenhong_utils import process_fadan_fenhong
from orders.utils import (
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from backend.utils import (
update_dashou_daily_by_action, update_guanshi_daily_by_action,
update_shangjia_daily, update_zuzhang_daily_by_action,
verify_kefu_permission
)
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
UserBoss, UserDashou, UserShangjia, UserGuanshi,
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
RankingRecord
)
from users.business_models import User
from ..tixian_shenhe_services import (
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
mark_transfer_success, release_collect_quota_for_record,
close_audit_wechat_failed, query_wechat_transfer_bill,
check_shijidaozhang_within_limit,
check_dashou_trial_blocks_commission_withdraw,
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
)
from ..tixian_shenhe_views import process_audit_collect
from orders.models import (
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
Penalty, PenaltyAppealImage
)
from products.models import (
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
)
from config.models import Qunpeizhi
from backend.models import MerchantDailyStats
from rank.models import (
KaohePayTemp, Chenghao, ShenheJilu,
KaoheCishuFeiyong, YonghuChenghao
)
from users.models import UserShenheguan
from products.utils import update_shangpin_daily_stat
from shop.utils import update_dianpu_daily_stat
from rank.utils import create_shenhe_jilu_from_temp
import logging
logger = logging.getLogger(__name__)
class DashouPermission:
"""
打手权限验证类
确保用户是打手且账号状态正常
"""
def has_permission(self, request, view):
user = request.user
# 检查用户是否已认证
if not user.is_authenticated:
return False
try:
# 通过反向关系获取打手扩展信息
dashou_profile = user.DashouProfile
# 检查账号状态是否为1(正常)
if dashou_profile.zhanghaozhuangtai != 1:
return False
return True
except UserDashou.DoesNotExist:
return False
except Exception as e:
logger.error(f"打手权限验证异常: {e}")
return False
class ChufaJiluHuoquView(APIView):
"""
打手获取处罚记录接口
路径: /yonghu/dshqcfjl
方法: POST
权限: JWT Token + 打手权限验证
"""
permission_classes = [IsAuthenticated, DashouPermission]
def post(self, request):
try:
# 获取当前用户信息
user_main = request.user
yonghuid = user_main.UserUID
# 通过反向关系获取打手扩展信息
try:
dashou_profile = user_main.DashouProfile
# 再次确认账号状态
if dashou_profile.zhanghaozhuangtai != 1:
return Response({
'code': 3,
'msg': '账号已被封禁,无法查看处罚记录',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
except UserDashou.DoesNotExist:
return Response({
'code': 2,
'msg': '用户不是打手身份',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 判断请求类型:统计信息 or 记录列表
qingqiu_tongji = request.data.get('qingqiu_tongji', False)
if qingqiu_tongji:
# 返回统计信息
return self.get_tongji_info(yonghuid)
else:
# 返回处罚记录列表
return self.get_chufa_list(yonghuid, request.data)
except Exception as e:
logger.error(f"获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
return Response({
'code': 99,
'msg': '系统繁忙,请稍后重试',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_tongji_info(self, yonghuid):
"""
获取统计信息(总记录、待处理、已处理)
"""
try:
# 使用单个查询获取所有统计信息
stats = PenaltyRecord.query.filter(PlayerID=yonghuid).aggregate(
# 总记录数
zongshu=Count('id'),
# 待处理数:状态为0(待处罚)和3(申诉中)
daichuli=Count(
Case(
When(Q(ApplyStatus=0) | Q(ApplyStatus=3), then=1),
output_field=IntegerField()
)
),
# 已处理数:状态为1(已处罚)和2(已驳回)
yichuli=Count(
Case(
When(Q(ApplyStatus=1) | Q(ApplyStatus=2), then=1),
output_field=IntegerField()
)
)
)
# 如果查询结果为None,则设置为0
tongji_data = {
'zongshu': stats.get('zongshu', 0) or 0,
'daichuli': stats.get('daichuli', 0) or 0,
'yichuli': stats.get('yichuli', 0) or 0
}
return Response({
'code': 0,
'msg': '获取统计信息成功',
'data': tongji_data
})
except Exception as e:
logger.error(f"获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}")
return Response({
'code': 1,
'msg': '获取统计信息失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_chufa_list(self, yonghuid, request_data):
"""
获取处罚记录列表(高性能批量查询)
🔴 只修改图片查询逻辑,字段保持原样
"""
try:
# 获取分页参数
page = int(request_data.get('page', 1))
page_size = int(request_data.get('page_size', 10))
# 限制最大每页数量,防止恶意请求
if page_size > 50:
page_size = 50
if page_size < 1:
page_size = 10
# 获取状态筛选参数
zhuangtai = request_data.get('zhuangtai', 0)
# 🔴 构建查询条件 - 打手端根据PlayerID查询
queryset = PenaltyRecord.query.filter(PlayerID=yonghuid)
if zhuangtai == 0:
# 待处理:状态为0(待处罚)和3(申诉中)
queryset = queryset.filter(Q(ApplyStatus=0) | Q(ApplyStatus=3))
elif zhuangtai == 1:
# 已处理:状态为1(已处罚)和2(已驳回)
queryset = queryset.filter(Q(ApplyStatus=1) | Q(ApplyStatus=2))
else:
# 默认返回所有状态
pass
# 按创建时间倒序排列(最新在前)
queryset = queryset.order_by('-CreateTime')
# 使用游标分页,避免传统分页的性能问题
paginator = Paginator(queryset, page_size)
try:
page_obj = paginator.page(page)
except:
# 页码超出范围,返回空列表
return Response({
'code': 0,
'msg': '获取成功',
'data': {
'list': [],
'has_more': False,
'page': page,
'page_size': page_size,
'current_count': 0
}
})
# 获取当前页的记录
chufa_records = list(page_obj)
if not chufa_records:
# 没有数据直接返回
return Response({
'code': 0,
'msg': '获取成功',
'data': {
'list': [],
'has_more': False,
'page': page,
'page_size': page_size,
'current_count': 0
}
})
# 🔴【核心修复】收集所有需要查询图片的组合
dingdan_ids = []
shangjia_ids = []
for record in chufa_records:
if record.OrderID:
dingdan_ids.append(record.OrderID)
if record.ApplicantID: # 商家ID
shangjia_ids.append(record.ApplicantID)
# 去重
dingdan_ids = list(set(dingdan_ids))
shangjia_ids = list(set(shangjia_ids))
# 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误)
# 1. 查询证据图片(商家上传的)
zhengju_mapping = {}
if dingdan_ids and shangjia_ids:
# 构建Q对象:OrderID在列表中 AND UserID在商家ID列表中
zhengju_q = Q(OrderID__in=dingdan_ids, UserID__in=shangjia_ids)
zhengju_queryset = PenaltyEvidenceImage.query.filter(zhengju_q).values('OrderID', 'UserID', 'ImageURL')
for item in zhengju_queryset:
key = f"{item['OrderID']}_{item['UserID']}"
if key not in zhengju_mapping:
zhengju_mapping[key] = []
if item['ImageURL']:
zhengju_mapping[key].append(item['ImageURL'])
# 2. 查询申诉图片(打手上传的)
shensu_mapping = {}
if dingdan_ids:
# 构建Q对象:OrderID在列表中 AND UserID=当前打手ID
shensu_q = Q(OrderID__in=dingdan_ids, UserID=yonghuid)
shensu_queryset = PenaltyEvidenceImage.query.filter(shensu_q).values('OrderID', 'UserID', 'ImageURL')
for item in shensu_queryset:
key = f"{item['OrderID']}_{item['UserID']}"
if key not in shensu_mapping:
shensu_mapping[key] = []
if item['ImageURL']:
shensu_mapping[key].append(item['ImageURL'])
# 🔴【核心修复】组装返回数据 - 字段保持原样!
chufa_list = []
for record in chufa_records:
dingdan_id = record.OrderID
shangjia_id = record.ApplicantID
# 获取证据图片(商家上传的)
zhengju_tupian = []
if dingdan_id and shangjia_id:
key = f"{dingdan_id}_{shangjia_id}"
zhengju_tupian = zhengju_mapping.get(key, [])
# 获取申诉图片(打手上传的)
shensu_tupian = []
if dingdan_id:
key = f"{dingdan_id}_{yonghuid}"
shensu_tupian = shensu_mapping.get(key, [])
# 🔴【字段保持原样!】与前端完全对应!
chufa_data = {
'id': record.id,
'dingdan_id': record.OrderID or '',
'qingqiuid': record.ApplicantID or '', # 商家ID
'cfliyou': record.PenaltyReason or '',
'sqzhuangtai': record.ApplyStatus or 0,
'ssliyou': record.AppealReason or '',
'jifen': record.DeductedPoints or 0,
'CreateTime': record.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if record.CreateTime else '',
'UpdateTime': record.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if record.UpdateTime else '',
'zhengju_tupian': zhengju_tupian, # 商家上传的证据图片
'shensu_tupian': shensu_tupian, # 打手上传的申诉图片
}
chufa_list.append(chufa_data)
# 判断是否还有更多数据
has_more = page_obj.has_next()
# 构建响应数据
response_data = {
'list': chufa_list,
'has_more': has_more,
'page': page,
'page_size': page_size,
'current_count': len(chufa_list)
}
return Response({
'code': 0,
'msg': '获取成功',
'data': response_data
})
except ValueError as e:
logger.error(f"参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}")
return Response({
'code': 4,
'msg': '参数格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
return Response({
'code': 5,
'msg': '获取记录列表失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class DashouShensuView(APIView):
"""
打手申诉接口
路径: /yonghu/dscfss
方法: POST
权限: JWT Token + 打手权限验证
功能: 打手对处罚进行申诉
"""
permission_classes = [IsAuthenticated, DashouPermission]
def post(self, request):
try:
# 获取当前用户信息
user_main = request.user
yonghuid = user_main.UserUID
# 🔴🆕【关键】验证打手账号状态
try:
dashou_profile = user_main.DashouProfile
if dashou_profile.zhanghaozhuangtai != 1:
return Response({
'code': 3,
'msg': '账号已被封禁,无法申诉',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
except UserDashou.DoesNotExist:
return Response({
'code': 2,
'msg': '用户不是打手身份',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 🔴🆕【关键】获取请求参数
chufa_id = request.data.get('chufa_id') # 处罚记录ID
shensu_liyou = request.data.get('shensu_liyou', '').strip() # 申诉理由
shensu_tupian_urls = request.data.get('shensu_tupian_urls', []) # 申诉图片URL数组
# 验证必填字段
if not chufa_id:
return Response({
'code': 6,
'msg': '处罚记录ID不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if not shensu_liyou:
return Response({
'code': 7,
'msg': '申诉理由不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 验证申诉理由长度
if len(shensu_liyou) > 500:
return Response({
'code': 8,
'msg': '申诉理由不能超过500字',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 验证图片数量
if len(shensu_tupian_urls) > 9:
return Response({
'code': 9,
'msg': '最多只能上传9张图片',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 🔴🆕【关键】使用事务确保数据一致性
with transaction.atomic():
# 1. 查询处罚记录
try:
chufa_record = PenaltyRecord.objects.select_for_update().get(
id=chufa_id,
PlayerID=yonghuid # 确保只能申诉自己的处罚记录
)
except PenaltyRecord.DoesNotExist:
return Response({
'code': 10,
'msg': '处罚记录不存在或无权申诉',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 2. 验证处罚记录状态(必须是待处理状态0)
if chufa_record.ApplyStatus != 0:
return Response({
'code': 11,
'msg': '该处罚记录已处理,无法申诉',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 验证订单是否存在(可选,根据需求决定)
# 这里可以根据OrderID去查询订单表,但根据需求描述,我们已经有了处罚记录
# 处罚记录中包含了OrderID,如果OrderID不存在,说明订单可能被删除
# 但根据业务逻辑,有处罚记录就应该有订单,所以这里可以跳过这个检查
# 4. 检查是否已有申诉记录(防止重复申诉)
if chufa_record.AppealReason or chufa_record.ApplyStatus == 3:
# 检查是否已经有申诉图片
existing_shensu_tupian = PenaltyEvidenceImage.query.filter(
OrderID=chufa_record.OrderID,
UserID=yonghuid
).exists()
if existing_shensu_tupian:
return Response({
'code': 12,
'msg': '您已经提交过申诉,请等待处理结果',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 更新处罚记录
chufa_record.AppealReason = shensu_liyou
chufa_record.ApplyStatus = 3 # 状态改为申诉中
chufa_record.save()
# 6. 保存申诉图片(如果有)
saved_tupian_urls = []
if shensu_tupian_urls:
for tupian_url in shensu_tupian_urls:
if tupian_url: # 确保URL不为空
chufa_tupian = PenaltyEvidenceImage.query.create(
OrderID=chufa_record.OrderID,
UserID=yonghuid,
ImageURL=tupian_url
)
saved_tupian_urls.append(tupian_url)
# 7. 构建返回数据
response_data = {
'chufa_id': chufa_record.id,
'dingdan_id': chufa_record.OrderID or '',
'shensu_liyou': shensu_liyou,
'shensu_tupian_urls': saved_tupian_urls,
'sqzhuangtai': chufa_record.ApplyStatus,
'UpdateTime': chufa_record.UpdateTime.strftime(
'%Y-%m-%d %H:%M:%S') if chufa_record.UpdateTime else ''
}
# 记录操作日志
logger.info(f"打手申诉成功 - 用户ID: {yonghuid}, 处罚ID: {chufa_id}, 订单ID: {chufa_record.OrderID}")
return Response({
'code': 0,
'msg': '申诉提交成功',
'data': response_data
})
except Exception as e:
logger.error(f"打手申诉异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
return Response({
'code': 99,
'msg': '系统繁忙,请稍后重试',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ShangjiaChufaJiluHuoquView(APIView):
"""
商家获取处罚记录接口
路径: /yonghu/sjcfjlhq
方法: POST
权限: JWT Token + 商家权限验证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 获取当前用户信息
user_main = request.user
yonghuid = user_main.UserUID
# 通过反向关系获取商家扩展信息
try:
shangjia_profile = user_main.ShopProfile
# 再次确认账号状态
if shangjia_profile.zhuangtai != 1:
return Response({
'code': 3,
'msg': '商家账号状态异常,无法查看处罚记录',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
except UserShangjia.DoesNotExist:
return Response({
'code': 2,
'msg': '用户不是商家身份',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 判断请求类型
qingqiu_tongji = request.data.get('qingqiu_tongji', False)
if qingqiu_tongji:
return self.get_tongji_info(yonghuid)
else:
return self.get_chufa_list(yonghuid, request.data)
except Exception as e:
logger.error(f"商家获取处罚记录异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
return Response({
'code': 99,
'msg': '系统繁忙,请稍后重试',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_tongji_info(self, yonghuid):
"""
获取统计信息
"""
try:
# 🔴 查询条件:ApplicantID = 当前商家ID
stats = PenaltyRecord.query.filter(ApplicantID=yonghuid).aggregate(
zongshu=Count('id'),
daichuli=Count(
Case(
When(Q(ApplyStatus=0) | Q(ApplyStatus=3), then=1),
output_field=IntegerField()
)
),
yichuli=Count(
Case(
When(Q(ApplyStatus=1) | Q(ApplyStatus=2), then=1),
output_field=IntegerField()
)
)
)
tongji_data = {
'zongshu': stats.get('zongshu', 0) or 0,
'daichuli': stats.get('daichuli', 0) or 0,
'yichuli': stats.get('yichuli', 0) or 0
}
return Response({
'code': 0,
'msg': '获取统计信息成功',
'data': tongji_data
})
except Exception as e:
logger.error(f"商家获取统计信息异常 - 用户ID: {yonghuid}, 错误: {str(e)}")
return Response({
'code': 1,
'msg': '获取统计信息失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_chufa_list(self, yonghuid, request_data):
"""
获取处罚记录列表(商家端)
🔴 只修改图片查询逻辑,字段保持原样
"""
try:
# 获取分页参数
page = int(request_data.get('page', 1))
page_size = int(request_data.get('page_size', 10))
if page_size > 50:
page_size = 50
if page_size < 1:
page_size = 10
# 获取状态筛选参数
zhuangtai = request_data.get('zhuangtai', 0)
# 🔴 查询条件:ApplicantID = 当前商家ID
queryset = PenaltyRecord.query.filter(ApplicantID=yonghuid)
if zhuangtai == 0:
queryset = queryset.filter(Q(ApplyStatus=0) | Q(ApplyStatus=3))
elif zhuangtai == 1:
queryset = queryset.filter(Q(ApplyStatus=1) | Q(ApplyStatus=2))
else:
pass
# 按创建时间倒序排列
queryset = queryset.order_by('-CreateTime')
# 分页逻辑
paginator = Paginator(queryset, page_size)
try:
page_obj = paginator.page(page)
except:
return Response({
'code': 0,
'msg': '获取成功',
'data': {
'list': [],
'has_more': False,
'page': page,
'page_size': page_size,
'current_count': 0
}
})
chufa_records = list(page_obj)
if not chufa_records:
return Response({
'code': 0,
'msg': '获取成功',
'data': {
'list': [],
'has_more': False,
'page': page,
'page_size': page_size,
'current_count': 0
}
})
# 🔴【核心修复】收集所有需要查询图片的组合
dingdan_ids = []
dashou_ids = []
for record in chufa_records:
if record.OrderID:
dingdan_ids.append(record.OrderID)
if record.PlayerID: # 打手ID
dashou_ids.append(record.PlayerID)
# 去重
dingdan_ids = list(set(dingdan_ids))
dashou_ids = list(set(dashou_ids))
# 🔴【核心修复】分别查询两种图片(避免if-elif逻辑错误)
# 1. 查询证据图片(商家自己上传的)
zhengju_mapping = {}
if dingdan_ids:
# 构建Q对象:OrderID在列表中 AND UserID=当前商家ID
zhengju_q = Q(OrderID__in=dingdan_ids, UserID=yonghuid)
zhengju_queryset = PenaltyEvidenceImage.query.filter(zhengju_q).values('OrderID', 'UserID', 'ImageURL')
for item in zhengju_queryset:
key = f"{item['OrderID']}_{item['UserID']}"
if key not in zhengju_mapping:
zhengju_mapping[key] = []
if item['ImageURL']:
zhengju_mapping[key].append(item['ImageURL'])
# 2. 查询申诉图片(打手上传的)
shensu_mapping = {}
if dingdan_ids and dashou_ids:
# 构建Q对象:OrderID在列表中 AND UserID在打手ID列表中
shensu_q = Q(OrderID__in=dingdan_ids, UserID__in=dashou_ids)
shensu_queryset = PenaltyEvidenceImage.query.filter(shensu_q).values('OrderID', 'UserID', 'ImageURL')
for item in shensu_queryset:
key = f"{item['OrderID']}_{item['UserID']}"
if key not in shensu_mapping:
shensu_mapping[key] = []
if item['ImageURL']:
shensu_mapping[key].append(item['ImageURL'])
# 🔴【核心修复】组装返回数据 - 字段保持原样!
chufa_list = []
for record in chufa_records:
dingdan_id = record.OrderID
dashou_id = record.PlayerID
# 获取证据图片(商家自己上传的)
zhengju_tupian = []
if dingdan_id:
key = f"{dingdan_id}_{yonghuid}"
zhengju_tupian = zhengju_mapping.get(key, [])
# 获取申诉图片(打手上传的)
shensu_tupian = []
if dingdan_id and dashou_id:
key = f"{dingdan_id}_{dashou_id}"
shensu_tupian = shensu_mapping.get(key, [])
# 🔴【字段保持原样!】与前端完全对应!
chufa_data = {
'id': record.id,
'dingdan_id': record.OrderID or '',
'qingqiuid': record.PlayerID or '', # 打手ID
'cfliyou': record.PenaltyReason or '',
'sqzhuangtai': record.ApplyStatus or 0,
'ssliyou': record.AppealReason or '',
'jifen': record.DeductedPoints or 0,
'CreateTime': record.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if record.CreateTime else '',
'UpdateTime': record.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if record.UpdateTime else '',
'zhengju_tupian': zhengju_tupian, # 商家自己上传的证据图片
'shensu_tupian': shensu_tupian, # 打手上传的申诉图片
}
chufa_list.append(chufa_data)
# 判断是否还有更多数据
has_more = page_obj.has_next()
response_data = {
'list': chufa_list,
'has_more': has_more,
'page': page,
'page_size': page_size,
'current_count': len(chufa_list)
}
return Response({
'code': 0,
'msg': '获取成功',
'data': response_data
})
except ValueError as e:
logger.error(f"商家端参数错误 - 用户ID: {yonghuid}, 错误: {str(e)}")
return Response({
'code': 4,
'msg': '参数格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f"商家获取处罚记录列表异常 - 用户ID: {yonghuid}, 错误: {str(e)}", exc_info=True)
return Response({
'code': 5,
'msg': '获取记录列表失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+3794
View File
File diff suppressed because it is too large. Load diff
+1073
View File
File diff suppressed because it is too large. Load diff
+734
View File
@@ -0,0 +1,734 @@
"""users.views.shangjia - auto-generated by split script."""
import os
import sys
import json
import re
import time
import uuid
import random
import string
import requests
import hashlib
import traceback
import decimal
import jwt
import ipaddress
import xmltodict
from datetime import datetime, timedelta
from decimal import Decimal
import defusedxml.ElementTree as ET
from django.conf import settings
from django.db import models, transaction, IntegrityError, DatabaseError, connection
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
from django.core.cache import cache
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.http import HttpResponse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.hashers import make_password
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.authentication import JWTAuthentication
from gvsdsdk.fluent import db, func, FQ
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import yuan_to_fen
from utils.fadan_utils import check_fadan_qiangdan_eligible
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from users.fadan_fenhong_utils import process_fadan_fenhong
from orders.utils import (
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from backend.utils import (
update_dashou_daily_by_action, update_guanshi_daily_by_action,
update_shangjia_daily, update_zuzhang_daily_by_action,
verify_kefu_permission
)
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
UserBoss, UserDashou, UserShangjia, UserGuanshi,
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
RankingRecord
)
from users.business_models import User
from ..tixian_shenhe_services import (
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
mark_transfer_success, release_collect_quota_for_record,
close_audit_wechat_failed, query_wechat_transfer_bill,
check_shijidaozhang_within_limit,
check_dashou_trial_blocks_commission_withdraw,
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
)
from ..tixian_shenhe_views import process_audit_collect
from .chufa import DashouPermission
from orders.models import (
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
Penalty, PenaltyAppealImage
)
from products.models import (
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
)
from config.models import Qunpeizhi
from backend.models import MerchantDailyStats
from rank.models import (
KaohePayTemp, Chenghao, ShenheJilu,
KaoheCishuFeiyong, YonghuChenghao
)
from users.models import UserShenheguan
from products.utils import update_shangpin_daily_stat
from shop.utils import update_dianpu_daily_stat
from rank.utils import create_shenhe_jilu_from_temp
import logging
logger = logging.getLogger(__name__)
class ShangjiaZhuceView(APIView):
"""
商家注册接口 (通过管理员邀请码)
访问路径: /api/yonghu/shangjiahuce/
请求方法: POST
权限要求: JWT Token认证通过的用户
请求体: { "inviteCode": "管理员邀请码" }
返回字段说明:
- code: 状态码(200成功,其他失败)
- message: 提示信息
- data: 商家信息数据,包含以下字段(与前端商家页面完全一致):
- nicheng: 商家昵称
- sjyue: 商家余额(字符串,保留两位小数)
- fabu: 发布订单总数
- chengjiao: 成交订单总数
- tuikuan: 退款订单总数
- jinridingdan: 今日订单
- jinriliushui: 今日流水(字符串)
- jinyuedingdan: 今月订单
- jinyueliushui: 今月流水(字符串)
- zhuangtai: 商家状态(1正常)
- dianhua: 商家电话
- wechat: 商家微信
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 获取并验证邀请码参数
yaoqingma = request.data.get('inviteCode')
if not yaoqingma:
return Response({
'code': 400,
'message': '邀请码不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
yaoqingma = yaoqingma.strip()
if len(yaoqingma) > 100:
return Response({
'code': 400,
'message': '邀请码长度超过限制',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
current_user = request.user
# 2. 检查用户是否已是商家
try:
shangjia_profile = current_user.ShopProfile
# 用户已是商家,直接返回现有信息
return self.fanhuiXianyouShangjiaXinxi(shangjia_profile, current_user)
except UserShangjia.DoesNotExist:
# 用户不是商家,继续注册流程
pass
# 2.5 子客服身份互斥:有效客服不可认证商家
from merchant_ops.services.authz import assert_no_active_staff_when_merchant_register
try:
assert_no_active_staff_when_merchant_register(current_user)
except Exception as e:
from merchant_ops.services.authz import StaffAuthError
if isinstance(e, StaffAuthError):
return Response({'code': 403, 'message': e.msg, 'data': None}, status=status.HTTP_403_FORBIDDEN)
raise
# 3. 根据邀请码查找对应的管理员
try:
# 从管理员表查找邀请码
admin_profile = AdminProfile.query.select_related('user').get(yaoqingma=yaoqingma)
except AdminProfile.DoesNotExist:
return Response({
'code': 404,
'message': '管理员邀请码无效或不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 4. 核心:在数据库事务中创建商家身份
try:
with transaction.atomic():
# 4.1 查询老板扩展表获取老板昵称
boss_nickname = '普通商家'
try:
boss_profile = current_user.BossProfile
if boss_profile.nickname and boss_profile.nickname.strip():
boss_nickname = boss_profile.nickname.strip()
except UserBoss.DoesNotExist:
# 老板扩展表不存在,使用默认值
pass
# 4.2 创建商家扩展表
# 使用老板昵称作为商家昵称的基础
shangjia_nicheng = boss_nickname
# 检查昵称是否已存在(安全考虑)
if UserShangjia.query.filter(nicheng=shangjia_nicheng).exists():
# 如果存在,添加随机数确保唯一
shangjia_nicheng = f"{boss_nickname}{random.randint(1000, 9999)}"
# 4.3 创建商家记录
shop_profile = UserShangjia.query.create(
user=current_user,
nicheng=shangjia_nicheng,
zhuangtai=1, # 正常状态
yue=0.00, # 商家余额
fabu=0, # 发布订单总数
tuikuan=0, # 退款订单总数
chengjiao=0, # 成交订单总数
jinridingdan=0, # 今日订单
jinriliushui=0.00, # 今日流水
jinyuedingdan=0, # 今月订单
jinyueliushui=0.00, # 今月流水
dianhua='', # 电话
wechat='', # 微信
)
except Exception as e:
# 事务会自动回滚
error_detail = traceback.format_exc()
#print(f"商家注册错误详情: {error_detail}")
return Response({
'code': 500,
'message': f'商家注册过程中发生系统错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 5. 注册成功,返回商家信息
fanhui_data = self.zhuangbeiFanhuiShuju(shop_profile, current_user)
return Response({
'code': 200,
'message': '商家注册成功!',
'data': fanhui_data
})
def fanhuiXianyouShangjiaXinxi(self, shangjia_profile, current_user):
"""处理已是商家的用户,返回其现有信息"""
fanhui_data = self.zhuangbeiFanhuiShuju(shangjia_profile, current_user)
return Response({
'code': 200,
'message': '您已是商家,返回当前信息。',
'data': fanhui_data
})
def zhuangbeiFanhuiShuju(self, shangjia_profile, current_user):
"""
准备返回给前端的商家数据
字段名必须与前端商家页面完全一致
"""
data = {
# 🟢 商家基本信息
'nicheng': shangjia_profile.nicheng, # 商家昵称
'sjyue': format(shangjia_profile.yue, '.2f'), # 商家余额,保留两位小数
'fabu': shangjia_profile.fabu, # 发布订单总数
'chengjiao': shangjia_profile.chengjiao, # 成交订单总数
'tuikuan': shangjia_profile.tuikuan, # 退款订单总数
'jinridingdan': shangjia_profile.jinridingdan, # 今日订单
'jinriliushui': format(shangjia_profile.jinriliushui, '.2f'), # 今日流水
'jinyuedingdan': shangjia_profile.jinyuedingdan, # 今月订单
'jinyueliushui': format(shangjia_profile.jinyueliushui, '.2f'), # 今月流水
'zhuangtai': shangjia_profile.zhuangtai, # 商家状态
'dianhua': shangjia_profile.dianhua or '', # 电话
'wechat': shangjia_profile.wechat or '', # 微信
# 🟢 可能需要的主表信息
'uid': current_user.UserUID, # 用户ID
}
return data
class FaKuanJiFenTongJiView(APIView):
"""
获取罚款与积分处罚统计总览
路径: POST /yonghu/cffktjhq
"""
permission_classes = [IsAuthenticated, DashouPermission]
def post(self, request):
try:
user_main = request.user
yonghuid = user_main.UserUID
# ================= 罚款统计 =================
fadan_stats = Penalty.query.filter(PenalizedUserID=yonghuid).aggregate(
total=Count('id'),
daijiaona=Count('id', filter=Q(Status=1)),
shensuzhong=Count('id', filter=Q(Status=3)),
pingtai_shenhe=Count('id', filter=Q(Status=5)),
yijiaona=Count('id', filter=Q(Status=2)),
yibohui=Count('id', filter=Q(Status=4)),
)
# ================= 积分处罚统计 =================
jifen_stats = PenaltyRecord.query.filter(PlayerID=yonghuid).aggregate(
total=Count('id'),
daichuli=Count('id', filter=Q(ApplyStatus=0) | Q(ApplyStatus=3)),
yichuli=Count('id', filter=Q(ApplyStatus=1) | Q(ApplyStatus=2)),
# 细分
daichuli_0=Count('id', filter=Q(ApplyStatus=0)),
shensuzhong_3=Count('id', filter=Q(ApplyStatus=3)),
yichufa_1=Count('id', filter=Q(ApplyStatus=1)),
yibohui_2=Count('id', filter=Q(ApplyStatus=2)),
)
data = {
"fakuan": {
"total": fadan_stats['total'],
"daijiaona": fadan_stats['daijiaona'],
"shensuzhong": fadan_stats['shensuzhong'],
"pingtai_shenhe": fadan_stats['pingtai_shenhe'],
"yijiaona": fadan_stats['yijiaona'],
"yibohui": fadan_stats['yibohui'],
},
"jifen": {
"total": jifen_stats['total'],
"daichuli": jifen_stats['daichuli'],
"yichuli": jifen_stats['yichuli'],
"daichuli_0": jifen_stats['daichuli_0'],
"shensuzhong_3": jifen_stats['shensuzhong_3'],
"yichufa_1": jifen_stats['yichufa_1'],
"yibohui_2": jifen_stats['yibohui_2'],
}
}
return Response({'code': 0, 'msg': '获取统计成功', 'data': data})
except Exception as e:
logger.error(f"获取统计异常 {yonghuid}: {e}", exc_info=True)
return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
class DaShouFaKuanLieBiaoView(APIView):
"""
打手罚款列表接口
路径:POST /yonghu/dsfklbhq
参数:
page: 页码(默认1)
page_size: 每页条数(默认10,最大50)
zhuangtai: 状态筛选(1待缴纳/2已缴纳/3申诉中/4申诉成功,0或不传表示全部)
sousuo_dingdan_id: 订单号模糊搜索
返回:{
list: [{
id, beichufa_id, guanliandingdan_id, chufaliyou, fakuanjine, zhuangtai,
yingxiang_qiangdan, shensuliyou, bohuiliyou,
zhengju_tupian: [...], // 证据图片(yongtu=1)
shensu_tupian: [...], // 申诉图片(yongtu=2)
CreateTime, UpdateTime
}],
has_more, page, page_size
}
"""
permission_classes = [IsAuthenticated, DashouPermission]
def post(self, request):
try:
user_main = request.user
yonghuid = user_main.UserUID
# 获取分页参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
if page_size > 50:
page_size = 50
if page_size < 1:
page_size = 10
# 状态筛选
zhuangtai = request.data.get('zhuangtai')
# 订单号搜索
sousuo_dingdan_id = request.data.get('sousuo_dingdan_id', '').strip()
# 基础查询:当前用户的罚单
qs = Penalty.query.filter(PenalizedUserID=yonghuid)
# 状态筛选
if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4, 5]:
qs = qs.filter(Status=int(zhuangtai))
# 订单号模糊搜索
if sousuo_dingdan_id:
qs = qs.filter(RelatedOrderID__icontains=sousuo_dingdan_id)
# 按创建时间倒序
qs = qs.order_by('-CreateTime')
# 分页
paginator = Paginator(qs, page_size)
try:
page_obj = paginator.page(page)
except Exception:
return Response({
'code': 0, 'msg': '获取成功',
'data': {'list': [], 'has_more': False, 'page': page, 'page_size': page_size}
})
records = list(page_obj)
fadan_ids = [r.id for r in records]
# 批量获取图片,按用途分组
zhengju_map = {} # key: fadan_id, value: [url, ...]
shensu_map = {}
if fadan_ids:
tupians = PenaltyAppealImage.query.filter(
Penalty_id__in=fadan_ids
).values('Penalty_id', 'ImageURL', 'Purpose')
for t in tupians:
fid = t['Penalty_id']
url = t['ImageURL']
if t['Purpose'] == 1:
zhengju_map.setdefault(fid, []).append(url)
else:
shensu_map.setdefault(fid, []).append(url)
# 组装返回数据
chufa_list = []
for r in records:
rid = r.id
item = {
'id': rid,
'beichufa_id': r.PenalizedUserID,
'guanliandingdan_id': r.RelatedOrderID or '',
'chufaliyou': r.Reason or '',
'fakuanjine': str(r.FineAmount),
'zhuangtai': r.Status,
'yingxiang_qiangdan': r.AffectsGrabbing,
'shensuliyou': r.AppealReason or '',
'bohuiliyou': r.RejectReason or '',
'zhengju_tupian': zhengju_map.get(rid, []), # 证据图片
'shensu_tupian': shensu_map.get(rid, []), # 申诉图片
'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '',
}
chufa_list.append(item)
has_more = page_obj.has_next()
return Response({
'code': 0,
'msg': '获取成功',
'data': {
'list': chufa_list,
'has_more': has_more,
'page': page,
'page_size': page_size,
}
})
except Exception as e:
logger.error(f"获取罚款列表异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True)
return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
def _merchant_penalty_auth(user):
"""校验商家身份,返回 (yonghuid, None) 或 (None, error_response)。"""
try:
shangjia_profile = user.ShopProfile
if shangjia_profile.zhuangtai != 1:
return None, Response({
'code': 3, 'msg': '商家账号状态异常', 'data': None
}, status=status.HTTP_403_FORBIDDEN)
except UserShangjia.DoesNotExist:
return None, Response({
'code': 2, 'msg': '用户不是商家身份', 'data': None
}, status=status.HTTP_403_FORBIDDEN)
return user.UserUID, None
def _money_str(val):
if val is None:
return '0.00'
return str(val)
class ShangjiaCufaTongjiView(APIView):
"""
商家端罚款+积分处罚综合统计(含金额汇总)
路径: POST /yonghu/sjcftjhq
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghuid, err = _merchant_penalty_auth(request.user)
if err:
return err
fadan_agg = Penalty.query.filter(ApplicantID=yonghuid).aggregate(
total=Count('id'),
total_jine=Sum('FineAmount'),
daijiaona=Count('id', filter=Q(Status=1)),
daijiaona_jine=Sum('FineAmount', filter=Q(Status=1)),
yijiaona=Count('id', filter=Q(Status=2)),
yijiaona_jine=Sum('FineAmount', filter=Q(Status=2)),
shensuzhong=Count('id', filter=Q(Status=3)),
shensuzhong_jine=Sum('FineAmount', filter=Q(Status=3)),
pingtai_shenhe=Count('id', filter=Q(Status=5)),
pingtai_shenhe_jine=Sum('FineAmount', filter=Q(Status=5)),
yibohui=Count('id', filter=Q(Status=4)),
yibohui_jine=Sum('FineAmount', filter=Q(Status=4)),
shijidaozhang_jine=Sum(
'ApplicantBonusAmount',
filter=Q(Status=2, BonusCredited=True),
),
)
jifen_agg = PenaltyRecord.query.filter(ApplicantID=yonghuid).aggregate(
total=Count('id'),
total_jifen=Sum('DeductedPoints'),
daichuli=Count('id', filter=Q(ApplyStatus=0) | Q(ApplyStatus=3)),
daichuli_jifen=Sum(
'DeductedPoints',
filter=Q(ApplyStatus=0) | Q(ApplyStatus=3),
),
yichuli=Count('id', filter=Q(ApplyStatus=1) | Q(ApplyStatus=2)),
yichuli_jifen=Sum(
'DeductedPoints',
filter=Q(ApplyStatus=1) | Q(ApplyStatus=2),
),
daichuli_0=Count('id', filter=Q(ApplyStatus=0)),
shensuzhong_3=Count('id', filter=Q(ApplyStatus=3)),
yichufa_1=Count('id', filter=Q(ApplyStatus=1)),
yibohui_2=Count('id', filter=Q(ApplyStatus=2)),
)
data = {
'fakuan': {
'total': fadan_agg['total'] or 0,
'total_jine': _money_str(fadan_agg['total_jine']),
'daijiaona': fadan_agg['daijiaona'] or 0,
'daijiaona_jine': _money_str(fadan_agg['daijiaona_jine']),
'yijiaona': fadan_agg['yijiaona'] or 0,
'yijiaona_jine': _money_str(fadan_agg['yijiaona_jine']),
'shensuzhong': fadan_agg['shensuzhong'] or 0,
'shensuzhong_jine': _money_str(fadan_agg['shensuzhong_jine']),
'pingtai_shenhe': fadan_agg['pingtai_shenhe'] or 0,
'pingtai_shenhe_jine': _money_str(fadan_agg['pingtai_shenhe_jine']),
'yibohui': fadan_agg['yibohui'] or 0,
'yibohui_jine': _money_str(fadan_agg['yibohui_jine']),
'shijidaozhang_jine': _money_str(fadan_agg['shijidaozhang_jine']),
},
'jifen': {
'total': jifen_agg['total'] or 0,
'total_jifen': jifen_agg['total_jifen'] or 0,
'daichuli': jifen_agg['daichuli'] or 0,
'daichuli_jifen': jifen_agg['daichuli_jifen'] or 0,
'yichuli': jifen_agg['yichuli'] or 0,
'yichuli_jifen': jifen_agg['yichuli_jifen'] or 0,
'daichuli_0': jifen_agg['daichuli_0'] or 0,
'shensuzhong_3': jifen_agg['shensuzhong_3'] or 0,
'yichufa_1': jifen_agg['yichufa_1'] or 0,
'yibohui_2': jifen_agg['yibohui_2'] or 0,
},
}
return Response({'code': 0, 'msg': '获取统计成功', 'data': data})
except Exception as e:
logger.error(f"商家处罚统计异常: {e}", exc_info=True)
return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
class ShangjiaFakuanLieBiaoView(APIView):
"""
商家端金额罚单列表
路径: POST /yonghu/sjfklbhq
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghuid, err = _merchant_penalty_auth(request.user)
if err:
return err
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
if page_size > 50:
page_size = 50
if page_size < 1:
page_size = 10
zhuangtai = request.data.get('zhuangtai')
sousuo_dingdan_id = str(request.data.get('sousuo_dingdan_id', '')).strip()
qs = Penalty.query.filter(ApplicantID=yonghuid)
if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4, 5]:
qs = qs.filter(Status=int(zhuangtai))
if sousuo_dingdan_id:
qs = qs.filter(RelatedOrderID__icontains=sousuo_dingdan_id)
qs = qs.order_by('-CreateTime')
paginator = Paginator(qs, page_size)
try:
page_obj = paginator.page(page)
except Exception:
return Response({
'code': 0, 'msg': '获取成功',
'data': {'list': [], 'has_more': False, 'page': page, 'page_size': page_size},
})
records = list(page_obj)
fadan_ids = [r.id for r in records]
staff_ids = list({r.ShopStaffUserID for r in records if r.ShopStaffUserID})
staff_name_map = {}
if staff_ids:
from merchant_ops.models import MerchantStaffMember
for m in MerchantStaffMember.query.filter(
merchant_id=yonghuid, staff_user_id__in=staff_ids
):
staff_name_map[m.staff_user_id] = m.display_name or m.staff_user_id
zhengju_map = {}
shensu_map = {}
if fadan_ids:
tupians = PenaltyAppealImage.query.filter(Penalty_id__in=fadan_ids).values(
'id', 'Penalty_id', 'ImageURL', 'Purpose'
)
for t in tupians:
fid = t['Penalty_id']
img = {'id': t['id'], 'url': t['ImageURL']}
if t['Purpose'] == 1:
zhengju_map.setdefault(fid, []).append(img)
else:
shensu_map.setdefault(fid, []).append(img)
chufa_list = []
for r in records:
rid = r.id
chufa_list.append({
'id': rid,
'beichufa_id': r.PenalizedUserID,
'guanliandingdan_id': r.RelatedOrderID or '',
'chufaliyou': r.Reason or '',
'fakuanjine': str(r.FineAmount),
'zhuangtai': r.Status,
'yingxiang_qiangdan': r.AffectsGrabbing,
'shensuliyou': r.AppealReason or '',
'bohuiliyou': r.RejectReason or '',
'applicant_bonus_amount': str(r.ApplicantBonusAmount or 0),
'applicant_bonus_rate': str(r.ApplicantBonusRate or 0),
'bonus_credited': bool(r.BonusCredited),
'shop_staff_user_id': r.ShopStaffUserID or '',
'staff_display_name': staff_name_map.get(r.ShopStaffUserID or '', ''),
'zhengju_tupian': zhengju_map.get(rid, []),
'shensu_tupian': shensu_map.get(rid, []),
'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '',
})
return Response({
'code': 0,
'msg': '获取成功',
'data': {
'list': chufa_list,
'has_more': page_obj.has_next(),
'page': page,
'page_size': page_size,
},
})
except Exception as e:
logger.error(f"商家罚款列表异常: {e}", exc_info=True)
return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
class FaKuanShenSuView(APIView):
"""
罚款申诉提交/修改接口
路径:POST /yonghu/fkss
参数:
fadan_id: 罚单ID
shensu_liyou: 申诉理由
shensu_tupian_urls: 图片相对URL数组(可空数组,但至少要有理由)
逻辑:
- 仅待缴纳状态且无驳回理由的罚单可申诉
- 允许对已有申诉进行修改:先删除旧申诉图片(yongtu=2),再添加新图片
"""
permission_classes = [IsAuthenticated, DashouPermission]
def post(self, request):
try:
user_main = request.user
yonghuid = user_main.UserUID
fadan_id = request.data.get('fadan_id')
shensu_liyou = request.data.get('shensu_liyou', '').strip()
tupian_urls = request.data.get('shensu_tupian_urls', [])
if not fadan_id:
return Response({'code': 400, 'msg': '缺少罚款ID'}, status=400)
if not shensu_liyou:
return Response({'code': 400, 'msg': '请输入申诉理由'}, status=400)
# 查询罚单,确保属于当前打手
try:
fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=yonghuid)
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在或无权操作'}, status=404)
# 待缴纳(1)或平台审核中(5)可申诉;平台审核中仅保存申诉内容,不改变状态
from utils.penalty_status import (
PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT,
)
if fadan.Status not in (PENALTY_PENDING_PAY, PENALTY_PLATFORM_AUDIT):
return Response({'code': 400, 'msg': '罚单状态不允许申诉'}, status=400)
if fadan.Status == PENALTY_PENDING_PAY and fadan.RejectReason:
return Response({'code': 400, 'msg': '该罚单申诉已被驳回,不可再次申诉'}, status=400)
if not isinstance(tupian_urls, list):
return Response({'code': 400, 'msg': '申诉图片格式错误'}, status=400)
tupian_urls = [str(u).strip() for u in tupian_urls if str(u).strip()]
if len(tupian_urls) > 9:
return Response({'code': 400, 'msg': '申诉图片最多9张'}, status=400)
if fadan.Status == PENALTY_PENDING_PAY:
fadan.Status = PENALTY_APPEALING
fadan.AppealReason = shensu_liyou
fadan.save()
# 删除旧的申诉图片(yongtu=2),仅删除打手自己上传的申诉图
PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=2).delete()
# 插入新上传的申诉图片
for url in tupian_urls:
PenaltyAppealImage.query.create(
Penalty=fadan,
PenalizedUserID=yonghuid,
ImageURL=url,
Purpose=2
)
return Response({'code': 0, 'msg': '申诉提交成功'})
except Exception as e:
logger.error(f"罚款申诉异常 {yonghuid if 'yonghuid' in locals() else ''}: {e}", exc_info=True)
return Response({'code': 99, 'msg': '系统繁忙'}, status=500)
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff