"""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)