From ed41afbbd75a04314a5ebc5b0cf1d7fe49fef552 Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 5 Aug 2026 00:45:37 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20H5=20=E5=8F=91=E5=9B=BE=E8=B5=B0=20OSS?= =?UTF-8?q?=20+=20=E5=90=8E=E7=AB=AF=20REST=20=E4=BB=A3=E5=8F=91=E5=85=9C?= =?UTF-8?q?=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- orders/urls.py | 3 +- orders/views/__init__.py | 2 + orders/views/chat.py | 170 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) diff --git a/orders/urls.py b/orders/urls.py index c653274..ce2f94a 100644 --- a/orders/urls.py +++ b/orders/urls.py @@ -11,7 +11,7 @@ from .views import CreateOrderView, WechatPayNotifyView, AlipayPayNotifyView, al ShangjiaLianjieLiuYanZhuanFaView, DingdanXiangqingView2, JiedanView2, ZxsjghdsView, \ ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView, ShangjiaShujuTongjiView, LianTongDuiHuaZhunBeiView, \ LianTongPeiDuiDingDanLieBiaoView, LianTongDingDanZhuangTaiView, LianTongDingDanZhuangTaiPiLiangView, \ - ZhiDingChaxunView, ZhiDingHuiFuView + ZhiDingChaxunView, ZhiDingHuiFuView, H5ChatSendImageView from jituan.services.fubei_notify import FubeiPayNotifyView from jituan.views_order_deal import MiniappMyDealStatView @@ -80,6 +80,7 @@ urlpatterns = [ path('dsxiugaidd', DashouXiugaiView.as_view(), name='打手修改订单详情'), path('kptxxfs', DaiLiQunLiaoXiaoXiView.as_view(), name='前端聊天转发'), # 群聊消息代理发送 + path('h5ftp', H5ChatSendImageView.as_view(), name='H5发图代发'), path('ltdhzb', LianTongDuiHuaZhunBeiView.as_view(), name='订单群聊准备'), path('ltpddd', LianTongPeiDuiDingDanLieBiaoView.as_view(), name='配对订单列表'), path('ltddzt', LianTongDingDanZhuangTaiView.as_view(), name='聊天订单状态'), diff --git a/orders/views/__init__.py b/orders/views/__init__.py index 098048b..417e10e 100644 --- a/orders/views/__init__.py +++ b/orders/views/__init__.py @@ -36,6 +36,7 @@ from .chat import ( LianTongPeiDuiDingDanLieBiaoView, LianTongDingDanZhuangTaiView, LianTongDingDanZhuangTaiPiLiangView, + H5ChatSendImageView, ) from .dashou import ( DashouDingdanHuoquView, @@ -80,6 +81,7 @@ __all__ = [ "DaiLiQunLiaoXiaoXiView", "ShangjiaLianjieLiuYanZhuanFaView", "LianTongDuiHuaZhunBeiView", "LianTongPeiDuiDingDanLieBiaoView", "LianTongDingDanZhuangTaiView", "LianTongDingDanZhuangTaiPiLiangView", + "H5ChatSendImageView", "DashouDingdanHuoquView", "ShangpinLeixingHuoquView", "QiangdanView", "DashouDingdanHuoquView1", "DashouCOSZhengshuView", "ShangjiaCOSZhengshuView", "DashouTijiaoView", "DashouDingdanXiangqingView", "DashouHuoquLeixingView", diff --git a/orders/views/chat.py b/orders/views/chat.py index 05891d8..4557c20 100644 --- a/orders/views/chat.py +++ b/orders/views/chat.py @@ -189,6 +189,176 @@ def send_group_message( return False +def send_im_message(appkey, secret, sender_id, sender_name, sender_avatar, + to_type, to_id, to_data, msg_type, payload, notification=None): + """ + 通过 GoEasy REST 发送私聊/群聊/客服消息。 + msg_type: text / image / 自定义类型 + """ + url = getattr(settings, 'GOEASY_REST_URL', None) or 'https://rest-hz.goeasy.io/v2/im/message' + body = { + "appkey": appkey, + "senderId": sender_id, + "senderData": {"avatar": sender_avatar or '', "name": sender_name or ''}, + "to": {"type": to_type, "id": to_id, "data": to_data or {}}, + "type": msg_type, + "payload": payload, + } + if notification: + body["notification"] = notification + headers = {"Content-Type": "application/json"} + if secret: + headers["Authorization"] = f"Bearer {secret}" + + last_text = 'send failed' + for attempt in range(3): + try: + r = requests.post(url, headers=headers, json=body, timeout=10) + if r.status_code == 200: + try: + data = r.json() + except Exception: + data = {} + if isinstance(data, dict) and data.get('code') not in (None, 200, 0, '200', '0'): + logger.warning( + "GoEasy REST 业务失败 (尝试 %s/3):%s", attempt + 1, r.text + ) + last_text = r.text + else: + logger.info( + "IM 消息发送成功:sender=%s, to=%s/%s, type=%s", + sender_id, to_type, to_id, msg_type, + ) + return True, data if isinstance(data, dict) else {} + logger.warning( + "IM 消息发送失败 (尝试 %s/3):状态码 %s, 响应 %s", + attempt + 1, r.status_code, r.text, + ) + last_text = r.text + except Exception as e: + last_text = str(e) + logger.warning("IM 消息发送异常 (尝试 %s/3):%s", attempt + 1, e) + if attempt < 2: + time.sleep(1) + return False, {'content': last_text} + + +class H5ChatSendImageView(APIView): + """ + H5 发图兜底:图片已上传到自有 OSS 后,由服务端 REST 代发 type=image。 + 路径:POST /dingdan/h5ftp + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + to_type = str(request.data.get('toType') or '').strip().lower() + to_id = str(request.data.get('toId') or '').strip() + to_data = request.data.get('toData') or {} + sender_id = str(request.data.get('senderId') or '').strip() + sender_name = str(request.data.get('senderName') or '').strip() + sender_avatar = str(request.data.get('senderAvatar') or '').strip() + image_url = str(request.data.get('imageUrl') or '').strip() + width = request.data.get('width') or 200 + height = request.data.get('height') or 200 + size = request.data.get('size') or 1 + name = str(request.data.get('name') or 'chat.jpg').strip() or 'chat.jpg' + + if to_type not in ('private', 'group', 'cs'): + return Response({'code': 400, 'msg': 'toType 无效'}) + if not to_id or not sender_id or not image_url: + return Response({'code': 400, 'msg': '缺少 toId/senderId/imageUrl'}) + if not (image_url.startswith('https://') or image_url.startswith('http://')): + return Response({'code': 400, 'msg': 'imageUrl 必须是 http(s) 地址'}) + + uid = str(getattr(request.user, 'UserUID', '') or '') + if not uid or uid not in sender_id: + return Response({'code': 403, 'msg': '发送者身份不匹配'}) + + if not isinstance(to_data, dict): + try: + to_data = json.loads(to_data) if isinstance(to_data, str) else {} + except Exception: + to_data = {} + + appkey = get_self_goeasy_appkey() + secret = get_self_goeasy_secret() + if not appkey: + return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) + + try: + width = int(width) or 200 + height = int(height) or 200 + size = int(size) or 1 + except (TypeError, ValueError): + width, height, size = 200, 200, 1 + + payload = { + 'url': image_url, + 'thumbnail': image_url, + 'name': name, + 'contentType': 'image/jpeg', + 'width': width, + 'height': height, + 'size': size, + } + notification = {'title': '新消息', 'body': '[图片]'} + + ok, data = send_im_message( + appkey=appkey, + secret=secret, + sender_id=sender_id, + sender_name=sender_name, + sender_avatar=full_avatar(sender_avatar) if sender_avatar else '', + to_type=to_type, + to_id=to_id, + to_data=to_data, + msg_type='image', + payload=payload, + notification=notification, + ) + if not ok: + # GoEasy REST 文档写不支持 image;再试自定义 picture(H5 可 normalize,小程序仍只认 image) + ok2, data2 = send_im_message( + appkey=appkey, + secret=secret, + sender_id=sender_id, + sender_name=sender_name, + sender_avatar=full_avatar(sender_avatar) if sender_avatar else '', + to_type=to_type, + to_id=to_id, + to_data=to_data, + msg_type='picture', + payload=payload, + notification=notification, + ) + if not ok2: + err = '' + if isinstance(data, dict): + err = str(data.get('content') or data.get('msg') or '') + if isinstance(data2, dict) and not err: + err = str(data2.get('content') or data2.get('msg') or '') + return Response({'code': 500, 'msg': err or '图片代发失败'}) + return Response({ + 'code': 200, + 'msg': '发送成功', + 'data': { + 'url': image_url, + 'type': 'picture', + 'goeasy': data2, + }, + }) + + return Response({ + 'code': 200, + 'msg': '发送成功', + 'data': { + 'url': image_url, + 'type': 'image', + 'goeasy': data, + }, + }) + + # ----------------- 权限校验 ----------------- def check_user_permission(identity_type, uid, order): """验证用户身份、账号状态、以及是否属于该订单"""