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