进行了完整的视图拆分,同步了 merchant_ops 的修改
This commit is contained in:
1 parent
9d98ac9e83
commit
c5ae30507b
46 files changed
+32768
-188172
No files matched your search
@@ -1,2 +0,0 @@
|
|||||||
# 3. 在项目的 `__init__.py` 文件(与manage.py同级)中,添加两行代码
|
|
||||||
# 文件路径:a_long_dianjing/__init__.py
|
|
||||||
+57
-41
@@ -188,57 +188,73 @@ class KhgglView(APIView):
|
|||||||
end = start + page_size
|
end = start + page_size
|
||||||
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
|
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
|
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
|
||||||
|
|
||||||
# ---------- 预计算每个板块的打手数量 ----------
|
# ---------- 优化:板块打手数量(1 次 annotate 替代 2N 次循环查询)----------
|
||||||
bankuai_dashou_count_map = {}
|
# 原逻辑:每板块查 dashou 标签 ID,再统计关联的不同用户数(要求有 DashouProfile)
|
||||||
all_banquai = Bankuai.query.all()
|
bankuai_dashou_qs = yonghu_chenghao_qs_for_request(
|
||||||
for bk in all_banquai:
|
YonghuChenghao.query.filter(
|
||||||
tag_ids = Chenghao.query.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True)
|
chenghao__leixing='dashou',
|
||||||
count = yonghu_chenghao_qs_for_request(
|
yonghu__DashouProfile__isnull=False,
|
||||||
YonghuChenghao.query.filter(
|
),
|
||||||
chenghao_id__in=tag_ids,
|
request,
|
||||||
yonghu__DashouProfile__isnull=False,
|
)
|
||||||
),
|
bankuai_dashou_count_map = {
|
||||||
request,
|
item['chenghao__bankuai_id']: item['user_count']
|
||||||
).values('yonghu').distinct().count()
|
for item in bankuai_dashou_qs.values('chenghao__bankuai_id').annotate(
|
||||||
bankuai_dashou_count_map[bk.bankuai_id] = count
|
user_count=Count('yonghu', distinct=True)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
# ---------- 新增:板块标签统计 ----------
|
# ---------- 优化:板块标签统计(1 次 annotate 替代 N*M 次循环查询)----------
|
||||||
bankuai_tags_data = []
|
# 原逻辑:每板块每标签分别统计关联的不同用户数(不要求 DashouProfile)
|
||||||
for bk in all_banquai:
|
tag_counts_qs = yonghu_chenghao_qs_for_request(
|
||||||
tags = Chenghao.query.filter(bankuai=bk, leixing='dashou')
|
YonghuChenghao.query.filter(chenghao__leixing='dashou'),
|
||||||
tag_list = []
|
request,
|
||||||
for tag in tags:
|
).values(
|
||||||
user_count = yonghu_chenghao_qs_for_request(
|
'chenghao_id', 'chenghao__bankuai_id', 'chenghao__mingcheng'
|
||||||
YonghuChenghao.query.filter(chenghao=tag),
|
).annotate(user_count=Count('yonghu', distinct=True))
|
||||||
request,
|
|
||||||
).values('yonghu').distinct().count()
|
tags_by_bankuai = defaultdict(list)
|
||||||
tag_list.append({
|
for item in tag_counts_qs:
|
||||||
'id': tag.id,
|
tags_by_bankuai[item['chenghao__bankuai_id']].append({
|
||||||
'name': tag.mingcheng,
|
'id': item['chenghao_id'],
|
||||||
'user_count': user_count,
|
'name': item['chenghao__mingcheng'],
|
||||||
})
|
'user_count': item['user_count'],
|
||||||
bankuai_tags_data.append({
|
})
|
||||||
|
|
||||||
|
all_banquai = Bankuai.query.all()
|
||||||
|
bankuai_tags_data = [
|
||||||
|
{
|
||||||
'bankuai_id': bk.bankuai_id,
|
'bankuai_id': bk.bankuai_id,
|
||||||
'mingcheng': bk.mingcheng,
|
'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 = []
|
data_list = []
|
||||||
for sg in shenheguan_list:
|
for sg in shenheguan_list:
|
||||||
user = sg.user
|
user = sg.user
|
||||||
# 获取该审核官关联的板块
|
bankuai_info = [
|
||||||
bk_qs = KaoheguanBankuai.query.filter(kaoheguan_id=user.UserUID).select_related('bankuai')
|
{
|
||||||
bankuai_info = []
|
'bankuai_id': kg.bankuai.bankuai_id,
|
||||||
for bk in bk_qs:
|
'mingcheng': kg.bankuai.mingcheng,
|
||||||
bid = bk.bankuai.bankuai_id
|
'dashou_count': bankuai_dashou_count_map.get(kg.bankuai.bankuai_id, 0),
|
||||||
bankuai_info.append({
|
}
|
||||||
'bankuai_id': bid,
|
for kg in kg_map.get(user.UserUID, [])
|
||||||
'mingcheng': bk.bankuai.mingcheng,
|
]
|
||||||
'dashou_count': bankuai_dashou_count_map.get(bid, 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
dashou_nick = user.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else ''
|
dashou_nick = user.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else ''
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ def get_active_staff_member(user):
|
|||||||
|
|
||||||
|
|
||||||
def require_active_staff_member(user):
|
def require_active_staff_member(user):
|
||||||
"""子客服接口鉴权:区分老板误入、未绑定、禁用、移除等场景。"""
|
"""子客服接口鉴权:区分未绑定、禁用、移除等场景(已是商家老板也可担任其他店子客服)。"""
|
||||||
if is_merchant_owner(user):
|
|
||||||
raise StaffAuthError(403, '您是商家老板,请使用商家端原接口')
|
|
||||||
member = MerchantStaffMember.query.filter(
|
member = MerchantStaffMember.query.filter(
|
||||||
staff_user_id=user.UserUID,
|
staff_user_id=user.UserUID,
|
||||||
).select_related('role').order_by('-id').first()
|
).select_related('role').order_by('-id').first()
|
||||||
@@ -132,9 +130,21 @@ def resolve_owner_or_staff(request, perm_code=None):
|
|||||||
return 'staff', member.merchant_id, member, user.UserUID
|
return 'staff', member.merchant_id, member, user.UserUID
|
||||||
|
|
||||||
|
|
||||||
def assert_not_merchant_when_bind(user):
|
def assert_not_self_merchant_when_bind(user, invite_merchant_id):
|
||||||
if is_merchant_owner(user):
|
"""不能绑定为自己店铺的客服;可以是其他商家的子客服。"""
|
||||||
raise StaffAuthError(403, '您已是商家,无法成为子客服')
|
if invite_merchant_id is not None and str(user.UserUID) == str(invite_merchant_id):
|
||||||
|
raise StaffAuthError(403, '不能绑定为自己商家的客服')
|
||||||
|
|
||||||
|
|
||||||
|
def assert_can_bind_staff(user, invite_merchant_id=None):
|
||||||
|
assert_not_self_merchant_when_bind(user, invite_merchant_id)
|
||||||
|
existing = MerchantStaffMember.query.filter(
|
||||||
|
staff_user_id=user.UserUID, status=MEMBER_STATUS_ACTIVE
|
||||||
|
).first()
|
||||||
|
if existing:
|
||||||
|
if invite_merchant_id and str(existing.merchant_id) == str(invite_merchant_id):
|
||||||
|
raise StaffAuthError(400, '您已是该商家客服')
|
||||||
|
raise StaffAuthError(403, '您已绑定其他商家客服,请先联系原商家解除后再绑定')
|
||||||
|
|
||||||
|
|
||||||
def assert_no_active_staff_when_merchant_register(user):
|
def assert_no_active_staff_when_merchant_register(user):
|
||||||
@@ -144,14 +154,6 @@ def assert_no_active_staff_when_merchant_register(user):
|
|||||||
raise StaffAuthError(403, '您当前是商家客服,请先联系商家解除后再认证商家')
|
raise StaffAuthError(403, '您当前是商家客服,请先联系商家解除后再认证商家')
|
||||||
|
|
||||||
|
|
||||||
def assert_can_bind_staff(user):
|
|
||||||
assert_not_merchant_when_bind(user)
|
|
||||||
if MerchantStaffMember.query.filter(
|
|
||||||
staff_user_id=user.UserUID, status=MEMBER_STATUS_ACTIVE
|
|
||||||
).exists():
|
|
||||||
raise StaffAuthError(403, '您已绑定商家客服,无法重复绑定')
|
|
||||||
|
|
||||||
|
|
||||||
def get_owner_permissions():
|
def get_owner_permissions():
|
||||||
return list(OWNER_ALL_PERMISSIONS)
|
return list(OWNER_ALL_PERMISSIONS)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class StaffBindView(APIView):
|
|||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
assert_can_bind_staff(request.user)
|
|
||||||
invite_code = str(request.data.get('inviteCode') or request.data.get('invite_code') or '').strip()
|
invite_code = str(request.data.get('inviteCode') or request.data.get('invite_code') or '').strip()
|
||||||
if not invite_code or invite_code == 'XQKF2026':
|
if not invite_code or invite_code == 'XQKF2026':
|
||||||
return Response({'code': 400, 'msg': '请使用商家提供的一次性邀请码', 'data': None})
|
return Response({'code': 400, 'msg': '请使用商家提供的一次性邀请码', 'data': None})
|
||||||
@@ -44,6 +43,7 @@ class StaffBindView(APIView):
|
|||||||
inv = MerchantStaffInvite.objects.select_for_update().get(
|
inv = MerchantStaffInvite.objects.select_for_update().get(
|
||||||
invite_code=invite_code, status=INVITE_STATUS_UNUSED,
|
invite_code=invite_code, status=INVITE_STATUS_UNUSED,
|
||||||
)
|
)
|
||||||
|
assert_can_bind_staff(request.user, invite_merchant_id=inv.merchant_id)
|
||||||
if inv.expire_at and inv.expire_at < timezone.now():
|
if inv.expire_at and inv.expire_at < timezone.now():
|
||||||
inv.status = 3
|
inv.status = 3
|
||||||
inv.save(update_fields=['status'])
|
inv.save(update_fields=['status'])
|
||||||
|
|||||||
-7101
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
@@ -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
@@ -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)
|
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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",
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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 验证店铺身份
|
||||||
|
"""
|
||||||
|
|
||||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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,
|
||||||
|
)
|
||||||
File diff suppressed because it is too large.
Load diff
+1936
File diff suppressed because it is too large.
Load diff
@@ -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
File diff suppressed because it is too large.
Load diff
+1073
File diff suppressed because it is too large.
Load diff
@@ -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
-157257
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user