# 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') # ========== 辅助配置获取(从 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 _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): 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' # to.data 包含群聊展示信息及业务字段 to_data = { "name": group_name or group_id, "avatar": sender_avatar or "", } if order_id: to_data["orderId"] = order_id 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}, "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 _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): group_id = f"group_{order.dingdan_id}" group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else (order.jieshao or f"订单{order.dingdan_id}") group_avatar = _full_local_avatar(order.tupian) if order.tupian else _full_local_avatar('') partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order) 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 ) msg_ok = _retry_call( _send_group_message, 3, 0.4, appkey, secret, group_id, dashou_goeasy_id, dashou_name, group_avatar, "订单已接单,请等待对方上线。", None, group_name=group_name, order_id=order.dingdan_id, is_cross=order.is_cross ) return subscribe_ok or msg_ok 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, group_avatar, init_msg_text, None, group_name=group_name, order_id=order.dingdan_id, is_cross=order.is_cross ) 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, group_name=group_name, order_id=order.dingdan_id, is_cross=order.is_cross ) 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 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): group_id = f"group_{order.dingdan_id}" group_name = (order.jieshao[:20] + '…') if order.jieshao and len(order.jieshao) > 20 else (order.jieshao or f"订单{order.dingdan_id}") group_avatar = _full_local_avatar(order.tupian) if order.tupian else _full_local_avatar('') 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, group_avatar, "订单已接单。", None, group_name=group_name, order_id=order.dingdan_id, is_cross=order.is_cross ) return subscribe_ok or msg_ok