diff --git a/orders/urls.py b/orders/urls.py index f315c21..4d2e543 100644 --- a/orders/urls.py +++ b/orders/urls.py @@ -9,7 +9,8 @@ from .views import CreateOrderView, WechatPayNotifyView, \ AdTongYiTuiKuanPingTai, AdTongYiTuiKuanShangJia, AdQiangZhiJieDan, AdJuJueTuiKuan,\ AdGengHuanDaShou,ShangjiaCOSZhengshuView, DashouHuoquLeixingView,AdJuJueJieSuan, AdZhuanYiDaTing, AdQuXiaoZhiDing,DashouXiugaiView,DaiLiQunLiaoXiaoXiView,\ ShangjiaLianjieLiuYanZhuanFaView, DingdanXiangqingView2, JiedanView2, ZxsjghdsView, \ - ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView, ShangjiaShujuTongjiView + ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView, ShangjiaShujuTongjiView, LianTongDuiHuaZhunBeiView, \ + LianTongPeiDuiDingDanLieBiaoView, LianTongDingDanZhuangTaiView, LianTongDingDanZhuangTaiPiLiangView urlpatterns = [ @@ -69,6 +70,10 @@ urlpatterns = [ path('dsxiugaidd', DashouXiugaiView.as_view(), name='打手修改订单详情'), path('kptxxfs', DaiLiQunLiaoXiaoXiView.as_view(), name='前端聊天转发'), # 群聊消息代理发送 + path('ltdhzb', LianTongDuiHuaZhunBeiView.as_view(), name='订单群聊准备'), + path('ltpddd', LianTongPeiDuiDingDanLieBiaoView.as_view(), name='配对订单列表'), + path('ltddzt', LianTongDingDanZhuangTaiView.as_view(), name='聊天订单状态'), + path('ltddztpl', LianTongDingDanZhuangTaiPiLiangView.as_view(), name='聊天订单状态批量'), path('sjljlbxxzf', ShangjiaLianjieLiuYanZhuanFaView.as_view(), name='商家链接老板聊天转发'), path('dingdanxiangqing2', DingdanXiangqingView2.as_view(), name='老板获取订单详情新街口'), diff --git a/orders/views.py b/orders/views.py index 77d2735..1e107f6 100644 --- a/orders/views.py +++ b/orders/views.py @@ -39,7 +39,12 @@ 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 +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 ( @@ -3562,8 +3567,8 @@ class DaiLiQunLiaoXiaoXiView(APIView): logger.error("[群聊代理] 我方 GoEasy AppKey 未配置") return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) - # ===== 6. 公共群信息 ===== - real_group_id = f"group_{order.OrderID}" + # ===== 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('') @@ -3633,7 +3638,7 @@ class ShangjiaLianjieLiuYanZhuanFaView(APIView): if not appkey: return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) - group_id = f"group_{order.OrderID}" + 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): @@ -3657,6 +3662,154 @@ class ShangjiaLianjieLiuYanZhuanFaView(APIView): 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}) + + class QiangdanView(APIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] diff --git a/utils/chat_utils.py b/utils/chat_utils.py index 37faacb..bd80e3e 100644 --- a/utils/chat_utils.py +++ b/utils/chat_utils.py @@ -1,14 +1,21 @@ # utils/chat_utils.py import json import logging +import re import requests from django.conf import settings +from django.db.models import Q from orders.models import Order, PlatformOrderExt, MerchantOrderExt from users.models import UserDashou, UserBoss, UserShangjia from users.business_models import User logger = logging.getLogger('chat_utils') +ORDER_STATUS_TEXT = { + 1: '待抢单', 2: '进行中', 3: '已完成', 4: '退款审核中', 5: '已退款', + 7: '指定中', 8: '结算中', 9: '未支付', 14: '支付失败', 25: '已废弃', +} + # ========== 辅助配置获取 ========== def _get_self_goeasy_appkey(): return getattr(settings, 'GOEASY_APPKEY', '') @@ -22,8 +29,207 @@ def _full_local_avatar(relative_url): return '' if relative_url.startswith('http'): return relative_url + domain = getattr(settings, 'COS_DOMAIN', '') or '' + if domain: + return domain.rstrip('/') + '/' + relative_url.lstrip('/') return relative_url + +def resolve_pair_group_id(order): + """打手+商家/老板配对群 ID,与前端 resolveLocalGroupId 一致(同两人共用一个群)""" + dashou_uid = order.PlayerID or '' + if not dashou_uid: + return None + if order.Platform == 2: + try: + sj_uid = order.shangjia_kuozhan.MerchantID + if sj_uid: + return f'group_Ds{dashou_uid}_Sj{sj_uid}' + except Exception as e: + logger.warning(f'配对群(商家)解析失败: {e}') + elif order.Platform == 1: + try: + boss_uid = order.pingtai_kuozhan.BossID + if boss_uid: + return f'group_Ds{dashou_uid}_Boss{boss_uid}' + except Exception as e: + logger.warning(f'配对群(老板)解析失败: {e}') + return None + + +def parse_pair_from_group_id(group_id): + """从配对群 ID 解析打手 uid、对方 uid、对方角色(shangjia/boss)""" + if not group_id: + return None, None, None + m = re.match(r'^group_Ds(\w+)_Sj(\w+)$', str(group_id)) + if m: + return m.group(1), m.group(2), 'shangjia' + m = re.match(r'^group_Ds(\w+)_Boss(\w+)$', str(group_id)) + if m: + return m.group(1), m.group(2), 'boss' + return None, None, None + + +def resolve_pair_from_order(order): + """从订单解析打手与下单方配对信息""" + dashou_uid = order.PlayerID or '' + if not dashou_uid: + return None, None, None + if order.Platform == 2: + try: + sj_uid = order.shangjia_kuozhan.MerchantID + if sj_uid: + return dashou_uid, sj_uid, 'shangjia' + except Exception: + pass + elif order.Platform == 1: + try: + boss_uid = order.pingtai_kuozhan.BossID + if boss_uid: + return dashou_uid, boss_uid, 'boss' + except Exception: + pass + return dashou_uid, None, None + + +def check_pair_chat_permission(identity_type, uid, dashou_uid, partner_uid, partner_role): + if identity_type == 'PlayerID': + if uid != dashou_uid: + return False, '你不是该会话的打手' + return True, '' + if identity_type == 'shangjia': + if partner_role != 'shangjia' or uid != partner_uid: + return False, '你不是该会话的商家' + return True, '' + if identity_type == 'boss': + if partner_role != 'boss' or uid != partner_uid: + return False, '你不是该会话的老板' + return True, '' + return False, '不支持的身份类型' + + +def order_to_chat_dict(order): + return { + 'dingdan_id': order.OrderID, + 'jieshao': order.Description or '', + 'jine': str(order.Amount) if order.Amount is not None else '', + 'beizhu': order.Remark or '', + 'zhuangtai': order.Status, + 'zhuangtaiText': ORDER_STATUS_TEXT.get(order.Status, ''), + 'nicheng': order.Nickname or '', + 'create_time': order.CreateTime.strftime('%Y-%m-%d %H:%M') if order.CreateTime else '', + } + + +def query_pair_orders(dashou_uid, partner_uid, partner_role, keyword='', limit=30): + if not dashou_uid or not partner_uid: + return [] + qs = Order.objects.filter(PlayerID=dashou_uid).select_related('pingtai_kuozhan', 'shangjia_kuozhan') + if partner_role == 'shangjia': + qs = qs.filter(Platform=2, shangjia_kuozhan__MerchantID=partner_uid) + elif partner_role == 'boss': + qs = qs.filter(Platform=1, pingtai_kuozhan__BossID=partner_uid) + else: + return [] + qs = qs.exclude(Status__in=[9, 14, 25]) + if keyword: + qs = qs.filter(Q(OrderID__icontains=keyword) | Q(Description__icontains=keyword)) + return [order_to_chat_dict(o) for o in qs.order_by('-CreateTime')[:limit]] + + +def _get_dashou_profile(order): + dashou_uid = order.PlayerID or '' + dashou_goeasy_id = f'Ds{dashou_uid}' if dashou_uid else '' + dashou_nickname = f'打手{dashou_uid[:6]}' if dashou_uid else '' + dashou_avatar = '' + if dashou_uid: + try: + dashou_user = User.objects.get(UserUID=dashou_uid) + dashou_avatar = _full_local_avatar(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}') + return dashou_uid, dashou_goeasy_id, dashou_nickname, dashou_avatar + + +def build_order_group_chat_payload(order, viewer_backend_identity, viewer_uid): + """构建前端 ltdhzb 所需群聊元数据(新群用打手+商家/老板配对 groupId)""" + group_id = resolve_pair_group_id(order) or f'group_{order.OrderID}' + order_desc = order.Description or '' + group_name = ( + (order_desc[:20] + '…') if order_desc and len(order_desc) > 20 + else (order_desc or f'订单{order.OrderID}') + ) + dashou_uid, dashou_goeasy_id, dashou_name, dashou_avatar = _get_dashou_profile(order) + partner_goeasy_id, partner_name, partner_avatar = _get_local_partner_info(order) + partner_uid = '' + if partner_goeasy_id: + partner_uid = partner_goeasy_id.replace('Ds', '').replace('Sj', '').replace('Boss', '') + + counterpart_id = '' + counterpart_yonghuid = '' + counterpart_name = '' + counterpart_avatar = '' + if viewer_backend_identity == 'PlayerID': + counterpart_id = partner_goeasy_id or '' + counterpart_yonghuid = partner_uid + counterpart_name = partner_name or '' + counterpart_avatar = partner_avatar or '' + else: + counterpart_id = dashou_goeasy_id or '' + counterpart_yonghuid = dashou_uid + counterpart_name = dashou_name or '' + counterpart_avatar = dashou_avatar or '' + + return { + 'groupId': group_id, + 'orderId': order.OrderID, + 'groupName': counterpart_name or group_name, + 'groupAvatar': counterpart_avatar or partner_avatar or dashou_avatar or '', + 'counterpartName': counterpart_name, + 'counterpartAvatar': counterpart_avatar, + 'counterpartId': counterpart_id, + 'counterpartYonghuid': counterpart_yonghuid, + 'orderDesc': order_desc, + 'orderJine': str(order.Amount) if order.Amount is not None else '', + 'orderZhuangtai': order.Status, + 'dashouGoEasyId': dashou_goeasy_id, + 'partnerGoEasyId': partner_goeasy_id or '', + 'dashouYonghuid': dashou_uid, + 'partnerYonghuid': partner_uid, + 'dashouName': dashou_name, + 'partnerName': partner_name or '', + 'dashouAvatar': dashou_avatar, + 'partnerAvatar': partner_avatar or '', + 'isCross': 0, + } + + +def prepare_order_group_chat(order, viewer_backend_identity, viewer_uid, subscribe=True): + """进入群聊前:订阅当前用户(及双方)到配对群,不重复发送抢单初始化消息""" + payload = build_order_group_chat_payload(order, viewer_backend_identity, viewer_uid) + group_id = payload['groupId'] + appkey = _get_self_goeasy_appkey() + secret = _get_self_goeasy_secret() + if not appkey: + logger.error('GoEasy AppKey 未配置') + return payload, False + + prefix_map = {'PlayerID': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'} + viewer_goeasy_id = prefix_map.get(viewer_backend_identity, 'Ds') + viewer_uid + user_ids = {viewer_goeasy_id} + if payload.get('dashouGoEasyId'): + user_ids.add(payload['dashouGoEasyId']) + if payload.get('partnerGoEasyId'): + user_ids.add(payload['partnerGoEasyId']) + + subscribed = True + if subscribe and user_ids: + subscribed = _subscribe_users_to_group(list(user_ids), [group_id], appkey, secret) + return payload, subscribed + # ========== GoEasy 订阅 ========== def _subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None): if not appkey: @@ -129,7 +335,7 @@ def establish_order_chat(dingdan_id): # ========== 普通订单 / 我方派单 ========== def _handle_local_order(order, dashou_goeasy_id, dashou_name, dashou_avatar, appkey, secret): - group_id = f"group_{order.OrderID}" + 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 (order.Description or f"订单{order.OrderID}") group_avatar = _full_local_avatar(order.ImageURL) if order.ImageURL else _full_local_avatar('')