diff --git a/_init_.py b/_init_.py
deleted file mode 100644
index a0b11ba..0000000
--- a/_init_.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# 3. 在项目的 `__init__.py` 文件(与manage.py同级)中,添加两行代码
-# 文件路径:a_long_dianjing/__init__.py
diff --git a/backend/views/exams.py b/backend/views/exams.py
index 13db028..aa32aab 100644
--- a/backend/views/exams.py
+++ b/backend/views/exams.py
@@ -188,57 +188,73 @@ class KhgglView(APIView):
end = start + page_size
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
+ from collections import defaultdict
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
- # ---------- 预计算每个板块的打手数量 ----------
- bankuai_dashou_count_map = {}
- all_banquai = Bankuai.query.all()
- for bk in all_banquai:
- tag_ids = Chenghao.query.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True)
- count = yonghu_chenghao_qs_for_request(
- YonghuChenghao.query.filter(
- chenghao_id__in=tag_ids,
- yonghu__DashouProfile__isnull=False,
- ),
- request,
- ).values('yonghu').distinct().count()
- bankuai_dashou_count_map[bk.bankuai_id] = count
+ # ---------- 优化:板块打手数量(1 次 annotate 替代 2N 次循环查询)----------
+ # 原逻辑:每板块查 dashou 标签 ID,再统计关联的不同用户数(要求有 DashouProfile)
+ bankuai_dashou_qs = yonghu_chenghao_qs_for_request(
+ YonghuChenghao.query.filter(
+ chenghao__leixing='dashou',
+ yonghu__DashouProfile__isnull=False,
+ ),
+ request,
+ )
+ bankuai_dashou_count_map = {
+ item['chenghao__bankuai_id']: item['user_count']
+ for item in bankuai_dashou_qs.values('chenghao__bankuai_id').annotate(
+ user_count=Count('yonghu', distinct=True)
+ )
+ }
- # ---------- 新增:板块标签统计 ----------
- bankuai_tags_data = []
- for bk in all_banquai:
- tags = Chenghao.query.filter(bankuai=bk, leixing='dashou')
- tag_list = []
- for tag in tags:
- user_count = yonghu_chenghao_qs_for_request(
- YonghuChenghao.query.filter(chenghao=tag),
- request,
- ).values('yonghu').distinct().count()
- tag_list.append({
- 'id': tag.id,
- 'name': tag.mingcheng,
- 'user_count': user_count,
- })
- bankuai_tags_data.append({
+ # ---------- 优化:板块标签统计(1 次 annotate 替代 N*M 次循环查询)----------
+ # 原逻辑:每板块每标签分别统计关联的不同用户数(不要求 DashouProfile)
+ tag_counts_qs = yonghu_chenghao_qs_for_request(
+ YonghuChenghao.query.filter(chenghao__leixing='dashou'),
+ request,
+ ).values(
+ 'chenghao_id', 'chenghao__bankuai_id', 'chenghao__mingcheng'
+ ).annotate(user_count=Count('yonghu', distinct=True))
+
+ tags_by_bankuai = defaultdict(list)
+ for item in tag_counts_qs:
+ tags_by_bankuai[item['chenghao__bankuai_id']].append({
+ 'id': item['chenghao_id'],
+ 'name': item['chenghao__mingcheng'],
+ 'user_count': item['user_count'],
+ })
+
+ all_banquai = Bankuai.query.all()
+ bankuai_tags_data = [
+ {
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
- 'tags': tag_list,
- })
+ 'tags': tags_by_bankuai.get(bk.bankuai_id, []),
+ }
+ for bk in all_banquai
+ ]
+
+ # ---------- 优化:审核官板块关联(1 次批量查询替代 P 次循环查询)----------
+ shenheguan_user_uids = [sg.user.UserUID for sg in shenheguan_list]
+ all_kgs = KaoheguanBankuai.query.filter(
+ kaoheguan_id__in=shenheguan_user_uids
+ ).select_related('bankuai')
+ kg_map = defaultdict(list)
+ for kg in all_kgs:
+ kg_map[kg.kaoheguan_id].append(kg)
# 构造返回数据(审核官列表)
data_list = []
for sg in shenheguan_list:
user = sg.user
- # 获取该审核官关联的板块
- bk_qs = KaoheguanBankuai.query.filter(kaoheguan_id=user.UserUID).select_related('bankuai')
- bankuai_info = []
- for bk in bk_qs:
- bid = bk.bankuai.bankuai_id
- bankuai_info.append({
- 'bankuai_id': bid,
- 'mingcheng': bk.bankuai.mingcheng,
- 'dashou_count': bankuai_dashou_count_map.get(bid, 0)
- })
+ bankuai_info = [
+ {
+ 'bankuai_id': kg.bankuai.bankuai_id,
+ 'mingcheng': kg.bankuai.mingcheng,
+ 'dashou_count': bankuai_dashou_count_map.get(kg.bankuai.bankuai_id, 0),
+ }
+ for kg in kg_map.get(user.UserUID, [])
+ ]
dashou_nick = user.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else ''
diff --git a/merchant_ops/services/authz.py b/merchant_ops/services/authz.py
index 63f3676..d4db512 100644
--- a/merchant_ops/services/authz.py
+++ b/merchant_ops/services/authz.py
@@ -33,9 +33,7 @@ def get_active_staff_member(user):
def require_active_staff_member(user):
- """子客服接口鉴权:区分老板误入、未绑定、禁用、移除等场景。"""
- if is_merchant_owner(user):
- raise StaffAuthError(403, '您是商家老板,请使用商家端原接口')
+ """子客服接口鉴权:区分未绑定、禁用、移除等场景(已是商家老板也可担任其他店子客服)。"""
member = MerchantStaffMember.query.filter(
staff_user_id=user.UserUID,
).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
-def assert_not_merchant_when_bind(user):
- if is_merchant_owner(user):
- raise StaffAuthError(403, '您已是商家,无法成为子客服')
+def assert_not_self_merchant_when_bind(user, invite_merchant_id):
+ """不能绑定为自己店铺的客服;可以是其他商家的子客服。"""
+ 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):
@@ -144,14 +154,6 @@ def assert_no_active_staff_when_merchant_register(user):
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():
return list(OWNER_ALL_PERMISSIONS)
diff --git a/merchant_ops/views_staff.py b/merchant_ops/views_staff.py
index 0349376..8992918 100644
--- a/merchant_ops/views_staff.py
+++ b/merchant_ops/views_staff.py
@@ -35,7 +35,6 @@ class StaffBindView(APIView):
def post(self, request):
try:
- assert_can_bind_staff(request.user)
invite_code = str(request.data.get('inviteCode') or request.data.get('invite_code') or '').strip()
if not invite_code or invite_code == 'XQKF2026':
return Response({'code': 400, 'msg': '请使用商家提供的一次性邀请码', 'data': None})
@@ -44,6 +43,7 @@ class StaffBindView(APIView):
inv = MerchantStaffInvite.objects.select_for_update().get(
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():
inv.status = 3
inv.save(update_fields=['status'])
diff --git a/orders/views.py b/orders/views.py
deleted file mode 100644
index 593102e..0000000
--- a/orders/views.py
+++ /dev/null
@@ -1,7101 +0,0 @@
-# ==================== 标准库 ====================
-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__)
-
-ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}
-
-
-class PaymentFailView(APIView):
- """
- 前端支付失败接口 - 处理用户主动取消或支付失败
- 只允许删除状态为9(未支付)的订单
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- dingdanid = request.data.get('dingdanid')
- if not dingdanid:
- return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None})
-
- with transaction.atomic():
- # 锁定订单防止并发
- dingdan = Order.query.select_for_update().filter(OrderID=dingdanid).first()
- if not dingdan:
- return Response({'code': 2, 'msg': '订单不存在', 'data': None})
-
- if dingdan.User1ID != f"Boss{request.user.UserUID}":
- return Response({'code': 3, 'msg': '无权操作此订单', 'data': None})
-
- # 只允许删除未支付订单(状态9)
- if dingdan.Status != 9:
- return Response({'code': 3, 'msg': '只能删除未支付的订单', 'data': None})
-
- # 删除扩展表和主表
- PlatformOrderExt.query.filter(Order=dingdan).delete()
- dingdan.delete()
-
- return Response({'code': 0, 'msg': '订单已删除', 'data': None})
-
- except Exception as e:
- return Response({'code': 99, 'msg': f'删除订单失败: {str(e)}', 'data': None})
-
-
-
-class WechatPayNotifyView(APIView):
- """微信支付回调接口 - 接收微信官方支付结果通知(含详细调试日志)"""
- throttle_classes = [AnonRateThrottle]
- permission_classes = [AllowAny]
-
- def post(self, request):
- # 记录原始请求数据(调试用)
- xml_data = request.body.decode('utf-8')
-
- # 最外层异常捕获,确保任何错误都能被记录并返回适当响应
- try:
- # 1. 解析 XML
- try:
- root = ET.fromstring(xml_data)
- except ET.ParseError as e:
- logger.error(f"XML解析失败: {e}")
- return self._gen_response_xml('FAIL', 'XML解析失败')
-
- # 2. 获取关键参数
- return_code = root.find('return_code').text if root.find('return_code') is not None else ''
- result_code = root.find('result_code').text if root.find('result_code') is not None else ''
- out_trade_no = root.find('out_trade_no').text if root.find('out_trade_no') is not None else ''
- transaction_id = root.find('transaction_id').text if root.find('transaction_id') is not None else ''
-
- from jituan.services.wechat_pay import club_id_from_order, verify_wechat_v2_signature
- notify_club_id = club_id_from_order(out_trade_no)
-
- # 3. 签名验证
- if not verify_wechat_v2_signature(xml_data, notify_club_id):
- logger.warning("签名验证失败")
- return self._gen_response_xml('FAIL', '签名验证失败')
-
- # 4. 根据支付结果处理
- if return_code == 'SUCCESS' and result_code == 'SUCCESS':
- self._handle_success(out_trade_no, transaction_id, root)
- else:
- logger.info(f"支付失败或异常,return_code={return_code}, result_code={result_code}")
- self._handle_failure(out_trade_no)
-
- # 返回微信要求的成功响应(必须返回SUCCESS,即使内部处理失败也要返回,否则微信会重复通知)
- return self._gen_response_xml('SUCCESS', 'OK')
-
- except Exception as e:
- # 捕获任何未处理的异常,记录完整堆栈
- logger.error(f"回调处理发生未捕获异常: {str(e)}")
- logger.error(traceback.format_exc())
- return self._gen_response_xml('FAIL', f'服务器内部错误: {str(e)}')
-
- def _verify_signature(self, xml_data):
- """微信支付签名验证(MD5)"""
- try:
- root = ET.fromstring(xml_data)
- sign = root.find('sign').text
- # 构建待签名字符串(排除sign字段)
- data = {child.tag: child.text for child in root if child.tag != 'sign'}
- # 按key排序
- sorted_keys = sorted(data.keys())
- stringA = '&'.join([f"{k}={data[k]}" for k in sorted_keys])
- stringSignTemp = f"{stringA}&key={settings.WEIXIN_SHANGHUMIYAO}"
- calc_sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
- logger.debug(f"计算签名: {calc_sign}, 微信签名: {sign}")
- return calc_sign == sign
- except Exception as e:
- logger.error(f"签名验证过程异常: {e}")
- logger.error(traceback.format_exc())
- return False
-
- def _gen_response_xml(self, return_code, return_msg):
- """生成返回给微信的XML响应"""
- xml = f"""
-
-
-"""
- logger.info(f"返回给微信的响应: {xml}")
- return HttpResponse(xml, content_type='application/xml')
-
- @transaction.atomic
- def _handle_success(self, out_trade_no, transaction_id, root):
- """处理支付成功"""
- logger.info(f"进入 _handle_success, out_trade_no={out_trade_no}, transaction_id={transaction_id}")
-
- # 锁定订单防止并发
- try:
- dingdan = Order.query.select_for_update().filter(OrderID=out_trade_no).first()
- except Exception as e:
- logger.error(f"查询订单异常: {e}")
- raise
-
- if not dingdan:
- logger.error(f"订单不存在: {out_trade_no}")
- return
-
- logger.info(f"订单当前状态: {dingdan.Status}")
-
- # 幂等:已支付则补试收支记账(防止首次入账失败后再也无法统计)
- if dingdan.Status != 9:
- logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.Status},补试收支记账")
- try:
- from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
- repair_wechat_income_if_missing(
- dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{dingdan.OrderID}',
- )
- except Exception as e:
- logger.error(f'订单补记收支失败 {out_trade_no}: {e}', exc_info=True)
- return
-
- # 保存微信交易号
- if transaction_id:
- dingdan.WechatTransactionID = transaction_id
- logger.info(f"保存微信交易号: {transaction_id}")
- else:
- logger.warning("微信未返回transaction_id")
-
- # 更新订单状态
- if dingdan.AssignedID and dingdan.AssignedID.strip():
- dingdan.Status = 7
- logger.info("指定单,状态设为7")
- else:
- dingdan.Status = 1
- logger.info("普通单,状态设为1")
-
- dingdan.save()
-
- logger.info(f"订单状态已更新为 {dingdan.Status}")
-
- # 删除同一用户同一商品的其他未支付订单(改为逻辑删除,状态25)
- try:
- abandoned_count = Order.query.filter(
- User1ID=dingdan.User1ID,
- ProductID=dingdan.ProductID,
- Status=9
- ).exclude(OrderID=out_trade_no).update(Status=25)
- logger.info(f"将 {abandoned_count} 个未支付订单标记为已废弃(25)")
- except Exception as e:
- logger.error(f"标记其他未支付订单异常: {e}")
-
- # 更新老板扩展表
- try:
- laoban_id = None
- if hasattr(dingdan, 'pingtai_kuozhan'):
- laoban_id = dingdan.pingtai_kuozhan.BossID
- if laoban_id:
- user_main = User.query.filter(UserUID=laoban_id).first()
- if user_main and hasattr(user_main, 'BossProfile'):
- boss = user_main.BossProfile
- boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
- boss.alldingdan = (boss.alldingdan or 0) + 1
- boss.save()
- logger.info(f"更新老板 {laoban_id} 数据成功")
- except Exception as e:
- logger.error(f"更新老板数据异常: {e}")
-
- # 更新商品库存
- try:
- if dingdan.ProductID:
- Shangpin.query.filter(id=dingdan.ProductID).update(
- kucun=F('kucun') - 1
- )
- logger.info(f"商品 {dingdan.ProductID} 库存减1")
- 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=1, # 1 = 下单
- dingdan_jiage=dingdan.Amount,
- PlatformIncome=pingtai_shouyi,
- dianpu_zongshouyi=dianpu_shouyi
- )
- logger.info(f"商品统计更新完成,商品ID: {dingdan.ProductID}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- 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=1, # 1 = 下单
- 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())
- # 统计失败不影响主流程
- # =============================================
-
-
- # 更新收支记录(总累计);统计失败不得回滚订单支付
- try:
- from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
- repair_wechat_income_if_missing(
- dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{dingdan.OrderID}',
- )
- except Exception as e:
- logger.error(f'订单收支统计失败 {out_trade_no}: {e}', exc_info=True)
-
- # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用)
- order_info = {
- 'OrderID': dingdan.OrderID,
- 'game_type': self._get_game_type_name(dingdan.ProductTypeID),
- 'amount': str(dingdan.Amount),
- 'order_desc': (dingdan.Description or '')[:50],
- }
- dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求
-
- # 异步广播通知
- #self._broadcast_async(dingdan)
-
- logger.info(f"订单 {out_trade_no} 支付成功处理完成")
-
- @transaction.atomic
- def _handle_failure(self, out_trade_no):
- """处理支付失败"""
- logger.info(f"进入 _handle_failure, out_trade_no={out_trade_no}")
-
- try:
- dingdan = Order.query.select_for_update().filter(OrderID=out_trade_no).first()
- if not dingdan:
- logger.warning(f"订单不存在: {out_trade_no}")
- return
-
- logger.info(f"订单当前状态: {dingdan.Status}")
-
- # 仅当订单状态为9(未支付)时才标记失败,不直接删除
- if dingdan.Status == 9:
- dingdan.Status = 14 # 自定义支付失败状态
- dingdan.save()
- logger.info(f"订单 {out_trade_no} 支付失败,标记为14")
- else:
- logger.info(f"订单 {out_trade_no} 状态非9,不处理")
- except Exception as e:
- logger.error(f"处理支付失败异常: {e}")
- logger.error(traceback.format_exc())
-
- def _update_szjilu(self, jine, club_id=None):
- """szjilu 已废弃,财务只写 daily_income_stat,此处不再操作。"""
- pass
-
- def _broadcast_async(self, dingdan):
- """异步广播通知"""
- try:
- def task():
- try:
- order_info = {
- 'OrderID': dingdan.OrderID,
- 'game_type': self._get_game_type_name(dingdan.ProductTypeID),
- 'amount': str(dingdan.Amount),
- 'order_desc': (dingdan.Description or '')[:50],
- }
- logger.info(f"准备广播订单: {order_info}")
- sender = WeixinBroadcastSender()
- result = sender.broadcast_order(order_info)
- if result.get('success'):
- logger.info(f"订单广播完成: {result.get('msg')}")
- else:
- logger.warning(f"订单广播异常: {result.get('msg')}")
- except Exception as e:
- logger.error(f"广播任务异常: {e}")
- logger.error(traceback.format_exc())
-
- thread = threading.Thread(target=task)
- thread.daemon = True
- thread.start()
- logger.info("广播线程已启动")
- except Exception as e:
- logger.error(f"启动广播线程异常: {e}")
-
- def _get_game_type_name(self, leixing_id):
- try:
- gt = ShangpinLeixing.query.filter(id=leixing_id).first()
- return gt.jieshao if gt else '游戏订单'
- except Exception as e:
- logger.error(f"获取游戏类型名称异常: {e}")
- return '游戏订单'
-
-
-
-class PaymentVerifyView(APIView):
- """
- 订单支付成功复查接口 - 前端支付成功后调用,确认订单状态
- 当订单状态为9时,主动查询微信支付状态,若已支付则更新本地状态
- 需要token认证
- """
- permission_classes = [permissions.IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # ---------- 1. 获取参数 ----------
- dingdanid = request.data.get('dingdanid')
- if not dingdanid:
- return Response({'code': 1, 'msg': '订单ID不能为空'})
-
- # ---------- 2. 查询订单 ----------
- dingdan = Order.query.filter(OrderID=dingdanid).first()
- if not dingdan:
- return Response({'code': 2, 'msg': '订单不存在'})
-
- # ---------- 3. 验证订单归属(安全) ----------
- current_user = request.user
- user_identifier = f"Boss{current_user.UserUID}"
- if dingdan.User1ID != user_identifier:
- logger.warning(f"用户 {current_user.UserUID} 尝试复查他人订单 {dingdanid}")
- return Response({'code': 403, 'msg': '无权访问'})
-
- # ---------- 4. 如果订单状态已是1/7,直接返回成功 ----------
- if dingdan.Status in [1, 7]:
- return Response({
- 'code': 0,
- 'msg': '订单状态正常',
- 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.Status}
- })
-
- # ---------- 5. 如果状态为9,则主动查询微信支付状态 ----------
- if dingdan.Status == 9:
- # 使用分布式锁防止并发处理同一订单
- lock_key = f"payment_verify_lock_{dingdanid}"
- if not cache.add(lock_key, "locked", timeout=10):
- # 已有其他进程在处理,让前端稍后重试
- return Response({'code': 1, 'msg': '订单正在处理中,请稍后'})
-
- try:
- # 调用微信订单查询接口
- trade_state, transaction_id = self._query_wechat_payment(dingdanid)
-
- if trade_state == 'SUCCESS':
- # 支付成功,执行本地更新
- self._process_payment_success(dingdan, transaction_id, current_user.UserUID)
- # 重新获取订单以获取最新状态
- dingdan.refresh_from_db()
- return Response({
- 'code': 0,
- 'msg': '支付成功',
- 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.Status}
- })
- elif trade_state in ['NOTPAY', 'USERPAYING']:
- # 用户尚未支付或支付中
- return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}})
- else:
- # 其他状态:CLOSED, REVOKED, PAYERROR 等,视为支付失败
- return Response({'code': 2, 'msg': '支付失败'})
-
- except Exception as e:
- logger.error(f"查询微信支付异常: {e}", exc_info=True)
- return Response({'code': 1, 'msg': '查询失败,请稍后重试'})
- finally:
- cache.delete(lock_key)
- else:
- # ---------- 6. 其他异常状态 ----------
- status_msg = {
- 2: '订单已取消',
- 3: '订单已完成',
- 4: '退款审核中',
- 5: '已退款',
- 6: '退款失败',
- 8: '结算中',
- 0: '订单已作废',
- }.get(dingdan.Status, '订单状态异常')
- return Response({'code': 3, 'msg': status_msg, 'data': {'zhuangtai': dingdan.Status}})
-
- except Exception as e:
- logger.error(f"订单复查接口异常: {e}", exc_info=True)
- return Response({'code': 99, 'msg': f'系统错误: {str(e)}'})
-
- def _process_payment_success(self, dingdan, transaction_id, operator_id):
- """
- 支付成功后的原子操作(与微信回调逻辑完全一致)
- 在事务内完成所有状态更新和数据一致性操作
- """
- with transaction.atomic():
- # 重新锁定订单,防止并发更新
- dingdan = Order.query.select_for_update().get(pk=dingdan.pk)
- if dingdan.Status != 9:
- try:
- from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
- repair_wechat_income_if_missing(
- dingdan.Amount,
- getattr(dingdan, 'ClubID', None),
- biz_ref=f'order:{dingdan.OrderID}',
- )
- except Exception as e:
- logger.error(f'订单复查补记收支失败 {dingdan.OrderID}: {e}', exc_info=True)
- return
-
- # 更新订单状态
- dingdan.Status = 7 if dingdan.AssignedID and dingdan.AssignedID.strip() else 1
- dingdan.WechatTransactionID = transaction_id
- dingdan.AssignedCS = operator_id # 可选:记录操作人ID
- dingdan.save()
-
- # 删除同一用户同一商品的其他未支付订单(逻辑删除,状态25)
- Order.query.filter(
- User1ID=dingdan.User1ID,
- ProductID=dingdan.ProductID,
- Status=9
- ).exclude(OrderID=dingdan.OrderID).update(Status=25)
-
- # 更新老板扩展表
- laoban_id = dingdan.pingtai_kuozhan.BossID if hasattr(dingdan, 'pingtai_kuozhan') else None
- if laoban_id:
- user_main = User.query.filter(UserUID=laoban_id).first()
- if user_main and hasattr(user_main, 'BossProfile'):
- boss = user_main.BossProfile
- boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
- boss.alldingdan = (boss.alldingdan or 0) + 1
- boss.save()
-
- # 更新商品库存
- if dingdan.ProductID:
- Shangpin.query.filter(id=dingdan.ProductID).update(
- kucun=F('kucun') - 1
- )
-
-
- # ========== 新增:商品和店铺每日统计 ==========
- 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=1, # 1 = 下单
- dingdan_jiage=dingdan.Amount,
- PlatformIncome=pingtai_shouyi,
- dianpu_zongshouyi=dianpu_shouyi
- )
- logger.info(f"商品统计更新完成,商品ID: {dingdan.ProductID}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- 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=1, # 1 = 下单
- 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())
- # 统计失败不影响主流程
- # =============================================
-
-
- try:
- from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
- repair_wechat_income_if_missing(
- dingdan.Amount,
- getattr(dingdan, 'ClubID', None),
- biz_ref=f'order:{dingdan.OrderID}',
- )
- except Exception as e:
- logger.error(f'订单收支统计失败 {dingdan.OrderID}: {e}', exc_info=True)
-
- def _query_wechat_payment(self, out_trade_no):
- """
- 调用微信支付订单查询接口,返回 (trade_state, transaction_id)
- 若查询失败,抛出异常
- """
- appid = settings.WEIXIN_APPID
- mch_id = settings.WEIXIN_MCHID
- key = settings.WEIXIN_SHANGHUMIYAO
- nonce_str = self._generate_nonce_str()
-
- params = {
- 'appid': appid,
- 'mch_id': mch_id,
- 'out_trade_no': out_trade_no,
- 'nonce_str': nonce_str,
- }
-
- # 生成签名
- 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/pay/orderquery'
- response = requests.post(url, data=xml_data, timeout=10)
- result = xmltodict.parse(response.content)['xml']
-
- if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
- return result.get('trade_state'), result.get('transaction_id')
- else:
- err_msg = result.get('return_msg', '未知错误')
- raise Exception(f"微信订单查询失败: {err_msg}")
-
- def _update_szjilu(self, jine, club_id=None):
- """szjilu 已废弃,财务只写 daily_income_stat,此处不再操作。"""
- pass
- # 不抛出异常,因为收支记录不是核心流程,但记录错误供排查
-
- def _generate_nonce_str(self):
- """生成随机字符串,长度32"""
- return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
-
- def _dict_to_xml(self, data):
- """将字典转换为XML字符串"""
- xml = ['']
- for k, v in data.items():
- xml.append(f'<{k}>{v}{k}>')
- xml.append('')
- return ''.join(xml)
-
-
-
-
-class CreateOrderView(APIView):
- """
- 创建订单并调用微信支付接口
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取前端传递的字段
- shangpin_id = request.data.get('shangpin_id')
- nicheng = request.data.get('nicheng', '').strip()
- beizhu = request.data.get('Remark', '').strip()
- zhiding = request.data.get('zhiding', '').strip()
- jine = request.data.get('jine', 0)
-
- # 2. 验证必填字段
- if not shangpin_id:
- return Response({'code': 1, 'msg': '商品ID不能为空', 'data': None})
-
- if not nicheng:
- return Response({'code': 2, 'msg': '游戏昵称不能为空', 'data': None})
-
- if len(nicheng) > 50:
- return Response({'code': 3, 'msg': '游戏昵称过长', 'data': None})
-
- # 3. 查询商品信息
- shangpin = Shangpin.query.filter(id=shangpin_id).first()
- if not shangpin:
- return Response({'code': 4, 'msg': '商品不存在', 'data': None})
-
-
- is_valid, err_msg, _ = validate_shangpin_and_dianpu(shangpin_id)
- if not is_valid:
- return Response({'code': 5, 'msg': err_msg, 'data': None})
-
- # 检查库存
- if hasattr(shangpin, 'kucun') and shangpin.kucun <= 0:
- return Response({'code': 5, 'msg': '商品库存不足', 'data': None})
-
- # 获取商品价格
- shangpin_jiage = getattr(shangpin, 'jiage', 0)
- if not shangpin_jiage:
- return Response({'code': 6, 'msg': '商品价格异常', 'data': None})
- #获取商品图片
- shangpin_tupian = getattr(shangpin, 'tupian_url', None)
-
- # 4. 验证指定打手(如果存在)
- zhiding_id_value = None
- if zhiding:
- # 查询主表中是否存在该打手
- zhiding_user = User.query.filter(
- UserUID=zhiding,
- #user_type='PlayerID'
- ).first()
-
- if not zhiding_user:
- return Response({'code': 7, 'msg': '指定打手不存在', 'data': None})
-
- # 查询打手扩展表
- dashou_profile = UserDashou.query.filter(user=zhiding_user).first()
-
- if not dashou_profile:
- return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None})
-
- # 检查在线状态
- if getattr(dashou_profile, 'zhanghaozhuangtai', 0) == 0:
- return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None})
-
- zhiding_id_value = zhiding
-
- # 5. 生成订单ID
- timestamp = int(time.time())
- timestamp_ns = int(time.time_ns() // 1000) % 1000000
- random_num = random.randint(0, 99)
- dingdanid = f"PT{timestamp:011d}{timestamp_ns:06d}{random_num:02d}"
-
- # 6. 查询利率并计算分成
- club_id = resolve_club_id_from_request(request)
- dashou_lilu = get_commission_rate_object(club_id, '1')
-
-
- # 🔥 新增:判断是否使用商品配置的额外分成
- if shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None:
- # 使用商品指定的额外分成金额
- dashou_fencheng = shangpin.ewai_dashou_fencheng
- else:
- # 按原有逻辑计算打手分成
- dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.Rate))
-
-
- # 查询管事利率(字符'2')
- #guanshi_lilu = CommissionRate.query.filter(Platform='2').first()
-
- if not (shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None):
- if get_commission_rate(club_id, '1') <= 0 and not CommissionRate.query.filter(Platform='1').exists():
- return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None})
-
-
- # 开始数据库事务
- with transaction.atomic():
- # 获取当前用户信息
- current_user = request.user
- laoban_yonghuid = getattr(current_user, 'UserUID', '') or getattr(current_user, 'yonghuid', '')
- user_openid, club_id = get_payment_openid(request, current_user, club_id)
-
- if not user_openid:
- return Response({'code': 10, 'msg': '用户openid不存在', 'data': None})
-
- # 创建订单主表数据
- order_data = {
- 'OrderID': dingdanid,
- 'Status': 9, # 未付款状态
- 'Amount': shangpin_jiage,
- 'Platform':1,
- 'PlayerCommission': dashou_fencheng,
- 'AssignedID': zhiding_id_value,
- 'ProductID': shangpin_id,
- 'GrabRequirement': getattr(shangpin, 'yaoqiuleixing', None),
- 'MembershipID': getattr(shangpin, 'huiyuan_id', ''),
- 'CommissionReq': getattr(shangpin, 'yongjin', 0),
- 'Description': getattr(shangpin, 'jieshao', ''),
- 'Remark': beizhu,
- 'Nickname': nicheng,
- 'ImageURL':shangpin_tupian,
- 'User1ID':f"Boss{request.user.UserUID}",
- 'ProductTypeID':shangpin.leixing_id,
- 'ClubID': club_id,
- }
-
- # 创建订单记录
-
- dingdan = Order.query.create(**order_data)
-
-
- # CreateOrderView.post 事务内部,替换原有的扩展表创建代码
-
- dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong = calculate_pingtai_and_dianpu_shouyi(
- shangpin_id=shangpin_id,
- dingdan_jiage=shangpin_jiage,
- dashou_fencheng=dashou_fencheng
- )
-
- pingtaikuozhan_data = {
- 'Order': dingdan,
- 'BossID': laoban_yonghuid,
- 'PlatformIncome': pingtai_shouyi,
- # ---------- 新增上级分红快照 ----------
- 'ParentShopID': shangji_dianpu_id,
- 'ParentBonus': shangji_fenhong if shangji_fenhong else Decimal('0.00'),
- }
- if dianpu_id:
- pingtaikuozhan_data['ShopID'] = dianpu_id
- pingtaikuozhan_data['ShopIncome'] = dianpu_shouyi
- else:
- pingtaikuozhan_data['ShopIncome'] = Decimal('0.00')
-
- PlatformOrderExt.query.create(**pingtaikuozhan_data)
-
-
- # 计算收益
- '''dianpu_id, pingtai_shouyi, dianpu_shouyi = calculate_pingtai_and_dianpu_shouyi(
- ProductID=shangpin_id,
- dingdan_jiage=shangpin_jiage,
- PlayerCommission=dashou_fencheng
- )
-
- # 构建扩展表数据,平台收益必定保存,店铺收益和店铺ID仅当dianpu_id存在时才赋值
- pingtaikuozhan_data = {
- 'dingdan': dingdan,
- 'laoban_id': laoban_yonghuid,
- 'pingtai_shouyi': pingtai_shouyi,
- }
- if dianpu_id:
- pingtaikuozhan_data['dianpu_id'] = dianpu_id
- pingtaikuozhan_data['dianpu_shouyi'] = dianpu_shouyi
- else:
- # 无店铺时,店铺收益字段保持默认(0.00),也可显式设为0
- pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00')
-
- PlatformOrderExt.query.create(**pingtaikuozhan_data)'''
-
-
- # 7. 生成微信支付参数(向微信官方发送请求)
- pay_params = self.generate_wechat_pay_params(
- dingdanid=dingdanid,
- jine=shangpin_jiage,
- beizhu=getattr(settings, 'WEIXIN_PAY_DESCRIPTION', '星阙订单'),
- user_ip=self.get_client_ip(request),
- openid=user_openid,
- club_id=club_id,
- )
-
- if not pay_params:
- # 手动抛出异常触发事务回滚
- raise Exception('支付参数生成失败')
-
- # 8. 返回成功响应
- response_data = {
- 'code': 0,
- 'msg': '订单创建成功',
- 'data': {
- 'dingdanid': dingdanid,
- 'payParams': pay_params
- }
- }
-
- return Response(response_data)
-
- except Exception as e:
- # 这里会自动回滚事务
- return Response({'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None})
-
- def get_client_ip(self, request):
- """获取客户端真实IP地址"""
- 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
-
- def generate_wechat_pay_params(self, dingdanid, jine, beizhu, user_ip, openid, club_id=None):
- """
- 调用微信支付统一下单接口,生成支付参数
- """
- try:
- from jituan.services.wechat_pay import get_wechat_v2_config
- pay_cfg = get_wechat_v2_config(club_id)
- APPID = pay_cfg['appid']
- MCHID = pay_cfg['mch_id']
- KEY = pay_cfg['key']
- NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '')
-
- if not all([APPID, MCHID, KEY, NOTIFY_URL]):
- raise Exception('微信支付配置不完整')
-
- # 生成随机字符串
- nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
-
- # 金额(单位:分)
- total_fee = yuan_to_fen(jine)
-
- # 交易类型
- trade_type = 'JSAPI'
-
- # 生成签名
- sign_data = {
- 'appid': APPID,
- 'mch_id': MCHID,
- 'nonce_str': nonce_str,
- 'body': beizhu,
- 'out_trade_no': dingdanid,
- 'total_fee': total_fee,
- 'spbill_create_ip': user_ip,
- 'notify_url': NOTIFY_URL,
- 'trade_type': trade_type,
- '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"""
-{APPID}
-{beizhu}
-{MCHID}
-{nonce_str}
-{NOTIFY_URL}
-{openid}
-{dingdanid}
-{user_ip}
-{total_fee}
-{trade_type}
-{sign}
-"""
-
- # 调用微信统一下单接口
- 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 = root.find('prepay_id').text
-
- # 生成前端支付参数
- timestamp = str(int(time.time()))
- nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
- package = f'prepay_id={prepay_id}'
-
- # 生成支付签名
- pay_sign_data = {
- 'appId': APPID,
- 'timeStamp': timestamp,
- 'nonceStr': nonce_str,
- 'package': package,
- 'signType': 'MD5'
- }
-
- 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()
-
- # 返回支付参数(格式与前端完全匹配)
- result = {
- 'timeStamp': timestamp,
- 'nonceStr': nonce_str,
- 'package': package,
- 'signType': 'MD5',
- 'paySign': pay_sign
- }
-
- return result
- 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}")
-
- except Exception as e:
- raise e
-
-
-
-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})
-
-
-
-
-class ShangpinLeixingView(APIView):
- """
- 获取商家可用的商品类型列表(含关联板块的称号)
- POST /dingdan/sjspleixing
- """
- permission_classes = [IsAuthenticated]
-
- @staticmethod
- def _can_access(request):
- """商家老板、有效子客服(含代理调用)方可获取"""
- if getattr(request, '_staff_member', None) is not None:
- return True
- from merchant_ops.services.authz import (
- get_active_staff_member, is_merchant_owner, member_has_perm, staff_order_read_perm,
- )
- if is_merchant_owner(request.user):
- return True
- member = get_active_staff_member(request.user)
- return bool(member and staff_order_read_perm(member))
-
- def post(self, request):
-
- if not self._can_access(request):
- return Response({'code': 403, 'msg': '非商家或有效客服,无权获取订单类型', 'data': []})
-
- try:
- # 1. 查询审核通过且抢单要求类型为1的商品类型,按排序权重升序
- shangpin_queryset = ShangpinLeixing.query.filter(
- yaoqiuleixing=1,
- shenhezhuangtai=1
- ).select_related('bankuai').order_by('paixu')
-
- # 2. 收集所有涉及到的板块ID
- bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct()
-
- # 3. 查询这些板块下的所有称号(每个称号绑定到一个板块)
- chenghao_list = Chenghao.query.filter(
- bankuai_id__in=bankuai_ids
- ).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id')
-
- # 4. 构建 板块ID -> 称号列表 的映射(确保准确关联)
- bankuai_chenghao_map = {}
- for ch in chenghao_list:
- bid = ch['bankuai_id']
- if bid not in bankuai_chenghao_map:
- bankuai_chenghao_map[bid] = []
- bankuai_chenghao_map[bid].append({
- 'id': ch['id'],
- 'mingcheng': ch['mingcheng'],
- 'texiao_json': ch['texiao_miaoshu'] # 前端 chenghao-tag 组件需要
- })
-
- # 5. 组装最终数据
- data_list = []
- for sp in shangpin_queryset:
- item = {
- 'id': sp.id,
- 'yaoqiuleixing': sp.yaoqiuleixing,
- 'huiyuan_id': sp.huiyuan_id,
- 'jieshao': sp.jieshao,
- # 当前类型所属板块的称号列表(无板块则为空数组)
- 'chenghaoList': bankuai_chenghao_map.get(sp.bankuai_id, [])
- }
- data_list.append(item)
-
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': data_list
- })
-
- except Exception as e:
- logger.error(f"获取商品类型失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
-
-
-
-class ShangjiaPaifaView(APIView):
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- logger.info(f"商家派单请求: user_id={request.user.UserUID}, data={request.data}")
-
- try:
- data = request.data
-
- # ----- 1. 获取原始参数(保持原类型)-----
- shangpin_type_id = data.get('shangpinTypeId')
- huiyuan_id = data.get('huiyuanId')
- yaoqiuleixing = data.get('GrabRequirement')
- dingdan_jieshao_raw = data.get('dingdanJieshao', '')
- dingdan_beizhu_raw = data.get('dingdanBeizhu', '')
- laoban_name_raw = data.get('laobanName', '')
- zhiding_uid_raw = data.get('zhidingUid', '')
- jiage_str = data.get('jiage')
-
- label_id = data.get('labelId') # 整数或 None
- commission_enabled = data.get('commissionEnabled', False)
- commission_value_raw = data.get('commissionValue')
- waibu_dingdan_id_raw = data.get('waibuDingdanId', '')
-
- # ----- 2. 字符串字段安全处理 -----
- dingdan_jieshao = str(dingdan_jieshao_raw).strip()
- dingdan_beizhu = str(dingdan_beizhu_raw).strip()
- laoban_name = str(laoban_name_raw).strip()
- zhiding_uid = str(zhiding_uid_raw).strip()
- commission_value_str = str(commission_value_raw).strip() if commission_value_raw is not None else ''
- waibu_dingdan_id = str(waibu_dingdan_id_raw).strip()[:64] if waibu_dingdan_id_raw else ''
-
- # ----- 3. 必填校验 -----
- if not all([shangpin_type_id, dingdan_jieshao, laoban_name, jiage_str]):
- logger.warning(f"参数缺失: {request.data}")
- return Response({'code': 400, 'msg': '缺少必填参数'})
-
- # ----- 4. 价格验证 -----
- try:
- jiage = float(jiage_str)
- if jiage < 1 or jiage > 10000:
- return Response({'code': 400, 'msg': '价格需在1-10000元之间'})
- if '.' in str(jiage_str) and len(str(jiage_str).split('.')[1]) > 2:
- return Response({'code': 400, 'msg': '最多支持两位小数'})
- except ValueError:
- return Response({'code': 400, 'msg': '价格格式错误'})
-
- # ----- 5. 商品类型 -----
- try:
- shangpin_type = ShangpinLeixing.query.get(id=shangpin_type_id)
- except ShangpinLeixing.DoesNotExist:
- logger.error(f"商品类型不存在: {shangpin_type_id}")
- return Response({'code': 400, 'msg': '商品类型不存在'})
-
- # ----- 6. 押金 / 会员模式决定 -----
- final_yongjin = None
- if commission_enabled and commission_value_str:
- try:
- cv = float(commission_value_str)
- if cv > 0:
- final_yaoqiu_type = 2
- final_yongjin = Decimal(str(cv))
- final_huiyuan_id = ''
- logger.info(f"押金模式: 佣金={cv}")
- else:
- final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
- final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
- logger.warning("佣金为0,降级为会员模式")
- except ValueError:
- final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
- final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
- logger.warning("佣金格式错误,降级为会员模式")
- else:
- final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
- final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
- logger.info(f"会员模式: 类型={final_yaoqiu_type}")
-
- # ----- 7. 称号(可选:不传/空值/无效/不存在均视为无标签)-----
- chenghao_obj = None
- label_id_str = str(label_id).strip() if label_id is not None else ''
- if label_id_str and label_id_str not in ('0', 'null', 'undefined'):
- try:
- lid = int(label_id_str)
- if lid > 0:
- chenghao_obj = Chenghao.query.filter(id=lid).first()
- if chenghao_obj:
- logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}")
- else:
- logger.info(f"标签不存在,按无标签派单: {label_id}")
- except (ValueError, TypeError):
- logger.info(f"标签ID无效,按无标签派单: {label_id}")
-
- # ----- 8. 商家信息 -----
- try:
- shangjia = request.user.ShopProfile
- except UserShangjia.DoesNotExist:
- logger.warning(f"用户不是商家: {request.user.UserUID}")
- return Response({'code': 403, 'msg': '您不是商家'})
-
- if shangjia.zhuangtai != 1:
- logger.warning(f"商家状态异常: {shangjia.zhuangtai}")
- return Response({'code': 403, 'msg': '商家状态异常'})
-
- if jiage > shangjia.yue:
- logger.warning(f"余额不足: {shangjia.yue} < {jiage}")
- return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元'})
-
- # ----- 9. 指定打手 -----
- zhiding_dashou = None
- if zhiding_uid:
- try:
- zhiding_user = User.query.get(UserUID=zhiding_uid)
- zhiding_dashou = zhiding_user.DashouProfile
- if zhiding_dashou.zhanghaozhuangtai != 1:
- return Response({'code': 400, 'msg': '指定打手状态异常'})
- except User.DoesNotExist:
- return Response({'code': 400, 'msg': '指定用户不存在'})
- except UserDashou.DoesNotExist:
- return Response({'code': 400, 'msg': '指定用户不是打手'})
-
- # ----- 10. 利率(打手+管事分成,打手优先) -----
- club_id = resolve_club_id_from_request(request)
- dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage, club_id=club_id)
-
- # ----- 11. 生成订单ID -----
- def generate_dingdan_id():
- ts = int(time.time() * 1000)
- pid = os.getpid() % 1000
- rnd = random.randint(1000, 9999)
- return f"SJ{ts}{pid:03d}{rnd}"
-
- # ----- 12. 事务:创建订单、更新商家统计 -----
- dingdan = None
- try:
- with transaction.atomic():
- dingdan = Order.query.create(
- OrderID=generate_dingdan_id(),
- Status=7 if zhiding_dashou else 1,
- Platform=2,
- Amount=jiage,
- PlayerCommission=dashou_fencheng,
- ManagerCommission=guanshi_fencheng,
- AssignedID=zhiding_uid if zhiding_dashou else '',
- ProductID=0,
- ProductTypeID=int(shangpin_type_id),
- GrabRequirement=final_yaoqiu_type,
- CommissionReq=final_yongjin,
- MembershipID=final_huiyuan_id,
- Description=dingdan_jieshao[:2000],
- Remark=dingdan_beizhu[:1000],
- Nickname=laoban_name[:50],
- ImageURL=request.user.Avatar,
- User1ID=f"Sj{request.user.UserUID}",
- ClubID=club_id,
- ExternalOrderID=waibu_dingdan_id or None,
- )
- logger.info(f"订单主表创建: {dingdan.OrderID}")
-
- MerchantOrderExt.query.create(
- Order=dingdan,
- MerchantID=request.user.UserUID,
- MerchantNickname=shangjia.nicheng,
- )
-
- if chenghao_obj:
- DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=chenghao_obj)
- logger.info(f"订单关联称号: {chenghao_obj.mingcheng}")
-
- # 更新商家统计
- UserShangjia.query.filter(id=shangjia.id).update(
- fabu=F('fabu') + 1,
- yue=F('yue') - jiage,
- jinridingdan=F('jinridingdan') + 1,
- jinriliushui=F('jinriliushui') + jiage,
- jinyuedingdan=F('jinyuedingdan') + 1,
- jinyueliushui=F('jinyueliushui') + jiage,
- )
-
- update_shangjia_daily(
- yonghuid=request.user.UserUID,
- amount=Decimal(str(jiage)),
- action=1,
- order_id=dingdan.OrderID,
- )
- # 事务提交,订单已持久化
- except Exception as e:
- logger.error(f"订单创建事务失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '订单创建失败,请稍后重试'})
-
- # ========== 以下操作均在事务外,避免异步任务查不到订单 ==========
- # Celery 广播任务(异步)
- try:
- order_info = {
- 'OrderID': dingdan.OrderID,
- 'game_type': self._get_game_type_name(dingdan.ProductTypeID),
- 'amount': str(dingdan.Amount),
- 'order_desc': (dingdan.Description or '')[:50],
- }
- dingdan_guangbo.delay(order_info)
- logger.info(f"已提交广播任务: {dingdan.OrderID}")
- except Exception as e:
- logger.error(f"广播任务提交失败: {e}", exc_info=True)
-
- logger.info(f"商家派单成功: {dingdan.OrderID}, 金额={jiage}")
- return Response({'code': 200, 'msg': '派单成功', 'data': {'jine': str(dingdan.Amount)}})
-
- except Exception as e:
- logger.error(f"商家派单接口未捕获异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '系统内部错误'})
-
- def _get_game_type_name(self, leixing_id):
- try:
- gt = ShangpinLeixing.query.filter(id=leixing_id).first()
- return gt.jieshao if gt else '游戏订单'
- except Exception:
- return '游戏订单'
-
-# views.py 中 ShangjiaDingdanHuoquView 完整改造
-class ShangjiaDingdanHuoquView(APIView):
- """
- 商家订单获取接口
- POST /dingdan/sjdingdanhq
- 需要JWT认证
- 支持分页、状态筛选、商品类型筛选、模糊搜索(订单ID/订单介绍/游戏昵称/打手ID)
- 返回待结算订单数量(pending_count)
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取当前登录用户的yonghuid
- try:
- current_user = request.user
- yonghuid = current_user.UserUID
- except AttributeError:
- return Response({
- 'code': 1,
- 'msg': '用户认证失败',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 解析参数
- try:
- data = request.data
- zhuangtai_list = data.get('zhuangtai_list', [])
- page = max(1, int(data.get('page', 1)))
- page_size = min(100, max(1, int(data.get('page_size', 10))))
-
- # 商品类型 / 关键字 / 时间段等见 apply_merchant_order_list_filters
- except (ValueError, TypeError):
- return Response({
- 'code': 2,
- 'msg': '参数格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 验证状态列表
- if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0:
- return Response({
- 'code': 3,
- 'msg': '状态列表不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- from orders.services.merchant_order_list_filters import apply_merchant_order_list_filters
-
- # 4. 构建基础查询(该商家的所有订单)
- base_qs = MerchantOrderExt.query.filter(
- MerchantID=yonghuid
- ).select_related('Order')
-
- filter_payload = dict(data)
- filter_payload['_merchant_id_filter'] = yonghuid
-
- # 5. 状态过滤
- base_qs = base_qs.filter(Order__Status__in=zhuangtai_list)
-
- # 6–7. 类型 / 关键字 / 时间段 / 客服筛选
- base_qs = apply_merchant_order_list_filters(base_qs, filter_payload)
-
- # 8. 待结算统计(全量聚合,非分页求和)
- from orders.services.merchant_order_stats import (
- build_merchant_order_stats, merchant_ext_qs, filter_by_staff_member,
- )
- stats_qs = merchant_ext_qs(yonghuid)
- stats_qs = apply_merchant_order_list_filters(stats_qs, filter_payload)
- order_stats = build_merchant_order_stats(stats_qs)
-
- # 9. 排序、分页
- total_count = base_qs.count()
- start_index = (page - 1) * page_size
- end_index = start_index + page_size
- current_page_orders = list(base_qs.order_by('-Order__CreateTime')[start_index:end_index])
-
- from jituan.services.club_context import resolve_club_id_from_request
- from orders.services.merchant_order_list_enrich import enrich_merchant_order_list_items
-
- club_id = resolve_club_id_from_request(request)
- order_list = enrich_merchant_order_list_items(current_page_orders, club_id=club_id)
-
- # 11. 判断是否还有更多
- current_page_count = len(current_page_orders)
- has_more = start_index + current_page_count < total_count
-
- return Response({
- 'code': 0,
- 'msg': '成功',
- 'data': {
- 'list': order_list,
- 'has_more': has_more,
- 'total': total_count,
- 'pending_count': order_stats['pending_count'],
- 'pending_amount': order_stats['pending_amount'],
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"商家订单获取失败: {str(e)}", exc_info=True)
- return Response({
- 'code': 4,
- 'msg': '服务器内部错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class ShangjiaShujuTongjiView(APIView):
- """
- 商家经营数据统计(订单表聚合)
- POST /dingdan/sjshujutj
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- current_user = request.user
- yonghuid = current_user.UserUID
- except AttributeError:
- return Response({'code': 1, 'msg': '用户认证失败', 'data': None}, status=401)
-
- try:
- shangjia = current_user.ShopProfile
- if shangjia.zhuangtai != 1:
- return Response({'code': 403, 'msg': '商家状态异常', 'data': None}, status=403)
- except Exception:
- return Response({'code': 403, 'msg': '非商家用户', 'data': None}, status=403)
-
- data = request.data or {}
- from orders.services.merchant_order_list_filters import apply_merchant_order_list_filters
- from orders.services.merchant_order_stats import merchant_ext_qs, build_merchant_order_stats
-
- filter_payload = dict(data)
- filter_payload['_merchant_id_filter'] = yonghuid
- qs = merchant_ext_qs(yonghuid)
- qs = apply_merchant_order_list_filters(qs, filter_payload)
- order_stats = build_merchant_order_stats(qs)
-
- penalty_stats = None
- try:
- from orders.models import Penalty, PenaltyRecord
- from django.db.models import Q, Sum
- fakuan_pending = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).count()
- fakuan_pending_amt = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).aggregate(
- amt=Sum('FineAmount')
- )['amt']
- jifen_pending = PenaltyRecord.query.filter(
- ApplicantID=yonghuid,
- ).filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)).count()
- penalty_stats = {
- 'fakuan_pending': fakuan_pending,
- 'fakuan_pending_amount': str(fakuan_pending_amt or '0.00'),
- 'jifen_pending': jifen_pending,
- }
- except Exception:
- penalty_stats = {
- 'fakuan_pending': 0,
- 'fakuan_pending_amount': '0.00',
- 'jifen_pending': 0,
- }
-
- return Response({
- 'code': 0,
- 'msg': '成功',
- 'data': {
- 'order': order_stats,
- 'penalty': penalty_stats,
- 'can_view_finance': True,
- },
- })
-
-
-
-
-class ShangjiaDingdanXiangqingView(APIView):
- """
- 商家订单详情接口
- 返回订单基础信息、打手信息、交付图片、罚款数据、分红利率、更换打手产生的新订单ID等
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取当前登录用户的 yonghuid
- try:
- current_user = request.user
- yonghuid = current_user.UserUID
- except AttributeError:
- return Response({
- 'code': 1,
- 'msg': '用户认证失败',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 获取前端参数
- try:
- data = request.data
- dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
- except Exception:
- return Response({
- 'code': 2,
- 'msg': '参数格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not dingdan_id:
- return Response({
- 'code': 3,
- 'msg': '订单ID不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- # 3. 查询商家订单扩展表,验证权限并获取基础订单数据
- shangjia_kuozhan = MerchantOrderExt.query.filter(
- MerchantID=yonghuid,
- Order__OrderID=dingdan_id
- ).select_related('Order').first()
-
- if not shangjia_kuozhan:
- return Response({
- 'code': 4,
- 'msg': '订单不存在或您无权限查看',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- order = shangjia_kuozhan.Order
-
- # 4. 交付图片列表
- dashou_tupian_list = PlayerDeliveryImage.query.filter(
- OrderID=dingdan_id
- ).order_by('CreateTime').values_list('ImageURL', flat=True)
- dashou_jiaofu = list(dashou_tupian_list)
-
- # 5. 打手详细信息(头像、昵称、用户ID)
- dashou_yonghuid = order.PlayerID or ''
- dashou_nicheng = ''
- dashou_avatar = ''
- if dashou_yonghuid:
- dashou_user = User.query.filter(
- UserUID=dashou_yonghuid
- ).select_related('DashouProfile').first()
- if dashou_user:
- dashou_avatar = dashou_user.Avatar or ''
- if hasattr(dashou_user, 'DashouProfile') and dashou_user.DashouProfile:
- dashou_nicheng = dashou_user.DashouProfile.nicheng or ''
-
- # 6. 罚款数据(如果存在)
- fadan_obj = Penalty.query.filter(
- RelatedOrderID=dingdan_id,
- PenalizedUserID=dashou_yonghuid
- ).first()
- fadan_dict = None
- if fadan_obj:
- ct = fmt_datetime(fadan_obj.CreateTime)
- ut = fmt_datetime(fadan_obj.UpdateTime)
- zhengju_urls = list(
- PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=1).values_list('ImageURL', flat=True)
- )
- shensu_urls = list(
- PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=2).values_list('ImageURL', flat=True)
- )
- zhengju_imgs = [
- {'id': t.id, 'url': t.ImageURL}
- for t in PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=1)
- ]
- shensu_imgs = [
- {'id': t.id, 'url': t.ImageURL}
- for t in PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=2)
- ]
- fadan_dict = {
- 'id': fadan_obj.id,
- 'chufaliyou': fadan_obj.Reason or '',
- 'fakuanjine': float(fadan_obj.FineAmount) if fadan_obj.FineAmount else 0.0,
- 'yingxiang_qiangdan': fadan_obj.AffectsGrabbing,
- 'zhuangtai': fadan_obj.Status,
- 'shensuliyou': fadan_obj.AppealReason or '',
- 'tongyi_chufaliyou': fadan_obj.ApproveReason or '',
- 'bohuiliyou': fadan_obj.RejectReason or '',
- 'yingde_fenhong': float(fadan_obj.ApplicantBonusAmount or 0),
- 'fenhong_lilv': float(fadan_obj.ApplicantBonusRate or 0),
- 'fenhong_yiruzhang': bool(fadan_obj.BonusCredited),
- 'zhengju_tupian': zhengju_imgs,
- 'shensu_tupian': shensu_imgs,
- 'CreateTime': ct,
- 'create_time': ct,
- 'UpdateTime': ut,
- 'update_time': ut,
- }
-
- # 7. 商家分红利率:已有罚单用锁定值;否则按商家所在俱乐部预览
- fenhong_lilv = 0.0
- try:
- if fadan_obj and fadan_obj.ApplicantBonusRate:
- fenhong_lilv = float(fadan_obj.ApplicantBonusRate)
- else:
- from users.fadan_fenhong_utils import get_merchant_fenhong_lilv
- from jituan.services.club_write import resolve_club_id_for_write
- merchant_club_id = resolve_club_id_for_write(request, current_user)
- fenhong_lilv = get_merchant_fenhong_lilv(yonghuid, club_id=merchant_club_id)
- except Exception as e:
- logger.warning(f'获取分红利率失败: {e}')
-
- # 8. 更换打手后产生的新订单ID(如果当前订单被更换过)
- xin_dingdan_id = ''
- try:
- # 查找以当前订单作为“上一个订单ID”的新订单扩展记录
- new_shangjia_order = MerchantOrderExt.query.filter(
- PrevOrderID=dingdan_id,
- MerchantID=yonghuid
- ).select_related('Order').first()
- if new_shangjia_order:
- xin_dingdan_id = new_shangjia_order.Order.OrderID
- except Exception as e:
- logger.warning(f'查找新订单ID失败: {e}')
-
- # 9. 构建返回数据
- response_data = {
- # 基础订单字段
- 'dingdan_id': order.OrderID,
- 'zhuangtai': order.Status or 0,
- 'jine': float(order.Amount) if order.Amount else 0.0,
- 'jieshao': order.Description or '',
- 'beizhu': order.Remark or '',
- 'nicheng': order.Nickname or '',
- **datetime_aliases(order.CreateTime),
-
- # 打手信息
- 'dashou_yonghuid': dashou_yonghuid,
- 'dashou_nicheng': dashou_nicheng,
- 'dashou_avatar': dashou_avatar,
-
- # 服务详情
- 'fuwudashou_id': dashou_yonghuid, # 保留旧字段兼容
- 'dashou_liuyan': order.PlayerRemark or '',
- 'dashou_jiaofu': dashou_jiaofu,
-
- # 商家扩展字段(旧)
- 'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '',
- 'tkliyou': order.RefundReason or '',
- 'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 保留,但不影响新罚款
- 'sqzhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0,
- 'bhliyou': shangjia_kuozhan.RejectReason or '',
- 'chufa_jieguo': '', # 已弃用
-
- # 新增:罚款数据
- 'fadan': fadan_dict,
-
- # 新增:分红利率
- 'fenhong_lilv': fenhong_lilv,
-
- # 新增:更换打手后的新订单ID
- 'xin_dingdan_id': xin_dingdan_id,
-
- # 拼多多订单号(商家/客服可见)
- 'waibu_dingdan_id': order.ExternalOrderID or '',
- 'laoban_shouji': getattr(shangjia_kuozhan, 'BossPhone', '') or '',
- }
-
- try:
- from merchant_ops.services.order_enrich import enrich_order_detail_data
- enrich_order_detail_data(response_data, dingdan_id)
- except Exception as e:
- logger.warning(f'派单客服信息附加失败: {e}')
-
- return Response({
- 'code': 0,
- 'msg': '成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"商家订单详情查询失败: {str(e)}", exc_info=True)
- return Response({
- 'code': 5,
- 'msg': '服务器内部错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-class ShangjiaJiesuanView(APIView):
- """商家结单接口"""
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- # 1. 获取当前登录用户
- try:
- current_user = request.user
- yonghuid = current_user.UserUID
-
- try:
- shangjia = current_user.ShopProfile
- except UserShangjia.DoesNotExist:
- return Response({
- 'code': 1,
- 'msg': '您不是商家或商家信息未完善',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- except AttributeError:
- return Response({
- 'code': 1,
- 'msg': '用户认证失败',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 获取前端参数
- try:
- data = request.data
- dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
- pingfen = int(data.get('pingfen', 0))
- liuyan = data.get('liuyan', '').strip()
- except (ValueError, TypeError):
- return Response({
- 'code': 2,
- 'msg': '参数格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 参数验证
- if not dingdan_id:
- return Response({
- 'code': 3,
- 'msg': '订单ID不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if pingfen < 0 or pingfen > 5:
- return Response({
- 'code': 4,
- 'msg': '评分必须在0-5分之间',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- with transaction.atomic():
- # 4. 查询订单并加锁(修复并发问题)
- order = Order.query.select_for_update().get(OrderID=dingdan_id)
-
- # 验证订单是否属于当前商家
- shangjia_kuozhan = MerchantOrderExt.query.filter(
- Order=order,
- MerchantID=yonghuid
- ).first()
-
- if not shangjia_kuozhan:
- return Response({
- 'code': 5,
- 'msg': '订单不存在或您无权限操作',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 5. 验证订单状态
- if order.Status != 8:
- return Response({
- 'code': 6,
- 'msg': '订单不在结算状态,无法结单',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取订单字段
- jine = order.Amount or Decimal('0')
- dashou_fencheng = order.PlayerCommission or Decimal('0')
- jiedan_dashou_id = order.PlayerID
-
- if not jiedan_dashou_id:
- return Response({
- 'code': 7,
- 'msg': '订单没有接单打手,无法结单',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # ========== 本地订单正常结算给打手 ==========
- # 查询打手信息
- try:
- dashou_user = User.query.get(UserUID=jiedan_dashou_id)
- dashou = dashou_user.DashouProfile
- except (User.DoesNotExist, UserDashou.DoesNotExist):
- return Response({
- 'code': 8,
- 'msg': '接单打手不存在或信息不完整',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 原子更新打手数据(使用 F 表达式)
- UserDashou.query.filter(id=dashou.id).update(
- chengjiaozongliang=F('chengjiaozongliang') + 1,
- yue=F('yue') + dashou_fencheng,
- zonge=F('zonge') + dashou_fencheng,
- jinrishouyi=F('jinrishouyi') + dashou_fencheng,
- jinyueshouyi=F('jinyueshouyi') + dashou_fencheng,
- )
-
- # 更新商家成交单量
- UserShangjia.query.filter(id=shangjia.id).update(
- chengjiao=F('chengjiao') + 1,
- )
-
- # 更新订单状态
- order.Status = 3
- order.save()
-
- # 处理评分(所有情况都处理)
- if pingfen > 0:
- pingfen_obj, created = PlayerRating.query.get_or_create(
- PlayerID=jiedan_dashou_id,
- defaults={'TotalScore': pingfen, 'RatingCount': 1}
- )
- if not created:
- PlayerRating.query.filter(id=pingfen_obj.id).update(
- TotalScore=F('TotalScore') + pingfen,
- RatingCount=F('RatingCount') + 1,
- )
-
- # 更新商家订单扩展表的评价
- MerchantOrderExt.query.filter(id=shangjia_kuozhan.id).update(
- MerchantReview=liuyan,
- )
-
- # 新增:调用打手每日统计(成交)
- 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}")
- # 不影响主流程
-
- # ✅ 新方法(行为驱动,更安全)
- update_shangjia_daily(
- yonghuid=request.user.UserUID,
- amount=Decimal(str(jine)),
- action=2, # 2 = 结算
- order_id=dingdan_id,
- )
-
- # 商家订单管事分红(仅 Platform=2)
- if order.Platform == 2:
- settle_shangjia_order_guanshi_fenhong(order, jiedan_dashou_id)
-
- # 返回成功
- return Response({
- 'code': 0,
- 'msg': '结单成功',
- 'data': {
- 'dingdan_id': dingdan_id,
- 'new_zhuangtai': 3,
- 'jine': float(jine),
- 'dashou_fencheng': float(dashou_fencheng),
- 'jiedan_dashou_id': jiedan_dashou_id,
- 'pingfen': pingfen,
- 'liuyan': liuyan
- }
- }, status=status.HTTP_200_OK)
-
- except Order.DoesNotExist:
- return Response({
- 'code': 5,
- 'msg': '订单不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"商家结单失败: {str(e)}", exc_info=True)
- return Response({
- 'code': 9,
- 'msg': '服务器内部错误,结单失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-class ShangjiaChexiaoView(APIView):
- """商家撤销订单申请接口"""
- permission_classes = [IsAuthenticated] # JWT认证
-
- def post(self, request):
- # 1. 获取当前登录用户并验证商家身份
- try:
- current_user = request.user
- # 使用反向查询获取商家扩展信息(性能最佳)
- shangjia = current_user.ShopProfile
- except UserShangjia.DoesNotExist:
- return Response({
- 'code': 1,
- 'msg': '您不是商家或商家信息未完善',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 2. 检查商家账号状态
- if shangjia.zhuangtai != 1:
- return Response({
- 'code': 2,
- 'msg': '商家账号状态异常,无法操作',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 获取前端参数
- try:
- data = request.data
- dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
- chexiao_liyou = data.get('chexiao_liyou', '').strip()
- except Exception:
- return Response({
- 'code': 3,
- 'msg': '参数格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 参数验证
- if not dingdan_id or not chexiao_liyou:
- return Response({
- 'code': 4,
- 'msg': '订单ID和撤销原因不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- # 5. 使用事务确保数据一致性
- with transaction.atomic():
- # 查询订单主表
- dingdan = Order.query.filter(OrderID=dingdan_id).first()
- if not dingdan:
- return Response({
- 'code': 5,
- 'msg': '订单不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 6. 验证订单状态(13=链接待填单,可撤销释放额度)
- if dingdan.Status not in [1, 7, 13]:
- return Response({
- 'code': 6,
- 'msg': '当前订单状态不可撤销',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 7. 验证订单所属商家(使用反向查询)
- shangjia_kuozhan = MerchantOrderExt.query.filter(
- Order__OrderID=dingdan_id,
- MerchantID=current_user.UserUID
- ).first()
-
- if not shangjia_kuozhan:
- return Response({
- 'code': 7,
- 'msg': '您无权撤销此订单',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 8. 创建退款记录(注意:申请状态直接设为1-成功)
- RefundRecord.query.create(
- OrderID=dingdan_id,
- ApplicantID=current_user.UserUID,
- Reason=chexiao_liyou,
- ApplyStatus=1, # 直接成功
- Amount=dingdan.Amount,
- PlayerCommission=dingdan.PlayerCommission,
- Description=dingdan.Description,
- Remark=dingdan.Remark,
- Nickname=dingdan.Nickname,
-
- )
-
- # 9. 更新商家扩展表(客服额度单不退商家余额,只释放额度)
- from merchant_ops.services.wallet import is_staff_quota_order
- staff_quota_order = is_staff_quota_order(dingdan_id)
- shangjia_updates = {
- 'tuikuan': F('tuikuan') + 1,
- 'jinridingdan': F('jinridingdan') - 1,
- 'jinyuedingdan': F('jinyuedingdan') - 1,
- 'jinriliushui': F('jinriliushui') - dingdan.Amount,
- 'jinyueliushui': F('jinyueliushui') - dingdan.Amount,
- }
- if not staff_quota_order:
- shangjia_updates['yue'] = F('yue') + dingdan.Amount
- UserShangjia.query.filter(id=shangjia.id).update(**shangjia_updates)
-
- # 10. 更新订单状态为5(已退款)
- dingdan.Status = 5
-
- dingdan.save()
-
- # 行为驱动
- update_shangjia_daily(
- yonghuid=request.user.UserUID,
- amount=Decimal(str(dingdan.Amount)),
- action=3, # 3 = 退款
- order_id=dingdan_id,
- )
-
- return Response({
- 'code': 0,
- 'msg': '撤销成功',
- 'data': {
- 'dingdan_id': dingdan_id,
- 'new_zhuangtai': 5,
- 'tuikuan_liyou': chexiao_liyou,
- 'jine': float(dingdan.Amount) if dingdan.Amount else 0.0
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- # 记录异常日志
- logger.error(f"商家撤销订单失败: {str(e)}", exc_info=True)
-
- return Response({
- 'code': 8,
- 'msg': '服务器内部错误,撤销失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class ShangjiaTuikuanShenqingView(APIView):
- """商家退款申请接口"""
- permission_classes = [IsAuthenticated] # JWT认证
-
- def post(self, request):
- # 1. 获取当前登录用户并验证商家身份
- try:
- current_user = request.user
- shangjia = current_user.ShopProfile
- except UserShangjia.DoesNotExist:
- return Response({
- 'code': 1,
- 'msg': '您不是商家或商家信息未完善',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 2. 检查商家账号状态
- if shangjia.zhuangtai != 1:
- return Response({
- 'code': 2,
- 'msg': '商家账号状态异常,无法操作',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 获取前端参数
- try:
- data = request.data
- dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
- tuikuan_liyou = data.get('tuikuan_liyou', '').strip()
- except Exception:
- return Response({
- 'code': 3,
- 'msg': '参数格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 参数验证
- if not dingdan_id or not tuikuan_liyou:
- return Response({
- 'code': 4,
- 'msg': '订单ID和退款原因不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- # 5. 使用事务确保数据一致性
- with transaction.atomic():
- # 查询订单主表
- dingdan = Order.query.filter(OrderID=dingdan_id).first()
- if not dingdan:
- return Response({
- 'code': 5,
- 'msg': '订单不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 6. 验证订单状态
- if dingdan.Status not in [2, 8]:
- return Response({
- 'code': 6,
- 'msg': '当前订单状态不可申请退款',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 7. 验证订单所属商家
- shangjia_kuozhan = MerchantOrderExt.query.filter(
- Order__OrderID=dingdan_id,
- MerchantID=current_user.UserUID
- ).first()
-
- if not shangjia_kuozhan:
- return Response({
- 'code': 7,
- 'msg': '您无权为此订单申请退款',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # ========== 8. 处理退款记录(修改点:先查询是否存在) ==========
- refund_record, created = RefundRecord.query.get_or_create(
- OrderID=dingdan_id,
- defaults={
- 'PlayerID': dingdan.PlayerID,
- 'ApplicantID': current_user.UserUID,
- 'Reason': tuikuan_liyou,
- 'ApplyStatus': 0, # 待处理
- 'Amount': dingdan.Amount,
- 'PlayerCommission': dingdan.PlayerCommission,
- 'Description': dingdan.Description,
- 'Remark': dingdan.Remark,
- 'Nickname': dingdan.Nickname,
- }
- )
- if not created:
- # 记录已存在,更新退款理由和申请时间
- refund_record.Reason = tuikuan_liyou
- refund_record.save(update_fields=['Reason', 'UpdateTime'])
- logger.info(f"订单 {dingdan_id} 已有退款记录,已更新退款理由")
-
- # 9. 更新订单状态为4(退款中)
- dingdan.Status = 4
- dingdan.RefundReason = tuikuan_liyou
- dingdan.save()
-
- # ====== 原有功能:更新打手状态 ======
- if dingdan.PlayerID:
- try:
- dashou_main = User.query.filter(
- UserUID=dingdan.PlayerID
- ).first()
- if dashou_main and hasattr(dashou_main, 'DashouProfile'):
- dashou_profile = dashou_main.DashouProfile
- dashou_profile.zhuangtai = 1
- dashou_profile.save()
- logger.info(f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.PlayerID}状态变更为1")
- except Exception as e:
- logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id},打手ID{dingdan.PlayerID}")
-
- return Response({
- 'code': 0,
- 'msg': '退款申请提交成功',
- 'data': {
- 'dingdan_id': dingdan_id,
- 'new_zhuangtai': 4,
- 'tuikuan_liyou': tuikuan_liyou,
- 'jine': float(dingdan.Amount) if dingdan.Amount else 0.0,
- 'dashou_id': dingdan.PlayerID
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"商家退款申请失败: {str(e)}", exc_info=True)
- return Response({
- 'code': 8,
- 'msg': '服务器内部错误,申请失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class ShangjiaChufaShenqingView(APIView):
- """商家处罚申请接口(已添加图片上传功能)"""
- permission_classes = [IsAuthenticated] # JWT认证
-
- def post(self, request):
- # 1. 获取当前登录用户并验证商家身份
- try:
- current_user = request.user
- shangjia = current_user.ShopProfile
- except UserShangjia.DoesNotExist:
- return Response({
- 'code': 1,
- 'msg': '您不是商家或商家信息未完善',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 2. 检查商家账号状态
- if shangjia.zhuangtai != 1:
- return Response({
- 'code': 2,
- 'msg': '商家账号状态异常,无法操作',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 获取前端参数
- try:
- data = request.data
- dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
- dashou_id = data.get('dashou_id', '').strip()
- chufa_liyou = data.get('chufa_liyou', '').strip()
- chufa_tupian_urls = data.get('chufa_tupian_urls', []) # 🔴 新增:图片URL数组
- except Exception:
- return Response({
- 'code': 3,
- 'msg': '参数格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 参数验证
- if not dingdan_id or not dashou_id or not chufa_liyou:
- return Response({
- 'code': 4,
- 'msg': '订单ID、打手ID和处罚原因不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- # 5. 使用事务确保数据一致性
- with transaction.atomic():
- # 验证打手身份
- try:
- dashou_user = User.query.get(UserUID=dashou_id)
- dashou = dashou_user.DashouProfile
- except User.DoesNotExist:
- return Response({
- 'code': 5,
- 'msg': '打手用户不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 5,
- 'msg': '该用户不是打手',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 查询订单主表
- dingdan = Order.query.filter(OrderID=dingdan_id).first()
- if not dingdan:
- return Response({
- 'code': 6,
- 'msg': '订单不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 验证订单状态
- if dingdan.Status not in [2, 8, 4, 5, 6, 3]:
- return Response({
- 'code': 7,
- 'msg': '当前订单状态不可申请处罚',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证订单所属商家
- shangjia_kuozhan = MerchantOrderExt.query.filter(
- Order__OrderID=dingdan_id,
- MerchantID=current_user.UserUID
- ).first()
-
- if not shangjia_kuozhan:
- return Response({
- 'code': 8,
- 'msg': '您无权为此订单申请处罚',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 验证订单的打手是否与提供的打手ID一致
- if dingdan.PlayerID != dashou_id:
- return Response({
- 'code': 9,
- 'msg': '该打手并非此订单的服务打手',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 创建处罚记录
- chufajilu = PenaltyRecord.query.create(
- PlayerID=dashou_id,
- ApplicantID=current_user.UserUID,
- PenaltyReason=chufa_liyou,
- ApplyStatus=0, # 待处理
- DeductedPoints=5, # 扣除5积分
- OrderID=dingdan_id,
- ClubID=resolve_penalty_record_club_id(
- order_id=dingdan_id,
- player_id=dashou_id,
- request=request,
- applicant_id=current_user.UserUID,
- user=current_user,
- ),
- )
- # 🔴【新增】在创建处罚记录后立即扣除打手积分
- # 注意:这里使用前面已经获取的dashou对象(打手扩展表)
- jifen_kouchu = chufajilu.DeductedPoints # 扣除的积分,这里是5
- if jifen_kouchu > 0:
- # 检查打手积分是否足够(UserDashou 字段为 jifen)
- if dashou.jifen < jifen_kouchu:
- dashou.jifen = 0
- else:
- dashou.jifen -= jifen_kouchu
- dashou.save()
-
- # 🔴【新增】保存处罚图片到数据库
- if chufa_tupian_urls and isinstance(chufa_tupian_urls, list):
- for tupian_url in chufa_tupian_urls:
- if tupian_url and tupian_url.strip():
- PenaltyEvidenceImage.query.create(
- OrderID=dingdan_id,
- UserID=current_user.UserUID,
- ImageURL=tupian_url
- )
-
- # 更新商家订单扩展表的处罚相关字段
- shangjia_kuozhan.PenaltyReason = chufa_liyou
- shangjia_kuozhan.PenaltyApplyStatus = 0
- shangjia_kuozhan.save()
-
- return Response({
- 'code': 0,
- 'msg': '处罚申请提交成功',
- 'data': {
- 'dingdan_id': dingdan_id,
- 'dashou_id': dashou_id,
- 'chufa_liyou': chufa_liyou,
- 'sqzhuangtai': 0,
- 'jifen': 5,
- 'tupian_count': len(chufa_tupian_urls) if chufa_tupian_urls else 0 # 返回图片数量
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"商家处罚申请失败: {str(e)}", exc_info=True)
-
- return Response({
- 'code': 10,
- 'msg': '服务器内部错误,申请失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-class DashouDingdanHuoquView(APIView):
- """
- 获取可抢订单列表(支持标签筛选)
- POST /dingdan/ddhq
- 需要JWT Token认证
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- from orders.services.fake_order_pool import (
- dashou_should_use_fake_pool,
- build_fake_order_pool_response,
- )
- if dashou_should_use_fake_pool(request.user):
- return build_fake_order_pool_response(request)
-
- # 1. 获取请求参数
- data = request.data
- leixing_id = pick_leixing_id(data)
- page = int(data.get('page', 1))
- page_size = int(data.get('page_size', 10))
- biaoqian_id = data.get('biaoqian_id')
-
- # 2. 构建基础查询条件
- base_query = Q(Status__in=[1, 7])
- if leixing_id is not None:
- base_query &= Q(ProductTypeID=leixing_id)
-
- # 3. 标签筛选:先查出符合条件的订单ID,避免跨表 JOIN 排序冲突
- if biaoqian_id and int(biaoqian_id) > 0:
- biaoqian_order_ids = DingdanBiaoqian.query.filter(
- chenghao_id=int(biaoqian_id)
- ).values_list('dingdan_id', flat=True)
- base_query &= Q(OrderID__in=list(biaoqian_order_ids))
-
- # 4. 构建基础查询集
- base_qs = Order.query.filter(base_query).order_by('-CreateTime').distinct()
-
- # 5. 获取总记录数
- total = base_qs.count()
-
- # 6. 计算分页偏移量
- offset = (page - 1) * page_size
- limit = page_size
-
- # 7. 获取当前页数据
- page_qs = base_qs[offset:offset + limit]
-
- # 8. 使用子查询获取商家昵称(Subquery 须用 Django 原生 QuerySet,不能用 .query.values())
- shangjia_sub = MerchantOrderExt.objects.filter(
- Order__OrderID=OuterRef('OrderID')
- ).values('MerchantNickname')[:1]
-
- # 9. 获取当前页所有需要的字段
- dingdan_values = page_qs.annotate(
- MerchantNickname=Subquery(shangjia_sub)
- ).values(
- 'OrderID', 'Status', 'Platform', 'PlayerCommission',
- 'AssignedID', 'AssignedResponse', 'AssignedResponseAt', 'AssignedLaterNote',
- 'ProductTypeID', 'ImageURL', 'GrabRequirement',
- 'MembershipID', 'CommissionReq', 'Description', 'Remark', 'CreateTime',
- 'MerchantNickname'
- )
-
- # 10. 转换为列表
- dingdan_list = list(dingdan_values)
-
- # 11. 批量获取指定打手的头像和昵称
- zhiding_ids = {item['AssignedID'] for item in dingdan_list if item.get('AssignedID')}
- zhiding_info = {}
- if zhiding_ids:
- dashou_query = User.query.filter(
- UserUID__in=zhiding_ids
- ).select_related('DashouProfile').annotate(
- Nickname=F('DashouProfile__nicheng')
- ).values('UserUID', 'Avatar', 'Nickname')
- zhiding_info = {
- item['UserUID']: {
- 'avatar': item['Avatar'] or '',
- 'nicheng': item['Nickname'] or ''
- }
- for item in dashou_query
- }
-
- # ============ 批量获取三层标签 ============
- order_ids = [item['OrderID'] for item in dingdan_list]
-
- # --- 订单需求标签 ---
- xuqiu_tags_map = {}
- if order_ids:
- try:
- dbq_qs = DingdanBiaoqian.query.filter(
- dingdan_id__in=order_ids
- ).select_related('chenghao')
- for dbq in dbq_qs:
- oid = dbq.dingdan_id
- if oid not in xuqiu_tags_map:
- xuqiu_tags_map[oid] = []
- ch = dbq.chenghao
- xuqiu_tags_map[oid].append({
- 'id': ch.id,
- 'mingcheng': ch.mingcheng,
- 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu)
- })
- except Exception as e:
- logger.error(f"查询订单需求标签失败: {e}", exc_info=True)
-
- # --- 所有需要查询标签的用户ID集合(指定打手 + 商家) ---
- yonghuid_set = set(zhiding_ids)
- shangjia_order_ids = [item['OrderID'] for item in dingdan_list if item['Platform'] == 2]
- shangjia_id_map = {}
- if shangjia_order_ids:
- # 使用跨表查询 Order__OrderID__in 匹配字符串订单ID
- sj_exts = MerchantOrderExt.query.filter(
- Order__OrderID__in=shangjia_order_ids
- ).values('Order__OrderID', 'MerchantID')
- for sj in sj_exts:
- shangjia_id_map[sj['Order__OrderID']] = sj['MerchantID']
- if sj['MerchantID']:
- yonghuid_set.add(sj['MerchantID'])
-
- # 将 yonghuid 字符串转换为 User 的主键 UserUUID
- user_id_map = {}
- user_avatar_map = {}
- if yonghuid_set:
- users = User.query.filter(UserUID__in=yonghuid_set).values('UserUUID', 'UserUID', 'Avatar')
- for u in users:
- user_id_map[u['UserUID']] = u['UserUUID']
- user_avatar_map[u['UserUID']] = (u['Avatar'] or '').strip()
-
- user_main_ids = list(user_id_map.values())
- user_tags_map = {}
- if user_main_ids:
- uc_qs = YonghuChenghao.query.filter(
- yonghu_id__in=user_main_ids
- ).select_related('chenghao')
- for uc in uc_qs:
- uid = uc.yonghu_id
- if uid not in user_tags_map:
- user_tags_map[uid] = []
- user_tags_map[uid].append(uc.chenghao)
-
- dashou_tags_map = {}
- shangjia_tags_map = {}
- for item in dingdan_list:
- oid = item['OrderID']
- # 指定打手标签
- zhid = item['AssignedID']
- dashou_tags = []
- if zhid and zhid in user_id_map:
- uid = user_id_map[zhid]
- if uid in user_tags_map:
- for ch in user_tags_map[uid]:
- if ch.leixing == 'dashou':
- dashou_tags.append({
- 'id': ch.id,
- 'mingcheng': ch.mingcheng,
- 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu)
- })
- dashou_tags_map[oid] = dashou_tags
-
- # 商家标签
- sj_tags = []
- if item['Platform'] == 2 and oid in shangjia_id_map:
- sj_yonghuid = shangjia_id_map[oid]
- if sj_yonghuid and sj_yonghuid in user_id_map:
- uid = user_id_map[sj_yonghuid]
- if uid in user_tags_map:
- for ch in user_tags_map[uid]:
- if ch.leixing == 'shangjia':
- sj_tags.append({
- 'id': ch.id,
- 'mingcheng': ch.mingcheng,
- 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu)
- })
- shangjia_tags_map[oid] = sj_tags
-
- # --- 身份装饰标签 + 优质商家 ---
- from jituan.services.identity_tag import (
- SHENFEN_DASHOU,
- SHENFEN_SHANGJIA,
- batch_identity_tags,
- )
- from jituan.services.club_context import resolve_effective_club_id
- from users.models import UserShangjia
-
- identity_pairs = []
- youzhi_map = {}
- merchant_uids = set(shangjia_id_map.values())
- if merchant_uids:
- for sj in UserShangjia.query.filter(
- user__UserUID__in=merchant_uids
- ).select_related('user').values('user__UserUID', 'is_youzhi_shangjia'):
- youzhi_map[sj['user__UserUID']] = bool(sj['is_youzhi_shangjia'])
-
- for item in dingdan_list:
- oid = item['OrderID']
- zhid = item.get('AssignedID')
- if zhid:
- identity_pairs.append((zhid, SHENFEN_DASHOU))
- if item['Platform'] == 2 and oid in shangjia_id_map:
- sj_uid = shangjia_id_map[oid]
- if sj_uid:
- identity_pairs.append((sj_uid, SHENFEN_SHANGJIA))
-
- club_id = resolve_effective_club_id(request, getattr(request, 'user', None))
- identity_tag_map = batch_identity_tags(club_id, identity_pairs) if identity_pairs else {}
-
- # 12. 格式化返回数据
- formatted_list = []
- for order in dingdan_list:
- oid = order['OrderID']
- zid = order.get('AssignedID')
- info = zhiding_info.get(zid, {})
- sj_uid = shangjia_id_map.get(oid) if order['Platform'] == 2 else None
- formatted_list.append({
- 'dingdan_id': order['OrderID'],
- 'zhuangtai': order['Status'],
- 'pingtai': order['Platform'],
- 'dashou_fencheng': float(order['PlayerCommission']) if order['PlayerCommission'] else 0.0,
- 'leixing_id': order['ProductTypeID'] or 0,
- 'tupian': order['ImageURL'] or '',
- 'yaoqiuleixing': order['GrabRequirement'] or 0,
- 'huiyuan_id': order['MembershipID'] or '',
- 'yajin': float(order['CommissionReq']) if order['CommissionReq'] else 0.0,
- 'jieshao': order['Description'] or '',
- 'beizhu': order['Remark'] or '',
- 'creat_time': fmt_datetime(order['CreateTime']),
- 'create_time': fmt_datetime(order['CreateTime']),
- 'sjnicheng': order['MerchantNickname'] or '',
- 'sj_avatar': user_avatar_map.get(sj_uid, '') if sj_uid else '',
- 'zhiding_uid': order['AssignedID'] or '',
- 'zhiding_avatar': info.get('avatar', ''),
- 'zhiding_nicheng': info.get('nicheng', ''),
- 'zhiding_hf': order.get('AssignedResponse'),
- 'zhiding_hf_text': ZHIDING_HF_TEXT.get(order.get('AssignedResponse'), ''),
- 'zhiding_dhj_sm': order.get('AssignedLaterNote') or '',
- 'zhiding_hf_sj': fmt_datetime(order['AssignedResponseAt']) if order.get('AssignedResponseAt') else '',
- # 三层标签
- 'xuqiu_biaoqian': xuqiu_tags_map.get(oid, []),
- 'dashou_biaoqian': dashou_tags_map.get(oid, []),
- 'shangjia_biaoqian': shangjia_tags_map.get(oid, []),
- # 身份装饰标签 + 优质商家
- 'shangjia_identity_biaoqian': identity_tag_map.get((sj_uid, SHENFEN_SHANGJIA), []) if sj_uid else [],
- 'zhiding_identity_biaoqian': identity_tag_map.get((zid, SHENFEN_DASHOU), []) if zid else [],
- 'shangjia_youzhi': bool(youzhi_map.get(sj_uid)) if sj_uid else False,
- })
-
- has_more = (page * page_size) < total
-
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': {
- 'list': formatted_list,
- 'has_more': has_more,
- 'total': total,
- 'current_page': page,
- 'page_size': page_size,
- 'total_pages': (total + page_size - 1) // page_size,
- }
- })
-
- except Exception as e:
- logger.error(f"订单列表查询失败: {e}", exc_info=True)
- return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
-
- def _parse_texiao_json(self, texiao_miaoshu):
- if not texiao_miaoshu:
- return {}
- try:
- return json.loads(texiao_miaoshu)
- except:
- return {}
-
-
-
-
-
-class ShangpinLeixingHuoquView(APIView):
- """
- 获取商品类型列表
- POST /dingdan/ddlxhq
- 需要JWT Token认证
- """
- authentication_classes = [JWTAuthentication]
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 查询商品类型表
- # 假设你的商品类型模型字段为:id, jieshao, tupian_url
- leixing_list = ShangpinLeixing.query.all().values(
- 'id', # 商品类型ID
- 'jieshao', # 类型介绍
- 'tupian_url' # 类型图片URL(相对URL)
- ).order_by('id') # 按ID排序,可根据需要调整
-
- # 2. 格式化返回数据
- formatted_list = []
- for leixing in leixing_list:
- formatted_leixing = {
- 'id': leixing['id'],
- 'jieshao': leixing['jieshao'] or '', # 处理空值
- 'tupian_url': leixing['tupian_url'] or '' # 处理空值
- }
- formatted_list.append(formatted_leixing)
-
- # 3. 返回响应
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': {
- 'list': formatted_list,
- 'total': len(formatted_list)
- }
- })
-
-
-
-
-# ----------------- 基础配置获取 -----------------
-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})
-
-
-class QiangdanView(APIView):
- authentication_classes = [JWTAuthentication]
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- data = request.data
- dingdan_id = data.get('dingdan_id') or data.get('OrderID')
- if not dingdan_id:
- return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400)
-
- dingdan_id = str(dingdan_id).strip()
- if dingdan_id.upper().startswith('JD'):
- return Response({'code': 400, 'msg': '您没充值会员或保证金不可接单'})
-
- try:
- with transaction.atomic():
- # 1. 验证打手身份和状态
- dashou_profile = self._validate_dashou_status(request.user)
- if not dashou_profile:
- return Response({'code': 403, 'msg': '您不是打手或账号状态异常'}, status=403)
-
-
- # ==================== 【新增】罚单检查 ====================
- fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(request.user.UserUID,1)
- if not fadan_ok:
- return Response({'code': 400, 'msg': fadan_msg})
- # =========================================================
- # ========== 标签快速校验(提前拒绝,避免锁资源) ==========
- biaoqian_ok, biaoqian_msg = check_dashou_biaoqian_required(dingdan_id, request.user.UserUID)
- if not biaoqian_ok:
- return Response({'code': 400, 'msg': biaoqian_msg})
- # ===========================================================
-
- # 2. 锁定并验证订单(行锁)
- order_result = self._validate_and_lock_order(dingdan_id, request.user.UserUID)
- if isinstance(order_result, str):
- return Response({'code': 400, 'msg': order_result})
- order = order_result
-
- validation_result = self._validate_order_requirements(order, dashou_profile, request.user.UserUID)
- if not validation_result['success']:
- return Response({'code': 400, 'msg': validation_result['message']})
- self._execute_qiangdan(order, dashou_profile, request.user.UserUID)
-
- # ========== 【唯一改动】聊天建立方式 ==========
- chat_success = establish_order_chat(dingdan_id)
-
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=order.PlayerCommission,
- action=1
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
-
-
- # 在 QiangdanView.post 里,_execute_qiangdan 之后添加:
- # 记录抢单表现(不影响主流程)
- # 如果订单表里有 bankuai 信息,可以用 order.bankuai 代替 None
- record_dashou_biaoxian(
- dingdan_id=dingdan_id,
- dashou_id=order.PlayerID,
- xingwei=1,
- jiedan_time=timezone.now(), # 直接取当前时间
- bankuai=None # 暂不填,后续提交时补上
- )
-
- return Response({
- 'code': 200,
- 'msg': '抢单成功',
- 'data': {
- 'dingdan_id': dingdan_id,
- 'chat_established': chat_success
- }
- })
-
- except Exception as e:
- logger.error(f"抢单异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '系统异常,请稍后重试'}, status=500)
-
- # ---------- 以下所有方法原封不动照抄旧版 ----------
- def _validate_dashou_status(self, user):
- try:
- dashou_profile = user.DashouProfile
- if (dashou_profile.zhuangtai != 1 or
- dashou_profile.zhanghaozhuangtai != 1 or
- dashou_profile.jifen < 5):
- return None
- return dashou_profile
- except UserDashou.DoesNotExist:
- return None
-
- def _validate_and_lock_order(self, dingdan_id, dashou_yonghuid):
- try:
- order = Order.query.select_for_update().get(OrderID=dingdan_id)
- if order.Status not in [1, 7]:
- return "订单状态不可抢"
- if order.Status == 7:
- if not order.AssignedID or order.AssignedID != dashou_yonghuid:
- return "此订单为指定单,您不是指定打手"
- if order.PlayerID:
- return "订单已被其他打手抢走"
- return order
- except Order.DoesNotExist:
- return "订单不存在"
-
- def _validate_order_requirements(self, order, dashou_profile, dashou_yonghuid):
- if order.GrabRequirement == 1:
- return self._validate_huiyuan_requirement(order, dashou_yonghuid)
- elif order.GrabRequirement == 2:
- return self._validate_yajin_requirement(order, dashou_profile)
- return {'success': True, 'message': '验证通过'}
-
- def _validate_huiyuan_requirement(self, order, dashou_yonghuid):
- try:
- huiyuan_record = Huiyuangoumai.query.get(
- huiyuan_id=order.MembershipID,
- yonghu_id=dashou_yonghuid
- )
- if huiyuan_record.jiance_shifou_daoqi() or huiyuan_record.huiyuan_zhuangtai != 1:
- return {'success': False, 'message': '会员已过期或未开通对应会员'}
- return {'success': True, 'message': '会员验证通过'}
- except Huiyuangoumai.DoesNotExist:
- return {'success': False, 'message': '未开通对应会员,无法抢此订单'}
-
- def _validate_yajin_requirement(self, order, dashou_profile):
- yajin_required = order.CommissionReq or 0
- dashou_yajin = dashou_profile.yajin or 0
- if dashou_yajin < yajin_required:
- return {'success': False, 'message': f'押金不足,需要{yajin_required}元,当前有{dashou_yajin}元'}
- return {'success': True, 'message': '押金验证通过'}
-
- def _execute_qiangdan(self, order, dashou_profile, dashou_yonghuid):
- order.Status = 2
- order.PlayerID = dashou_yonghuid
- order.User2ID = f"Ds{dashou_yonghuid}"
- order.save()
-
- dashou_profile.zhuangtai = 0
- dashou_profile.jiedanzongliang = F('jiedanzongliang') + 1
- dashou_profile.jinrijiedan = F('jinrijiedan') + 1
- dashou_profile.jinyuejiedan = F('jinyuejiedan') + 1
- dashou_profile.save()
-
-
-class DashouDingdanHuoquView1(APIView):
- """
- 打手订单获取接口
- 支持筛选条件:
- - zhuangtai_list: 订单状态列表(可选,空数组或不传表示全部状态)
- - leixing_id: 商品类型ID(可选)
- - keyword: 搜索关键词,模糊匹配订单ID和订单介绍(可选)
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取当前登录用户的 yonghuid
- try:
- current_user = request.user
- yonghuid = current_user.UserUID
- except AttributeError:
- return Response({
- 'code': 1,
- 'msg': '用户认证失败',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 获取并校验前端参数
- try:
- data = request.data
- zhuangtai_list = data.get('zhuangtai_list', []) # 可选,默认空列表(全部)
- page = max(1, int(data.get('page', 1)))
- page_size = min(100, max(1, int(data.get('page_size', 10))))
- leixing_id = pick_leixing_id(data) # 可选,商品类型ID
- keyword = data.get('keyword', '').strip() # 可选,搜索关键词
- except (ValueError, TypeError):
- return Response({
- 'code': 2,
- 'msg': '参数格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 参数默认值与类型修正
- if not isinstance(zhuangtai_list, list):
- zhuangtai_list = [] # 非列表则视为全部
-
- # 4. 构建查询条件
- try:
- # 基础条件:当前打手接单
- query = Q(PlayerID=yonghuid)
-
- # 状态筛选:如果 zhuangtai_list 不为空列表,则加入筛选;否则查询全部状态
- if zhuangtai_list:
- query &= Q(Status__in=zhuangtai_list)
-
- # 商品类型筛选
- if leixing_id is not None:
- query &= Q(ProductTypeID=leixing_id)
-
- # 关键词搜索:模糊匹配订单ID和订单介绍
- if keyword:
- keyword_query = Q(OrderID__icontains=keyword) | Q(Description__icontains=keyword)
- query &= keyword_query
-
- # 5. 执行查询,按创建时间倒序
- dingdan_query = Order.query.filter(query).order_by('-CreateTime')
-
- # 只选择需要的字段,提高性能
- dingdan_query = dingdan_query.only(
- 'OrderID',
- 'Status',
- 'PlayerCommission', # 打手分成金额
- 'Description',
- 'ImageURL',
- 'Nickname',
- 'CreateTime'
- )
-
- total_count = dingdan_query.count()
-
- # 6. 分页处理
- start_index = (page - 1) * page_size
- end_index = start_index + page_size
- current_page_orders = dingdan_query[start_index:end_index]
- current_page_count = len(current_page_orders)
-
- # 7. 构建返回数据列表
- order_list = []
- for order in current_page_orders:
- order_data = {
- 'dingdan_id': order.OrderID,
- 'zhuangtai': order.Status,
- 'jine': float(order.PlayerCommission) if order.PlayerCommission else 0.0,
- 'jieshao': order.Description or '',
- 'tupian': order.ImageURL or '',
- 'nicheng': order.Nickname or '',
- **datetime_aliases(order.CreateTime),
- }
- order_list.append(order_data)
-
- # 8. 判断是否还有更多数据
- has_more = (current_page_count == page_size) and (end_index < total_count)
- if total_count == 0:
- has_more = False
-
- return Response({
- 'code': 0,
- 'msg': '成功',
- 'data': {
- 'list': order_list,
- 'has_more': has_more,
- 'total': total_count
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"打手订单获取失败: {str(e)}", exc_info=True)
-
- return Response({
- 'code': 4,
- 'msg': '服务器内部错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-class DashouCOSZhengshuView(APIView):
- """打手获取COS上传凭证接口(调试版本)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 接收参数(兼容 OrderID / dingdan_id)
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
- yongtu = request.data.get('yongtu', 'dingdan')
-
- # 2. 条件判断修正:除了 fakuan 之外,其他用途均需提供 dingdan_id
- if yongtu != 'fakuan' and not dingdan_id:
- return Response(
- {'code': 400, 'msg': '订单ID不能为空', 'error': 'dingdan_id is required when yongtu is not fakuan'},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 3. 业务验证逻辑(根据用途分别验证)
- if yongtu == 'chufa':
- try:
- chufa_record = PenaltyRecord.query.filter(
- OrderID=dingdan_id,
- PlayerID=request.user.UserUID,
- ApplyStatus__in=[0, 3]
- ).first()
- if not chufa_record:
- return Response(
- {'code': 403, 'msg': '处罚记录无权限或状态不对',
- 'error': f'No punishment record found for user {request.user.UserUID} on order {dingdan_id}'},
- status=status.HTTP_403_FORBIDDEN
- )
- except Exception as e:
- logger.error(f"查处罚记录异常: {str(e)}")
- return Response(
- {'code': 500, 'msg': '服务器内部错误', 'error': str(e)},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
- elif yongtu == 'fakuan':
- # 罚款申诉不需要订单验证
- pass
- elif yongtu == 'fakuan_evidence':
- # 商家上传罚单证据图(Purpose=1)
- fadan_id = request.data.get('fadan_id')
- if not dingdan_id or not fadan_id:
- return Response(
- {'code': 400, 'msg': '缺少订单ID或罚单ID'},
- status=status.HTTP_400_BAD_REQUEST,
- )
- try:
- shangjia_profile = request.user.ShopProfile
- if shangjia_profile.zhuangtai != 1:
- return Response({'code': 403, 'msg': '商家状态异常'}, status=403)
- except Exception:
- return Response({'code': 403, 'msg': '非商家无权上传'}, status=403)
- fadan = Penalty.query.filter(
- id=fadan_id,
- RelatedOrderID=dingdan_id,
- ApplicantID=request.user.UserUID,
- ).first()
- if not fadan:
- return Response({'code': 403, 'msg': '罚单不存在或无权操作'}, status=403)
- elif yongtu == 'fakuan_evidence_draft':
- # 商家申请罚款前上传证据图(仅需订单归属校验)
- if not dingdan_id:
- return Response({'code': 400, 'msg': '缺少订单ID'}, status=400)
- try:
- shangjia_profile = request.user.ShopProfile
- if shangjia_profile.zhuangtai != 1:
- return Response({'code': 403, 'msg': '商家状态异常'}, status=403)
- except Exception:
- return Response({'code': 403, 'msg': '非商家无权上传'}, status=403)
- try:
- MerchantOrderExt.query.get(Order__OrderID=dingdan_id, MerchantID=request.user.UserUID)
- except MerchantOrderExt.DoesNotExist:
- return Response({'code': 403, 'msg': '订单不存在或无权操作'}, status=403)
- else:
- # 默认为订单相关,需要验证订单且打手必须接单
- try:
- dingdan = Order.query.get(
- OrderID=dingdan_id,
- PlayerID=request.user.UserUID,
- Status__in=[2, 8]
- )
- except Order.DoesNotExist:
- return Response(
- {'code': 403, 'msg': '此功能暂未开放', 'error': f'Order {dingdan_id} not found or not assigned to user {request.user.UserUID}'},
- status=status.HTTP_403_FORBIDDEN
- )
-
- # 4. 获取腾讯云STS临时密钥
- try:
- # 初始化固定凭证
- cred = credential.Credential(
- settings.COS_SECRET_ID,
- settings.COS_SECRET_KEY
- )
-
- # 创建STS客户端
- http_profile = HttpProfile()
- http_profile.endpoint = "sts.tencentcloudapi.com"
- client_profile = ClientProfile()
- client_profile.httpProfile = http_profile
- client = sts_client.StsClient(cred, "ap-shanghai", client_profile)
-
- # 构建策略资源
- resources = []
- if yongtu == 'chufa':
- resources.append(
- f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/dssstp/*"
- )
- elif yongtu == 'fakuan':
- resources.append(
- f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/dashoufakuan/dashoufakuanshensu/*"
- )
- elif yongtu == 'fakuan_evidence':
- resources.append(
- f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/shangjiafakuan/zhengju/*"
- )
- elif yongtu == 'fakuan_evidence_draft':
- resources.append(
- f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/shangjiafakuan/zhengju/*"
- )
- else:
- resources.append(
- f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*"
- )
-
- policy = {
- "version": "2.0",
- "statement": [{
- "effect": "allow",
- "action": [
- "cos:PutObject",
- "cos:PostObject",
- "cos:InitiateMultipartUpload",
- "cos:UploadPart",
- "cos:CompleteMultipartUpload"
- ],
- "resource": resources
- }]
- }
-
- req = models.GetFederationTokenRequest()
- req.Name = f"dashou_{dingdan_id}_{int(time.time())}"
- req.Policy = json.dumps(policy)
- req.DurationSeconds = 7200
-
- resp = client.GetFederationToken(req)
- credentials = resp.Credentials
-
- except TencentCloudSDKException as e:
- logger.error(f"获取STS临时密钥失败: {str(e)}")
- return Response({
- 'code': 500,
- 'msg': f'获取上传凭证失败: {str(e)}',
- 'error_detail': str(e)
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 5. 构建响应
- current_time = int(time.time())
- upload_dir = (
- 'fakuan/shangjiafakuan/zhengju/' if yongtu == 'fakuan_evidence' else
- 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else (
- 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/'
- )
- )
- response_data = {
- 'credentials': {
- 'tmpSecretId': credentials.TmpSecretId,
- 'tmpSecretKey': credentials.TmpSecretKey,
- 'sessionToken': credentials.Token,
- },
- 'startTime': current_time,
- 'expiredTime': current_time + 7200,
- 'bucket': settings.COS_BUCKET,
- 'region': settings.COS_REGION,
- 'upload_dir': upload_dir,
- }
- return Response({'code': 0, 'msg': '成功', 'data': response_data})
-
- except Exception as e:
- # 捕捉所有未预期的异常,返回完整 traceback
- logger.error(f'COS凭证接口未知错误: {traceback.format_exc()}')
- return Response({
- 'code': 500,
- 'msg': f'服务器内部错误: {str(e)}',
- 'error_detail': traceback.format_exc()
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class ShangjiaCOSZhengshuView(APIView):
- """商家获取COS上传凭证接口(用于处罚申诉图片上传)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 接收参数
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
- yongtu = request.data.get('yongtu', 'chufa') # 用途:chufa=处罚申诉
-
- if not dingdan_id:
- return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 🔴【修改】验证商家身份 - 通过反向查询shop_profile判断是否为商家
- try:
- # 检查是否有商家扩展表
- shangjia_profile = request.user.ShopProfile
-
- # 验证商家状态是否为1(正常)
- if shangjia_profile.zhuangtai != 1:
- return Response({'code': 3, 'msg': '商家账号状态异常,无法操作', 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- # 🔴【重要】获取商家ID(从商家扩展表中获取)
- shangjia_id = request.user.UserUID # 假设商家扩展表中有yonghuid字段
-
- except AttributeError:
- # 没有shop_profile属性,说明不是商家
- return Response({'code': 3, 'msg': '非商家用户无权限', 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- # 3. 🔴【修改】验证订单权限 - 通过商家订单扩展表验证
- try:
- # 获取订单
- dingdan = Order.query.get(OrderID=dingdan_id)
-
- # 🔴【重要】通过反向查询商家扩展表验证订单是否属于当前商家
- try:
- shangjia_kuozhan = dingdan.shangjia_kuozhan # 使用反向关系名
-
- # 验证商家ID是否匹配
- if str(shangjia_kuozhan.MerchantID) != str(shangjia_id):
- return Response({'code': 2, 'msg': '订单不属于当前商家', 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- # 验证订单状态是否允许申请处罚(根据前端逻辑)
- allowed_zhuangtai = [2, 4, 5, 6, 8]
- if dingdan.Status not in allowed_zhuangtai:
- return Response({'code': 4, 'msg': '当前订单状态不允许申请处罚', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 🔴【新增】检查是否已存在处罚申请(通过Chufajilu模型)
- # 注意:这里假设Chufajilu模型存在,并且有shangjia_id字段
- try:
- existing_chufa = PenaltyRecord.query.filter(
- OrderID=dingdan_id,
- ApplicantID=shangjia_id,
- ApplyStatus__in=[0, 1] # 审核中或已处罚状态
- ).exists()
-
- if existing_chufa:
- return Response({'code': 5, 'msg': '该订单已有处罚申请,请勿重复上传', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
- except ImportError:
- # 如果没有Chufajilu模型,跳过这个检查
- pass
-
- except MerchantOrderExt.DoesNotExist:
- # 没有商家扩展记录,说明这不是商家订单
- return Response({'code': 2, 'msg': '非商家订单无法操作', 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- except Order.DoesNotExist:
- return Response({'code': 2, 'msg': '订单不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"订单验证异常: {str(e)}")
- return Response({'code': 500, 'msg': '服务器验证异常', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 4. 获取腾讯云STS临时密钥
- try:
- # 初始化固定凭证
- cred = credential.Credential(
- settings.COS_SECRET_ID,
- settings.COS_SECRET_KEY
- )
-
- # 创建STS客户端
- http_profile = HttpProfile()
- http_profile.endpoint = "sts.tencentcloudapi.com"
- client_profile = ClientProfile()
- client_profile.httpProfile = http_profile
-
- client = sts_client.StsClient(cred, "ap-shanghai", client_profile)
-
- # 🔴【修改】构建策略 - 只允许上传到商家处罚证据图片目录
- resources = [
- f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/sjzjtp/*"
- ]
-
- policy = {
- "version": "2.0",
- "statement": [
- {
- "effect": "allow",
- "action": [
- "cos:PutObject",
- "cos:PostObject",
- "cos:InitiateMultipartUpload",
- "cos:UploadPart",
- "cos:CompleteMultipartUpload"
- ],
- "resource": resources
- }
- ]
- }
-
- # 请求STS临时凭证
- req = models.GetFederationTokenRequest()
- # 🔴【修改】命名包含商家ID
- req.Name = f"shangjia_{shangjia_id}_{dingdan_id}_{int(time.time())}"
- req.Policy = json.dumps(policy)
- req.DurationSeconds = 7200 # 2小时有效期
-
- resp = client.GetFederationToken(req)
-
- # 提取临时密钥
- credentials = resp.Credentials
-
- except TencentCloudSDKException as e:
- logger.error(f"商家获取STS临时密钥失败: {str(e)}")
- return Response({
- 'code': 500,
- 'msg': f'获取上传凭证失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 5. 构建响应数据
- current_time = int(time.time())
- response_data = {
- 'credentials': {
- 'tmpSecretId': credentials.TmpSecretId,
- 'tmpSecretKey': credentials.TmpSecretKey,
- 'sessionToken': credentials.Token,
- },
- 'startTime': current_time,
- 'expiredTime': current_time + 7200,
- 'bucket': settings.COS_BUCKET,
- 'region': settings.COS_REGION,
- # 🔴【修改】商家专用上传目录
- 'upload_dir': 'a_long/chfajltp/sjzjtp/',
- # 返回商家ID和订单ID,便于前端生成文件名
- 'dingdan_id': dingdan_id,
- 'shangjia_id': shangjia_id,
- # 返回商家昵称,便于前端显示
- 'shangjia_nicheng': shangjia_profile.nicheng if hasattr(shangjia_profile, 'nicheng') else ''
- }
-
- # 6. 返回成功
- return Response({
- 'code': 0,
- 'msg': '成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f'商家COS凭证接口未知错误: {str(e)}', exc_info=True)
- return Response({
- 'code': 500,
- 'msg': f'服务器内部错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class DashouTijiaoView(APIView):
- """打手提交订单完成接口"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取参数
- data = request.data
- dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
- liuyan = data.get('liuyan', '')
- jiaofu_tupian_urls = data.get('jiaofu_tupian_urls', [])
-
- # 2. 参数验证
- if not dingdan_id:
- return Response({
- 'code': 1,
- 'msg': '订单ID不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not isinstance(jiaofu_tupian_urls, list):
- return Response({
- 'code': 2,
- 'msg': '图片URL列表格式错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 限制图片数量(最多9张,最少1张)
- if len(jiaofu_tupian_urls) == 0:
- return Response({
- 'code': 3,
- 'msg': '至少需要上传一张图片',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if len(jiaofu_tupian_urls) > 9:
- return Response({
- 'code': 4,
- 'msg': '最多只能上传9张图片',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 留言长度限制(50字)
- if liuyan and len(liuyan) > 50:
- return Response({
- 'code': 5,
- 'msg': '留言最多50个字',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 获取当前用户并验证打手身份
- current_user = request.user
- yonghuid = current_user.UserUID
-
- try:
- dashou_profile = current_user.DashouProfile
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 6,
- 'msg': '用户不是打手身份',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 4. 验证订单状态和权限
- try:
- dingdan = Order.query.get(
- OrderID=dingdan_id,
- PlayerID=yonghuid,
- Status=2 # 必须是进行中的订单
- )
- except Order.DoesNotExist:
- return Response({
- 'code': 7,
- 'msg': '订单不存在或没有提交权限',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 5. 存储图片到Dashoutupian表
- for tupian_url in jiaofu_tupian_urls:
- # 创建打手提交图片记录
- PlayerDeliveryImage.query.create(
- OrderID=dingdan_id,
- PlayerID=yonghuid,
- ImageURL=tupian_url,
- CreateTime=timezone.now()
- )
-
- # 6. 更新订单主表
- dingdan.PlayerRemark = liuyan
- dingdan.Status = 8 # 状态改为结算中
- dingdan.UpdateTime = timezone.now()
- dingdan.save()
-
- # 7. 更新打手扩展表状态
- dashou_profile.zhuangtai = 1 # 改为数字1(根据你之前说的)
- dashou_profile.save()
-
- # ========== 新增:记录打手提交时间 ==========
- record_dashou_biaoxian(
- dingdan_id=dingdan_id,
- dashou_id=yonghuid,
- xingwei=2, # 提交
- tijiao_time=timezone.now()
- )
- # ===========================================
-
- # 8. 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '订单提交成功',
- 'data': {
- 'dingdan_id': dingdan_id,
- 'new_zhuangtai': 8, # 新的订单状态
- 'dashou_zhuangtai': 1 # 打手新的状态
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"打手提交订单失败: {str(e)}", exc_info=True)
-
- return Response({
- 'code': 8,
- 'msg': '服务器内部错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class DashouDingdanXiangqingView(APIView):
- """打手订单详情获取接口"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取订单ID
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
- if not dingdan_id:
- return Response({
- 'code': 1,
- 'msg': '订单ID不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 获取当前用户并验证权限
- current_user = request.user
- yonghuid = current_user.UserUID
-
- try:
- dashou_profile = current_user.DashouProfile
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 2,
- 'msg': '用户不是打手身份',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 查询订单主表
- try:
- dingdan = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({
- 'code': 3,
- 'msg': '订单不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 验证订单是否属于当前打手
- if dingdan.PlayerID != yonghuid:
- return Response({
- 'code': 4,
- 'msg': '没有权限查看此订单',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 5. 构建基础订单数据
- dingdan_data = {
- 'dingdan_id': dingdan.OrderID,
- 'zhuangtai': dingdan.Status,
- 'jine': float(dingdan.PlayerCommission) if dingdan.PlayerCommission else 0.0,
- 'jieshao': dingdan.Description or '',
- 'tupian': dingdan.ImageURL or '',
- 'nicheng': dingdan.Nickname or '',
- 'beizhu': dingdan.Remark or '',
- **datetime_aliases(dingdan.CreateTime),
- 'tuikuan_liyou':dingdan.RefundReason or '',
- 'dashou_liuyan': dingdan.PlayerRemark or '',
- 'fuwudashou_id': dingdan.PlayerID or '',
- 'fadanpingtai': dingdan.Platform or ''
- }
-
- # 6. 查询打手提交的图片
- dashou_tupian_list = PlayerDeliveryImage.query.filter(
- OrderID=dingdan_id,
- PlayerID=yonghuid
- ).order_by('CreateTime')
-
- jiaofu_tupian_urls = []
- for tupian_record in dashou_tupian_list:
- if tupian_record.ImageURL:
- jiaofu_tupian_urls.append(tupian_record.ImageURL)
-
- dingdan_data['dashou_jiaofu'] = jiaofu_tupian_urls
-
- # 7. 查询商家扩展表信息
- shangjia_info = {}
- try:
- shangjia_kuozhan = dingdan.shangjia_kuozhan
- shangjia_info = {
- 'shangjia_id': shangjia_kuozhan.MerchantID or '',
- 'shangjia_nicheng': shangjia_kuozhan.MerchantNickname or '',
- 'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '',
- #'tuikuan_liyou': '',
- 'chufa_liyou': shangjia_kuozhan.PenaltyReason or '',
- 'chufa_zhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0,
- #'chufa_jieguo': shangjia_kuozhan.chufa_jieguo or '',
- 'bohui_liyou': shangjia_kuozhan.RejectReason or ''
- }
- except MerchantOrderExt.DoesNotExist:
- # 没有商家扩展信息,返回空值
- shangjia_info = {
- 'shangjia_id': '',
- 'shangjia_nicheng': '',
- 'shangjia_liuyan': '',
- 'tuikuan_liyou': '',
- 'chufa_liyou': '',
- 'chufa_zhuangtai': 0,
- 'chufa_jieguo': '',
- 'bohui_liyou': ''
- }
-
- # 8. 查询平台扩展表信息(如果有的话)
- pingtai_info = {}
- try:
- pingtai_kuozhan = dingdan.pingtai_kuozhan
- pingtai_info = {
- 'laoban_id': pingtai_kuozhan.BossID or '',
- 'laoban_pingjia': pingtai_kuozhan.BossReview or ''
- }
- except PlatformOrderExt.DoesNotExist:
- pingtai_info = {
- 'laoban_id': '',
- 'laoban_pingjia': ''
- }
-
- # 9. 合并所有数据
- response_data = {
- **dingdan_data,
- **shangjia_info,
- **pingtai_info
- }
-
- return Response({
- 'code': 0,
- 'msg': '成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"获取打手订单详情失败: {str(e)}", exc_info=True)
-
- return Response({
- 'code': 5,
- 'msg': '服务器内部错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-class AdGetDingdanXiangQing(APIView):
- """
- 后台订单详情获取接口
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /dingdan/adddxq
- 参数: {"zhanghao": "管理员手机号", "dingdan_id": "订单ID"}
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- """
- 获取订单详情数据
- 返回: 订单主表数据 + 扩展表数据 + 打手图片列表
- """
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
-
- if not qianzhanghao or not dingdan_id:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证 (IsAuthenticated已处理)
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 查询订单主表
- try:
-
- # 查询订单主表 - 使用select_related预加载关联表
- dingdan_obj = Order.query.select_related(
- 'pingtai_kuozhan', 'shangjia_kuozhan'
- ).get(OrderID=dingdan_id)
-
- except Order.DoesNotExist:
- return Response(
- {'code': 404, 'message': '订单不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 7. 构建返回数据 - 主表字段
- response_data = {
- # 基础信息字段
- 'dingdan_id': dingdan_obj.OrderID or '',
- 'jieshao': dingdan_obj.Description or '',
- 'zhuangtai': dingdan_obj.Status or 0,
- 'leixing': dingdan_obj.ProductTypeID or 0, # 前端需要 leixing,对应 leixing_id
- 'fadanpingtai': dingdan_obj.Platform or 0, # 前端需要 fadanpingtai,对应 fadan_pingtai
- 'jiage': float(dingdan_obj.Amount) if dingdan_obj.Amount else 0.00, # 前端需要 jiage,对应 jine
- 'tupian': dingdan_obj.ImageURL or '',
- 'beizhu': dingdan_obj.Remark or '',
-
- # 服务信息字段
- 'jiedan_dashou_id': dingdan_obj.PlayerID or '',
- 'dashou_fencheng': float(dingdan_obj.PlayerCommission) if dingdan_obj.PlayerCommission else 0.00,
- 'zhiding_id': dingdan_obj.AssignedID or '',
- 'dashou_liuyan': dingdan_obj.PlayerRemark or '',
-
- # 时间字段
- 'chuangjianshijian': fmt_datetime(dingdan_obj.CreateTime),
- 'create_time': fmt_datetime(dingdan_obj.CreateTime),
- 'genggaishijian': fmt_datetime(dingdan_obj.UpdateTime),
- 'update_time': fmt_datetime(dingdan_obj.UpdateTime),
- }
-
- # 8. 根据发单平台查询扩展表数据
- fadan_pingtai = dingdan_obj.Platform
-
- if fadan_pingtai == 1:
- # 平台发单 - 查询平台订单扩展表
- try:
- # 使用反向关联 pingtai_kuozhan
- pingtai_kuozhan = dingdan_obj.pingtai_kuozhan
-
- response_data.update({
- 'laoban_id': pingtai_kuozhan.BossID or '',
- 'nicheng': dingdan_obj.Nickname or '', # 这个字段在订单主表
- 'laoban_pingjia': pingtai_kuozhan.BossReview or '',
- })
- except PlatformOrderExt.DoesNotExist:
- # 扩展表不存在,返回空值
- response_data.update({
- 'laoban_id': '',
- 'nicheng': dingdan_obj.Nickname or '',
- 'laoban_pingjia': '',
- })
-
- elif fadan_pingtai == 2:
- # 商家发单 - 查询商家订单扩展表
- try:
- # 使用反向关联 shangjia_kuozhan
- shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
-
- response_data.update({
- 'shangjia_id': shangjia_kuozhan.MerchantID or '',
- 'shangjia_nicheng': shangjia_kuozhan.MerchantNickname or '',
- 'shangjia_pingjia': shangjia_kuozhan.MerchantReview or '',
- 'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 商家处罚理由
- 'bhliyou': shangjia_kuozhan.RejectReason or '', # 驳回理由
- })
- except MerchantOrderExt.DoesNotExist:
- # 扩展表不存在,返回空值
- response_data.update({
- 'shangjia_id': '',
- 'shangjia_nicheng': '',
- 'shangjia_pingjia': '',
- 'cfliyou': '',
- 'bhliyou': '',
- })
-
- # 9. 查询打手提交的图片
- try:
- # 查询该订单的所有打手提交图片
- dashou_tupian_list = PlayerDeliveryImage.query.filter(
- OrderID=dingdan_id
- ).values_list('ImageURL', flat=True)
-
- # 转换为列表,过滤空值
- dashou_images = [img for img in dashou_tupian_list if img]
- response_data['dashou_images'] = dashou_images
- except Exception as e:
- logger.warning(f"查询打手图片失败: {str(e)}")
- response_data['dashou_images'] = []
-
- # 🔥 新增:查询退款记录表中的商家退款理由
- try:
- # 导入退款记录模型
- # 查询该订单的退款记录(一个订单可能有多次退款申请,我们取最新的)
- tuikuan_record = RefundRecord.query.filter(
- OrderID=dingdan_id
- ).order_by('-CreateTime').first() # 按创建时间倒序,取最新的
-
- if tuikuan_record and tuikuan_record.Reason:
- # 如果有退款记录且理由不为空,添加到返回数据
- response_data['tuikuan_liyou'] = tuikuan_record.Reason
- else:
- # 没有找到退款记录或理由为空
- response_data['tuikuan_liyou'] = ''
- except RefundRecord.DoesNotExist:
- # 退款记录表不存在该订单的记录
- response_data['tuikuan_liyou'] = ''
- except Exception as e:
- # 查询过程中出现其他异常
- logger.warning(f"查询退款记录失败: {str(e)}")
- response_data['tuikuan_liyou'] = ''
-
- # 10. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': response_data
- })
-
- except Exception as e:
- logger.error(f"获取订单详情异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-# 获取日志器
-logger = logging.getLogger(__name__)
-
-
-class AdGengHuanDaShou(APIView):
- """
- 后台更换打手接口
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /dingdan/adddgh
- 参数: {
- "zhanghao": "管理员账号",
- "dingdan_id": "订单ID",
- "new_dashou_id": "新打手ID"
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- """
- 更换订单的打手
- 核心逻辑:
- 1. 验证管理员身份
- 2. 验证订单存在且状态为进行中(2)或结算中(8)
- 3. 验证新打手ID存在且是打手身份
- 4. 验证新打手状态正常(Status=1且zhanghaoStatus=1)
- 5. 根据订单的抢单要求类型进行验证:
- - 会员抢单(1):验证打手有对应会员且未过期
- - 押金抢单(2):验证打手押金≥订单押金
- 6. 更新新打手状态和统计数据
- 7. 更新原打手状态和统计数据
- 8. 更新订单信息
- """
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
- new_dashou_id = request.data.get('new_dashou_id', '').strip()
-
- if not qianzhanghao or not dingdan_id or not new_dashou_id:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 查询订单主表
- try:
- dingdan_obj = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response(
- {'code': 404, 'message': '订单不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 7. 验证订单状态是否为进行中(2)或结算中(8)
- if dingdan_obj.Status not in [2, 8]:
- return Response(
- {'code': 400, 'message': '订单状态不允许更换打手', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 8. 验证新打手ID是否存在,并且是打手身份
- try:
- new_dashou_user = User.query.get(
- UserUID=new_dashou_id,
- #user_type='PlayerID'
- )
- except User.DoesNotExist:
- return Response(
- {'code': 404, 'message': '打手不存在或不是打手身份', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 9. 获取新打手扩展表
- try:
- new_dashou_profile = new_dashou_user.DashouProfile
- except AttributeError:
- return Response(
- {'code': 404, 'message': '打手信息不完整', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 10. 验证新打手状态:Status=1且zhanghaoStatus=1
- if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
- return Response(
- {'code': 400, 'message': '打手状态不允许接单', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 11. 根据订单的抢单要求类型进行验证
- yaoqiuleixing = dingdan_obj.GrabRequirement
-
- if yaoqiuleixing == 1: # 会员抢单
- # 获取订单的会员ID
- huiyuan_id = dingdan_obj.MembershipID
- if not huiyuan_id:
- return Response(
- {'code': 400, 'message': '订单会员ID为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 查询会员购买记录
- try:
- huiyuan_goumai = Huiyuangoumai.query.get(
- huiyuan_id=huiyuan_id,
- yonghu_id=new_dashou_id # 注意:yonghu_id存储的是用户ID(yonghuid)
- )
- except Huiyuangoumai.DoesNotExist:
- return Response(
- {'code': 400, 'message': '打手未购买该订单要求的会员', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 检查会员是否过期
- # ✅ 可以调用!因为jiance_shifou_daoqi()是Huiyuangoumai模型的实例方法
- is_expired = huiyuan_goumai.jiance_shifou_daoqi()
- if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
- return Response(
- {'code': 400, 'message': '打手的会员已过期', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- elif yaoqiuleixing == 2: # 押金抢单
- # 获取订单的押金(yongjin字段)
- order_yajin = dingdan_obj.CommissionReq
- if order_yajin is None:
- return Response(
- {'code': 400, 'message': '订单押金为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证打手押金是否大于等于订单押金
- if new_dashou_profile.yajin < order_yajin:
- return Response(
- {'code': 400, 'message': '打手押金不足', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- else:
- # 未知的抢单要求类型
- return Response(
- {'code': 400, 'message': '订单抢单要求类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 12. 所有验证通过,开始更换打手(使用事务保证一致性)
- with transaction.atomic():
- # 12.1 更新新打手扩展表
- new_dashou_profile.zhuangtai = 0 # 接单中
- new_dashou_profile.jiedanzongliang += 1
- new_dashou_profile.jinrijiedan += 1
- new_dashou_profile.jinyuejiedan += 1
- new_dashou_profile.save()
-
- # 12.2 如果原订单有接单打手,则更新原打手状态
- old_dashou_id = dingdan_obj.PlayerID
- if old_dashou_id:
- try:
- # 查询原打手主表
- old_dashou_user = User.query.get(
- UserUID=old_dashou_id,
- #user_type='PlayerID'
- )
-
- # 获取原打手扩展表
- old_dashou_profile = old_dashou_user.DashouProfile
-
- # 更新原打手状态
- old_dashou_profile.zhuangtai = 1 # 空闲
- old_dashou_profile.tuikuanliang += 1 # 退款订单总量+1
- old_dashou_profile.save()
-
- logger.info(f"成功更新原打手{old_dashou_id}状态")
- except User.DoesNotExist:
- # 原打手不存在,继续执行不影响更换
- logger.warning(f"原打手{old_dashou_id}不存在,跳过更新")
- except AttributeError:
- # 原打手扩展表不存在,继续执行不影响更换
- logger.warning(f"原打手{old_dashou_id}扩展信息不存在,跳过更新")
- except Exception as e:
- # 其他异常,记录日志但继续执行
- logger.error(f"更新原打手状态异常: {str(e)}", exc_info=True)
-
- # 12.3 更新订单表
- dingdan_obj.PlayerID = new_dashou_id
- dingdan_obj.Status = 2 # 进行中状态
- dingdan_obj.save()
-
- # 13. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '更换打手成功',
- 'data': None
- })
-
- except Exception as e:
- logger.error(f"更换打手接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-class AdQiangZhiJieDan(APIView):
- """
- 后台强制结单接口
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /dingdan/adddqzjd
- 参数: {
- "zhanghao": "管理员账号",
- "dingdan_id": "订单ID"
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- """
- 强制结单接口
- 核心逻辑:
- 1. 验证管理员身份
- 2. 验证订单状态为2(进行中)、8(结算中)或4(退款审核中)才能强制结单
- 3. 根据发单平台(1=平台发单, 2=商家发单)分别处理
- 4. 更新订单状态为3(已完成)
- 5. 给打手结算分成金额
- 6. 如果是商家发单,还需要更新商家成交订单数量
- """
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
-
- if not qianzhanghao or not dingdan_id:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 查询订单主表
- try:
- dingdan_obj = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response(
- {'code': 404, 'message': '订单不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 7. 验证订单状态是否为2、8或4
- if dingdan_obj.Status not in [2, 8, 4]:
- return Response(
- {'code': 400, 'message': '订单状态不允许强制结单', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 8. 获取订单的重要字段
- jiedan_dashou_id = dingdan_obj.PlayerID # 接单打手ID
- dashou_fencheng = dingdan_obj.PlayerCommission # 打手分成金额
- fadan_pingtai = dingdan_obj.Platform # 发单平台
-
- # 9. 验证打手分成金额是否存在
- if dashou_fencheng is None:
- return Response(
- {'code': 400, 'message': '订单缺少打手分成金额', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 10. 使用事务保证数据一致性
- with transaction.atomic():
- # 10.1 更新订单状态为3(已完成)
- dingdan_obj.Status = 3
- dingdan_obj.save()
-
- # 10.2 处理打手结算
- if jiedan_dashou_id:
- try:
- # 查询打手用户主表
- dashou_user = User.query.get(
- UserUID=jiedan_dashou_id,
- #user_type='PlayerID'
- )
-
- # 获取打手扩展表
- dashou_profile = dashou_user.DashouProfile
-
- # 更新打手扩展表数据
- # ✅ 确认字段:chengjiaozongliang 成交单总量
- dashou_profile.chengjiaozongliang += 1
-
- # ✅ 确认字段:yue 可提现余额
- dashou_profile.yue += dashou_fencheng
-
- # ✅ 确认字段:zonge 打单赚取总额
- dashou_profile.zonge += dashou_fencheng
-
- # ✅ 确认字段:jinrishouyi 今日收益金额
- dashou_profile.jinrishouyi += dashou_fencheng
-
- # ✅ 确认字段:jinyueshouyi 今月收益金额
- dashou_profile.jinyueshouyi += dashou_fencheng
-
- dashou_profile.save()
- logger.info(f"强制结单成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}")
-
- except User.DoesNotExist:
- # 打手用户不存在,记录日志但不影响强制结单
- logger.warning(f"强制结单:打手{jiedan_dashou_id}不存在,跳过打手结算")
- except AttributeError:
- # 打手扩展表不存在,记录日志但不影响强制结单
- logger.warning(f"强制结单:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算")
-
- # 10.3 如果是商家发单,还需要更新商家扩展表
- if fadan_pingtai == 2:
- try:
- # 获取商家订单扩展表
- shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
- shangjia_id = shangjia_kuozhan.MerchantID
-
- if shangjia_id:
- # 查询商家用户主表
- shangjia_user = User.query.get(
- UserUID=shangjia_id,
- #user_type='shop'
- )
-
- # 获取商家扩展表
- shangjia_profile = shangjia_user.ShopProfile
-
- # ✅ 确认字段:chengjiao 商家成交订单总量
- shangjia_profile.chengjiao += 1
- shangjia_profile.save()
-
- logger.info(f"强制结单成功,商家{shangjia_id}成交订单+1")
-
- except MerchantOrderExt.DoesNotExist:
- # 商家订单扩展表不存在,跳过商家更新
- logger.warning(f"强制结单:订单{dingdan_id}商家扩展表不存在,跳过商家更新")
- except User.DoesNotExist:
- # 商家用户不存在,跳过商家更新
- logger.warning(f"强制结单:商家{shangjia_id}不存在,跳过商家更新")
- except AttributeError:
- # 商家扩展表不存在,跳过商家更新
- logger.warning(f"强制结单:商家{shangjia_id}扩展信息不存在,跳过商家更新")
-
- # 11. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '强制结单成功',
- 'data': None
- })
-
- except Exception as e:
- logger.error(f"强制结单接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-class AdJuJueTuiKuan(APIView):
- """
- 后台拒绝退款接口
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /dingdan/adddjj
- 参数: {
- "zhanghao": "管理员账号",
- "dingdan_id": "订单ID",
- "jujue_liyou": "拒绝退款理由(可选)"
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- """
- 拒绝退款接口
- 核心逻辑:
- 1. 验证管理员身份
- 2. 拒绝退款后订单状态改为6(退款失败)
- 3. 给打手结算分成金额
- 4. 如果是商家发单,更新商家成交订单数量
- 5. 更新退款记录表,状态改为2(驳回)
- """
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
- jujue_liyou = request.data.get('jujue_liyou', '').strip()
-
- if not qianzhanghao or not dingdan_id:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 查询订单主表
- try:
- dingdan_obj = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response(
- {'code': 404, 'message': '订单不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 7. 获取订单的重要字段
- jiedan_dashou_id = dingdan_obj.PlayerID # 接单打手ID
- dashou_fencheng = dingdan_obj.PlayerCommission # 打手分成金额
- fadan_pingtai = dingdan_obj.Platform # 发单平台
-
- # 8. 验证打手分成金额是否存在
- if dashou_fencheng is None:
- return Response(
- {'code': 400, 'message': '订单缺少打手分成金额', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 9. 使用事务保证数据一致性
- with transaction.atomic():
- # 9.1 更新订单状态为6(退款失败)
- dingdan_obj.Status = 6
- dingdan_obj.save()
-
- # 9.2 处理打手结算
- if jiedan_dashou_id:
- try:
- # 查询打手用户主表
- dashou_user = User.query.get(
- UserUID=jiedan_dashou_id,
- #user_type='PlayerID'
- )
-
- # 获取打手扩展表
- dashou_profile = dashou_user.DashouProfile
-
- # 更新打手扩展表数据
- # ✅ 确认字段:chengjiaozongliang 成交单总量
- dashou_profile.chengjiaozongliang += 1
-
- # ✅ 确认字段:yue 可提现余额
- dashou_profile.yue += dashou_fencheng
-
- # ✅ 确认字段:zonge 打单赚取总额
- dashou_profile.zonge += dashou_fencheng
-
- # ✅ 确认字段:jinrishouyi 今日收益金额
- dashou_profile.jinrishouyi += dashou_fencheng
-
- # ✅ 确认字段:jinyueshouyi 今月收益金额
- dashou_profile.jinyueshouyi += dashou_fencheng
-
- dashou_profile.save()
- logger.info(f"拒绝退款成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}")
-
- except User.DoesNotExist:
- # 打手用户不存在,记录日志但不影响操作
- logger.warning(f"拒绝退款:打手{jiedan_dashou_id}不存在,跳过打手结算")
- except AttributeError:
- # 打手扩展表不存在,记录日志但不影响操作
- logger.warning(f"拒绝退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算")
-
- # 9.3 如果是商家发单,还需要更新商家扩展表
- if fadan_pingtai == 2:
- try:
- # 获取商家订单扩展表
- shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
- shangjia_id = shangjia_kuozhan.MerchantID
-
- if shangjia_id:
- # 查询商家用户主表
- shangjia_user = User.query.get(
- UserUID=shangjia_id,
- #user_type='shop'
- )
-
- # 获取商家扩展表
- shangjia_profile = shangjia_user.ShopProfile
-
- # ✅ 确认字段:chengjiao 商家成交订单总量
- shangjia_profile.chengjiao += 1
- shangjia_profile.save()
-
- logger.info(f"拒绝退款成功,商家{shangjia_id}成交订单+1")
-
- except MerchantOrderExt.DoesNotExist:
- # 商家订单扩展表不存在,跳过商家更新
- logger.warning(f"拒绝退款:订单{dingdan_id}商家扩展表不存在,跳过商家更新")
- except User.DoesNotExist:
- # 商家用户不存在,跳过商家更新
- logger.warning(f"拒绝退款:商家{shangjia_id}不存在,跳过商家更新")
- except AttributeError:
- # 商家扩展表不存在,跳过商家更新
- logger.warning(f"拒绝退款:商家{shangjia_id}扩展信息不存在,跳过商家更新")
-
- # 9.4 更新退款记录表
- try:
- # 查询退款记录表
- tuikuan_jilu = RefundRecord.query.get(OrderID=dingdan_id)
-
- # ✅ 确认字段:sqzhuangtai 申请状态,改为2(驳回)
- tuikuan_jilu.ApplyStatus = 2
-
- # ✅ 确认字段:bhliyou 驳回理由
- tuikuan_jilu.RejectReason = jujue_liyou
-
- # ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号)
- tuikuan_jilu.ProcessorID = qianzhanghao
-
- tuikuan_jilu.save()
- logger.info(f"拒绝退款成功,更新退款记录表,订单:{dingdan_id}")
-
- except RefundRecord.DoesNotExist:
- # 退款记录表不存在,记录日志但不影响主流程
- logger.warning(f"拒绝退款:订单{dingdan_id}退款记录不存在,跳过更新")
-
- # 10. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '拒绝退款成功',
- 'data': None
- })
-
- except Exception as e:
- logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-class AdTongYiTuiKuanShangJia(APIView):
- """
- 后台同意退款接口(商家发单)
- 权限: JWT Token认证 + 管理员身份验证 + 密码验证
- 前端调用: POST /dingdan/adddsjtk
- 参数: {
- "zhanghao": "管理员账号",
- "dingdan_id": "订单ID",
- "tuikuan_liyou": "退款理由(可选)",
- "pay_password": "支付密码(openid)"
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- """
- 同意退款接口(商家发单)
- 核心逻辑:
- 1. 验证管理员身份
- 2. 验证支付密码(openid)
- 3. 检查订单状态为4(退款审核中)
- 4. 更新订单状态为5(已退款)
- 5. 更新打手退款订单数量
- 6. 更新商家退款订单数量和余额(加上订单金额)
- 7. 更新退款记录表,状态改为1(成功)
- """
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
- tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
- pay_password = request.data.get('pay_password', '').strip()
-
- if not qianzhanghao or not dingdan_id or not pay_password:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 验证支付密码(对比openid)
- if pay_password != dangqianyonghu.OpenID:
- return Response(
- {'code': 401, 'message': '支付密码错误', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 7. 查询订单主表
- try:
- dingdan_obj = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response(
- {'code': 404, 'message': '订单不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 8. 验证订单状态为4(退款审核中)
- if dingdan_obj.Status != 4:
- return Response(
- {'code': 400, 'message': '订单状态不是退款审核中', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 9. 验证订单为商家发单
- if dingdan_obj.Platform != 2:
- return Response(
- {'code': 400, 'message': '此接口只处理商家发单的退款', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 10. 获取订单的重要字段
- jiedan_dashou_id = dingdan_obj.PlayerID # 接单打手ID
- jine = dingdan_obj.Amount # 订单金额(不是打手分成)
-
- # 11. 验证订单金额是否存在
- if jine is None:
- return Response(
- {'code': 400, 'message': '订单缺少订单金额', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 12. 获取商家ID
- try:
- shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
- shangjia_id = shangjia_kuozhan.MerchantID
- except MerchantOrderExt.DoesNotExist:
- return Response(
- {'code': 400, 'message': '订单商家信息不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 13. 使用事务保证数据一致性
- with transaction.atomic():
- # 13.1 更新订单状态为5(已退款)
- dingdan_obj.Status = 5
- dingdan_obj.save()
-
- # 13.2 更新打手扩展表(退款订单总量+1)
- if jiedan_dashou_id:
- try:
- # 查询打手用户主表
- dashou_user = User.query.get(
- UserUID=jiedan_dashou_id,
- #user_type='PlayerID'
- )
-
- # 获取打手扩展表
- dashou_profile = dashou_user.DashouProfile
-
- # ✅ 确认字段:tuikuanliang 退款订单总量
- dashou_profile.tuikuanliang += 1
-
- dashou_profile.save()
- logger.info(f"同意退款成功,打手{jiedan_dashou_id}退款订单+1")
-
- except User.DoesNotExist:
- # 打手用户不存在,记录日志但不影响操作
- logger.warning(f"同意退款:打手{jiedan_dashou_id}不存在,跳过打手更新")
- except AttributeError:
- # 打手扩展表不存在,记录日志但不影响操作
- logger.warning(f"同意退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手更新")
-
- # 13.3 更新商家扩展表
- if shangjia_id:
- try:
- # 查询商家用户主表
- shangjia_user = User.query.get(
- UserUID=shangjia_id,
- #user_type='shop'
- )
-
- # 获取商家扩展表
- shangjia_profile = shangjia_user.ShopProfile
-
- # ✅ 确认字段:tuikuan 退款订单总量
- shangjia_profile.tuikuan += 1
-
- from merchant_ops.services.wallet import is_staff_quota_order
- if not is_staff_quota_order(dingdan_id):
- shangjia_profile.yue += jine
-
- shangjia_profile.save()
- logger.info(f"同意退款成功,商家{shangjia_id}退款订单+1,余额增加:{jine}")
-
- update_shangjia_daily(
- yonghuid=shangjia_id,
- amount=Decimal(str(jine)),
- action=3,
- order_id=dingdan_id,
- )
-
- except User.DoesNotExist:
- # 商家用户不存在,记录日志但不影响操作
- logger.warning(f"同意退款:商家{shangjia_id}不存在,跳过商家更新")
- except AttributeError:
- # 商家扩展表不存在,记录日志但不影响操作
- logger.warning(f"同意退款:商家{shangjia_id}扩展信息不存在,跳过商家更新")
-
- # 13.4 更新退款记录表
- try:
- # 查询退款记录表
- tuikuan_jilu = RefundRecord.query.get(OrderID=dingdan_id)
-
- # ✅ 确认字段:sqzhuangtai 申请状态,改为1(成功)
- tuikuan_jilu.PenaltyApplyStatus = 1
-
- # 可以添加同意理由字段,如:tongyi_liyou
- # tuikuan_jilu.tongyi_liyou = tuikuan_liyou
-
- # ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号)
- tuikuan_jilu.ProcessorID = qianzhanghao
-
- tuikuan_jilu.save()
- logger.info(f"同意退款成功,更新退款记录表,订单:{dingdan_id}")
-
- except RefundRecord.DoesNotExist:
- # 退款记录表不存在,记录日志但不影响主流程
- logger.warning(f"同意退款:订单{dingdan_id}退款记录不存在,跳过更新")
-
- # 14. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '同意退款成功',
- 'data': None
- })
-
- except Exception as e:
- logger.error(f"同意退款接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-class AdTongYiTuiKuanPingTai(APIView):
- """
- 后台同意退款接口(平台发单)
- 权限: JWT Token认证 + 管理员身份验证 + 密码验证
- 前端调用: POST /dingdan/adddpttk
- 参数: {
- "zhanghao": "管理员账号",
- "dingdan_id": "订单ID",
- "tuikuan_liyou": "退款理由(可选)",
- "pay_password": "支付密码(openid)"
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
- tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
- pay_password = request.data.get('pay_password', '').strip()
-
- if not qianzhanghao or not dingdan_id or not pay_password:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if str(getattr(dangqianyonghu, 'Phone', '')) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 验证支付密码(对比openid)
- if pay_password != dangqianyonghu.OpenID:
- return Response(
- {'code': 401, 'message': '支付密码错误', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 7. 查询订单主表(加锁)
- with transaction.atomic():
- try:
- dingdan_obj = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response(
- {'code': 404, 'message': '订单不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 8. 验证订单状态为1、7、2、8、4(允许退款的状态)
- if dingdan_obj.Status not in [1, 7, 2, 8, 4]:
- return Response(
- {'code': 400, 'message': '订单状态不允许退款', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 9. 验证订单为平台发单
- if dingdan_obj.Platform != 1:
- return Response(
- {'code': 400, 'message': '此接口只处理平台发单的退款', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 10. 获取订单的重要字段
- jiedan_dashou_id = dingdan_obj.PlayerID
- jine = dingdan_obj.Amount
- if jine is None:
- return Response(
- {'code': 400, 'message': '订单缺少订单金额', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 11. 获取老板ID(从平台订单扩展表)
- if not hasattr(dingdan_obj, 'pingtai_kuozhan') or not dingdan_obj.pingtai_kuozhan:
- return Response(
- {'code': 400, 'message': '订单老板信息不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- laoban_id = dingdan_obj.pingtai_kuozhan.BossID
-
- # 12. 调用微信退款接口(优先使用 wechat_transaction_id)
- transaction_id = getattr(dingdan_obj, 'WechatTransactionID', None)
- weixin_refund_result = self.call_weixin_refund(
- out_trade_no=dingdan_id,
- Amount=jine,
- transaction_id=transaction_id
- )
-
- if not weixin_refund_result.get('success'):
- logger.error(f"微信退款失败,订单:{dingdan_id},错误:{weixin_refund_result.get('msg')}")
- return Response(
- {'code': 400, 'message': weixin_refund_result.get('msg', '退款失败')},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 13. 更新订单状态为5(已退款)
- dingdan_obj.Status = 5
- dingdan_obj.save()
-
- # 14. 更新打手扩展表
- if jiedan_dashou_id:
- try:
- dashou_user = User.query.get(UserUID=jiedan_dashou_id)
- dashou_profile = dashou_user.DashouProfile
- dashou_profile.zhuangtai = 1 # 打手状态设为空闲
- dashou_profile.tuikuanliang += 1
- dashou_profile.save()
- except (User.DoesNotExist, AttributeError):
- logger.warning(f"平台退款:打手{jiedan_dashou_id}不存在或扩展信息缺失,跳过")
-
- # 15. 更新老板扩展表
- if laoban_id:
- try:
- laoban_user = User.query.get(UserUID=laoban_id)
- laoban_profile = laoban_user.BossProfile
- laoban_profile.alltui += 1
- laoban_profile.zonge -= jine
- if laoban_profile.zonge < 0:
- laoban_profile.zonge = 0
- laoban_profile.save()
- except (User.DoesNotExist, AttributeError):
- logger.warning(f"平台退款:老板{laoban_id}不存在或扩展信息缺失,跳过")
-
- # 16. 更新收支记录表(按俱乐部)
- try:
- from jituan.services.szjilu_accounting import apply_szjilu_expense
- update_daily_payout(jine, getattr(dingdan_obj, 'ClubID', None))
- apply_szjilu_expense(jine, getattr(dingdan_obj, 'ClubID', None))
- except Exception as e:
- logger.error(f"更新收支记录表异常: {str(e)}")
-
- # 17. 更新退款记录表
- try:
- tuikuan_jilu, created = RefundRecord.query.get_or_create(
- OrderID=dingdan_id,
- defaults={
- 'PlayerID': jiedan_dashou_id if jiedan_dashou_id else '',
- 'ApplicantID': laoban_id if laoban_id else '',
- 'ProcessorID': qianzhanghao,
- 'Reason': tuikuan_liyou if tuikuan_liyou else '给老板退款',
- 'ApplyStatus': 1,
- 'Amount': jine,
- 'PlayerCommission': dingdan_obj.PlayerCommission or 0,
- 'Description': dingdan_obj.Description or '',
- 'Remark': '给老板退款',
- 'Nickname': dingdan_obj.Nickname or '',
- }
- )
- if not created:
- tuikuan_jilu.ApplyStatus = 1
- if tuikuan_liyou:
- tuikuan_jilu.Reason = tuikuan_liyou
- tuikuan_jilu.ProcessorID = qianzhanghao
- tuikuan_jilu.save()
- except Exception as e:
- logger.error(f"更新退款记录表异常: {str(e)}")
-
- logger.info(f"平台退款成功,订单:{dingdan_id},退款单号:{weixin_refund_result.get('refund_id')}")
-
- return Response({'code': 0, 'message': '退款处理成功', 'data': None})
-
- except Exception as e:
- logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def call_weixin_refund(self, out_trade_no, jine, transaction_id=None):
- """
- 调用微信退款接口
- :param out_trade_no: 商户订单号
- :param jine: 退款金额
- :param transaction_id: 微信支付订单号(优先使用)
- :return: {'success': True/False, 'msg': '消息', 'refund_id': '退款单号'}
- """
- try:
- # 检查配置
- required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH', 'WEIXIN_KEY_PATH']
- for setting in required_settings:
- if not hasattr(settings, setting) or not getattr(settings, setting):
- return {'success': False, 'msg': f'缺少微信支付配置: {setting}'}
-
- # 生成退款单号
- refund_id = f"TK{int(time.time())}{random.randint(1000, 9999)}"
- total_fee_in_fen = yuan_to_fen(jine)
-
- # 构造请求参数
- refund_data = {
- 'appid': settings.WEIXIN_APPID,
- 'mch_id': settings.WEIXIN_MCHID,
- 'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(),
- 'out_refund_no': refund_id,
- 'total_fee': total_fee_in_fen,
- 'refund_fee': total_fee_in_fen,
- 'op_user_id': settings.WEIXIN_MCHID,
- 'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS'
- }
- # 优先使用 transaction_id,否则用 out_trade_no
- if transaction_id:
- refund_data['transaction_id'] = transaction_id
- else:
- refund_data['out_trade_no'] = out_trade_no
-
- # 生成签名
- sorted_params = sorted(refund_data.items())
- sign_string = '&'.join([f"{k}={v}" for k, v in sorted_params if v])
- sign_string += f"&key={settings.WEIXIN_SHANGHUMIYAO}"
- sign = hashlib.md5(sign_string.encode()).hexdigest().upper()
- refund_data['sign'] = sign
-
- xml_data = self.dict_to_xml(refund_data)
- url = "https://api.mch.weixin.qq.com/secapi/pay/refund"
- response = requests.post(
- url,
- data=xml_data.encode('utf-8'),
- headers={'Content-Type': 'application/xml'},
- cert=(settings.WEIXIN_CERT_PATH, settings.WEIXIN_KEY_PATH),
- timeout=30
- )
-
- if response.status_code == 200:
- result = self.xml_to_dict(response.content)
- if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
- logger.info(f"微信退款成功,订单:{out_trade_no},退款单号:{refund_id}")
- return {'success': True, 'msg': '退款申请提交成功', 'refund_id': refund_id}
- else:
- error_msg = result.get('err_code_des', result.get('return_msg', '退款申请失败'))
- logger.error(f"微信退款失败,订单:{out_trade_no},错误:{error_msg}")
- return {'success': False, 'msg': f'微信退款失败: {error_msg}'}
- else:
- logger.error(f"微信退款接口调用失败,状态码:{response.status_code}")
- return {'success': False, 'msg': '微信退款接口调用失败'}
-
- except requests.exceptions.Timeout:
- logger.error(f"微信退款接口请求超时,订单:{out_trade_no}")
- return {'success': False, 'msg': '微信退款接口请求超时'}
- except Exception as e:
- logger.error(f"调用微信退款接口异常: {str(e)}")
- return {'success': False, 'msg': f'退款处理异常: {str(e)}'}
-
- def dict_to_xml(self, dict_data):
- xml = [""]
- for k, v in dict_data.items():
- xml.append(f"<{k}>{v}{k}>")
- xml.append("")
- return "".join(xml)
-
- def xml_to_dict(self, xml_data):
- try:
- root = ET.fromstring(xml_data)
- return {child.tag: child.text for child in root}
- except ET.ParseError as e:
- logger.error(f"XML解析失败: {str(e)}")
- return {'return_code': 'FAIL', 'return_msg': 'XML解析失败'}
-
-
-logger = logging.getLogger(__name__)
-
-
-class DashouHuoquLeixingView(APIView):
- """
- 打手获取商品类型接口 - 抢单页面使用
- 接口地址:POST /dingdan/dsqdhqddlx
- 功能:返回所有正常的商品类型,已按排序权重排序
-
- 返回格式(与前端原数据格式完全一致):
- {
- "code": 200,
- "msg": "成功",
- "data": {
- "list": [
- {
- "id": 1,
- "jieshao": "王者荣耀",
- "tupian_url": "leixing/wzry.jpg",
- "yaoqiuleixing": null,
- "huiyuan_id": null,
- "yongjin": null,
- "paixu": 100,
- "shenhezhuangtai": 1
- },
- // ... 更多类型
- ]
- }
- }
- """
-
- # 🔴 使用JWT Token认证
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- """
- 处理POST请求,返回商品类型列表
-
- 请求参数:无特殊参数,只验证用户身份
-
- 安全验证:
- 1. JWT Token验证用户身份
- 2. 只返回 shenhezhuangtai=1(正常状态)的类型
- 3. 数据已排序:paixu降序 → id升序
- """
- try:
- logger.info(f"打手获取商品类型请求 - 用户ID: {request.user.UserUID}")
-
- 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, request.user)
- leixing_list = build_club_leixing_list_for_api(club_id)
-
- logger.info(f"返回商品类型数据:{len(leixing_list)}条,俱乐部:{club_id} 用户: {request.user.UserUID}")
-
- # 🔴 返回成功响应
- # 注意:保持与前端原数据格式完全一致
- return Response({
- "code": 200,
- "msg": "成功",
- "data": {
- "list": leixing_list # 🔴 保持原字段名 "list"
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- # 🔴 异常处理
- logger.error(f"打手获取商品类型失败 - 用户ID: {request.user.UserUID}, 错误: {str(e)}", exc_info=True)
-
- return Response({
- "code": 500,
- "msg": f"服务器内部错误: {str(e)}",
- "data": None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-
-
-class AdJuJueJieSuan(APIView):
- """
- 后台拒绝结算接口(平台发单)
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /dingdan/adddjjjs
- 参数: {
- "zhanghao": "管理员账号(手机号)",
- "dingdan_id": "订单ID",
- "tkly": "拒绝结算理由(必填)"
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
- tkly = request.data.get('tkly', '').strip()
-
- if not all([qianzhanghao, dingdan_id, tkly]):
- return Response(
- {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id, tkly)', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT用户验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号匹配(管理员账号通常为手机号)
- if str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 查询订单
- try:
- dingdan = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response(
- {'code': 404, 'message': '订单不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 6. 校验订单条件:平台发单(1)且状态为2或8
- if dingdan.Platform != 1 or dingdan.Status not in [2, 8]:
- return Response(
- {'code': 400, 'message': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- jiedan_dashou_id = dingdan.PlayerID
- if not jiedan_dashou_id:
- return Response(
- {'code': 400, 'message': '订单暂无接单打手', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- with transaction.atomic():
- # 7. 更新订单状态为5(已退款),并保存拒绝理由
- dingdan.Status = 5
- dingdan.RefundReason = tkly
- dingdan.save()
-
- # 8. 更新打手扩展表
- try:
- dashou_user = User.query.get(UserUID=jiedan_dashou_id)
- dashou_profile = dashou_user.DashouProfile
- dashou_profile.zhuangtai = 1 # 打手状态设为正常
- dashou_profile.tuikuanliang += 1 # 退款订单总量+1
- dashou_profile.save()
- except (User.DoesNotExist, AttributeError) as e:
- logger.warning(f"拒绝结算:打手{jiedan_dashou_id}信息更新失败 - {str(e)}")
-
- return Response({'code': 0, 'message': '拒绝结算成功', 'data': None})
-
- except Exception as e:
- logger.error(f"拒绝结算接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-class AdZhuanYiDaTing(APIView):
- """
- 后台转移大厅接口
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /dingdan/adddzydt
- 参数: {
- "zhanghao": "管理员账号(手机号)",
- "dingdan_id": "订单ID"
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
-
- if not all([qianzhanghao, dingdan_id]):
- return Response(
- {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id)', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- dangqianyonghu = request.user
-
- if str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- try:
- dingdan = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response(
- {'code': 404, 'message': '订单不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 校验订单状态:允许2、8、4
- if dingdan.Status not in [2, 8, 4]:
- return Response(
- {'code': 400, 'message': '仅状态为进行中(2)、结算中(8)或退款审核(4)的订单可转移大厅', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- jiedan_dashou_id = dingdan.PlayerID
-
- with transaction.atomic():
- # 更新订单:状态改为1(下单中),清空接单打手ID
- dingdan.Status = 1
- dingdan.PlayerID = None
- dingdan.save()
-
- # 如果有接单打手,更新其扩展表
- if jiedan_dashou_id:
- try:
- dashou_user = User.query.get(UserUID=jiedan_dashou_id)
- dashou_profile = dashou_user.DashouProfile
- dashou_profile.zhuangtai = 1
- dashou_profile.tuikuanliang += 1
- dashou_profile.save()
- except (User.DoesNotExist, AttributeError) as e:
- logger.warning(f"转移大厅:打手{jiedan_dashou_id}信息更新失败 - {str(e)}")
-
- return Response({'code': 0, 'message': '转移大厅成功', 'data': None})
-
- except Exception as e:
- logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-class AdQuXiaoZhiDing(APIView):
- """
- 后台取消指定接口
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /dingdan/adddqxzd
- 参数: {
- "zhanghao": "管理员账号(手机号)",
- "dingdan_id": "订单ID"
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
-
- if not all([qianzhanghao, dingdan_id]):
- return Response(
- {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id)', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- dangqianyonghu = request.user
-
- if str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- try:
- dingdan = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response(
- {'code': 404, 'message': '订单不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 校验订单状态:必须为7(指定中)
- if dingdan.Status != 7:
- return Response(
- {'code': 400, 'message': '仅状态为指定中(7)的订单可取消指定', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- with transaction.atomic():
- # 更新订单:状态改为1(下单中),清空指定ID
- dingdan.Status = 1
- dingdan.AssignedID = None
- dingdan.save()
-
- return Response({'code': 0, 'message': '取消指定成功', 'data': None})
-
- except Exception as e:
- logger.error(f"取消指定接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-class ZhiDingChaxunView(APIView):
- """
- 查询当前打手被指定的待接订单
- POST /dingdan/zdcx
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- dashou_uid = str(request.user.UserUID)
- try:
- dashou_profile = request.user.DashouProfile
- if dashou_profile.zhanghaozhuangtai != 1:
- return Response({'code': 403, 'msg': '账号状态异常'}, status=403)
- except Exception:
- return Response({'code': 403, 'msg': '您不是打手'}, status=403)
-
- orders = Order.query.filter(
- Status=7,
- AssignedID=dashou_uid,
- ).filter(
- Q(PlayerID__isnull=True) | Q(PlayerID='')
- ).order_by('-CreateTime')[:20]
-
- result = []
- for o in orders:
- hf = getattr(o, 'AssignedResponse', None)
- result.append({
- 'dingdan_id': o.OrderID,
- 'zhuangtai': o.Status,
- 'pingtai': o.Platform,
- 'dashou_fencheng': float(o.PlayerCommission or 0),
- 'leixing_id': o.ProductTypeID or 0,
- 'tupian': o.ImageURL or '',
- 'jieshao': o.Description or '',
- 'beizhu': o.Remark or '',
- 'creat_time': fmt_datetime(o.CreateTime),
- 'create_time': fmt_datetime(o.CreateTime),
- 'zhiding_uid': o.AssignedID or '',
- 'zhiding_hf': hf,
- 'zhiding_hf_text': ZHIDING_HF_TEXT.get(hf, ''),
- 'zhiding_dhj_sm': getattr(o, 'AssignedLaterNote', '') or '',
- 'zhiding_hf_sj': fmt_datetime(o.AssignedResponseAt) if getattr(o, 'AssignedResponseAt', None) else '',
- 'yaoqiuleixing': o.GrabRequirement or 0,
- 'huiyuan_id': o.MembershipID or '',
- 'yajin': float(o.CommissionReq or 0),
- })
-
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': {'list': result, 'count': len(result)},
- })
- except Exception as e:
- logger.error(f"指定订单查询失败: {e}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
-
-
-class ZhiDingHuiFuView(APIView):
- """
- 指定单打手响应:想接 / 不想接 / 等会接
- POST /dingdan/zdhf
- 参数: dingdan_id, huifu (1|2|3), deng_hui_note (等会接说明,可选)
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- data = request.data
- dingdan_id = str(data.get('dingdan_id') or data.get('OrderID') or '').strip()
- huifu = int(data.get('huifu') or 0)
- deng_hui_note = str(data.get('deng_hui_note') or '').strip()[:100]
-
- if not dingdan_id:
- return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400)
- if huifu not in (1, 2, 3):
- return Response({'code': 400, 'msg': '无效的响应类型'}, status=400)
- if huifu == 3 and not deng_hui_note:
- return Response({'code': 400, 'msg': '请填写预计接待时间'}, status=400)
-
- dashou_uid = str(request.user.UserUID)
-
- with transaction.atomic():
- try:
- order = Order.query.select_for_update().get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=404)
-
- if order.Status != 7:
- return Response({'code': 400, 'msg': '订单已不是指定中状态'}, status=400)
- if str(order.AssignedID or '') != dashou_uid:
- return Response({'code': 403, 'msg': '您不是该订单的指定打手'}, status=403)
- if order.PlayerID:
- return Response({'code': 400, 'msg': '订单已被接单'}, status=400)
-
- now = timezone.now()
- order.AssignedResponse = huifu
- order.AssignedResponseAt = now
-
- if huifu == 2:
- order.Status = 1
- order.AssignedID = None
- order.AssignedLaterNote = ''
- elif huifu == 3:
- order.AssignedLaterNote = deng_hui_note
- else:
- order.AssignedLaterNote = ''
-
- order.save()
-
- msg_map = {1: '已标记想接,请点击接待完成接单', 2: '已婉拒,订单已退回大厅', 3: '已记录,请按时接待'}
- return Response({
- 'code': 200,
- 'msg': msg_map.get(huifu, '操作成功'),
- 'data': {
- 'dingdan_id': dingdan_id,
- 'huifu': huifu,
- 'huifu_text': ZHIDING_HF_TEXT.get(huifu, ''),
- },
- })
- except Exception as e:
- logger.error(f"指定订单响应失败: {e}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
-
-
-class DashouXiugaiView(APIView):
- """
- 打手修改订单交付内容接口
- 请求方式:POST
- 请求路径:/dingdan/dsxiugaidd
- 权限:需携带有效JWT Token,且用户为打手身份
- 请求参数:
- dingdan_id : 订单ID(必填)
- liuyan : 新的留言内容(可选,最长50字)
- deleted_images : 要删除的图片URL列表(数组,每个元素为完整COS URL)
- new_images : 新增的图片URL列表(数组,每个元素为完整COS URL)
- 返回:
- 成功:{"code":0,"msg":"修改成功","data":null}
- 失败:返回相应错误码和提示
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 获取请求数据
- data = request.data
- dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
- liuyan = data.get('liuyan', '')
- deleted_images = data.get('deleted_images', [])
- new_images = data.get('new_images', [])
-
- # ---------- 参数校验 ----------
- if not dingdan_id:
- return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if not isinstance(deleted_images, list):
- return Response({'code': 2, 'msg': 'deleted_images 必须为数组', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if not isinstance(new_images, list):
- return Response({'code': 3, 'msg': 'new_images 必须为数组', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if liuyan and len(liuyan) > 50:
- return Response({'code': 4, 'msg': '留言最多50个字', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # ---------- 用户身份验证 ----------
- current_user = request.user
- yonghuid = current_user.UserUID
-
- # 检查是否为打手
- try:
- dashou_profile = current_user.DashouProfile # User 模型反向 related_name='DashouProfile'
- except UserDashou.DoesNotExist:
- return Response({'code': 5, 'msg': '用户不是打手身份', 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- # ---------- 订单查询及权限验证 ----------
- try:
- # 订单必须属于该打手,且状态为4(退款中)或8(结算中)
- dingdan = Order.query.get(
- OrderID=dingdan_id,
- PlayerID=yonghuid,
- Status__in=[4, 8] # 只允许修改退款中或结算中的订单
- )
- except Order.DoesNotExist:
- return Response({'code': 6, 'msg': '订单不存在或没有修改权限', 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- # ---------- 执行修改(使用事务保证原子性) ----------
- try:
- with transaction.atomic():
- # 1. 处理删除的图片:从 Dashoutupian 表中删除记录
- if deleted_images:
- # 根据订单ID、打手ID和图片URL删除记录
- delete_count, _ = PlayerDeliveryImage.query.filter(
- OrderID=dingdan_id,
- PlayerID=yonghuid,
- ImageURL__in=deleted_images
- ).delete()
- # 可选:记录删除数量
- logger.info(f"订单 {dingdan_id} 删除图片 {delete_count} 张")
-
- # 2. 处理新增的图片:插入新记录
- if new_images:
- # 批量创建
- new_records = []
- for url in new_images:
- new_records.append(PlayerDeliveryImage(
- OrderID=dingdan_id,
- PlayerID=yonghuid,
- ImageURL=url,
- CreateTime=timezone.now()
- ))
- PlayerDeliveryImage.objects.bulk_create(new_records)
- logger.info(f"订单 {dingdan_id} 新增图片 {len(new_records)} 张")
-
- # 3. 更新订单留言(如果发生变化)
- # 注意:即使留言为空字符串,也允许更新,表示清空留言
- if 'liuyan' in data: # 判断是否传了liuyan字段(可能为空字符串)
- dingdan.PlayerRemark = liuyan
- dingdan.UpdateTime = timezone.now()
- dingdan.save(update_fields=['PlayerRemark', 'UpdateTime'])
-
- # 全部成功,返回成功响应
- return Response({
- 'code': 0,
- 'msg': '修改成功',
- 'data': None
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f"修改订单失败: {str(e)}", exc_info=True)
- return Response({
- 'code': 7,
- 'msg': '服务器内部错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-class ZxsjghdsView(APIView):
- """
- 商家更换打手接口
- 路径:POST /dingdan/zxsjghds
- 权限:仅限已认证商家(通过JWT)
- 功能:
- 1. 将原订单状态改为已退款(5),释放接单打手(若有)
- 2. 更新打手统计(退款量、状态、每日统计)
- 3. 写入退款记录
- 4. 若为跨平台订单,通知对方平台退款(必须成功)
- 5. 基于原订单生成新订单(状态直接为1),复制核心数据
- 6. 不操作商家余额、退款次数等(替换订单)
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # ========== 1. 获取当前商家用户 ==========
- current_user = request.user
- try:
- yonghuid = current_user.UserUID
- except AttributeError:
- return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # ========== 2. 提取订单ID ==========
- dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
- if not dingdan_id:
- return Response({'code': 2, 'msg': '订单ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- # ========== 3. 查询订单并校验权限 ==========
- try:
- # 关联查询商家扩展表,确保当前商家是该订单的发布者
- shangjia_kuozhan = MerchantOrderExt.query.select_related('Order').get(
- Order__OrderID=dingdan_id,
- MerchantID=yonghuid
- )
- except MerchantOrderExt.DoesNotExist:
- return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN)
-
- old_order = shangjia_kuozhan.Order
-
- # ========== 4. 状态校验(仅允许进行中或结算中) ==========
- if old_order.Status not in [2, 8]:
- return Response({'code': 4, 'msg': '当前订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # ========== 5. 收集订单关键数据(用于新订单复制) ==========
- # 这些字段将在退款处理后用于创建新订单
- jine = old_order.Amount
- dashou_fencheng = old_order.PlayerCommission
- leixing_id = old_order.ProductTypeID
- yaoqiuleixing = old_order.GrabRequirement
- huiyuan_id = old_order.MembershipID
- yongjin = old_order.CommissionReq
- jieshao = old_order.Description
- nicheng = old_order.Nickname
- beizhu = old_order.Remark
- waibu_dingdan_id = old_order.ExternalOrderID
- user1_id = old_order.User1ID
- tupian = old_order.ImageURL
- fadan_pingtai = old_order.Platform
-
- dashou_id = old_order.PlayerID # 可能为空
-
- # ========== 6. 退款处理(事务块1:退款原订单 + 打手统计 + 跨平台通知) ==========
- try:
- with transaction.atomic():
- # 6.1 订单状态更新为已退款
- old_order.Status = 5
- old_order.save(update_fields=['Status'])
-
- # 6.2 释放打手
- if dashou_id:
- try:
- dashou_user = User.query.get(UserUID=dashou_id)
- dashou_profile = dashou_user.DashouProfile
- dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1
- dashou_profile.zhuangtai = 1 # 空闲
- dashou_profile.save(update_fields=['tuikuanliang', 'zhuangtai'])
- except (User.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"打手 {dashou_id} 不存在或扩展表缺失,跳过更新")
-
- # 6.3 更新退款记录
- RefundRecord.query.update_or_create(
- OrderID=dingdan_id,
- defaults={
- 'PlayerID': dashou_id or '',
- 'ApplicantID': yonghuid, # 申请人为当前商家
- 'ProcessorID': yonghuid, # 处理人也是商家自己
- 'Reason': '商家更换打手',
- 'ApplyStatus': 1, # 状态 1=成功
- 'Amount': jine or 0,
- 'PlayerCommission': dashou_fencheng or 0,
- 'Description': jieshao or '',
- 'Remark': '更换打手系统自动退款',
- 'Nickname': nicheng or '',
- }
- )
-
- # 6.4 打手每日统计(退款行为)
- if dashou_id:
- try:
- update_dashou_daily_by_action(
- yonghuid=dashou_id,
- amount=dashou_fencheng,
- action=3 # 3=退款
- )
- except Exception as e:
- logger.error(f"更新打手每日统计失败: {e}")
- # 统计失败不应阻断主流程,但可记录
-
- except Exception as e:
- logger.error(f"退款处理事务失败: {e}", exc_info=True)
- return Response({'code': 500, 'msg': '退款处理失败'})
-
- # ========== 7. 生成新订单(事务块2) ==========
- try:
- with transaction.atomic():
- # 7.1 生成新的订单ID
- def generate_dingdan_id():
- timestamp = int(time.time() * 1000)
- process_id = os.getpid() % 1000
- random_num = random.randint(1000, 9999)
- return f"SJ{timestamp}{process_id:03d}{random_num}"
-
- new_dingdan_id = generate_dingdan_id()
- # 确保唯一
- while Order.query.filter(OrderID=new_dingdan_id).exists():
- new_dingdan_id = generate_dingdan_id()
-
- # 7.2 创建新订单(复制数据,状态为 1)
- new_order = Order.query.create(
- OrderID=new_dingdan_id,
- Status=1, # 已下单
- Platform=fadan_pingtai,
- Amount=jine,
- PlayerCommission=dashou_fencheng,
- ProductTypeID=leixing_id,
- GrabRequirement=yaoqiuleixing,
- MembershipID=huiyuan_id,
- CommissionReq=yongjin,
- Description=jieshao,
- Nickname=nicheng,
- Remark=beizhu,
- User1ID=user1_id, # 商家标识
- ImageURL=tupian,
- ExternalOrderID=waibu_dingdan_id,
- ClubID=getattr(old_order, 'ClubID', None) or 'xq',
- # 不继承:zhiding_id, jiedan_dashou_id, dashou_liuyan 等
- )
-
- # 7.3 创建商家扩展表(关联新订单)
- MerchantOrderExt.query.create(
- Order=new_order,
- MerchantID=yonghuid,
- MerchantNickname=shangjia_kuozhan.MerchantNickname or '',
- )
-
- # 7.4 异步通知打手(广播新订单)
- try:
- order_info = {
- 'OrderID': new_dingdan_id,
- 'game_type': self._get_game_type_name(leixing_id),
- 'amount': str(jine),
- 'order_desc': (jieshao or '')[:50],
- }
- dingdan_guangbo.delay(order_info)
- except Exception as e:
- logger.error(f"广播通知失败: {e}")
-
- # 注意:此处不更新商家余额、发布量等统计,因为是替换订单
-
- logger.info(f"商家{yonghuid}更换打手成功,旧订单{dingdan_id}已退款,新订单{new_dingdan_id}")
-
- return Response({
- 'code': 0,
- 'msg': '更换成功',
- 'data': {
- 'xin_dingdan_id': new_dingdan_id,
- }
- })
-
- except Exception as e:
- logger.error(f"生成新订单事务失败: {e}", exc_info=True)
- return Response({'code': 500, 'msg': '生成新订单失败'})
-
- def _get_game_type_name(self, leixing_id):
- """根据类型ID获取游戏类型名称"""
- try:
- gt = ShangpinLeixing.query.filter(id=leixing_id).first()
- return gt.jieshao if gt else '游戏订单'
- except Exception:
- return '游戏订单'
-
-
-# ===================== 视图:商家罚款申请 =====================
-# 文件路径: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': '证据图片已更新'})
\ No newline at end of file
diff --git a/orders/views/__init__.py b/orders/views/__init__.py
new file mode 100644
index 0000000..9d4bb1e
--- /dev/null
+++ b/orders/views/__init__.py
@@ -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",
+]
diff --git a/orders/views/admin.py b/orders/views/admin.py
new file mode 100644
index 0000000..5663a9d
--- /dev/null
+++ b/orders/views/admin.py
@@ -0,0 +1,1958 @@
+"""orders.views.admin - 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__)
+
+ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}
+
+class AdGetDingdanXiangQing(APIView):
+ """
+ 后台订单详情获取接口
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /dingdan/adddxq
+ 参数: {"zhanghao": "管理员手机号", "dingdan_id": "订单ID"}
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ """
+ 获取订单详情数据
+ 返回: 订单主表数据 + 扩展表数据 + 打手图片列表
+ """
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+
+ if not qianzhanghao or not dingdan_id:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证 (IsAuthenticated已处理)
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 查询订单主表
+ try:
+
+ # 查询订单主表 - 使用select_related预加载关联表
+ dingdan_obj = Order.query.select_related(
+ 'pingtai_kuozhan', 'shangjia_kuozhan'
+ ).get(OrderID=dingdan_id)
+
+ except Order.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '订单不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 7. 构建返回数据 - 主表字段
+ response_data = {
+ # 基础信息字段
+ 'dingdan_id': dingdan_obj.OrderID or '',
+ 'jieshao': dingdan_obj.Description or '',
+ 'zhuangtai': dingdan_obj.Status or 0,
+ 'leixing': dingdan_obj.ProductTypeID or 0, # 前端需要 leixing,对应 leixing_id
+ 'fadanpingtai': dingdan_obj.Platform or 0, # 前端需要 fadanpingtai,对应 fadan_pingtai
+ 'jiage': float(dingdan_obj.Amount) if dingdan_obj.Amount else 0.00, # 前端需要 jiage,对应 jine
+ 'tupian': dingdan_obj.ImageURL or '',
+ 'beizhu': dingdan_obj.Remark or '',
+
+ # 服务信息字段
+ 'jiedan_dashou_id': dingdan_obj.PlayerID or '',
+ 'dashou_fencheng': float(dingdan_obj.PlayerCommission) if dingdan_obj.PlayerCommission else 0.00,
+ 'zhiding_id': dingdan_obj.AssignedID or '',
+ 'dashou_liuyan': dingdan_obj.PlayerRemark or '',
+
+ # 时间字段
+ 'chuangjianshijian': fmt_datetime(dingdan_obj.CreateTime),
+ 'create_time': fmt_datetime(dingdan_obj.CreateTime),
+ 'genggaishijian': fmt_datetime(dingdan_obj.UpdateTime),
+ 'update_time': fmt_datetime(dingdan_obj.UpdateTime),
+ }
+
+ # 8. 根据发单平台查询扩展表数据
+ fadan_pingtai = dingdan_obj.Platform
+
+ if fadan_pingtai == 1:
+ # 平台发单 - 查询平台订单扩展表
+ try:
+ # 使用反向关联 pingtai_kuozhan
+ pingtai_kuozhan = dingdan_obj.pingtai_kuozhan
+
+ response_data.update({
+ 'laoban_id': pingtai_kuozhan.BossID or '',
+ 'nicheng': dingdan_obj.Nickname or '', # 这个字段在订单主表
+ 'laoban_pingjia': pingtai_kuozhan.BossReview or '',
+ })
+ except PlatformOrderExt.DoesNotExist:
+ # 扩展表不存在,返回空值
+ response_data.update({
+ 'laoban_id': '',
+ 'nicheng': dingdan_obj.Nickname or '',
+ 'laoban_pingjia': '',
+ })
+
+ elif fadan_pingtai == 2:
+ # 商家发单 - 查询商家订单扩展表
+ try:
+ # 使用反向关联 shangjia_kuozhan
+ shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
+
+ response_data.update({
+ 'shangjia_id': shangjia_kuozhan.MerchantID or '',
+ 'shangjia_nicheng': shangjia_kuozhan.MerchantNickname or '',
+ 'shangjia_pingjia': shangjia_kuozhan.MerchantReview or '',
+ 'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 商家处罚理由
+ 'bhliyou': shangjia_kuozhan.RejectReason or '', # 驳回理由
+ })
+ except MerchantOrderExt.DoesNotExist:
+ # 扩展表不存在,返回空值
+ response_data.update({
+ 'shangjia_id': '',
+ 'shangjia_nicheng': '',
+ 'shangjia_pingjia': '',
+ 'cfliyou': '',
+ 'bhliyou': '',
+ })
+
+ # 9. 查询打手提交的图片
+ try:
+ # 查询该订单的所有打手提交图片
+ dashou_tupian_list = PlayerDeliveryImage.query.filter(
+ OrderID=dingdan_id
+ ).values_list('ImageURL', flat=True)
+
+ # 转换为列表,过滤空值
+ dashou_images = [img for img in dashou_tupian_list if img]
+ response_data['dashou_images'] = dashou_images
+ except Exception as e:
+ logger.warning(f"查询打手图片失败: {str(e)}")
+ response_data['dashou_images'] = []
+
+ # 🔥 新增:查询退款记录表中的商家退款理由
+ try:
+ # 导入退款记录模型
+ # 查询该订单的退款记录(一个订单可能有多次退款申请,我们取最新的)
+ tuikuan_record = RefundRecord.query.filter(
+ OrderID=dingdan_id
+ ).order_by('-CreateTime').first() # 按创建时间倒序,取最新的
+
+ if tuikuan_record and tuikuan_record.Reason:
+ # 如果有退款记录且理由不为空,添加到返回数据
+ response_data['tuikuan_liyou'] = tuikuan_record.Reason
+ else:
+ # 没有找到退款记录或理由为空
+ response_data['tuikuan_liyou'] = ''
+ except RefundRecord.DoesNotExist:
+ # 退款记录表不存在该订单的记录
+ response_data['tuikuan_liyou'] = ''
+ except Exception as e:
+ # 查询过程中出现其他异常
+ logger.warning(f"查询退款记录失败: {str(e)}")
+ response_data['tuikuan_liyou'] = ''
+
+ # 10. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ logger.error(f"获取订单详情异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+# 获取日志器
+logger = logging.getLogger(__name__)
+
+
+class AdGengHuanDaShou(APIView):
+ """
+ 后台更换打手接口
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /dingdan/adddgh
+ 参数: {
+ "zhanghao": "管理员账号",
+ "dingdan_id": "订单ID",
+ "new_dashou_id": "新打手ID"
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ """
+ 更换订单的打手
+ 核心逻辑:
+ 1. 验证管理员身份
+ 2. 验证订单存在且状态为进行中(2)或结算中(8)
+ 3. 验证新打手ID存在且是打手身份
+ 4. 验证新打手状态正常(Status=1且zhanghaoStatus=1)
+ 5. 根据订单的抢单要求类型进行验证:
+ - 会员抢单(1):验证打手有对应会员且未过期
+ - 押金抢单(2):验证打手押金≥订单押金
+ 6. 更新新打手状态和统计数据
+ 7. 更新原打手状态和统计数据
+ 8. 更新订单信息
+ """
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+ new_dashou_id = request.data.get('new_dashou_id', '').strip()
+
+ if not qianzhanghao or not dingdan_id or not new_dashou_id:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 查询订单主表
+ try:
+ dingdan_obj = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '订单不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 7. 验证订单状态是否为进行中(2)或结算中(8)
+ if dingdan_obj.Status not in [2, 8]:
+ return Response(
+ {'code': 400, 'message': '订单状态不允许更换打手', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 8. 验证新打手ID是否存在,并且是打手身份
+ try:
+ new_dashou_user = User.query.get(
+ UserUID=new_dashou_id,
+ #user_type='PlayerID'
+ )
+ except User.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '打手不存在或不是打手身份', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 9. 获取新打手扩展表
+ try:
+ new_dashou_profile = new_dashou_user.DashouProfile
+ except AttributeError:
+ return Response(
+ {'code': 404, 'message': '打手信息不完整', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 10. 验证新打手状态:Status=1且zhanghaoStatus=1
+ if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
+ return Response(
+ {'code': 400, 'message': '打手状态不允许接单', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 11. 根据订单的抢单要求类型进行验证
+ yaoqiuleixing = dingdan_obj.GrabRequirement
+
+ if yaoqiuleixing == 1: # 会员抢单
+ # 获取订单的会员ID
+ huiyuan_id = dingdan_obj.MembershipID
+ if not huiyuan_id:
+ return Response(
+ {'code': 400, 'message': '订单会员ID为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 查询会员购买记录
+ try:
+ huiyuan_goumai = Huiyuangoumai.query.get(
+ huiyuan_id=huiyuan_id,
+ yonghu_id=new_dashou_id # 注意:yonghu_id存储的是用户ID(yonghuid)
+ )
+ except Huiyuangoumai.DoesNotExist:
+ return Response(
+ {'code': 400, 'message': '打手未购买该订单要求的会员', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 检查会员是否过期
+ # ✅ 可以调用!因为jiance_shifou_daoqi()是Huiyuangoumai模型的实例方法
+ is_expired = huiyuan_goumai.jiance_shifou_daoqi()
+ if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
+ return Response(
+ {'code': 400, 'message': '打手的会员已过期', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ elif yaoqiuleixing == 2: # 押金抢单
+ # 获取订单的押金(yongjin字段)
+ order_yajin = dingdan_obj.CommissionReq
+ if order_yajin is None:
+ return Response(
+ {'code': 400, 'message': '订单押金为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证打手押金是否大于等于订单押金
+ if new_dashou_profile.yajin < order_yajin:
+ return Response(
+ {'code': 400, 'message': '打手押金不足', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ else:
+ # 未知的抢单要求类型
+ return Response(
+ {'code': 400, 'message': '订单抢单要求类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 12. 所有验证通过,开始更换打手(使用事务保证一致性)
+ with transaction.atomic():
+ # 12.1 更新新打手扩展表
+ new_dashou_profile.zhuangtai = 0 # 接单中
+ new_dashou_profile.jiedanzongliang += 1
+ new_dashou_profile.jinrijiedan += 1
+ new_dashou_profile.jinyuejiedan += 1
+ new_dashou_profile.save()
+
+ # 12.2 如果原订单有接单打手,则更新原打手状态
+ old_dashou_id = dingdan_obj.PlayerID
+ if old_dashou_id:
+ try:
+ # 查询原打手主表
+ old_dashou_user = User.query.get(
+ UserUID=old_dashou_id,
+ #user_type='PlayerID'
+ )
+
+ # 获取原打手扩展表
+ old_dashou_profile = old_dashou_user.DashouProfile
+
+ # 更新原打手状态
+ old_dashou_profile.zhuangtai = 1 # 空闲
+ old_dashou_profile.tuikuanliang += 1 # 退款订单总量+1
+ old_dashou_profile.save()
+
+ logger.info(f"成功更新原打手{old_dashou_id}状态")
+ except User.DoesNotExist:
+ # 原打手不存在,继续执行不影响更换
+ logger.warning(f"原打手{old_dashou_id}不存在,跳过更新")
+ except AttributeError:
+ # 原打手扩展表不存在,继续执行不影响更换
+ logger.warning(f"原打手{old_dashou_id}扩展信息不存在,跳过更新")
+ except Exception as e:
+ # 其他异常,记录日志但继续执行
+ logger.error(f"更新原打手状态异常: {str(e)}", exc_info=True)
+
+ # 12.3 更新订单表
+ dingdan_obj.PlayerID = new_dashou_id
+ dingdan_obj.Status = 2 # 进行中状态
+ dingdan_obj.save()
+
+ # 13. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '更换打手成功',
+ 'data': None
+ })
+
+ except Exception as e:
+ logger.error(f"更换打手接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdQiangZhiJieDan(APIView):
+ """
+ 后台强制结单接口
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /dingdan/adddqzjd
+ 参数: {
+ "zhanghao": "管理员账号",
+ "dingdan_id": "订单ID"
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ """
+ 强制结单接口
+ 核心逻辑:
+ 1. 验证管理员身份
+ 2. 验证订单状态为2(进行中)、8(结算中)或4(退款审核中)才能强制结单
+ 3. 根据发单平台(1=平台发单, 2=商家发单)分别处理
+ 4. 更新订单状态为3(已完成)
+ 5. 给打手结算分成金额
+ 6. 如果是商家发单,还需要更新商家成交订单数量
+ """
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+
+ if not qianzhanghao or not dingdan_id:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 查询订单主表
+ try:
+ dingdan_obj = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '订单不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 7. 验证订单状态是否为2、8或4
+ if dingdan_obj.Status not in [2, 8, 4]:
+ return Response(
+ {'code': 400, 'message': '订单状态不允许强制结单', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 8. 获取订单的重要字段
+ jiedan_dashou_id = dingdan_obj.PlayerID # 接单打手ID
+ dashou_fencheng = dingdan_obj.PlayerCommission # 打手分成金额
+ fadan_pingtai = dingdan_obj.Platform # 发单平台
+
+ # 9. 验证打手分成金额是否存在
+ if dashou_fencheng is None:
+ return Response(
+ {'code': 400, 'message': '订单缺少打手分成金额', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 10. 使用事务保证数据一致性
+ with transaction.atomic():
+ # 10.1 更新订单状态为3(已完成)
+ dingdan_obj.Status = 3
+ dingdan_obj.save()
+
+ # 10.2 处理打手结算
+ if jiedan_dashou_id:
+ try:
+ # 查询打手用户主表
+ dashou_user = User.query.get(
+ UserUID=jiedan_dashou_id,
+ #user_type='PlayerID'
+ )
+
+ # 获取打手扩展表
+ dashou_profile = dashou_user.DashouProfile
+
+ # 更新打手扩展表数据
+ # ✅ 确认字段:chengjiaozongliang 成交单总量
+ dashou_profile.chengjiaozongliang += 1
+
+ # ✅ 确认字段:yue 可提现余额
+ dashou_profile.yue += dashou_fencheng
+
+ # ✅ 确认字段:zonge 打单赚取总额
+ dashou_profile.zonge += dashou_fencheng
+
+ # ✅ 确认字段:jinrishouyi 今日收益金额
+ dashou_profile.jinrishouyi += dashou_fencheng
+
+ # ✅ 确认字段:jinyueshouyi 今月收益金额
+ dashou_profile.jinyueshouyi += dashou_fencheng
+
+ dashou_profile.save()
+ logger.info(f"强制结单成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}")
+
+ except User.DoesNotExist:
+ # 打手用户不存在,记录日志但不影响强制结单
+ logger.warning(f"强制结单:打手{jiedan_dashou_id}不存在,跳过打手结算")
+ except AttributeError:
+ # 打手扩展表不存在,记录日志但不影响强制结单
+ logger.warning(f"强制结单:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算")
+
+ # 10.3 如果是商家发单,还需要更新商家扩展表
+ if fadan_pingtai == 2:
+ try:
+ # 获取商家订单扩展表
+ shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
+ shangjia_id = shangjia_kuozhan.MerchantID
+
+ if shangjia_id:
+ # 查询商家用户主表
+ shangjia_user = User.query.get(
+ UserUID=shangjia_id,
+ #user_type='shop'
+ )
+
+ # 获取商家扩展表
+ shangjia_profile = shangjia_user.ShopProfile
+
+ # ✅ 确认字段:chengjiao 商家成交订单总量
+ shangjia_profile.chengjiao += 1
+ shangjia_profile.save()
+
+ logger.info(f"强制结单成功,商家{shangjia_id}成交订单+1")
+
+ except MerchantOrderExt.DoesNotExist:
+ # 商家订单扩展表不存在,跳过商家更新
+ logger.warning(f"强制结单:订单{dingdan_id}商家扩展表不存在,跳过商家更新")
+ except User.DoesNotExist:
+ # 商家用户不存在,跳过商家更新
+ logger.warning(f"强制结单:商家{shangjia_id}不存在,跳过商家更新")
+ except AttributeError:
+ # 商家扩展表不存在,跳过商家更新
+ logger.warning(f"强制结单:商家{shangjia_id}扩展信息不存在,跳过商家更新")
+
+ # 11. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '强制结单成功',
+ 'data': None
+ })
+
+ except Exception as e:
+ logger.error(f"强制结单接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdJuJueTuiKuan(APIView):
+ """
+ 后台拒绝退款接口
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /dingdan/adddjj
+ 参数: {
+ "zhanghao": "管理员账号",
+ "dingdan_id": "订单ID",
+ "jujue_liyou": "拒绝退款理由(可选)"
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ """
+ 拒绝退款接口
+ 核心逻辑:
+ 1. 验证管理员身份
+ 2. 拒绝退款后订单状态改为6(退款失败)
+ 3. 给打手结算分成金额
+ 4. 如果是商家发单,更新商家成交订单数量
+ 5. 更新退款记录表,状态改为2(驳回)
+ """
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+ jujue_liyou = request.data.get('jujue_liyou', '').strip()
+
+ if not qianzhanghao or not dingdan_id:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 查询订单主表
+ try:
+ dingdan_obj = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '订单不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 7. 获取订单的重要字段
+ jiedan_dashou_id = dingdan_obj.PlayerID # 接单打手ID
+ dashou_fencheng = dingdan_obj.PlayerCommission # 打手分成金额
+ fadan_pingtai = dingdan_obj.Platform # 发单平台
+
+ # 8. 验证打手分成金额是否存在
+ if dashou_fencheng is None:
+ return Response(
+ {'code': 400, 'message': '订单缺少打手分成金额', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 9. 使用事务保证数据一致性
+ with transaction.atomic():
+ # 9.1 更新订单状态为6(退款失败)
+ dingdan_obj.Status = 6
+ dingdan_obj.save()
+
+ # 9.2 处理打手结算
+ if jiedan_dashou_id:
+ try:
+ # 查询打手用户主表
+ dashou_user = User.query.get(
+ UserUID=jiedan_dashou_id,
+ #user_type='PlayerID'
+ )
+
+ # 获取打手扩展表
+ dashou_profile = dashou_user.DashouProfile
+
+ # 更新打手扩展表数据
+ # ✅ 确认字段:chengjiaozongliang 成交单总量
+ dashou_profile.chengjiaozongliang += 1
+
+ # ✅ 确认字段:yue 可提现余额
+ dashou_profile.yue += dashou_fencheng
+
+ # ✅ 确认字段:zonge 打单赚取总额
+ dashou_profile.zonge += dashou_fencheng
+
+ # ✅ 确认字段:jinrishouyi 今日收益金额
+ dashou_profile.jinrishouyi += dashou_fencheng
+
+ # ✅ 确认字段:jinyueshouyi 今月收益金额
+ dashou_profile.jinyueshouyi += dashou_fencheng
+
+ dashou_profile.save()
+ logger.info(f"拒绝退款成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}")
+
+ except User.DoesNotExist:
+ # 打手用户不存在,记录日志但不影响操作
+ logger.warning(f"拒绝退款:打手{jiedan_dashou_id}不存在,跳过打手结算")
+ except AttributeError:
+ # 打手扩展表不存在,记录日志但不影响操作
+ logger.warning(f"拒绝退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算")
+
+ # 9.3 如果是商家发单,还需要更新商家扩展表
+ if fadan_pingtai == 2:
+ try:
+ # 获取商家订单扩展表
+ shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
+ shangjia_id = shangjia_kuozhan.MerchantID
+
+ if shangjia_id:
+ # 查询商家用户主表
+ shangjia_user = User.query.get(
+ UserUID=shangjia_id,
+ #user_type='shop'
+ )
+
+ # 获取商家扩展表
+ shangjia_profile = shangjia_user.ShopProfile
+
+ # ✅ 确认字段:chengjiao 商家成交订单总量
+ shangjia_profile.chengjiao += 1
+ shangjia_profile.save()
+
+ logger.info(f"拒绝退款成功,商家{shangjia_id}成交订单+1")
+
+ except MerchantOrderExt.DoesNotExist:
+ # 商家订单扩展表不存在,跳过商家更新
+ logger.warning(f"拒绝退款:订单{dingdan_id}商家扩展表不存在,跳过商家更新")
+ except User.DoesNotExist:
+ # 商家用户不存在,跳过商家更新
+ logger.warning(f"拒绝退款:商家{shangjia_id}不存在,跳过商家更新")
+ except AttributeError:
+ # 商家扩展表不存在,跳过商家更新
+ logger.warning(f"拒绝退款:商家{shangjia_id}扩展信息不存在,跳过商家更新")
+
+ # 9.4 更新退款记录表
+ try:
+ # 查询退款记录表
+ tuikuan_jilu = RefundRecord.query.get(OrderID=dingdan_id)
+
+ # ✅ 确认字段:sqzhuangtai 申请状态,改为2(驳回)
+ tuikuan_jilu.ApplyStatus = 2
+
+ # ✅ 确认字段:bhliyou 驳回理由
+ tuikuan_jilu.RejectReason = jujue_liyou
+
+ # ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号)
+ tuikuan_jilu.ProcessorID = qianzhanghao
+
+ tuikuan_jilu.save()
+ logger.info(f"拒绝退款成功,更新退款记录表,订单:{dingdan_id}")
+
+ except RefundRecord.DoesNotExist:
+ # 退款记录表不存在,记录日志但不影响主流程
+ logger.warning(f"拒绝退款:订单{dingdan_id}退款记录不存在,跳过更新")
+
+ # 10. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '拒绝退款成功',
+ 'data': None
+ })
+
+ except Exception as e:
+ logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdTongYiTuiKuanShangJia(APIView):
+ """
+ 后台同意退款接口(商家发单)
+ 权限: JWT Token认证 + 管理员身份验证 + 密码验证
+ 前端调用: POST /dingdan/adddsjtk
+ 参数: {
+ "zhanghao": "管理员账号",
+ "dingdan_id": "订单ID",
+ "tuikuan_liyou": "退款理由(可选)",
+ "pay_password": "支付密码(openid)"
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ """
+ 同意退款接口(商家发单)
+ 核心逻辑:
+ 1. 验证管理员身份
+ 2. 验证支付密码(openid)
+ 3. 检查订单状态为4(退款审核中)
+ 4. 更新订单状态为5(已退款)
+ 5. 更新打手退款订单数量
+ 6. 更新商家退款订单数量和余额(加上订单金额)
+ 7. 更新退款记录表,状态改为1(成功)
+ """
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+ tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
+ pay_password = request.data.get('pay_password', '').strip()
+
+ if not qianzhanghao or not dingdan_id or not pay_password:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 验证支付密码(对比openid)
+ if pay_password != dangqianyonghu.OpenID:
+ return Response(
+ {'code': 401, 'message': '支付密码错误', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 7. 查询订单主表
+ try:
+ dingdan_obj = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '订单不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 8. 验证订单状态为4(退款审核中)
+ if dingdan_obj.Status != 4:
+ return Response(
+ {'code': 400, 'message': '订单状态不是退款审核中', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 9. 验证订单为商家发单
+ if dingdan_obj.Platform != 2:
+ return Response(
+ {'code': 400, 'message': '此接口只处理商家发单的退款', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 10. 获取订单的重要字段
+ jiedan_dashou_id = dingdan_obj.PlayerID # 接单打手ID
+ jine = dingdan_obj.Amount # 订单金额(不是打手分成)
+
+ # 11. 验证订单金额是否存在
+ if jine is None:
+ return Response(
+ {'code': 400, 'message': '订单缺少订单金额', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 12. 获取商家ID
+ try:
+ shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
+ shangjia_id = shangjia_kuozhan.MerchantID
+ except MerchantOrderExt.DoesNotExist:
+ return Response(
+ {'code': 400, 'message': '订单商家信息不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 13. 使用事务保证数据一致性
+ with transaction.atomic():
+ # 13.1 更新订单状态为5(已退款)
+ dingdan_obj.Status = 5
+ dingdan_obj.save()
+
+ # 13.2 更新打手扩展表(退款订单总量+1)
+ if jiedan_dashou_id:
+ try:
+ # 查询打手用户主表
+ dashou_user = User.query.get(
+ UserUID=jiedan_dashou_id,
+ #user_type='PlayerID'
+ )
+
+ # 获取打手扩展表
+ dashou_profile = dashou_user.DashouProfile
+
+ # ✅ 确认字段:tuikuanliang 退款订单总量
+ dashou_profile.tuikuanliang += 1
+
+ dashou_profile.save()
+ logger.info(f"同意退款成功,打手{jiedan_dashou_id}退款订单+1")
+
+ except User.DoesNotExist:
+ # 打手用户不存在,记录日志但不影响操作
+ logger.warning(f"同意退款:打手{jiedan_dashou_id}不存在,跳过打手更新")
+ except AttributeError:
+ # 打手扩展表不存在,记录日志但不影响操作
+ logger.warning(f"同意退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手更新")
+
+ # 13.3 更新商家扩展表
+ if shangjia_id:
+ try:
+ # 查询商家用户主表
+ shangjia_user = User.query.get(
+ UserUID=shangjia_id,
+ #user_type='shop'
+ )
+
+ # 获取商家扩展表
+ shangjia_profile = shangjia_user.ShopProfile
+
+ # ✅ 确认字段:tuikuan 退款订单总量
+ shangjia_profile.tuikuan += 1
+
+ from merchant_ops.services.wallet import is_staff_quota_order
+ if not is_staff_quota_order(dingdan_id):
+ shangjia_profile.yue += jine
+
+ shangjia_profile.save()
+ logger.info(f"同意退款成功,商家{shangjia_id}退款订单+1,余额增加:{jine}")
+
+ update_shangjia_daily(
+ yonghuid=shangjia_id,
+ amount=Decimal(str(jine)),
+ action=3,
+ order_id=dingdan_id,
+ )
+
+ except User.DoesNotExist:
+ # 商家用户不存在,记录日志但不影响操作
+ logger.warning(f"同意退款:商家{shangjia_id}不存在,跳过商家更新")
+ except AttributeError:
+ # 商家扩展表不存在,记录日志但不影响操作
+ logger.warning(f"同意退款:商家{shangjia_id}扩展信息不存在,跳过商家更新")
+
+ # 13.4 更新退款记录表
+ try:
+ # 查询退款记录表
+ tuikuan_jilu = RefundRecord.query.get(OrderID=dingdan_id)
+
+ # ✅ 确认字段:sqzhuangtai 申请状态,改为1(成功)
+ tuikuan_jilu.PenaltyApplyStatus = 1
+
+ # 可以添加同意理由字段,如:tongyi_liyou
+ # tuikuan_jilu.tongyi_liyou = tuikuan_liyou
+
+ # ✅ 确认字段:chuliid 处理人ID(使用管理员账号,即手机号)
+ tuikuan_jilu.ProcessorID = qianzhanghao
+
+ tuikuan_jilu.save()
+ logger.info(f"同意退款成功,更新退款记录表,订单:{dingdan_id}")
+
+ except RefundRecord.DoesNotExist:
+ # 退款记录表不存在,记录日志但不影响主流程
+ logger.warning(f"同意退款:订单{dingdan_id}退款记录不存在,跳过更新")
+
+ # 14. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '同意退款成功',
+ 'data': None
+ })
+
+ except Exception as e:
+ logger.error(f"同意退款接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdTongYiTuiKuanPingTai(APIView):
+ """
+ 后台同意退款接口(平台发单)
+ 权限: JWT Token认证 + 管理员身份验证 + 密码验证
+ 前端调用: POST /dingdan/adddpttk
+ 参数: {
+ "zhanghao": "管理员账号",
+ "dingdan_id": "订单ID",
+ "tuikuan_liyou": "退款理由(可选)",
+ "pay_password": "支付密码(openid)"
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+ tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
+ pay_password = request.data.get('pay_password', '').strip()
+
+ if not qianzhanghao or not dingdan_id or not pay_password:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if str(getattr(dangqianyonghu, 'Phone', '')) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 验证支付密码(对比openid)
+ if pay_password != dangqianyonghu.OpenID:
+ return Response(
+ {'code': 401, 'message': '支付密码错误', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 7. 查询订单主表(加锁)
+ with transaction.atomic():
+ try:
+ dingdan_obj = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '订单不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 8. 验证订单状态为1、7、2、8、4(允许退款的状态)
+ if dingdan_obj.Status not in [1, 7, 2, 8, 4]:
+ return Response(
+ {'code': 400, 'message': '订单状态不允许退款', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 9. 验证订单为平台发单
+ if dingdan_obj.Platform != 1:
+ return Response(
+ {'code': 400, 'message': '此接口只处理平台发单的退款', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 10. 获取订单的重要字段
+ jiedan_dashou_id = dingdan_obj.PlayerID
+ jine = dingdan_obj.Amount
+ if jine is None:
+ return Response(
+ {'code': 400, 'message': '订单缺少订单金额', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 11. 获取老板ID(从平台订单扩展表)
+ if not hasattr(dingdan_obj, 'pingtai_kuozhan') or not dingdan_obj.pingtai_kuozhan:
+ return Response(
+ {'code': 400, 'message': '订单老板信息不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ laoban_id = dingdan_obj.pingtai_kuozhan.BossID
+
+ # 12. 调用微信退款接口(优先使用 wechat_transaction_id)
+ transaction_id = getattr(dingdan_obj, 'WechatTransactionID', None)
+ weixin_refund_result = self.call_weixin_refund(
+ out_trade_no=dingdan_id,
+ Amount=jine,
+ transaction_id=transaction_id
+ )
+
+ if not weixin_refund_result.get('success'):
+ logger.error(f"微信退款失败,订单:{dingdan_id},错误:{weixin_refund_result.get('msg')}")
+ return Response(
+ {'code': 400, 'message': weixin_refund_result.get('msg', '退款失败')},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 13. 更新订单状态为5(已退款)
+ dingdan_obj.Status = 5
+ dingdan_obj.save()
+
+ # 14. 更新打手扩展表
+ if jiedan_dashou_id:
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ dashou_profile.zhuangtai = 1 # 打手状态设为空闲
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.save()
+ except (User.DoesNotExist, AttributeError):
+ logger.warning(f"平台退款:打手{jiedan_dashou_id}不存在或扩展信息缺失,跳过")
+
+ # 15. 更新老板扩展表
+ if laoban_id:
+ try:
+ laoban_user = User.query.get(UserUID=laoban_id)
+ laoban_profile = laoban_user.BossProfile
+ laoban_profile.alltui += 1
+ laoban_profile.zonge -= jine
+ if laoban_profile.zonge < 0:
+ laoban_profile.zonge = 0
+ laoban_profile.save()
+ except (User.DoesNotExist, AttributeError):
+ logger.warning(f"平台退款:老板{laoban_id}不存在或扩展信息缺失,跳过")
+
+ # 16. 更新收支记录表(按俱乐部)
+ try:
+ from jituan.services.szjilu_accounting import apply_szjilu_expense
+ update_daily_payout(jine, getattr(dingdan_obj, 'ClubID', None))
+ apply_szjilu_expense(jine, getattr(dingdan_obj, 'ClubID', None))
+ except Exception as e:
+ logger.error(f"更新收支记录表异常: {str(e)}")
+
+ # 17. 更新退款记录表
+ try:
+ tuikuan_jilu, created = RefundRecord.query.get_or_create(
+ OrderID=dingdan_id,
+ defaults={
+ 'PlayerID': jiedan_dashou_id if jiedan_dashou_id else '',
+ 'ApplicantID': laoban_id if laoban_id else '',
+ 'ProcessorID': qianzhanghao,
+ 'Reason': tuikuan_liyou if tuikuan_liyou else '给老板退款',
+ 'ApplyStatus': 1,
+ 'Amount': jine,
+ 'PlayerCommission': dingdan_obj.PlayerCommission or 0,
+ 'Description': dingdan_obj.Description or '',
+ 'Remark': '给老板退款',
+ 'Nickname': dingdan_obj.Nickname or '',
+ }
+ )
+ if not created:
+ tuikuan_jilu.ApplyStatus = 1
+ if tuikuan_liyou:
+ tuikuan_jilu.Reason = tuikuan_liyou
+ tuikuan_jilu.ProcessorID = qianzhanghao
+ tuikuan_jilu.save()
+ except Exception as e:
+ logger.error(f"更新退款记录表异常: {str(e)}")
+
+ logger.info(f"平台退款成功,订单:{dingdan_id},退款单号:{weixin_refund_result.get('refund_id')}")
+
+ return Response({'code': 0, 'message': '退款处理成功', 'data': None})
+
+ except Exception as e:
+ logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ def call_weixin_refund(self, out_trade_no, jine, transaction_id=None):
+ """
+ 调用微信退款接口
+ :param out_trade_no: 商户订单号
+ :param jine: 退款金额
+ :param transaction_id: 微信支付订单号(优先使用)
+ :return: {'success': True/False, 'msg': '消息', 'refund_id': '退款单号'}
+ """
+ try:
+ # 检查配置
+ required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH', 'WEIXIN_KEY_PATH']
+ for setting in required_settings:
+ if not hasattr(settings, setting) or not getattr(settings, setting):
+ return {'success': False, 'msg': f'缺少微信支付配置: {setting}'}
+
+ # 生成退款单号
+ refund_id = f"TK{int(time.time())}{random.randint(1000, 9999)}"
+ total_fee_in_fen = yuan_to_fen(jine)
+
+ # 构造请求参数
+ refund_data = {
+ 'appid': settings.WEIXIN_APPID,
+ 'mch_id': settings.WEIXIN_MCHID,
+ 'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(),
+ 'out_refund_no': refund_id,
+ 'total_fee': total_fee_in_fen,
+ 'refund_fee': total_fee_in_fen,
+ 'op_user_id': settings.WEIXIN_MCHID,
+ 'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS'
+ }
+ # 优先使用 transaction_id,否则用 out_trade_no
+ if transaction_id:
+ refund_data['transaction_id'] = transaction_id
+ else:
+ refund_data['out_trade_no'] = out_trade_no
+
+ # 生成签名
+ sorted_params = sorted(refund_data.items())
+ sign_string = '&'.join([f"{k}={v}" for k, v in sorted_params if v])
+ sign_string += f"&key={settings.WEIXIN_SHANGHUMIYAO}"
+ sign = hashlib.md5(sign_string.encode()).hexdigest().upper()
+ refund_data['sign'] = sign
+
+ xml_data = self.dict_to_xml(refund_data)
+ url = "https://api.mch.weixin.qq.com/secapi/pay/refund"
+ response = requests.post(
+ url,
+ data=xml_data.encode('utf-8'),
+ headers={'Content-Type': 'application/xml'},
+ cert=(settings.WEIXIN_CERT_PATH, settings.WEIXIN_KEY_PATH),
+ timeout=30
+ )
+
+ if response.status_code == 200:
+ result = self.xml_to_dict(response.content)
+ if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
+ logger.info(f"微信退款成功,订单:{out_trade_no},退款单号:{refund_id}")
+ return {'success': True, 'msg': '退款申请提交成功', 'refund_id': refund_id}
+ else:
+ error_msg = result.get('err_code_des', result.get('return_msg', '退款申请失败'))
+ logger.error(f"微信退款失败,订单:{out_trade_no},错误:{error_msg}")
+ return {'success': False, 'msg': f'微信退款失败: {error_msg}'}
+ else:
+ logger.error(f"微信退款接口调用失败,状态码:{response.status_code}")
+ return {'success': False, 'msg': '微信退款接口调用失败'}
+
+ except requests.exceptions.Timeout:
+ logger.error(f"微信退款接口请求超时,订单:{out_trade_no}")
+ return {'success': False, 'msg': '微信退款接口请求超时'}
+ except Exception as e:
+ logger.error(f"调用微信退款接口异常: {str(e)}")
+ return {'success': False, 'msg': f'退款处理异常: {str(e)}'}
+
+ def dict_to_xml(self, dict_data):
+ xml = [""]
+ for k, v in dict_data.items():
+ xml.append(f"<{k}>{v}{k}>")
+ xml.append("")
+ return "".join(xml)
+
+ def xml_to_dict(self, xml_data):
+ try:
+ root = ET.fromstring(xml_data)
+ return {child.tag: child.text for child in root}
+ except ET.ParseError as e:
+ logger.error(f"XML解析失败: {str(e)}")
+ return {'return_code': 'FAIL', 'return_msg': 'XML解析失败'}
+
+
+logger = logging.getLogger(__name__)
+
+
+
+
+class AdJuJueJieSuan(APIView):
+ """
+ 后台拒绝结算接口(平台发单)
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /dingdan/adddjjjs
+ 参数: {
+ "zhanghao": "管理员账号(手机号)",
+ "dingdan_id": "订单ID",
+ "tkly": "拒绝结算理由(必填)"
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+ tkly = request.data.get('tkly', '').strip()
+
+ if not all([qianzhanghao, dingdan_id, tkly]):
+ return Response(
+ {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id, tkly)', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT用户验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号匹配(管理员账号通常为手机号)
+ if str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 查询订单
+ try:
+ dingdan = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '订单不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 6. 校验订单条件:平台发单(1)且状态为2或8
+ if dingdan.Platform != 1 or dingdan.Status not in [2, 8]:
+ return Response(
+ {'code': 400, 'message': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ jiedan_dashou_id = dingdan.PlayerID
+ if not jiedan_dashou_id:
+ return Response(
+ {'code': 400, 'message': '订单暂无接单打手', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ with transaction.atomic():
+ # 7. 更新订单状态为5(已退款),并保存拒绝理由
+ dingdan.Status = 5
+ dingdan.RefundReason = tkly
+ dingdan.save()
+
+ # 8. 更新打手扩展表
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ dashou_profile.zhuangtai = 1 # 打手状态设为正常
+ dashou_profile.tuikuanliang += 1 # 退款订单总量+1
+ dashou_profile.save()
+ except (User.DoesNotExist, AttributeError) as e:
+ logger.warning(f"拒绝结算:打手{jiedan_dashou_id}信息更新失败 - {str(e)}")
+
+ return Response({'code': 0, 'message': '拒绝结算成功', 'data': None})
+
+ except Exception as e:
+ logger.error(f"拒绝结算接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdZhuanYiDaTing(APIView):
+ """
+ 后台转移大厅接口
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /dingdan/adddzydt
+ 参数: {
+ "zhanghao": "管理员账号(手机号)",
+ "dingdan_id": "订单ID"
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+
+ if not all([qianzhanghao, dingdan_id]):
+ return Response(
+ {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id)', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ dangqianyonghu = request.user
+
+ if str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ try:
+ dingdan = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '订单不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 校验订单状态:允许2、8、4
+ if dingdan.Status not in [2, 8, 4]:
+ return Response(
+ {'code': 400, 'message': '仅状态为进行中(2)、结算中(8)或退款审核(4)的订单可转移大厅', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ jiedan_dashou_id = dingdan.PlayerID
+
+ with transaction.atomic():
+ # 更新订单:状态改为1(下单中),清空接单打手ID
+ dingdan.Status = 1
+ dingdan.PlayerID = None
+ dingdan.save()
+
+ # 如果有接单打手,更新其扩展表
+ if jiedan_dashou_id:
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ dashou_profile.zhuangtai = 1
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.save()
+ except (User.DoesNotExist, AttributeError) as e:
+ logger.warning(f"转移大厅:打手{jiedan_dashou_id}信息更新失败 - {str(e)}")
+
+ return Response({'code': 0, 'message': '转移大厅成功', 'data': None})
+
+ except Exception as e:
+ logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdQuXiaoZhiDing(APIView):
+ """
+ 后台取消指定接口
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /dingdan/adddqxzd
+ 参数: {
+ "zhanghao": "管理员账号(手机号)",
+ "dingdan_id": "订单ID"
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+
+ if not all([qianzhanghao, dingdan_id]):
+ return Response(
+ {'code': 400, 'message': '参数不完整(需要zhanghao, dingdan_id)', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ dangqianyonghu = request.user
+
+ if str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ try:
+ dingdan = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '订单不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 校验订单状态:必须为7(指定中)
+ if dingdan.Status != 7:
+ return Response(
+ {'code': 400, 'message': '仅状态为指定中(7)的订单可取消指定', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ with transaction.atomic():
+ # 更新订单:状态改为1(下单中),清空指定ID
+ dingdan.Status = 1
+ dingdan.AssignedID = None
+ dingdan.save()
+
+ return Response({'code': 0, 'message': '取消指定成功', 'data': None})
+
+ except Exception as e:
+ logger.error(f"取消指定接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+
+
+class ZhiDingChaxunView(APIView):
+ """
+ 查询当前打手被指定的待接订单
+ POST /dingdan/zdcx
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ dashou_uid = str(request.user.UserUID)
+ try:
+ dashou_profile = request.user.DashouProfile
+ if dashou_profile.zhanghaozhuangtai != 1:
+ return Response({'code': 403, 'msg': '账号状态异常'}, status=403)
+ except Exception:
+ return Response({'code': 403, 'msg': '您不是打手'}, status=403)
+
+ orders = Order.query.filter(
+ Status=7,
+ AssignedID=dashou_uid,
+ ).filter(
+ Q(PlayerID__isnull=True) | Q(PlayerID='')
+ ).order_by('-CreateTime')[:20]
+
+ result = []
+ for o in orders:
+ hf = getattr(o, 'AssignedResponse', None)
+ result.append({
+ 'dingdan_id': o.OrderID,
+ 'zhuangtai': o.Status,
+ 'pingtai': o.Platform,
+ 'dashou_fencheng': float(o.PlayerCommission or 0),
+ 'leixing_id': o.ProductTypeID or 0,
+ 'tupian': o.ImageURL or '',
+ 'jieshao': o.Description or '',
+ 'beizhu': o.Remark or '',
+ 'creat_time': fmt_datetime(o.CreateTime),
+ 'create_time': fmt_datetime(o.CreateTime),
+ 'zhiding_uid': o.AssignedID or '',
+ 'zhiding_hf': hf,
+ 'zhiding_hf_text': ZHIDING_HF_TEXT.get(hf, ''),
+ 'zhiding_dhj_sm': getattr(o, 'AssignedLaterNote', '') or '',
+ 'zhiding_hf_sj': fmt_datetime(o.AssignedResponseAt) if getattr(o, 'AssignedResponseAt', None) else '',
+ 'yaoqiuleixing': o.GrabRequirement or 0,
+ 'huiyuan_id': o.MembershipID or '',
+ 'yajin': float(o.CommissionReq or 0),
+ })
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': {'list': result, 'count': len(result)},
+ })
+ except Exception as e:
+ logger.error(f"指定订单查询失败: {e}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
+
+
+class ZhiDingHuiFuView(APIView):
+ """
+ 指定单打手响应:想接 / 不想接 / 等会接
+ POST /dingdan/zdhf
+ 参数: dingdan_id, huifu (1|2|3), deng_hui_note (等会接说明,可选)
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ data = request.data
+ dingdan_id = str(data.get('dingdan_id') or data.get('OrderID') or '').strip()
+ huifu = int(data.get('huifu') or 0)
+ deng_hui_note = str(data.get('deng_hui_note') or '').strip()[:100]
+
+ if not dingdan_id:
+ return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400)
+ if huifu not in (1, 2, 3):
+ return Response({'code': 400, 'msg': '无效的响应类型'}, status=400)
+ if huifu == 3 and not deng_hui_note:
+ return Response({'code': 400, 'msg': '请填写预计接待时间'}, status=400)
+
+ dashou_uid = str(request.user.UserUID)
+
+ with transaction.atomic():
+ try:
+ order = Order.query.select_for_update().get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=404)
+
+ if order.Status != 7:
+ return Response({'code': 400, 'msg': '订单已不是指定中状态'}, status=400)
+ if str(order.AssignedID or '') != dashou_uid:
+ return Response({'code': 403, 'msg': '您不是该订单的指定打手'}, status=403)
+ if order.PlayerID:
+ return Response({'code': 400, 'msg': '订单已被接单'}, status=400)
+
+ now = timezone.now()
+ order.AssignedResponse = huifu
+ order.AssignedResponseAt = now
+
+ if huifu == 2:
+ order.Status = 1
+ order.AssignedID = None
+ order.AssignedLaterNote = ''
+ elif huifu == 3:
+ order.AssignedLaterNote = deng_hui_note
+ else:
+ order.AssignedLaterNote = ''
+
+ order.save()
+
+ msg_map = {1: '已标记想接,请点击接待完成接单', 2: '已婉拒,订单已退回大厅', 3: '已记录,请按时接待'}
+ return Response({
+ 'code': 200,
+ 'msg': msg_map.get(huifu, '操作成功'),
+ 'data': {
+ 'dingdan_id': dingdan_id,
+ 'huifu': huifu,
+ 'huifu_text': ZHIDING_HF_TEXT.get(huifu, ''),
+ },
+ })
+ except Exception as e:
+ logger.error(f"指定订单响应失败: {e}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
+
+
+
+
+class ZxsjghdsView(APIView):
+ """
+ 商家更换打手接口
+ 路径:POST /dingdan/zxsjghds
+ 权限:仅限已认证商家(通过JWT)
+ 功能:
+ 1. 将原订单状态改为已退款(5),释放接单打手(若有)
+ 2. 更新打手统计(退款量、状态、每日统计)
+ 3. 写入退款记录
+ 4. 若为跨平台订单,通知对方平台退款(必须成功)
+ 5. 基于原订单生成新订单(状态直接为1),复制核心数据
+ 6. 不操作商家余额、退款次数等(替换订单)
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # ========== 1. 获取当前商家用户 ==========
+ current_user = request.user
+ try:
+ yonghuid = current_user.UserUID
+ except AttributeError:
+ return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # ========== 2. 提取订单ID ==========
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+ if not dingdan_id:
+ return Response({'code': 2, 'msg': '订单ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # ========== 3. 查询订单并校验权限 ==========
+ try:
+ # 关联查询商家扩展表,确保当前商家是该订单的发布者
+ shangjia_kuozhan = MerchantOrderExt.query.select_related('Order').get(
+ Order__OrderID=dingdan_id,
+ MerchantID=yonghuid
+ )
+ except MerchantOrderExt.DoesNotExist:
+ return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN)
+
+ old_order = shangjia_kuozhan.Order
+
+ # ========== 4. 状态校验(仅允许进行中或结算中) ==========
+ if old_order.Status not in [2, 8]:
+ return Response({'code': 4, 'msg': '当前订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # ========== 5. 收集订单关键数据(用于新订单复制) ==========
+ # 这些字段将在退款处理后用于创建新订单
+ jine = old_order.Amount
+ dashou_fencheng = old_order.PlayerCommission
+ leixing_id = old_order.ProductTypeID
+ yaoqiuleixing = old_order.GrabRequirement
+ huiyuan_id = old_order.MembershipID
+ yongjin = old_order.CommissionReq
+ jieshao = old_order.Description
+ nicheng = old_order.Nickname
+ beizhu = old_order.Remark
+ waibu_dingdan_id = old_order.ExternalOrderID
+ user1_id = old_order.User1ID
+ tupian = old_order.ImageURL
+ fadan_pingtai = old_order.Platform
+
+ dashou_id = old_order.PlayerID # 可能为空
+
+ # ========== 6. 退款处理(事务块1:退款原订单 + 打手统计 + 跨平台通知) ==========
+ try:
+ with transaction.atomic():
+ # 6.1 订单状态更新为已退款
+ old_order.Status = 5
+ old_order.save(update_fields=['Status'])
+
+ # 6.2 释放打手
+ if dashou_id:
+ try:
+ dashou_user = User.query.get(UserUID=dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1
+ dashou_profile.zhuangtai = 1 # 空闲
+ dashou_profile.save(update_fields=['tuikuanliang', 'zhuangtai'])
+ except (User.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"打手 {dashou_id} 不存在或扩展表缺失,跳过更新")
+
+ # 6.3 更新退款记录
+ RefundRecord.query.update_or_create(
+ OrderID=dingdan_id,
+ defaults={
+ 'PlayerID': dashou_id or '',
+ 'ApplicantID': yonghuid, # 申请人为当前商家
+ 'ProcessorID': yonghuid, # 处理人也是商家自己
+ 'Reason': '商家更换打手',
+ 'ApplyStatus': 1, # 状态 1=成功
+ 'Amount': jine or 0,
+ 'PlayerCommission': dashou_fencheng or 0,
+ 'Description': jieshao or '',
+ 'Remark': '更换打手系统自动退款',
+ 'Nickname': nicheng or '',
+ }
+ )
+
+ # 6.4 打手每日统计(退款行为)
+ if dashou_id:
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=dashou_id,
+ amount=dashou_fencheng,
+ action=3 # 3=退款
+ )
+ except Exception as e:
+ logger.error(f"更新打手每日统计失败: {e}")
+ # 统计失败不应阻断主流程,但可记录
+
+ except Exception as e:
+ logger.error(f"退款处理事务失败: {e}", exc_info=True)
+ return Response({'code': 500, 'msg': '退款处理失败'})
+
+ # ========== 7. 生成新订单(事务块2) ==========
+ try:
+ with transaction.atomic():
+ # 7.1 生成新的订单ID
+ def generate_dingdan_id():
+ timestamp = int(time.time() * 1000)
+ process_id = os.getpid() % 1000
+ random_num = random.randint(1000, 9999)
+ return f"SJ{timestamp}{process_id:03d}{random_num}"
+
+ new_dingdan_id = generate_dingdan_id()
+ # 确保唯一
+ while Order.query.filter(OrderID=new_dingdan_id).exists():
+ new_dingdan_id = generate_dingdan_id()
+
+ # 7.2 创建新订单(复制数据,状态为 1)
+ new_order = Order.query.create(
+ OrderID=new_dingdan_id,
+ Status=1, # 已下单
+ Platform=fadan_pingtai,
+ Amount=jine,
+ PlayerCommission=dashou_fencheng,
+ ProductTypeID=leixing_id,
+ GrabRequirement=yaoqiuleixing,
+ MembershipID=huiyuan_id,
+ CommissionReq=yongjin,
+ Description=jieshao,
+ Nickname=nicheng,
+ Remark=beizhu,
+ User1ID=user1_id, # 商家标识
+ ImageURL=tupian,
+ ExternalOrderID=waibu_dingdan_id,
+ ClubID=getattr(old_order, 'ClubID', None) or 'xq',
+ # 不继承:zhiding_id, jiedan_dashou_id, dashou_liuyan 等
+ )
+
+ # 7.3 创建商家扩展表(关联新订单)
+ MerchantOrderExt.query.create(
+ Order=new_order,
+ MerchantID=yonghuid,
+ MerchantNickname=shangjia_kuozhan.MerchantNickname or '',
+ )
+
+ # 7.4 异步通知打手(广播新订单)
+ try:
+ order_info = {
+ 'OrderID': new_dingdan_id,
+ 'game_type': self._get_game_type_name(leixing_id),
+ 'amount': str(jine),
+ 'order_desc': (jieshao or '')[:50],
+ }
+ dingdan_guangbo.delay(order_info)
+ except Exception as e:
+ logger.error(f"广播通知失败: {e}")
+
+ # 注意:此处不更新商家余额、发布量等统计,因为是替换订单
+
+ logger.info(f"商家{yonghuid}更换打手成功,旧订单{dingdan_id}已退款,新订单{new_dingdan_id}")
+
+ return Response({
+ 'code': 0,
+ 'msg': '更换成功',
+ 'data': {
+ 'xin_dingdan_id': new_dingdan_id,
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"生成新订单事务失败: {e}", exc_info=True)
+ return Response({'code': 500, 'msg': '生成新订单失败'})
+
+ def _get_game_type_name(self, leixing_id):
+ """根据类型ID获取游戏类型名称"""
+ try:
+ gt = ShangpinLeixing.query.filter(id=leixing_id).first()
+ return gt.jieshao if gt else '游戏订单'
+ except Exception:
+ return '游戏订单'
+
+
diff --git a/orders/views/boss_orders.py b/orders/views/boss_orders.py
new file mode 100644
index 0000000..88155ba
--- /dev/null
+++ b/orders/views/boss_orders.py
@@ -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})
+
+
+
+
diff --git a/orders/views/chat.py b/orders/views/chat.py
new file mode 100644
index 0000000..9cf7a3c
--- /dev/null
+++ b/orders/views/chat.py
@@ -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})
+
+
diff --git a/orders/views/dashou.py b/orders/views/dashou.py
new file mode 100644
index 0000000..b465895
--- /dev/null
+++ b/orders/views/dashou.py
@@ -0,0 +1,1513 @@
+"""orders.views.dashou - 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 DashouDingdanHuoquView(APIView):
+ """
+ 获取可抢订单列表(支持标签筛选)
+ POST /dingdan/ddhq
+ 需要JWT Token认证
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ from orders.services.fake_order_pool import (
+ dashou_should_use_fake_pool,
+ build_fake_order_pool_response,
+ )
+ if dashou_should_use_fake_pool(request.user):
+ return build_fake_order_pool_response(request)
+
+ # 1. 获取请求参数
+ data = request.data
+ leixing_id = pick_leixing_id(data)
+ page = int(data.get('page', 1))
+ page_size = int(data.get('page_size', 10))
+ biaoqian_id = data.get('biaoqian_id')
+
+ # 2. 构建基础查询条件
+ base_query = Q(Status__in=[1, 7])
+ if leixing_id is not None:
+ base_query &= Q(ProductTypeID=leixing_id)
+
+ # 3. 标签筛选:先查出符合条件的订单ID,避免跨表 JOIN 排序冲突
+ if biaoqian_id and int(biaoqian_id) > 0:
+ biaoqian_order_ids = DingdanBiaoqian.query.filter(
+ chenghao_id=int(biaoqian_id)
+ ).values_list('dingdan_id', flat=True)
+ base_query &= Q(OrderID__in=list(biaoqian_order_ids))
+
+ # 4. 构建基础查询集
+ base_qs = Order.query.filter(base_query).order_by('-CreateTime').distinct()
+
+ # 5. 获取总记录数
+ total = base_qs.count()
+
+ # 6. 计算分页偏移量
+ offset = (page - 1) * page_size
+ limit = page_size
+
+ # 7. 获取当前页数据
+ page_qs = base_qs[offset:offset + limit]
+
+ # 8. 使用子查询获取商家昵称(Subquery 须用 Django 原生 QuerySet,不能用 .query.values())
+ shangjia_sub = MerchantOrderExt.objects.filter(
+ Order__OrderID=OuterRef('OrderID')
+ ).values('MerchantNickname')[:1]
+
+ # 9. 获取当前页所有需要的字段
+ dingdan_values = page_qs.annotate(
+ MerchantNickname=Subquery(shangjia_sub)
+ ).values(
+ 'OrderID', 'Status', 'Platform', 'PlayerCommission',
+ 'AssignedID', 'AssignedResponse', 'AssignedResponseAt', 'AssignedLaterNote',
+ 'ProductTypeID', 'ImageURL', 'GrabRequirement',
+ 'MembershipID', 'CommissionReq', 'Description', 'Remark', 'CreateTime',
+ 'MerchantNickname'
+ )
+
+ # 10. 转换为列表
+ dingdan_list = list(dingdan_values)
+
+ # 11. 批量获取指定打手的头像和昵称
+ zhiding_ids = {item['AssignedID'] for item in dingdan_list if item.get('AssignedID')}
+ zhiding_info = {}
+ if zhiding_ids:
+ dashou_query = User.query.filter(
+ UserUID__in=zhiding_ids
+ ).select_related('DashouProfile').annotate(
+ Nickname=F('DashouProfile__nicheng')
+ ).values('UserUID', 'Avatar', 'Nickname')
+ zhiding_info = {
+ item['UserUID']: {
+ 'avatar': item['Avatar'] or '',
+ 'nicheng': item['Nickname'] or ''
+ }
+ for item in dashou_query
+ }
+
+ # ============ 批量获取三层标签 ============
+ order_ids = [item['OrderID'] for item in dingdan_list]
+
+ # --- 订单需求标签 ---
+ xuqiu_tags_map = {}
+ if order_ids:
+ try:
+ dbq_qs = DingdanBiaoqian.query.filter(
+ dingdan_id__in=order_ids
+ ).select_related('chenghao')
+ for dbq in dbq_qs:
+ oid = dbq.dingdan_id
+ if oid not in xuqiu_tags_map:
+ xuqiu_tags_map[oid] = []
+ ch = dbq.chenghao
+ xuqiu_tags_map[oid].append({
+ 'id': ch.id,
+ 'mingcheng': ch.mingcheng,
+ 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu)
+ })
+ except Exception as e:
+ logger.error(f"查询订单需求标签失败: {e}", exc_info=True)
+
+ # --- 所有需要查询标签的用户ID集合(指定打手 + 商家) ---
+ yonghuid_set = set(zhiding_ids)
+ shangjia_order_ids = [item['OrderID'] for item in dingdan_list if item['Platform'] == 2]
+ shangjia_id_map = {}
+ if shangjia_order_ids:
+ # 使用跨表查询 Order__OrderID__in 匹配字符串订单ID
+ sj_exts = MerchantOrderExt.query.filter(
+ Order__OrderID__in=shangjia_order_ids
+ ).values('Order__OrderID', 'MerchantID')
+ for sj in sj_exts:
+ shangjia_id_map[sj['Order__OrderID']] = sj['MerchantID']
+ if sj['MerchantID']:
+ yonghuid_set.add(sj['MerchantID'])
+
+ # 将 yonghuid 字符串转换为 User 的主键 UserUUID
+ user_id_map = {}
+ user_avatar_map = {}
+ if yonghuid_set:
+ users = User.query.filter(UserUID__in=yonghuid_set).values('UserUUID', 'UserUID', 'Avatar')
+ for u in users:
+ user_id_map[u['UserUID']] = u['UserUUID']
+ user_avatar_map[u['UserUID']] = (u['Avatar'] or '').strip()
+
+ user_main_ids = list(user_id_map.values())
+ user_tags_map = {}
+ if user_main_ids:
+ uc_qs = YonghuChenghao.query.filter(
+ yonghu_id__in=user_main_ids
+ ).select_related('chenghao')
+ for uc in uc_qs:
+ uid = uc.yonghu_id
+ if uid not in user_tags_map:
+ user_tags_map[uid] = []
+ user_tags_map[uid].append(uc.chenghao)
+
+ dashou_tags_map = {}
+ shangjia_tags_map = {}
+ for item in dingdan_list:
+ oid = item['OrderID']
+ # 指定打手标签
+ zhid = item['AssignedID']
+ dashou_tags = []
+ if zhid and zhid in user_id_map:
+ uid = user_id_map[zhid]
+ if uid in user_tags_map:
+ for ch in user_tags_map[uid]:
+ if ch.leixing == 'dashou':
+ dashou_tags.append({
+ 'id': ch.id,
+ 'mingcheng': ch.mingcheng,
+ 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu)
+ })
+ dashou_tags_map[oid] = dashou_tags
+
+ # 商家标签
+ sj_tags = []
+ if item['Platform'] == 2 and oid in shangjia_id_map:
+ sj_yonghuid = shangjia_id_map[oid]
+ if sj_yonghuid and sj_yonghuid in user_id_map:
+ uid = user_id_map[sj_yonghuid]
+ if uid in user_tags_map:
+ for ch in user_tags_map[uid]:
+ if ch.leixing == 'shangjia':
+ sj_tags.append({
+ 'id': ch.id,
+ 'mingcheng': ch.mingcheng,
+ 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu)
+ })
+ shangjia_tags_map[oid] = sj_tags
+
+ # --- 身份装饰标签 + 优质商家 ---
+ from jituan.services.identity_tag import (
+ SHENFEN_DASHOU,
+ SHENFEN_SHANGJIA,
+ batch_identity_tags,
+ )
+ from jituan.services.club_context import resolve_effective_club_id
+ from users.models import UserShangjia
+
+ identity_pairs = []
+ youzhi_map = {}
+ merchant_uids = set(shangjia_id_map.values())
+ if merchant_uids:
+ for sj in UserShangjia.query.filter(
+ user__UserUID__in=merchant_uids
+ ).select_related('user').values('user__UserUID', 'is_youzhi_shangjia'):
+ youzhi_map[sj['user__UserUID']] = bool(sj['is_youzhi_shangjia'])
+
+ for item in dingdan_list:
+ oid = item['OrderID']
+ zhid = item.get('AssignedID')
+ if zhid:
+ identity_pairs.append((zhid, SHENFEN_DASHOU))
+ if item['Platform'] == 2 and oid in shangjia_id_map:
+ sj_uid = shangjia_id_map[oid]
+ if sj_uid:
+ identity_pairs.append((sj_uid, SHENFEN_SHANGJIA))
+
+ club_id = resolve_effective_club_id(request, getattr(request, 'user', None))
+ identity_tag_map = batch_identity_tags(club_id, identity_pairs) if identity_pairs else {}
+
+ # 12. 格式化返回数据
+ formatted_list = []
+ for order in dingdan_list:
+ oid = order['OrderID']
+ zid = order.get('AssignedID')
+ info = zhiding_info.get(zid, {})
+ sj_uid = shangjia_id_map.get(oid) if order['Platform'] == 2 else None
+ formatted_list.append({
+ 'dingdan_id': order['OrderID'],
+ 'zhuangtai': order['Status'],
+ 'pingtai': order['Platform'],
+ 'dashou_fencheng': float(order['PlayerCommission']) if order['PlayerCommission'] else 0.0,
+ 'leixing_id': order['ProductTypeID'] or 0,
+ 'tupian': order['ImageURL'] or '',
+ 'yaoqiuleixing': order['GrabRequirement'] or 0,
+ 'huiyuan_id': order['MembershipID'] or '',
+ 'yajin': float(order['CommissionReq']) if order['CommissionReq'] else 0.0,
+ 'jieshao': order['Description'] or '',
+ 'beizhu': order['Remark'] or '',
+ 'creat_time': fmt_datetime(order['CreateTime']),
+ 'create_time': fmt_datetime(order['CreateTime']),
+ 'sjnicheng': order['MerchantNickname'] or '',
+ 'sj_avatar': user_avatar_map.get(sj_uid, '') if sj_uid else '',
+ 'zhiding_uid': order['AssignedID'] or '',
+ 'zhiding_avatar': info.get('avatar', ''),
+ 'zhiding_nicheng': info.get('nicheng', ''),
+ 'zhiding_hf': order.get('AssignedResponse'),
+ 'zhiding_hf_text': ZHIDING_HF_TEXT.get(order.get('AssignedResponse'), ''),
+ 'zhiding_dhj_sm': order.get('AssignedLaterNote') or '',
+ 'zhiding_hf_sj': fmt_datetime(order['AssignedResponseAt']) if order.get('AssignedResponseAt') else '',
+ # 三层标签
+ 'xuqiu_biaoqian': xuqiu_tags_map.get(oid, []),
+ 'dashou_biaoqian': dashou_tags_map.get(oid, []),
+ 'shangjia_biaoqian': shangjia_tags_map.get(oid, []),
+ # 身份装饰标签 + 优质商家
+ 'shangjia_identity_biaoqian': identity_tag_map.get((sj_uid, SHENFEN_SHANGJIA), []) if sj_uid else [],
+ 'zhiding_identity_biaoqian': identity_tag_map.get((zid, SHENFEN_DASHOU), []) if zid else [],
+ 'shangjia_youzhi': bool(youzhi_map.get(sj_uid)) if sj_uid else False,
+ })
+
+ has_more = (page * page_size) < total
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': formatted_list,
+ 'has_more': has_more,
+ 'total': total,
+ 'current_page': page,
+ 'page_size': page_size,
+ 'total_pages': (total + page_size - 1) // page_size,
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"订单列表查询失败: {e}", exc_info=True)
+ return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
+
+ def _parse_texiao_json(self, texiao_miaoshu):
+ if not texiao_miaoshu:
+ return {}
+ try:
+ return json.loads(texiao_miaoshu)
+ except:
+ return {}
+
+
+
+
+
+class ShangpinLeixingHuoquView(APIView):
+ """
+ 获取商品类型列表
+ POST /dingdan/ddlxhq
+ 需要JWT Token认证
+ """
+ authentication_classes = [JWTAuthentication]
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 查询商品类型表
+ # 假设你的商品类型模型字段为:id, jieshao, tupian_url
+ leixing_list = ShangpinLeixing.query.all().values(
+ 'id', # 商品类型ID
+ 'jieshao', # 类型介绍
+ 'tupian_url' # 类型图片URL(相对URL)
+ ).order_by('id') # 按ID排序,可根据需要调整
+
+ # 2. 格式化返回数据
+ formatted_list = []
+ for leixing in leixing_list:
+ formatted_leixing = {
+ 'id': leixing['id'],
+ 'jieshao': leixing['jieshao'] or '', # 处理空值
+ 'tupian_url': leixing['tupian_url'] or '' # 处理空值
+ }
+ formatted_list.append(formatted_leixing)
+
+ # 3. 返回响应
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': formatted_list,
+ 'total': len(formatted_list)
+ }
+ })
+
+
+
+
+# ----------------- 基础配置获取 -----------------
+
+
+class QiangdanView(APIView):
+ authentication_classes = [JWTAuthentication]
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ data = request.data
+ dingdan_id = data.get('dingdan_id') or data.get('OrderID')
+ if not dingdan_id:
+ return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400)
+
+ dingdan_id = str(dingdan_id).strip()
+ if dingdan_id.upper().startswith('JD'):
+ return Response({'code': 400, 'msg': '您没充值会员或保证金不可接单'})
+
+ try:
+ with transaction.atomic():
+ # 1. 验证打手身份和状态
+ dashou_profile = self._validate_dashou_status(request.user)
+ if not dashou_profile:
+ return Response({'code': 403, 'msg': '您不是打手或账号状态异常'}, status=403)
+
+
+ # ==================== 【新增】罚单检查 ====================
+ fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(request.user.UserUID,1)
+ if not fadan_ok:
+ return Response({'code': 400, 'msg': fadan_msg})
+ # =========================================================
+ # ========== 标签快速校验(提前拒绝,避免锁资源) ==========
+ biaoqian_ok, biaoqian_msg = check_dashou_biaoqian_required(dingdan_id, request.user.UserUID)
+ if not biaoqian_ok:
+ return Response({'code': 400, 'msg': biaoqian_msg})
+ # ===========================================================
+
+ # 2. 锁定并验证订单(行锁)
+ order_result = self._validate_and_lock_order(dingdan_id, request.user.UserUID)
+ if isinstance(order_result, str):
+ return Response({'code': 400, 'msg': order_result})
+ order = order_result
+
+ validation_result = self._validate_order_requirements(order, dashou_profile, request.user.UserUID)
+ if not validation_result['success']:
+ return Response({'code': 400, 'msg': validation_result['message']})
+ self._execute_qiangdan(order, dashou_profile, request.user.UserUID)
+
+ # ========== 【唯一改动】聊天建立方式 ==========
+ chat_success = establish_order_chat(dingdan_id)
+
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=order.PlayerCommission,
+ action=1
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+
+
+ # 在 QiangdanView.post 里,_execute_qiangdan 之后添加:
+ # 记录抢单表现(不影响主流程)
+ # 如果订单表里有 bankuai 信息,可以用 order.bankuai 代替 None
+ record_dashou_biaoxian(
+ dingdan_id=dingdan_id,
+ dashou_id=order.PlayerID,
+ xingwei=1,
+ jiedan_time=timezone.now(), # 直接取当前时间
+ bankuai=None # 暂不填,后续提交时补上
+ )
+
+ return Response({
+ 'code': 200,
+ 'msg': '抢单成功',
+ 'data': {
+ 'dingdan_id': dingdan_id,
+ 'chat_established': chat_success
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"抢单异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '系统异常,请稍后重试'}, status=500)
+
+ # ---------- 以下所有方法原封不动照抄旧版 ----------
+ def _validate_dashou_status(self, user):
+ try:
+ dashou_profile = user.DashouProfile
+ if (dashou_profile.zhuangtai != 1 or
+ dashou_profile.zhanghaozhuangtai != 1 or
+ dashou_profile.jifen < 5):
+ return None
+ return dashou_profile
+ except UserDashou.DoesNotExist:
+ return None
+
+ def _validate_and_lock_order(self, dingdan_id, dashou_yonghuid):
+ try:
+ order = Order.query.select_for_update().get(OrderID=dingdan_id)
+ if order.Status not in [1, 7]:
+ return "订单状态不可抢"
+ if order.Status == 7:
+ if not order.AssignedID or order.AssignedID != dashou_yonghuid:
+ return "此订单为指定单,您不是指定打手"
+ if order.PlayerID:
+ return "订单已被其他打手抢走"
+ return order
+ except Order.DoesNotExist:
+ return "订单不存在"
+
+ def _validate_order_requirements(self, order, dashou_profile, dashou_yonghuid):
+ if order.GrabRequirement == 1:
+ return self._validate_huiyuan_requirement(order, dashou_yonghuid)
+ elif order.GrabRequirement == 2:
+ return self._validate_yajin_requirement(order, dashou_profile)
+ return {'success': True, 'message': '验证通过'}
+
+ def _validate_huiyuan_requirement(self, order, dashou_yonghuid):
+ try:
+ huiyuan_record = Huiyuangoumai.query.get(
+ huiyuan_id=order.MembershipID,
+ yonghu_id=dashou_yonghuid
+ )
+ if huiyuan_record.jiance_shifou_daoqi() or huiyuan_record.huiyuan_zhuangtai != 1:
+ return {'success': False, 'message': '会员已过期或未开通对应会员'}
+ return {'success': True, 'message': '会员验证通过'}
+ except Huiyuangoumai.DoesNotExist:
+ return {'success': False, 'message': '未开通对应会员,无法抢此订单'}
+
+ def _validate_yajin_requirement(self, order, dashou_profile):
+ yajin_required = order.CommissionReq or 0
+ dashou_yajin = dashou_profile.yajin or 0
+ if dashou_yajin < yajin_required:
+ return {'success': False, 'message': f'押金不足,需要{yajin_required}元,当前有{dashou_yajin}元'}
+ return {'success': True, 'message': '押金验证通过'}
+
+ def _execute_qiangdan(self, order, dashou_profile, dashou_yonghuid):
+ order.Status = 2
+ order.PlayerID = dashou_yonghuid
+ order.User2ID = f"Ds{dashou_yonghuid}"
+ order.save()
+
+ dashou_profile.zhuangtai = 0
+ dashou_profile.jiedanzongliang = F('jiedanzongliang') + 1
+ dashou_profile.jinrijiedan = F('jinrijiedan') + 1
+ dashou_profile.jinyuejiedan = F('jinyuejiedan') + 1
+ dashou_profile.save()
+
+
+class DashouDingdanHuoquView1(APIView):
+ """
+ 打手订单获取接口
+ 支持筛选条件:
+ - zhuangtai_list: 订单状态列表(可选,空数组或不传表示全部状态)
+ - leixing_id: 商品类型ID(可选)
+ - keyword: 搜索关键词,模糊匹配订单ID和订单介绍(可选)
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取当前登录用户的 yonghuid
+ try:
+ current_user = request.user
+ yonghuid = current_user.UserUID
+ except AttributeError:
+ return Response({
+ 'code': 1,
+ 'msg': '用户认证失败',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 获取并校验前端参数
+ try:
+ data = request.data
+ zhuangtai_list = data.get('zhuangtai_list', []) # 可选,默认空列表(全部)
+ page = max(1, int(data.get('page', 1)))
+ page_size = min(100, max(1, int(data.get('page_size', 10))))
+ leixing_id = pick_leixing_id(data) # 可选,商品类型ID
+ keyword = data.get('keyword', '').strip() # 可选,搜索关键词
+ except (ValueError, TypeError):
+ return Response({
+ 'code': 2,
+ 'msg': '参数格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 参数默认值与类型修正
+ if not isinstance(zhuangtai_list, list):
+ zhuangtai_list = [] # 非列表则视为全部
+
+ # 4. 构建查询条件
+ try:
+ # 基础条件:当前打手接单
+ query = Q(PlayerID=yonghuid)
+
+ # 状态筛选:如果 zhuangtai_list 不为空列表,则加入筛选;否则查询全部状态
+ if zhuangtai_list:
+ query &= Q(Status__in=zhuangtai_list)
+
+ # 商品类型筛选
+ if leixing_id is not None:
+ query &= Q(ProductTypeID=leixing_id)
+
+ # 关键词搜索:模糊匹配订单ID和订单介绍
+ if keyword:
+ keyword_query = Q(OrderID__icontains=keyword) | Q(Description__icontains=keyword)
+ query &= keyword_query
+
+ # 5. 执行查询,按创建时间倒序
+ dingdan_query = Order.query.filter(query).order_by('-CreateTime')
+
+ # 只选择需要的字段,提高性能
+ dingdan_query = dingdan_query.only(
+ 'OrderID',
+ 'Status',
+ 'PlayerCommission', # 打手分成金额
+ 'Description',
+ 'ImageURL',
+ 'Nickname',
+ 'CreateTime'
+ )
+
+ total_count = dingdan_query.count()
+
+ # 6. 分页处理
+ start_index = (page - 1) * page_size
+ end_index = start_index + page_size
+ current_page_orders = dingdan_query[start_index:end_index]
+ current_page_count = len(current_page_orders)
+
+ # 7. 构建返回数据列表
+ order_list = []
+ for order in current_page_orders:
+ order_data = {
+ 'dingdan_id': order.OrderID,
+ 'zhuangtai': order.Status,
+ 'jine': float(order.PlayerCommission) if order.PlayerCommission else 0.0,
+ 'jieshao': order.Description or '',
+ 'tupian': order.ImageURL or '',
+ 'nicheng': order.Nickname or '',
+ **datetime_aliases(order.CreateTime),
+ }
+ order_list.append(order_data)
+
+ # 8. 判断是否还有更多数据
+ has_more = (current_page_count == page_size) and (end_index < total_count)
+ if total_count == 0:
+ has_more = False
+
+ return Response({
+ 'code': 0,
+ 'msg': '成功',
+ 'data': {
+ 'list': order_list,
+ 'has_more': has_more,
+ 'total': total_count
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"打手订单获取失败: {str(e)}", exc_info=True)
+
+ return Response({
+ 'code': 4,
+ 'msg': '服务器内部错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+class DashouCOSZhengshuView(APIView):
+ """打手获取COS上传凭证接口(调试版本)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 接收参数(兼容 OrderID / dingdan_id)
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+ yongtu = request.data.get('yongtu', 'dingdan')
+
+ # 2. 条件判断修正:除了 fakuan 之外,其他用途均需提供 dingdan_id
+ if yongtu != 'fakuan' and not dingdan_id:
+ return Response(
+ {'code': 400, 'msg': '订单ID不能为空', 'error': 'dingdan_id is required when yongtu is not fakuan'},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 3. 业务验证逻辑(根据用途分别验证)
+ if yongtu == 'chufa':
+ try:
+ chufa_record = PenaltyRecord.query.filter(
+ OrderID=dingdan_id,
+ PlayerID=request.user.UserUID,
+ ApplyStatus__in=[0, 3]
+ ).first()
+ if not chufa_record:
+ return Response(
+ {'code': 403, 'msg': '处罚记录无权限或状态不对',
+ 'error': f'No punishment record found for user {request.user.UserUID} on order {dingdan_id}'},
+ status=status.HTTP_403_FORBIDDEN
+ )
+ except Exception as e:
+ logger.error(f"查处罚记录异常: {str(e)}")
+ return Response(
+ {'code': 500, 'msg': '服务器内部错误', 'error': str(e)},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ elif yongtu == 'fakuan':
+ # 罚款申诉不需要订单验证
+ pass
+ elif yongtu == 'fakuan_evidence':
+ # 商家上传罚单证据图(Purpose=1)
+ fadan_id = request.data.get('fadan_id')
+ if not dingdan_id or not fadan_id:
+ return Response(
+ {'code': 400, 'msg': '缺少订单ID或罚单ID'},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+ try:
+ shangjia_profile = request.user.ShopProfile
+ if shangjia_profile.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '商家状态异常'}, status=403)
+ except Exception:
+ return Response({'code': 403, 'msg': '非商家无权上传'}, status=403)
+ fadan = Penalty.query.filter(
+ id=fadan_id,
+ RelatedOrderID=dingdan_id,
+ ApplicantID=request.user.UserUID,
+ ).first()
+ if not fadan:
+ return Response({'code': 403, 'msg': '罚单不存在或无权操作'}, status=403)
+ elif yongtu == 'fakuan_evidence_draft':
+ # 商家申请罚款前上传证据图(仅需订单归属校验)
+ if not dingdan_id:
+ return Response({'code': 400, 'msg': '缺少订单ID'}, status=400)
+ try:
+ shangjia_profile = request.user.ShopProfile
+ if shangjia_profile.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '商家状态异常'}, status=403)
+ except Exception:
+ return Response({'code': 403, 'msg': '非商家无权上传'}, status=403)
+ try:
+ MerchantOrderExt.query.get(Order__OrderID=dingdan_id, MerchantID=request.user.UserUID)
+ except MerchantOrderExt.DoesNotExist:
+ return Response({'code': 403, 'msg': '订单不存在或无权操作'}, status=403)
+ else:
+ # 默认为订单相关,需要验证订单且打手必须接单
+ try:
+ dingdan = Order.query.get(
+ OrderID=dingdan_id,
+ PlayerID=request.user.UserUID,
+ Status__in=[2, 8]
+ )
+ except Order.DoesNotExist:
+ return Response(
+ {'code': 403, 'msg': '此功能暂未开放', 'error': f'Order {dingdan_id} not found or not assigned to user {request.user.UserUID}'},
+ status=status.HTTP_403_FORBIDDEN
+ )
+
+ # 4. 获取腾讯云STS临时密钥
+ try:
+ # 初始化固定凭证
+ cred = credential.Credential(
+ settings.COS_SECRET_ID,
+ settings.COS_SECRET_KEY
+ )
+
+ # 创建STS客户端
+ http_profile = HttpProfile()
+ http_profile.endpoint = "sts.tencentcloudapi.com"
+ client_profile = ClientProfile()
+ client_profile.httpProfile = http_profile
+ client = sts_client.StsClient(cred, "ap-shanghai", client_profile)
+
+ # 构建策略资源
+ resources = []
+ if yongtu == 'chufa':
+ resources.append(
+ f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/dssstp/*"
+ )
+ elif yongtu == 'fakuan':
+ resources.append(
+ f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/dashoufakuan/dashoufakuanshensu/*"
+ )
+ elif yongtu == 'fakuan_evidence':
+ resources.append(
+ f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/shangjiafakuan/zhengju/*"
+ )
+ elif yongtu == 'fakuan_evidence_draft':
+ resources.append(
+ f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/shangjiafakuan/zhengju/*"
+ )
+ else:
+ resources.append(
+ f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*"
+ )
+
+ policy = {
+ "version": "2.0",
+ "statement": [{
+ "effect": "allow",
+ "action": [
+ "cos:PutObject",
+ "cos:PostObject",
+ "cos:InitiateMultipartUpload",
+ "cos:UploadPart",
+ "cos:CompleteMultipartUpload"
+ ],
+ "resource": resources
+ }]
+ }
+
+ req = models.GetFederationTokenRequest()
+ req.Name = f"dashou_{dingdan_id}_{int(time.time())}"
+ req.Policy = json.dumps(policy)
+ req.DurationSeconds = 7200
+
+ resp = client.GetFederationToken(req)
+ credentials = resp.Credentials
+
+ except TencentCloudSDKException as e:
+ logger.error(f"获取STS临时密钥失败: {str(e)}")
+ return Response({
+ 'code': 500,
+ 'msg': f'获取上传凭证失败: {str(e)}',
+ 'error_detail': str(e)
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 5. 构建响应
+ current_time = int(time.time())
+ upload_dir = (
+ 'fakuan/shangjiafakuan/zhengju/' if yongtu == 'fakuan_evidence' else
+ 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else (
+ 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/'
+ )
+ )
+ response_data = {
+ 'credentials': {
+ 'tmpSecretId': credentials.TmpSecretId,
+ 'tmpSecretKey': credentials.TmpSecretKey,
+ 'sessionToken': credentials.Token,
+ },
+ 'startTime': current_time,
+ 'expiredTime': current_time + 7200,
+ 'bucket': settings.COS_BUCKET,
+ 'region': settings.COS_REGION,
+ 'upload_dir': upload_dir,
+ }
+ return Response({'code': 0, 'msg': '成功', 'data': response_data})
+
+ except Exception as e:
+ # 捕捉所有未预期的异常,返回完整 traceback
+ logger.error(f'COS凭证接口未知错误: {traceback.format_exc()}')
+ return Response({
+ 'code': 500,
+ 'msg': f'服务器内部错误: {str(e)}',
+ 'error_detail': traceback.format_exc()
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class ShangjiaCOSZhengshuView(APIView):
+ """商家获取COS上传凭证接口(用于处罚申诉图片上传)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 接收参数
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+ yongtu = request.data.get('yongtu', 'chufa') # 用途:chufa=处罚申诉
+
+ if not dingdan_id:
+ return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 🔴【修改】验证商家身份 - 通过反向查询shop_profile判断是否为商家
+ try:
+ # 检查是否有商家扩展表
+ shangjia_profile = request.user.ShopProfile
+
+ # 验证商家状态是否为1(正常)
+ if shangjia_profile.zhuangtai != 1:
+ return Response({'code': 3, 'msg': '商家账号状态异常,无法操作', 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ # 🔴【重要】获取商家ID(从商家扩展表中获取)
+ shangjia_id = request.user.UserUID # 假设商家扩展表中有yonghuid字段
+
+ except AttributeError:
+ # 没有shop_profile属性,说明不是商家
+ return Response({'code': 3, 'msg': '非商家用户无权限', 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 🔴【修改】验证订单权限 - 通过商家订单扩展表验证
+ try:
+ # 获取订单
+ dingdan = Order.query.get(OrderID=dingdan_id)
+
+ # 🔴【重要】通过反向查询商家扩展表验证订单是否属于当前商家
+ try:
+ shangjia_kuozhan = dingdan.shangjia_kuozhan # 使用反向关系名
+
+ # 验证商家ID是否匹配
+ if str(shangjia_kuozhan.MerchantID) != str(shangjia_id):
+ return Response({'code': 2, 'msg': '订单不属于当前商家', 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ # 验证订单状态是否允许申请处罚(根据前端逻辑)
+ allowed_zhuangtai = [2, 4, 5, 6, 8]
+ if dingdan.Status not in allowed_zhuangtai:
+ return Response({'code': 4, 'msg': '当前订单状态不允许申请处罚', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 🔴【新增】检查是否已存在处罚申请(通过Chufajilu模型)
+ # 注意:这里假设Chufajilu模型存在,并且有shangjia_id字段
+ try:
+ existing_chufa = PenaltyRecord.query.filter(
+ OrderID=dingdan_id,
+ ApplicantID=shangjia_id,
+ ApplyStatus__in=[0, 1] # 审核中或已处罚状态
+ ).exists()
+
+ if existing_chufa:
+ return Response({'code': 5, 'msg': '该订单已有处罚申请,请勿重复上传', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+ except ImportError:
+ # 如果没有Chufajilu模型,跳过这个检查
+ pass
+
+ except MerchantOrderExt.DoesNotExist:
+ # 没有商家扩展记录,说明这不是商家订单
+ return Response({'code': 2, 'msg': '非商家订单无法操作', 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ except Order.DoesNotExist:
+ return Response({'code': 2, 'msg': '订单不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"订单验证异常: {str(e)}")
+ return Response({'code': 500, 'msg': '服务器验证异常', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 4. 获取腾讯云STS临时密钥
+ try:
+ # 初始化固定凭证
+ cred = credential.Credential(
+ settings.COS_SECRET_ID,
+ settings.COS_SECRET_KEY
+ )
+
+ # 创建STS客户端
+ http_profile = HttpProfile()
+ http_profile.endpoint = "sts.tencentcloudapi.com"
+ client_profile = ClientProfile()
+ client_profile.httpProfile = http_profile
+
+ client = sts_client.StsClient(cred, "ap-shanghai", client_profile)
+
+ # 🔴【修改】构建策略 - 只允许上传到商家处罚证据图片目录
+ resources = [
+ f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/sjzjtp/*"
+ ]
+
+ policy = {
+ "version": "2.0",
+ "statement": [
+ {
+ "effect": "allow",
+ "action": [
+ "cos:PutObject",
+ "cos:PostObject",
+ "cos:InitiateMultipartUpload",
+ "cos:UploadPart",
+ "cos:CompleteMultipartUpload"
+ ],
+ "resource": resources
+ }
+ ]
+ }
+
+ # 请求STS临时凭证
+ req = models.GetFederationTokenRequest()
+ # 🔴【修改】命名包含商家ID
+ req.Name = f"shangjia_{shangjia_id}_{dingdan_id}_{int(time.time())}"
+ req.Policy = json.dumps(policy)
+ req.DurationSeconds = 7200 # 2小时有效期
+
+ resp = client.GetFederationToken(req)
+
+ # 提取临时密钥
+ credentials = resp.Credentials
+
+ except TencentCloudSDKException as e:
+ logger.error(f"商家获取STS临时密钥失败: {str(e)}")
+ return Response({
+ 'code': 500,
+ 'msg': f'获取上传凭证失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 5. 构建响应数据
+ current_time = int(time.time())
+ response_data = {
+ 'credentials': {
+ 'tmpSecretId': credentials.TmpSecretId,
+ 'tmpSecretKey': credentials.TmpSecretKey,
+ 'sessionToken': credentials.Token,
+ },
+ 'startTime': current_time,
+ 'expiredTime': current_time + 7200,
+ 'bucket': settings.COS_BUCKET,
+ 'region': settings.COS_REGION,
+ # 🔴【修改】商家专用上传目录
+ 'upload_dir': 'a_long/chfajltp/sjzjtp/',
+ # 返回商家ID和订单ID,便于前端生成文件名
+ 'dingdan_id': dingdan_id,
+ 'shangjia_id': shangjia_id,
+ # 返回商家昵称,便于前端显示
+ 'shangjia_nicheng': shangjia_profile.nicheng if hasattr(shangjia_profile, 'nicheng') else ''
+ }
+
+ # 6. 返回成功
+ return Response({
+ 'code': 0,
+ 'msg': '成功',
+ 'data': response_data
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f'商家COS凭证接口未知错误: {str(e)}', exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': f'服务器内部错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class DashouTijiaoView(APIView):
+ """打手提交订单完成接口"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 获取参数
+ data = request.data
+ dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
+ liuyan = data.get('liuyan', '')
+ jiaofu_tupian_urls = data.get('jiaofu_tupian_urls', [])
+
+ # 2. 参数验证
+ if not dingdan_id:
+ return Response({
+ 'code': 1,
+ 'msg': '订单ID不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not isinstance(jiaofu_tupian_urls, list):
+ return Response({
+ 'code': 2,
+ 'msg': '图片URL列表格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 限制图片数量(最多9张,最少1张)
+ if len(jiaofu_tupian_urls) == 0:
+ return Response({
+ 'code': 3,
+ 'msg': '至少需要上传一张图片',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if len(jiaofu_tupian_urls) > 9:
+ return Response({
+ 'code': 4,
+ 'msg': '最多只能上传9张图片',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 留言长度限制(50字)
+ if liuyan and len(liuyan) > 50:
+ return Response({
+ 'code': 5,
+ 'msg': '留言最多50个字',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 获取当前用户并验证打手身份
+ current_user = request.user
+ yonghuid = current_user.UserUID
+
+ try:
+ dashou_profile = current_user.DashouProfile
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 6,
+ 'msg': '用户不是打手身份',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 4. 验证订单状态和权限
+ try:
+ dingdan = Order.query.get(
+ OrderID=dingdan_id,
+ PlayerID=yonghuid,
+ Status=2 # 必须是进行中的订单
+ )
+ except Order.DoesNotExist:
+ return Response({
+ 'code': 7,
+ 'msg': '订单不存在或没有提交权限',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 5. 存储图片到Dashoutupian表
+ for tupian_url in jiaofu_tupian_urls:
+ # 创建打手提交图片记录
+ PlayerDeliveryImage.query.create(
+ OrderID=dingdan_id,
+ PlayerID=yonghuid,
+ ImageURL=tupian_url,
+ CreateTime=timezone.now()
+ )
+
+ # 6. 更新订单主表
+ dingdan.PlayerRemark = liuyan
+ dingdan.Status = 8 # 状态改为结算中
+ dingdan.UpdateTime = timezone.now()
+ dingdan.save()
+
+ # 7. 更新打手扩展表状态
+ dashou_profile.zhuangtai = 1 # 改为数字1(根据你之前说的)
+ dashou_profile.save()
+
+ # ========== 新增:记录打手提交时间 ==========
+ record_dashou_biaoxian(
+ dingdan_id=dingdan_id,
+ dashou_id=yonghuid,
+ xingwei=2, # 提交
+ tijiao_time=timezone.now()
+ )
+ # ===========================================
+
+ # 8. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '订单提交成功',
+ 'data': {
+ 'dingdan_id': dingdan_id,
+ 'new_zhuangtai': 8, # 新的订单状态
+ 'dashou_zhuangtai': 1 # 打手新的状态
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"打手提交订单失败: {str(e)}", exc_info=True)
+
+ return Response({
+ 'code': 8,
+ 'msg': '服务器内部错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class DashouDingdanXiangqingView(APIView):
+ """打手订单详情获取接口"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 获取订单ID
+ dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
+ if not dingdan_id:
+ return Response({
+ 'code': 1,
+ 'msg': '订单ID不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 获取当前用户并验证权限
+ current_user = request.user
+ yonghuid = current_user.UserUID
+
+ try:
+ dashou_profile = current_user.DashouProfile
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 2,
+ 'msg': '用户不是打手身份',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 查询订单主表
+ try:
+ dingdan = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({
+ 'code': 3,
+ 'msg': '订单不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 验证订单是否属于当前打手
+ if dingdan.PlayerID != yonghuid:
+ return Response({
+ 'code': 4,
+ 'msg': '没有权限查看此订单',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 5. 构建基础订单数据
+ dingdan_data = {
+ 'dingdan_id': dingdan.OrderID,
+ 'zhuangtai': dingdan.Status,
+ 'jine': float(dingdan.PlayerCommission) if dingdan.PlayerCommission else 0.0,
+ 'jieshao': dingdan.Description or '',
+ 'tupian': dingdan.ImageURL or '',
+ 'nicheng': dingdan.Nickname or '',
+ 'beizhu': dingdan.Remark or '',
+ **datetime_aliases(dingdan.CreateTime),
+ 'tuikuan_liyou':dingdan.RefundReason or '',
+ 'dashou_liuyan': dingdan.PlayerRemark or '',
+ 'fuwudashou_id': dingdan.PlayerID or '',
+ 'fadanpingtai': dingdan.Platform or ''
+ }
+
+ # 6. 查询打手提交的图片
+ dashou_tupian_list = PlayerDeliveryImage.query.filter(
+ OrderID=dingdan_id,
+ PlayerID=yonghuid
+ ).order_by('CreateTime')
+
+ jiaofu_tupian_urls = []
+ for tupian_record in dashou_tupian_list:
+ if tupian_record.ImageURL:
+ jiaofu_tupian_urls.append(tupian_record.ImageURL)
+
+ dingdan_data['dashou_jiaofu'] = jiaofu_tupian_urls
+
+ # 7. 查询商家扩展表信息
+ shangjia_info = {}
+ try:
+ shangjia_kuozhan = dingdan.shangjia_kuozhan
+ shangjia_info = {
+ 'shangjia_id': shangjia_kuozhan.MerchantID or '',
+ 'shangjia_nicheng': shangjia_kuozhan.MerchantNickname or '',
+ 'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '',
+ #'tuikuan_liyou': '',
+ 'chufa_liyou': shangjia_kuozhan.PenaltyReason or '',
+ 'chufa_zhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0,
+ #'chufa_jieguo': shangjia_kuozhan.chufa_jieguo or '',
+ 'bohui_liyou': shangjia_kuozhan.RejectReason or ''
+ }
+ except MerchantOrderExt.DoesNotExist:
+ # 没有商家扩展信息,返回空值
+ shangjia_info = {
+ 'shangjia_id': '',
+ 'shangjia_nicheng': '',
+ 'shangjia_liuyan': '',
+ 'tuikuan_liyou': '',
+ 'chufa_liyou': '',
+ 'chufa_zhuangtai': 0,
+ 'chufa_jieguo': '',
+ 'bohui_liyou': ''
+ }
+
+ # 8. 查询平台扩展表信息(如果有的话)
+ pingtai_info = {}
+ try:
+ pingtai_kuozhan = dingdan.pingtai_kuozhan
+ pingtai_info = {
+ 'laoban_id': pingtai_kuozhan.BossID or '',
+ 'laoban_pingjia': pingtai_kuozhan.BossReview or ''
+ }
+ except PlatformOrderExt.DoesNotExist:
+ pingtai_info = {
+ 'laoban_id': '',
+ 'laoban_pingjia': ''
+ }
+
+ # 9. 合并所有数据
+ response_data = {
+ **dingdan_data,
+ **shangjia_info,
+ **pingtai_info
+ }
+
+ return Response({
+ 'code': 0,
+ 'msg': '成功',
+ 'data': response_data
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"获取打手订单详情失败: {str(e)}", exc_info=True)
+
+ return Response({
+ 'code': 5,
+ 'msg': '服务器内部错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+
+class DashouHuoquLeixingView(APIView):
+ """
+ 打手获取商品类型接口 - 抢单页面使用
+ 接口地址:POST /dingdan/dsqdhqddlx
+ 功能:返回所有正常的商品类型,已按排序权重排序
+
+ 返回格式(与前端原数据格式完全一致):
+ {
+ "code": 200,
+ "msg": "成功",
+ "data": {
+ "list": [
+ {
+ "id": 1,
+ "jieshao": "王者荣耀",
+ "tupian_url": "leixing/wzry.jpg",
+ "yaoqiuleixing": null,
+ "huiyuan_id": null,
+ "yongjin": null,
+ "paixu": 100,
+ "shenhezhuangtai": 1
+ },
+ // ... 更多类型
+ ]
+ }
+ }
+ """
+
+ # 🔴 使用JWT Token认证
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ """
+ 处理POST请求,返回商品类型列表
+
+ 请求参数:无特殊参数,只验证用户身份
+
+ 安全验证:
+ 1. JWT Token验证用户身份
+ 2. 只返回 shenhezhuangtai=1(正常状态)的类型
+ 3. 数据已排序:paixu降序 → id升序
+ """
+ try:
+ logger.info(f"打手获取商品类型请求 - 用户ID: {request.user.UserUID}")
+
+ 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, request.user)
+ leixing_list = build_club_leixing_list_for_api(club_id)
+
+ logger.info(f"返回商品类型数据:{len(leixing_list)}条,俱乐部:{club_id} 用户: {request.user.UserUID}")
+
+ # 🔴 返回成功响应
+ # 注意:保持与前端原数据格式完全一致
+ return Response({
+ "code": 200,
+ "msg": "成功",
+ "data": {
+ "list": leixing_list # 🔴 保持原字段名 "list"
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ # 🔴 异常处理
+ logger.error(f"打手获取商品类型失败 - 用户ID: {request.user.UserUID}, 错误: {str(e)}", exc_info=True)
+
+ return Response({
+ "code": 500,
+ "msg": f"服务器内部错误: {str(e)}",
+ "data": None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+
+
+
+
+class DashouXiugaiView(APIView):
+ """
+ 打手修改订单交付内容接口
+ 请求方式:POST
+ 请求路径:/dingdan/dsxiugaidd
+ 权限:需携带有效JWT Token,且用户为打手身份
+ 请求参数:
+ dingdan_id : 订单ID(必填)
+ liuyan : 新的留言内容(可选,最长50字)
+ deleted_images : 要删除的图片URL列表(数组,每个元素为完整COS URL)
+ new_images : 新增的图片URL列表(数组,每个元素为完整COS URL)
+ 返回:
+ 成功:{"code":0,"msg":"修改成功","data":null}
+ 失败:返回相应错误码和提示
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 获取请求数据
+ data = request.data
+ dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
+ liuyan = data.get('liuyan', '')
+ deleted_images = data.get('deleted_images', [])
+ new_images = data.get('new_images', [])
+
+ # ---------- 参数校验 ----------
+ if not dingdan_id:
+ return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if not isinstance(deleted_images, list):
+ return Response({'code': 2, 'msg': 'deleted_images 必须为数组', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if not isinstance(new_images, list):
+ return Response({'code': 3, 'msg': 'new_images 必须为数组', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if liuyan and len(liuyan) > 50:
+ return Response({'code': 4, 'msg': '留言最多50个字', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # ---------- 用户身份验证 ----------
+ current_user = request.user
+ yonghuid = current_user.UserUID
+
+ # 检查是否为打手
+ try:
+ dashou_profile = current_user.DashouProfile # User 模型反向 related_name='DashouProfile'
+ except UserDashou.DoesNotExist:
+ return Response({'code': 5, 'msg': '用户不是打手身份', 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ # ---------- 订单查询及权限验证 ----------
+ try:
+ # 订单必须属于该打手,且状态为4(退款中)或8(结算中)
+ dingdan = Order.query.get(
+ OrderID=dingdan_id,
+ PlayerID=yonghuid,
+ Status__in=[4, 8] # 只允许修改退款中或结算中的订单
+ )
+ except Order.DoesNotExist:
+ return Response({'code': 6, 'msg': '订单不存在或没有修改权限', 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ # ---------- 执行修改(使用事务保证原子性) ----------
+ try:
+ with transaction.atomic():
+ # 1. 处理删除的图片:从 Dashoutupian 表中删除记录
+ if deleted_images:
+ # 根据订单ID、打手ID和图片URL删除记录
+ delete_count, _ = PlayerDeliveryImage.query.filter(
+ OrderID=dingdan_id,
+ PlayerID=yonghuid,
+ ImageURL__in=deleted_images
+ ).delete()
+ # 可选:记录删除数量
+ logger.info(f"订单 {dingdan_id} 删除图片 {delete_count} 张")
+
+ # 2. 处理新增的图片:插入新记录
+ if new_images:
+ # 批量创建
+ new_records = []
+ for url in new_images:
+ new_records.append(PlayerDeliveryImage(
+ OrderID=dingdan_id,
+ PlayerID=yonghuid,
+ ImageURL=url,
+ CreateTime=timezone.now()
+ ))
+ PlayerDeliveryImage.objects.bulk_create(new_records)
+ logger.info(f"订单 {dingdan_id} 新增图片 {len(new_records)} 张")
+
+ # 3. 更新订单留言(如果发生变化)
+ # 注意:即使留言为空字符串,也允许更新,表示清空留言
+ if 'liuyan' in data: # 判断是否传了liuyan字段(可能为空字符串)
+ dingdan.PlayerRemark = liuyan
+ dingdan.UpdateTime = timezone.now()
+ dingdan.save(update_fields=['PlayerRemark', 'UpdateTime'])
+
+ # 全部成功,返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '修改成功',
+ 'data': None
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"修改订单失败: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 7,
+ 'msg': '服务器内部错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
diff --git a/orders/views/merchant.py b/orders/views/merchant.py
new file mode 100644
index 0000000..17df04d
--- /dev/null
+++ b/orders/views/merchant.py
@@ -0,0 +1,1408 @@
+"""orders.views.merchant - 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 ShangpinLeixingView(APIView):
+ """
+ 获取商家可用的商品类型列表(含关联板块的称号)
+ POST /dingdan/sjspleixing
+ """
+ permission_classes = [IsAuthenticated]
+
+ @staticmethod
+ def _can_access(request):
+ """商家老板、有效子客服(含代理调用)方可获取"""
+ if getattr(request, '_staff_member', None) is not None:
+ return True
+ from merchant_ops.services.authz import (
+ get_active_staff_member, is_merchant_owner, member_has_perm, staff_order_read_perm,
+ )
+ if is_merchant_owner(request.user):
+ return True
+ member = get_active_staff_member(request.user)
+ return bool(member and staff_order_read_perm(member))
+
+ def post(self, request):
+
+ if not self._can_access(request):
+ return Response({'code': 403, 'msg': '非商家或有效客服,无权获取订单类型', 'data': []})
+
+ try:
+ # 1. 查询审核通过且抢单要求类型为1的商品类型,按排序权重升序
+ shangpin_queryset = ShangpinLeixing.query.filter(
+ yaoqiuleixing=1,
+ shenhezhuangtai=1
+ ).select_related('bankuai').order_by('paixu')
+
+ # 2. 收集所有涉及到的板块ID
+ bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct()
+
+ # 3. 查询这些板块下的所有称号(每个称号绑定到一个板块)
+ chenghao_list = Chenghao.query.filter(
+ bankuai_id__in=bankuai_ids
+ ).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id')
+
+ # 4. 构建 板块ID -> 称号列表 的映射(确保准确关联)
+ bankuai_chenghao_map = {}
+ for ch in chenghao_list:
+ bid = ch['bankuai_id']
+ if bid not in bankuai_chenghao_map:
+ bankuai_chenghao_map[bid] = []
+ bankuai_chenghao_map[bid].append({
+ 'id': ch['id'],
+ 'mingcheng': ch['mingcheng'],
+ 'texiao_json': ch['texiao_miaoshu'] # 前端 chenghao-tag 组件需要
+ })
+
+ # 5. 组装最终数据
+ data_list = []
+ for sp in shangpin_queryset:
+ item = {
+ 'id': sp.id,
+ 'yaoqiuleixing': sp.yaoqiuleixing,
+ 'huiyuan_id': sp.huiyuan_id,
+ 'jieshao': sp.jieshao,
+ # 当前类型所属板块的称号列表(无板块则为空数组)
+ 'chenghaoList': bankuai_chenghao_map.get(sp.bankuai_id, [])
+ }
+ data_list.append(item)
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': data_list
+ })
+
+ except Exception as e:
+ logger.error(f"获取商品类型失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
+
+
+
+class ShangjiaPaifaView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ logger.info(f"商家派单请求: user_id={request.user.UserUID}, data={request.data}")
+
+ try:
+ data = request.data
+
+ # ----- 1. 获取原始参数(保持原类型)-----
+ shangpin_type_id = data.get('shangpinTypeId')
+ huiyuan_id = data.get('huiyuanId')
+ yaoqiuleixing = data.get('GrabRequirement')
+ dingdan_jieshao_raw = data.get('dingdanJieshao', '')
+ dingdan_beizhu_raw = data.get('dingdanBeizhu', '')
+ laoban_name_raw = data.get('laobanName', '')
+ zhiding_uid_raw = data.get('zhidingUid', '')
+ jiage_str = data.get('jiage')
+
+ label_id = data.get('labelId') # 整数或 None
+ commission_enabled = data.get('commissionEnabled', False)
+ commission_value_raw = data.get('commissionValue')
+ waibu_dingdan_id_raw = data.get('waibuDingdanId', '')
+
+ # ----- 2. 字符串字段安全处理 -----
+ dingdan_jieshao = str(dingdan_jieshao_raw).strip()
+ dingdan_beizhu = str(dingdan_beizhu_raw).strip()
+ laoban_name = str(laoban_name_raw).strip()
+ zhiding_uid = str(zhiding_uid_raw).strip()
+ commission_value_str = str(commission_value_raw).strip() if commission_value_raw is not None else ''
+ waibu_dingdan_id = str(waibu_dingdan_id_raw).strip()[:64] if waibu_dingdan_id_raw else ''
+
+ # ----- 3. 必填校验 -----
+ if not all([shangpin_type_id, dingdan_jieshao, laoban_name, jiage_str]):
+ logger.warning(f"参数缺失: {request.data}")
+ return Response({'code': 400, 'msg': '缺少必填参数'})
+
+ # ----- 4. 价格验证 -----
+ try:
+ jiage = float(jiage_str)
+ if jiage < 1 or jiage > 10000:
+ return Response({'code': 400, 'msg': '价格需在1-10000元之间'})
+ if '.' in str(jiage_str) and len(str(jiage_str).split('.')[1]) > 2:
+ return Response({'code': 400, 'msg': '最多支持两位小数'})
+ except ValueError:
+ return Response({'code': 400, 'msg': '价格格式错误'})
+
+ # ----- 5. 商品类型 -----
+ try:
+ shangpin_type = ShangpinLeixing.query.get(id=shangpin_type_id)
+ except ShangpinLeixing.DoesNotExist:
+ logger.error(f"商品类型不存在: {shangpin_type_id}")
+ return Response({'code': 400, 'msg': '商品类型不存在'})
+
+ # ----- 6. 押金 / 会员模式决定 -----
+ final_yongjin = None
+ if commission_enabled and commission_value_str:
+ try:
+ cv = float(commission_value_str)
+ if cv > 0:
+ final_yaoqiu_type = 2
+ final_yongjin = Decimal(str(cv))
+ final_huiyuan_id = ''
+ logger.info(f"押金模式: 佣金={cv}")
+ else:
+ final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
+ final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
+ logger.warning("佣金为0,降级为会员模式")
+ except ValueError:
+ final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
+ final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
+ logger.warning("佣金格式错误,降级为会员模式")
+ else:
+ final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
+ final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
+ logger.info(f"会员模式: 类型={final_yaoqiu_type}")
+
+ # ----- 7. 称号(可选:不传/空值/无效/不存在均视为无标签)-----
+ chenghao_obj = None
+ label_id_str = str(label_id).strip() if label_id is not None else ''
+ if label_id_str and label_id_str not in ('0', 'null', 'undefined'):
+ try:
+ lid = int(label_id_str)
+ if lid > 0:
+ chenghao_obj = Chenghao.query.filter(id=lid).first()
+ if chenghao_obj:
+ logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}")
+ else:
+ logger.info(f"标签不存在,按无标签派单: {label_id}")
+ except (ValueError, TypeError):
+ logger.info(f"标签ID无效,按无标签派单: {label_id}")
+
+ # ----- 8. 商家信息 -----
+ try:
+ shangjia = request.user.ShopProfile
+ except UserShangjia.DoesNotExist:
+ logger.warning(f"用户不是商家: {request.user.UserUID}")
+ return Response({'code': 403, 'msg': '您不是商家'})
+
+ if shangjia.zhuangtai != 1:
+ logger.warning(f"商家状态异常: {shangjia.zhuangtai}")
+ return Response({'code': 403, 'msg': '商家状态异常'})
+
+ if jiage > shangjia.yue:
+ logger.warning(f"余额不足: {shangjia.yue} < {jiage}")
+ return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元'})
+
+ # ----- 9. 指定打手 -----
+ zhiding_dashou = None
+ if zhiding_uid:
+ try:
+ zhiding_user = User.query.get(UserUID=zhiding_uid)
+ zhiding_dashou = zhiding_user.DashouProfile
+ if zhiding_dashou.zhanghaozhuangtai != 1:
+ return Response({'code': 400, 'msg': '指定打手状态异常'})
+ except User.DoesNotExist:
+ return Response({'code': 400, 'msg': '指定用户不存在'})
+ except UserDashou.DoesNotExist:
+ return Response({'code': 400, 'msg': '指定用户不是打手'})
+
+ # ----- 10. 利率(打手+管事分成,打手优先) -----
+ club_id = resolve_club_id_from_request(request)
+ dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage, club_id=club_id)
+
+ # ----- 11. 生成订单ID -----
+ def generate_dingdan_id():
+ ts = int(time.time() * 1000)
+ pid = os.getpid() % 1000
+ rnd = random.randint(1000, 9999)
+ return f"SJ{ts}{pid:03d}{rnd}"
+
+ # ----- 12. 事务:创建订单、更新商家统计 -----
+ dingdan = None
+ try:
+ with transaction.atomic():
+ dingdan = Order.query.create(
+ OrderID=generate_dingdan_id(),
+ Status=7 if zhiding_dashou else 1,
+ Platform=2,
+ Amount=jiage,
+ PlayerCommission=dashou_fencheng,
+ ManagerCommission=guanshi_fencheng,
+ AssignedID=zhiding_uid if zhiding_dashou else '',
+ ProductID=0,
+ ProductTypeID=int(shangpin_type_id),
+ GrabRequirement=final_yaoqiu_type,
+ CommissionReq=final_yongjin,
+ MembershipID=final_huiyuan_id,
+ Description=dingdan_jieshao[:2000],
+ Remark=dingdan_beizhu[:1000],
+ Nickname=laoban_name[:50],
+ ImageURL=request.user.Avatar,
+ User1ID=f"Sj{request.user.UserUID}",
+ ClubID=club_id,
+ ExternalOrderID=waibu_dingdan_id or None,
+ )
+ logger.info(f"订单主表创建: {dingdan.OrderID}")
+
+ MerchantOrderExt.query.create(
+ Order=dingdan,
+ MerchantID=request.user.UserUID,
+ MerchantNickname=shangjia.nicheng,
+ )
+
+ if chenghao_obj:
+ DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=chenghao_obj)
+ logger.info(f"订单关联称号: {chenghao_obj.mingcheng}")
+
+ # 更新商家统计
+ UserShangjia.query.filter(id=shangjia.id).update(
+ fabu=F('fabu') + 1,
+ yue=F('yue') - jiage,
+ jinridingdan=F('jinridingdan') + 1,
+ jinriliushui=F('jinriliushui') + jiage,
+ jinyuedingdan=F('jinyuedingdan') + 1,
+ jinyueliushui=F('jinyueliushui') + jiage,
+ )
+
+ update_shangjia_daily(
+ yonghuid=request.user.UserUID,
+ amount=Decimal(str(jiage)),
+ action=1,
+ order_id=dingdan.OrderID,
+ )
+ # 事务提交,订单已持久化
+ except Exception as e:
+ logger.error(f"订单创建事务失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '订单创建失败,请稍后重试'})
+
+ # ========== 以下操作均在事务外,避免异步任务查不到订单 ==========
+ # Celery 广播任务(异步)
+ try:
+ order_info = {
+ 'OrderID': dingdan.OrderID,
+ 'game_type': self._get_game_type_name(dingdan.ProductTypeID),
+ 'amount': str(dingdan.Amount),
+ 'order_desc': (dingdan.Description or '')[:50],
+ }
+ dingdan_guangbo.delay(order_info)
+ logger.info(f"已提交广播任务: {dingdan.OrderID}")
+ except Exception as e:
+ logger.error(f"广播任务提交失败: {e}", exc_info=True)
+
+ logger.info(f"商家派单成功: {dingdan.OrderID}, 金额={jiage}")
+ return Response({'code': 200, 'msg': '派单成功', 'data': {'jine': str(dingdan.Amount)}})
+
+ except Exception as e:
+ logger.error(f"商家派单接口未捕获异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '系统内部错误'})
+
+ def _get_game_type_name(self, leixing_id):
+ try:
+ gt = ShangpinLeixing.query.filter(id=leixing_id).first()
+ return gt.jieshao if gt else '游戏订单'
+ except Exception:
+ return '游戏订单'
+
+# views.py 中 ShangjiaDingdanHuoquView 完整改造
+class ShangjiaDingdanHuoquView(APIView):
+ """
+ 商家订单获取接口
+ POST /dingdan/sjdingdanhq
+ 需要JWT认证
+ 支持分页、状态筛选、商品类型筛选、模糊搜索(订单ID/订单介绍/游戏昵称/打手ID)
+ 返回待结算订单数量(pending_count)
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取当前登录用户的yonghuid
+ try:
+ current_user = request.user
+ yonghuid = current_user.UserUID
+ except AttributeError:
+ return Response({
+ 'code': 1,
+ 'msg': '用户认证失败',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 解析参数
+ try:
+ data = request.data
+ zhuangtai_list = data.get('zhuangtai_list', [])
+ page = max(1, int(data.get('page', 1)))
+ page_size = min(100, max(1, int(data.get('page_size', 10))))
+
+ # 商品类型 / 关键字 / 时间段等见 apply_merchant_order_list_filters
+ except (ValueError, TypeError):
+ return Response({
+ 'code': 2,
+ 'msg': '参数格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 验证状态列表
+ if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0:
+ return Response({
+ 'code': 3,
+ 'msg': '状态列表不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ from orders.services.merchant_order_list_filters import apply_merchant_order_list_filters
+
+ # 4. 构建基础查询(该商家的所有订单)
+ base_qs = MerchantOrderExt.query.filter(
+ MerchantID=yonghuid
+ ).select_related('Order')
+
+ filter_payload = dict(data)
+ filter_payload['_merchant_id_filter'] = yonghuid
+
+ # 5. 状态过滤
+ base_qs = base_qs.filter(Order__Status__in=zhuangtai_list)
+
+ # 6–7. 类型 / 关键字 / 时间段 / 客服筛选
+ base_qs = apply_merchant_order_list_filters(base_qs, filter_payload)
+
+ # 8. 待结算统计(全量聚合,非分页求和)
+ from orders.services.merchant_order_stats import (
+ build_merchant_order_stats, merchant_ext_qs, filter_by_staff_member,
+ )
+ stats_qs = merchant_ext_qs(yonghuid)
+ stats_qs = apply_merchant_order_list_filters(stats_qs, filter_payload)
+ order_stats = build_merchant_order_stats(stats_qs)
+
+ # 9. 排序、分页
+ total_count = base_qs.count()
+ start_index = (page - 1) * page_size
+ end_index = start_index + page_size
+ current_page_orders = list(base_qs.order_by('-Order__CreateTime')[start_index:end_index])
+
+ from jituan.services.club_context import resolve_club_id_from_request
+ from orders.services.merchant_order_list_enrich import enrich_merchant_order_list_items
+
+ club_id = resolve_club_id_from_request(request)
+ order_list = enrich_merchant_order_list_items(current_page_orders, club_id=club_id)
+
+ # 11. 判断是否还有更多
+ current_page_count = len(current_page_orders)
+ has_more = start_index + current_page_count < total_count
+
+ return Response({
+ 'code': 0,
+ 'msg': '成功',
+ 'data': {
+ 'list': order_list,
+ 'has_more': has_more,
+ 'total': total_count,
+ 'pending_count': order_stats['pending_count'],
+ 'pending_amount': order_stats['pending_amount'],
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"商家订单获取失败: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 4,
+ 'msg': '服务器内部错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class ShangjiaShujuTongjiView(APIView):
+ """
+ 商家经营数据统计(订单表聚合)
+ POST /dingdan/sjshujutj
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ current_user = request.user
+ yonghuid = current_user.UserUID
+ except AttributeError:
+ return Response({'code': 1, 'msg': '用户认证失败', 'data': None}, status=401)
+
+ try:
+ shangjia = current_user.ShopProfile
+ if shangjia.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '商家状态异常', 'data': None}, status=403)
+ except Exception:
+ return Response({'code': 403, 'msg': '非商家用户', 'data': None}, status=403)
+
+ data = request.data or {}
+ from orders.services.merchant_order_list_filters import apply_merchant_order_list_filters
+ from orders.services.merchant_order_stats import merchant_ext_qs, build_merchant_order_stats
+
+ filter_payload = dict(data)
+ filter_payload['_merchant_id_filter'] = yonghuid
+ qs = merchant_ext_qs(yonghuid)
+ qs = apply_merchant_order_list_filters(qs, filter_payload)
+ order_stats = build_merchant_order_stats(qs)
+
+ penalty_stats = None
+ try:
+ from orders.models import Penalty, PenaltyRecord
+ from django.db.models import Q, Sum
+ fakuan_pending = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).count()
+ fakuan_pending_amt = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).aggregate(
+ amt=Sum('FineAmount')
+ )['amt']
+ jifen_pending = PenaltyRecord.query.filter(
+ ApplicantID=yonghuid,
+ ).filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)).count()
+ penalty_stats = {
+ 'fakuan_pending': fakuan_pending,
+ 'fakuan_pending_amount': str(fakuan_pending_amt or '0.00'),
+ 'jifen_pending': jifen_pending,
+ }
+ except Exception:
+ penalty_stats = {
+ 'fakuan_pending': 0,
+ 'fakuan_pending_amount': '0.00',
+ 'jifen_pending': 0,
+ }
+
+ return Response({
+ 'code': 0,
+ 'msg': '成功',
+ 'data': {
+ 'order': order_stats,
+ 'penalty': penalty_stats,
+ 'can_view_finance': True,
+ },
+ })
+
+
+
+
+class ShangjiaDingdanXiangqingView(APIView):
+ """
+ 商家订单详情接口
+ 返回订单基础信息、打手信息、交付图片、罚款数据、分红利率、更换打手产生的新订单ID等
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取当前登录用户的 yonghuid
+ try:
+ current_user = request.user
+ yonghuid = current_user.UserUID
+ except AttributeError:
+ return Response({
+ 'code': 1,
+ 'msg': '用户认证失败',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 获取前端参数
+ try:
+ data = request.data
+ dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
+ except Exception:
+ return Response({
+ 'code': 2,
+ 'msg': '参数格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not dingdan_id:
+ return Response({
+ 'code': 3,
+ 'msg': '订单ID不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ # 3. 查询商家订单扩展表,验证权限并获取基础订单数据
+ shangjia_kuozhan = MerchantOrderExt.query.filter(
+ MerchantID=yonghuid,
+ Order__OrderID=dingdan_id
+ ).select_related('Order').first()
+
+ if not shangjia_kuozhan:
+ return Response({
+ 'code': 4,
+ 'msg': '订单不存在或您无权限查看',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ order = shangjia_kuozhan.Order
+
+ # 4. 交付图片列表
+ dashou_tupian_list = PlayerDeliveryImage.query.filter(
+ OrderID=dingdan_id
+ ).order_by('CreateTime').values_list('ImageURL', flat=True)
+ dashou_jiaofu = list(dashou_tupian_list)
+
+ # 5. 打手详细信息(头像、昵称、用户ID)
+ dashou_yonghuid = order.PlayerID or ''
+ dashou_nicheng = ''
+ dashou_avatar = ''
+ if dashou_yonghuid:
+ dashou_user = User.query.filter(
+ UserUID=dashou_yonghuid
+ ).select_related('DashouProfile').first()
+ if dashou_user:
+ dashou_avatar = dashou_user.Avatar or ''
+ if hasattr(dashou_user, 'DashouProfile') and dashou_user.DashouProfile:
+ dashou_nicheng = dashou_user.DashouProfile.nicheng or ''
+
+ # 6. 罚款数据(如果存在)
+ fadan_obj = Penalty.query.filter(
+ RelatedOrderID=dingdan_id,
+ PenalizedUserID=dashou_yonghuid
+ ).first()
+ fadan_dict = None
+ if fadan_obj:
+ ct = fmt_datetime(fadan_obj.CreateTime)
+ ut = fmt_datetime(fadan_obj.UpdateTime)
+ zhengju_urls = list(
+ PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=1).values_list('ImageURL', flat=True)
+ )
+ shensu_urls = list(
+ PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=2).values_list('ImageURL', flat=True)
+ )
+ zhengju_imgs = [
+ {'id': t.id, 'url': t.ImageURL}
+ for t in PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=1)
+ ]
+ shensu_imgs = [
+ {'id': t.id, 'url': t.ImageURL}
+ for t in PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=2)
+ ]
+ fadan_dict = {
+ 'id': fadan_obj.id,
+ 'chufaliyou': fadan_obj.Reason or '',
+ 'fakuanjine': float(fadan_obj.FineAmount) if fadan_obj.FineAmount else 0.0,
+ 'yingxiang_qiangdan': fadan_obj.AffectsGrabbing,
+ 'zhuangtai': fadan_obj.Status,
+ 'shensuliyou': fadan_obj.AppealReason or '',
+ 'tongyi_chufaliyou': fadan_obj.ApproveReason or '',
+ 'bohuiliyou': fadan_obj.RejectReason or '',
+ 'yingde_fenhong': float(fadan_obj.ApplicantBonusAmount or 0),
+ 'fenhong_lilv': float(fadan_obj.ApplicantBonusRate or 0),
+ 'fenhong_yiruzhang': bool(fadan_obj.BonusCredited),
+ 'zhengju_tupian': zhengju_imgs,
+ 'shensu_tupian': shensu_imgs,
+ 'CreateTime': ct,
+ 'create_time': ct,
+ 'UpdateTime': ut,
+ 'update_time': ut,
+ }
+
+ # 7. 商家分红利率:已有罚单用锁定值;否则按商家所在俱乐部预览
+ fenhong_lilv = 0.0
+ try:
+ if fadan_obj and fadan_obj.ApplicantBonusRate:
+ fenhong_lilv = float(fadan_obj.ApplicantBonusRate)
+ else:
+ from users.fadan_fenhong_utils import get_merchant_fenhong_lilv
+ from jituan.services.club_write import resolve_club_id_for_write
+ merchant_club_id = resolve_club_id_for_write(request, current_user)
+ fenhong_lilv = get_merchant_fenhong_lilv(yonghuid, club_id=merchant_club_id)
+ except Exception as e:
+ logger.warning(f'获取分红利率失败: {e}')
+
+ # 8. 更换打手后产生的新订单ID(如果当前订单被更换过)
+ xin_dingdan_id = ''
+ try:
+ # 查找以当前订单作为“上一个订单ID”的新订单扩展记录
+ new_shangjia_order = MerchantOrderExt.query.filter(
+ PrevOrderID=dingdan_id,
+ MerchantID=yonghuid
+ ).select_related('Order').first()
+ if new_shangjia_order:
+ xin_dingdan_id = new_shangjia_order.Order.OrderID
+ except Exception as e:
+ logger.warning(f'查找新订单ID失败: {e}')
+
+ # 9. 构建返回数据
+ response_data = {
+ # 基础订单字段
+ 'dingdan_id': order.OrderID,
+ 'zhuangtai': order.Status or 0,
+ 'jine': float(order.Amount) if order.Amount else 0.0,
+ 'jieshao': order.Description or '',
+ 'beizhu': order.Remark or '',
+ 'nicheng': order.Nickname or '',
+ **datetime_aliases(order.CreateTime),
+
+ # 打手信息
+ 'dashou_yonghuid': dashou_yonghuid,
+ 'dashou_nicheng': dashou_nicheng,
+ 'dashou_avatar': dashou_avatar,
+
+ # 服务详情
+ 'fuwudashou_id': dashou_yonghuid, # 保留旧字段兼容
+ 'dashou_liuyan': order.PlayerRemark or '',
+ 'dashou_jiaofu': dashou_jiaofu,
+
+ # 商家扩展字段(旧)
+ 'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '',
+ 'tkliyou': order.RefundReason or '',
+ 'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 保留,但不影响新罚款
+ 'sqzhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0,
+ 'bhliyou': shangjia_kuozhan.RejectReason or '',
+ 'chufa_jieguo': '', # 已弃用
+
+ # 新增:罚款数据
+ 'fadan': fadan_dict,
+
+ # 新增:分红利率
+ 'fenhong_lilv': fenhong_lilv,
+
+ # 新增:更换打手后的新订单ID
+ 'xin_dingdan_id': xin_dingdan_id,
+
+ # 拼多多订单号(商家/客服可见)
+ 'waibu_dingdan_id': order.ExternalOrderID or '',
+ 'laoban_shouji': getattr(shangjia_kuozhan, 'BossPhone', '') or '',
+ }
+
+ try:
+ from merchant_ops.services.order_enrich import enrich_order_detail_data
+ enrich_order_detail_data(response_data, dingdan_id)
+ except Exception as e:
+ logger.warning(f'派单客服信息附加失败: {e}')
+
+ return Response({
+ 'code': 0,
+ 'msg': '成功',
+ 'data': response_data
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"商家订单详情查询失败: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 5,
+ 'msg': '服务器内部错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+class ShangjiaJiesuanView(APIView):
+ """商家结单接口"""
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取当前登录用户
+ try:
+ current_user = request.user
+ yonghuid = current_user.UserUID
+
+ try:
+ shangjia = current_user.ShopProfile
+ except UserShangjia.DoesNotExist:
+ return Response({
+ 'code': 1,
+ 'msg': '您不是商家或商家信息未完善',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ except AttributeError:
+ return Response({
+ 'code': 1,
+ 'msg': '用户认证失败',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 获取前端参数
+ try:
+ data = request.data
+ dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
+ pingfen = int(data.get('pingfen', 0))
+ liuyan = data.get('liuyan', '').strip()
+ except (ValueError, TypeError):
+ return Response({
+ 'code': 2,
+ 'msg': '参数格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 参数验证
+ if not dingdan_id:
+ return Response({
+ 'code': 3,
+ 'msg': '订单ID不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if pingfen < 0 or pingfen > 5:
+ return Response({
+ 'code': 4,
+ 'msg': '评分必须在0-5分之间',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ with transaction.atomic():
+ # 4. 查询订单并加锁(修复并发问题)
+ order = Order.query.select_for_update().get(OrderID=dingdan_id)
+
+ # 验证订单是否属于当前商家
+ shangjia_kuozhan = MerchantOrderExt.query.filter(
+ Order=order,
+ MerchantID=yonghuid
+ ).first()
+
+ if not shangjia_kuozhan:
+ return Response({
+ 'code': 5,
+ 'msg': '订单不存在或您无权限操作',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 5. 验证订单状态
+ if order.Status != 8:
+ return Response({
+ 'code': 6,
+ 'msg': '订单不在结算状态,无法结单',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取订单字段
+ jine = order.Amount or Decimal('0')
+ dashou_fencheng = order.PlayerCommission or Decimal('0')
+ jiedan_dashou_id = order.PlayerID
+
+ if not jiedan_dashou_id:
+ return Response({
+ 'code': 7,
+ 'msg': '订单没有接单打手,无法结单',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # ========== 本地订单正常结算给打手 ==========
+ # 查询打手信息
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_dashou_id)
+ dashou = dashou_user.DashouProfile
+ except (User.DoesNotExist, UserDashou.DoesNotExist):
+ return Response({
+ 'code': 8,
+ 'msg': '接单打手不存在或信息不完整',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 原子更新打手数据(使用 F 表达式)
+ UserDashou.query.filter(id=dashou.id).update(
+ chengjiaozongliang=F('chengjiaozongliang') + 1,
+ yue=F('yue') + dashou_fencheng,
+ zonge=F('zonge') + dashou_fencheng,
+ jinrishouyi=F('jinrishouyi') + dashou_fencheng,
+ jinyueshouyi=F('jinyueshouyi') + dashou_fencheng,
+ )
+
+ # 更新商家成交单量
+ UserShangjia.query.filter(id=shangjia.id).update(
+ chengjiao=F('chengjiao') + 1,
+ )
+
+ # 更新订单状态
+ order.Status = 3
+ order.save()
+
+ # 处理评分(所有情况都处理)
+ if pingfen > 0:
+ pingfen_obj, created = PlayerRating.query.get_or_create(
+ PlayerID=jiedan_dashou_id,
+ defaults={'TotalScore': pingfen, 'RatingCount': 1}
+ )
+ if not created:
+ PlayerRating.query.filter(id=pingfen_obj.id).update(
+ TotalScore=F('TotalScore') + pingfen,
+ RatingCount=F('RatingCount') + 1,
+ )
+
+ # 更新商家订单扩展表的评价
+ MerchantOrderExt.query.filter(id=shangjia_kuozhan.id).update(
+ MerchantReview=liuyan,
+ )
+
+ # 新增:调用打手每日统计(成交)
+ 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}")
+ # 不影响主流程
+
+ # ✅ 新方法(行为驱动,更安全)
+ update_shangjia_daily(
+ yonghuid=request.user.UserUID,
+ amount=Decimal(str(jine)),
+ action=2, # 2 = 结算
+ order_id=dingdan_id,
+ )
+
+ # 商家订单管事分红(仅 Platform=2)
+ if order.Platform == 2:
+ settle_shangjia_order_guanshi_fenhong(order, jiedan_dashou_id)
+
+ # 返回成功
+ return Response({
+ 'code': 0,
+ 'msg': '结单成功',
+ 'data': {
+ 'dingdan_id': dingdan_id,
+ 'new_zhuangtai': 3,
+ 'jine': float(jine),
+ 'dashou_fencheng': float(dashou_fencheng),
+ 'jiedan_dashou_id': jiedan_dashou_id,
+ 'pingfen': pingfen,
+ 'liuyan': liuyan
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Order.DoesNotExist:
+ return Response({
+ 'code': 5,
+ 'msg': '订单不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"商家结单失败: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 9,
+ 'msg': '服务器内部错误,结单失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+class ShangjiaChexiaoView(APIView):
+ """商家撤销订单申请接口"""
+ permission_classes = [IsAuthenticated] # JWT认证
+
+ def post(self, request):
+ # 1. 获取当前登录用户并验证商家身份
+ try:
+ current_user = request.user
+ # 使用反向查询获取商家扩展信息(性能最佳)
+ shangjia = current_user.ShopProfile
+ except UserShangjia.DoesNotExist:
+ return Response({
+ 'code': 1,
+ 'msg': '您不是商家或商家信息未完善',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 2. 检查商家账号状态
+ if shangjia.zhuangtai != 1:
+ return Response({
+ 'code': 2,
+ 'msg': '商家账号状态异常,无法操作',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 获取前端参数
+ try:
+ data = request.data
+ dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
+ chexiao_liyou = data.get('chexiao_liyou', '').strip()
+ except Exception:
+ return Response({
+ 'code': 3,
+ 'msg': '参数格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 参数验证
+ if not dingdan_id or not chexiao_liyou:
+ return Response({
+ 'code': 4,
+ 'msg': '订单ID和撤销原因不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ # 5. 使用事务确保数据一致性
+ with transaction.atomic():
+ # 查询订单主表
+ dingdan = Order.query.filter(OrderID=dingdan_id).first()
+ if not dingdan:
+ return Response({
+ 'code': 5,
+ 'msg': '订单不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 6. 验证订单状态(13=链接待填单,可撤销释放额度)
+ if dingdan.Status not in [1, 7, 13]:
+ return Response({
+ 'code': 6,
+ 'msg': '当前订单状态不可撤销',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 7. 验证订单所属商家(使用反向查询)
+ shangjia_kuozhan = MerchantOrderExt.query.filter(
+ Order__OrderID=dingdan_id,
+ MerchantID=current_user.UserUID
+ ).first()
+
+ if not shangjia_kuozhan:
+ return Response({
+ 'code': 7,
+ 'msg': '您无权撤销此订单',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 8. 创建退款记录(注意:申请状态直接设为1-成功)
+ RefundRecord.query.create(
+ OrderID=dingdan_id,
+ ApplicantID=current_user.UserUID,
+ Reason=chexiao_liyou,
+ ApplyStatus=1, # 直接成功
+ Amount=dingdan.Amount,
+ PlayerCommission=dingdan.PlayerCommission,
+ Description=dingdan.Description,
+ Remark=dingdan.Remark,
+ Nickname=dingdan.Nickname,
+
+ )
+
+ # 9. 更新商家扩展表(客服额度单不退商家余额,只释放额度)
+ from merchant_ops.services.wallet import is_staff_quota_order
+ staff_quota_order = is_staff_quota_order(dingdan_id)
+ shangjia_updates = {
+ 'tuikuan': F('tuikuan') + 1,
+ 'jinridingdan': F('jinridingdan') - 1,
+ 'jinyuedingdan': F('jinyuedingdan') - 1,
+ 'jinriliushui': F('jinriliushui') - dingdan.Amount,
+ 'jinyueliushui': F('jinyueliushui') - dingdan.Amount,
+ }
+ if not staff_quota_order:
+ shangjia_updates['yue'] = F('yue') + dingdan.Amount
+ UserShangjia.query.filter(id=shangjia.id).update(**shangjia_updates)
+
+ # 10. 更新订单状态为5(已退款)
+ dingdan.Status = 5
+
+ dingdan.save()
+
+ # 行为驱动
+ update_shangjia_daily(
+ yonghuid=request.user.UserUID,
+ amount=Decimal(str(dingdan.Amount)),
+ action=3, # 3 = 退款
+ order_id=dingdan_id,
+ )
+
+ return Response({
+ 'code': 0,
+ 'msg': '撤销成功',
+ 'data': {
+ 'dingdan_id': dingdan_id,
+ 'new_zhuangtai': 5,
+ 'tuikuan_liyou': chexiao_liyou,
+ 'jine': float(dingdan.Amount) if dingdan.Amount else 0.0
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ # 记录异常日志
+ logger.error(f"商家撤销订单失败: {str(e)}", exc_info=True)
+
+ return Response({
+ 'code': 8,
+ 'msg': '服务器内部错误,撤销失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class ShangjiaTuikuanShenqingView(APIView):
+ """商家退款申请接口"""
+ permission_classes = [IsAuthenticated] # JWT认证
+
+ def post(self, request):
+ # 1. 获取当前登录用户并验证商家身份
+ try:
+ current_user = request.user
+ shangjia = current_user.ShopProfile
+ except UserShangjia.DoesNotExist:
+ return Response({
+ 'code': 1,
+ 'msg': '您不是商家或商家信息未完善',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 2. 检查商家账号状态
+ if shangjia.zhuangtai != 1:
+ return Response({
+ 'code': 2,
+ 'msg': '商家账号状态异常,无法操作',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 获取前端参数
+ try:
+ data = request.data
+ dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
+ tuikuan_liyou = data.get('tuikuan_liyou', '').strip()
+ except Exception:
+ return Response({
+ 'code': 3,
+ 'msg': '参数格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 参数验证
+ if not dingdan_id or not tuikuan_liyou:
+ return Response({
+ 'code': 4,
+ 'msg': '订单ID和退款原因不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ # 5. 使用事务确保数据一致性
+ with transaction.atomic():
+ # 查询订单主表
+ dingdan = Order.query.filter(OrderID=dingdan_id).first()
+ if not dingdan:
+ return Response({
+ 'code': 5,
+ 'msg': '订单不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 6. 验证订单状态
+ if dingdan.Status not in [2, 8]:
+ return Response({
+ 'code': 6,
+ 'msg': '当前订单状态不可申请退款',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 7. 验证订单所属商家
+ shangjia_kuozhan = MerchantOrderExt.query.filter(
+ Order__OrderID=dingdan_id,
+ MerchantID=current_user.UserUID
+ ).first()
+
+ if not shangjia_kuozhan:
+ return Response({
+ 'code': 7,
+ 'msg': '您无权为此订单申请退款',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # ========== 8. 处理退款记录(修改点:先查询是否存在) ==========
+ refund_record, created = RefundRecord.query.get_or_create(
+ OrderID=dingdan_id,
+ defaults={
+ 'PlayerID': dingdan.PlayerID,
+ 'ApplicantID': current_user.UserUID,
+ 'Reason': tuikuan_liyou,
+ 'ApplyStatus': 0, # 待处理
+ 'Amount': dingdan.Amount,
+ 'PlayerCommission': dingdan.PlayerCommission,
+ 'Description': dingdan.Description,
+ 'Remark': dingdan.Remark,
+ 'Nickname': dingdan.Nickname,
+ }
+ )
+ if not created:
+ # 记录已存在,更新退款理由和申请时间
+ refund_record.Reason = tuikuan_liyou
+ refund_record.save(update_fields=['Reason', 'UpdateTime'])
+ logger.info(f"订单 {dingdan_id} 已有退款记录,已更新退款理由")
+
+ # 9. 更新订单状态为4(退款中)
+ dingdan.Status = 4
+ dingdan.RefundReason = tuikuan_liyou
+ dingdan.save()
+
+ # ====== 原有功能:更新打手状态 ======
+ if dingdan.PlayerID:
+ try:
+ dashou_main = User.query.filter(
+ UserUID=dingdan.PlayerID
+ ).first()
+ if dashou_main and hasattr(dashou_main, 'DashouProfile'):
+ dashou_profile = dashou_main.DashouProfile
+ dashou_profile.zhuangtai = 1
+ dashou_profile.save()
+ logger.info(f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.PlayerID}状态变更为1")
+ except Exception as e:
+ logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id},打手ID{dingdan.PlayerID}")
+
+ return Response({
+ 'code': 0,
+ 'msg': '退款申请提交成功',
+ 'data': {
+ 'dingdan_id': dingdan_id,
+ 'new_zhuangtai': 4,
+ 'tuikuan_liyou': tuikuan_liyou,
+ 'jine': float(dingdan.Amount) if dingdan.Amount else 0.0,
+ 'dashou_id': dingdan.PlayerID
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"商家退款申请失败: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 8,
+ 'msg': '服务器内部错误,申请失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class ShangjiaChufaShenqingView(APIView):
+ """商家处罚申请接口(已添加图片上传功能)"""
+ permission_classes = [IsAuthenticated] # JWT认证
+
+ def post(self, request):
+ # 1. 获取当前登录用户并验证商家身份
+ try:
+ current_user = request.user
+ shangjia = current_user.ShopProfile
+ except UserShangjia.DoesNotExist:
+ return Response({
+ 'code': 1,
+ 'msg': '您不是商家或商家信息未完善',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 2. 检查商家账号状态
+ if shangjia.zhuangtai != 1:
+ return Response({
+ 'code': 2,
+ 'msg': '商家账号状态异常,无法操作',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 获取前端参数
+ try:
+ data = request.data
+ dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
+ dashou_id = data.get('dashou_id', '').strip()
+ chufa_liyou = data.get('chufa_liyou', '').strip()
+ chufa_tupian_urls = data.get('chufa_tupian_urls', []) # 🔴 新增:图片URL数组
+ except Exception:
+ return Response({
+ 'code': 3,
+ 'msg': '参数格式错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 参数验证
+ if not dingdan_id or not dashou_id or not chufa_liyou:
+ return Response({
+ 'code': 4,
+ 'msg': '订单ID、打手ID和处罚原因不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ # 5. 使用事务确保数据一致性
+ with transaction.atomic():
+ # 验证打手身份
+ try:
+ dashou_user = User.query.get(UserUID=dashou_id)
+ dashou = dashou_user.DashouProfile
+ except User.DoesNotExist:
+ return Response({
+ 'code': 5,
+ 'msg': '打手用户不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 5,
+ 'msg': '该用户不是打手',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 查询订单主表
+ dingdan = Order.query.filter(OrderID=dingdan_id).first()
+ if not dingdan:
+ return Response({
+ 'code': 6,
+ 'msg': '订单不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 验证订单状态
+ if dingdan.Status not in [2, 8, 4, 5, 6, 3]:
+ return Response({
+ 'code': 7,
+ 'msg': '当前订单状态不可申请处罚',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证订单所属商家
+ shangjia_kuozhan = MerchantOrderExt.query.filter(
+ Order__OrderID=dingdan_id,
+ MerchantID=current_user.UserUID
+ ).first()
+
+ if not shangjia_kuozhan:
+ return Response({
+ 'code': 8,
+ 'msg': '您无权为此订单申请处罚',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 验证订单的打手是否与提供的打手ID一致
+ if dingdan.PlayerID != dashou_id:
+ return Response({
+ 'code': 9,
+ 'msg': '该打手并非此订单的服务打手',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 创建处罚记录
+ chufajilu = PenaltyRecord.query.create(
+ PlayerID=dashou_id,
+ ApplicantID=current_user.UserUID,
+ PenaltyReason=chufa_liyou,
+ ApplyStatus=0, # 待处理
+ DeductedPoints=5, # 扣除5积分
+ OrderID=dingdan_id,
+ ClubID=resolve_penalty_record_club_id(
+ order_id=dingdan_id,
+ player_id=dashou_id,
+ request=request,
+ applicant_id=current_user.UserUID,
+ user=current_user,
+ ),
+ )
+ # 🔴【新增】在创建处罚记录后立即扣除打手积分
+ # 注意:这里使用前面已经获取的dashou对象(打手扩展表)
+ jifen_kouchu = chufajilu.DeductedPoints # 扣除的积分,这里是5
+ if jifen_kouchu > 0:
+ # 检查打手积分是否足够(UserDashou 字段为 jifen)
+ if dashou.jifen < jifen_kouchu:
+ dashou.jifen = 0
+ else:
+ dashou.jifen -= jifen_kouchu
+ dashou.save()
+
+ # 🔴【新增】保存处罚图片到数据库
+ if chufa_tupian_urls and isinstance(chufa_tupian_urls, list):
+ for tupian_url in chufa_tupian_urls:
+ if tupian_url and tupian_url.strip():
+ PenaltyEvidenceImage.query.create(
+ OrderID=dingdan_id,
+ UserID=current_user.UserUID,
+ ImageURL=tupian_url
+ )
+
+ # 更新商家订单扩展表的处罚相关字段
+ shangjia_kuozhan.PenaltyReason = chufa_liyou
+ shangjia_kuozhan.PenaltyApplyStatus = 0
+ shangjia_kuozhan.save()
+
+ return Response({
+ 'code': 0,
+ 'msg': '处罚申请提交成功',
+ 'data': {
+ 'dingdan_id': dingdan_id,
+ 'dashou_id': dashou_id,
+ 'chufa_liyou': chufa_liyou,
+ 'sqzhuangtai': 0,
+ 'jifen': 5,
+ 'tupian_count': len(chufa_tupian_urls) if chufa_tupian_urls else 0 # 返回图片数量
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f"商家处罚申请失败: {str(e)}", exc_info=True)
+
+ return Response({
+ 'code': 10,
+ 'msg': '服务器内部错误,申请失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
diff --git a/orders/views/merchant_penalty.py b/orders/views/merchant_penalty.py
new file mode 100644
index 0000000..d71dff4
--- /dev/null
+++ b/orders/views/merchant_penalty.py
@@ -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': '证据图片已更新'})
diff --git a/orders/views/payment.py b/orders/views/payment.py
new file mode 100644
index 0000000..3e4af80
--- /dev/null
+++ b/orders/views/payment.py
@@ -0,0 +1,1009 @@
+"""orders.views.payment - 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 PaymentFailView(APIView):
+ """
+ 前端支付失败接口 - 处理用户主动取消或支付失败
+ 只允许删除状态为9(未支付)的订单
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ dingdanid = request.data.get('dingdanid')
+ if not dingdanid:
+ return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None})
+
+ with transaction.atomic():
+ # 锁定订单防止并发
+ dingdan = Order.query.select_for_update().filter(OrderID=dingdanid).first()
+ if not dingdan:
+ return Response({'code': 2, 'msg': '订单不存在', 'data': None})
+
+ if dingdan.User1ID != f"Boss{request.user.UserUID}":
+ return Response({'code': 3, 'msg': '无权操作此订单', 'data': None})
+
+ # 只允许删除未支付订单(状态9)
+ if dingdan.Status != 9:
+ return Response({'code': 3, 'msg': '只能删除未支付的订单', 'data': None})
+
+ # 删除扩展表和主表
+ PlatformOrderExt.query.filter(Order=dingdan).delete()
+ dingdan.delete()
+
+ return Response({'code': 0, 'msg': '订单已删除', 'data': None})
+
+ except Exception as e:
+ return Response({'code': 99, 'msg': f'删除订单失败: {str(e)}', 'data': None})
+
+
+
+class WechatPayNotifyView(APIView):
+ """微信支付回调接口 - 接收微信官方支付结果通知(含详细调试日志)"""
+ throttle_classes = [AnonRateThrottle]
+ permission_classes = [AllowAny]
+
+ def post(self, request):
+ # 记录原始请求数据(调试用)
+ xml_data = request.body.decode('utf-8')
+
+ # 最外层异常捕获,确保任何错误都能被记录并返回适当响应
+ try:
+ # 1. 解析 XML
+ try:
+ root = ET.fromstring(xml_data)
+ except ET.ParseError as e:
+ logger.error(f"XML解析失败: {e}")
+ return self._gen_response_xml('FAIL', 'XML解析失败')
+
+ # 2. 获取关键参数
+ return_code = root.find('return_code').text if root.find('return_code') is not None else ''
+ result_code = root.find('result_code').text if root.find('result_code') is not None else ''
+ out_trade_no = root.find('out_trade_no').text if root.find('out_trade_no') is not None else ''
+ transaction_id = root.find('transaction_id').text if root.find('transaction_id') is not None else ''
+
+ from jituan.services.wechat_pay import club_id_from_order, verify_wechat_v2_signature
+ notify_club_id = club_id_from_order(out_trade_no)
+
+ # 3. 签名验证
+ if not verify_wechat_v2_signature(xml_data, notify_club_id):
+ logger.warning("签名验证失败")
+ return self._gen_response_xml('FAIL', '签名验证失败')
+
+ # 4. 根据支付结果处理
+ if return_code == 'SUCCESS' and result_code == 'SUCCESS':
+ self._handle_success(out_trade_no, transaction_id, root)
+ else:
+ logger.info(f"支付失败或异常,return_code={return_code}, result_code={result_code}")
+ self._handle_failure(out_trade_no)
+
+ # 返回微信要求的成功响应(必须返回SUCCESS,即使内部处理失败也要返回,否则微信会重复通知)
+ return self._gen_response_xml('SUCCESS', 'OK')
+
+ except Exception as e:
+ # 捕获任何未处理的异常,记录完整堆栈
+ logger.error(f"回调处理发生未捕获异常: {str(e)}")
+ logger.error(traceback.format_exc())
+ return self._gen_response_xml('FAIL', f'服务器内部错误: {str(e)}')
+
+ def _verify_signature(self, xml_data):
+ """微信支付签名验证(MD5)"""
+ try:
+ root = ET.fromstring(xml_data)
+ sign = root.find('sign').text
+ # 构建待签名字符串(排除sign字段)
+ data = {child.tag: child.text for child in root if child.tag != 'sign'}
+ # 按key排序
+ sorted_keys = sorted(data.keys())
+ stringA = '&'.join([f"{k}={data[k]}" for k in sorted_keys])
+ stringSignTemp = f"{stringA}&key={settings.WEIXIN_SHANGHUMIYAO}"
+ calc_sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
+ logger.debug(f"计算签名: {calc_sign}, 微信签名: {sign}")
+ return calc_sign == sign
+ except Exception as e:
+ logger.error(f"签名验证过程异常: {e}")
+ logger.error(traceback.format_exc())
+ return False
+
+ def _gen_response_xml(self, return_code, return_msg):
+ """生成返回给微信的XML响应"""
+ xml = f"""
+
+
+"""
+ logger.info(f"返回给微信的响应: {xml}")
+ return HttpResponse(xml, content_type='application/xml')
+
+ @transaction.atomic
+ def _handle_success(self, out_trade_no, transaction_id, root):
+ """处理支付成功"""
+ logger.info(f"进入 _handle_success, out_trade_no={out_trade_no}, transaction_id={transaction_id}")
+
+ # 锁定订单防止并发
+ try:
+ dingdan = Order.query.select_for_update().filter(OrderID=out_trade_no).first()
+ except Exception as e:
+ logger.error(f"查询订单异常: {e}")
+ raise
+
+ if not dingdan:
+ logger.error(f"订单不存在: {out_trade_no}")
+ return
+
+ logger.info(f"订单当前状态: {dingdan.Status}")
+
+ # 幂等:已支付则补试收支记账(防止首次入账失败后再也无法统计)
+ if dingdan.Status != 9:
+ logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.Status},补试收支记账")
+ try:
+ from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
+ repair_wechat_income_if_missing(
+ dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{dingdan.OrderID}',
+ )
+ except Exception as e:
+ logger.error(f'订单补记收支失败 {out_trade_no}: {e}', exc_info=True)
+ return
+
+ # 保存微信交易号
+ if transaction_id:
+ dingdan.WechatTransactionID = transaction_id
+ logger.info(f"保存微信交易号: {transaction_id}")
+ else:
+ logger.warning("微信未返回transaction_id")
+
+ # 更新订单状态
+ if dingdan.AssignedID and dingdan.AssignedID.strip():
+ dingdan.Status = 7
+ logger.info("指定单,状态设为7")
+ else:
+ dingdan.Status = 1
+ logger.info("普通单,状态设为1")
+
+ dingdan.save()
+
+ logger.info(f"订单状态已更新为 {dingdan.Status}")
+
+ # 删除同一用户同一商品的其他未支付订单(改为逻辑删除,状态25)
+ try:
+ abandoned_count = Order.query.filter(
+ User1ID=dingdan.User1ID,
+ ProductID=dingdan.ProductID,
+ Status=9
+ ).exclude(OrderID=out_trade_no).update(Status=25)
+ logger.info(f"将 {abandoned_count} 个未支付订单标记为已废弃(25)")
+ except Exception as e:
+ logger.error(f"标记其他未支付订单异常: {e}")
+
+ # 更新老板扩展表
+ try:
+ laoban_id = None
+ if hasattr(dingdan, 'pingtai_kuozhan'):
+ laoban_id = dingdan.pingtai_kuozhan.BossID
+ if laoban_id:
+ user_main = User.query.filter(UserUID=laoban_id).first()
+ if user_main and hasattr(user_main, 'BossProfile'):
+ boss = user_main.BossProfile
+ boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
+ boss.alldingdan = (boss.alldingdan or 0) + 1
+ boss.save()
+ logger.info(f"更新老板 {laoban_id} 数据成功")
+ except Exception as e:
+ logger.error(f"更新老板数据异常: {e}")
+
+ # 更新商品库存
+ try:
+ if dingdan.ProductID:
+ Shangpin.query.filter(id=dingdan.ProductID).update(
+ kucun=F('kucun') - 1
+ )
+ logger.info(f"商品 {dingdan.ProductID} 库存减1")
+ 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=1, # 1 = 下单
+ dingdan_jiage=dingdan.Amount,
+ PlatformIncome=pingtai_shouyi,
+ dianpu_zongshouyi=dianpu_shouyi
+ )
+ logger.info(f"商品统计更新完成,商品ID: {dingdan.ProductID}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ 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=1, # 1 = 下单
+ 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())
+ # 统计失败不影响主流程
+ # =============================================
+
+
+ # 更新收支记录(总累计);统计失败不得回滚订单支付
+ try:
+ from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
+ repair_wechat_income_if_missing(
+ dingdan.Amount, getattr(dingdan, 'ClubID', None), biz_ref=f'order:{dingdan.OrderID}',
+ )
+ except Exception as e:
+ logger.error(f'订单收支统计失败 {out_trade_no}: {e}', exc_info=True)
+
+ # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用)
+ order_info = {
+ 'OrderID': dingdan.OrderID,
+ 'game_type': self._get_game_type_name(dingdan.ProductTypeID),
+ 'amount': str(dingdan.Amount),
+ 'order_desc': (dingdan.Description or '')[:50],
+ }
+ dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求
+
+ # 异步广播通知
+ #self._broadcast_async(dingdan)
+
+ logger.info(f"订单 {out_trade_no} 支付成功处理完成")
+
+ @transaction.atomic
+ def _handle_failure(self, out_trade_no):
+ """处理支付失败"""
+ logger.info(f"进入 _handle_failure, out_trade_no={out_trade_no}")
+
+ try:
+ dingdan = Order.query.select_for_update().filter(OrderID=out_trade_no).first()
+ if not dingdan:
+ logger.warning(f"订单不存在: {out_trade_no}")
+ return
+
+ logger.info(f"订单当前状态: {dingdan.Status}")
+
+ # 仅当订单状态为9(未支付)时才标记失败,不直接删除
+ if dingdan.Status == 9:
+ dingdan.Status = 14 # 自定义支付失败状态
+ dingdan.save()
+ logger.info(f"订单 {out_trade_no} 支付失败,标记为14")
+ else:
+ logger.info(f"订单 {out_trade_no} 状态非9,不处理")
+ except Exception as e:
+ logger.error(f"处理支付失败异常: {e}")
+ logger.error(traceback.format_exc())
+
+ def _update_szjilu(self, jine, club_id=None):
+ """szjilu 已废弃,财务只写 daily_income_stat,此处不再操作。"""
+ pass
+
+ def _broadcast_async(self, dingdan):
+ """异步广播通知"""
+ try:
+ def task():
+ try:
+ order_info = {
+ 'OrderID': dingdan.OrderID,
+ 'game_type': self._get_game_type_name(dingdan.ProductTypeID),
+ 'amount': str(dingdan.Amount),
+ 'order_desc': (dingdan.Description or '')[:50],
+ }
+ logger.info(f"准备广播订单: {order_info}")
+ sender = WeixinBroadcastSender()
+ result = sender.broadcast_order(order_info)
+ if result.get('success'):
+ logger.info(f"订单广播完成: {result.get('msg')}")
+ else:
+ logger.warning(f"订单广播异常: {result.get('msg')}")
+ except Exception as e:
+ logger.error(f"广播任务异常: {e}")
+ logger.error(traceback.format_exc())
+
+ thread = threading.Thread(target=task)
+ thread.daemon = True
+ thread.start()
+ logger.info("广播线程已启动")
+ except Exception as e:
+ logger.error(f"启动广播线程异常: {e}")
+
+ def _get_game_type_name(self, leixing_id):
+ try:
+ gt = ShangpinLeixing.query.filter(id=leixing_id).first()
+ return gt.jieshao if gt else '游戏订单'
+ except Exception as e:
+ logger.error(f"获取游戏类型名称异常: {e}")
+ return '游戏订单'
+
+
+
+class PaymentVerifyView(APIView):
+ """
+ 订单支付成功复查接口 - 前端支付成功后调用,确认订单状态
+ 当订单状态为9时,主动查询微信支付状态,若已支付则更新本地状态
+ 需要token认证
+ """
+ permission_classes = [permissions.IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # ---------- 1. 获取参数 ----------
+ dingdanid = request.data.get('dingdanid')
+ if not dingdanid:
+ return Response({'code': 1, 'msg': '订单ID不能为空'})
+
+ # ---------- 2. 查询订单 ----------
+ dingdan = Order.query.filter(OrderID=dingdanid).first()
+ if not dingdan:
+ return Response({'code': 2, 'msg': '订单不存在'})
+
+ # ---------- 3. 验证订单归属(安全) ----------
+ current_user = request.user
+ user_identifier = f"Boss{current_user.UserUID}"
+ if dingdan.User1ID != user_identifier:
+ logger.warning(f"用户 {current_user.UserUID} 尝试复查他人订单 {dingdanid}")
+ return Response({'code': 403, 'msg': '无权访问'})
+
+ # ---------- 4. 如果订单状态已是1/7,直接返回成功 ----------
+ if dingdan.Status in [1, 7]:
+ return Response({
+ 'code': 0,
+ 'msg': '订单状态正常',
+ 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.Status}
+ })
+
+ # ---------- 5. 如果状态为9,则主动查询微信支付状态 ----------
+ if dingdan.Status == 9:
+ # 使用分布式锁防止并发处理同一订单
+ lock_key = f"payment_verify_lock_{dingdanid}"
+ if not cache.add(lock_key, "locked", timeout=10):
+ # 已有其他进程在处理,让前端稍后重试
+ return Response({'code': 1, 'msg': '订单正在处理中,请稍后'})
+
+ try:
+ # 调用微信订单查询接口
+ trade_state, transaction_id = self._query_wechat_payment(dingdanid)
+
+ if trade_state == 'SUCCESS':
+ # 支付成功,执行本地更新
+ self._process_payment_success(dingdan, transaction_id, current_user.UserUID)
+ # 重新获取订单以获取最新状态
+ dingdan.refresh_from_db()
+ return Response({
+ 'code': 0,
+ 'msg': '支付成功',
+ 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.Status}
+ })
+ elif trade_state in ['NOTPAY', 'USERPAYING']:
+ # 用户尚未支付或支付中
+ return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}})
+ else:
+ # 其他状态:CLOSED, REVOKED, PAYERROR 等,视为支付失败
+ return Response({'code': 2, 'msg': '支付失败'})
+
+ except Exception as e:
+ logger.error(f"查询微信支付异常: {e}", exc_info=True)
+ return Response({'code': 1, 'msg': '查询失败,请稍后重试'})
+ finally:
+ cache.delete(lock_key)
+ else:
+ # ---------- 6. 其他异常状态 ----------
+ status_msg = {
+ 2: '订单已取消',
+ 3: '订单已完成',
+ 4: '退款审核中',
+ 5: '已退款',
+ 6: '退款失败',
+ 8: '结算中',
+ 0: '订单已作废',
+ }.get(dingdan.Status, '订单状态异常')
+ return Response({'code': 3, 'msg': status_msg, 'data': {'zhuangtai': dingdan.Status}})
+
+ except Exception as e:
+ logger.error(f"订单复查接口异常: {e}", exc_info=True)
+ return Response({'code': 99, 'msg': f'系统错误: {str(e)}'})
+
+ def _process_payment_success(self, dingdan, transaction_id, operator_id):
+ """
+ 支付成功后的原子操作(与微信回调逻辑完全一致)
+ 在事务内完成所有状态更新和数据一致性操作
+ """
+ with transaction.atomic():
+ # 重新锁定订单,防止并发更新
+ dingdan = Order.query.select_for_update().get(pk=dingdan.pk)
+ if dingdan.Status != 9:
+ try:
+ from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
+ repair_wechat_income_if_missing(
+ dingdan.Amount,
+ getattr(dingdan, 'ClubID', None),
+ biz_ref=f'order:{dingdan.OrderID}',
+ )
+ except Exception as e:
+ logger.error(f'订单复查补记收支失败 {dingdan.OrderID}: {e}', exc_info=True)
+ return
+
+ # 更新订单状态
+ dingdan.Status = 7 if dingdan.AssignedID and dingdan.AssignedID.strip() else 1
+ dingdan.WechatTransactionID = transaction_id
+ dingdan.AssignedCS = operator_id # 可选:记录操作人ID
+ dingdan.save()
+
+ # 删除同一用户同一商品的其他未支付订单(逻辑删除,状态25)
+ Order.query.filter(
+ User1ID=dingdan.User1ID,
+ ProductID=dingdan.ProductID,
+ Status=9
+ ).exclude(OrderID=dingdan.OrderID).update(Status=25)
+
+ # 更新老板扩展表
+ laoban_id = dingdan.pingtai_kuozhan.BossID if hasattr(dingdan, 'pingtai_kuozhan') else None
+ if laoban_id:
+ user_main = User.query.filter(UserUID=laoban_id).first()
+ if user_main and hasattr(user_main, 'BossProfile'):
+ boss = user_main.BossProfile
+ boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
+ boss.alldingdan = (boss.alldingdan or 0) + 1
+ boss.save()
+
+ # 更新商品库存
+ if dingdan.ProductID:
+ Shangpin.query.filter(id=dingdan.ProductID).update(
+ kucun=F('kucun') - 1
+ )
+
+
+ # ========== 新增:商品和店铺每日统计 ==========
+ 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=1, # 1 = 下单
+ dingdan_jiage=dingdan.Amount,
+ PlatformIncome=pingtai_shouyi,
+ dianpu_zongshouyi=dianpu_shouyi
+ )
+ logger.info(f"商品统计更新完成,商品ID: {dingdan.ProductID}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ 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=1, # 1 = 下单
+ 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())
+ # 统计失败不影响主流程
+ # =============================================
+
+
+ try:
+ from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
+ repair_wechat_income_if_missing(
+ dingdan.Amount,
+ getattr(dingdan, 'ClubID', None),
+ biz_ref=f'order:{dingdan.OrderID}',
+ )
+ except Exception as e:
+ logger.error(f'订单收支统计失败 {dingdan.OrderID}: {e}', exc_info=True)
+
+ def _query_wechat_payment(self, out_trade_no):
+ """
+ 调用微信支付订单查询接口,返回 (trade_state, transaction_id)
+ 若查询失败,抛出异常
+ """
+ appid = settings.WEIXIN_APPID
+ mch_id = settings.WEIXIN_MCHID
+ key = settings.WEIXIN_SHANGHUMIYAO
+ nonce_str = self._generate_nonce_str()
+
+ params = {
+ 'appid': appid,
+ 'mch_id': mch_id,
+ 'out_trade_no': out_trade_no,
+ 'nonce_str': nonce_str,
+ }
+
+ # 生成签名
+ 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/pay/orderquery'
+ response = requests.post(url, data=xml_data, timeout=10)
+ result = xmltodict.parse(response.content)['xml']
+
+ if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
+ return result.get('trade_state'), result.get('transaction_id')
+ else:
+ err_msg = result.get('return_msg', '未知错误')
+ raise Exception(f"微信订单查询失败: {err_msg}")
+
+ def _update_szjilu(self, jine, club_id=None):
+ """szjilu 已废弃,财务只写 daily_income_stat,此处不再操作。"""
+ pass
+ # 不抛出异常,因为收支记录不是核心流程,但记录错误供排查
+
+ def _generate_nonce_str(self):
+ """生成随机字符串,长度32"""
+ return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+
+ def _dict_to_xml(self, data):
+ """将字典转换为XML字符串"""
+ xml = ['']
+ for k, v in data.items():
+ xml.append(f'<{k}>{v}{k}>')
+ xml.append('')
+ return ''.join(xml)
+
+
+
+
+class CreateOrderView(APIView):
+ """
+ 创建订单并调用微信支付接口
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端传递的字段
+ shangpin_id = request.data.get('shangpin_id')
+ nicheng = request.data.get('nicheng', '').strip()
+ beizhu = request.data.get('Remark', '').strip()
+ zhiding = request.data.get('zhiding', '').strip()
+ jine = request.data.get('jine', 0)
+
+ # 2. 验证必填字段
+ if not shangpin_id:
+ return Response({'code': 1, 'msg': '商品ID不能为空', 'data': None})
+
+ if not nicheng:
+ return Response({'code': 2, 'msg': '游戏昵称不能为空', 'data': None})
+
+ if len(nicheng) > 50:
+ return Response({'code': 3, 'msg': '游戏昵称过长', 'data': None})
+
+ # 3. 查询商品信息
+ shangpin = Shangpin.query.filter(id=shangpin_id).first()
+ if not shangpin:
+ return Response({'code': 4, 'msg': '商品不存在', 'data': None})
+
+
+ is_valid, err_msg, _ = validate_shangpin_and_dianpu(shangpin_id)
+ if not is_valid:
+ return Response({'code': 5, 'msg': err_msg, 'data': None})
+
+ # 检查库存
+ if hasattr(shangpin, 'kucun') and shangpin.kucun <= 0:
+ return Response({'code': 5, 'msg': '商品库存不足', 'data': None})
+
+ # 获取商品价格
+ shangpin_jiage = getattr(shangpin, 'jiage', 0)
+ if not shangpin_jiage:
+ return Response({'code': 6, 'msg': '商品价格异常', 'data': None})
+ #获取商品图片
+ shangpin_tupian = getattr(shangpin, 'tupian_url', None)
+
+ # 4. 验证指定打手(如果存在)
+ zhiding_id_value = None
+ if zhiding:
+ # 查询主表中是否存在该打手
+ zhiding_user = User.query.filter(
+ UserUID=zhiding,
+ #user_type='PlayerID'
+ ).first()
+
+ if not zhiding_user:
+ return Response({'code': 7, 'msg': '指定打手不存在', 'data': None})
+
+ # 查询打手扩展表
+ dashou_profile = UserDashou.query.filter(user=zhiding_user).first()
+
+ if not dashou_profile:
+ return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None})
+
+ # 检查在线状态
+ if getattr(dashou_profile, 'zhanghaozhuangtai', 0) == 0:
+ return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None})
+
+ zhiding_id_value = zhiding
+
+ # 5. 生成订单ID
+ timestamp = int(time.time())
+ timestamp_ns = int(time.time_ns() // 1000) % 1000000
+ random_num = random.randint(0, 99)
+ dingdanid = f"PT{timestamp:011d}{timestamp_ns:06d}{random_num:02d}"
+
+ # 6. 查询利率并计算分成
+ club_id = resolve_club_id_from_request(request)
+ dashou_lilu = get_commission_rate_object(club_id, '1')
+
+
+ # 🔥 新增:判断是否使用商品配置的额外分成
+ if shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None:
+ # 使用商品指定的额外分成金额
+ dashou_fencheng = shangpin.ewai_dashou_fencheng
+ else:
+ # 按原有逻辑计算打手分成
+ dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.Rate))
+
+
+ # 查询管事利率(字符'2')
+ #guanshi_lilu = CommissionRate.query.filter(Platform='2').first()
+
+ if not (shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None):
+ if get_commission_rate(club_id, '1') <= 0 and not CommissionRate.query.filter(Platform='1').exists():
+ return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None})
+
+
+ # 开始数据库事务
+ with transaction.atomic():
+ # 获取当前用户信息
+ current_user = request.user
+ laoban_yonghuid = getattr(current_user, 'UserUID', '') or getattr(current_user, 'yonghuid', '')
+ user_openid, club_id = get_payment_openid(request, current_user, club_id)
+
+ if not user_openid:
+ return Response({'code': 10, 'msg': '用户openid不存在', 'data': None})
+
+ # 创建订单主表数据
+ order_data = {
+ 'OrderID': dingdanid,
+ 'Status': 9, # 未付款状态
+ 'Amount': shangpin_jiage,
+ 'Platform':1,
+ 'PlayerCommission': dashou_fencheng,
+ 'AssignedID': zhiding_id_value,
+ 'ProductID': shangpin_id,
+ 'GrabRequirement': getattr(shangpin, 'yaoqiuleixing', None),
+ 'MembershipID': getattr(shangpin, 'huiyuan_id', ''),
+ 'CommissionReq': getattr(shangpin, 'yongjin', 0),
+ 'Description': getattr(shangpin, 'jieshao', ''),
+ 'Remark': beizhu,
+ 'Nickname': nicheng,
+ 'ImageURL':shangpin_tupian,
+ 'User1ID':f"Boss{request.user.UserUID}",
+ 'ProductTypeID':shangpin.leixing_id,
+ 'ClubID': club_id,
+ }
+
+ # 创建订单记录
+
+ dingdan = Order.query.create(**order_data)
+
+
+ # CreateOrderView.post 事务内部,替换原有的扩展表创建代码
+
+ dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong = calculate_pingtai_and_dianpu_shouyi(
+ shangpin_id=shangpin_id,
+ dingdan_jiage=shangpin_jiage,
+ dashou_fencheng=dashou_fencheng
+ )
+
+ pingtaikuozhan_data = {
+ 'Order': dingdan,
+ 'BossID': laoban_yonghuid,
+ 'PlatformIncome': pingtai_shouyi,
+ # ---------- 新增上级分红快照 ----------
+ 'ParentShopID': shangji_dianpu_id,
+ 'ParentBonus': shangji_fenhong if shangji_fenhong else Decimal('0.00'),
+ }
+ if dianpu_id:
+ pingtaikuozhan_data['ShopID'] = dianpu_id
+ pingtaikuozhan_data['ShopIncome'] = dianpu_shouyi
+ else:
+ pingtaikuozhan_data['ShopIncome'] = Decimal('0.00')
+
+ PlatformOrderExt.query.create(**pingtaikuozhan_data)
+
+
+ # 计算收益
+ '''dianpu_id, pingtai_shouyi, dianpu_shouyi = calculate_pingtai_and_dianpu_shouyi(
+ ProductID=shangpin_id,
+ dingdan_jiage=shangpin_jiage,
+ PlayerCommission=dashou_fencheng
+ )
+
+ # 构建扩展表数据,平台收益必定保存,店铺收益和店铺ID仅当dianpu_id存在时才赋值
+ pingtaikuozhan_data = {
+ 'dingdan': dingdan,
+ 'laoban_id': laoban_yonghuid,
+ 'pingtai_shouyi': pingtai_shouyi,
+ }
+ if dianpu_id:
+ pingtaikuozhan_data['dianpu_id'] = dianpu_id
+ pingtaikuozhan_data['dianpu_shouyi'] = dianpu_shouyi
+ else:
+ # 无店铺时,店铺收益字段保持默认(0.00),也可显式设为0
+ pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00')
+
+ PlatformOrderExt.query.create(**pingtaikuozhan_data)'''
+
+
+ # 7. 生成微信支付参数(向微信官方发送请求)
+ pay_params = self.generate_wechat_pay_params(
+ dingdanid=dingdanid,
+ jine=shangpin_jiage,
+ beizhu=getattr(settings, 'WEIXIN_PAY_DESCRIPTION', '星阙订单'),
+ user_ip=self.get_client_ip(request),
+ openid=user_openid,
+ club_id=club_id,
+ )
+
+ if not pay_params:
+ # 手动抛出异常触发事务回滚
+ raise Exception('支付参数生成失败')
+
+ # 8. 返回成功响应
+ response_data = {
+ 'code': 0,
+ 'msg': '订单创建成功',
+ 'data': {
+ 'dingdanid': dingdanid,
+ 'payParams': pay_params
+ }
+ }
+
+ return Response(response_data)
+
+ except Exception as e:
+ # 这里会自动回滚事务
+ return Response({'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None})
+
+ def get_client_ip(self, request):
+ """获取客户端真实IP地址"""
+ 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
+
+ def generate_wechat_pay_params(self, dingdanid, jine, beizhu, user_ip, openid, club_id=None):
+ """
+ 调用微信支付统一下单接口,生成支付参数
+ """
+ try:
+ from jituan.services.wechat_pay import get_wechat_v2_config
+ pay_cfg = get_wechat_v2_config(club_id)
+ APPID = pay_cfg['appid']
+ MCHID = pay_cfg['mch_id']
+ KEY = pay_cfg['key']
+ NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '')
+
+ if not all([APPID, MCHID, KEY, NOTIFY_URL]):
+ raise Exception('微信支付配置不完整')
+
+ # 生成随机字符串
+ nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+
+ # 金额(单位:分)
+ total_fee = yuan_to_fen(jine)
+
+ # 交易类型
+ trade_type = 'JSAPI'
+
+ # 生成签名
+ sign_data = {
+ 'appid': APPID,
+ 'mch_id': MCHID,
+ 'nonce_str': nonce_str,
+ 'body': beizhu,
+ 'out_trade_no': dingdanid,
+ 'total_fee': total_fee,
+ 'spbill_create_ip': user_ip,
+ 'notify_url': NOTIFY_URL,
+ 'trade_type': trade_type,
+ '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"""
+{APPID}
+{beizhu}
+{MCHID}
+{nonce_str}
+{NOTIFY_URL}
+{openid}
+{dingdanid}
+{user_ip}
+{total_fee}
+{trade_type}
+{sign}
+"""
+
+ # 调用微信统一下单接口
+ 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 = root.find('prepay_id').text
+
+ # 生成前端支付参数
+ timestamp = str(int(time.time()))
+ nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+ package = f'prepay_id={prepay_id}'
+
+ # 生成支付签名
+ pay_sign_data = {
+ 'appId': APPID,
+ 'timeStamp': timestamp,
+ 'nonceStr': nonce_str,
+ 'package': package,
+ 'signType': 'MD5'
+ }
+
+ 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()
+
+ # 返回支付参数(格式与前端完全匹配)
+ result = {
+ 'timeStamp': timestamp,
+ 'nonceStr': nonce_str,
+ 'package': package,
+ 'signType': 'MD5',
+ 'paySign': pay_sign
+ }
+
+ return result
+ 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}")
+
+ except Exception as e:
+ raise e
+
+
+
diff --git a/products/views.py b/products/views.py
deleted file mode 100644
index f49255c..0000000
--- a/products/views.py
+++ /dev/null
@@ -1,6105 +0,0 @@
-# ==================== 标准库 ====================
-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 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"""
-{APPID}
-{PAY_DESCRIPTION}
-{MCHID}
-{nonce_str}
-{NOTIFY_URL}
-{openid}
-{dingdanid}
-{user_ip}
-{total_fee}
-{trade_type}
-{sign}
-"""
-
- # 调用微信统一下单接口(官方地址)
- 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"""
-
-
-"""
-
- 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
-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"""
-{APPID}
-{PAY_DESCRIPTION}
-{MCHID}
-{nonce_str}
-{NOTIFY_URL}
-{openid}
-{dingdanid}
-{user_ip}
-{total_fee}
-{trade_type}
-{sign}
-"""
-
- # 调用微信统一下单接口(官方地址)
- 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"""
-
-
-"""
-
- 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)
-
-
-
-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"""
-{APPID}
-{PAY_DESCRIPTION}
-{MCHID}
-{nonce_str}
-{NOTIFY_URL}
-{openid}
-{dingdanid}
-{user_ip}
-{total_fee}
-{trade_type}
-{sign}
-"""
-
- # 调用微信统一下单接口(官方地址)
- 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"""
-
-
-"""
- 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
-
-class ShangjiaChongzhi(APIView):
- """
- 商家余额充值支付参数接口
- 路径:/shangpin/sjcz
- 请求方式:POST
- 权限:JWT认证
- 参数:jine(金额,1-10000元)
- 返回:支付参数(payParams)和订单ID(dingdanid)
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 验证用户身份和商家状态
- try:
- shangjia = request.user.ShopProfile
- except UserShangjia.DoesNotExist:
- logger.warning(f"用户{request.user.UserUID}尝试充值但不是商家")
- return Response({
- 'code': 400,
- 'message': '用户不是商家,无法充值'
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 检查商家状态
- if shangjia.zhuangtai != 1:
- logger.warning(f"商家{shangjia.nicheng}状态异常: {shangjia.zhuangtai}")
- 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 = quantize_yuan(jine)
- except (ValueError, TypeError, InvalidOperation):
- return Response({
- 'code': 400,
- 'message': '金额格式错误,请填写有效的数字'
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证金额范围
- if jine < settings.SHANGJIA_CZ_MIN_AMOUNT or jine > settings.SHANGJIA_CZ_MAX_AMOUNT:
- return Response({
- 'code': 400,
- 'message': f'充值金额必须在{settings.SHANGJIA_CZ_MIN_AMOUNT}-{settings.SHANGJIA_CZ_MAX_AMOUNT}元之间'
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 生成订单ID(使用配置的前缀)
- timestamp_ms = int(time.time() * 1000)
- timestamp_ns = int(time.time_ns() // 1000) % 1000000
- random_num = random.randint(0, 99)
-
- dingdanid = f"{settings.SHANGJIA_CZ_PREFIX}{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
-
- # 4. 创建充值记录订单
- try:
- chongzhi_jilu = Czjilu.query.create(
- dingdan_id=dingdanid,
- zhuangtai=9, # 未支付状态
- yonghuid=request.user.UserUID,
- jine=jine,
- leixing=4, # 充值类型:4=商家充值
- shuoming=settings.SHANGJIA_CZ_PAY_DESCRIPTION,
- club_id=resolve_club_id_for_write(request, request.user),
- )
- logger.info(f"创建商家充值订单: 商家{shangjia.nicheng}, 订单{dingdanid}, 金额{jine}元")
- except Exception as e:
- logger.error(f"创建订单失败: {str(e)}")
- return Response({
- 'code': 500,
- 'message': '订单创建失败,请重试'
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 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='shangjia'
- )
- 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
- logger.info(f"商家{shangjia.nicheng}支付参数生成成功: {dingdanid}")
- return Response({
- 'code': 200,
- 'message': '支付参数生成成功',
- 'payParams': pay_params,
- '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):
- """生成微信小程序支付参数"""
- # 获取配置
- 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 == 'shangjia':
- PAY_DESCRIPTION = settings.SHANGJIA_CZ_PAY_DESCRIPTION
- NOTIFY_URL = settings.SHANGJIA_CZ_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"""
-{APPID}
-{PAY_DESCRIPTION}
-{MCHID}
-{nonce_str}
-{NOTIFY_URL}
-{openid}
-{dingdanid}
-{user_ip}
-{total_fee}
-{trade_type}
-{sign}
-"""
-
- # 调用微信统一下单接口
- 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'
-
- # 生成支付签名
- pay_sign_data = {
- 'appId': APPID,
- '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,
- 'timeStamp': timestamp,
- 'nonceStr': nonce_str,
- 'package': package,
- 'signType': sign_type,
- 'paySign': pay_sign
- }
- 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 ShangjiaHuitiao(View):
- """
- 商家支付回调接口
- 路径:/shangpin/sjhuitiao
- 注意:这个接口不需要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', '订单不存在')
-
- from products.czjilu_types import is_merchant_recharge_czjilu
- if not is_merchant_recharge_czjilu(order):
- logger.error(
- '非商家充值订单误入商家回调: %s, leixing=%s, shuoming=%s',
- out_trade_no, order.leixing, order.shuoming,
- )
- 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:{order.dingdan_id}',
- )
- 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 = Decimal(str(total_fee)) / Decimal('100') # 转换为元
- order_amount_yuan = Decimal(str(order.jine)) # 订单金额(元)
-
- # 允许1分钱的误差
- if abs(wechat_amount_yuan - order_amount_yuan) > Decimal('0.01'):
- logger.error(f"商家订单金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
- return self.wechat_response('FAIL', '金额不一致')
-
- # ===== 在这里添加收支记录更新代码 =====
- # 更新收支记录表
- try:
- jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
- from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
- repair_wechat_income_if_missing(
- jine_decimal,
- getattr(order, 'club_id', None),
- biz_ref=f'cz:{order.dingdan_id}',
- )
-
- logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元")
-
- except Exception as e:
- # 收支记录更新失败不影响主流程,只记录日志
- logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
- # ===== 添加结束 =====
-
-
-
- # 7. 使用事务更新订单状态和商家余额
- with transaction.atomic():
- # 更新订单状态为已支付(3)
- order.zhuangtai = 3
- order.save(update_fields=['zhuangtai'])
- logger.info(f"商家订单状态更新成功: {order.dingdan_id} -> 3")
-
- # 更新商家余额
- try:
- # 通过用户ID查找商家(高效查询)
- shangjia = UserShangjia.query.get(user__UserUID=order.yonghuid)
-
- # 增加余额
- shangjia.yue += order_amount_yuan
- shangjia.save(update_fields=['yue'])
-
- logger.info(f"商家余额更新成功: 商家{shangjia.nicheng}, 增加{order_amount_yuan}元, 新余额{shangjia.yue}")
-
- except UserShangjia.DoesNotExist:
- # 如果找不到商家,记录错误,但依然返回成功(避免微信重复回调)
- logger.error(f"商家信息不存在: 用户{order.yonghuid}")
- return self.wechat_response('SUCCESS', 'OK')
- except Exception as e:
- logger.error(f"更新商家余额失败: {str(e)}", exc_info=True)
- # 回滚事务
- raise
-
- # 8. 返回成功响应给微信
- 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"""
-
-
-"""
-
- logger.info(f"返回微信响应: {return_code} - {return_msg}")
- return HttpResponse(xml_response, content_type='application/xml')
-
-
-
-
-# views.py
-class ShangjiaShibai(APIView):
- """
- 商家支付失败处理接口
- 路径:/shangpin/sjshibai
- 请求方式: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,
- leixing=4 # 确保是商家充值订单
- )
- except Czjilu.DoesNotExist:
- logger.warning(f"商家订单不存在或不属于当前用户: {dingdanid}, 用户{request.user.UserUID}")
- return Response({
- 'code': 400,
- 'message': '订单不存在或不属于当前用户'
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 检查订单状态是否为未支付(9)
- if order.zhuangtai == 9:
- # 删除订单
- order.delete()
- logger.info(f"商家{request.user.ShopProfile.nicheng}删除未支付订单: {dingdanid}")
-
- return Response({
- 'code': 200,
- 'message': '订单已删除'
- }, status=status.HTTP_200_OK)
- else:
- return Response({
- 'code': 400,
- 'message': '订单状态不是未支付,无法删除'
- }, status=status.HTTP_400_BAD_REQUEST)
-
- except UserShangjia.DoesNotExist:
- 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 ShangjiaChenggong(APIView):
- """
- 商家支付成功轮询接口
- 路径:/shangpin/sjcg
- 请求方式: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,
- leixing=4 # 商家充值类型
- )
- except Czjilu.DoesNotExist:
- logger.warning(f"商家订单不存在: {dingdanid}, 用户{request.user.UserUID}")
- return Response({
- 'code': 400,
- 'message': '订单不存在或不属于当前用户'
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 检查订单状态是否为已支付(3)
- if order.zhuangtai != 3:
- logger.info(f"商家订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
- return Response({
- 'code': 400,
- 'message': '订单尚未支付完成'
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 获取商家余额(通过反向查询)
- try:
- shangjia = request.user.ShopProfile
- yue = float(shangjia.yue) # 转为浮点数方便前端处理
-
- logger.info(f"商家轮询成功: 订单{dingdanid}已支付,商家{shangjia.nicheng}余额为{yue}元")
-
- return Response({
- 'code': 200,
- 'message': '支付流程全部完成',
- 'yue': yue # 返回当前商家余额(前端需要更新全局变量)
- }, status=status.HTTP_200_OK)
-
- except UserShangjia.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)
-
-
-# 管事分红记录查询API
-
-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 ShangpinLeixingFabuView(APIView):
- """
- 商品类型发布接口
- 接收图片、类型介绍、抢单要求类型等字段
- 需要管理员权限
- """
- permission_classes = [permissions.IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传
-
- def post(self, request):
- try:
- # === 1. 权限验证 ===
- # 获取当前用户
- user_main = request.user
-
- # 1.1 验证前端传递的账号是否匹配(使用phone字段)
- zhanghao = request.data.get('zhanghao', '').strip()
- if not zhanghao:
- return Response(
- {'code': 401, 'msg': '未提供账号信息', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 验证账号是否匹配(使用phone字段比较)
- if user_main.Phone != zhanghao:
- return Response(
- {'code': 401, 'msg': '账号验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.2 验证是否为管理员(检查user_type字段)
- if user_main.UserType != 'admin':
- return Response(
- {'code': 401, 'msg': '权限不足', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.3 验证管理员扩展表(使用AdminProfile模型)
- try:
- admin_profile = AdminProfile.query.get(user=user_main)
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'msg': '管理员信息不存在', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # === 2. 接收并验证字段 ===
- # 2.1 必填字段验证
- jieshao = request.data.get('jieshao', '').strip()
- yaoqiuleixing = request.data.get('yaoqiuleixing')
-
-
- if not jieshao:
- return Response(
- {'code': 400, 'msg': '类型介绍不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- if yaoqiuleixing not in ['1', '2']:
- return Response(
- {'code': 400, 'msg': '抢单要求类型无效', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2.2 验证图片
- tupian_file = request.FILES.get('file')
- if not tupian_file:
-
- return Response(
- {'code': 400, 'msg': '请上传类型图片', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证图片格式和大小
- is_valid, error_msg = validate_image(tupian_file)
- if not is_valid:
- return Response(
- {'code': 400, 'msg': f'图片验证失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2.3 根据抢单要求类型验证对应字段
- huiyuan_id = request.data.get('huiyuan_id', '').strip()
- yongjin = request.data.get('yongjin', '').strip()
-
-
- # 转换yaoqiuleixing为整数便于处理
- yaoqiuleixing_int = int(yaoqiuleixing)
-
- if yaoqiuleixing_int == 1: # 会员抢单
- if not huiyuan_id:
- return Response(
- {'code': 400, 'msg': '会员抢单必须选择会员', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- # 验证会员ID是否存在
- try:
- huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response(
- {'code': 400, 'msg': '会员ID不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- # 会员ID为字符串,佣金为空
- yongjin_value = None
- huiyuan_id_value = huiyuan_id
- else: # yaoqiuleixing_int == 2 佣金抢单
- if not yongjin:
- return Response(
- {'code': 400, 'msg': '佣金抢单必须填写佣金金额', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- try:
- # 验证佣金为有效数字
- yongjin_value = float(yongjin)
- if yongjin_value <= 0:
- return Response(
- {'code': 400, 'msg': '佣金金额必须大于0', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except ValueError:
- return Response(
- {'code': 400, 'msg': '佣金金额格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- huiyuan_id_value = None
-
- # === 3. 创建商品类型记录 ===
- with transaction.atomic():
- # 3.1 创建商品类型对象
- leixing_obj = ShangpinLeixing(
- jieshao=jieshao,
- yaoqiuleixing=yaoqiuleixing_int,
- huiyuan_id=huiyuan_id_value,
- yongjin=yongjin_value,
- # tupian_url 先留空,上传成功后再更新
- )
-
- # 保存到数据库获取ID
- leixing_obj.save()
- leixing_id = leixing_obj.id
-
- # === 4. 处理图片上传 ===
- try:
- # 4.1 生成文件名和路径
- file_extension = os.path.splitext(tupian_file.name)[1].lower()
- if not file_extension:
- file_extension = '.jpg'
-
- # 生成唯一文件名:商品类型ID + 随机UUID + 后缀
- unique_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}"
-
- # 构建OSS文件路径:a_long/leixing/{leixing_id}/{filename}
- oss_folder = 'a_long' # 根文件夹
- leixing_folder = 'leixing' # 类型文件夹
-
- # 构建完整路径
- oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{unique_filename}"
-
- # 4.2 上传到OSS
- tupian_url = upload_to_oss(tupian_file, oss_file_path)
-
- if not tupian_url:
- # 上传失败,抛出异常触发回滚
- raise Exception("图片上传到OSS失败")
-
- # 4.3 更新商品类型的图片URL(存储相对路径)
- leixing_obj.tupian_url = oss_file_path # 存相对路径,方便拼接
- leixing_obj.save()
-
- except Exception as oss_error:
- # 图片上传失败,事务会回滚,商品类型记录也会被删除
- print(f"OSS上传失败: {oss_error}")
- # 手动抛出异常触发事务回滚
- raise Exception(f"图片上传失败: {str(oss_error)}")
-
- # === 5. 构建返回数据 ===
- response_data = {
- 'id': leixing_id,
- 'jieshao': jieshao,
- 'yaoqiuleixing': yaoqiuleixing_int,
- 'tupian_url': leixing_obj.tupian_url, # 相对路径
- 'huiyuan_id': huiyuan_id_value,
- 'yongjin': str(yongjin_value) if yongjin_value else None
- }
-
- # 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '商品类型添加成功',
- 'data': response_data
- })
-
- except Exception as e:
- # 捕获所有异常,返回500错误
- traceback.print_exc()
- return Response({
- 'code': 500,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class ShangpinZhuanquFabuView(APIView):
- """
- 商品专区发布接口
- 接收专区名称、类型ID
- 需要管理员权限
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- # === 1. 权限验证 ===
- user_main = request.user
-
- # 1.1 验证账号(使用phone字段)
- zhanghao = request.data.get('zhanghao', '').strip()
- if not zhanghao:
- return Response(
- {'code': 401, 'msg': '未提供账号信息', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if user_main.Phone != zhanghao:
- return Response(
- {'code': 401, 'msg': '账号验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.2 验证是否为管理员(检查user_type字段)
- if user_main.UserType != 'admin':
- return Response(
- {'code': 401, 'msg': '权限不足', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.3 验证管理员扩展表(使用AdminProfile模型)
- try:
- admin_profile = AdminProfile.query.get(user=user_main)
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'msg': '管理员信息不存在', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # === 2. 接收并验证字段 ===
- mingzi = request.data.get('mingzi', '').strip()
- leixing_id = request.data.get('leixing_id')
-
- # 2.1 验证必填字段
- if not mingzi:
- return Response(
- {'code': 400, 'msg': '专区名称不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- if not leixing_id:
- return Response(
- {'code': 400, 'msg': '类型ID不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2.2 验证类型ID是否存在
- try:
- # 验证商品类型是否存在
- leixing_obj = ShangpinLeixing.query.get(id=leixing_id)
- except ShangpinLeixing.DoesNotExist:
- return Response(
- {'code': 400, 'msg': '商品类型不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2.3 验证名称长度(15汉字以内)
- if len(mingzi) > 45: # 数据库字段最大长度
- return Response(
- {'code': 400, 'msg': '专区名称过长', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # === 3. 创建商品专区记录 ===
- zhuanqu_obj = ShangpinZhuanqu(
- mingzi=mingzi,
- leixing_id=leixing_id
- )
-
- zhuanqu_obj.save()
-
- # === 4. 构建返回数据 ===
- response_data = {
- 'id': zhuanqu_obj.id,
- 'mingzi': mingzi,
- 'leixing_id': leixing_id
- }
-
- # 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '商品专区添加成功',
- 'data': response_data
- })
-
- except Exception as e:
- traceback.print_exc()
- return Response({
- 'code': 500,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class ShangpinFabuView(APIView):
- """
- 商品发布接口
- 接收商品信息、图片等字段
- 需要管理员权限
- """
- permission_classes = [permissions.IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传
-
- def post(self, request):
- try:
- # === 1. 权限验证 ===
- user_main = request.user
-
- # 1.1 验证账号(使用phone字段)
- zhanghao = request.data.get('zhanghao', '').strip()
- if not zhanghao:
- return Response(
- {'code': 401, 'msg': '未提供账号信息', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if user_main.Phone != zhanghao:
- return Response(
- {'code': 401, 'msg': '账号验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.2 验证是否为管理员(检查user_type字段)
- if user_main.UserType != 'admin':
- return Response(
- {'code': 401, 'msg': '权限不足', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.3 验证管理员扩展表(使用AdminProfile模型)
- try:
- admin_profile = AdminProfile.query.get(user=user_main)
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'msg': '管理员信息不存在', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # === 2. 接收并验证字段 ===
- # 2.1 验证必填字段
- biaoti = request.data.get('biaoti', '').strip()
- jiage = request.data.get('jiage', '').strip()
- kucun = request.data.get('kucun', '').strip()
- leixing_id = request.data.get('leixing_id')
- zhuanqu_id = request.data.get('zhuanqu_id')
- yaoqiuleixing = request.data.get('yaoqiuleixing')
- jieshao = request.data.get('jieshao', '').strip()
-
- # 验证图片
- shangpin_tupian = request.FILES.get('shangpin_tupian')
- if not shangpin_tupian:
-
- return Response(
- {'code': 400, 'msg': '请上传商品图片', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证图片格式和大小
- is_valid, error_msg = validate_image(shangpin_tupian)
- if not is_valid:
- return Response(
- {'code': 400, 'msg': f'商品图片验证失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2.2 验证必填字段是否为空
- required_fields = {
- 'biaoti': biaoti,
- 'jiage': jiage,
- 'kucun': kucun,
- 'leixing_id': leixing_id,
- 'zhuanqu_id': zhuanqu_id,
- 'yaoqiuleixing': yaoqiuleixing,
- 'jieshao': jieshao
- }
-
- for field_name, field_value in required_fields.items():
- if not field_value:
- return Response(
- {'code': 400, 'msg': f'{field_name}不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2.3 验证数值字段
- try:
- jiage_value = float(jiage)
- if jiage_value <= 0:
- return Response(
- {'code': 400, 'msg': '商品价格必须大于0', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except ValueError:
- return Response(
- {'code': 400, 'msg': '商品价格格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- try:
- kucun_value = int(kucun)
- if kucun_value < 0:
- return Response(
- {'code': 400, 'msg': '库存不能为负数', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except ValueError:
- return Response(
- {'code': 400, 'msg': '库存必须为整数', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证抢单要求类型
- if yaoqiuleixing not in ['1', '2']:
- return Response(
- {'code': 400, 'msg': '抢单要求类型无效', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # === 3. 验证关联数据 ===
- # 3.1 验证商品类型
- try:
- leixing_obj = ShangpinLeixing.query.get(id=leixing_id)
- except ShangpinLeixing.DoesNotExist:
- return Response(
- {'code': 400, 'msg': '商品类型不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 3.2 根据抢单要求类型验证商品类型
- yaoqiuleixing_int = int(yaoqiuleixing)
-
- if yaoqiuleixing_int == 1: # 会员抢单
- # 需要验证商品类型的抢单要求类型是否为1
- if leixing_obj.yaoqiuleixing != 1:
- return Response(
- {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 获取会员ID(从前端或商品类型)
- huiyuan_id = request.data.get('huiyuan_id', leixing_obj.huiyuan_id)
- if not huiyuan_id:
- return Response(
- {'code': 400, 'msg': '会员ID不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证会员是否存在
- try:
- huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response(
- {'code': 400, 'msg': '会员不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 设置会员相关字段
- huiyuan_id_value = huiyuan_id
- yongjin_value = None
- else: # 佣金抢单
- # 需要验证商品类型的抢单要求类型是否为2
- if leixing_obj.yaoqiuleixing != 2:
- return Response(
- {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 获取佣金(从前端或商品类型)
- yongjin = request.data.get('yongjin', leixing_obj.yongjin)
- if not yongjin:
- return Response(
- {'code': 400, 'msg': '佣金金额不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- try:
- yongjin_value = float(yongjin)
- if yongjin_value <= 0:
- return Response(
- {'code': 400, 'msg': '佣金金额必须大于0', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except ValueError:
- return Response(
- {'code': 400, 'msg': '佣金金额格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- huiyuan_id_value = None
-
- # 3.3 验证商品专区
- try:
- zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id, leixing_id=leixing_id)
- except ShangpinZhuanqu.DoesNotExist:
- return Response(
- {'code': 400, 'msg': '商品专区不存在或与商品类型不匹配', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # === 4. 创建商品记录 ===
- with transaction.atomic():
- # 4.1 获取其他字段
- xuzhi = request.data.get('xuzhi', '').strip()
- xiaoliang = request.data.get('xiaoliang', '0').strip()
-
- # 转换销量为整数
- try:
- duiwai_xiaoliang_value = int(xiaoliang) if xiaoliang else 0
- except ValueError:
- duiwai_xiaoliang_value = 0
-
- # 4.2 创建商品对象
- shangpin_obj = Shangpin(
- biaoqian=biaoti,
- jiage=jiage_value,
- kucun=kucun_value,
- leixing_id=leixing_id,
- zhuanqu_id=zhuanqu_id,
- duiwai_xiaoliang=duiwai_xiaoliang_value,
- jieshao=jieshao,
- xiadan_xuzhi=xuzhi,
- yaoqiuleixing=yaoqiuleixing_int,
- huiyuan_id=huiyuan_id_value,
- yongjin=yongjin_value,
- # tupian_url 先留空,上传成功后再更新
- )
-
- # 保存到数据库获取ID
- shangpin_obj.save()
- shangpin_id = shangpin_obj.id
-
- # === 5. 处理商品图片上传 ===
- try:
- # 5.1 生成文件名和路径
- file_extension = os.path.splitext(shangpin_tupian.name)[1].lower()
- if not file_extension:
- file_extension = '.jpg'
-
- # 构建OSS文件路径:a_long/Spcc{shangpin_id}/{filename}
- oss_folder = 'a_long' # 根文件夹
- spcc_folder = f"Spcc{shangpin_id}" # 商品图片文件夹
-
- # 生成文件名:Spcc{shangpin_id}{file_extension}
- unique_filename = f"Spcc{shangpin_id}{file_extension}"
-
- # 构建完整路径
- oss_file_path = f"{oss_folder}/{spcc_folder}/{unique_filename}"
-
- # 5.2 上传到OSS
- tupian_url = upload_to_oss(shangpin_tupian, oss_file_path)
-
- if not tupian_url:
- # 上传失败,抛出异常触发回滚
- raise Exception("商品图片上传到OSS失败")
-
- # 5.3 更新商品的图片URL(存储相对路径)
- shangpin_obj.tupian_url = oss_file_path # 存相对路径
- shangpin_obj.save()
-
- except Exception as oss_error:
- # 图片上传失败,事务会回滚,商品记录也会被删除
- #print(f"商品图片OSS上传失败: {oss_error}")
- # 手动抛出异常触发事务回滚
- raise Exception(f"商品图片上传失败: {str(oss_error)}")
-
- # === 6. 构建返回数据 ===
- response_data = {
- 'shangpin_id': shangpin_id,
- 'biaoti': biaoti,
- 'jiage': str(jiage_value),
- 'kucun': kucun_value,
- 'leixing_id': leixing_id,
- 'zhuanqu_id': zhuanqu_id,
- 'xiaoliang': duiwai_xiaoliang_value,
- 'jieshao': jieshao,
- 'xuzhi': xuzhi,
- 'yaoqiuleixing': yaoqiuleixing_int,
- 'huiyuan_id': huiyuan_id_value,
- 'yongjin': str(yongjin_value) if yongjin_value else None,
- 'tupian_url': shangpin_obj.tupian_url # 相对路径
- }
-
- # 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '商品发布成功',
- 'data': response_data
- })
-
- except Exception as e:
- # 捕获所有异常,返回500错误
- traceback.print_exc()
- return Response({
- 'code': 500,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class GuizeTupianShangchuanView(APIView):
- """
- 规则图片上传接口
- 接收商品ID和规则图片
- 需要管理员权限
- """
- permission_classes = [permissions.IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传
-
- def post(self, request):
- try:
- # === 1. 权限验证 ===
- user_main = request.user
-
- # 1.1 验证账号(使用phone字段)
- zhanghao = request.data.get('zhanghao', '').strip()
- if not zhanghao:
- return Response(
- {'code': 401, 'msg': '未提供账号信息', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if user_main.Phone != zhanghao:
- return Response(
- {'code': 401, 'msg': '账号验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.2 验证是否为管理员(检查user_type字段)
- if user_main.UserType != 'admin':
- return Response(
- {'code': 401, 'msg': '权限不足', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.3 验证管理员扩展表(使用AdminProfile模型)
- try:
- admin_profile = AdminProfile.query.get(user=user_main)
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'msg': '管理员信息不存在', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # === 2. 接收并验证字段 ===
- # 2.1 验证商品ID
- shangpin_id = request.data.get('shangpin_id')
- if not shangpin_id:
- return Response(
- {'code': 400, 'msg': '商品ID不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2.2 验证规则图片
- guize_tupian = request.FILES.get('guize_tupian')
- if not guize_tupian:
- return Response(
- {'code': 400, 'msg': '请上传规则图片', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证图片格式和大小
- is_valid, error_msg = validate_image(guize_tupian)
- if not is_valid:
- return Response(
- {'code': 400, 'msg': f'规则图片验证失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # === 3. 验证商品是否存在 ===
- try:
- shangpin_obj = Shangpin.query.get(id=shangpin_id)
- except Shangpin.DoesNotExist:
- return Response(
- {'code': 400, 'msg': '商品不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # === 4. 处理规则图片上传 ===
- try:
- # 4.1 生成文件名和路径
- file_extension = os.path.splitext(guize_tupian.name)[1].lower()
- if not file_extension:
- file_extension = '.jpg'
-
- # 构建OSS文件路径:a_long/Spgz{shangpin_id}/{filename}
- oss_folder = 'a_long' # 根文件夹
- spgz_folder = f"Spgz{shangpin_id}" # 规则图片文件夹
-
- # 生成文件名:Spgz{shangpin_id}{file_extension}
- unique_filename = f"Spgz{shangpin_id}{file_extension}"
-
- # 构建完整路径
- oss_file_path = f"{oss_folder}/{spgz_folder}/{unique_filename}"
-
- # 4.2 上传到OSS
- tupian_url = upload_to_oss(guize_tupian, oss_file_path)
-
- if not tupian_url:
- # 上传失败
- return Response({
- 'code': 500,
- 'msg': '规则图片上传失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 4.3 更新商品的规则图片URL(存储相对路径)
- shangpin_obj.guize_tupian = oss_file_path # 存相对路径
- shangpin_obj.save()
-
- except Exception as oss_error:
- # OSS上传失败
- print(f"规则图片OSS上传失败: {oss_error}")
- return Response({
- 'code': 500,
- 'msg': f'规则图片上传失败: {str(oss_error)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # === 5. 构建返回数据 ===
- response_data = {
- 'shangpin_id': shangpin_id,
- 'guize_tupian_url': oss_file_path # 相对路径
- }
-
- # 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '规则图片上传成功',
- 'data': response_data
- })
-
- except Exception as e:
- # 捕获所有异常,返回500错误
- traceback.print_exc()
- return Response({
- 'code': 500,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-class AdHuiyuanHqView(APIView):
- """
- 管理员获取会员信息和商品类型接口
- 需要管理员权限
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- # === 1. 权限验证 ===
- # 获取当前用户
- user_main = request.user
-
- # 1.1 验证前端传递的账号是否匹配(使用phone字段)
- zhanghao = request.data.get('zhanghao', '').strip()
- if not zhanghao:
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 验证账号是否匹配(使用phone字段比较)
- if user_main.Phone != zhanghao:
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.2 验证是否为管理员(检查user_type字段)
- if user_main.UserType != 'admin':
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.3 验证管理员扩展表(使用AdminProfile模型)
- try:
- admin_profile = AdminProfile.query.get(user=user_main)
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # === 2. 查询会员表数据 ===
- # 查询所有会员数据,按创建时间倒序排列
- huiyuan_queryset = Huiyuan.query.all().order_by('-CreateTime')
-
- # 构建会员数据列表
- huiyuan_list = []
- for huiyuan in huiyuan_queryset:
- huiyuan_data = {
- 'huiyuan_id': huiyuan.huiyuan_id, # 会员ID
- 'jieshao': huiyuan.jieshao, # 会员介绍(会员标题)
- 'jtjieshao': huiyuan.jtjieshao if huiyuan.jtjieshao else '', # 会员规则介绍(具体介绍)
- 'jiage': str(huiyuan.jiage), # 会员价格(转换为字符串,避免Decimal序列化问题)
- 'guanshifc': str(huiyuan.guanshifc), # 管事分成(转换为字符串)
- 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数(销量)
- 'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None,
- # 创建时间
- 'UpdateTime': huiyuan.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.UpdateTime else None
- # 更新时间
- }
- huiyuan_list.append(huiyuan_data)
-
- # === 3. 查询商品类型表数据 ===
- # 筛选抢单要求类型等于数字1的所有数据(yaoqiuleixing=1)
- shangpinleixing_queryset = ShangpinLeixing.query.filter(
- yaoqiuleixing=1 # 抢单要求类型为1(会员抢单)
- ).order_by('id') # 按ID排序
-
- # 构建商品类型数据列表
- shangpinleixing_list = []
- for leixing in shangpinleixing_queryset:
- # 注意:这里只需要特定的字段
- leixing_data = {
- 'id': leixing.id, # 商品类型ID
- 'huiyuan_id': leixing.huiyuan_id, # 会员ID(关联字段)
- 'jieshao': leixing.jieshao, # 类型介绍(商品类型名称)
- 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' # 商品类型图片相对URL
- }
- shangpinleixing_list.append(leixing_data)
-
- # === 4. 构建返回数据 ===
- response_data = {
- 'huiyuanList': huiyuan_list,
- 'shangpinleixingList': shangpinleixing_list
- }
-
- # 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '获取数据成功',
- 'data': response_data
- })
-
- except Exception as e:
- # 捕获所有异常,返回500错误
- traceback.print_exc()
- return Response({
- 'code': 500,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-class HuiyuanXiugaiView(APIView):
- """
- 会员修改接口
- 需要管理员权限
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- # === 1. 权限验证 ===
- user_main = request.user
-
- # 1.1 验证前端传递的账号是否匹配(使用phone字段)
- zhanghao = request.data.get('zhanghao', '').strip()
- if not zhanghao:
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if user_main.Phone != zhanghao:
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.2 验证是否为管理员(检查user_type字段)
- if user_main.UserType != 'admin':
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.3 验证管理员扩展表
- try:
- admin_profile = AdminProfile.query.get(user=user_main)
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # === 2. 接收并验证必填字段 ===
- huiyuan_id = request.data.get('huiyuan_id', '').strip()
- if not huiyuan_id:
- return Response(
- {'code': 400, 'msg': '会员ID不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # === 3. 查询会员数据 ===
- try:
- huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response(
- {'code': 400, 'msg': '会员不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # === 4. 更新会员信息 ===
- # 4.1 获取前端传递的字段(只更新有值的字段)
- jieshao = request.data.get('jieshao', '').strip()
- jtjieshao = request.data.get('jtjieshao', '').strip()
- jiage = request.data.get('jiage', '').strip()
- guanshifc = request.data.get('guanshifc', '').strip()
-
- # 4.2 更新字段
- if jieshao:
- # 验证名称长度(30个字符以内)
- if len(jieshao) > 30:
- return Response(
- {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- huiyuan_obj.jieshao = jieshao
-
- if jtjieshao:
- # 验证介绍长度(没有长度限制,但做一下基本检查)
- if len(jtjieshao) > 2000:
- return Response(
- {'code': 400, 'msg': '会员介绍过长', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- huiyuan_obj.jtjieshao = jtjieshao
-
- if jiage:
- try:
- jiage_value = float(jiage)
- if jiage_value < 0:
- return Response(
- {'code': 400, 'msg': '会员价格不能为负数', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- if jiage_value > 100000:
- return Response(
- {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- huiyuan_obj.jiage = jiage_value
- except ValueError:
- return Response(
- {'code': 400, 'msg': '会员价格格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- if guanshifc:
- try:
- guanshifc_value = float(guanshifc)
- if guanshifc_value < 0:
- return Response(
- {'code': 400, 'msg': '管事分成不能为负数', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- # 检查管事分成是否超过会员价格
- if hasattr(huiyuan_obj, 'jiage') and huiyuan_obj.jiage:
- if guanshifc_value > float(huiyuan_obj.jiage):
- return Response(
- {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- huiyuan_obj.guanshifc = guanshifc_value
- except ValueError:
- return Response(
- {'code': 400, 'msg': '管事分成格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 4.3 检查是否有任何字段需要更新
- if not (jieshao or jtjieshao or jiage or guanshifc):
- return Response(
- {'code': 400, 'msg': '没有需要更新的字段', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 4.4 保存更新
- huiyuan_obj.save()
-
- # === 5. 构建返回数据 ===
- response_data = {
- 'huiyuan_id': huiyuan_obj.huiyuan_id,
- 'jieshao': huiyuan_obj.jieshao,
- 'jtjieshao': huiyuan_obj.jtjieshao if huiyuan_obj.jtjieshao else '',
- 'jiage': str(huiyuan_obj.jiage),
- 'guanshifc': str(huiyuan_obj.guanshifc),
- 'goumai_cishu': huiyuan_obj.goumai_cishu,
- 'CreateTime': huiyuan_obj.CreateTime.strftime(
- '%Y-%m-%d %H:%M:%S') if huiyuan_obj.CreateTime else None,
- 'UpdateTime': huiyuan_obj.UpdateTime.strftime(
- '%Y-%m-%d %H:%M:%S') if huiyuan_obj.UpdateTime else None
- }
-
- # 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '会员信息修改成功',
- 'data': response_data
- })
-
- except Exception as e:
- # 捕获所有异常,返回500错误
- traceback.print_exc()
- return Response({
- 'code': 500,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-
-
-class AdHuiyuanFbView(APIView):
- """
- 会员发布接口
- 需要管理员权限
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- # === 1. 权限验证 ===
- user_main = request.user
-
- # 1.1 验证前端传递的账号是否匹配
- zhanghao = request.data.get('zhanghao', '').strip()
- if not zhanghao:
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if user_main.Phone != zhanghao:
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.2 验证是否为管理员
- if user_main.UserType != 'admin':
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 1.3 验证管理员扩展表
- try:
- admin_profile = AdminProfile.query.get(user=user_main)
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'msg': '权限验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # === 2. 接收并验证字段 ===
- jieshao = request.data.get('jieshao', '').strip()
- jtjieshao = request.data.get('jtjieshao', '').strip()
- jiage = request.data.get('jiage', '').strip()
- guanshifc = request.data.get('guanshifc', '').strip()
-
- # 2.1 验证必填字段
- if not jieshao:
- return Response(
- {'code': 400, 'msg': '会员名称不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- if not jiage:
- return Response(
- {'code': 400, 'msg': '会员价格不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- if not guanshifc:
- return Response(
- {'code': 400, 'msg': '管事分成不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2.2 验证字段长度和格式
- if len(jieshao) > 30:
- return Response(
- {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- if jtjieshao and len(jtjieshao) > 2000:
- return Response(
- {'code': 400, 'msg': '会员介绍过长', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2.3 验证价格和分成格式
- try:
- jiage_value = float(jiage)
- if jiage_value <= 0:
- return Response(
- {'code': 400, 'msg': '会员价格必须大于0', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- if jiage_value > 100000:
- return Response(
- {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except ValueError:
- return Response(
- {'code': 400, 'msg': '会员价格格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- try:
- guanshifc_value = float(guanshifc)
- if guanshifc_value < 0:
- return Response(
- {'code': 400, 'msg': '管事分成不能为负数', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- if guanshifc_value > jiage_value:
- return Response(
- {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except ValueError:
- return Response(
- {'code': 400, 'msg': '管事分成格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # === 3. 生成会员ID ===
- # 生成6位唯一会员ID,格式:V + 5位随机数字字母
- def generate_huiyuan_id():
- # 使用V开头,后跟5位随机字符(数字和大写字母)
- chars = string.digits + string.ascii_uppercase
- while True:
- random_part = ''.join(random.choices(chars, k=5))
- huiyuan_id = f"V{random_part}"
- # 检查是否已存在
- if not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists():
- return huiyuan_id
-
- huiyuan_id = generate_huiyuan_id()
-
- # === 4. 创建会员记录 ===
- with transaction.atomic():
- huiyuan_obj = Huiyuan(
- huiyuan_id=huiyuan_id,
- jieshao=jieshao,
- jtjieshao=jtjieshao if jtjieshao else '',
- jiage=jiage_value,
- guanshifc=guanshifc_value,
- goumai_cishu=0, # 初始购买次数为0
- )
-
- huiyuan_obj.save()
-
- # === 5. 构建返回数据 ===
- response_data = {
- 'huiyuan_id': huiyuan_obj.huiyuan_id,
- 'jieshao': huiyuan_obj.jieshao,
- 'jtjieshao': huiyuan_obj.jtjieshao,
- 'jiage': str(huiyuan_obj.jiage),
- 'guanshifc': str(huiyuan_obj.guanshifc),
- 'goumai_cishu': huiyuan_obj.goumai_cishu,
- 'CreateTime': huiyuan_obj.CreateTime.strftime(
- '%Y-%m-%d %H:%M:%S') if huiyuan_obj.CreateTime else None,
- 'UpdateTime': huiyuan_obj.UpdateTime.strftime(
- '%Y-%m-%d %H:%M:%S') if huiyuan_obj.UpdateTime else None
- }
-
- # 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '会员发布成功',
- 'data': response_data
- })
-
- except Exception as e:
- # 捕获所有异常,返回500错误
- traceback.print_exc()
- return Response({
- 'code': 500,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-class AdShangpinHuQu(APIView):
- """
- 后台商品管理页面数据获取接口
- 路径: /shangpin/adsphq/
- 方法: POST
- 权限: JWT认证 + 管理员权限
- 功能: 获取商品管理页面需要的所有数据
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取前端传递的账号
- qianzhanghao = request.data.get('zhanghao', '').strip()
- if not qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 2. 从JWT Token获取当前用户
- dangqianyonghu = request.user
-
- # 3. 验证账号匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表数据
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 查询公告与轮播(按俱乐部,默认抢单池)
- from jituan.services.display_config import get_gonggao_content, get_lunbo_urls
- shangpingonggao = get_gonggao_content(request, notice_type=1)
- shangpinlunbo = get_lunbo_urls(request, page_key='order_pool', image_type=1)
-
- # 8. 查询商品类型数据
- shangpinleixing_data = ShangpinLeixing.query.all().values(
- 'id', # 类型ID
- 'jieshao', # 类型介绍(类型名称)
- 'tupian_url', # 类型图片URL
- 'yaoqiuleixing', # 抢单要求类型(1=会员抢单,2=佣金抢单)
- 'huiyuan_id', # 会员ID(如果yaoqiuleixing=1)
- 'yongjin' # 佣金要求(如果yaoqiuleixing=2)
- )
-
- # 9. 查询商品专区数据
- shangpinzhuanqu_data = ShangpinZhuanqu.query.all().values(
- 'id', # 专区ID
- 'mingzi', # 专区名称
- 'leixing_id' # 所属类型ID
- )
-
- # 10. 查询会员数据
- huiyuan_data = Huiyuan.query.all().values(
- 'huiyuan_id', # 会员ID
- 'jieshao', # 会员介绍(会员名称)
- 'jiage' # 会员价格
- )
-
- # 11. 查询商品数据
- shangpin_data = Shangpin.query.all().values(
- 'id', # 商品ID
- 'biaoqian', # 商品标题/标签
- 'jiage', # 商品价格
- 'leixing_id', # 商品类型ID
- 'zhuanqu_id', # 商品专区ID
- 'tupian_url', # 商品图片URL
- 'duiwai_xiaoliang' # 商品对外销量
- )
-
- # 12. 构造返回数据
- response_data = {
- 'code': 0,
- 'message': '获取成功',
- 'data': {
- 'shangpingonggao': shangpingonggao, # 商品公告
- 'shangpinlunbo': shangpinlunbo, # 轮播图相对URL数组
- 'shangpinleixing': list(shangpinleixing_data), # 商品类型列表
- 'shangpinzhuanqu': list(shangpinzhuanqu_data), # 商品专区列表
- 'huiyuanliebiao': list(huiyuan_data), # 会员列表
- 'shangpinliebiao': list(shangpin_data), # 商品列表
- }
- }
-
- return Response(response_data, status=status.HTTP_200_OK)
-
- except Exception as e:
- print(f"获取商品管理数据异常: {e}")
- return Response(
- {'code': 500, 'message': '服务器内部错误'},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-# shangpin/views.py
-
-class AdGonggaoXiuGai(APIView):
- """
- 后台编辑公告接口
- 路径: /shangpin/adggxg/
- 方法: POST
- 权限: JWT认证 + 管理员权限
- 功能: 修改公告内容
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取前端传递的账号
- qianzhanghao = request.data.get('zhanghao', '').strip()
- if not qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 2. 从JWT Token获取当前用户
- dangqianyonghu = request.user
-
- # 3. 验证账号匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表数据
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 获取公告内容
- gonggao_neirong = request.data.get('gonggao', '').strip()
-
- # 7. 验证公告内容
- if not gonggao_neirong:
- return Response(
- {'code': 400, 'message': '公告内容不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- if len(gonggao_neirong) > 300:
- return Response(
- {'code': 400, 'message': '公告内容不能超过300字', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 8. 查询公告表,类型为1的公告
- gonggao_obj = Gonggao.query.filter(NoticeType=1).first()
-
- # 如果不存在,则创建
- if not gonggao_obj:
- gonggao_obj = Gonggao(NoticeType=1)
-
- # 9. 更新公告内容
- gonggao_obj.Content = gonggao_neirong
- gonggao_obj.save()
-
- # 10. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '公告更新成功',
- 'data': {
- 'gonggao': gonggao_neirong
- }
- })
-
- except Exception as e:
- print(f"编辑公告异常: {e}")
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-class AdZhuanquXiuGai(APIView):
- """
- 后台修改专区名称接口
- 路径: /shangpin/adzqxg/
- 方法: POST
- 权限: JWT认证 + 管理员权限
- 功能: 修改专区名称
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 权限验证
- qianzhanghao = request.data.get('zhanghao', '').strip()
- if not qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- dangqianyonghu = request.user
-
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 2. 获取专区ID和新的专区名称
- zhuanqu_id = request.data.get('zhuanqu_id')
- mingzi = request.data.get('mingzi', '').strip()
-
- if not zhuanqu_id:
- return Response(
- {'code': 400, 'message': '专区ID不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- if not mingzi:
- return Response(
- {'code': 400, 'message': '专区名称不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- if len(mingzi) > 10:
- return Response(
- {'code': 400, 'message': '专区名称不能大于10个字', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 3. 查询专区
- try:
- zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id)
- except ShangpinZhuanqu.DoesNotExist:
- return Response(
- {'code': 400, 'message': '专区不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 4. 更新专区名称
- zhuanqu_obj.mingzi = mingzi
- zhuanqu_obj.save()
-
- # 5. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '专区名称修改成功',
- 'data': {
- 'id': zhuanqu_obj.id,
- 'mingzi': zhuanqu_obj.mingzi,
- 'leixing_id': zhuanqu_obj.leixing_id
- }
- })
-
- except Exception as e:
- print(f"修改专区名称异常: {e}")
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-class AdLunboXiuGai(APIView):
- """
- 后台编辑轮播图接口
- 路径: /shangpin/adlunboxg/
- 方法: POST
- 权限: JWT认证 + 管理员权限
- 功能: 添加或删除轮播图
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser,JSONParser)
-
- def post(self, request):
- try:
- # 1. 权限验证
- qianzhanghao = request.data.get('zhanghao', '').strip()
- if not qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- dangqianyonghu = request.user
-
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'message': '身份验证失败'},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 2. 获取操作类型
- action = request.data.get('action', '').strip() # 'add' 或 'delete'
-
- if action not in ['add', 'delete']:
- return Response(
- {'code': 400, 'message': '操作类型无效', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 3. 处理删除操作
- if action == 'delete':
- tupian_url = request.data.get('tupian_url', '').strip()
-
- if not tupian_url:
- return Response(
- {'code': 400, 'message': '图片URL不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 查询轮播图记录
- lunbo_obj = Lunbo.query.filter(ImageURL=tupian_url, ImageType=1).first()
-
- if not lunbo_obj:
- return Response(
- {'code': 400, 'message': '轮播图不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 从OSS删除图片
- delete_success = delete_from_oss(tupian_url)
-
- if not delete_success:
- return Response(
- {'code': 500, 'message': 'OSS删除失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 从数据库删除记录
- lunbo_obj.delete()
-
- return Response({
- 'code': 0,
- 'message': '删除成功',
- 'data': None
- })
-
- # 4. 处理添加操作
- else: # action == 'add'
- tupian_file = request.FILES.get('file')
-
- if not tupian_file:
-
-
- return Response(
- {'code': 400, 'message': '请上传图片', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证图片
- is_valid, error_msg = validate_image(tupian_file)
- if not is_valid:
- #print(f"[轮播图上传] 图片验证失败详情: {error_msg}")
- # 还可以记录文件大小、名称等信息
- #print(f"[轮播图上传] 问题文件: {tupian_file.name}, Size: {tupian_file.size}")
- #print(is_valid)
- return Response(
- {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 生成文件名和路径
- file_extension = os.path.splitext(tupian_file.name)[1].lower()
- if not file_extension:
- file_extension = '.jpg'
-
- # 生成6位随机字符串作为文件名
- random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
- unique_filename = f"{random_str}{file_extension}"
-
- # 构建OSS文件路径:a_long/lunbo/{filename}
- oss_folder = 'a_long'
- lunbo_folder = 'lunbo'
-
- oss_file_path = f"{oss_folder}/{lunbo_folder}/{unique_filename}"
-
- # 上传到OSS
- tupian_url = upload_to_oss(tupian_file, oss_file_path)
-
- if not tupian_url:
- return Response(
- {'code': 500, 'message': '图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 创建轮播图记录
- lunbo_obj = Lunbo.query.create(
- ImageURL=oss_file_path, # 存相对路径
- ImageType=1
- )
-
- return Response({
- 'code': 0,
- 'message': '添加成功',
- 'data': {
- 'tupian_url': lunbo_obj.ImageURL
- }
- })
-
- except Exception as e:
- #print(f"编辑轮播图异常: {e}")
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-
-class AdLeixingXiuGai(APIView):
- """
- 后台编辑商品类型接口 - 优化版
- 支持:只改图片、只改字段、两者都改
- 拒绝:两者都不改
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser, JSONParser) # 添加JSONParser支持
-
- def post(self, request):
- try:
- # 1. 权限验证(保持不变)
- qianzhanghao = request.data.get('zhanghao', '').strip()
- if not qianzhanghao:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- dangqianyonghu = request.user
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if dangqianyonghu.UserType != 'admin':
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AdminProfile.DoesNotExist:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 获取商品类型ID
- leixing_id = request.data.get('leixing_id')
- if not leixing_id:
- return Response({'code': 400, 'message': '商品类型ID不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 查询商品类型
- try:
- leixing_obj = ShangpinLeixing.query.get(id=leixing_id)
- except ShangpinLeixing.DoesNotExist:
- return Response({'code': 400, 'message': '商品类型不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 获取字段并初始化变更标记
- has_changed = False
- jieshao = request.data.get('jieshao', '').strip()
- yaoqiuleixing = request.data.get('yaoqiuleixing')
- huiyuan_id = request.data.get('huiyuan_id', '').strip()
- yongjin = request.data.get('yongjin', '').strip()
- tupian_url = request.data.get('tupian_url', '').strip() # 原有图片URL
-
- # 5. 检查非图片字段是否有变更
- if jieshao and jieshao != leixing_obj.jieshao:
- leixing_obj.jieshao = jieshao
- has_changed = True
-
- if yaoqiuleixing and int(yaoqiuleixing) != leixing_obj.yaoqiuleixing:
- leixing_obj.yaoqiuleixing = int(yaoqiuleixing)
- has_changed = True
-
- # 根据类型清空另一个字段
- if leixing_obj.yaoqiuleixing == 1:
- leixing_obj.yongjin = None
- if huiyuan_id:
- leixing_obj.huiyuan_id = huiyuan_id
- # 验证会员存在
- try:
- Huiyuan.query.get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response({'code': 400, 'msg': '会员ID不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
- else: # 类型2
- leixing_obj.huiyuan_id = None
- if yongjin:
- try:
- leixing_obj.yongjin = float(yongjin)
- if leixing_obj.yongjin <= 0:
- return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
- except ValueError:
- return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # ===== 在现有代码的第5步后面添加 =====
-
- # 5.5 检查抢单要求类型不变时,会员ID或佣金是否有变更
- if yaoqiuleixing and int(yaoqiuleixing) == leixing_obj.yaoqiuleixing:
- # 类型没有变化,但需要检查对应字段的变化
-
- # 情况1:会员抢单 (yaoqiuleixing == 1)
- if leixing_obj.yaoqiuleixing == 1:
- # 检查会员ID是否变更
- if huiyuan_id and huiyuan_id != leixing_obj.huiyuan_id:
- # 验证会员存在
- try:
- Huiyuan.query.get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response({'code': 400, 'msg': '会员ID不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- leixing_obj.huiyuan_id = huiyuan_id
- has_changed = True
- # print(f"会员ID变更: {leixing_obj.huiyuan_id} -> {huiyuan_id}")
-
- # 情况2:佣金抢单 (yaoqiuleixing == 2)
- elif leixing_obj.yaoqiuleixing == 2:
- # 检查佣金是否变更
- if yongjin:
- try:
- new_yongjin = float(yongjin)
- if new_yongjin <= 0:
- return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 判断是否有变化(考虑None的情况)
- current_yongjin = leixing_obj.yongjin
- if (current_yongjin is None and new_yongjin != 0) or \
- (current_yongjin is not None and abs(
- float(current_yongjin) - new_yongjin) > 0.001):
- leixing_obj.yongjin = new_yongjin
- has_changed = True
- # print(f"佣金变更: {current_yongjin} -> {new_yongjin}")
-
- except ValueError:
- return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # ===== 添加结束 =====
-
- # 6. 处理图片上传(如果有新图片)
- tupian_file = request.FILES.get('file')
- if tupian_file:
- # 验证图片
- is_valid, error_msg = validate_image(tupian_file)
- if not is_valid:
- return Response({'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 删除旧图片(如果存在)
- if leixing_obj.tupian_url:
- delete_from_oss(leixing_obj.tupian_url)
-
- # 生成文件名和路径
- file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg'
- oss_folder = 'a_long'
- leixing_folder = 'leixing'
-
- # 使用原文件名或生成新文件名
- if leixing_obj.tupian_url:
- original_filename = leixing_obj.tupian_url.split('/')[-1]
- else:
- original_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}"
-
- oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{original_filename}"
-
- # 上传到OSS
- new_tupian_url = upload_to_oss(tupian_file, oss_file_path)
- if not new_tupian_url:
- return Response({'code': 500, 'message': '图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- leixing_obj.tupian_url = oss_file_path # 存相对路径
- has_changed = True
- elif tupian_url and tupian_url != leixing_obj.tupian_url:
- # 前端传了新URL(可能是从其他地方选择的)
- leixing_obj.tupian_url = tupian_url
- has_changed = True
-
- # 7. 验证是否至少有变更
- if not has_changed:
- return Response({
- 'code': 400,
- 'message': '没有检测到任何更改',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 8. 保存并返回
- leixing_obj.save()
- # 8. 保存商品类型
- leixing_obj.save()
-
- # 🔴 ===========【新增:同步更新关联商品】===========
- try:
- # 1. 准备商品更新字典
- shangpin_update_fields = {}
-
- # 始终同步抢单要求类型
- shangpin_update_fields['yaoqiuleixing'] = leixing_obj.yaoqiuleixing
-
- # 根据抢单类型,同步对应字段并清空另一个
- if leixing_obj.yaoqiuleixing == 1: # 会员抢单
- if leixing_obj.huiyuan_id: # 仅当会员ID非空时更新
- shangpin_update_fields['huiyuan_id'] = leixing_obj.huiyuan_id
- # 清空佣金字段
- shangpin_update_fields['yongjin'] = None
- elif leixing_obj.yaoqiuleixing == 2: # 佣金抢单
- if leixing_obj.yongjin: # 仅当佣金非空时更新
- shangpin_update_fields['yongjin'] = leixing_obj.yongjin
- # 清空会员字段
- shangpin_update_fields['huiyuan_id'] = None
-
- # 2. 执行批量更新(确保只有实际有变化的字段才会执行)
- if shangpin_update_fields:
- # 查询所有属于该类型的商品(使用filter而非all以提高效率)
- affected_count = Shangpin.query.filter(
- leixing_id=leixing_obj.id
- ).update(**shangpin_update_fields)
-
- # 可选:记录日志以便追踪
- # logger.info(f"商品类型{leixing_obj.id}修改成功,已同步更新{affected_count}个关联商品")
-
- except Exception as sync_error:
- # 商品同步失败不影响商品类型本身的修改,但记录错误
- # 可根据需求决定是否抛出异常回滚整个事务
- # 当前设计:记录错误但允许商品类型修改成功
- logger.error(f"修改十五")
-
- # 可在此添加更详细的错误日志记录
- # 🔴 ===========【新增结束】===========
-
-
-
-
-
-
-
- response_data = {
- 'id': leixing_obj.id,
- 'jieshao': leixing_obj.jieshao,
- 'tupian_url': leixing_obj.tupian_url,
- 'yaoqiuleixing': leixing_obj.yaoqiuleixing,
- 'huiyuan_id': leixing_obj.huiyuan_id,
- 'yongjin': str(leixing_obj.yongjin) if leixing_obj.yongjin else None
- }
-
- return Response({
- 'code': 0,
- 'message': '商品类型修改成功',
- 'data': response_data
- })
-
- except Exception as e:
- print(f"编辑商品类型异常: {e}")
- return Response({'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-logger = logging.getLogger(__name__)
-
-
-class AdShangpinNeiRongHuoQu(APIView):
- """
- 后台获取商品详情接口
- 路径: /shangpin/adspnrhq/
- 方法: POST
- 权限: JWT认证 + 管理员权限
- 功能: 获取指定商品的详细信息
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取前端数据
- qianzhanghao = request.data.get('zhanghao', '').strip()
- shangpin_id = request.data.get('shangpin_id')
-
- if not qianzhanghao or not shangpin_id:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. 权限验证
- dangqianyonghu = request.user
-
- # 验证phone字段匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- logger.warning(
- f"账号不匹配: 当前用户phone={getattr(dangqianyonghu, 'Phone', '无')}, 前端账号={qianzhanghao}")
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 验证用户类型
- if dangqianyonghu.UserType != 'admin':
- logger.warning(f"非管理员访问: user_type={dangqianyonghu.UserType}")
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 3. 验证管理员扩展表
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AdminProfile.DoesNotExist:
- logger.warning(f"管理员扩展表不存在: user_id={dangqianyonghu.id}")
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 查询商品数据
- try:
- shangpin_obj = Shangpin.query.get(id=shangpin_id)
- except Shangpin.DoesNotExist:
- return Response(
- {'code': 404, 'message': '商品不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 5. 构建返回数据(与前端字段对应)
- response_data = {
- 'tupian_url': shangpin_obj.tupian_url or '', # 商品图片相对URL
- 'biaoti': shangpin_obj.biaoqian or '', # 商品标题(前端用biaoti,对应biaoqian)
- 'jieshao': shangpin_obj.jieshao or '', # 商品介绍
- 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', # 商品价格
- 'kucun': shangpin_obj.kucun, # 商品库存
- 'zhenshixiaoliang': shangpin_obj.zhenshi_xiaoliang, # 商品真实销量
- 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, # 商品对外销量
- 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', # 下单须知
- 'guizetupian_url': shangpin_obj.guize_tupian or '', # 规则图片相对URL
- 'yajin': str(shangpin_obj.yajin) if hasattr(shangpin_obj, 'yajin') and shangpin_obj.yajin else None,
- # 押金(可能不存在)
- 'chuangjianshijian': shangpin_obj.CreateTime.strftime(
- '%Y-%m-%d %H:%M:%S') if shangpin_obj.CreateTime else '',
- 'genggaishijian': shangpin_obj.UpdateTime.strftime(
- '%Y-%m-%d %H:%M:%S') if shangpin_obj.UpdateTime else '',
- # 额外字段,前端可能不需要但可以保留
- 'leixing_id': shangpin_obj.leixing_id,
- 'zhuanqu_id': shangpin_obj.zhuanqu_id,
- 'yaoqiuleixing': shangpin_obj.yaoqiuleixing,
- 'huiyuan_id': shangpin_obj.huiyuan_id,
- 'yongjin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None,
- }
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': response_data
- })
-
- except Exception as e:
- logger.error(f"获取商品详情异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-
-
-
-class AdShangpinXiuGai(APIView):
- """
- 后台更改商品数据接口 - 修复版
- 修复:1. 每次上传生成新文件名 2. 解决浏览器缓存问题
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser, JSONParser)
-
- def post(self, request):
- try:
- # 1. 权限验证
- qianzhanghao = request.data.get('zhanghao', '').strip()
- shangpin_id = request.data.get('shangpin_id')
-
- if not qianzhanghao or not shangpin_id:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证身份
- dangqianyonghu = request.user
-
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 2. 判断操作类型:删除还是修改
- action = request.data.get('action', 'update').strip().lower()
-
- if action == 'delete':
- return self._delete_shangpin(shangpin_id)
- else:
- return self._update_shangpin(request, shangpin_id)
-
- except Exception as e:
- logger.error(f"更改商品数据异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def _delete_shangpin(self, shangpin_id):
- """删除商品逻辑"""
- try:
- # 查询商品
- try:
- shangpin_obj = Shangpin.query.get(id=shangpin_id)
- except Shangpin.DoesNotExist:
- return Response(
- {'code': 404, 'message': '商品不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 获取图片URL(如果有)
- tupian_url = shangpin_obj.tupian_url
- guizetupian_url = shangpin_obj.guize_tupian
-
- # 异步或同步删除OSS图片
- if tupian_url:
- try:
- delete_from_oss(tupian_url)
- except Exception as e:
- logger.warning(f"删除商品图片失败: {tupian_url}, 错误: {e}")
-
- if guizetupian_url:
- try:
- delete_from_oss(guizetupian_url)
- except Exception as e:
- logger.warning(f"删除规则图片失败: {guizetupian_url}, 错误: {e}")
-
- # 删除数据库记录
- shangpin_obj.delete()
-
- return Response({
- 'code': 0,
- 'message': '删除成功',
- 'data': {
- 'shangpin_id': shangpin_id,
- 'deleted': True
- }
- })
-
- except Exception as e:
- logger.error(f"删除商品异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '删除失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def _update_shangpin(self, request, shangpin_id):
- """修改商品逻辑 - 修复:每次上传生成新文件名"""
- try:
- # 查询商品
- try:
- shangpin_obj = Shangpin.query.get(id=shangpin_id)
- except Shangpin.DoesNotExist:
- return Response(
- {'code': 404, 'message': '商品不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- has_changes = False
-
- # 获取原图片URL(如果有)
- old_tupian_url = shangpin_obj.tupian_url
-
- # 3. 处理商品图片上传(如果有新商品图片)
- tupian_file = request.FILES.get('tupian_file')
- new_tupian_url = None
-
- if tupian_file:
- # 验证图片
- is_valid, error_msg = validate_image(tupian_file)
- if not is_valid:
- return Response(
- {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 生成新文件名:时间戳 + 随机数 + 原扩展名
- timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳
- random_num = random.randint(1000, 9999)
- file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg'
- new_filename = f"sp_{timestamp}_{random_num}{file_extension}"
-
- # 确定OSS路径
- oss_folder = 'a_long'
- shangpin_folder = 'shangpin'
- oss_file_path = f"{oss_folder}/{shangpin_folder}/{new_filename}"
-
- # 删除旧图片(如果存在)
- if old_tupian_url:
- try:
- delete_from_oss(old_tupian_url)
- logger.info(f"已删除旧商品图片: {old_tupian_url}")
- except Exception as e:
- logger.warning(f"删除旧商品图片失败: {old_tupian_url}, 错误: {e}")
- # 继续上传新图片,不中断流程
-
- # 上传到OSS
- new_tupian_url = upload_to_oss(tupian_file, oss_file_path)
-
- if not new_tupian_url:
- return Response(
- {'code': 500, 'message': '商品图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 更新数据库中的图片URL
- shangpin_obj.tupian_url = oss_file_path # 存相对路径
- has_changes = True
- logger.info(f"商品图片更新成功: {oss_file_path}")
-
- # 4. 处理其他字段(只更新有变化的字段)
- # 注意:这里同时处理 request.data(文本字段)和可能有文件的情况
- field_mapping = {
- 'biaoti': 'biaoqian',
- 'jieshao': 'jieshao',
- 'jiage': 'jiage',
- 'kucun': 'kucun',
- 'duiwaixiaoliang': 'duiwai_xiaoliang',
- 'xiadanxuzhi': 'xiadan_xuzhi',
- 'yajin': 'yajin',
- }
-
- for front_field, db_field in field_mapping.items():
- # 注意:request.data 可能包含文件和其他字段,但DRF会处理好
- if front_field in request.data:
- new_value = request.data.get(front_field)
-
- # 空字符串处理
- if new_value == '':
- new_value = None
-
- if new_value is not None:
- # 获取当前值
- current_value = getattr(shangpin_obj, db_field)
-
- # 类型转换
- try:
- if db_field in ['jiage', 'yajin']:
- # 价格和押金
- if new_value:
- new_value = Decimal(str(new_value))
- else:
- new_value = None
- elif db_field in ['kucun', 'duiwai_xiaoliang']:
- # 库存和对外销量
- if new_value:
- new_value = int(float(new_value))
- else:
- new_value = 0
- except (ValueError, TypeError, InvalidOperation) as e:
- logger.warning(f"字段{front_field}转换失败: {new_value}, 错误: {e}")
- continue
-
- # 检查是否有变化(处理None值)
- if current_value != new_value:
- setattr(shangpin_obj, db_field, new_value)
- has_changes = True
-
- # 5. 如果没有检测到任何更改
- if not has_changes:
- return Response({
- 'code': 400,
- 'message': '没有检测到任何更改',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 保存更改
- shangpin_obj.save()
-
- # 7. 返回更新后的数据
- response_data = {
- 'tupian_url': shangpin_obj.tupian_url or '',
- 'biaoti': shangpin_obj.biaoqian or '',
- 'jieshao': shangpin_obj.jieshao or '',
- 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00',
- 'kucun': shangpin_obj.kucun,
- 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang,
- 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '',
- 'yajin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None,
- 'genggaishijian': shangpin_obj.UpdateTime.strftime(
- '%Y-%m-%d %H:%M:%S') if shangpin_obj.UpdateTime else '',
- }
-
- return Response({
- 'code': 0,
- 'message': '更新成功',
- 'data': response_data
- })
-
- except Exception as e:
- logger.error(f"更新商品异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '更新失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-class AdShangpinGuizeXiuGai(APIView):
- """
- 后台规则图片更换接口 - 修复版
- 修复:每次上传生成新文件名,避免浏览器缓存
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser, JSONParser)
-
- def post(self, request):
- try:
- # 1. 权限验证
- qianzhanghao = request.data.get('zhanghao', '').strip()
- shangpin_id = request.data.get('shangpin_id')
- action = request.data.get('action', 'update').strip().lower()
-
- if not qianzhanghao or not shangpin_id:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证身份
- dangqianyonghu = request.user
-
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AdminProfile.DoesNotExist:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 2. 查询商品
- try:
- shangpin_obj = Shangpin.query.get(id=shangpin_id)
- except Shangpin.DoesNotExist:
- return Response(
- {'code': 404, 'message': '商品不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 3. 处理不同操作
- if action == 'delete':
- return self._delete_guize_image(shangpin_obj)
- elif action == 'update':
- return self._update_guize_image(request, shangpin_obj)
- else:
- return Response(
- {'code': 400, 'message': '操作类型无效', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- except Exception as e:
- logger.error(f"规则图片操作异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def _delete_guize_image(self, shangpin_obj):
- """删除规则图片"""
- guizetupian_url = shangpin_obj.guize_tupian
-
- if guizetupian_url:
- # 从OSS删除
- try:
- delete_success = delete_from_oss(guizetupian_url)
- '''if delete_success:
- logger.info(f"已删除规则图片: {guizetupian_url}")
- else:
- logger.warning(f"OSS删除失败,但继续删除数据库记录: {guizetupian_url}")'''
- except Exception as e:
- logger.warning(f"删除规则图片异常: {e}")
-
- # 清除数据库记录
- shangpin_obj.guize_tupian = ''
- shangpin_obj.save()
-
- return Response({
- 'code': 0,
- 'message': '删除成功',
- 'data': {
- 'guizetupian_url': ''
- }
- })
-
- def _update_guize_image(self, request, shangpin_obj):
- """更新规则图片 - 修复:每次上传生成新文件名"""
- guize_file = request.FILES.get('guize_file')
-
- if not guize_file:
- return Response(
- {'code': 400, 'message': '请上传规则图片', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证图片
- is_valid, error_msg = validate_image(guize_file)
- if not is_valid:
- return Response(
- {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 获取原规则图片URL
- old_guizetupian_url = shangpin_obj.guize_tupian
-
- # 生成新文件名:时间戳 + 随机数 + 原扩展名
- timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳
- random_num = random.randint(1000, 9999)
- file_extension = os.path.splitext(guize_file.name)[1].lower() or '.jpg'
- new_filename = f"gz_{timestamp}_{random_num}{file_extension}"
-
- # 确定OSS路径
- oss_folder = 'a_long'
- guize_folder = 'guize' # 修改为你的要求:a_long/guize/
- oss_file_path = f"{oss_folder}/{guize_folder}/{new_filename}"
-
- # 删除旧图片(如果存在)
- if old_guizetupian_url:
- try:
- delete_from_oss(old_guizetupian_url)
- #logger.info(f"已删除旧规则图片: {old_guizetupian_url}")
- except Exception as e:
- logger.warning(f"删除旧规则图片失败: {old_guizetupian_url}, 错误: {e}")
- # 继续上传新图片,不中断流程
-
- # 上传到OSS
- new_guizetupian_url = upload_to_oss(guize_file, oss_file_path)
-
- if not new_guizetupian_url:
- return Response(
- {'code': 500, 'message': '规则图片上传失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 更新数据库(存相对路径)
- shangpin_obj.guize_tupian = oss_file_path
- shangpin_obj.save()
-
- #logger.info(f"规则图片更新成功: {oss_file_path}")
-
- return Response({
- 'code': 0,
- 'message': '规则图片更新成功',
- 'data': {
- 'guizetupian_url': oss_file_path # 返回相对路径
- }
- })
-
-
-
-
-# views.py
-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
-
-
-
-# 常量定义(可根据需要调整)
-INTEGRAL_PRICE = Decimal('5.00') # 积分购买固定价格(元)
-
-
-class CzhqdyView(APIView):
- """
- 获取可用抵扣身份接口
- 路径:/shangpin/czhqdy
- 方法:POST
- 权限:JWT认证
- 请求参数:
- leixing: int # 1=会员,2=押金,3=积分,4=罚款
- huiyuan_id: str # 当 leixing=1 时必传,会员ID
- yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元)
- fadan_id: int # 当 leixing=4 时必传,罚单ID
- 返回:
- {
- "code": 200,
- "msg": "success",
- "data": [
- {
- "id": 1, # 身份ID:1打手佣金 2管事分红 3组长分红 4打手押金
- "jieshao": "打手佣金抵扣",
- "xuyao": "22.22" # 所需抵扣金额(保留两位小数)
- },
- ...
- ]
- }
- """
- permission_classes = [permissions.IsAuthenticated]
-
- # 身份ID与对应扩展表模型、余额字段、费率查询条件的映射
- IDENTITY_CONFIG = [
- {
- 'id': 1,
- 'name': '打手佣金抵扣',
- 'profile_attr': 'DashouProfile', # User 的反向 related_name
- 'balance_field': 'yue',
- 'rate_key': '5',
- 'model': UserDashou,
- },
- {
- 'id': 2,
- 'name': '管事分红抵扣',
- 'profile_attr': 'GuanshiProfile',
- 'balance_field': 'yue',
- 'rate_key': '6',
- 'model': UserGuanshi,
- },
- {
- 'id': 3,
- 'name': '组长分红抵扣',
- 'profile_attr': 'ZuzhangProfile',
- 'balance_field': 'ketixian_jine',
- 'rate_key': '8',
- 'model': UserZuzhang,
- },
- # 新增打手押金抵扣
- {
- 'id': 4,
- 'name': '打手押金抵扣',
- 'profile_attr': 'DashouProfile', # 与打手佣金共用扩展表
- 'balance_field': 'yajin', # 押金字段
- 'rate_key': '11', # 押金提现费率对应的 Platform
- 'model': UserDashou,
- },
- ]
-
- def post(self, request):
- # 获取参数
- leixing = request.data.get('leixing')
- huiyuan_id = request.data.get('huiyuan_id')
- yajin_jine = request.data.get('yajin_jine')
- fadan_id = request.data.get('fadan_id')
- is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial'))
-
- # 参数校验
- if leixing not in [1, 2, 3, 4]:
- return Response({'code': 400, 'msg': 'leixing 必须为 1,2,3,4'}, status=status.HTTP_400_BAD_REQUEST)
-
- if leixing == 1 and not huiyuan_id:
- return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST)
-
- if leixing == 1 and is_trial:
- return Response({
- 'code': 400,
- 'msg': '体验会员仅支持微信支付,不可用佣金、分红或押金抵扣',
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if leixing == 2:
- if not yajin_jine:
- return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
- try:
- yajin_jine = Decimal(str(yajin_jine))
- if yajin_jine <= 0:
- raise ValueError
- except:
- return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
-
- if leixing == 4 and not fadan_id:
- return Response({'code': 400, 'msg': '缴纳罚款时必须提供 fadan_id'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取当前用户
- user = request.user
-
- # 获取目标价格(需支付的原价)
- target_price = None
- if leixing == 1:
- # 会员
- try:
- huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
- club_id = resolve_effective_club_id(request, request.user)
- target_price = get_huiyuan_price(club_id, huiyuan_id, huiyuan)
- except Huiyuan.DoesNotExist:
- return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
- elif leixing == 2:
- # 押金
- target_price = yajin_jine
- elif leixing == 3:
- # 积分(固定价格)
- target_price = INTEGRAL_PRICE
- elif leixing == 4:
- # 罚款缴纳
- try:
- fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=user.UserUID)
- target_price = fadan.FineAmount
- except Penalty.DoesNotExist:
- return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
-
- # 获取所有费率(打手佣金、管事分红、组长分红、打手押金)
- rates = {}
- club_id = resolve_effective_club_id(request, request.user)
- for key in ['5', '6', '8', '11']:
- rates[key] = get_commission_rate(club_id, key)
-
- # 构建可用身份列表
- available_list = []
-
- for ident in self.IDENTITY_CONFIG:
- # 检查用户是否有该扩展表
- profile = getattr(user, ident['profile_attr'], None)
- if not profile:
- continue
-
- # 获取余额
- balance = getattr(profile, ident['balance_field'], Decimal('0.00'))
-
- # 获取费率
- rate = rates.get(ident['rate_key'])
- if rate is None:
- continue # 费率未配置,跳过该身份
-
- # 计算所需抵扣金额: target_price / (1 - rate) ,向上取整到分
- try:
- if rate >= 1:
- continue # 防止除零及不合理费率
- required = target_price / (Decimal('1.0') - rate)
- # 向上取整到分(保留两位小数,最后一位进位)
- required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING)
- except Exception:
- continue # 计算异常,跳过
-
- # 余额是否足够
- if balance >= required:
- available_list.append({
- 'id': ident['id'],
- 'jieshao': ident['name'],
- 'xuyao': str(required),
- })
-
- return Response({
- 'code': 200,
- 'msg': 'success',
- 'data': available_list
- })
-
-
-
-INTEGRAL_INCREMENT = 5
-MAX_INTEGRAL = 10
-
-IDENTITY_CONFIG = {
- 1: {
- 'name': '打手佣金',
- 'model': UserDashou,
- 'balance_field': 'yue',
- 'status_field': 'zhanghaozhuangtai',
- 'rate_key': '5',
- },
- 2: {
- 'name': '管事分红',
- 'model': UserGuanshi,
- 'balance_field': 'yue',
- 'status_field': 'zhuangtai',
- 'rate_key': '6',
- },
- 3: {
- 'name': '组长分红',
- 'model': UserZuzhang,
- 'balance_field': 'ketixian_jine',
- 'status_field': 'zhuangtai',
- 'rate_key': '8',
- },
- 4: {
- 'name': '打手押金',
- 'model': UserDashou,
- 'balance_field': 'yajin',
- 'status_field': 'zhanghaozhuangtai',
- 'rate_key': '11',
- },
-}
-
-
-class DsqrgmdhView(APIView):
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- leixing = request.data.get('leixing')
- shenfen_id = request.data.get('shenfen_id')
- huiyuan_id = request.data.get('huiyuan_id')
- yajin_jine = request.data.get('yajin_jine')
- fadan_id = request.data.get('fadan_id')
-
- is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial'))
-
- logger.info(f"抵扣支付请求: leixing={leixing}, shenfen_id={shenfen_id}, huiyuan_id={huiyuan_id}, yajin_jine={yajin_jine}, fadan_id={fadan_id}, is_trial={is_trial}")
-
- if leixing not in [1, 2, 3, 4]:
- return Response({'code': 400, 'msg': 'leixing 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
- if shenfen_id not in [1, 2, 3, 4]:
- return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
-
- if leixing == 1 and is_trial:
- return Response({
- 'code': 400,
- 'msg': '体验会员仅支持微信支付,不可用佣金、分红或押金抵扣',
- }, status=status.HTTP_400_BAD_REQUEST)
- if leixing == 2:
- if not yajin_jine:
- return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
- try:
- yajin_jine = Decimal(str(yajin_jine))
- if yajin_jine <= 0:
- raise ValueError
- if yajin_jine > Decimal('10000'):
- return Response({'code': 400, 'msg': '押金金额不能超过10000元'}, status=status.HTTP_400_BAD_REQUEST)
- except:
- return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
-
- target_price = None
- club_id = resolve_effective_club_id(request, request.user)
- if leixing == 1:
- try:
- huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
- target_price = get_huiyuan_price(club_id, huiyuan_id, huiyuan)
- except Huiyuan.DoesNotExist:
- return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
- elif leixing == 2:
- target_price = yajin_jine
- elif leixing == 3:
- target_price = INTEGRAL_PRICE
- elif leixing == 4:
- if not fadan_id:
- return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST)
- try:
- fadan_obj = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.UserUID)
- target_price = fadan_obj.FineAmount
- except Penalty.DoesNotExist:
- return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
-
- identity = IDENTITY_CONFIG.get(shenfen_id)
- from jituan.services.club_config import get_commission_rate
- try:
- rate = get_commission_rate(club_id, identity['rate_key'])
- if rate >= 1:
- return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST)
- except Exception:
- return Response({'code': 400, 'msg': f"{identity['name']}费率未配置"}, status=status.HTTP_400_BAD_REQUEST)
-
- logger.info(f"目标价格={target_price}, 费率={rate}")
-
- try:
- required = target_price / (Decimal('1.0') - rate)
- required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING)
- logger.info(f"所需抵扣金额={required}")
- except Exception as e:
- logger.error(f"金额计算错误: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': f'金额计算错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- user = request.user
- yonghuid = user.UserUID
-
- try:
- profile = identity['model'].query.get(user=user)
- except identity['model'].DoesNotExist:
- return Response({'code': 403, 'msg': f'您不是{identity["name"]}身份'})
-
- status_value = getattr(profile, identity['status_field'])
- if status_value != 1:
- return Response({'code': 403, 'msg': f'{identity["name"]}账号状态异常,无法使用'})
-
- balance = getattr(profile, identity['balance_field'])
- if balance < required:
- return Response({'code': 400, 'msg': f'{identity["name"]}余额不足,需要{required}元,当前{balance}元'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- with transaction.atomic():
- locked_profile = identity['model'].objects.select_for_update().get(id=profile.id)
- current_balance = getattr(locked_profile, identity['balance_field'])
- if current_balance < required:
- raise ValueError(f"{identity['name']}余额不足,需要{required}元,当前{current_balance}元")
-
- setattr(locked_profile, identity['balance_field'], current_balance - required)
- locked_profile.save(update_fields=[identity['balance_field']])
- logger.info(f"扣款成功: {identity['name']} 扣除 {required} 元,剩余 {current_balance - required} 元")
-
- timestamp = str(int(time.time() * 1000))[-12:]
- rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
- dingdan_id = f"CZ{timestamp}{rand_str}"
-
- if leixing == 1:
- action_desc = f"购买会员 {huiyuan.jieshao}"
- elif leixing == 2:
- action_desc = f"充值押金 {yajin_jine}元"
- elif leixing == 3:
- action_desc = "补充积分"
- elif leixing == 4:
- action_desc = f"缴纳罚款 {target_price}元"
-
- shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}元"
- cz_leixing = leixing
- cz_club_id = club_id
- cz_zhuangtai = 1
- if leixing == 4:
- from jituan.services.club_penalty import penalty_cz_club_id
- from products.czjilu_types import (
- CZJILU_LEIXING_FAKUAN,
- CZJILU_PAID_STATUS,
- PENALTY_FADAN_TAG,
- )
- cz_leixing = CZJILU_LEIXING_FAKUAN
- cz_club_id = penalty_cz_club_id(fadan_obj)
- cz_zhuangtai = CZJILU_PAID_STATUS
- shuoming = f"{shuoming}{PENALTY_FADAN_TAG}{fadan_obj.id}"
-
- member_purchase_days = 0
- czjilu_kwargs = {
- 'dingdan_id': dingdan_id,
- 'zhuangtai': cz_zhuangtai,
- 'yonghuid': yonghuid,
- 'jine': required,
- 'leixing': cz_leixing,
- 'shuoming': shuoming,
- 'club_id': cz_club_id,
- }
- if leixing == 1:
- from jituan.services.member_recharge import club_huiyuan_sellable
- _, _, member_purchase_days = club_huiyuan_sellable(
- club_id, huiyuan_id, is_trial=False,
- )
- czjilu_kwargs.update({
- 'huiyuan_id': huiyuan_id,
- 'is_trial': False,
- 'purchase_days': member_purchase_days,
- })
-
- czjilu = Czjilu.query.create(**czjilu_kwargs)
-
- response_data = {}
-
- if leixing == 1:
- from jituan.services.member_recharge import fulfill_member_balance_purchase
- huiyuan_record, is_first_buy = fulfill_member_balance_purchase(
- czjilu, huiyuan, club_id=club_id,
- )
- response_data['huiyuan'] = {
- 'huiyuanid': huiyuan_id,
- 'huiyuanming': huiyuan.jieshao,
- 'daoqi': huiyuan_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(huiyuan_record, 'daoqi_time') else '',
- }
- if is_first_buy:
- try:
- dashou = UserDashou.query.get(user=user)
- dashou.jifen += 10
- dashou.save(update_fields=['jifen'])
- response_data['jifen'] = dashou.jifen
- except UserDashou.DoesNotExist:
- pass
-
- elif leixing == 2:
- try:
- dashou = UserDashou.objects.select_for_update().get(user=user)
- dashou.yajin += yajin_jine
- dashou.save(update_fields=['yajin'])
- response_data['yajin'] = dashou.yajin
- except UserDashou.DoesNotExist:
- return Response({'code': 500, 'msg': '打手信息不存在,无法充值押金'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- elif leixing == 4:
- if fadan_obj.Status not in [1, 3]:
- return Response({'code': 400, 'msg': '罚单状态不可缴纳'}, status=status.HTTP_400_BAD_REQUEST)
- fadan_obj.Status = 2
- fadan_obj.save(update_fields=['Status'])
- response_data['fadan_id'] = fadan_obj.id
- response_data['fadan_status'] = fadan_obj.Status
-
- # ========== 修改点2: 使用最新的分红处理方法 ==========
- success, msg = process_fadan_fenhong(fadan_obj.id)
- if not success:
- logger.error(f"罚单 {fadan_obj.id} 分红处理失败: {msg}")
- else:
- logger.info(f"罚单 {fadan_obj.id} 分红处理成功: {msg}")
-
- else: # leixing == 3
- try:
- dashou = UserDashou.objects.select_for_update().get(user=user)
- new_jifen = dashou.jifen + INTEGRAL_INCREMENT
- dashou.jifen = min(new_jifen, MAX_INTEGRAL)
- dashou.save(update_fields=['jifen'])
- response_data['jifen'] = dashou.jifen
- except UserDashou.DoesNotExist:
- return Response({'code': 500, 'msg': '打手信息不存在,无法补充积分'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- except Exception as e:
- logger.error(f"事务处理异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': f'支付处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- final_profile = identity['model'].query.get(user=user)
- response_data['balance'] = str(getattr(final_profile, identity['balance_field']))
-
- return Response({
- 'code': 200,
- 'msg': 'success',
- 'data': response_data,
- })
-
- except Exception as e:
- logger.error(f"接口顶层异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': f'系统错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
\ No newline at end of file
diff --git a/products/views/__init__.py b/products/views/__init__.py
new file mode 100644
index 0000000..256c8bc
--- /dev/null
+++ b/products/views/__init__.py
@@ -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',
+]
diff --git a/products/views/chongzhi_payment.py b/products/views/chongzhi_payment.py
new file mode 100644
index 0000000..f8eb5df
--- /dev/null
+++ b/products/views/chongzhi_payment.py
@@ -0,0 +1,1105 @@
+"""商家充值与充值抵扣视图。
+
+包含商家充值支付参数/回调/失败/成功轮询,以及充值打手抵押查询与抵扣接口。
+含模块级常量 INTEGRAL_PRICE / INTEGRAL_INCREMENT / MAX_INTEGRAL / IDENTITY_CONFIG。
+"""
+# ==================== 标准库 ====================
+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 ShangjiaChongzhi(APIView):
+ """
+ 商家余额充值支付参数接口
+ 路径:/shangpin/sjcz
+ 请求方式:POST
+ 权限:JWT认证
+ 参数:jine(金额,1-10000元)
+ 返回:支付参数(payParams)和订单ID(dingdanid)
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 验证用户身份和商家状态
+ try:
+ shangjia = request.user.ShopProfile
+ except UserShangjia.DoesNotExist:
+ logger.warning(f"用户{request.user.UserUID}尝试充值但不是商家")
+ return Response({
+ 'code': 400,
+ 'message': '用户不是商家,无法充值'
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 检查商家状态
+ if shangjia.zhuangtai != 1:
+ logger.warning(f"商家{shangjia.nicheng}状态异常: {shangjia.zhuangtai}")
+ 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 = quantize_yuan(jine)
+ except (ValueError, TypeError, InvalidOperation):
+ return Response({
+ 'code': 400,
+ 'message': '金额格式错误,请填写有效的数字'
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证金额范围
+ if jine < settings.SHANGJIA_CZ_MIN_AMOUNT or jine > settings.SHANGJIA_CZ_MAX_AMOUNT:
+ return Response({
+ 'code': 400,
+ 'message': f'充值金额必须在{settings.SHANGJIA_CZ_MIN_AMOUNT}-{settings.SHANGJIA_CZ_MAX_AMOUNT}元之间'
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 生成订单ID(使用配置的前缀)
+ timestamp_ms = int(time.time() * 1000)
+ timestamp_ns = int(time.time_ns() // 1000) % 1000000
+ random_num = random.randint(0, 99)
+
+ dingdanid = f"{settings.SHANGJIA_CZ_PREFIX}{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
+
+ # 4. 创建充值记录订单
+ try:
+ chongzhi_jilu = Czjilu.query.create(
+ dingdan_id=dingdanid,
+ zhuangtai=9, # 未支付状态
+ yonghuid=request.user.UserUID,
+ jine=jine,
+ leixing=4, # 充值类型:4=商家充值
+ shuoming=settings.SHANGJIA_CZ_PAY_DESCRIPTION,
+ club_id=resolve_club_id_for_write(request, request.user),
+ )
+ logger.info(f"创建商家充值订单: 商家{shangjia.nicheng}, 订单{dingdanid}, 金额{jine}元")
+ except Exception as e:
+ logger.error(f"创建订单失败: {str(e)}")
+ return Response({
+ 'code': 500,
+ 'message': '订单创建失败,请重试'
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 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='shangjia'
+ )
+ 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
+ logger.info(f"商家{shangjia.nicheng}支付参数生成成功: {dingdanid}")
+ return Response({
+ 'code': 200,
+ 'message': '支付参数生成成功',
+ 'payParams': pay_params,
+ '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):
+ """生成微信小程序支付参数"""
+ # 获取配置
+ 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 == 'shangjia':
+ PAY_DESCRIPTION = settings.SHANGJIA_CZ_PAY_DESCRIPTION
+ NOTIFY_URL = settings.SHANGJIA_CZ_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"""
+{APPID}
+{PAY_DESCRIPTION}
+{MCHID}
+{nonce_str}
+{NOTIFY_URL}
+{openid}
+{dingdanid}
+{user_ip}
+{total_fee}
+{trade_type}
+{sign}
+"""
+
+ # 调用微信统一下单接口
+ 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'
+
+ # 生成支付签名
+ pay_sign_data = {
+ 'appId': APPID,
+ '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,
+ 'timeStamp': timestamp,
+ 'nonceStr': nonce_str,
+ 'package': package,
+ 'signType': sign_type,
+ 'paySign': pay_sign
+ }
+ 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 ShangjiaHuitiao(View):
+ """
+ 商家支付回调接口
+ 路径:/shangpin/sjhuitiao
+ 注意:这个接口不需要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', '订单不存在')
+
+ from products.czjilu_types import is_merchant_recharge_czjilu
+ if not is_merchant_recharge_czjilu(order):
+ logger.error(
+ '非商家充值订单误入商家回调: %s, leixing=%s, shuoming=%s',
+ out_trade_no, order.leixing, order.shuoming,
+ )
+ 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:{order.dingdan_id}',
+ )
+ 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 = Decimal(str(total_fee)) / Decimal('100') # 转换为元
+ order_amount_yuan = Decimal(str(order.jine)) # 订单金额(元)
+
+ # 允许1分钱的误差
+ if abs(wechat_amount_yuan - order_amount_yuan) > Decimal('0.01'):
+ logger.error(f"商家订单金额不一致: 订单金额{order_amount_yuan}元, 支付金额{wechat_amount_yuan}元")
+ return self.wechat_response('FAIL', '金额不一致')
+
+ # ===== 在这里添加收支记录更新代码 =====
+ # 更新收支记录表
+ try:
+ jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
+ from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
+ repair_wechat_income_if_missing(
+ jine_decimal,
+ getattr(order, 'club_id', None),
+ biz_ref=f'cz:{order.dingdan_id}',
+ )
+
+ logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元")
+
+ except Exception as e:
+ # 收支记录更新失败不影响主流程,只记录日志
+ logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
+ # ===== 添加结束 =====
+
+
+
+ # 7. 使用事务更新订单状态和商家余额
+ with transaction.atomic():
+ # 更新订单状态为已支付(3)
+ order.zhuangtai = 3
+ order.save(update_fields=['zhuangtai'])
+ logger.info(f"商家订单状态更新成功: {order.dingdan_id} -> 3")
+
+ # 更新商家余额
+ try:
+ # 通过用户ID查找商家(高效查询)
+ shangjia = UserShangjia.query.get(user__UserUID=order.yonghuid)
+
+ # 增加余额
+ shangjia.yue += order_amount_yuan
+ shangjia.save(update_fields=['yue'])
+
+ logger.info(f"商家余额更新成功: 商家{shangjia.nicheng}, 增加{order_amount_yuan}元, 新余额{shangjia.yue}")
+
+ except UserShangjia.DoesNotExist:
+ # 如果找不到商家,记录错误,但依然返回成功(避免微信重复回调)
+ logger.error(f"商家信息不存在: 用户{order.yonghuid}")
+ return self.wechat_response('SUCCESS', 'OK')
+ except Exception as e:
+ logger.error(f"更新商家余额失败: {str(e)}", exc_info=True)
+ # 回滚事务
+ raise
+
+ # 8. 返回成功响应给微信
+ 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"""
+
+
+"""
+
+ logger.info(f"返回微信响应: {return_code} - {return_msg}")
+ return HttpResponse(xml_response, content_type='application/xml')
+
+
+
+
+# views.py
+class ShangjiaShibai(APIView):
+ """
+ 商家支付失败处理接口
+ 路径:/shangpin/sjshibai
+ 请求方式: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,
+ leixing=4 # 确保是商家充值订单
+ )
+ except Czjilu.DoesNotExist:
+ logger.warning(f"商家订单不存在或不属于当前用户: {dingdanid}, 用户{request.user.UserUID}")
+ return Response({
+ 'code': 400,
+ 'message': '订单不存在或不属于当前用户'
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 检查订单状态是否为未支付(9)
+ if order.zhuangtai == 9:
+ # 删除订单
+ order.delete()
+ logger.info(f"商家{request.user.ShopProfile.nicheng}删除未支付订单: {dingdanid}")
+
+ return Response({
+ 'code': 200,
+ 'message': '订单已删除'
+ }, status=status.HTTP_200_OK)
+ else:
+ return Response({
+ 'code': 400,
+ 'message': '订单状态不是未支付,无法删除'
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ except UserShangjia.DoesNotExist:
+ 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 ShangjiaChenggong(APIView):
+ """
+ 商家支付成功轮询接口
+ 路径:/shangpin/sjcg
+ 请求方式: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,
+ leixing=4 # 商家充值类型
+ )
+ except Czjilu.DoesNotExist:
+ logger.warning(f"商家订单不存在: {dingdanid}, 用户{request.user.UserUID}")
+ return Response({
+ 'code': 400,
+ 'message': '订单不存在或不属于当前用户'
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 检查订单状态是否为已支付(3)
+ if order.zhuangtai != 3:
+ logger.info(f"商家订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}")
+ return Response({
+ 'code': 400,
+ 'message': '订单尚未支付完成'
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 获取商家余额(通过反向查询)
+ try:
+ shangjia = request.user.ShopProfile
+ yue = float(shangjia.yue) # 转为浮点数方便前端处理
+
+ logger.info(f"商家轮询成功: 订单{dingdanid}已支付,商家{shangjia.nicheng}余额为{yue}元")
+
+ return Response({
+ 'code': 200,
+ 'message': '支付流程全部完成',
+ 'yue': yue # 返回当前商家余额(前端需要更新全局变量)
+ }, status=status.HTTP_200_OK)
+
+ except UserShangjia.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)
+
+
+# 管事分红记录查询API
+
+
+
+INTEGRAL_PRICE = Decimal('5.00') # 积分购买固定价格(元)
+
+
+class CzhqdyView(APIView):
+ """
+ 获取可用抵扣身份接口
+ 路径:/shangpin/czhqdy
+ 方法:POST
+ 权限:JWT认证
+ 请求参数:
+ leixing: int # 1=会员,2=押金,3=积分,4=罚款
+ huiyuan_id: str # 当 leixing=1 时必传,会员ID
+ yajin_jine: decimal # 当 leixing=2 时必传,押金金额(元)
+ fadan_id: int # 当 leixing=4 时必传,罚单ID
+ 返回:
+ {
+ "code": 200,
+ "msg": "success",
+ "data": [
+ {
+ "id": 1, # 身份ID:1打手佣金 2管事分红 3组长分红 4打手押金
+ "jieshao": "打手佣金抵扣",
+ "xuyao": "22.22" # 所需抵扣金额(保留两位小数)
+ },
+ ...
+ ]
+ }
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ # 身份ID与对应扩展表模型、余额字段、费率查询条件的映射
+ IDENTITY_CONFIG = [
+ {
+ 'id': 1,
+ 'name': '打手佣金抵扣',
+ 'profile_attr': 'DashouProfile', # User 的反向 related_name
+ 'balance_field': 'yue',
+ 'rate_key': '5',
+ 'model': UserDashou,
+ },
+ {
+ 'id': 2,
+ 'name': '管事分红抵扣',
+ 'profile_attr': 'GuanshiProfile',
+ 'balance_field': 'yue',
+ 'rate_key': '6',
+ 'model': UserGuanshi,
+ },
+ {
+ 'id': 3,
+ 'name': '组长分红抵扣',
+ 'profile_attr': 'ZuzhangProfile',
+ 'balance_field': 'ketixian_jine',
+ 'rate_key': '8',
+ 'model': UserZuzhang,
+ },
+ # 新增打手押金抵扣
+ {
+ 'id': 4,
+ 'name': '打手押金抵扣',
+ 'profile_attr': 'DashouProfile', # 与打手佣金共用扩展表
+ 'balance_field': 'yajin', # 押金字段
+ 'rate_key': '11', # 押金提现费率对应的 Platform
+ 'model': UserDashou,
+ },
+ ]
+
+ def post(self, request):
+ # 获取参数
+ leixing = request.data.get('leixing')
+ huiyuan_id = request.data.get('huiyuan_id')
+ yajin_jine = request.data.get('yajin_jine')
+ fadan_id = request.data.get('fadan_id')
+ is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial'))
+
+ # 参数校验
+ if leixing not in [1, 2, 3, 4]:
+ return Response({'code': 400, 'msg': 'leixing 必须为 1,2,3,4'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if leixing == 1 and not huiyuan_id:
+ return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if leixing == 1 and is_trial:
+ return Response({
+ 'code': 400,
+ 'msg': '体验会员仅支持微信支付,不可用佣金、分红或押金抵扣',
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if leixing == 2:
+ if not yajin_jine:
+ return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
+ try:
+ yajin_jine = Decimal(str(yajin_jine))
+ if yajin_jine <= 0:
+ raise ValueError
+ except:
+ return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if leixing == 4 and not fadan_id:
+ return Response({'code': 400, 'msg': '缴纳罚款时必须提供 fadan_id'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取当前用户
+ user = request.user
+
+ # 获取目标价格(需支付的原价)
+ target_price = None
+ if leixing == 1:
+ # 会员
+ try:
+ huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
+ club_id = resolve_effective_club_id(request, request.user)
+ target_price = get_huiyuan_price(club_id, huiyuan_id, huiyuan)
+ except Huiyuan.DoesNotExist:
+ return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
+ elif leixing == 2:
+ # 押金
+ target_price = yajin_jine
+ elif leixing == 3:
+ # 积分(固定价格)
+ target_price = INTEGRAL_PRICE
+ elif leixing == 4:
+ # 罚款缴纳
+ try:
+ fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=user.UserUID)
+ target_price = fadan.FineAmount
+ except Penalty.DoesNotExist:
+ return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 获取所有费率(打手佣金、管事分红、组长分红、打手押金)
+ rates = {}
+ club_id = resolve_effective_club_id(request, request.user)
+ for key in ['5', '6', '8', '11']:
+ rates[key] = get_commission_rate(club_id, key)
+
+ # 构建可用身份列表
+ available_list = []
+
+ for ident in self.IDENTITY_CONFIG:
+ # 检查用户是否有该扩展表
+ profile = getattr(user, ident['profile_attr'], None)
+ if not profile:
+ continue
+
+ # 获取余额
+ balance = getattr(profile, ident['balance_field'], Decimal('0.00'))
+
+ # 获取费率
+ rate = rates.get(ident['rate_key'])
+ if rate is None:
+ continue # 费率未配置,跳过该身份
+
+ # 计算所需抵扣金额: target_price / (1 - rate) ,向上取整到分
+ try:
+ if rate >= 1:
+ continue # 防止除零及不合理费率
+ required = target_price / (Decimal('1.0') - rate)
+ # 向上取整到分(保留两位小数,最后一位进位)
+ required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING)
+ except Exception:
+ continue # 计算异常,跳过
+
+ # 余额是否足够
+ if balance >= required:
+ available_list.append({
+ 'id': ident['id'],
+ 'jieshao': ident['name'],
+ 'xuyao': str(required),
+ })
+
+ return Response({
+ 'code': 200,
+ 'msg': 'success',
+ 'data': available_list
+ })
+
+
+
+INTEGRAL_INCREMENT = 5
+MAX_INTEGRAL = 10
+
+IDENTITY_CONFIG = {
+ 1: {
+ 'name': '打手佣金',
+ 'model': UserDashou,
+ 'balance_field': 'yue',
+ 'status_field': 'zhanghaozhuangtai',
+ 'rate_key': '5',
+ },
+ 2: {
+ 'name': '管事分红',
+ 'model': UserGuanshi,
+ 'balance_field': 'yue',
+ 'status_field': 'zhuangtai',
+ 'rate_key': '6',
+ },
+ 3: {
+ 'name': '组长分红',
+ 'model': UserZuzhang,
+ 'balance_field': 'ketixian_jine',
+ 'status_field': 'zhuangtai',
+ 'rate_key': '8',
+ },
+ 4: {
+ 'name': '打手押金',
+ 'model': UserDashou,
+ 'balance_field': 'yajin',
+ 'status_field': 'zhanghaozhuangtai',
+ 'rate_key': '11',
+ },
+}
+
+
+class DsqrgmdhView(APIView):
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ leixing = request.data.get('leixing')
+ shenfen_id = request.data.get('shenfen_id')
+ huiyuan_id = request.data.get('huiyuan_id')
+ yajin_jine = request.data.get('yajin_jine')
+ fadan_id = request.data.get('fadan_id')
+
+ is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial'))
+
+ logger.info(f"抵扣支付请求: leixing={leixing}, shenfen_id={shenfen_id}, huiyuan_id={huiyuan_id}, yajin_jine={yajin_jine}, fadan_id={fadan_id}, is_trial={is_trial}")
+
+ if leixing not in [1, 2, 3, 4]:
+ return Response({'code': 400, 'msg': 'leixing 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
+ if shenfen_id not in [1, 2, 3, 4]:
+ return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if leixing == 1 and is_trial:
+ return Response({
+ 'code': 400,
+ 'msg': '体验会员仅支持微信支付,不可用佣金、分红或押金抵扣',
+ }, status=status.HTTP_400_BAD_REQUEST)
+ if leixing == 2:
+ if not yajin_jine:
+ return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
+ try:
+ yajin_jine = Decimal(str(yajin_jine))
+ if yajin_jine <= 0:
+ raise ValueError
+ if yajin_jine > Decimal('10000'):
+ return Response({'code': 400, 'msg': '押金金额不能超过10000元'}, status=status.HTTP_400_BAD_REQUEST)
+ except:
+ return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
+
+ target_price = None
+ club_id = resolve_effective_club_id(request, request.user)
+ if leixing == 1:
+ try:
+ huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
+ target_price = get_huiyuan_price(club_id, huiyuan_id, huiyuan)
+ except Huiyuan.DoesNotExist:
+ return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
+ elif leixing == 2:
+ target_price = yajin_jine
+ elif leixing == 3:
+ target_price = INTEGRAL_PRICE
+ elif leixing == 4:
+ if not fadan_id:
+ return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST)
+ try:
+ fadan_obj = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.UserUID)
+ target_price = fadan_obj.FineAmount
+ except Penalty.DoesNotExist:
+ return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND)
+
+ identity = IDENTITY_CONFIG.get(shenfen_id)
+ from jituan.services.club_config import get_commission_rate
+ try:
+ rate = get_commission_rate(club_id, identity['rate_key'])
+ if rate >= 1:
+ return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST)
+ except Exception:
+ return Response({'code': 400, 'msg': f"{identity['name']}费率未配置"}, status=status.HTTP_400_BAD_REQUEST)
+
+ logger.info(f"目标价格={target_price}, 费率={rate}")
+
+ try:
+ required = target_price / (Decimal('1.0') - rate)
+ required = required.quantize(Decimal('0.01'), rounding=ROUND_CEILING)
+ logger.info(f"所需抵扣金额={required}")
+ except Exception as e:
+ logger.error(f"金额计算错误: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': f'金额计算错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ user = request.user
+ yonghuid = user.UserUID
+
+ try:
+ profile = identity['model'].query.get(user=user)
+ except identity['model'].DoesNotExist:
+ return Response({'code': 403, 'msg': f'您不是{identity["name"]}身份'})
+
+ status_value = getattr(profile, identity['status_field'])
+ if status_value != 1:
+ return Response({'code': 403, 'msg': f'{identity["name"]}账号状态异常,无法使用'})
+
+ balance = getattr(profile, identity['balance_field'])
+ if balance < required:
+ return Response({'code': 400, 'msg': f'{identity["name"]}余额不足,需要{required}元,当前{balance}元'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ with transaction.atomic():
+ locked_profile = identity['model'].objects.select_for_update().get(id=profile.id)
+ current_balance = getattr(locked_profile, identity['balance_field'])
+ if current_balance < required:
+ raise ValueError(f"{identity['name']}余额不足,需要{required}元,当前{current_balance}元")
+
+ setattr(locked_profile, identity['balance_field'], current_balance - required)
+ locked_profile.save(update_fields=[identity['balance_field']])
+ logger.info(f"扣款成功: {identity['name']} 扣除 {required} 元,剩余 {current_balance - required} 元")
+
+ timestamp = str(int(time.time() * 1000))[-12:]
+ rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
+ dingdan_id = f"CZ{timestamp}{rand_str}"
+
+ if leixing == 1:
+ action_desc = f"购买会员 {huiyuan.jieshao}"
+ elif leixing == 2:
+ action_desc = f"充值押金 {yajin_jine}元"
+ elif leixing == 3:
+ action_desc = "补充积分"
+ elif leixing == 4:
+ action_desc = f"缴纳罚款 {target_price}元"
+
+ shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}元"
+ cz_leixing = leixing
+ cz_club_id = club_id
+ cz_zhuangtai = 1
+ if leixing == 4:
+ from jituan.services.club_penalty import penalty_cz_club_id
+ from products.czjilu_types import (
+ CZJILU_LEIXING_FAKUAN,
+ CZJILU_PAID_STATUS,
+ PENALTY_FADAN_TAG,
+ )
+ cz_leixing = CZJILU_LEIXING_FAKUAN
+ cz_club_id = penalty_cz_club_id(fadan_obj)
+ cz_zhuangtai = CZJILU_PAID_STATUS
+ shuoming = f"{shuoming}{PENALTY_FADAN_TAG}{fadan_obj.id}"
+
+ member_purchase_days = 0
+ czjilu_kwargs = {
+ 'dingdan_id': dingdan_id,
+ 'zhuangtai': cz_zhuangtai,
+ 'yonghuid': yonghuid,
+ 'jine': required,
+ 'leixing': cz_leixing,
+ 'shuoming': shuoming,
+ 'club_id': cz_club_id,
+ }
+ if leixing == 1:
+ from jituan.services.member_recharge import club_huiyuan_sellable
+ _, _, member_purchase_days = club_huiyuan_sellable(
+ club_id, huiyuan_id, is_trial=False,
+ )
+ czjilu_kwargs.update({
+ 'huiyuan_id': huiyuan_id,
+ 'is_trial': False,
+ 'purchase_days': member_purchase_days,
+ })
+
+ czjilu = Czjilu.query.create(**czjilu_kwargs)
+
+ response_data = {}
+
+ if leixing == 1:
+ from jituan.services.member_recharge import fulfill_member_balance_purchase
+ huiyuan_record, is_first_buy = fulfill_member_balance_purchase(
+ czjilu, huiyuan, club_id=club_id,
+ )
+ response_data['huiyuan'] = {
+ 'huiyuanid': huiyuan_id,
+ 'huiyuanming': huiyuan.jieshao,
+ 'daoqi': huiyuan_record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S') if hasattr(huiyuan_record, 'daoqi_time') else '',
+ }
+ if is_first_buy:
+ try:
+ dashou = UserDashou.query.get(user=user)
+ dashou.jifen += 10
+ dashou.save(update_fields=['jifen'])
+ response_data['jifen'] = dashou.jifen
+ except UserDashou.DoesNotExist:
+ pass
+
+ elif leixing == 2:
+ try:
+ dashou = UserDashou.objects.select_for_update().get(user=user)
+ dashou.yajin += yajin_jine
+ dashou.save(update_fields=['yajin'])
+ response_data['yajin'] = dashou.yajin
+ except UserDashou.DoesNotExist:
+ return Response({'code': 500, 'msg': '打手信息不存在,无法充值押金'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ elif leixing == 4:
+ if fadan_obj.Status not in [1, 3]:
+ return Response({'code': 400, 'msg': '罚单状态不可缴纳'}, status=status.HTTP_400_BAD_REQUEST)
+ fadan_obj.Status = 2
+ fadan_obj.save(update_fields=['Status'])
+ response_data['fadan_id'] = fadan_obj.id
+ response_data['fadan_status'] = fadan_obj.Status
+
+ # ========== 修改点2: 使用最新的分红处理方法 ==========
+ success, msg = process_fadan_fenhong(fadan_obj.id)
+ if not success:
+ logger.error(f"罚单 {fadan_obj.id} 分红处理失败: {msg}")
+ else:
+ logger.info(f"罚单 {fadan_obj.id} 分红处理成功: {msg}")
+
+ else: # leixing == 3
+ try:
+ dashou = UserDashou.objects.select_for_update().get(user=user)
+ new_jifen = dashou.jifen + INTEGRAL_INCREMENT
+ dashou.jifen = min(new_jifen, MAX_INTEGRAL)
+ dashou.save(update_fields=['jifen'])
+ response_data['jifen'] = dashou.jifen
+ except UserDashou.DoesNotExist:
+ return Response({'code': 500, 'msg': '打手信息不存在,无法补充积分'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ except Exception as e:
+ logger.error(f"事务处理异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': f'支付处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ final_profile = identity['model'].query.get(user=user)
+ response_data['balance'] = str(getattr(final_profile, identity['balance_field']))
+
+ return Response({
+ 'code': 200,
+ 'msg': 'success',
+ 'data': response_data,
+ })
+
+ except Exception as e:
+ logger.error(f"接口顶层异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': f'系统错误: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
diff --git a/products/views/huiyuan_payment.py b/products/views/huiyuan_payment.py
new file mode 100644
index 0000000..69a0293
--- /dev/null
+++ b/products/views/huiyuan_payment.py
@@ -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"""
+{APPID}
+{PAY_DESCRIPTION}
+{MCHID}
+{nonce_str}
+{NOTIFY_URL}
+{openid}
+{dingdanid}
+{user_ip}
+{total_fee}
+{trade_type}
+{sign}
+"""
+
+ # 调用微信统一下单接口(官方地址)
+ 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"""
+
+
+"""
+ 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
+
+
+
diff --git a/products/views/jifen_payment.py b/products/views/jifen_payment.py
new file mode 100644
index 0000000..b842343
--- /dev/null
+++ b/products/views/jifen_payment.py
@@ -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"""
+{APPID}
+{PAY_DESCRIPTION}
+{MCHID}
+{nonce_str}
+{NOTIFY_URL}
+{openid}
+{dingdanid}
+{user_ip}
+{total_fee}
+{trade_type}
+{sign}
+"""
+
+ # 调用微信统一下单接口(官方地址)
+ 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"""
+
+
+"""
+
+ 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)
+
+
+
+
+
diff --git a/products/views/product_query.py b/products/views/product_query.py
new file mode 100644
index 0000000..dbff1c9
--- /dev/null
+++ b/products/views/product_query.py
@@ -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
+
+
+
+# 常量定义(可根据需要调整)
+
+
diff --git a/products/views/shangpin_admin.py b/products/views/shangpin_admin.py
new file mode 100644
index 0000000..77da00a
--- /dev/null
+++ b/products/views/shangpin_admin.py
@@ -0,0 +1,2544 @@
+"""商品发布与管理后台视图。
+
+包含商品类型/专区/商品发布、规则图片上传、会员信息查询/修改/发布、
+管理员商品/公告/专区/轮播/类型修改、商品内容获取与修改、规则修改。
+"""
+# ==================== 标准库 ====================
+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 ShangpinLeixingFabuView(APIView):
+ """
+ 商品类型发布接口
+ 接收图片、类型介绍、抢单要求类型等字段
+ 需要管理员权限
+ """
+ permission_classes = [permissions.IsAuthenticated]
+ parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传
+
+ def post(self, request):
+ try:
+ # === 1. 权限验证 ===
+ # 获取当前用户
+ user_main = request.user
+
+ # 1.1 验证前端传递的账号是否匹配(使用phone字段)
+ zhanghao = request.data.get('zhanghao', '').strip()
+ if not zhanghao:
+ return Response(
+ {'code': 401, 'msg': '未提供账号信息', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 验证账号是否匹配(使用phone字段比较)
+ if user_main.Phone != zhanghao:
+ return Response(
+ {'code': 401, 'msg': '账号验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.2 验证是否为管理员(检查user_type字段)
+ if user_main.UserType != 'admin':
+ return Response(
+ {'code': 401, 'msg': '权限不足', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.3 验证管理员扩展表(使用AdminProfile模型)
+ try:
+ admin_profile = AdminProfile.query.get(user=user_main)
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'msg': '管理员信息不存在', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # === 2. 接收并验证字段 ===
+ # 2.1 必填字段验证
+ jieshao = request.data.get('jieshao', '').strip()
+ yaoqiuleixing = request.data.get('yaoqiuleixing')
+
+
+ if not jieshao:
+ return Response(
+ {'code': 400, 'msg': '类型介绍不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ if yaoqiuleixing not in ['1', '2']:
+ return Response(
+ {'code': 400, 'msg': '抢单要求类型无效', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2.2 验证图片
+ tupian_file = request.FILES.get('file')
+ if not tupian_file:
+
+ return Response(
+ {'code': 400, 'msg': '请上传类型图片', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证图片格式和大小
+ is_valid, error_msg = validate_image(tupian_file)
+ if not is_valid:
+ return Response(
+ {'code': 400, 'msg': f'图片验证失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2.3 根据抢单要求类型验证对应字段
+ huiyuan_id = request.data.get('huiyuan_id', '').strip()
+ yongjin = request.data.get('yongjin', '').strip()
+
+
+ # 转换yaoqiuleixing为整数便于处理
+ yaoqiuleixing_int = int(yaoqiuleixing)
+
+ if yaoqiuleixing_int == 1: # 会员抢单
+ if not huiyuan_id:
+ return Response(
+ {'code': 400, 'msg': '会员抢单必须选择会员', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ # 验证会员ID是否存在
+ try:
+ huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id)
+ except Huiyuan.DoesNotExist:
+ return Response(
+ {'code': 400, 'msg': '会员ID不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ # 会员ID为字符串,佣金为空
+ yongjin_value = None
+ huiyuan_id_value = huiyuan_id
+ else: # yaoqiuleixing_int == 2 佣金抢单
+ if not yongjin:
+ return Response(
+ {'code': 400, 'msg': '佣金抢单必须填写佣金金额', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ try:
+ # 验证佣金为有效数字
+ yongjin_value = float(yongjin)
+ if yongjin_value <= 0:
+ return Response(
+ {'code': 400, 'msg': '佣金金额必须大于0', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except ValueError:
+ return Response(
+ {'code': 400, 'msg': '佣金金额格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ huiyuan_id_value = None
+
+ # === 3. 创建商品类型记录 ===
+ with transaction.atomic():
+ # 3.1 创建商品类型对象
+ leixing_obj = ShangpinLeixing(
+ jieshao=jieshao,
+ yaoqiuleixing=yaoqiuleixing_int,
+ huiyuan_id=huiyuan_id_value,
+ yongjin=yongjin_value,
+ # tupian_url 先留空,上传成功后再更新
+ )
+
+ # 保存到数据库获取ID
+ leixing_obj.save()
+ leixing_id = leixing_obj.id
+
+ # === 4. 处理图片上传 ===
+ try:
+ # 4.1 生成文件名和路径
+ file_extension = os.path.splitext(tupian_file.name)[1].lower()
+ if not file_extension:
+ file_extension = '.jpg'
+
+ # 生成唯一文件名:商品类型ID + 随机UUID + 后缀
+ unique_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}"
+
+ # 构建OSS文件路径:a_long/leixing/{leixing_id}/{filename}
+ oss_folder = 'a_long' # 根文件夹
+ leixing_folder = 'leixing' # 类型文件夹
+
+ # 构建完整路径
+ oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{unique_filename}"
+
+ # 4.2 上传到OSS
+ tupian_url = upload_to_oss(tupian_file, oss_file_path)
+
+ if not tupian_url:
+ # 上传失败,抛出异常触发回滚
+ raise Exception("图片上传到OSS失败")
+
+ # 4.3 更新商品类型的图片URL(存储相对路径)
+ leixing_obj.tupian_url = oss_file_path # 存相对路径,方便拼接
+ leixing_obj.save()
+
+ except Exception as oss_error:
+ # 图片上传失败,事务会回滚,商品类型记录也会被删除
+ print(f"OSS上传失败: {oss_error}")
+ # 手动抛出异常触发事务回滚
+ raise Exception(f"图片上传失败: {str(oss_error)}")
+
+ # === 5. 构建返回数据 ===
+ response_data = {
+ 'id': leixing_id,
+ 'jieshao': jieshao,
+ 'yaoqiuleixing': yaoqiuleixing_int,
+ 'tupian_url': leixing_obj.tupian_url, # 相对路径
+ 'huiyuan_id': huiyuan_id_value,
+ 'yongjin': str(yongjin_value) if yongjin_value else None
+ }
+
+ # 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '商品类型添加成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 捕获所有异常,返回500错误
+ traceback.print_exc()
+ return Response({
+ 'code': 500,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class ShangpinZhuanquFabuView(APIView):
+ """
+ 商品专区发布接口
+ 接收专区名称、类型ID
+ 需要管理员权限
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # === 1. 权限验证 ===
+ user_main = request.user
+
+ # 1.1 验证账号(使用phone字段)
+ zhanghao = request.data.get('zhanghao', '').strip()
+ if not zhanghao:
+ return Response(
+ {'code': 401, 'msg': '未提供账号信息', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if user_main.Phone != zhanghao:
+ return Response(
+ {'code': 401, 'msg': '账号验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.2 验证是否为管理员(检查user_type字段)
+ if user_main.UserType != 'admin':
+ return Response(
+ {'code': 401, 'msg': '权限不足', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.3 验证管理员扩展表(使用AdminProfile模型)
+ try:
+ admin_profile = AdminProfile.query.get(user=user_main)
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'msg': '管理员信息不存在', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # === 2. 接收并验证字段 ===
+ mingzi = request.data.get('mingzi', '').strip()
+ leixing_id = request.data.get('leixing_id')
+
+ # 2.1 验证必填字段
+ if not mingzi:
+ return Response(
+ {'code': 400, 'msg': '专区名称不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ if not leixing_id:
+ return Response(
+ {'code': 400, 'msg': '类型ID不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2.2 验证类型ID是否存在
+ try:
+ # 验证商品类型是否存在
+ leixing_obj = ShangpinLeixing.query.get(id=leixing_id)
+ except ShangpinLeixing.DoesNotExist:
+ return Response(
+ {'code': 400, 'msg': '商品类型不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2.3 验证名称长度(15汉字以内)
+ if len(mingzi) > 45: # 数据库字段最大长度
+ return Response(
+ {'code': 400, 'msg': '专区名称过长', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # === 3. 创建商品专区记录 ===
+ zhuanqu_obj = ShangpinZhuanqu(
+ mingzi=mingzi,
+ leixing_id=leixing_id
+ )
+
+ zhuanqu_obj.save()
+
+ # === 4. 构建返回数据 ===
+ response_data = {
+ 'id': zhuanqu_obj.id,
+ 'mingzi': mingzi,
+ 'leixing_id': leixing_id
+ }
+
+ # 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '商品专区添加成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ traceback.print_exc()
+ return Response({
+ 'code': 500,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class ShangpinFabuView(APIView):
+ """
+ 商品发布接口
+ 接收商品信息、图片等字段
+ 需要管理员权限
+ """
+ permission_classes = [permissions.IsAuthenticated]
+ parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传
+
+ def post(self, request):
+ try:
+ # === 1. 权限验证 ===
+ user_main = request.user
+
+ # 1.1 验证账号(使用phone字段)
+ zhanghao = request.data.get('zhanghao', '').strip()
+ if not zhanghao:
+ return Response(
+ {'code': 401, 'msg': '未提供账号信息', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if user_main.Phone != zhanghao:
+ return Response(
+ {'code': 401, 'msg': '账号验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.2 验证是否为管理员(检查user_type字段)
+ if user_main.UserType != 'admin':
+ return Response(
+ {'code': 401, 'msg': '权限不足', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.3 验证管理员扩展表(使用AdminProfile模型)
+ try:
+ admin_profile = AdminProfile.query.get(user=user_main)
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'msg': '管理员信息不存在', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # === 2. 接收并验证字段 ===
+ # 2.1 验证必填字段
+ biaoti = request.data.get('biaoti', '').strip()
+ jiage = request.data.get('jiage', '').strip()
+ kucun = request.data.get('kucun', '').strip()
+ leixing_id = request.data.get('leixing_id')
+ zhuanqu_id = request.data.get('zhuanqu_id')
+ yaoqiuleixing = request.data.get('yaoqiuleixing')
+ jieshao = request.data.get('jieshao', '').strip()
+
+ # 验证图片
+ shangpin_tupian = request.FILES.get('shangpin_tupian')
+ if not shangpin_tupian:
+
+ return Response(
+ {'code': 400, 'msg': '请上传商品图片', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证图片格式和大小
+ is_valid, error_msg = validate_image(shangpin_tupian)
+ if not is_valid:
+ return Response(
+ {'code': 400, 'msg': f'商品图片验证失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2.2 验证必填字段是否为空
+ required_fields = {
+ 'biaoti': biaoti,
+ 'jiage': jiage,
+ 'kucun': kucun,
+ 'leixing_id': leixing_id,
+ 'zhuanqu_id': zhuanqu_id,
+ 'yaoqiuleixing': yaoqiuleixing,
+ 'jieshao': jieshao
+ }
+
+ for field_name, field_value in required_fields.items():
+ if not field_value:
+ return Response(
+ {'code': 400, 'msg': f'{field_name}不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2.3 验证数值字段
+ try:
+ jiage_value = float(jiage)
+ if jiage_value <= 0:
+ return Response(
+ {'code': 400, 'msg': '商品价格必须大于0', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except ValueError:
+ return Response(
+ {'code': 400, 'msg': '商品价格格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ try:
+ kucun_value = int(kucun)
+ if kucun_value < 0:
+ return Response(
+ {'code': 400, 'msg': '库存不能为负数', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except ValueError:
+ return Response(
+ {'code': 400, 'msg': '库存必须为整数', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证抢单要求类型
+ if yaoqiuleixing not in ['1', '2']:
+ return Response(
+ {'code': 400, 'msg': '抢单要求类型无效', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # === 3. 验证关联数据 ===
+ # 3.1 验证商品类型
+ try:
+ leixing_obj = ShangpinLeixing.query.get(id=leixing_id)
+ except ShangpinLeixing.DoesNotExist:
+ return Response(
+ {'code': 400, 'msg': '商品类型不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 3.2 根据抢单要求类型验证商品类型
+ yaoqiuleixing_int = int(yaoqiuleixing)
+
+ if yaoqiuleixing_int == 1: # 会员抢单
+ # 需要验证商品类型的抢单要求类型是否为1
+ if leixing_obj.yaoqiuleixing != 1:
+ return Response(
+ {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 获取会员ID(从前端或商品类型)
+ huiyuan_id = request.data.get('huiyuan_id', leixing_obj.huiyuan_id)
+ if not huiyuan_id:
+ return Response(
+ {'code': 400, 'msg': '会员ID不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证会员是否存在
+ try:
+ huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id)
+ except Huiyuan.DoesNotExist:
+ return Response(
+ {'code': 400, 'msg': '会员不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 设置会员相关字段
+ huiyuan_id_value = huiyuan_id
+ yongjin_value = None
+ else: # 佣金抢单
+ # 需要验证商品类型的抢单要求类型是否为2
+ if leixing_obj.yaoqiuleixing != 2:
+ return Response(
+ {'code': 400, 'msg': '商品类型与抢单要求类型不匹配', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 获取佣金(从前端或商品类型)
+ yongjin = request.data.get('yongjin', leixing_obj.yongjin)
+ if not yongjin:
+ return Response(
+ {'code': 400, 'msg': '佣金金额不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ try:
+ yongjin_value = float(yongjin)
+ if yongjin_value <= 0:
+ return Response(
+ {'code': 400, 'msg': '佣金金额必须大于0', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except ValueError:
+ return Response(
+ {'code': 400, 'msg': '佣金金额格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ huiyuan_id_value = None
+
+ # 3.3 验证商品专区
+ try:
+ zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id, leixing_id=leixing_id)
+ except ShangpinZhuanqu.DoesNotExist:
+ return Response(
+ {'code': 400, 'msg': '商品专区不存在或与商品类型不匹配', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # === 4. 创建商品记录 ===
+ with transaction.atomic():
+ # 4.1 获取其他字段
+ xuzhi = request.data.get('xuzhi', '').strip()
+ xiaoliang = request.data.get('xiaoliang', '0').strip()
+
+ # 转换销量为整数
+ try:
+ duiwai_xiaoliang_value = int(xiaoliang) if xiaoliang else 0
+ except ValueError:
+ duiwai_xiaoliang_value = 0
+
+ # 4.2 创建商品对象
+ shangpin_obj = Shangpin(
+ biaoqian=biaoti,
+ jiage=jiage_value,
+ kucun=kucun_value,
+ leixing_id=leixing_id,
+ zhuanqu_id=zhuanqu_id,
+ duiwai_xiaoliang=duiwai_xiaoliang_value,
+ jieshao=jieshao,
+ xiadan_xuzhi=xuzhi,
+ yaoqiuleixing=yaoqiuleixing_int,
+ huiyuan_id=huiyuan_id_value,
+ yongjin=yongjin_value,
+ # tupian_url 先留空,上传成功后再更新
+ )
+
+ # 保存到数据库获取ID
+ shangpin_obj.save()
+ shangpin_id = shangpin_obj.id
+
+ # === 5. 处理商品图片上传 ===
+ try:
+ # 5.1 生成文件名和路径
+ file_extension = os.path.splitext(shangpin_tupian.name)[1].lower()
+ if not file_extension:
+ file_extension = '.jpg'
+
+ # 构建OSS文件路径:a_long/Spcc{shangpin_id}/{filename}
+ oss_folder = 'a_long' # 根文件夹
+ spcc_folder = f"Spcc{shangpin_id}" # 商品图片文件夹
+
+ # 生成文件名:Spcc{shangpin_id}{file_extension}
+ unique_filename = f"Spcc{shangpin_id}{file_extension}"
+
+ # 构建完整路径
+ oss_file_path = f"{oss_folder}/{spcc_folder}/{unique_filename}"
+
+ # 5.2 上传到OSS
+ tupian_url = upload_to_oss(shangpin_tupian, oss_file_path)
+
+ if not tupian_url:
+ # 上传失败,抛出异常触发回滚
+ raise Exception("商品图片上传到OSS失败")
+
+ # 5.3 更新商品的图片URL(存储相对路径)
+ shangpin_obj.tupian_url = oss_file_path # 存相对路径
+ shangpin_obj.save()
+
+ except Exception as oss_error:
+ # 图片上传失败,事务会回滚,商品记录也会被删除
+ #print(f"商品图片OSS上传失败: {oss_error}")
+ # 手动抛出异常触发事务回滚
+ raise Exception(f"商品图片上传失败: {str(oss_error)}")
+
+ # === 6. 构建返回数据 ===
+ response_data = {
+ 'shangpin_id': shangpin_id,
+ 'biaoti': biaoti,
+ 'jiage': str(jiage_value),
+ 'kucun': kucun_value,
+ 'leixing_id': leixing_id,
+ 'zhuanqu_id': zhuanqu_id,
+ 'xiaoliang': duiwai_xiaoliang_value,
+ 'jieshao': jieshao,
+ 'xuzhi': xuzhi,
+ 'yaoqiuleixing': yaoqiuleixing_int,
+ 'huiyuan_id': huiyuan_id_value,
+ 'yongjin': str(yongjin_value) if yongjin_value else None,
+ 'tupian_url': shangpin_obj.tupian_url # 相对路径
+ }
+
+ # 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '商品发布成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 捕获所有异常,返回500错误
+ traceback.print_exc()
+ return Response({
+ 'code': 500,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class GuizeTupianShangchuanView(APIView):
+ """
+ 规则图片上传接口
+ 接收商品ID和规则图片
+ 需要管理员权限
+ """
+ permission_classes = [permissions.IsAuthenticated]
+ parser_classes = (MultiPartParser, FormParser) # 需要处理文件上传
+
+ def post(self, request):
+ try:
+ # === 1. 权限验证 ===
+ user_main = request.user
+
+ # 1.1 验证账号(使用phone字段)
+ zhanghao = request.data.get('zhanghao', '').strip()
+ if not zhanghao:
+ return Response(
+ {'code': 401, 'msg': '未提供账号信息', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if user_main.Phone != zhanghao:
+ return Response(
+ {'code': 401, 'msg': '账号验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.2 验证是否为管理员(检查user_type字段)
+ if user_main.UserType != 'admin':
+ return Response(
+ {'code': 401, 'msg': '权限不足', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.3 验证管理员扩展表(使用AdminProfile模型)
+ try:
+ admin_profile = AdminProfile.query.get(user=user_main)
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'msg': '管理员信息不存在', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # === 2. 接收并验证字段 ===
+ # 2.1 验证商品ID
+ shangpin_id = request.data.get('shangpin_id')
+ if not shangpin_id:
+ return Response(
+ {'code': 400, 'msg': '商品ID不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2.2 验证规则图片
+ guize_tupian = request.FILES.get('guize_tupian')
+ if not guize_tupian:
+ return Response(
+ {'code': 400, 'msg': '请上传规则图片', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证图片格式和大小
+ is_valid, error_msg = validate_image(guize_tupian)
+ if not is_valid:
+ return Response(
+ {'code': 400, 'msg': f'规则图片验证失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # === 3. 验证商品是否存在 ===
+ try:
+ shangpin_obj = Shangpin.query.get(id=shangpin_id)
+ except Shangpin.DoesNotExist:
+ return Response(
+ {'code': 400, 'msg': '商品不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # === 4. 处理规则图片上传 ===
+ try:
+ # 4.1 生成文件名和路径
+ file_extension = os.path.splitext(guize_tupian.name)[1].lower()
+ if not file_extension:
+ file_extension = '.jpg'
+
+ # 构建OSS文件路径:a_long/Spgz{shangpin_id}/{filename}
+ oss_folder = 'a_long' # 根文件夹
+ spgz_folder = f"Spgz{shangpin_id}" # 规则图片文件夹
+
+ # 生成文件名:Spgz{shangpin_id}{file_extension}
+ unique_filename = f"Spgz{shangpin_id}{file_extension}"
+
+ # 构建完整路径
+ oss_file_path = f"{oss_folder}/{spgz_folder}/{unique_filename}"
+
+ # 4.2 上传到OSS
+ tupian_url = upload_to_oss(guize_tupian, oss_file_path)
+
+ if not tupian_url:
+ # 上传失败
+ return Response({
+ 'code': 500,
+ 'msg': '规则图片上传失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 4.3 更新商品的规则图片URL(存储相对路径)
+ shangpin_obj.guize_tupian = oss_file_path # 存相对路径
+ shangpin_obj.save()
+
+ except Exception as oss_error:
+ # OSS上传失败
+ print(f"规则图片OSS上传失败: {oss_error}")
+ return Response({
+ 'code': 500,
+ 'msg': f'规则图片上传失败: {str(oss_error)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # === 5. 构建返回数据 ===
+ response_data = {
+ 'shangpin_id': shangpin_id,
+ 'guize_tupian_url': oss_file_path # 相对路径
+ }
+
+ # 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '规则图片上传成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 捕获所有异常,返回500错误
+ traceback.print_exc()
+ return Response({
+ 'code': 500,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+class AdHuiyuanHqView(APIView):
+ """
+ 管理员获取会员信息和商品类型接口
+ 需要管理员权限
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # === 1. 权限验证 ===
+ # 获取当前用户
+ user_main = request.user
+
+ # 1.1 验证前端传递的账号是否匹配(使用phone字段)
+ zhanghao = request.data.get('zhanghao', '').strip()
+ if not zhanghao:
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 验证账号是否匹配(使用phone字段比较)
+ if user_main.Phone != zhanghao:
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.2 验证是否为管理员(检查user_type字段)
+ if user_main.UserType != 'admin':
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.3 验证管理员扩展表(使用AdminProfile模型)
+ try:
+ admin_profile = AdminProfile.query.get(user=user_main)
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # === 2. 查询会员表数据 ===
+ # 查询所有会员数据,按创建时间倒序排列
+ huiyuan_queryset = Huiyuan.query.all().order_by('-CreateTime')
+
+ # 构建会员数据列表
+ huiyuan_list = []
+ for huiyuan in huiyuan_queryset:
+ huiyuan_data = {
+ 'huiyuan_id': huiyuan.huiyuan_id, # 会员ID
+ 'jieshao': huiyuan.jieshao, # 会员介绍(会员标题)
+ 'jtjieshao': huiyuan.jtjieshao if huiyuan.jtjieshao else '', # 会员规则介绍(具体介绍)
+ 'jiage': str(huiyuan.jiage), # 会员价格(转换为字符串,避免Decimal序列化问题)
+ 'guanshifc': str(huiyuan.guanshifc), # 管事分成(转换为字符串)
+ 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数(销量)
+ 'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None,
+ # 创建时间
+ 'UpdateTime': huiyuan.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.UpdateTime else None
+ # 更新时间
+ }
+ huiyuan_list.append(huiyuan_data)
+
+ # === 3. 查询商品类型表数据 ===
+ # 筛选抢单要求类型等于数字1的所有数据(yaoqiuleixing=1)
+ shangpinleixing_queryset = ShangpinLeixing.query.filter(
+ yaoqiuleixing=1 # 抢单要求类型为1(会员抢单)
+ ).order_by('id') # 按ID排序
+
+ # 构建商品类型数据列表
+ shangpinleixing_list = []
+ for leixing in shangpinleixing_queryset:
+ # 注意:这里只需要特定的字段
+ leixing_data = {
+ 'id': leixing.id, # 商品类型ID
+ 'huiyuan_id': leixing.huiyuan_id, # 会员ID(关联字段)
+ 'jieshao': leixing.jieshao, # 类型介绍(商品类型名称)
+ 'tupian_url': leixing.tupian_url if leixing.tupian_url else '' # 商品类型图片相对URL
+ }
+ shangpinleixing_list.append(leixing_data)
+
+ # === 4. 构建返回数据 ===
+ response_data = {
+ 'huiyuanList': huiyuan_list,
+ 'shangpinleixingList': shangpinleixing_list
+ }
+
+ # 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '获取数据成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 捕获所有异常,返回500错误
+ traceback.print_exc()
+ return Response({
+ 'code': 500,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+class HuiyuanXiugaiView(APIView):
+ """
+ 会员修改接口
+ 需要管理员权限
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # === 1. 权限验证 ===
+ user_main = request.user
+
+ # 1.1 验证前端传递的账号是否匹配(使用phone字段)
+ zhanghao = request.data.get('zhanghao', '').strip()
+ if not zhanghao:
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if user_main.Phone != zhanghao:
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.2 验证是否为管理员(检查user_type字段)
+ if user_main.UserType != 'admin':
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.3 验证管理员扩展表
+ try:
+ admin_profile = AdminProfile.query.get(user=user_main)
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # === 2. 接收并验证必填字段 ===
+ huiyuan_id = request.data.get('huiyuan_id', '').strip()
+ if not huiyuan_id:
+ return Response(
+ {'code': 400, 'msg': '会员ID不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # === 3. 查询会员数据 ===
+ try:
+ huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id)
+ except Huiyuan.DoesNotExist:
+ return Response(
+ {'code': 400, 'msg': '会员不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # === 4. 更新会员信息 ===
+ # 4.1 获取前端传递的字段(只更新有值的字段)
+ jieshao = request.data.get('jieshao', '').strip()
+ jtjieshao = request.data.get('jtjieshao', '').strip()
+ jiage = request.data.get('jiage', '').strip()
+ guanshifc = request.data.get('guanshifc', '').strip()
+
+ # 4.2 更新字段
+ if jieshao:
+ # 验证名称长度(30个字符以内)
+ if len(jieshao) > 30:
+ return Response(
+ {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ huiyuan_obj.jieshao = jieshao
+
+ if jtjieshao:
+ # 验证介绍长度(没有长度限制,但做一下基本检查)
+ if len(jtjieshao) > 2000:
+ return Response(
+ {'code': 400, 'msg': '会员介绍过长', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ huiyuan_obj.jtjieshao = jtjieshao
+
+ if jiage:
+ try:
+ jiage_value = float(jiage)
+ if jiage_value < 0:
+ return Response(
+ {'code': 400, 'msg': '会员价格不能为负数', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ if jiage_value > 100000:
+ return Response(
+ {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ huiyuan_obj.jiage = jiage_value
+ except ValueError:
+ return Response(
+ {'code': 400, 'msg': '会员价格格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ if guanshifc:
+ try:
+ guanshifc_value = float(guanshifc)
+ if guanshifc_value < 0:
+ return Response(
+ {'code': 400, 'msg': '管事分成不能为负数', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ # 检查管事分成是否超过会员价格
+ if hasattr(huiyuan_obj, 'jiage') and huiyuan_obj.jiage:
+ if guanshifc_value > float(huiyuan_obj.jiage):
+ return Response(
+ {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ huiyuan_obj.guanshifc = guanshifc_value
+ except ValueError:
+ return Response(
+ {'code': 400, 'msg': '管事分成格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 4.3 检查是否有任何字段需要更新
+ if not (jieshao or jtjieshao or jiage or guanshifc):
+ return Response(
+ {'code': 400, 'msg': '没有需要更新的字段', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 4.4 保存更新
+ huiyuan_obj.save()
+
+ # === 5. 构建返回数据 ===
+ response_data = {
+ 'huiyuan_id': huiyuan_obj.huiyuan_id,
+ 'jieshao': huiyuan_obj.jieshao,
+ 'jtjieshao': huiyuan_obj.jtjieshao if huiyuan_obj.jtjieshao else '',
+ 'jiage': str(huiyuan_obj.jiage),
+ 'guanshifc': str(huiyuan_obj.guanshifc),
+ 'goumai_cishu': huiyuan_obj.goumai_cishu,
+ 'CreateTime': huiyuan_obj.CreateTime.strftime(
+ '%Y-%m-%d %H:%M:%S') if huiyuan_obj.CreateTime else None,
+ 'UpdateTime': huiyuan_obj.UpdateTime.strftime(
+ '%Y-%m-%d %H:%M:%S') if huiyuan_obj.UpdateTime else None
+ }
+
+ # 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '会员信息修改成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 捕获所有异常,返回500错误
+ traceback.print_exc()
+ return Response({
+ 'code': 500,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+
+
+class AdHuiyuanFbView(APIView):
+ """
+ 会员发布接口
+ 需要管理员权限
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # === 1. 权限验证 ===
+ user_main = request.user
+
+ # 1.1 验证前端传递的账号是否匹配
+ zhanghao = request.data.get('zhanghao', '').strip()
+ if not zhanghao:
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if user_main.Phone != zhanghao:
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.2 验证是否为管理员
+ if user_main.UserType != 'admin':
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 1.3 验证管理员扩展表
+ try:
+ admin_profile = AdminProfile.query.get(user=user_main)
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'msg': '权限验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # === 2. 接收并验证字段 ===
+ jieshao = request.data.get('jieshao', '').strip()
+ jtjieshao = request.data.get('jtjieshao', '').strip()
+ jiage = request.data.get('jiage', '').strip()
+ guanshifc = request.data.get('guanshifc', '').strip()
+
+ # 2.1 验证必填字段
+ if not jieshao:
+ return Response(
+ {'code': 400, 'msg': '会员名称不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ if not jiage:
+ return Response(
+ {'code': 400, 'msg': '会员价格不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ if not guanshifc:
+ return Response(
+ {'code': 400, 'msg': '管事分成不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2.2 验证字段长度和格式
+ if len(jieshao) > 30:
+ return Response(
+ {'code': 400, 'msg': '会员名称不能超过30个字符', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ if jtjieshao and len(jtjieshao) > 2000:
+ return Response(
+ {'code': 400, 'msg': '会员介绍过长', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2.3 验证价格和分成格式
+ try:
+ jiage_value = float(jiage)
+ if jiage_value <= 0:
+ return Response(
+ {'code': 400, 'msg': '会员价格必须大于0', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ if jiage_value > 100000:
+ return Response(
+ {'code': 400, 'msg': '会员价格不能超过10万元', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except ValueError:
+ return Response(
+ {'code': 400, 'msg': '会员价格格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ try:
+ guanshifc_value = float(guanshifc)
+ if guanshifc_value < 0:
+ return Response(
+ {'code': 400, 'msg': '管事分成不能为负数', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ if guanshifc_value > jiage_value:
+ return Response(
+ {'code': 400, 'msg': '管事分成不能超过会员价格', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except ValueError:
+ return Response(
+ {'code': 400, 'msg': '管事分成格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # === 3. 生成会员ID ===
+ # 生成6位唯一会员ID,格式:V + 5位随机数字字母
+ def generate_huiyuan_id():
+ # 使用V开头,后跟5位随机字符(数字和大写字母)
+ chars = string.digits + string.ascii_uppercase
+ while True:
+ random_part = ''.join(random.choices(chars, k=5))
+ huiyuan_id = f"V{random_part}"
+ # 检查是否已存在
+ if not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists():
+ return huiyuan_id
+
+ huiyuan_id = generate_huiyuan_id()
+
+ # === 4. 创建会员记录 ===
+ with transaction.atomic():
+ huiyuan_obj = Huiyuan(
+ huiyuan_id=huiyuan_id,
+ jieshao=jieshao,
+ jtjieshao=jtjieshao if jtjieshao else '',
+ jiage=jiage_value,
+ guanshifc=guanshifc_value,
+ goumai_cishu=0, # 初始购买次数为0
+ )
+
+ huiyuan_obj.save()
+
+ # === 5. 构建返回数据 ===
+ response_data = {
+ 'huiyuan_id': huiyuan_obj.huiyuan_id,
+ 'jieshao': huiyuan_obj.jieshao,
+ 'jtjieshao': huiyuan_obj.jtjieshao,
+ 'jiage': str(huiyuan_obj.jiage),
+ 'guanshifc': str(huiyuan_obj.guanshifc),
+ 'goumai_cishu': huiyuan_obj.goumai_cishu,
+ 'CreateTime': huiyuan_obj.CreateTime.strftime(
+ '%Y-%m-%d %H:%M:%S') if huiyuan_obj.CreateTime else None,
+ 'UpdateTime': huiyuan_obj.UpdateTime.strftime(
+ '%Y-%m-%d %H:%M:%S') if huiyuan_obj.UpdateTime else None
+ }
+
+ # 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '会员发布成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 捕获所有异常,返回500错误
+ traceback.print_exc()
+ return Response({
+ 'code': 500,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+class AdShangpinHuQu(APIView):
+ """
+ 后台商品管理页面数据获取接口
+ 路径: /shangpin/adsphq/
+ 方法: POST
+ 权限: JWT认证 + 管理员权限
+ 功能: 获取商品管理页面需要的所有数据
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端传递的账号
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ if not qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 2. 从JWT Token获取当前用户
+ dangqianyonghu = request.user
+
+ # 3. 验证账号匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表数据
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 查询公告与轮播(按俱乐部,默认抢单池)
+ from jituan.services.display_config import get_gonggao_content, get_lunbo_urls
+ shangpingonggao = get_gonggao_content(request, notice_type=1)
+ shangpinlunbo = get_lunbo_urls(request, page_key='order_pool', image_type=1)
+
+ # 8. 查询商品类型数据
+ shangpinleixing_data = ShangpinLeixing.query.all().values(
+ 'id', # 类型ID
+ 'jieshao', # 类型介绍(类型名称)
+ 'tupian_url', # 类型图片URL
+ 'yaoqiuleixing', # 抢单要求类型(1=会员抢单,2=佣金抢单)
+ 'huiyuan_id', # 会员ID(如果yaoqiuleixing=1)
+ 'yongjin' # 佣金要求(如果yaoqiuleixing=2)
+ )
+
+ # 9. 查询商品专区数据
+ shangpinzhuanqu_data = ShangpinZhuanqu.query.all().values(
+ 'id', # 专区ID
+ 'mingzi', # 专区名称
+ 'leixing_id' # 所属类型ID
+ )
+
+ # 10. 查询会员数据
+ huiyuan_data = Huiyuan.query.all().values(
+ 'huiyuan_id', # 会员ID
+ 'jieshao', # 会员介绍(会员名称)
+ 'jiage' # 会员价格
+ )
+
+ # 11. 查询商品数据
+ shangpin_data = Shangpin.query.all().values(
+ 'id', # 商品ID
+ 'biaoqian', # 商品标题/标签
+ 'jiage', # 商品价格
+ 'leixing_id', # 商品类型ID
+ 'zhuanqu_id', # 商品专区ID
+ 'tupian_url', # 商品图片URL
+ 'duiwai_xiaoliang' # 商品对外销量
+ )
+
+ # 12. 构造返回数据
+ response_data = {
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': {
+ 'shangpingonggao': shangpingonggao, # 商品公告
+ 'shangpinlunbo': shangpinlunbo, # 轮播图相对URL数组
+ 'shangpinleixing': list(shangpinleixing_data), # 商品类型列表
+ 'shangpinzhuanqu': list(shangpinzhuanqu_data), # 商品专区列表
+ 'huiyuanliebiao': list(huiyuan_data), # 会员列表
+ 'shangpinliebiao': list(shangpin_data), # 商品列表
+ }
+ }
+
+ return Response(response_data, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ print(f"获取商品管理数据异常: {e}")
+ return Response(
+ {'code': 500, 'message': '服务器内部错误'},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+
+# shangpin/views.py
+
+class AdGonggaoXiuGai(APIView):
+ """
+ 后台编辑公告接口
+ 路径: /shangpin/adggxg/
+ 方法: POST
+ 权限: JWT认证 + 管理员权限
+ 功能: 修改公告内容
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端传递的账号
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ if not qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 2. 从JWT Token获取当前用户
+ dangqianyonghu = request.user
+
+ # 3. 验证账号匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表数据
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 获取公告内容
+ gonggao_neirong = request.data.get('gonggao', '').strip()
+
+ # 7. 验证公告内容
+ if not gonggao_neirong:
+ return Response(
+ {'code': 400, 'message': '公告内容不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ if len(gonggao_neirong) > 300:
+ return Response(
+ {'code': 400, 'message': '公告内容不能超过300字', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 8. 查询公告表,类型为1的公告
+ gonggao_obj = Gonggao.query.filter(NoticeType=1).first()
+
+ # 如果不存在,则创建
+ if not gonggao_obj:
+ gonggao_obj = Gonggao(NoticeType=1)
+
+ # 9. 更新公告内容
+ gonggao_obj.Content = gonggao_neirong
+ gonggao_obj.save()
+
+ # 10. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '公告更新成功',
+ 'data': {
+ 'gonggao': gonggao_neirong
+ }
+ })
+
+ except Exception as e:
+ print(f"编辑公告异常: {e}")
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdZhuanquXiuGai(APIView):
+ """
+ 后台修改专区名称接口
+ 路径: /shangpin/adzqxg/
+ 方法: POST
+ 权限: JWT认证 + 管理员权限
+ 功能: 修改专区名称
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 权限验证
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ if not qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ dangqianyonghu = request.user
+
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 2. 获取专区ID和新的专区名称
+ zhuanqu_id = request.data.get('zhuanqu_id')
+ mingzi = request.data.get('mingzi', '').strip()
+
+ if not zhuanqu_id:
+ return Response(
+ {'code': 400, 'message': '专区ID不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ if not mingzi:
+ return Response(
+ {'code': 400, 'message': '专区名称不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ if len(mingzi) > 10:
+ return Response(
+ {'code': 400, 'message': '专区名称不能大于10个字', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 3. 查询专区
+ try:
+ zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id)
+ except ShangpinZhuanqu.DoesNotExist:
+ return Response(
+ {'code': 400, 'message': '专区不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 4. 更新专区名称
+ zhuanqu_obj.mingzi = mingzi
+ zhuanqu_obj.save()
+
+ # 5. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '专区名称修改成功',
+ 'data': {
+ 'id': zhuanqu_obj.id,
+ 'mingzi': zhuanqu_obj.mingzi,
+ 'leixing_id': zhuanqu_obj.leixing_id
+ }
+ })
+
+ except Exception as e:
+ print(f"修改专区名称异常: {e}")
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+
+
+class AdLunboXiuGai(APIView):
+ """
+ 后台编辑轮播图接口
+ 路径: /shangpin/adlunboxg/
+ 方法: POST
+ 权限: JWT认证 + 管理员权限
+ 功能: 添加或删除轮播图
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = (MultiPartParser, FormParser,JSONParser)
+
+ def post(self, request):
+ try:
+ # 1. 权限验证
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ if not qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ dangqianyonghu = request.user
+
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'message': '身份验证失败'},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 2. 获取操作类型
+ action = request.data.get('action', '').strip() # 'add' 或 'delete'
+
+ if action not in ['add', 'delete']:
+ return Response(
+ {'code': 400, 'message': '操作类型无效', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 3. 处理删除操作
+ if action == 'delete':
+ tupian_url = request.data.get('tupian_url', '').strip()
+
+ if not tupian_url:
+ return Response(
+ {'code': 400, 'message': '图片URL不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 查询轮播图记录
+ lunbo_obj = Lunbo.query.filter(ImageURL=tupian_url, ImageType=1).first()
+
+ if not lunbo_obj:
+ return Response(
+ {'code': 400, 'message': '轮播图不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 从OSS删除图片
+ delete_success = delete_from_oss(tupian_url)
+
+ if not delete_success:
+ return Response(
+ {'code': 500, 'message': 'OSS删除失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ # 从数据库删除记录
+ lunbo_obj.delete()
+
+ return Response({
+ 'code': 0,
+ 'message': '删除成功',
+ 'data': None
+ })
+
+ # 4. 处理添加操作
+ else: # action == 'add'
+ tupian_file = request.FILES.get('file')
+
+ if not tupian_file:
+
+
+ return Response(
+ {'code': 400, 'message': '请上传图片', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证图片
+ is_valid, error_msg = validate_image(tupian_file)
+ if not is_valid:
+ #print(f"[轮播图上传] 图片验证失败详情: {error_msg}")
+ # 还可以记录文件大小、名称等信息
+ #print(f"[轮播图上传] 问题文件: {tupian_file.name}, Size: {tupian_file.size}")
+ #print(is_valid)
+ return Response(
+ {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 生成文件名和路径
+ file_extension = os.path.splitext(tupian_file.name)[1].lower()
+ if not file_extension:
+ file_extension = '.jpg'
+
+ # 生成6位随机字符串作为文件名
+ random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
+ unique_filename = f"{random_str}{file_extension}"
+
+ # 构建OSS文件路径:a_long/lunbo/{filename}
+ oss_folder = 'a_long'
+ lunbo_folder = 'lunbo'
+
+ oss_file_path = f"{oss_folder}/{lunbo_folder}/{unique_filename}"
+
+ # 上传到OSS
+ tupian_url = upload_to_oss(tupian_file, oss_file_path)
+
+ if not tupian_url:
+ return Response(
+ {'code': 500, 'message': '图片上传失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ # 创建轮播图记录
+ lunbo_obj = Lunbo.query.create(
+ ImageURL=oss_file_path, # 存相对路径
+ ImageType=1
+ )
+
+ return Response({
+ 'code': 0,
+ 'message': '添加成功',
+ 'data': {
+ 'tupian_url': lunbo_obj.ImageURL
+ }
+ })
+
+ except Exception as e:
+ #print(f"编辑轮播图异常: {e}")
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+
+
+
+class AdLeixingXiuGai(APIView):
+ """
+ 后台编辑商品类型接口 - 优化版
+ 支持:只改图片、只改字段、两者都改
+ 拒绝:两者都不改
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = (MultiPartParser, FormParser, JSONParser) # 添加JSONParser支持
+
+ def post(self, request):
+ try:
+ # 1. 权限验证(保持不变)
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ if not qianzhanghao:
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ dangqianyonghu = request.user
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if dangqianyonghu.UserType != 'admin':
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AdminProfile.DoesNotExist:
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 获取商品类型ID
+ leixing_id = request.data.get('leixing_id')
+ if not leixing_id:
+ return Response({'code': 400, 'message': '商品类型ID不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 查询商品类型
+ try:
+ leixing_obj = ShangpinLeixing.query.get(id=leixing_id)
+ except ShangpinLeixing.DoesNotExist:
+ return Response({'code': 400, 'message': '商品类型不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 获取字段并初始化变更标记
+ has_changed = False
+ jieshao = request.data.get('jieshao', '').strip()
+ yaoqiuleixing = request.data.get('yaoqiuleixing')
+ huiyuan_id = request.data.get('huiyuan_id', '').strip()
+ yongjin = request.data.get('yongjin', '').strip()
+ tupian_url = request.data.get('tupian_url', '').strip() # 原有图片URL
+
+ # 5. 检查非图片字段是否有变更
+ if jieshao and jieshao != leixing_obj.jieshao:
+ leixing_obj.jieshao = jieshao
+ has_changed = True
+
+ if yaoqiuleixing and int(yaoqiuleixing) != leixing_obj.yaoqiuleixing:
+ leixing_obj.yaoqiuleixing = int(yaoqiuleixing)
+ has_changed = True
+
+ # 根据类型清空另一个字段
+ if leixing_obj.yaoqiuleixing == 1:
+ leixing_obj.yongjin = None
+ if huiyuan_id:
+ leixing_obj.huiyuan_id = huiyuan_id
+ # 验证会员存在
+ try:
+ Huiyuan.query.get(huiyuan_id=huiyuan_id)
+ except Huiyuan.DoesNotExist:
+ return Response({'code': 400, 'msg': '会员ID不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+ else: # 类型2
+ leixing_obj.huiyuan_id = None
+ if yongjin:
+ try:
+ leixing_obj.yongjin = float(yongjin)
+ if leixing_obj.yongjin <= 0:
+ return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+ except ValueError:
+ return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # ===== 在现有代码的第5步后面添加 =====
+
+ # 5.5 检查抢单要求类型不变时,会员ID或佣金是否有变更
+ if yaoqiuleixing and int(yaoqiuleixing) == leixing_obj.yaoqiuleixing:
+ # 类型没有变化,但需要检查对应字段的变化
+
+ # 情况1:会员抢单 (yaoqiuleixing == 1)
+ if leixing_obj.yaoqiuleixing == 1:
+ # 检查会员ID是否变更
+ if huiyuan_id and huiyuan_id != leixing_obj.huiyuan_id:
+ # 验证会员存在
+ try:
+ Huiyuan.query.get(huiyuan_id=huiyuan_id)
+ except Huiyuan.DoesNotExist:
+ return Response({'code': 400, 'msg': '会员ID不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ leixing_obj.huiyuan_id = huiyuan_id
+ has_changed = True
+ # print(f"会员ID变更: {leixing_obj.huiyuan_id} -> {huiyuan_id}")
+
+ # 情况2:佣金抢单 (yaoqiuleixing == 2)
+ elif leixing_obj.yaoqiuleixing == 2:
+ # 检查佣金是否变更
+ if yongjin:
+ try:
+ new_yongjin = float(yongjin)
+ if new_yongjin <= 0:
+ return Response({'code': 400, 'message': '佣金金额必须大于0', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 判断是否有变化(考虑None的情况)
+ current_yongjin = leixing_obj.yongjin
+ if (current_yongjin is None and new_yongjin != 0) or \
+ (current_yongjin is not None and abs(
+ float(current_yongjin) - new_yongjin) > 0.001):
+ leixing_obj.yongjin = new_yongjin
+ has_changed = True
+ # print(f"佣金变更: {current_yongjin} -> {new_yongjin}")
+
+ except ValueError:
+ return Response({'code': 400, 'message': '佣金金额格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # ===== 添加结束 =====
+
+ # 6. 处理图片上传(如果有新图片)
+ tupian_file = request.FILES.get('file')
+ if tupian_file:
+ # 验证图片
+ is_valid, error_msg = validate_image(tupian_file)
+ if not is_valid:
+ return Response({'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 删除旧图片(如果存在)
+ if leixing_obj.tupian_url:
+ delete_from_oss(leixing_obj.tupian_url)
+
+ # 生成文件名和路径
+ file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg'
+ oss_folder = 'a_long'
+ leixing_folder = 'leixing'
+
+ # 使用原文件名或生成新文件名
+ if leixing_obj.tupian_url:
+ original_filename = leixing_obj.tupian_url.split('/')[-1]
+ else:
+ original_filename = f"{leixing_id}_{uuid.uuid4().hex}{file_extension}"
+
+ oss_file_path = f"{oss_folder}/{leixing_folder}/{leixing_id}/{original_filename}"
+
+ # 上传到OSS
+ new_tupian_url = upload_to_oss(tupian_file, oss_file_path)
+ if not new_tupian_url:
+ return Response({'code': 500, 'message': '图片上传失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ leixing_obj.tupian_url = oss_file_path # 存相对路径
+ has_changed = True
+ elif tupian_url and tupian_url != leixing_obj.tupian_url:
+ # 前端传了新URL(可能是从其他地方选择的)
+ leixing_obj.tupian_url = tupian_url
+ has_changed = True
+
+ # 7. 验证是否至少有变更
+ if not has_changed:
+ return Response({
+ 'code': 400,
+ 'message': '没有检测到任何更改',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 8. 保存并返回
+ leixing_obj.save()
+ # 8. 保存商品类型
+ leixing_obj.save()
+
+ # 🔴 ===========【新增:同步更新关联商品】===========
+ try:
+ # 1. 准备商品更新字典
+ shangpin_update_fields = {}
+
+ # 始终同步抢单要求类型
+ shangpin_update_fields['yaoqiuleixing'] = leixing_obj.yaoqiuleixing
+
+ # 根据抢单类型,同步对应字段并清空另一个
+ if leixing_obj.yaoqiuleixing == 1: # 会员抢单
+ if leixing_obj.huiyuan_id: # 仅当会员ID非空时更新
+ shangpin_update_fields['huiyuan_id'] = leixing_obj.huiyuan_id
+ # 清空佣金字段
+ shangpin_update_fields['yongjin'] = None
+ elif leixing_obj.yaoqiuleixing == 2: # 佣金抢单
+ if leixing_obj.yongjin: # 仅当佣金非空时更新
+ shangpin_update_fields['yongjin'] = leixing_obj.yongjin
+ # 清空会员字段
+ shangpin_update_fields['huiyuan_id'] = None
+
+ # 2. 执行批量更新(确保只有实际有变化的字段才会执行)
+ if shangpin_update_fields:
+ # 查询所有属于该类型的商品(使用filter而非all以提高效率)
+ affected_count = Shangpin.query.filter(
+ leixing_id=leixing_obj.id
+ ).update(**shangpin_update_fields)
+
+ # 可选:记录日志以便追踪
+ # logger.info(f"商品类型{leixing_obj.id}修改成功,已同步更新{affected_count}个关联商品")
+
+ except Exception as sync_error:
+ # 商品同步失败不影响商品类型本身的修改,但记录错误
+ # 可根据需求决定是否抛出异常回滚整个事务
+ # 当前设计:记录错误但允许商品类型修改成功
+ logger.error(f"修改十五")
+
+ # 可在此添加更详细的错误日志记录
+ # 🔴 ===========【新增结束】===========
+
+
+
+
+
+
+
+ response_data = {
+ 'id': leixing_obj.id,
+ 'jieshao': leixing_obj.jieshao,
+ 'tupian_url': leixing_obj.tupian_url,
+ 'yaoqiuleixing': leixing_obj.yaoqiuleixing,
+ 'huiyuan_id': leixing_obj.huiyuan_id,
+ 'yongjin': str(leixing_obj.yongjin) if leixing_obj.yongjin else None
+ }
+
+ return Response({
+ 'code': 0,
+ 'message': '商品类型修改成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ print(f"编辑商品类型异常: {e}")
+ return Response({'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
+class AdShangpinNeiRongHuoQu(APIView):
+ """
+ 后台获取商品详情接口
+ 路径: /shangpin/adspnrhq/
+ 方法: POST
+ 权限: JWT认证 + 管理员权限
+ 功能: 获取指定商品的详细信息
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端数据
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ shangpin_id = request.data.get('shangpin_id')
+
+ if not qianzhanghao or not shangpin_id:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. 权限验证
+ dangqianyonghu = request.user
+
+ # 验证phone字段匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ logger.warning(
+ f"账号不匹配: 当前用户phone={getattr(dangqianyonghu, 'Phone', '无')}, 前端账号={qianzhanghao}")
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 验证用户类型
+ if dangqianyonghu.UserType != 'admin':
+ logger.warning(f"非管理员访问: user_type={dangqianyonghu.UserType}")
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 3. 验证管理员扩展表
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AdminProfile.DoesNotExist:
+ logger.warning(f"管理员扩展表不存在: user_id={dangqianyonghu.id}")
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 查询商品数据
+ try:
+ shangpin_obj = Shangpin.query.get(id=shangpin_id)
+ except Shangpin.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '商品不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 5. 构建返回数据(与前端字段对应)
+ response_data = {
+ 'tupian_url': shangpin_obj.tupian_url or '', # 商品图片相对URL
+ 'biaoti': shangpin_obj.biaoqian or '', # 商品标题(前端用biaoti,对应biaoqian)
+ 'jieshao': shangpin_obj.jieshao or '', # 商品介绍
+ 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00', # 商品价格
+ 'kucun': shangpin_obj.kucun, # 商品库存
+ 'zhenshixiaoliang': shangpin_obj.zhenshi_xiaoliang, # 商品真实销量
+ 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang, # 商品对外销量
+ 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '', # 下单须知
+ 'guizetupian_url': shangpin_obj.guize_tupian or '', # 规则图片相对URL
+ 'yajin': str(shangpin_obj.yajin) if hasattr(shangpin_obj, 'yajin') and shangpin_obj.yajin else None,
+ # 押金(可能不存在)
+ 'chuangjianshijian': shangpin_obj.CreateTime.strftime(
+ '%Y-%m-%d %H:%M:%S') if shangpin_obj.CreateTime else '',
+ 'genggaishijian': shangpin_obj.UpdateTime.strftime(
+ '%Y-%m-%d %H:%M:%S') if shangpin_obj.UpdateTime else '',
+ # 额外字段,前端可能不需要但可以保留
+ 'leixing_id': shangpin_obj.leixing_id,
+ 'zhuanqu_id': shangpin_obj.zhuanqu_id,
+ 'yaoqiuleixing': shangpin_obj.yaoqiuleixing,
+ 'huiyuan_id': shangpin_obj.huiyuan_id,
+ 'yongjin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None,
+ }
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ logger.error(f"获取商品详情异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+
+
+
+
+
+class AdShangpinXiuGai(APIView):
+ """
+ 后台更改商品数据接口 - 修复版
+ 修复:1. 每次上传生成新文件名 2. 解决浏览器缓存问题
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = (MultiPartParser, FormParser, JSONParser)
+
+ def post(self, request):
+ try:
+ # 1. 权限验证
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ shangpin_id = request.data.get('shangpin_id')
+
+ if not qianzhanghao or not shangpin_id:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证身份
+ dangqianyonghu = request.user
+
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 2. 判断操作类型:删除还是修改
+ action = request.data.get('action', 'update').strip().lower()
+
+ if action == 'delete':
+ return self._delete_shangpin(shangpin_id)
+ else:
+ return self._update_shangpin(request, shangpin_id)
+
+ except Exception as e:
+ logger.error(f"更改商品数据异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ def _delete_shangpin(self, shangpin_id):
+ """删除商品逻辑"""
+ try:
+ # 查询商品
+ try:
+ shangpin_obj = Shangpin.query.get(id=shangpin_id)
+ except Shangpin.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '商品不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 获取图片URL(如果有)
+ tupian_url = shangpin_obj.tupian_url
+ guizetupian_url = shangpin_obj.guize_tupian
+
+ # 异步或同步删除OSS图片
+ if tupian_url:
+ try:
+ delete_from_oss(tupian_url)
+ except Exception as e:
+ logger.warning(f"删除商品图片失败: {tupian_url}, 错误: {e}")
+
+ if guizetupian_url:
+ try:
+ delete_from_oss(guizetupian_url)
+ except Exception as e:
+ logger.warning(f"删除规则图片失败: {guizetupian_url}, 错误: {e}")
+
+ # 删除数据库记录
+ shangpin_obj.delete()
+
+ return Response({
+ 'code': 0,
+ 'message': '删除成功',
+ 'data': {
+ 'shangpin_id': shangpin_id,
+ 'deleted': True
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"删除商品异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '删除失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ def _update_shangpin(self, request, shangpin_id):
+ """修改商品逻辑 - 修复:每次上传生成新文件名"""
+ try:
+ # 查询商品
+ try:
+ shangpin_obj = Shangpin.query.get(id=shangpin_id)
+ except Shangpin.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '商品不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ has_changes = False
+
+ # 获取原图片URL(如果有)
+ old_tupian_url = shangpin_obj.tupian_url
+
+ # 3. 处理商品图片上传(如果有新商品图片)
+ tupian_file = request.FILES.get('tupian_file')
+ new_tupian_url = None
+
+ if tupian_file:
+ # 验证图片
+ is_valid, error_msg = validate_image(tupian_file)
+ if not is_valid:
+ return Response(
+ {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 生成新文件名:时间戳 + 随机数 + 原扩展名
+ timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳
+ random_num = random.randint(1000, 9999)
+ file_extension = os.path.splitext(tupian_file.name)[1].lower() or '.jpg'
+ new_filename = f"sp_{timestamp}_{random_num}{file_extension}"
+
+ # 确定OSS路径
+ oss_folder = 'a_long'
+ shangpin_folder = 'shangpin'
+ oss_file_path = f"{oss_folder}/{shangpin_folder}/{new_filename}"
+
+ # 删除旧图片(如果存在)
+ if old_tupian_url:
+ try:
+ delete_from_oss(old_tupian_url)
+ logger.info(f"已删除旧商品图片: {old_tupian_url}")
+ except Exception as e:
+ logger.warning(f"删除旧商品图片失败: {old_tupian_url}, 错误: {e}")
+ # 继续上传新图片,不中断流程
+
+ # 上传到OSS
+ new_tupian_url = upload_to_oss(tupian_file, oss_file_path)
+
+ if not new_tupian_url:
+ return Response(
+ {'code': 500, 'message': '商品图片上传失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ # 更新数据库中的图片URL
+ shangpin_obj.tupian_url = oss_file_path # 存相对路径
+ has_changes = True
+ logger.info(f"商品图片更新成功: {oss_file_path}")
+
+ # 4. 处理其他字段(只更新有变化的字段)
+ # 注意:这里同时处理 request.data(文本字段)和可能有文件的情况
+ field_mapping = {
+ 'biaoti': 'biaoqian',
+ 'jieshao': 'jieshao',
+ 'jiage': 'jiage',
+ 'kucun': 'kucun',
+ 'duiwaixiaoliang': 'duiwai_xiaoliang',
+ 'xiadanxuzhi': 'xiadan_xuzhi',
+ 'yajin': 'yajin',
+ }
+
+ for front_field, db_field in field_mapping.items():
+ # 注意:request.data 可能包含文件和其他字段,但DRF会处理好
+ if front_field in request.data:
+ new_value = request.data.get(front_field)
+
+ # 空字符串处理
+ if new_value == '':
+ new_value = None
+
+ if new_value is not None:
+ # 获取当前值
+ current_value = getattr(shangpin_obj, db_field)
+
+ # 类型转换
+ try:
+ if db_field in ['jiage', 'yajin']:
+ # 价格和押金
+ if new_value:
+ new_value = Decimal(str(new_value))
+ else:
+ new_value = None
+ elif db_field in ['kucun', 'duiwai_xiaoliang']:
+ # 库存和对外销量
+ if new_value:
+ new_value = int(float(new_value))
+ else:
+ new_value = 0
+ except (ValueError, TypeError, InvalidOperation) as e:
+ logger.warning(f"字段{front_field}转换失败: {new_value}, 错误: {e}")
+ continue
+
+ # 检查是否有变化(处理None值)
+ if current_value != new_value:
+ setattr(shangpin_obj, db_field, new_value)
+ has_changes = True
+
+ # 5. 如果没有检测到任何更改
+ if not has_changes:
+ return Response({
+ 'code': 400,
+ 'message': '没有检测到任何更改',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 6. 保存更改
+ shangpin_obj.save()
+
+ # 7. 返回更新后的数据
+ response_data = {
+ 'tupian_url': shangpin_obj.tupian_url or '',
+ 'biaoti': shangpin_obj.biaoqian or '',
+ 'jieshao': shangpin_obj.jieshao or '',
+ 'jiage': str(shangpin_obj.jiage) if shangpin_obj.jiage else '0.00',
+ 'kucun': shangpin_obj.kucun,
+ 'duiwaixiaoliang': shangpin_obj.duiwai_xiaoliang,
+ 'xiadanxuzhi': shangpin_obj.xiadan_xuzhi or '',
+ 'yajin': str(shangpin_obj.yongjin) if shangpin_obj.yongjin else None,
+ 'genggaishijian': shangpin_obj.UpdateTime.strftime(
+ '%Y-%m-%d %H:%M:%S') if shangpin_obj.UpdateTime else '',
+ }
+
+ return Response({
+ 'code': 0,
+ 'message': '更新成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ logger.error(f"更新商品异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '更新失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdShangpinGuizeXiuGai(APIView):
+ """
+ 后台规则图片更换接口 - 修复版
+ 修复:每次上传生成新文件名,避免浏览器缓存
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = (MultiPartParser, FormParser, JSONParser)
+
+ def post(self, request):
+ try:
+ # 1. 权限验证
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ shangpin_id = request.data.get('shangpin_id')
+ action = request.data.get('action', 'update').strip().lower()
+
+ if not qianzhanghao or not shangpin_id:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证身份
+ dangqianyonghu = request.user
+
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AdminProfile.DoesNotExist:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 2. 查询商品
+ try:
+ shangpin_obj = Shangpin.query.get(id=shangpin_id)
+ except Shangpin.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '商品不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 3. 处理不同操作
+ if action == 'delete':
+ return self._delete_guize_image(shangpin_obj)
+ elif action == 'update':
+ return self._update_guize_image(request, shangpin_obj)
+ else:
+ return Response(
+ {'code': 400, 'message': '操作类型无效', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ except Exception as e:
+ logger.error(f"规则图片操作异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ def _delete_guize_image(self, shangpin_obj):
+ """删除规则图片"""
+ guizetupian_url = shangpin_obj.guize_tupian
+
+ if guizetupian_url:
+ # 从OSS删除
+ try:
+ delete_success = delete_from_oss(guizetupian_url)
+ '''if delete_success:
+ logger.info(f"已删除规则图片: {guizetupian_url}")
+ else:
+ logger.warning(f"OSS删除失败,但继续删除数据库记录: {guizetupian_url}")'''
+ except Exception as e:
+ logger.warning(f"删除规则图片异常: {e}")
+
+ # 清除数据库记录
+ shangpin_obj.guize_tupian = ''
+ shangpin_obj.save()
+
+ return Response({
+ 'code': 0,
+ 'message': '删除成功',
+ 'data': {
+ 'guizetupian_url': ''
+ }
+ })
+
+ def _update_guize_image(self, request, shangpin_obj):
+ """更新规则图片 - 修复:每次上传生成新文件名"""
+ guize_file = request.FILES.get('guize_file')
+
+ if not guize_file:
+ return Response(
+ {'code': 400, 'message': '请上传规则图片', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证图片
+ is_valid, error_msg = validate_image(guize_file)
+ if not is_valid:
+ return Response(
+ {'code': 400, 'message': f'图片验证失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 获取原规则图片URL
+ old_guizetupian_url = shangpin_obj.guize_tupian
+
+ # 生成新文件名:时间戳 + 随机数 + 原扩展名
+ timestamp = int(time.time() * 1000) % 100000000 # 8位时间戳
+ random_num = random.randint(1000, 9999)
+ file_extension = os.path.splitext(guize_file.name)[1].lower() or '.jpg'
+ new_filename = f"gz_{timestamp}_{random_num}{file_extension}"
+
+ # 确定OSS路径
+ oss_folder = 'a_long'
+ guize_folder = 'guize' # 修改为你的要求:a_long/guize/
+ oss_file_path = f"{oss_folder}/{guize_folder}/{new_filename}"
+
+ # 删除旧图片(如果存在)
+ if old_guizetupian_url:
+ try:
+ delete_from_oss(old_guizetupian_url)
+ #logger.info(f"已删除旧规则图片: {old_guizetupian_url}")
+ except Exception as e:
+ logger.warning(f"删除旧规则图片失败: {old_guizetupian_url}, 错误: {e}")
+ # 继续上传新图片,不中断流程
+
+ # 上传到OSS
+ new_guizetupian_url = upload_to_oss(guize_file, oss_file_path)
+
+ if not new_guizetupian_url:
+ return Response(
+ {'code': 500, 'message': '规则图片上传失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ # 更新数据库(存相对路径)
+ shangpin_obj.guize_tupian = oss_file_path
+ shangpin_obj.save()
+
+ #logger.info(f"规则图片更新成功: {oss_file_path}")
+
+ return Response({
+ 'code': 0,
+ 'message': '规则图片更新成功',
+ 'data': {
+ 'guizetupian_url': oss_file_path # 返回相对路径
+ }
+ })
+
+
+
+
+# views.py
+
+
diff --git a/products/views/yajin_payment.py b/products/views/yajin_payment.py
new file mode 100644
index 0000000..7a5491b
--- /dev/null
+++ b/products/views/yajin_payment.py
@@ -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"""
+{APPID}
+{PAY_DESCRIPTION}
+{MCHID}
+{nonce_str}
+{NOTIFY_URL}
+{openid}
+{dingdanid}
+{user_ip}
+{total_fee}
+{trade_type}
+{sign}
+"""
+
+ # 调用微信统一下单接口(官方地址)
+ 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"""
+
+
+"""
+
+ 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
+
+
diff --git a/rank/views.py b/rank/views.py
deleted file mode 100644
index 3eb7c35..0000000
--- a/rank/views.py
+++ /dev/null
@@ -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)
\ No newline at end of file
diff --git a/rank/views/__init__.py b/rank/views/__init__.py
new file mode 100644
index 0000000..c25c1bf
--- /dev/null
+++ b/rank/views/__init__.py
@@ -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,
+)
diff --git a/rank/views/biaoqian.py b/rank/views/biaoqian.py
new file mode 100644
index 0000000..dde3b3b
--- /dev/null
+++ b/rank/views/biaoqian.py
@@ -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)
diff --git a/rank/views/kaohe_jilu.py b/rank/views/kaohe_jilu.py
new file mode 100644
index 0000000..aa4294b
--- /dev/null
+++ b/rank/views/kaohe_jilu.py
@@ -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)
\ No newline at end of file
diff --git a/rank/views/kaoheguan.py b/rank/views/kaoheguan.py
new file mode 100644
index 0000000..534a258
--- /dev/null
+++ b/rank/views/kaoheguan.py
@@ -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)
diff --git a/rank/views/shenhe.py b/rank/views/shenhe.py
new file mode 100644
index 0000000..3018704
--- /dev/null
+++ b/rank/views/shenhe.py
@@ -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)
diff --git a/rename_columns.sql b/rename_columns.sql
deleted file mode 100644
index bd35c2e..0000000
--- a/rename_columns.sql
+++ /dev/null
@@ -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;
-
--- 完成
\ No newline at end of file
diff --git a/shop/views.py b/shop/views.py
deleted file mode 100644
index da3d54b..0000000
--- a/shop/views.py
+++ /dev/null
@@ -1,3124 +0,0 @@
-# ==================== 标准库 ====================
-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
-
-
-
-
-
-
-class ShopProductConfigView(APIView):
- """
- 获取商品发布所需配置:公共类型、店铺商品类型、专区及各自商品数量
- URL: /shangdian/dphqspxx
- 权限: JWT认证 + 店铺归属验证
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 身份验证,获取店铺对象
- frontend_user_id = request.data.get('userId')
- if not frontend_user_id:
- return Response({'code': 400, 'msg': '缺少必要参数 userId'})
-
- dianpu, error = verify_shop_permission(request, frontend_user_id)
- if error:
- return error
-
- # ---------- 2. 公共商品类型(需审核正常) ----------
- try:
- public_types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values(
- 'id', 'jieshao', 'tupian_url'
- )
- public_types = [{
- 'id': pt['id'],
- 'jieshao': pt['jieshao'] or '',
- 'tupian_url': pt['tupian_url'] or '',
- } for pt in public_types_qs]
- except Exception as e:
- logger.exception("查询公共商品类型失败")
- return Response({'code': 500, 'msg': '服务器错误'})
-
- # ---------- 3. 店铺商品类型 ----------
- try:
- shop_types = ShangpinLeixingDianpu.query.filter(
- dianpu_id=dianpu.id
- ).select_related('gonggong_leixing').order_by('-paixu')
- except Exception as e:
- logger.exception("查询店铺商品类型失败")
- return Response({'code': 500, 'msg': '服务器错误'})
-
- type_ids = []
- shop_product_types = []
- for t in shop_types:
- shop_product_types.append({
- 'id': t.id,
- 'jieshao': t.jieshao or '',
- 'tupian_url': t.tupian_url or '',
- 'paixu': t.paixu,
- 'shangjia_zhuangtai': t.shangjia_zhuangtai,
- 'fengjin_zhuangtai': t.fengjin_zhuangtai,
- 'shenhe_zhuangtai': t.shenhe_zhuangtai,
- 'gonggong_leixing_id': t.gonggong_leixing_id,
- })
- type_ids.append(t.id)
-
- # ---------- 4. 各类型商品数量统计 ----------
- type_product_counts = {}
- if type_ids:
- counts = Shangpin.query.filter(
- dianpu_id=dianpu.id,
- dianpu_leixing_id__in=type_ids
- ).values('dianpu_leixing_id').annotate(cnt=Count('id'))
- type_product_counts = {item['dianpu_leixing_id']: item['cnt'] for item in counts}
-
- # ---------- 5. 专区列表 ----------
- try:
- zhuanqu_qs = ShangpinZhuanqu.query.filter(
- dianpu_id=dianpu.id
- ).order_by('-paixu').values(
- 'id', 'mingzi', 'paixu', 'shangjia_zhuangtai',
- 'fengjin_zhuangtai', 'dianpu_leixing_id'
- )
- zhuanqu_list = []
- zhuanqu_ids = []
- for zq in zhuanqu_qs:
- zhuanqu_list.append({
- 'id': zq['id'],
- 'mingzi': zq['mingzi'] or '',
- 'paixu': zq['paixu'],
- 'shangjia_zhuangtai': zq['shangjia_zhuangtai'],
- 'fengjin_zhuangtai': zq['fengjin_zhuangtai'],
- 'dianpu_leixing_id': zq['dianpu_leixing_id'],
- })
- zhuanqu_ids.append(zq['id'])
- except Exception as e:
- logger.exception("查询专区失败")
- return Response({'code': 500, 'msg': '服务器错误'})
-
- # ---------- 6. 各专区商品数量统计 ----------
- zhuanqu_product_counts = {}
- if zhuanqu_ids:
- counts = Shangpin.query.filter(
- dianpu_id=dianpu.id,
- zhuanqu_id__in=zhuanqu_ids
- ).values('zhuanqu_id').annotate(cnt=Count('id'))
- zhuanqu_product_counts = {item['zhuanqu_id']: item['cnt'] for item in counts}
-
- return Response({
- 'code': 0,
- 'msg': 'success',
- 'data': {
- 'public_types': public_types,
- 'shop_product_types': shop_product_types,
- 'zhuanqu_list': zhuanqu_list,
- 'type_product_counts': type_product_counts,
- 'zhuanqu_product_counts': zhuanqu_product_counts,
- }
- })
-
-
-
-
-class ShopProductModifyView(APIView):
- """店铺商品类型与专区管理(增、改、删)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- frontend_user_id = request.data.get('userId') or request.POST.get('userId')
- if not frontend_user_id:
- return Response({'code': 400, 'msg': '缺少必要参数 userId'})
-
- dianpu, error = verify_shop_permission(request, frontend_user_id)
- if error:
- return error
-
- action = request.data.get('action') or request.POST.get('action')
- if not action:
- return Response({'code': 400, 'msg': '缺少操作类型 action'})
-
- handlers = {
- 'add_type': self._add_type,
- 'update_type': self._update_type,
- 'delete_type': self._delete_type,
- 'add_zhuanqu': self._add_zhuanqu,
- 'update_zhuanqu': self._update_zhuanqu,
- 'delete_zhuanqu': self._delete_zhuanqu,
- }
- handler = handlers.get(action)
- if not handler:
- return Response({'code': 400, 'msg': '不支持的操作类型'})
-
- return handler(dianpu, request)
-
- except Exception as e:
- logger.exception("ShopProductModifyView 全局异常")
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
- # ---------- 商品类型操作 ----------
- @transaction.atomic
- def _add_type(self, dianpu, request):
- """添加店铺商品类型"""
- try:
- jieshao = request.data.get('jieshao') or request.POST.get('jieshao')
- gonggong_leixing_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id')
- paixu = request.data.get('paixu', 0) or request.POST.get('paixu', 0)
-
- if not jieshao or not gonggong_leixing_id:
- return Response({'code': 400, 'msg': '缺少介绍或公共类型ID'})
-
- # 验证公共类型存在且正常
- if not ShangpinLeixing.query.filter(id=gonggong_leixing_id, shenhezhuangtai=1).exists():
- return Response({'code': 400, 'msg': '所选公共商品类型无效'})
-
- image_file = request.FILES.get('image')
- if not image_file:
- return Response({'code': 400, 'msg': '必须上传类型图片'})
-
- # 上传图片
- file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png"
- full_url = upload_to_oss(image_file, file_path)
- if not full_url:
- return Response({'code': 500, 'msg': '图片上传失败'})
-
- # 提取相对路径
- tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
-
- # 审核状态
- audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
- shenhe_status = not (audit_config and audit_config.kaiqi_shenhe)
-
- ShangpinLeixingDianpu.query.create(
- dianpu_id=dianpu.id,
- gonggong_leixing_id=gonggong_leixing_id,
- jieshao=jieshao,
- tupian_url=tupian_url,
- paixu=paixu,
- shangjia_zhuangtai=True,
- fengjin_zhuangtai=False,
- shenhe_zhuangtai=shenhe_status,
- )
- return Response({'code': 0, 'msg': '商品类型添加成功'})
- except Exception as e:
- logger.exception("添加商品类型失败")
- return Response({'code': 500, 'msg': '添加失败,请稍后重试'})
-
- @transaction.atomic
- def _update_type(self, dianpu, request):
- """修改店铺商品类型,支持更换图片(先删旧图)"""
- type_id = request.data.get('type_id') or request.POST.get('type_id')
- if not type_id:
- return Response({'code': 400, 'msg': '缺少 type_id'})
-
- try:
- obj = ShangpinLeixingDianpu.objects.select_for_update().get(
- id=type_id, dianpu_id=dianpu.id
- )
- except ShangpinLeixingDianpu.DoesNotExist:
- return Response({'code': 404, 'msg': '商品类型不存在'})
- except Exception as e:
- logger.exception("查询商品类型异常")
- return Response({'code': 500, 'msg': '服务器错误'})
-
- try:
- update_fields = []
- content_changed = False
-
- # 简介
- if 'jieshao' in request.data or 'jieshao' in request.POST:
- obj.jieshao = request.data.get('jieshao') or request.POST.get('jieshao')
- update_fields.append('jieshao')
- content_changed = True
-
- # 映射公共类型
- if 'gonggong_leixing_id' in request.data or 'gonggong_leixing_id' in request.POST:
- new_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id')
- if not ShangpinLeixing.query.filter(id=new_id, shenhezhuangtai=1).exists():
- return Response({'code': 400, 'msg': '公共商品类型无效'})
- obj.gonggong_leixing_id = new_id
- update_fields.append('gonggong_leixing_id')
- content_changed = True
-
- # 排序
- if 'paixu' in request.data or 'paixu' in request.POST:
- obj.paixu = request.data.get('paixu') or request.POST.get('paixu')
- update_fields.append('paixu')
-
- # 上架状态
- if 'shangjia_zhuangtai' in request.data or 'shangjia_zhuangtai' in request.POST:
- val = request.data.get('shangjia_zhuangtai') or request.POST.get('shangjia_zhuangtai')
- if isinstance(val, str):
- val = val.lower() in ('true', '1', 'yes')
- obj.shangjia_zhuangtai = bool(val)
- update_fields.append('shangjia_zhuangtai')
-
- # 图片更换:先删除旧图,再上传新图
- image_file = request.FILES.get('image')
- if image_file:
- if obj.tupian_url:
- delete_from_oss(obj.tupian_url) # 删除旧图片
- file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png"
- full_url = upload_to_oss(image_file, file_path)
- if not full_url:
- return Response({'code': 500, 'msg': '图片上传失败'})
- obj.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
- update_fields.append('tupian_url')
- content_changed = True
-
- # 审核重置
- if content_changed:
- audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
- if audit_config and audit_config.kaiqi_shenhe:
- obj.shenhe_zhuangtai = False
- update_fields.append('shenhe_zhuangtai')
-
- if update_fields:
- obj.save(update_fields=update_fields)
- return Response({'code': 0, 'msg': '商品类型更新成功'})
- except Exception as e:
- logger.exception("修改商品类型失败")
- return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
-
- @transaction.atomic
- def _delete_type(self, dianpu, request):
- """删除店铺商品类型,可选是否同时删除关联商品"""
- type_id = request.data.get('id') or request.POST.get('id')
- if not type_id:
- return Response({'code': 400, 'msg': '缺少类型ID'})
-
- try:
- obj = ShangpinLeixingDianpu.objects.select_for_update().get(
- id=type_id, dianpu_id=dianpu.id
- )
- except ShangpinLeixingDianpu.DoesNotExist:
- return Response({'code': 404, 'msg': '商品类型不存在'})
- except Exception as e:
- logger.exception("查询商品类型异常")
- return Response({'code': 500, 'msg': '服务器错误'})
-
- try:
- delete_products = request.data.get('delete_products') or request.POST.get('delete_products')
- delete_products = str(delete_products).lower() in ('true', '1', 'yes')
-
- if delete_products:
- # 删除该类型下所有关联商品(同时清理图片)
- products = Shangpin.query.filter(
- dianpu_leixing_id=obj.id, dianpu_id=dianpu.id
- )
- for p in products:
- if p.tupian_url:
- delete_from_oss(p.tupian_url)
- if p.guize_tupian:
- delete_from_oss(p.guize_tupian)
- p.delete()
- else:
- # 仅解除关联,不删商品
- Shangpin.query.filter(
- dianpu_leixing_id=obj.id, dianpu_id=dianpu.id
- ).update(dianpu_leixing=None)
-
- # 删除类型自身图片
- if obj.tupian_url:
- delete_from_oss(obj.tupian_url)
- obj.delete()
- return Response({'code': 0, 'msg': '类型删除成功'})
- except Exception as e:
- logger.exception("删除商品类型失败")
- return Response({'code': 500, 'msg': '删除失败,请稍后重试'})
-
- # ---------- 专区操作 ----------
- @transaction.atomic
- def _add_zhuanqu(self, dianpu, request):
- """添加专区,自动从店铺商品类型获取 leixing_id(公共商品类型ID)"""
- try:
- mingzi = request.data.get('mingzi')
- dianpu_leixing_id = request.data.get('dianpu_leixing_id')
- paixu = request.data.get('paixu', 0)
-
- if not mingzi or not dianpu_leixing_id:
- return Response({'code': 400, 'msg': '缺少名称或所属商品类型'})
-
- # 获取店铺商品类型,并拿到映射的公共类型ID
- try:
- shop_type = ShangpinLeixingDianpu.query.get(
- id=dianpu_leixing_id, dianpu_id=dianpu.id
- )
- leixing_id = shop_type.gonggong_leixing_id # 这就是专区需要的 leixing_id
- except ShangpinLeixingDianpu.DoesNotExist:
- return Response({'code': 400, 'msg': '所选商品类型无效'})
-
- # 验证公共类型有效
- if not ShangpinLeixing.query.filter(id=leixing_id, shenhezhuangtai=1).exists():
- return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
-
- ShangpinZhuanqu.query.create(
- dianpu_id=dianpu.id,
- mingzi=mingzi,
- dianpu_leixing_id=dianpu_leixing_id,
- leixing_id=leixing_id, # 自动填充
- paixu=paixu,
- shangjia_zhuangtai=True,
- fengjin_zhuangtai=False,
- )
- return Response({'code': 0, 'msg': '专区添加成功'})
- except Exception as e:
- logger.exception("添加专区失败")
- return Response({'code': 500, 'msg': '添加失败,请稍后重试'})
-
- @transaction.atomic
- def _update_zhuanqu(self, dianpu, request):
- """修改专区,若更换所属商品类型则同步更新 leixing_id"""
- zhuanqu_id = request.data.get('zhuanqu_id')
- if not zhuanqu_id:
- return Response({'code': 400, 'msg': '缺少 zhuanqu_id'})
-
- try:
- obj = ShangpinZhuanqu.objects.select_for_update().get(
- id=zhuanqu_id, dianpu_id=dianpu.id
- )
- except ShangpinZhuanqu.DoesNotExist:
- return Response({'code': 404, 'msg': '专区不存在'})
- except Exception as e:
- logger.exception("查询专区异常")
- return Response({'code': 500, 'msg': '服务器错误'})
-
- try:
- update_fields = []
-
- if 'mingzi' in request.data:
- obj.mingzi = request.data.get('mingzi')
- update_fields.append('mingzi')
-
- if 'dianpu_leixing_id' in request.data:
- new_id = request.data.get('dianpu_leixing_id')
- try:
- shop_type = ShangpinLeixingDianpu.query.get(
- id=new_id, dianpu_id=dianpu.id
- )
- obj.dianpu_leixing_id = new_id
- obj.leixing_id = shop_type.gonggong_leixing_id # 同步更新公共类型ID
- update_fields.extend(['dianpu_leixing_id', 'leixing_id'])
- except ShangpinLeixingDianpu.DoesNotExist:
- return Response({'code': 400, 'msg': '所选商品类型无效'})
-
- if 'paixu' in request.data:
- obj.paixu = request.data.get('paixu')
- update_fields.append('paixu')
-
- if 'shangjia_zhuangtai' in request.data:
- val = request.data.get('shangjia_zhuangtai')
- if isinstance(val, str):
- val = val.lower() in ('true', '1', 'yes')
- obj.shangjia_zhuangtai = bool(val)
- update_fields.append('shangjia_zhuangtai')
-
- if update_fields:
- obj.save(update_fields=update_fields)
- return Response({'code': 0, 'msg': '专区更新成功'})
- except Exception as e:
- logger.exception("修改专区失败")
- return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
-
- @transaction.atomic
- def _delete_zhuanqu(self, dianpu, request):
- """删除专区,可选是否同时删除关联商品"""
- zhuanqu_id = request.data.get('id')
- if not zhuanqu_id:
- return Response({'code': 400, 'msg': '缺少专区ID'})
-
- try:
- obj = ShangpinZhuanqu.objects.select_for_update().get(
- id=zhuanqu_id, dianpu_id=dianpu.id
- )
- except ShangpinZhuanqu.DoesNotExist:
- return Response({'code': 404, 'msg': '专区不存在'})
- except Exception as e:
- logger.exception("查询专区异常")
- return Response({'code': 500, 'msg': '服务器错误'})
-
- try:
- delete_products = request.data.get('delete_products')
- delete_products = str(delete_products).lower() in ('true', '1', 'yes')
-
- if delete_products:
- products = Shangpin.query.filter(
- zhuanqu_id=obj.id, dianpu_id=dianpu.id
- )
- for p in products:
- if p.tupian_url:
- delete_from_oss(p.tupian_url)
- if p.guize_tupian:
- delete_from_oss(p.guize_tupian)
- p.delete()
- else:
- Shangpin.query.filter(
- zhuanqu_id=obj.id, dianpu_id=dianpu.id
- ).update(zhuanqu_id=None)
-
- obj.delete()
- return Response({'code': 0, 'msg': '专区删除成功'})
- except Exception as e:
- logger.exception("删除专区失败")
- return Response({'code': 500, 'msg': '删除失败,请稍后重试'})
-
-
-
-class ShopProductListView(APIView):
- """
- 商品列表接口,支持筛选、分页。
- URL: /shangdian/sdhqspsj
- 权限: JWT认证 + 店铺验证
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 身份验证,获取店铺对象
- frontend_user_id = request.data.get('userId')
- if not frontend_user_id:
- return Response({'code': 400, 'msg': '缺少必要参数 userId'})
-
- dianpu, error = verify_shop_permission(request, frontend_user_id)
- if error:
- return error
-
- # 2. 安全解析分页与筛选参数
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '分页参数格式错误'})
-
- keyword = str(request.data.get('keyword', '')).strip()
- shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型ID
- zhuanqu_id = request.data.get('zhuanqu_id') # 专区ID
-
- # 布尔型筛选字段
- def parse_bool(val):
- if val is None or val == '':
- return None
- if isinstance(val, bool):
- return val
- if isinstance(val, str):
- return val.lower() in ('true', '1', 'yes')
- return bool(val)
-
- shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
- shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
- fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
-
- # 3. 构建查询(只查当前店铺的商品)
- products = Shangpin.query.filter(dianpu_id=dianpu.id).select_related(
- 'dianpu_leixing'
- ).only(
- 'id', 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'zhuanqu_id',
- 'zhenshi_xiaoliang', 'duiwai_xiaoliang', 'jieshao', 'xiadan_xuzhi',
- 'guize_tupian', 'yaoqiuleixing', 'huiyuan_id', 'yongjin',
- 'tupian_url', 'ewai_dashou_fencheng', 'paixu',
- 'dianpu_leixing_id', 'shangjia_zhuangtai', 'fengjin_zhuangtai',
- 'shenhe_zhuangtai', 'CreateTime', 'UpdateTime'
- )
-
- # 筛选
- if shop_product_type_id is not None:
- try:
- products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
- except (ValueError, TypeError):
- return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
-
- if zhuanqu_id is not None:
- try:
- products = products.filter(zhuanqu_id=int(zhuanqu_id))
- except (ValueError, TypeError):
- return Response({'code': 400, 'msg': 'zhuanqu_id 格式错误'})
-
- if shenhe is not None:
- products = products.filter(shenhe_zhuangtai=shenhe)
- if shangjia is not None:
- products = products.filter(shangjia_zhuangtai=shangjia)
- if fengjin is not None:
- products = products.filter(fengjin_zhuangtai=fengjin)
-
- # 关键词搜索(商品ID、标题、介绍)
- if keyword:
- products = products.filter(
- models.Q(id__icontains=keyword) |
- models.Q(biaoqian__icontains=keyword) |
- models.Q(jieshao__icontains=keyword)
- )
-
- # 4. 分页
- total_count = products.count()
- paginator = Paginator(products, page_size)
- page_obj = paginator.get_page(page)
-
- # 5. 构造返回数据
- product_list = []
- for p in page_obj:
- product_list.append({
- 'id': p.id,
- 'biaoqian': p.biaoqian or '',
- 'jiage': p.jiage,
- 'kucun': p.kucun,
- 'leixing_id': p.leixing_id,
- 'zhuanqu_id': p.zhuanqu_id,
- 'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
- 'duiwai_xiaoliang': p.duiwai_xiaoliang,
- 'jieshao': p.jieshao or '',
- 'xiadan_xuzhi': p.xiadan_xuzhi or '',
- 'guize_tupian': p.guize_tupian or '',
- 'yaoqiuleixing': p.yaoqiuleixing,
- 'huiyuan_id': p.huiyuan_id or '',
- 'yongjin': p.yongjin,
- 'tupian_url': p.tupian_url or '',
- 'ewai_dashou_fencheng': p.ewai_dashou_fencheng,
- 'paixu': p.paixu,
- 'dianpu_leixing_id': p.dianpu_leixing_id,
- 'shangjia_zhuangtai': p.shangjia_zhuangtai,
- 'fengjin_zhuangtai': p.fengjin_zhuangtai,
- 'shenhe_zhuangtai': p.shenhe_zhuangtai,
- 'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '',
- 'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '',
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': product_list,
- 'total': total_count,
- }
- })
-
-
-
-class ShopProductModifyView1(APIView):
- """
- 商品统一操作:修改、添加、删除
- URL: /shangdian/xgdpsplx1
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- frontend_user_id = request.data.get('userId')
- if not frontend_user_id:
- return Response({'code': 400, 'msg': '缺少必要参数 userId'})
-
- dianpu, error = verify_shop_permission(request, frontend_user_id)
- if error:
- return error
-
- action = request.data.get('action')
- if not action:
- return Response({'code': 400, 'msg': '缺少操作类型 action'})
-
- if action == 'update_product':
- return self._update_product(dianpu, request.data, request.FILES)
- elif action == 'add_product':
- return self._add_product(dianpu, request.data, request.FILES)
- elif action == 'delete_product':
- return self._delete_product(dianpu, request.data)
- else:
- return Response({'code': 400, 'msg': '不支持的操作类型'})
-
- # ---------- 修改商品 ----------
- @transaction.atomic
- def _update_product(self, dianpu, data, files):
- product_id = data.get('product_id')
- if not product_id:
- return Response({'code': 400, 'msg': '缺少 product_id'})
-
- try:
- product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id)
- except Shangpin.DoesNotExist:
- return Response({'code': 404, 'msg': '商品不存在'})
-
- update_fields = []
- content_changed = False
-
- # 辅助函数:安全转换为整数,失败则返回原值(让其报错)
- def to_int(val):
- try:
- return int(val)
- except (TypeError, ValueError):
- return val
-
- def to_float(val):
- try:
- return float(val)
- except (TypeError, ValueError):
- return val
-
- # 标题
- if 'biaoqian' in data:
- product.biaoqian = data['biaoqian']
- update_fields.append('biaoqian')
- content_changed = True
-
- # 价格
- if 'jiage' in data:
- product.jiage = to_float(data['jiage'])
- update_fields.append('jiage')
- content_changed = True
-
- # 库存
- if 'kucun' in data:
- product.kucun = to_int(data['kucun'])
- update_fields.append('kucun')
- content_changed = True
-
- # 店铺商品类型
- if 'dianpu_leixing_id' in data:
- new_type_id = to_int(data['dianpu_leixing_id'])
- if not ShangpinLeixingDianpu.query.filter(id=new_type_id, dianpu_id=dianpu.id).exists():
- return Response({'code': 400, 'msg': '无效的店铺商品类型'})
- product.dianpu_leixing_id = new_type_id
- update_fields.append('dianpu_leixing_id')
- content_changed = True
-
- # 专区
- if 'zhuanqu_id' in data:
- new_zq_id = data['zhuanqu_id']
- if new_zq_id is not None and new_zq_id != '':
- new_zq_id = to_int(new_zq_id)
- if not ShangpinZhuanqu.query.filter(id=new_zq_id, dianpu_id=dianpu.id).exists():
- return Response({'code': 400, 'msg': '无效的专区'})
- product.zhuanqu_id = new_zq_id
- else:
- product.zhuanqu_id = None
- update_fields.append('zhuanqu_id')
- content_changed = True
-
- # 介绍
- if 'jieshao' in data:
- product.jieshao = data['jieshao']
- update_fields.append('jieshao')
- content_changed = True
-
- # 下单须知
- if 'xiadan_xuzhi' in data:
- product.xiadan_xuzhi = data['xiadan_xuzhi']
- update_fields.append('xiadan_xuzhi')
-
- # ★ 抢单要求类型及会员/佣金自动处理
- if 'yaoqiuleixing' in data:
- yqlx = to_int(data['yaoqiuleixing'])
- if yqlx not in [1, 2]:
- return Response({'code': 400, 'msg': '抢单要求类型无效'})
- product.yaoqiuleixing = yqlx
- update_fields.append('yaoqiuleixing')
- content_changed = True
-
- if yqlx == 1:
- # 会员抢单:优先使用前端显式传递的非空会员ID
- if 'huiyuan_id' in data and data.get('huiyuan_id'):
- product.huiyuan_id = data['huiyuan_id']
- update_fields.append('huiyuan_id')
- product.yongjin = None
- if 'yongjin' not in update_fields:
- update_fields.append('yongjin')
- else:
- # 自动从公共类型获取会员ID,或转为佣金
- try:
- shop_type = ShangpinLeixingDianpu.query.get(
- id=product.dianpu_leixing_id, dianpu_id=dianpu.id
- )
- public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id)
- auto_huiyuan_id = public_type.huiyuan_id
- if auto_huiyuan_id:
- product.huiyuan_id = auto_huiyuan_id
- update_fields.append('huiyuan_id')
- product.yongjin = None
- if 'yongjin' not in update_fields:
- update_fields.append('yongjin')
- elif public_type.yaoqiuleixing == 2 and public_type.yongjin:
- # 无会员,强制转为佣金
- product.yaoqiuleixing = 2
- product.yongjin = public_type.yongjin
- product.huiyuan_id = None
- # yaoqiuleixing 已在 update_fields 中,需要添加 huiyuan_id 和 yongjin
- update_fields.append('huiyuan_id')
- update_fields.append('yongjin')
- else:
- return Response({'code': 400, 'msg': '会员抢单缺少会员ID,公共类型也无会员ID或佣金'})
- except (ShangpinLeixingDianpu.DoesNotExist, ShangpinLeixing.DoesNotExist):
- return Response({'code': 400, 'msg': '店铺商品类型无效'})
- elif yqlx == 2:
- # 佣金抢单,清除会员ID,设置佣金
- product.huiyuan_id = None
- update_fields.append('huiyuan_id')
- if 'yongjin' in data:
- product.yongjin = to_float(data['yongjin']) or 0
- else:
- product.yongjin = 0
- update_fields.append('yongjin')
- else:
- # 未修改抢单类型,保留原有独立修改逻辑
- if 'huiyuan_id' in data:
- product.huiyuan_id = data['huiyuan_id'] or None
- update_fields.append('huiyuan_id')
- content_changed = True
- if 'yongjin' in data:
- product.yongjin = to_float(data['yongjin']) or 0
- update_fields.append('yongjin')
- content_changed = True
-
- # 打手额外分成金额
- if 'ewai_dashou_fencheng' in data:
- fencheng = to_float(data['ewai_dashou_fencheng']) or 0
-
-
- if fencheng > (product.jiage or 0):
- return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})
- product.ewai_dashou_fencheng = fencheng
- update_fields.append('ewai_dashou_fencheng')
- content_changed = True
-
- # 排序
- if 'paixu' in data:
- product.paixu = to_int(data['paixu'])
- update_fields.append('paixu')
-
- # 上架状态(保持原逻辑,兼容字符串)
- if 'shangjia_zhuangtai' in data:
- val = data['shangjia_zhuangtai']
- if isinstance(val, str):
- val = val.lower() in ('true', '1', 'yes')
- product.shangjia_zhuangtai = bool(val)
- update_fields.append('shangjia_zhuangtai')
-
- # ★ 商品图片真上传
- image_file = files.get('image')
- if image_file:
- if product.tupian_url:
- delete_from_oss(product.tupian_url)
- file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png"
- full_url = upload_to_oss(image_file, file_path)
- if not full_url:
- return Response({'code': 500, 'msg': '商品图片上传失败'})
- product.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
- update_fields.append('tupian_url')
- content_changed = True
-
- # ★ 规则图片真上传
- rule_file = files.get('rule_image')
- remove_rule = data.get('remove_rule_image', '0')
- if rule_file:
- if product.guize_tupian:
- delete_from_oss(product.guize_tupian)
- file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png"
- full_url = upload_to_oss(rule_file, file_path)
- if full_url:
- product.guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
- update_fields.append('guize_tupian')
- content_changed = True
- elif remove_rule == '1' and product.guize_tupian:
- delete_from_oss(product.guize_tupian)
- product.guize_tupian = None
- update_fields.append('guize_tupian')
- content_changed = True
-
- if content_changed:
- audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
- if audit_config and audit_config.kaiqi_shenhe:
- product.shenhe_zhuangtai = False
- update_fields.append('shenhe_zhuangtai')
-
- if update_fields:
- product.save(update_fields=update_fields + ['UpdateTime'])
- return Response({'code': 0, 'msg': '商品修改成功'})
-
- # ---------- 添加商品(增强会员/佣金默认逻辑) ----------
- @transaction.atomic
- def _add_product(self, dianpu, data, files):
- biaoqian = data.get('biaoqian')
- jiage = data.get('jiage')
- kucun = data.get('kucun')
- dianpu_leixing_id = data.get('dianpu_leixing_id')
- zhuanqu_id = data.get('zhuanqu_id')
- jieshao = data.get('jieshao')
-
- if not all([biaoqian, jiage is not None, kucun is not None, dianpu_leixing_id, zhuanqu_id, jieshao]):
- return Response({'code': 400, 'msg': '必填项缺失'})
-
- try:
- shop_type = ShangpinLeixingDianpu.query.get(id=dianpu_leixing_id, dianpu_id=dianpu.id)
- except ShangpinLeixingDianpu.DoesNotExist:
- return Response({'code': 400, 'msg': '无效的店铺商品类型'})
-
- try:
- public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1)
- except ShangpinLeixing.DoesNotExist:
- return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
-
- # 商品图片上传
- image_file = files.get('image')
- if not image_file:
- return Response({'code': 400, 'msg': '必须上传商品图片'})
- file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png"
- full_url = upload_to_oss(image_file, file_path)
- if not full_url:
- return Response({'code': 500, 'msg': '商品图片上传失败'})
- tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
-
- # 规则图片
- guize_tupian = None
- rule_file = files.get('rule_image')
- if rule_file:
- file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png"
- full_url = upload_to_oss(rule_file, file_path)
- if full_url:
- guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
-
- yaoqiuleixing = int(data.get('yaoqiuleixing', 1))
- if yaoqiuleixing == 1:
- huiyuan_id = data.get('huiyuan_id')
- # 如果前端没传有效的会员ID,尝试从公共类型获取
- if not huiyuan_id:
- huiyuan_id = public_type.huiyuan_id
- # 仍然没有会员ID,检查公共类型是否可转为佣金
- if not huiyuan_id:
- if public_type.yaoqiuleixing == 2 and public_type.yongjin:
- # 自动转为佣金抢单
- yaoqiuleixing = 2
- yongjin = public_type.yongjin
- huiyuan_id = None
- else:
- return Response({'code': 400, 'msg': '会员抢单缺少会员ID,且无法自动获取'})
- else:
- yongjin = None
- else:
- huiyuan_id = None
- yongjin = data.get('yongjin') or public_type.yongjin or 0
-
- ewai_fencheng = data.get('ewai_dashou_fencheng') or 0
-
-
- try:
- ewai_val = float(ewai_fencheng)
- jiage_val = float(jiage)
- if ewai_val > jiage_val:
- return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})
- except ValueError:
- # 处理转换失败的情况,比如记录日志或设为默认值
- print("无法转换为数字")
-
-
-
-
- '''if ewai_fencheng > jiage:
- return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})'''
-
- shenhe_status = True
- audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
- if audit_config and audit_config.kaiqi_shenhe:
- shenhe_status = False
-
- product = Shangpin.query.create(
- biaoqian=biaoqian,
- jiage=jiage,
- kucun=kucun,
- dianpu_id=dianpu.id,
- dianpu_leixing_id=dianpu_leixing_id,
- zhuanqu_id=zhuanqu_id,
- leixing_id=public_type.id,
- jieshao=jieshao,
- xiadan_xuzhi=data.get('xiadan_xuzhi', ''),
- guize_tupian=guize_tupian,
- tupian_url=tupian_url,
- yaoqiuleixing=yaoqiuleixing,
- huiyuan_id=huiyuan_id,
- yongjin=yongjin,
- ewai_dashou_fencheng=ewai_fencheng,
- kaioi_ewai_dashou_fencheng=True,
- paixu=data.get('paixu', 0),
- shangjia_zhuangtai=data.get('shangjia_zhuangtai', True),
- fengjin_zhuangtai=False,
- shenhe_zhuangtai=shenhe_status,
- shenhezhuangtai=1,
- )
- return Response({'code': 0, 'msg': '商品添加成功', 'product_id': product.id})
-
- # ---------- 删除商品(完全不变) ----------
- @transaction.atomic
- def _delete_product(self, dianpu, data):
- product_id = data.get('product_id')
- if not product_id:
- return Response({'code': 400, 'msg': '缺少 product_id'})
-
- try:
- product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id)
- except Shangpin.DoesNotExist:
- return Response({'code': 404, 'msg': '商品不存在'})
-
- if product.tupian_url:
- delete_from_oss(product.tupian_url)
- if product.guize_tupian:
- delete_from_oss(product.guize_tupian)
-
- product.delete()
- return Response({'code': 0, 'msg': '商品已删除'})
-
-
-
-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 验证店铺身份
-"""
-
-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
- }
- })
-
-
-
-
-
-
-# ================================================================
-# shangdian/views/refund_view.py
-# 店铺退款接口 —— 基于跨平台退款接口二改,只处理平台订单,我方派单
-# ================================================================
-
-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 = ['']
- for k, v in data.items():
- xml.append(f'<{k}>{v}{k}>')
- xml.append('')
- 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': '服务器内部错误'})
\ No newline at end of file
diff --git a/shop/views/__init__.py b/shop/views/__init__.py
new file mode 100644
index 0000000..211d8f4
--- /dev/null
+++ b/shop/views/__init__.py
@@ -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",
+]
diff --git a/shop/views/shop_base_views.py b/shop/views/shop_base_views.py
new file mode 100644
index 0000000..be7c677
--- /dev/null
+++ b/shop/views/shop_base_views.py
@@ -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
+
diff --git a/shop/views/shop_member_views.py b/shop/views/shop_member_views.py
new file mode 100644
index 0000000..d2627ca
--- /dev/null
+++ b/shop/views/shop_member_views.py
@@ -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 验证店铺身份
+"""
+
diff --git a/shop/views/shop_order_views.py b/shop/views/shop_order_views.py
new file mode 100644
index 0000000..2de0a08
--- /dev/null
+++ b/shop/views/shop_order_views.py
@@ -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
+ }
+ })
+
diff --git a/shop/views/shop_product_views.py b/shop/views/shop_product_views.py
new file mode 100644
index 0000000..40c497d
--- /dev/null
+++ b/shop/views/shop_product_views.py
@@ -0,0 +1,1012 @@
+"""shop.views.shop_product_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 ShopProductConfigView(APIView):
+ """
+ 获取商品发布所需配置:公共类型、店铺商品类型、专区及各自商品数量
+ URL: /shangdian/dphqspxx
+ 权限: JWT认证 + 店铺归属验证
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 身份验证,获取店铺对象
+ frontend_user_id = request.data.get('userId')
+ if not frontend_user_id:
+ return Response({'code': 400, 'msg': '缺少必要参数 userId'})
+
+ dianpu, error = verify_shop_permission(request, frontend_user_id)
+ if error:
+ return error
+
+ # ---------- 2. 公共商品类型(需审核正常) ----------
+ try:
+ public_types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values(
+ 'id', 'jieshao', 'tupian_url'
+ )
+ public_types = [{
+ 'id': pt['id'],
+ 'jieshao': pt['jieshao'] or '',
+ 'tupian_url': pt['tupian_url'] or '',
+ } for pt in public_types_qs]
+ except Exception as e:
+ logger.exception("查询公共商品类型失败")
+ return Response({'code': 500, 'msg': '服务器错误'})
+
+ # ---------- 3. 店铺商品类型 ----------
+ try:
+ shop_types = ShangpinLeixingDianpu.query.filter(
+ dianpu_id=dianpu.id
+ ).select_related('gonggong_leixing').order_by('-paixu')
+ except Exception as e:
+ logger.exception("查询店铺商品类型失败")
+ return Response({'code': 500, 'msg': '服务器错误'})
+
+ type_ids = []
+ shop_product_types = []
+ for t in shop_types:
+ shop_product_types.append({
+ 'id': t.id,
+ 'jieshao': t.jieshao or '',
+ 'tupian_url': t.tupian_url or '',
+ 'paixu': t.paixu,
+ 'shangjia_zhuangtai': t.shangjia_zhuangtai,
+ 'fengjin_zhuangtai': t.fengjin_zhuangtai,
+ 'shenhe_zhuangtai': t.shenhe_zhuangtai,
+ 'gonggong_leixing_id': t.gonggong_leixing_id,
+ })
+ type_ids.append(t.id)
+
+ # ---------- 4. 各类型商品数量统计 ----------
+ type_product_counts = {}
+ if type_ids:
+ counts = Shangpin.query.filter(
+ dianpu_id=dianpu.id,
+ dianpu_leixing_id__in=type_ids
+ ).values('dianpu_leixing_id').annotate(cnt=Count('id'))
+ type_product_counts = {item['dianpu_leixing_id']: item['cnt'] for item in counts}
+
+ # ---------- 5. 专区列表 ----------
+ try:
+ zhuanqu_qs = ShangpinZhuanqu.query.filter(
+ dianpu_id=dianpu.id
+ ).order_by('-paixu').values(
+ 'id', 'mingzi', 'paixu', 'shangjia_zhuangtai',
+ 'fengjin_zhuangtai', 'dianpu_leixing_id'
+ )
+ zhuanqu_list = []
+ zhuanqu_ids = []
+ for zq in zhuanqu_qs:
+ zhuanqu_list.append({
+ 'id': zq['id'],
+ 'mingzi': zq['mingzi'] or '',
+ 'paixu': zq['paixu'],
+ 'shangjia_zhuangtai': zq['shangjia_zhuangtai'],
+ 'fengjin_zhuangtai': zq['fengjin_zhuangtai'],
+ 'dianpu_leixing_id': zq['dianpu_leixing_id'],
+ })
+ zhuanqu_ids.append(zq['id'])
+ except Exception as e:
+ logger.exception("查询专区失败")
+ return Response({'code': 500, 'msg': '服务器错误'})
+
+ # ---------- 6. 各专区商品数量统计 ----------
+ zhuanqu_product_counts = {}
+ if zhuanqu_ids:
+ counts = Shangpin.query.filter(
+ dianpu_id=dianpu.id,
+ zhuanqu_id__in=zhuanqu_ids
+ ).values('zhuanqu_id').annotate(cnt=Count('id'))
+ zhuanqu_product_counts = {item['zhuanqu_id']: item['cnt'] for item in counts}
+
+ return Response({
+ 'code': 0,
+ 'msg': 'success',
+ 'data': {
+ 'public_types': public_types,
+ 'shop_product_types': shop_product_types,
+ 'zhuanqu_list': zhuanqu_list,
+ 'type_product_counts': type_product_counts,
+ 'zhuanqu_product_counts': zhuanqu_product_counts,
+ }
+ })
+
+
+
+
+class ShopProductModifyView(APIView):
+ """店铺商品类型与专区管理(增、改、删)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ frontend_user_id = request.data.get('userId') or request.POST.get('userId')
+ if not frontend_user_id:
+ return Response({'code': 400, 'msg': '缺少必要参数 userId'})
+
+ dianpu, error = verify_shop_permission(request, frontend_user_id)
+ if error:
+ return error
+
+ action = request.data.get('action') or request.POST.get('action')
+ if not action:
+ return Response({'code': 400, 'msg': '缺少操作类型 action'})
+
+ handlers = {
+ 'add_type': self._add_type,
+ 'update_type': self._update_type,
+ 'delete_type': self._delete_type,
+ 'add_zhuanqu': self._add_zhuanqu,
+ 'update_zhuanqu': self._update_zhuanqu,
+ 'delete_zhuanqu': self._delete_zhuanqu,
+ }
+ handler = handlers.get(action)
+ if not handler:
+ return Response({'code': 400, 'msg': '不支持的操作类型'})
+
+ return handler(dianpu, request)
+
+ except Exception as e:
+ logger.exception("ShopProductModifyView 全局异常")
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+ # ---------- 商品类型操作 ----------
+ @transaction.atomic
+ def _add_type(self, dianpu, request):
+ """添加店铺商品类型"""
+ try:
+ jieshao = request.data.get('jieshao') or request.POST.get('jieshao')
+ gonggong_leixing_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id')
+ paixu = request.data.get('paixu', 0) or request.POST.get('paixu', 0)
+
+ if not jieshao or not gonggong_leixing_id:
+ return Response({'code': 400, 'msg': '缺少介绍或公共类型ID'})
+
+ # 验证公共类型存在且正常
+ if not ShangpinLeixing.query.filter(id=gonggong_leixing_id, shenhezhuangtai=1).exists():
+ return Response({'code': 400, 'msg': '所选公共商品类型无效'})
+
+ image_file = request.FILES.get('image')
+ if not image_file:
+ return Response({'code': 400, 'msg': '必须上传类型图片'})
+
+ # 上传图片
+ file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png"
+ full_url = upload_to_oss(image_file, file_path)
+ if not full_url:
+ return Response({'code': 500, 'msg': '图片上传失败'})
+
+ # 提取相对路径
+ tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
+
+ # 审核状态
+ audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
+ shenhe_status = not (audit_config and audit_config.kaiqi_shenhe)
+
+ ShangpinLeixingDianpu.query.create(
+ dianpu_id=dianpu.id,
+ gonggong_leixing_id=gonggong_leixing_id,
+ jieshao=jieshao,
+ tupian_url=tupian_url,
+ paixu=paixu,
+ shangjia_zhuangtai=True,
+ fengjin_zhuangtai=False,
+ shenhe_zhuangtai=shenhe_status,
+ )
+ return Response({'code': 0, 'msg': '商品类型添加成功'})
+ except Exception as e:
+ logger.exception("添加商品类型失败")
+ return Response({'code': 500, 'msg': '添加失败,请稍后重试'})
+
+ @transaction.atomic
+ def _update_type(self, dianpu, request):
+ """修改店铺商品类型,支持更换图片(先删旧图)"""
+ type_id = request.data.get('type_id') or request.POST.get('type_id')
+ if not type_id:
+ return Response({'code': 400, 'msg': '缺少 type_id'})
+
+ try:
+ obj = ShangpinLeixingDianpu.objects.select_for_update().get(
+ id=type_id, dianpu_id=dianpu.id
+ )
+ except ShangpinLeixingDianpu.DoesNotExist:
+ return Response({'code': 404, 'msg': '商品类型不存在'})
+ except Exception as e:
+ logger.exception("查询商品类型异常")
+ return Response({'code': 500, 'msg': '服务器错误'})
+
+ try:
+ update_fields = []
+ content_changed = False
+
+ # 简介
+ if 'jieshao' in request.data or 'jieshao' in request.POST:
+ obj.jieshao = request.data.get('jieshao') or request.POST.get('jieshao')
+ update_fields.append('jieshao')
+ content_changed = True
+
+ # 映射公共类型
+ if 'gonggong_leixing_id' in request.data or 'gonggong_leixing_id' in request.POST:
+ new_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id')
+ if not ShangpinLeixing.query.filter(id=new_id, shenhezhuangtai=1).exists():
+ return Response({'code': 400, 'msg': '公共商品类型无效'})
+ obj.gonggong_leixing_id = new_id
+ update_fields.append('gonggong_leixing_id')
+ content_changed = True
+
+ # 排序
+ if 'paixu' in request.data or 'paixu' in request.POST:
+ obj.paixu = request.data.get('paixu') or request.POST.get('paixu')
+ update_fields.append('paixu')
+
+ # 上架状态
+ if 'shangjia_zhuangtai' in request.data or 'shangjia_zhuangtai' in request.POST:
+ val = request.data.get('shangjia_zhuangtai') or request.POST.get('shangjia_zhuangtai')
+ if isinstance(val, str):
+ val = val.lower() in ('true', '1', 'yes')
+ obj.shangjia_zhuangtai = bool(val)
+ update_fields.append('shangjia_zhuangtai')
+
+ # 图片更换:先删除旧图,再上传新图
+ image_file = request.FILES.get('image')
+ if image_file:
+ if obj.tupian_url:
+ delete_from_oss(obj.tupian_url) # 删除旧图片
+ file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png"
+ full_url = upload_to_oss(image_file, file_path)
+ if not full_url:
+ return Response({'code': 500, 'msg': '图片上传失败'})
+ obj.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
+ update_fields.append('tupian_url')
+ content_changed = True
+
+ # 审核重置
+ if content_changed:
+ audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
+ if audit_config and audit_config.kaiqi_shenhe:
+ obj.shenhe_zhuangtai = False
+ update_fields.append('shenhe_zhuangtai')
+
+ if update_fields:
+ obj.save(update_fields=update_fields)
+ return Response({'code': 0, 'msg': '商品类型更新成功'})
+ except Exception as e:
+ logger.exception("修改商品类型失败")
+ return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
+
+ @transaction.atomic
+ def _delete_type(self, dianpu, request):
+ """删除店铺商品类型,可选是否同时删除关联商品"""
+ type_id = request.data.get('id') or request.POST.get('id')
+ if not type_id:
+ return Response({'code': 400, 'msg': '缺少类型ID'})
+
+ try:
+ obj = ShangpinLeixingDianpu.objects.select_for_update().get(
+ id=type_id, dianpu_id=dianpu.id
+ )
+ except ShangpinLeixingDianpu.DoesNotExist:
+ return Response({'code': 404, 'msg': '商品类型不存在'})
+ except Exception as e:
+ logger.exception("查询商品类型异常")
+ return Response({'code': 500, 'msg': '服务器错误'})
+
+ try:
+ delete_products = request.data.get('delete_products') or request.POST.get('delete_products')
+ delete_products = str(delete_products).lower() in ('true', '1', 'yes')
+
+ if delete_products:
+ # 删除该类型下所有关联商品(同时清理图片)
+ products = Shangpin.query.filter(
+ dianpu_leixing_id=obj.id, dianpu_id=dianpu.id
+ )
+ for p in products:
+ if p.tupian_url:
+ delete_from_oss(p.tupian_url)
+ if p.guize_tupian:
+ delete_from_oss(p.guize_tupian)
+ p.delete()
+ else:
+ # 仅解除关联,不删商品
+ Shangpin.query.filter(
+ dianpu_leixing_id=obj.id, dianpu_id=dianpu.id
+ ).update(dianpu_leixing=None)
+
+ # 删除类型自身图片
+ if obj.tupian_url:
+ delete_from_oss(obj.tupian_url)
+ obj.delete()
+ return Response({'code': 0, 'msg': '类型删除成功'})
+ except Exception as e:
+ logger.exception("删除商品类型失败")
+ return Response({'code': 500, 'msg': '删除失败,请稍后重试'})
+
+ # ---------- 专区操作 ----------
+ @transaction.atomic
+ def _add_zhuanqu(self, dianpu, request):
+ """添加专区,自动从店铺商品类型获取 leixing_id(公共商品类型ID)"""
+ try:
+ mingzi = request.data.get('mingzi')
+ dianpu_leixing_id = request.data.get('dianpu_leixing_id')
+ paixu = request.data.get('paixu', 0)
+
+ if not mingzi or not dianpu_leixing_id:
+ return Response({'code': 400, 'msg': '缺少名称或所属商品类型'})
+
+ # 获取店铺商品类型,并拿到映射的公共类型ID
+ try:
+ shop_type = ShangpinLeixingDianpu.query.get(
+ id=dianpu_leixing_id, dianpu_id=dianpu.id
+ )
+ leixing_id = shop_type.gonggong_leixing_id # 这就是专区需要的 leixing_id
+ except ShangpinLeixingDianpu.DoesNotExist:
+ return Response({'code': 400, 'msg': '所选商品类型无效'})
+
+ # 验证公共类型有效
+ if not ShangpinLeixing.query.filter(id=leixing_id, shenhezhuangtai=1).exists():
+ return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
+
+ ShangpinZhuanqu.query.create(
+ dianpu_id=dianpu.id,
+ mingzi=mingzi,
+ dianpu_leixing_id=dianpu_leixing_id,
+ leixing_id=leixing_id, # 自动填充
+ paixu=paixu,
+ shangjia_zhuangtai=True,
+ fengjin_zhuangtai=False,
+ )
+ return Response({'code': 0, 'msg': '专区添加成功'})
+ except Exception as e:
+ logger.exception("添加专区失败")
+ return Response({'code': 500, 'msg': '添加失败,请稍后重试'})
+
+ @transaction.atomic
+ def _update_zhuanqu(self, dianpu, request):
+ """修改专区,若更换所属商品类型则同步更新 leixing_id"""
+ zhuanqu_id = request.data.get('zhuanqu_id')
+ if not zhuanqu_id:
+ return Response({'code': 400, 'msg': '缺少 zhuanqu_id'})
+
+ try:
+ obj = ShangpinZhuanqu.objects.select_for_update().get(
+ id=zhuanqu_id, dianpu_id=dianpu.id
+ )
+ except ShangpinZhuanqu.DoesNotExist:
+ return Response({'code': 404, 'msg': '专区不存在'})
+ except Exception as e:
+ logger.exception("查询专区异常")
+ return Response({'code': 500, 'msg': '服务器错误'})
+
+ try:
+ update_fields = []
+
+ if 'mingzi' in request.data:
+ obj.mingzi = request.data.get('mingzi')
+ update_fields.append('mingzi')
+
+ if 'dianpu_leixing_id' in request.data:
+ new_id = request.data.get('dianpu_leixing_id')
+ try:
+ shop_type = ShangpinLeixingDianpu.query.get(
+ id=new_id, dianpu_id=dianpu.id
+ )
+ obj.dianpu_leixing_id = new_id
+ obj.leixing_id = shop_type.gonggong_leixing_id # 同步更新公共类型ID
+ update_fields.extend(['dianpu_leixing_id', 'leixing_id'])
+ except ShangpinLeixingDianpu.DoesNotExist:
+ return Response({'code': 400, 'msg': '所选商品类型无效'})
+
+ if 'paixu' in request.data:
+ obj.paixu = request.data.get('paixu')
+ update_fields.append('paixu')
+
+ if 'shangjia_zhuangtai' in request.data:
+ val = request.data.get('shangjia_zhuangtai')
+ if isinstance(val, str):
+ val = val.lower() in ('true', '1', 'yes')
+ obj.shangjia_zhuangtai = bool(val)
+ update_fields.append('shangjia_zhuangtai')
+
+ if update_fields:
+ obj.save(update_fields=update_fields)
+ return Response({'code': 0, 'msg': '专区更新成功'})
+ except Exception as e:
+ logger.exception("修改专区失败")
+ return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
+
+ @transaction.atomic
+ def _delete_zhuanqu(self, dianpu, request):
+ """删除专区,可选是否同时删除关联商品"""
+ zhuanqu_id = request.data.get('id')
+ if not zhuanqu_id:
+ return Response({'code': 400, 'msg': '缺少专区ID'})
+
+ try:
+ obj = ShangpinZhuanqu.objects.select_for_update().get(
+ id=zhuanqu_id, dianpu_id=dianpu.id
+ )
+ except ShangpinZhuanqu.DoesNotExist:
+ return Response({'code': 404, 'msg': '专区不存在'})
+ except Exception as e:
+ logger.exception("查询专区异常")
+ return Response({'code': 500, 'msg': '服务器错误'})
+
+ try:
+ delete_products = request.data.get('delete_products')
+ delete_products = str(delete_products).lower() in ('true', '1', 'yes')
+
+ if delete_products:
+ products = Shangpin.query.filter(
+ zhuanqu_id=obj.id, dianpu_id=dianpu.id
+ )
+ for p in products:
+ if p.tupian_url:
+ delete_from_oss(p.tupian_url)
+ if p.guize_tupian:
+ delete_from_oss(p.guize_tupian)
+ p.delete()
+ else:
+ Shangpin.query.filter(
+ zhuanqu_id=obj.id, dianpu_id=dianpu.id
+ ).update(zhuanqu_id=None)
+
+ obj.delete()
+ return Response({'code': 0, 'msg': '专区删除成功'})
+ except Exception as e:
+ logger.exception("删除专区失败")
+ return Response({'code': 500, 'msg': '删除失败,请稍后重试'})
+
+
+
+class ShopProductListView(APIView):
+ """
+ 商品列表接口,支持筛选、分页。
+ URL: /shangdian/sdhqspsj
+ 权限: JWT认证 + 店铺验证
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 身份验证,获取店铺对象
+ frontend_user_id = request.data.get('userId')
+ if not frontend_user_id:
+ return Response({'code': 400, 'msg': '缺少必要参数 userId'})
+
+ dianpu, error = verify_shop_permission(request, frontend_user_id)
+ if error:
+ return error
+
+ # 2. 安全解析分页与筛选参数
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '分页参数格式错误'})
+
+ keyword = str(request.data.get('keyword', '')).strip()
+ shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型ID
+ zhuanqu_id = request.data.get('zhuanqu_id') # 专区ID
+
+ # 布尔型筛选字段
+ def parse_bool(val):
+ if val is None or val == '':
+ return None
+ if isinstance(val, bool):
+ return val
+ if isinstance(val, str):
+ return val.lower() in ('true', '1', 'yes')
+ return bool(val)
+
+ shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
+ shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
+ fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
+
+ # 3. 构建查询(只查当前店铺的商品)
+ products = Shangpin.query.filter(dianpu_id=dianpu.id).select_related(
+ 'dianpu_leixing'
+ ).only(
+ 'id', 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'zhuanqu_id',
+ 'zhenshi_xiaoliang', 'duiwai_xiaoliang', 'jieshao', 'xiadan_xuzhi',
+ 'guize_tupian', 'yaoqiuleixing', 'huiyuan_id', 'yongjin',
+ 'tupian_url', 'ewai_dashou_fencheng', 'paixu',
+ 'dianpu_leixing_id', 'shangjia_zhuangtai', 'fengjin_zhuangtai',
+ 'shenhe_zhuangtai', 'CreateTime', 'UpdateTime'
+ )
+
+ # 筛选
+ if shop_product_type_id is not None:
+ try:
+ products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
+ except (ValueError, TypeError):
+ return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
+
+ if zhuanqu_id is not None:
+ try:
+ products = products.filter(zhuanqu_id=int(zhuanqu_id))
+ except (ValueError, TypeError):
+ return Response({'code': 400, 'msg': 'zhuanqu_id 格式错误'})
+
+ if shenhe is not None:
+ products = products.filter(shenhe_zhuangtai=shenhe)
+ if shangjia is not None:
+ products = products.filter(shangjia_zhuangtai=shangjia)
+ if fengjin is not None:
+ products = products.filter(fengjin_zhuangtai=fengjin)
+
+ # 关键词搜索(商品ID、标题、介绍)
+ if keyword:
+ products = products.filter(
+ models.Q(id__icontains=keyword) |
+ models.Q(biaoqian__icontains=keyword) |
+ models.Q(jieshao__icontains=keyword)
+ )
+
+ # 4. 分页
+ total_count = products.count()
+ paginator = Paginator(products, page_size)
+ page_obj = paginator.get_page(page)
+
+ # 5. 构造返回数据
+ product_list = []
+ for p in page_obj:
+ product_list.append({
+ 'id': p.id,
+ 'biaoqian': p.biaoqian or '',
+ 'jiage': p.jiage,
+ 'kucun': p.kucun,
+ 'leixing_id': p.leixing_id,
+ 'zhuanqu_id': p.zhuanqu_id,
+ 'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
+ 'duiwai_xiaoliang': p.duiwai_xiaoliang,
+ 'jieshao': p.jieshao or '',
+ 'xiadan_xuzhi': p.xiadan_xuzhi or '',
+ 'guize_tupian': p.guize_tupian or '',
+ 'yaoqiuleixing': p.yaoqiuleixing,
+ 'huiyuan_id': p.huiyuan_id or '',
+ 'yongjin': p.yongjin,
+ 'tupian_url': p.tupian_url or '',
+ 'ewai_dashou_fencheng': p.ewai_dashou_fencheng,
+ 'paixu': p.paixu,
+ 'dianpu_leixing_id': p.dianpu_leixing_id,
+ 'shangjia_zhuangtai': p.shangjia_zhuangtai,
+ 'fengjin_zhuangtai': p.fengjin_zhuangtai,
+ 'shenhe_zhuangtai': p.shenhe_zhuangtai,
+ 'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '',
+ 'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '',
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': product_list,
+ 'total': total_count,
+ }
+ })
+
+
+
+class ShopProductModifyView1(APIView):
+ """
+ 商品统一操作:修改、添加、删除
+ URL: /shangdian/xgdpsplx1
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ frontend_user_id = request.data.get('userId')
+ if not frontend_user_id:
+ return Response({'code': 400, 'msg': '缺少必要参数 userId'})
+
+ dianpu, error = verify_shop_permission(request, frontend_user_id)
+ if error:
+ return error
+
+ action = request.data.get('action')
+ if not action:
+ return Response({'code': 400, 'msg': '缺少操作类型 action'})
+
+ if action == 'update_product':
+ return self._update_product(dianpu, request.data, request.FILES)
+ elif action == 'add_product':
+ return self._add_product(dianpu, request.data, request.FILES)
+ elif action == 'delete_product':
+ return self._delete_product(dianpu, request.data)
+ else:
+ return Response({'code': 400, 'msg': '不支持的操作类型'})
+
+ # ---------- 修改商品 ----------
+ @transaction.atomic
+ def _update_product(self, dianpu, data, files):
+ product_id = data.get('product_id')
+ if not product_id:
+ return Response({'code': 400, 'msg': '缺少 product_id'})
+
+ try:
+ product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id)
+ except Shangpin.DoesNotExist:
+ return Response({'code': 404, 'msg': '商品不存在'})
+
+ update_fields = []
+ content_changed = False
+
+ # 辅助函数:安全转换为整数,失败则返回原值(让其报错)
+ def to_int(val):
+ try:
+ return int(val)
+ except (TypeError, ValueError):
+ return val
+
+ def to_float(val):
+ try:
+ return float(val)
+ except (TypeError, ValueError):
+ return val
+
+ # 标题
+ if 'biaoqian' in data:
+ product.biaoqian = data['biaoqian']
+ update_fields.append('biaoqian')
+ content_changed = True
+
+ # 价格
+ if 'jiage' in data:
+ product.jiage = to_float(data['jiage'])
+ update_fields.append('jiage')
+ content_changed = True
+
+ # 库存
+ if 'kucun' in data:
+ product.kucun = to_int(data['kucun'])
+ update_fields.append('kucun')
+ content_changed = True
+
+ # 店铺商品类型
+ if 'dianpu_leixing_id' in data:
+ new_type_id = to_int(data['dianpu_leixing_id'])
+ if not ShangpinLeixingDianpu.query.filter(id=new_type_id, dianpu_id=dianpu.id).exists():
+ return Response({'code': 400, 'msg': '无效的店铺商品类型'})
+ product.dianpu_leixing_id = new_type_id
+ update_fields.append('dianpu_leixing_id')
+ content_changed = True
+
+ # 专区
+ if 'zhuanqu_id' in data:
+ new_zq_id = data['zhuanqu_id']
+ if new_zq_id is not None and new_zq_id != '':
+ new_zq_id = to_int(new_zq_id)
+ if not ShangpinZhuanqu.query.filter(id=new_zq_id, dianpu_id=dianpu.id).exists():
+ return Response({'code': 400, 'msg': '无效的专区'})
+ product.zhuanqu_id = new_zq_id
+ else:
+ product.zhuanqu_id = None
+ update_fields.append('zhuanqu_id')
+ content_changed = True
+
+ # 介绍
+ if 'jieshao' in data:
+ product.jieshao = data['jieshao']
+ update_fields.append('jieshao')
+ content_changed = True
+
+ # 下单须知
+ if 'xiadan_xuzhi' in data:
+ product.xiadan_xuzhi = data['xiadan_xuzhi']
+ update_fields.append('xiadan_xuzhi')
+
+ # ★ 抢单要求类型及会员/佣金自动处理
+ if 'yaoqiuleixing' in data:
+ yqlx = to_int(data['yaoqiuleixing'])
+ if yqlx not in [1, 2]:
+ return Response({'code': 400, 'msg': '抢单要求类型无效'})
+ product.yaoqiuleixing = yqlx
+ update_fields.append('yaoqiuleixing')
+ content_changed = True
+
+ if yqlx == 1:
+ # 会员抢单:优先使用前端显式传递的非空会员ID
+ if 'huiyuan_id' in data and data.get('huiyuan_id'):
+ product.huiyuan_id = data['huiyuan_id']
+ update_fields.append('huiyuan_id')
+ product.yongjin = None
+ if 'yongjin' not in update_fields:
+ update_fields.append('yongjin')
+ else:
+ # 自动从公共类型获取会员ID,或转为佣金
+ try:
+ shop_type = ShangpinLeixingDianpu.query.get(
+ id=product.dianpu_leixing_id, dianpu_id=dianpu.id
+ )
+ public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id)
+ auto_huiyuan_id = public_type.huiyuan_id
+ if auto_huiyuan_id:
+ product.huiyuan_id = auto_huiyuan_id
+ update_fields.append('huiyuan_id')
+ product.yongjin = None
+ if 'yongjin' not in update_fields:
+ update_fields.append('yongjin')
+ elif public_type.yaoqiuleixing == 2 and public_type.yongjin:
+ # 无会员,强制转为佣金
+ product.yaoqiuleixing = 2
+ product.yongjin = public_type.yongjin
+ product.huiyuan_id = None
+ # yaoqiuleixing 已在 update_fields 中,需要添加 huiyuan_id 和 yongjin
+ update_fields.append('huiyuan_id')
+ update_fields.append('yongjin')
+ else:
+ return Response({'code': 400, 'msg': '会员抢单缺少会员ID,公共类型也无会员ID或佣金'})
+ except (ShangpinLeixingDianpu.DoesNotExist, ShangpinLeixing.DoesNotExist):
+ return Response({'code': 400, 'msg': '店铺商品类型无效'})
+ elif yqlx == 2:
+ # 佣金抢单,清除会员ID,设置佣金
+ product.huiyuan_id = None
+ update_fields.append('huiyuan_id')
+ if 'yongjin' in data:
+ product.yongjin = to_float(data['yongjin']) or 0
+ else:
+ product.yongjin = 0
+ update_fields.append('yongjin')
+ else:
+ # 未修改抢单类型,保留原有独立修改逻辑
+ if 'huiyuan_id' in data:
+ product.huiyuan_id = data['huiyuan_id'] or None
+ update_fields.append('huiyuan_id')
+ content_changed = True
+ if 'yongjin' in data:
+ product.yongjin = to_float(data['yongjin']) or 0
+ update_fields.append('yongjin')
+ content_changed = True
+
+ # 打手额外分成金额
+ if 'ewai_dashou_fencheng' in data:
+ fencheng = to_float(data['ewai_dashou_fencheng']) or 0
+
+
+ if fencheng > (product.jiage or 0):
+ return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})
+ product.ewai_dashou_fencheng = fencheng
+ update_fields.append('ewai_dashou_fencheng')
+ content_changed = True
+
+ # 排序
+ if 'paixu' in data:
+ product.paixu = to_int(data['paixu'])
+ update_fields.append('paixu')
+
+ # 上架状态(保持原逻辑,兼容字符串)
+ if 'shangjia_zhuangtai' in data:
+ val = data['shangjia_zhuangtai']
+ if isinstance(val, str):
+ val = val.lower() in ('true', '1', 'yes')
+ product.shangjia_zhuangtai = bool(val)
+ update_fields.append('shangjia_zhuangtai')
+
+ # ★ 商品图片真上传
+ image_file = files.get('image')
+ if image_file:
+ if product.tupian_url:
+ delete_from_oss(product.tupian_url)
+ file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png"
+ full_url = upload_to_oss(image_file, file_path)
+ if not full_url:
+ return Response({'code': 500, 'msg': '商品图片上传失败'})
+ product.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
+ update_fields.append('tupian_url')
+ content_changed = True
+
+ # ★ 规则图片真上传
+ rule_file = files.get('rule_image')
+ remove_rule = data.get('remove_rule_image', '0')
+ if rule_file:
+ if product.guize_tupian:
+ delete_from_oss(product.guize_tupian)
+ file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png"
+ full_url = upload_to_oss(rule_file, file_path)
+ if full_url:
+ product.guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
+ update_fields.append('guize_tupian')
+ content_changed = True
+ elif remove_rule == '1' and product.guize_tupian:
+ delete_from_oss(product.guize_tupian)
+ product.guize_tupian = None
+ update_fields.append('guize_tupian')
+ content_changed = True
+
+ if content_changed:
+ audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
+ if audit_config and audit_config.kaiqi_shenhe:
+ product.shenhe_zhuangtai = False
+ update_fields.append('shenhe_zhuangtai')
+
+ if update_fields:
+ product.save(update_fields=update_fields + ['UpdateTime'])
+ return Response({'code': 0, 'msg': '商品修改成功'})
+
+ # ---------- 添加商品(增强会员/佣金默认逻辑) ----------
+ @transaction.atomic
+ def _add_product(self, dianpu, data, files):
+ biaoqian = data.get('biaoqian')
+ jiage = data.get('jiage')
+ kucun = data.get('kucun')
+ dianpu_leixing_id = data.get('dianpu_leixing_id')
+ zhuanqu_id = data.get('zhuanqu_id')
+ jieshao = data.get('jieshao')
+
+ if not all([biaoqian, jiage is not None, kucun is not None, dianpu_leixing_id, zhuanqu_id, jieshao]):
+ return Response({'code': 400, 'msg': '必填项缺失'})
+
+ try:
+ shop_type = ShangpinLeixingDianpu.query.get(id=dianpu_leixing_id, dianpu_id=dianpu.id)
+ except ShangpinLeixingDianpu.DoesNotExist:
+ return Response({'code': 400, 'msg': '无效的店铺商品类型'})
+
+ try:
+ public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1)
+ except ShangpinLeixing.DoesNotExist:
+ return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
+
+ # 商品图片上传
+ image_file = files.get('image')
+ if not image_file:
+ return Response({'code': 400, 'msg': '必须上传商品图片'})
+ file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png"
+ full_url = upload_to_oss(image_file, file_path)
+ if not full_url:
+ return Response({'code': 500, 'msg': '商品图片上传失败'})
+ tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
+
+ # 规则图片
+ guize_tupian = None
+ rule_file = files.get('rule_image')
+ if rule_file:
+ file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png"
+ full_url = upload_to_oss(rule_file, file_path)
+ if full_url:
+ guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1)
+
+ yaoqiuleixing = int(data.get('yaoqiuleixing', 1))
+ if yaoqiuleixing == 1:
+ huiyuan_id = data.get('huiyuan_id')
+ # 如果前端没传有效的会员ID,尝试从公共类型获取
+ if not huiyuan_id:
+ huiyuan_id = public_type.huiyuan_id
+ # 仍然没有会员ID,检查公共类型是否可转为佣金
+ if not huiyuan_id:
+ if public_type.yaoqiuleixing == 2 and public_type.yongjin:
+ # 自动转为佣金抢单
+ yaoqiuleixing = 2
+ yongjin = public_type.yongjin
+ huiyuan_id = None
+ else:
+ return Response({'code': 400, 'msg': '会员抢单缺少会员ID,且无法自动获取'})
+ else:
+ yongjin = None
+ else:
+ huiyuan_id = None
+ yongjin = data.get('yongjin') or public_type.yongjin or 0
+
+ ewai_fencheng = data.get('ewai_dashou_fencheng') or 0
+
+
+ try:
+ ewai_val = float(ewai_fencheng)
+ jiage_val = float(jiage)
+ if ewai_val > jiage_val:
+ return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})
+ except ValueError:
+ # 处理转换失败的情况,比如记录日志或设为默认值
+ print("无法转换为数字")
+
+
+
+
+ '''if ewai_fencheng > jiage:
+ return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})'''
+
+ shenhe_status = True
+ audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
+ if audit_config and audit_config.kaiqi_shenhe:
+ shenhe_status = False
+
+ product = Shangpin.query.create(
+ biaoqian=biaoqian,
+ jiage=jiage,
+ kucun=kucun,
+ dianpu_id=dianpu.id,
+ dianpu_leixing_id=dianpu_leixing_id,
+ zhuanqu_id=zhuanqu_id,
+ leixing_id=public_type.id,
+ jieshao=jieshao,
+ xiadan_xuzhi=data.get('xiadan_xuzhi', ''),
+ guize_tupian=guize_tupian,
+ tupian_url=tupian_url,
+ yaoqiuleixing=yaoqiuleixing,
+ huiyuan_id=huiyuan_id,
+ yongjin=yongjin,
+ ewai_dashou_fencheng=ewai_fencheng,
+ kaioi_ewai_dashou_fencheng=True,
+ paixu=data.get('paixu', 0),
+ shangjia_zhuangtai=data.get('shangjia_zhuangtai', True),
+ fengjin_zhuangtai=False,
+ shenhe_zhuangtai=shenhe_status,
+ shenhezhuangtai=1,
+ )
+ return Response({'code': 0, 'msg': '商品添加成功', 'product_id': product.id})
+
+ # ---------- 删除商品(完全不变) ----------
+ @transaction.atomic
+ def _delete_product(self, dianpu, data):
+ product_id = data.get('product_id')
+ if not product_id:
+ return Response({'code': 400, 'msg': '缺少 product_id'})
+
+ try:
+ product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id)
+ except Shangpin.DoesNotExist:
+ return Response({'code': 404, 'msg': '商品不存在'})
+
+ if product.tupian_url:
+ delete_from_oss(product.tupian_url)
+ if product.guize_tupian:
+ delete_from_oss(product.guize_tupian)
+
+ product.delete()
+ return Response({'code': 0, 'msg': '商品已删除'})
+
diff --git a/shop/views/shop_refund_views.py b/shop/views/shop_refund_views.py
new file mode 100644
index 0000000..0786cba
--- /dev/null
+++ b/shop/views/shop_refund_views.py
@@ -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 = ['']
+ for k, v in data.items():
+ xml.append(f'<{k}>{v}{k}>')
+ xml.append('')
+ 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': '服务器内部错误'})
diff --git a/users/views.py b/users/views.py
deleted file mode 100644
index cdd3187..0000000
--- a/users/views.py
+++ /dev/null
@@ -1,13458 +0,0 @@
-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__)
-
-
-
-
-
-
-
-
-# 获取用户代理信息(用于更详细的追踪)
-def huoquYonghuDaili(request):
- """
- 获取用户浏览器/设备信息
- """
- user_agent = request.META.get('HTTP_USER_AGENT', '')
-
- # 这里可以添加更复杂的用户代理解析
- # 为简化,我们只返回原始字符串的前100个字符
- return user_agent[:100] if user_agent else ''
-
-
-
-
-
-def huoquKehuduanIP(request):
- """
- 强化版:安全、兼容地获取客户端真实IP地址
- """
- ip = None
-
- # 1. 从标准头部获取IP链
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
- if x_forwarded_for:
- # 按逗号分割,取第一个(即最原始的客户端IP)
- ip = x_forwarded_for.split(',')[0].strip()
-
- # 2. 如果上面没有,则用Nginx传递的X-Real-IP
- if not ip:
- ip = request.META.get('HTTP_X_REAL_IP')
-
- # 3. 最后降级到连接地址
- if not ip:
- ip = request.META.get('REMOTE_ADDR')
-
- # ===== 核心:清洗和验证IP格式 =====
- if ip:
- # 情况A: 处理IPv4映射的IPv6地址 (::ffff:192.168.1.1)
- if ip.startswith('::ffff:'):
- ip = ip[7:]
-
- # 情况B: 处理带端口的IPv4地址 (223.91.110.123:62417)
- # 这是导致您记录“三段”的最可能原因!
- # 如果IP字符串中有冒号,且最后一个冒号前的部分是IPv4,则分离端口。
- if ':' in ip and '.' in ip:
- # 尝试按冒号分割,取第一部分
- possible_ip = ip.rsplit(':', 1)[0] # 从右边分割一次
- # 验证它是否是有效的IPv4地址
- try:
- ipaddress.IPv4Address(possible_ip) # 如果通过验证,说明格式正确
- ip = possible_ip
- except (ipaddress.AddressValueError, ValueError):
- # 如果验证失败,说明可能不是IPv4带端口,保持原样或进行其他处理
- pass
-
- # 情况C: 验证最终结果是否为有效IPv4(可选,但推荐)
- try:
- # 此步骤会抛出异常,如果ip变量此时仍是“223.91.110.”这种格式
- ipaddress.IPv4Address(ip)
- except (ipaddress.AddressValueError, ValueError):
- # 记录一个警告日志,并将IP标记为无效或存为原始字符串
- print(f"警告:获取到无效或非标准IP地址格式: {ip}")
- # 您可以选择在此处 return None 或存储原始字符串以供排查
-
- return ip
-
-
-
-
-class WechatMiniProgramLoginView(APIView):
- """
- 微信小程序登录接口
- """
- throttle_classes = [AnonRateThrottle]
- permission_classes = [AllowAny]
-
- def post(self, request):
- try:
- code = request.data.get('code', '').strip()
- if not code:
- return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 获取微信openid和unionid
- wechat_data = self.get_wechat_openid(code)
- if not wechat_data or 'openid' not in wechat_data:
- error_msg = wechat_data.get('errmsg', '微信授权失败')
- return Response({'code': 2, 'msg': f'微信登录失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- openid = wechat_data['openid']
- session_key = wechat_data.get('session_key', '')
- unionid = wechat_data.get('unionid', '') # 🆕【新增】获取unionid
-
- # 获取客户端真实IP
- kehuduan_ip = self.huoquKehuduanIP(request)
-
- with transaction.atomic():
- # 创建或获取用户
- user_main, created = User.objects.select_for_update().get_or_create(
- OpenID=openid,
- defaults={'UserUID': self.shengchengYonghuID(), 'UserName': openid}
- )
-
- # 保存unionid
- if unionid and unionid != user_main.UnionID:
- user_main.UnionID = unionid
-
- user_main.IP = kehuduan_ip
- user_main.UserLastLoginDate = timezone.now()
- user_main.save()
-
- if created:
- UserBoss.query.create(user=user_main, nickname='板板大人')
- # 新用户默认分配消费者角色
- from users.business_models import UserRole, Role
- normal_role = Role.objects.filter(RoleName='消费者').first()
- if normal_role:
- UserRole.objects.create(
- UserRoleUUID=uuid.uuid4().bytes,
- UserUUID=user_main.UserUUID,
- RoleUUID=normal_role.RoleUUID,
- CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
- )
-
- # 检查扩展表
- dashou_status = 0
- shangjia_status = 0
- guanshi_status = 0
- boss_nickname = '微信用户'
-
- try:
- boss_profile = UserBoss.query.get(user=user_main)
- boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户'
- except UserBoss.DoesNotExist:
- pass
-
- try:
- UserDashou.query.get(user=user_main)
- dashou_status = 1
- except UserDashou.DoesNotExist:
- dashou_status = None
-
- try:
- UserShangjia.query.get(user=user_main)
- shangjia_status = 1
- except UserShangjia.DoesNotExist:
- shangjia_status = None
-
- try:
- UserGuanshi.query.get(user=user_main)
- guanshi_status = 1
- except UserGuanshi.DoesNotExist:
- guanshi_status = None
-
-
- try:
- UserZuzhang.query.get(user=user_main)
- zuzhang_status = 1
- except UserZuzhang.DoesNotExist:
- zuzhang_status = None
-
-
- # 在检查 zuzhang 扩展表之后,插入以下代码
- try:
- kg = UserShenheguan.query.get(user=user_main)
- kaoheguan_status = 1 if kg.zhuangtai == 1 else 0
- except UserShenheguan.DoesNotExist:
- kaoheguan_status = 0
-
- # 生成JWT token
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- # 计算订单数量
- order_counts = self.jisuanOrderShuliang(user_main.UserUID)
-
- # 获取群配置
- group_info = self.huoquQunPeizhi()
-
- from merchant_ops.services.authz import staff_fields_for_login
- staff_fields = staff_fields_for_login(user_main)
-
- # 返回数据
- response_data = {
- 'token': token,
- 'nicheng': boss_nickname,
- 'uid': user_main.UserUID,
- 'touxiang': user_main.Avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- 'zuzhangstatus': zuzhang_status,
- 'kaoheguanstatus': kaoheguan_status,
- 'dingdantiaoshu': order_counts,
- 'dashouqun': group_info.get('dashouqun', ''),
- 'dashouqunid': group_info.get('dashouqunid', ''),
- 'guanshiqun': group_info.get('guanshiqun', ''),
- 'guanshiqunid': group_info.get('guanshiqunid', ''),
- **staff_fields,
- }
-
- return Response({'code': 0, 'msg': '登录成功', 'data': response_data})
-
- except Exception as e:
- error_openid = locals().get('openid', 'N/A')
- logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}",
- exc_info=True)
-
- return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def get_wechat_openid(self, code):
- """
- 调用微信接口获取openid和unionid
- """
- try:
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
-
- if not appid or not secret:
- raise ValueError('微信配置未设置')
-
- url = 'https://api.weixin.qq.com/sns/jscode2session'
- params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'}
-
- response = requests.get(url, params=params, timeout=10)
- result = response.json()
-
- if 'openid' in result:
- return {'openid': result['openid'], 'session_key': result.get('session_key', ''),
- 'unionid': result.get('unionid', '')}
- else:
- errcode = result.get('errcode', 'unknown')
- errmsg = result.get('errmsg', '未知错误')
- return {'errmsg': f'[{errcode}]{errmsg}'}
-
- except requests.exceptions.Timeout:
- return {'errmsg': '请求微信接口超时'}
- except Exception as e:
- return {'errmsg': f'请求微信接口异常: {str(e)}'}
-
- def shengchengYonghuID(self):
- """
- 生成7位数字用户ID
- """
- for _ in range(10):
- timestamp_part = str(int(time.time()))[-5:].zfill(5)
- random_part = str(random.randint(0, 99)).zfill(2)
- user_id = timestamp_part + random_part
-
- if len(user_id) == 7 and user_id.isdigit():
- if not User.query.filter(UserUID=user_id).exists():
- return user_id
-
- raise Exception('生成用户ID失败')
-
- def huoquKehuduanIP(self, request):
- """
- 获取客户端真实IP地址
- """
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
- if x_forwarded_for:
- ip = x_forwarded_for.split(',')[0].strip()
- if ip:
- return ip
-
- ip = request.META.get('REMOTE_ADDR', '')
- return ip if ip else '0.0.0.0'
-
- def jisuanOrderShuliang(self, yonghuid):
- """
- 统计订单数量
- """
- try:
- if not PlatformOrderExt.query.filter(BossID=yonghuid).exists():
- return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
-
- dingdan_ids = PlatformOrderExt.query.filter(BossID=yonghuid).values_list('Order__id', flat=True)
-
- results = Order.query.filter(id__in=dingdan_ids).aggregate(
- daifuwu=Count(Case(When(Q(Status=1) | Q(Status=7), then=1), output_field=IntegerField())),
- fuwuzhong=Count(Case(When(Status=2, then=1), output_field=IntegerField())),
- yiwancheng=Count(Case(When(Status=3, then=1), output_field=IntegerField())),
- yituikuan=Count(Case(When(Status=5, then=1), output_field=IntegerField()))
- )
-
- return {
- 'daifuwu': results['daifuwu'] or 0,
- 'fuwuzhong': results['fuwuzhong'] or 0,
- 'yiwancheng': results['yiwancheng'] or 0,
- 'yituikuan': results['yituikuan'] or 0
- }
-
- except Exception:
- return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
-
- def huoquQunPeizhi(self):
- """
- 获取群配置信息
- """
- try:
- qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
-
- result = {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
-
- for config in qun_configs:
- if config.id == 1:
- result['dashouqun'] = config.GroupContent or ''
- result['dashouqunid'] = config.GroupID or ''
- elif config.id == 2:
- result['guanshiqun'] = config.GroupContent or ''
- result['guanshiqunid'] = config.GroupID or ''
-
- return result
-
- except Exception:
- return {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
-
-# views/weixin_official.py - 完整文件
-class WeixinOfficialCallbackView(APIView):
- """
- 微信服务号回调接口
- 处理用户关注、取消关注等事件
- 核心:有关注事件就存,不管有没有UnionID
- """
- throttle_classes = []
- permission_classes = [AllowAny]
- authentication_classes = []
-
- def get(self, request):
- """验证服务器配置(微信官方要求)"""
- try:
- signature = request.GET.get('signature', '')
- timestamp = request.GET.get('timestamp', '')
- nonce = request.GET.get('nonce', '')
- echostr = request.GET.get('echostr', '')
-
- token = getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', '')
-
- if not token:
- logger.error("服务号Token未配置")
- return HttpResponse('Token not configured', status=500)
-
- # 验证签名
- tmp_list = sorted([token, timestamp, nonce])
- tmp_str = ''.join(tmp_list)
- hash_str = hashlib.sha1(tmp_str.encode()).hexdigest()
-
- if hash_str == signature:
- return HttpResponse(echostr)
- else:
- logger.error(f"签名验证失败")
- return HttpResponse('Signature verification failed', status=403)
-
- except Exception as e:
- logger.error(f"验证服务器异常: {str(e)}")
- return HttpResponse('Internal Server Error', status=500)
-
- def post(self, request):
- """处理事件推送(关注、取关)"""
- try:
- xml_str = request.body.decode('utf-8')
- logger.info(f"收到服务号事件,原始数据(前200字符): {xml_str[:200]}")
-
- root = ET.fromstring(xml_str)
-
- msg_type = root.find('MsgType').text
- from_user = root.find('FromUserName').text # 服务号OpenID
-
- if msg_type == 'event':
- event = root.find('Event').text
-
- if event == 'subscribe':
- return self.handle_subscribe_event(root, from_user)
- elif event == 'unsubscribe':
- return self.handle_unsubscribe_event(root, from_user)
- else:
- # 其他事件(如菜单点击)直接返回success
- return HttpResponse('success')
-
- # 非事件消息也直接返回success
- return HttpResponse('success')
-
- except Exception as e:
- logger.error(f"处理服务号事件异常: {str(e)}", exc_info=True)
- # 即使异常也返回success,避免微信服务器重试
- return HttpResponse('success')
-
- def handle_subscribe_event(self, root, from_user):
- """处理用户关注事件 - 简化版:有就存,没有拉倒"""
- try:
- # 尝试获取UnionID(有就有,没有也没关系)
- unionid_elem = root.find('UnionId')
- unionid = unionid_elem.text if unionid_elem is not None else None
-
- logger.info(f"用户关注,OpenID: {from_user}, UnionID: {unionid}")
-
- # 核心逻辑:更新或创建记录
- obj, created = OfficialAccountUser.query.update_or_create(
- official_openid=from_user,
- defaults={
- 'unionid': unionid,
- 'is_subscribed': True,
- 'subscribe_time': timezone.now()
- }
- )
-
- action = "新建" if created else "更新"
- logger.info(f"{action}用户记录成功: OpenID={from_user}")
-
- # 回复欢迎消息(可选)
- return self.generate_text_response(root, "感谢关注星阙网络服务!平台有新订单时您会收到通知。")
-
- except Exception as e:
- logger.error(f"处理关注事件异常: {str(e)}", exc_info=True)
- # 依然返回success,不让微信重试
- return HttpResponse('success')
-
- def handle_unsubscribe_event(self, root, from_user):
- """处理用户取消关注事件 - 简化版:标记为未订阅"""
- try:
- updated_count = OfficialAccountUser.query.filter(
- official_openid=from_user
- ).update(is_subscribed=False)
-
- if updated_count > 0:
- logger.info(f"用户取消关注,已标记为未订阅: {from_user}")
- else:
- logger.info(f"用户取消关注,但记录不存在(无需处理): {from_user}")
-
- return HttpResponse('success')
-
- except Exception as e:
- logger.error(f"处理取消关注事件异常: {str(e)}", exc_info=True)
- return HttpResponse('success')
-
- def generate_text_response(self, root, content):
- """生成文本消息响应"""
- try:
- to_user = root.find('ToUserName').text
- from_user = root.find('FromUserName').text
- response_xml = f"""
-
-
-{int(time.time())}
-
-
-"""
- return HttpResponse(response_xml, content_type='application/xml')
-
- except Exception as e:
- logger.error(f"生成回复消息异常: {str(e)}")
- # 即使生成回复失败,也返回一个空的success
- return HttpResponse('success')
-
-
-
-
-# yonghu/views.py
-
-class UserInfoUpdateView(APIView):
- permission_classes = [permissions.IsAuthenticated]
-
- # 进程内缓存,防止同一code被重复调用微信(单进程安全,多进程小概率重复,但用户极少可忽略)
- _phone_cache = {}
-
- def post(self, request):
- try:
- user = request.user
- boss = self._get_or_create_boss(user)
- result = {}
-
- # 头像
- av = request.FILES.get('avatar')
- if av:
- ok, msg = validate_image(av)
- if not ok:
- return Response({'code': 1, 'msg': msg, 'data': None}, status=400)
- url = self._upload_avatar(user, av)
- if url:
- result['touxiang'] = url
- user.Avatar = url
- else:
- return Response({'code': 2, 'msg': '头像上传失败', 'data': None}, status=500)
-
- # 昵称
- nick = request.data.get('nicheng')
- if nick:
- nick = str(nick).strip()
- if len(nick) > 20:
- return Response({'code': 3, 'msg': '昵称过长', 'data': None}, status=400)
- boss.nickname = nick
- result['nicheng'] = nick
-
- # 手机号 —— 极简可靠版
- code = request.data.get('shoujihao_code')
- if code:
- phone = self._get_phone_safe(code, request)
- if phone is None:
- return Response({'code': 6, 'msg': '手机号获取失败,请重新授权', 'data': None}, status=400)
- user.Phone = phone
- user.PhoneVerified = True
- result['phone'] = phone
-
- if not result:
- return Response({'code': 5, 'msg': '无更新', 'data': None}, status=400)
-
- with transaction.atomic():
- user.save()
- boss.save()
- return Response({'code': 0, 'msg': '更新成功', 'data': result})
-
- except Exception as e:
- logger.exception("系统异常")
- return Response({'code': 99, 'msg': str(e), 'data': None}, status=500)
-
- # ---------- 安全手机号(内存去重) ----------
- def _get_phone_safe(self, code, request):
- cache_key = f'{code}:{self._resolve_wx_club_id(request, request.user)}'
- if cache_key in self._phone_cache:
- cached = self._phone_cache[cache_key]
- return cached if cached != '__FAIL__' else None
-
- self._phone_cache[cache_key] = '__PROCESSING__'
- try:
- phone = self._call_wechat(code, request)
- self._phone_cache[cache_key] = phone
- return phone
- except Exception as e:
- logger.error(f"微信调用失败: {str(e)}")
- self._phone_cache[cache_key] = '__FAIL__'
- return None
-
- def _resolve_wx_club_id(self, request, user):
- """手机号 code 与小程序 appid 绑定,须用对应俱乐部的 wx 配置换 token。"""
- from jituan.constants import CLUB_ID_DEFAULT
- from jituan.services.club_context import resolve_club_id_from_request
- from jituan.services.club_user import get_user_club_id
-
- club_id = resolve_club_id_from_request(request)
- if club_id == CLUB_ID_DEFAULT:
- user_club = get_user_club_id(user)
- if user_club:
- return user_club
- return club_id
-
- # ---------- 微信接口调用(按俱乐部 appid 换 token) ----------
- def _call_wechat(self, code, request):
- club_id = self._resolve_wx_club_id(request, request.user)
- token = self._get_new_token(club_id, force_refresh=True)
- url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
- resp = requests.post(url, json={'code': code}, timeout=10)
- data = resp.json()
- errcode = data.get('errcode')
- if errcode == 0:
- return data['phone_info']['purePhoneNumber']
- if errcode == 40001:
- token = self._get_new_token(club_id, force_refresh=True)
- url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
- resp = requests.post(url, json={'code': code}, timeout=10)
- data = resp.json()
- if data.get('errcode') == 0:
- return data['phone_info']['purePhoneNumber']
- raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}")
-
- def _get_new_token(self, club_id, force_refresh=False):
- from utils.weixin_token import get_club_mini_access_token
-
- token = get_club_mini_access_token(club_id, force_refresh=force_refresh)
- if token:
- return token
- raise Exception(f'俱乐部 {club_id} 缺少微信配置或获取 access_token 失败')
-
- # ---------- 工具 ----------
- def _get_or_create_boss(self, user):
- try:
- return UserBoss.query.get(user=user)
- except UserBoss.DoesNotExist:
- return UserBoss.query.create(user=user, nickname='微信用户')
-
- def _upload_avatar(self, user, file):
- try:
- uid = user.UserUID
- if not uid: return None
- old = user.Avatar
- if old: delete_from_oss(old)
- ext = os.path.splitext(file.name)[1] or '.jpg'
- name = f"{uuid.uuid4().hex}{ext}"
- path = f"avatar/{uid}/{name}"
- return path if upload_to_oss(file, path) else None
- except:
- return None
-
-
-
-# yonghu/views.py 或 yonghu/api_views.py
-
-class DashouXinxiAPIView(APIView):
- """
- 获取打手信息接口
- 请求方式:POST
- 认证方式:JWT Token
- 返回:打手基本信息、资产信息、会员信息,金牌打手额外返回专属字段
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取当前登录用户
- current_user = request.user
- yonghuid = current_user.UserUID
-
- # 2. 查询打手扩展表
- try:
- dashou_profile = current_user.DashouProfile
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 400,
- 'message': '您不是打手身份',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 构建基础信息响应(原有字段完全保留)
- data = {
- 'dashounicheng': dashou_profile.nicheng or '',
- 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'dashouzhuangtai': dashou_profile.zhuangtai,
- 'yongjin': str(dashou_profile.yue) if dashou_profile.yue is not None else '0.00',
- 'zonge': str(dashou_profile.zonge) if dashou_profile.zonge is not None else '0.00',
- 'yajin': str(dashou_profile.yajin) if dashou_profile.yajin is not None else '0.00',
- 'jiedanZongliang':dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0',
- 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00',
- 'chengjiaoliang': dashou_profile.chengjiaozongliang,
- 'jifen': dashou_profile.jifen,
- 'chenghao': dashou_profile.chenghao or '',
- }
-
- # 4. 查询会员信息(原有逻辑)
- huiyuan_list = []
- try:
- huiyuan_records = Huiyuangoumai.query.filter(
- yonghu_id=yonghuid
- ).select_related()
-
- for record in huiyuan_records:
- # 过期检测会同步更新 huiyuan_zhuangtai
- if record.jiance_shifou_daoqi():
- continue
- if record.huiyuan_zhuangtai == 1:
- huiyuan_list.append({
- 'huiyuanid': record.huiyuan_id,
- 'huiyuanming': record.jieshao or f"会员{record.huiyuan_id}",
- 'daoqi': record.daoqi_time
- })
- except Exception as e:
- # 会员信息查询失败不影响主要功能,返回空列表
- huiyuan_list = []
-
- data['clumber'] = huiyuan_list
-
- # 5. 如果是金牌打手,添加专属字段
- if dashou_profile.chenghao == "金牌打手":
- data.update({
- 'jiedanZongliang': dashou_profile.jiedanzongliang, # 接单总量
- 'jinriJiedan': str(dashou_profile.jinrishouyi) if dashou_profile.jinrishouyi is not None else '0.00', # 今日接单金额
- 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', # 最近提现金额(今日已提现)
- })
-
- # 6. 返回成功响应
- return Response({
- 'code': 200,
- 'message': '获取成功',
- 'data': data
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- # 生产环境应使用 logging 记录错误,而不是 print
- # logger.error(f"获取打手信息失败: {e}", exc_info=True)
- return Response({
- 'code': 500,
- 'message': f'服务器内部错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class HuoQuYaoQingRenView(APIView):
- """
- 获取当前打手的邀请人信息
- POST /yonghu/hqwdyqr
- Headers: Authorization: Bearer
- 返回邀请人的用户ID、昵称(来自老板扩展表)、头像相对URL
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- current_user = request.user # 当前登录用户
- yonghuid = current_user.UserUID # 7位用户ID
-
- # 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态)
- try:
- dashou = UserDashou.query.get(user=current_user)
- if dashou.zhanghaozhuangtai != 1:
- return Response({
- 'code': 403,
- 'message': '您的账号已被封禁,无法查看邀请人',
- 'data': None
- })
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 404,
- 'message': '您还不是打手,无法查看邀请人',
- 'data': None
- })
-
- # 2. 获取邀请人ID
- yaoqingren_id = dashou.yaoqingren
- if not yaoqingren_id:
- return Response({
- 'code': 404,
- 'message': '您没有邀请人',
- 'data': None
- })
-
- # 3. 查询邀请人主表(头像)
- try:
- inviter_main = User.query.get(UserUID=yaoqingren_id)
- touxiang = inviter_main.Avatar or ''
- except User.DoesNotExist:
- return Response({
- 'code': 404,
- 'message': '邀请人信息不存在',
- 'data': None
- })
-
- # 4. 查询老板扩展表(昵称),如果没有则使用空字符串
- nicheng = ''
- try:
- boss = UserBoss.query.get(user=inviter_main)
- nicheng = boss.nickname or ''
- except UserBoss.DoesNotExist:
- pass # 邀请人可能不是老板,昵称留空
-
- response_data = {
- 'uid': yaoqingren_id,
- 'nicheng': nicheng,
- 'touxiang': touxiang # 相对路径,前端自行拼接
- }
-
- return Response({
- 'code': 200,
- 'message': '获取成功',
- 'data': response_data
- })
-
- except Exception as e:
- logger.error(f'获取邀请人失败: {str(e)}', exc_info=True)
- return Response({
- 'code': 500,
- 'message': '系统繁忙,请稍后重试',
- 'data': None
- })
-
-
-
-
-# yonghu/views.py 或 yonghu/api_views.py
-
-class ZaixianZhuangtaiAPIView(APIView):
- """
- 更改打手在线状态接口
- 请求方式:POST
- 认证方式:JWT Token
- 请求参数:status(0=离线,1=在线)
- 返回:更新后的状态信息
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 1. 获取请求参数
- new_status = request.data.get('status')
-
- if new_status is None:
- return Response({
- 'code': 400,
- 'message': '缺少状态参数',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 转换为整数,确保是0或1
- try:
- new_status = int(new_status)
- if new_status not in [0, 1]:
- return Response({
- 'code': 400,
- 'message': '状态参数必须是0或1',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- except ValueError:
- return Response({
- 'code': 400,
- 'message': '状态参数必须是整数',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 获取当前登录用户
- current_user = request.user
- yonghuid = current_user.UserUID
-
-
-
- # 3. 查询打手扩展表
- try:
- dashou_profile = current_user.DashouProfile
- except UserDashou.DoesNotExist:
-
- return Response({
- 'code': 400,
- 'message': '您不是打手身份',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 检查账号状态(是否被封禁)
- if dashou_profile.zhanghaozhuangtai != 1:
-
- return Response({
- 'code': 403,
- 'message': '账号状态异常,无法更改在线状态',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 5. 更新在线状态
- old_status = dashou_profile.zaixianzhuangtai
-
- # 如果状态没有变化,直接返回成功
- if old_status == new_status:
-
- return Response({
- 'code': 200,
- 'message': '状态未变化',
- 'data': {
- 'zaixianzhuangtai': new_status,
- 'status_text': '在线' if new_status == 1 else '离线'
- }
- }, status=status.HTTP_200_OK)
-
- # 更新状态
- dashou_profile.zaixianzhuangtai = new_status
- dashou_profile.save()
-
-
-
- # 6. 返回成功响应
- return Response({
- 'code': 200,
- 'message': '状态更新成功',
- 'data': {
- 'zaixianzhuangtai': new_status,
- 'status_text': '在线' if new_status == 1 else '离线'
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
-
- return Response({
- 'code': 500,
- 'message': f'服务器内部错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class GuanshiXinxiView(APIView):
- """
- 获取管事信息接口
- 路径:/yonghu/guanshixinxi
- 方法:POST
- 权限:JWT认证
- 功能:获取当前管事的详细信息,包括基本信息、分红信息以及已充值打手数量
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 获取当前用户(通过JWT认证)
- current_user = request.user
-
- # 查询管事扩展表
- try:
- guanshi_profile = UserGuanshi.query.get(user=current_user)
-
- # 🔥【新增】查询已充值打手数量(从分红表中统计)
- # Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid)
- yichongzhi_count = Gsfenhong.query.filter(guanshi=current_user.UserUID).count()
-
- # 返回管事信息
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': {
- 'gszhstatus': guanshi_profile.zhuangtai, # 管事账号状态
- 'yaoqingzongshu': guanshi_profile.yaogingshuliang, # 邀请打手总数
- 'fenyongzonge': float(guanshi_profile.chongzhifenrun), # 充值分佣总额
- 'fenyongtixian': float(guanshi_profile.yue), # 可提现余额
- 'yichongzhiDashou': yichongzhi_count, # 已充值打手数量
- }
- })
-
- except UserGuanshi.DoesNotExist:
- # 用户不是管事,返回错误
- return Response({
- 'code': 400,
- 'msg': '您还不是管事,请先注册',
- 'data': None
- }, status=400)
-
- except Exception as e:
- # 捕获所有异常,返回错误信息并记录日志
- logger.error(f"获取管事信息异常: {str(e)}", exc_info=True)
- return Response({
- 'code': 500,
- 'msg': f'获取信息失败: {str(e)}',
- 'data': None
- }, status=500)
-
-
-
-
-# yonghu/views.py
-class ShangJiaXinXiView(APIView):
- """
- 获取商家信息接口 (POST /yonghu/shangjiaxinxi)
- 权限:需JWT认证
- 返回:商家基本信息 + 称号列表 + 今日/本月统计
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 获取商家扩展资料
- shangjia = request.user.ShopProfile
- except ObjectDoesNotExist:
- return Response({
- 'code': 4001,
- 'msg': '您还未注册成为商家或商家资料不存在',
- 'data': None
- }, status=status.HTTP_200_OK)
-
- user = request.user
- yonghuid = user.UserUID
- today = timezone.now().date()
- this_month_start = today.replace(day=1)
-
- # ------------------- 1. 从 UserShangjia 获取基础字段 -------------------
- response_data = {
- 'nicheng': shangjia.nicheng or '',
- 'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态
- 'fadanzong': shangjia.fabu, # 发单总量(累计)
- 'fabu': shangjia.fabu,
- 'tuikuanzong': shangjia.tuikuan, # 退款总量(累计)
- 'sjyue': float(shangjia.yue), # 商家余额
- # 'riliushui' 和 'yueliushui' 将从统计表计算,不再使用原字段
- }
-
- # ------------------- 2. 获取称号列表(仅限商家称号) -------------------
- chenghao_list = []
- # 查询当前用户的所有称号关联
- yonghu_chenghao_qs = YonghuChenghao.query.filter(yonghu=user).select_related('chenghao')
- for yc in yonghu_chenghao_qs:
- chenghao_obj = yc.chenghao
- # 只保留类型为“商家”的称号
- if chenghao_obj.leixing == 'shangjia':
- chenghao_list.append({
- 'mingcheng': chenghao_obj.mingcheng,
- 'texiao_json': chenghao_obj.texiao_miaoshu, # 前端需要的特效描述
- # 如果还需要其他字段(如图标URL)可在此添加,打手端有 texiao_json
- })
- response_data['chenghao_list'] = chenghao_list
-
- # ------------------- 3. 从 MerchantDailyStats 获取今日统计 -------------------
- try:
- today_stat = MerchantDailyStats.query.get(MerchantID=yonghuid, Date=today)
- jinri_paifa_dingdan = today_stat.AssignedOrderCount
- jinri_paifa_jine = float(today_stat.AssignedAmount)
- jinri_tuikuan_dingdan = today_stat.RefundOrderCount
- except MerchantDailyStats.DoesNotExist:
- jinri_paifa_dingdan = 0
- jinri_paifa_jine = 0.00
- jinri_tuikuan_dingdan = 0
-
- response_data['jinripaidan'] = jinri_paifa_dingdan # 今日派单数(新增)
- response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增)
- response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额)
-
- # ------------------- 4. 从 MerchantDailyStats 获取本月统计 -------------------
- month_stats = MerchantDailyStats.query.filter(
- MerchantID=yonghuid,
- Date__gte=this_month_start,
- Date__lte=today
- ).aggregate(
- total_paifa_jine=Sum('AssignedAmount'),
- total_paifa_dingdan=Sum('AssignedOrderCount')
- )
- yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00)
- # yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加
-
- response_data['yueliushui'] = yue_paifa_jine # 今月流水(派发总金额)
-
- # (可选)如果前端还需要今月派单总数,取消下面注释
- # response_data['jinyuepaidan'] = yue_paifa_dingdan
-
- return Response({
- 'code': 200,
- 'msg': '获取商家信息成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)
-
-
-
-
-
-
-
-
-
-
-
-
-class ShangjiaNichengGengxinView(APIView):
- """商家老板修改昵称 POST /yonghu/shangjiagxnc"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- from merchant_ops.services.authz import is_merchant_owner
-
- if not is_merchant_owner(request.user):
- return Response({'code': 403, 'msg': '仅商家老板可修改昵称', 'data': None}, status=status.HTTP_200_OK)
-
- nicheng = (request.data.get('nicheng') or '').strip()
- if not nicheng:
- return Response({'code': 400, 'msg': '昵称不能为空', 'data': None}, status=status.HTTP_200_OK)
- if len(nicheng) > 20:
- return Response({'code': 400, 'msg': '昵称不能超过20字', 'data': None}, status=status.HTTP_200_OK)
-
- try:
- shangjia = request.user.ShopProfile
- except ObjectDoesNotExist:
- return Response({'code': 4001, 'msg': '商家资料不存在', 'data': None}, status=status.HTTP_200_OK)
-
- if UserShangjia.query.filter(nicheng=nicheng).exclude(user=request.user).exists():
- return Response({'code': 400, 'msg': '该昵称已被使用', 'data': None}, status=status.HTTP_200_OK)
-
- shangjia.nicheng = nicheng
- shangjia.save(update_fields=['nicheng'])
-
- return Response({'code': 200, 'msg': '昵称已更新', 'data': {'nicheng': nicheng}}, status=status.HTTP_200_OK)
-
-
-class BossBiaoshiView(APIView):
- """
- 获取用户的GoEasy标识 - 极简实现
- POST /yonghu/bossbiaoshi
- 需要JWT Token认证
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 直接从request.user获取用户
- user = request.user
-
- # 2. 获取yonghuid(确保用户已登录)
- if not user or not hasattr(user, 'UserUID'):
- return Response({
- 'code': 401,
- 'msg': '用户未认证或用户信息不完整'
- }, status=401)
-
- # 3. 生成标识:Boss + yonghuid(与前端约定)
- boss_biaoshi = f"Boss{user.UserUID}"
-
- # 4. 返回给前端
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': {
- 'boss_biaoshi': boss_biaoshi,
- 'yonghuid': user.UserUID,
- 'user_type': user.UserType
- }
- })
-
-
-"""
-阿龙电竞陪玩平台 - 邀请码相关API接口
-"""
-
-class YaoqingmaView(APIView):
- """
- 获取或生成管事邀请码
- 访问路径: /api/yonghu/yaoqingma/
- 请求方法: GET
- 权限要求: JWT Token认证通过的用户
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- """
- 获取当前用户的邀请码
- 逻辑:
- 1. 通过JWT获取当前用户 (request.user)
- 2. 反向查询UserGuanshi扩展表
- 3. 验证管事状态(zhuangtai=1)
- 4. 已有邀请码直接返回,没有则生成并保存
- """
- # 获取当前认证用户
- current_user = request.user
-
- # 1. 使用反向查询获取管事扩展信息 (性能最优方式)
- try:
- guanshi_profile = current_user.GuanshiProfile
- except UserGuanshi.DoesNotExist:
- # 用户不是管事,没有扩展表记录
- return Response({
- 'code': 403,
- 'message': '当前用户不是管事身份,无法生成邀请码',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 2. 验证管事状态 (zhuangtai 必须等于 1)
- if guanshi_profile.zhuangtai != 1:
- return Response({
- 'code': 403,
- 'message': '管事账号状态异常,无法生成邀请码',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 检查是否已存在邀请码
- existing_yaoqingma = guanshi_profile.yaoqingma
-
- # 如果存在且非空,直接返回
- if existing_yaoqingma and existing_yaoqingma.strip() != '':
- return Response({
- 'code': 200,
- 'message': '成功获取现有邀请码',
- 'data': {
- 'yaoqingma': existing_yaoqingma,
- 'is_new': False # 标记是否为新生成的
- }
- })
-
- # 4. 生成新的邀请码 (使用用户ID字符串)
- # 注意: 这里使用yonghuid,不是主键id,如你要求的"123456"这种形式
- yonghuid_str = str(current_user.UserUID)
- new_yaoqingma = CreateInvitationCode(yonghuid_str)
-
- # 5. 验证生成的邀请码格式
- if not VerifyInvitationCode(new_yaoqingma):
- return Response({
- 'code': 500,
- 'message': '邀请码生成失败,格式验证错误',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 6. 保存到数据库 (使用事务确保数据一致性)
- try:
- with transaction.atomic():
- guanshi_profile.yaoqingma = new_yaoqingma
- guanshi_profile.save()
- except Exception as e:
- # 这里可以记录日志
- return Response({
- 'code': 500,
- 'message': f'保存邀请码到数据库失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 7. 返回新生成的邀请码
- return Response({
- 'code': 200,
- 'message': '成功生成新的邀请码',
- 'data': {
- 'yaoqingma': new_yaoqingma,
- 'is_new': True # 标记是新生成的
- }
- })
-
-
-
-"""
-阿龙电竞陪玩平台 - 打手注册及相关API接口
-"""
-
-
-class DashouZhuceView(APIView):
- """
- 打手注册接口 (通过管事邀请码)
- 访问路径: /api/yonghu/dashouzhuce/
- 请求方法: POST
- 权限要求: JWT Token认证通过的用户
- 请求体: { "inviteCode": "AbC123..." }
- """
- 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
- # 注意:这里request.user是User实例
-
- # 2. 检查用户是否已是打手 (使用反向查询,性能最优)
- try:
- dashou_profile = current_user.DashouProfile
- # 用户已是打手,直接返回现有信息
- return self.fanhuiXianyouDashouXinxi(dashou_profile, current_user)
- except UserDashou.DoesNotExist:
- # 用户不是打手,继续注册流程
- pass
-
- # 3. 根据邀请码查找对应的管事
- try:
- # 使用select_related一次性获取管事及其关联的主表用户信息,性能最高
- guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma)
- except UserGuanshi.DoesNotExist:
- return Response({
- 'code': 404,
- 'message': '邀请码无效或不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- from jituan.services.invite_guard import assert_invite_same_club
- ok, msg = assert_invite_same_club(request, guanshi_profile.user, current_user)
- if not ok:
- return Response({
- 'code': 403,
- 'message': msg,
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 4. 验证管事状态
-
- # 5. 核心:在数据库事务中创建用户的所有身份
- try:
- with transaction.atomic():
- # 获取管事的用户ID (yonghuid)
- guanshi_yonghuid = guanshi_profile.user.UserUID
-
- # 5.1 创建打手扩展表
- dashou_profile = UserDashou.query.create(
- user=current_user,
- nicheng='大手子', # 默认昵称
- chenghao='普通大手', # 默认称号
- yaoqingren=guanshi_yonghuid, # 设置邀请人ID
- # 其他字段全部使用模型定义的默认值
- )
-
- # 5.2 创建商家扩展表
- try:
- boss_profile = current_user.BossProfile
- if boss_profile.nickname:
- boss_nickname = boss_profile.nickname
- except:
- pass
-
-
- # 5.3 创建管事扩展表
-
- # 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑)
- # current_user.UserType = 'dashou' # 根据你的业务逻辑决定
- # current_user.save()
-
- # 5.5 更新原管事的邀请数量 (使用行锁防止并发问题)
- # 重新查询并锁定该行记录
- locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
- locked_guanshi.yaogingshuliang += 1
- locked_guanshi.save()
-
- # ========== 新增:管事每日统计(邀请打手) ==========
- try:
- update_guanshi_daily_by_action(
- yonghuid=guanshi_yonghuid,
- action=1, # 1 = 邀请打手
- amount=Decimal('0.00')
- )
- logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
- except Exception as e:
- logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
-
- except Exception as e:
-
- # 事务会自动回滚
- # 这里可以记录更详细的日志
- return Response({
- 'code': 500,
- 'message': f'注册过程中发生系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 6. 注册成功,返回信息 (新注册用户没有会员记录,clumber返回空列表)
- fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True)
- return Response({
- 'code': 200,
- 'message': '注册成功!已开通打手、管事身份。',
- 'data': fanhui_data
- })
-
- def fanhuiXianyouDashouXinxi(self, dashou_profile, current_user):
- """处理已是打手的用户,返回其现有信息"""
- fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=False)
- return Response({
- 'code': 200,
- 'message': '您已是打手,返回当前信息。',
- 'data': fanhui_data
- })
-
- def zhuangbeiFanhuiShuju(self, dashou_profile, current_user, is_new=False):
- """准备返回给前端的打手数据,字段名严格匹配前端"""
-
-
-
- # 构建基础信息
- data = {
- 'dashounicheng': dashou_profile.nicheng,
- 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
- 'yongjin': str(dashou_profile.yue), # Decimal转字符串
- 'zonge': str(dashou_profile.zonge),
- 'yajin': str(dashou_profile.yajin),
- 'chenghao': dashou_profile.chenghao,
- 'jinfen': dashou_profile.jifen,
- 'chengjiaoliang': dashou_profile.chengjiaozongliang,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'dashouzhuangtai': dashou_profile.zhuangtai,
- }
-
- # 查询并处理会员列表 (clumber)
- # 使用当前用户的yonghuid进行查询
- huiyuan_goumai_list = []
- if not is_new: # 如果是新用户,直接返回空列表,无需查询
- goumai_records = Huiyuangoumai.query.filter(yonghu_id=current_user.UserUID)
- for record in goumai_records:
- # 调用方法检查是否到期,此方法内部会更新状态
- shifou_daoqi = record.jiance_shifou_daoqi()
- huiyuan_goumai_list.append({
- 'huiyuan_id': record.huiyuan_id,
- 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
- 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
- 'shifou_daoqi': shifou_daoqi,
- 'jieshao': record.jieshao
- })
-
- data['clumber'] = huiyuan_goumai_list
- return data
-
-
-
-
-
-
-class GuanshiYaoqingDashouListView(APIView):
- """
- 管事获取已邀请打手列表(支持筛选、搜索、分页)
- POST /api/yonghu/gshqyqds/
- Headers: Authorization: Bearer
- Body:
- {
- "page": 1, // 页码,从1开始
- "pageSize": 30, // 每页条数,最大100
- "keyword": "", // 搜索关键词(用户ID或昵称)
- "zhanghaozhuangtai": 1, // 账号状态:1=正常,2=封禁(可选)
- "zaixianzhuangtai": 1 // 在线状态:1=在线,2=离线(可选)
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- current_user = request.user
- guanshi_yonghuid = current_user.UserUID
-
- # 1. 分页参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 30))
- if page < 1 or page_size < 1:
- return Response({'code': 400, 'message': '分页参数必须大于0'}, status=400)
- if page_size > 100:
- page_size = 100
-
- # 2. 筛选参数
- keyword = request.data.get('keyword', '').strip()
- zhanghaozhuangtai = request.data.get('zhanghaozhuangtai') # 可选:1=正常,2=封禁
- zaixianzhuangtai = request.data.get('zaixianzhuangtai') # 可选:1=在线,2=离线
-
- # 3. 基础查询:当前管事邀请的所有打手
- base_qs = UserDashou.query.select_related('user').filter(
- yaoqingren=guanshi_yonghuid
- )
-
- # 4. 全局统计(不受筛选影响,直接基于 base_qs)
- total_all = base_qs.count()
- zaixian_all = base_qs.filter(zaixianzhuangtai=1).count()
- zhengchang_all = base_qs.filter(zhanghaozhuangtai=1).count()
- fengjin_all = total_all - zhengchang_all
-
- # 5. 应用筛选条件
- if keyword:
- base_qs = base_qs.filter(
- Q(nicheng__icontains=keyword) | Q(user__UserUID__icontains=keyword)
- )
- if zhanghaozhuangtai is not None:
- try:
- val = int(zhanghaozhuangtai)
- if val == 1:
- base_qs = base_qs.filter(zhanghaozhuangtai=1)
- else:
- base_qs = base_qs.exclude(zhanghaozhuangtai=1)
- except (ValueError, TypeError):
- pass
- if zaixianzhuangtai is not None:
- try:
- val = int(zaixianzhuangtai)
- if val == 1:
- base_qs = base_qs.filter(zaixianzhuangtai=1)
- else:
- base_qs = base_qs.exclude(zaixianzhuangtai=1)
- except (ValueError, TypeError):
- pass
-
- # 6. 排序和分页
- base_qs = base_qs.order_by('-CreateTime')
- paginator = Paginator(base_qs, page_size)
- try:
- page_obj = paginator.page(page)
- except EmptyPage:
- page_obj = []
- has_more = False
- filtered_total = paginator.count if paginator.count else 0
- else:
- has_more = page_obj.has_next()
- filtered_total = paginator.count
-
- # 7. 构建打手列表
- dashou_list = []
- for dashou in page_obj:
- user_main = dashou.user
- dashou_list.append({
- 'uid': user_main.UserUID,
- 'nicheng': dashou.nicheng or '打手',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'touxiang': user_main.Avatar or '',
- 'jiedanzongliang': dashou.jiedanzongliang,
- 'chengjiaozongliang': dashou.chengjiaozongliang,
- 'tuikuanliang': dashou.tuikuanliang,
- 'yue': str(dashou.zonge),
- })
-
- # 8. 获取管事邀请总数(来自管事扩展表)
- try:
- yaoqingzongshu = current_user.GuanshiProfile.yaogingshuliang
- except Exception:
- yaoqingzongshu = total_all
-
- response_data = {
- 'dashouList': dashou_list,
- 'yaoqingzongshu': yaoqingzongshu, # 邀请总数(全局)
- 'totalCount': filtered_total, # 当前筛选条件下的总数
- 'zaixianCount': zaixian_all, # 全局在线
- 'zhengchangCount': zhengchang_all, # 全局正常
- 'fengjinCount': fengjin_all, # 全局封禁
- 'currentPage': page,
- 'pageSize': page_size,
- 'hasMore': has_more,
- }
-
- return Response({'code': 200, 'message': '获取成功', 'data': response_data})
-
- except ValueError:
- return Response({'code': 400, 'message': '分页参数格式错误'}, status=400)
- except Exception as e:
- logger.error(f'获取打手列表失败: {str(e)}', exc_info=True)
- return Response({'code': 500, 'message': '系统错误'}, status=500)
-
-
-
-
-
-class TixianXinxiHuoquView(APIView):
- """
- 获取用户提现收款信息接口
- 返回用户已设置的手机号、收款码、收款账号
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- """
- 处理获取收款信息的POST请求
- 直接返回用户当前的收款设置
- """
- try:
- # 从request.user获取当前用户
- user_main = request.user
-
- from jituan.services.club_user import get_user_club_id
- from jituan.services.withdraw_config import get_tixian_feilv
-
- club_id = get_user_club_id(user_main)
- dashou_rate = get_tixian_feilv(club_id, 1)
- guanshi_rate = get_tixian_feilv(club_id, 2)
-
- # 构建返回数据
- response_data = {
- 'txdianhua': user_main.Phone if user_main.Phone else '',
- 'txtupian': user_main.PaymentQRCode if user_main.PaymentQRCode else '',
- 'txzh': user_main.PaymentAccount if user_main.PaymentAccount else '',
- 'dashou_rate': dashou_rate,
- 'guanshi_rate': guanshi_rate,
- }
-
- # 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': response_data
- })
-
- except Exception as e:
- return Response({
- 'code': 99,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-class ShoukuanXinxiShangchuanView(APIView):
- """
- 上传用户收款信息接口
- 支持同时上传手机号、收款账号、收款码图片
- 使用multipart/form-data格式
- """
- permission_classes = [permissions.IsAuthenticated]
- parser_classes = (MultiPartParser,FormParser,JSONParser)
-
- def post(self, request):
- """
- 处理上传收款信息的POST请求
- 验证并保存用户的收款设置
- """
- try:
- # 获取当前用户
- user_main = request.user
- yonghuid = user_main.UserUID
-
- # 准备响应数据
- response_data = {}
-
- # 1. 验证必填字段:手机号必须要有
- txdianhua = request.data.get('txdianhua', '').strip()
- if not txdianhua:
- return Response({
- 'code': 1,
- 'msg': '收款人电话不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证手机号格式(11位数字)
- if not txdianhua.isdigit() or len(txdianhua) != 11:
- return Response({
- 'code': 2,
- 'msg': '请输入11位有效的手机号码',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 获取收款账号(可能为空)
- txzh = request.data.get('txzh', '').strip()
-
- # 3. 处理收款码图片上传(如果有)
- txtupian_file = request.FILES.get('file') # 注意:前端upload.js使用'file'作为字段名
- txtupian_url = ''
-
- if txtupian_file:
- # 验证图片
- is_valid, error_msg = validate_image(txtupian_file)
- if not is_valid:
- return Response({
- 'code': 3,
- 'msg': f'图片验证失败: {error_msg}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 上传收款码图片
- txtupian_url = self.handle_shoukuanma_upload(user_main, txtupian_file)
- if txtupian_url:
- response_data['txtupian'] = txtupian_url
- else:
- return Response({
- 'code': 4,
- 'msg': '收款码上传失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
- else:
- response_data['txtupian'] = ''
-
- # 4. 验证收款账号和收款码至少有一个
- if not txzh and not txtupian_url:
- return Response({
- 'code': 5,
- 'msg': '请填写收款账号或上传收款码',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 保存所有信息到数据库
- with transaction.atomic():
- user_main.Phone = txdianhua
- user_main.PaymentAccount = txzh if txzh else ''
-
- # 只有上传了图片才更新zhifu字段
- if txtupian_url:
- user_main.PaymentQRCode = txtupian_url
-
- user_main.save()
-
- # 6. 构建完整的返回数据
- response_data['txdianhua'] = txdianhua
- response_data['txzh'] = txzh if txzh else ''
-
- # 7. 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '收款信息设置成功',
- 'data': response_data
- })
-
- except Exception as e:
- traceback.print_exc() # 打印详细错误信息到控制台
- return Response({
- 'code': 99,
- 'msg': f'系统错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def handle_shoukuanma_upload(self, user_main, image_file):
- """
- 处理收款码图片上传到腾讯云COS
- 存储路径: tixian/{yonghuid}/{yonghuid}.扩展名
- """
- try:
- yonghuid = user_main.UserUID
- if not yonghuid:
- return None
-
- # 获取旧收款码URL
- old_shoukuanma_url = user_main.PaymentQRCode
-
- # 生成文件扩展名
- file_name = image_file.name
- file_extension = os.path.splitext(file_name)[1].lower()
- if not file_extension:
- file_extension = '.jpg'
-
- # 构建OSS文件路径: tixian/{yonghuid}/{yonghuid}{扩展名}
- # 使用用户ID作为文件名,确保同一用户多次上传会覆盖
- oss_file_name = f"{yonghuid}{file_extension}"
- oss_file_path = f"tixian/{yonghuid}/{oss_file_name}"
-
- # 删除旧收款码(如果存在)
- if old_shoukuanma_url and old_shoukuanma_url.strip():
- delete_from_oss(old_shoukuanma_url)
-
- # 上传新收款码到OSS
- new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
-
- if new_shoukuanma_url:
- # 返回相对路径(从tixian文件夹开始)
- return oss_file_path
- else:
- return None
-
- except Exception as e:
- print(f"处理收款码上传失败: {e}")
- return None
-
-
-
-
-
-
-# ========== 订单状态常量(请根据您的实际业务修改)==========
-# 假设状态值:已完成=9,已退款=10(示例,实际以您的项目为准)
-ORDER_STATUS_COMPLETED = 3 # 订单已完成
-ORDER_STATUS_REFUNDED = 5 # 订单已退款
-ORDER_STATUS_FAIL = 6
-# 进行中状态(不可提现)例如:待接单、已接单、进行中等,您需要定义排除列表
-FORBIDDEN_STATUS_FOR_WITHDRAW = [1, 2, 3, 4, 5, 6, 7, 8] # 除已完成和已退款外的所有状态
-
-
-class TixianShenqingView(APIView):
- """
- 用户提现申请接口(完整版)
- 支持类型:
- 1-打手佣金提现
- 2-管事分红提现
- 3-组长分红提现
- 4-审核官分佣提现
- 5-打手押金提现(退出平台)
- 6-商家余额提现(退出平台)
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- user_main = request.user
- yonghuid = user_main.UserUID
-
- # 1. 获取并验证参数
- leixing = request.data.get('leixing')
- jine = request.data.get('jine')
- fangshi = request.data.get('fangshi')
- txskfs = request.data.get('txskfs', 0)
-
- # 罚单检查(共用)
- fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
- if not fadan_ok:
- return Response({'code': 400, 'msg': fadan_msg})
-
- # 参数完整性
- missing = []
- if leixing is None: missing.append('leixing')
- if jine is None: missing.append('jine')
- if fangshi is None: missing.append('fangshi')
- if missing:
- return Response({'code': 1, 'msg': f'缺少参数: {", ".join(missing)}'},
- status=status.HTTP_400_BAD_REQUEST)
-
- try:
- leixing = int(leixing)
- jine = decimal.Decimal(str(jine))
- fangshi = int(fangshi)
- txskfs = int(txskfs)
- except (ValueError, TypeError, decimal.InvalidOperation) as e:
- return Response({'code': 2, 'msg': f'参数格式错误: {str(e)}'},
- status=status.HTTP_400_BAD_REQUEST)
-
- if jine <= decimal.Decimal('0'):
- return Response({'code': 3, 'msg': '提现金额必须大于0'},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 支持的提现类型(1-6)
- if leixing not in [1, 2, 3, 4, 5, 6]:
- return Response({'code': 4, 'msg': '无效的提现类型'},
- status=status.HTTP_400_BAD_REQUEST)
-
- if fangshi not in [1, 2]:
- return Response({'code': 5, 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)'},
- status=status.HTTP_400_BAD_REQUEST)
-
- from jituan.services.club_user import get_user_club_id
- from jituan.services.withdraw_config import get_tixian_feilv
-
- club_id = get_user_club_id(user_main)
- current_rate = decimal.Decimal('0.00')
- rate_key = {
- 1: '5', # 打手佣金
- 2: '6', # 管事分红
- 3: '8', # 组长分红
- 4: '9', # 审核官分佣
- 5: '11', # 打手押金提现
- 6: '10', # 商家余额提现
- }.get(leixing)
-
- if rate_key:
- try:
- current_rate = get_tixian_feilv(club_id, leixing)
- except ValueError:
- return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing})'},
- status=status.HTTP_400_BAD_REQUEST)
-
- shouxufei = jine * current_rate
- shijidaozhang = jine - shouxufei
- if shijidaozhang <= decimal.Decimal('0'):
- return Response({'code': 6, 'msg': '提现金额过低,扣除手续费后无实际到账'},
- status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 根据提现类型执行校验和扣款
- nicheng = ''
- if leixing == 1:
- result = self._check_dashou_commission_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- nicheng = result['data']['nicheng']
- # 扣款(佣金)
- with transaction.atomic():
- profile = UserDashou.objects.select_for_update().get(user=user_main)
- if profile.yue < jine:
- return Response({'code': 13, 'msg': '余额不足'})
- profile.yue = F('yue') - jine
- profile.save()
- elif leixing == 2:
- result = self._check_guanshi_dividend_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- with transaction.atomic():
- profile = UserGuanshi.objects.select_for_update().get(user=user_main)
- if profile.yue < jine:
- return Response({'code': 22, 'msg': '余额不足'})
- profile.yue = F('yue') - jine
- profile.save()
- nicheng = "管事用户"
- elif leixing == 3:
- result = self._check_zuzhang_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- with transaction.atomic():
- profile = UserZuzhang.objects.select_for_update().get(user=user_main)
- if profile.ketixian_jine < jine:
- return Response({'code': 32, 'msg': '可提现金额不足'})
- profile.ketixian_jine = F('ketixian_jine') - jine
- profile.save()
- nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else ''
- elif leixing == 4:
- result = self._check_shenheguan_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- with transaction.atomic():
- profile = UserShenheguan.objects.select_for_update().get(user=user_main)
- if profile.yue < jine:
- return Response({'code': 42, 'msg': '可提现金额不足'})
- profile.yue = F('yue') - jine
- profile.save()
- nicheng = "审核官"
- elif leixing == 5:
- # 打手押金提现(退出)
- result = self._check_dashou_yajin_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- with transaction.atomic():
- profile = UserDashou.objects.select_for_update().get(user=user_main)
- if profile.yajin < jine:
- return Response({'code': 53, 'msg': '押金余额不足'})
- profile.yajin = F('yajin') - jine
- # 押金提现后封禁打手账号
- profile.zhanghaozhuangtai = 0 # 0表示禁用
- profile.save()
- nicheng = profile.nicheng or ''
- elif leixing == 6:
- # 商家余额提现(退出)
- result = self._check_shangjia_withdraw(user_main, jine)
- if result['code'] != 0:
- return Response(result, status=status.HTTP_400_BAD_REQUEST)
- with transaction.atomic():
- profile = UserShangjia.objects.select_for_update().get(user=user_main)
- if profile.yue < jine:
- return Response({'code': 63, 'msg': '商家余额不足'})
- profile.yue = F('yue') - jine
- # 注意:是否封禁商家账号?根据需求,用户未明确要求封禁,暂时只扣款,可后续扩展。
- profile.save()
- nicheng = profile.nicheng or ''
-
- # 4. 创建提现记录(先扣款后创建,确保资金安全)
- try:
- avatar = user_main.Avatar or ''
- phone = user_main.Phone or ''
- zhifu = user_main.PaymentQRCode or ''
- skzhanghao = user_main.PaymentAccount or ''
- from jituan.services.club_user import get_user_club_id
- tixian_record = Tixianjilu.query.create(
- club_id=get_user_club_id(user_main),
- yonghuid=yonghuid,
- avatar=avatar,
- phone=phone,
- nicheng=nicheng,
- leixing=leixing,
- zhifu=zhifu,
- skzhanghao=skzhanghao,
- jine=shijidaozhang, # 记录实际到账金额
- zhuangtai=1, # 审核中
- fangshi=fangshi,
- shenheid=None,
- bhliyou='',
- )
- except Exception as e:
- return Response({'code': 99, 'msg': f'创建提现记录失败: {str(e)}'},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 5. 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '提现申请提交成功',
- 'data': {
- 'tixian_id': tixian_record.id,
- 'leixing': leixing,
- 'jine': str(jine.quantize(decimal.Decimal('0.01'))),
- 'fangshi': fangshi,
- 'zhuangtai': 1,
- 'CreateTime': tixian_record.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
- 'current_rate': str(current_rate.quantize(decimal.Decimal('0.0000'))),
- 'shouxufei': str(shouxufei.quantize(decimal.Decimal('0.01'))),
- 'shijidaozhang': str(shijidaozhang.quantize(decimal.Decimal('0.01'))),
- 'tip': f'本次提现手续费率{current_rate * 100}%,手续费{shouxufei}元,实际到账{shijidaozhang}元'
- }
- })
-
- except Exception as e:
- traceback.print_exc()
- return Response({'code': 99, 'msg': f'系统错误: {str(e)}'},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # ==================== 校验函数 ====================
-
- def _check_dashou_commission_withdraw(self, user_main, jine):
- """打手佣金提现校验"""
- try:
- dashou = UserDashou.query.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 10, 'msg': '您不是打手身份,无法提现佣金'}
-
- if dashou.zhanghaozhuangtai != 1:
- return {'code': 11, 'msg': '打手账号已被封禁,无法提现'}
- if dashou.zhuangtai != 1:
- return {'code': 12, 'msg': '您有订单进行中,请完成后提现'}
- if dashou.jifen != 10:
- return {'code': 15, 'msg': '积分不足10分,请补充积分后提现'}
- if dashou.yue < jine:
- return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'}
-
- trial_ok, trial_msg = check_dashou_trial_blocks_commission_withdraw(user_main.UserUID)
- if not trial_ok:
- return {'code': 16, 'msg': trial_msg}
-
- return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
-
- def _check_guanshi_dividend_withdraw(self, user_main, jine):
- """管事分红提现校验"""
- try:
- guanshi = UserGuanshi.query.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 20, 'msg': '您不是管事身份,无法提现分红'}
- if guanshi.zhuangtai != 1:
- return {'code': 21, 'msg': '管事账号已被封禁,无法提现'}
- if guanshi.yue < jine:
- return {'code': 22, 'msg': f'余额不足,当前余额: {guanshi.yue}'}
- return {'code': 0, 'msg': 'ok', 'data': {}}
-
- def _check_zuzhang_withdraw(self, user_main, jine):
- """组长分红提现校验"""
- try:
- zuzhang = UserZuzhang.query.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 30, 'msg': '您不是组长身份,无法提现组长分红'}
- if zuzhang.zhuangtai != 1:
- return {'code': 31, 'msg': '组长账号已被封禁,无法提现'}
- if zuzhang.ketixian_jine < jine:
- return {'code': 32, 'msg': f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}'}
- return {'code': 0, 'msg': 'ok', 'data': {}}
-
- def _check_shenheguan_withdraw(self, user_main, jine):
- """审核官分佣提现校验"""
- try:
- shenheguan = UserShenheguan.query.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 40, 'msg': '您不是审核官身份,无法提现分佣'}
- if shenheguan.zhuangtai != 1:
- return {'code': 41, 'msg': '审核官账号已被封禁,无法提现'}
- if shenheguan.yue < jine:
- return {'code': 42, 'msg': f'可提现金额不足,当前可提: {shenheguan.yue}'}
- return {'code': 0, 'msg': 'ok', 'data': {}}
-
-
- def _check_shangjia_withdraw(self, user_main, jine):
- """
- 商家余额提现校验(退出平台)
- 包含:订单、自身罚单、申请的罚单检查
- """
- try:
- shangjia = UserShangjia.query.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 60, 'msg': '您不是商家身份,无法提现商家余额'}
-
- if shangjia.zhuangtai != 1:
- return {'code': 61, 'msg': '商家账号已被禁用,无法提现'}
- if shangjia.yue < jine:
- return {'code': 62, 'msg': f'商家余额不足,当前余额: {shangjia.yue}'}
-
- # 订单检查(使用 Q 取反)
- unfinished_orders = MerchantOrderExt.query.filter(
- MerchantID=user_main.UserUID,
- ).exclude(
- Order__Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED,ORDER_STATUS_FAIL]
- ).exists()
- if unfinished_orders:
- return {'code': 63, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提现余额'}
-
- # 自身被处罚的罚单检查(状态不是已缴纳(2)和已驳回(4))
- self_fadan = Penalty.query.filter(
- PenalizedUserID=user_main.UserUID,
- ).exclude(
- Status__in=[2, 4]
- ).exists()
- if self_fadan:
- return {'code': 64, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
-
- # 申请的对他人处罚罚单检查(状态不是已缴纳(2)和已驳回(4))
- applied_fadan = Penalty.query.filter(
- ApplicantID=user_main.UserUID,
- ).exclude(
- Status__in=[2, 4]
- ).exists()
- if applied_fadan:
- return {'code': 65, 'msg': '您发起的对其他用户的处罚尚未处理完毕(待缴纳/申诉中),请等待完结'}
-
- return {'code': 0, 'msg': 'ok', 'data': {'nicheng': shangjia.nicheng or ''}}
-
- def _check_dashou_yajin_withdraw(self, user_main, jine):
- """
- 打手押金提现校验(退出平台)
- 要求:无进行中的订单,无未结算订单,自身罚单需已处理
- """
- try:
- dashou = UserDashou.query.get(user=user_main)
- except ObjectDoesNotExist:
- return {'code': 50, 'msg': '您不是打手身份,无法提取押金'}
-
- if dashou.zhanghaozhuangtai != 1:
- return {'code': 51, 'msg': '打手账号已禁用,无法提取押金'}
- if dashou.zhuangtai != 1:
- return {'code': 52, 'msg': '您有订单进行中,请完成后提取押金'}
- if dashou.yajin < jine:
- return {'code': 53, 'msg': f'押金余额不足,当前押金: {dashou.yajin}'}
-
- # 订单检查(使用 exclude + __in)
- unfinished_orders = Order.query.filter(
- PlayerID=user_main.UserUID,
- ).exclude(
- Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]
- ).exists()
- if unfinished_orders:
- return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'}
-
- # 自身罚单检查
- self_fadan = Penalty.query.filter(
- PenalizedUserID=user_main.UserUID,
- ).exclude(
- Status__in=[2, 4]
- ).exists()
- if self_fadan:
- return {'code': 55, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
-
- return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
-
-
-
-
-
-
-
-
-
-class TixianJiluHuoquViewV2(APIView):
- """
- 提现记录获取接口
- mode=1:手动提现记录(默认,兼容旧版)
- mode=2:自动提现审核记录(含 shenhe_jilu_id / shenhe_danhao,支持 zhuangtai 筛选)
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- user_main = request.user
- yonghuid = user_main.UserUID
- page = request.data.get('page', 1)
- limit = request.data.get('limit', 50)
- mode = int(request.data.get('mode', 1) or 1)
- zhuangtai_filter = int(request.data.get('zhuangtai', 0) or 0)
-
- try:
- page = int(page)
- limit = int(limit)
- if page < 1 or limit < 1:
- return Response({'code': 1, 'msg': '参数错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
- except (TypeError, ValueError):
- return Response({'code': 1, 'msg': '参数格式错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if limit > 100:
- limit = 100
- offset = (page - 1) * limit
-
- qs = Tixianjilu.query.filter(yonghuid=yonghuid)
- if mode == 2:
- # 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选
- if zhuangtai_filter > 0:
- qs = qs.filter(zhuangtai=zhuangtai_filter)
- qs = qs.order_by('-CreateTime')
-
- records_queryset = qs.values(
- 'id', 'leixing', 'jine', 'zhuangtai', 'CreateTime', 'fangshi',
- 'phone', 'skzhanghao', 'zhifu', 'bhliyou',
- 'shenhe_jilu_id', 'shenhe_danhao',
- )[offset:offset + limit + 1]
-
- records_list = list(records_queryset)
- has_more = len(records_list) > limit
- records_to_return = records_list[:limit] if has_more else records_list
-
- # 批量取审核表 fail_reason
- audit_ids = [r['shenhe_jilu_id'] for r in records_to_return if r.get('shenhe_jilu_id')]
- audit_map = {}
- if audit_ids:
- for a in TixianShenheJilu.query.filter(id__in=audit_ids).values(
- 'id', 'fail_reason', 'shenqing_jine', 'shenhe_danhao'
- ):
- audit_map[a['id']] = a
-
- # 批量取自动打款失败原因
- danhao_list = [r['shenhe_danhao'] for r in records_to_return if r.get('shenhe_danhao')]
- auto_fail_map = {}
- if danhao_list:
- for ar in TixianAutoRecord.query.filter(
- shenhe_danhao__in=danhao_list, zhuangtai=2
- ).order_by('-CreateTime').values('shenhe_danhao', 'fail_reason'):
- if ar['shenhe_danhao'] not in auto_fail_map:
- auto_fail_map[ar['shenhe_danhao']] = ar['fail_reason']
-
- formatted_records = []
- for record in records_to_return:
- audit = audit_map.get(record.get('shenhe_jilu_id')) if record.get('shenhe_jilu_id') else None
- fail_reason = ''
- if audit and audit.get('fail_reason'):
- fail_reason = audit['fail_reason']
- elif record.get('shenhe_danhao'):
- fail_reason = auto_fail_map.get(record['shenhe_danhao'], '') or ''
-
- jine_val = float(record['jine']) if record['jine'] else 0.00
- if mode == 2 and audit and audit.get('shenqing_jine') is not None:
- jine_val = float(audit['shenqing_jine'])
-
- shenhe_danhao_val = record.get('shenhe_danhao') or (audit.get('shenhe_danhao') if audit else '')
- formatted_records.append({
- 'id': record['id'],
- 'tixianjilu_id': record['id'],
- 'tixian_id': record['id'],
- 'leixing': record['leixing'],
- 'jine': jine_val,
- 'zhuangtai': record['zhuangtai'],
- 'create': record['CreateTime'].strftime('%Y-%m-%d %H:%M:%S'),
- 'fangshi': record['fangshi'],
- 'txdianhua': record['phone'] or '',
- 'txzh': record['skzhanghao'] or '',
- 'txtupian': record['zhifu'] or '',
- 'shibaiyuanyin': record['bhliyou'] or '',
- 'bhliyou': record['bhliyou'] or '',
- 'fail_reason': fail_reason,
- 'shenhe_jilu_id': record.get('shenhe_jilu_id'),
- 'shenhe_danhao': shenhe_danhao_val,
- })
-
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': {
- 'list': formatted_records,
- 'has_more': has_more,
- 'page': page,
- 'limit': limit,
- 'current_count': len(formatted_records),
- },
- })
-
- except Exception as e:
- return Response({
- 'code': 99,
- 'msg': f'系统错误: {str(e)}',
- 'data': None,
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-# yonghu/views.py
-class AdminLoginView(APIView):
- """
- 管理员登录接口
- 路径:/yonghu/adlog/
- 方法:POST
- 字段:zhanghao(账号), mima(密码), ip(二次验证码)
- """
-
- # 限制匿名用户访问频率,防止暴力破解
- throttle_classes = [AnonRateThrottle]
- # 允许任何人访问(登录接口不需要认证)
- permission_classes = [AllowAny]
-
- def post(self, request):
- """
- 处理管理员登录请求
- 安全策略:任何错误都返回401,不透露具体信息
- """
- try:
- # 1. 获取客户端真实IP和用户代理信息
- kehuduan_ip = huoquKehuduanIP(request)
- yonghu_daili = huoquYonghuDaili(request)
-
-
- # 2. 获取前端传递的数据
- data = request.data
-
- # 检查必需字段是否存在
- bixu_ziduan = ['zhanghao', 'mima', 'ip']
- for ziduan in bixu_ziduan:
- if ziduan not in data:
- # 字段缺失,返回401
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 提取并清理数据
- zhanghao = data.get('zhanghao', '').strip()
- mima = data.get('mima', '').strip()
- erci_yanzhengma = data.get('ip', '').strip() # 二次验证码(前端叫ip)
-
- # 简单验证字段非空
- if not zhanghao or not mima or not erci_yanzhengma:
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 3. 使用事务确保数据一致性
- with transaction.atomic():
- # 【优化点1】使用select_related一次性获取管理员扩展表
- # 只查询管理员类型的用户(密码用 CheckPassword 验证,不能在 ORM 中直接匹配哈希)
- user = User.query.filter(
- Phone=zhanghao, # 手机号作为账号
- AdminProfile__isnull=False # 必须是管理员类型
- ).select_related('AdminProfile').first()
-
- # 如果用户不存在或不是管理员,返回401
- if not user:
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 验证密码(bcrypt 哈希验证)
- if not user.CheckPassword(mima):
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 【优化点2】检查管理员扩展表是否存在
- if not hasattr(user, 'AdminProfile'):
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 【优化点3】验证二次密码(管理员扩展表中的password字段)
- admin_profile = user.AdminProfile
- if admin_profile.password != erci_yanzhengma:
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 更新用户登录信息
- # 获取当前时间
- now_time = timezone.now()
-
- # 处理IP地址长度问题
- # 如果原ip字段长度不够,截取前11个字符(保持兼容性)
-
- cunchu_ip = kehuduan_ip
- #print(cunchu_ip)
-
- # 更新用户信息
- user.IP = cunchu_ip # 存储客户端IP
- user.UserLastLoginDate = now_time # 更新最后登录时间
- user.save()
-
- # 【优化点4】生成JWT token(使用和微信登录相同的方式)
- # 使用djangorestframework-simplejwt生成token
- refresh = RefreshToken.for_user(user)
- token = str(refresh.access_token)
-
- # 5. 准备返回数据
- # 返回用户基本信息(可根据需要调整)
- response_data = {
- 'token': token,
- 'yonghuid': user.UserUID,
- 'phone': user.Phone or '',
- 'user_type': user.UserType,
- 'last_login_time': now_time.strftime('%Y-%m-%d %H:%M:%S') if now_time else '',
- }
-
- # 可以添加管理员特定信息
- if hasattr(user, 'AdminProfile'):
- response_data['wechat'] = user.AdminProfile.wechat or ''
-
- return Response({
- 'code': 0,
- 'msg': '登录成功',
- 'data': response_data
- })
-
- except Exception as e:
- # 【安全点】任何异常都返回401,不暴露具体错误信息
- # 记录日志用于排查问题,但前端只收到通用错误信息
- # 这里可以添加日志记录,比如:
- logging.error(f'管理员登录异常: {str(e)}', exc_info=True)
-
- return Response(
- {'code': 401, 'msg': '登录失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
-
-# yonghu/views.py
-class AdminFinancialDataView(APIView):
- """
- 管理员财务数据接口
- 路径:/yonghu/adhq/
- 方法:POST
- 认证:JWT Token
- 权限:仅管理员
- 返回:收支记录数据
- """
-
- # 使用JWT认证
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- """
- 处理管理员财务数据请求
- 安全策略:任何错误都返回401,不透露具体信息
- """
- try:
- # 1. 获取前端传递的数据
- data = request.data
-
- # 检查必需字段
- if 'zhanghao' not in data:
- return Response(
- {'code': 401, 'msg': '请求参数错误', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- qianzhanghao = data.get('zhanghao', '').strip()
-
- # 简单验证字段非空
- if not qianzhanghao:
- return Response(
- {'code': 401, 'msg': '请求参数错误', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 2. 获取当前登录用户(JWT认证后,request.user就是User实例)
- dangqianyonghu = request.user
-
- # 3. 验证用户类型是否为admin
- if not hasattr(dangqianyonghu, 'UserType') or dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'msg': '权限不足', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证前端传递的账号与当前用户的phone字段是否一致
- if not dangqianyonghu.Phone or dangqianyonghu.Phone != qianzhanghao:
- return Response(
- {'code': 401, 'msg': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在(反向查询)
- # 使用高效的ORM查询,避免额外的数据库查询
- try:
- # 使用select_related一次性获取管理员扩展表
- # UserType 是 @property 非数据库字段,用 AdminProfile__isnull=False 替代
- yonghu_xiangxi = User.query.select_related('AdminProfile').get(
- pk=dangqianyonghu.pk,
- AdminProfile__isnull=False
- )
-
- # 检查管理员扩展表是否存在
- if not hasattr(yonghu_xiangxi, 'AdminProfile'):
- return Response(
- {'code': 401, 'msg': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- except User.DoesNotExist:
- return Response(
- {'code': 401, 'msg': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 查询收支记录表(按俱乐部 xq,旧管理员端默认)
- from jituan.constants import CLUB_ID_DEFAULT
- from jituan.services.szjilu_accounting import get_szjilu_snapshot
- huizongshuju = get_szjilu_snapshot(CLUB_ID_DEFAULT)
-
- # 7. 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '获取成功',
- 'data': huizongshuju
- })
-
- except Exception as e:
- # 【安全策略】任何异常都返回401,不暴露具体错误信息
- # 这里可以记录日志,但不返回给前端
- # logging.error(f'管理员财务数据接口异常: {str(e)}', exc_info=True)
-
- return Response(
- {'code': 401, 'msg': '获取失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
-
-
-
-
-
-# shangpin/views.py
-class AdGetOrderTypes(APIView):
- """
- 后台获取订单类型列表接口
- 前端调用地址: /shangpin/getOrderTypes
- 前端需要字段: id, biaoti, tupian_url
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
-
- if not qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. 身份验证(按你说的方式)
- dangqianyonghu = request.user
-
- # 验证手机号
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证管理员身份
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 3. 验证管理员扩展表
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 4. 查询订单类型(商品类型)表
- # 注意:这里需要根据你实际的模型名调整
- # 如果模型名不是ShangpinType,请告诉我正确的模型名
-
- # 假设模型名是ShangpinLeixing(因为我之前看到你有商品类型表)
- # 你需要告诉我确切的模型名
- try:
- # 请替换为你的实际模型名
- # 只查询前端需要的三个字段
- shangpin_types = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
-
- # 构建响应数据
- type_list = [
- {
- 'id': item.id,
- 'biaoti': item.jieshao or '',
- 'tupian_url': item.tupian_url or ''
- }
- for item in shangpin_types
- ]
-
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': type_list
- })
-
- except Exception as e:
- logger.error(f"查询商品类型表失败: {str(e)}")
- # 如果表不存在,返回空数组
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': []
- })
-
- except Exception as e:
- logger.error(f"获取订单类型异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-
-# yonghu/views.py 或 dingdan/views.py
-class AdGetOrderList(APIView):
- """
- 后台订单列表获取接口
- 前端调用地址: /yonghu/adddhq
- 前端需要字段: dingdan_id, jieshao, zhuangtai, leixing, fadanpingtai, jiage, tupian_url, fadanshijian
- 统计字段: stats.platform.total, stats.platform.completed, stats.merchant.total, stats.merchant.completed
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取参数(严格按前端传的字段名)
- qianzhanghao = request.data.get('zhanghao', '').strip()
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 30))
-
- # 搜索相关参数
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- # 筛选相关参数(严格按前端字段名)
- fadanpingtai = request.data.get('fadanpingtai') # 前端叫 fadanpingtai
- leixing = request.data.get('leixing') # 前端叫 leixing
- zhuangtai = request.data.get('zhuangtai', '') # 前端叫 zhuangtai
-
- if not qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. 身份验证(与你说的完全一样)
- dangqianyonghu = request.user
-
- # 验证手机号
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证管理员身份
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 3. 验证管理员扩展表
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 5. 构建查询条件(严格按前端字段名映射到数据库字段)
- query_conditions = Q()
-
- # 判断是否为搜索模式
- is_search_mode = bool(dingdan_id)
-
- if is_search_mode:
- # 搜索模式:只按订单ID查询
- query_conditions &= Q(OrderID=dingdan_id)
- else:
- # 筛选模式
-
- # 发单平台筛选(前端: fadanpingtai -> 数据库: Platform)
- if fadanpingtai is not None:
- try:
- fadanpingtai_int = int(fadanpingtai)
- if fadanpingtai_int in [1, 2]:
- query_conditions &= Q(Platform=fadanpingtai_int)
- except (ValueError, TypeError):
- pass
-
- # 订单类型筛选(前端: leixing -> 数据库: ProductTypeID)
- if leixing is not None:
- try:
- leixing_int = int(leixing)
- query_conditions &= Q(ProductTypeID=leixing_int)
- except (ValueError, TypeError):
- pass
-
- # 订单状态筛选(前端: zhuangtai -> 数据库: Status)
- if zhuangtai and zhuangtai != 'all':
- try:
- # 处理多个状态,如 "1,7"
- status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
- if status_list:
- query_conditions &= Q(Status__in=status_list)
- except (ValueError, TypeError):
- pass
-
- # 6. 获取统计数据(平台订单和商家订单)
- # 注意:这里统计所有订单,不考虑筛选条件
- try:
- # 平台订单统计 (Platform=1)
- platform_total = Order.query.filter(Platform=1).count()
- platform_completed = Order.query.filter(Platform=1, Status=3).count()
-
- # 商家订单统计 (Platform=2)
- merchant_total = Order.query.filter(Platform=2).count()
- merchant_completed = Order.query.filter(Platform=2, Status=3).count()
-
- platform_stats = {
- 'total': platform_total,
- 'completed': platform_completed
- }
- merchant_stats = {
- 'total': merchant_total,
- 'completed': merchant_completed
- }
- except Exception as e:
- logger.warning(f"统计数据查询失败: {str(e)}")
- platform_stats = {'total': 0, 'completed': 0}
- merchant_stats = {'total': 0, 'completed': 0}
-
- # 7. 执行分页查询(只查询主表,不查扩展表)
- start_time = time.time()
-
- # 计算分页
- offset = (page - 1) * page_size
-
- # 查询符合条件的订单总数
- total_count = Order.query.filter(query_conditions).count()
-
- # 查询当前页的数据(只查询主表,只查询前端需要的字段)
- dingdan_list = Order.query.filter(query_conditions).only(
- 'OrderID', 'Description', 'Status', 'ProductTypeID',
- 'Platform', 'Amount', 'ImageURL', 'CreateTime'
- ).order_by('-CreateTime')[offset:offset + page_size]
-
- query_time = time.time() - start_time
- logger.info(f"订单查询完成,耗时: {query_time:.3f}s,查询条数: {len(dingdan_list)}")
-
- # 8. 构建响应数据(严格按前端需要的字段)
- result_list = []
-
- for dingdan in dingdan_list:
- # 注意:这里字段名必须与前端完全一致
- order_data = {
- 'dingdan_id': dingdan.OrderID or '',
- 'jieshao': dingdan.Description or '',
- 'zhuangtai': dingdan.Status or 1,
- 'leixing': dingdan.ProductTypeID or 0, # 前端需要 leixing
- 'fadanpingtai': dingdan.Platform or 1, # 前端需要 fadanpingtai
- 'jiage': float(dingdan.Amount) if dingdan.Amount else 0.00, # 价格对应 Amount 字段
- 'tupian_url': dingdan.ImageURL or '', # 图片对应 ImageURL 字段
- 'fadanshijian': dingdan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan.CreateTime else ''
- }
- result_list.append(order_data)
-
- # 9. 判断是否有更多数据
- current_loaded_count = offset + len(result_list)
- has_more = len(result_list) >= page_size and current_loaded_count < total_count
-
- # 10. 构建完整响应(严格按前端需要的结构)
- response_data = {
- 'stats': {
- 'platform': platform_stats,
- 'merchant': merchant_stats
- },
- 'list': result_list,
- 'hasMore': has_more,
- 'total': total_count,
- 'currentPage': page,
- 'pageSize': page_size
- }
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': response_data
- })
-
- except Exception as e:
- logger.error(f"获取订单列表异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-
-
-class AdYaoQingDaShou(APIView):
- """
- 后台获取邀请码接口(用于邀请打手)
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /yonghu/adyqds
- 参数: {
- "zhanghao": "管理员账号"
- }
- 返回: {
- "code": 0,
- "message": "成功",
- "data": {
- "yaoqingma": "邀请码"
- }
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
-
- if not qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 获取用户ID(yonghuid),如果获取不到就用'123456'
- yonghuid_str = getattr(dangqianyonghu, 'yonghuid', '123456')
-
- # 7. 尝试获取或创建管事扩展表
- try:
- # 尝试获取现有的管事扩展表
- guanshi_profile = dangqianyonghu.GuanshiProfile
- except AttributeError:
- # 管事扩展表不存在,创建一个
- try:
- # 创建默认的管事扩展表
- guanshi_profile = UserGuanshi.query.create(
- user=dangqianyonghu,
- yaoqingma='', # 暂时为空,后面生成
- dianhua='', # 默认空
- wechat='', # 默认空
- yaogingshuliang=0, # 邀请数量为0
- zhuangtai=1, # 正常状态
- jinrichongzhi=0, # 今日充值0
- jinyuechongzhi=0, # 今月充值0
- chongzhifenrun=0, # 充值分佣0
- yue=0 # 余额0
- )
- #logger.info(f"为管理员{dangqianyonghu.UserUID}创建了管事扩展表")
- except Exception as e:
- logger.error(f"创建管事扩展表失败: {str(e)}")
- return Response(
- {'code': 500, 'message': '创建用户信息失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 8. 检查是否已有邀请码
- existing_yaoqingma = guanshi_profile.yaoqingma
-
- if existing_yaoqingma and existing_yaoqingma.strip():
- # 已有邀请码,直接返回
- return Response({
- 'code': 0,
- 'message': '成功获取邀请码',
- 'data': {
- 'yaoqingma': existing_yaoqingma
- }
- })
-
- # 9. 生成新的邀请码
- try:
- # 生成邀请码
- new_yaoqingma = CreateInvitationCode(str(yonghuid_str))
-
- # 验证邀请码格式
- if not VerifyInvitationCode(new_yaoqingma):
- raise Exception('邀请码格式验证失败')
-
- except ImportError:
- # 如果导入失败,使用简单方法生成邀请码
- timestamp = str(int(time.time()))
- random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
- new_yaoqingma = f"GL{timestamp[-6:]}{random_str}"
-
- logger.warning(f"使用简单方法生成邀请码: {new_yaoqingma}")
- except Exception as e:
- logger.error(f"生成邀请码失败: {str(e)}")
- return Response(
- {'code': 500, 'message': '生成邀请码失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 10. 保存邀请码到数据库
- try:
- with transaction.atomic():
- guanshi_profile.yaoqingma = new_yaoqingma
- guanshi_profile.save()
-
- logger.info(f"管理员{dangqianyonghu.UserUID}生成邀请码: {new_yaoqingma}")
- except Exception as e:
- logger.error(f"保存邀请码失败: {str(e)}")
- return Response(
- {'code': 500, 'message': '保存邀请码失败', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- # 11. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '成功生成邀请码',
- 'data': {
- 'yaoqingma': new_yaoqingma
- }
- })
-
- except Exception as e:
- logger.error(f"获取邀请码接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-class AdGuanLiYongHu(APIView):
- """
- 后台用户管理列表接口
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /yonghu/adgl
- 参数: {
- "zhanghao": "管理员账号",
- "user_type": "用户类型(1-4)",
- "keyword": "搜索关键词(可选)",
- "page": "页码(从1开始)",
- "page_size": "每页数量"
- }
- 返回: {
- "code": 0,
- "message": "成功",
- "data": {
- "list": [用户列表],
- "stats": {"1": 总数, "2": 总数, "3": 总数, "4": 总数},
- "has_more": true/false
- }
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- user_type = request.data.get('user_type')
- keyword = request.data.get('keyword', '').strip()
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 30))
-
- # 验证参数
- if not qianzhanghao or not user_type:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 根据用户类型选择查询的模型
- user_type_int = int(user_type)
-
- if user_type_int not in [1, 2, 3, 4]:
- return Response(
- {'code': 400, 'message': '用户类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 7. 统计四种用户类型的总数
- stats = self._get_user_stats()
-
- # 8. 根据用户类型查询用户列表
- user_list, has_more = self._query_users_by_type(
- user_type_int, keyword, page, page_size
- )
-
- # 9. 构建返回数据
- return Response({
- 'code': 0,
- 'message': '成功',
- 'data': {
- 'list': user_list,
- 'stats': stats,
- 'has_more': has_more
- }
- })
-
- except ValueError:
- logger.error("参数类型错误")
- return Response(
- {'code': 400, 'message': '参数类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- logger.error(f"用户管理列表接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
- def _get_user_stats(self):
- """
- 获取四种用户类型的统计数量
- """
- try:
-
- stats = {
- '1': UserBoss.query.count(),
- '2': UserDashou.query.count(),
- '3': UserGuanshi.query.count(),
- '4': UserShangjia.query.count()
- }
-
- return stats
-
- except Exception as e:
- logger.error(f"获取用户统计失败: {str(e)}")
- return {'1': 0, '2': 0, '3': 0, '4': 0}
-
- def _query_users_by_type(self, user_type_int, keyword, page, page_size):
- """
- 根据用户类型查询用户列表
- 返回: (用户列表, 是否还有更多数据)
- """
- try:
- # 计算偏移量
- offset = (page - 1) * page_size
-
- if user_type_int == 1:
- return self._query_boss_users(keyword, offset, page_size)
- elif user_type_int == 2:
- return self._query_dashou_users(keyword, offset, page_size)
- elif user_type_int == 3:
- return self._query_guanshi_users(keyword, offset, page_size)
- elif user_type_int == 4:
- return self._query_shangjia_users(keyword, offset, page_size)
- else:
- return [], False
-
- except Exception as e:
- logger.error(f"查询用户列表失败: {str(e)}")
- return [], False
-
- def _query_boss_users(self, keyword, offset, limit):
- """查询普通用户(老板)"""
- try:
- queryset = UserBoss.query.select_related('user')
-
- if keyword:
- queryset = queryset.filter(
- Q(user__UserUID__icontains=keyword) |
- Q(nickname__icontains=keyword)
- )
-
- # 获取总数
- total_count = queryset.count()
-
- # 获取当前页的数据
- users = list(queryset.order_by('-CreateTime')[offset:offset + limit])
-
- # 计算是否有更多数据
- current_count = len(users)
- has_more = (offset + current_count) < total_count
-
-
-
- # 构建返回数据
- user_list = []
- for boss in users:
- user_main = boss.user
-
- user_data = {
- 'yonghuid': user_main.UserUID,
- 'avatar': user_main.Avatar or '',
- 'nicheng': boss.nickname or '',
- }
-
- user_list.append(user_data)
-
- return user_list, has_more
-
- except Exception as e:
- logger.error(f"查询老板用户失败: {str(e)}")
- return [], False
-
- def _query_dashou_users(self, keyword, offset, limit):
- """查询打手用户"""
- try:
- queryset = UserDashou.query.select_related('user')
-
- if keyword:
- queryset = queryset.filter(
- Q(user__UserUID__icontains=keyword) |
- Q(nicheng__icontains=keyword) |
- Q(chenghao__icontains=keyword)
- )
-
- # 获取总数
- total_count = queryset.count()
-
- # 获取当前页的数据
- dashou_list = list(queryset.order_by('-CreateTime')[offset:offset + limit])
-
- # 计算是否有更多数据
- current_count = len(dashou_list)
- has_more = (offset + current_count) < total_count
-
-
-
- # 构建返回数据
- user_list = []
- for dashou in dashou_list:
- user_main = dashou.user
-
- user_data = {
- 'yonghuid': user_main.UserUID,
- 'avatar': user_main.Avatar or '',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'nicheng': dashou.nicheng or '',
- 'zhuangtai': dashou.zhuangtai,
- }
-
- user_list.append(user_data)
-
- return user_list, has_more
-
- except Exception as e:
- logger.error(f"查询打手用户失败: {str(e)}")
- return [], False
-
- def _query_guanshi_users(self, keyword, offset, limit):
- """查询管事用户"""
- try:
- queryset = UserGuanshi.query.select_related('user')
-
- if keyword:
- queryset = queryset.filter(
- Q(user__UserUID__icontains=keyword)
- )
-
- # 获取总数
- total_count = queryset.count()
-
- # 获取当前页的数据
- guanshi_list = list(queryset.order_by('-CreateTime')[offset:offset + limit])
-
- # 计算是否有更多数据
- current_count = len(guanshi_list)
- has_more = (offset + current_count) < total_count
-
-
-
- # 构建返回数据
- user_list = []
- for guanshi in guanshi_list:
- user_main = guanshi.user
-
- user_data = {
- 'yonghuid': user_main.UserUID,
- 'avatar': user_main.Avatar or '',
- 'zhanghaozhuangtai': guanshi.zhuangtai,
- 'nicheng': f'管事{user_main.UserUID}',
- }
-
- user_list.append(user_data)
-
- return user_list, has_more
-
- except Exception as e:
- logger.error(f"查询管事用户失败: {str(e)}")
- return [], False
-
- def _query_shangjia_users(self, keyword, offset, limit):
- """查询商家用户"""
- try:
- queryset = UserShangjia.query.select_related('user')
-
- if keyword:
- queryset = queryset.filter(
- Q(user__UserUID__icontains=keyword) |
- Q(nicheng__icontains=keyword)
- )
-
- # 获取总数
- total_count = queryset.count()
-
- # 获取当前页的数据
- shangjia_list = list(queryset.order_by('-CreateTime')[offset:offset + limit])
-
- # 计算是否有更多数据
- current_count = len(shangjia_list)
- has_more = (offset + current_count) < total_count
-
-
-
- # 构建返回数据
- user_list = []
- for shangjia in shangjia_list:
- user_main = shangjia.user
-
- user_data = {
- 'yonghuid': user_main.UserUID,
- 'avatar': user_main.Avatar or '',
- 'zhanghaozhuangtai': shangjia.zhuangtai,
- 'nicheng': shangjia.nicheng or '',
- }
-
- user_list.append(user_data)
-
- return user_list, has_more
-
- except Exception as e:
- logger.error(f"查询商家用户失败: {str(e)}")
- return [], False
-
-
-
-
-
-class CfGuanLi(APIView):
- """
- 后台处罚管理列表接口
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /yonghu/cfgl
- 参数: {
- "zhanghao": "管理员账号",
- "qingqiu_tongji": true/false, # 是否只请求统计信息
- "sqzhuangtai": 0 或 [1,2], # 处罚申请状态筛选(可选)
- "page": "页码(从1开始)", # 分页参数
- "page_size": "每页数量" # 分页参数
- }
- 返回: {
- "code": 0,
- "message": "成功",
- "data": {
- "list": [处罚记录列表],
- "zongshu": 总数,
- "daichuli": 待处理数量,
- "yichuli": 已处理数量,
- "haiyougengduo": true/false
- }
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- qingqiu_tongji = request.data.get('qingqiu_tongji', False)
- sqzhuangtai = request.data.get('sqzhuangtai')
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 30))
-
- # 验证参数
- if not qianzhanghao:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 7. 获取统计信息(总是需要统计)
- zongshu = PenaltyRecord.query.count()
- daichuli = PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count()
- yichuli = PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count()
-
- # 8. 如果只请求统计信息,直接返回
- if qingqiu_tongji:
- return Response({
- 'code': 0,
- 'message': '成功',
- 'data': {
- 'zongshu': zongshu,
- 'daichuli': daichuli,
- 'yichuli': yichuli
- }
- })
-
- # 9. 构建查询集
- queryset = PenaltyRecord.query.all()
-
- # 10. 根据状态筛选
- if sqzhuangtai is not None:
- if isinstance(sqzhuangtai, list):
- # 如果是数组,表示多个状态
- queryset = queryset.filter(ApplyStatus__in=sqzhuangtai)
- else:
- # 单个状态
- queryset = queryset.filter(ApplyStatus=sqzhuangtai)
-
- # 11. 获取总数(用于分页计算)
- total_count = queryset.count()
-
- # 12. 计算分页偏移量
- offset = (page - 1) * page_size
-
- # 13. 分页查询
- chufa_list = list(queryset.order_by('-CreateTime')[offset:offset + page_size])
-
- # 🔴【新增】高效获取图片信息
- if chufa_list:
- # 收集所有需要查询的组合
- dingdan_ids = []
- yonghu_combinations = [] # 保存 (dingdan_id, yonghuid) 组合
-
- for chufa in chufa_list:
- if chufa.OrderID:
- dingdan_ids.append(chufa.OrderID)
- # 商家图片组合
- if chufa.ApplicantID:
- yonghu_combinations.append((chufa.OrderID, chufa.ApplicantID))
- # 打手图片组合
- if chufa.PlayerID:
- yonghu_combinations.append((chufa.OrderID, chufa.PlayerID))
-
- # 去重
- dingdan_ids = list(set(dingdan_ids))
- yonghu_combinations = list(set(yonghu_combinations))
-
- # 🔴【新增】批量查询图片表(高效查询,避免循环)
- tupian_mapping = {}
- if yonghu_combinations:
- # 构建查询条件
- query_conditions = Q()
- for dingdan_id, yonghuid in yonghu_combinations:
- query_conditions |= Q(OrderID=dingdan_id, UserID=yonghuid)
-
- # 批量查询
- tupian_queryset = PenaltyEvidenceImage.query.filter(query_conditions).values(
- 'OrderID', 'UserID', 'ImageURL'
- )
-
- # 构建映射:key为"OrderID_UserID",value为图片URL列表
- for item in tupian_queryset:
- key = f"{item['OrderID']}_{item['UserID']}"
- if key not in tupian_mapping:
- tupian_mapping[key] = []
- if item['ImageURL']:
- tupian_mapping[key].append(item['ImageURL'])
- else:
- tupian_mapping = {}
-
- # 🔴【修改】14. 构建返回数据 - 添加图片和申诉理由字段
- chufa_data = []
- for chufa in chufa_list:
- # 🔴【新增】获取商家证据图片
- zhengju_tupian = []
- if chufa.OrderID and chufa.ApplicantID:
- key = f"{chufa.OrderID}_{chufa.ApplicantID}"
- zhengju_tupian = tupian_mapping.get(key, [])
-
- # 🔴【新增】获取打手申诉图片
- shensu_tupian = []
- if chufa.OrderID and chufa.PlayerID:
- key = f"{chufa.OrderID}_{chufa.PlayerID}"
- shensu_tupian = tupian_mapping.get(key, [])
-
- chufa_data.append({
- 'dashouid': chufa.PlayerID,
- 'qingqiuid': chufa.ApplicantID,
- 'chuliid': chufa.ProcessorID,
- 'cfliyou': chufa.PenaltyReason,
- 'sqzhuangtai': chufa.ApplyStatus,
- 'bhliyou': chufa.RejectReason,
- 'jifen': chufa.DeductedPoints,
- 'dingdan_id': chufa.OrderID,
- 'CreateTime': chufa.CreateTime,
- 'UpdateTime': chufa.UpdateTime,
- # 🔴【新增】申诉理由字段
- 'ssliyou': chufa.AppealReason or '',
- # 🔴【新增】商家证据图片
- 'zhengju_tupian': zhengju_tupian,
- # 🔴【新增】打手申诉图片
- 'shensu_tupian': shensu_tupian,
- })
-
- # 15. 计算是否还有更多数据
- current_count = len(chufa_list)
- haiyougengduo = (offset + current_count) < total_count
-
- return Response({
- 'code': 0,
- 'message': '成功',
- 'data': {
- 'list': chufa_data,
- 'zongshu': zongshu,
- 'daichuli': daichuli,
- 'yichuli': yichuli,
- 'haiyougengduo': haiyougengduo
- }
- })
-
- except ValueError:
- return Response(
- {'code': 400, 'message': '参数类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- logger.error(f"处罚管理列表接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-logger = logging.getLogger(__name__)
-
-
-class AdTongYiChuFa(APIView):
- """
- 后台同意/驳回处罚接口(已修复重复记录问题)
- 权限: JWT Token认证 + 管理员身份验证
- 前端调用: POST /yonghu/adtycf
- 参数: {
- "zhanghao": "管理员账号",
- "dingdan_id": "订单ID",
- "caozuo": 1或2, # 1=同意处罚,2=拒绝处罚
- "bohui_liyou": "拒绝理由" # 拒绝处罚时必填
- }
- 返回: {
- "code": 0,
- "message": "成功",
- "data": null
- }
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- try:
- # 1. 获取前端参数
- qianzhanghao = request.data.get('zhanghao', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- caozuo = request.data.get('caozuo') # 1=同意,2=拒绝
- bohui_liyou = request.data.get('bohui_liyou', '').strip()
-
- # 验证参数
- if not qianzhanghao or not dingdan_id or caozuo is None:
- return Response(
- {'code': 400, 'message': '参数不完整', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 验证操作类型
- if caozuo not in [1, 2]:
- return Response(
- {'code': 400, 'message': '操作类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 如果是拒绝处罚,需要拒绝理由
- if caozuo == 2 and not bohui_liyou:
- return Response(
- {'code': 400, 'message': '拒绝处罚需要填写理由', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 2. JWT Token身份验证
- dangqianyonghu = request.user
-
- # 3. 验证手机号是否匹配
- if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 4. 验证用户类型是否为管理员
- if dangqianyonghu.UserType != 'admin':
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 5. 验证管理员扩展表是否存在
- try:
- guanliyuan = dangqianyonghu.AdminProfile
- except AttributeError:
- return Response(
- {'code': 401, 'message': '身份验证失败', 'data': None},
- status=status.HTTP_401_UNAUTHORIZED
- )
-
- # 6. 使用事务保证数据一致性
- with transaction.atomic():
- # 8. 🔴 修复:查询处罚记录(处理重复记录问题)
- # 使用 filter().first() 而不是 get(),以防出现多条记录
- chufajilu = PenaltyRecord.query.filter(
- OrderID=dingdan_id,
- ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中状态
- ).order_by('-CreateTime').first() # 取最新的一条
-
- if not chufajilu:
- # 如果没有待处理的处罚记录,检查是否有已处理的
- exists_processed = PenaltyRecord.query.filter(OrderID=dingdan_id).exists()
- if exists_processed:
- return Response(
- {'code': 400, 'message': '该处罚记录已处理', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- else:
- return Response(
- {'code': 404, 'message': '处罚记录不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 9. 验证处罚记录状态(必须是待处理或申诉中状态才能处理)
- if chufajilu.ApplyStatus not in [0, 3]: # 🔴【修改】包括待处理(0)和申诉中(3)
- return Response(
- {'code': 400, 'message': '该处罚记录已处理', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 10. 🔴 修复:检查是否有重复的待处理/申诉中记录,并记录警告
- duplicate_count = PenaltyRecord.query.filter(
- OrderID=dingdan_id,
- ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中
- ).count()
-
- if duplicate_count > 1:
- # 记录警告日志
- logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理/申诉中处罚记录,处理最新的一条")
-
- # 🔴 可选:将其他重复的待处理/申诉中记录标记为已拒绝
- # 防止用户重复提交相同订单的处罚申请
- duplicate_records = PenaltyRecord.query.filter(
- OrderID=dingdan_id,
- ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中
- ).exclude(id=chufajilu.id)
-
- for dup in duplicate_records:
- dup.ApplyStatus = 2 # 已拒绝
- dup.ProcessorID = qianzhanghao
- dup.RejectReason = "系统自动拒绝:存在重复处罚记录"
- dup.save()
-
- # 11. 根据操作类型处理
- if caozuo == 1:
- # 同意处罚 - 不扣分,只改状态
- # 11.1 检查打手是否存在
- if not chufajilu.PlayerID:
- return Response(
- {'code': 400, 'message': '被处罚打手ID不存在', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
-
- # 11.2 查询打手扩展表(只是验证存在性)
- try:
- dashou = UserDashou.query.get(user__UserUID=chufajilu.PlayerID)
- except UserDashou.DoesNotExist:
- return Response(
- {'code': 404, 'message': '被处罚打手不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND
- )
-
- # 🔴【修改】11.3 同意处罚时不扣分(因为申请时已经扣了),只更新状态
- chufajilu.ApplyStatus = 1 # 已处罚
- chufajilu.ProcessorID = qianzhanghao # 处理人ID为管理员账号
- chufajilu.save()
-
- # 11.4 更新商家订单扩展表
- try:
- dingdan = Order.query.get(OrderID=dingdan_id)
- if hasattr(dingdan, 'shangjia_kuozhan'):
- shangjia_kuozhan = dingdan.shangjia_kuozhan
- shangjia_kuozhan.PenaltyApplyStatus = 1 # 已处罚
- shangjia_kuozhan.save()
- except (Order.DoesNotExist, AttributeError):
- # 如果订单或扩展表不存在,继续执行,不中断
- pass
-
- logger.info(f"同意处罚成功:订单ID={dingdan_id},打手ID={chufajilu.PlayerID}")
-
- elif caozuo == 2:
- # 拒绝处罚 - 需要返还积分(加分)
- # 🔴【新增】11.5 返还打手积分(加5分)
- if chufajilu.DeductedPoints > 0:
- try:
- dashou = UserDashou.query.get(user__UserUID=chufajilu.PlayerID)
-
- # 🔴【新增】安全验证:必须积分小于等于5分才允许加分
- if dashou.jifen <= 5: # 判断积分是否小于等于5分
- dashou.jifen += chufajilu.DeductedPoints # 加5分
- dashou.save()
- logger.info(
- f"拒绝处罚,给打手{dashou.user.UserUID}返还积分{chufajilu.DeductedPoints}分,当前积分{dashou.jifen}")
- else:
- # 积分已超过5分,可能是异常情况,记录警告但不中断处理
- logger.warning(
- f"拒绝处罚时打手积分异常:打手ID={chufajilu.PlayerID},当前积分{dashou.jifen}已超过5分,不再加分")
- except UserDashou.DoesNotExist:
- # 打手不存在,记录错误,但继续处理状态
- logger.error(f"拒绝处罚时打手不存在:打手ID={chufajilu.PlayerID}")
-
- # 11.6 更新处罚记录
- chufajilu.ApplyStatus = 2 # 已拒绝
- chufajilu.ProcessorID = qianzhanghao # 处理人ID为管理员账号
- chufajilu.RejectReason = bohui_liyou # 拒绝理由
- chufajilu.save()
-
- # 11.7 更新商家订单扩展表
- try:
- dingdan = Order.query.get(OrderID=dingdan_id)
- if hasattr(dingdan, 'shangjia_kuozhan'):
- shangjia_kuozhan = dingdan.shangjia_kuozhan
- shangjia_kuozhan.PenaltyApplyStatus = 2 # 已拒绝
- shangjia_kuozhan.RejectReason = bohui_liyou # 拒绝理由
- shangjia_kuozhan.save()
- except (Order.DoesNotExist, AttributeError):
- # 如果订单或扩展表不存在,继续执行,不中断
- pass
-
- logger.info(f"拒绝处罚成功:订单ID={dingdan_id},拒绝理由={bohui_liyou}")
-
- # 12. 返回成功响应
- return Response({
- 'code': 0,
- 'message': '处理成功',
- 'data': None
- })
-
- except ValueError:
- return Response(
- {'code': 400, 'message': '参数类型错误', 'data': None},
- status=status.HTTP_400_BAD_REQUEST
- )
- except Exception as e:
- logger.error(f"处理处罚接口异常: {str(e)}", exc_info=True)
- return Response(
- {'code': 500, 'message': '服务器内部错误', 'data': None},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
- )
-
-
-
-
-# yonghu/views.py
-class AdckyhxqView(APIView):
- """
- 获取用户详情接口
- 前端需要传递: zhanghao, uid, user_type
- 返回用户信息和会员列表
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # ==================== 管理员身份验证 ====================
- # 1. 验证JWT Token
- if not request.user.is_authenticated:
- return Response({
- 'code': 401,
- 'message': '未认证',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 验证用户类型为管理员
- if request.user.UserType != 'admin':
- return Response({
- 'code': 403,
- 'message': '权限不足',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 验证管理员扩展表存在
- admin_profile = getattr(request.user, 'AdminProfile', None)
- if not admin_profile:
- return Response({
- 'code': 403,
- 'message': '管理员信息不完整',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 4. 验证管理员账号(phone)与前端传递的zhanghao一致
- zhanghao = request.data.get('zhanghao')
- if not zhanghao or request.user.Phone != zhanghao:
- return Response({
- 'code': 401,
- 'message': '账号验证失败',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
- # ==================== 身份验证结束 ====================
-
- # 获取业务参数
- uid = request.data.get('uid') # 用户ID (yonghuid)
- user_type = request.data.get('user_type') # 用户类型 (1,2,3,4)
-
- if not uid or not user_type:
- return Response({
- 'code': 400,
- 'message': '参数缺失',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证用户类型
- try:
- user_type = int(user_type)
- if user_type not in [1, 2, 3, 4]:
- raise ValueError
- except ValueError:
- return Response({
- 'code': 400,
- 'message': '用户类型无效',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 查询用户主表 - 使用select_related优化查询
- user_main = User.query.filter(UserUID=uid).first()
- if not user_main:
- return Response({
- 'code': 404,
- 'message': '用户不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 构建响应数据
- response_data = {
- 'user_info': {},
- 'member_list': [],
- 'all_member_list': []
- }
-
- # 通用字段(所有用户都有)
- user_info = {
- 'yonghuid': user_main.UserUID,
- 'avatar': user_main.Avatar,
- 'phone': user_main.Phone,
- 'ip': user_main.IP,
- 'CreateTime': user_main.UserCreateTime,
- 'UpdateTime': user_main.UserCreateTime,
- 'user_type': user_type
- }
-
- # 根据用户类型查询扩展信息
- if user_type == 1: # 老板
- # 使用select_related预取boss_profile
- user_main = User.query.filter(UserUID=uid).select_related('BossProfile').first()
- if getattr(user_main, 'BossProfile', None):
- boss_profile = user_main.BossProfile
- user_info.update({
- 'nickname': boss_profile.nickname,
- 'zonge': boss_profile.zonge,
- 'alldingdan': boss_profile.alldingdan,
- 'alltui': boss_profile.alltui,
- 'CreateTime_boss': boss_profile.CreateTime
- })
-
- elif user_type == 2: # 打手
- # 使用select_related预取dashou_profile
- user_main = User.query.filter(UserUID=uid).select_related('DashouProfile').first()
- if getattr(user_main, 'DashouProfile', None):
- dashou_profile = user_main.DashouProfile
-
- # 构建打手信息
- user_info.update({
- 'nicheng': dashou_profile.nicheng,
- 'chenghao': dashou_profile.chenghao,
- 'zhuangtai': dashou_profile.zhuangtai,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
- 'jieshao': dashou_profile.jieshao,
- 'jiedanzongliang': dashou_profile.jiedanzongliang,
- 'chengjiaozongliang': dashou_profile.chengjiaozongliang,
- 'tuikuanliang': dashou_profile.tuikuanliang,
- 'yue': dashou_profile.yue, # 打手可提现余额
- 'zonge': dashou_profile.zonge, # 打手赚取总额
- 'dianhua': dashou_profile.dianhua,
- 'wechat': dashou_profile.wechat,
- 'yaoqingren': dashou_profile.yaoqingren,
- 'jinrijiedan': dashou_profile.jinrijiedan,
- 'jinrishouyi': dashou_profile.jinrishouyi,
- 'jinyuejiedan': dashou_profile.jinyuejiedan,
- 'jinyueshouyi': dashou_profile.jinyueshouyi,
- 'jifen': dashou_profile.jifen,
- 'yajin': dashou_profile.yajin,
- 'CreateTime_dashou': dashou_profile.CreateTime
- })
-
- # 查询打手的会员信息(只返回未过期的)
- # 使用select_related预取huiyuan信息(如果有关联的话)
- member_records = Huiyuangoumai.query.filter(
- yonghu_id=uid
- ).order_by('-CreateTime')
-
- member_list = []
- for record in member_records:
- # 调用方法检查是否过期,并更新状态
- is_expired = record.jiance_shifou_daoqi()
-
- if not is_expired: # 只返回未过期的
- # 查询会员详情
- huiyuan = Huiyuan.query.filter(
- huiyuan_id=record.huiyuan_id
- ).first()
-
- if huiyuan:
- member_list.append({
- 'huiyuan_id': record.huiyuan_id,
- 'huiyuan_name': huiyuan.jieshao,
- 'daoqi_time': record.daoqi_time,
- 'is_active': record.huiyuan_zhuangtai == 1,
- 'CreateTime': record.CreateTime
- })
-
- response_data['member_list'] = member_list
-
- elif user_type == 3: # 管事
- # 使用select_related预取guanshi_profile
- user_main = User.query.filter(UserUID=uid).select_related('GuanshiProfile').first()
- if getattr(user_main, 'GuanshiProfile', None):
- guanshi_profile = user_main.GuanshiProfile
- user_info.update({
- 'nicheng': user_main.Phone, # 管事昵称用手机号
- 'yaoqingma': guanshi_profile.yaoqingma,
- 'dianhua': guanshi_profile.dianhua,
- 'wechat': guanshi_profile.wechat,
- 'yaogingshuliang': guanshi_profile.yaogingshuliang,
- 'zhuangtai': guanshi_profile.zhuangtai,
- 'jinrichongzhi': guanshi_profile.jinrichongzhi,
- 'jinyuechongzhi': guanshi_profile.jinyuechongzhi,
- 'chongzhifenrun': guanshi_profile.chongzhifenrun,
- 'yue': guanshi_profile.yue, # 管事可提现余额
- 'CreateTime_guanshi': guanshi_profile.CreateTime
- })
-
- elif user_type == 4: # 商家
- # 使用select_related预取shop_profile
- user_main = User.query.filter(UserUID=uid).select_related('ShopProfile').first()
- if getattr(user_main, 'ShopProfile', None):
- shop_profile = user_main.ShopProfile
- user_info.update({
- 'nicheng': shop_profile.nicheng,
- 'zhuangtai': shop_profile.zhuangtai,
- 'dianhua': shop_profile.dianhua,
- 'wechat': shop_profile.wechat,
- 'fabu': shop_profile.fabu,
- 'tuikuan': shop_profile.tuikuan,
- 'yue': shop_profile.yue, # 商家余额
- 'chengjiao': shop_profile.chengjiao,
- 'jinridingdan': shop_profile.jinridingdan,
- 'jinriliushui': shop_profile.jinriliushui,
- 'jinyuedingdan': shop_profile.jinyuedingdan,
- 'jinyueliushui': shop_profile.jinyueliushui,
- 'CreateTime_shop': shop_profile.CreateTime
- })
-
- # 获取所有会员列表(用于前端下拉选择)
- all_huiyuan = Huiyuan.query.all().order_by('jiage')
- all_member_list = [
- {
- 'id': huiyuan.huiyuan_id,
- 'nicheng': huiyuan.jieshao,
- 'jiage': huiyuan.jiage,
- 'jieshao': huiyuan.jtjieshao
- }
- for huiyuan in all_huiyuan
- ]
-
- response_data['user_info'] = user_info
- response_data['all_member_list'] = all_member_list
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': response_data
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- return Response({
- 'code': 500,
- 'message': f'获取用户详情失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class AdcjxgView(APIView):
- """
- 修改用户信息接口
- 前端需要传递: zhanghao, uid, user_type, 以及其他修改的字段
- 支持打手会员添加功能
- 敏感字段修改会记录到修改记录表
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # ==================== 管理员身份验证 ====================
- if not request.user.is_authenticated:
- return Response({
- 'code': 401,
- 'message': '未认证',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- if request.user.UserType != 'admin':
- return Response({
- 'code': 403,
- 'message': '权限不足',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- admin_profile = getattr(request.user, 'AdminProfile', None)
- if not admin_profile:
- return Response({
- 'code': 403,
- 'message': '管理员信息不完整',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- zhanghao = request.data.get('zhanghao')
- if not zhanghao or request.user.Phone != zhanghao:
- return Response({
- 'code': 401,
- 'message': '账号验证失败',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
- # ==================== 身份验证结束 ====================
-
- # 获取业务参数
- uid = request.data.get('uid')
- user_type = request.data.get('user_type')
-
- if not uid or not user_type:
- return Response({
- 'code': 400,
- 'message': '参数缺失',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- user_type = int(user_type)
- if user_type not in [1, 2, 3, 4]:
- raise ValueError
- except ValueError:
- return Response({
- 'code': 400,
- 'message': '用户类型无效',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 查询用户主表
- user_main = User.query.filter(UserUID=uid).first()
- if not user_main:
- return Response({
- 'code': 404,
- 'message': '用户不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 使用事务确保数据一致性
- with transaction.atomic():
- # 存储修改记录的数据
- modify_record_data = {
- 'yonghuid': uid,
- 'xiugaiid': zhanghao,
- 'leixing': user_type,
- 'qitashuoming': '',
- 'CreateTime': timezone.now()
- }
-
- # 标记是否有敏感字段被修改
- has_sensitive_change = False
-
- if user_type == 1: # 老板 - 不允许修改
- return Response({
- 'code': 403,
- 'message': '老板信息不允许修改',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- elif user_type == 2: # 打手
- # 获取或创建打手扩展记录
- dashou_profile, created = UserDashou.query.get_or_create(
- user=user_main,
- defaults={'nicheng': user_main.Phone or f'打手{uid}'}
- )
-
- # 记录敏感字段的旧值
- old_yue = dashou_profile.yue # 打手可提现余额
- old_zonge = dashou_profile.zonge # 打手提现金额
- old_jifen = dashou_profile.jifen # 打手积分
- old_yajin = dashou_profile.yajin # 打手押金
-
- # 需要更新的字段列表
- update_fields = [
- 'chenghao', 'dianhua', 'yue', 'zonge', 'jifen',
- 'yajin', 'yaoqingren', 'jieshao', 'zhuangtai',
- 'zaixianzhuangtai', 'zhanghaozhuangtai'
- ]
-
- # 记录被修改的字段
- modified_fields = []
-
- for field in update_fields:
- if field in request.data:
- new_value = request.data.get(field)
- old_value = getattr(dashou_profile, field)
-
- # 检查值是否改变
- if str(new_value) != str(old_value):
- # 处理特殊字段类型
- if field in ['yue', 'zonge', 'yajin']:
- new_value = Decimal(str(new_value))
- setattr(dashou_profile, field, new_value)
- elif field in ['jifen']:
- new_value = int(new_value)
- setattr(dashou_profile, field, new_value)
- elif field in ['zhuangtai', 'zaixianzhuangtai', 'zhanghaozhuangtai']:
- new_value = int(new_value)
- setattr(dashou_profile, field, new_value)
- else:
- setattr(dashou_profile, field, new_value)
-
- modified_fields.append(field)
-
- # 记录敏感字段修改
- if field == 'yue':
- has_sensitive_change = True
- modify_record_data['xiugaitijiao'] = new_value # 使用xiugaitijiao字段记录打手余额修改
- modify_record_data['xiugaitijiaoq'] = old_yue
- elif field == 'zonge':
- has_sensitive_change = True
- # zonge字段在修改记录表中可能没有直接对应,可以记录在备注中
- modify_record_data['qitashuoming'] += f' 打手总额从{old_zonge}修改为{new_value}'
- elif field == 'jifen':
- has_sensitive_change = True
- modify_record_data['jifen'] = new_value
- modify_record_data['yjifen'] = old_jifen
- elif field == 'yajin':
- has_sensitive_change = True
- modify_record_data['yajin'] = new_value
- modify_record_data['yyajin'] = old_yajin
-
- # 保存打手信息
- dashou_profile.save()
-
- # 记录被修改的非敏感字段
- non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'zonge', 'jifen', 'yajin']]
- if non_sensitive_fields:
- modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
-
- elif user_type == 3: # 管事
- # 获取或创建管事扩展记录
- guanshi_profile, created = UserGuanshi.query.get_or_create(
- user=user_main,
- defaults={'yaoqingma': f'GS{uid[-4:]}'}
- )
-
- # 记录敏感字段的旧值
- old_yue = guanshi_profile.yue # 管事可提现余额
-
- update_fields = [
- 'yaogingshuliang', 'chongzhifenrun', 'yue',
- 'zhuangtai', 'dianhua', 'wechat'
- ]
-
- modified_fields = []
-
- for field in update_fields:
- if field in request.data:
- new_value = request.data.get(field)
- old_value = getattr(guanshi_profile, field)
-
- if str(new_value) != str(old_value):
- if field in ['chongzhifenrun', 'yue']:
- new_value = Decimal(str(new_value))
- setattr(guanshi_profile, field, new_value)
- elif field in ['yaogingshuliang']:
- new_value = int(new_value)
- setattr(guanshi_profile, field, new_value)
- elif field in ['zhuangtai']:
- new_value = int(new_value)
- setattr(guanshi_profile, field, new_value)
- else:
- setattr(guanshi_profile, field, new_value)
-
- modified_fields.append(field)
-
- # 记录敏感字段修改
- if field == 'yue':
- has_sensitive_change = True
- modify_record_data['guanshiyue'] = new_value
- modify_record_data['guanshiyueq'] = old_yue
- elif field == 'chongzhifenrun':
- has_sensitive_change = True
- modify_record_data['qitashuoming'] += f' 管事分佣从{old_value}修改为{new_value}'
-
- guanshi_profile.save()
-
- # 记录非敏感字段
- non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'chongzhifenrun']]
- if non_sensitive_fields:
- modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
-
- elif user_type == 4: # 商家
- # 获取或创建商家扩展记录
- shop_profile, created = UserShangjia.query.get_or_create(
- user=user_main,
- defaults={'nicheng': user_main.Phone or f'商家{uid}'}
- )
-
- # 记录敏感字段的旧值
- old_yue = shop_profile.yue # 商家余额
-
- update_fields = ['yue', 'zhuangtai', 'dianhua', 'wechat']
-
- modified_fields = []
-
- for field in update_fields:
- if field in request.data:
- new_value = request.data.get(field)
- old_value = getattr(shop_profile, field)
-
- if str(new_value) != str(old_value):
- if field == 'yue':
- new_value = Decimal(str(new_value))
- setattr(shop_profile, field, new_value)
- elif field == 'zhuangtai':
- new_value = int(new_value)
- setattr(shop_profile, field, new_value)
- else:
- setattr(shop_profile, field, new_value)
-
- modified_fields.append(field)
-
- # 记录敏感字段修改
- if field == 'yue':
- has_sensitive_change = True
- modify_record_data['shangjiayue'] = new_value
- modify_record_data['shangjiayueq'] = old_yue
-
- shop_profile.save()
-
- # 记录非敏感字段
- non_sensitive_fields = [f for f in modified_fields if f != 'yue']
- if non_sensitive_fields:
- modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
-
- # 处理打手会员添加(只有打手可以添加会员)
-
- # 🔥🔥🔥 后端修改:支持正负数会员天数
- # 修改会员处理逻辑部分
-
- if user_type == 2 and 'huiyuan_id' in request.data and 'days' in request.data:
- huiyuan_id = request.data.get('huiyuan_id')
- days = int(request.data.get('days', 0))
-
- # 🔥 修改:允许正负数,但不能为0
- if huiyuan_id and days != 0:
- # 🔥 修改:检查绝对值范围
- if abs(days) < 1 or abs(days) > 10000:
- return Response({
- 'code': 400,
- 'message': '会员天数必须在1-10000之间(正数增加,负数减少)',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 检查会员是否存在
- huiyuan = Huiyuan.query.filter(huiyuan_id=huiyuan_id).first()
- if not huiyuan:
- return Response({
- 'code': 404,
- 'message': '会员不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 检查是否已购买该会员
- existing_record = Huiyuangoumai.query.filter(
- yonghu_id=uid,
- huiyuan_id=huiyuan_id
- ).first()
-
- if existing_record:
- # 🔥🔥🔥 重要修改:支持减少天数
- # 计算新的到期时间
- new_daoqi_time = existing_record.daoqi_time + timedelta(days=days)
-
- # 🔥 安全验证:减少天数后不能小于当前时间前90天(防止过度减少)
- # 这个限制可以根据业务调整
- min_allowed_time = timezone.now() - timedelta(days=90)
- if new_daoqi_time < min_allowed_time:
- return Response({
- 'code': 400,
- 'message': f'减少天数过多,调整后到期时间不能早于 {(min_allowed_time + timedelta(days=90)).strftime("%Y-%m-%d")}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- existing_record.daoqi_time = new_daoqi_time
- existing_record.save()
-
- # 记录会员操作
- has_sensitive_change = True
- modify_record_data['huiyuants'] = days
- modify_record_data['huiyuan_id'] = huiyuan_id
-
- if days > 0:
- modify_record_data['qitashuoming'] += f' 续费会员 {huiyuan.jieshao} {days}天'
- # 只有增加天数时才增加购买次数
- huiyuan.goumai_cishu += 1
- huiyuan.save()
- else:
- modify_record_data['qitashuoming'] += f' 减少会员 {huiyuan.jieshao} {abs(days)}天'
- # 减少天数时不增加购买次数
-
- else:
- # 🔥 修改:新购买时只能增加天数
- if days > 0:
- daoqi_time = timezone.now() + timedelta(days=days)
- from jituan.services.club_user import get_user_club_id
- new_record = Huiyuangoumai.query.create(
- huiyuan_id=huiyuan_id,
- yonghu_id=uid,
- jieshao=huiyuan.jieshao,
- daoqi_time=daoqi_time,
- club_id=get_user_club_id(User.query.filter(UserUID=uid).first()),
- )
-
- # 记录会员购买
- has_sensitive_change = True
- modify_record_data['huiyuants'] = days
- modify_record_data['huiyuan_id'] = huiyuan_id
- modify_record_data['qitashuoming'] += f' 购买会员 {huiyuan.jieshao} {days}天'
-
- # 更新会员购买次数
- huiyuan.goumai_cishu += 1
- huiyuan.save()
- else:
- # 🔥 不能为不存在的会员减少天数
- return Response({
- 'code': 400,
- 'message': '用户未购买该会员,无法减少天数',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
-
-
-
-
-
-
-
-
-
-
- # 如果有敏感字段被修改或会员操作,创建修改记录
- if has_sensitive_change or 'huiyuan_id' in request.data:
- # 创建修改记录
- Xiugaijilu.query.create(**modify_record_data)
- elif modify_record_data.get('qitashuoming', '').strip():
- # 只有非敏感字段修改,也记录但qitashuoming不同
- Xiugaijilu.query.create(**modify_record_data)
-
- return Response({
- 'code': 0,
- 'message': '修改成功',
- 'data': None
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- return Response({
- 'code': 500,
- 'message': f'修改失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-# 管理员认证装饰器
-def admin_required(view_func):
- """管理员权限验证装饰器"""
-
- def wrapped_view(self, request, *args, **kwargs):
- try:
- # 1. 验证JWT Token
- if not request.user.is_authenticated:
- return Response({
- 'code': 401,
- 'message': '未认证',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 验证用户类型为管理员
- if request.user.UserType != 'admin':
- return Response({
- 'code': 403,
- 'message': '权限不足',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 3. 验证管理员扩展表存在
- admin_profile = getattr(request.user, 'AdminProfile', None)
- if not admin_profile:
- return Response({
- 'code': 403,
- 'message': '管理员信息不完整',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- # 4. 验证管理员账号(phone)与前端传递的zhanghao一致
- zhanghao = request.data.get('zhanghao')
- if not zhanghao or request.user.Phone != zhanghao:
- return Response({
- 'code': 401,
- 'message': '账号验证失败',
- 'data': None
- }, status=status.HTTP_401_UNAUTHORIZED)
-
- return view_func(self, request, *args, **kwargs)
- except Exception as e:
- return Response({
- 'code': 500,
- 'message': f'权限验证异常: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- return wrapped_view
-
-
-class AddtxshView(APIView):
- """
- 获取提现审核列表接口
- 与用户管理页面保持一致的分页逻辑
- 支持按用户ID搜索,排序改为按申请时间升序(最早申请排最前)
- """
- permission_classes = [IsAuthenticated]
-
- @admin_required
- def post(self, request):
- try:
- # 获取参数
- zhanghao = request.data.get('zhanghao')
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 5))
- zhuangtai = int(request.data.get('zhuangtai', 1))
- leixing = int(request.data.get('leixing', 1))
- fangshi = request.data.get('fangshi')
-
- # 🔴【新增】获取搜索参数(用户ID)
- search_uid = request.data.get('search_uid', '').strip()
-
- if page < 1:
- page = 1
-
- # 计算偏移量(与用户管理页面完全一致)
- offset = (page - 1) * page_size
-
- # 构建查询条件
- query = Q()
-
- # 状态筛选
- if zhuangtai == 1:
- query &= Q(zhuangtai=1)
- elif zhuangtai == 2:
- query &= Q(zhuangtai__in=[2, 3])
-
- # 类型筛选
- query &= Q(leixing=leixing)
-
- # 收款方式筛选(可选)
- if fangshi:
- query &= Q(fangshi=int(fangshi))
-
- # 🔴【新增】如果搜索了用户ID,添加条件
- if search_uid:
- query &= Q(yonghuid=search_uid)
-
- # 🔴【修改】排序改为按申请时间升序(最早申请排最前)
- queryset = Tixianjilu.query.filter(query).order_by('CreateTime')
-
- # 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录
- if search_uid:
- records = list(queryset)
- total_count = len(records)
- has_more = False # 搜索模式没有更多数据
- else:
- total_count = queryset.count()
- records = list(queryset[offset:offset + page_size])
- current_count = len(records)
- has_more = (offset + current_count) < total_count
-
- # 获取状态统计(统计与搜索无关,依然按筛选条件统计)
- status_counts = {
- 'awaiting': Tixianjilu.query.filter(
- zhuangtai=1,
- leixing=leixing
- ).count(),
- 'processed': Tixianjilu.query.filter(
- zhuangtai__in=[2, 3],
- leixing=leixing
- ).count()
- }
-
- # 构建响应数据
- withdrawal_list = []
- for record in records:
- withdrawal_list.append({
- 'id': record.id,
- 'yonghuid': record.yonghuid,
- 'avatar': record.avatar,
- 'phone': record.phone,
- 'nicheng': record.nicheng,
- 'leixing': record.leixing,
- 'zhifu': record.zhifu,
- 'skzhanghao': record.skzhanghao,
- 'jine': str(record.jine) if record.jine else '0.00',
- 'zhuangtai': record.zhuangtai,
- 'fangshi': record.fangshi,
- 'shenheid': record.shenheid,
- 'bhliyou': record.bhliyou,
- 'CreateTime': record.CreateTime,
- 'UpdateTime': record.UpdateTime
- })
-
- return Response({
- 'code': 0,
- 'message': '获取成功',
- 'data': {
- 'list': withdrawal_list,
- 'total_count': total_count,
- 'status_counts': status_counts,
- 'has_more': has_more
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- return Response({
- 'code': 500,
- 'message': f'获取提现列表失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-class AdtixianqkView(APIView):
- """
- 处理提现申请接口(已更新:添加收支记录表更新)
- """
- permission_classes = [IsAuthenticated]
-
- @admin_required # 假设你有这个装饰器
- def post(self, request):
- try:
- zhanghao = request.data.get('zhanghao')
- tixian_id = request.data.get('tixian_id')
- result = int(request.data.get('result', 0))
- reason = request.data.get('reason', '')
-
- if not tixian_id:
- return Response({
- 'code': 400,
- 'message': '提现记录ID不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if result not in [2, 3]:
- return Response({
- 'code': 400,
- 'message': '操作结果无效',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 如果是拒绝,需要理由
- if result == 3 and not reason:
- return Response({
- 'code': 400,
- 'message': '拒绝提现需要提供理由',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取提现记录
- try:
- withdrawal = Tixianjilu.query.get(id=tixian_id)
- except Tixianjilu.DoesNotExist:
- return Response({
- 'code': 404,
- 'message': '提现记录不存在',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 检查状态,只有待审核的可以处理
- if withdrawal.zhuangtai != 1:
- return Response({
- 'code': 400,
- 'message': '该提现申请已被处理',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 使用事务确保数据一致性
- with transaction.atomic():
- # 更新提现记录
- withdrawal.zhuangtai = result
- withdrawal.shenheid = request.user.UserUID
-
- if result == 3: # 拒绝
- withdrawal.bhliyou = reason
-
- withdrawal.UpdateTime = timezone.now()
- withdrawal.save()
-
- # 🟢 新增:如果同意提现(result=2),更新收支记录表
- if result == 2:
- try:
- from jituan.services.szjilu_accounting import apply_szjilu_expense
- tixian_jine = withdrawal.jine
- from jituan.services.club_user import get_user_club_id
- payout_club = get_user_club_id(
- User.query.filter(UserUID=withdrawal.yonghuid).first()
- )
- update_daily_payout(tixian_jine, payout_club)
- apply_szjilu_expense(tixian_jine, payout_club)
- except Exception as e:
- # 如果更新收支记录失败,抛出异常,事务会回滚
- raise Exception(f"更新收支记录失败: {str(e)}")
-
- return Response({
- 'code': 0,
- 'message': '处理成功',
- 'data': {
- 'id': withdrawal.id,
- 'zhuangtai': withdrawal.zhuangtai,
- 'shenheid': withdrawal.shenheid,
- 'UpdateTime': withdrawal.UpdateTime
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- return Response({
- 'code': 500,
- 'message': f'处理提现失败: {str(e)}',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-# views.py (yonghu应用)
-class DashouZiliaoHuoquView(APIView):
- """
- 打手资料获取接口
- URL: /yonghu/dszjxxhq
- 方法: POST
- 认证: JWT Token
- 返回: 打手扩展表信息
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 使用select_related高效查询,避免N+1问题
- dashou = UserDashou.query.select_related('user').get(user=request.user)
-
- # 构建返回数据
- data = {
- 'nicheng': dashou.nicheng if dashou.nicheng else '',
- 'jieshao': dashou.jieshao if dashou.jieshao else '',
- 'dianhua': dashou.dianhua if dashou.dianhua else '',
- 'wechat': dashou.wechat if dashou.wechat else '',
- 'uid': request.user.UserUID, # 从主表获取UID
- 'avatar': request.user.Avatar if request.user.Avatar else '', # 头像相对URL
- }
-
- return Response({
- 'code': 200,
- 'msg': 'success',
- 'data': data
- }, status=status.HTTP_200_OK)
-
- except UserDashou.DoesNotExist:
- logger.warning(f'打手扩展表不存在: user_id={request.user.id}')
- return Response({
- 'code': 404,
- 'msg': '打手信息不存在'
- }, status=status.HTTP_404_NOT_FOUND)
-
- except Exception as e:
- logger.error(f'获取打手资料异常: {str(e)}', exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '服务器内部错误'
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-# views.py (yonghu应用)
-
-class DashouGengxinView(APIView):
- """
- 打手信息更新接口
- URL: /yonghu/dsgxxx
- 方法: POST
- 认证: JWT Token
- 参数: nicheng(昵称), dianhua(电话), wechat(微信), jieshao(介绍)
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- # 获取请求数据
- nicheng = request.data.get('nicheng', '').strip()
- dianhua = request.data.get('dianhua', '').strip()
- wechat = request.data.get('wechat', '').strip()
- jieshao = request.data.get('jieshao', '').strip()
-
- # 数据验证
- validation_errors = self.validate_data(nicheng, dianhua, wechat, jieshao)
- if validation_errors:
- return Response({
- 'code': 400,
- 'msg': validation_errors
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取打手扩展表
- try:
- dashou = UserDashou.query.get(user=request.user)
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 404,
- 'msg': '打手信息不存在'
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 更新数据
- update_fields = []
-
- if nicheng is not None:
- dashou.nicheng = nicheng if nicheng != '' else None
- update_fields.append('nicheng')
-
- if dianhua is not None:
- dashou.dianhua = dianhua if dianhua != '' else None
- update_fields.append('dianhua')
-
- if wechat is not None:
- dashou.wechat = wechat if wechat != '' else None
- update_fields.append('wechat')
-
- if jieshao is not None:
- dashou.jieshao = jieshao if jieshao != '' else None
- update_fields.append('jieshao')
-
- # 保存更新(只更新修改的字段)
- if update_fields:
- dashou.save(update_fields=update_fields)
-
- return Response({
- 'code': 200,
- 'msg': '更新成功',
- 'data': {
- 'nicheng': dashou.nicheng or '',
- 'dianhua': dashou.dianhua or '',
- 'wechat': dashou.wechat or '',
- 'jieshao': dashou.jieshao or ''
- }
- }, status=status.HTTP_200_OK)
-
- except Exception as e:
- logger.error(f'更新打手信息异常: {str(e)}', exc_info=True)
- return Response({
- 'code': 500,
- 'msg': '服务器内部错误'
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def validate_data(self, nicheng, dianhua, wechat, jieshao):
- """数据验证方法"""
- errors = []
-
- # 1. 昵称验证(必填)
- if not nicheng:
- errors.append('昵称不能为空')
- elif len(nicheng) > 20:
- errors.append('昵称不能超过20个字符')
-
- # 2. 手机号验证(非必填,但填写时必须正确)
- if dianhua:
- # 手机号正则:1开头,11位数字
- phone_pattern = r'^1[3-9]\d{9}$'
- if not re.match(phone_pattern, dianhua):
- errors.append('手机号格式不正确')
-
- # 3. 微信号验证(非必填)
- if wechat and len(wechat) > 30:
- errors.append('微信号不能超过30个字符')
-
- # 4. 个人介绍验证
- if jieshao and len(jieshao) > 200:
- errors.append('个人介绍不能超过200个字符')
-
- return '; '.join(errors) if errors else None
-
-
-
-
-
-
-
-
-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
-
-
-
-
-
-
-
-# yonghu/ranking_views.py
-"""
-排行榜查询接口 - 管理员专用
-安全认证:JWT Token + 管理员权限验证
-"""
-
-# yonghu/ranking_views.py - 修改PaihangbangGuanliQueryView的post方法参数接收
-class PaihangbangGuanliQueryView(APIView):
- """
- 排行榜查询接口 - 管理员专用
- 安全要求:
- 1. JWT Token认证
- 2. 管理员权限验证
- 3. 参数严格验证
- 4. 返回统一格式
- """
-
- # JWT Token认证
- permission_classes = [IsAuthenticated]
-
- @admin_required
- def post(self, request):
- """
- POST请求查询排行榜数据
- 参数:
- - zhanghao: 管理员账号
- - shenfen: 用户身份(必须,2=打手,3=管事,4=商家)
- - zhouqi: 周期类型(必须,1=日榜,2=月榜)
- - date: 查询日期(必须,格式:YYYY-MM-DD)
-
- 返回格式:
- {
- "code": 0, # 0=成功,非0=失败
- "message": "成功",
- "data": {
- "list": [...], # 排行榜数据列表
- "query_info": {...} # 查询信息
- }
- }
- """
- try:
- # 1. 获取请求数据
- data = request.data
- zhanghao = data.get('zhanghao')
- shenfen = data.get('shenfen')
- zhouqi = data.get('zhouqi')
- date_str = data.get('date')
-
- # 2. 参数验证
- if not zhanghao:
- return Response({
- 'code': 400,
- 'message': '管理员账号不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not shenfen:
- return Response({
- 'code': 400,
- 'message': '用户身份不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not zhouqi:
- return Response({
- 'code': 400,
- 'message': '周期类型不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not date_str:
- return Response({
- 'code': 400,
- 'message': '查询日期不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 参数类型和范围验证
- try:
- zhouqi_int = int(zhouqi)
- shenfen_int = int(shenfen)
- date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
- except ValueError as e:
- return Response({
- 'code': 400,
- 'message': f'参数格式错误: {str(e)}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证周期类型范围
- if zhouqi_int not in [1, 2]:
- return Response({
- 'code': 400,
- 'message': '周期类型必须为1(日榜)或2(月榜)',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证用户身份范围
- if shenfen_int not in [2, 3, 4]:
- return Response({
- 'code': 400,
- 'message': '用户身份必须为2(打手)、3(管事)或4(商家)',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 构建查询条件
- query = Q(
- zhouqi=zhouqi_int,
- tongji_date=date_obj,
- shenfen=shenfen_int
- )
-
- # 5. 执行查询(按排名升序)
- records = RankingRecord.query.filter(query).order_by('paiming')
-
- # 6. 格式化返回数据
- data_list = []
- for record in records:
- # 根据用户身份确定指标值单位
- zhizhi_danwei = self._get_zhizhi_danwei(shenfen_int)
-
- data_list.append({
- 'paiming': record.paiming,
- 'yonghuid': record.yonghuid,
- 'nicheng': record.nicheng,
- 'avatar': record.avatar or '',
- 'zhizhi': float(record.zhizhi),
- 'zhizhi_danwei': zhizhi_danwei,
- })
-
- # 7. 返回成功响应
- response_data = {
- 'code': 0,
- 'message': '查询成功',
- 'data': {
- 'list': data_list,
- 'total_count': len(data_list),
- 'query_info': {
- 'zhouqi': zhouqi_int,
- 'zhouqi_text': '日榜' if zhouqi_int == 1 else '月榜',
- 'shenfen': shenfen_int,
- 'shenfen_text': self._get_shenfen_text(shenfen_int),
- 'date': date_str,
- }
- }
- }
-
- 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)
-
- def _get_shenfen_text(self, shenfen):
- """获取身份文本"""
- shenfen_map = {2: '打手', 3: '管事', 4: '商家'}
- return shenfen_map.get(shenfen, '未知')
-
- def _get_zhizhi_danwei(self, shenfen):
- """获取指标值单位"""
- danwei_map = {2: '元', 3: '人', 4: '元'}
- return danwei_map.get(shenfen, '')
-
-
-
-
-
-
-
-# yonghu/ranking_views.py - 添加以下代码
-class PaihangbangRiqiliebiaoView(APIView):
- """
- 获取排行榜可查询日期列表 - 管理员专用
- 返回当前排行榜表中存在的日期(日榜和月榜分开)
- """
-
- # JWT Token认证
- permission_classes = [IsAuthenticated]
-
- @admin_required
- def post(self, request):
- """
- POST请求获取可查询日期列表
- 参数:
- - zhanghao: 管理员账号(用于验证)
-
- 返回格式:
- {
- "code": 0,
- "message": "成功",
- "data": {
- "ribang_dates": ["2024-01-15", "2024-01-14", ...], # 日榜日期列表
- "yuebang_dates": ["2024-01-01", "2023-12-01", ...], # 月榜日期列表
- }
- }
- """
- try:
- # 1. 获取请求数据
- data = request.data
- zhanghao = data.get('zhanghao')
-
- if not zhanghao:
- return Response({
- 'code': 400,
- 'message': '管理员账号不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 查询日榜日期(最近3个月内的)
- three_months_ago = timezone.now().date() - timedelta(days=90)
-
- ribang_dates = RankingRecord.query.filter(
- zhouqi=1, # 日榜
- tongji_date__gte=three_months_ago # 只查询最近3个月
- ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date')
-
- # 3. 查询月榜日期(最近12个月内的)
- twelve_months_ago = timezone.now().date() - timedelta(days=365)
-
- yuebang_dates = RankingRecord.query.filter(
- zhouqi=2, # 月榜
- tongji_date__gte=twelve_months_ago # 只查询最近12个月
- ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date')
-
- # 4. 格式化日期
- ribang_date_list = [date.strftime('%Y-%m-%d') for date in ribang_dates]
- yuebang_date_list = [date.strftime('%Y-%m-%d') for date in yuebang_dates]
-
- # 5. 返回成功响应
- response_data = {
- 'code': 0,
- 'message': '获取成功',
- 'data': {
- 'ribang_dates': ribang_date_list,
- 'yuebang_dates': yuebang_date_list,
- 'ribang_count': len(ribang_date_list),
- 'yuebang_count': len(yuebang_date_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)
-
-
-
-
-
-
-
-
-class WechatLoginAndDashouRegisterView(APIView):
- """
- 微信登录并注册打手一体化接口
- 访问路径: /api/yonghu/wdlyhdl
- 请求方法: POST
- 权限要求: 无需登录
- 请求体: { "code": "微信登录code", "inviteCode": "邀请码" }
-
- 功能: 用户扫码进入但未登录时,一次性完成微信登录和打手注册
- """
-
- throttle_classes = [AnonRateThrottle]
- permission_classes = [AllowAny]
-
- def post(self, request):
- """
- 处理微信登录并注册打手请求
- """
- try:
- # 1. 获取并验证参数
- code = request.data.get('code', '').strip()
- yaoqingma = request.data.get('inviteCode', '').strip()
-
- if not code:
- return Response({
- 'code': 1,
- 'msg': '微信授权码不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if not yaoqingma:
- return Response({
- 'code': 2,
- 'msg': '邀请码不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- if len(yaoqingma) > 100:
- return Response({
- 'code': 3,
- 'msg': '邀请码长度超过限制',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 获取微信openid
- wechat_data = self.get_wechat_openid(code)
- if not wechat_data or 'openid' not in wechat_data:
- error_msg = wechat_data.get('errmsg', '微信授权失败')
- return Response({
- 'code': 4,
- 'msg': f'微信登录失败: {error_msg}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- openid = wechat_data['openid']
-
- # 3. 获取客户端真实IP
- kehuduan_ip = self.huoquKehuduanIP(request)
-
- # 4. 开始数据库事务(确保登录和注册的原子性)
- with transaction.atomic():
- # 4.1 创建或获取用户(登录逻辑)
- user_main, created = User.objects.select_for_update().get_or_create(
- OpenID=openid,
- defaults={
- 'UserUID': self.shengchengYonghuID(),
- 'UserName': openid,
- }
- )
-
- # 更新用户IP和最后登录时间
- cunchu_ip = kehuduan_ip
- user_main.IP = cunchu_ip
- user_main.UserLastLoginDate = timezone.now()
- user_main.save()
-
- if created:
- # 新用户:创建老板扩展表
- UserBoss.query.create(user=user_main, nickname='微信用户')
-
- # 4.2 验证邀请码对应的管事(注册逻辑)
- try:
- # 使用select_related获取管事及其关联的主表用户信息
- guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma)
- except UserGuanshi.DoesNotExist:
- # 邀请码无效,但用户已登录,可以返回登录成功但注册失败
- # 这里我们继续执行,但只返回登录信息
- return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True)
-
- from jituan.services.invite_guard import assert_invite_same_club
- from jituan.services.club_user import ensure_user_club_id
- from jituan.services.club_context import resolve_club_id_from_request
-
- ok, invite_msg = assert_invite_same_club(
- request, guanshi_profile.user, user_main,
- )
- if not ok:
- return Response({
- 'code': 403,
- 'msg': invite_msg,
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
-
- ensure_user_club_id(user_main, resolve_club_id_from_request(request))
-
- # 验证管事状态
-
- # 4.3 检查用户是否已是打手
- dashou_exists = UserDashou.query.filter(user=user_main).exists()
- if dashou_exists:
- # 用户已是打手,直接返回信息
- dashou_profile = UserDashou.query.get(user=user_main)
- return self.fanhuiDengluZhuceshuju(user_main, dashou_profile)
-
- # 4.4 核心:为用户创建所有身份(打手、商家、管事)
- guanshi_yonghuid = guanshi_profile.user.UserUID
-
- # 创建打手扩展表
- dashou_profile = UserDashou.query.create(
- user=user_main,
- nicheng='大神',
- chenghao='普通大神',
- yaoqingren=guanshi_yonghuid,
- # 其他字段使用模型默认值
- )
-
- # 创建商家扩展表
-
- # 创建管事扩展表
-
- # 更新原管事的邀请数量
- locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
- locked_guanshi.yaogingshuliang += 1
- locked_guanshi.save()
-
- # ========== 新增:管事每日统计(邀请打手) ==========
- try:
- update_guanshi_daily_by_action(
- yonghuid=guanshi_yonghuid,
- action=1, # 1 = 邀请打手
- amount=Decimal('0.00')
- )
- logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
- except Exception as e:
- logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
-
- # 5. 生成JWT token
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- # 6. 准备返回数据
- response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token)
-
- return Response({
- 'code': 0,
- 'msg': '登录并注册成功!已开通打手、管事等身份。',
- 'data': response_data
- })
-
- except Exception as e:
- # 记录错误日志
- error_openid = locals().get('openid', 'N/A')
- logger.error(
- f"微信登录注册一体化接口异常 - openid: {error_openid}, "
- f"错误类型: {type(e).__name__}, 错误详情: {str(e)}",
- exc_info=True
- )
-
- # 返回友好错误信息
- return Response({
- 'code': 99,
- 'msg': '系统繁忙,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def get_wechat_openid(self, code):
- """
- 调用微信接口获取openid
- """
- try:
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
-
- if not appid or not secret:
- raise ValueError('微信配置未设置')
-
- url = 'https://api.weixin.qq.com/sns/jscode2session'
- params = {
- 'appid': appid,
- 'secret': secret,
- 'js_code': code,
- 'grant_type': 'authorization_code'
- }
-
- response = requests.get(url, params=params, timeout=10)
- result = response.json()
-
- if 'openid' in result:
- return result
- else:
- errcode = result.get('errcode', 'unknown')
- errmsg = result.get('errmsg', '未知错误')
- return {'errmsg': f'[{errcode}]{errmsg}'}
-
- except requests.exceptions.Timeout:
- return {'errmsg': '请求微信接口超时'}
- except Exception as e:
- return {'errmsg': f'请求微信接口异常: {str(e)}'}
-
- def shengchengYonghuID(self):
- """
- 生成7位数字用户ID
- """
- for _ in range(10):
- timestamp_part = str(int(time.time()))[-5:].zfill(5)
- random_part = str(random.randint(0, 99)).zfill(2)
- user_id = timestamp_part + random_part
-
- if len(user_id) == 7 and user_id.isdigit():
- if not User.query.filter(UserUID=user_id).exists():
- return user_id
-
- raise Exception('生成用户ID失败')
-
- def huoquKehuduanIP(self, request):
- """
- 获取客户端真实IP地址
- """
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
- if x_forwarded_for:
- ip = x_forwarded_for.split(',')[0].strip()
- if ip:
- return ip
-
- ip = request.META.get('REMOTE_ADDR', '')
- return ip if ip else '0.0.0.0'
-
- def huoquQunPeizhi(self):
- """
- 获取群配置信息
- """
- try:
- qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
-
- result = {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
-
- for config in qun_configs:
- if config.id == 1:
- result['dashouqun'] = config.GroupContent or ''
- result['dashouqunid'] = config.GroupID or ''
- elif config.id == 2:
- result['guanshiqun'] = config.GroupContent or ''
- result['guanshiqunid'] = config.GroupID or ''
-
- return result
-
- except Exception:
- return {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
-
- def fanhuiZhihouDengluXinxi(self, user_main, yaoqingma_wuxiao=False, guanshi_jinyong=False):
- """
- 返回只登录成功的信息(当邀请码无效或管事禁用时)
- """
- # 获取老板昵称
- boss_nickname = '微信用户'
- try:
- boss_profile = UserBoss.query.get(user=user_main)
- if boss_profile.nickname:
- boss_nickname = boss_profile.nickname
- except UserBoss.DoesNotExist:
- pass
-
- # 检查各身份状态
- dashou_status = 1 if UserDashou.query.filter(user=user_main).exists() else 0
- shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 0
- guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 0
-
- # 生成token
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- # 获取群配置
- group_info = self.huoquQunPeizhi()
-
- # 构建返回数据
- response_data = {
- 'token': token,
- 'nicheng': boss_nickname,
- 'uid': user_main.UserUID,
- 'touxiang': user_main.Avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- # 🟢 注意:这里没有返回dingdantiaoshu(订单数量),与前端需求一致
- 'dashouqun': group_info.get('dashouqun', ''),
- 'dashouqunid': group_info.get('dashouqunid', ''),
- 'guanshiqun': group_info.get('guanshiqun', ''),
- 'guanshiqunid': group_info.get('guanshiqunid', '')
- }
-
- # 根据情况设置返回消息
- if yaoqingma_wuxiao:
- msg = '登录成功,但邀请码无效'
- elif guanshi_jinyong:
- msg = '登录成功,但邀请码对应的管事账号已被禁用'
- else:
- msg = '登录成功'
-
- return Response({
- 'code': 0,
- 'msg': msg,
- 'data': response_data
- })
-
- def fanhuiDengluZhuceshuju(self, user_main, dashou_profile):
- """
- 返回登录并注册的数据(用户已是打手的情况)
- """
- # 获取老板昵称
- boss_nickname = '微信用户'
- try:
- boss_profile = UserBoss.query.get(user=user_main)
- if boss_profile.nickname:
- boss_nickname = boss_profile.nickname
- except UserBoss.DoesNotExist:
- pass
-
- # 检查各身份状态(这里用户已是打手,所以都是1)
- dashou_status = 1
- shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 1 # 确保是1
- guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 1 # 确保是1
-
- # 生成token
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- # 获取群配置
- group_info = self.huoquQunPeizhi()
-
- # 准备打手信息
- dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
-
- # 合并所有数据
- response_data = {
- 'token': token,
- 'nicheng': boss_nickname,
- 'uid': user_main.UserUID,
- 'touxiang': user_main.Avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- 'dashouqun': group_info.get('dashouqun', ''),
- 'dashouqunid': group_info.get('dashouqunid', ''),
- 'guanshiqun': group_info.get('guanshiqun', ''),
- 'guanshiqunid': group_info.get('guanshiqunid', ''),
- # 合并打手信息
- **dashou_data
- }
-
- return Response({
- 'code': 0,
- 'msg': '您已是打手,登录成功',
- 'data': response_data
- })
-
- def zhuangbeiFanhuiShuju(self, user_main, dashou_profile, token):
- """
- 准备完整的返回数据(登录+注册成功的情况)
- """
- # 获取老板昵称
- boss_nickname = '微信用户'
- try:
- boss_profile = UserBoss.query.get(user=user_main)
- if boss_profile.nickname:
- boss_nickname = boss_profile.nickname
- except UserBoss.DoesNotExist:
- pass
-
- # 身份状态(刚注册成功,应该都是1)
- dashou_status = 1
- shangjia_status = 1
- guanshi_status = 1
-
- # 获取群配置
- group_info = self.huoquQunPeizhi()
-
- # 准备打手信息
- dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
-
- # 合并所有数据
- response_data = {
- 'token': token,
- 'nicheng': boss_nickname,
- 'uid': user_main.UserUID,
- 'touxiang': user_main.Avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- 'dashouqun': group_info.get('dashouqun', ''),
- 'dashouqunid': group_info.get('dashouqunid', ''),
- 'guanshiqun': group_info.get('guanshiqun', ''),
- 'guanshiqunid': group_info.get('guanshiqunid', ''),
- # 合并打手信息
- **dashou_data
- }
-
- return response_data
-
- def zhuangbeiDashouFanhuiShuju(self, dashou_profile):
- """
- 准备打手返回数据(字段名严格匹配前端)
- """
- # 构建基础信息
- data = {
- 'dashounicheng': dashou_profile.nicheng,
- 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
- 'yongjin': str(dashou_profile.yue), # Decimal转字符串
- 'zonge': str(dashou_profile.zonge),
- 'yajin': str(dashou_profile.yajin),
- 'chenghao': dashou_profile.chenghao,
- 'jinfen': dashou_profile.jifen,
- 'chengjiaoliang': dashou_profile.chengjiaozongliang,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'dashouzhuangtai': dashou_profile.zhuangtai,
- }
-
- # 查询并处理会员列表 (clumber)
- # 使用当前用户的yonghuid进行查询
- huiyuan_goumai_list = []
- goumai_records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.UserUID)
- for record in goumai_records:
- # 调用方法检查是否到期,此方法内部会更新状态
- shifou_daoqi = record.jiance_shifou_daoqi()
- huiyuan_goumai_list.append({
- 'huiyuan_id': record.huiyuan_id,
- 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
- 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
- 'shifou_daoqi': shifou_daoqi,
- 'jieshao': record.jieshao
- })
-
- data['clumber'] = huiyuan_goumai_list
- return data
-
-class YonghuTixianShenheXiugaiView(APIView):
- """
- 用户修改审核中提现申请接口
- 访问路径: /api/yonghu/yonghutxshxg
- 请求方法: POST
- 权限要求: JWT Token认证通过的用户
- 请求体: multipart/form-data 格式
- 支持字段: tixian_id, fangshi, txdianhua, txzh, file (图片文件)
-
- 功能: 允许用户修改审核中提现申请的收款方式、电话、账号、收款码等
- """
-
- permission_classes = [IsAuthenticated]
- parser_classes = (MultiPartParser, FormParser, JSONParser)
-
- def post(self, request):
- """
- 处理用户修改审核中提现申请的POST请求
- """
- try:
- # 1. 获取当前用户
- user_main = request.user
- current_yonghuid = user_main.UserUID
-
- # 2. 验证必填字段:提现记录ID
- tixian_id = request.data.get('tixian_id')
- if not tixian_id:
- return Response({
- 'code': 1,
- 'msg': '提现记录ID不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 查询提现记录(使用select_for_update确保事务安全)
- with transaction.atomic():
- try:
- tixian_record = Tixianjilu.objects.select_for_update().get(
- id=tixian_id,
- yonghuid=current_yonghuid # 确保只能修改自己的记录
- )
- except Tixianjilu.DoesNotExist:
- return Response({
- 'code': 2,
- 'msg': '提现记录不存在或无权修改',
- 'data': None
- }, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 验证提现记录状态(必须是审核中)
- if tixian_record.zhuangtai != 1:
- return Response({
- 'code': 3,
- 'msg': '只有审核中的提现申请可以修改',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 获取并验证提现方式
- fangshi = request.data.get('fangshi')
- if fangshi:
- fangshi = int(fangshi)
- if fangshi not in [1, 2]:
- return Response({
- 'code': 4,
- 'msg': '提现方式无效,请选择1(微信)或2(支付宝)',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 检查提现类型与方式的匹配(如果前端提供了leixing)
- leixing = request.data.get('leixing')
- if leixing:
- leixing = int(leixing)
- # 这里可以根据业务逻辑添加类型与方式的验证
- # 例如:某些类型只能使用某些方式
-
- # 6. 验证手机号(必填)
- txdianhua = request.data.get('txdianhua', '').strip()
- if not txdianhua:
- return Response({
- 'code': 5,
- 'msg': '收款人电话不能为空',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证手机号格式
- if not txdianhua.isdigit() or len(txdianhua) != 11:
- return Response({
- 'code': 6,
- 'msg': '请输入11位有效的手机号码',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 7. 获取收款账号(可选)
- txzh = request.data.get('txzh', '').strip()
-
- # 8. 处理收款码图片上传(如果有)
- txtupian_file = request.FILES.get('file')
- new_zhifu_url = None
- old_zhifu_url = tixian_record.zhifu
-
- if txtupian_file:
- # 验证图片
- is_valid, error_msg = validate_image(txtupian_file)
- if not is_valid:
- return Response({
- 'code': 7,
- 'msg': f'图片验证失败: {error_msg}',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 上传收款码图片
- new_zhifu_url = self.handle_shoukuanma_upload(
- user_main,
- txtupian_file,
- old_zhifu_url,
- tixian_record.leixing
- )
-
- if not new_zhifu_url:
- return Response({
- 'code': 8,
- 'msg': '收款码上传失败',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 9. 验证收款信息(账号和图片至少有一个)
- if not txzh and not (new_zhifu_url or old_zhifu_url):
- return Response({
- 'code': 9,
- 'msg': '请填写收款账号或上传收款码',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 10. 更新提现记录
- update_fields = []
-
- # 更新提现方式(如果有)
- if fangshi:
- tixian_record.fangshi = fangshi
- update_fields.append('fangshi')
-
- # 更新手机号(必填)
- tixian_record.phone = txdianhua
- update_fields.append('phone')
-
- # 更新收款账号(如果有)
- if txzh is not None: # 注意:空字符串也要更新
- tixian_record.skzhanghao = txzh
- update_fields.append('skzhanghao')
-
- # 更新收款码图片(如果有新图片)
- if new_zhifu_url:
- tixian_record.zhifu = new_zhifu_url
- update_fields.append('zhifu')
-
- # 保存更新
- if update_fields:
- tixian_record.save(update_fields=update_fields)
-
- # 11. 构建返回数据
- response_data = {
- 'tixian_id': tixian_record.id,
- 'fangshi': tixian_record.fangshi,
- 'txdianhua': tixian_record.phone,
- 'txzh': tixian_record.skzhanghao or '',
- 'txtupian': tixian_record.zhifu or '' # 返回相对URL
- }
-
- # 12. 返回成功响应
- return Response({
- 'code': 0,
- 'msg': '提现申请修改成功',
- 'data': response_data
- })
-
- except Exception as e:
- # 记录详细错误日志
- logger.error(
- f"修改提现申请异常 - 用户ID: {getattr(user_main, 'yonghuid', 'N/A')}, "
- f"提现ID: {tixian_id}, "
- f"错误类型: {type(e).__name__}, "
- f"错误详情: {str(e)}",
- exc_info=True
- )
-
- # 返回友好错误信息
- return Response({
- 'code': 99,
- 'msg': '系统繁忙,请稍后重试',
- 'data': None
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def handle_shoukuanma_upload(self, user_main, image_file, old_zhifu_url, leixing=None):
- """
- 处理收款码图片上传到腾讯云COS
- 存储路径: tixian/{yonghuid}/{timestamp_uuid}.扩展名
-
- 参数:
- - user_main: 当前用户对象
- - image_file: 图片文件对象
- - old_zhifu_url: 旧收款码URL(用于删除)
- - leixing: 提现类型(可选,用于文件夹分类)
-
- 返回: 新的图片相对路径
- """
- try:
- yonghuid = user_main.UserUID
- if not yonghuid:
- return None
-
- # 删除旧收款码(如果存在且不是空字符串)
- if old_zhifu_url and old_zhifu_url.strip():
- delete_from_oss(old_zhifu_url)
-
- # 获取文件扩展名
- file_name = image_file.name
- file_extension = os.path.splitext(file_name)[1].lower()
- if not file_extension:
- file_extension = '.jpg'
-
- # 生成唯一文件名(使用时间戳+UUID确保不重复)
- timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
- unique_id = str(uuid.uuid4())[:8] # 取UUID前8位
- unique_file_name = f"{timestamp}_{unique_id}{file_extension}"
-
- # 根据提现类型构建文件夹路径(如果提供了leixing)
- if leixing:
- # leixing=1:打手佣金, leixing=2:管事分红
- type_folder = 'dashou' if leixing == 1 else 'guanshi'
- oss_file_path = f"tixian/{type_folder}/{yonghuid}/{unique_file_name}"
- else:
- # 默认路径
- oss_file_path = f"tixian/{yonghuid}/{unique_file_name}"
-
- # 上传新收款码到OSS
- new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
-
- if new_shoukuanma_url:
- # 返回相对路径(从tixian文件夹开始)
- return oss_file_path
- else:
- return None
-
- except Exception as e:
- print(f"处理收款码上传失败: {e}")
- return None
-
-
-# views.py
-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)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-class KefuLoginView(APIView):
- """
- 客服/管理员登录接口(支持手机号重复)
- 请求:POST /yonghu/kefujinru
- 请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
- 返回:
- code=0 成功,data包含token,nicheng,yonghuid
- code=1 失败(msg区分:用户不存在/密码错误/二级密码错误)
- code=4 账号被封禁
-
- 管理员(IsSuperuser 或 UserType='admin')无需 erjimima 和 KefuProfile。
- """
- throttle_classes = [AnonRateThrottle] # 限制匿名请求频率
- permission_classes = [AllowAny] # 任何人都可访问
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- password = request.data.get('password', '')
- erjimima = request.data.get('erjimima', '')
-
- if not phone or not password:
- return Response({
- 'code': 1,
- 'msg': '请填写手机号和密码',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 获取客户端真实IP
- client_ip = self.get_client_ip(request)
-
- # 3. 查询该手机号下所有账号(客服 + 管理员)
- # 先查客服(有 KefuProfile),再查管理员(IsSuperuser 或 UserType='admin')
- user_mains = list(
- User.query.filter(
- Phone=phone,
- KefuProfile__isnull=False,
- ).select_related('KefuProfile')
- )
-
- # 如果没有客服账号,尝试查找管理员账号
- if not user_mains:
- admin_candidates = User.query.filter(Phone=phone)
- for admin_user in admin_candidates:
- if admin_user.IsSuperuser or admin_user.UserType == 'admin':
- user_mains = [admin_user]
- break
-
- if not user_mains:
- logger.warning(f"登录失败,用户不存在: {phone}, IP: {client_ip}")
- return Response({
- 'code': 1,
- 'msg': '用户不存在',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 遍历所有匹配的账号,验证密码和二级密码
- target_user = None
- target_kefu = None
- password_matched = False
-
- for user in user_mains:
- # 验证主密码(bcrypt 哈希验证)
- if not user.CheckPassword(password):
- continue
- password_matched = True
-
- # 管理员:跳过二级密码和客服扩展表检查
- is_admin = user.IsSuperuser or user.UserType == 'admin'
- if is_admin:
- target_user = user
- target_kefu = None
- break
-
- # 客服:验证二级密码
- kefu = user.KefuProfile
- stored_erji = kefu.erjimima or ''
- if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60:
- import bcrypt as _bcrypt
- if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')):
- continue
- elif stored_erji != erjimima:
- continue
-
- # 如果账号被禁用,记录但继续查找
- if kefu.zhuangtai != 1:
- if target_user is None:
- target_user = user
- target_kefu = kefu
- continue
-
- # 找到完全匹配且状态正常的账号,立即使用
- target_user = user
- target_kefu = kefu
- break
- else:
- # 循环结束未找到正常账号
- if target_user and target_kefu:
- # 只有被禁用的账号匹配,返回封禁提示
- logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}")
- return Response({
- 'code': 4,
- 'msg': '账号已被封禁',
- 'data': None
- }, status=status.HTTP_403_FORBIDDEN)
- elif password_matched:
- # 密码正确但二级密码错误
- logger.warning(f"登录失败,二级密码错误: {phone}, IP: {client_ip}")
- return Response({
- 'code': 1,
- 'msg': '二级密码错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
- else:
- # 密码错误
- logger.warning(f"登录失败,密码错误: {phone}, IP: {client_ip}")
- return Response({
- 'code': 1,
- 'msg': '密码错误',
- 'data': None
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 更新IP和最后登录时间
- target_user.IP = client_ip
- target_user.UserLastLoginDate = timezone.now()
- target_user.save(update_fields=['IP', 'UserLastLoginDate'])
-
- # 6. 生成JWT token
- refresh = RefreshToken.for_user(target_user)
- token = str(refresh.access_token)
-
- # 7. 返回成功数据(管理员无 KefuProfile,nicheng 用 UserName 或 Phone 兜底)
- nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
- return Response({
- 'code': 0,
- 'msg': '登录成功',
- 'data': {
- 'token': token,
- 'nicheng': nicheng,
- 'yonghuid': target_user.UserUID,
- }
- })
-
- def get_client_ip(self, request):
- """
- 获取客户端真实IP
- """
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
- if x_forwarded_for:
- ip = x_forwarded_for.split(',')[0].strip()
- if ip:
- return ip
- return request.META.get('REMOTE_ADDR', '0.0.0.0')
-
-
-
-
-
-
-logger = logging.getLogger(__name__)
-
-class KefuStatsView(APIView):
- """
- 客服统计数据接口
- 请求:POST /yonghu/kfjrhqzl
- 请求体:{"phone": "13800138000"}
- 认证:必须携带有效的 JWT token(放在 Authorization 头)
- 返回:
- code=0 成功,data 包含 jinrichuli, jinyuechuli, zongchuli
- 验证失败统一返回 401,不透露具体原因
- """
-
- permission_classes = [IsAuthenticated] # 必须登录
-
-
- def post(self, request):
- # 1. 获取请求中的 phone
- phone = request.data.get('phone', '').strip()
- if not phone:
- # 缺少参数,但按约定返回 401(可以改为 400,但为了统一,用401)
- logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.UserUID}")
- return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 获取当前登录用户(从 JWT 中解析)
- current_user = request.user
-
- # 3. 验证前端传来的 phone 是否与当前用户的 phone 一致
- if current_user.Phone != phone:
- # 不一致,可能 token 被冒用或参数错误
- logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
- return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 4. 验证是否为后台客服账号
- from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
- if not is_kefu_backend_account(current_user):
- logger.warning(f"统计接口用户类型非客服: {current_user.UserUID}")
- return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 5. 尝试获取客服扩展表数据(管理员跳过)
- is_admin = is_system_super_admin(current_user)
- if is_admin:
- return Response({
- 'code': 0,
- 'data': {
- 'jinrichuli': 0,
- 'jinyuechuli': 0,
- 'zongchuli': 0,
- }
- }, status=status.HTTP_200_OK)
-
- try:
- kefu_profile = current_user.KefuProfile # OneToOneField 反向关系
- except UserKefu.DoesNotExist:
- logger.warning(f"统计接口客服扩展表不存在: {current_user.UserUID}")
- return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 6. 返回统计数据(字段名与前端约定一致)
- return Response({
- 'code': 0,
- 'data': {
- 'jinrichuli': kefu_profile.jinrichuli,
- 'jinyuechuli': kefu_profile.jinyuechuli,
- 'zongchuli': kefu_profile.zongchuli,
- }
- }, status=status.HTTP_200_OK)
-
-
-
-
-
-
-class KefuGetOrderTypesView(APIView):
- """
- 客服获取订单类型列表接口(平台订单用)
- 请求:POST /yonghu/kfhqptddlx
- 参数:{"phone": "13800138000"}
- 认证:JWT
- 返回:code=0 + 类型列表
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- if not phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- current_user = request.user
-
- # 2. 验证手机号一致性
- if getattr(current_user, 'Phone', '') != phone:
- logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 验证用户类型及客服状态
- if current_user.UserType not in ('kefu', 'admin'):
- logger.warning(f"用户类型非客服: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 管理员跳过客服扩展表检查
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- if not is_admin:
- try:
- kefu_profile = current_user.KefuProfile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu_profile.zhuangtai != 1:
- logger.warning(f"客服账号已被禁用: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 4. 查询订单类型(商品类型)
- try:
- # 只查询需要的字段,减少数据传输
- types_qs = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
- type_list = [
- {
- 'id': t.id,
- 'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao
- 'tupian_url': t.tupian_url or ''
- }
- for t in types_qs
- ]
- return Response({'code': 0, 'data': type_list})
- except Exception as e:
- logger.error(f"查询商品类型表失败: {str(e)}")
- # 表不存在或查询失败,返回空列表
- return Response({'code': 0, 'data': []})
-
-
-
-
-
-class KefuGetOrderListView(APIView):
- """
- 客服获取平台订单列表接口(发单平台=1,非跨平台)
- 需要权限:002ab(平台订单管理)
- 请求参数:
- username: 客服账号(用于权限校验)
- page: 页码
- pageSize: 每页条数
- dingdan_id: 订单ID(精确搜索)
- laoban_id: 老板ID(搜索平台扩展表中的老板)
- dashou_id: 打手ID(搜索主表中的接单打手)
- leixing: 商品类型ID
- zhuangtai: 状态(多个用逗号分隔,如 "1,7")
- date: 日期(YYYY-MM-DD),筛选订单创建时间
- nicheng: 游戏昵称(订单主表的 nicheng 字段)
- jieshao: 订单介绍(订单主表的 jieshao 字段)
- clkf: 处理客服(订单主表的 clkf 字段)
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- start_total = time.time()
-
- # 1. 获取前端参数
- username = request.data.get('phone', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 3. 检查平台订单管理权限(002ab)
- if '002ab' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
-
- # 4. 获取分页和筛选参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 20))
-
- dingdan_id = request.data.get('dingdan_id', '').strip()
- laoban_id = request.data.get('laoban_id', '').strip()
- dashou_id = request.data.get('dashou_id', '').strip()
- leixing = request.data.get('leixing')
- zhuangtai = request.data.get('zhuangtai', '')
- date = request.data.get('date', '').strip()
- # 新增筛选条件
- nicheng = request.data.get('nicheng', '').strip()
- jieshao = request.data.get('jieshao', '').strip()
- clkf = request.data.get('clkf', '').strip()
-
- # ---------- 查询条件 ----------
- # 基础条件:平台发单 + 兼容历史 Platform 为空
- from jituan.services.club_context import platform_order_base_q
- base_q = platform_order_base_q()
-
- if dingdan_id:
- q_conditions = base_q & Q(OrderID=dingdan_id)
- else:
- q_conditions = base_q
- if leixing is not None and str(leixing).strip() != '':
- try:
- leixing_int = int(leixing)
- q_conditions &= Q(ProductTypeID=leixing_int)
- except (ValueError, TypeError):
- pass
- if zhuangtai and zhuangtai != 'all':
- try:
- status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
- if status_list:
- q_conditions &= Q(Status__in=status_list)
- except (ValueError, TypeError):
- pass
- if dashou_id:
- q_conditions &= Q(PlayerID=dashou_id)
- if laoban_id:
- q_conditions &= Q(pingtai_kuozhan__BossID=laoban_id)
- if date:
- q_conditions &= Q(CreateTime__date=date)
- # 新增筛选条件
- if nicheng:
- q_conditions &= Q(Nickname__icontains=nicheng)
- if jieshao:
- q_conditions &= Q(Description__icontains=jieshao)
- if clkf:
- q_conditions &= Q(AssignedCS__icontains=clkf)
-
- from jituan.services.club_context import (
- count_order_status_buckets,
- order_scope_qs,
- paginate_fluent_query,
- )
- from orders.models import PlatformOrderExt
-
- # 按俱乐部过滤订单列表
- stats_qs = order_scope_qs(request).filter(q_conditions)
- stats = stats_qs.aggregate(
- total_orders=Count('id'),
- completed_orders=Count('id', filter=Q(Status=3)),
- refund_orders=Count('id', filter=Q(Status=5)),
- pending_orders=Count('id', filter=Q(Status__in=[4, 8]))
- )
- status_counts = count_order_status_buckets(q_conditions, request)
-
- list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime')
- total_count, orders_page = paginate_fluent_query(list_qs, page, page_size)
- if total_count > 0 and not orders_page:
- start = (max(1, page) - 1) * page_size
- orders_page = list(list_qs._qs[start:start + page_size])
-
- order_ids = [o.OrderID for o in orders_page if o.OrderID]
- ext_map = {}
- if order_ids:
- for ext in PlatformOrderExt.query.filter(Order__OrderID__in=order_ids).select_related('Order'):
- if ext.Order:
- ext_map[ext.Order.OrderID] = ext
-
- laoban_ids = [
- ext.BossID for ext in ext_map.values() if ext.BossID
- ]
- nickname_map = {}
- if laoban_ids:
- users = User.query.filter(UserUID__in=laoban_ids).select_related('BossProfile').only(
- 'UserUID', 'BossProfile__nickname'
- )
- for user in users:
- nickname_map[user.UserUID] = (user.BossProfile.nickname or '') if getattr(user, 'BossProfile', None) else ''
-
- result_list = []
- for order in orders_page:
- ext = ext_map.get(order.OrderID)
- laoban_id = ext.BossID if ext else ''
- youxi_nicheng = nickname_map.get(laoban_id, order.Nickname or '')
- result_list.append({
- 'dingdan_id': order.OrderID or '',
- 'jieshao': order.Description or '',
- 'zhuangtai': order.Status,
- 'leixing': order.ProductTypeID,
- 'fadanpingtai': order.Platform,
- 'jiage': float(order.Amount) if order.Amount else 0.00,
- 'tupian_url': order.ImageURL or '',
- 'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
- 'youxi_nicheng': youxi_nicheng
- })
-
- from jituan.services.club_user_access import list_response_meta
-
- response_data = {
- 'stats': {
- 'totalOrders': stats['total_orders'] or 0,
- 'completedOrders': stats['completed_orders'] or 0,
- 'refundOrders': stats['refund_orders'] or 0,
- 'pendingOrders': stats['pending_orders'] or 0
- },
- 'statusCounts': status_counts,
- 'list': result_list,
- 'total': total_count,
- 'page': page,
- 'pageSize': page_size,
- **list_response_meta(request),
- }
-
- logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s")
- return Response({'code': 0, 'data': response_data})
-
-class KefuGetShangjiaOrderListView(APIView):
- """
- 客服获取商家订单列表接口(发单平台=2,非跨平台)
- 需要权限:002ac(商家订单管理)—— 如不区分可使用 002ab
- 请求参数:
- username: 客服账号(用于权限校验)
- page: 页码
- pageSize: 每页条数
- dingdan_id: 订单ID(精确搜索)
- shangjia_id: 商家ID(搜索商家扩展表中的商家ID)
- dashou_id: 打手ID(搜索主表中的接单打手)
- leixing: 商品类型ID
- zhuangtai: 状态(多个用逗号分隔,如 "1,7")
- date: 日期(YYYY-MM-DD),筛选订单创建时间
- nicheng: 游戏昵称(订单主表的 nicheng 字段)
- jieshao: 订单介绍(订单主表的 jieshao 字段)
- clkf: 处理客服(订单主表的 clkf 字段)
- shangjia_nicheng: 商家昵称(商家扩展表中的昵称)
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- start_total = time.time()
-
- # 1. 获取前端参数(兼容 phone / username)
- username = request.data.get('username', '').strip() or request.data.get('phone', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 3. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab)
- if '002ac' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看商家订单列表'})
-
- # 4. 获取分页和筛选参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 20))
-
- dingdan_id = request.data.get('dingdan_id', '').strip()
- shangjia_id = request.data.get('shangjia_id', '').strip()
- dashou_id = request.data.get('dashou_id', '').strip()
- leixing = request.data.get('leixing')
- zhuangtai = request.data.get('zhuangtai', '')
- date = request.data.get('date', '').strip()
- # 新增筛选条件
- nicheng = request.data.get('nicheng', '').strip()
- jieshao = request.data.get('jieshao', '').strip()
- clkf = request.data.get('clkf', '').strip()
- shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip()
-
- # ---------- 查询条件 ----------
- from jituan.services.club_context import merchant_order_base_q
- base_q = merchant_order_base_q()
-
- if dingdan_id:
- q_conditions = base_q & Q(OrderID=dingdan_id)
- else:
- q_conditions = base_q
- if leixing is not None and str(leixing).strip() != '':
- try:
- leixing_int = int(leixing)
- q_conditions &= Q(ProductTypeID=leixing_int)
- except (ValueError, TypeError):
- pass
- if zhuangtai and zhuangtai != 'all':
- try:
- status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
- if status_list:
- q_conditions &= Q(Status__in=status_list)
- except (ValueError, TypeError):
- pass
- if dashou_id:
- q_conditions &= Q(PlayerID=dashou_id)
- if shangjia_id:
- q_conditions &= Q(shangjia_kuozhan__MerchantID=shangjia_id)
- if date:
- q_conditions &= Q(CreateTime__date=date)
- if nicheng:
- q_conditions &= Q(Nickname__icontains=nicheng)
- if jieshao:
- q_conditions &= Q(Description__icontains=jieshao)
- if clkf:
- q_conditions &= Q(AssignedCS__icontains=clkf)
- if shangjia_nicheng:
- # 通过商家扩展表关联 UserShangjia 的昵称
- q_conditions &= Q(shangjia_kuozhan__MerchantID__in=User.query.filter(
- ShopProfile__nicheng__icontains=shangjia_nicheng
- ).values_list('UserUID', flat=True))
-
- from jituan.services.club_context import (
- count_order_status_buckets,
- order_scope_qs,
- paginate_fluent_query,
- )
- from orders.models import MerchantOrderExt
-
- stats_qs = order_scope_qs(request).filter(q_conditions)
- stats = stats_qs.aggregate(
- total_orders=Count('id'),
- completed_orders=Count('id', filter=Q(Status=3)),
- refund_orders=Count('id', filter=Q(Status=5)),
- pending_orders=Count('id', filter=Q(Status__in=[4, 8]))
- )
- status_counts = count_order_status_buckets(q_conditions, request)
-
- list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime')
- total_count, orders_page = paginate_fluent_query(list_qs, page, page_size)
- if total_count > 0 and not orders_page:
- start = (max(1, page) - 1) * page_size
- orders_page = list(list_qs._qs[start:start + page_size])
-
- order_ids = [o.OrderID for o in orders_page if o.OrderID]
- ext_map = {}
- if order_ids:
- for ext in MerchantOrderExt.query.filter(Order__OrderID__in=order_ids).select_related('Order'):
- if ext.Order:
- ext_map[ext.Order.OrderID] = ext
-
- shangjia_ids = [
- ext.MerchantID for ext in ext_map.values() if ext.MerchantID
- ]
- nickname_map = {}
- if shangjia_ids:
- users = User.query.filter(UserUID__in=shangjia_ids).select_related('ShopProfile').only(
- 'UserUID', 'ShopProfile__nicheng'
- )
- for user in users:
- nickname_map[user.UserUID] = user.ShopProfile.nicheng or '' if hasattr(user, 'ShopProfile') else ''
-
- result_list = []
- for order in orders_page:
- ext = ext_map.get(order.OrderID)
- shangjia_id = ext.MerchantID if ext else ''
- youxi_nicheng = order.Nickname or ''
- result_list.append({
- 'dingdan_id': order.OrderID or '',
- 'jieshao': order.Description or '',
- 'zhuangtai': order.Status,
- 'leixing': order.ProductTypeID,
- 'fadanpingtai': order.Platform,
- 'jiage': float(order.Amount) if order.Amount else 0.00,
- 'tupian_url': order.ImageURL or '',
- 'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
- 'youxi_nicheng': youxi_nicheng
- })
-
- from jituan.services.club_user_access import list_response_meta
-
- response_data = {
- 'stats': {
- 'totalOrders': stats['total_orders'] or 0,
- 'completedOrders': stats['completed_orders'] or 0,
- 'refundOrders': stats['refund_orders'] or 0,
- 'pendingOrders': stats['pending_orders'] or 0
- },
- 'statusCounts': status_counts,
- 'list': result_list,
- 'total': total_count,
- 'page': page,
- 'pageSize': page_size,
- **list_response_meta(request),
- }
-
- logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s")
- return Response({'code': 0, 'data': response_data})
-
-
-
-
-
-
-
-
-
-
-
-class KefuChangeDashouView(APIView):
- """
- 客服更换打手接口
- 请求:POST /yonghu/kefughds
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "new_dashou_id": "新打手ID"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- new_dashou_id = request.data.get('new_dashou_id', '').strip()
-
- if not phone or not dingdan_id or not new_dashou_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- logger.warning(f"用户类型非客服: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单
- try:
- order = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8)
- if order.Status not in [2, 8]:
- return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 查询新打手
- try:
- new_dashou_user = User.query.get(UserUID=new_dashou_id)
- except User.DoesNotExist:
- return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 6. 验证打手扩展表
- try:
- new_dashou_profile = new_dashou_user.DashouProfile
- except ObjectDoesNotExist:
- return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND)
-
- # 7. 验证打手状态(打手状态和账号状态必须为1)
- if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
- return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST)
- # 8. 验证打手积分是否足够(至少5分)
- if new_dashou_profile.jifen < 5:
- return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 8. 根据订单的抢单要求类型进行额外验证
- yaoqiuleixing = order.GrabRequirement
-
- if yaoqiuleixing == 1: # 会员抢单
- huiyuan_id = order.MembershipID
- if not huiyuan_id:
- return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- # 查询打手的会员购买记录
- huiyuan_goumai = Huiyuangoumai.query.get(
- huiyuan_id=huiyuan_id,
- yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID
- )
- except Huiyuangoumai.DoesNotExist:
- return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 检查会员是否过期(调用模型方法)
- is_expired = huiyuan_goumai.jiance_shifou_daoqi()
- if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
- return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST)
-
- elif yaoqiuleixing == 2: # 押金抢单
- order_yajin = order.CommissionReq # 假设订单的 yongjin 字段存储押金要求
- if order_yajin is None:
- return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- if new_dashou_profile.yajin < order_yajin:
- return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST)
- else:
- return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 9. 所有验证通过,开始更换打手(使用事务)
- with transaction.atomic():
- # 9.1 更新新打手扩展表
- new_dashou_profile.zhuangtai = 0 # 设为接单中
- new_dashou_profile.jiedanzongliang += 1
- new_dashou_profile.jinrijiedan += 1
- new_dashou_profile.jinyuejiedan += 1
- new_dashou_profile.save()
-
- # 9.2 处理原打手(如果存在)
- old_dashou_id = order.PlayerID
- if old_dashou_id:
- try:
- old_dashou_user = User.query.get(UserUID=old_dashou_id)
- old_dashou_profile = old_dashou_user.DashouProfile
- old_dashou_profile.zhuangtai = 1 # 恢复空闲
- old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑)
- old_dashou_profile.save()
- logger.info(f"原打手 {old_dashou_id} 状态已更新")
- except (User.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新")
- except Exception as e:
- logger.error(f"更新原打手时异常: {str(e)}", exc_info=True)
-
- # 9.3 更新订单
- order.PlayerID = new_dashou_id
- order.Status = 2 # 进行中
- order.AssignedCS = current_user.UserUID # 记录处理客服ID
- order.save()
-
- return Response({'code': 0, 'msg': '更换打手成功'})
-
-
-
-
-
-class KefuRecoverOrderView(APIView):
- """
- 客服恢复订单接口(仅状态9可恢复)
- 请求:POST /yonghu/kefuhf
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单
- try:
- order = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 验证订单状态是否为9(未付款)
- if order.Status != 9:
- return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 根据是否有指定ID决定新状态
- if order.AssignedID:
- new_status = 7 # 指定中
- else:
- new_status = 1 # 下单中
-
- # 6. 更新订单
- order.Status = new_status
- order.AssignedCS = current_user.UserUID
- order.save()
-
- return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}})
-
-
-
-
-
-
-
-
-# dingdan/views.py
-class KefuPlatformRefundView(APIView):
- """
- 客服平台订单退款接口(调用微信支付)
- 请求:POST /yonghu/kefupttk
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "tuikuan_liyou": "退款理由(可选)"
- }
- 认证:JWT
- 返回:code=0 成功,code=其他 失败
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- logger.warning(f"用户类型非客服: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单并加锁
- try:
- with transaction.atomic():
- order = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id)
-
- # 4. 验证平台订单
- if order.Platform != 1:
- return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中)
- allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态
- if order.Status not in allowed_statuses:
- return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 获取微信交易号(如果存在)
- wechat_transaction_id = getattr(order, 'WechatTransactionID', None)
-
- # 7. 调用微信退款接口
- refund_result = self.call_wechat_refund(
- dingdan_id=order.OrderID,
- jine=order.Amount,
- transaction_id=wechat_transaction_id
- )
-
- if refund_result['code'] != 0:
- # 微信退款失败,直接返回错误,数据库不更新
- logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}")
- return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST)
-
- # 8. 更新订单状态为5(已退款)
- order.Status = 5
- order.AssignedCS = current_user.Phone # 记录处理客服手机号
- if tuikuan_liyou:
- order.RefundReason = tuikuan_liyou
- order.save()
-
- # 9. 更新打手数据
- jiedan_dashou_id = order.PlayerID
- if jiedan_dashou_id:
- try:
- dashou_user = User.query.get(UserUID=jiedan_dashou_id)
- dashou_profile = dashou_user.DashouProfile
- dashou_profile.tuikuanliang += 1
- dashou_profile.zhuangtai = 1 # 打手状态设为空闲
- dashou_profile.save()
- logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1")
- except (User.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
-
- # ========== 新增:商品和店铺每日统计 ==========
- 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.ProductID}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- dianpu_id = None
- if hasattr(order, 'pingtai_kuozhan'):
- dianpu_id = order.pingtai_kuozhan.ShopID
- 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())
- # 统计失败不影响主流程
- # =============================================
-
- # 10. 更新退款记录表(假设存在 RefundRecord 模型,字段需根据实际调整)
-
- #更新支出记录
- update_daily_payout(order.Amount, getattr(order, 'ClubID', None))
-
-
- try:
- refund_record, created = RefundRecord.query.get_or_create(
- OrderID=dingdan_id,
- defaults={
- 'PlayerID': jiedan_dashou_id or '',
- 'ApplicantID': '',
- 'ProcessorID': phone,
- 'Reason': tuikuan_liyou or '客服退款',
- 'ApplyStatus': 1, # 成功
- 'Amount': order.Amount,
- 'PlayerCommission': order.PlayerCommission or 0,
- 'Description': order.Description or '',
- 'Remark': '客服平台退款',
- 'Nickname': order.Nickname or '',
- }
- )
- if not created:
- refund_record.ApplyStatus = 1
- refund_record.ProcessorID = phone
- if tuikuan_liyou:
- refund_record.Reason = tuikuan_liyou
- refund_record.save()
- except Exception as e:
- logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True)
-
- # 11. 更新客服统计数据
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=order.PlayerCommission,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
- logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}")
- return Response({'code': 0, 'msg': '退款成功'})
-
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def call_wechat_refund(self, dingdan_id, jine, transaction_id=None):
- """
- 调用微信支付退款接口
- :param dingdan_id: 商户订单号(我们的订单ID)
- :param jine: 退款金额
- :param transaction_id: 微信支付订单号(优先使用)
- :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
- """
- # 获取配置
- 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]):
- missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
- logger.error(f"微信支付配置缺失: {missing}")
- return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'}
-
- # 生成退款单号
- out_refund_no = self.generate_refund_no(dingdan_id)
- total_fee = yuan_to_fen(jine)
- refund_fee = total_fee
- refund_desc = '客服退款'
-
- # 构造请求参数
- 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': refund_desc,
- }
- if transaction_id:
- params['transaction_id'] = transaction_id
- else:
- params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号
-
- # 生成签名
- sorted_keys = sorted(params.keys())
- stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
- stringSignTemp = f"{stringA}&key={key}"
- sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
- params['sign'] = sign
-
- xml_data = self.dict_to_xml(params)
- url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
-
- try:
- response = requests.post(
- url,
- data=xml_data,
- cert=(cert_path, key_path),
- timeout=10
- )
- result = xmltodict.parse(response.content)['xml']
- logger.info(f"微信退款响应: {result}")
- except Exception as e:
- logger.error(f"微信退款请求异常: {str(e)}", exc_info=True)
- return {'code': 500, 'msg': '微信退款请求异常'}
-
- if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
- return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no}
- else:
- err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
- logger.warning(f"微信退款失败: {err_msg}")
- return {'code': 400, 'msg': err_msg}
-
- 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 = ['']
- for k, v in data.items():
- xml.append(f'<{k}>{v}{k}>')
- xml.append('')
- return ''.join(xml)
-
-
-
-
-
-# dingdan/views.py
-class KefuRejectRefundView(APIView):
- """
- 客服拒绝退款接口
- 请求:POST /yonghu/kefujjtk
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "jujue_liyou": "拒绝理由(可选)"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- jujue_liyou = request.data.get('jujue_liyou', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单并加锁
- try:
- with transaction.atomic():
- order = Order.objects.select_for_update().get(OrderID=dingdan_id)
-
- # 4. 验证订单状态为退款审核中(4)
- if order.Status != 4:
- return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取必要字段
- jiedan_dashou_id = order.PlayerID
- dashou_fencheng = order.PlayerCommission
- fadan_pingtai = order.Platform
-
- # 5. 验证打手分成金额是否存在
- if dashou_fencheng is None:
- return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 更新订单状态为退款失败(6)
- order.Status = 6
- order.AssignedCS = current_user.Phone # 记录处理客服手机号
- if jujue_liyou:
- order.RefundReason = jujue_liyou
- order.save()
-
- # 7. 给打手结算(返还分成)
- if jiedan_dashou_id:
- try:
- dashou_user = User.query.get(UserUID=jiedan_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.save()
- logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}")
- except (User.DoesNotExist, AttributeError):
- logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
-
- # 8. 如果是商家发单,更新商家成交订单数量
- if fadan_pingtai == 2:
- try:
- # 获取商家订单扩展表
- shangjia_kuozhan = order.shangjia_kuozhan
- shangjia_id = shangjia_kuozhan.MerchantID
- if shangjia_id:
- shangjia_user = User.query.get(UserUID=shangjia_id)
- shangjia_profile = shangjia_user.ShopProfile
- shangjia_profile.chengjiao += 1
- shangjia_profile.save()
- logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1")
-
- # ✅ 新方法(行为驱动,更安全)
- update_shangjia_daily(
- yonghuid=shangjia_id,
- amount=Decimal(str(order.Amount)),
- action=2, # 2 = 结算
- order_id=dingdan_id,
- )
- except (ObjectDoesNotExist, User.DoesNotExist, AttributeError):
- logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新")
-
- # 9. 更新退款记录表
- try:
- refund_record = RefundRecord.query.get(OrderID=dingdan_id)
- refund_record.ApplyStatus = 2 # 2表示拒绝
- refund_record.ProcessorID = phone
- refund_record.RejectReason = jujue_liyou # 记录驳回理由(假设模型有此字段)
- refund_record.save()
- logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}")
- except RefundRecord.DoesNotExist:
- # 如果退款记录不存在,可以创建一条记录(可选)
- RefundRecord.query.create(
- OrderID=dingdan_id,
- PlayerID=jiedan_dashou_id or '',
- ApplicantID='',
- ProcessorID=phone,
- Reason=jujue_liyou or '客服拒绝退款',
- ApplyStatus=2,
- Amount=order.Amount,
- PlayerCommission=dashou_fencheng or 0,
- Description=order.Description or '',
- Remark='客服拒绝退款',
- Nickname=order.Nickname or '',
- RejectReason=jujue_liyou or '',
- )
- except Exception as e:
- logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True)
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=dashou_fencheng,
- action=2 # 2 表示成交(结算)
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
- # 10. 更新客服统计数据
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- return Response({'code': 0, 'msg': '拒绝退款成功'})
-
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-
-
-
-
-
-
-
-class KefuMerchantRefundView(APIView):
- """
- 客服商家订单退款接口(发单平台=2)
- 请求:POST /yonghu/kefusjtk
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "tuikuan_liyou": "退款理由(可选)"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 客服身份验证(同前,省略以节省篇幅,但实际必须完整)
- current_user = request.user
- # ... 验证
- if getattr(current_user, 'Phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
-
- # 查询订单,预加载商家扩展表
- try:
- order = 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 order.Platform != 2:
- return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证状态允许退款
- allowed_statuses = [1, 7, 2, 8, 4]
- if order.Status not in allowed_statuses:
- return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取必要数据
- jiedan_dashou_id = order.PlayerID
- jine = order.Amount
- if jine is None:
- return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- shangjia_kuozhan = order.shangjia_kuozhan
- shangjia_id = shangjia_kuozhan.MerchantID
- except ObjectDoesNotExist:
- return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST)
-
- with transaction.atomic():
- # 更新订单
- order.Status = 5
- order.AssignedCS = current_user.Phone
- if tuikuan_liyou:
- order.RefundReason = tuikuan_liyou
- order.save()
-
- # 更新打手
- if jiedan_dashou_id:
- try:
- dashou_user = User.query.get(UserUID=jiedan_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"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新")
-
- # 更新商家(客服额度出资的订单不退商家余额,仅释放子客服额度)
- try:
- from merchant_ops.services.wallet import is_staff_quota_order
- shangjia_user = User.query.get(UserUID=shangjia_id)
- shangjia_profile = shangjia_user.ShopProfile
- shangjia_profile.tuikuan += 1
- if not is_staff_quota_order(dingdan_id):
- shangjia_profile.yue += jine
- shangjia_profile.save()
- except (User.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新")
-
- # 更新退款记录表
- try:
- refund_record = RefundRecord.query.get(OrderID=dingdan_id)
- refund_record.ApplyStatus = 1
- refund_record.ProcessorID = phone
- refund_record.save()
- except RefundRecord.DoesNotExist:
- pass
-
- # 更新客服统计
- update_shangjia_daily(
- yonghuid=shangjia_id,
- amount=Decimal(str(order.Amount)),
- action=3, # 3 = 退款
- order_id=dingdan_id,
- )
-
-
- # 调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=order.PlayerCommission,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
-
-
- # 管理员无客服扩展表,跳过计数器更新
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- if not is_admin:
- kefu = current_user.KefuProfile
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- return Response({'code': 0, 'msg': '退款成功'})
-
-
-
-class KefuRejectSettlementView(APIView):
- """
- 客服拒绝结算接口(仅平台订单,状态2/8)
- 请求:POST /yonghu/kefujjjs
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "tkly": "拒绝结算理由(必填)"
- }
- 认证:JWT
- 返回:code=0 成功,其他失败
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- tkly = request.data.get('tkly', '').strip()
-
- if not phone or not dingdan_id or not tkly:
- return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- logger.warning(f"用户类型非客服: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单,预加载平台扩展表
- try:
- order = Order.query.select_related('pingtai_kuozhan').get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 校验订单条件:平台发单(1)且状态为2或8
- if order.Platform != 1 or order.Status not in [2, 8]:
- return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST)
-
- jiedan_dashou_id = order.PlayerID
- if not jiedan_dashou_id:
- return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 使用事务更新
- with transaction.atomic():
- # 5.1 更新订单主表
- order.Status = 5 # 已退款
- order.RefundReason = tkly # 拒绝理由
- order.AssignedCS = phone # 记录客服手机号
- order.save()
-
- # 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi)
- # 注意:需要确保 PlatformOrderExt 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0
- if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan:
- pingtai_ext = order.pingtai_kuozhan
- pingtai_ext.RejectSettlementFlag = 1 # 1表示拒绝结算
- pingtai_ext.save()
- else:
- # 理论上应该存在,但若不存在则记录日志
- logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算")
-
- # 5.3 更新打手扩展表
- try:
- dashou_user = User.query.get(UserUID=jiedan_dashou_id)
- dashou_profile = dashou_user.DashouProfile
- dashou_profile.zhuangtai = 1 # 打手状态设为空闲
- dashou_profile.tuikuanliang += 1 # 退款订单总量+1
- dashou_profile.save()
- logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲")
- except (User.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
- except Exception as e:
- logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True)
-
- # 5.4 更新客服扩展表统计数据
-
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=order.PlayerCommission,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
-
-
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- return Response({'code': 0, 'msg': '拒绝结算成功'})
-
-
-
-class KefuTransferHallView(APIView):
- """
- 客服恢复大厅(转移大厅)接口
- 请求:POST /yonghu/kefuzydt
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证(同前,略,但必须完整实现)
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- if current_user.UserType not in ('kefu', 'admin'):
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单
- try:
- order = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 校验订单状态(允许状态:2,8,4)
- if order.Status not in [2, 8, 4]:
- return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST)
-
-
- if order.PlayerID:
- UserDashou.query.filter(user__UserUID=order.PlayerID).update(zhuangtai=1)
- else:
- pass
-
- # 5. 根据是否有指定ID决定新状态
- if order.AssignedID:
- new_status = 7 # 指定中
- else:
- new_status = 1 # 下单中
-
- # 6. 使用事务更新
- with transaction.atomic():
- # 清空接单打手ID,更新状态,记录客服
- order.PlayerID = None
- order.Status = new_status
- order.AssignedCS = phone
- order.save()
-
- return Response({'code': 0, 'msg': '转移大厅成功'})
-
-
-class KefuCancelDesignationView(APIView):
- """
- 客服取消指定接口(仅状态7)
- 请求:POST /yonghu/kefuqxzd
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证(同前,略,但必须完整实现)
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- if current_user.UserType not in ('kefu', 'admin'):
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单
- try:
- order = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 校验订单状态为7(指定中)
-
- if order.Status != 7:
- print(999)
- return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 使用事务更新
- with transaction.atomic():
- # 状态改为1(下单中),清空指定ID,记录客服
- order.Status = 1
- order.AssignedID = None
- order.AssignedCS = phone
- order.save()
-
- return Response({'code': 0, 'msg': '取消指定成功'})
-
-
-
-
-
-
-
-
-class KefuGetDashouListView(APIView):
- """
- 客服获取打手列表接口(仅打手类型)
- 请求:POST /yonghu/kefuhqdslb
- 参数:{
- "phone": "客服手机号",
- "keyword": "搜索关键词(可选,搜索用户ID或昵称)",
- "page": "页码(从1开始)",
- "page_size": "每页数量"
- }
- 认证:JWT
- 返回:{
- "code": 0,
- "data": {
- "list": [打手对象],
- "has_more": true/false
- }
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('zhanghao', '').strip()
- keyword = request.data.get('keyword', '').strip()
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- except ValueError:
- return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- if not phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- logger.warning(f"用户类型非客服: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 构建查询条件(只查询打手)
- # 通过 UserDashou 扩展表关联 User
- queryset = UserDashou.query.select_related('user').all()
-
- if keyword:
- # 搜索用户ID或打手昵称
- queryset = queryset.filter(
- Q(user__UserUID__icontains=keyword) |
- Q(nicheng__icontains=keyword)
- )
-
- # 4. 计算总数并分页
- total = queryset.count()
- offset = (page - 1) * page_size
- dashou_list = queryset.order_by('-CreateTime')[offset:offset + page_size]
-
- # 5. 构建返回数据(只取需要的字段)
- result = []
- for dashou in dashou_list:
- user = dashou.user
- result.append({
- 'yonghuid': user.UserUID,
- 'avatar': user.Avatar or '',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'nicheng': dashou.nicheng or '',
- 'zhuangtai': dashou.zhuangtai,
- })
-
- # 6. 判断是否有更多数据
- has_more = (offset + len(dashou_list)) < total
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': result,
- 'has_more': has_more
- }
- })
-
-
-
-
-
-
-
-class KefuGetDashouDetailView(APIView):
- """
- 客服获取打手详情接口
- 请求:POST /yonghu/kefuhqdsxq
- 参数:{
- "phone": "客服手机号",
- "uid": "打手ID (yonghuid)"
- }
- 认证:JWT
- 返回:code=0 + 打手信息 + 会员列表
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- uid = request.data.get('uid', '').strip()
-
- if not phone or not uid:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- logger.warning(f"用户类型非客服: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询打手主表和扩展表
- try:
- # 使用 select_related 预加载 dashou_profile,减少数据库查询
- user_main = User.query.select_related('DashouProfile').get(
- UserUID=uid
- )
- except User.DoesNotExist:
- return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 验证用户类型是否为打手(可选,但建议验证)
- # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
- try:
- dashou = user_main.DashouProfile
- except ObjectDoesNotExist:
- # 用户存在但未注册打手身份,返回基本信息
- user_info = {
- 'yonghuid': user_main.UserUID,
- 'avatar': user_main.Avatar or '',
- 'zaixianzhuangtai': 0,
- 'zhanghaozhuangtai': 0,
- 'zhuangtai': 0,
- 'nicheng': '',
- 'chenghao': '',
- 'dianhua': '',
- 'wechat': '',
- 'phone': user_main.Phone or '',
- 'CreateTime': user_main.UserCreateTime,
- 'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
- 'ip': user_main.IP or '',
- 'jiedanzongliang': 0,
- 'chengjiaozongliang': 0,
- 'tuikuanliang': 0,
- 'jinrijiedan': 0,
- 'yue': 0.00,
- 'zonge': 0.00,
- 'jifen': 0,
- 'yajin': 0.00,
- 'jinrishouyi': 0.00,
- 'jinyueshouyi': 0.00,
- 'yaoqingren': '',
- 'jieshao': '',
- }
- member_list = []
- return Response({
- 'code': 0,
- 'data': {
- 'user_info': user_info,
- 'member_list': member_list
- }
- })
-
- # 4. 构建打手信息
- user_info = {
- 'yonghuid': user_main.UserUID,
- 'avatar': user_main.Avatar or '',
- 'zaixianzhuangtai': dashou.zaixianzhuangtai,
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
- 'zhuangtai': dashou.zhuangtai,
- 'nicheng': dashou.nicheng or '',
- 'chenghao': dashou.chenghao or '',
- 'dianhua': dashou.dianhua or '',
- 'wechat': dashou.wechat or '',
- 'phone': user_main.Phone or '',
- 'CreateTime': user_main.UserCreateTime,
- 'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
- 'ip': user_main.IP or '',
- 'jiedanzongliang': dashou.jiedanzongliang,
- 'chengjiaozongliang': dashou.chengjiaozongliang,
- 'tuikuanliang': dashou.tuikuanliang,
- 'jinrijiedan': dashou.jinrijiedan,
- 'yue': float(dashou.yue) if dashou.yue else 0.00,
- 'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
- 'jifen': dashou.jifen,
- 'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
- 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
- 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
- 'yaoqingren': dashou.yaoqingren or '',
- 'jieshao': dashou.jieshao or '',
- }
-
- # 5. 查询打手会员信息(仅未过期)
- member_records = Huiyuangoumai.query.filter(
- yonghu_id=uid
- ).order_by('-CreateTime')
-
- member_list = []
- for record in member_records:
- # 调用模型方法检查是否过期
- if hasattr(record, 'jiance_shifou_daoqi'):
- is_expired = record.jiance_shifou_daoqi()
- else:
- # 如果方法不存在,简单判断
- is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
-
- if not is_expired:
- # 获取会员详情
- try:
- huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id)
- huiyuan_name = huiyuan.jieshao
- except Huiyuan.DoesNotExist:
- huiyuan_name = '未知会员'
-
- member_list.append({
- 'id': record.id,
- 'huiyuan_name': huiyuan_name,
- 'daoqi_time': record.daoqi_time,
- 'is_active': record.huiyuan_zhuangtai == 1,
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'user_info': user_info,
- 'member_list': member_list
- }
- })
-
-
-
-
-class KefuUpdateDashouView(APIView):
- permission_classes = [IsAuthenticated]
- """
- 客服修改打手信息接口
- 支持操作:
- - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
- - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
- - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
- - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
- - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
- - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
- - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
- """
- def post(self, request):
- # 1. 获取前端参数
- username = request.data.get('username', '').strip()
- dashou_id = request.data.get('dashou_id', '').strip()
- caozuo = request.data.get('caozuo', '').strip()
- value = request.data.get('value') # 金额、积分、天数等
- huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID(加/减会员时使用)
-
- if not username or not dashou_id or not caozuo:
- return Response({'code': 400, 'msg': '参数不完整'})
-
- # 2. 权限校验(统一方法)
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions # 直接返回错误响应
-
- # 3. 查询打手是否存在
- try:
- dashou_user = User.query.get(UserUID=dashou_id)
- dashou_profile = dashou_user.DashouProfile
- except User.DoesNotExist:
- return Response({'code': 404, 'msg': '打手不存在'})
- except UserDashou.DoesNotExist:
- return Response({'code': 404, 'msg': '打手扩展信息缺失'})
-
- # 4. 根据操作类型执行(事务内)
- with transaction.atomic():
- try:
- # 记录修改前的值(用于日志)
- before = {
- 'yue': dashou_profile.yue,
- 'yajin': dashou_profile.yajin,
- 'jifen': dashou_profile.jifen,
- 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'zhuangtai': dashou_profile.zhuangtai,
- }
-
- # ---------- 余额操作 ----------
- if caozuo == 'jian_yue':
- if '001aa' not in permissions:
- return Response({'code': 403, 'msg': '无权限减打手余额'})
- amount = Decimal(str(value))
- if dashou_profile.yue < amount:
- return Response({'code': 400, 'msg': '余额不足'})
- dashou_profile.yue -= amount
- dashou_profile.save()
- after_yue = dashou_profile.yue
- # 记录日志
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2, # 2=打手
- xiugaitijiao=after_yue,
- xiugaitijiaoq=before['yue'],
- )
- return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}})
-
- elif caozuo == 'jia_yue':
- if '001bb' not in permissions:
- return Response({'code': 403, 'msg': '无权限加打手余额'})
- from jituan.services.group_finance_gate import deny_group_finance_exec
- deny = deny_group_finance_exec(request.user, request)
- if deny:
- return deny
- amount = Decimal(str(value))
- dashou_profile.yue += amount
- dashou_profile.save()
- after_yue = dashou_profile.yue
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2,
- xiugaitijiao=after_yue,
- xiugaitijiaoq=before['yue'],
- )
- return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}})
-
- # ---------- 押金操作 ----------
- elif caozuo == 'jian_yajin':
- if '001aa' not in permissions:
- return Response({'code': 403, 'msg': '无权限减打手押金'})
- amount = Decimal(str(value))
- if dashou_profile.yajin < amount:
- return Response({'code': 400, 'msg': '押金不足'})
- dashou_profile.yajin -= amount
- dashou_profile.save()
- after_yajin = dashou_profile.yajin
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2,
- yajin=after_yajin,
- yyajin=before['yajin'],
- )
- return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}})
-
- elif caozuo == 'jia_yajin':
- if '001bb' not in permissions:
- return Response({'code': 403, 'msg': '无权限加打手押金'})
- from jituan.services.group_finance_gate import deny_group_finance_exec
- deny = deny_group_finance_exec(request.user, request)
- if deny:
- return deny
- amount = Decimal(str(value))
- dashou_profile.yajin += amount
- dashou_profile.save()
- after_yajin = dashou_profile.yajin
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2,
- yajin=after_yajin,
- yyajin=before['yajin'],
- )
- return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}})
-
- # ---------- 积分操作 ----------
- elif caozuo == 'jian_jifen':
- if '001cc' not in permissions:
- return Response({'code': 403, 'msg': '无权限减打手积分'})
- jifen = int(value)
- if dashou_profile.jifen < jifen:
- return Response({'code': 400, 'msg': '积分不足'})
- dashou_profile.jifen -= jifen
- dashou_profile.save()
- after_jifen = dashou_profile.jifen
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2,
- jifen=after_jifen,
- yjifen=before['jifen'],
- )
- return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}})
-
- elif caozuo == 'jia_jifen':
- if '001dd' not in permissions:
- return Response({'code': 403, 'msg': '无权限加打手积分'})
- jifen = int(value)
- dashou_profile.jifen += jifen
- dashou_profile.save()
- after_jifen = dashou_profile.jifen
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2,
- jifen=after_jifen,
- yjifen=before['jifen'],
- )
- return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}})
-
- # ---------- 状态修改 ----------
- elif caozuo == 'gai_zhanghaozhuangtai':
- if '001ee' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改打手账号状态'})
- new_status = int(value)
- if new_status not in [0, 1]:
- return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'})
- dashou_profile.zhanghaozhuangtai = new_status
- dashou_profile.save()
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2,
- qitashuoming=f'修改账号状态为{new_status}',
- )
- return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}})
-
- elif caozuo == 'gai_zaixianzhuangtai':
- if '001ee' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改打手在线状态'})
- new_status = int(value)
- if new_status not in [0, 1]:
- return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'})
- dashou_profile.zaixianzhuangtai = new_status
- dashou_profile.save()
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2,
- qitashuoming=f'修改在线状态为{new_status}',
- )
- return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}})
-
- elif caozuo == 'gai_zhuangtai':
- if '001ee' not in permissions:
- return Response({'code': 403, 'msg': '无权限修改打手接单状态'})
- new_status = int(value)
- if new_status not in [0, 1]:
- return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'})
- dashou_profile.zhuangtai = new_status
- dashou_profile.save()
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2,
- qitashuoming=f'修改接单状态为{new_status}',
- )
- return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
-
- # ---------- 会员操作 ----------
- elif caozuo == 'jia_huiyuan':
- if '001ff' not in permissions:
- return Response({'code': 403, 'msg': '无权限给打手加会员'})
- if not huiyuan_id:
- return Response({'code': 400, 'msg': '缺少会员ID'})
- try:
- huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
- except Huiyuan.DoesNotExist:
- return Response({'code': 404, 'msg': '会员类型不存在'})
- from jituan.services.club_user import get_user_club_id
- from jituan.services.member_recharge import extend_member_entitlement_admin, get_club_huiyuan_row
- player_club = get_user_club_id(dashou_user)
- row = get_club_huiyuan_row(player_club, huiyuan_id, require_enabled=False)
- days = int(value) if value else int(row.formal_days or 30) if row else 30
- record, err = extend_member_entitlement_admin(
- dashou_id, huiyuan_id, days, player_club,
- )
- if err:
- return Response({'code': 400, 'msg': err})
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2,
- huiyuants=days,
- huiyuan_id=huiyuan_id,
- qitashuoming=f'加正式会员 {huiyuan_id},增加{days}天(无支付单、无分红)',
- )
- return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
-
- elif caozuo == 'jian_huiyuan':
- if '001gg' not in permissions:
- return Response({'code': 403, 'msg': '无权限给打手减会员'})
- if not huiyuan_id:
- return Response({'code': 400, 'msg': '缺少会员ID'})
- try:
- record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
- except Huiyuangoumai.DoesNotExist:
- return Response({'code': 404, 'msg': '该打手未购买此会员'})
- # 删除会员记录(或设置为过期,根据业务要求)
- record.delete()
- Xiugaijilu.query.create(
- yonghuid=dashou_id,
- xiugaiid=kefu.user.Phone,
- leixing=2,
- huiyuan_id=huiyuan_id,
- qitashuoming=f'移除会员 {huiyuan_id}',
- )
- return Response({'code': 0, 'msg': '减会员成功'})
-
- else:
- return Response({'code': 400, 'msg': '未知操作类型'})
-
- except Exception as e:
- logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-
-
-
-
-
-
-class KefuPunishmentListView(APIView):
- """
- 客服获取处罚记录列表接口
- 请求:POST /yonghu/kefu_cfgl
- 参数:{
- "phone": "客服手机号",
- "status": [0,3] 或 [1,2], # 状态数组
- "dingdan_id": "订单ID(可选)",
- "dashouid": "打手ID(可选)",
- "page": 1,
- "page_size": 20
- }
- 认证:JWT
- 返回:{
- "code": 0,
- "data": {
- "list": [处罚记录],
- "total": 总数,
- "stats": {
- "zongshu": 总记录数,
- "daichuli": 待处理数,
- "yichuli": 已处理数
- }
- }
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- @staticmethod
- def _parse_status_list(raw):
- """兼容 status=[0,3]、\"0,3\"、单数字;空/all 表示不过滤。"""
- if raw is None:
- return None
- if isinstance(raw, str):
- s = raw.strip().lower()
- if not s or s in ('all', 'null', 'none'):
- return None
- if s.startswith('['):
- import json
- try:
- raw = json.loads(s)
- except (TypeError, ValueError):
- return None
- else:
- raw = [x.strip() for x in s.split(',') if x.strip()]
- if isinstance(raw, (int, float)):
- return [int(raw)]
- if isinstance(raw, list):
- out = []
- for x in raw:
- try:
- out.append(int(x))
- except (TypeError, ValueError):
- continue
- return out or None
- return None
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- status_list = self._parse_status_list(request.data.get('status'))
- dingdan_id = request.data.get('dingdan_id', '').strip()
- dashouid = request.data.get('dashouid', '').strip()
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- except ValueError:
- return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- if not phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- current_user = request.user
- # 1. 获取前端参数
- username = request.data.get('phone', '').strip()
- if not username:
- return Response({'code': 400, 'msg': '缺少username'})
-
- # 2. 公共权限校验
- kefu, permissions = verify_kefu_permission(request, username)
- if kefu is None:
- return permissions
-
- # 3. 检查平台订单管理权限(002ab)
- if '005aa' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
-
- # 3. 获取统计信息(不分页)
- from jituan.services.club_penalty import filter_penalty_record_qs
- from jituan.services.club_user_access import list_response_meta
-
- base_qs = filter_penalty_record_qs(PenaltyRecord.query.all(), request)
- stats = {
- 'zongshu': base_qs.count(),
- 'daichuli': base_qs.filter(ApplyStatus__in=[0, 3]).count(),
- 'yichuli': base_qs.filter(ApplyStatus__in=[1, 2]).count(),
- }
-
- queryset = base_qs
-
- if status_list and isinstance(status_list, list):
- queryset = queryset.filter(ApplyStatus__in=status_list)
-
- if dingdan_id:
- queryset = queryset.filter(OrderID=dingdan_id)
-
- if dashouid:
- queryset = queryset.filter(PlayerID=dashouid)
-
- # 5. 分页
- page_obj = queryset.order_by('-CreateTime').paginate(page=page, per_page=page_size)
- total = page_obj.total
- records = page_obj.items
-
- # 6. 构建返回列表
- list_data = []
- for item in records:
- ct = item.CreateTime
- ut = item.UpdateTime
- list_data.append({
- 'dingdan_id': item.OrderID,
- 'dashouid': item.PlayerID,
- 'qingqiuid': item.ApplicantID,
- 'cfliyou': item.PenaltyReason,
- 'sqzhuangtai': item.ApplyStatus,
- 'jifen': item.DeductedPoints,
- 'CreateTime': ct,
- 'UpdateTime': ut,
- 'create_time': ct.isoformat() if ct else '',
- 'update_time': ut.isoformat() if ut else '',
- })
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': list_data,
- 'total': total,
- 'stats': stats,
- **list_response_meta(request),
- }
- })
-
-
-
-
-
-
-class KefuGetOrderDetailView(APIView):
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 获取当前用户
- current_user = request.user
-
- # 验证手机号
- if getattr(current_user, 'Phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 验证用户类型
- if current_user.UserType not in ('kefu', 'admin'):
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 客服扩展表
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 获取客服扩展表异常 user={getattr(current_user, 'yonghuid', '?')}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- # 3. 查询订单
- try:
- dingdan_obj = Order.query.select_related(
- 'pingtai_kuozhan', 'shangjia_kuozhan'
- ).get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=404)
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 订单查询异常 dingdan_id={dingdan_id}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- from jituan.services.club_user_access import forbid_if_order_out_of_scope
- deny = forbid_if_order_out_of_scope(request, dingdan_obj)
- if deny:
- return deny
-
- # 4. 构建基础数据
- try:
- response_data = {
- 'dingdan_id': dingdan_obj.OrderID or '',
- 'zhuangtai': dingdan_obj.Status or 0,
- 'fadanpingtai': dingdan_obj.Platform or 0,
- 'jiage': float(dingdan_obj.Amount) if dingdan_obj.Amount else 0.00,
- 'dashou_fencheng': float(dingdan_obj.PlayerCommission) if dingdan_obj.PlayerCommission else 0.00,
- 'jiedan_dashou_id': dingdan_obj.PlayerID or '',
- 'dashou_liuyan': dingdan_obj.PlayerRemark or '',
- 'zhiding_id': dingdan_obj.AssignedID or '',
- 'shangpin_id': dingdan_obj.ProductID or '',
- 'leixing': dingdan_obj.ProductTypeID or 0,
- 'tupian_url': dingdan_obj.ImageURL or '',
- 'jieshao': dingdan_obj.Description or '',
- 'beizhu': dingdan_obj.Remark or '',
- 'tkly': dingdan_obj.RefundReason or '',
- 'nicheng': dingdan_obj.Nickname or '',
- 'clkf': dingdan_obj.AssignedCS or '',
- 'chuangjianshijian': dingdan_obj.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.CreateTime else '',
- 'genggaishijian': dingdan_obj.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.UpdateTime else '',
- }
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 构建基础数据异常 dingdan_id={dingdan_id}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- # 5. 扩展表数据
- try:
- fadan_pingtai = dingdan_obj.Platform
- if fadan_pingtai == 1:
- if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan:
- pingtai = dingdan_obj.pingtai_kuozhan
- response_data['pingtai_kuozhan'] = {
- 'laoban_id': pingtai.BossID or '',
- 'laoban_pingjia': pingtai.BossReview or '',
- }
- else:
- response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''}
- elif fadan_pingtai == 2:
- if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan:
- shangjia = dingdan_obj.shangjia_kuozhan
- response_data['shangjia_kuozhan'] = {
- 'shangjia_id': shangjia.MerchantID or '',
- 'sjnicheng': shangjia.MerchantNickname or '',
- 'shangjia_pingjia': shangjia.MerchantReview or '',
- 'cfliyou': shangjia.PenaltyReason or '',
- 'sqzhuangtai': shangjia.PenaltyApplyStatus if shangjia.PenaltyApplyStatus is not None else 0,
- 'bhliyou': shangjia.RejectReason or '',
- }
- else:
- response_data['shangjia_kuozhan'] = {
- 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '',
- 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': ''
- }
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 扩展数据异常 dingdan_id={dingdan_id}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- # 6. 打手图片
- try:
- dashou_images = PlayerDeliveryImage.query.filter(OrderID=dingdan_id).values_list('ImageURL', flat=True)
- response_data['dashou_images'] = [img for img in dashou_images if img]
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}")
- response_data['dashou_images'] = []
-
- # 7. 退款理由
- try:
- tuikuan_record = RefundRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
- response_data['tuikuan_liyou'] = tuikuan_record.Reason if tuikuan_record and tuikuan_record.Reason else ''
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 退款理由查询异常 dingdan_id={dingdan_id}")
- response_data['tuikuan_liyou'] = ''
-
- # 8. 指定打手响应
- ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}
- hf = getattr(dingdan_obj, 'AssignedResponse', None)
- response_data['zhiding_hf'] = hf
- response_data['zhiding_hf_text'] = ZHIDING_HF_TEXT.get(hf, '') if hf else ''
- response_data['zhiding_hf_sj'] = (
- dingdan_obj.AssignedResponseAt.strftime('%Y-%m-%d %H:%M:%S')
- if getattr(dingdan_obj, 'AssignedResponseAt', None) else ''
- )
- response_data['zhiding_dhj_sm'] = getattr(dingdan_obj, 'AssignedLaterNote', '') or ''
-
- # 9. 关联罚款(打手)
- from utils.penalty_status import PENALTY_STATUS_LABELS as PENALTY_STATUS
- try:
- from orders.models import Penalty
- dashou_id = dingdan_obj.PlayerID or dingdan_obj.AssignedID or ''
- fakuan_qs = Penalty.query.filter(RelatedOrderID=dingdan_id)
- if dashou_id:
- fakuan_qs = fakuan_qs.filter(PenalizedUserID=dashou_id)
- fakuan_list = []
- for p in fakuan_qs.order_by('-CreateTime')[:5]:
- fakuan_list.append({
- 'amount': str(p.FineAmount or '0.00'),
- 'status': p.Status,
- 'status_text': PENALTY_STATUS.get(p.Status, ''),
- 'reason': p.Reason or '',
- 'create_time': p.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if p.CreateTime else '',
- })
- response_data['fakuan_list'] = fakuan_list
- latest = fakuan_list[0] if fakuan_list else None
- response_data['fakuan_info'] = latest
- response_data['is_fadaned'] = Penalty.query.filter(
- RelatedOrderID=dingdan_id, ApplicantID=getattr(current_user, 'UserUID', '')
- ).exists()
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 罚款查询异常 dingdan_id={dingdan_id}")
- response_data['fakuan_list'] = []
- response_data['fakuan_info'] = None
- response_data['is_fadaned'] = False
-
- return Response({'code': 0, 'data': response_data})
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-class KefuPunishmentDetailView(APIView):
- """
- 客服获取处罚详情接口
- 请求:POST /yonghu/kefu_cf_detail
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证:JWT
- 返回:code=0 + 处罚记录详情(含图片)
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- logger.warning(f"用户类型非客服: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 查询处罚记录(按创建时间倒序取最新一条)
- try:
- record = PenaltyRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
- if not record:
- return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"查询处罚记录失败: {e}")
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # 查询商家证据图片
- zhengju_images = []
- shensu_images = []
- try:
- # 商家证据图片:上传者为请求人 (ApplicantID)
- zhengju_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.ApplicantID)
- zhengju_images = [item.ImageURL for item in zhengju_qs if item.ImageURL]
-
- # 打手申诉图片:上传者为打手 (PlayerID)
- shensu_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.PlayerID)
- shensu_images = [item.ImageURL for item in shensu_qs if item.ImageURL]
- except Exception as e:
- logger.error(f"查询图片失败: {e}")
-
- # 构建返回数据
- data = {
- 'dingdan_id': record.OrderID,
- 'dashouid': record.PlayerID,
- 'qingqiuid': record.ApplicantID,
- 'chuliid': record.ProcessorID,
- 'cfliyou': record.PenaltyReason,
- 'sqzhuangtai': record.ApplyStatus,
- 'bhliyou': record.RejectReason,
- 'jifen': record.DeductedPoints,
- 'ssliyou': record.AppealReason or '',
- 'zhengju_tupian': zhengju_images,
- 'shensu_tupian': shensu_images,
- 'CreateTime': record.CreateTime,
- 'UpdateTime': record.UpdateTime,
- }
-
- return Response({'code': 0, 'data': data})
-
-
-
-
-class KefuPunishmentActionView(APIView):
- """
- 客服同意/驳回处罚接口
- 请求:POST /yonghu/kefu_cf_action
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "action": 1(同意) / 2(驳回),
- "reject_reason": "驳回理由(驳回时必填)"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- action = request.data.get('action')
- reject_reason = request.data.get('reject_reason', '').strip()
-
- if not phone or not dingdan_id or action not in [1, 2]:
- return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- if action == 2 and not reject_reason:
- return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 客服身份验证(完整验证,此处略)
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- if current_user.UserType not in ('kefu', 'admin'):
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 查询待处理的处罚记录(状态0或3)
- try:
- record = PenaltyRecord.query.filter(
- OrderID=dingdan_id,
- ApplyStatus__in=[0, 3]
- ).order_by('-CreateTime').first()
- if not record:
- exists = PenaltyRecord.query.filter(OrderID=dingdan_id).exists()
- if exists:
- return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST)
- else:
- return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"查询处罚记录失败: {e}")
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- with transaction.atomic():
- # 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回
- duplicate_records = PenaltyRecord.query.filter(
- OrderID=dingdan_id,
- ApplyStatus__in=[0, 3]
- ).exclude(id=record.id)
- for dup in duplicate_records:
- dup.ApplyStatus = 2
- dup.ProcessorID = phone
- dup.RejectReason = "系统自动驳回:存在重复处罚记录"
- dup.save()
- logger.info(f"自动驳回重复处罚记录 ID={dup.id}")
-
- if action == 1: # 同意
- # 可选验证打手存在
- if record.PlayerID:
- try:
- dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
- except UserDashou.DoesNotExist:
- return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST)
-
- record.ApplyStatus = 1
- record.ProcessorID = phone
- record.save()
-
- # 更新商家订单扩展表
- try:
- dingdan = Order.query.get(OrderID=dingdan_id)
- if hasattr(dingdan, 'shangjia_kuozhan'):
- dingdan.shangjia_kuozhan.PenaltyApplyStatus = 1
- dingdan.shangjia_kuozhan.save()
- except Exception as e:
- logger.warning(f"更新商家扩展表失败: {e}")
-
- elif action == 2: # 驳回
- # 返还积分
- if record.DeductedPoints > 0 and record.PlayerID:
- try:
- dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
- if dashou.jifen <= 1000: # 安全限制
- dashou.jifen += record.DeductedPoints
- dashou.save()
- logger.info(f"驳回处罚,打手{record.PlayerID}返还积分{record.DeductedPoints}")
- else:
- logger.warning(f"打手积分异常({dashou.jifen}),不再返还")
- except UserDashou.DoesNotExist:
- logger.warning(f"打手{record.PlayerID}不存在,无法返还积分")
-
- record.ApplyStatus = 2
- record.ProcessorID = phone
- record.RejectReason = reject_reason
- record.save()
-
- try:
- dingdan = Order.query.get(OrderID=dingdan_id)
- if hasattr(dingdan, 'shangjia_kuozhan'):
- dingdan.shangjia_kuozhan.PenaltyApplyStatus = 2
- dingdan.shangjia_kuozhan.RejectReason = reject_reason
- dingdan.shangjia_kuozhan.save()
- except Exception as e:
- logger.warning(f"更新商家扩展表失败: {e}")
-
- return Response({'code': 0, 'msg': '处理成功'})
-
-
-
-
-
-
-
-
-class KefuForceCompleteView(APIView):
- """
- 客服强制结单接口(仅本地订单)
- 请求:POST /yonghu/kefuqiangzhijiedan
- 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"}
- 认证:JWT,仅限状态正常的客服
- 逻辑:
- 1. 验证客服身份及状态
- 2. 查询订单并加锁(事务内)
- 3. 校验订单状态是否为8(结算中)
- 4. 校验订单必须有接单打手且打手分成金额有效
- 5. 给打手增加余额、成交统计、今日/本月收益
- 6. 如果是商家发单,更新商家成交订单数量
- 7. 更新订单状态为3(已完成)
- 8. 更新客服处理统计
- 9. 返回成功
- """
- permission_classes = [permissions.IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '参数缺失'})
-
- # 1. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'})
-
- if current_user.UserType not in ('kefu', 'admin'):
- return Response({'code': 401, 'msg': '认证失败'})
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'})
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '客服状态异常'})
-
- # 2. 订单处理(事务内)
- try:
- with transaction.atomic():
- order = Order.objects.select_for_update().get(OrderID=dingdan_id)
-
- # 3. 校验订单状态
- if order.Status != 8:
- return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})
-
- # 4. 校验必要字段
- dashou_id = order.PlayerID
- dashou_fencheng = order.PlayerCommission
- if not dashou_id:
- return Response({'code': 400, 'msg': '订单无接单打手'})
- if not dashou_fencheng or dashou_fencheng <= 0:
- return Response({'code': 400, 'msg': '打手分成金额无效'})
-
- # 5. 给打手结算
- try:
- dashou_user = User.query.get(UserUID=dashou_id)
- dashou_profile = dashou_user.DashouProfile
- # 原子更新(避免并发重复加)
- dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
- dashou_profile.yue = F('yue') + dashou_fencheng
- dashou_profile.zonge = F('zonge') + dashou_fencheng
- dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng
- dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng
- dashou_profile.save(update_fields=[
- 'chengjiaozongliang', 'yue', 'zonge',
- 'jinrishouyi', 'jinyueshouyi'
- ])
- except (User.DoesNotExist, AttributeError):
- return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'})
-
- # 6. 如果是商家发单,更新商家成交订单数量
- if order.Platform == 2:
- try:
- # 通过 MerchantOrderExt 获取商家ID
- shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan'
- if shangjia_ext and shangjia_ext.shangjia_id:
- shangjia_user = User.query.get(UserUID=shangjia_ext.shangjia_id)
- shangjia_profile = shangjia_user.ShopProfile
- shangjia_profile.chengjiao = F('chengjiao') + 1
- shangjia_profile.save(update_fields=['chengjiao'])
-
- # (行为驱动,更安全)
- update_shangjia_daily(
- yonghuid=shangjia_ext.shangjia_id,
- amount=Decimal(str(order.Amount)),
- action=2, # 2 = 结算
- order_id=dingdan_id,
- )
-
- except ObjectDoesNotExist:
- # 没有商家扩展记录则忽略
- pass
-
- # 7. 更新订单状态
- order.Status = 3
- order.AssignedCS = phone
- order.save(update_fields=['Status', 'AssignedCS'])
-
-
- if order.Platform == 1:
-
- # ========== 新增:商品和店铺每日统计 ==========
- 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, # 2 = 结单
- dingdan_jiage=order.Amount,
- pingtai_shouyi=pingtai_shouyi,
- dianpu_zongshouyi=dianpu_shouyi
- )
- logger.info(f"商品统计更新完成,商品ID: {order.ProductID}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- dianpu_id = None
- if hasattr(order, 'pingtai_kuozhan'):
- dianpu_id = order.pingtai_kuozhan.ShopID
- if dianpu_id:
- dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
- update_dianpu_daily_stat(
- dianpu_id=dianpu_id,
- caozuo_leixing=2, # 2 = 结单
- 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())
- # 统计失败不影响主流程
- # =============================================
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=dashou_fencheng,
- action=2 # 2 表示成交(结算)
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
- # 商家订单管事分红
- if order.Platform == 2:
- settle_shangjia_order_guanshi_fenhong(order, dashou_id)
-
- # 8. 更新客服统计
- kefu.jinrichuli = F('jinrichuli') + 1
- kefu.jinyuechuli = F('jinyuechuli') + 1
- kefu.zongchuli = F('zongchuli') + 1
- kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli'])
-
- return Response({'code': 0, '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': '服务器内部错误'})
-
-
-
-
-
-
-kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list')
-if not kefu_txsh_list_logger.handlers:
- _console_handler = logging.StreamHandler(sys.stderr)
- _console_handler.setFormatter(logging.Formatter(
- '[%(asctime)s] %(levelname)s %(name)s: %(message)s'
- ))
- kefu_txsh_list_logger.addHandler(_console_handler)
- kefu_txsh_list_logger.setLevel(logging.DEBUG)
- kefu_txsh_list_logger.propagate = False
-
-_TIXIAN_LIST_BASE_FIELDS = (
- 'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing',
- 'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi',
- 'shenheid', 'bhliyou', 'CreateTime', 'UpdateTime',
-)
-_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao')
-
-
-class KefuWithdrawListView(APIView):
- """
- 客服获取提现审核列表接口
- 请求:POST /yonghu/kefu_txsh_list
- 参数:{
- "phone": "客服手机号",
- "page": 1,
- "page_size": 20,
- "status": 1(待审核)/2(已处理),
- "type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选
- "payment": 1(微信)/2(支付宝) 可选,
- "search_uid": "用户ID搜索" 可选,
- "date": "YYYY-MM-DD" 可选,
- "min_amount": 0, # 最小提现金额(元)可选
- "max_amount": 999999, # 最大提现金额(元)可选
- "dakuan_mode": 1 # 打款方式:1手动打款 2自动打款,可选
- }
- 认证:JWT
- 返回:{
- "code": 0,
- "data": {
- "list": [提现记录],
- "total": 总数,
- "stats": {
- "total": 总记录数,
- "awaiting": 待审核数,
- "processed": 已处理数,
- "total_amount": "总金额(元)"
- }
- }
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def _safe_request_params(self, request):
- try:
- return dict(request.data)
- except Exception:
- return {'raw': str(request.data)}
-
- def _shenhe_jilu_field_available(self):
- """探测 shenhe_jilu_id 字段是否已在数据库中就绪"""
- try:
- Tixianjilu.query.values_list('shenhe_jilu_id', flat=True).first()
- return True
- except DatabaseError as e:
- kefu_txsh_list_logger.error(
- 'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e,
- exc_info=True,
- )
- return False
-
- def _list_queryset(self, q, shenhe_field_ok, request):
- """字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500"""
- fields = _TIXIAN_LIST_BASE_FIELDS + (
- _TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else ()
- )
- from jituan.services.tixian_club import filter_tixianjilu_by_request
- qs = Tixianjilu.query.filter(q).only(*fields)
- return filter_tixianjilu_by_request(qs, request)
-
- def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok):
- """无关联审核或审核表未标记自动 → 默认手动(1)"""
- if not shenhe_field_ok:
- return 1
- sid = getattr(item, 'shenhe_jilu_id', None)
- if not sid:
- return 1
- if audit_mode_map.get(sid) == 2:
- return 2
- return 1
-
- def post(self, request):
- from jituan.services.group_finance_gate import require_withdraw_audit_access
-
- dakuan_mode_filter = request.data.get('dakuan_mode')
- req_params = self._safe_request_params(request)
- kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params)
-
- try:
- try:
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('page_size', 20))
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- if page < 1:
- page = 1
- if page_size < 1 or page_size > 100:
- page_size = min(max(page_size, 1), 100)
-
- status_filter = request.data.get('status')
- type_filter = request.data.get('type')
- payment_filter = request.data.get('payment')
- search_uid = (request.data.get('search_uid') or '').strip()
- date_filter = (request.data.get('date') or '').strip()
- min_amount = request.data.get('min_amount')
- max_amount = request.data.get('max_amount')
-
- phone = (request.data.get('phone') or '').strip()
- if not phone:
- return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED)
-
- kefu, err_resp = require_withdraw_audit_access(request, phone)
- if err_resp:
- return err_resp
-
- q = Q()
-
- try:
- status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None
- except (TypeError, ValueError):
- status_val = None
-
- try:
- dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None
- except (TypeError, ValueError):
- dakuan_mode_val = None
-
- # 未传打款方式参数 → 默认手动打款(1)
- effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1
-
- if status_val == 1:
- q &= Q(zhuangtai=1)
- elif status_val == 2:
- if effective_dakuan == 2:
- q &= Q(zhuangtai__in=[2, 3, 4, 5, 6])
- else:
- q &= Q(zhuangtai__in=[2, 3])
-
- if type_filter is not None and str(type_filter).strip() != '':
- try:
- type_val = int(type_filter)
- if type_val in [1, 2, 3, 4, 5, 6]:
- q &= Q(leixing=type_val)
- except (TypeError, ValueError):
- pass
-
- if payment_filter is not None and str(payment_filter).strip() != '':
- try:
- payment_val = int(payment_filter)
- if payment_val in [1, 2]:
- q &= Q(fangshi=payment_val)
- except (TypeError, ValueError):
- pass
-
- if search_uid:
- q &= Q(yonghuid=search_uid)
-
- if date_filter:
- q &= Q(UpdateTime__date=date_filter)
-
- if min_amount is not None and str(min_amount).strip() != '':
- try:
- q &= Q(jine__gte=float(min_amount))
- except (TypeError, ValueError):
- pass
- if max_amount is not None and str(max_amount).strip() != '':
- try:
- q &= Q(jine__lte=float(max_amount))
- except (TypeError, ValueError):
- pass
-
- shenhe_field_ok = self._shenhe_jilu_field_available()
- if shenhe_field_ok:
- if effective_dakuan == 2:
- q &= Q(shenhe_jilu_id__isnull=False)
- else:
- q &= Q(shenhe_jilu_id__isnull=True)
- elif effective_dakuan == 2:
- kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表')
- return Response({
- 'code': 0,
- 'data': {
- 'list': [],
- 'total': 0,
- 'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'},
- },
- })
-
- processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3]
-
- base_qs = self._list_queryset(q, shenhe_field_ok, request)
- total_count = base_qs.count()
- awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok, request).count()
- processed_count = self._list_queryset(
- q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok, request,
- ).count()
- agg = base_qs.aggregate(total=Sum('jine'))
- total_amount = agg.get('total') or 0
-
- stats = {
- 'total': total_count,
- 'awaiting': awaiting_count,
- 'processed': processed_count,
- 'total_amount': str(round(float(total_amount), 2)),
- }
-
- queryset = base_qs.order_by('CreateTime')
- total = queryset.count()
- records = list(queryset[(page - 1) * page_size: page * page_size])
-
- audit_ids = [
- getattr(item, 'shenhe_jilu_id', None)
- for item in records if getattr(item, 'shenhe_jilu_id', None)
- ]
- audit_meta_map = {}
- if audit_ids:
- try:
- audit_meta_map = load_audit_meta_map(audit_ids)
- except Exception as e:
- kefu_txsh_list_logger.error(
- 'kefu_txsh_list: 查询审核表信息失败: %s', e,
- exc_info=True,
- )
-
- audit_mode_map = {
- aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items()
- }
- list_data = []
- for item in records:
- shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None
- shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else ''
- if shenhe_jilu_id and not shenhe_danhao:
- shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '')
- list_data.append({
- 'id': item.id,
- 'shenhe_jilu_id': shenhe_jilu_id,
- 'shenhe_danhao': shenhe_danhao,
- 'yonghuid': item.yonghuid,
- 'avatar': item.avatar or '',
- 'phone': item.phone or '',
- 'nicheng': item.nicheng or '',
- 'leixing': item.leixing,
- 'zhifu': item.zhifu or '',
- 'skzhanghao': item.skzhanghao or '',
- 'jine': str(item.jine) if item.jine is not None else '0.00',
- 'zhuangtai': item.zhuangtai,
- 'fangshi': item.fangshi,
- 'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok),
- 'shenheid': item.shenheid or '',
- 'bhliyou': item.bhliyou or '',
- 'CreateTime': item.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if item.CreateTime else '',
- 'UpdateTime': item.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if item.UpdateTime else '',
- })
-
- from jituan.services.club_user_access import list_response_meta
-
- return Response({
- 'code': 0,
- 'data': {
- 'list': list_data,
- 'total': total,
- 'stats': stats,
- **list_response_meta(request),
- },
- })
-
- except DatabaseError as e:
- kefu_txsh_list_logger.error(
- 'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s',
- dakuan_mode_filter, req_params, e,
- exc_info=True,
- )
- return Response({
- 'code': 500,
- 'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu',
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
- except Exception as e:
- kefu_txsh_list_logger.error(
- 'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s',
- dakuan_mode_filter, req_params, e,
- exc_info=True,
- )
- return Response({
- 'code': 500,
- 'msg': '获取提现列表失败',
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-
-
-class KefuWithdrawDetailView(APIView):
- """
- 客服获取提现详情接口
- 请求:POST /yonghu/kefu_txsh_detail
- 参数:{
- "phone": "客服手机号",
- "tixian_id": "提现记录ID"
- }
- 认证:JWT
- 返回:code=0 + 提现记录详情 + 用户扩展信息
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- phone = request.data.get('phone', '').strip()
- tixian_id = request.data.get('tixian_id')
-
- if not phone or not tixian_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- logger.warning(f"用户类型非客服: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- from jituan.services.group_finance_gate import require_withdraw_audit_access
- _, err_resp = require_withdraw_audit_access(request, phone)
- if err_resp:
- return err_resp
-
- # 查询提现记录
- try:
- record = Tixianjilu.query.get(id=tixian_id)
- except Tixianjilu.DoesNotExist:
- return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
- deny = forbid_if_tixian_out_of_scope(request, record)
- if deny:
- return deny
-
- # 查询用户主表
- try:
- user = User.query.get(UserUID=record.yonghuid)
- except User.DoesNotExist:
- user = None
-
- # 构建基础返回数据(字段必须与前端一致)
- data = {
- 'id': record.id,
- 'yonghuid': record.yonghuid,
- 'avatar': record.avatar or '',
- 'phone': record.phone or '',
- 'nicheng': record.nicheng or '',
- 'leixing': record.leixing, # 1打手 2管事
- 'zhifu': record.zhifu or '',
- 'skzhanghao': record.skzhanghao or '',
- 'jine': str(record.jine) if record.jine else '0.00',
- 'zhuangtai': record.zhuangtai,
- 'fangshi': record.fangshi,
- 'shenheid': record.shenheid or '',
- 'bhliyou': record.bhliyou or '',
- 'CreateTime': record.CreateTime,
- 'UpdateTime': record.UpdateTime,
- }
-
- # 根据提现类型添加扩展信息
- if user:
- if record.leixing == 1: # 打手
- try:
- dashou = UserDashou.query.get(user=user)
- # 计算成交率和退款率
- total = dashou.jiedanzongliang or 0
- completed = dashou.chengjiaozongliang or 0
- refund = dashou.tuikuanliang or 0
- completion_rate = round((completed / total * 100) if total > 0 else 0)
- refund_rate = round((refund / completed * 100) if completed > 0 else 0)
-
- data['dashouInfo'] = {
- 'zhuangtai': dashou.zhuangtai, # 工作状态
- 'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态
- 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态
- 'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额
- 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额
- 'jifen': dashou.jifen, # 积分
- 'jiedanzongliang': dashou.jiedanzongliang, # 接单总量
- 'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量
- 'tuikuanliang': dashou.tuikuanliang, # 退款总量
- 'completionRate': completion_rate,
- 'refundRate': refund_rate,
- }
- except ObjectDoesNotExist:
- data['dashouInfo'] = None
- elif record.leixing == 2: # 管事
- try:
- guanshi = UserGuanshi.query.get(user=user)
- data['guanshiInfo'] = {
- 'zhuangtai': guanshi.zhuangtai, # 账号状态
- 'yue': float(guanshi.yue) if guanshi.yue else 0.00,
- 'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数
- 'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值
- 'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值
- 'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00,
- }
- except ObjectDoesNotExist:
- data['guanshiInfo'] = None
-
- return Response({'code': 0, 'data': data})
-
-
-class KefuWithdrawActionView(APIView):
- """
- 客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款
- POST /yonghu/kefu_txsh_action
-
- 单条:{ phone, action, tixian_id, yonghuid, leixing, reason? }
- 批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? }
-
- 自动打款(dakuan_mode=2):同意→6待收款;拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计
- 手动打款(dakuan_mode=1):原逻辑不变
- 状态要求:Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1(审核中),其他状态一律拒绝
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def _verify_kefu(self, request, phone):
- if not phone:
- return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- if current_user.UserType not in ('kefu', 'admin'):
- return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- # 管理员跳过客服扩展表检查
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- if kefu.zhuangtai != 1:
- return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- return current_user, None
-
- def _parse_items(self, request):
- """解析单条或 batch_list,返回 [{tixian_id, yonghuid, leixing}]"""
- batch_list = request.data.get('batch_list')
- if batch_list is not None:
- if not isinstance(batch_list, list) or len(batch_list) == 0:
- return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST)
- items = []
- for idx, row in enumerate(batch_list):
- if not isinstance(row, dict):
- return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST)
- tid = row.get('tixian_id')
- yid = (row.get('yonghuid') or '').strip()
- lx = row.get('leixing')
- if not tid or not yid or lx is None:
- return None, Response(
- {'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'},
- status=status.HTTP_400_BAD_REQUEST,
- )
- try:
- items.append({
- 'tixian_id': int(tid),
- 'yonghuid': yid,
- 'leixing': int(lx),
- })
- except (TypeError, ValueError):
- return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
- return items, None
-
- tixian_id = request.data.get('tixian_id')
- if not tixian_id:
- return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
- yonghuid = (request.data.get('yonghuid') or '').strip()
- leixing = request.data.get('leixing')
- try:
- item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None}
- except (TypeError, ValueError):
- return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
- return [item], None
-
- def _process_auto_item(self, request, item, action, reason, kefu_user):
- """自动打款单条处理(须在 transaction.atomic 内调用)"""
- tixian_id = item['tixian_id']
- req_yonghuid = item['yonghuid']
- req_leixing = item['leixing']
-
- if req_leixing not in [1, 2, 3, 4, 5, 6]:
- raise ValueError(f'提现记录{tixian_id}:提现类型无效')
-
- try:
- tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
- except Tixianjilu.DoesNotExist:
- raise ValueError(f'提现记录{tixian_id}不存在')
-
- from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
- deny = forbid_if_tixian_out_of_scope(request, tixian)
- if deny:
- raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
-
- if tixian.yonghuid != req_yonghuid:
- raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
- if tixian.leixing != req_leixing:
- raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
- if tixian.zhuangtai != 1:
- raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
- if not tixian.shenhe_jilu_id:
- raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录')
-
- try:
- audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id)
- except TixianShenheJilu.DoesNotExist:
- raise ValueError(f'提现记录{tixian_id}:审核记录不存在')
-
- if audit.yonghuid != req_yonghuid:
- raise ValueError(f'提现记录{tixian_id}:审核记录用户ID不匹配')
- if audit.leixing != req_leixing:
- raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配')
- if audit.zhuangtai != 1:
- raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态')
-
- try:
- user = User.objects.select_for_update().get(UserUID=req_yonghuid)
- except User.DoesNotExist:
- raise ValueError(f'用户{req_yonghuid}不存在')
-
- if action == 1:
- limit_ok, limit_msg = check_shijidaozhang_within_limit(audit.shijidaozhang)
- if not limit_ok:
- sync_audit_and_jilu_status(
- audit, 5,
- bhliyou=limit_msg,
- bo_hui_ren_id=kefu_user.UserUID,
- )
- refund_balance(user, audit.leixing, audit.shijidaozhang)
- tixian.shenheid = kefu_user.UserUID
- tixian.bhliyou = limit_msg
- tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
- raise ValueError(f'提现记录{tixian_id}:{limit_msg},已自动驳回并退款')
- sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.UserUID)
- tixian.shenheid = kefu_user.UserUID
- tixian.save(update_fields=['shenheid', 'UpdateTime'])
- else:
- sync_audit_and_jilu_status(
- audit, 5,
- bhliyou=reason,
- bo_hui_ren_id=kefu_user.UserUID,
- )
- # 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退
- refund_balance(user, audit.leixing, audit.shijidaozhang)
- tixian.shenheid = kefu_user.UserUID
- tixian.bhliyou = reason
- tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
-
- def _process_manual_item(self, request, item, action, reason, phone):
- """手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)"""
- tixian_id = item['tixian_id']
- try:
- tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
- except Tixianjilu.DoesNotExist:
- raise ValueError(f'提现记录{tixian_id}不存在')
-
- from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
- deny = forbid_if_tixian_out_of_scope(request, tixian)
- if deny:
- raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
-
- req_yonghuid = item.get('yonghuid')
- req_leixing = item.get('leixing')
- if req_yonghuid and tixian.yonghuid != req_yonghuid:
- raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
- if req_leixing is not None and tixian.leixing != req_leixing:
- raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
- if tixian.zhuangtai != 1:
- raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
-
- if getattr(tixian, 'shenhe_jilu_id', None):
- raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程')
-
- try:
- user = User.query.get(UserUID=tixian.yonghuid)
- except User.DoesNotExist:
- raise ValueError(f'用户{tixian.yonghuid}不存在')
-
- from jituan.services.club_user import get_user_club_id
-
- if action == 1:
- tixian.zhuangtai = 2
- try:
- from jituan.services.szjilu_accounting import apply_szjilu_expense
- update_daily_payout(tixian.jine, get_user_club_id(user))
- apply_szjilu_expense(tixian.jine, get_user_club_id(user))
- except Exception as e:
- logger.error(f'更新收支记录失败: {e}')
- else:
- try:
- refund_balance(user, tixian.leixing, tixian.jine)
- except ValueError as e:
- raise ValueError(str(e))
- tixian.zhuangtai = 3
- tixian.bhliyou = reason
-
- tixian.shenheid = phone
- tixian.UpdateTime = timezone.now()
- tixian.save()
-
- def post(self, request):
- logger.info(f'接收到提现处理请求,数据: {request.data}')
-
- phone = request.data.get('phone', '').strip()
- action_raw = request.data.get('action')
- reason = request.data.get('reason', '').strip()
-
- try:
- action = int(action_raw)
- except (TypeError, ValueError):
- return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
- if action not in [1, 2]:
- return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST)
- if action == 2 and not reason:
- return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- kefu_user, err_resp = self._verify_kefu(request, phone)
- if err_resp:
- return err_resp
-
- from jituan.services.group_finance_gate import require_withdraw_audit_access
- _, audit_err = require_withdraw_audit_access(request, phone)
- if audit_err:
- return audit_err
-
- items, err_resp = self._parse_items(request)
- if err_resp:
- return err_resp
-
- is_batch = isinstance(request.data.get('batch_list'), list) and len(request.data.get('batch_list')) > 0
-
- try:
- with transaction.atomic():
- for item in items:
- tixian_id = item['tixian_id']
- try:
- tixian_probe = Tixianjilu.objects.select_for_update().get(id=tixian_id)
- except Tixianjilu.DoesNotExist:
- raise ValueError(f'提现记录{tixian_id}不存在')
-
- from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
- deny = forbid_if_tixian_out_of_scope(request, tixian_probe)
- if deny:
- raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
-
- # 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段
- is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
-
- if is_auto:
- self._process_auto_item(request, item, action, reason, kefu_user)
- else:
- if is_batch:
- raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
- self._process_manual_item(request, item, action, reason, phone)
-
- except ValueError as e:
- return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- logger.error(f'处理提现事务失败: {e}', exc_info=True)
- return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功'
- return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}})
-
-
-
-
-
-
-
-
-# dingdan/views.py
-class KefuPunishView(APIView):
- """
- 客服处罚打手接口
- 请求:POST /yonghu/kefuchufa
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "reason": "处罚原因(可选)"
- }
- 认证:JWT
- 返回:code=0 成功
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 获取参数
- phone = request.data.get('phone', '').strip()
- dingdan_id = request.data.get('dingdan_id', '').strip()
- reason = request.data.get('reason', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- logger.warning(f"手机号不匹配: {phone}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType not in ('kefu', 'admin'):
- logger.warning(f"用户类型非客服: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- logger.warning(f"客服账号已禁用: {current_user.UserUID}")
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单
- try:
- order = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 获取打手ID
- dashou_id = order.PlayerID
- if not dashou_id:
- return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 查询打手
- try:
- dashou_user = User.query.get(UserUID=dashou_id)
- dashou = dashou_user.DashouProfile
- except User.DoesNotExist:
- return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND)
- except ObjectDoesNotExist:
- return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 使用事务
- from jituan.services.club_penalty import resolve_penalty_record_club_id
- with transaction.atomic():
- # 创建处罚记录(积分扣除5分)
- jifen = 5
- chufa = PenaltyRecord.query.create(
- PlayerID=dashou_id,
- ApplicantID=phone, # 客服手机号作为请求人
- PenaltyReason=reason or '',
- ApplyStatus=0, # 待处理
- DeductedPoints=jifen,
- OrderID=dingdan_id,
- ClubID=resolve_penalty_record_club_id(
- order_id=dingdan_id,
- player_id=dashou_id,
- request=request,
- user=request.user,
- ),
- )
-
- # 扣除打手积分
- if dashou.jifen >= jifen:
- dashou.jifen -= jifen
- else:
- dashou.jifen = 0
- dashou.save()
-
- # 可选:更新订单的 clkf 字段记录处理客服
- order.AssignedCS = phone
- order.save()
-
- return Response({'code': 0, 'msg': '处罚成功'})
-
-
-
-class AdKftjView(APIView):
- """
- 添加客服接口
- 权限:管理员JWT
- 请求:POST /yonghu/adkftj
- 参数:{
- "zhanghao": "管理员账号",
- "phone": "客服手机号(必填)",
- "password": "密码(必填)",
- "erjimima": "二级密码(必填)",
- "nicheng": "昵称(可选)"
- }
- 返回:code=0
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 参数
- zhanghao = request.data.get('zhanghao', '').strip()
- phone = request.data.get('phone', '').strip()
- password = request.data.get('password', '').strip()
- erjimima = request.data.get('erjimima', '').strip()
- nicheng = request.data.get('nicheng', '').strip()
-
- if not all([zhanghao, phone, password, erjimima]):
- return Response({'code': 400, 'message': '手机号、密码、二级密码不能为空'})
-
- # 2. 管理员验证(同前)
- current_user = request.user
- if current_user.Phone != zhanghao or current_user.UserType != 'admin':
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
- try:
- admin_profile = current_user.AdminProfile
- except:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 检查手机号是否已被使用
- if User.query.filter(Phone=phone).exists():
- return Response({'code': 400, 'message': '该手机号已被注册'})
-
- # 4. 生成唯一 yonghuid
- def generate_yonghuid():
- for _ in range(10):
- timestamp = str(int(time.time()))[-5:]
- rand = str(random.randint(0, 99)).zfill(2)
- uid = timestamp + rand
- if len(uid) == 7 and not User.query.filter(UserUID=uid).exists():
- return uid
- raise Exception('无法生成唯一用户ID')
-
- with transaction.atomic():
- # 创建主表用户
- user = User.query.create(
- UserUID=generate_yonghuid(),
- UserName=f'kefu_{phone}',
- Phone=phone,
- OpenID=f'kefu_{phone}' # 临时openid
- )
- user.SetPassword(password)
- user.save(update_fields=['UserPassword'])
- # 创建客服扩展表
- import bcrypt as _bcrypt
- erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
- kefu = UserKefu.query.create(
- user=user,
- nicheng=nicheng or f'客服{user.UserUID}',
- erjimima=erjimima_hashed,
- zhuangtai=1,
- jinrichuli=0,
- jinyuechuli=0,
- zongchuli=0
- )
-
- return Response({'code': 0, 'message': '添加成功'})
-
-
-
-class AdKfglView(APIView):
- """
- 客服管理列表接口
- 权限:管理员JWT
- 请求:POST /yonghu/adkfgl
- 参数:{
- "zhanghao": "管理员账号",
- "keyword": "搜索关键词(可选,匹配客服ID或昵称)"
- }
- 返回:{
- "code": 0,
- "data": {
- "list": [客服对象...]
- }
- }
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- # 1. 参数获取
- zhanghao = request.data.get('zhanghao', '').strip()
- keyword = request.data.get('keyword', '').strip()
-
- if not zhanghao:
- return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 2. 管理员身份验证
- current_user = request.user
- if not hasattr(current_user, 'Phone') or str(current_user.Phone) != zhanghao:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if current_user.UserType != 'admin':
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- try:
- admin_profile = current_user.AdminProfile
- except AttributeError:
- return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询所有客服(通过扩展表关联主表)
- queryset = UserKefu.query.select_related('user').all()
-
- if keyword:
- queryset = queryset.filter(
- #Q(user__UserUID__icontains=keyword) |
- Q(nicheng__icontains=keyword) |
- Q(user__Phone__icontains=keyword)
- )
-
- # 4. 构建返回数据
- kefu_list = []
- for kefu in queryset:
- user = kefu.user
- kefu_list.append({
- 'yonghuid': user.Phone,
- 'phone': user.Phone or '',
- 'nicheng': kefu.nicheng or '',
- 'zhuangtai': kefu.zhuangtai,
- 'jinrichuli': kefu.jinrichuli,
- 'jinyuechuli': kefu.jinyuechuli,
- 'zongchuli': kefu.zongchuli,
- })
-
- return Response({
- 'code': 0,
- 'data': {'list': kefu_list}
- })
-
-
-
-
-
-class AdKfxgView(APIView):
- """
- 修改客服信息接口
- 权限:管理员JWT
- 请求:POST /yonghu/adkfxg
- 参数:{
- "zhanghao": "管理员账号",
- "uid": "客服ID (yonghuid)",
- "nicheng": "新昵称(可选)",
- "phone": "新手机号(可选)",
- "password": "新密码(可选)",
- "erjimima": "新二级密码(可选)",
- "zhuangtai": 0/1(可选)
- }
- 返回:code=0
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- zhanghao = request.data.get('zhanghao', '').strip()
- uid = request.data.get('uid', '').strip()
-
- if not zhanghao or not uid:
- return Response({'code': 400, 'message': '参数不完整'})
-
- # 管理员验证
- current_user = request.user
- if current_user.Phone != zhanghao or current_user.UserType != 'admin':
- return Response({'code': 401, 'message': '身份验证失败'})
- try:
- admin_profile = current_user.AdminProfile
- except:
- return Response({'code': 401, 'message': '身份验证失败'})
-
- # 查询客服
- try:
- user = User.query.get(Phone=uid)
- kefu = user.KefuProfile
- except (User.DoesNotExist, UserKefu.DoesNotExist):
- return Response({'code': 404, 'message': '客服不存在'})
-
- # 更新数据
- with transaction.atomic():
- # 更新主表字段
- if 'phone' in request.data:
- new_phone = request.data.get('phone').strip()
- if new_phone and User.query.filter(Phone=new_phone).exclude(UserUID=uid).exists():
- return Response({'code': 400, 'message': '手机号已被使用'})
- user.Phone = new_phone
- if 'password' in request.data:
- user.UserPassword = request.data['password']
- user.save()
-
- # 更新扩展表
- if 'nicheng' in request.data:
- kefu.nicheng = request.data['nicheng']
- if 'erjimima' in request.data:
- import bcrypt as _bcrypt
- new_erji = request.data['erjimima']
- kefu.erjimima = _bcrypt.hashpw(new_erji.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
- if 'zhuangtai' in request.data:
- kefu.zhuangtai = int(request.data['zhuangtai'])
- kefu.save()
-
- return Response({'code': 0, 'message': '修改成功'})
-
-
-
-
-
-
-
-
-class WechatLoginAndGuanshiRegisterView(APIView):
- """
- 未登录用户微信登录+管事注册一体化接口
- 路径: /api/yonghu/zuzhangyqmzc
- 方法: POST
- 权限: 允许所有
- 请求体: { "code": "微信登录code", "inviteCode": "组长邀请码" }
- 成功返回 code: 200,数据同 GuanshiRegisterView
- """
- permission_classes = [AllowAny]
-
- def post(self, request):
- code = request.data.get('code', '').strip()
- yaoqingma = request.data.get('inviteCode', '').strip()
-
- if not code:
- return Response({'code': 400, 'msg': '微信授权码不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if not yaoqingma:
- return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if len(yaoqingma) > 100:
- return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- wechat_data = self._get_wechat_openid(code)
- if not wechat_data or 'openid' not in wechat_data:
- error_msg = wechat_data.get('errmsg', '微信授权失败')
- return Response({'code': 400, 'msg': f'微信登录失败: {error_msg}', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- openid = wechat_data['openid']
- kehuduan_ip = self._huoqu_kehuduan_ip(request)
-
- with transaction.atomic():
- user_main, created = User.objects.select_for_update().get_or_create(
- OpenID=openid,
- defaults={
- 'UserUID': self._shengcheng_yonghu_id(),
- 'UserName': openid,
- }
- )
- user_main.IP = kehuduan_ip
- user_main.UserLastLoginDate = timezone.now()
- user_main.save()
-
- if created:
- UserBoss.query.create(user=user_main, nickname='微信用户')
-
- if hasattr(user_main, 'GuanshiProfile'):
- return self._fanhui_xianyou_guanshi_info(user_main)
-
- try:
- zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma)
- except UserZuzhang.DoesNotExist:
- return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND)
-
- if zuzhang.zhuangtai != 1:
- return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- zuzhang_yonghuid = zuzhang.user.UserUID
-
- guanshi_yaoqingma = CreateInvitationCode(str(user_main.UserUID))
- guanshi_profile = UserGuanshi.query.create(
- user=user_main,
- yaoqingma=guanshi_yaoqingma,
- yaoqingren=zuzhang_yonghuid,
- )
-
- UserZuzhang.query.filter(pk=zuzhang.pk).update(
- yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
- )
-
- # ========== 新增:组长每日统计(邀请管事 action=1) ==========
- try:
- update_zuzhang_daily_by_action(
- yonghuid=zuzhang_yonghuid,
- action=1 # 1 = 邀请管事
- )
- logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
- except Exception as e:
- logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
-
-
- if not hasattr(user_main, 'DashouProfile'):
- dashou_profile = UserDashou.query.create(
- user=user_main,
- nicheng='大神',
- chenghao='普通大神',
- yaoqingren=zuzhang_yonghuid,
- )
- else:
- dashou_profile = user_main.DashouProfile
-
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- response_data = self._zhuangbei_fanhui_shuju(
- user_main, dashou_profile, guanshi_profile, token
- )
-
- return Response({'code': 0, 'msg': '登录并注册成功!', 'data': response_data})
-
- # ---------- 辅助方法(与接口1完全相同,完整列出以保证完整性) ----------
-
- def _get_wechat_openid(self, code):
- try:
- appid = getattr(settings, 'WEIXIN_APPID', '')
- secret = getattr(settings, 'WEIXIN_SECRET', '')
- if not appid or not secret:
- raise ValueError('微信配置未设置')
- url = 'https://api.weixin.qq.com/sns/jscode2session'
- params = {
- 'appid': appid,
- 'secret': secret,
- 'js_code': code,
- 'grant_type': 'authorization_code'
- }
- response = requests.get(url, params=params, timeout=10)
- result = response.json()
- if 'openid' in result:
- return result
- else:
- errcode = result.get('errcode', 'unknown')
- errmsg = result.get('errmsg', '未知错误')
- return {'errmsg': f'[{errcode}]{errmsg}'}
- except requests.exceptions.Timeout:
- logger.error('请求微信接口超时')
- return {'errmsg': '请求微信接口超时'}
- except Exception as e:
- logger.error(f'请求微信接口异常: {e}')
- return {'errmsg': f'请求微信接口异常: {str(e)}'}
-
- def _shengcheng_yonghu_id(self):
- for _ in range(10):
- timestamp_part = str(int(time.time()))[-5:].zfill(5)
- random_part = str(random.randint(0, 99)).zfill(2)
- user_id = timestamp_part + random_part
- if len(user_id) == 7 and user_id.isdigit():
- if not User.query.filter(UserUID=user_id).exists():
- return user_id
- raise Exception('生成用户ID失败')
-
- def _huoqu_kehuduan_ip(self, request):
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
- if x_forwarded_for:
- ip = x_forwarded_for.split(',')[0].strip()
- if ip:
- return ip
- ip = request.META.get('REMOTE_ADDR', '')
- return ip if ip else '0.0.0.0'
-
- def _huoqu_qun_peizhi(self):
- try:
- qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
- result = {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
- for config in qun_configs:
- if config.id == 1:
- result['dashouqun'] = config.GroupContent or ''
- result['dashouqunid'] = config.GroupID or ''
- elif config.id == 2:
- result['guanshiqun'] = config.GroupContent or ''
- result['guanshiqunid'] = config.GroupID or ''
- return result
- except Exception as e:
- logger.error(f"获取群配置失败: {e}")
- return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
-
- def _zhuangbei_dashou_fanhui(self, dashou_profile):
- data = {
- 'dashounicheng': dashou_profile.nicheng,
- 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
- 'yongjin': str(dashou_profile.yue),
- 'zonge': str(dashou_profile.zonge),
- 'yajin': str(dashou_profile.yajin),
- 'chenghao': dashou_profile.chenghao,
- 'jinfen': dashou_profile.jifen,
- 'chengjiaoliang': dashou_profile.chengjiaozongliang,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'dashouzhuangtai': dashou_profile.zhuangtai,
- }
- huiyuan_list = []
- try:
- records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.UserUID)
- for record in records:
- shifou_daoqi = record.jiance_shifou_daoqi()
- huiyuan_list.append({
- 'huiyuan_id': record.huiyuan_id,
- 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
- 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
- 'shifou_daoqi': shifou_daoqi,
- 'jieshao': record.jieshao
- })
- except Exception as e:
- logger.warning(f"获取会员列表失败: {e}")
- data['clumber'] = huiyuan_list
- return data
-
- def _fanhui_xianyou_guanshi_info(self, user_main):
- guanshi_profile = user_main.GuanshiProfile
- dashou_profile = getattr(user_main, 'DashouProfile', None)
- group_info = self._huoqu_qun_peizhi()
- dashou_status = 1 if dashou_profile else 0
- shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
- guanshi_status = 1
- try:
- nicheng = user_main.BossProfile.nickname or '微信用户'
- except:
- nicheng = '微信用户'
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
- data = {
- 'token': token,
- 'nicheng': nicheng,
- 'uid': user_main.UserUID,
- 'touxiang': user_main.Avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- **group_info,
- }
- guanshi_data = {
- 'gszhstatus': guanshi_profile.zhuangtai,
- 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
- 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
- 'fenyongtixian': str(guanshi_profile.yue),
- 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
- }
- data.update(guanshi_data)
- if dashou_profile:
- dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
- data.update(dashou_data)
- else:
- data.update({
- 'dashounicheng': '',
- 'zhanghaostatus': 0,
- 'yongjin': '0.00',
- 'zonge': '0.00',
- 'yajin': '0.00',
- 'chenghao': '',
- 'jinfen': 0,
- 'chengjiaoliang': 0,
- 'zaixianzhuangtai': 0,
- 'dashouzhuangtai': 0,
- 'clumber': []
- })
- return Response({'code': 0, 'msg': '您已是管事', 'data': data})
-
- def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
- try:
- nicheng = user_main.BossProfile.nickname or '微信用户'
- except:
- nicheng = '微信用户'
- dashou_status = 1
- shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
- guanshi_status = 1
- group_info = self._huoqu_qun_peizhi()
- data = {
- 'token': token,
- 'nicheng': nicheng,
- 'uid': user_main.UserUID,
- 'touxiang': user_main.Avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- **group_info,
- }
- guanshi_data = {
- 'gszhstatus': guanshi_profile.zhuangtai,
- 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
- 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
- 'fenyongtixian': str(guanshi_profile.yue),
- 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
- }
- data.update(guanshi_data)
- dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
- data.update(dashou_data)
- return data
-
-
-
-
-class GuanshiRegisterView(APIView):
- """
- 已登录用户注册管事接口
- 路径: /api/yonghu/guanshizhuce
- 方法: POST
- 权限: JWT认证
- 请求体: { "inviteCode": "组长邀请码" }
- 成功返回 code: 200
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取并验证邀请码
- yaoqingma = request.data.get('inviteCode', '').strip()
- if not yaoqingma:
- return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- if len(yaoqingma) > 100:
- return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
- status=status.HTTP_400_BAD_REQUEST)
-
- current_user = request.user
-
- # 2. 检查用户是否已是管事
- if hasattr(current_user, 'GuanshiProfile'):
- # 用户已是管事,直接返回现有信息
- return self._fanhui_xianyou_guanshi_info(current_user)
-
- # 3. 验证邀请码对应的组长
- try:
- zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma)
- except UserZuzhang.DoesNotExist:
- return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
- status=status.HTTP_404_NOT_FOUND)
-
- if zuzhang.zhuangtai != 1:
- return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- from jituan.services.invite_guard import assert_invite_same_club
- ok, invite_msg = assert_invite_same_club(request, zuzhang.user, current_user)
- if not ok:
- return Response({'code': 403, 'msg': invite_msg, 'data': None},
- status=status.HTTP_403_FORBIDDEN)
-
- zuzhang_yonghuid = zuzhang.user.UserUID
-
- # 4. 事务内创建管事实体和打手(如果需要)
- with transaction.atomic():
- # 创建管事扩展表
- guanshi_yaoqingma = CreateInvitationCode(str(current_user.UserUID))
- guanshi_profile = UserGuanshi.query.create(
- user=current_user,
- yaoqingma=guanshi_yaoqingma,
- yaoqingren=zuzhang_yonghuid,
- )
-
- # 更新组长的邀请总数
- UserZuzhang.query.filter(pk=zuzhang.pk).update(
- yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
- )
-
- # ========== 新增:组长每日统计(邀请管事 action=1) ==========
- try:
- update_zuzhang_daily_by_action(
- yonghuid=zuzhang_yonghuid,
- action=1 # 1 = 邀请管事
- )
- logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
- except Exception as e:
- logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
-
- # 检查用户是否已是打手,如果不是则创建
- if not hasattr(current_user, 'DashouProfile'):
- dashou_profile = UserDashou.query.create(
- user=current_user,
- nicheng='大手子',
- chenghao='普通大手',
- yaoqingren=zuzhang_yonghuid,
- )
- else:
- dashou_profile = current_user.DashouProfile
-
- # 5. 生成新token
- refresh = RefreshToken.for_user(current_user)
- token = str(refresh.access_token)
-
- # 6. 准备返回数据(包含所有字段)
- response_data = self._zhuangbei_fanhui_shuju(
- current_user, dashou_profile, guanshi_profile, token
- )
-
- return Response({'code': 200, 'msg': '注册成功!', 'data': response_data})
-
- # ---------- 辅助方法 ----------
-
- def _huoqu_qun_peizhi(self):
- """获取群配置信息"""
- try:
- qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
- result = {
- 'dashouqun': '',
- 'dashouqunid': '',
- 'guanshiqun': '',
- 'guanshiqunid': ''
- }
- for config in qun_configs:
- if config.id == 1:
- result['dashouqun'] = config.GroupContent or ''
- result['dashouqunid'] = config.GroupID or ''
- elif config.id == 2:
- result['guanshiqun'] = config.GroupContent or ''
- result['guanshiqunid'] = config.GroupID or ''
- return result
- except Exception as e:
- logger.error(f"获取群配置失败: {e}")
- return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
-
- def _zhuangbei_dashou_fanhui(self, dashou_profile):
- """组装打手信息(字段名与前端完全匹配)"""
- data = {
- 'dashounicheng': dashou_profile.nicheng,
- 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
- 'yongjin': str(dashou_profile.yue),
- 'zonge': str(dashou_profile.zonge),
- 'yajin': str(dashou_profile.yajin),
- 'chenghao': dashou_profile.chenghao,
- 'jinfen': dashou_profile.jifen,
- 'chengjiaoliang': dashou_profile.chengjiaozongliang,
- 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
- 'dashouzhuangtai': dashou_profile.zhuangtai,
- }
-
- # 会员列表
- huiyuan_list = []
- try:
- records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.UserUID)
- for record in records:
- shifou_daoqi = record.jiance_shifou_daoqi()
- huiyuan_list.append({
- 'huiyuan_id': record.huiyuan_id,
- 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
- 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
- 'shifou_daoqi': shifou_daoqi,
- 'jieshao': record.jieshao
- })
- except Exception as e:
- logger.warning(f"获取会员列表失败: {e}")
- data['clumber'] = huiyuan_list
- return data
-
- def _fanhui_xianyou_guanshi_info(self, user_main):
- """用户已是管事时的返回"""
- guanshi_profile = user_main.GuanshiProfile
- dashou_profile = getattr(user_main, 'DashouProfile', None)
-
- group_info = self._huoqu_qun_peizhi()
-
- dashou_status = 1 if dashou_profile else 0
- shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
- guanshi_status = 1
-
- try:
- nicheng = user_main.BossProfile.nickname or '微信用户'
- except:
- nicheng = '微信用户'
-
- refresh = RefreshToken.for_user(user_main)
- token = str(refresh.access_token)
-
- data = {
- 'token': token,
- 'nicheng': nicheng,
- 'uid': user_main.UserUID,
- 'touxiang': user_main.Avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- **group_info,
- }
-
- # 管事信息
- guanshi_data = {
- 'gszhstatus': guanshi_profile.zhuangtai,
- 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
- 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
- 'fenyongtixian': str(guanshi_profile.yue),
- 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
- }
- data.update(guanshi_data)
-
- if dashou_profile:
- dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
- data.update(dashou_data)
- else:
- data.update({
- 'dashounicheng': '',
- 'zhanghaostatus': 0,
- 'yongjin': '0.00',
- 'zonge': '0.00',
- 'yajin': '0.00',
- 'chenghao': '',
- 'jinfen': 0,
- 'chengjiaoliang': 0,
- 'zaixianzhuangtai': 0,
- 'dashouzhuangtai': 0,
- 'clumber': []
- })
-
- return Response({'code': 200, 'msg': '您已是管事', 'data': data})
-
- def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
- """组装完整返回数据(新注册)"""
- try:
- nicheng = user_main.BossProfile.nickname or '微信用户'
- except:
- nicheng = '微信用户'
-
- dashou_status = 1
- shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
- guanshi_status = 1
-
- group_info = self._huoqu_qun_peizhi()
-
- data = {
- 'token': token,
- 'nicheng': nicheng,
- 'uid': user_main.UserUID,
- 'touxiang': user_main.Avatar or '',
- 'shangjiastatus': shangjia_status,
- 'dashoustatus': dashou_status,
- 'guanshistatus': guanshi_status,
- **group_info,
- }
-
- # 管事信息
- guanshi_data = {
- 'gszhstatus': guanshi_profile.zhuangtai,
- 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
- 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
- 'fenyongtixian': str(guanshi_profile.yue),
- 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
- }
- data.update(guanshi_data)
-
- dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
- data.update(dashou_data)
-
- return data
-
-
-
-
-
-
-
-class ZuzhangZhuceView(APIView):
- """
- 组长注册/激活接口
- POST /yonghu/zuzhangzhuce
- 请求体: {"inviteCode": "xxx"}
- 返回:
- {
- "code": 200,
- "msg": "success",
- "data": {
- "ketixian": "0.00", # 可提现金额
- "guanshiCount": 0, # 邀请的管事数量
- "fenhongZonge": "0.00" # 分红总额
- }
- }
- 或错误码
- """
-
- permission_classes = [IsAuthenticated]
-
- # 硬编码激活码(注意转义特殊字符)
- ACTIVATION_CODE = "237huwehdw77e777eyw224fr"
-
- def post(self, request):
- user = request.user
- invite_code = request.data.get('inviteCode', '').strip()
-
- if not invite_code:
- return Response({'code': 400, 'msg': '邀请码不能为空'}, status=400)
-
- # 检查是否已是组长
- try:
- zuzhang = UserZuzhang.query.select_related('user').get(user=user)
- # 已注册,直接返回数据
- data = self._build_response_data(zuzhang, user)
- return Response({'code':200, 'msg': '已注册', 'data': data})
- except UserZuzhang.DoesNotExist:
- pass
-
- # 比对激活码
- if invite_code != self.ACTIVATION_CODE:
- return Response({'code': 400, 'msg': '邀请码无效'}, status=400)
-
- from jituan.services.club_user import ensure_user_club_id
- from jituan.services.club_context import resolve_club_id_from_request
- ensure_user_club_id(user, resolve_club_id_from_request(request))
-
- # 生成唯一组长邀请码:时间戳 + 用户ID后6位 + 随机字符
- yaoqingma = self._generate_unique_yaoqingma(user.UserUID)
-
- # 创建组长扩展记录
- try:
- with transaction.atomic():
- zuzhang = UserZuzhang.query.create(
- user=user,
- yaoqingma=yaoqingma,
- fenyong_zonge=0.00,
- ketixian_jine=0.00,
- yaoqing_zongshu=0,
- jinri_fenyong=0.00,
- jinyue_fenyong=0.00,
- zhuangtai=1,
- jinri_tixian=0.00,
- ewai_tixian_xiane=0.00,
- kaioi_ewai_tixian=False,
- kaioi_ewai_fenhong=False,
- ewai_fenhong_jine=0.00
- )
- except IntegrityError:
- # 极低概率重复,重试一次
- yaoqingma = self._generate_unique_yaoqingma(user.UserUID, retry=True)
- zuzhang = UserZuzhang.query.create(
- user=user,
- yaoqingma=yaoqingma,
- # 其他字段默认同上
- fenyong_zonge=0.00,
- ketixian_jine=0.00,
- yaoqing_zongshu=0,
- jinri_fenyong=0.00,
- jinyue_fenyong=0.00,
- zhuangtai=1,
- jinri_tixian=0.00,
- ewai_tixian_xiane=0.00,
- kaioi_ewai_tixian=False,
- kaioi_ewai_fenhong=False,
- ewai_fenhong_jine=0.00
- )
-
- data = self._build_response_data(zuzhang, user)
- return Response({'code':200, 'msg': '注册成功', 'data': data})
-
- def _generate_unique_yaoqingma(self, yonghuid, retry=False):
- """生成唯一邀请码:时间戳+用户ID后6位+随机2位字符"""
- timestamp = str(int(time.time()))[-6:] # 取后6位
- uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
- rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
- base = f"Z{timestamp}{uid_part}{rand_str}"
- # 确保唯一性(若已存在则追加随机数,最多尝试3次)
- for _ in range(3):
- if not UserZuzhang.query.filter(yaoqingma=base).exists():
- return base
- base = base + random.choice(string.digits)
- # 极端情况,再附加时间戳毫秒
- millis = str(time.time()).replace('.', '')[-4:]
- return base + millis
-
- def _build_response_data(self, zuzhang, user):
- """组装前端所需数据"""
- # 计算邀请的管事数量(通过UserGuanshi表的yaoqingren字段关联到user的yonghuid)
- guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
- return {
- 'ketixian': str(zuzhang.ketixian_jine), # 可提现余额
- 'guanshiCount': guanshi_count, # 邀请管事数
- 'fenhongZonge': str(zuzhang.fenyong_zonge) # 分红总额
- }
-
-
-
-class ZuzhangXinxiView(APIView):
- """
- 组长信息查询接口
- POST /yonghu/zuzhangxinxi
- 请求体: 无
- 返回: 同注册接口成功返回格式
- """
-
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- user = request.user
- try:
- zuzhang = UserZuzhang.query.get(user=user)
- except UserZuzhang.DoesNotExist:
- return Response({'code': 404, 'msg': '用户不是组长'}, status=404)
-
- guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
- data = {
- 'ketixian': str(zuzhang.ketixian_jine),
- 'guanshiCount': guanshi_count,
- 'fenhongZonge': str(zuzhang.fenyong_zonge)
- }
- return Response({'code': 200, 'msg': 'success', 'data': data})
-
-
-
-
-
-# apps/yonghu/views_dashou_jianquan.py
-"""
-打手 IM 消息权限鉴权接口
-URL: /yonghu/dshqltqx
-方法: POST
-认证: JWT Token (request.user)
-
-鉴权逻辑(满足任意一条即通过):
- 1. 押金 >= 5 元 → 通过
- 2. 拥有未过期会员 → 通过
- 3. 存在订单状态为 2 或 8 的订单(作为接单打手) → 通过
- 以上均不满足 → 拒绝
-"""
-
-
-class DashouJianquanView(APIView):
- """打手消息权限鉴权"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # ========== 1. 通过 JWT 获取当前登录用户 ==========
- user = request.user # User 实例
-
- # ========== 2. 检查是否为已注册打手 ==========
- try:
- dashou = user.DashouProfile # OneToOneField 反向查询
- except UserDashou.DoesNotExist:
- return Response({
- 'code': 0,
- 'allow': 0,
- 'msg': '您尚未注册打手身份,请先注册'
- })
-
- # ========== 3. 条件一:押金 >= 5 元 ==========
- if dashou.yajin and dashou.yajin >= 5:
- return Response({
- 'code': 0,
- 'allow': 1,
- 'msg': '鉴权通过(押金满足条件)'
- })
-
- # ========== 4. 条件二:存在未过期会员 ==========
- # 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引
- huiyuan_list = Huiyuangoumai.query.filter(
- yonghu_id=user.UserUID,
- huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历
- ).only('id', 'daoqi_time', 'huiyuan_zhuangtai')
-
- for hy in huiyuan_list:
- if not hy.jiance_shifou_daoqi(): # 未过期返回 False
- return Response({
- 'code': 0,
- 'allow': 1,
- 'msg': '鉴权通过(会员未过期)'
- })
-
- # ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ==========
- # 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条
- has_valid_order = Order.query.filter(
- PlayerID=user.UserUID,
- Status__in=[2, 8]
- ).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1
-
- if has_valid_order:
- return Response({
- 'code': 0,
- 'allow': 1,
- 'msg': '鉴权通过(存在有效订单)'
- })
-
- # ========== 6. 以上均不满足 → 拒绝 ==========
- return Response({
- 'code': 0,
- 'allow': 0,
- 'msg': '消息权限不足,请充值会员或联系管理员'
- })
-
-
-
-# apps/yonghu/views_laoban_jianquan.py
-"""
-老板(普通用户) IM 消息权限鉴权接口
-URL: /yonghu/lbhqltqx
-方法: POST
-认证: JWT Token (request.user)
-
-鉴权逻辑:
- 查询平台订单扩展表(PlatformOrderExt),
- 通过关联的订单主表(Order)查找该老板的订单,
- 只要存在任意一条订单状态在 1~8 范围内 → 通过
-"""
-
-
-
-class LaobanJianquanView(APIView):
- """老板消息权限鉴权"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # ========== 1. 通过 JWT 获取当前登录用户 ==========
- user = request.user # User 实例
-
- # ========== 2. 高效查询平台订单扩展表 ==========
- # BossID 为模型字段名(db_column=laoban_id);订单状态用 Order.Status
- has_valid_order = PlatformOrderExt.query.filter(
- BossID=user.UserUID
- ).filter(
- Order__Status__in=[1, 2, 3, 4, 5, 6, 7, 8]
- ).only('id').exists()
-
- if has_valid_order:
- return Response({
- 'code': 0,
- 'allow': 1,
- 'msg': '鉴权通过'
- })
-
- return Response({
- 'code': 0,
- 'allow': 0,
- 'msg': '您暂无订单记录,消息功能暂不可用'
- })
-
-
-
-
-
-
-
-# views.py (在处罚相关app的views中)
-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)
-
-
-
-
-
-
-
-# 需要替换为你自己项目中的导入路径
-
-# XIAOCHENGXU LOGGER?
-# ==================== 1. 罚款支付下单接口 ====================
-
-class FaKuanPayView(APIView):
- """
- 罚款微信支付下单接口(完全照抄 YajinGoumai)
- 路径:POST /yonghu/fkjn
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- fadan_id = request.data.get('fadan_id')
- if not fadan_id:
- return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.UserUID)
- except Penalty.DoesNotExist:
- return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND)
-
- if fadan.Status not in [1, 3]:
- return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST)
-
- jine = float(fadan.FineAmount)
-
- timestamp_ms = int(time.time() * 1000)
- timestamp_ns = int(time.time_ns() // 1000) % 1000000
- random_num = random.randint(0, 99)
- dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
-
- from jituan.services.club_penalty import penalty_cz_club_id
- from jituan.services.club_user import get_payment_openid
- from products.czjilu_types import CZJILU_LEIXING_FAKUAN, penalty_cz_shuoming
-
- pay_openid, _ = get_payment_openid(request)
- if not pay_openid:
- return Response({
- 'code': 400,
- 'message': '用户openid不存在,无法发起微信支付',
- }, status=status.HTTP_400_BAD_REQUEST)
-
- chongzhi_jilu = Czjilu.query.create(
- dingdan_id=dingdanid,
- zhuangtai=9,
- yonghuid=request.user.UserUID,
- jine=jine,
- leixing=CZJILU_LEIXING_FAKUAN,
- shuoming=penalty_cz_shuoming(fadan.id),
- club_id=penalty_cz_club_id(fadan),
- )
-
- try:
- pay_params = self.generate_wechat_pay_params(
- dingdanid=dingdanid,
- jine=jine,
- openid=pay_openid,
- pay_type='fakuan'
- )
- 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)
-
- return Response({
- 'code': 200,
- 'message': '支付参数生成成功',
- 'payParams': pay_params,
- '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):
- 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 == 'fakuan':
- PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION
- NOTIFY_URL = settings.FAKUAN_PAY_NOTIFY_URL # 回调地址在这里
- else:
- raise Exception('未知的支付类型')
-
- nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
- total_fee = yuan_to_fen(jine)
- trade_type = 'JSAPI'
- user_ip = self.get_client_ip()
-
- # 第一次签名
- 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
- }
-
- 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}'
- sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
-
- # 构建 XML,这里必须包含 notify_url
- xml_data = f"""
-{APPID}
-{PAY_DESCRIPTION}
-{MCHID}
-{nonce_str}
-{NOTIFY_URL}
-{openid}
-{dingdanid}
-{user_ip}
-{total_fee}
-{trade_type}
-{sign}
-"""
-
- response = requests.post(
- 'https://api.mch.weixin.qq.com/pay/unifiedorder',
- data=xml_data.encode('utf-8'),
- headers={'Content-Type': 'application/xml'},
- timeout=10
- )
-
- logger.info(f"罚款微信支付响应: {response.text}")
-
- root = ET.fromstring(response.content)
-
- return_code_node = root.find('return_code')
- if return_code_node is None or return_code_node.text is None:
- logger.error(f"微信返回缺少return_code,原始: {response.text}")
- raise Exception('微信支付接口异常,缺少return_code')
- return_code = return_code_node.text
-
- result_code_node = root.find('result_code')
- result_code = result_code_node.text if result_code_node is not None else None
-
- if return_code == 'SUCCESS' and result_code == 'SUCCESS':
- prepay_id_node = root.find('prepay_id')
- if prepay_id_node is None or prepay_id_node.text is None:
- logger.error(f"统一下单成功但无prepay_id,原始: {response.text}")
- raise Exception('微信支付接口异常,缺少prepay_id')
- prepay_id = prepay_id_node.text
- 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'
-
- pay_sign_data = {
- 'appId': APPID,
- '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,
- 'timeStamp': timestamp,
- 'nonceStr': nonce_str,
- 'package': package,
- 'signType': sign_type,
- 'paySign': pay_sign
- }
- 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 result_code:
- error_details += f", result_code: {result_code}"
- if return_msg is not None and return_msg.text:
- error_details += f", return_msg: {return_msg.text}"
- if err_code is not None and err_code.text:
- error_details += f", err_code: {err_code.text}"
- if err_code_des is not None and err_code_des.text:
- error_details += f", err_code_des: {err_code_des.text}"
- raise Exception(f"微信支付下单失败: {error_details}")
-
- def get_client_ip(self):
- 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'
-
-
-# ==================== 2. 罚款支付回调接口 ====================
-class FaKuanHuitiaoView(View):
-
- """
- 罚款支付回调接口(严格借鉴 YajinHuitiao 接口,参数一个不少)
- 路径:POST /yonghu/fk_huitiao
- """
- 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', '订单不存在')
-
- from products.czjilu_types import is_penalty_czjilu
- if not is_penalty_czjilu(order):
- logger.error(f"非罚款支付订单误入罚款回调: {out_trade_no}, leixing={order.leixing}")
- 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', '金额不一致')
-
- # 7. 更新订单状态为已支付(3)
- order.zhuangtai = 3
- order.save(update_fields=['zhuangtai'])
- logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3")
-
- # 8. 更新罚单状态为已缴纳(2)
- try:
- fadan = None
- m = re.search(r'#fadan:(\d+)', order.shuoming or '')
- if m:
- fadan = Penalty.query.filter(
- id=int(m.group(1)),
- PenalizedUserID=order.yonghuid,
- Status__in=[1, 3],
- ).first()
- if not fadan:
- fadan = Penalty.query.filter(
- PenalizedUserID=order.yonghuid,
- FineAmount=order.jine,
- Status__in=[1, 3],
- ).order_by('-CreateTime').first()
- if fadan:
- fadan.Status = 2 # 已缴纳
- fadan.save(update_fields=['Status'])
- logger.info(f"罚单 {fadan.id} 已自动标记为已缴纳")
-
- success, msg = process_fadan_fenhong(fadan.id)
- if not success:
- logger.error(f"罚单 {fadan.id} 分红处理失败: {msg}")
- else:
- logger.info(f"罚单 {fadan.id} 分红处理完成: {msg}")
-
- except Exception as e:
- logger.error(f"更新罚单状态失败: {str(e)}", exc_info=True)
-
- # 9. 更新收支记录表
- try:
- jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
- from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
- repair_wechat_income_if_missing(
- jine_decimal,
- getattr(order, 'club_id', None),
- biz_ref=f'cz:{order.dingdan_id}',
- )
-
- logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元")
-
- except Exception as e:
- # 收支记录更新失败不影响主流程,只记录日志
- logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
-
- # 10. 返回成功响应给微信
- 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):
- """验证微信支付签名(完全照搬 YajinHuitiao 的 verify_signature)"""
- 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):
- """返回微信支付回调响应(完全照搬 YajinHuitiao 的 wechat_response)"""
- xml_response = f"""
-
-
-"""
-
- logger.info(f"返回微信响应: {return_code} - {return_msg}")
- return HttpResponse(xml_response, content_type='application/xml')
-
-
-# ==================== 3. 支付轮询接口 ====================
-class FaKuanPayPollView(APIView):
- """罚款支付轮询接口(借鉴 YajinLunxun)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- dingdanid = request.data.get('dingdanid')
- if not dingdanid:
- return Response({'code': 400, 'message': '订单ID不能为空'})
-
- order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
- if order.zhuangtai == 3:
- return Response({'code': 200, 'message': '支付成功'})
- else:
- return Response({'code': 400, 'message': '订单尚未支付'})
- except Czjilu.DoesNotExist:
- return Response({'code': 400, 'message': '订单不存在'})
- except Exception as e:
- logger.error(f"轮询异常: {str(e)}")
- return Response({'code': 500, 'message': '服务器内部错误'})
-
-
-# ==================== 4. 支付失败处理接口 ====================
-class FaKuanPayFailView(APIView):
- """罚款支付失败处理接口(借鉴 YajinShibai)"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- try:
- dingdanid = request.data.get('dingdanid')
- if not dingdanid:
- return Response({'code': 400, 'message': '订单ID不能为空'})
-
- order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
- if order.zhuangtai == 9: # 未支付
- order.delete()
- logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}")
- return Response({'code': 200, 'message': '订单已删除'})
- else:
- return Response({'code': 400, 'message': '订单状态不是未支付,无法删除'})
- except Czjilu.DoesNotExist:
- return Response({'code': 400, 'message': '订单不存在'})
- except Exception as e:
- logger.error(f"支付失败处理异常: {str(e)}")
- return Response({'code': 500, 'message': '服务器内部错误'})
-
-
-
-
-
-
-class KaohePayView(APIView):
- """
- 考核支付下单接口
- 路径:POST /yonghu/khzf
- 参数:
- tag_id: 标签ID
- game_nick: 游戏昵称
- remark: 备注
- reapply: 是否再次申请 (true/false)
- jilu_id: 再次申请时的原记录ID(reapply=true时必传)
- shenheguan_id: 指定考核官ID(可选)
- """
- 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()
-
- logger.info(f"用户 {user.UserUID} 发起考核支付申请,标签ID: {tag_id}, 再次申请: {reapply}")
-
- if not tag_id or not game_nick:
- logger.warning(f"用户 {user.UserUID} 参数不完整: tag_id={tag_id}, game_nick={game_nick}")
- return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'})
-
- # 1. 检查是否有未支付的考核订单
- """unpaid_order = Czjilu.query.filter(
- UserUID=user.UserUID,
- leixing=5,
- zhuangtai=9
- ).exists()
- if unpaid_order:
- logger.warning(f"用户 {user.UserUID} 存在未支付的考核订单")
- return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})"""
-
- # 2. 验证标签
- try:
- chenghao = Chenghao.query.get(id=tag_id)
- logger.info(f"标签验证通过: {chenghao.mingcheng}")
- except Chenghao.DoesNotExist:
- logger.error(f"标签不存在: tag_id={tag_id}")
- return Response({'code': 404, 'message': '标签不存在'})
-
- # 3. 检查用户是否已拥有该标签
- if YonghuChenghao.query.filter(yonghu=user, chenghao=chenghao).exists():
- logger.warning(f"用户 {user.UserUID} 已拥有标签 {chenghao.mingcheng}")
- return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'})
-
- # 4. 检查是否有未完成的申请
- if ShenheJilu.query.filter(
- shenqingren_id=user.UserUID,
- chenghao=chenghao,
- zhuangtai__in=[0, 3] # 审核中、待审核
- ).exists():
- logger.warning(f"用户 {user.UserUID} 有未完成的考核申请")
- return Response({'code': 400, 'message': '您有未完成的考核申请,请等待结果'})
-
- # 5. 计算考核次数和费用
- if reapply and jilu_id:
- last = ShenheJilu.query.filter(
- jilu_id=jilu_id,
- shenqingren_id=user.UserUID,
- zhuangtai=2
- ).first()
- if not last:
- logger.warning(f"用户 {user.UserUID} 再次申请失败:原记录不存在或状态不是未通过,jilu_id={jilu_id}")
- return Response({'code': 400, 'message': '原记录不存在或状态不是未通过'})
- new_cishu = last.cishu + 1
- logger.info(f"再次申请,原记录ID: {jilu_id},新次数: {new_cishu}")
- else:
- max_cishu = ShenheJilu.query.filter(
- shenqingren_id=user.UserUID,
- chenghao=chenghao,
- zhuangtai__in=[1, 2]
- ).aggregate(max=models.Max('cishu'))['max'] or 0
- new_cishu = max_cishu + 1
- logger.info(f"首次申请,计算次数: {new_cishu}")
-
- # 6. 获取本次费用
- fee = get_tag_fee(chenghao, new_cishu)
- logger.info(f"本次考核费用: {fee}元,次数: {new_cishu}")
-
- if fee <= 0:
- logger.warning(f"费用为0,应使用免费申请接口,tag_id={tag_id}, cishu={new_cishu}")
- return Response({'code': 400, 'message': '本次费用为0,请直接使用免费申请接口'})
-
- # 7. 生成订单ID
- timestamp_ms = int(time.time() * 1000)
- timestamp_ns = int(time.time_ns() // 1000) % 1000000
- random_num = random.randint(0, 99)
- dingdanid = f"KH{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
- logger.info(f"生成订单ID: {dingdanid}")
-
- # 8. 创建支付订单(Czjilu)
- from jituan.services.club_write import resolve_club_id_for_write
- order = Czjilu.query.create(
- dingdan_id=dingdanid,
- zhuangtai=9, # 未支付
- yonghuid=user.UserUID,
- jine=float(fee),
- leixing=5, # 5-考核支付
- shuoming=settings.KAOHE_PAY_DESCRIPTION,
- club_id=resolve_club_id_for_write(request, user),
- )
- logger.info(f"创建支付订单成功: {dingdanid}")
-
- # 9. 保存考核支付临时上下文(用于回调时取业务参数)
- try:
- KaohePayTemp.query.create(
- dingdan_id=dingdanid,
- yonghuid=user.UserUID,
- tag_id=tag_id,
- game_nick=game_nick,
- remark=remark,
- reapply=reapply,
- jilu_id=jilu_id if reapply else None,
- shenheguan_id=shenheguan_id,
- cishu=new_cishu,
- fee=fee,
- )
- logger.info(f"保存临时上下文成功: {dingdanid}")
- except Exception as e:
- logger.error(f"保存临时上下文失败: {str(e)}", exc_info=True)
- # 临时表保存失败应该回滚订单
- order.delete()
- return Response({'code': 500, 'message': '系统异常,请稍后重试'})
-
- # 10. 构造 attach 参数(只传订单ID,确保不超127字节)
- attach_str = json.dumps({'oid': dingdanid}, ensure_ascii=False)
- logger.info(f"attach内容: {attach_str},长度: {len(attach_str.encode('utf-8'))}字节")
-
- # 11. 生成微信支付参数
- try:
- pay_params = self.generate_wechat_pay_params(
- dingdanid=dingdanid,
- jine=float(fee),
- openid=user.OpenID,
- attach=attach_str,
- pay_type='kaohe'
- )
- logger.info(f"微信支付参数生成成功: {dingdanid}")
- except Exception as e:
- order.delete() # 下单失败,删除订单记录
- # 同时删除临时表记录
- try:
- KaohePayTemp.query.filter(dingdan_id=dingdanid).delete()
- except Exception as del_e:
- logger.error(f"删除临时表记录失败: {str(del_e)}")
- logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True)
- return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'})
-
- return Response({
- 'code': 200,
- 'message': '支付参数生成成功',
- 'payParams': pay_params,
- 'dingdanid': dingdanid
- })
-
- except Exception as e:
- logger.error(f"考核支付下单异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'message': '服务器内部错误'})
-
- def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type):
- """生成微信JSAPI支付参数"""
- 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 == 'kaohe':
- PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION
- NOTIFY_URL = settings.KAOHE_PAY_NOTIFY_URL
- else:
- raise Exception('未知支付类型')
-
- nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
- total_fee = yuan_to_fen(jine)
- trade_type = 'JSAPI'
- user_ip = self.get_client_ip()
-
- logger.info(f"微信支付下单参数: out_trade_no={dingdanid}, total_fee={total_fee}, openid={openid}, ip={user_ip}")
-
- # 第一次签名(统一下单)
- 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,
- 'attach': attach,
- }
-
- 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}'
- sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
-
- # 构建XML请求体
- xml_data = f"""
-{APPID}
-{PAY_DESCRIPTION}
-{MCHID}
-{nonce_str}
-{NOTIFY_URL}
-{openid}
-{dingdanid}
-{user_ip}
-{total_fee}
-{trade_type}
-
-{sign}
-"""
-
- logger.info(f"微信支付请求XML: {xml_data[:500]}...") # 只打印前500字符
-
- response = requests.post(
- 'https://api.mch.weixin.qq.com/pay/unifiedorder',
- data=xml_data.encode('utf-8'),
- headers={'Content-Type': 'application/xml'},
- timeout=10
- )
-
- logger.info(f"微信支付响应: {response.text}")
-
- root = ET.fromstring(response.content)
-
- # 安全获取return_code
- return_code_node = root.find('return_code')
- if return_code_node is None or return_code_node.text is None:
- logger.error(f"微信返回缺少return_code节点,原始内容: {response.text}")
- raise Exception('微信支付接口异常,缺少return_code')
- return_code = return_code_node.text
-
- # 安全获取result_code
- result_code_node = root.find('result_code')
- if result_code_node is None or result_code_node.text is None:
- logger.error(f"微信返回缺少result_code节点,原始内容: {response.text}")
- raise Exception('微信支付接口异常,缺少result_code')
- result_code = result_code_node.text
-
- if return_code == 'SUCCESS' and result_code == 'SUCCESS':
- prepay_id_node = root.find('prepay_id')
- if prepay_id_node is None or prepay_id_node.text is None:
- logger.error(f"统一下单成功但未返回prepay_id,原始内容: {response.text}")
- raise Exception('微信支付接口异常,缺少prepay_id')
- prepay_id = prepay_id_node.text
-
- 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'
-
- # 第二次签名(前端调起支付用)
- pay_sign_data = {
- 'appId': APPID,
- '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()
-
- logger.info(f"微信支付预下单成功,prepay_id={prepay_id}")
-
- return {
- 'appId': APPID,
- 'timeStamp': timestamp,
- 'nonceStr': nonce_str,
- 'package': package,
- 'signType': sign_type,
- 'paySign': pay_sign,
- }
- else:
- # 安全获取错误信息
- err_code_node = root.find('err_code')
- err_code = err_code_node.text if err_code_node is not None else '未知错误码'
- err_code_des_node = root.find('err_code_des')
- err_code_des = err_code_des_node.text if err_code_des_node is not None else '未知错误描述'
- error_details = f"return_code: {return_code}, result_code: {result_code}, err_code: {err_code}, err_code_des: {err_code_des}"
- logger.error(f"微信支付下单失败: {error_details}")
- 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', '127.0.0.1')
- return ip if ip else '127.0.0.1'
-
-
-class KaohePayCallbackView(View):
- """
- 考核支付回调接口
- 路径:/yonghu/kh_huitiao
- """
-
- def post(self, request):
- try:
- request_body = request.body
- logger.info(f"收到考核支付回调: {request_body.decode('utf-8')}")
-
- root = ET.fromstring(request_body)
-
- # 安全获取return_code
- return_code_node = root.find('return_code')
- if return_code_node is None or return_code_node.text is None:
- logger.error(f"微信回调XML缺少return_code节点,原始内容: {request_body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- return_code = return_code_node.text
-
- # 安全获取result_code
- result_code_node = root.find('result_code')
- if result_code_node is None or result_code_node.text is None:
- logger.error(f"微信回调XML缺少result_code节点,原始内容: {request_body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- result_code = result_code_node.text
-
- # 安全获取out_trade_no
- out_trade_no_node = root.find('out_trade_no')
- if out_trade_no_node is None or out_trade_no_node.text is None:
- logger.error(f"微信回调XML缺少out_trade_no节点,原始内容: {request_body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- out_trade_no = out_trade_no_node.text
-
- # 安全获取transaction_id
- transaction_id_node = root.find('transaction_id')
- if transaction_id_node is None or transaction_id_node.text is None:
- logger.error(f"微信回调XML缺少transaction_id节点,原始内容: {request_body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- transaction_id = transaction_id_node.text
-
- # 安全获取total_fee
- total_fee_node = root.find('total_fee')
- if total_fee_node is None or total_fee_node.text is None:
- logger.error(f"微信回调XML缺少total_fee节点,原始内容: {request_body.decode('utf-8')}")
- return self.wechat_response('FAIL', '数据格式错误')
- total_fee = int(total_fee_node.text)
-
- logger.info(
- f"回调参数解析成功: out_trade_no={out_trade_no}, transaction_id={transaction_id}, total_fee={total_fee}")
-
- if return_code != 'SUCCESS' or result_code != 'SUCCESS':
- logger.warning(f"支付状态异常: return_code={return_code}, result_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', '签名验证失败')
- logger.info(f"签名验证成功,订单号: {out_trade_no}")
-
- # 查找订单
- try:
- order = Czjilu.query.get(dingdan_id=out_trade_no, leixing=5)
- logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.zhuangtai}")
- except Czjilu.DoesNotExist:
- logger.error(f"考核订单不存在: {out_trade_no}")
- return self.wechat_response('FAIL', '订单不存在')
-
- if order.zhuangtai != 9:
- logger.warning(f"订单状态非未支付: {order.zhuangtai}, 订单号: {out_trade_no}")
- if order.zhuangtai == 3:
- logger.info(f"订单已支付,补试收支记账: {out_trade_no}")
- 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')
- return self.wechat_response('FAIL', '订单状态异常')
-
- # 验证金额
- wechat_yuan = total_fee / 100
- if abs(wechat_yuan - float(order.jine)) > 0.01:
- logger.error(f"金额不一致: 订单金额={order.jine}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}")
- return self.wechat_response('FAIL', '金额不一致')
- logger.info(f"金额验证通过: {wechat_yuan}元")
-
- # 更新订单状态为已支付
- order.zhuangtai = 3
- order.save(update_fields=['zhuangtai'])
- logger.info(f"订单状态更新为已支付: {out_trade_no}")
-
- # 从临时表获取业务参数并创建审核记录
- try:
- temp = KaohePayTemp.query.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid)
- logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}")
-
- user = User.query.get(UserUID=order.yonghuid)
- logger.info(f"获取用户信息成功: {user.UserUID}")
-
- success, msg, record = create_shenhe_jilu_from_temp(user, temp)
- if success:
- logger.info(f"创建审核记录成功: jilu_id={record.jilu_id}, 订单号: {out_trade_no}")
- else:
- logger.error(f"创建审核记录失败: {msg}, 订单号: {out_trade_no}")
- # 订单已支付但记录创建失败,需要标记订单异常,人工介入
- order.zhuangtai = 10 # 10表示支付成功但业务处理失败
- order.save(update_fields=['zhuangtai'])
- logger.error(f"订单状态标记为异常(10): {out_trade_no}")
-
- except KaohePayTemp.DoesNotExist:
- logger.error(f"临时表无记录,订单号: {out_trade_no}, 用户ID: {order.yonghuid}")
- # 尝试从attach解析(兼容旧数据)
- attach_element = root.find('attach')
- if attach_element is not None and attach_element.text:
- try:
- attach_data = json.loads(attach_element.text)
- logger.info(f"从attach解析数据成功: {attach_data}")
- # 这里可以保留旧逻辑,但建议只记录日志
- except Exception as e:
- logger.error(f"解析attach失败: {str(e)}")
- except Exception as e:
- logger.error(f"处理临时表或创建审核记录异常: {str(e)}", exc_info=True)
- # 标记订单异常
- order.zhuangtai = 10
- order.save(update_fields=['zhuangtai'])
- logger.error(f"订单状态标记为异常(10): {out_trade_no}")
-
- # 更新收支记录
- try:
- jine_decimal = Decimal(str(order.jine))
- from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
- repair_wechat_income_if_missing(
- jine_decimal,
- getattr(order, 'club_id', None),
- biz_ref=f'cz:{order.dingdan_id}',
- )
- logger.info(f"更新收支记录成功: {jine_decimal}元")
- except Exception as e:
- logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
-
- logger.info(f"考核支付回调处理完成,订单号: {out_trade_no}")
- return self.wechat_response('SUCCESS', 'OK')
-
- except ET.ParseError as e:
- logger.error(f"XML解析失败: {str(e)}, 原始数据: {request.body.decode('utf-8')}")
- 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_node = root.find('sign')
- if sign_node is None or sign_node.text is None:
- logger.error("微信回调XML缺少sign节点")
- return False
- sign = sign_node.text
-
- 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()
-
- result = sign == calculated_sign
- if not result:
- logger.error(f"签名验证失败: 原签名={sign}, 计算签名={calculated_sign}")
- return result
- except Exception as e:
- logger.error(f"签名验证异常: {str(e)}", exc_info=True)
- return False
-
- def wechat_response(self, return_code, return_msg):
- """构造微信回调响应XML"""
- xml = f"""
-
-
-"""
- logger.info(f"返回微信响应: {xml}")
- return HttpResponse(xml, content_type='application/xml')
-
-
-
-
-
-
-
-class KaohePayPollView(APIView):
- """考核支付轮询"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- dingdanid = request.data.get('dingdanid')
- if not dingdanid:
- return Response({'code': 400, 'message': '订单ID不能为空'})
- try:
- order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5)
- if order.zhuangtai == 3:
- return Response({'code': 200, 'message': '支付成功'})
- return Response({'code': 400, 'message': '订单尚未支付'})
- except Czjilu.DoesNotExist:
- return Response({'code': 400, 'message': '订单不存在'})
-
-
-class KaohePayFailView(APIView):
- """考核支付失败处理"""
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- dingdanid = request.data.get('dingdanid')
- if not dingdanid:
- return Response({'code': 400, 'message': '订单ID不能为空'})
- try:
- order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5)
- if order.zhuangtai == 9:
- order.delete()
- return Response({'code': 200, 'message': '订单已删除'})
- return Response({'code': 400, 'message': '订单状态无法删除'})
- except Czjilu.DoesNotExist:
- return Response({'code': 400, 'message': '订单不存在'})
-
-
-
-
-
-
-
-
-class TixianAssetView(APIView):
- """
- 获取用户所有可提现资产及费率
- 请求方式:POST
- 认证:JWT Token
- 返回:
- {
- "code": 200,
- "msg": "success",
- "data": {
- "dashou_yue": "123.45",
- "dashou_yajin": "500.00",
- "shangjia_yue": "0.00",
- "guanshi_fenyong": "67.89",
- "zuzhang_fenyong": "0.00",
- "kaoheguan_fenyong": "0.00",
- "dashou_rate": 0.08,
- "dashou_yajin_rate": 0.05,
- "shangjia_rate": 0.06,
- "guanshi_rate": 0.08,
- "zuzhang_rate": 0.05,
- "kaoheguan_rate": 0.07
- }
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- user = request.user
- # 初始化返回数据,默认全部为0
- data = {
- 'dashou_yue': '0.00',
- 'dashou_yajin': '0.00',
- 'shangjia_yue': '0.00',
- 'guanshi_fenyong': '0.00',
- 'zuzhang_fenyong': '0.00',
- 'kaoheguan_fenyong': '0.00',
- 'dashou_rate': 0.00,
- 'dashou_yajin_rate': 0.00,
- 'shangjia_rate': 0.00,
- 'guanshi_rate': 0.00,
- 'zuzhang_rate': 0.00,
- 'kaoheguan_rate': 0.00,
- }
-
- # ------------------- 打手资产 -------------------
- try:
- dashou = user.DashouProfile
- data['dashou_yue'] = str(dashou.yue)
- data['dashou_yajin'] = str(dashou.yajin)
- except ObjectDoesNotExist:
- pass
-
- # ------------------- 商家资产 -------------------
- try:
- shangjia = user.ShopProfile
- data['shangjia_yue'] = str(shangjia.yue)
- except ObjectDoesNotExist:
- pass
-
- # ------------------- 管事资产 -------------------
- try:
- guanshi = user.GuanshiProfile
- data['guanshi_fenyong'] = str(guanshi.yue)
- except ObjectDoesNotExist:
- pass
-
- # ------------------- 组长资产 -------------------
- try:
- zuzhang = user.ZuzhangProfile
- data['zuzhang_fenyong'] = str(zuzhang.ketixian_jine)
- except ObjectDoesNotExist:
- pass
-
- # ------------------- 审核官(考核官)资产 -------------------
- try:
- kaoheguan = user.ShenheguanProfile
- data['kaoheguan_fenyong'] = str(kaoheguan.yue)
- except ObjectDoesNotExist:
- pass
-
- from jituan.services.club_user import get_user_club_id
- from jituan.services.withdraw_config import build_tixian_asset_rates
-
- club_id = get_user_club_id(user)
- data.update(build_tixian_asset_rates(club_id))
-
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': data
- }, status=status.HTTP_200_OK)
-
-
-
-
-
-class GuanshiContactView(APIView):
- """
- 管事获取自己的邀请人(组长)信息
- 路径: /api/yonghu/guanshi_contact
- 方法: POST
- 权限: JWT认证
- 请求体: 无(仅需携带合法token)
- 成功返回:
- code: 200,
- data: {
- uid: "1234567", // 组长用户ID
- nicheng: "张三", // 组长昵称
- touxiang: "avatar/xxx.jpg" // 组长头像相对路径(前端拼接完整URL)
- }
- """
- permission_classes = [IsAuthenticated]
-
- def post(self, request):
- # 1. 获取当前登录用户(必须已通过JWT认证)
- current_user = request.user # 类型: User
-
- # 2. 校验是否为管事身份(存在管事扩展表记录)
- try:
- guanshi_profile = current_user.GuanshiProfile
- except ObjectDoesNotExist:
- return Response({
- 'code': 403,
- 'msg': '您还不是管事,无法获取组长信息',
- 'data': None
- })
-
- # 3. 获取邀请人ID(管事扩展表中的 yaoqingren 字段存储的是邀请人的 yonghuid)
- inviter_yonghuid = guanshi_profile.yaoqingren
- if not inviter_yonghuid:
- return Response({
- 'code': 404,
- 'msg': '未找到邀请人信息',
- 'data': None
- })
-
- # 4. 根据邀请人用户ID查询用户主表
- try:
- inviter_user = User.query.get(UserUID=inviter_yonghuid)
- except User.DoesNotExist:
- logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}")
- return Response({
- 'code': 404,
- 'msg': '邀请人账号异常',
- 'data': None
- })
-
- # 5. 获取邀请人昵称(从老板扩展表取,若不存在则给默认值)
- try:
- inviter_nicheng = inviter_user.BossProfile.nickname
- except ObjectDoesNotExist:
- inviter_nicheng = '组长' # 兜底昵称
-
- # 6. 构造返回数据
- response_data = {
- 'uid': inviter_user.UserUID,
- 'nicheng': inviter_nicheng,
- 'touxiang': inviter_user.Avatar or '' # 头像相对路径
- }
-
- return Response({
- 'code': 200,
- 'msg': '获取成功',
- 'data': response_data
- })
-
-
-
-
-
-
-
-
-
-logger = logging.getLogger(__name__) # 恢复区间?
-
-def generate_tixian_id():
- """生成唯一提现订单号:TX + 时间戳(13位) + 4位随机数"""
- timestamp = str(int(time.time() * 1000))
- rand = ''.join(random.choices(string.digits, k=4))
- return f'TX{timestamp}{rand}'
-
-
-class TixianShenqingV3View(APIView):
- """
- 自动提现收款接口(新流程第二步)
- POST /yonghu/tixiansq
- 必须传 shenhe_danhao;审核状态须为 6=待收款;申请时已扣款,本接口不再扣款
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip()
- if shenhe_danhao:
- return process_audit_collect(request)
- return Response({
- 'code': 7,
- 'msg': '缺少审核单号,请先提交提现申请(/yonghu/zddksh)并等待审核通过后再收款',
- })
-
-
-
-class TixianCallbackV3View(APIView):
- """
- 微信转账结果回调接口(最终状态通知)
- 处理微信异步发送的转账成功/失败结果
- """
- permission_classes = [] # 无需认证,签名验证保证安全
-
- def post(self, request):
- headers = request.headers
- body = request.body.decode('utf-8')
-
- # 1. 验证签名(多商户依次验签)
- from utils.wechat_v3 import resolve_callback_club_id, decrypt_callback_ciphertext
- callback_club_id = resolve_callback_club_id(headers, body)
- if not callback_club_id:
- return Response({'code': 'FAIL', 'message': '签名验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 解密数据
- try:
- data = json.loads(body)
- resource = data.get('resource')
- if not resource:
- return Response({'code': 'FAIL', 'message': 'resource不存在'}, status=status.HTTP_400_BAD_REQUEST)
-
- associated_data = resource.get('associated_data', '')
- nonce = resource.get('nonce')
- ciphertext = resource.get('ciphertext')
- if not nonce or not ciphertext:
- return Response({'code': 'FAIL', 'message': '解密字段缺失'}, status=status.HTTP_400_BAD_REQUEST)
-
- plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext, club_id=callback_club_id)
- callback_data = json.loads(plaintext)
- except Exception as e:
- logger.error(f"回调解密失败: {str(e)}", exc_info=True)
- return Response({'code': 'FAIL', 'message': f'解密失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 3. 获取关键信息
- out_bill_no = callback_data.get('out_bill_no') # 商户提现单号(即 tixian_id)
- transfer_status = callback_data.get('transfer_status') # SUCCESS / FAIL
- wechat_transfer_no = callback_data.get('transfer_no') # 微信转账单号
- fail_reason = callback_data.get('fail_reason', '')
-
- if not out_bill_no:
- return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 4. 幂等处理:只更新状态为0(处理中)的记录,已最终状态不再处理
- with transaction.atomic():
- try:
- auto_record = TixianAutoRecord.objects.select_for_update().get(tixian_id=out_bill_no)
- except TixianAutoRecord.DoesNotExist:
- # 记录不存在,视为已处理,返回成功
- return Response({'code': 'SUCCESS', 'message': 'ok'})
-
- # 如果已经是最新状态,直接返回成功(幂等)
- if auto_record.zhuangtai != 0:
- return Response({'code': 'SUCCESS', 'message': 'already processed'})
-
- if transfer_status == 'SUCCESS':
- mark_transfer_success(
- auto_record,
- wechat_transfer_no=wechat_transfer_no,
- with_accounting=True,
- )
-
- else: # transfer_status == 'FAIL'
- auto_record.zhuangtai = 2
- auto_record.fail_reason = fail_reason or '微信转账失败'
- auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
- release_collect_quota_for_record(auto_record)
-
- if not auto_record.shenhe_danhao:
- logger.error('回调失败但打款记录无审核单号 bill=%s', out_bill_no)
- else:
- try:
- audit = TixianShenheJilu.objects.select_for_update().get(
- shenhe_danhao=auto_record.shenhe_danhao
- )
- close_audit_wechat_failed(
- audit, None, auto_record.fail_reason,
- )
- except TixianShenheJilu.DoesNotExist:
- logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}')
-
- return Response({'code': 'SUCCESS', 'message': 'ok'})
-
-
-
-class TixianQueRenAutoView(APIView):
- """
- 用户确认提现结果
- POST /yonghu/tixianqr
- 以微信官方查单状态为准:SUCCESS 落库成功;FAIL/CANCELLED 关闭审核单+退款;进行中则提示等待
- """
- permission_classes = [permissions.IsAuthenticated]
-
- def post(self, request):
- try:
- tixian_id = request.data.get('tixian_id')
- result = request.data.get('result') # 1成功 0失败/取消
-
- if not tixian_id or result is None:
- return Response({'code': 1, 'msg': '缺少参数'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- result = int(result)
- except ValueError:
- return Response({'code': 2, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- if result not in [0, 1]:
- return Response({'code': 3, 'msg': 'result值无效'}, status=status.HTTP_400_BAD_REQUEST)
-
- user_main = request.user
-
- with transaction.atomic():
- try:
- auto_record = TixianAutoRecord.objects.select_for_update().get(
- tixian_id=tixian_id,
- yonghuid=user_main.UserUID,
- )
- except TixianAutoRecord.DoesNotExist:
- return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- if result == 1:
- if auto_record.zhuangtai == 1:
- return Response({'code': 0, 'msg': '提现成功', 'data': None})
-
- wx_data = query_wechat_transfer_bill(tixian_id)
- if wx_data is None:
- return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
-
- if wx_data.get('_not_found'):
- return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
-
- state = (wx_data.get('state') or '').upper()
- if state in WX_STATE_SUCCESS:
- mark_transfer_success(
- auto_record,
- wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'),
- with_accounting=True,
- )
- return Response({'code': 0, 'msg': '提现成功', 'data': None})
-
- if state in WX_STATE_FAIL:
- fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or state
- auto_record.zhuangtai = 2
- auto_record.fail_reason = str(fail_reason)[:500]
- auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
- release_collect_quota_for_record(auto_record)
- if auto_record.shenhe_danhao:
- audit = TixianShenheJilu.objects.select_for_update().get(
- shenhe_danhao=auto_record.shenhe_danhao,
- )
- close_audit_wechat_failed(audit, None, auto_record.fail_reason)
- return Response({'code': 400, 'msg': '微信已确认转账失败,该笔提现已关闭,请联系客服处理'})
-
- if state in WX_STATE_WAIT or state in WX_STATE_CANCELING:
- return Response({'code': 12, 'msg': '收款处理中,请在微信中完成或稍后再试'})
-
- return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
-
- # 用户报告取消/失败:以微信查单为准,不本地盲目标记
- if auto_record.zhuangtai == 2:
- return Response({'code': 0, 'msg': '提现已取消', 'data': None})
- if auto_record.zhuangtai == 1:
- return Response({'code': 0, 'msg': '提现成功', 'data': None})
-
- wx_data = query_wechat_transfer_bill(tixian_id)
- if wx_data is None:
- return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试,切勿重复操作'})
-
- if wx_data.get('_not_found'):
- return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
-
- state = (wx_data.get('state') or '').upper()
- if state in WX_STATE_SUCCESS:
- mark_transfer_success(
- auto_record,
- wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'),
- with_accounting=True,
- )
- return Response({'code': 0, 'msg': '提现成功', 'data': None})
-
- if state in WX_STATE_FAIL:
- fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or '用户取消收款'
- auto_record.zhuangtai = 2
- auto_record.fail_reason = str(fail_reason)[:500]
- auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
- release_collect_quota_for_record(auto_record)
- if auto_record.shenhe_danhao:
- audit = TixianShenheJilu.objects.select_for_update().get(
- shenhe_danhao=auto_record.shenhe_danhao,
- )
- close_audit_wechat_failed(audit, None, auto_record.fail_reason)
- return Response({'code': 0, 'msg': '提现已关闭,请联系客服处理'})
-
- if state in WX_STATE_WAIT or state in WX_STATE_CANCELING:
- return Response({'code': 12, 'msg': '收款仍在进行中,请在微信中操作或稍后再试'})
-
- return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
-
- except Exception as e:
- logger.error(f'提现确认异常: {str(e)}', exc_info=True)
- return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
diff --git a/users/views/__init__.py b/users/views/__init__.py
new file mode 100644
index 0000000..f8be391
--- /dev/null
+++ b/users/views/__init__.py
@@ -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,
+)
diff --git a/users/views/admin.py b/users/views/admin.py
new file mode 100644
index 0000000..288e3fd
--- /dev/null
+++ b/users/views/admin.py
@@ -0,0 +1,2890 @@
+"""users.views.admin - 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 AdminLoginView(APIView):
+ """
+ 管理员登录接口
+ 路径:/yonghu/adlog/
+ 方法:POST
+ 字段:zhanghao(账号), mima(密码), ip(二次验证码)
+ """
+
+ # 限制匿名用户访问频率,防止暴力破解
+ throttle_classes = [AnonRateThrottle]
+ # 允许任何人访问(登录接口不需要认证)
+ permission_classes = [AllowAny]
+
+ def post(self, request):
+ """
+ 处理管理员登录请求
+ 安全策略:任何错误都返回401,不透露具体信息
+ """
+ try:
+ # 1. 获取客户端真实IP和用户代理信息
+ kehuduan_ip = huoquKehuduanIP(request)
+ yonghu_daili = huoquYonghuDaili(request)
+
+
+ # 2. 获取前端传递的数据
+ data = request.data
+
+ # 检查必需字段是否存在
+ bixu_ziduan = ['zhanghao', 'mima', 'ip']
+ for ziduan in bixu_ziduan:
+ if ziduan not in data:
+ # 字段缺失,返回401
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 提取并清理数据
+ zhanghao = data.get('zhanghao', '').strip()
+ mima = data.get('mima', '').strip()
+ erci_yanzhengma = data.get('ip', '').strip() # 二次验证码(前端叫ip)
+
+ # 简单验证字段非空
+ if not zhanghao or not mima or not erci_yanzhengma:
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 3. 使用事务确保数据一致性
+ with transaction.atomic():
+ # 【优化点1】使用select_related一次性获取管理员扩展表
+ # 只查询管理员类型的用户(密码用 CheckPassword 验证,不能在 ORM 中直接匹配哈希)
+ user = User.query.filter(
+ Phone=zhanghao, # 手机号作为账号
+ AdminProfile__isnull=False # 必须是管理员类型
+ ).select_related('AdminProfile').first()
+
+ # 如果用户不存在或不是管理员,返回401
+ if not user:
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 验证密码(bcrypt 哈希验证)
+ if not user.CheckPassword(mima):
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 【优化点2】检查管理员扩展表是否存在
+ if not hasattr(user, 'AdminProfile'):
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 【优化点3】验证二次密码(管理员扩展表中的password字段)
+ admin_profile = user.AdminProfile
+ if admin_profile.password != erci_yanzhengma:
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 更新用户登录信息
+ # 获取当前时间
+ now_time = timezone.now()
+
+ # 处理IP地址长度问题
+ # 如果原ip字段长度不够,截取前11个字符(保持兼容性)
+
+ cunchu_ip = kehuduan_ip
+ #print(cunchu_ip)
+
+ # 更新用户信息
+ user.IP = cunchu_ip # 存储客户端IP
+ user.UserLastLoginDate = now_time # 更新最后登录时间
+ user.save()
+
+ # 【优化点4】生成JWT token(使用和微信登录相同的方式)
+ # 使用djangorestframework-simplejwt生成token
+ refresh = RefreshToken.for_user(user)
+ token = str(refresh.access_token)
+
+ # 5. 准备返回数据
+ # 返回用户基本信息(可根据需要调整)
+ response_data = {
+ 'token': token,
+ 'yonghuid': user.UserUID,
+ 'phone': user.Phone or '',
+ 'user_type': user.UserType,
+ 'last_login_time': now_time.strftime('%Y-%m-%d %H:%M:%S') if now_time else '',
+ }
+
+ # 可以添加管理员特定信息
+ if hasattr(user, 'AdminProfile'):
+ response_data['wechat'] = user.AdminProfile.wechat or ''
+
+ return Response({
+ 'code': 0,
+ 'msg': '登录成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 【安全点】任何异常都返回401,不暴露具体错误信息
+ # 记录日志用于排查问题,但前端只收到通用错误信息
+ # 这里可以添加日志记录,比如:
+ logging.error(f'管理员登录异常: {str(e)}', exc_info=True)
+
+ return Response(
+ {'code': 401, 'msg': '登录失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+
+class AdminFinancialDataView(APIView):
+ """
+ 管理员财务数据接口
+ 路径:/yonghu/adhq/
+ 方法:POST
+ 认证:JWT Token
+ 权限:仅管理员
+ 返回:收支记录数据
+ """
+
+ # 使用JWT认证
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ """
+ 处理管理员财务数据请求
+ 安全策略:任何错误都返回401,不透露具体信息
+ """
+ try:
+ # 1. 获取前端传递的数据
+ data = request.data
+
+ # 检查必需字段
+ if 'zhanghao' not in data:
+ return Response(
+ {'code': 401, 'msg': '请求参数错误', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ qianzhanghao = data.get('zhanghao', '').strip()
+
+ # 简单验证字段非空
+ if not qianzhanghao:
+ return Response(
+ {'code': 401, 'msg': '请求参数错误', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 2. 获取当前登录用户(JWT认证后,request.user就是User实例)
+ dangqianyonghu = request.user
+
+ # 3. 验证用户类型是否为admin
+ if not hasattr(dangqianyonghu, 'UserType') or dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'msg': '权限不足', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证前端传递的账号与当前用户的phone字段是否一致
+ if not dangqianyonghu.Phone or dangqianyonghu.Phone != qianzhanghao:
+ return Response(
+ {'code': 401, 'msg': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在(反向查询)
+ # 使用高效的ORM查询,避免额外的数据库查询
+ try:
+ # 使用select_related一次性获取管理员扩展表
+ # UserType 是 @property 非数据库字段,用 AdminProfile__isnull=False 替代
+ yonghu_xiangxi = User.query.select_related('AdminProfile').get(
+ pk=dangqianyonghu.pk,
+ AdminProfile__isnull=False
+ )
+
+ # 检查管理员扩展表是否存在
+ if not hasattr(yonghu_xiangxi, 'AdminProfile'):
+ return Response(
+ {'code': 401, 'msg': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ except User.DoesNotExist:
+ return Response(
+ {'code': 401, 'msg': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 查询收支记录表(按俱乐部 xq,旧管理员端默认)
+ from jituan.constants import CLUB_ID_DEFAULT
+ from jituan.services.szjilu_accounting import get_szjilu_snapshot
+ huizongshuju = get_szjilu_snapshot(CLUB_ID_DEFAULT)
+
+ # 7. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': huizongshuju
+ })
+
+ except Exception as e:
+ # 【安全策略】任何异常都返回401,不暴露具体错误信息
+ # 这里可以记录日志,但不返回给前端
+ # logging.error(f'管理员财务数据接口异常: {str(e)}', exc_info=True)
+
+ return Response(
+ {'code': 401, 'msg': '获取失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+
+class AdGetOrderTypes(APIView):
+ """
+ 后台获取订单类型列表接口
+ 前端调用地址: /shangpin/getOrderTypes
+ 前端需要字段: id, biaoti, tupian_url
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+
+ if not qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. 身份验证(按你说的方式)
+ dangqianyonghu = request.user
+
+ # 验证手机号
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证管理员身份
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 3. 验证管理员扩展表
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 4. 查询订单类型(商品类型)表
+ # 注意:这里需要根据你实际的模型名调整
+ # 如果模型名不是ShangpinType,请告诉我正确的模型名
+
+ # 假设模型名是ShangpinLeixing(因为我之前看到你有商品类型表)
+ # 你需要告诉我确切的模型名
+ try:
+ # 请替换为你的实际模型名
+ # 只查询前端需要的三个字段
+ shangpin_types = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
+
+ # 构建响应数据
+ type_list = [
+ {
+ 'id': item.id,
+ 'biaoti': item.jieshao or '',
+ 'tupian_url': item.tupian_url or ''
+ }
+ for item in shangpin_types
+ ]
+
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': type_list
+ })
+
+ except Exception as e:
+ logger.error(f"查询商品类型表失败: {str(e)}")
+ # 如果表不存在,返回空数组
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': []
+ })
+
+ except Exception as e:
+ logger.error(f"获取订单类型异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdGetOrderList(APIView):
+ """
+ 后台订单列表获取接口
+ 前端调用地址: /yonghu/adddhq
+ 前端需要字段: dingdan_id, jieshao, zhuangtai, leixing, fadanpingtai, jiage, tupian_url, fadanshijian
+ 统计字段: stats.platform.total, stats.platform.completed, stats.merchant.total, stats.merchant.completed
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取参数(严格按前端传的字段名)
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 30))
+
+ # 搜索相关参数
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ # 筛选相关参数(严格按前端字段名)
+ fadanpingtai = request.data.get('fadanpingtai') # 前端叫 fadanpingtai
+ leixing = request.data.get('leixing') # 前端叫 leixing
+ zhuangtai = request.data.get('zhuangtai', '') # 前端叫 zhuangtai
+
+ if not qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. 身份验证(与你说的完全一样)
+ dangqianyonghu = request.user
+
+ # 验证手机号
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证管理员身份
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 3. 验证管理员扩展表
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 5. 构建查询条件(严格按前端字段名映射到数据库字段)
+ query_conditions = Q()
+
+ # 判断是否为搜索模式
+ is_search_mode = bool(dingdan_id)
+
+ if is_search_mode:
+ # 搜索模式:只按订单ID查询
+ query_conditions &= Q(OrderID=dingdan_id)
+ else:
+ # 筛选模式
+
+ # 发单平台筛选(前端: fadanpingtai -> 数据库: Platform)
+ if fadanpingtai is not None:
+ try:
+ fadanpingtai_int = int(fadanpingtai)
+ if fadanpingtai_int in [1, 2]:
+ query_conditions &= Q(Platform=fadanpingtai_int)
+ except (ValueError, TypeError):
+ pass
+
+ # 订单类型筛选(前端: leixing -> 数据库: ProductTypeID)
+ if leixing is not None:
+ try:
+ leixing_int = int(leixing)
+ query_conditions &= Q(ProductTypeID=leixing_int)
+ except (ValueError, TypeError):
+ pass
+
+ # 订单状态筛选(前端: zhuangtai -> 数据库: Status)
+ if zhuangtai and zhuangtai != 'all':
+ try:
+ # 处理多个状态,如 "1,7"
+ status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
+ if status_list:
+ query_conditions &= Q(Status__in=status_list)
+ except (ValueError, TypeError):
+ pass
+
+ # 6. 获取统计数据(平台订单和商家订单)
+ # 注意:这里统计所有订单,不考虑筛选条件
+ try:
+ # 平台订单统计 (Platform=1)
+ platform_total = Order.query.filter(Platform=1).count()
+ platform_completed = Order.query.filter(Platform=1, Status=3).count()
+
+ # 商家订单统计 (Platform=2)
+ merchant_total = Order.query.filter(Platform=2).count()
+ merchant_completed = Order.query.filter(Platform=2, Status=3).count()
+
+ platform_stats = {
+ 'total': platform_total,
+ 'completed': platform_completed
+ }
+ merchant_stats = {
+ 'total': merchant_total,
+ 'completed': merchant_completed
+ }
+ except Exception as e:
+ logger.warning(f"统计数据查询失败: {str(e)}")
+ platform_stats = {'total': 0, 'completed': 0}
+ merchant_stats = {'total': 0, 'completed': 0}
+
+ # 7. 执行分页查询(只查询主表,不查扩展表)
+ start_time = time.time()
+
+ # 计算分页
+ offset = (page - 1) * page_size
+
+ # 查询符合条件的订单总数
+ total_count = Order.query.filter(query_conditions).count()
+
+ # 查询当前页的数据(只查询主表,只查询前端需要的字段)
+ dingdan_list = Order.query.filter(query_conditions).only(
+ 'OrderID', 'Description', 'Status', 'ProductTypeID',
+ 'Platform', 'Amount', 'ImageURL', 'CreateTime'
+ ).order_by('-CreateTime')[offset:offset + page_size]
+
+ query_time = time.time() - start_time
+ logger.info(f"订单查询完成,耗时: {query_time:.3f}s,查询条数: {len(dingdan_list)}")
+
+ # 8. 构建响应数据(严格按前端需要的字段)
+ result_list = []
+
+ for dingdan in dingdan_list:
+ # 注意:这里字段名必须与前端完全一致
+ order_data = {
+ 'dingdan_id': dingdan.OrderID or '',
+ 'jieshao': dingdan.Description or '',
+ 'zhuangtai': dingdan.Status or 1,
+ 'leixing': dingdan.ProductTypeID or 0, # 前端需要 leixing
+ 'fadanpingtai': dingdan.Platform or 1, # 前端需要 fadanpingtai
+ 'jiage': float(dingdan.Amount) if dingdan.Amount else 0.00, # 价格对应 Amount 字段
+ 'tupian_url': dingdan.ImageURL or '', # 图片对应 ImageURL 字段
+ 'fadanshijian': dingdan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan.CreateTime else ''
+ }
+ result_list.append(order_data)
+
+ # 9. 判断是否有更多数据
+ current_loaded_count = offset + len(result_list)
+ has_more = len(result_list) >= page_size and current_loaded_count < total_count
+
+ # 10. 构建完整响应(严格按前端需要的结构)
+ response_data = {
+ 'stats': {
+ 'platform': platform_stats,
+ 'merchant': merchant_stats
+ },
+ 'list': result_list,
+ 'hasMore': has_more,
+ 'total': total_count,
+ 'currentPage': page,
+ 'pageSize': page_size
+ }
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ logger.error(f"获取订单列表异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdYaoQingDaShou(APIView):
+ """
+ 后台获取邀请码接口(用于邀请打手)
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /yonghu/adyqds
+ 参数: {
+ "zhanghao": "管理员账号"
+ }
+ 返回: {
+ "code": 0,
+ "message": "成功",
+ "data": {
+ "yaoqingma": "邀请码"
+ }
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+
+ if not qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 获取用户ID(yonghuid),如果获取不到就用'123456'
+ yonghuid_str = getattr(dangqianyonghu, 'yonghuid', '123456')
+
+ # 7. 尝试获取或创建管事扩展表
+ try:
+ # 尝试获取现有的管事扩展表
+ guanshi_profile = dangqianyonghu.GuanshiProfile
+ except AttributeError:
+ # 管事扩展表不存在,创建一个
+ try:
+ # 创建默认的管事扩展表
+ guanshi_profile = UserGuanshi.query.create(
+ user=dangqianyonghu,
+ yaoqingma='', # 暂时为空,后面生成
+ dianhua='', # 默认空
+ wechat='', # 默认空
+ yaogingshuliang=0, # 邀请数量为0
+ zhuangtai=1, # 正常状态
+ jinrichongzhi=0, # 今日充值0
+ jinyuechongzhi=0, # 今月充值0
+ chongzhifenrun=0, # 充值分佣0
+ yue=0 # 余额0
+ )
+ #logger.info(f"为管理员{dangqianyonghu.UserUID}创建了管事扩展表")
+ except Exception as e:
+ logger.error(f"创建管事扩展表失败: {str(e)}")
+ return Response(
+ {'code': 500, 'message': '创建用户信息失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ # 8. 检查是否已有邀请码
+ existing_yaoqingma = guanshi_profile.yaoqingma
+
+ if existing_yaoqingma and existing_yaoqingma.strip():
+ # 已有邀请码,直接返回
+ return Response({
+ 'code': 0,
+ 'message': '成功获取邀请码',
+ 'data': {
+ 'yaoqingma': existing_yaoqingma
+ }
+ })
+
+ # 9. 生成新的邀请码
+ try:
+ # 生成邀请码
+ new_yaoqingma = CreateInvitationCode(str(yonghuid_str))
+
+ # 验证邀请码格式
+ if not VerifyInvitationCode(new_yaoqingma):
+ raise Exception('邀请码格式验证失败')
+
+ except ImportError:
+ # 如果导入失败,使用简单方法生成邀请码
+ timestamp = str(int(time.time()))
+ random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
+ new_yaoqingma = f"GL{timestamp[-6:]}{random_str}"
+
+ logger.warning(f"使用简单方法生成邀请码: {new_yaoqingma}")
+ except Exception as e:
+ logger.error(f"生成邀请码失败: {str(e)}")
+ return Response(
+ {'code': 500, 'message': '生成邀请码失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ # 10. 保存邀请码到数据库
+ try:
+ with transaction.atomic():
+ guanshi_profile.yaoqingma = new_yaoqingma
+ guanshi_profile.save()
+
+ logger.info(f"管理员{dangqianyonghu.UserUID}生成邀请码: {new_yaoqingma}")
+ except Exception as e:
+ logger.error(f"保存邀请码失败: {str(e)}")
+ return Response(
+ {'code': 500, 'message': '保存邀请码失败', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ # 11. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '成功生成邀请码',
+ 'data': {
+ 'yaoqingma': new_yaoqingma
+ }
+ })
+
+ except Exception as e:
+ logger.error(f"获取邀请码接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdGuanLiYongHu(APIView):
+ """
+ 后台用户管理列表接口
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /yonghu/adgl
+ 参数: {
+ "zhanghao": "管理员账号",
+ "user_type": "用户类型(1-4)",
+ "keyword": "搜索关键词(可选)",
+ "page": "页码(从1开始)",
+ "page_size": "每页数量"
+ }
+ 返回: {
+ "code": 0,
+ "message": "成功",
+ "data": {
+ "list": [用户列表],
+ "stats": {"1": 总数, "2": 总数, "3": 总数, "4": 总数},
+ "has_more": true/false
+ }
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ user_type = request.data.get('user_type')
+ keyword = request.data.get('keyword', '').strip()
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 30))
+
+ # 验证参数
+ if not qianzhanghao or not user_type:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 根据用户类型选择查询的模型
+ user_type_int = int(user_type)
+
+ if user_type_int not in [1, 2, 3, 4]:
+ return Response(
+ {'code': 400, 'message': '用户类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 7. 统计四种用户类型的总数
+ stats = self._get_user_stats()
+
+ # 8. 根据用户类型查询用户列表
+ user_list, has_more = self._query_users_by_type(
+ user_type_int, keyword, page, page_size
+ )
+
+ # 9. 构建返回数据
+ return Response({
+ 'code': 0,
+ 'message': '成功',
+ 'data': {
+ 'list': user_list,
+ 'stats': stats,
+ 'has_more': has_more
+ }
+ })
+
+ except ValueError:
+ logger.error("参数类型错误")
+ return Response(
+ {'code': 400, 'message': '参数类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except Exception as e:
+ logger.error(f"用户管理列表接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ def _get_user_stats(self):
+ """
+ 获取四种用户类型的统计数量
+ """
+ try:
+
+ stats = {
+ '1': UserBoss.query.count(),
+ '2': UserDashou.query.count(),
+ '3': UserGuanshi.query.count(),
+ '4': UserShangjia.query.count()
+ }
+
+ return stats
+
+ except Exception as e:
+ logger.error(f"获取用户统计失败: {str(e)}")
+ return {'1': 0, '2': 0, '3': 0, '4': 0}
+
+ def _query_users_by_type(self, user_type_int, keyword, page, page_size):
+ """
+ 根据用户类型查询用户列表
+ 返回: (用户列表, 是否还有更多数据)
+ """
+ try:
+ # 计算偏移量
+ offset = (page - 1) * page_size
+
+ if user_type_int == 1:
+ return self._query_boss_users(keyword, offset, page_size)
+ elif user_type_int == 2:
+ return self._query_dashou_users(keyword, offset, page_size)
+ elif user_type_int == 3:
+ return self._query_guanshi_users(keyword, offset, page_size)
+ elif user_type_int == 4:
+ return self._query_shangjia_users(keyword, offset, page_size)
+ else:
+ return [], False
+
+ except Exception as e:
+ logger.error(f"查询用户列表失败: {str(e)}")
+ return [], False
+
+ def _query_boss_users(self, keyword, offset, limit):
+ """查询普通用户(老板)"""
+ try:
+ queryset = UserBoss.query.select_related('user')
+
+ if keyword:
+ queryset = queryset.filter(
+ Q(user__UserUID__icontains=keyword) |
+ Q(nickname__icontains=keyword)
+ )
+
+ # 获取总数
+ total_count = queryset.count()
+
+ # 获取当前页的数据
+ users = list(queryset.order_by('-CreateTime')[offset:offset + limit])
+
+ # 计算是否有更多数据
+ current_count = len(users)
+ has_more = (offset + current_count) < total_count
+
+
+
+ # 构建返回数据
+ user_list = []
+ for boss in users:
+ user_main = boss.user
+
+ user_data = {
+ 'yonghuid': user_main.UserUID,
+ 'avatar': user_main.Avatar or '',
+ 'nicheng': boss.nickname or '',
+ }
+
+ user_list.append(user_data)
+
+ return user_list, has_more
+
+ except Exception as e:
+ logger.error(f"查询老板用户失败: {str(e)}")
+ return [], False
+
+ def _query_dashou_users(self, keyword, offset, limit):
+ """查询打手用户"""
+ try:
+ queryset = UserDashou.query.select_related('user')
+
+ if keyword:
+ queryset = queryset.filter(
+ Q(user__UserUID__icontains=keyword) |
+ Q(nicheng__icontains=keyword) |
+ Q(chenghao__icontains=keyword)
+ )
+
+ # 获取总数
+ total_count = queryset.count()
+
+ # 获取当前页的数据
+ dashou_list = list(queryset.order_by('-CreateTime')[offset:offset + limit])
+
+ # 计算是否有更多数据
+ current_count = len(dashou_list)
+ has_more = (offset + current_count) < total_count
+
+
+
+ # 构建返回数据
+ user_list = []
+ for dashou in dashou_list:
+ user_main = dashou.user
+
+ user_data = {
+ 'yonghuid': user_main.UserUID,
+ 'avatar': user_main.Avatar or '',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'nicheng': dashou.nicheng or '',
+ 'zhuangtai': dashou.zhuangtai,
+ }
+
+ user_list.append(user_data)
+
+ return user_list, has_more
+
+ except Exception as e:
+ logger.error(f"查询打手用户失败: {str(e)}")
+ return [], False
+
+ def _query_guanshi_users(self, keyword, offset, limit):
+ """查询管事用户"""
+ try:
+ queryset = UserGuanshi.query.select_related('user')
+
+ if keyword:
+ queryset = queryset.filter(
+ Q(user__UserUID__icontains=keyword)
+ )
+
+ # 获取总数
+ total_count = queryset.count()
+
+ # 获取当前页的数据
+ guanshi_list = list(queryset.order_by('-CreateTime')[offset:offset + limit])
+
+ # 计算是否有更多数据
+ current_count = len(guanshi_list)
+ has_more = (offset + current_count) < total_count
+
+
+
+ # 构建返回数据
+ user_list = []
+ for guanshi in guanshi_list:
+ user_main = guanshi.user
+
+ user_data = {
+ 'yonghuid': user_main.UserUID,
+ 'avatar': user_main.Avatar or '',
+ 'zhanghaozhuangtai': guanshi.zhuangtai,
+ 'nicheng': f'管事{user_main.UserUID}',
+ }
+
+ user_list.append(user_data)
+
+ return user_list, has_more
+
+ except Exception as e:
+ logger.error(f"查询管事用户失败: {str(e)}")
+ return [], False
+
+ def _query_shangjia_users(self, keyword, offset, limit):
+ """查询商家用户"""
+ try:
+ queryset = UserShangjia.query.select_related('user')
+
+ if keyword:
+ queryset = queryset.filter(
+ Q(user__UserUID__icontains=keyword) |
+ Q(nicheng__icontains=keyword)
+ )
+
+ # 获取总数
+ total_count = queryset.count()
+
+ # 获取当前页的数据
+ shangjia_list = list(queryset.order_by('-CreateTime')[offset:offset + limit])
+
+ # 计算是否有更多数据
+ current_count = len(shangjia_list)
+ has_more = (offset + current_count) < total_count
+
+
+
+ # 构建返回数据
+ user_list = []
+ for shangjia in shangjia_list:
+ user_main = shangjia.user
+
+ user_data = {
+ 'yonghuid': user_main.UserUID,
+ 'avatar': user_main.Avatar or '',
+ 'zhanghaozhuangtai': shangjia.zhuangtai,
+ 'nicheng': shangjia.nicheng or '',
+ }
+
+ user_list.append(user_data)
+
+ return user_list, has_more
+
+ except Exception as e:
+ logger.error(f"查询商家用户失败: {str(e)}")
+ return [], False
+
+
+class CfGuanLi(APIView):
+ """
+ 后台处罚管理列表接口
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /yonghu/cfgl
+ 参数: {
+ "zhanghao": "管理员账号",
+ "qingqiu_tongji": true/false, # 是否只请求统计信息
+ "sqzhuangtai": 0 或 [1,2], # 处罚申请状态筛选(可选)
+ "page": "页码(从1开始)", # 分页参数
+ "page_size": "每页数量" # 分页参数
+ }
+ 返回: {
+ "code": 0,
+ "message": "成功",
+ "data": {
+ "list": [处罚记录列表],
+ "zongshu": 总数,
+ "daichuli": 待处理数量,
+ "yichuli": 已处理数量,
+ "haiyougengduo": true/false
+ }
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ qingqiu_tongji = request.data.get('qingqiu_tongji', False)
+ sqzhuangtai = request.data.get('sqzhuangtai')
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 30))
+
+ # 验证参数
+ if not qianzhanghao:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 7. 获取统计信息(总是需要统计)
+ zongshu = PenaltyRecord.query.count()
+ daichuli = PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count()
+ yichuli = PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count()
+
+ # 8. 如果只请求统计信息,直接返回
+ if qingqiu_tongji:
+ return Response({
+ 'code': 0,
+ 'message': '成功',
+ 'data': {
+ 'zongshu': zongshu,
+ 'daichuli': daichuli,
+ 'yichuli': yichuli
+ }
+ })
+
+ # 9. 构建查询集
+ queryset = PenaltyRecord.query.all()
+
+ # 10. 根据状态筛选
+ if sqzhuangtai is not None:
+ if isinstance(sqzhuangtai, list):
+ # 如果是数组,表示多个状态
+ queryset = queryset.filter(ApplyStatus__in=sqzhuangtai)
+ else:
+ # 单个状态
+ queryset = queryset.filter(ApplyStatus=sqzhuangtai)
+
+ # 11. 获取总数(用于分页计算)
+ total_count = queryset.count()
+
+ # 12. 计算分页偏移量
+ offset = (page - 1) * page_size
+
+ # 13. 分页查询
+ chufa_list = list(queryset.order_by('-CreateTime')[offset:offset + page_size])
+
+ # 🔴【新增】高效获取图片信息
+ if chufa_list:
+ # 收集所有需要查询的组合
+ dingdan_ids = []
+ yonghu_combinations = [] # 保存 (dingdan_id, yonghuid) 组合
+
+ for chufa in chufa_list:
+ if chufa.OrderID:
+ dingdan_ids.append(chufa.OrderID)
+ # 商家图片组合
+ if chufa.ApplicantID:
+ yonghu_combinations.append((chufa.OrderID, chufa.ApplicantID))
+ # 打手图片组合
+ if chufa.PlayerID:
+ yonghu_combinations.append((chufa.OrderID, chufa.PlayerID))
+
+ # 去重
+ dingdan_ids = list(set(dingdan_ids))
+ yonghu_combinations = list(set(yonghu_combinations))
+
+ # 🔴【新增】批量查询图片表(高效查询,避免循环)
+ tupian_mapping = {}
+ if yonghu_combinations:
+ # 构建查询条件
+ query_conditions = Q()
+ for dingdan_id, yonghuid in yonghu_combinations:
+ query_conditions |= Q(OrderID=dingdan_id, UserID=yonghuid)
+
+ # 批量查询
+ tupian_queryset = PenaltyEvidenceImage.query.filter(query_conditions).values(
+ 'OrderID', 'UserID', 'ImageURL'
+ )
+
+ # 构建映射:key为"OrderID_UserID",value为图片URL列表
+ for item in tupian_queryset:
+ key = f"{item['OrderID']}_{item['UserID']}"
+ if key not in tupian_mapping:
+ tupian_mapping[key] = []
+ if item['ImageURL']:
+ tupian_mapping[key].append(item['ImageURL'])
+ else:
+ tupian_mapping = {}
+
+ # 🔴【修改】14. 构建返回数据 - 添加图片和申诉理由字段
+ chufa_data = []
+ for chufa in chufa_list:
+ # 🔴【新增】获取商家证据图片
+ zhengju_tupian = []
+ if chufa.OrderID and chufa.ApplicantID:
+ key = f"{chufa.OrderID}_{chufa.ApplicantID}"
+ zhengju_tupian = tupian_mapping.get(key, [])
+
+ # 🔴【新增】获取打手申诉图片
+ shensu_tupian = []
+ if chufa.OrderID and chufa.PlayerID:
+ key = f"{chufa.OrderID}_{chufa.PlayerID}"
+ shensu_tupian = tupian_mapping.get(key, [])
+
+ chufa_data.append({
+ 'dashouid': chufa.PlayerID,
+ 'qingqiuid': chufa.ApplicantID,
+ 'chuliid': chufa.ProcessorID,
+ 'cfliyou': chufa.PenaltyReason,
+ 'sqzhuangtai': chufa.ApplyStatus,
+ 'bhliyou': chufa.RejectReason,
+ 'jifen': chufa.DeductedPoints,
+ 'dingdan_id': chufa.OrderID,
+ 'CreateTime': chufa.CreateTime,
+ 'UpdateTime': chufa.UpdateTime,
+ # 🔴【新增】申诉理由字段
+ 'ssliyou': chufa.AppealReason or '',
+ # 🔴【新增】商家证据图片
+ 'zhengju_tupian': zhengju_tupian,
+ # 🔴【新增】打手申诉图片
+ 'shensu_tupian': shensu_tupian,
+ })
+
+ # 15. 计算是否还有更多数据
+ current_count = len(chufa_list)
+ haiyougengduo = (offset + current_count) < total_count
+
+ return Response({
+ 'code': 0,
+ 'message': '成功',
+ 'data': {
+ 'list': chufa_data,
+ 'zongshu': zongshu,
+ 'daichuli': daichuli,
+ 'yichuli': yichuli,
+ 'haiyougengduo': haiyougengduo
+ }
+ })
+
+ except ValueError:
+ return Response(
+ {'code': 400, 'message': '参数类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except Exception as e:
+ logger.error(f"处罚管理列表接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdTongYiChuFa(APIView):
+ """
+ 后台同意/驳回处罚接口(已修复重复记录问题)
+ 权限: JWT Token认证 + 管理员身份验证
+ 前端调用: POST /yonghu/adtycf
+ 参数: {
+ "zhanghao": "管理员账号",
+ "dingdan_id": "订单ID",
+ "caozuo": 1或2, # 1=同意处罚,2=拒绝处罚
+ "bohui_liyou": "拒绝理由" # 拒绝处罚时必填
+ }
+ 返回: {
+ "code": 0,
+ "message": "成功",
+ "data": null
+ }
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ try:
+ # 1. 获取前端参数
+ qianzhanghao = request.data.get('zhanghao', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ caozuo = request.data.get('caozuo') # 1=同意,2=拒绝
+ bohui_liyou = request.data.get('bohui_liyou', '').strip()
+
+ # 验证参数
+ if not qianzhanghao or not dingdan_id or caozuo is None:
+ return Response(
+ {'code': 400, 'message': '参数不完整', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 验证操作类型
+ if caozuo not in [1, 2]:
+ return Response(
+ {'code': 400, 'message': '操作类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 如果是拒绝处罚,需要拒绝理由
+ if caozuo == 2 and not bohui_liyou:
+ return Response(
+ {'code': 400, 'message': '拒绝处罚需要填写理由', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 2. JWT Token身份验证
+ dangqianyonghu = request.user
+
+ # 3. 验证手机号是否匹配
+ if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 4. 验证用户类型是否为管理员
+ if dangqianyonghu.UserType != 'admin':
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 5. 验证管理员扩展表是否存在
+ try:
+ guanliyuan = dangqianyonghu.AdminProfile
+ except AttributeError:
+ return Response(
+ {'code': 401, 'message': '身份验证失败', 'data': None},
+ status=status.HTTP_401_UNAUTHORIZED
+ )
+
+ # 6. 使用事务保证数据一致性
+ with transaction.atomic():
+ # 8. 🔴 修复:查询处罚记录(处理重复记录问题)
+ # 使用 filter().first() 而不是 get(),以防出现多条记录
+ chufajilu = PenaltyRecord.query.filter(
+ OrderID=dingdan_id,
+ ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中状态
+ ).order_by('-CreateTime').first() # 取最新的一条
+
+ if not chufajilu:
+ # 如果没有待处理的处罚记录,检查是否有已处理的
+ exists_processed = PenaltyRecord.query.filter(OrderID=dingdan_id).exists()
+ if exists_processed:
+ return Response(
+ {'code': 400, 'message': '该处罚记录已处理', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ else:
+ return Response(
+ {'code': 404, 'message': '处罚记录不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 9. 验证处罚记录状态(必须是待处理或申诉中状态才能处理)
+ if chufajilu.ApplyStatus not in [0, 3]: # 🔴【修改】包括待处理(0)和申诉中(3)
+ return Response(
+ {'code': 400, 'message': '该处罚记录已处理', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 10. 🔴 修复:检查是否有重复的待处理/申诉中记录,并记录警告
+ duplicate_count = PenaltyRecord.query.filter(
+ OrderID=dingdan_id,
+ ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中
+ ).count()
+
+ if duplicate_count > 1:
+ # 记录警告日志
+ logger.warning(f"订单ID {dingdan_id} 存在 {duplicate_count} 条待处理/申诉中处罚记录,处理最新的一条")
+
+ # 🔴 可选:将其他重复的待处理/申诉中记录标记为已拒绝
+ # 防止用户重复提交相同订单的处罚申请
+ duplicate_records = PenaltyRecord.query.filter(
+ OrderID=dingdan_id,
+ ApplyStatus__in=[0, 3] # 🔴【修改】包括待处理和申诉中
+ ).exclude(id=chufajilu.id)
+
+ for dup in duplicate_records:
+ dup.ApplyStatus = 2 # 已拒绝
+ dup.ProcessorID = qianzhanghao
+ dup.RejectReason = "系统自动拒绝:存在重复处罚记录"
+ dup.save()
+
+ # 11. 根据操作类型处理
+ if caozuo == 1:
+ # 同意处罚 - 不扣分,只改状态
+ # 11.1 检查打手是否存在
+ if not chufajilu.PlayerID:
+ return Response(
+ {'code': 400, 'message': '被处罚打手ID不存在', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # 11.2 查询打手扩展表(只是验证存在性)
+ try:
+ dashou = UserDashou.query.get(user__UserUID=chufajilu.PlayerID)
+ except UserDashou.DoesNotExist:
+ return Response(
+ {'code': 404, 'message': '被处罚打手不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # 🔴【修改】11.3 同意处罚时不扣分(因为申请时已经扣了),只更新状态
+ chufajilu.ApplyStatus = 1 # 已处罚
+ chufajilu.ProcessorID = qianzhanghao # 处理人ID为管理员账号
+ chufajilu.save()
+
+ # 11.4 更新商家订单扩展表
+ try:
+ dingdan = Order.query.get(OrderID=dingdan_id)
+ if hasattr(dingdan, 'shangjia_kuozhan'):
+ shangjia_kuozhan = dingdan.shangjia_kuozhan
+ shangjia_kuozhan.PenaltyApplyStatus = 1 # 已处罚
+ shangjia_kuozhan.save()
+ except (Order.DoesNotExist, AttributeError):
+ # 如果订单或扩展表不存在,继续执行,不中断
+ pass
+
+ logger.info(f"同意处罚成功:订单ID={dingdan_id},打手ID={chufajilu.PlayerID}")
+
+ elif caozuo == 2:
+ # 拒绝处罚 - 需要返还积分(加分)
+ # 🔴【新增】11.5 返还打手积分(加5分)
+ if chufajilu.DeductedPoints > 0:
+ try:
+ dashou = UserDashou.query.get(user__UserUID=chufajilu.PlayerID)
+
+ # 🔴【新增】安全验证:必须积分小于等于5分才允许加分
+ if dashou.jifen <= 5: # 判断积分是否小于等于5分
+ dashou.jifen += chufajilu.DeductedPoints # 加5分
+ dashou.save()
+ logger.info(
+ f"拒绝处罚,给打手{dashou.user.UserUID}返还积分{chufajilu.DeductedPoints}分,当前积分{dashou.jifen}")
+ else:
+ # 积分已超过5分,可能是异常情况,记录警告但不中断处理
+ logger.warning(
+ f"拒绝处罚时打手积分异常:打手ID={chufajilu.PlayerID},当前积分{dashou.jifen}已超过5分,不再加分")
+ except UserDashou.DoesNotExist:
+ # 打手不存在,记录错误,但继续处理状态
+ logger.error(f"拒绝处罚时打手不存在:打手ID={chufajilu.PlayerID}")
+
+ # 11.6 更新处罚记录
+ chufajilu.ApplyStatus = 2 # 已拒绝
+ chufajilu.ProcessorID = qianzhanghao # 处理人ID为管理员账号
+ chufajilu.RejectReason = bohui_liyou # 拒绝理由
+ chufajilu.save()
+
+ # 11.7 更新商家订单扩展表
+ try:
+ dingdan = Order.query.get(OrderID=dingdan_id)
+ if hasattr(dingdan, 'shangjia_kuozhan'):
+ shangjia_kuozhan = dingdan.shangjia_kuozhan
+ shangjia_kuozhan.PenaltyApplyStatus = 2 # 已拒绝
+ shangjia_kuozhan.RejectReason = bohui_liyou # 拒绝理由
+ shangjia_kuozhan.save()
+ except (Order.DoesNotExist, AttributeError):
+ # 如果订单或扩展表不存在,继续执行,不中断
+ pass
+
+ logger.info(f"拒绝处罚成功:订单ID={dingdan_id},拒绝理由={bohui_liyou}")
+
+ # 12. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'message': '处理成功',
+ 'data': None
+ })
+
+ except ValueError:
+ return Response(
+ {'code': 400, 'message': '参数类型错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except Exception as e:
+ logger.error(f"处理处罚接口异常: {str(e)}", exc_info=True)
+ return Response(
+ {'code': 500, 'message': '服务器内部错误', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+
+class AdckyhxqView(APIView):
+ """
+ 获取用户详情接口
+ 前端需要传递: zhanghao, uid, user_type
+ 返回用户信息和会员列表
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # ==================== 管理员身份验证 ====================
+ # 1. 验证JWT Token
+ if not request.user.is_authenticated:
+ return Response({
+ 'code': 401,
+ 'message': '未认证',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 验证用户类型为管理员
+ if request.user.UserType != 'admin':
+ return Response({
+ 'code': 403,
+ 'message': '权限不足',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 验证管理员扩展表存在
+ admin_profile = getattr(request.user, 'AdminProfile', None)
+ if not admin_profile:
+ return Response({
+ 'code': 403,
+ 'message': '管理员信息不完整',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 4. 验证管理员账号(phone)与前端传递的zhanghao一致
+ zhanghao = request.data.get('zhanghao')
+ if not zhanghao or request.user.Phone != zhanghao:
+ return Response({
+ 'code': 401,
+ 'message': '账号验证失败',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+ # ==================== 身份验证结束 ====================
+
+ # 获取业务参数
+ uid = request.data.get('uid') # 用户ID (yonghuid)
+ user_type = request.data.get('user_type') # 用户类型 (1,2,3,4)
+
+ if not uid or not user_type:
+ return Response({
+ 'code': 400,
+ 'message': '参数缺失',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证用户类型
+ try:
+ user_type = int(user_type)
+ if user_type not in [1, 2, 3, 4]:
+ raise ValueError
+ except ValueError:
+ return Response({
+ 'code': 400,
+ 'message': '用户类型无效',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 查询用户主表 - 使用select_related优化查询
+ user_main = User.query.filter(UserUID=uid).first()
+ if not user_main:
+ return Response({
+ 'code': 404,
+ 'message': '用户不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 构建响应数据
+ response_data = {
+ 'user_info': {},
+ 'member_list': [],
+ 'all_member_list': []
+ }
+
+ # 通用字段(所有用户都有)
+ user_info = {
+ 'yonghuid': user_main.UserUID,
+ 'avatar': user_main.Avatar,
+ 'phone': user_main.Phone,
+ 'ip': user_main.IP,
+ 'CreateTime': user_main.UserCreateTime,
+ 'UpdateTime': user_main.UserCreateTime,
+ 'user_type': user_type
+ }
+
+ # 根据用户类型查询扩展信息
+ if user_type == 1: # 老板
+ # 使用select_related预取boss_profile
+ user_main = User.query.filter(UserUID=uid).select_related('BossProfile').first()
+ if getattr(user_main, 'BossProfile', None):
+ boss_profile = user_main.BossProfile
+ user_info.update({
+ 'nickname': boss_profile.nickname,
+ 'zonge': boss_profile.zonge,
+ 'alldingdan': boss_profile.alldingdan,
+ 'alltui': boss_profile.alltui,
+ 'CreateTime_boss': boss_profile.CreateTime
+ })
+
+ elif user_type == 2: # 打手
+ # 使用select_related预取dashou_profile
+ user_main = User.query.filter(UserUID=uid).select_related('DashouProfile').first()
+ if getattr(user_main, 'DashouProfile', None):
+ dashou_profile = user_main.DashouProfile
+
+ # 构建打手信息
+ user_info.update({
+ 'nicheng': dashou_profile.nicheng,
+ 'chenghao': dashou_profile.chenghao,
+ 'zhuangtai': dashou_profile.zhuangtai,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
+ 'jieshao': dashou_profile.jieshao,
+ 'jiedanzongliang': dashou_profile.jiedanzongliang,
+ 'chengjiaozongliang': dashou_profile.chengjiaozongliang,
+ 'tuikuanliang': dashou_profile.tuikuanliang,
+ 'yue': dashou_profile.yue, # 打手可提现余额
+ 'zonge': dashou_profile.zonge, # 打手赚取总额
+ 'dianhua': dashou_profile.dianhua,
+ 'wechat': dashou_profile.wechat,
+ 'yaoqingren': dashou_profile.yaoqingren,
+ 'jinrijiedan': dashou_profile.jinrijiedan,
+ 'jinrishouyi': dashou_profile.jinrishouyi,
+ 'jinyuejiedan': dashou_profile.jinyuejiedan,
+ 'jinyueshouyi': dashou_profile.jinyueshouyi,
+ 'jifen': dashou_profile.jifen,
+ 'yajin': dashou_profile.yajin,
+ 'CreateTime_dashou': dashou_profile.CreateTime
+ })
+
+ # 查询打手的会员信息(只返回未过期的)
+ # 使用select_related预取huiyuan信息(如果有关联的话)
+ member_records = Huiyuangoumai.query.filter(
+ yonghu_id=uid
+ ).order_by('-CreateTime')
+
+ member_list = []
+ for record in member_records:
+ # 调用方法检查是否过期,并更新状态
+ is_expired = record.jiance_shifou_daoqi()
+
+ if not is_expired: # 只返回未过期的
+ # 查询会员详情
+ huiyuan = Huiyuan.query.filter(
+ huiyuan_id=record.huiyuan_id
+ ).first()
+
+ if huiyuan:
+ member_list.append({
+ 'huiyuan_id': record.huiyuan_id,
+ 'huiyuan_name': huiyuan.jieshao,
+ 'daoqi_time': record.daoqi_time,
+ 'is_active': record.huiyuan_zhuangtai == 1,
+ 'CreateTime': record.CreateTime
+ })
+
+ response_data['member_list'] = member_list
+
+ elif user_type == 3: # 管事
+ # 使用select_related预取guanshi_profile
+ user_main = User.query.filter(UserUID=uid).select_related('GuanshiProfile').first()
+ if getattr(user_main, 'GuanshiProfile', None):
+ guanshi_profile = user_main.GuanshiProfile
+ user_info.update({
+ 'nicheng': user_main.Phone, # 管事昵称用手机号
+ 'yaoqingma': guanshi_profile.yaoqingma,
+ 'dianhua': guanshi_profile.dianhua,
+ 'wechat': guanshi_profile.wechat,
+ 'yaogingshuliang': guanshi_profile.yaogingshuliang,
+ 'zhuangtai': guanshi_profile.zhuangtai,
+ 'jinrichongzhi': guanshi_profile.jinrichongzhi,
+ 'jinyuechongzhi': guanshi_profile.jinyuechongzhi,
+ 'chongzhifenrun': guanshi_profile.chongzhifenrun,
+ 'yue': guanshi_profile.yue, # 管事可提现余额
+ 'CreateTime_guanshi': guanshi_profile.CreateTime
+ })
+
+ elif user_type == 4: # 商家
+ # 使用select_related预取shop_profile
+ user_main = User.query.filter(UserUID=uid).select_related('ShopProfile').first()
+ if getattr(user_main, 'ShopProfile', None):
+ shop_profile = user_main.ShopProfile
+ user_info.update({
+ 'nicheng': shop_profile.nicheng,
+ 'zhuangtai': shop_profile.zhuangtai,
+ 'dianhua': shop_profile.dianhua,
+ 'wechat': shop_profile.wechat,
+ 'fabu': shop_profile.fabu,
+ 'tuikuan': shop_profile.tuikuan,
+ 'yue': shop_profile.yue, # 商家余额
+ 'chengjiao': shop_profile.chengjiao,
+ 'jinridingdan': shop_profile.jinridingdan,
+ 'jinriliushui': shop_profile.jinriliushui,
+ 'jinyuedingdan': shop_profile.jinyuedingdan,
+ 'jinyueliushui': shop_profile.jinyueliushui,
+ 'CreateTime_shop': shop_profile.CreateTime
+ })
+
+ # 获取所有会员列表(用于前端下拉选择)
+ all_huiyuan = Huiyuan.query.all().order_by('jiage')
+ all_member_list = [
+ {
+ 'id': huiyuan.huiyuan_id,
+ 'nicheng': huiyuan.jieshao,
+ 'jiage': huiyuan.jiage,
+ 'jieshao': huiyuan.jtjieshao
+ }
+ for huiyuan in all_huiyuan
+ ]
+
+ response_data['user_info'] = user_info
+ response_data['all_member_list'] = all_member_list
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': response_data
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ return Response({
+ 'code': 500,
+ 'message': f'获取用户详情失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class AdcjxgView(APIView):
+ """
+ 修改用户信息接口
+ 前端需要传递: zhanghao, uid, user_type, 以及其他修改的字段
+ 支持打手会员添加功能
+ 敏感字段修改会记录到修改记录表
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # ==================== 管理员身份验证 ====================
+ if not request.user.is_authenticated:
+ return Response({
+ 'code': 401,
+ 'message': '未认证',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ if request.user.UserType != 'admin':
+ return Response({
+ 'code': 403,
+ 'message': '权限不足',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ admin_profile = getattr(request.user, 'AdminProfile', None)
+ if not admin_profile:
+ return Response({
+ 'code': 403,
+ 'message': '管理员信息不完整',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ zhanghao = request.data.get('zhanghao')
+ if not zhanghao or request.user.Phone != zhanghao:
+ return Response({
+ 'code': 401,
+ 'message': '账号验证失败',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+ # ==================== 身份验证结束 ====================
+
+ # 获取业务参数
+ uid = request.data.get('uid')
+ user_type = request.data.get('user_type')
+
+ if not uid or not user_type:
+ return Response({
+ 'code': 400,
+ 'message': '参数缺失',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ user_type = int(user_type)
+ if user_type not in [1, 2, 3, 4]:
+ raise ValueError
+ except ValueError:
+ return Response({
+ 'code': 400,
+ 'message': '用户类型无效',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 查询用户主表
+ user_main = User.query.filter(UserUID=uid).first()
+ if not user_main:
+ return Response({
+ 'code': 404,
+ 'message': '用户不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 使用事务确保数据一致性
+ with transaction.atomic():
+ # 存储修改记录的数据
+ modify_record_data = {
+ 'yonghuid': uid,
+ 'xiugaiid': zhanghao,
+ 'leixing': user_type,
+ 'qitashuoming': '',
+ 'CreateTime': timezone.now()
+ }
+
+ # 标记是否有敏感字段被修改
+ has_sensitive_change = False
+
+ if user_type == 1: # 老板 - 不允许修改
+ return Response({
+ 'code': 403,
+ 'message': '老板信息不允许修改',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ elif user_type == 2: # 打手
+ # 获取或创建打手扩展记录
+ dashou_profile, created = UserDashou.query.get_or_create(
+ user=user_main,
+ defaults={'nicheng': user_main.Phone or f'打手{uid}'}
+ )
+
+ # 记录敏感字段的旧值
+ old_yue = dashou_profile.yue # 打手可提现余额
+ old_zonge = dashou_profile.zonge # 打手提现金额
+ old_jifen = dashou_profile.jifen # 打手积分
+ old_yajin = dashou_profile.yajin # 打手押金
+
+ # 需要更新的字段列表
+ update_fields = [
+ 'chenghao', 'dianhua', 'yue', 'zonge', 'jifen',
+ 'yajin', 'yaoqingren', 'jieshao', 'zhuangtai',
+ 'zaixianzhuangtai', 'zhanghaozhuangtai'
+ ]
+
+ # 记录被修改的字段
+ modified_fields = []
+
+ for field in update_fields:
+ if field in request.data:
+ new_value = request.data.get(field)
+ old_value = getattr(dashou_profile, field)
+
+ # 检查值是否改变
+ if str(new_value) != str(old_value):
+ # 处理特殊字段类型
+ if field in ['yue', 'zonge', 'yajin']:
+ new_value = Decimal(str(new_value))
+ setattr(dashou_profile, field, new_value)
+ elif field in ['jifen']:
+ new_value = int(new_value)
+ setattr(dashou_profile, field, new_value)
+ elif field in ['zhuangtai', 'zaixianzhuangtai', 'zhanghaozhuangtai']:
+ new_value = int(new_value)
+ setattr(dashou_profile, field, new_value)
+ else:
+ setattr(dashou_profile, field, new_value)
+
+ modified_fields.append(field)
+
+ # 记录敏感字段修改
+ if field == 'yue':
+ has_sensitive_change = True
+ modify_record_data['xiugaitijiao'] = new_value # 使用xiugaitijiao字段记录打手余额修改
+ modify_record_data['xiugaitijiaoq'] = old_yue
+ elif field == 'zonge':
+ has_sensitive_change = True
+ # zonge字段在修改记录表中可能没有直接对应,可以记录在备注中
+ modify_record_data['qitashuoming'] += f' 打手总额从{old_zonge}修改为{new_value}'
+ elif field == 'jifen':
+ has_sensitive_change = True
+ modify_record_data['jifen'] = new_value
+ modify_record_data['yjifen'] = old_jifen
+ elif field == 'yajin':
+ has_sensitive_change = True
+ modify_record_data['yajin'] = new_value
+ modify_record_data['yyajin'] = old_yajin
+
+ # 保存打手信息
+ dashou_profile.save()
+
+ # 记录被修改的非敏感字段
+ non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'zonge', 'jifen', 'yajin']]
+ if non_sensitive_fields:
+ modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
+
+ elif user_type == 3: # 管事
+ # 获取或创建管事扩展记录
+ guanshi_profile, created = UserGuanshi.query.get_or_create(
+ user=user_main,
+ defaults={'yaoqingma': f'GS{uid[-4:]}'}
+ )
+
+ # 记录敏感字段的旧值
+ old_yue = guanshi_profile.yue # 管事可提现余额
+
+ update_fields = [
+ 'yaogingshuliang', 'chongzhifenrun', 'yue',
+ 'zhuangtai', 'dianhua', 'wechat'
+ ]
+
+ modified_fields = []
+
+ for field in update_fields:
+ if field in request.data:
+ new_value = request.data.get(field)
+ old_value = getattr(guanshi_profile, field)
+
+ if str(new_value) != str(old_value):
+ if field in ['chongzhifenrun', 'yue']:
+ new_value = Decimal(str(new_value))
+ setattr(guanshi_profile, field, new_value)
+ elif field in ['yaogingshuliang']:
+ new_value = int(new_value)
+ setattr(guanshi_profile, field, new_value)
+ elif field in ['zhuangtai']:
+ new_value = int(new_value)
+ setattr(guanshi_profile, field, new_value)
+ else:
+ setattr(guanshi_profile, field, new_value)
+
+ modified_fields.append(field)
+
+ # 记录敏感字段修改
+ if field == 'yue':
+ has_sensitive_change = True
+ modify_record_data['guanshiyue'] = new_value
+ modify_record_data['guanshiyueq'] = old_yue
+ elif field == 'chongzhifenrun':
+ has_sensitive_change = True
+ modify_record_data['qitashuoming'] += f' 管事分佣从{old_value}修改为{new_value}'
+
+ guanshi_profile.save()
+
+ # 记录非敏感字段
+ non_sensitive_fields = [f for f in modified_fields if f not in ['yue', 'chongzhifenrun']]
+ if non_sensitive_fields:
+ modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
+
+ elif user_type == 4: # 商家
+ # 获取或创建商家扩展记录
+ shop_profile, created = UserShangjia.query.get_or_create(
+ user=user_main,
+ defaults={'nicheng': user_main.Phone or f'商家{uid}'}
+ )
+
+ # 记录敏感字段的旧值
+ old_yue = shop_profile.yue # 商家余额
+
+ update_fields = ['yue', 'zhuangtai', 'dianhua', 'wechat']
+
+ modified_fields = []
+
+ for field in update_fields:
+ if field in request.data:
+ new_value = request.data.get(field)
+ old_value = getattr(shop_profile, field)
+
+ if str(new_value) != str(old_value):
+ if field == 'yue':
+ new_value = Decimal(str(new_value))
+ setattr(shop_profile, field, new_value)
+ elif field == 'zhuangtai':
+ new_value = int(new_value)
+ setattr(shop_profile, field, new_value)
+ else:
+ setattr(shop_profile, field, new_value)
+
+ modified_fields.append(field)
+
+ # 记录敏感字段修改
+ if field == 'yue':
+ has_sensitive_change = True
+ modify_record_data['shangjiayue'] = new_value
+ modify_record_data['shangjiayueq'] = old_yue
+
+ shop_profile.save()
+
+ # 记录非敏感字段
+ non_sensitive_fields = [f for f in modified_fields if f != 'yue']
+ if non_sensitive_fields:
+ modify_record_data['qitashuoming'] += f' 修改了字段: {",".join(non_sensitive_fields)}'
+
+ # 处理打手会员添加(只有打手可以添加会员)
+
+ # 🔥🔥🔥 后端修改:支持正负数会员天数
+ # 修改会员处理逻辑部分
+
+ if user_type == 2 and 'huiyuan_id' in request.data and 'days' in request.data:
+ huiyuan_id = request.data.get('huiyuan_id')
+ days = int(request.data.get('days', 0))
+
+ # 🔥 修改:允许正负数,但不能为0
+ if huiyuan_id and days != 0:
+ # 🔥 修改:检查绝对值范围
+ if abs(days) < 1 or abs(days) > 10000:
+ return Response({
+ 'code': 400,
+ 'message': '会员天数必须在1-10000之间(正数增加,负数减少)',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 检查会员是否存在
+ huiyuan = Huiyuan.query.filter(huiyuan_id=huiyuan_id).first()
+ if not huiyuan:
+ return Response({
+ 'code': 404,
+ 'message': '会员不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 检查是否已购买该会员
+ existing_record = Huiyuangoumai.query.filter(
+ yonghu_id=uid,
+ huiyuan_id=huiyuan_id
+ ).first()
+
+ if existing_record:
+ # 🔥🔥🔥 重要修改:支持减少天数
+ # 计算新的到期时间
+ new_daoqi_time = existing_record.daoqi_time + timedelta(days=days)
+
+ # 🔥 安全验证:减少天数后不能小于当前时间前90天(防止过度减少)
+ # 这个限制可以根据业务调整
+ min_allowed_time = timezone.now() - timedelta(days=90)
+ if new_daoqi_time < min_allowed_time:
+ return Response({
+ 'code': 400,
+ 'message': f'减少天数过多,调整后到期时间不能早于 {(min_allowed_time + timedelta(days=90)).strftime("%Y-%m-%d")}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ existing_record.daoqi_time = new_daoqi_time
+ existing_record.save()
+
+ # 记录会员操作
+ has_sensitive_change = True
+ modify_record_data['huiyuants'] = days
+ modify_record_data['huiyuan_id'] = huiyuan_id
+
+ if days > 0:
+ modify_record_data['qitashuoming'] += f' 续费会员 {huiyuan.jieshao} {days}天'
+ # 只有增加天数时才增加购买次数
+ huiyuan.goumai_cishu += 1
+ huiyuan.save()
+ else:
+ modify_record_data['qitashuoming'] += f' 减少会员 {huiyuan.jieshao} {abs(days)}天'
+ # 减少天数时不增加购买次数
+
+ else:
+ # 🔥 修改:新购买时只能增加天数
+ if days > 0:
+ daoqi_time = timezone.now() + timedelta(days=days)
+ from jituan.services.club_user import get_user_club_id
+ new_record = Huiyuangoumai.query.create(
+ huiyuan_id=huiyuan_id,
+ yonghu_id=uid,
+ jieshao=huiyuan.jieshao,
+ daoqi_time=daoqi_time,
+ club_id=get_user_club_id(User.query.filter(UserUID=uid).first()),
+ )
+
+ # 记录会员购买
+ has_sensitive_change = True
+ modify_record_data['huiyuants'] = days
+ modify_record_data['huiyuan_id'] = huiyuan_id
+ modify_record_data['qitashuoming'] += f' 购买会员 {huiyuan.jieshao} {days}天'
+
+ # 更新会员购买次数
+ huiyuan.goumai_cishu += 1
+ huiyuan.save()
+ else:
+ # 🔥 不能为不存在的会员减少天数
+ return Response({
+ 'code': 400,
+ 'message': '用户未购买该会员,无法减少天数',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+
+
+
+
+
+
+
+
+
+
+ # 如果有敏感字段被修改或会员操作,创建修改记录
+ if has_sensitive_change or 'huiyuan_id' in request.data:
+ # 创建修改记录
+ Xiugaijilu.query.create(**modify_record_data)
+ elif modify_record_data.get('qitashuoming', '').strip():
+ # 只有非敏感字段修改,也记录但qitashuoming不同
+ Xiugaijilu.query.create(**modify_record_data)
+
+ return Response({
+ 'code': 0,
+ 'message': '修改成功',
+ 'data': None
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ return Response({
+ 'code': 500,
+ 'message': f'修改失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+def admin_required(view_func):
+ """管理员权限验证装饰器"""
+
+ def wrapped_view(self, request, *args, **kwargs):
+ try:
+ # 1. 验证JWT Token
+ if not request.user.is_authenticated:
+ return Response({
+ 'code': 401,
+ 'message': '未认证',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 验证用户类型为管理员
+ if request.user.UserType != 'admin':
+ return Response({
+ 'code': 403,
+ 'message': '权限不足',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 验证管理员扩展表存在
+ admin_profile = getattr(request.user, 'AdminProfile', None)
+ if not admin_profile:
+ return Response({
+ 'code': 403,
+ 'message': '管理员信息不完整',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 4. 验证管理员账号(phone)与前端传递的zhanghao一致
+ zhanghao = request.data.get('zhanghao')
+ if not zhanghao or request.user.Phone != zhanghao:
+ return Response({
+ 'code': 401,
+ 'message': '账号验证失败',
+ 'data': None
+ }, status=status.HTTP_401_UNAUTHORIZED)
+
+ return view_func(self, request, *args, **kwargs)
+ except Exception as e:
+ return Response({
+ 'code': 500,
+ 'message': f'权限验证异常: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ return wrapped_view
+
+
+class AddtxshView(APIView):
+ """
+ 获取提现审核列表接口
+ 与用户管理页面保持一致的分页逻辑
+ 支持按用户ID搜索,排序改为按申请时间升序(最早申请排最前)
+ """
+ permission_classes = [IsAuthenticated]
+
+ @admin_required
+ def post(self, request):
+ try:
+ # 获取参数
+ zhanghao = request.data.get('zhanghao')
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 5))
+ zhuangtai = int(request.data.get('zhuangtai', 1))
+ leixing = int(request.data.get('leixing', 1))
+ fangshi = request.data.get('fangshi')
+
+ # 🔴【新增】获取搜索参数(用户ID)
+ search_uid = request.data.get('search_uid', '').strip()
+
+ if page < 1:
+ page = 1
+
+ # 计算偏移量(与用户管理页面完全一致)
+ offset = (page - 1) * page_size
+
+ # 构建查询条件
+ query = Q()
+
+ # 状态筛选
+ if zhuangtai == 1:
+ query &= Q(zhuangtai=1)
+ elif zhuangtai == 2:
+ query &= Q(zhuangtai__in=[2, 3])
+
+ # 类型筛选
+ query &= Q(leixing=leixing)
+
+ # 收款方式筛选(可选)
+ if fangshi:
+ query &= Q(fangshi=int(fangshi))
+
+ # 🔴【新增】如果搜索了用户ID,添加条件
+ if search_uid:
+ query &= Q(yonghuid=search_uid)
+
+ # 🔴【修改】排序改为按申请时间升序(最早申请排最前)
+ queryset = Tixianjilu.query.filter(query).order_by('CreateTime')
+
+ # 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录
+ if search_uid:
+ records = list(queryset)
+ total_count = len(records)
+ has_more = False # 搜索模式没有更多数据
+ else:
+ total_count = queryset.count()
+ records = list(queryset[offset:offset + page_size])
+ current_count = len(records)
+ has_more = (offset + current_count) < total_count
+
+ # 获取状态统计(统计与搜索无关,依然按筛选条件统计)
+ status_counts = {
+ 'awaiting': Tixianjilu.query.filter(
+ zhuangtai=1,
+ leixing=leixing
+ ).count(),
+ 'processed': Tixianjilu.query.filter(
+ zhuangtai__in=[2, 3],
+ leixing=leixing
+ ).count()
+ }
+
+ # 构建响应数据
+ withdrawal_list = []
+ for record in records:
+ withdrawal_list.append({
+ 'id': record.id,
+ 'yonghuid': record.yonghuid,
+ 'avatar': record.avatar,
+ 'phone': record.phone,
+ 'nicheng': record.nicheng,
+ 'leixing': record.leixing,
+ 'zhifu': record.zhifu,
+ 'skzhanghao': record.skzhanghao,
+ 'jine': str(record.jine) if record.jine else '0.00',
+ 'zhuangtai': record.zhuangtai,
+ 'fangshi': record.fangshi,
+ 'shenheid': record.shenheid,
+ 'bhliyou': record.bhliyou,
+ 'CreateTime': record.CreateTime,
+ 'UpdateTime': record.UpdateTime
+ })
+
+ return Response({
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': {
+ 'list': withdrawal_list,
+ 'total_count': total_count,
+ 'status_counts': status_counts,
+ 'has_more': has_more
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ return Response({
+ 'code': 500,
+ 'message': f'获取提现列表失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class AdtixianqkView(APIView):
+ """
+ 处理提现申请接口(已更新:添加收支记录表更新)
+ """
+ permission_classes = [IsAuthenticated]
+
+ @admin_required # 假设你有这个装饰器
+ def post(self, request):
+ try:
+ zhanghao = request.data.get('zhanghao')
+ tixian_id = request.data.get('tixian_id')
+ result = int(request.data.get('result', 0))
+ reason = request.data.get('reason', '')
+
+ if not tixian_id:
+ return Response({
+ 'code': 400,
+ 'message': '提现记录ID不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if result not in [2, 3]:
+ return Response({
+ 'code': 400,
+ 'message': '操作结果无效',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 如果是拒绝,需要理由
+ if result == 3 and not reason:
+ return Response({
+ 'code': 400,
+ 'message': '拒绝提现需要提供理由',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取提现记录
+ try:
+ withdrawal = Tixianjilu.query.get(id=tixian_id)
+ except Tixianjilu.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'message': '提现记录不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 检查状态,只有待审核的可以处理
+ if withdrawal.zhuangtai != 1:
+ return Response({
+ 'code': 400,
+ 'message': '该提现申请已被处理',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 使用事务确保数据一致性
+ with transaction.atomic():
+ # 更新提现记录
+ withdrawal.zhuangtai = result
+ withdrawal.shenheid = request.user.UserUID
+
+ if result == 3: # 拒绝
+ withdrawal.bhliyou = reason
+
+ withdrawal.UpdateTime = timezone.now()
+ withdrawal.save()
+
+ # 🟢 新增:如果同意提现(result=2),更新收支记录表
+ if result == 2:
+ try:
+ from jituan.services.szjilu_accounting import apply_szjilu_expense
+ tixian_jine = withdrawal.jine
+ from jituan.services.club_user import get_user_club_id
+ payout_club = get_user_club_id(
+ User.query.filter(UserUID=withdrawal.yonghuid).first()
+ )
+ update_daily_payout(tixian_jine, payout_club)
+ apply_szjilu_expense(tixian_jine, payout_club)
+ except Exception as e:
+ # 如果更新收支记录失败,抛出异常,事务会回滚
+ raise Exception(f"更新收支记录失败: {str(e)}")
+
+ return Response({
+ 'code': 0,
+ 'message': '处理成功',
+ 'data': {
+ 'id': withdrawal.id,
+ 'zhuangtai': withdrawal.zhuangtai,
+ 'shenheid': withdrawal.shenheid,
+ 'UpdateTime': withdrawal.UpdateTime
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ return Response({
+ 'code': 500,
+ 'message': f'处理提现失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class PaihangbangGuanliQueryView(APIView):
+ """
+ 排行榜查询接口 - 管理员专用
+ 安全要求:
+ 1. JWT Token认证
+ 2. 管理员权限验证
+ 3. 参数严格验证
+ 4. 返回统一格式
+ """
+
+ # JWT Token认证
+ permission_classes = [IsAuthenticated]
+
+ @admin_required
+ def post(self, request):
+ """
+ POST请求查询排行榜数据
+ 参数:
+ - zhanghao: 管理员账号
+ - shenfen: 用户身份(必须,2=打手,3=管事,4=商家)
+ - zhouqi: 周期类型(必须,1=日榜,2=月榜)
+ - date: 查询日期(必须,格式:YYYY-MM-DD)
+
+ 返回格式:
+ {
+ "code": 0, # 0=成功,非0=失败
+ "message": "成功",
+ "data": {
+ "list": [...], # 排行榜数据列表
+ "query_info": {...} # 查询信息
+ }
+ }
+ """
+ try:
+ # 1. 获取请求数据
+ data = request.data
+ zhanghao = data.get('zhanghao')
+ shenfen = data.get('shenfen')
+ zhouqi = data.get('zhouqi')
+ date_str = data.get('date')
+
+ # 2. 参数验证
+ if not zhanghao:
+ return Response({
+ 'code': 400,
+ 'message': '管理员账号不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not shenfen:
+ return Response({
+ 'code': 400,
+ 'message': '用户身份不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not zhouqi:
+ return Response({
+ 'code': 400,
+ 'message': '周期类型不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not date_str:
+ return Response({
+ 'code': 400,
+ 'message': '查询日期不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 参数类型和范围验证
+ try:
+ zhouqi_int = int(zhouqi)
+ shenfen_int = int(shenfen)
+ date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
+ except ValueError as e:
+ return Response({
+ 'code': 400,
+ 'message': f'参数格式错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证周期类型范围
+ if zhouqi_int not in [1, 2]:
+ return Response({
+ 'code': 400,
+ 'message': '周期类型必须为1(日榜)或2(月榜)',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证用户身份范围
+ if shenfen_int not in [2, 3, 4]:
+ return Response({
+ 'code': 400,
+ 'message': '用户身份必须为2(打手)、3(管事)或4(商家)',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 构建查询条件
+ query = Q(
+ zhouqi=zhouqi_int,
+ tongji_date=date_obj,
+ shenfen=shenfen_int
+ )
+
+ # 5. 执行查询(按排名升序)
+ records = RankingRecord.query.filter(query).order_by('paiming')
+
+ # 6. 格式化返回数据
+ data_list = []
+ for record in records:
+ # 根据用户身份确定指标值单位
+ zhizhi_danwei = self._get_zhizhi_danwei(shenfen_int)
+
+ data_list.append({
+ 'paiming': record.paiming,
+ 'yonghuid': record.yonghuid,
+ 'nicheng': record.nicheng,
+ 'avatar': record.avatar or '',
+ 'zhizhi': float(record.zhizhi),
+ 'zhizhi_danwei': zhizhi_danwei,
+ })
+
+ # 7. 返回成功响应
+ response_data = {
+ 'code': 0,
+ 'message': '查询成功',
+ 'data': {
+ 'list': data_list,
+ 'total_count': len(data_list),
+ 'query_info': {
+ 'zhouqi': zhouqi_int,
+ 'zhouqi_text': '日榜' if zhouqi_int == 1 else '月榜',
+ 'shenfen': shenfen_int,
+ 'shenfen_text': self._get_shenfen_text(shenfen_int),
+ 'date': date_str,
+ }
+ }
+ }
+
+ 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)
+
+ def _get_shenfen_text(self, shenfen):
+ """获取身份文本"""
+ shenfen_map = {2: '打手', 3: '管事', 4: '商家'}
+ return shenfen_map.get(shenfen, '未知')
+
+ def _get_zhizhi_danwei(self, shenfen):
+ """获取指标值单位"""
+ danwei_map = {2: '元', 3: '人', 4: '元'}
+ return danwei_map.get(shenfen, '')
+
+
+class PaihangbangRiqiliebiaoView(APIView):
+ """
+ 获取排行榜可查询日期列表 - 管理员专用
+ 返回当前排行榜表中存在的日期(日榜和月榜分开)
+ """
+
+ # JWT Token认证
+ permission_classes = [IsAuthenticated]
+
+ @admin_required
+ def post(self, request):
+ """
+ POST请求获取可查询日期列表
+ 参数:
+ - zhanghao: 管理员账号(用于验证)
+
+ 返回格式:
+ {
+ "code": 0,
+ "message": "成功",
+ "data": {
+ "ribang_dates": ["2024-01-15", "2024-01-14", ...], # 日榜日期列表
+ "yuebang_dates": ["2024-01-01", "2023-12-01", ...], # 月榜日期列表
+ }
+ }
+ """
+ try:
+ # 1. 获取请求数据
+ data = request.data
+ zhanghao = data.get('zhanghao')
+
+ if not zhanghao:
+ return Response({
+ 'code': 400,
+ 'message': '管理员账号不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 查询日榜日期(最近3个月内的)
+ three_months_ago = timezone.now().date() - timedelta(days=90)
+
+ ribang_dates = RankingRecord.query.filter(
+ zhouqi=1, # 日榜
+ tongji_date__gte=three_months_ago # 只查询最近3个月
+ ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date')
+
+ # 3. 查询月榜日期(最近12个月内的)
+ twelve_months_ago = timezone.now().date() - timedelta(days=365)
+
+ yuebang_dates = RankingRecord.query.filter(
+ zhouqi=2, # 月榜
+ tongji_date__gte=twelve_months_ago # 只查询最近12个月
+ ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date')
+
+ # 4. 格式化日期
+ ribang_date_list = [date.strftime('%Y-%m-%d') for date in ribang_dates]
+ yuebang_date_list = [date.strftime('%Y-%m-%d') for date in yuebang_dates]
+
+ # 5. 返回成功响应
+ response_data = {
+ 'code': 0,
+ 'message': '获取成功',
+ 'data': {
+ 'ribang_dates': ribang_date_list,
+ 'yuebang_dates': yuebang_date_list,
+ 'ribang_count': len(ribang_date_list),
+ 'yuebang_count': len(yuebang_date_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)
+
+
+class AdKftjView(APIView):
+ """
+ 添加客服接口
+ 权限:管理员JWT
+ 请求:POST /yonghu/adkftj
+ 参数:{
+ "zhanghao": "管理员账号",
+ "phone": "客服手机号(必填)",
+ "password": "密码(必填)",
+ "erjimima": "二级密码(必填)",
+ "nicheng": "昵称(可选)"
+ }
+ 返回:code=0
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 参数
+ zhanghao = request.data.get('zhanghao', '').strip()
+ phone = request.data.get('phone', '').strip()
+ password = request.data.get('password', '').strip()
+ erjimima = request.data.get('erjimima', '').strip()
+ nicheng = request.data.get('nicheng', '').strip()
+
+ if not all([zhanghao, phone, password, erjimima]):
+ return Response({'code': 400, 'message': '手机号、密码、二级密码不能为空'})
+
+ # 2. 管理员验证(同前)
+ current_user = request.user
+ if current_user.Phone != zhanghao or current_user.UserType != 'admin':
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ try:
+ admin_profile = current_user.AdminProfile
+ except:
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 检查手机号是否已被使用
+ if User.query.filter(Phone=phone).exists():
+ return Response({'code': 400, 'message': '该手机号已被注册'})
+
+ # 4. 生成唯一 yonghuid
+ def generate_yonghuid():
+ for _ in range(10):
+ timestamp = str(int(time.time()))[-5:]
+ rand = str(random.randint(0, 99)).zfill(2)
+ uid = timestamp + rand
+ if len(uid) == 7 and not User.query.filter(UserUID=uid).exists():
+ return uid
+ raise Exception('无法生成唯一用户ID')
+
+ with transaction.atomic():
+ # 创建主表用户
+ user = User.query.create(
+ UserUID=generate_yonghuid(),
+ UserName=f'kefu_{phone}',
+ Phone=phone,
+ OpenID=f'kefu_{phone}' # 临时openid
+ )
+ user.SetPassword(password)
+ user.save(update_fields=['UserPassword'])
+ # 创建客服扩展表
+ import bcrypt as _bcrypt
+ erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
+ kefu = UserKefu.query.create(
+ user=user,
+ nicheng=nicheng or f'客服{user.UserUID}',
+ erjimima=erjimima_hashed,
+ zhuangtai=1,
+ jinrichuli=0,
+ jinyuechuli=0,
+ zongchuli=0
+ )
+
+ return Response({'code': 0, 'message': '添加成功'})
+
+
+class AdKfglView(APIView):
+ """
+ 客服管理列表接口
+ 权限:管理员JWT
+ 请求:POST /yonghu/adkfgl
+ 参数:{
+ "zhanghao": "管理员账号",
+ "keyword": "搜索关键词(可选,匹配客服ID或昵称)"
+ }
+ 返回:{
+ "code": 0,
+ "data": {
+ "list": [客服对象...]
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 参数获取
+ zhanghao = request.data.get('zhanghao', '').strip()
+ keyword = request.data.get('keyword', '').strip()
+
+ if not zhanghao:
+ return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 管理员身份验证
+ current_user = request.user
+ if not hasattr(current_user, 'Phone') or str(current_user.Phone) != zhanghao:
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType != 'admin':
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ try:
+ admin_profile = current_user.AdminProfile
+ except AttributeError:
+ return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询所有客服(通过扩展表关联主表)
+ queryset = UserKefu.query.select_related('user').all()
+
+ if keyword:
+ queryset = queryset.filter(
+ #Q(user__UserUID__icontains=keyword) |
+ Q(nicheng__icontains=keyword) |
+ Q(user__Phone__icontains=keyword)
+ )
+
+ # 4. 构建返回数据
+ kefu_list = []
+ for kefu in queryset:
+ user = kefu.user
+ kefu_list.append({
+ 'yonghuid': user.Phone,
+ 'phone': user.Phone or '',
+ 'nicheng': kefu.nicheng or '',
+ 'zhuangtai': kefu.zhuangtai,
+ 'jinrichuli': kefu.jinrichuli,
+ 'jinyuechuli': kefu.jinyuechuli,
+ 'zongchuli': kefu.zongchuli,
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {'list': kefu_list}
+ })
+
+
+class AdKfxgView(APIView):
+ """
+ 修改客服信息接口
+ 权限:管理员JWT
+ 请求:POST /yonghu/adkfxg
+ 参数:{
+ "zhanghao": "管理员账号",
+ "uid": "客服ID (yonghuid)",
+ "nicheng": "新昵称(可选)",
+ "phone": "新手机号(可选)",
+ "password": "新密码(可选)",
+ "erjimima": "新二级密码(可选)",
+ "zhuangtai": 0/1(可选)
+ }
+ 返回:code=0
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ zhanghao = request.data.get('zhanghao', '').strip()
+ uid = request.data.get('uid', '').strip()
+
+ if not zhanghao or not uid:
+ return Response({'code': 400, 'message': '参数不完整'})
+
+ # 管理员验证
+ current_user = request.user
+ if current_user.Phone != zhanghao or current_user.UserType != 'admin':
+ return Response({'code': 401, 'message': '身份验证失败'})
+ try:
+ admin_profile = current_user.AdminProfile
+ except:
+ return Response({'code': 401, 'message': '身份验证失败'})
+
+ # 查询客服
+ try:
+ user = User.query.get(Phone=uid)
+ kefu = user.KefuProfile
+ except (User.DoesNotExist, UserKefu.DoesNotExist):
+ return Response({'code': 404, 'message': '客服不存在'})
+
+ # 更新数据
+ with transaction.atomic():
+ # 更新主表字段
+ if 'phone' in request.data:
+ new_phone = request.data.get('phone').strip()
+ if new_phone and User.query.filter(Phone=new_phone).exclude(UserUID=uid).exists():
+ return Response({'code': 400, 'message': '手机号已被使用'})
+ user.Phone = new_phone
+ if 'password' in request.data:
+ user.UserPassword = request.data['password']
+ user.save()
+
+ # 更新扩展表
+ if 'nicheng' in request.data:
+ kefu.nicheng = request.data['nicheng']
+ if 'erjimima' in request.data:
+ import bcrypt as _bcrypt
+ new_erji = request.data['erjimima']
+ kefu.erjimima = _bcrypt.hashpw(new_erji.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
+ if 'zhuangtai' in request.data:
+ kefu.zhuangtai = int(request.data['zhuangtai'])
+ kefu.save()
+
+ return Response({'code': 0, 'message': '修改成功'})
diff --git a/users/views/auth.py b/users/views/auth.py
new file mode 100644
index 0000000..36d8c64
--- /dev/null
+++ b/users/views/auth.py
@@ -0,0 +1,1936 @@
+"""users.views.auth - 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 WechatMiniProgramLoginView(APIView):
+ """
+ 微信小程序登录接口
+ """
+ throttle_classes = [AnonRateThrottle]
+ permission_classes = [AllowAny]
+
+ def post(self, request):
+ try:
+ code = request.data.get('code', '').strip()
+ if not code:
+ return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取微信openid和unionid
+ wechat_data = self.get_wechat_openid(code)
+ if not wechat_data or 'openid' not in wechat_data:
+ error_msg = wechat_data.get('errmsg', '微信授权失败')
+ return Response({'code': 2, 'msg': f'微信登录失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ openid = wechat_data['openid']
+ session_key = wechat_data.get('session_key', '')
+ unionid = wechat_data.get('unionid', '') # 🆕【新增】获取unionid
+
+ # 获取客户端真实IP
+ kehuduan_ip = self.huoquKehuduanIP(request)
+
+ with transaction.atomic():
+ # 创建或获取用户
+ user_main, created = User.objects.select_for_update().get_or_create(
+ OpenID=openid,
+ defaults={'UserUID': self.shengchengYonghuID(), 'UserName': openid}
+ )
+
+ # 保存unionid
+ if unionid and unionid != user_main.UnionID:
+ user_main.UnionID = unionid
+
+ user_main.IP = kehuduan_ip
+ user_main.UserLastLoginDate = timezone.now()
+ user_main.save()
+
+ if created:
+ UserBoss.query.create(user=user_main, nickname='板板大人')
+ # 新用户默认分配消费者角色
+ from users.business_models import UserRole, Role
+ normal_role = Role.objects.filter(RoleName='消费者').first()
+ if normal_role:
+ UserRole.objects.create(
+ UserRoleUUID=uuid.uuid4().bytes,
+ UserUUID=user_main.UserUUID,
+ RoleUUID=normal_role.RoleUUID,
+ CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
+ )
+
+ # 检查扩展表
+ dashou_status = 0
+ shangjia_status = 0
+ guanshi_status = 0
+ boss_nickname = '微信用户'
+
+ try:
+ boss_profile = UserBoss.query.get(user=user_main)
+ boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户'
+ except UserBoss.DoesNotExist:
+ pass
+
+ try:
+ UserDashou.query.get(user=user_main)
+ dashou_status = 1
+ except UserDashou.DoesNotExist:
+ dashou_status = None
+
+ try:
+ UserShangjia.query.get(user=user_main)
+ shangjia_status = 1
+ except UserShangjia.DoesNotExist:
+ shangjia_status = None
+
+ try:
+ UserGuanshi.query.get(user=user_main)
+ guanshi_status = 1
+ except UserGuanshi.DoesNotExist:
+ guanshi_status = None
+
+
+ try:
+ UserZuzhang.query.get(user=user_main)
+ zuzhang_status = 1
+ except UserZuzhang.DoesNotExist:
+ zuzhang_status = None
+
+
+ # 在检查 zuzhang 扩展表之后,插入以下代码
+ try:
+ kg = UserShenheguan.query.get(user=user_main)
+ kaoheguan_status = 1 if kg.zhuangtai == 1 else 0
+ except UserShenheguan.DoesNotExist:
+ kaoheguan_status = 0
+
+ # 生成JWT token
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ # 计算订单数量
+ order_counts = self.jisuanOrderShuliang(user_main.UserUID)
+
+ # 获取群配置
+ group_info = self.huoquQunPeizhi()
+
+ from merchant_ops.services.authz import staff_fields_for_login
+ staff_fields = staff_fields_for_login(user_main)
+
+ # 返回数据
+ response_data = {
+ 'token': token,
+ 'nicheng': boss_nickname,
+ 'uid': user_main.UserUID,
+ 'touxiang': user_main.Avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ 'zuzhangstatus': zuzhang_status,
+ 'kaoheguanstatus': kaoheguan_status,
+ 'dingdantiaoshu': order_counts,
+ 'dashouqun': group_info.get('dashouqun', ''),
+ 'dashouqunid': group_info.get('dashouqunid', ''),
+ 'guanshiqun': group_info.get('guanshiqun', ''),
+ 'guanshiqunid': group_info.get('guanshiqunid', ''),
+ **staff_fields,
+ }
+
+ return Response({'code': 0, 'msg': '登录成功', 'data': response_data})
+
+ except Exception as e:
+ error_openid = locals().get('openid', 'N/A')
+ logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}",
+ exc_info=True)
+
+ return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def get_wechat_openid(self, code):
+ """
+ 调用微信接口获取openid和unionid
+ """
+ try:
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ secret = getattr(settings, 'WEIXIN_SECRET', '')
+
+ if not appid or not secret:
+ raise ValueError('微信配置未设置')
+
+ url = 'https://api.weixin.qq.com/sns/jscode2session'
+ params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'}
+
+ response = requests.get(url, params=params, timeout=10)
+ result = response.json()
+
+ if 'openid' in result:
+ return {'openid': result['openid'], 'session_key': result.get('session_key', ''),
+ 'unionid': result.get('unionid', '')}
+ else:
+ errcode = result.get('errcode', 'unknown')
+ errmsg = result.get('errmsg', '未知错误')
+ return {'errmsg': f'[{errcode}]{errmsg}'}
+
+ except requests.exceptions.Timeout:
+ return {'errmsg': '请求微信接口超时'}
+ except Exception as e:
+ return {'errmsg': f'请求微信接口异常: {str(e)}'}
+
+ def shengchengYonghuID(self):
+ """
+ 生成7位数字用户ID
+ """
+ for _ in range(10):
+ timestamp_part = str(int(time.time()))[-5:].zfill(5)
+ random_part = str(random.randint(0, 99)).zfill(2)
+ user_id = timestamp_part + random_part
+
+ if len(user_id) == 7 and user_id.isdigit():
+ if not User.query.filter(UserUID=user_id).exists():
+ return user_id
+
+ raise Exception('生成用户ID失败')
+
+ def huoquKehuduanIP(self, request):
+ """
+ 获取客户端真实IP地址
+ """
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(',')[0].strip()
+ if ip:
+ return ip
+
+ ip = request.META.get('REMOTE_ADDR', '')
+ return ip if ip else '0.0.0.0'
+
+ def jisuanOrderShuliang(self, yonghuid):
+ """
+ 统计订单数量
+ """
+ try:
+ if not PlatformOrderExt.query.filter(BossID=yonghuid).exists():
+ return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
+
+ dingdan_ids = PlatformOrderExt.query.filter(BossID=yonghuid).values_list('Order__id', flat=True)
+
+ results = Order.query.filter(id__in=dingdan_ids).aggregate(
+ daifuwu=Count(Case(When(Q(Status=1) | Q(Status=7), then=1), output_field=IntegerField())),
+ fuwuzhong=Count(Case(When(Status=2, then=1), output_field=IntegerField())),
+ yiwancheng=Count(Case(When(Status=3, then=1), output_field=IntegerField())),
+ yituikuan=Count(Case(When(Status=5, then=1), output_field=IntegerField()))
+ )
+
+ return {
+ 'daifuwu': results['daifuwu'] or 0,
+ 'fuwuzhong': results['fuwuzhong'] or 0,
+ 'yiwancheng': results['yiwancheng'] or 0,
+ 'yituikuan': results['yituikuan'] or 0
+ }
+
+ except Exception:
+ return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
+
+ def huoquQunPeizhi(self):
+ """
+ 获取群配置信息
+ """
+ try:
+ qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
+
+ result = {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+
+ for config in qun_configs:
+ if config.id == 1:
+ result['dashouqun'] = config.GroupContent or ''
+ result['dashouqunid'] = config.GroupID or ''
+ elif config.id == 2:
+ result['guanshiqun'] = config.GroupContent or ''
+ result['guanshiqunid'] = config.GroupID or ''
+
+ return result
+
+ except Exception:
+ return {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+
+
+class WeixinOfficialCallbackView(APIView):
+ """
+ 微信服务号回调接口
+ 处理用户关注、取消关注等事件
+ 核心:有关注事件就存,不管有没有UnionID
+ """
+ throttle_classes = []
+ permission_classes = [AllowAny]
+ authentication_classes = []
+
+ def get(self, request):
+ """验证服务器配置(微信官方要求)"""
+ try:
+ signature = request.GET.get('signature', '')
+ timestamp = request.GET.get('timestamp', '')
+ nonce = request.GET.get('nonce', '')
+ echostr = request.GET.get('echostr', '')
+
+ token = getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', '')
+
+ if not token:
+ logger.error("服务号Token未配置")
+ return HttpResponse('Token not configured', status=500)
+
+ # 验证签名
+ tmp_list = sorted([token, timestamp, nonce])
+ tmp_str = ''.join(tmp_list)
+ hash_str = hashlib.sha1(tmp_str.encode()).hexdigest()
+
+ if hash_str == signature:
+ return HttpResponse(echostr)
+ else:
+ logger.error(f"签名验证失败")
+ return HttpResponse('Signature verification failed', status=403)
+
+ except Exception as e:
+ logger.error(f"验证服务器异常: {str(e)}")
+ return HttpResponse('Internal Server Error', status=500)
+
+ def post(self, request):
+ """处理事件推送(关注、取关)"""
+ try:
+ xml_str = request.body.decode('utf-8')
+ logger.info(f"收到服务号事件,原始数据(前200字符): {xml_str[:200]}")
+
+ root = ET.fromstring(xml_str)
+
+ msg_type = root.find('MsgType').text
+ from_user = root.find('FromUserName').text # 服务号OpenID
+
+ if msg_type == 'event':
+ event = root.find('Event').text
+
+ if event == 'subscribe':
+ return self.handle_subscribe_event(root, from_user)
+ elif event == 'unsubscribe':
+ return self.handle_unsubscribe_event(root, from_user)
+ else:
+ # 其他事件(如菜单点击)直接返回success
+ return HttpResponse('success')
+
+ # 非事件消息也直接返回success
+ return HttpResponse('success')
+
+ except Exception as e:
+ logger.error(f"处理服务号事件异常: {str(e)}", exc_info=True)
+ # 即使异常也返回success,避免微信服务器重试
+ return HttpResponse('success')
+
+ def handle_subscribe_event(self, root, from_user):
+ """处理用户关注事件 - 简化版:有就存,没有拉倒"""
+ try:
+ # 尝试获取UnionID(有就有,没有也没关系)
+ unionid_elem = root.find('UnionId')
+ unionid = unionid_elem.text if unionid_elem is not None else None
+
+ logger.info(f"用户关注,OpenID: {from_user}, UnionID: {unionid}")
+
+ # 核心逻辑:更新或创建记录
+ obj, created = OfficialAccountUser.query.update_or_create(
+ official_openid=from_user,
+ defaults={
+ 'unionid': unionid,
+ 'is_subscribed': True,
+ 'subscribe_time': timezone.now()
+ }
+ )
+
+ action = "新建" if created else "更新"
+ logger.info(f"{action}用户记录成功: OpenID={from_user}")
+
+ # 回复欢迎消息(可选)
+ return self.generate_text_response(root, "感谢关注星阙网络服务!平台有新订单时您会收到通知。")
+
+ except Exception as e:
+ logger.error(f"处理关注事件异常: {str(e)}", exc_info=True)
+ # 依然返回success,不让微信重试
+ return HttpResponse('success')
+
+ def handle_unsubscribe_event(self, root, from_user):
+ """处理用户取消关注事件 - 简化版:标记为未订阅"""
+ try:
+ updated_count = OfficialAccountUser.query.filter(
+ official_openid=from_user
+ ).update(is_subscribed=False)
+
+ if updated_count > 0:
+ logger.info(f"用户取消关注,已标记为未订阅: {from_user}")
+ else:
+ logger.info(f"用户取消关注,但记录不存在(无需处理): {from_user}")
+
+ return HttpResponse('success')
+
+ except Exception as e:
+ logger.error(f"处理取消关注事件异常: {str(e)}", exc_info=True)
+ return HttpResponse('success')
+
+ def generate_text_response(self, root, content):
+ """生成文本消息响应"""
+ try:
+ to_user = root.find('ToUserName').text
+ from_user = root.find('FromUserName').text
+ response_xml = f"""
+
+
+{int(time.time())}
+
+
+"""
+ return HttpResponse(response_xml, content_type='application/xml')
+
+ except Exception as e:
+ logger.error(f"生成回复消息异常: {str(e)}")
+ # 即使生成回复失败,也返回一个空的success
+ return HttpResponse('success')
+
+
+class DashouZhuceView(APIView):
+ """
+ 打手注册接口 (通过管事邀请码)
+ 访问路径: /api/yonghu/dashouzhuce/
+ 请求方法: POST
+ 权限要求: JWT Token认证通过的用户
+ 请求体: { "inviteCode": "AbC123..." }
+ """
+ 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
+ # 注意:这里request.user是User实例
+
+ # 2. 检查用户是否已是打手 (使用反向查询,性能最优)
+ try:
+ dashou_profile = current_user.DashouProfile
+ # 用户已是打手,直接返回现有信息
+ return self.fanhuiXianyouDashouXinxi(dashou_profile, current_user)
+ except UserDashou.DoesNotExist:
+ # 用户不是打手,继续注册流程
+ pass
+
+ # 3. 根据邀请码查找对应的管事
+ try:
+ # 使用select_related一次性获取管事及其关联的主表用户信息,性能最高
+ guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma)
+ except UserGuanshi.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'message': '邀请码无效或不存在',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ from jituan.services.invite_guard import assert_invite_same_club
+ ok, msg = assert_invite_same_club(request, guanshi_profile.user, current_user)
+ if not ok:
+ return Response({
+ 'code': 403,
+ 'message': msg,
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 4. 验证管事状态
+
+ # 5. 核心:在数据库事务中创建用户的所有身份
+ try:
+ with transaction.atomic():
+ # 获取管事的用户ID (yonghuid)
+ guanshi_yonghuid = guanshi_profile.user.UserUID
+
+ # 5.1 创建打手扩展表
+ dashou_profile = UserDashou.query.create(
+ user=current_user,
+ nicheng='大手子', # 默认昵称
+ chenghao='普通大手', # 默认称号
+ yaoqingren=guanshi_yonghuid, # 设置邀请人ID
+ # 其他字段全部使用模型定义的默认值
+ )
+
+ # 5.2 创建商家扩展表
+ try:
+ boss_profile = current_user.BossProfile
+ if boss_profile.nickname:
+ boss_nickname = boss_profile.nickname
+ except:
+ pass
+
+
+ # 5.3 创建管事扩展表
+
+ # 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑)
+ # current_user.UserType = 'dashou' # 根据你的业务逻辑决定
+ # current_user.save()
+
+ # 5.5 更新原管事的邀请数量 (使用行锁防止并发问题)
+ # 重新查询并锁定该行记录
+ locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
+ locked_guanshi.yaogingshuliang += 1
+ locked_guanshi.save()
+
+ # ========== 新增:管事每日统计(邀请打手) ==========
+ try:
+ update_guanshi_daily_by_action(
+ yonghuid=guanshi_yonghuid,
+ action=1, # 1 = 邀请打手
+ amount=Decimal('0.00')
+ )
+ logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
+ except Exception as e:
+ logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
+
+ except Exception as e:
+
+ # 事务会自动回滚
+ # 这里可以记录更详细的日志
+ return Response({
+ 'code': 500,
+ 'message': f'注册过程中发生系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 6. 注册成功,返回信息 (新注册用户没有会员记录,clumber返回空列表)
+ fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True)
+ return Response({
+ 'code': 200,
+ 'message': '注册成功!已开通打手、管事身份。',
+ 'data': fanhui_data
+ })
+
+ def fanhuiXianyouDashouXinxi(self, dashou_profile, current_user):
+ """处理已是打手的用户,返回其现有信息"""
+ fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=False)
+ return Response({
+ 'code': 200,
+ 'message': '您已是打手,返回当前信息。',
+ 'data': fanhui_data
+ })
+
+ def zhuangbeiFanhuiShuju(self, dashou_profile, current_user, is_new=False):
+ """准备返回给前端的打手数据,字段名严格匹配前端"""
+
+
+
+ # 构建基础信息
+ data = {
+ 'dashounicheng': dashou_profile.nicheng,
+ 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
+ 'yongjin': str(dashou_profile.yue), # Decimal转字符串
+ 'zonge': str(dashou_profile.zonge),
+ 'yajin': str(dashou_profile.yajin),
+ 'chenghao': dashou_profile.chenghao,
+ 'jinfen': dashou_profile.jifen,
+ 'chengjiaoliang': dashou_profile.chengjiaozongliang,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'dashouzhuangtai': dashou_profile.zhuangtai,
+ }
+
+ # 查询并处理会员列表 (clumber)
+ # 使用当前用户的yonghuid进行查询
+ huiyuan_goumai_list = []
+ if not is_new: # 如果是新用户,直接返回空列表,无需查询
+ goumai_records = Huiyuangoumai.query.filter(yonghu_id=current_user.UserUID)
+ for record in goumai_records:
+ # 调用方法检查是否到期,此方法内部会更新状态
+ shifou_daoqi = record.jiance_shifou_daoqi()
+ huiyuan_goumai_list.append({
+ 'huiyuan_id': record.huiyuan_id,
+ 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
+ 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
+ 'shifou_daoqi': shifou_daoqi,
+ 'jieshao': record.jieshao
+ })
+
+ data['clumber'] = huiyuan_goumai_list
+ return data
+
+
+class GuanshiYaoqingDashouListView(APIView):
+ """
+ 管事获取已邀请打手列表(支持筛选、搜索、分页)
+ POST /api/yonghu/gshqyqds/
+ Headers: Authorization: Bearer
+ Body:
+ {
+ "page": 1, // 页码,从1开始
+ "pageSize": 30, // 每页条数,最大100
+ "keyword": "", // 搜索关键词(用户ID或昵称)
+ "zhanghaozhuangtai": 1, // 账号状态:1=正常,2=封禁(可选)
+ "zaixianzhuangtai": 1 // 在线状态:1=在线,2=离线(可选)
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ current_user = request.user
+ guanshi_yonghuid = current_user.UserUID
+
+ # 1. 分页参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 30))
+ if page < 1 or page_size < 1:
+ return Response({'code': 400, 'message': '分页参数必须大于0'}, status=400)
+ if page_size > 100:
+ page_size = 100
+
+ # 2. 筛选参数
+ keyword = request.data.get('keyword', '').strip()
+ zhanghaozhuangtai = request.data.get('zhanghaozhuangtai') # 可选:1=正常,2=封禁
+ zaixianzhuangtai = request.data.get('zaixianzhuangtai') # 可选:1=在线,2=离线
+
+ # 3. 基础查询:当前管事邀请的所有打手
+ base_qs = UserDashou.query.select_related('user').filter(
+ yaoqingren=guanshi_yonghuid
+ )
+
+ # 4. 全局统计(不受筛选影响,直接基于 base_qs)
+ total_all = base_qs.count()
+ zaixian_all = base_qs.filter(zaixianzhuangtai=1).count()
+ zhengchang_all = base_qs.filter(zhanghaozhuangtai=1).count()
+ fengjin_all = total_all - zhengchang_all
+
+ # 5. 应用筛选条件
+ if keyword:
+ base_qs = base_qs.filter(
+ Q(nicheng__icontains=keyword) | Q(user__UserUID__icontains=keyword)
+ )
+ if zhanghaozhuangtai is not None:
+ try:
+ val = int(zhanghaozhuangtai)
+ if val == 1:
+ base_qs = base_qs.filter(zhanghaozhuangtai=1)
+ else:
+ base_qs = base_qs.exclude(zhanghaozhuangtai=1)
+ except (ValueError, TypeError):
+ pass
+ if zaixianzhuangtai is not None:
+ try:
+ val = int(zaixianzhuangtai)
+ if val == 1:
+ base_qs = base_qs.filter(zaixianzhuangtai=1)
+ else:
+ base_qs = base_qs.exclude(zaixianzhuangtai=1)
+ except (ValueError, TypeError):
+ pass
+
+ # 6. 排序和分页
+ base_qs = base_qs.order_by('-CreateTime')
+ paginator = Paginator(base_qs, page_size)
+ try:
+ page_obj = paginator.page(page)
+ except EmptyPage:
+ page_obj = []
+ has_more = False
+ filtered_total = paginator.count if paginator.count else 0
+ else:
+ has_more = page_obj.has_next()
+ filtered_total = paginator.count
+
+ # 7. 构建打手列表
+ dashou_list = []
+ for dashou in page_obj:
+ user_main = dashou.user
+ dashou_list.append({
+ 'uid': user_main.UserUID,
+ 'nicheng': dashou.nicheng or '打手',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'touxiang': user_main.Avatar or '',
+ 'jiedanzongliang': dashou.jiedanzongliang,
+ 'chengjiaozongliang': dashou.chengjiaozongliang,
+ 'tuikuanliang': dashou.tuikuanliang,
+ 'yue': str(dashou.zonge),
+ })
+
+ # 8. 获取管事邀请总数(来自管事扩展表)
+ try:
+ yaoqingzongshu = current_user.GuanshiProfile.yaogingshuliang
+ except Exception:
+ yaoqingzongshu = total_all
+
+ response_data = {
+ 'dashouList': dashou_list,
+ 'yaoqingzongshu': yaoqingzongshu, # 邀请总数(全局)
+ 'totalCount': filtered_total, # 当前筛选条件下的总数
+ 'zaixianCount': zaixian_all, # 全局在线
+ 'zhengchangCount': zhengchang_all, # 全局正常
+ 'fengjinCount': fengjin_all, # 全局封禁
+ 'currentPage': page,
+ 'pageSize': page_size,
+ 'hasMore': has_more,
+ }
+
+ return Response({'code': 200, 'message': '获取成功', 'data': response_data})
+
+ except ValueError:
+ return Response({'code': 400, 'message': '分页参数格式错误'}, status=400)
+ except Exception as e:
+ logger.error(f'获取打手列表失败: {str(e)}', exc_info=True)
+ return Response({'code': 500, 'message': '系统错误'}, status=500)
+
+
+class WechatLoginAndDashouRegisterView(APIView):
+ """
+ 微信登录并注册打手一体化接口
+ 访问路径: /api/yonghu/wdlyhdl
+ 请求方法: POST
+ 权限要求: 无需登录
+ 请求体: { "code": "微信登录code", "inviteCode": "邀请码" }
+
+ 功能: 用户扫码进入但未登录时,一次性完成微信登录和打手注册
+ """
+
+ throttle_classes = [AnonRateThrottle]
+ permission_classes = [AllowAny]
+
+ def post(self, request):
+ """
+ 处理微信登录并注册打手请求
+ """
+ try:
+ # 1. 获取并验证参数
+ code = request.data.get('code', '').strip()
+ yaoqingma = request.data.get('inviteCode', '').strip()
+
+ if not code:
+ return Response({
+ 'code': 1,
+ 'msg': '微信授权码不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if not yaoqingma:
+ return Response({
+ 'code': 2,
+ 'msg': '邀请码不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ if len(yaoqingma) > 100:
+ return Response({
+ 'code': 3,
+ 'msg': '邀请码长度超过限制',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 获取微信openid
+ wechat_data = self.get_wechat_openid(code)
+ if not wechat_data or 'openid' not in wechat_data:
+ error_msg = wechat_data.get('errmsg', '微信授权失败')
+ return Response({
+ 'code': 4,
+ 'msg': f'微信登录失败: {error_msg}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ openid = wechat_data['openid']
+
+ # 3. 获取客户端真实IP
+ kehuduan_ip = self.huoquKehuduanIP(request)
+
+ # 4. 开始数据库事务(确保登录和注册的原子性)
+ with transaction.atomic():
+ # 4.1 创建或获取用户(登录逻辑)
+ user_main, created = User.objects.select_for_update().get_or_create(
+ OpenID=openid,
+ defaults={
+ 'UserUID': self.shengchengYonghuID(),
+ 'UserName': openid,
+ }
+ )
+
+ # 更新用户IP和最后登录时间
+ cunchu_ip = kehuduan_ip
+ user_main.IP = cunchu_ip
+ user_main.UserLastLoginDate = timezone.now()
+ user_main.save()
+
+ if created:
+ # 新用户:创建老板扩展表
+ UserBoss.query.create(user=user_main, nickname='微信用户')
+
+ # 4.2 验证邀请码对应的管事(注册逻辑)
+ try:
+ # 使用select_related获取管事及其关联的主表用户信息
+ guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma)
+ except UserGuanshi.DoesNotExist:
+ # 邀请码无效,但用户已登录,可以返回登录成功但注册失败
+ # 这里我们继续执行,但只返回登录信息
+ return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True)
+
+ from jituan.services.invite_guard import assert_invite_same_club
+ from jituan.services.club_user import ensure_user_club_id
+ from jituan.services.club_context import resolve_club_id_from_request
+
+ ok, invite_msg = assert_invite_same_club(
+ request, guanshi_profile.user, user_main,
+ )
+ if not ok:
+ return Response({
+ 'code': 403,
+ 'msg': invite_msg,
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ ensure_user_club_id(user_main, resolve_club_id_from_request(request))
+
+ # 验证管事状态
+
+ # 4.3 检查用户是否已是打手
+ dashou_exists = UserDashou.query.filter(user=user_main).exists()
+ if dashou_exists:
+ # 用户已是打手,直接返回信息
+ dashou_profile = UserDashou.query.get(user=user_main)
+ return self.fanhuiDengluZhuceshuju(user_main, dashou_profile)
+
+ # 4.4 核心:为用户创建所有身份(打手、商家、管事)
+ guanshi_yonghuid = guanshi_profile.user.UserUID
+
+ # 创建打手扩展表
+ dashou_profile = UserDashou.query.create(
+ user=user_main,
+ nicheng='大神',
+ chenghao='普通大神',
+ yaoqingren=guanshi_yonghuid,
+ # 其他字段使用模型默认值
+ )
+
+ # 创建商家扩展表
+
+ # 创建管事扩展表
+
+ # 更新原管事的邀请数量
+ locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
+ locked_guanshi.yaogingshuliang += 1
+ locked_guanshi.save()
+
+ # ========== 新增:管事每日统计(邀请打手) ==========
+ try:
+ update_guanshi_daily_by_action(
+ yonghuid=guanshi_yonghuid,
+ action=1, # 1 = 邀请打手
+ amount=Decimal('0.00')
+ )
+ logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
+ except Exception as e:
+ logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
+
+ # 5. 生成JWT token
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ # 6. 准备返回数据
+ response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token)
+
+ return Response({
+ 'code': 0,
+ 'msg': '登录并注册成功!已开通打手、管事等身份。',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 记录错误日志
+ error_openid = locals().get('openid', 'N/A')
+ logger.error(
+ f"微信登录注册一体化接口异常 - openid: {error_openid}, "
+ f"错误类型: {type(e).__name__}, 错误详情: {str(e)}",
+ exc_info=True
+ )
+
+ # 返回友好错误信息
+ return Response({
+ 'code': 99,
+ 'msg': '系统繁忙,请稍后重试',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def get_wechat_openid(self, code):
+ """
+ 调用微信接口获取openid
+ """
+ try:
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ secret = getattr(settings, 'WEIXIN_SECRET', '')
+
+ if not appid or not secret:
+ raise ValueError('微信配置未设置')
+
+ url = 'https://api.weixin.qq.com/sns/jscode2session'
+ params = {
+ 'appid': appid,
+ 'secret': secret,
+ 'js_code': code,
+ 'grant_type': 'authorization_code'
+ }
+
+ response = requests.get(url, params=params, timeout=10)
+ result = response.json()
+
+ if 'openid' in result:
+ return result
+ else:
+ errcode = result.get('errcode', 'unknown')
+ errmsg = result.get('errmsg', '未知错误')
+ return {'errmsg': f'[{errcode}]{errmsg}'}
+
+ except requests.exceptions.Timeout:
+ return {'errmsg': '请求微信接口超时'}
+ except Exception as e:
+ return {'errmsg': f'请求微信接口异常: {str(e)}'}
+
+ def shengchengYonghuID(self):
+ """
+ 生成7位数字用户ID
+ """
+ for _ in range(10):
+ timestamp_part = str(int(time.time()))[-5:].zfill(5)
+ random_part = str(random.randint(0, 99)).zfill(2)
+ user_id = timestamp_part + random_part
+
+ if len(user_id) == 7 and user_id.isdigit():
+ if not User.query.filter(UserUID=user_id).exists():
+ return user_id
+
+ raise Exception('生成用户ID失败')
+
+ def huoquKehuduanIP(self, request):
+ """
+ 获取客户端真实IP地址
+ """
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(',')[0].strip()
+ if ip:
+ return ip
+
+ ip = request.META.get('REMOTE_ADDR', '')
+ return ip if ip else '0.0.0.0'
+
+ def huoquQunPeizhi(self):
+ """
+ 获取群配置信息
+ """
+ try:
+ qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
+
+ result = {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+
+ for config in qun_configs:
+ if config.id == 1:
+ result['dashouqun'] = config.GroupContent or ''
+ result['dashouqunid'] = config.GroupID or ''
+ elif config.id == 2:
+ result['guanshiqun'] = config.GroupContent or ''
+ result['guanshiqunid'] = config.GroupID or ''
+
+ return result
+
+ except Exception:
+ return {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+
+ def fanhuiZhihouDengluXinxi(self, user_main, yaoqingma_wuxiao=False, guanshi_jinyong=False):
+ """
+ 返回只登录成功的信息(当邀请码无效或管事禁用时)
+ """
+ # 获取老板昵称
+ boss_nickname = '微信用户'
+ try:
+ boss_profile = UserBoss.query.get(user=user_main)
+ if boss_profile.nickname:
+ boss_nickname = boss_profile.nickname
+ except UserBoss.DoesNotExist:
+ pass
+
+ # 检查各身份状态
+ dashou_status = 1 if UserDashou.query.filter(user=user_main).exists() else 0
+ shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 0
+ guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 0
+
+ # 生成token
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ # 获取群配置
+ group_info = self.huoquQunPeizhi()
+
+ # 构建返回数据
+ response_data = {
+ 'token': token,
+ 'nicheng': boss_nickname,
+ 'uid': user_main.UserUID,
+ 'touxiang': user_main.Avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ # 🟢 注意:这里没有返回dingdantiaoshu(订单数量),与前端需求一致
+ 'dashouqun': group_info.get('dashouqun', ''),
+ 'dashouqunid': group_info.get('dashouqunid', ''),
+ 'guanshiqun': group_info.get('guanshiqun', ''),
+ 'guanshiqunid': group_info.get('guanshiqunid', '')
+ }
+
+ # 根据情况设置返回消息
+ if yaoqingma_wuxiao:
+ msg = '登录成功,但邀请码无效'
+ elif guanshi_jinyong:
+ msg = '登录成功,但邀请码对应的管事账号已被禁用'
+ else:
+ msg = '登录成功'
+
+ return Response({
+ 'code': 0,
+ 'msg': msg,
+ 'data': response_data
+ })
+
+ def fanhuiDengluZhuceshuju(self, user_main, dashou_profile):
+ """
+ 返回登录并注册的数据(用户已是打手的情况)
+ """
+ # 获取老板昵称
+ boss_nickname = '微信用户'
+ try:
+ boss_profile = UserBoss.query.get(user=user_main)
+ if boss_profile.nickname:
+ boss_nickname = boss_profile.nickname
+ except UserBoss.DoesNotExist:
+ pass
+
+ # 检查各身份状态(这里用户已是打手,所以都是1)
+ dashou_status = 1
+ shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 1 # 确保是1
+ guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 1 # 确保是1
+
+ # 生成token
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ # 获取群配置
+ group_info = self.huoquQunPeizhi()
+
+ # 准备打手信息
+ dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
+
+ # 合并所有数据
+ response_data = {
+ 'token': token,
+ 'nicheng': boss_nickname,
+ 'uid': user_main.UserUID,
+ 'touxiang': user_main.Avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ 'dashouqun': group_info.get('dashouqun', ''),
+ 'dashouqunid': group_info.get('dashouqunid', ''),
+ 'guanshiqun': group_info.get('guanshiqun', ''),
+ 'guanshiqunid': group_info.get('guanshiqunid', ''),
+ # 合并打手信息
+ **dashou_data
+ }
+
+ return Response({
+ 'code': 0,
+ 'msg': '您已是打手,登录成功',
+ 'data': response_data
+ })
+
+ def zhuangbeiFanhuiShuju(self, user_main, dashou_profile, token):
+ """
+ 准备完整的返回数据(登录+注册成功的情况)
+ """
+ # 获取老板昵称
+ boss_nickname = '微信用户'
+ try:
+ boss_profile = UserBoss.query.get(user=user_main)
+ if boss_profile.nickname:
+ boss_nickname = boss_profile.nickname
+ except UserBoss.DoesNotExist:
+ pass
+
+ # 身份状态(刚注册成功,应该都是1)
+ dashou_status = 1
+ shangjia_status = 1
+ guanshi_status = 1
+
+ # 获取群配置
+ group_info = self.huoquQunPeizhi()
+
+ # 准备打手信息
+ dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
+
+ # 合并所有数据
+ response_data = {
+ 'token': token,
+ 'nicheng': boss_nickname,
+ 'uid': user_main.UserUID,
+ 'touxiang': user_main.Avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ 'dashouqun': group_info.get('dashouqun', ''),
+ 'dashouqunid': group_info.get('dashouqunid', ''),
+ 'guanshiqun': group_info.get('guanshiqun', ''),
+ 'guanshiqunid': group_info.get('guanshiqunid', ''),
+ # 合并打手信息
+ **dashou_data
+ }
+
+ return response_data
+
+ def zhuangbeiDashouFanhuiShuju(self, dashou_profile):
+ """
+ 准备打手返回数据(字段名严格匹配前端)
+ """
+ # 构建基础信息
+ data = {
+ 'dashounicheng': dashou_profile.nicheng,
+ 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
+ 'yongjin': str(dashou_profile.yue), # Decimal转字符串
+ 'zonge': str(dashou_profile.zonge),
+ 'yajin': str(dashou_profile.yajin),
+ 'chenghao': dashou_profile.chenghao,
+ 'jinfen': dashou_profile.jifen,
+ 'chengjiaoliang': dashou_profile.chengjiaozongliang,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'dashouzhuangtai': dashou_profile.zhuangtai,
+ }
+
+ # 查询并处理会员列表 (clumber)
+ # 使用当前用户的yonghuid进行查询
+ huiyuan_goumai_list = []
+ goumai_records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.UserUID)
+ for record in goumai_records:
+ # 调用方法检查是否到期,此方法内部会更新状态
+ shifou_daoqi = record.jiance_shifou_daoqi()
+ huiyuan_goumai_list.append({
+ 'huiyuan_id': record.huiyuan_id,
+ 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
+ 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
+ 'shifou_daoqi': shifou_daoqi,
+ 'jieshao': record.jieshao
+ })
+
+ data['clumber'] = huiyuan_goumai_list
+ return data
+
+
+class WechatLoginAndGuanshiRegisterView(APIView):
+ """
+ 未登录用户微信登录+管事注册一体化接口
+ 路径: /api/yonghu/zuzhangyqmzc
+ 方法: POST
+ 权限: 允许所有
+ 请求体: { "code": "微信登录code", "inviteCode": "组长邀请码" }
+ 成功返回 code: 200,数据同 GuanshiRegisterView
+ """
+ permission_classes = [AllowAny]
+
+ def post(self, request):
+ code = request.data.get('code', '').strip()
+ yaoqingma = request.data.get('inviteCode', '').strip()
+
+ if not code:
+ return Response({'code': 400, 'msg': '微信授权码不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if not yaoqingma:
+ return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if len(yaoqingma) > 100:
+ return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ wechat_data = self._get_wechat_openid(code)
+ if not wechat_data or 'openid' not in wechat_data:
+ error_msg = wechat_data.get('errmsg', '微信授权失败')
+ return Response({'code': 400, 'msg': f'微信登录失败: {error_msg}', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ openid = wechat_data['openid']
+ kehuduan_ip = self._huoqu_kehuduan_ip(request)
+
+ with transaction.atomic():
+ user_main, created = User.objects.select_for_update().get_or_create(
+ OpenID=openid,
+ defaults={
+ 'UserUID': self._shengcheng_yonghu_id(),
+ 'UserName': openid,
+ }
+ )
+ user_main.IP = kehuduan_ip
+ user_main.UserLastLoginDate = timezone.now()
+ user_main.save()
+
+ if created:
+ UserBoss.query.create(user=user_main, nickname='微信用户')
+
+ if hasattr(user_main, 'GuanshiProfile'):
+ return self._fanhui_xianyou_guanshi_info(user_main)
+
+ try:
+ zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma)
+ except UserZuzhang.DoesNotExist:
+ return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND)
+
+ if zuzhang.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ zuzhang_yonghuid = zuzhang.user.UserUID
+
+ guanshi_yaoqingma = CreateInvitationCode(str(user_main.UserUID))
+ guanshi_profile = UserGuanshi.query.create(
+ user=user_main,
+ yaoqingma=guanshi_yaoqingma,
+ yaoqingren=zuzhang_yonghuid,
+ )
+
+ UserZuzhang.query.filter(pk=zuzhang.pk).update(
+ yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
+ )
+
+ # ========== 新增:组长每日统计(邀请管事 action=1) ==========
+ try:
+ update_zuzhang_daily_by_action(
+ yonghuid=zuzhang_yonghuid,
+ action=1 # 1 = 邀请管事
+ )
+ logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
+ except Exception as e:
+ logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
+
+
+ if not hasattr(user_main, 'DashouProfile'):
+ dashou_profile = UserDashou.query.create(
+ user=user_main,
+ nicheng='大神',
+ chenghao='普通大神',
+ yaoqingren=zuzhang_yonghuid,
+ )
+ else:
+ dashou_profile = user_main.DashouProfile
+
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ response_data = self._zhuangbei_fanhui_shuju(
+ user_main, dashou_profile, guanshi_profile, token
+ )
+
+ return Response({'code': 0, 'msg': '登录并注册成功!', 'data': response_data})
+
+ # ---------- 辅助方法(与接口1完全相同,完整列出以保证完整性) ----------
+
+ def _get_wechat_openid(self, code):
+ try:
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ secret = getattr(settings, 'WEIXIN_SECRET', '')
+ if not appid or not secret:
+ raise ValueError('微信配置未设置')
+ url = 'https://api.weixin.qq.com/sns/jscode2session'
+ params = {
+ 'appid': appid,
+ 'secret': secret,
+ 'js_code': code,
+ 'grant_type': 'authorization_code'
+ }
+ response = requests.get(url, params=params, timeout=10)
+ result = response.json()
+ if 'openid' in result:
+ return result
+ else:
+ errcode = result.get('errcode', 'unknown')
+ errmsg = result.get('errmsg', '未知错误')
+ return {'errmsg': f'[{errcode}]{errmsg}'}
+ except requests.exceptions.Timeout:
+ logger.error('请求微信接口超时')
+ return {'errmsg': '请求微信接口超时'}
+ except Exception as e:
+ logger.error(f'请求微信接口异常: {e}')
+ return {'errmsg': f'请求微信接口异常: {str(e)}'}
+
+ def _shengcheng_yonghu_id(self):
+ for _ in range(10):
+ timestamp_part = str(int(time.time()))[-5:].zfill(5)
+ random_part = str(random.randint(0, 99)).zfill(2)
+ user_id = timestamp_part + random_part
+ if len(user_id) == 7 and user_id.isdigit():
+ if not User.query.filter(UserUID=user_id).exists():
+ return user_id
+ raise Exception('生成用户ID失败')
+
+ def _huoqu_kehuduan_ip(self, request):
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(',')[0].strip()
+ if ip:
+ return ip
+ ip = request.META.get('REMOTE_ADDR', '')
+ return ip if ip else '0.0.0.0'
+
+ def _huoqu_qun_peizhi(self):
+ try:
+ qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
+ result = {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+ for config in qun_configs:
+ if config.id == 1:
+ result['dashouqun'] = config.GroupContent or ''
+ result['dashouqunid'] = config.GroupID or ''
+ elif config.id == 2:
+ result['guanshiqun'] = config.GroupContent or ''
+ result['guanshiqunid'] = config.GroupID or ''
+ return result
+ except Exception as e:
+ logger.error(f"获取群配置失败: {e}")
+ return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
+
+ def _zhuangbei_dashou_fanhui(self, dashou_profile):
+ data = {
+ 'dashounicheng': dashou_profile.nicheng,
+ 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
+ 'yongjin': str(dashou_profile.yue),
+ 'zonge': str(dashou_profile.zonge),
+ 'yajin': str(dashou_profile.yajin),
+ 'chenghao': dashou_profile.chenghao,
+ 'jinfen': dashou_profile.jifen,
+ 'chengjiaoliang': dashou_profile.chengjiaozongliang,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'dashouzhuangtai': dashou_profile.zhuangtai,
+ }
+ huiyuan_list = []
+ try:
+ records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.UserUID)
+ for record in records:
+ shifou_daoqi = record.jiance_shifou_daoqi()
+ huiyuan_list.append({
+ 'huiyuan_id': record.huiyuan_id,
+ 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
+ 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
+ 'shifou_daoqi': shifou_daoqi,
+ 'jieshao': record.jieshao
+ })
+ except Exception as e:
+ logger.warning(f"获取会员列表失败: {e}")
+ data['clumber'] = huiyuan_list
+ return data
+
+ def _fanhui_xianyou_guanshi_info(self, user_main):
+ guanshi_profile = user_main.GuanshiProfile
+ dashou_profile = getattr(user_main, 'DashouProfile', None)
+ group_info = self._huoqu_qun_peizhi()
+ dashou_status = 1 if dashou_profile else 0
+ shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
+ guanshi_status = 1
+ try:
+ nicheng = user_main.BossProfile.nickname or '微信用户'
+ except:
+ nicheng = '微信用户'
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+ data = {
+ 'token': token,
+ 'nicheng': nicheng,
+ 'uid': user_main.UserUID,
+ 'touxiang': user_main.Avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ **group_info,
+ }
+ guanshi_data = {
+ 'gszhstatus': guanshi_profile.zhuangtai,
+ 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
+ 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
+ 'fenyongtixian': str(guanshi_profile.yue),
+ 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
+ }
+ data.update(guanshi_data)
+ if dashou_profile:
+ dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
+ data.update(dashou_data)
+ else:
+ data.update({
+ 'dashounicheng': '',
+ 'zhanghaostatus': 0,
+ 'yongjin': '0.00',
+ 'zonge': '0.00',
+ 'yajin': '0.00',
+ 'chenghao': '',
+ 'jinfen': 0,
+ 'chengjiaoliang': 0,
+ 'zaixianzhuangtai': 0,
+ 'dashouzhuangtai': 0,
+ 'clumber': []
+ })
+ return Response({'code': 0, 'msg': '您已是管事', 'data': data})
+
+ def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
+ try:
+ nicheng = user_main.BossProfile.nickname or '微信用户'
+ except:
+ nicheng = '微信用户'
+ dashou_status = 1
+ shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
+ guanshi_status = 1
+ group_info = self._huoqu_qun_peizhi()
+ data = {
+ 'token': token,
+ 'nicheng': nicheng,
+ 'uid': user_main.UserUID,
+ 'touxiang': user_main.Avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ **group_info,
+ }
+ guanshi_data = {
+ 'gszhstatus': guanshi_profile.zhuangtai,
+ 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
+ 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
+ 'fenyongtixian': str(guanshi_profile.yue),
+ 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
+ }
+ data.update(guanshi_data)
+ dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
+ data.update(dashou_data)
+ return data
+
+
+class GuanshiRegisterView(APIView):
+ """
+ 已登录用户注册管事接口
+ 路径: /api/yonghu/guanshizhuce
+ 方法: POST
+ 权限: JWT认证
+ 请求体: { "inviteCode": "组长邀请码" }
+ 成功返回 code: 200
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取并验证邀请码
+ yaoqingma = request.data.get('inviteCode', '').strip()
+ if not yaoqingma:
+ return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if len(yaoqingma) > 100:
+ return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ current_user = request.user
+
+ # 2. 检查用户是否已是管事
+ if hasattr(current_user, 'GuanshiProfile'):
+ # 用户已是管事,直接返回现有信息
+ return self._fanhui_xianyou_guanshi_info(current_user)
+
+ # 3. 验证邀请码对应的组长
+ try:
+ zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma)
+ except UserZuzhang.DoesNotExist:
+ return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
+ status=status.HTTP_404_NOT_FOUND)
+
+ if zuzhang.zhuangtai != 1:
+ return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ from jituan.services.invite_guard import assert_invite_same_club
+ ok, invite_msg = assert_invite_same_club(request, zuzhang.user, current_user)
+ if not ok:
+ return Response({'code': 403, 'msg': invite_msg, 'data': None},
+ status=status.HTTP_403_FORBIDDEN)
+
+ zuzhang_yonghuid = zuzhang.user.UserUID
+
+ # 4. 事务内创建管事实体和打手(如果需要)
+ with transaction.atomic():
+ # 创建管事扩展表
+ guanshi_yaoqingma = CreateInvitationCode(str(current_user.UserUID))
+ guanshi_profile = UserGuanshi.query.create(
+ user=current_user,
+ yaoqingma=guanshi_yaoqingma,
+ yaoqingren=zuzhang_yonghuid,
+ )
+
+ # 更新组长的邀请总数
+ UserZuzhang.query.filter(pk=zuzhang.pk).update(
+ yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
+ )
+
+ # ========== 新增:组长每日统计(邀请管事 action=1) ==========
+ try:
+ update_zuzhang_daily_by_action(
+ yonghuid=zuzhang_yonghuid,
+ action=1 # 1 = 邀请管事
+ )
+ logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
+ except Exception as e:
+ logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
+
+ # 检查用户是否已是打手,如果不是则创建
+ if not hasattr(current_user, 'DashouProfile'):
+ dashou_profile = UserDashou.query.create(
+ user=current_user,
+ nicheng='大手子',
+ chenghao='普通大手',
+ yaoqingren=zuzhang_yonghuid,
+ )
+ else:
+ dashou_profile = current_user.DashouProfile
+
+ # 5. 生成新token
+ refresh = RefreshToken.for_user(current_user)
+ token = str(refresh.access_token)
+
+ # 6. 准备返回数据(包含所有字段)
+ response_data = self._zhuangbei_fanhui_shuju(
+ current_user, dashou_profile, guanshi_profile, token
+ )
+
+ return Response({'code': 200, 'msg': '注册成功!', 'data': response_data})
+
+ # ---------- 辅助方法 ----------
+
+ def _huoqu_qun_peizhi(self):
+ """获取群配置信息"""
+ try:
+ qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
+ result = {
+ 'dashouqun': '',
+ 'dashouqunid': '',
+ 'guanshiqun': '',
+ 'guanshiqunid': ''
+ }
+ for config in qun_configs:
+ if config.id == 1:
+ result['dashouqun'] = config.GroupContent or ''
+ result['dashouqunid'] = config.GroupID or ''
+ elif config.id == 2:
+ result['guanshiqun'] = config.GroupContent or ''
+ result['guanshiqunid'] = config.GroupID or ''
+ return result
+ except Exception as e:
+ logger.error(f"获取群配置失败: {e}")
+ return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
+
+ def _zhuangbei_dashou_fanhui(self, dashou_profile):
+ """组装打手信息(字段名与前端完全匹配)"""
+ data = {
+ 'dashounicheng': dashou_profile.nicheng,
+ 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
+ 'yongjin': str(dashou_profile.yue),
+ 'zonge': str(dashou_profile.zonge),
+ 'yajin': str(dashou_profile.yajin),
+ 'chenghao': dashou_profile.chenghao,
+ 'jinfen': dashou_profile.jifen,
+ 'chengjiaoliang': dashou_profile.chengjiaozongliang,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'dashouzhuangtai': dashou_profile.zhuangtai,
+ }
+
+ # 会员列表
+ huiyuan_list = []
+ try:
+ records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.UserUID)
+ for record in records:
+ shifou_daoqi = record.jiance_shifou_daoqi()
+ huiyuan_list.append({
+ 'huiyuan_id': record.huiyuan_id,
+ 'huiyuan_zhuangtai': record.huiyuan_zhuangtai,
+ 'daoqi_time': record.daoqi_time.isoformat() if record.daoqi_time else None,
+ 'shifou_daoqi': shifou_daoqi,
+ 'jieshao': record.jieshao
+ })
+ except Exception as e:
+ logger.warning(f"获取会员列表失败: {e}")
+ data['clumber'] = huiyuan_list
+ return data
+
+ def _fanhui_xianyou_guanshi_info(self, user_main):
+ """用户已是管事时的返回"""
+ guanshi_profile = user_main.GuanshiProfile
+ dashou_profile = getattr(user_main, 'DashouProfile', None)
+
+ group_info = self._huoqu_qun_peizhi()
+
+ dashou_status = 1 if dashou_profile else 0
+ shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
+ guanshi_status = 1
+
+ try:
+ nicheng = user_main.BossProfile.nickname or '微信用户'
+ except:
+ nicheng = '微信用户'
+
+ refresh = RefreshToken.for_user(user_main)
+ token = str(refresh.access_token)
+
+ data = {
+ 'token': token,
+ 'nicheng': nicheng,
+ 'uid': user_main.UserUID,
+ 'touxiang': user_main.Avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ **group_info,
+ }
+
+ # 管事信息
+ guanshi_data = {
+ 'gszhstatus': guanshi_profile.zhuangtai,
+ 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
+ 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
+ 'fenyongtixian': str(guanshi_profile.yue),
+ 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
+ }
+ data.update(guanshi_data)
+
+ if dashou_profile:
+ dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
+ data.update(dashou_data)
+ else:
+ data.update({
+ 'dashounicheng': '',
+ 'zhanghaostatus': 0,
+ 'yongjin': '0.00',
+ 'zonge': '0.00',
+ 'yajin': '0.00',
+ 'chenghao': '',
+ 'jinfen': 0,
+ 'chengjiaoliang': 0,
+ 'zaixianzhuangtai': 0,
+ 'dashouzhuangtai': 0,
+ 'clumber': []
+ })
+
+ return Response({'code': 200, 'msg': '您已是管事', 'data': data})
+
+ def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
+ """组装完整返回数据(新注册)"""
+ try:
+ nicheng = user_main.BossProfile.nickname or '微信用户'
+ except:
+ nicheng = '微信用户'
+
+ dashou_status = 1
+ shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
+ guanshi_status = 1
+
+ group_info = self._huoqu_qun_peizhi()
+
+ data = {
+ 'token': token,
+ 'nicheng': nicheng,
+ 'uid': user_main.UserUID,
+ 'touxiang': user_main.Avatar or '',
+ 'shangjiastatus': shangjia_status,
+ 'dashoustatus': dashou_status,
+ 'guanshistatus': guanshi_status,
+ **group_info,
+ }
+
+ # 管事信息
+ guanshi_data = {
+ 'gszhstatus': guanshi_profile.zhuangtai,
+ 'yaoqingzongshu': guanshi_profile.yaogingshuliang,
+ 'fenyongzonge': str(guanshi_profile.chongzhifenrun),
+ 'fenyongtixian': str(guanshi_profile.yue),
+ 'yichongzhiDashou': guanshi_profile.jinrichongzhi,
+ }
+ data.update(guanshi_data)
+
+ dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
+ data.update(dashou_data)
+
+ return data
+
+
+class ZuzhangZhuceView(APIView):
+ """
+ 组长注册/激活接口
+ POST /yonghu/zuzhangzhuce
+ 请求体: {"inviteCode": "xxx"}
+ 返回:
+ {
+ "code": 200,
+ "msg": "success",
+ "data": {
+ "ketixian": "0.00", # 可提现金额
+ "guanshiCount": 0, # 邀请的管事数量
+ "fenhongZonge": "0.00" # 分红总额
+ }
+ }
+ 或错误码
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ # 硬编码激活码(注意转义特殊字符)
+ ACTIVATION_CODE = "237huwehdw77e777eyw224fr"
+
+ def post(self, request):
+ user = request.user
+ invite_code = request.data.get('inviteCode', '').strip()
+
+ if not invite_code:
+ return Response({'code': 400, 'msg': '邀请码不能为空'}, status=400)
+
+ # 检查是否已是组长
+ try:
+ zuzhang = UserZuzhang.query.select_related('user').get(user=user)
+ # 已注册,直接返回数据
+ data = self._build_response_data(zuzhang, user)
+ return Response({'code':200, 'msg': '已注册', 'data': data})
+ except UserZuzhang.DoesNotExist:
+ pass
+
+ # 比对激活码
+ if invite_code != self.ACTIVATION_CODE:
+ return Response({'code': 400, 'msg': '邀请码无效'}, status=400)
+
+ from jituan.services.club_user import ensure_user_club_id
+ from jituan.services.club_context import resolve_club_id_from_request
+ ensure_user_club_id(user, resolve_club_id_from_request(request))
+
+ # 生成唯一组长邀请码:时间戳 + 用户ID后6位 + 随机字符
+ yaoqingma = self._generate_unique_yaoqingma(user.UserUID)
+
+ # 创建组长扩展记录
+ try:
+ with transaction.atomic():
+ zuzhang = UserZuzhang.query.create(
+ user=user,
+ yaoqingma=yaoqingma,
+ fenyong_zonge=0.00,
+ ketixian_jine=0.00,
+ yaoqing_zongshu=0,
+ jinri_fenyong=0.00,
+ jinyue_fenyong=0.00,
+ zhuangtai=1,
+ jinri_tixian=0.00,
+ ewai_tixian_xiane=0.00,
+ kaioi_ewai_tixian=False,
+ kaioi_ewai_fenhong=False,
+ ewai_fenhong_jine=0.00
+ )
+ except IntegrityError:
+ # 极低概率重复,重试一次
+ yaoqingma = self._generate_unique_yaoqingma(user.UserUID, retry=True)
+ zuzhang = UserZuzhang.query.create(
+ user=user,
+ yaoqingma=yaoqingma,
+ # 其他字段默认同上
+ fenyong_zonge=0.00,
+ ketixian_jine=0.00,
+ yaoqing_zongshu=0,
+ jinri_fenyong=0.00,
+ jinyue_fenyong=0.00,
+ zhuangtai=1,
+ jinri_tixian=0.00,
+ ewai_tixian_xiane=0.00,
+ kaioi_ewai_tixian=False,
+ kaioi_ewai_fenhong=False,
+ ewai_fenhong_jine=0.00
+ )
+
+ data = self._build_response_data(zuzhang, user)
+ return Response({'code':200, 'msg': '注册成功', 'data': data})
+
+ def _generate_unique_yaoqingma(self, yonghuid, retry=False):
+ """生成唯一邀请码:时间戳+用户ID后6位+随机2位字符"""
+ timestamp = str(int(time.time()))[-6:] # 取后6位
+ uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
+ rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
+ base = f"Z{timestamp}{uid_part}{rand_str}"
+ # 确保唯一性(若已存在则追加随机数,最多尝试3次)
+ for _ in range(3):
+ if not UserZuzhang.query.filter(yaoqingma=base).exists():
+ return base
+ base = base + random.choice(string.digits)
+ # 极端情况,再附加时间戳毫秒
+ millis = str(time.time()).replace('.', '')[-4:]
+ return base + millis
+
+ def _build_response_data(self, zuzhang, user):
+ """组装前端所需数据"""
+ # 计算邀请的管事数量(通过UserGuanshi表的yaoqingren字段关联到user的yonghuid)
+ guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
+ return {
+ 'ketixian': str(zuzhang.ketixian_jine), # 可提现余额
+ 'guanshiCount': guanshi_count, # 邀请管事数
+ 'fenhongZonge': str(zuzhang.fenyong_zonge) # 分红总额
+ }
+
+
+class ZuzhangXinxiView(APIView):
+ """
+ 组长信息查询接口
+ POST /yonghu/zuzhangxinxi
+ 请求体: 无
+ 返回: 同注册接口成功返回格式
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ user = request.user
+ try:
+ zuzhang = UserZuzhang.query.get(user=user)
+ except UserZuzhang.DoesNotExist:
+ return Response({'code': 404, 'msg': '用户不是组长'}, status=404)
+
+ guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
+ data = {
+ 'ketixian': str(zuzhang.ketixian_jine),
+ 'guanshiCount': guanshi_count,
+ 'fenhongZonge': str(zuzhang.fenyong_zonge)
+ }
+ return Response({'code': 200, 'msg': 'success', 'data': data})
diff --git a/users/views/chufa.py b/users/views/chufa.py
new file mode 100644
index 0000000..4908bd8
--- /dev/null
+++ b/users/views/chufa.py
@@ -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)
diff --git a/users/views/kefu.py b/users/views/kefu.py
new file mode 100644
index 0000000..b53ae32
--- /dev/null
+++ b/users/views/kefu.py
@@ -0,0 +1,3794 @@
+"""users.views.kefu - 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 KefuLoginView(APIView):
+ """
+ 客服/管理员登录接口(支持手机号重复)
+ 请求:POST /yonghu/kefujinru
+ 请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
+ 返回:
+ code=0 成功,data包含token,nicheng,yonghuid
+ code=1 失败(msg区分:用户不存在/密码错误/二级密码错误)
+ code=4 账号被封禁
+
+ 管理员(IsSuperuser 或 UserType='admin')无需 erjimima 和 KefuProfile。
+ """
+ throttle_classes = [AnonRateThrottle] # 限制匿名请求频率
+ permission_classes = [AllowAny] # 任何人都可访问
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ password = request.data.get('password', '')
+ erjimima = request.data.get('erjimima', '')
+
+ if not phone or not password:
+ return Response({
+ 'code': 1,
+ 'msg': '请填写手机号和密码',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 获取客户端真实IP
+ client_ip = self.get_client_ip(request)
+
+ # 3. 查询该手机号下所有账号(客服 + 管理员)
+ # 先查客服(有 KefuProfile),再查管理员(IsSuperuser 或 UserType='admin')
+ user_mains = list(
+ User.query.filter(
+ Phone=phone,
+ KefuProfile__isnull=False,
+ ).select_related('KefuProfile')
+ )
+
+ # 如果没有客服账号,尝试查找管理员账号
+ if not user_mains:
+ admin_candidates = User.query.filter(Phone=phone)
+ for admin_user in admin_candidates:
+ if admin_user.IsSuperuser or admin_user.UserType == 'admin':
+ user_mains = [admin_user]
+ break
+
+ if not user_mains:
+ logger.warning(f"登录失败,用户不存在: {phone}, IP: {client_ip}")
+ return Response({
+ 'code': 1,
+ 'msg': '用户不存在',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 遍历所有匹配的账号,验证密码和二级密码
+ target_user = None
+ target_kefu = None
+ password_matched = False
+
+ for user in user_mains:
+ # 验证主密码(bcrypt 哈希验证)
+ if not user.CheckPassword(password):
+ continue
+ password_matched = True
+
+ # 管理员:跳过二级密码和客服扩展表检查
+ is_admin = user.IsSuperuser or user.UserType == 'admin'
+ if is_admin:
+ target_user = user
+ target_kefu = None
+ break
+
+ # 客服:验证二级密码
+ kefu = user.KefuProfile
+ stored_erji = kefu.erjimima or ''
+ if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60:
+ import bcrypt as _bcrypt
+ if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')):
+ continue
+ elif stored_erji != erjimima:
+ continue
+
+ # 如果账号被禁用,记录但继续查找
+ if kefu.zhuangtai != 1:
+ if target_user is None:
+ target_user = user
+ target_kefu = kefu
+ continue
+
+ # 找到完全匹配且状态正常的账号,立即使用
+ target_user = user
+ target_kefu = kefu
+ break
+ else:
+ # 循环结束未找到正常账号
+ if target_user and target_kefu:
+ # 只有被禁用的账号匹配,返回封禁提示
+ logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}")
+ return Response({
+ 'code': 4,
+ 'msg': '账号已被封禁',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+ elif password_matched:
+ # 密码正确但二级密码错误
+ logger.warning(f"登录失败,二级密码错误: {phone}, IP: {client_ip}")
+ return Response({
+ 'code': 1,
+ 'msg': '二级密码错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+ else:
+ # 密码错误
+ logger.warning(f"登录失败,密码错误: {phone}, IP: {client_ip}")
+ return Response({
+ 'code': 1,
+ 'msg': '密码错误',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 更新IP和最后登录时间
+ target_user.IP = client_ip
+ target_user.UserLastLoginDate = timezone.now()
+ target_user.save(update_fields=['IP', 'UserLastLoginDate'])
+
+ # 6. 生成JWT token
+ refresh = RefreshToken.for_user(target_user)
+ token = str(refresh.access_token)
+
+ # 7. 返回成功数据(管理员无 KefuProfile,nicheng 用 UserName 或 Phone 兜底)
+ nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
+ return Response({
+ 'code': 0,
+ 'msg': '登录成功',
+ 'data': {
+ 'token': token,
+ 'nicheng': nicheng,
+ 'yonghuid': target_user.UserUID,
+ }
+ })
+
+ def get_client_ip(self, request):
+ """
+ 获取客户端真实IP
+ """
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(',')[0].strip()
+ if ip:
+ return ip
+ return request.META.get('REMOTE_ADDR', '0.0.0.0')
+
+
+class KefuStatsView(APIView):
+ """
+ 客服统计数据接口
+ 请求:POST /yonghu/kfjrhqzl
+ 请求体:{"phone": "13800138000"}
+ 认证:必须携带有效的 JWT token(放在 Authorization 头)
+ 返回:
+ code=0 成功,data 包含 jinrichuli, jinyuechuli, zongchuli
+ 验证失败统一返回 401,不透露具体原因
+ """
+
+ permission_classes = [IsAuthenticated] # 必须登录
+
+
+ def post(self, request):
+ # 1. 获取请求中的 phone
+ phone = request.data.get('phone', '').strip()
+ if not phone:
+ # 缺少参数,但按约定返回 401(可以改为 400,但为了统一,用401)
+ logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.UserUID}")
+ return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 获取当前登录用户(从 JWT 中解析)
+ current_user = request.user
+
+ # 3. 验证前端传来的 phone 是否与当前用户的 phone 一致
+ if current_user.Phone != phone:
+ # 不一致,可能 token 被冒用或参数错误
+ logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
+ return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 4. 验证是否为后台客服账号
+ from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
+ if not is_kefu_backend_account(current_user):
+ logger.warning(f"统计接口用户类型非客服: {current_user.UserUID}")
+ return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 5. 尝试获取客服扩展表数据(管理员跳过)
+ is_admin = is_system_super_admin(current_user)
+ if is_admin:
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'jinrichuli': 0,
+ 'jinyuechuli': 0,
+ 'zongchuli': 0,
+ }
+ }, status=status.HTTP_200_OK)
+
+ try:
+ kefu_profile = current_user.KefuProfile # OneToOneField 反向关系
+ except UserKefu.DoesNotExist:
+ logger.warning(f"统计接口客服扩展表不存在: {current_user.UserUID}")
+ return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 6. 返回统计数据(字段名与前端约定一致)
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'jinrichuli': kefu_profile.jinrichuli,
+ 'jinyuechuli': kefu_profile.jinyuechuli,
+ 'zongchuli': kefu_profile.zongchuli,
+ }
+ }, status=status.HTTP_200_OK)
+
+
+class KefuGetOrderTypesView(APIView):
+ """
+ 客服获取订单类型列表接口(平台订单用)
+ 请求:POST /yonghu/kfhqptddlx
+ 参数:{"phone": "13800138000"}
+ 认证:JWT
+ 返回:code=0 + 类型列表
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ if not phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ current_user = request.user
+
+ # 2. 验证手机号一致性
+ if getattr(current_user, 'Phone', '') != phone:
+ logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 验证用户类型及客服状态
+ if current_user.UserType not in ('kefu', 'admin'):
+ logger.warning(f"用户类型非客服: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 管理员跳过客服扩展表检查
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ if not is_admin:
+ try:
+ kefu_profile = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu_profile.zhuangtai != 1:
+ logger.warning(f"客服账号已被禁用: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 4. 查询订单类型(商品类型)
+ try:
+ # 只查询需要的字段,减少数据传输
+ types_qs = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
+ type_list = [
+ {
+ 'id': t.id,
+ 'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao
+ 'tupian_url': t.tupian_url or ''
+ }
+ for t in types_qs
+ ]
+ return Response({'code': 0, 'data': type_list})
+ except Exception as e:
+ logger.error(f"查询商品类型表失败: {str(e)}")
+ # 表不存在或查询失败,返回空列表
+ return Response({'code': 0, 'data': []})
+
+
+class KefuGetOrderListView(APIView):
+ """
+ 客服获取平台订单列表接口(发单平台=1,非跨平台)
+ 需要权限:002ab(平台订单管理)
+ 请求参数:
+ username: 客服账号(用于权限校验)
+ page: 页码
+ pageSize: 每页条数
+ dingdan_id: 订单ID(精确搜索)
+ laoban_id: 老板ID(搜索平台扩展表中的老板)
+ dashou_id: 打手ID(搜索主表中的接单打手)
+ leixing: 商品类型ID
+ zhuangtai: 状态(多个用逗号分隔,如 "1,7")
+ date: 日期(YYYY-MM-DD),筛选订单创建时间
+ nicheng: 游戏昵称(订单主表的 nicheng 字段)
+ jieshao: 订单介绍(订单主表的 jieshao 字段)
+ clkf: 处理客服(订单主表的 clkf 字段)
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ start_total = time.time()
+
+ # 1. 获取前端参数
+ username = request.data.get('phone', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 3. 检查平台订单管理权限(002ab)
+ if '002ab' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
+
+ # 4. 获取分页和筛选参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 20))
+
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ laoban_id = request.data.get('laoban_id', '').strip()
+ dashou_id = request.data.get('dashou_id', '').strip()
+ leixing = request.data.get('leixing')
+ zhuangtai = request.data.get('zhuangtai', '')
+ date = request.data.get('date', '').strip()
+ # 新增筛选条件
+ nicheng = request.data.get('nicheng', '').strip()
+ jieshao = request.data.get('jieshao', '').strip()
+ clkf = request.data.get('clkf', '').strip()
+
+ # ---------- 查询条件 ----------
+ # 基础条件:平台发单 + 兼容历史 Platform 为空
+ from jituan.services.club_context import platform_order_base_q
+ base_q = platform_order_base_q()
+
+ if dingdan_id:
+ q_conditions = base_q & Q(OrderID=dingdan_id)
+ else:
+ q_conditions = base_q
+ if leixing is not None and str(leixing).strip() != '':
+ try:
+ leixing_int = int(leixing)
+ q_conditions &= Q(ProductTypeID=leixing_int)
+ except (ValueError, TypeError):
+ pass
+ if zhuangtai and zhuangtai != 'all':
+ try:
+ status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
+ if status_list:
+ q_conditions &= Q(Status__in=status_list)
+ except (ValueError, TypeError):
+ pass
+ if dashou_id:
+ q_conditions &= Q(PlayerID=dashou_id)
+ if laoban_id:
+ q_conditions &= Q(pingtai_kuozhan__BossID=laoban_id)
+ if date:
+ q_conditions &= Q(CreateTime__date=date)
+ # 新增筛选条件
+ if nicheng:
+ q_conditions &= Q(Nickname__icontains=nicheng)
+ if jieshao:
+ q_conditions &= Q(Description__icontains=jieshao)
+ if clkf:
+ q_conditions &= Q(AssignedCS__icontains=clkf)
+
+ from jituan.services.club_context import (
+ count_order_status_buckets,
+ order_scope_qs,
+ paginate_fluent_query,
+ )
+ from orders.models import PlatformOrderExt
+
+ # 按俱乐部过滤订单列表
+ stats_qs = order_scope_qs(request).filter(q_conditions)
+ stats = stats_qs.aggregate(
+ total_orders=Count('id'),
+ completed_orders=Count('id', filter=Q(Status=3)),
+ refund_orders=Count('id', filter=Q(Status=5)),
+ pending_orders=Count('id', filter=Q(Status__in=[4, 8]))
+ )
+ status_counts = count_order_status_buckets(q_conditions, request)
+
+ list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime')
+ total_count, orders_page = paginate_fluent_query(list_qs, page, page_size)
+ if total_count > 0 and not orders_page:
+ start = (max(1, page) - 1) * page_size
+ orders_page = list(list_qs._qs[start:start + page_size])
+
+ order_ids = [o.OrderID for o in orders_page if o.OrderID]
+ ext_map = {}
+ if order_ids:
+ for ext in PlatformOrderExt.query.filter(Order__OrderID__in=order_ids).select_related('Order'):
+ if ext.Order:
+ ext_map[ext.Order.OrderID] = ext
+
+ laoban_ids = [
+ ext.BossID for ext in ext_map.values() if ext.BossID
+ ]
+ nickname_map = {}
+ if laoban_ids:
+ users = User.query.filter(UserUID__in=laoban_ids).select_related('BossProfile').only(
+ 'UserUID', 'BossProfile__nickname'
+ )
+ for user in users:
+ nickname_map[user.UserUID] = (user.BossProfile.nickname or '') if getattr(user, 'BossProfile', None) else ''
+
+ result_list = []
+ for order in orders_page:
+ ext = ext_map.get(order.OrderID)
+ laoban_id = ext.BossID if ext else ''
+ youxi_nicheng = nickname_map.get(laoban_id, order.Nickname or '')
+ result_list.append({
+ 'dingdan_id': order.OrderID or '',
+ 'jieshao': order.Description or '',
+ 'zhuangtai': order.Status,
+ 'leixing': order.ProductTypeID,
+ 'fadanpingtai': order.Platform,
+ 'jiage': float(order.Amount) if order.Amount else 0.00,
+ 'tupian_url': order.ImageURL or '',
+ 'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
+ 'youxi_nicheng': youxi_nicheng
+ })
+
+ from jituan.services.club_user_access import list_response_meta
+
+ response_data = {
+ 'stats': {
+ 'totalOrders': stats['total_orders'] or 0,
+ 'completedOrders': stats['completed_orders'] or 0,
+ 'refundOrders': stats['refund_orders'] or 0,
+ 'pendingOrders': stats['pending_orders'] or 0
+ },
+ 'statusCounts': status_counts,
+ 'list': result_list,
+ 'total': total_count,
+ 'page': page,
+ 'pageSize': page_size,
+ **list_response_meta(request),
+ }
+
+ logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s")
+ return Response({'code': 0, 'data': response_data})
+
+
+class KefuGetShangjiaOrderListView(APIView):
+ """
+ 客服获取商家订单列表接口(发单平台=2,非跨平台)
+ 需要权限:002ac(商家订单管理)—— 如不区分可使用 002ab
+ 请求参数:
+ username: 客服账号(用于权限校验)
+ page: 页码
+ pageSize: 每页条数
+ dingdan_id: 订单ID(精确搜索)
+ shangjia_id: 商家ID(搜索商家扩展表中的商家ID)
+ dashou_id: 打手ID(搜索主表中的接单打手)
+ leixing: 商品类型ID
+ zhuangtai: 状态(多个用逗号分隔,如 "1,7")
+ date: 日期(YYYY-MM-DD),筛选订单创建时间
+ nicheng: 游戏昵称(订单主表的 nicheng 字段)
+ jieshao: 订单介绍(订单主表的 jieshao 字段)
+ clkf: 处理客服(订单主表的 clkf 字段)
+ shangjia_nicheng: 商家昵称(商家扩展表中的昵称)
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ start_total = time.time()
+
+ # 1. 获取前端参数(兼容 phone / username)
+ username = request.data.get('username', '').strip() or request.data.get('phone', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 3. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab)
+ if '002ac' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看商家订单列表'})
+
+ # 4. 获取分页和筛选参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 20))
+
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ shangjia_id = request.data.get('shangjia_id', '').strip()
+ dashou_id = request.data.get('dashou_id', '').strip()
+ leixing = request.data.get('leixing')
+ zhuangtai = request.data.get('zhuangtai', '')
+ date = request.data.get('date', '').strip()
+ # 新增筛选条件
+ nicheng = request.data.get('nicheng', '').strip()
+ jieshao = request.data.get('jieshao', '').strip()
+ clkf = request.data.get('clkf', '').strip()
+ shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip()
+
+ # ---------- 查询条件 ----------
+ from jituan.services.club_context import merchant_order_base_q
+ base_q = merchant_order_base_q()
+
+ if dingdan_id:
+ q_conditions = base_q & Q(OrderID=dingdan_id)
+ else:
+ q_conditions = base_q
+ if leixing is not None and str(leixing).strip() != '':
+ try:
+ leixing_int = int(leixing)
+ q_conditions &= Q(ProductTypeID=leixing_int)
+ except (ValueError, TypeError):
+ pass
+ if zhuangtai and zhuangtai != 'all':
+ try:
+ status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
+ if status_list:
+ q_conditions &= Q(Status__in=status_list)
+ except (ValueError, TypeError):
+ pass
+ if dashou_id:
+ q_conditions &= Q(PlayerID=dashou_id)
+ if shangjia_id:
+ q_conditions &= Q(shangjia_kuozhan__MerchantID=shangjia_id)
+ if date:
+ q_conditions &= Q(CreateTime__date=date)
+ if nicheng:
+ q_conditions &= Q(Nickname__icontains=nicheng)
+ if jieshao:
+ q_conditions &= Q(Description__icontains=jieshao)
+ if clkf:
+ q_conditions &= Q(AssignedCS__icontains=clkf)
+ if shangjia_nicheng:
+ # 通过商家扩展表关联 UserShangjia 的昵称
+ q_conditions &= Q(shangjia_kuozhan__MerchantID__in=User.query.filter(
+ ShopProfile__nicheng__icontains=shangjia_nicheng
+ ).values_list('UserUID', flat=True))
+
+ from jituan.services.club_context import (
+ count_order_status_buckets,
+ order_scope_qs,
+ paginate_fluent_query,
+ )
+ from orders.models import MerchantOrderExt
+
+ stats_qs = order_scope_qs(request).filter(q_conditions)
+ stats = stats_qs.aggregate(
+ total_orders=Count('id'),
+ completed_orders=Count('id', filter=Q(Status=3)),
+ refund_orders=Count('id', filter=Q(Status=5)),
+ pending_orders=Count('id', filter=Q(Status__in=[4, 8]))
+ )
+ status_counts = count_order_status_buckets(q_conditions, request)
+
+ list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime')
+ total_count, orders_page = paginate_fluent_query(list_qs, page, page_size)
+ if total_count > 0 and not orders_page:
+ start = (max(1, page) - 1) * page_size
+ orders_page = list(list_qs._qs[start:start + page_size])
+
+ order_ids = [o.OrderID for o in orders_page if o.OrderID]
+ ext_map = {}
+ if order_ids:
+ for ext in MerchantOrderExt.query.filter(Order__OrderID__in=order_ids).select_related('Order'):
+ if ext.Order:
+ ext_map[ext.Order.OrderID] = ext
+
+ shangjia_ids = [
+ ext.MerchantID for ext in ext_map.values() if ext.MerchantID
+ ]
+ nickname_map = {}
+ if shangjia_ids:
+ users = User.query.filter(UserUID__in=shangjia_ids).select_related('ShopProfile').only(
+ 'UserUID', 'ShopProfile__nicheng'
+ )
+ for user in users:
+ nickname_map[user.UserUID] = user.ShopProfile.nicheng or '' if hasattr(user, 'ShopProfile') else ''
+
+ result_list = []
+ for order in orders_page:
+ ext = ext_map.get(order.OrderID)
+ shangjia_id = ext.MerchantID if ext else ''
+ youxi_nicheng = order.Nickname or ''
+ result_list.append({
+ 'dingdan_id': order.OrderID or '',
+ 'jieshao': order.Description or '',
+ 'zhuangtai': order.Status,
+ 'leixing': order.ProductTypeID,
+ 'fadanpingtai': order.Platform,
+ 'jiage': float(order.Amount) if order.Amount else 0.00,
+ 'tupian_url': order.ImageURL or '',
+ 'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
+ 'youxi_nicheng': youxi_nicheng
+ })
+
+ from jituan.services.club_user_access import list_response_meta
+
+ response_data = {
+ 'stats': {
+ 'totalOrders': stats['total_orders'] or 0,
+ 'completedOrders': stats['completed_orders'] or 0,
+ 'refundOrders': stats['refund_orders'] or 0,
+ 'pendingOrders': stats['pending_orders'] or 0
+ },
+ 'statusCounts': status_counts,
+ 'list': result_list,
+ 'total': total_count,
+ 'page': page,
+ 'pageSize': page_size,
+ **list_response_meta(request),
+ }
+
+ logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s")
+ return Response({'code': 0, 'data': response_data})
+
+
+class KefuChangeDashouView(APIView):
+ """
+ 客服更换打手接口
+ 请求:POST /yonghu/kefughds
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "new_dashou_id": "新打手ID"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ new_dashou_id = request.data.get('new_dashou_id', '').strip()
+
+ if not phone or not dingdan_id or not new_dashou_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ logger.warning(f"用户类型非客服: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单
+ try:
+ order = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8)
+ if order.Status not in [2, 8]:
+ return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 查询新打手
+ try:
+ new_dashou_user = User.query.get(UserUID=new_dashou_id)
+ except User.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 6. 验证打手扩展表
+ try:
+ new_dashou_profile = new_dashou_user.DashouProfile
+ except ObjectDoesNotExist:
+ return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 7. 验证打手状态(打手状态和账号状态必须为1)
+ if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
+ return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST)
+ # 8. 验证打手积分是否足够(至少5分)
+ if new_dashou_profile.jifen < 5:
+ return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 8. 根据订单的抢单要求类型进行额外验证
+ yaoqiuleixing = order.GrabRequirement
+
+ if yaoqiuleixing == 1: # 会员抢单
+ huiyuan_id = order.MembershipID
+ if not huiyuan_id:
+ return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ # 查询打手的会员购买记录
+ huiyuan_goumai = Huiyuangoumai.query.get(
+ huiyuan_id=huiyuan_id,
+ yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID
+ )
+ except Huiyuangoumai.DoesNotExist:
+ return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 检查会员是否过期(调用模型方法)
+ is_expired = huiyuan_goumai.jiance_shifou_daoqi()
+ if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
+ return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST)
+
+ elif yaoqiuleixing == 2: # 押金抢单
+ order_yajin = order.CommissionReq # 假设订单的 yongjin 字段存储押金要求
+ if order_yajin is None:
+ return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if new_dashou_profile.yajin < order_yajin:
+ return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST)
+ else:
+ return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 9. 所有验证通过,开始更换打手(使用事务)
+ with transaction.atomic():
+ # 9.1 更新新打手扩展表
+ new_dashou_profile.zhuangtai = 0 # 设为接单中
+ new_dashou_profile.jiedanzongliang += 1
+ new_dashou_profile.jinrijiedan += 1
+ new_dashou_profile.jinyuejiedan += 1
+ new_dashou_profile.save()
+
+ # 9.2 处理原打手(如果存在)
+ old_dashou_id = order.PlayerID
+ if old_dashou_id:
+ try:
+ old_dashou_user = User.query.get(UserUID=old_dashou_id)
+ old_dashou_profile = old_dashou_user.DashouProfile
+ old_dashou_profile.zhuangtai = 1 # 恢复空闲
+ old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑)
+ old_dashou_profile.save()
+ logger.info(f"原打手 {old_dashou_id} 状态已更新")
+ except (User.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新")
+ except Exception as e:
+ logger.error(f"更新原打手时异常: {str(e)}", exc_info=True)
+
+ # 9.3 更新订单
+ order.PlayerID = new_dashou_id
+ order.Status = 2 # 进行中
+ order.AssignedCS = current_user.UserUID # 记录处理客服ID
+ order.save()
+
+ return Response({'code': 0, 'msg': '更换打手成功'})
+
+
+class KefuRecoverOrderView(APIView):
+ """
+ 客服恢复订单接口(仅状态9可恢复)
+ 请求:POST /yonghu/kefuhf
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单
+ try:
+ order = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 验证订单状态是否为9(未付款)
+ if order.Status != 9:
+ return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 根据是否有指定ID决定新状态
+ if order.AssignedID:
+ new_status = 7 # 指定中
+ else:
+ new_status = 1 # 下单中
+
+ # 6. 更新订单
+ order.Status = new_status
+ order.AssignedCS = current_user.UserUID
+ order.save()
+
+ return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}})
+
+
+class KefuPlatformRefundView(APIView):
+ """
+ 客服平台订单退款接口(调用微信支付)
+ 请求:POST /yonghu/kefupttk
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "tuikuan_liyou": "退款理由(可选)"
+ }
+ 认证:JWT
+ 返回:code=0 成功,code=其他 失败
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ logger.warning(f"用户类型非客服: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单并加锁
+ try:
+ with transaction.atomic():
+ order = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id)
+
+ # 4. 验证平台订单
+ if order.Platform != 1:
+ return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中)
+ allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态
+ if order.Status not in allowed_statuses:
+ return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 6. 获取微信交易号(如果存在)
+ wechat_transaction_id = getattr(order, 'WechatTransactionID', None)
+
+ # 7. 调用微信退款接口
+ refund_result = self.call_wechat_refund(
+ dingdan_id=order.OrderID,
+ jine=order.Amount,
+ transaction_id=wechat_transaction_id
+ )
+
+ if refund_result['code'] != 0:
+ # 微信退款失败,直接返回错误,数据库不更新
+ logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}")
+ return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 8. 更新订单状态为5(已退款)
+ order.Status = 5
+ order.AssignedCS = current_user.Phone # 记录处理客服手机号
+ if tuikuan_liyou:
+ order.RefundReason = tuikuan_liyou
+ order.save()
+
+ # 9. 更新打手数据
+ jiedan_dashou_id = order.PlayerID
+ if jiedan_dashou_id:
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.zhuangtai = 1 # 打手状态设为空闲
+ dashou_profile.save()
+ logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1")
+ except (User.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
+
+ # ========== 新增:商品和店铺每日统计 ==========
+ 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.ProductID}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ dianpu_id = None
+ if hasattr(order, 'pingtai_kuozhan'):
+ dianpu_id = order.pingtai_kuozhan.ShopID
+ 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())
+ # 统计失败不影响主流程
+ # =============================================
+
+ # 10. 更新退款记录表(假设存在 RefundRecord 模型,字段需根据实际调整)
+
+ #更新支出记录
+ update_daily_payout(order.Amount, getattr(order, 'ClubID', None))
+
+
+ try:
+ refund_record, created = RefundRecord.query.get_or_create(
+ OrderID=dingdan_id,
+ defaults={
+ 'PlayerID': jiedan_dashou_id or '',
+ 'ApplicantID': '',
+ 'ProcessorID': phone,
+ 'Reason': tuikuan_liyou or '客服退款',
+ 'ApplyStatus': 1, # 成功
+ 'Amount': order.Amount,
+ 'PlayerCommission': order.PlayerCommission or 0,
+ 'Description': order.Description or '',
+ 'Remark': '客服平台退款',
+ 'Nickname': order.Nickname or '',
+ }
+ )
+ if not created:
+ refund_record.ApplyStatus = 1
+ refund_record.ProcessorID = phone
+ if tuikuan_liyou:
+ refund_record.Reason = tuikuan_liyou
+ refund_record.save()
+ except Exception as e:
+ logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True)
+
+ # 11. 更新客服统计数据
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=order.PlayerCommission,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+ logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}")
+ return Response({'code': 0, 'msg': '退款成功'})
+
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def call_wechat_refund(self, dingdan_id, jine, transaction_id=None):
+ """
+ 调用微信支付退款接口
+ :param dingdan_id: 商户订单号(我们的订单ID)
+ :param jine: 退款金额
+ :param transaction_id: 微信支付订单号(优先使用)
+ :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
+ """
+ # 获取配置
+ 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]):
+ missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
+ logger.error(f"微信支付配置缺失: {missing}")
+ return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'}
+
+ # 生成退款单号
+ out_refund_no = self.generate_refund_no(dingdan_id)
+ total_fee = yuan_to_fen(jine)
+ refund_fee = total_fee
+ refund_desc = '客服退款'
+
+ # 构造请求参数
+ 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': refund_desc,
+ }
+ if transaction_id:
+ params['transaction_id'] = transaction_id
+ else:
+ params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号
+
+ # 生成签名
+ sorted_keys = sorted(params.keys())
+ stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
+ stringSignTemp = f"{stringA}&key={key}"
+ sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
+ params['sign'] = sign
+
+ xml_data = self.dict_to_xml(params)
+ url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
+
+ try:
+ response = requests.post(
+ url,
+ data=xml_data,
+ cert=(cert_path, key_path),
+ timeout=10
+ )
+ result = xmltodict.parse(response.content)['xml']
+ logger.info(f"微信退款响应: {result}")
+ except Exception as e:
+ logger.error(f"微信退款请求异常: {str(e)}", exc_info=True)
+ return {'code': 500, 'msg': '微信退款请求异常'}
+
+ if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
+ return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no}
+ else:
+ err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
+ logger.warning(f"微信退款失败: {err_msg}")
+ return {'code': 400, 'msg': err_msg}
+
+ 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 = ['']
+ for k, v in data.items():
+ xml.append(f'<{k}>{v}{k}>')
+ xml.append('')
+ return ''.join(xml)
+
+
+class KefuRejectRefundView(APIView):
+ """
+ 客服拒绝退款接口
+ 请求:POST /yonghu/kefujjtk
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "jujue_liyou": "拒绝理由(可选)"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ jujue_liyou = request.data.get('jujue_liyou', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单并加锁
+ try:
+ with transaction.atomic():
+ order = Order.objects.select_for_update().get(OrderID=dingdan_id)
+
+ # 4. 验证订单状态为退款审核中(4)
+ if order.Status != 4:
+ return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取必要字段
+ jiedan_dashou_id = order.PlayerID
+ dashou_fencheng = order.PlayerCommission
+ fadan_pingtai = order.Platform
+
+ # 5. 验证打手分成金额是否存在
+ if dashou_fencheng is None:
+ return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 6. 更新订单状态为退款失败(6)
+ order.Status = 6
+ order.AssignedCS = current_user.Phone # 记录处理客服手机号
+ if jujue_liyou:
+ order.RefundReason = jujue_liyou
+ order.save()
+
+ # 7. 给打手结算(返还分成)
+ if jiedan_dashou_id:
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_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.save()
+ logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}")
+ except (User.DoesNotExist, AttributeError):
+ logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
+
+ # 8. 如果是商家发单,更新商家成交订单数量
+ if fadan_pingtai == 2:
+ try:
+ # 获取商家订单扩展表
+ shangjia_kuozhan = order.shangjia_kuozhan
+ shangjia_id = shangjia_kuozhan.MerchantID
+ if shangjia_id:
+ shangjia_user = User.query.get(UserUID=shangjia_id)
+ shangjia_profile = shangjia_user.ShopProfile
+ shangjia_profile.chengjiao += 1
+ shangjia_profile.save()
+ logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1")
+
+ # ✅ 新方法(行为驱动,更安全)
+ update_shangjia_daily(
+ yonghuid=shangjia_id,
+ amount=Decimal(str(order.Amount)),
+ action=2, # 2 = 结算
+ order_id=dingdan_id,
+ )
+ except (ObjectDoesNotExist, User.DoesNotExist, AttributeError):
+ logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新")
+
+ # 9. 更新退款记录表
+ try:
+ refund_record = RefundRecord.query.get(OrderID=dingdan_id)
+ refund_record.ApplyStatus = 2 # 2表示拒绝
+ refund_record.ProcessorID = phone
+ refund_record.RejectReason = jujue_liyou # 记录驳回理由(假设模型有此字段)
+ refund_record.save()
+ logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}")
+ except RefundRecord.DoesNotExist:
+ # 如果退款记录不存在,可以创建一条记录(可选)
+ RefundRecord.query.create(
+ OrderID=dingdan_id,
+ PlayerID=jiedan_dashou_id or '',
+ ApplicantID='',
+ ProcessorID=phone,
+ Reason=jujue_liyou or '客服拒绝退款',
+ ApplyStatus=2,
+ Amount=order.Amount,
+ PlayerCommission=dashou_fencheng or 0,
+ Description=order.Description or '',
+ Remark='客服拒绝退款',
+ Nickname=order.Nickname or '',
+ RejectReason=jujue_liyou or '',
+ )
+ except Exception as e:
+ logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True)
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=dashou_fencheng,
+ action=2 # 2 表示成交(结算)
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+ # 10. 更新客服统计数据
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ return Response({'code': 0, 'msg': '拒绝退款成功'})
+
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class KefuMerchantRefundView(APIView):
+ """
+ 客服商家订单退款接口(发单平台=2)
+ 请求:POST /yonghu/kefusjtk
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "tuikuan_liyou": "退款理由(可选)"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 客服身份验证(同前,省略以节省篇幅,但实际必须完整)
+ current_user = request.user
+ # ... 验证
+ if getattr(current_user, 'Phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+
+ # 查询订单,预加载商家扩展表
+ try:
+ order = 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 order.Platform != 2:
+ return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证状态允许退款
+ allowed_statuses = [1, 7, 2, 8, 4]
+ if order.Status not in allowed_statuses:
+ return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取必要数据
+ jiedan_dashou_id = order.PlayerID
+ jine = order.Amount
+ if jine is None:
+ return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ shangjia_kuozhan = order.shangjia_kuozhan
+ shangjia_id = shangjia_kuozhan.MerchantID
+ except ObjectDoesNotExist:
+ return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST)
+
+ with transaction.atomic():
+ # 更新订单
+ order.Status = 5
+ order.AssignedCS = current_user.Phone
+ if tuikuan_liyou:
+ order.RefundReason = tuikuan_liyou
+ order.save()
+
+ # 更新打手
+ if jiedan_dashou_id:
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_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"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新")
+
+ # 更新商家(客服额度出资的订单不退商家余额,仅释放子客服额度)
+ try:
+ from merchant_ops.services.wallet import is_staff_quota_order
+ shangjia_user = User.query.get(UserUID=shangjia_id)
+ shangjia_profile = shangjia_user.ShopProfile
+ shangjia_profile.tuikuan += 1
+ if not is_staff_quota_order(dingdan_id):
+ shangjia_profile.yue += jine
+ shangjia_profile.save()
+ except (User.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新")
+
+ # 更新退款记录表
+ try:
+ refund_record = RefundRecord.query.get(OrderID=dingdan_id)
+ refund_record.ApplyStatus = 1
+ refund_record.ProcessorID = phone
+ refund_record.save()
+ except RefundRecord.DoesNotExist:
+ pass
+
+ # 更新客服统计
+ update_shangjia_daily(
+ yonghuid=shangjia_id,
+ amount=Decimal(str(order.Amount)),
+ action=3, # 3 = 退款
+ order_id=dingdan_id,
+ )
+
+
+ # 调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=order.PlayerCommission,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+
+
+ # 管理员无客服扩展表,跳过计数器更新
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ if not is_admin:
+ kefu = current_user.KefuProfile
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ return Response({'code': 0, 'msg': '退款成功'})
+
+
+class KefuRejectSettlementView(APIView):
+ """
+ 客服拒绝结算接口(仅平台订单,状态2/8)
+ 请求:POST /yonghu/kefujjjs
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "tkly": "拒绝结算理由(必填)"
+ }
+ 认证:JWT
+ 返回:code=0 成功,其他失败
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ tkly = request.data.get('tkly', '').strip()
+
+ if not phone or not dingdan_id or not tkly:
+ return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ logger.warning(f"用户类型非客服: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单,预加载平台扩展表
+ try:
+ order = Order.query.select_related('pingtai_kuozhan').get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 校验订单条件:平台发单(1)且状态为2或8
+ if order.Platform != 1 or order.Status not in [2, 8]:
+ return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST)
+
+ jiedan_dashou_id = order.PlayerID
+ if not jiedan_dashou_id:
+ return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 使用事务更新
+ with transaction.atomic():
+ # 5.1 更新订单主表
+ order.Status = 5 # 已退款
+ order.RefundReason = tkly # 拒绝理由
+ order.AssignedCS = phone # 记录客服手机号
+ order.save()
+
+ # 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi)
+ # 注意:需要确保 PlatformOrderExt 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0
+ if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan:
+ pingtai_ext = order.pingtai_kuozhan
+ pingtai_ext.RejectSettlementFlag = 1 # 1表示拒绝结算
+ pingtai_ext.save()
+ else:
+ # 理论上应该存在,但若不存在则记录日志
+ logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算")
+
+ # 5.3 更新打手扩展表
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ dashou_profile.zhuangtai = 1 # 打手状态设为空闲
+ dashou_profile.tuikuanliang += 1 # 退款订单总量+1
+ dashou_profile.save()
+ logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲")
+ except (User.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
+ except Exception as e:
+ logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True)
+
+ # 5.4 更新客服扩展表统计数据
+
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=order.PlayerCommission,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+
+
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ return Response({'code': 0, 'msg': '拒绝结算成功'})
+
+
+class KefuTransferHallView(APIView):
+ """
+ 客服恢复大厅(转移大厅)接口
+ 请求:POST /yonghu/kefuzydt
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证(同前,略,但必须完整实现)
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ if current_user.UserType not in ('kefu', 'admin'):
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单
+ try:
+ order = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 校验订单状态(允许状态:2,8,4)
+ if order.Status not in [2, 8, 4]:
+ return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST)
+
+
+ if order.PlayerID:
+ UserDashou.query.filter(user__UserUID=order.PlayerID).update(zhuangtai=1)
+ else:
+ pass
+
+ # 5. 根据是否有指定ID决定新状态
+ if order.AssignedID:
+ new_status = 7 # 指定中
+ else:
+ new_status = 1 # 下单中
+
+ # 6. 使用事务更新
+ with transaction.atomic():
+ # 清空接单打手ID,更新状态,记录客服
+ order.PlayerID = None
+ order.Status = new_status
+ order.AssignedCS = phone
+ order.save()
+
+ return Response({'code': 0, 'msg': '转移大厅成功'})
+
+
+class KefuCancelDesignationView(APIView):
+ """
+ 客服取消指定接口(仅状态7)
+ 请求:POST /yonghu/kefuqxzd
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证(同前,略,但必须完整实现)
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ if current_user.UserType not in ('kefu', 'admin'):
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单
+ try:
+ order = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 校验订单状态为7(指定中)
+
+ if order.Status != 7:
+ print(999)
+ return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 使用事务更新
+ with transaction.atomic():
+ # 状态改为1(下单中),清空指定ID,记录客服
+ order.Status = 1
+ order.AssignedID = None
+ order.AssignedCS = phone
+ order.save()
+
+ return Response({'code': 0, 'msg': '取消指定成功'})
+
+
+class KefuGetDashouListView(APIView):
+ """
+ 客服获取打手列表接口(仅打手类型)
+ 请求:POST /yonghu/kefuhqdslb
+ 参数:{
+ "phone": "客服手机号",
+ "keyword": "搜索关键词(可选,搜索用户ID或昵称)",
+ "page": "页码(从1开始)",
+ "page_size": "每页数量"
+ }
+ 认证:JWT
+ 返回:{
+ "code": 0,
+ "data": {
+ "list": [打手对象],
+ "has_more": true/false
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('zhanghao', '').strip()
+ keyword = request.data.get('keyword', '').strip()
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ except ValueError:
+ return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if not phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ logger.warning(f"用户类型非客服: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 构建查询条件(只查询打手)
+ # 通过 UserDashou 扩展表关联 User
+ queryset = UserDashou.query.select_related('user').all()
+
+ if keyword:
+ # 搜索用户ID或打手昵称
+ queryset = queryset.filter(
+ Q(user__UserUID__icontains=keyword) |
+ Q(nicheng__icontains=keyword)
+ )
+
+ # 4. 计算总数并分页
+ total = queryset.count()
+ offset = (page - 1) * page_size
+ dashou_list = queryset.order_by('-CreateTime')[offset:offset + page_size]
+
+ # 5. 构建返回数据(只取需要的字段)
+ result = []
+ for dashou in dashou_list:
+ user = dashou.user
+ result.append({
+ 'yonghuid': user.UserUID,
+ 'avatar': user.Avatar or '',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'nicheng': dashou.nicheng or '',
+ 'zhuangtai': dashou.zhuangtai,
+ })
+
+ # 6. 判断是否有更多数据
+ has_more = (offset + len(dashou_list)) < total
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': result,
+ 'has_more': has_more
+ }
+ })
+
+
+class KefuGetDashouDetailView(APIView):
+ """
+ 客服获取打手详情接口
+ 请求:POST /yonghu/kefuhqdsxq
+ 参数:{
+ "phone": "客服手机号",
+ "uid": "打手ID (yonghuid)"
+ }
+ 认证:JWT
+ 返回:code=0 + 打手信息 + 会员列表
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ uid = request.data.get('uid', '').strip()
+
+ if not phone or not uid:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ logger.warning(f"用户类型非客服: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询打手主表和扩展表
+ try:
+ # 使用 select_related 预加载 dashou_profile,减少数据库查询
+ user_main = User.query.select_related('DashouProfile').get(
+ UserUID=uid
+ )
+ except User.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 验证用户类型是否为打手(可选,但建议验证)
+ # 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
+ try:
+ dashou = user_main.DashouProfile
+ except ObjectDoesNotExist:
+ # 用户存在但未注册打手身份,返回基本信息
+ user_info = {
+ 'yonghuid': user_main.UserUID,
+ 'avatar': user_main.Avatar or '',
+ 'zaixianzhuangtai': 0,
+ 'zhanghaozhuangtai': 0,
+ 'zhuangtai': 0,
+ 'nicheng': '',
+ 'chenghao': '',
+ 'dianhua': '',
+ 'wechat': '',
+ 'phone': user_main.Phone or '',
+ 'CreateTime': user_main.UserCreateTime,
+ 'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
+ 'ip': user_main.IP or '',
+ 'jiedanzongliang': 0,
+ 'chengjiaozongliang': 0,
+ 'tuikuanliang': 0,
+ 'jinrijiedan': 0,
+ 'yue': 0.00,
+ 'zonge': 0.00,
+ 'jifen': 0,
+ 'yajin': 0.00,
+ 'jinrishouyi': 0.00,
+ 'jinyueshouyi': 0.00,
+ 'yaoqingren': '',
+ 'jieshao': '',
+ }
+ member_list = []
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'user_info': user_info,
+ 'member_list': member_list
+ }
+ })
+
+ # 4. 构建打手信息
+ user_info = {
+ 'yonghuid': user_main.UserUID,
+ 'avatar': user_main.Avatar or '',
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai,
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
+ 'zhuangtai': dashou.zhuangtai,
+ 'nicheng': dashou.nicheng or '',
+ 'chenghao': dashou.chenghao or '',
+ 'dianhua': dashou.dianhua or '',
+ 'wechat': dashou.wechat or '',
+ 'phone': user_main.Phone or '',
+ 'CreateTime': user_main.UserCreateTime,
+ 'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
+ 'ip': user_main.IP or '',
+ 'jiedanzongliang': dashou.jiedanzongliang,
+ 'chengjiaozongliang': dashou.chengjiaozongliang,
+ 'tuikuanliang': dashou.tuikuanliang,
+ 'jinrijiedan': dashou.jinrijiedan,
+ 'yue': float(dashou.yue) if dashou.yue else 0.00,
+ 'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
+ 'jifen': dashou.jifen,
+ 'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
+ 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
+ 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
+ 'yaoqingren': dashou.yaoqingren or '',
+ 'jieshao': dashou.jieshao or '',
+ }
+
+ # 5. 查询打手会员信息(仅未过期)
+ member_records = Huiyuangoumai.query.filter(
+ yonghu_id=uid
+ ).order_by('-CreateTime')
+
+ member_list = []
+ for record in member_records:
+ # 调用模型方法检查是否过期
+ if hasattr(record, 'jiance_shifou_daoqi'):
+ is_expired = record.jiance_shifou_daoqi()
+ else:
+ # 如果方法不存在,简单判断
+ is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
+
+ if not is_expired:
+ # 获取会员详情
+ try:
+ huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id)
+ huiyuan_name = huiyuan.jieshao
+ except Huiyuan.DoesNotExist:
+ huiyuan_name = '未知会员'
+
+ member_list.append({
+ 'id': record.id,
+ 'huiyuan_name': huiyuan_name,
+ 'daoqi_time': record.daoqi_time,
+ 'is_active': record.huiyuan_zhuangtai == 1,
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'user_info': user_info,
+ 'member_list': member_list
+ }
+ })
+
+
+class KefuUpdateDashouView(APIView):
+ permission_classes = [IsAuthenticated]
+ """
+ 客服修改打手信息接口
+ 支持操作:
+ - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
+ - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
+ - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
+ - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
+ - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
+ - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
+ - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
+ """
+ def post(self, request):
+ # 1. 获取前端参数
+ username = request.data.get('username', '').strip()
+ dashou_id = request.data.get('dashou_id', '').strip()
+ caozuo = request.data.get('caozuo', '').strip()
+ value = request.data.get('value') # 金额、积分、天数等
+ huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID(加/减会员时使用)
+
+ if not username or not dashou_id or not caozuo:
+ return Response({'code': 400, 'msg': '参数不完整'})
+
+ # 2. 权限校验(统一方法)
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions # 直接返回错误响应
+
+ # 3. 查询打手是否存在
+ try:
+ dashou_user = User.query.get(UserUID=dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ except User.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手不存在'})
+ except UserDashou.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手扩展信息缺失'})
+
+ # 4. 根据操作类型执行(事务内)
+ with transaction.atomic():
+ try:
+ # 记录修改前的值(用于日志)
+ before = {
+ 'yue': dashou_profile.yue,
+ 'yajin': dashou_profile.yajin,
+ 'jifen': dashou_profile.jifen,
+ 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'zhuangtai': dashou_profile.zhuangtai,
+ }
+
+ # ---------- 余额操作 ----------
+ if caozuo == 'jian_yue':
+ if '001aa' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减打手余额'})
+ amount = Decimal(str(value))
+ if dashou_profile.yue < amount:
+ return Response({'code': 400, 'msg': '余额不足'})
+ dashou_profile.yue -= amount
+ dashou_profile.save()
+ after_yue = dashou_profile.yue
+ # 记录日志
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2, # 2=打手
+ xiugaitijiao=after_yue,
+ xiugaitijiaoq=before['yue'],
+ )
+ return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}})
+
+ elif caozuo == 'jia_yue':
+ if '001bb' not in permissions:
+ return Response({'code': 403, 'msg': '无权限加打手余额'})
+ from jituan.services.group_finance_gate import deny_group_finance_exec
+ deny = deny_group_finance_exec(request.user, request)
+ if deny:
+ return deny
+ amount = Decimal(str(value))
+ dashou_profile.yue += amount
+ dashou_profile.save()
+ after_yue = dashou_profile.yue
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2,
+ xiugaitijiao=after_yue,
+ xiugaitijiaoq=before['yue'],
+ )
+ return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}})
+
+ # ---------- 押金操作 ----------
+ elif caozuo == 'jian_yajin':
+ if '001aa' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减打手押金'})
+ amount = Decimal(str(value))
+ if dashou_profile.yajin < amount:
+ return Response({'code': 400, 'msg': '押金不足'})
+ dashou_profile.yajin -= amount
+ dashou_profile.save()
+ after_yajin = dashou_profile.yajin
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2,
+ yajin=after_yajin,
+ yyajin=before['yajin'],
+ )
+ return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}})
+
+ elif caozuo == 'jia_yajin':
+ if '001bb' not in permissions:
+ return Response({'code': 403, 'msg': '无权限加打手押金'})
+ from jituan.services.group_finance_gate import deny_group_finance_exec
+ deny = deny_group_finance_exec(request.user, request)
+ if deny:
+ return deny
+ amount = Decimal(str(value))
+ dashou_profile.yajin += amount
+ dashou_profile.save()
+ after_yajin = dashou_profile.yajin
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2,
+ yajin=after_yajin,
+ yyajin=before['yajin'],
+ )
+ return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}})
+
+ # ---------- 积分操作 ----------
+ elif caozuo == 'jian_jifen':
+ if '001cc' not in permissions:
+ return Response({'code': 403, 'msg': '无权限减打手积分'})
+ jifen = int(value)
+ if dashou_profile.jifen < jifen:
+ return Response({'code': 400, 'msg': '积分不足'})
+ dashou_profile.jifen -= jifen
+ dashou_profile.save()
+ after_jifen = dashou_profile.jifen
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2,
+ jifen=after_jifen,
+ yjifen=before['jifen'],
+ )
+ return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}})
+
+ elif caozuo == 'jia_jifen':
+ if '001dd' not in permissions:
+ return Response({'code': 403, 'msg': '无权限加打手积分'})
+ jifen = int(value)
+ dashou_profile.jifen += jifen
+ dashou_profile.save()
+ after_jifen = dashou_profile.jifen
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2,
+ jifen=after_jifen,
+ yjifen=before['jifen'],
+ )
+ return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}})
+
+ # ---------- 状态修改 ----------
+ elif caozuo == 'gai_zhanghaozhuangtai':
+ if '001ee' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改打手账号状态'})
+ new_status = int(value)
+ if new_status not in [0, 1]:
+ return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'})
+ dashou_profile.zhanghaozhuangtai = new_status
+ dashou_profile.save()
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2,
+ qitashuoming=f'修改账号状态为{new_status}',
+ )
+ return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}})
+
+ elif caozuo == 'gai_zaixianzhuangtai':
+ if '001ee' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改打手在线状态'})
+ new_status = int(value)
+ if new_status not in [0, 1]:
+ return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'})
+ dashou_profile.zaixianzhuangtai = new_status
+ dashou_profile.save()
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2,
+ qitashuoming=f'修改在线状态为{new_status}',
+ )
+ return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}})
+
+ elif caozuo == 'gai_zhuangtai':
+ if '001ee' not in permissions:
+ return Response({'code': 403, 'msg': '无权限修改打手接单状态'})
+ new_status = int(value)
+ if new_status not in [0, 1]:
+ return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'})
+ dashou_profile.zhuangtai = new_status
+ dashou_profile.save()
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2,
+ qitashuoming=f'修改接单状态为{new_status}',
+ )
+ return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
+
+ # ---------- 会员操作 ----------
+ elif caozuo == 'jia_huiyuan':
+ if '001ff' not in permissions:
+ return Response({'code': 403, 'msg': '无权限给打手加会员'})
+ if not huiyuan_id:
+ return Response({'code': 400, 'msg': '缺少会员ID'})
+ try:
+ huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
+ except Huiyuan.DoesNotExist:
+ return Response({'code': 404, 'msg': '会员类型不存在'})
+ from jituan.services.club_user import get_user_club_id
+ from jituan.services.member_recharge import extend_member_entitlement_admin, get_club_huiyuan_row
+ player_club = get_user_club_id(dashou_user)
+ row = get_club_huiyuan_row(player_club, huiyuan_id, require_enabled=False)
+ days = int(value) if value else int(row.formal_days or 30) if row else 30
+ record, err = extend_member_entitlement_admin(
+ dashou_id, huiyuan_id, days, player_club,
+ )
+ if err:
+ return Response({'code': 400, 'msg': err})
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2,
+ huiyuants=days,
+ huiyuan_id=huiyuan_id,
+ qitashuoming=f'加正式会员 {huiyuan_id},增加{days}天(无支付单、无分红)',
+ )
+ return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
+
+ elif caozuo == 'jian_huiyuan':
+ if '001gg' not in permissions:
+ return Response({'code': 403, 'msg': '无权限给打手减会员'})
+ if not huiyuan_id:
+ return Response({'code': 400, 'msg': '缺少会员ID'})
+ try:
+ record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
+ except Huiyuangoumai.DoesNotExist:
+ return Response({'code': 404, 'msg': '该打手未购买此会员'})
+ # 删除会员记录(或设置为过期,根据业务要求)
+ record.delete()
+ Xiugaijilu.query.create(
+ yonghuid=dashou_id,
+ xiugaiid=kefu.user.Phone,
+ leixing=2,
+ huiyuan_id=huiyuan_id,
+ qitashuoming=f'移除会员 {huiyuan_id}',
+ )
+ return Response({'code': 0, 'msg': '减会员成功'})
+
+ else:
+ return Response({'code': 400, 'msg': '未知操作类型'})
+
+ except Exception as e:
+ logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
+
+
+class KefuPunishmentListView(APIView):
+ """
+ 客服获取处罚记录列表接口
+ 请求:POST /yonghu/kefu_cfgl
+ 参数:{
+ "phone": "客服手机号",
+ "status": [0,3] 或 [1,2], # 状态数组
+ "dingdan_id": "订单ID(可选)",
+ "dashouid": "打手ID(可选)",
+ "page": 1,
+ "page_size": 20
+ }
+ 认证:JWT
+ 返回:{
+ "code": 0,
+ "data": {
+ "list": [处罚记录],
+ "total": 总数,
+ "stats": {
+ "zongshu": 总记录数,
+ "daichuli": 待处理数,
+ "yichuli": 已处理数
+ }
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ @staticmethod
+ def _parse_status_list(raw):
+ """兼容 status=[0,3]、\"0,3\"、单数字;空/all 表示不过滤。"""
+ if raw is None:
+ return None
+ if isinstance(raw, str):
+ s = raw.strip().lower()
+ if not s or s in ('all', 'null', 'none'):
+ return None
+ if s.startswith('['):
+ import json
+ try:
+ raw = json.loads(s)
+ except (TypeError, ValueError):
+ return None
+ else:
+ raw = [x.strip() for x in s.split(',') if x.strip()]
+ if isinstance(raw, (int, float)):
+ return [int(raw)]
+ if isinstance(raw, list):
+ out = []
+ for x in raw:
+ try:
+ out.append(int(x))
+ except (TypeError, ValueError):
+ continue
+ return out or None
+ return None
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ status_list = self._parse_status_list(request.data.get('status'))
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ dashouid = request.data.get('dashouid', '').strip()
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ except ValueError:
+ return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if not phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ # 1. 获取前端参数
+ username = request.data.get('phone', '').strip()
+ if not username:
+ return Response({'code': 400, 'msg': '缺少username'})
+
+ # 2. 公共权限校验
+ kefu, permissions = verify_kefu_permission(request, username)
+ if kefu is None:
+ return permissions
+
+ # 3. 检查平台订单管理权限(002ab)
+ if '005aa' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
+
+ # 3. 获取统计信息(不分页)
+ from jituan.services.club_penalty import filter_penalty_record_qs
+ from jituan.services.club_user_access import list_response_meta
+
+ base_qs = filter_penalty_record_qs(PenaltyRecord.query.all(), request)
+ stats = {
+ 'zongshu': base_qs.count(),
+ 'daichuli': base_qs.filter(ApplyStatus__in=[0, 3]).count(),
+ 'yichuli': base_qs.filter(ApplyStatus__in=[1, 2]).count(),
+ }
+
+ queryset = base_qs
+
+ if status_list and isinstance(status_list, list):
+ queryset = queryset.filter(ApplyStatus__in=status_list)
+
+ if dingdan_id:
+ queryset = queryset.filter(OrderID=dingdan_id)
+
+ if dashouid:
+ queryset = queryset.filter(PlayerID=dashouid)
+
+ # 5. 分页
+ page_obj = queryset.order_by('-CreateTime').paginate(page=page, per_page=page_size)
+ total = page_obj.total
+ records = page_obj.items
+
+ # 6. 构建返回列表
+ list_data = []
+ for item in records:
+ ct = item.CreateTime
+ ut = item.UpdateTime
+ list_data.append({
+ 'dingdan_id': item.OrderID,
+ 'dashouid': item.PlayerID,
+ 'qingqiuid': item.ApplicantID,
+ 'cfliyou': item.PenaltyReason,
+ 'sqzhuangtai': item.ApplyStatus,
+ 'jifen': item.DeductedPoints,
+ 'CreateTime': ct,
+ 'UpdateTime': ut,
+ 'create_time': ct.isoformat() if ct else '',
+ 'update_time': ut.isoformat() if ut else '',
+ })
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': list_data,
+ 'total': total,
+ 'stats': stats,
+ **list_response_meta(request),
+ }
+ })
+
+
+class KefuGetOrderDetailView(APIView):
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 获取当前用户
+ current_user = request.user
+
+ # 验证手机号
+ if getattr(current_user, 'Phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 验证用户类型
+ if current_user.UserType not in ('kefu', 'admin'):
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 客服扩展表
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 获取客服扩展表异常 user={getattr(current_user, 'yonghuid', '?')}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ # 3. 查询订单
+ try:
+ dingdan_obj = Order.query.select_related(
+ 'pingtai_kuozhan', 'shangjia_kuozhan'
+ ).get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=404)
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 订单查询异常 dingdan_id={dingdan_id}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ from jituan.services.club_user_access import forbid_if_order_out_of_scope
+ deny = forbid_if_order_out_of_scope(request, dingdan_obj)
+ if deny:
+ return deny
+
+ # 4. 构建基础数据
+ try:
+ response_data = {
+ 'dingdan_id': dingdan_obj.OrderID or '',
+ 'zhuangtai': dingdan_obj.Status or 0,
+ 'fadanpingtai': dingdan_obj.Platform or 0,
+ 'jiage': float(dingdan_obj.Amount) if dingdan_obj.Amount else 0.00,
+ 'dashou_fencheng': float(dingdan_obj.PlayerCommission) if dingdan_obj.PlayerCommission else 0.00,
+ 'jiedan_dashou_id': dingdan_obj.PlayerID or '',
+ 'dashou_liuyan': dingdan_obj.PlayerRemark or '',
+ 'zhiding_id': dingdan_obj.AssignedID or '',
+ 'shangpin_id': dingdan_obj.ProductID or '',
+ 'leixing': dingdan_obj.ProductTypeID or 0,
+ 'tupian_url': dingdan_obj.ImageURL or '',
+ 'jieshao': dingdan_obj.Description or '',
+ 'beizhu': dingdan_obj.Remark or '',
+ 'tkly': dingdan_obj.RefundReason or '',
+ 'nicheng': dingdan_obj.Nickname or '',
+ 'clkf': dingdan_obj.AssignedCS or '',
+ 'chuangjianshijian': dingdan_obj.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.CreateTime else '',
+ 'genggaishijian': dingdan_obj.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.UpdateTime else '',
+ }
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 构建基础数据异常 dingdan_id={dingdan_id}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ # 5. 扩展表数据
+ try:
+ fadan_pingtai = dingdan_obj.Platform
+ if fadan_pingtai == 1:
+ if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan:
+ pingtai = dingdan_obj.pingtai_kuozhan
+ response_data['pingtai_kuozhan'] = {
+ 'laoban_id': pingtai.BossID or '',
+ 'laoban_pingjia': pingtai.BossReview or '',
+ }
+ else:
+ response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''}
+ elif fadan_pingtai == 2:
+ if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan:
+ shangjia = dingdan_obj.shangjia_kuozhan
+ response_data['shangjia_kuozhan'] = {
+ 'shangjia_id': shangjia.MerchantID or '',
+ 'sjnicheng': shangjia.MerchantNickname or '',
+ 'shangjia_pingjia': shangjia.MerchantReview or '',
+ 'cfliyou': shangjia.PenaltyReason or '',
+ 'sqzhuangtai': shangjia.PenaltyApplyStatus if shangjia.PenaltyApplyStatus is not None else 0,
+ 'bhliyou': shangjia.RejectReason or '',
+ }
+ else:
+ response_data['shangjia_kuozhan'] = {
+ 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '',
+ 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': ''
+ }
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 扩展数据异常 dingdan_id={dingdan_id}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ # 6. 打手图片
+ try:
+ dashou_images = PlayerDeliveryImage.query.filter(OrderID=dingdan_id).values_list('ImageURL', flat=True)
+ response_data['dashou_images'] = [img for img in dashou_images if img]
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}")
+ response_data['dashou_images'] = []
+
+ # 7. 退款理由
+ try:
+ tuikuan_record = RefundRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
+ response_data['tuikuan_liyou'] = tuikuan_record.Reason if tuikuan_record and tuikuan_record.Reason else ''
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 退款理由查询异常 dingdan_id={dingdan_id}")
+ response_data['tuikuan_liyou'] = ''
+
+ # 8. 指定打手响应
+ ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}
+ hf = getattr(dingdan_obj, 'AssignedResponse', None)
+ response_data['zhiding_hf'] = hf
+ response_data['zhiding_hf_text'] = ZHIDING_HF_TEXT.get(hf, '') if hf else ''
+ response_data['zhiding_hf_sj'] = (
+ dingdan_obj.AssignedResponseAt.strftime('%Y-%m-%d %H:%M:%S')
+ if getattr(dingdan_obj, 'AssignedResponseAt', None) else ''
+ )
+ response_data['zhiding_dhj_sm'] = getattr(dingdan_obj, 'AssignedLaterNote', '') or ''
+
+ # 9. 关联罚款(打手)
+ from utils.penalty_status import PENALTY_STATUS_LABELS as PENALTY_STATUS
+ try:
+ from orders.models import Penalty
+ dashou_id = dingdan_obj.PlayerID or dingdan_obj.AssignedID or ''
+ fakuan_qs = Penalty.query.filter(RelatedOrderID=dingdan_id)
+ if dashou_id:
+ fakuan_qs = fakuan_qs.filter(PenalizedUserID=dashou_id)
+ fakuan_list = []
+ for p in fakuan_qs.order_by('-CreateTime')[:5]:
+ fakuan_list.append({
+ 'amount': str(p.FineAmount or '0.00'),
+ 'status': p.Status,
+ 'status_text': PENALTY_STATUS.get(p.Status, ''),
+ 'reason': p.Reason or '',
+ 'create_time': p.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if p.CreateTime else '',
+ })
+ response_data['fakuan_list'] = fakuan_list
+ latest = fakuan_list[0] if fakuan_list else None
+ response_data['fakuan_info'] = latest
+ response_data['is_fadaned'] = Penalty.query.filter(
+ RelatedOrderID=dingdan_id, ApplicantID=getattr(current_user, 'UserUID', '')
+ ).exists()
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 罚款查询异常 dingdan_id={dingdan_id}")
+ response_data['fakuan_list'] = []
+ response_data['fakuan_info'] = None
+ response_data['is_fadaned'] = False
+
+ return Response({'code': 0, 'data': response_data})
+
+
+class KefuPunishmentDetailView(APIView):
+ """
+ 客服获取处罚详情接口
+ 请求:POST /yonghu/kefu_cf_detail
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证:JWT
+ 返回:code=0 + 处罚记录详情(含图片)
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ logger.warning(f"用户类型非客服: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 查询处罚记录(按创建时间倒序取最新一条)
+ try:
+ record = PenaltyRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
+ if not record:
+ return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"查询处罚记录失败: {e}")
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 查询商家证据图片
+ zhengju_images = []
+ shensu_images = []
+ try:
+ # 商家证据图片:上传者为请求人 (ApplicantID)
+ zhengju_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.ApplicantID)
+ zhengju_images = [item.ImageURL for item in zhengju_qs if item.ImageURL]
+
+ # 打手申诉图片:上传者为打手 (PlayerID)
+ shensu_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.PlayerID)
+ shensu_images = [item.ImageURL for item in shensu_qs if item.ImageURL]
+ except Exception as e:
+ logger.error(f"查询图片失败: {e}")
+
+ # 构建返回数据
+ data = {
+ 'dingdan_id': record.OrderID,
+ 'dashouid': record.PlayerID,
+ 'qingqiuid': record.ApplicantID,
+ 'chuliid': record.ProcessorID,
+ 'cfliyou': record.PenaltyReason,
+ 'sqzhuangtai': record.ApplyStatus,
+ 'bhliyou': record.RejectReason,
+ 'jifen': record.DeductedPoints,
+ 'ssliyou': record.AppealReason or '',
+ 'zhengju_tupian': zhengju_images,
+ 'shensu_tupian': shensu_images,
+ 'CreateTime': record.CreateTime,
+ 'UpdateTime': record.UpdateTime,
+ }
+
+ return Response({'code': 0, 'data': data})
+
+
+class KefuPunishmentActionView(APIView):
+ """
+ 客服同意/驳回处罚接口
+ 请求:POST /yonghu/kefu_cf_action
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "action": 1(同意) / 2(驳回),
+ "reject_reason": "驳回理由(驳回时必填)"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ action = request.data.get('action')
+ reject_reason = request.data.get('reject_reason', '').strip()
+
+ if not phone or not dingdan_id or action not in [1, 2]:
+ return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if action == 2 and not reject_reason:
+ return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 客服身份验证(完整验证,此处略)
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ if current_user.UserType not in ('kefu', 'admin'):
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 查询待处理的处罚记录(状态0或3)
+ try:
+ record = PenaltyRecord.query.filter(
+ OrderID=dingdan_id,
+ ApplyStatus__in=[0, 3]
+ ).order_by('-CreateTime').first()
+ if not record:
+ exists = PenaltyRecord.query.filter(OrderID=dingdan_id).exists()
+ if exists:
+ return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST)
+ else:
+ return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"查询处罚记录失败: {e}")
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ with transaction.atomic():
+ # 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回
+ duplicate_records = PenaltyRecord.query.filter(
+ OrderID=dingdan_id,
+ ApplyStatus__in=[0, 3]
+ ).exclude(id=record.id)
+ for dup in duplicate_records:
+ dup.ApplyStatus = 2
+ dup.ProcessorID = phone
+ dup.RejectReason = "系统自动驳回:存在重复处罚记录"
+ dup.save()
+ logger.info(f"自动驳回重复处罚记录 ID={dup.id}")
+
+ if action == 1: # 同意
+ # 可选验证打手存在
+ if record.PlayerID:
+ try:
+ dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
+ except UserDashou.DoesNotExist:
+ return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST)
+
+ record.ApplyStatus = 1
+ record.ProcessorID = phone
+ record.save()
+
+ # 更新商家订单扩展表
+ try:
+ dingdan = Order.query.get(OrderID=dingdan_id)
+ if hasattr(dingdan, 'shangjia_kuozhan'):
+ dingdan.shangjia_kuozhan.PenaltyApplyStatus = 1
+ dingdan.shangjia_kuozhan.save()
+ except Exception as e:
+ logger.warning(f"更新商家扩展表失败: {e}")
+
+ elif action == 2: # 驳回
+ # 返还积分
+ if record.DeductedPoints > 0 and record.PlayerID:
+ try:
+ dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
+ if dashou.jifen <= 1000: # 安全限制
+ dashou.jifen += record.DeductedPoints
+ dashou.save()
+ logger.info(f"驳回处罚,打手{record.PlayerID}返还积分{record.DeductedPoints}")
+ else:
+ logger.warning(f"打手积分异常({dashou.jifen}),不再返还")
+ except UserDashou.DoesNotExist:
+ logger.warning(f"打手{record.PlayerID}不存在,无法返还积分")
+
+ record.ApplyStatus = 2
+ record.ProcessorID = phone
+ record.RejectReason = reject_reason
+ record.save()
+
+ try:
+ dingdan = Order.query.get(OrderID=dingdan_id)
+ if hasattr(dingdan, 'shangjia_kuozhan'):
+ dingdan.shangjia_kuozhan.PenaltyApplyStatus = 2
+ dingdan.shangjia_kuozhan.RejectReason = reject_reason
+ dingdan.shangjia_kuozhan.save()
+ except Exception as e:
+ logger.warning(f"更新商家扩展表失败: {e}")
+
+ return Response({'code': 0, 'msg': '处理成功'})
+
+
+class KefuForceCompleteView(APIView):
+ """
+ 客服强制结单接口(仅本地订单)
+ 请求:POST /yonghu/kefuqiangzhijiedan
+ 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"}
+ 认证:JWT,仅限状态正常的客服
+ 逻辑:
+ 1. 验证客服身份及状态
+ 2. 查询订单并加锁(事务内)
+ 3. 校验订单状态是否为8(结算中)
+ 4. 校验订单必须有接单打手且打手分成金额有效
+ 5. 给打手增加余额、成交统计、今日/本月收益
+ 6. 如果是商家发单,更新商家成交订单数量
+ 7. 更新订单状态为3(已完成)
+ 8. 更新客服处理统计
+ 9. 返回成功
+ """
+ permission_classes = [permissions.IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '参数缺失'})
+
+ # 1. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'})
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ return Response({'code': 401, 'msg': '认证失败'})
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'})
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '客服状态异常'})
+
+ # 2. 订单处理(事务内)
+ try:
+ with transaction.atomic():
+ order = Order.objects.select_for_update().get(OrderID=dingdan_id)
+
+ # 3. 校验订单状态
+ if order.Status != 8:
+ return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})
+
+ # 4. 校验必要字段
+ dashou_id = order.PlayerID
+ dashou_fencheng = order.PlayerCommission
+ if not dashou_id:
+ return Response({'code': 400, 'msg': '订单无接单打手'})
+ if not dashou_fencheng or dashou_fencheng <= 0:
+ return Response({'code': 400, 'msg': '打手分成金额无效'})
+
+ # 5. 给打手结算
+ try:
+ dashou_user = User.query.get(UserUID=dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ # 原子更新(避免并发重复加)
+ dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
+ dashou_profile.yue = F('yue') + dashou_fencheng
+ dashou_profile.zonge = F('zonge') + dashou_fencheng
+ dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng
+ dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng
+ dashou_profile.save(update_fields=[
+ 'chengjiaozongliang', 'yue', 'zonge',
+ 'jinrishouyi', 'jinyueshouyi'
+ ])
+ except (User.DoesNotExist, AttributeError):
+ return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'})
+
+ # 6. 如果是商家发单,更新商家成交订单数量
+ if order.Platform == 2:
+ try:
+ # 通过 MerchantOrderExt 获取商家ID
+ shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan'
+ if shangjia_ext and shangjia_ext.shangjia_id:
+ shangjia_user = User.query.get(UserUID=shangjia_ext.shangjia_id)
+ shangjia_profile = shangjia_user.ShopProfile
+ shangjia_profile.chengjiao = F('chengjiao') + 1
+ shangjia_profile.save(update_fields=['chengjiao'])
+
+ # (行为驱动,更安全)
+ update_shangjia_daily(
+ yonghuid=shangjia_ext.shangjia_id,
+ amount=Decimal(str(order.Amount)),
+ action=2, # 2 = 结算
+ order_id=dingdan_id,
+ )
+
+ except ObjectDoesNotExist:
+ # 没有商家扩展记录则忽略
+ pass
+
+ # 7. 更新订单状态
+ order.Status = 3
+ order.AssignedCS = phone
+ order.save(update_fields=['Status', 'AssignedCS'])
+
+
+ if order.Platform == 1:
+
+ # ========== 新增:商品和店铺每日统计 ==========
+ 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, # 2 = 结单
+ dingdan_jiage=order.Amount,
+ pingtai_shouyi=pingtai_shouyi,
+ dianpu_zongshouyi=dianpu_shouyi
+ )
+ logger.info(f"商品统计更新完成,商品ID: {order.ProductID}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ dianpu_id = None
+ if hasattr(order, 'pingtai_kuozhan'):
+ dianpu_id = order.pingtai_kuozhan.ShopID
+ if dianpu_id:
+ dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
+ update_dianpu_daily_stat(
+ dianpu_id=dianpu_id,
+ caozuo_leixing=2, # 2 = 结单
+ 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())
+ # 统计失败不影响主流程
+ # =============================================
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=dashou_fencheng,
+ action=2 # 2 表示成交(结算)
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+ # 商家订单管事分红
+ if order.Platform == 2:
+ settle_shangjia_order_guanshi_fenhong(order, dashou_id)
+
+ # 8. 更新客服统计
+ kefu.jinrichuli = F('jinrichuli') + 1
+ kefu.jinyuechuli = F('jinyuechuli') + 1
+ kefu.zongchuli = F('zongchuli') + 1
+ kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli'])
+
+ return Response({'code': 0, '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': '服务器内部错误'})
+
+
+kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list')
+if not kefu_txsh_list_logger.handlers:
+ _console_handler = logging.StreamHandler(sys.stderr)
+ _console_handler.setFormatter(logging.Formatter(
+ '[%(asctime)s] %(levelname)s %(name)s: %(message)s'
+ ))
+ kefu_txsh_list_logger.addHandler(_console_handler)
+ kefu_txsh_list_logger.setLevel(logging.DEBUG)
+ kefu_txsh_list_logger.propagate = False
+
+_TIXIAN_LIST_BASE_FIELDS = (
+ 'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing',
+ 'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi',
+ 'shenheid', 'bhliyou', 'CreateTime', 'UpdateTime',
+)
+_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao')
+
+
+class KefuWithdrawListView(APIView):
+ """
+ 客服获取提现审核列表接口
+ 请求:POST /yonghu/kefu_txsh_list
+ 参数:{
+ "phone": "客服手机号",
+ "page": 1,
+ "page_size": 20,
+ "status": 1(待审核)/2(已处理),
+ "type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选
+ "payment": 1(微信)/2(支付宝) 可选,
+ "search_uid": "用户ID搜索" 可选,
+ "date": "YYYY-MM-DD" 可选,
+ "min_amount": 0, # 最小提现金额(元)可选
+ "max_amount": 999999, # 最大提现金额(元)可选
+ "dakuan_mode": 1 # 打款方式:1手动打款 2自动打款,可选
+ }
+ 认证:JWT
+ 返回:{
+ "code": 0,
+ "data": {
+ "list": [提现记录],
+ "total": 总数,
+ "stats": {
+ "total": 总记录数,
+ "awaiting": 待审核数,
+ "processed": 已处理数,
+ "total_amount": "总金额(元)"
+ }
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def _safe_request_params(self, request):
+ try:
+ return dict(request.data)
+ except Exception:
+ return {'raw': str(request.data)}
+
+ def _shenhe_jilu_field_available(self):
+ """探测 shenhe_jilu_id 字段是否已在数据库中就绪"""
+ try:
+ Tixianjilu.query.values_list('shenhe_jilu_id', flat=True).first()
+ return True
+ except DatabaseError as e:
+ kefu_txsh_list_logger.error(
+ 'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e,
+ exc_info=True,
+ )
+ return False
+
+ def _list_queryset(self, q, shenhe_field_ok, request):
+ """字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500"""
+ fields = _TIXIAN_LIST_BASE_FIELDS + (
+ _TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else ()
+ )
+ from jituan.services.tixian_club import filter_tixianjilu_by_request
+ qs = Tixianjilu.query.filter(q).only(*fields)
+ return filter_tixianjilu_by_request(qs, request)
+
+ def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok):
+ """无关联审核或审核表未标记自动 → 默认手动(1)"""
+ if not shenhe_field_ok:
+ return 1
+ sid = getattr(item, 'shenhe_jilu_id', None)
+ if not sid:
+ return 1
+ if audit_mode_map.get(sid) == 2:
+ return 2
+ return 1
+
+ def post(self, request):
+ from jituan.services.group_finance_gate import require_withdraw_audit_access
+
+ dakuan_mode_filter = request.data.get('dakuan_mode')
+ req_params = self._safe_request_params(request)
+ kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params)
+
+ try:
+ try:
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('page_size', 20))
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if page < 1:
+ page = 1
+ if page_size < 1 or page_size > 100:
+ page_size = min(max(page_size, 1), 100)
+
+ status_filter = request.data.get('status')
+ type_filter = request.data.get('type')
+ payment_filter = request.data.get('payment')
+ search_uid = (request.data.get('search_uid') or '').strip()
+ date_filter = (request.data.get('date') or '').strip()
+ min_amount = request.data.get('min_amount')
+ max_amount = request.data.get('max_amount')
+
+ phone = (request.data.get('phone') or '').strip()
+ if not phone:
+ return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ kefu, err_resp = require_withdraw_audit_access(request, phone)
+ if err_resp:
+ return err_resp
+
+ q = Q()
+
+ try:
+ status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None
+ except (TypeError, ValueError):
+ status_val = None
+
+ try:
+ dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None
+ except (TypeError, ValueError):
+ dakuan_mode_val = None
+
+ # 未传打款方式参数 → 默认手动打款(1)
+ effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1
+
+ if status_val == 1:
+ q &= Q(zhuangtai=1)
+ elif status_val == 2:
+ if effective_dakuan == 2:
+ q &= Q(zhuangtai__in=[2, 3, 4, 5, 6])
+ else:
+ q &= Q(zhuangtai__in=[2, 3])
+
+ if type_filter is not None and str(type_filter).strip() != '':
+ try:
+ type_val = int(type_filter)
+ if type_val in [1, 2, 3, 4, 5, 6]:
+ q &= Q(leixing=type_val)
+ except (TypeError, ValueError):
+ pass
+
+ if payment_filter is not None and str(payment_filter).strip() != '':
+ try:
+ payment_val = int(payment_filter)
+ if payment_val in [1, 2]:
+ q &= Q(fangshi=payment_val)
+ except (TypeError, ValueError):
+ pass
+
+ if search_uid:
+ q &= Q(yonghuid=search_uid)
+
+ if date_filter:
+ q &= Q(UpdateTime__date=date_filter)
+
+ if min_amount is not None and str(min_amount).strip() != '':
+ try:
+ q &= Q(jine__gte=float(min_amount))
+ except (TypeError, ValueError):
+ pass
+ if max_amount is not None and str(max_amount).strip() != '':
+ try:
+ q &= Q(jine__lte=float(max_amount))
+ except (TypeError, ValueError):
+ pass
+
+ shenhe_field_ok = self._shenhe_jilu_field_available()
+ if shenhe_field_ok:
+ if effective_dakuan == 2:
+ q &= Q(shenhe_jilu_id__isnull=False)
+ else:
+ q &= Q(shenhe_jilu_id__isnull=True)
+ elif effective_dakuan == 2:
+ kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表')
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': [],
+ 'total': 0,
+ 'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'},
+ },
+ })
+
+ processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3]
+
+ base_qs = self._list_queryset(q, shenhe_field_ok, request)
+ total_count = base_qs.count()
+ awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok, request).count()
+ processed_count = self._list_queryset(
+ q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok, request,
+ ).count()
+ agg = base_qs.aggregate(total=Sum('jine'))
+ total_amount = agg.get('total') or 0
+
+ stats = {
+ 'total': total_count,
+ 'awaiting': awaiting_count,
+ 'processed': processed_count,
+ 'total_amount': str(round(float(total_amount), 2)),
+ }
+
+ queryset = base_qs.order_by('CreateTime')
+ total = queryset.count()
+ records = list(queryset[(page - 1) * page_size: page * page_size])
+
+ audit_ids = [
+ getattr(item, 'shenhe_jilu_id', None)
+ for item in records if getattr(item, 'shenhe_jilu_id', None)
+ ]
+ audit_meta_map = {}
+ if audit_ids:
+ try:
+ audit_meta_map = load_audit_meta_map(audit_ids)
+ except Exception as e:
+ kefu_txsh_list_logger.error(
+ 'kefu_txsh_list: 查询审核表信息失败: %s', e,
+ exc_info=True,
+ )
+
+ audit_mode_map = {
+ aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items()
+ }
+ list_data = []
+ for item in records:
+ shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None
+ shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else ''
+ if shenhe_jilu_id and not shenhe_danhao:
+ shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '')
+ list_data.append({
+ 'id': item.id,
+ 'shenhe_jilu_id': shenhe_jilu_id,
+ 'shenhe_danhao': shenhe_danhao,
+ 'yonghuid': item.yonghuid,
+ 'avatar': item.avatar or '',
+ 'phone': item.phone or '',
+ 'nicheng': item.nicheng or '',
+ 'leixing': item.leixing,
+ 'zhifu': item.zhifu or '',
+ 'skzhanghao': item.skzhanghao or '',
+ 'jine': str(item.jine) if item.jine is not None else '0.00',
+ 'zhuangtai': item.zhuangtai,
+ 'fangshi': item.fangshi,
+ 'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok),
+ 'shenheid': item.shenheid or '',
+ 'bhliyou': item.bhliyou or '',
+ 'CreateTime': item.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if item.CreateTime else '',
+ 'UpdateTime': item.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if item.UpdateTime else '',
+ })
+
+ from jituan.services.club_user_access import list_response_meta
+
+ return Response({
+ 'code': 0,
+ 'data': {
+ 'list': list_data,
+ 'total': total,
+ 'stats': stats,
+ **list_response_meta(request),
+ },
+ })
+
+ except DatabaseError as e:
+ kefu_txsh_list_logger.error(
+ 'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s',
+ dakuan_mode_filter, req_params, e,
+ exc_info=True,
+ )
+ return Response({
+ 'code': 500,
+ 'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu',
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ except Exception as e:
+ kefu_txsh_list_logger.error(
+ 'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s',
+ dakuan_mode_filter, req_params, e,
+ exc_info=True,
+ )
+ return Response({
+ 'code': 500,
+ 'msg': '获取提现列表失败',
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class KefuWithdrawDetailView(APIView):
+ """
+ 客服获取提现详情接口
+ 请求:POST /yonghu/kefu_txsh_detail
+ 参数:{
+ "phone": "客服手机号",
+ "tixian_id": "提现记录ID"
+ }
+ 认证:JWT
+ 返回:code=0 + 提现记录详情 + 用户扩展信息
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ phone = request.data.get('phone', '').strip()
+ tixian_id = request.data.get('tixian_id')
+
+ if not phone or not tixian_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ logger.warning(f"用户类型非客服: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ from jituan.services.group_finance_gate import require_withdraw_audit_access
+ _, err_resp = require_withdraw_audit_access(request, phone)
+ if err_resp:
+ return err_resp
+
+ # 查询提现记录
+ try:
+ record = Tixianjilu.query.get(id=tixian_id)
+ except Tixianjilu.DoesNotExist:
+ return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
+ deny = forbid_if_tixian_out_of_scope(request, record)
+ if deny:
+ return deny
+
+ # 查询用户主表
+ try:
+ user = User.query.get(UserUID=record.yonghuid)
+ except User.DoesNotExist:
+ user = None
+
+ # 构建基础返回数据(字段必须与前端一致)
+ data = {
+ 'id': record.id,
+ 'yonghuid': record.yonghuid,
+ 'avatar': record.avatar or '',
+ 'phone': record.phone or '',
+ 'nicheng': record.nicheng or '',
+ 'leixing': record.leixing, # 1打手 2管事
+ 'zhifu': record.zhifu or '',
+ 'skzhanghao': record.skzhanghao or '',
+ 'jine': str(record.jine) if record.jine else '0.00',
+ 'zhuangtai': record.zhuangtai,
+ 'fangshi': record.fangshi,
+ 'shenheid': record.shenheid or '',
+ 'bhliyou': record.bhliyou or '',
+ 'CreateTime': record.CreateTime,
+ 'UpdateTime': record.UpdateTime,
+ }
+
+ # 根据提现类型添加扩展信息
+ if user:
+ if record.leixing == 1: # 打手
+ try:
+ dashou = UserDashou.query.get(user=user)
+ # 计算成交率和退款率
+ total = dashou.jiedanzongliang or 0
+ completed = dashou.chengjiaozongliang or 0
+ refund = dashou.tuikuanliang or 0
+ completion_rate = round((completed / total * 100) if total > 0 else 0)
+ refund_rate = round((refund / completed * 100) if completed > 0 else 0)
+
+ data['dashouInfo'] = {
+ 'zhuangtai': dashou.zhuangtai, # 工作状态
+ 'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态
+ 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态
+ 'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额
+ 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额
+ 'jifen': dashou.jifen, # 积分
+ 'jiedanzongliang': dashou.jiedanzongliang, # 接单总量
+ 'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量
+ 'tuikuanliang': dashou.tuikuanliang, # 退款总量
+ 'completionRate': completion_rate,
+ 'refundRate': refund_rate,
+ }
+ except ObjectDoesNotExist:
+ data['dashouInfo'] = None
+ elif record.leixing == 2: # 管事
+ try:
+ guanshi = UserGuanshi.query.get(user=user)
+ data['guanshiInfo'] = {
+ 'zhuangtai': guanshi.zhuangtai, # 账号状态
+ 'yue': float(guanshi.yue) if guanshi.yue else 0.00,
+ 'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数
+ 'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值
+ 'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值
+ 'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00,
+ }
+ except ObjectDoesNotExist:
+ data['guanshiInfo'] = None
+
+ return Response({'code': 0, 'data': data})
+
+
+class KefuWithdrawActionView(APIView):
+ """
+ 客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款
+ POST /yonghu/kefu_txsh_action
+
+ 单条:{ phone, action, tixian_id, yonghuid, leixing, reason? }
+ 批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? }
+
+ 自动打款(dakuan_mode=2):同意→6待收款;拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计
+ 手动打款(dakuan_mode=1):原逻辑不变
+ 状态要求:Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1(审核中),其他状态一律拒绝
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def _verify_kefu(self, request, phone):
+ if not phone:
+ return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ if current_user.UserType not in ('kefu', 'admin'):
+ return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ # 管理员跳过客服扩展表检查
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ if kefu.zhuangtai != 1:
+ return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+ return current_user, None
+
+ def _parse_items(self, request):
+ """解析单条或 batch_list,返回 [{tixian_id, yonghuid, leixing}]"""
+ batch_list = request.data.get('batch_list')
+ if batch_list is not None:
+ if not isinstance(batch_list, list) or len(batch_list) == 0:
+ return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+ items = []
+ for idx, row in enumerate(batch_list):
+ if not isinstance(row, dict):
+ return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST)
+ tid = row.get('tixian_id')
+ yid = (row.get('yonghuid') or '').strip()
+ lx = row.get('leixing')
+ if not tid or not yid or lx is None:
+ return None, Response(
+ {'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+ try:
+ items.append({
+ 'tixian_id': int(tid),
+ 'yonghuid': yid,
+ 'leixing': int(lx),
+ })
+ except (TypeError, ValueError):
+ return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
+ return items, None
+
+ tixian_id = request.data.get('tixian_id')
+ if not tixian_id:
+ return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+ yonghuid = (request.data.get('yonghuid') or '').strip()
+ leixing = request.data.get('leixing')
+ try:
+ item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None}
+ except (TypeError, ValueError):
+ return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
+ return [item], None
+
+ def _process_auto_item(self, request, item, action, reason, kefu_user):
+ """自动打款单条处理(须在 transaction.atomic 内调用)"""
+ tixian_id = item['tixian_id']
+ req_yonghuid = item['yonghuid']
+ req_leixing = item['leixing']
+
+ if req_leixing not in [1, 2, 3, 4, 5, 6]:
+ raise ValueError(f'提现记录{tixian_id}:提现类型无效')
+
+ try:
+ tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
+ except Tixianjilu.DoesNotExist:
+ raise ValueError(f'提现记录{tixian_id}不存在')
+
+ from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
+ deny = forbid_if_tixian_out_of_scope(request, tixian)
+ if deny:
+ raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
+
+ if tixian.yonghuid != req_yonghuid:
+ raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
+ if tixian.leixing != req_leixing:
+ raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
+ if tixian.zhuangtai != 1:
+ raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
+ if not tixian.shenhe_jilu_id:
+ raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录')
+
+ try:
+ audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id)
+ except TixianShenheJilu.DoesNotExist:
+ raise ValueError(f'提现记录{tixian_id}:审核记录不存在')
+
+ if audit.yonghuid != req_yonghuid:
+ raise ValueError(f'提现记录{tixian_id}:审核记录用户ID不匹配')
+ if audit.leixing != req_leixing:
+ raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配')
+ if audit.zhuangtai != 1:
+ raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态')
+
+ try:
+ user = User.objects.select_for_update().get(UserUID=req_yonghuid)
+ except User.DoesNotExist:
+ raise ValueError(f'用户{req_yonghuid}不存在')
+
+ if action == 1:
+ limit_ok, limit_msg = check_shijidaozhang_within_limit(audit.shijidaozhang)
+ if not limit_ok:
+ sync_audit_and_jilu_status(
+ audit, 5,
+ bhliyou=limit_msg,
+ bo_hui_ren_id=kefu_user.UserUID,
+ )
+ refund_balance(user, audit.leixing, audit.shijidaozhang)
+ tixian.shenheid = kefu_user.UserUID
+ tixian.bhliyou = limit_msg
+ tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
+ raise ValueError(f'提现记录{tixian_id}:{limit_msg},已自动驳回并退款')
+ sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.UserUID)
+ tixian.shenheid = kefu_user.UserUID
+ tixian.save(update_fields=['shenheid', 'UpdateTime'])
+ else:
+ sync_audit_and_jilu_status(
+ audit, 5,
+ bhliyou=reason,
+ bo_hui_ren_id=kefu_user.UserUID,
+ )
+ # 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退
+ refund_balance(user, audit.leixing, audit.shijidaozhang)
+ tixian.shenheid = kefu_user.UserUID
+ tixian.bhliyou = reason
+ tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
+
+ def _process_manual_item(self, request, item, action, reason, phone):
+ """手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)"""
+ tixian_id = item['tixian_id']
+ try:
+ tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
+ except Tixianjilu.DoesNotExist:
+ raise ValueError(f'提现记录{tixian_id}不存在')
+
+ from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
+ deny = forbid_if_tixian_out_of_scope(request, tixian)
+ if deny:
+ raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
+
+ req_yonghuid = item.get('yonghuid')
+ req_leixing = item.get('leixing')
+ if req_yonghuid and tixian.yonghuid != req_yonghuid:
+ raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
+ if req_leixing is not None and tixian.leixing != req_leixing:
+ raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
+ if tixian.zhuangtai != 1:
+ raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
+
+ if getattr(tixian, 'shenhe_jilu_id', None):
+ raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程')
+
+ try:
+ user = User.query.get(UserUID=tixian.yonghuid)
+ except User.DoesNotExist:
+ raise ValueError(f'用户{tixian.yonghuid}不存在')
+
+ from jituan.services.club_user import get_user_club_id
+
+ if action == 1:
+ tixian.zhuangtai = 2
+ try:
+ from jituan.services.szjilu_accounting import apply_szjilu_expense
+ update_daily_payout(tixian.jine, get_user_club_id(user))
+ apply_szjilu_expense(tixian.jine, get_user_club_id(user))
+ except Exception as e:
+ logger.error(f'更新收支记录失败: {e}')
+ else:
+ try:
+ refund_balance(user, tixian.leixing, tixian.jine)
+ except ValueError as e:
+ raise ValueError(str(e))
+ tixian.zhuangtai = 3
+ tixian.bhliyou = reason
+
+ tixian.shenheid = phone
+ tixian.UpdateTime = timezone.now()
+ tixian.save()
+
+ def post(self, request):
+ logger.info(f'接收到提现处理请求,数据: {request.data}')
+
+ phone = request.data.get('phone', '').strip()
+ action_raw = request.data.get('action')
+ reason = request.data.get('reason', '').strip()
+
+ try:
+ action = int(action_raw)
+ except (TypeError, ValueError):
+ return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
+ if action not in [1, 2]:
+ return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST)
+ if action == 2 and not reason:
+ return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ kefu_user, err_resp = self._verify_kefu(request, phone)
+ if err_resp:
+ return err_resp
+
+ from jituan.services.group_finance_gate import require_withdraw_audit_access
+ _, audit_err = require_withdraw_audit_access(request, phone)
+ if audit_err:
+ return audit_err
+
+ items, err_resp = self._parse_items(request)
+ if err_resp:
+ return err_resp
+
+ is_batch = isinstance(request.data.get('batch_list'), list) and len(request.data.get('batch_list')) > 0
+
+ try:
+ with transaction.atomic():
+ for item in items:
+ tixian_id = item['tixian_id']
+ try:
+ tixian_probe = Tixianjilu.objects.select_for_update().get(id=tixian_id)
+ except Tixianjilu.DoesNotExist:
+ raise ValueError(f'提现记录{tixian_id}不存在')
+
+ from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
+ deny = forbid_if_tixian_out_of_scope(request, tixian_probe)
+ if deny:
+ raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
+
+ # 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段
+ is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
+
+ if is_auto:
+ self._process_auto_item(request, item, action, reason, kefu_user)
+ else:
+ if is_batch:
+ raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
+ self._process_manual_item(request, item, action, reason, phone)
+
+ except ValueError as e:
+ return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ logger.error(f'处理提现事务失败: {e}', exc_info=True)
+ return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功'
+ return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}})
+
+
+class KefuPunishView(APIView):
+ """
+ 客服处罚打手接口
+ 请求:POST /yonghu/kefuchufa
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "reason": "处罚原因(可选)"
+ }
+ 认证:JWT
+ 返回:code=0 成功
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ # 1. 获取参数
+ phone = request.data.get('phone', '').strip()
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ reason = request.data.get('reason', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ logger.warning(f"手机号不匹配: {phone}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ logger.warning(f"用户类型非客服: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ logger.warning(f"客服账号已禁用: {current_user.UserUID}")
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单
+ try:
+ order = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 获取打手ID
+ dashou_id = order.PlayerID
+ if not dashou_id:
+ return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 查询打手
+ try:
+ dashou_user = User.query.get(UserUID=dashou_id)
+ dashou = dashou_user.DashouProfile
+ except User.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except ObjectDoesNotExist:
+ return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 6. 使用事务
+ from jituan.services.club_penalty import resolve_penalty_record_club_id
+ with transaction.atomic():
+ # 创建处罚记录(积分扣除5分)
+ jifen = 5
+ chufa = PenaltyRecord.query.create(
+ PlayerID=dashou_id,
+ ApplicantID=phone, # 客服手机号作为请求人
+ PenaltyReason=reason or '',
+ ApplyStatus=0, # 待处理
+ DeductedPoints=jifen,
+ OrderID=dingdan_id,
+ ClubID=resolve_penalty_record_club_id(
+ order_id=dingdan_id,
+ player_id=dashou_id,
+ request=request,
+ user=request.user,
+ ),
+ )
+
+ # 扣除打手积分
+ if dashou.jifen >= jifen:
+ dashou.jifen -= jifen
+ else:
+ dashou.jifen = 0
+ dashou.save()
+
+ # 可选:更新订单的 clkf 字段记录处理客服
+ order.AssignedCS = phone
+ order.save()
+
+ return Response({'code': 0, 'msg': '处罚成功'})
diff --git a/users/views/pay.py b/users/views/pay.py
new file mode 100644
index 0000000..9e836c1
--- /dev/null
+++ b/users/views/pay.py
@@ -0,0 +1,1073 @@
+"""users.views.pay - 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 FaKuanPayView(APIView):
+ """
+ 罚款微信支付下单接口(完全照抄 YajinGoumai)
+ 路径:POST /yonghu/fkjn
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ fadan_id = request.data.get('fadan_id')
+ if not fadan_id:
+ return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ fadan = Penalty.query.get(id=fadan_id, PenalizedUserID=request.user.UserUID)
+ except Penalty.DoesNotExist:
+ return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND)
+
+ if fadan.Status not in [1, 3]:
+ return Response({'code': 400, 'message': '罚单当前状态不可支付'}, status=status.HTTP_400_BAD_REQUEST)
+
+ jine = float(fadan.FineAmount)
+
+ timestamp_ms = int(time.time() * 1000)
+ timestamp_ns = int(time.time_ns() // 1000) % 1000000
+ random_num = random.randint(0, 99)
+ dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
+
+ from jituan.services.club_penalty import penalty_cz_club_id
+ from jituan.services.club_user import get_payment_openid
+ from products.czjilu_types import CZJILU_LEIXING_FAKUAN, penalty_cz_shuoming
+
+ pay_openid, _ = get_payment_openid(request)
+ if not pay_openid:
+ return Response({
+ 'code': 400,
+ 'message': '用户openid不存在,无法发起微信支付',
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ chongzhi_jilu = Czjilu.query.create(
+ dingdan_id=dingdanid,
+ zhuangtai=9,
+ yonghuid=request.user.UserUID,
+ jine=jine,
+ leixing=CZJILU_LEIXING_FAKUAN,
+ shuoming=penalty_cz_shuoming(fadan.id),
+ club_id=penalty_cz_club_id(fadan),
+ )
+
+ try:
+ pay_params = self.generate_wechat_pay_params(
+ dingdanid=dingdanid,
+ jine=jine,
+ openid=pay_openid,
+ pay_type='fakuan'
+ )
+ 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)
+
+ return Response({
+ 'code': 200,
+ 'message': '支付参数生成成功',
+ 'payParams': pay_params,
+ '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):
+ 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 == 'fakuan':
+ PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION
+ NOTIFY_URL = settings.FAKUAN_PAY_NOTIFY_URL # 回调地址在这里
+ else:
+ raise Exception('未知的支付类型')
+
+ nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+ total_fee = yuan_to_fen(jine)
+ trade_type = 'JSAPI'
+ user_ip = self.get_client_ip()
+
+ # 第一次签名
+ 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
+ }
+
+ 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}'
+ sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
+
+ # 构建 XML,这里必须包含 notify_url
+ xml_data = f"""
+{APPID}
+{PAY_DESCRIPTION}
+{MCHID}
+{nonce_str}
+{NOTIFY_URL}
+{openid}
+{dingdanid}
+{user_ip}
+{total_fee}
+{trade_type}
+{sign}
+"""
+
+ response = requests.post(
+ 'https://api.mch.weixin.qq.com/pay/unifiedorder',
+ data=xml_data.encode('utf-8'),
+ headers={'Content-Type': 'application/xml'},
+ timeout=10
+ )
+
+ logger.info(f"罚款微信支付响应: {response.text}")
+
+ root = ET.fromstring(response.content)
+
+ return_code_node = root.find('return_code')
+ if return_code_node is None or return_code_node.text is None:
+ logger.error(f"微信返回缺少return_code,原始: {response.text}")
+ raise Exception('微信支付接口异常,缺少return_code')
+ return_code = return_code_node.text
+
+ result_code_node = root.find('result_code')
+ result_code = result_code_node.text if result_code_node is not None else None
+
+ if return_code == 'SUCCESS' and result_code == 'SUCCESS':
+ prepay_id_node = root.find('prepay_id')
+ if prepay_id_node is None or prepay_id_node.text is None:
+ logger.error(f"统一下单成功但无prepay_id,原始: {response.text}")
+ raise Exception('微信支付接口异常,缺少prepay_id')
+ prepay_id = prepay_id_node.text
+ 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'
+
+ pay_sign_data = {
+ 'appId': APPID,
+ '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,
+ 'timeStamp': timestamp,
+ 'nonceStr': nonce_str,
+ 'package': package,
+ 'signType': sign_type,
+ 'paySign': pay_sign
+ }
+ 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 result_code:
+ error_details += f", result_code: {result_code}"
+ if return_msg is not None and return_msg.text:
+ error_details += f", return_msg: {return_msg.text}"
+ if err_code is not None and err_code.text:
+ error_details += f", err_code: {err_code.text}"
+ if err_code_des is not None and err_code_des.text:
+ error_details += f", err_code_des: {err_code_des.text}"
+ raise Exception(f"微信支付下单失败: {error_details}")
+
+ def get_client_ip(self):
+ 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 FaKuanHuitiaoView(View):
+
+ """
+ 罚款支付回调接口(严格借鉴 YajinHuitiao 接口,参数一个不少)
+ 路径:POST /yonghu/fk_huitiao
+ """
+ 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', '订单不存在')
+
+ from products.czjilu_types import is_penalty_czjilu
+ if not is_penalty_czjilu(order):
+ logger.error(f"非罚款支付订单误入罚款回调: {out_trade_no}, leixing={order.leixing}")
+ 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', '金额不一致')
+
+ # 7. 更新订单状态为已支付(3)
+ order.zhuangtai = 3
+ order.save(update_fields=['zhuangtai'])
+ logger.info(f"订单状态更新成功: {order.dingdan_id} -> 3")
+
+ # 8. 更新罚单状态为已缴纳(2)
+ try:
+ fadan = None
+ m = re.search(r'#fadan:(\d+)', order.shuoming or '')
+ if m:
+ fadan = Penalty.query.filter(
+ id=int(m.group(1)),
+ PenalizedUserID=order.yonghuid,
+ Status__in=[1, 3],
+ ).first()
+ if not fadan:
+ fadan = Penalty.query.filter(
+ PenalizedUserID=order.yonghuid,
+ FineAmount=order.jine,
+ Status__in=[1, 3],
+ ).order_by('-CreateTime').first()
+ if fadan:
+ fadan.Status = 2 # 已缴纳
+ fadan.save(update_fields=['Status'])
+ logger.info(f"罚单 {fadan.id} 已自动标记为已缴纳")
+
+ success, msg = process_fadan_fenhong(fadan.id)
+ if not success:
+ logger.error(f"罚单 {fadan.id} 分红处理失败: {msg}")
+ else:
+ logger.info(f"罚单 {fadan.id} 分红处理完成: {msg}")
+
+ except Exception as e:
+ logger.error(f"更新罚单状态失败: {str(e)}", exc_info=True)
+
+ # 9. 更新收支记录表
+ try:
+ jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
+ from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
+ repair_wechat_income_if_missing(
+ jine_decimal,
+ getattr(order, 'club_id', None),
+ biz_ref=f'cz:{order.dingdan_id}',
+ )
+
+ logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}元")
+
+ except Exception as e:
+ # 收支记录更新失败不影响主流程,只记录日志
+ logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
+
+ # 10. 返回成功响应给微信
+ 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):
+ """验证微信支付签名(完全照搬 YajinHuitiao 的 verify_signature)"""
+ 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):
+ """返回微信支付回调响应(完全照搬 YajinHuitiao 的 wechat_response)"""
+ xml_response = f"""
+
+
+"""
+
+ logger.info(f"返回微信响应: {return_code} - {return_msg}")
+ return HttpResponse(xml_response, content_type='application/xml')
+
+
+class FaKuanPayPollView(APIView):
+ """罚款支付轮询接口(借鉴 YajinLunxun)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ dingdanid = request.data.get('dingdanid')
+ if not dingdanid:
+ return Response({'code': 400, 'message': '订单ID不能为空'})
+
+ order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
+ if order.zhuangtai == 3:
+ return Response({'code': 200, 'message': '支付成功'})
+ else:
+ return Response({'code': 400, 'message': '订单尚未支付'})
+ except Czjilu.DoesNotExist:
+ return Response({'code': 400, 'message': '订单不存在'})
+ except Exception as e:
+ logger.error(f"轮询异常: {str(e)}")
+ return Response({'code': 500, 'message': '服务器内部错误'})
+
+
+class FaKuanPayFailView(APIView):
+ """罚款支付失败处理接口(借鉴 YajinShibai)"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ dingdanid = request.data.get('dingdanid')
+ if not dingdanid:
+ return Response({'code': 400, 'message': '订单ID不能为空'})
+
+ order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID)
+ if order.zhuangtai == 9: # 未支付
+ order.delete()
+ logger.info(f"用户{request.user.UserUID}删除未支付订单: {dingdanid}")
+ return Response({'code': 200, 'message': '订单已删除'})
+ else:
+ return Response({'code': 400, 'message': '订单状态不是未支付,无法删除'})
+ except Czjilu.DoesNotExist:
+ return Response({'code': 400, 'message': '订单不存在'})
+ except Exception as e:
+ logger.error(f"支付失败处理异常: {str(e)}")
+ return Response({'code': 500, 'message': '服务器内部错误'})
+
+
+class KaohePayView(APIView):
+ """
+ 考核支付下单接口
+ 路径:POST /yonghu/khzf
+ 参数:
+ tag_id: 标签ID
+ game_nick: 游戏昵称
+ remark: 备注
+ reapply: 是否再次申请 (true/false)
+ jilu_id: 再次申请时的原记录ID(reapply=true时必传)
+ shenheguan_id: 指定考核官ID(可选)
+ """
+ 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()
+
+ logger.info(f"用户 {user.UserUID} 发起考核支付申请,标签ID: {tag_id}, 再次申请: {reapply}")
+
+ if not tag_id or not game_nick:
+ logger.warning(f"用户 {user.UserUID} 参数不完整: tag_id={tag_id}, game_nick={game_nick}")
+ return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'})
+
+ # 1. 检查是否有未支付的考核订单
+ """unpaid_order = Czjilu.query.filter(
+ UserUID=user.UserUID,
+ leixing=5,
+ zhuangtai=9
+ ).exists()
+ if unpaid_order:
+ logger.warning(f"用户 {user.UserUID} 存在未支付的考核订单")
+ return Response({'code': 400, 'message': '您有未支付的考核订单,请先完成支付或取消'})"""
+
+ # 2. 验证标签
+ try:
+ chenghao = Chenghao.query.get(id=tag_id)
+ logger.info(f"标签验证通过: {chenghao.mingcheng}")
+ except Chenghao.DoesNotExist:
+ logger.error(f"标签不存在: tag_id={tag_id}")
+ return Response({'code': 404, 'message': '标签不存在'})
+
+ # 3. 检查用户是否已拥有该标签
+ if YonghuChenghao.query.filter(yonghu=user, chenghao=chenghao).exists():
+ logger.warning(f"用户 {user.UserUID} 已拥有标签 {chenghao.mingcheng}")
+ return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'})
+
+ # 4. 检查是否有未完成的申请
+ if ShenheJilu.query.filter(
+ shenqingren_id=user.UserUID,
+ chenghao=chenghao,
+ zhuangtai__in=[0, 3] # 审核中、待审核
+ ).exists():
+ logger.warning(f"用户 {user.UserUID} 有未完成的考核申请")
+ return Response({'code': 400, 'message': '您有未完成的考核申请,请等待结果'})
+
+ # 5. 计算考核次数和费用
+ if reapply and jilu_id:
+ last = ShenheJilu.query.filter(
+ jilu_id=jilu_id,
+ shenqingren_id=user.UserUID,
+ zhuangtai=2
+ ).first()
+ if not last:
+ logger.warning(f"用户 {user.UserUID} 再次申请失败:原记录不存在或状态不是未通过,jilu_id={jilu_id}")
+ return Response({'code': 400, 'message': '原记录不存在或状态不是未通过'})
+ new_cishu = last.cishu + 1
+ logger.info(f"再次申请,原记录ID: {jilu_id},新次数: {new_cishu}")
+ else:
+ max_cishu = ShenheJilu.query.filter(
+ shenqingren_id=user.UserUID,
+ chenghao=chenghao,
+ zhuangtai__in=[1, 2]
+ ).aggregate(max=models.Max('cishu'))['max'] or 0
+ new_cishu = max_cishu + 1
+ logger.info(f"首次申请,计算次数: {new_cishu}")
+
+ # 6. 获取本次费用
+ fee = get_tag_fee(chenghao, new_cishu)
+ logger.info(f"本次考核费用: {fee}元,次数: {new_cishu}")
+
+ if fee <= 0:
+ logger.warning(f"费用为0,应使用免费申请接口,tag_id={tag_id}, cishu={new_cishu}")
+ return Response({'code': 400, 'message': '本次费用为0,请直接使用免费申请接口'})
+
+ # 7. 生成订单ID
+ timestamp_ms = int(time.time() * 1000)
+ timestamp_ns = int(time.time_ns() // 1000) % 1000000
+ random_num = random.randint(0, 99)
+ dingdanid = f"KH{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
+ logger.info(f"生成订单ID: {dingdanid}")
+
+ # 8. 创建支付订单(Czjilu)
+ from jituan.services.club_write import resolve_club_id_for_write
+ order = Czjilu.query.create(
+ dingdan_id=dingdanid,
+ zhuangtai=9, # 未支付
+ yonghuid=user.UserUID,
+ jine=float(fee),
+ leixing=5, # 5-考核支付
+ shuoming=settings.KAOHE_PAY_DESCRIPTION,
+ club_id=resolve_club_id_for_write(request, user),
+ )
+ logger.info(f"创建支付订单成功: {dingdanid}")
+
+ # 9. 保存考核支付临时上下文(用于回调时取业务参数)
+ try:
+ KaohePayTemp.query.create(
+ dingdan_id=dingdanid,
+ yonghuid=user.UserUID,
+ tag_id=tag_id,
+ game_nick=game_nick,
+ remark=remark,
+ reapply=reapply,
+ jilu_id=jilu_id if reapply else None,
+ shenheguan_id=shenheguan_id,
+ cishu=new_cishu,
+ fee=fee,
+ )
+ logger.info(f"保存临时上下文成功: {dingdanid}")
+ except Exception as e:
+ logger.error(f"保存临时上下文失败: {str(e)}", exc_info=True)
+ # 临时表保存失败应该回滚订单
+ order.delete()
+ return Response({'code': 500, 'message': '系统异常,请稍后重试'})
+
+ # 10. 构造 attach 参数(只传订单ID,确保不超127字节)
+ attach_str = json.dumps({'oid': dingdanid}, ensure_ascii=False)
+ logger.info(f"attach内容: {attach_str},长度: {len(attach_str.encode('utf-8'))}字节")
+
+ # 11. 生成微信支付参数
+ try:
+ pay_params = self.generate_wechat_pay_params(
+ dingdanid=dingdanid,
+ jine=float(fee),
+ openid=user.OpenID,
+ attach=attach_str,
+ pay_type='kaohe'
+ )
+ logger.info(f"微信支付参数生成成功: {dingdanid}")
+ except Exception as e:
+ order.delete() # 下单失败,删除订单记录
+ # 同时删除临时表记录
+ try:
+ KaohePayTemp.query.filter(dingdan_id=dingdanid).delete()
+ except Exception as del_e:
+ logger.error(f"删除临时表记录失败: {str(del_e)}")
+ logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'message': f'支付参数生成失败: {str(e)}'})
+
+ return Response({
+ 'code': 200,
+ 'message': '支付参数生成成功',
+ 'payParams': pay_params,
+ 'dingdanid': dingdanid
+ })
+
+ except Exception as e:
+ logger.error(f"考核支付下单异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'message': '服务器内部错误'})
+
+ def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type):
+ """生成微信JSAPI支付参数"""
+ 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 == 'kaohe':
+ PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION
+ NOTIFY_URL = settings.KAOHE_PAY_NOTIFY_URL
+ else:
+ raise Exception('未知支付类型')
+
+ nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+ total_fee = yuan_to_fen(jine)
+ trade_type = 'JSAPI'
+ user_ip = self.get_client_ip()
+
+ logger.info(f"微信支付下单参数: out_trade_no={dingdanid}, total_fee={total_fee}, openid={openid}, ip={user_ip}")
+
+ # 第一次签名(统一下单)
+ 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,
+ 'attach': attach,
+ }
+
+ 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}'
+ sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
+
+ # 构建XML请求体
+ xml_data = f"""
+{APPID}
+{PAY_DESCRIPTION}
+{MCHID}
+{nonce_str}
+{NOTIFY_URL}
+{openid}
+{dingdanid}
+{user_ip}
+{total_fee}
+{trade_type}
+
+{sign}
+"""
+
+ logger.info(f"微信支付请求XML: {xml_data[:500]}...") # 只打印前500字符
+
+ response = requests.post(
+ 'https://api.mch.weixin.qq.com/pay/unifiedorder',
+ data=xml_data.encode('utf-8'),
+ headers={'Content-Type': 'application/xml'},
+ timeout=10
+ )
+
+ logger.info(f"微信支付响应: {response.text}")
+
+ root = ET.fromstring(response.content)
+
+ # 安全获取return_code
+ return_code_node = root.find('return_code')
+ if return_code_node is None or return_code_node.text is None:
+ logger.error(f"微信返回缺少return_code节点,原始内容: {response.text}")
+ raise Exception('微信支付接口异常,缺少return_code')
+ return_code = return_code_node.text
+
+ # 安全获取result_code
+ result_code_node = root.find('result_code')
+ if result_code_node is None or result_code_node.text is None:
+ logger.error(f"微信返回缺少result_code节点,原始内容: {response.text}")
+ raise Exception('微信支付接口异常,缺少result_code')
+ result_code = result_code_node.text
+
+ if return_code == 'SUCCESS' and result_code == 'SUCCESS':
+ prepay_id_node = root.find('prepay_id')
+ if prepay_id_node is None or prepay_id_node.text is None:
+ logger.error(f"统一下单成功但未返回prepay_id,原始内容: {response.text}")
+ raise Exception('微信支付接口异常,缺少prepay_id')
+ prepay_id = prepay_id_node.text
+
+ 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'
+
+ # 第二次签名(前端调起支付用)
+ pay_sign_data = {
+ 'appId': APPID,
+ '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()
+
+ logger.info(f"微信支付预下单成功,prepay_id={prepay_id}")
+
+ return {
+ 'appId': APPID,
+ 'timeStamp': timestamp,
+ 'nonceStr': nonce_str,
+ 'package': package,
+ 'signType': sign_type,
+ 'paySign': pay_sign,
+ }
+ else:
+ # 安全获取错误信息
+ err_code_node = root.find('err_code')
+ err_code = err_code_node.text if err_code_node is not None else '未知错误码'
+ err_code_des_node = root.find('err_code_des')
+ err_code_des = err_code_des_node.text if err_code_des_node is not None else '未知错误描述'
+ error_details = f"return_code: {return_code}, result_code: {result_code}, err_code: {err_code}, err_code_des: {err_code_des}"
+ logger.error(f"微信支付下单失败: {error_details}")
+ 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', '127.0.0.1')
+ return ip if ip else '127.0.0.1'
+
+
+class KaohePayCallbackView(View):
+ """
+ 考核支付回调接口
+ 路径:/yonghu/kh_huitiao
+ """
+
+ def post(self, request):
+ try:
+ request_body = request.body
+ logger.info(f"收到考核支付回调: {request_body.decode('utf-8')}")
+
+ root = ET.fromstring(request_body)
+
+ # 安全获取return_code
+ return_code_node = root.find('return_code')
+ if return_code_node is None or return_code_node.text is None:
+ logger.error(f"微信回调XML缺少return_code节点,原始内容: {request_body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ return_code = return_code_node.text
+
+ # 安全获取result_code
+ result_code_node = root.find('result_code')
+ if result_code_node is None or result_code_node.text is None:
+ logger.error(f"微信回调XML缺少result_code节点,原始内容: {request_body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ result_code = result_code_node.text
+
+ # 安全获取out_trade_no
+ out_trade_no_node = root.find('out_trade_no')
+ if out_trade_no_node is None or out_trade_no_node.text is None:
+ logger.error(f"微信回调XML缺少out_trade_no节点,原始内容: {request_body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ out_trade_no = out_trade_no_node.text
+
+ # 安全获取transaction_id
+ transaction_id_node = root.find('transaction_id')
+ if transaction_id_node is None or transaction_id_node.text is None:
+ logger.error(f"微信回调XML缺少transaction_id节点,原始内容: {request_body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ transaction_id = transaction_id_node.text
+
+ # 安全获取total_fee
+ total_fee_node = root.find('total_fee')
+ if total_fee_node is None or total_fee_node.text is None:
+ logger.error(f"微信回调XML缺少total_fee节点,原始内容: {request_body.decode('utf-8')}")
+ return self.wechat_response('FAIL', '数据格式错误')
+ total_fee = int(total_fee_node.text)
+
+ logger.info(
+ f"回调参数解析成功: out_trade_no={out_trade_no}, transaction_id={transaction_id}, total_fee={total_fee}")
+
+ if return_code != 'SUCCESS' or result_code != 'SUCCESS':
+ logger.warning(f"支付状态异常: return_code={return_code}, result_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', '签名验证失败')
+ logger.info(f"签名验证成功,订单号: {out_trade_no}")
+
+ # 查找订单
+ try:
+ order = Czjilu.query.get(dingdan_id=out_trade_no, leixing=5)
+ logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.zhuangtai}")
+ except Czjilu.DoesNotExist:
+ logger.error(f"考核订单不存在: {out_trade_no}")
+ return self.wechat_response('FAIL', '订单不存在')
+
+ if order.zhuangtai != 9:
+ logger.warning(f"订单状态非未支付: {order.zhuangtai}, 订单号: {out_trade_no}")
+ if order.zhuangtai == 3:
+ logger.info(f"订单已支付,补试收支记账: {out_trade_no}")
+ 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')
+ return self.wechat_response('FAIL', '订单状态异常')
+
+ # 验证金额
+ wechat_yuan = total_fee / 100
+ if abs(wechat_yuan - float(order.jine)) > 0.01:
+ logger.error(f"金额不一致: 订单金额={order.jine}, 回调金额={wechat_yuan}, 订单号: {out_trade_no}")
+ return self.wechat_response('FAIL', '金额不一致')
+ logger.info(f"金额验证通过: {wechat_yuan}元")
+
+ # 更新订单状态为已支付
+ order.zhuangtai = 3
+ order.save(update_fields=['zhuangtai'])
+ logger.info(f"订单状态更新为已支付: {out_trade_no}")
+
+ # 从临时表获取业务参数并创建审核记录
+ try:
+ temp = KaohePayTemp.query.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid)
+ logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}")
+
+ user = User.query.get(UserUID=order.yonghuid)
+ logger.info(f"获取用户信息成功: {user.UserUID}")
+
+ success, msg, record = create_shenhe_jilu_from_temp(user, temp)
+ if success:
+ logger.info(f"创建审核记录成功: jilu_id={record.jilu_id}, 订单号: {out_trade_no}")
+ else:
+ logger.error(f"创建审核记录失败: {msg}, 订单号: {out_trade_no}")
+ # 订单已支付但记录创建失败,需要标记订单异常,人工介入
+ order.zhuangtai = 10 # 10表示支付成功但业务处理失败
+ order.save(update_fields=['zhuangtai'])
+ logger.error(f"订单状态标记为异常(10): {out_trade_no}")
+
+ except KaohePayTemp.DoesNotExist:
+ logger.error(f"临时表无记录,订单号: {out_trade_no}, 用户ID: {order.yonghuid}")
+ # 尝试从attach解析(兼容旧数据)
+ attach_element = root.find('attach')
+ if attach_element is not None and attach_element.text:
+ try:
+ attach_data = json.loads(attach_element.text)
+ logger.info(f"从attach解析数据成功: {attach_data}")
+ # 这里可以保留旧逻辑,但建议只记录日志
+ except Exception as e:
+ logger.error(f"解析attach失败: {str(e)}")
+ except Exception as e:
+ logger.error(f"处理临时表或创建审核记录异常: {str(e)}", exc_info=True)
+ # 标记订单异常
+ order.zhuangtai = 10
+ order.save(update_fields=['zhuangtai'])
+ logger.error(f"订单状态标记为异常(10): {out_trade_no}")
+
+ # 更新收支记录
+ try:
+ jine_decimal = Decimal(str(order.jine))
+ from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
+ repair_wechat_income_if_missing(
+ jine_decimal,
+ getattr(order, 'club_id', None),
+ biz_ref=f'cz:{order.dingdan_id}',
+ )
+ logger.info(f"更新收支记录成功: {jine_decimal}元")
+ except Exception as e:
+ logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
+
+ logger.info(f"考核支付回调处理完成,订单号: {out_trade_no}")
+ return self.wechat_response('SUCCESS', 'OK')
+
+ except ET.ParseError as e:
+ logger.error(f"XML解析失败: {str(e)}, 原始数据: {request.body.decode('utf-8')}")
+ 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_node = root.find('sign')
+ if sign_node is None or sign_node.text is None:
+ logger.error("微信回调XML缺少sign节点")
+ return False
+ sign = sign_node.text
+
+ 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()
+
+ result = sign == calculated_sign
+ if not result:
+ logger.error(f"签名验证失败: 原签名={sign}, 计算签名={calculated_sign}")
+ return result
+ except Exception as e:
+ logger.error(f"签名验证异常: {str(e)}", exc_info=True)
+ return False
+
+ def wechat_response(self, return_code, return_msg):
+ """构造微信回调响应XML"""
+ xml = f"""
+
+
+"""
+ logger.info(f"返回微信响应: {xml}")
+ return HttpResponse(xml, content_type='application/xml')
+
+
+class KaohePayPollView(APIView):
+ """考核支付轮询"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ dingdanid = request.data.get('dingdanid')
+ if not dingdanid:
+ return Response({'code': 400, 'message': '订单ID不能为空'})
+ try:
+ order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5)
+ if order.zhuangtai == 3:
+ return Response({'code': 200, 'message': '支付成功'})
+ return Response({'code': 400, 'message': '订单尚未支付'})
+ except Czjilu.DoesNotExist:
+ return Response({'code': 400, 'message': '订单不存在'})
+
+
+class KaohePayFailView(APIView):
+ """考核支付失败处理"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ dingdanid = request.data.get('dingdanid')
+ if not dingdanid:
+ return Response({'code': 400, 'message': '订单ID不能为空'})
+ try:
+ order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.UserUID, leixing=5)
+ if order.zhuangtai == 9:
+ order.delete()
+ return Response({'code': 200, 'message': '订单已删除'})
+ return Response({'code': 400, 'message': '订单状态无法删除'})
+ except Czjilu.DoesNotExist:
+ return Response({'code': 400, 'message': '订单不存在'})
diff --git a/users/views/shangjia.py b/users/views/shangjia.py
new file mode 100644
index 0000000..dd9e835
--- /dev/null
+++ b/users/views/shangjia.py
@@ -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)
diff --git a/users/views/tixian.py b/users/views/tixian.py
new file mode 100644
index 0000000..6364d08
--- /dev/null
+++ b/users/views/tixian.py
@@ -0,0 +1,1418 @@
+"""users.views.tixian - 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 TixianXinxiHuoquView(APIView):
+ """
+ 获取用户提现收款信息接口
+ 返回用户已设置的手机号、收款码、收款账号
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ """
+ 处理获取收款信息的POST请求
+ 直接返回用户当前的收款设置
+ """
+ try:
+ # 从request.user获取当前用户
+ user_main = request.user
+
+ from jituan.services.club_user import get_user_club_id
+ from jituan.services.withdraw_config import get_tixian_feilv
+
+ club_id = get_user_club_id(user_main)
+ dashou_rate = get_tixian_feilv(club_id, 1)
+ guanshi_rate = get_tixian_feilv(club_id, 2)
+
+ # 构建返回数据
+ response_data = {
+ 'txdianhua': user_main.Phone if user_main.Phone else '',
+ 'txtupian': user_main.PaymentQRCode if user_main.PaymentQRCode else '',
+ 'txzh': user_main.PaymentAccount if user_main.PaymentAccount else '',
+ 'dashou_rate': dashou_rate,
+ 'guanshi_rate': guanshi_rate,
+ }
+
+ # 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ return Response({
+ 'code': 99,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class ShoukuanXinxiShangchuanView(APIView):
+ """
+ 上传用户收款信息接口
+ 支持同时上传手机号、收款账号、收款码图片
+ 使用multipart/form-data格式
+ """
+ permission_classes = [permissions.IsAuthenticated]
+ parser_classes = (MultiPartParser,FormParser,JSONParser)
+
+ def post(self, request):
+ """
+ 处理上传收款信息的POST请求
+ 验证并保存用户的收款设置
+ """
+ try:
+ # 获取当前用户
+ user_main = request.user
+ yonghuid = user_main.UserUID
+
+ # 准备响应数据
+ response_data = {}
+
+ # 1. 验证必填字段:手机号必须要有
+ txdianhua = request.data.get('txdianhua', '').strip()
+ if not txdianhua:
+ return Response({
+ 'code': 1,
+ 'msg': '收款人电话不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证手机号格式(11位数字)
+ if not txdianhua.isdigit() or len(txdianhua) != 11:
+ return Response({
+ 'code': 2,
+ 'msg': '请输入11位有效的手机号码',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 获取收款账号(可能为空)
+ txzh = request.data.get('txzh', '').strip()
+
+ # 3. 处理收款码图片上传(如果有)
+ txtupian_file = request.FILES.get('file') # 注意:前端upload.js使用'file'作为字段名
+ txtupian_url = ''
+
+ if txtupian_file:
+ # 验证图片
+ is_valid, error_msg = validate_image(txtupian_file)
+ if not is_valid:
+ return Response({
+ 'code': 3,
+ 'msg': f'图片验证失败: {error_msg}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 上传收款码图片
+ txtupian_url = self.handle_shoukuanma_upload(user_main, txtupian_file)
+ if txtupian_url:
+ response_data['txtupian'] = txtupian_url
+ else:
+ return Response({
+ 'code': 4,
+ 'msg': '收款码上传失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ else:
+ response_data['txtupian'] = ''
+
+ # 4. 验证收款账号和收款码至少有一个
+ if not txzh and not txtupian_url:
+ return Response({
+ 'code': 5,
+ 'msg': '请填写收款账号或上传收款码',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 保存所有信息到数据库
+ with transaction.atomic():
+ user_main.Phone = txdianhua
+ user_main.PaymentAccount = txzh if txzh else ''
+
+ # 只有上传了图片才更新zhifu字段
+ if txtupian_url:
+ user_main.PaymentQRCode = txtupian_url
+
+ user_main.save()
+
+ # 6. 构建完整的返回数据
+ response_data['txdianhua'] = txdianhua
+ response_data['txzh'] = txzh if txzh else ''
+
+ # 7. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '收款信息设置成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ traceback.print_exc() # 打印详细错误信息到控制台
+ return Response({
+ 'code': 99,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def handle_shoukuanma_upload(self, user_main, image_file):
+ """
+ 处理收款码图片上传到腾讯云COS
+ 存储路径: tixian/{yonghuid}/{yonghuid}.扩展名
+ """
+ try:
+ yonghuid = user_main.UserUID
+ if not yonghuid:
+ return None
+
+ # 获取旧收款码URL
+ old_shoukuanma_url = user_main.PaymentQRCode
+
+ # 生成文件扩展名
+ file_name = image_file.name
+ file_extension = os.path.splitext(file_name)[1].lower()
+ if not file_extension:
+ file_extension = '.jpg'
+
+ # 构建OSS文件路径: tixian/{yonghuid}/{yonghuid}{扩展名}
+ # 使用用户ID作为文件名,确保同一用户多次上传会覆盖
+ oss_file_name = f"{yonghuid}{file_extension}"
+ oss_file_path = f"tixian/{yonghuid}/{oss_file_name}"
+
+ # 删除旧收款码(如果存在)
+ if old_shoukuanma_url and old_shoukuanma_url.strip():
+ delete_from_oss(old_shoukuanma_url)
+
+ # 上传新收款码到OSS
+ new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
+
+ if new_shoukuanma_url:
+ # 返回相对路径(从tixian文件夹开始)
+ return oss_file_path
+ else:
+ return None
+
+ except Exception as e:
+ print(f"处理收款码上传失败: {e}")
+ return None
+
+
+# ========== 订单状态常量(请根据您的实际业务修改)==========
+# 假设状态值:已完成=9,已退款=10(示例,实际以您的项目为准)
+ORDER_STATUS_COMPLETED = 3 # 订单已完成
+ORDER_STATUS_REFUNDED = 5 # 订单已退款
+ORDER_STATUS_FAIL = 6
+# 进行中状态(不可提现)例如:待接单、已接单、进行中等,您需要定义排除列表
+FORBIDDEN_STATUS_FOR_WITHDRAW = [1, 2, 3, 4, 5, 6, 7, 8] # 除已完成和已退款外的所有状态
+
+
+class TixianShenqingView(APIView):
+ """
+ 用户提现申请接口(完整版)
+ 支持类型:
+ 1-打手佣金提现
+ 2-管事分红提现
+ 3-组长分红提现
+ 4-审核官分佣提现
+ 5-打手押金提现(退出平台)
+ 6-商家余额提现(退出平台)
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user_main = request.user
+ yonghuid = user_main.UserUID
+
+ # 1. 获取并验证参数
+ leixing = request.data.get('leixing')
+ jine = request.data.get('jine')
+ fangshi = request.data.get('fangshi')
+ txskfs = request.data.get('txskfs', 0)
+
+ # 罚单检查(共用)
+ fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
+ if not fadan_ok:
+ return Response({'code': 400, 'msg': fadan_msg})
+
+ # 参数完整性
+ missing = []
+ if leixing is None: missing.append('leixing')
+ if jine is None: missing.append('jine')
+ if fangshi is None: missing.append('fangshi')
+ if missing:
+ return Response({'code': 1, 'msg': f'缺少参数: {", ".join(missing)}'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ leixing = int(leixing)
+ jine = decimal.Decimal(str(jine))
+ fangshi = int(fangshi)
+ txskfs = int(txskfs)
+ except (ValueError, TypeError, decimal.InvalidOperation) as e:
+ return Response({'code': 2, 'msg': f'参数格式错误: {str(e)}'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if jine <= decimal.Decimal('0'):
+ return Response({'code': 3, 'msg': '提现金额必须大于0'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 支持的提现类型(1-6)
+ if leixing not in [1, 2, 3, 4, 5, 6]:
+ return Response({'code': 4, 'msg': '无效的提现类型'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if fangshi not in [1, 2]:
+ return Response({'code': 5, 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ from jituan.services.club_user import get_user_club_id
+ from jituan.services.withdraw_config import get_tixian_feilv
+
+ club_id = get_user_club_id(user_main)
+ current_rate = decimal.Decimal('0.00')
+ rate_key = {
+ 1: '5', # 打手佣金
+ 2: '6', # 管事分红
+ 3: '8', # 组长分红
+ 4: '9', # 审核官分佣
+ 5: '11', # 打手押金提现
+ 6: '10', # 商家余额提现
+ }.get(leixing)
+
+ if rate_key:
+ try:
+ current_rate = get_tixian_feilv(club_id, leixing)
+ except ValueError:
+ return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing})'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ shouxufei = jine * current_rate
+ shijidaozhang = jine - shouxufei
+ if shijidaozhang <= decimal.Decimal('0'):
+ return Response({'code': 6, 'msg': '提现金额过低,扣除手续费后无实际到账'},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 根据提现类型执行校验和扣款
+ nicheng = ''
+ if leixing == 1:
+ result = self._check_dashou_commission_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ nicheng = result['data']['nicheng']
+ # 扣款(佣金)
+ with transaction.atomic():
+ profile = UserDashou.objects.select_for_update().get(user=user_main)
+ if profile.yue < jine:
+ return Response({'code': 13, 'msg': '余额不足'})
+ profile.yue = F('yue') - jine
+ profile.save()
+ elif leixing == 2:
+ result = self._check_guanshi_dividend_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ with transaction.atomic():
+ profile = UserGuanshi.objects.select_for_update().get(user=user_main)
+ if profile.yue < jine:
+ return Response({'code': 22, 'msg': '余额不足'})
+ profile.yue = F('yue') - jine
+ profile.save()
+ nicheng = "管事用户"
+ elif leixing == 3:
+ result = self._check_zuzhang_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ with transaction.atomic():
+ profile = UserZuzhang.objects.select_for_update().get(user=user_main)
+ if profile.ketixian_jine < jine:
+ return Response({'code': 32, 'msg': '可提现金额不足'})
+ profile.ketixian_jine = F('ketixian_jine') - jine
+ profile.save()
+ nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else ''
+ elif leixing == 4:
+ result = self._check_shenheguan_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ with transaction.atomic():
+ profile = UserShenheguan.objects.select_for_update().get(user=user_main)
+ if profile.yue < jine:
+ return Response({'code': 42, 'msg': '可提现金额不足'})
+ profile.yue = F('yue') - jine
+ profile.save()
+ nicheng = "审核官"
+ elif leixing == 5:
+ # 打手押金提现(退出)
+ result = self._check_dashou_yajin_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ with transaction.atomic():
+ profile = UserDashou.objects.select_for_update().get(user=user_main)
+ if profile.yajin < jine:
+ return Response({'code': 53, 'msg': '押金余额不足'})
+ profile.yajin = F('yajin') - jine
+ # 押金提现后封禁打手账号
+ profile.zhanghaozhuangtai = 0 # 0表示禁用
+ profile.save()
+ nicheng = profile.nicheng or ''
+ elif leixing == 6:
+ # 商家余额提现(退出)
+ result = self._check_shangjia_withdraw(user_main, jine)
+ if result['code'] != 0:
+ return Response(result, status=status.HTTP_400_BAD_REQUEST)
+ with transaction.atomic():
+ profile = UserShangjia.objects.select_for_update().get(user=user_main)
+ if profile.yue < jine:
+ return Response({'code': 63, 'msg': '商家余额不足'})
+ profile.yue = F('yue') - jine
+ # 注意:是否封禁商家账号?根据需求,用户未明确要求封禁,暂时只扣款,可后续扩展。
+ profile.save()
+ nicheng = profile.nicheng or ''
+
+ # 4. 创建提现记录(先扣款后创建,确保资金安全)
+ try:
+ avatar = user_main.Avatar or ''
+ phone = user_main.Phone or ''
+ zhifu = user_main.PaymentQRCode or ''
+ skzhanghao = user_main.PaymentAccount or ''
+ from jituan.services.club_user import get_user_club_id
+ tixian_record = Tixianjilu.query.create(
+ club_id=get_user_club_id(user_main),
+ yonghuid=yonghuid,
+ avatar=avatar,
+ phone=phone,
+ nicheng=nicheng,
+ leixing=leixing,
+ zhifu=zhifu,
+ skzhanghao=skzhanghao,
+ jine=shijidaozhang, # 记录实际到账金额
+ zhuangtai=1, # 审核中
+ fangshi=fangshi,
+ shenheid=None,
+ bhliyou='',
+ )
+ except Exception as e:
+ return Response({'code': 99, 'msg': f'创建提现记录失败: {str(e)}'},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 5. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '提现申请提交成功',
+ 'data': {
+ 'tixian_id': tixian_record.id,
+ 'leixing': leixing,
+ 'jine': str(jine.quantize(decimal.Decimal('0.01'))),
+ 'fangshi': fangshi,
+ 'zhuangtai': 1,
+ 'CreateTime': tixian_record.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
+ 'current_rate': str(current_rate.quantize(decimal.Decimal('0.0000'))),
+ 'shouxufei': str(shouxufei.quantize(decimal.Decimal('0.01'))),
+ 'shijidaozhang': str(shijidaozhang.quantize(decimal.Decimal('0.01'))),
+ 'tip': f'本次提现手续费率{current_rate * 100}%,手续费{shouxufei}元,实际到账{shijidaozhang}元'
+ }
+ })
+
+ except Exception as e:
+ traceback.print_exc()
+ return Response({'code': 99, 'msg': f'系统错误: {str(e)}'},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # ==================== 校验函数 ====================
+
+ def _check_dashou_commission_withdraw(self, user_main, jine):
+ """打手佣金提现校验"""
+ try:
+ dashou = UserDashou.query.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 10, 'msg': '您不是打手身份,无法提现佣金'}
+
+ if dashou.zhanghaozhuangtai != 1:
+ return {'code': 11, 'msg': '打手账号已被封禁,无法提现'}
+ if dashou.zhuangtai != 1:
+ return {'code': 12, 'msg': '您有订单进行中,请完成后提现'}
+ if dashou.jifen != 10:
+ return {'code': 15, 'msg': '积分不足10分,请补充积分后提现'}
+ if dashou.yue < jine:
+ return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'}
+
+ trial_ok, trial_msg = check_dashou_trial_blocks_commission_withdraw(user_main.UserUID)
+ if not trial_ok:
+ return {'code': 16, 'msg': trial_msg}
+
+ return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
+
+ def _check_guanshi_dividend_withdraw(self, user_main, jine):
+ """管事分红提现校验"""
+ try:
+ guanshi = UserGuanshi.query.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 20, 'msg': '您不是管事身份,无法提现分红'}
+ if guanshi.zhuangtai != 1:
+ return {'code': 21, 'msg': '管事账号已被封禁,无法提现'}
+ if guanshi.yue < jine:
+ return {'code': 22, 'msg': f'余额不足,当前余额: {guanshi.yue}'}
+ return {'code': 0, 'msg': 'ok', 'data': {}}
+
+ def _check_zuzhang_withdraw(self, user_main, jine):
+ """组长分红提现校验"""
+ try:
+ zuzhang = UserZuzhang.query.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 30, 'msg': '您不是组长身份,无法提现组长分红'}
+ if zuzhang.zhuangtai != 1:
+ return {'code': 31, 'msg': '组长账号已被封禁,无法提现'}
+ if zuzhang.ketixian_jine < jine:
+ return {'code': 32, 'msg': f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}'}
+ return {'code': 0, 'msg': 'ok', 'data': {}}
+
+ def _check_shenheguan_withdraw(self, user_main, jine):
+ """审核官分佣提现校验"""
+ try:
+ shenheguan = UserShenheguan.query.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 40, 'msg': '您不是审核官身份,无法提现分佣'}
+ if shenheguan.zhuangtai != 1:
+ return {'code': 41, 'msg': '审核官账号已被封禁,无法提现'}
+ if shenheguan.yue < jine:
+ return {'code': 42, 'msg': f'可提现金额不足,当前可提: {shenheguan.yue}'}
+ return {'code': 0, 'msg': 'ok', 'data': {}}
+
+
+ def _check_shangjia_withdraw(self, user_main, jine):
+ """
+ 商家余额提现校验(退出平台)
+ 包含:订单、自身罚单、申请的罚单检查
+ """
+ try:
+ shangjia = UserShangjia.query.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 60, 'msg': '您不是商家身份,无法提现商家余额'}
+
+ if shangjia.zhuangtai != 1:
+ return {'code': 61, 'msg': '商家账号已被禁用,无法提现'}
+ if shangjia.yue < jine:
+ return {'code': 62, 'msg': f'商家余额不足,当前余额: {shangjia.yue}'}
+
+ # 订单检查(使用 Q 取反)
+ unfinished_orders = MerchantOrderExt.query.filter(
+ MerchantID=user_main.UserUID,
+ ).exclude(
+ Order__Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED,ORDER_STATUS_FAIL]
+ ).exists()
+ if unfinished_orders:
+ return {'code': 63, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提现余额'}
+
+ # 自身被处罚的罚单检查(状态不是已缴纳(2)和已驳回(4))
+ self_fadan = Penalty.query.filter(
+ PenalizedUserID=user_main.UserUID,
+ ).exclude(
+ Status__in=[2, 4]
+ ).exists()
+ if self_fadan:
+ return {'code': 64, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
+
+ # 申请的对他人处罚罚单检查(状态不是已缴纳(2)和已驳回(4))
+ applied_fadan = Penalty.query.filter(
+ ApplicantID=user_main.UserUID,
+ ).exclude(
+ Status__in=[2, 4]
+ ).exists()
+ if applied_fadan:
+ return {'code': 65, 'msg': '您发起的对其他用户的处罚尚未处理完毕(待缴纳/申诉中),请等待完结'}
+
+ return {'code': 0, 'msg': 'ok', 'data': {'nicheng': shangjia.nicheng or ''}}
+
+ def _check_dashou_yajin_withdraw(self, user_main, jine):
+ """
+ 打手押金提现校验(退出平台)
+ 要求:无进行中的订单,无未结算订单,自身罚单需已处理
+ """
+ try:
+ dashou = UserDashou.query.get(user=user_main)
+ except ObjectDoesNotExist:
+ return {'code': 50, 'msg': '您不是打手身份,无法提取押金'}
+
+ if dashou.zhanghaozhuangtai != 1:
+ return {'code': 51, 'msg': '打手账号已禁用,无法提取押金'}
+ if dashou.zhuangtai != 1:
+ return {'code': 52, 'msg': '您有订单进行中,请完成后提取押金'}
+ if dashou.yajin < jine:
+ return {'code': 53, 'msg': f'押金余额不足,当前押金: {dashou.yajin}'}
+
+ # 订单检查(使用 exclude + __in)
+ unfinished_orders = Order.query.filter(
+ PlayerID=user_main.UserUID,
+ ).exclude(
+ Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]
+ ).exists()
+ if unfinished_orders:
+ return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'}
+
+ # 自身罚单检查
+ self_fadan = Penalty.query.filter(
+ PenalizedUserID=user_main.UserUID,
+ ).exclude(
+ Status__in=[2, 4]
+ ).exists()
+ if self_fadan:
+ return {'code': 55, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
+
+ return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
+
+
+class TixianJiluHuoquViewV2(APIView):
+ """
+ 提现记录获取接口
+ mode=1:手动提现记录(默认,兼容旧版)
+ mode=2:自动提现审核记录(含 shenhe_jilu_id / shenhe_danhao,支持 zhuangtai 筛选)
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ user_main = request.user
+ yonghuid = user_main.UserUID
+ page = request.data.get('page', 1)
+ limit = request.data.get('limit', 50)
+ mode = int(request.data.get('mode', 1) or 1)
+ zhuangtai_filter = int(request.data.get('zhuangtai', 0) or 0)
+
+ try:
+ page = int(page)
+ limit = int(limit)
+ if page < 1 or limit < 1:
+ return Response({'code': 1, 'msg': '参数错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+ except (TypeError, ValueError):
+ return Response({'code': 1, 'msg': '参数格式错误', 'data': None},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if limit > 100:
+ limit = 100
+ offset = (page - 1) * limit
+
+ qs = Tixianjilu.query.filter(yonghuid=yonghuid)
+ if mode == 2:
+ # 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选
+ if zhuangtai_filter > 0:
+ qs = qs.filter(zhuangtai=zhuangtai_filter)
+ qs = qs.order_by('-CreateTime')
+
+ records_queryset = qs.values(
+ 'id', 'leixing', 'jine', 'zhuangtai', 'CreateTime', 'fangshi',
+ 'phone', 'skzhanghao', 'zhifu', 'bhliyou',
+ 'shenhe_jilu_id', 'shenhe_danhao',
+ )[offset:offset + limit + 1]
+
+ records_list = list(records_queryset)
+ has_more = len(records_list) > limit
+ records_to_return = records_list[:limit] if has_more else records_list
+
+ # 批量取审核表 fail_reason
+ audit_ids = [r['shenhe_jilu_id'] for r in records_to_return if r.get('shenhe_jilu_id')]
+ audit_map = {}
+ if audit_ids:
+ for a in TixianShenheJilu.query.filter(id__in=audit_ids).values(
+ 'id', 'fail_reason', 'shenqing_jine', 'shenhe_danhao'
+ ):
+ audit_map[a['id']] = a
+
+ # 批量取自动打款失败原因
+ danhao_list = [r['shenhe_danhao'] for r in records_to_return if r.get('shenhe_danhao')]
+ auto_fail_map = {}
+ if danhao_list:
+ for ar in TixianAutoRecord.query.filter(
+ shenhe_danhao__in=danhao_list, zhuangtai=2
+ ).order_by('-CreateTime').values('shenhe_danhao', 'fail_reason'):
+ if ar['shenhe_danhao'] not in auto_fail_map:
+ auto_fail_map[ar['shenhe_danhao']] = ar['fail_reason']
+
+ formatted_records = []
+ for record in records_to_return:
+ audit = audit_map.get(record.get('shenhe_jilu_id')) if record.get('shenhe_jilu_id') else None
+ fail_reason = ''
+ if audit and audit.get('fail_reason'):
+ fail_reason = audit['fail_reason']
+ elif record.get('shenhe_danhao'):
+ fail_reason = auto_fail_map.get(record['shenhe_danhao'], '') or ''
+
+ jine_val = float(record['jine']) if record['jine'] else 0.00
+ if mode == 2 and audit and audit.get('shenqing_jine') is not None:
+ jine_val = float(audit['shenqing_jine'])
+
+ shenhe_danhao_val = record.get('shenhe_danhao') or (audit.get('shenhe_danhao') if audit else '')
+ formatted_records.append({
+ 'id': record['id'],
+ 'tixianjilu_id': record['id'],
+ 'tixian_id': record['id'],
+ 'leixing': record['leixing'],
+ 'jine': jine_val,
+ 'zhuangtai': record['zhuangtai'],
+ 'create': record['CreateTime'].strftime('%Y-%m-%d %H:%M:%S'),
+ 'fangshi': record['fangshi'],
+ 'txdianhua': record['phone'] or '',
+ 'txzh': record['skzhanghao'] or '',
+ 'txtupian': record['zhifu'] or '',
+ 'shibaiyuanyin': record['bhliyou'] or '',
+ 'bhliyou': record['bhliyou'] or '',
+ 'fail_reason': fail_reason,
+ 'shenhe_jilu_id': record.get('shenhe_jilu_id'),
+ 'shenhe_danhao': shenhe_danhao_val,
+ })
+
+ return Response({
+ 'code': 0,
+ 'msg': '获取成功',
+ 'data': {
+ 'list': formatted_records,
+ 'has_more': has_more,
+ 'page': page,
+ 'limit': limit,
+ 'current_count': len(formatted_records),
+ },
+ })
+
+ except Exception as e:
+ return Response({
+ 'code': 99,
+ 'msg': f'系统错误: {str(e)}',
+ 'data': None,
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class YonghuTixianShenheXiugaiView(APIView):
+ """
+ 用户修改审核中提现申请接口
+ 访问路径: /api/yonghu/yonghutxshxg
+ 请求方法: POST
+ 权限要求: JWT Token认证通过的用户
+ 请求体: multipart/form-data 格式
+ 支持字段: tixian_id, fangshi, txdianhua, txzh, file (图片文件)
+
+ 功能: 允许用户修改审核中提现申请的收款方式、电话、账号、收款码等
+ """
+
+ permission_classes = [IsAuthenticated]
+ parser_classes = (MultiPartParser, FormParser, JSONParser)
+
+ def post(self, request):
+ """
+ 处理用户修改审核中提现申请的POST请求
+ """
+ try:
+ # 1. 获取当前用户
+ user_main = request.user
+ current_yonghuid = user_main.UserUID
+
+ # 2. 验证必填字段:提现记录ID
+ tixian_id = request.data.get('tixian_id')
+ if not tixian_id:
+ return Response({
+ 'code': 1,
+ 'msg': '提现记录ID不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 查询提现记录(使用select_for_update确保事务安全)
+ with transaction.atomic():
+ try:
+ tixian_record = Tixianjilu.objects.select_for_update().get(
+ id=tixian_id,
+ yonghuid=current_yonghuid # 确保只能修改自己的记录
+ )
+ except Tixianjilu.DoesNotExist:
+ return Response({
+ 'code': 2,
+ 'msg': '提现记录不存在或无权修改',
+ 'data': None
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 验证提现记录状态(必须是审核中)
+ if tixian_record.zhuangtai != 1:
+ return Response({
+ 'code': 3,
+ 'msg': '只有审核中的提现申请可以修改',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 获取并验证提现方式
+ fangshi = request.data.get('fangshi')
+ if fangshi:
+ fangshi = int(fangshi)
+ if fangshi not in [1, 2]:
+ return Response({
+ 'code': 4,
+ 'msg': '提现方式无效,请选择1(微信)或2(支付宝)',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 检查提现类型与方式的匹配(如果前端提供了leixing)
+ leixing = request.data.get('leixing')
+ if leixing:
+ leixing = int(leixing)
+ # 这里可以根据业务逻辑添加类型与方式的验证
+ # 例如:某些类型只能使用某些方式
+
+ # 6. 验证手机号(必填)
+ txdianhua = request.data.get('txdianhua', '').strip()
+ if not txdianhua:
+ return Response({
+ 'code': 5,
+ 'msg': '收款人电话不能为空',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证手机号格式
+ if not txdianhua.isdigit() or len(txdianhua) != 11:
+ return Response({
+ 'code': 6,
+ 'msg': '请输入11位有效的手机号码',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 7. 获取收款账号(可选)
+ txzh = request.data.get('txzh', '').strip()
+
+ # 8. 处理收款码图片上传(如果有)
+ txtupian_file = request.FILES.get('file')
+ new_zhifu_url = None
+ old_zhifu_url = tixian_record.zhifu
+
+ if txtupian_file:
+ # 验证图片
+ is_valid, error_msg = validate_image(txtupian_file)
+ if not is_valid:
+ return Response({
+ 'code': 7,
+ 'msg': f'图片验证失败: {error_msg}',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 上传收款码图片
+ new_zhifu_url = self.handle_shoukuanma_upload(
+ user_main,
+ txtupian_file,
+ old_zhifu_url,
+ tixian_record.leixing
+ )
+
+ if not new_zhifu_url:
+ return Response({
+ 'code': 8,
+ 'msg': '收款码上传失败',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 9. 验证收款信息(账号和图片至少有一个)
+ if not txzh and not (new_zhifu_url or old_zhifu_url):
+ return Response({
+ 'code': 9,
+ 'msg': '请填写收款账号或上传收款码',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 10. 更新提现记录
+ update_fields = []
+
+ # 更新提现方式(如果有)
+ if fangshi:
+ tixian_record.fangshi = fangshi
+ update_fields.append('fangshi')
+
+ # 更新手机号(必填)
+ tixian_record.phone = txdianhua
+ update_fields.append('phone')
+
+ # 更新收款账号(如果有)
+ if txzh is not None: # 注意:空字符串也要更新
+ tixian_record.skzhanghao = txzh
+ update_fields.append('skzhanghao')
+
+ # 更新收款码图片(如果有新图片)
+ if new_zhifu_url:
+ tixian_record.zhifu = new_zhifu_url
+ update_fields.append('zhifu')
+
+ # 保存更新
+ if update_fields:
+ tixian_record.save(update_fields=update_fields)
+
+ # 11. 构建返回数据
+ response_data = {
+ 'tixian_id': tixian_record.id,
+ 'fangshi': tixian_record.fangshi,
+ 'txdianhua': tixian_record.phone,
+ 'txzh': tixian_record.skzhanghao or '',
+ 'txtupian': tixian_record.zhifu or '' # 返回相对URL
+ }
+
+ # 12. 返回成功响应
+ return Response({
+ 'code': 0,
+ 'msg': '提现申请修改成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ # 记录详细错误日志
+ logger.error(
+ f"修改提现申请异常 - 用户ID: {getattr(user_main, 'yonghuid', 'N/A')}, "
+ f"提现ID: {tixian_id}, "
+ f"错误类型: {type(e).__name__}, "
+ f"错误详情: {str(e)}",
+ exc_info=True
+ )
+
+ # 返回友好错误信息
+ return Response({
+ 'code': 99,
+ 'msg': '系统繁忙,请稍后重试',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def handle_shoukuanma_upload(self, user_main, image_file, old_zhifu_url, leixing=None):
+ """
+ 处理收款码图片上传到腾讯云COS
+ 存储路径: tixian/{yonghuid}/{timestamp_uuid}.扩展名
+
+ 参数:
+ - user_main: 当前用户对象
+ - image_file: 图片文件对象
+ - old_zhifu_url: 旧收款码URL(用于删除)
+ - leixing: 提现类型(可选,用于文件夹分类)
+
+ 返回: 新的图片相对路径
+ """
+ try:
+ yonghuid = user_main.UserUID
+ if not yonghuid:
+ return None
+
+ # 删除旧收款码(如果存在且不是空字符串)
+ if old_zhifu_url and old_zhifu_url.strip():
+ delete_from_oss(old_zhifu_url)
+
+ # 获取文件扩展名
+ file_name = image_file.name
+ file_extension = os.path.splitext(file_name)[1].lower()
+ if not file_extension:
+ file_extension = '.jpg'
+
+ # 生成唯一文件名(使用时间戳+UUID确保不重复)
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
+ unique_id = str(uuid.uuid4())[:8] # 取UUID前8位
+ unique_file_name = f"{timestamp}_{unique_id}{file_extension}"
+
+ # 根据提现类型构建文件夹路径(如果提供了leixing)
+ if leixing:
+ # leixing=1:打手佣金, leixing=2:管事分红
+ type_folder = 'dashou' if leixing == 1 else 'guanshi'
+ oss_file_path = f"tixian/{type_folder}/{yonghuid}/{unique_file_name}"
+ else:
+ # 默认路径
+ oss_file_path = f"tixian/{yonghuid}/{unique_file_name}"
+
+ # 上传新收款码到OSS
+ new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
+
+ if new_shoukuanma_url:
+ # 返回相对路径(从tixian文件夹开始)
+ return oss_file_path
+ else:
+ return None
+
+ except Exception as e:
+ print(f"处理收款码上传失败: {e}")
+ return None
+
+
+class TixianAssetView(APIView):
+ """
+ 获取用户所有可提现资产及费率
+ 请求方式:POST
+ 认证:JWT Token
+ 返回:
+ {
+ "code": 200,
+ "msg": "success",
+ "data": {
+ "dashou_yue": "123.45",
+ "dashou_yajin": "500.00",
+ "shangjia_yue": "0.00",
+ "guanshi_fenyong": "67.89",
+ "zuzhang_fenyong": "0.00",
+ "kaoheguan_fenyong": "0.00",
+ "dashou_rate": 0.08,
+ "dashou_yajin_rate": 0.05,
+ "shangjia_rate": 0.06,
+ "guanshi_rate": 0.08,
+ "zuzhang_rate": 0.05,
+ "kaoheguan_rate": 0.07
+ }
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ user = request.user
+ # 初始化返回数据,默认全部为0
+ data = {
+ 'dashou_yue': '0.00',
+ 'dashou_yajin': '0.00',
+ 'shangjia_yue': '0.00',
+ 'guanshi_fenyong': '0.00',
+ 'zuzhang_fenyong': '0.00',
+ 'kaoheguan_fenyong': '0.00',
+ 'dashou_rate': 0.00,
+ 'dashou_yajin_rate': 0.00,
+ 'shangjia_rate': 0.00,
+ 'guanshi_rate': 0.00,
+ 'zuzhang_rate': 0.00,
+ 'kaoheguan_rate': 0.00,
+ }
+
+ # ------------------- 打手资产 -------------------
+ try:
+ dashou = user.DashouProfile
+ data['dashou_yue'] = str(dashou.yue)
+ data['dashou_yajin'] = str(dashou.yajin)
+ except ObjectDoesNotExist:
+ pass
+
+ # ------------------- 商家资产 -------------------
+ try:
+ shangjia = user.ShopProfile
+ data['shangjia_yue'] = str(shangjia.yue)
+ except ObjectDoesNotExist:
+ pass
+
+ # ------------------- 管事资产 -------------------
+ try:
+ guanshi = user.GuanshiProfile
+ data['guanshi_fenyong'] = str(guanshi.yue)
+ except ObjectDoesNotExist:
+ pass
+
+ # ------------------- 组长资产 -------------------
+ try:
+ zuzhang = user.ZuzhangProfile
+ data['zuzhang_fenyong'] = str(zuzhang.ketixian_jine)
+ except ObjectDoesNotExist:
+ pass
+
+ # ------------------- 审核官(考核官)资产 -------------------
+ try:
+ kaoheguan = user.ShenheguanProfile
+ data['kaoheguan_fenyong'] = str(kaoheguan.yue)
+ except ObjectDoesNotExist:
+ pass
+
+ from jituan.services.club_user import get_user_club_id
+ from jituan.services.withdraw_config import build_tixian_asset_rates
+
+ club_id = get_user_club_id(user)
+ data.update(build_tixian_asset_rates(club_id))
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': data
+ }, status=status.HTTP_200_OK)
+
+
+class GuanshiContactView(APIView):
+ """
+ 管事获取自己的邀请人(组长)信息
+ 路径: /api/yonghu/guanshi_contact
+ 方法: POST
+ 权限: JWT认证
+ 请求体: 无(仅需携带合法token)
+ 成功返回:
+ code: 200,
+ data: {
+ uid: "1234567", // 组长用户ID
+ nicheng: "张三", // 组长昵称
+ touxiang: "avatar/xxx.jpg" // 组长头像相对路径(前端拼接完整URL)
+ }
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 获取当前登录用户(必须已通过JWT认证)
+ current_user = request.user # 类型: User
+
+ # 2. 校验是否为管事身份(存在管事扩展表记录)
+ try:
+ guanshi_profile = current_user.GuanshiProfile
+ except ObjectDoesNotExist:
+ return Response({
+ 'code': 403,
+ 'msg': '您还不是管事,无法获取组长信息',
+ 'data': None
+ })
+
+ # 3. 获取邀请人ID(管事扩展表中的 yaoqingren 字段存储的是邀请人的 yonghuid)
+ inviter_yonghuid = guanshi_profile.yaoqingren
+ if not inviter_yonghuid:
+ return Response({
+ 'code': 404,
+ 'msg': '未找到邀请人信息',
+ 'data': None
+ })
+
+ # 4. 根据邀请人用户ID查询用户主表
+ try:
+ inviter_user = User.query.get(UserUID=inviter_yonghuid)
+ except User.DoesNotExist:
+ logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}")
+ return Response({
+ 'code': 404,
+ 'msg': '邀请人账号异常',
+ 'data': None
+ })
+
+ # 5. 获取邀请人昵称(从老板扩展表取,若不存在则给默认值)
+ try:
+ inviter_nicheng = inviter_user.BossProfile.nickname
+ except ObjectDoesNotExist:
+ inviter_nicheng = '组长' # 兜底昵称
+
+ # 6. 构造返回数据
+ response_data = {
+ 'uid': inviter_user.UserUID,
+ 'nicheng': inviter_nicheng,
+ 'touxiang': inviter_user.Avatar or '' # 头像相对路径
+ }
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': response_data
+ })
+
+
+def generate_tixian_id():
+ """生成唯一提现订单号:TX + 时间戳(13位) + 4位随机数"""
+ timestamp = str(int(time.time() * 1000))
+ rand = ''.join(random.choices(string.digits, k=4))
+ return f'TX{timestamp}{rand}'
+
+
+class TixianShenqingV3View(APIView):
+ """
+ 自动提现收款接口(新流程第二步)
+ POST /yonghu/tixiansq
+ 必须传 shenhe_danhao;审核状态须为 6=待收款;申请时已扣款,本接口不再扣款
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip()
+ if shenhe_danhao:
+ return process_audit_collect(request)
+ return Response({
+ 'code': 7,
+ 'msg': '缺少审核单号,请先提交提现申请(/yonghu/zddksh)并等待审核通过后再收款',
+ })
+
+
+class TixianCallbackV3View(APIView):
+ """
+ 微信转账结果回调接口(最终状态通知)
+ 处理微信异步发送的转账成功/失败结果
+ """
+ permission_classes = [] # 无需认证,签名验证保证安全
+
+ def post(self, request):
+ headers = request.headers
+ body = request.body.decode('utf-8')
+
+ # 1. 验证签名(多商户依次验签)
+ from utils.wechat_v3 import resolve_callback_club_id, decrypt_callback_ciphertext
+ callback_club_id = resolve_callback_club_id(headers, body)
+ if not callback_club_id:
+ return Response({'code': 'FAIL', 'message': '签名验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 解密数据
+ try:
+ data = json.loads(body)
+ resource = data.get('resource')
+ if not resource:
+ return Response({'code': 'FAIL', 'message': 'resource不存在'}, status=status.HTTP_400_BAD_REQUEST)
+
+ associated_data = resource.get('associated_data', '')
+ nonce = resource.get('nonce')
+ ciphertext = resource.get('ciphertext')
+ if not nonce or not ciphertext:
+ return Response({'code': 'FAIL', 'message': '解密字段缺失'}, status=status.HTTP_400_BAD_REQUEST)
+
+ plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext, club_id=callback_club_id)
+ callback_data = json.loads(plaintext)
+ except Exception as e:
+ logger.error(f"回调解密失败: {str(e)}", exc_info=True)
+ return Response({'code': 'FAIL', 'message': f'解密失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 获取关键信息
+ out_bill_no = callback_data.get('out_bill_no') # 商户提现单号(即 tixian_id)
+ transfer_status = callback_data.get('transfer_status') # SUCCESS / FAIL
+ wechat_transfer_no = callback_data.get('transfer_no') # 微信转账单号
+ fail_reason = callback_data.get('fail_reason', '')
+
+ if not out_bill_no:
+ return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 幂等处理:只更新状态为0(处理中)的记录,已最终状态不再处理
+ with transaction.atomic():
+ try:
+ auto_record = TixianAutoRecord.objects.select_for_update().get(tixian_id=out_bill_no)
+ except TixianAutoRecord.DoesNotExist:
+ # 记录不存在,视为已处理,返回成功
+ return Response({'code': 'SUCCESS', 'message': 'ok'})
+
+ # 如果已经是最新状态,直接返回成功(幂等)
+ if auto_record.zhuangtai != 0:
+ return Response({'code': 'SUCCESS', 'message': 'already processed'})
+
+ if transfer_status == 'SUCCESS':
+ mark_transfer_success(
+ auto_record,
+ wechat_transfer_no=wechat_transfer_no,
+ with_accounting=True,
+ )
+
+ else: # transfer_status == 'FAIL'
+ auto_record.zhuangtai = 2
+ auto_record.fail_reason = fail_reason or '微信转账失败'
+ auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
+ release_collect_quota_for_record(auto_record)
+
+ if not auto_record.shenhe_danhao:
+ logger.error('回调失败但打款记录无审核单号 bill=%s', out_bill_no)
+ else:
+ try:
+ audit = TixianShenheJilu.objects.select_for_update().get(
+ shenhe_danhao=auto_record.shenhe_danhao
+ )
+ close_audit_wechat_failed(
+ audit, None, auto_record.fail_reason,
+ )
+ except TixianShenheJilu.DoesNotExist:
+ logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}')
+
+ return Response({'code': 'SUCCESS', 'message': 'ok'})
+
+
+class TixianQueRenAutoView(APIView):
+ """
+ 用户确认提现结果
+ POST /yonghu/tixianqr
+ 以微信官方查单状态为准:SUCCESS 落库成功;FAIL/CANCELLED 关闭审核单+退款;进行中则提示等待
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ try:
+ tixian_id = request.data.get('tixian_id')
+ result = request.data.get('result') # 1成功 0失败/取消
+
+ if not tixian_id or result is None:
+ return Response({'code': 1, 'msg': '缺少参数'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ result = int(result)
+ except ValueError:
+ return Response({'code': 2, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if result not in [0, 1]:
+ return Response({'code': 3, 'msg': 'result值无效'}, status=status.HTTP_400_BAD_REQUEST)
+
+ user_main = request.user
+
+ with transaction.atomic():
+ try:
+ auto_record = TixianAutoRecord.objects.select_for_update().get(
+ tixian_id=tixian_id,
+ yonghuid=user_main.UserUID,
+ )
+ except TixianAutoRecord.DoesNotExist:
+ return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ if result == 1:
+ if auto_record.zhuangtai == 1:
+ return Response({'code': 0, 'msg': '提现成功', 'data': None})
+
+ wx_data = query_wechat_transfer_bill(tixian_id)
+ if wx_data is None:
+ return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
+
+ if wx_data.get('_not_found'):
+ return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
+
+ state = (wx_data.get('state') or '').upper()
+ if state in WX_STATE_SUCCESS:
+ mark_transfer_success(
+ auto_record,
+ wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'),
+ with_accounting=True,
+ )
+ return Response({'code': 0, 'msg': '提现成功', 'data': None})
+
+ if state in WX_STATE_FAIL:
+ fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or state
+ auto_record.zhuangtai = 2
+ auto_record.fail_reason = str(fail_reason)[:500]
+ auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
+ release_collect_quota_for_record(auto_record)
+ if auto_record.shenhe_danhao:
+ audit = TixianShenheJilu.objects.select_for_update().get(
+ shenhe_danhao=auto_record.shenhe_danhao,
+ )
+ close_audit_wechat_failed(audit, None, auto_record.fail_reason)
+ return Response({'code': 400, 'msg': '微信已确认转账失败,该笔提现已关闭,请联系客服处理'})
+
+ if state in WX_STATE_WAIT or state in WX_STATE_CANCELING:
+ return Response({'code': 12, 'msg': '收款处理中,请在微信中完成或稍后再试'})
+
+ return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
+
+ # 用户报告取消/失败:以微信查单为准,不本地盲目标记
+ if auto_record.zhuangtai == 2:
+ return Response({'code': 0, 'msg': '提现已取消', 'data': None})
+ if auto_record.zhuangtai == 1:
+ return Response({'code': 0, 'msg': '提现成功', 'data': None})
+
+ wx_data = query_wechat_transfer_bill(tixian_id)
+ if wx_data is None:
+ return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试,切勿重复操作'})
+
+ if wx_data.get('_not_found'):
+ return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
+
+ state = (wx_data.get('state') or '').upper()
+ if state in WX_STATE_SUCCESS:
+ mark_transfer_success(
+ auto_record,
+ wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'),
+ with_accounting=True,
+ )
+ return Response({'code': 0, 'msg': '提现成功', 'data': None})
+
+ if state in WX_STATE_FAIL:
+ fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or '用户取消收款'
+ auto_record.zhuangtai = 2
+ auto_record.fail_reason = str(fail_reason)[:500]
+ auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
+ release_collect_quota_for_record(auto_record)
+ if auto_record.shenhe_danhao:
+ audit = TixianShenheJilu.objects.select_for_update().get(
+ shenhe_danhao=auto_record.shenhe_danhao,
+ )
+ close_audit_wechat_failed(audit, None, auto_record.fail_reason)
+ return Response({'code': 0, 'msg': '提现已关闭,请联系客服处理'})
+
+ if state in WX_STATE_WAIT or state in WX_STATE_CANCELING:
+ return Response({'code': 12, 'msg': '收款仍在进行中,请在微信中操作或稍后再试'})
+
+ return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
+
+ except Exception as e:
+ logger.error(f'提现确认异常: {str(e)}', exc_info=True)
+ return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
diff --git a/users/views/user_info.py b/users/views/user_info.py
new file mode 100644
index 0000000..d93c8b2
--- /dev/null
+++ b/users/views/user_info.py
@@ -0,0 +1,1112 @@
+"""users.views.user_info - 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__)
+
+def huoquYonghuDaili(request):
+ """
+ 获取用户浏览器/设备信息
+ """
+ user_agent = request.META.get('HTTP_USER_AGENT', '')
+
+ # 这里可以添加更复杂的用户代理解析
+ # 为简化,我们只返回原始字符串的前100个字符
+ return user_agent[:100] if user_agent else ''
+
+
+def huoquKehuduanIP(request):
+ """
+ 强化版:安全、兼容地获取客户端真实IP地址
+ """
+ ip = None
+
+ # 1. 从标准头部获取IP链
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ # 按逗号分割,取第一个(即最原始的客户端IP)
+ ip = x_forwarded_for.split(',')[0].strip()
+
+ # 2. 如果上面没有,则用Nginx传递的X-Real-IP
+ if not ip:
+ ip = request.META.get('HTTP_X_REAL_IP')
+
+ # 3. 最后降级到连接地址
+ if not ip:
+ ip = request.META.get('REMOTE_ADDR')
+
+ # ===== 核心:清洗和验证IP格式 =====
+ if ip:
+ # 情况A: 处理IPv4映射的IPv6地址 (::ffff:192.168.1.1)
+ if ip.startswith('::ffff:'):
+ ip = ip[7:]
+
+ # 情况B: 处理带端口的IPv4地址 (223.91.110.123:62417)
+ # 这是导致您记录“三段”的最可能原因!
+ # 如果IP字符串中有冒号,且最后一个冒号前的部分是IPv4,则分离端口。
+ if ':' in ip and '.' in ip:
+ # 尝试按冒号分割,取第一部分
+ possible_ip = ip.rsplit(':', 1)[0] # 从右边分割一次
+ # 验证它是否是有效的IPv4地址
+ try:
+ ipaddress.IPv4Address(possible_ip) # 如果通过验证,说明格式正确
+ ip = possible_ip
+ except (ipaddress.AddressValueError, ValueError):
+ # 如果验证失败,说明可能不是IPv4带端口,保持原样或进行其他处理
+ pass
+
+ # 情况C: 验证最终结果是否为有效IPv4(可选,但推荐)
+ try:
+ # 此步骤会抛出异常,如果ip变量此时仍是“223.91.110.”这种格式
+ ipaddress.IPv4Address(ip)
+ except (ipaddress.AddressValueError, ValueError):
+ # 记录一个警告日志,并将IP标记为无效或存为原始字符串
+ print(f"警告:获取到无效或非标准IP地址格式: {ip}")
+ # 您可以选择在此处 return None 或存储原始字符串以供排查
+
+ return ip
+
+
+class UserInfoUpdateView(APIView):
+ permission_classes = [permissions.IsAuthenticated]
+
+ # 进程内缓存,防止同一code被重复调用微信(单进程安全,多进程小概率重复,但用户极少可忽略)
+ _phone_cache = {}
+
+ def post(self, request):
+ try:
+ user = request.user
+ boss = self._get_or_create_boss(user)
+ result = {}
+
+ # 头像
+ av = request.FILES.get('avatar')
+ if av:
+ ok, msg = validate_image(av)
+ if not ok:
+ return Response({'code': 1, 'msg': msg, 'data': None}, status=400)
+ url = self._upload_avatar(user, av)
+ if url:
+ result['touxiang'] = url
+ user.Avatar = url
+ else:
+ return Response({'code': 2, 'msg': '头像上传失败', 'data': None}, status=500)
+
+ # 昵称
+ nick = request.data.get('nicheng')
+ if nick:
+ nick = str(nick).strip()
+ if len(nick) > 20:
+ return Response({'code': 3, 'msg': '昵称过长', 'data': None}, status=400)
+ boss.nickname = nick
+ result['nicheng'] = nick
+
+ # 手机号 —— 极简可靠版
+ code = request.data.get('shoujihao_code')
+ if code:
+ phone = self._get_phone_safe(code, request)
+ if phone is None:
+ return Response({'code': 6, 'msg': '手机号获取失败,请重新授权', 'data': None}, status=400)
+ user.Phone = phone
+ user.PhoneVerified = True
+ result['phone'] = phone
+
+ if not result:
+ return Response({'code': 5, 'msg': '无更新', 'data': None}, status=400)
+
+ with transaction.atomic():
+ user.save()
+ boss.save()
+ return Response({'code': 0, 'msg': '更新成功', 'data': result})
+
+ except Exception as e:
+ logger.exception("系统异常")
+ return Response({'code': 99, 'msg': str(e), 'data': None}, status=500)
+
+ # ---------- 安全手机号(内存去重) ----------
+ def _get_phone_safe(self, code, request):
+ cache_key = f'{code}:{self._resolve_wx_club_id(request, request.user)}'
+ if cache_key in self._phone_cache:
+ cached = self._phone_cache[cache_key]
+ return cached if cached != '__FAIL__' else None
+
+ self._phone_cache[cache_key] = '__PROCESSING__'
+ try:
+ phone = self._call_wechat(code, request)
+ self._phone_cache[cache_key] = phone
+ return phone
+ except Exception as e:
+ logger.error(f"微信调用失败: {str(e)}")
+ self._phone_cache[cache_key] = '__FAIL__'
+ return None
+
+ def _resolve_wx_club_id(self, request, user):
+ """手机号 code 与小程序 appid 绑定,须用对应俱乐部的 wx 配置换 token。"""
+ from jituan.constants import CLUB_ID_DEFAULT
+ from jituan.services.club_context import resolve_club_id_from_request
+ from jituan.services.club_user import get_user_club_id
+
+ club_id = resolve_club_id_from_request(request)
+ if club_id == CLUB_ID_DEFAULT:
+ user_club = get_user_club_id(user)
+ if user_club:
+ return user_club
+ return club_id
+
+ # ---------- 微信接口调用(按俱乐部 appid 换 token) ----------
+ def _call_wechat(self, code, request):
+ club_id = self._resolve_wx_club_id(request, request.user)
+ token = self._get_new_token(club_id, force_refresh=True)
+ url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
+ resp = requests.post(url, json={'code': code}, timeout=10)
+ data = resp.json()
+ errcode = data.get('errcode')
+ if errcode == 0:
+ return data['phone_info']['purePhoneNumber']
+ if errcode == 40001:
+ token = self._get_new_token(club_id, force_refresh=True)
+ url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
+ resp = requests.post(url, json={'code': code}, timeout=10)
+ data = resp.json()
+ if data.get('errcode') == 0:
+ return data['phone_info']['purePhoneNumber']
+ raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}")
+
+ def _get_new_token(self, club_id, force_refresh=False):
+ from utils.weixin_token import get_club_mini_access_token
+
+ token = get_club_mini_access_token(club_id, force_refresh=force_refresh)
+ if token:
+ return token
+ raise Exception(f'俱乐部 {club_id} 缺少微信配置或获取 access_token 失败')
+
+ # ---------- 工具 ----------
+ def _get_or_create_boss(self, user):
+ try:
+ return UserBoss.query.get(user=user)
+ except UserBoss.DoesNotExist:
+ return UserBoss.query.create(user=user, nickname='微信用户')
+
+ def _upload_avatar(self, user, file):
+ try:
+ uid = user.UserUID
+ if not uid: return None
+ old = user.Avatar
+ if old: delete_from_oss(old)
+ ext = os.path.splitext(file.name)[1] or '.jpg'
+ name = f"{uuid.uuid4().hex}{ext}"
+ path = f"avatar/{uid}/{name}"
+ return path if upload_to_oss(file, path) else None
+ except:
+ return None
+
+
+class DashouXinxiAPIView(APIView):
+ """
+ 获取打手信息接口
+ 请求方式:POST
+ 认证方式:JWT Token
+ 返回:打手基本信息、资产信息、会员信息,金牌打手额外返回专属字段
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 获取当前登录用户
+ current_user = request.user
+ yonghuid = current_user.UserUID
+
+ # 2. 查询打手扩展表
+ try:
+ dashou_profile = current_user.DashouProfile
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 400,
+ 'message': '您不是打手身份',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 3. 构建基础信息响应(原有字段完全保留)
+ data = {
+ 'dashounicheng': dashou_profile.nicheng or '',
+ 'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
+ 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
+ 'dashouzhuangtai': dashou_profile.zhuangtai,
+ 'yongjin': str(dashou_profile.yue) if dashou_profile.yue is not None else '0.00',
+ 'zonge': str(dashou_profile.zonge) if dashou_profile.zonge is not None else '0.00',
+ 'yajin': str(dashou_profile.yajin) if dashou_profile.yajin is not None else '0.00',
+ 'jiedanZongliang':dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0',
+ 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00',
+ 'chengjiaoliang': dashou_profile.chengjiaozongliang,
+ 'jifen': dashou_profile.jifen,
+ 'chenghao': dashou_profile.chenghao or '',
+ }
+
+ # 4. 查询会员信息(原有逻辑)
+ huiyuan_list = []
+ try:
+ huiyuan_records = Huiyuangoumai.query.filter(
+ yonghu_id=yonghuid
+ ).select_related()
+
+ for record in huiyuan_records:
+ # 过期检测会同步更新 huiyuan_zhuangtai
+ if record.jiance_shifou_daoqi():
+ continue
+ if record.huiyuan_zhuangtai == 1:
+ huiyuan_list.append({
+ 'huiyuanid': record.huiyuan_id,
+ 'huiyuanming': record.jieshao or f"会员{record.huiyuan_id}",
+ 'daoqi': record.daoqi_time
+ })
+ except Exception as e:
+ # 会员信息查询失败不影响主要功能,返回空列表
+ huiyuan_list = []
+
+ data['clumber'] = huiyuan_list
+
+ # 5. 如果是金牌打手,添加专属字段
+ if dashou_profile.chenghao == "金牌打手":
+ data.update({
+ 'jiedanZongliang': dashou_profile.jiedanzongliang, # 接单总量
+ 'jinriJiedan': str(dashou_profile.jinrishouyi) if dashou_profile.jinrishouyi is not None else '0.00', # 今日接单金额
+ 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', # 最近提现金额(今日已提现)
+ })
+
+ # 6. 返回成功响应
+ return Response({
+ 'code': 200,
+ 'message': '获取成功',
+ 'data': data
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ # 生产环境应使用 logging 记录错误,而不是 print
+ # logger.error(f"获取打手信息失败: {e}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'message': f'服务器内部错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class HuoQuYaoQingRenView(APIView):
+ """
+ 获取当前打手的邀请人信息
+ POST /yonghu/hqwdyqr
+ Headers: Authorization: Bearer
+ 返回邀请人的用户ID、昵称(来自老板扩展表)、头像相对URL
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ current_user = request.user # 当前登录用户
+ yonghuid = current_user.UserUID # 7位用户ID
+
+ # 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态)
+ try:
+ dashou = UserDashou.query.get(user=current_user)
+ if dashou.zhanghaozhuangtai != 1:
+ return Response({
+ 'code': 403,
+ 'message': '您的账号已被封禁,无法查看邀请人',
+ 'data': None
+ })
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'message': '您还不是打手,无法查看邀请人',
+ 'data': None
+ })
+
+ # 2. 获取邀请人ID
+ yaoqingren_id = dashou.yaoqingren
+ if not yaoqingren_id:
+ return Response({
+ 'code': 404,
+ 'message': '您没有邀请人',
+ 'data': None
+ })
+
+ # 3. 查询邀请人主表(头像)
+ try:
+ inviter_main = User.query.get(UserUID=yaoqingren_id)
+ touxiang = inviter_main.Avatar or ''
+ except User.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'message': '邀请人信息不存在',
+ 'data': None
+ })
+
+ # 4. 查询老板扩展表(昵称),如果没有则使用空字符串
+ nicheng = ''
+ try:
+ boss = UserBoss.query.get(user=inviter_main)
+ nicheng = boss.nickname or ''
+ except UserBoss.DoesNotExist:
+ pass # 邀请人可能不是老板,昵称留空
+
+ response_data = {
+ 'uid': yaoqingren_id,
+ 'nicheng': nicheng,
+ 'touxiang': touxiang # 相对路径,前端自行拼接
+ }
+
+ return Response({
+ 'code': 200,
+ 'message': '获取成功',
+ 'data': response_data
+ })
+
+ except Exception as e:
+ logger.error(f'获取邀请人失败: {str(e)}', exc_info=True)
+ return Response({
+ 'code': 500,
+ 'message': '系统繁忙,请稍后重试',
+ 'data': None
+ })
+
+
+class ZaixianZhuangtaiAPIView(APIView):
+ """
+ 更改打手在线状态接口
+ 请求方式:POST
+ 认证方式:JWT Token
+ 请求参数:status(0=离线,1=在线)
+ 返回:更新后的状态信息
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 1. 获取请求参数
+ new_status = request.data.get('status')
+
+ if new_status is None:
+ return Response({
+ 'code': 400,
+ 'message': '缺少状态参数',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 转换为整数,确保是0或1
+ try:
+ new_status = int(new_status)
+ if new_status not in [0, 1]:
+ return Response({
+ 'code': 400,
+ 'message': '状态参数必须是0或1',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+ except ValueError:
+ return Response({
+ 'code': 400,
+ 'message': '状态参数必须是整数',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 2. 获取当前登录用户
+ current_user = request.user
+ yonghuid = current_user.UserUID
+
+
+
+ # 3. 查询打手扩展表
+ try:
+ dashou_profile = current_user.DashouProfile
+ except UserDashou.DoesNotExist:
+
+ return Response({
+ 'code': 400,
+ 'message': '您不是打手身份',
+ 'data': None
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 4. 检查账号状态(是否被封禁)
+ if dashou_profile.zhanghaozhuangtai != 1:
+
+ return Response({
+ 'code': 403,
+ 'message': '账号状态异常,无法更改在线状态',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 5. 更新在线状态
+ old_status = dashou_profile.zaixianzhuangtai
+
+ # 如果状态没有变化,直接返回成功
+ if old_status == new_status:
+
+ return Response({
+ 'code': 200,
+ 'message': '状态未变化',
+ 'data': {
+ 'zaixianzhuangtai': new_status,
+ 'status_text': '在线' if new_status == 1 else '离线'
+ }
+ }, status=status.HTTP_200_OK)
+
+ # 更新状态
+ dashou_profile.zaixianzhuangtai = new_status
+ dashou_profile.save()
+
+
+
+ # 6. 返回成功响应
+ return Response({
+ 'code': 200,
+ 'message': '状态更新成功',
+ 'data': {
+ 'zaixianzhuangtai': new_status,
+ 'status_text': '在线' if new_status == 1 else '离线'
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+
+ return Response({
+ 'code': 500,
+ 'message': f'服务器内部错误: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class GuanshiXinxiView(APIView):
+ """
+ 获取管事信息接口
+ 路径:/yonghu/guanshixinxi
+ 方法:POST
+ 权限:JWT认证
+ 功能:获取当前管事的详细信息,包括基本信息、分红信息以及已充值打手数量
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 获取当前用户(通过JWT认证)
+ current_user = request.user
+
+ # 查询管事扩展表
+ try:
+ guanshi_profile = UserGuanshi.query.get(user=current_user)
+
+ # 🔥【新增】查询已充值打手数量(从分红表中统计)
+ # Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid)
+ yichongzhi_count = Gsfenhong.query.filter(guanshi=current_user.UserUID).count()
+
+ # 返回管事信息
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': {
+ 'gszhstatus': guanshi_profile.zhuangtai, # 管事账号状态
+ 'yaoqingzongshu': guanshi_profile.yaogingshuliang, # 邀请打手总数
+ 'fenyongzonge': float(guanshi_profile.chongzhifenrun), # 充值分佣总额
+ 'fenyongtixian': float(guanshi_profile.yue), # 可提现余额
+ 'yichongzhiDashou': yichongzhi_count, # 已充值打手数量
+ }
+ })
+
+ except UserGuanshi.DoesNotExist:
+ # 用户不是管事,返回错误
+ return Response({
+ 'code': 400,
+ 'msg': '您还不是管事,请先注册',
+ 'data': None
+ }, status=400)
+
+ except Exception as e:
+ # 捕获所有异常,返回错误信息并记录日志
+ logger.error(f"获取管事信息异常: {str(e)}", exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': f'获取信息失败: {str(e)}',
+ 'data': None
+ }, status=500)
+
+
+class ShangJiaXinXiView(APIView):
+ """
+ 获取商家信息接口 (POST /yonghu/shangjiaxinxi)
+ 权限:需JWT认证
+ 返回:商家基本信息 + 称号列表 + 今日/本月统计
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 获取商家扩展资料
+ shangjia = request.user.ShopProfile
+ except ObjectDoesNotExist:
+ return Response({
+ 'code': 4001,
+ 'msg': '您还未注册成为商家或商家资料不存在',
+ 'data': None
+ }, status=status.HTTP_200_OK)
+
+ user = request.user
+ yonghuid = user.UserUID
+ today = timezone.now().date()
+ this_month_start = today.replace(day=1)
+
+ # ------------------- 1. 从 UserShangjia 获取基础字段 -------------------
+ response_data = {
+ 'nicheng': shangjia.nicheng or '',
+ 'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态
+ 'fadanzong': shangjia.fabu, # 发单总量(累计)
+ 'fabu': shangjia.fabu,
+ 'tuikuanzong': shangjia.tuikuan, # 退款总量(累计)
+ 'sjyue': float(shangjia.yue), # 商家余额
+ # 'riliushui' 和 'yueliushui' 将从统计表计算,不再使用原字段
+ }
+
+ # ------------------- 2. 获取称号列表(仅限商家称号) -------------------
+ chenghao_list = []
+ # 查询当前用户的所有称号关联
+ yonghu_chenghao_qs = YonghuChenghao.query.filter(yonghu=user).select_related('chenghao')
+ for yc in yonghu_chenghao_qs:
+ chenghao_obj = yc.chenghao
+ # 只保留类型为“商家”的称号
+ if chenghao_obj.leixing == 'shangjia':
+ chenghao_list.append({
+ 'mingcheng': chenghao_obj.mingcheng,
+ 'texiao_json': chenghao_obj.texiao_miaoshu, # 前端需要的特效描述
+ # 如果还需要其他字段(如图标URL)可在此添加,打手端有 texiao_json
+ })
+ response_data['chenghao_list'] = chenghao_list
+
+ # ------------------- 3. 从 MerchantDailyStats 获取今日统计 -------------------
+ try:
+ today_stat = MerchantDailyStats.query.get(MerchantID=yonghuid, Date=today)
+ jinri_paifa_dingdan = today_stat.AssignedOrderCount
+ jinri_paifa_jine = float(today_stat.AssignedAmount)
+ jinri_tuikuan_dingdan = today_stat.RefundOrderCount
+ except MerchantDailyStats.DoesNotExist:
+ jinri_paifa_dingdan = 0
+ jinri_paifa_jine = 0.00
+ jinri_tuikuan_dingdan = 0
+
+ response_data['jinripaidan'] = jinri_paifa_dingdan # 今日派单数(新增)
+ response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增)
+ response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额)
+
+ # ------------------- 4. 从 MerchantDailyStats 获取本月统计 -------------------
+ month_stats = MerchantDailyStats.query.filter(
+ MerchantID=yonghuid,
+ Date__gte=this_month_start,
+ Date__lte=today
+ ).aggregate(
+ total_paifa_jine=Sum('AssignedAmount'),
+ total_paifa_dingdan=Sum('AssignedOrderCount')
+ )
+ yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00)
+ # yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加
+
+ response_data['yueliushui'] = yue_paifa_jine # 今月流水(派发总金额)
+
+ # (可选)如果前端还需要今月派单总数,取消下面注释
+ # response_data['jinyuepaidan'] = yue_paifa_dingdan
+
+ return Response({
+ 'code': 200,
+ 'msg': '获取商家信息成功',
+ 'data': response_data
+ }, status=status.HTTP_200_OK)
+
+
+class ShangjiaNichengGengxinView(APIView):
+ """商家老板修改昵称 POST /yonghu/shangjiagxnc"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ from merchant_ops.services.authz import is_merchant_owner
+
+ if not is_merchant_owner(request.user):
+ return Response({'code': 403, 'msg': '仅商家老板可修改昵称', 'data': None}, status=status.HTTP_200_OK)
+
+ nicheng = (request.data.get('nicheng') or '').strip()
+ if not nicheng:
+ return Response({'code': 400, 'msg': '昵称不能为空', 'data': None}, status=status.HTTP_200_OK)
+ if len(nicheng) > 20:
+ return Response({'code': 400, 'msg': '昵称不能超过20字', 'data': None}, status=status.HTTP_200_OK)
+
+ try:
+ shangjia = request.user.ShopProfile
+ except ObjectDoesNotExist:
+ return Response({'code': 4001, 'msg': '商家资料不存在', 'data': None}, status=status.HTTP_200_OK)
+
+ if UserShangjia.query.filter(nicheng=nicheng).exclude(user=request.user).exists():
+ return Response({'code': 400, 'msg': '该昵称已被使用', 'data': None}, status=status.HTTP_200_OK)
+
+ shangjia.nicheng = nicheng
+ shangjia.save(update_fields=['nicheng'])
+
+ return Response({'code': 200, 'msg': '昵称已更新', 'data': {'nicheng': nicheng}}, status=status.HTTP_200_OK)
+
+
+class BossBiaoshiView(APIView):
+ """
+ 获取用户的GoEasy标识 - 极简实现
+ POST /yonghu/bossbiaoshi
+ 需要JWT Token认证
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # 1. 直接从request.user获取用户
+ user = request.user
+
+ # 2. 获取yonghuid(确保用户已登录)
+ if not user or not hasattr(user, 'UserUID'):
+ return Response({
+ 'code': 401,
+ 'msg': '用户未认证或用户信息不完整'
+ }, status=401)
+
+ # 3. 生成标识:Boss + yonghuid(与前端约定)
+ boss_biaoshi = f"Boss{user.UserUID}"
+
+ # 4. 返回给前端
+ return Response({
+ 'code': 200,
+ 'msg': '获取成功',
+ 'data': {
+ 'boss_biaoshi': boss_biaoshi,
+ 'yonghuid': user.UserUID,
+ 'user_type': user.UserType
+ }
+ })
+
+
+class YaoqingmaView(APIView):
+ """
+ 获取或生成管事邀请码
+ 访问路径: /api/yonghu/yaoqingma/
+ 请求方法: GET
+ 权限要求: JWT Token认证通过的用户
+ """
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ """
+ 获取当前用户的邀请码
+ 逻辑:
+ 1. 通过JWT获取当前用户 (request.user)
+ 2. 反向查询UserGuanshi扩展表
+ 3. 验证管事状态(zhuangtai=1)
+ 4. 已有邀请码直接返回,没有则生成并保存
+ """
+ # 获取当前认证用户
+ current_user = request.user
+
+ # 1. 使用反向查询获取管事扩展信息 (性能最优方式)
+ try:
+ guanshi_profile = current_user.GuanshiProfile
+ except UserGuanshi.DoesNotExist:
+ # 用户不是管事,没有扩展表记录
+ return Response({
+ 'code': 403,
+ 'message': '当前用户不是管事身份,无法生成邀请码',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 2. 验证管事状态 (zhuangtai 必须等于 1)
+ if guanshi_profile.zhuangtai != 1:
+ return Response({
+ 'code': 403,
+ 'message': '管事账号状态异常,无法生成邀请码',
+ 'data': None
+ }, status=status.HTTP_403_FORBIDDEN)
+
+ # 3. 检查是否已存在邀请码
+ existing_yaoqingma = guanshi_profile.yaoqingma
+
+ # 如果存在且非空,直接返回
+ if existing_yaoqingma and existing_yaoqingma.strip() != '':
+ return Response({
+ 'code': 200,
+ 'message': '成功获取现有邀请码',
+ 'data': {
+ 'yaoqingma': existing_yaoqingma,
+ 'is_new': False # 标记是否为新生成的
+ }
+ })
+
+ # 4. 生成新的邀请码 (使用用户ID字符串)
+ # 注意: 这里使用yonghuid,不是主键id,如你要求的"123456"这种形式
+ yonghuid_str = str(current_user.UserUID)
+ new_yaoqingma = CreateInvitationCode(yonghuid_str)
+
+ # 5. 验证生成的邀请码格式
+ if not VerifyInvitationCode(new_yaoqingma):
+ return Response({
+ 'code': 500,
+ 'message': '邀请码生成失败,格式验证错误',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 6. 保存到数据库 (使用事务确保数据一致性)
+ try:
+ with transaction.atomic():
+ guanshi_profile.yaoqingma = new_yaoqingma
+ guanshi_profile.save()
+ except Exception as e:
+ # 这里可以记录日志
+ return Response({
+ 'code': 500,
+ 'message': f'保存邀请码到数据库失败: {str(e)}',
+ 'data': None
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # 7. 返回新生成的邀请码
+ return Response({
+ 'code': 200,
+ 'message': '成功生成新的邀请码',
+ 'data': {
+ 'yaoqingma': new_yaoqingma,
+ 'is_new': True # 标记是新生成的
+ }
+ })
+
+
+class DashouZiliaoHuoquView(APIView):
+ """
+ 打手资料获取接口
+ URL: /yonghu/dszjxxhq
+ 方法: POST
+ 认证: JWT Token
+ 返回: 打手扩展表信息
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 使用select_related高效查询,避免N+1问题
+ dashou = UserDashou.query.select_related('user').get(user=request.user)
+
+ # 构建返回数据
+ data = {
+ 'nicheng': dashou.nicheng if dashou.nicheng else '',
+ 'jieshao': dashou.jieshao if dashou.jieshao else '',
+ 'dianhua': dashou.dianhua if dashou.dianhua else '',
+ 'wechat': dashou.wechat if dashou.wechat else '',
+ 'uid': request.user.UserUID, # 从主表获取UID
+ 'avatar': request.user.Avatar if request.user.Avatar else '', # 头像相对URL
+ }
+
+ return Response({
+ 'code': 200,
+ 'msg': 'success',
+ 'data': data
+ }, status=status.HTTP_200_OK)
+
+ except UserDashou.DoesNotExist:
+ logger.warning(f'打手扩展表不存在: user_id={request.user.id}')
+ return Response({
+ 'code': 404,
+ 'msg': '打手信息不存在'
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ except Exception as e:
+ logger.error(f'获取打手资料异常: {str(e)}', exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': '服务器内部错误'
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class DashouGengxinView(APIView):
+ """
+ 打手信息更新接口
+ URL: /yonghu/dsgxxx
+ 方法: POST
+ 认证: JWT Token
+ 参数: nicheng(昵称), dianhua(电话), wechat(微信), jieshao(介绍)
+ """
+
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ try:
+ # 获取请求数据
+ nicheng = request.data.get('nicheng', '').strip()
+ dianhua = request.data.get('dianhua', '').strip()
+ wechat = request.data.get('wechat', '').strip()
+ jieshao = request.data.get('jieshao', '').strip()
+
+ # 数据验证
+ validation_errors = self.validate_data(nicheng, dianhua, wechat, jieshao)
+ if validation_errors:
+ return Response({
+ 'code': 400,
+ 'msg': validation_errors
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取打手扩展表
+ try:
+ dashou = UserDashou.query.get(user=request.user)
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 404,
+ 'msg': '打手信息不存在'
+ }, status=status.HTTP_404_NOT_FOUND)
+
+ # 更新数据
+ update_fields = []
+
+ if nicheng is not None:
+ dashou.nicheng = nicheng if nicheng != '' else None
+ update_fields.append('nicheng')
+
+ if dianhua is not None:
+ dashou.dianhua = dianhua if dianhua != '' else None
+ update_fields.append('dianhua')
+
+ if wechat is not None:
+ dashou.wechat = wechat if wechat != '' else None
+ update_fields.append('wechat')
+
+ if jieshao is not None:
+ dashou.jieshao = jieshao if jieshao != '' else None
+ update_fields.append('jieshao')
+
+ # 保存更新(只更新修改的字段)
+ if update_fields:
+ dashou.save(update_fields=update_fields)
+
+ return Response({
+ 'code': 200,
+ 'msg': '更新成功',
+ 'data': {
+ 'nicheng': dashou.nicheng or '',
+ 'dianhua': dashou.dianhua or '',
+ 'wechat': dashou.wechat or '',
+ 'jieshao': dashou.jieshao or ''
+ }
+ }, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ logger.error(f'更新打手信息异常: {str(e)}', exc_info=True)
+ return Response({
+ 'code': 500,
+ 'msg': '服务器内部错误'
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def validate_data(self, nicheng, dianhua, wechat, jieshao):
+ """数据验证方法"""
+ errors = []
+
+ # 1. 昵称验证(必填)
+ if not nicheng:
+ errors.append('昵称不能为空')
+ elif len(nicheng) > 20:
+ errors.append('昵称不能超过20个字符')
+
+ # 2. 手机号验证(非必填,但填写时必须正确)
+ if dianhua:
+ # 手机号正则:1开头,11位数字
+ phone_pattern = r'^1[3-9]\d{9}$'
+ if not re.match(phone_pattern, dianhua):
+ errors.append('手机号格式不正确')
+
+ # 3. 微信号验证(非必填)
+ if wechat and len(wechat) > 30:
+ errors.append('微信号不能超过30个字符')
+
+ # 4. 个人介绍验证
+ if jieshao and len(jieshao) > 200:
+ errors.append('个人介绍不能超过200个字符')
+
+ return '; '.join(errors) if errors else None
+
+
+class DashouJianquanView(APIView):
+ """打手消息权限鉴权"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # ========== 1. 通过 JWT 获取当前登录用户 ==========
+ user = request.user # User 实例
+
+ # ========== 2. 检查是否为已注册打手 ==========
+ try:
+ dashou = user.DashouProfile # OneToOneField 反向查询
+ except UserDashou.DoesNotExist:
+ return Response({
+ 'code': 0,
+ 'allow': 0,
+ 'msg': '您尚未注册打手身份,请先注册'
+ })
+
+ # ========== 3. 条件一:押金 >= 5 元 ==========
+ if dashou.yajin and dashou.yajin >= 5:
+ return Response({
+ 'code': 0,
+ 'allow': 1,
+ 'msg': '鉴权通过(押金满足条件)'
+ })
+
+ # ========== 4. 条件二:存在未过期会员 ==========
+ # 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引
+ huiyuan_list = Huiyuangoumai.query.filter(
+ yonghu_id=user.UserUID,
+ huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历
+ ).only('id', 'daoqi_time', 'huiyuan_zhuangtai')
+
+ for hy in huiyuan_list:
+ if not hy.jiance_shifou_daoqi(): # 未过期返回 False
+ return Response({
+ 'code': 0,
+ 'allow': 1,
+ 'msg': '鉴权通过(会员未过期)'
+ })
+
+ # ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ==========
+ # 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条
+ has_valid_order = Order.query.filter(
+ PlayerID=user.UserUID,
+ Status__in=[2, 8]
+ ).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1
+
+ if has_valid_order:
+ return Response({
+ 'code': 0,
+ 'allow': 1,
+ 'msg': '鉴权通过(存在有效订单)'
+ })
+
+ # ========== 6. 以上均不满足 → 拒绝 ==========
+ return Response({
+ 'code': 0,
+ 'allow': 0,
+ 'msg': '消息权限不足,请充值会员或联系管理员'
+ })
+
+
+class LaobanJianquanView(APIView):
+ """老板消息权限鉴权"""
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ # ========== 1. 通过 JWT 获取当前登录用户 ==========
+ user = request.user # User 实例
+
+ # ========== 2. 高效查询平台订单扩展表 ==========
+ # BossID 为模型字段名(db_column=laoban_id);订单状态用 Order.Status
+ has_valid_order = PlatformOrderExt.query.filter(
+ BossID=user.UserUID
+ ).filter(
+ Order__Status__in=[1, 2, 3, 4, 5, 6, 7, 8]
+ ).only('id').exists()
+
+ if has_valid_order:
+ return Response({
+ 'code': 0,
+ 'allow': 1,
+ 'msg': '鉴权通过'
+ })
+
+ return Response({
+ 'code': 0,
+ 'allow': 0,
+ 'msg': '您暂无订单记录,消息功能暂不可用'
+ })