Files
along_django/utils/chat_utils.py

944 lines
37 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.
# utils/chat_utils.py
import json
import logging
import time
import requests
from django.conf import settings
from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Liaotian
from peizhi.models import ClubConfig
from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia
logger = logging.getLogger('chat_utils')
ORDER_CARD_PREFIX = '[ORDER_CARD]'
def make_dashou_shangjia_group_id(dashou_yonghuid, shangjia_yonghuid):
return f"group_Ds{dashou_yonghuid}_Sj{shangjia_yonghuid}"
def make_dashou_boss_group_id(dashou_yonghuid, boss_yonghuid):
return f"group_Ds{dashou_yonghuid}_Boss{boss_yonghuid}"
def _get_shangjia_id_from_order(order):
try:
return order.shangjia_kuozhan.shangjia_id
except Exception:
ext = DingdanShangjia.objects.filter(dingdan__dingdan_id=order.dingdan_id).first()
return ext.shangjia_id if ext else None
def _get_boss_id_from_order(order):
try:
return order.pingtai_kuozhan.laoban_id
except Exception:
ext = DingdanPingtai.objects.filter(dingdan__dingdan_id=order.dingdan_id).first()
return ext.laoban_id if ext else None
def resolve_group_id_for_order(order, dashou_yonghuid=None):
"""打手+商家/老板配对群 ID跨平台或缺信息时回退订单群 ID"""
dashou_yonghuid = dashou_yonghuid or order.jiedan_dashou_id
if order.is_cross == 1 and order.dispatch_type == 2:
return f"group_{order.dingdan_id}"
if not dashou_yonghuid:
return f"group_{order.dingdan_id}"
if order.fadan_pingtai == 2:
shangjia_id = _get_shangjia_id_from_order(order)
if shangjia_id:
return make_dashou_shangjia_group_id(dashou_yonghuid, shangjia_id)
elif order.fadan_pingtai == 1:
boss_id = _get_boss_id_from_order(order)
if boss_id:
return make_dashou_boss_group_id(dashou_yonghuid, boss_id)
return f"group_{order.dingdan_id}"
def _build_order_card_text(order):
payload = {
'dingdan_id': order.dingdan_id,
'jieshao': order.jieshao or '',
'beizhu': order.beizhu or '',
'jine': str(order.jine),
'zhuangtai': order.zhuangtai,
'nicheng': order.nicheng or '',
}
if order.create_time:
payload['create_time'] = order.create_time.strftime('%Y-%m-%d %H:%M:%S')
return ORDER_CARD_PREFIX + json.dumps(payload, ensure_ascii=False)
# ========== 辅助配置获取(从 ClubConfig ==========
def _get_self_club_config():
if not hasattr(_get_self_club_config, '_config'):
try:
_get_self_club_config._config = ClubConfig.objects.filter(is_self=1).first()
except Exception as e:
logger.warning(f"获取我方俱乐部配置失败: {e}")
_get_self_club_config._config = None
return _get_self_club_config._config
def _get_self_goeasy_appkey():
cfg = _get_self_club_config()
if cfg and cfg.chat_api_key:
return cfg.chat_api_key
return getattr(settings, 'GOEASY_APPKEY', '')
def _get_self_goeasy_secret():
cfg = _get_self_club_config()
if cfg and cfg.chat_api_id:
return cfg.chat_api_id
return getattr(settings, 'GOEASY_SECRET', '')
def _get_self_storage_domain():
cfg = _get_self_club_config()
if cfg and cfg.storage_bucket_domain:
return cfg.storage_bucket_domain
return ''
def _get_self_default_avatar():
cfg = _get_self_club_config()
if cfg and cfg.club_avatar:
return cfg.club_avatar
return ''
# ========== 头像拼接 ==========
def _full_local_avatar(relative_url):
storage_domain = _get_self_storage_domain()
default_avatar = _get_self_default_avatar()
if not relative_url:
return default_avatar or ''
if relative_url.startswith('http'):
return relative_url
if storage_domain.endswith('/'):
return storage_domain + relative_url.lstrip('/')
return storage_domain + '/' + relative_url if storage_domain else relative_url
def _full_remote_avatar(storage_bucket_domain, relative_url):
if not relative_url:
return ''
if relative_url.startswith('http'):
return relative_url
if storage_bucket_domain.endswith('/'):
return storage_bucket_domain + relative_url.lstrip('/')
return storage_bucket_domain + '/' + relative_url if storage_bucket_domain else relative_url
# ========== GoEasy 订阅 ==========
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:
resp = requests.post(url, headers=headers, json=body, timeout=10)
if resp.status_code == 200:
return True
logger.error(f"订阅失败: {resp.status_code} {resp.text}")
return False
except Exception as e:
logger.error(f"订阅异常: {e}", exc_info=True)
return False
def _goeasy_to_yonghuid(goeasy_id):
if not goeasy_id:
return ''
if goeasy_id.startswith('Boss'):
return goeasy_id[4:]
if goeasy_id.startswith('Ds') or goeasy_id.startswith('Sj'):
return goeasy_id[2:]
return goeasy_id
def _ensure_avatar_url(avatar_url):
"""头像为空时返回俱乐部默认头像(完整 URL"""
return avatar_url or _full_local_avatar('')
def _build_pair_group_to_data(dashou_goeasy_id, dashou_name, dashou_avatar,
partner_goeasy_id, partner_name, partner_avatar,
order_id=None, order_desc=None, is_cross=0,
viewer_goeasy_id=None, order_zuangtai=None):
"""群会话 to.data同时写入打手/对方双方 ID、昵称、头像前端按当前身份取对方展示"""
dashou_avatar = _ensure_avatar_url(dashou_avatar)
partner_avatar = _ensure_avatar_url(partner_avatar)
dashou_uid = _goeasy_to_yonghuid(dashou_goeasy_id)
partner_uid = _goeasy_to_yonghuid(partner_goeasy_id)
dashou_name = dashou_name or f'打手{dashou_uid}'
partner_name = partner_name or f'用户{partner_uid}'
to_data = {
'dashouGoEasyId': dashou_goeasy_id,
'partnerGoEasyId': partner_goeasy_id,
'dashouYonghuid': dashou_uid,
'partnerYonghuid': partner_uid,
'dashouName': dashou_name,
'partnerName': partner_name,
'dashouAvatar': dashou_avatar,
'partnerAvatar': partner_avatar,
'isCross': is_cross,
}
if order_id:
to_data['orderId'] = order_id
if order_desc:
to_data['orderDesc'] = order_desc[:300]
if order_zuangtai is not None:
to_data['orderZhuangtai'] = order_zuangtai
if viewer_goeasy_id == dashou_goeasy_id:
to_data['name'] = partner_name
to_data['avatar'] = partner_avatar
elif viewer_goeasy_id == partner_goeasy_id:
to_data['name'] = dashou_name
to_data['avatar'] = dashou_avatar
else:
to_data['name'] = partner_name
to_data['avatar'] = partner_avatar
return to_data
def build_pair_to_data_for_order(order, viewer_goeasy_id=None):
dashou_uid = order.jiedan_dashou_id
if not dashou_uid:
return None
dashou_goeasy_id = f'Ds{dashou_uid}'
dashou_name, dashou_avatar = _resolve_goeasy_user_display(dashou_goeasy_id)
partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order)
if not partner_goeasy_id:
return None
order_desc = (order.jieshao or '').strip()
if order.beizhu:
order_desc = f"{order_desc} | 备注:{order.beizhu}" if order_desc else f"备注:{order.beizhu}"
return _build_pair_group_to_data(
dashou_goeasy_id, dashou_name, dashou_avatar,
partner_goeasy_id, partner_name, partner_avatar,
order_id=order.dingdan_id, order_desc=order_desc,
is_cross=order.is_cross or 0, viewer_goeasy_id=viewer_goeasy_id,
order_zuangtai=order.zhuangtai,
)
# ========== 发送群消息(完整参数) ==========
def _send_group_message(appkey, secret, group_id, sender_id, sender_name, sender_avatar,
message_text, custom_payload=None, group_name=None, order_id=None, is_cross=0,
list_avatar=None, order_desc=None, to_data_extra=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/message'
sender_avatar = _ensure_avatar_url(sender_avatar)
# to.data优先使用配对群完整元数据双方头像/昵称)
if to_data_extra:
to_data = dict(to_data_extra)
if order_id and 'orderId' not in to_data:
to_data['orderId'] = order_id
if order_desc and 'orderDesc' not in to_data:
to_data['orderDesc'] = order_desc[:300]
else:
to_data = {
"name": group_name or group_id,
"avatar": _ensure_avatar_url(list_avatar or sender_avatar),
}
if order_id:
to_data["orderId"] = order_id
if order_desc:
to_data["orderDesc"] = order_desc[:300]
to_data["isCross"] = is_cross
payload_str = custom_payload.get("text", message_text) if custom_payload else message_text
request_body = {
"appkey": appkey,
"senderId": sender_id,
"senderData": {"avatar": sender_avatar, "name": sender_name or sender_id},
"to": {
"type": "group",
"id": group_id,
"data": to_data
},
"type": "text",
"payload": payload_str
}
headers = {"Content-Type": "application/json"}
if secret:
headers["Authorization"] = f"Bearer {secret}"
try:
resp = requests.post(url, headers=headers, json=request_body, timeout=10)
if resp.status_code == 200:
return True
logger.error(f"群聊消息发送失败,状态码:{resp.status_code},响应:{resp.text}")
return False
except Exception as e:
logger.error(f"发送群聊消息异常: {e}", exc_info=True)
return False
def _retry_call(func, max_retries=5, delay=0.4, *args, **kwargs):
"""对 GoEasy 等外部调用做短重试,提高抢单后建聊成功率"""
for attempt in range(1, max_retries + 1):
try:
if func(*args, **kwargs):
return True
except Exception as e:
logger.warning(f"{func.__name__}{attempt}次失败: {e}")
if attempt < max_retries:
time.sleep(delay * attempt)
return False
def _send_private_init_message(appkey, secret, sender_id, sender_name, sender_avatar,
receiver_id, receiver_name, receiver_avatar, message_text):
if not appkey:
appkey = _get_self_goeasy_appkey()
if not secret:
secret = _get_self_goeasy_secret()
if not appkey:
return False
url = 'https://rest-hangzhou.goeasy.io/v2/im/message'
request_body = {
"appkey": appkey,
"senderId": sender_id,
"senderData": {"avatar": sender_avatar or "", "name": sender_name or sender_id},
"to": {
"type": "private",
"id": receiver_id,
"data": {"avatar": receiver_avatar or "", "name": receiver_name or receiver_id}
},
"type": "text",
"payload": message_text
}
headers = {"Content-Type": "application/json"}
if secret:
headers["Authorization"] = f"Bearer {secret}"
try:
resp = requests.post(url, headers=headers, json=request_body, timeout=10)
if resp.status_code == 200:
return True
logger.error(f"私聊初始化消息失败: {resp.status_code} {resp.text}")
return False
except Exception as e:
logger.error(f"私聊初始化消息异常: {e}", exc_info=True)
return False
def _ensure_liaotian_record(dingdan_id, partner_goeasy_id, dashou_goeasy_id):
try:
Liaotian.objects.update_or_create(
dingdan_id=dingdan_id,
defaults={
'user1_id': partner_goeasy_id,
'user2_id': dashou_goeasy_id,
'zhuangtai': 1,
}
)
return True
except Exception as e:
logger.error(f"写入 Liaotian 记录失败 order={dingdan_id}: {e}", exc_info=True)
return False
def _establish_private_chat(appkey, secret, dashou_goeasy_id, dashou_name, dashou_avatar,
partner_goeasy_id, partner_name, partner_avatar):
msg_dashou = "订单已接单,可在此处沟通。"
msg_partner = "打手已接单,可在此处沟通。"
ok1 = _retry_call(
_send_private_init_message, 3, 0.4,
appkey, secret, dashou_goeasy_id, dashou_name, dashou_avatar,
partner_goeasy_id, partner_name, partner_avatar, msg_dashou
)
ok2 = _retry_call(
_send_private_init_message, 3, 0.4,
appkey, secret, partner_goeasy_id, partner_name, partner_avatar,
dashou_goeasy_id, dashou_name, dashou_avatar, msg_partner
)
return ok1 or ok2
def _resolve_goeasy_user_display(goeasy_id):
if not goeasy_id:
return "用户", _full_local_avatar('')
avatar = _full_local_avatar('')
name = f"用户{goeasy_id[-6:]}"
try:
if goeasy_id.startswith('Boss'):
uid = goeasy_id[4:]
boss_user = UserMain.objects.get(yonghuid=uid)
avatar = _full_local_avatar(boss_user.avatar or '')
boss_profile = UserBoss.objects.get(user=boss_user)
if boss_profile.nickname:
name = boss_profile.nickname
elif goeasy_id.startswith('Sj'):
uid = goeasy_id[2:]
sj_user = UserMain.objects.get(yonghuid=uid)
avatar = _full_local_avatar(sj_user.avatar or '')
try:
sj_profile = UserShangjia.objects.get(user=sj_user)
if sj_profile.nicheng:
name = sj_profile.nicheng
except Exception:
pass
elif goeasy_id.startswith('Ds'):
uid = goeasy_id[2:]
ds_user = UserMain.objects.get(yonghuid=uid)
avatar = _full_local_avatar(ds_user.avatar or '')
ds_profile = UserDashou.objects.get(user=ds_user)
if ds_profile.nicheng:
name = ds_profile.nicheng
except Exception as e:
logger.warning(f"解析 GoEasy 用户展示信息失败 {goeasy_id}: {e}")
return name, avatar
# ========== 核心入口 ==========
def establish_order_chat(dingdan_id):
try:
order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
logger.error(f"订单 {dingdan_id} 不存在")
return False
dashou_uid = order.jiedan_dashou_id
dashou_goeasy_id = f"Ds{dashou_uid}"
dashou_nickname = f'打手{dashou_uid[:6]}'
dashou_avatar_relative = ''
try:
dashou_user = UserMain.objects.get(yonghuid=dashou_uid)
dashou_avatar_relative = dashou_user.avatar or ''
dashou_profile = UserDashou.objects.get(user=dashou_user)
if dashou_profile.nicheng:
dashou_nickname = dashou_profile.nicheng
except Exception as e:
logger.warning(f"获取打手信息失败: {e}")
dashou_avatar = _full_local_avatar(dashou_avatar_relative)
appkey = _get_self_goeasy_appkey()
secret = _get_self_goeasy_secret()
if not appkey:
logger.error("GoEasy AppKey 未配置")
return False
# 判断是否跨平台
if order.is_cross != 1:
return _handle_local_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret)
else:
if order.dispatch_type == 1:
# 我方派单,与普通订单处理一致
return _handle_local_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret)
elif order.dispatch_type == 2:
return _handle_cross_partner_order(order, dashou_goeasy_id, dashou_nickname, dashou_avatar, appkey, secret)
else:
return False
def establish_order_chat_with_retry(dingdan_id, max_rounds=3):
"""抢单后建聊入口:多轮重试,尽量保证建立成功"""
for round_num in range(1, max_rounds + 1):
try:
if establish_order_chat(dingdan_id):
logger.info(f"订单 {dingdan_id} 建聊成功(第{round_num}轮)")
return True
except Exception as e:
logger.error(f"订单 {dingdan_id} 建聊第{round_num}轮异常: {e}", exc_info=True)
if round_num < max_rounds:
time.sleep(0.8 * round_num)
logger.error(f"订单 {dingdan_id} 建聊最终失败")
return False
# ========== 普通订单 / 我方派单 ==========
def _push_order_card_message(appkey, secret, order, group_id, sender_id, sender_name, sender_avatar,
to_data_extra):
card_text = _build_order_card_text(order)
return _send_group_message(
appkey, secret, group_id, sender_id, sender_name, sender_avatar,
card_text, None, order_id=order.dingdan_id,
is_cross=order.is_cross or 0, to_data_extra=to_data_extra,
)
def prepare_order_group_chat(dingdan_id, caller_goeasy_id, push_order_card=False):
"""
订单详情进入聊天前:确保配对群已订阅,可选推送订单卡片。
返回前端跳转所需字段 dict失败返回 None。
"""
try:
order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
logger.error(f"prepare_order_group_chat: 订单 {dingdan_id} 不存在")
return None
dashou_uid = order.jiedan_dashou_id
if not dashou_uid:
logger.error(f"prepare_order_group_chat: 订单 {dingdan_id} 无接单打手")
return None
dashou_goeasy_id = f"Ds{dashou_uid}"
partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order)
if not partner_goeasy_id:
logger.error(f"prepare_order_group_chat: 订单 {dingdan_id} 无下单方")
return None
group_id = resolve_group_id_for_order(order, dashou_uid)
dashou_name, dashou_avatar = _resolve_goeasy_user_display(dashou_goeasy_id)
if caller_goeasy_id == dashou_goeasy_id:
counterpart_id = partner_goeasy_id
counterpart_name = partner_name
counterpart_avatar = partner_avatar
elif caller_goeasy_id == partner_goeasy_id:
counterpart_id = dashou_goeasy_id
counterpart_name = dashou_name
counterpart_avatar = dashou_avatar
else:
logger.warning(f"prepare_order_group_chat: caller {caller_goeasy_id} 不属于订单 {dingdan_id}")
return None
appkey = _get_self_goeasy_appkey()
secret = _get_self_goeasy_secret()
if not appkey:
return None
order_desc = (order.jieshao or '').strip()
if order.beizhu:
order_desc = f"{order_desc} | 备注:{order.beizhu}" if order_desc else f"备注:{order.beizhu}"
display_group_name = counterpart_name or '聊天'
pair_to_data = _build_pair_group_to_data(
dashou_goeasy_id, dashou_name, dashou_avatar,
partner_goeasy_id, partner_name, partner_avatar,
order_id=order.dingdan_id, order_desc=order_desc,
is_cross=1 if order.is_cross == 1 else 0,
viewer_goeasy_id=caller_goeasy_id,
)
user_ids = list({dashou_goeasy_id, partner_goeasy_id, caller_goeasy_id})
subscribe_ok = _retry_call(
_subscribe_users_to_group, 5, 0.4,
user_ids, [group_id], appkey, secret
)
card_ok = False
liaotian_exists = Liaotian.objects.filter(dingdan_id=order.dingdan_id).exists()
if push_order_card and not liaotian_exists:
caller_name, caller_avatar = _resolve_goeasy_user_display(caller_goeasy_id)
card_ok = _retry_call(
_push_order_card_message, 3, 0.4,
appkey, secret, order, group_id, caller_goeasy_id, caller_name, caller_avatar,
pair_to_data,
)
_ensure_liaotian_record(order.dingdan_id, partner_goeasy_id, dashou_goeasy_id)
return {
'groupId': group_id,
'orderId': order.dingdan_id,
'groupName': display_group_name,
'groupAvatar': counterpart_avatar or _full_local_avatar(''),
'counterpartId': counterpart_id,
'counterpartName': counterpart_name,
'counterpartAvatar': counterpart_avatar or _full_local_avatar(''),
'counterpartYonghuid': _goeasy_to_yonghuid(counterpart_id),
'dashouGoEasyId': dashou_goeasy_id,
'partnerGoEasyId': partner_goeasy_id,
'dashouYonghuid': dashou_uid,
'partnerYonghuid': _goeasy_to_yonghuid(partner_goeasy_id),
'dashouName': dashou_name,
'partnerName': partner_name,
'dashouAvatar': dashou_avatar or _full_local_avatar(''),
'partnerAvatar': partner_avatar or _full_local_avatar(''),
'orderDesc': order_desc,
'orderJine': str(order.jine),
'orderZhuangtai': order.zhuangtai,
'isCross': 1 if order.is_cross == 1 else 0,
'subscribeOk': subscribe_ok,
'orderCardPushed': card_ok,
}
def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret):
dashou_uid = dashou_goeasy_id[2:] if dashou_goeasy_id.startswith('Ds') else dashou_goeasy_id
group_id = resolve_group_id_for_order(order, dashou_uid)
partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order)
group_name = partner_name or f"用户{partner_goeasy_id[-6:] if partner_goeasy_id else ''}"
order_desc = (order.jieshao or '').strip()
if order.beizhu:
order_desc = f"{order_desc} | 备注:{order.beizhu}" if order_desc else f"备注:{order.beizhu}"
if not partner_goeasy_id:
logger.error(f"订单 {order.dingdan_id} 无法获取下单方 GoEasy 标识")
subscribe_ok = _retry_call(
_subscribe_users_to_group, 5, 0.4,
[dashou_goeasy_id], [group_id], appkey, secret
)
fallback_to_data = {
'name': dashou_name,
'avatar': _ensure_avatar_url(dashou_avatar),
'dashouGoEasyId': dashou_goeasy_id,
'dashouYonghuid': dashou_uid,
'dashouName': dashou_name,
'dashouAvatar': _ensure_avatar_url(dashou_avatar),
'orderId': order.dingdan_id,
'orderDesc': order_desc,
'isCross': order.is_cross or 0,
}
msg_ok = _retry_call(
_send_group_message, 3, 0.4,
appkey, secret, group_id, dashou_goeasy_id, dashou_name, dashou_avatar,
"订单已接单,请等待对方上线。", None,
order_id=order.dingdan_id, is_cross=order.is_cross,
to_data_extra=fallback_to_data,
)
return subscribe_ok or msg_ok
pair_to_data = _build_pair_group_to_data(
dashou_goeasy_id, dashou_name, dashou_avatar,
partner_goeasy_id, partner_name, partner_avatar,
order_id=order.dingdan_id, order_desc=order_desc,
is_cross=order.is_cross or 0,
)
subscribe_ok = _retry_call(
_subscribe_users_to_group, 5, 0.4,
[dashou_goeasy_id, partner_goeasy_id], [group_id], appkey, secret
)
init_msg_text = f"订单已接单,内容:{order.jieshao},备注:{order.beizhu}游戏ID{order.nicheng}"
success1 = _retry_call(
_send_group_message, 3, 0.4,
appkey, secret, group_id, dashou_goeasy_id, dashou_name, dashou_avatar,
init_msg_text, None, order_id=order.dingdan_id,
is_cross=order.is_cross, to_data_extra=pair_to_data,
)
partner_msg = "订单已确认,请开始服务。"
success2 = _retry_call(
_send_group_message, 3, 0.4,
appkey, secret, group_id, partner_goeasy_id, partner_name, partner_avatar,
partner_msg, None, order_id=order.dingdan_id,
is_cross=order.is_cross, to_data_extra=pair_to_data,
)
card_ok = _retry_call(
_push_order_card_message, 3, 0.4,
appkey, secret, order, group_id, dashou_goeasy_id, dashou_name, dashou_avatar,
pair_to_data,
)
liaotian_ok = _ensure_liaotian_record(order.dingdan_id, partner_goeasy_id, dashou_goeasy_id)
private_ok = _establish_private_chat(
appkey, secret, dashou_goeasy_id, dashou_name, dashou_avatar,
partner_goeasy_id, partner_name, partner_avatar
)
result = subscribe_ok or success1 or success2 or card_ok or liaotian_ok or private_ok
if not result:
logger.error(f"订单 {order.dingdan_id} 群聊/私聊均未建立成功")
return result
def _get_local_partner_info(order):
"""本地订单下单方:老板或商家"""
if order.fadan_pingtai == 1:
try:
ext = order.pingtai_kuozhan
laoban_id = ext.laoban_id
except Exception:
ext = DingdanPingtai.objects.filter(dingdan__dingdan_id=order.dingdan_id).first()
laoban_id = ext.laoban_id if ext else None
if laoban_id:
avatar_full = _full_local_avatar('')
nickname = f'老板{laoban_id[:6]}'
try:
boss_user = UserMain.objects.get(yonghuid=laoban_id)
avatar_full = _full_local_avatar(boss_user.avatar or '')
boss_profile = UserBoss.objects.get(user=boss_user)
if boss_profile.nickname:
nickname = boss_profile.nickname
except Exception:
pass
return f"Boss{laoban_id}", nickname, avatar_full
elif order.fadan_pingtai == 2:
try:
ext = order.shangjia_kuozhan
shangjia_id = ext.shangjia_id
nickname = ext.sjnicheng or f'商家{shangjia_id[:6]}'
except Exception:
ext = DingdanShangjia.objects.filter(dingdan__dingdan_id=order.dingdan_id).first()
if not ext:
shangjia_id = None
nickname = '商家'
else:
shangjia_id = ext.shangjia_id
nickname = ext.sjnicheng or f'商家{shangjia_id[:6]}'
if shangjia_id:
avatar_full = _full_local_avatar('')
try:
sj_user = UserMain.objects.get(yonghuid=shangjia_id)
avatar_full = _full_local_avatar(sj_user.avatar or '')
except Exception:
pass
return f"Sj{shangjia_id}", nickname, avatar_full
if order.user1_id:
name, avatar = _resolve_goeasy_user_display(order.user1_id)
return order.user1_id, name, avatar
return None, None, None
# ========== 跨平台 + 对方派单 ==========
def _handle_cross_partner_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret):
partner_club_id = order.partner_club_id
if not partner_club_id:
logger.warning("缺少 partner_club_id")
return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret)
try:
partner_config = ClubConfig.objects.get(club_id=partner_club_id, is_self=0)
except ClubConfig.DoesNotExist:
logger.error(f"未找到对方俱乐部配置: {partner_club_id}")
return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret)
partner_order_id = order.partner_order_id
if not partner_order_id:
logger.error("partner_order_id 为空")
return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret)
# 推断对方下单方标识
target_partner_goeasy_id = _get_cross_partner_identity(order, partner_order_id)
if not target_partner_goeasy_id:
logger.error("无法推断对方下单方标识")
return _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret)
# 对方默认头像
partner_default_avatar = _full_remote_avatar(partner_config.storage_bucket_domain or '', partner_config.club_avatar or '') or _full_local_avatar('')
# ---- 1. 在我方建群 ----
local_group_id = f"group_{order.dingdan_id}"
local_group_name = (order.jieshao[:20] + '') if order.jieshao and len(order.jieshao) > 20 else f"跨平台订单{order.dingdan_id}"
local_group_avatar = _full_local_avatar(order.tupian) if order.tupian else _full_local_avatar('')
proxy_partner_id = f"{partner_order_id}_{target_partner_goeasy_id}"
proxy_partner_name = f"对方用户({partner_club_id})"
# 对方代理头像使用对方默认头像
proxy_partner_avatar = partner_default_avatar
if not _subscribe_users_to_group([dashou_goeasy_id, proxy_partner_id], [local_group_id], appkey, secret):
return False
# 1. 打手发送初始化消息
init_msg_text = f"跨平台订单已接单,内容:{order.jieshao},备注:{order.beizhu}"
_send_group_message(
appkey, secret, local_group_id, dashou_goeasy_id, dashou_name, local_group_avatar,
init_msg_text, None, group_name=local_group_name, order_id=order.dingdan_id,
is_cross=1
)
# 2. 【新增】对方代理也发一条消息,确保对方能看到群聊
partner_msg = f"订单已确认,请等待服务。"
_send_group_message(
appkey, secret, local_group_id, proxy_partner_id, proxy_partner_name, proxy_partner_avatar,
partner_msg, None, group_name=local_group_name, order_id=order.dingdan_id,
is_cross=1
)
# ---- 2. 在对方建群 ----
partner_appkey = partner_config.chat_api_key
partner_secret = partner_config.chat_api_id or ''
if not partner_appkey:
return True # 无对方 key仅完成我方建群
partner_group_id = f"group_{partner_order_id}"
partner_group_name = f"跨平台订单-我方打手已接单"
# 代理我方打手 ID
our_agent_id = f"{order.dingdan_id}_{dashou_goeasy_id}"
our_agent_name = f"[我方]{dashou_name}"
# 对方频道头像用对方俱乐部默认头像
partner_group_avatar = partner_default_avatar
if not _subscribe_users_to_group([our_agent_id, target_partner_goeasy_id], [partner_group_id], partner_appkey, partner_secret):
logger.error("向对方频道订阅失败")
return True
# 我方打手在对方频道发消息
cross_msg_text = f"我方打手已接单,订单:{order.jieshao},请确认"
_send_group_message(
partner_appkey, partner_secret, partner_group_id, our_agent_id, our_agent_name, partner_group_avatar,
cross_msg_text, None, group_name=partner_group_name, order_id=partner_order_id,
is_cross=1
)
# 【新增】对方用户在对方频道也发一条消息(以对方原始身份)
target_partner_name = f"用户{target_partner_goeasy_id}" # 简单命名,可按需优化
_send_group_message(
partner_appkey, partner_secret, partner_group_id, target_partner_goeasy_id, target_partner_name, partner_default_avatar,
"订单信息已收到。", None, group_name=partner_group_name, order_id=partner_order_id,
is_cross=1
)
return True
def _get_cross_partner_identity(order, partner_order_id):
"""
获取对方下单方的 GoEasy 标识。
优先从商家扩展表取 partner_yonghu_id否则按 partner_order_id 前缀推断。
"""
# 尝试从商家扩展表取(仅当发单平台为商家且存在 partner_yonghu_id
if order.fadan_pingtai == 2 and hasattr(order, 'shangjia_kuozhan'):
ext = order.shangjia_kuozhan
if ext and ext.partner_yonghu_id:
# 直接根据订单前缀构造标识
prefix = partner_order_id[:2].upper()
if prefix == 'SJ':
return f"Sj{ext.partner_yonghu_id}"
elif prefix == 'PT':
return f"Boss{ext.partner_yonghu_id}"
else:
return f"User{ext.partner_yonghu_id}"
# 后备:根据 partner_order_id 前缀推断
prefix = partner_order_id[:2].upper()
uid = partner_order_id[2:] if len(partner_order_id) > 2 else ''
if prefix == 'SJ':
return f"Sj{uid}"
elif prefix == 'PT':
return f"Boss{uid}"
else:
return f"User{uid}"
def _create_local_group_only(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret):
dashou_uid = dashou_goeasy_id[2:] if dashou_goeasy_id.startswith('Ds') else dashou_goeasy_id
group_id = resolve_group_id_for_order(order, dashou_uid)
partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order)
group_name = partner_name or (order.jieshao or f"订单{order.dingdan_id}")
order_desc = (order.jieshao or '').strip()
if order.beizhu:
order_desc = f"{order_desc} | 备注:{order.beizhu}" if order_desc else f"备注:{order.beizhu}"
to_data = None
if partner_goeasy_id:
to_data = _build_pair_group_to_data(
dashou_goeasy_id, dashou_name, dashou_avatar,
partner_goeasy_id, partner_name, partner_avatar,
order_id=order.dingdan_id, order_desc=order_desc,
is_cross=order.is_cross or 0,
)
subscribe_ok = _retry_call(
_subscribe_users_to_group, 5, 0.4,
[dashou_goeasy_id], [group_id], appkey, secret
)
msg_ok = _retry_call(
_send_group_message, 3, 0.4,
appkey, secret, group_id, dashou_goeasy_id, dashou_name, dashou_avatar,
"订单已接单。", None, order_id=order.dingdan_id,
is_cross=order.is_cross, to_data_extra=to_data,
)
return subscribe_ok or msg_ok
STATUS_NOTIFY_TEXT = {
2: '订单已开始服务',
3: '订单已完成',
4: '订单退款申请中',
5: '订单已退款',
6: '退款未通过',
8: '打手已提交交付,订单进入结算中',
}
def schedule_order_status_chat_push(dingdan_id):
"""订单状态变更后,事务提交后异步推送群聊状态更新"""
if getattr(settings, 'CELERY_ONLY_ORDER_BROADCAST', True):
return
if not dingdan_id:
return
def _run():
try:
from dingdan.tasks import push_order_status_chat_task
push_order_status_chat_task.apply_async(args=[dingdan_id], countdown=1)
except Exception as e:
logger.error(f"调度订单状态聊天推送失败 {dingdan_id}: {e}", exc_info=True)
try:
from django.db import transaction
transaction.on_commit(_run)
except Exception:
_run()
def push_order_status_chat_update(dingdan_id):
"""向订单群推送状态提示 + 最新订单卡片(含实时 zhuangtai"""
try:
order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
logger.warning(f"push_order_status_chat_update: 订单 {dingdan_id} 不存在")
return False
dashou_uid = order.jiedan_dashou_id
if not dashou_uid:
return False
appkey = _get_self_goeasy_appkey()
secret = _get_self_goeasy_secret()
if not appkey:
return False
dashou_goeasy_id = f'Ds{dashou_uid}'
dashou_name, dashou_avatar = _resolve_goeasy_user_display(dashou_goeasy_id)
partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order)
if not partner_goeasy_id:
return False
group_id = resolve_group_id_for_order(order, dashou_uid)
order_desc = (order.jieshao or '').strip()
if order.beizhu:
order_desc = f"{order_desc} | 备注:{order.beizhu}" if order_desc else f"备注:{order.beizhu}"
pair_to_data = _build_pair_group_to_data(
dashou_goeasy_id, dashou_name, dashou_avatar,
partner_goeasy_id, partner_name, partner_avatar,
order_id=order.dingdan_id, order_desc=order_desc,
is_cross=order.is_cross or 0, order_zuangtai=order.zhuangtai,
)
user_ids = list({dashou_goeasy_id, partner_goeasy_id})
_retry_call(_subscribe_users_to_group, 3, 0.4, user_ids, [group_id], appkey, secret)
status_text = STATUS_NOTIFY_TEXT.get(order.zhuangtai, '订单状态已更新')
notify_text = f"【订单状态】{status_text}"
_retry_call(
_send_group_message, 3, 0.4,
appkey, secret, group_id, dashou_goeasy_id, dashou_name, dashou_avatar,
notify_text, None, order_id=order.dingdan_id,
is_cross=order.is_cross or 0, to_data_extra=pair_to_data,
)
card_ok = _retry_call(
_push_order_card_message, 3, 0.4,
appkey, secret, order, group_id, dashou_goeasy_id, dashou_name, dashou_avatar,
pair_to_data,
)
partner_order_id = order.partner_order_id
partner_club_id = order.partner_club_id
if order.is_cross == 1 and partner_order_id and partner_club_id:
try:
partner_cfg = ClubConfig.objects.get(club_id=partner_club_id, is_self=0)
if partner_cfg and partner_cfg.chat_api_key:
partner_appkey = partner_cfg.chat_api_key
partner_secret = partner_cfg.chat_api_id or ''
partner_group_id = f"group_{partner_order_id}"
agent_id = f"{order.dingdan_id}_{dashou_goeasy_id}"
agent_name = f"[状态]{dashou_name}"
_retry_call(
_send_group_message, 2, 0.4,
partner_appkey, partner_secret, partner_group_id, agent_id, agent_name, '',
notify_text, None, order_id=partner_order_id, is_cross=1,
)
except Exception as e:
logger.warning(f"跨平台状态推送失败: {e}")
return card_ok