From 9d37f59971128101cb80516fce35edd6175e7945 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 5 Jul 2026 10:01:09 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E4=BA=86=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E7=9A=84=E8=A7=86=E5=9B=BE=E6=8B=86=E5=88=86=EF=BC=8C=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E4=BA=86=20merchant=5Fops=20=E7=9A=84=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _init_.py | 2 - backend/views/exams.py | 98 +- merchant_ops/services/authz.py | 30 +- merchant_ops/views_staff.py | 2 +- orders/views.py | 7101 -------------- orders/views/__init__.py | 87 + orders/views/admin.py | 1958 ++++ orders/views/boss_orders.py | 821 ++ orders/views/chat.py | 541 ++ orders/views/dashou.py | 1513 +++ orders/views/merchant.py | 1408 +++ orders/views/merchant_penalty.py | 378 + orders/views/payment.py | 1009 ++ products/views.py | 6105 ------------ products/views/__init__.py | 108 + products/views/chongzhi_payment.py | 1105 +++ products/views/huiyuan_payment.py | 677 ++ products/views/jifen_payment.py | 607 ++ products/views/product_query.py | 971 ++ products/views/shangpin_admin.py | 2544 +++++ products/views/yajin_payment.py | 613 ++ rank/views.py | 957 -- rank/views/__init__.py | 30 + rank/views/biaoqian.py | 179 + rank/views/kaohe_jilu.py | 243 + rank/views/kaoheguan.py | 186 + rank/views/shenhe.py | 401 + rename_columns.sql | 112 - shop/views.py | 3124 ------- shop/views/__init__.py | 51 + shop/views/shop_base_views.py | 771 ++ shop/views/shop_member_views.py | 375 + shop/views/shop_order_views.py | 531 ++ shop/views/shop_product_views.py | 1012 ++ shop/views/shop_refund_views.py | 660 ++ users/views.py | 13458 --------------------------- users/views/__init__.py | 128 + users/views/admin.py | 2890 ++++++ users/views/auth.py | 1936 ++++ users/views/chufa.py | 830 ++ users/views/kefu.py | 3794 ++++++++ users/views/pay.py | 1073 +++ users/views/shangjia.py | 734 ++ users/views/tixian.py | 1418 +++ users/views/user_info.py | 1112 +++ 45 files changed, 32768 insertions(+), 30915 deletions(-) delete mode 100644 _init_.py delete mode 100644 orders/views.py create mode 100644 orders/views/__init__.py create mode 100644 orders/views/admin.py create mode 100644 orders/views/boss_orders.py create mode 100644 orders/views/chat.py create mode 100644 orders/views/dashou.py create mode 100644 orders/views/merchant.py create mode 100644 orders/views/merchant_penalty.py create mode 100644 orders/views/payment.py delete mode 100644 products/views.py create mode 100644 products/views/__init__.py create mode 100644 products/views/chongzhi_payment.py create mode 100644 products/views/huiyuan_payment.py create mode 100644 products/views/jifen_payment.py create mode 100644 products/views/product_query.py create mode 100644 products/views/shangpin_admin.py create mode 100644 products/views/yajin_payment.py delete mode 100644 rank/views.py create mode 100644 rank/views/__init__.py create mode 100644 rank/views/biaoqian.py create mode 100644 rank/views/kaohe_jilu.py create mode 100644 rank/views/kaoheguan.py create mode 100644 rank/views/shenhe.py delete mode 100644 rename_columns.sql delete mode 100644 shop/views.py create mode 100644 shop/views/__init__.py create mode 100644 shop/views/shop_base_views.py create mode 100644 shop/views/shop_member_views.py create mode 100644 shop/views/shop_order_views.py create mode 100644 shop/views/shop_product_views.py create mode 100644 shop/views/shop_refund_views.py delete mode 100644 users/views.py create mode 100644 users/views/__init__.py create mode 100644 users/views/admin.py create mode 100644 users/views/auth.py create mode 100644 users/views/chufa.py create mode 100644 users/views/kefu.py create mode 100644 users/views/pay.py create mode 100644 users/views/shangjia.py create mode 100644 users/views/tixian.py create mode 100644 users/views/user_info.py 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}') - 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}") - 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}") + 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}') + 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}') - 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}') + 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}') - 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}') + 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': '您暂无订单记录,消息功能暂不可用' + })