Files
Django/orders/views/chat.py

542 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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})