# utils/weixin_broadcast.py - 强调试版本 import time import logging import json # 新增,用于格式化打印 import requests from django.core.cache import cache from django.conf import settings from yonghu.models import OfficialAccountUser logger = logging.getLogger('weixin_broadcast') class WeixinBroadcastSender: """ 微信模板消息广播发送器 - 调试版 """ def __init__(self): self.appid = getattr(settings, 'WEIXIN_OFFICIAL_APPID', '') self.secret = getattr(settings, 'WEIXIN_OFFICIAL_SECRET', '') self.template_id = getattr(settings, 'WEIXIN_TEMPLATE_ID', '') # 发送控制参数 self.batch_size = 100 self.delay_between_batches = 1.5 def get_access_token(self): """获取服务号access_token""" cache_key = f'weixin_official_token_{self.appid}' token = cache.get(cache_key) if token: return token url = 'https://api.weixin.qq.com/cgi-bin/token' params = {'grant_type': 'client_credential', 'appid': self.appid, 'secret': self.secret} try: response = requests.get(url, params=params, timeout=10) result = response.json() if 'access_token' in result: token = result['access_token'] expires_in = result.get('expires_in', 7200) - 300 cache.set(cache_key, token, expires_in) logger.info("✅ 获取access_token成功") return token else: logger.error(f"❌ 获取access_token失败: {result}") return None except Exception as e: logger.error(f"❌ 获取access_token异常: {str(e)}") return None def build_order_message_data(self, order_info): """ 构建订单消息数据 - 【调试关键点1】 打印传入的 order_info 和构建出的最终数据 """ # 🟡 调试点1:打印传入的参数 logger.info( f"🟡【调试-输入】build_order_message_data 收到的 order_info: {json.dumps(order_info, ensure_ascii=False, indent=2)}") # 这是你现在的构建逻辑,我们原样保留但加上调试 # 注意:这里保持你目前的字段名(short_thing5, short_thing4, thing15, amount8) data = { "short_thing5": { "value": order_info.get('order_type', '平台发单'), "color": "#173177" }, "short_thing4": { "value": order_info.get('game_type', '游戏订单'), "color": "#173177" }, "thing15": { "value": str(order_info.get('order_desc', '新订单'))[:20], "color": "#173177" }, "amount8": { "value": f"{order_info.get('amount', '0')}元", "color": "#173177" } } # 🟡 调试点2:打印构建出的最终数据结构 logger.info(f"🟡【调试-输出】构建完成的模板数据 data 字段:") for key, content in data.items(): logger.info(f" 字段名: '{key}' -> 值: '{content.get('value', '')}'") logger.info(f"🟡【调试-输出】完整 data 结构: {json.dumps(data, ensure_ascii=False)}") # 🔴 关键检查:确认 short_thing5 是否存在,值是否为空 if 'short_thing5' not in data: logger.error("❌【致命错误】构建的数据中根本没有 'short_thing5' 这个字段!") else: st5_value = data['short_thing5'].get('value', '') if not st5_value: logger.error(f"❌【致命错误】'short_thing5' 字段的值为空!") else: logger.info(f"✅ 'short_thing5' 字段存在,值为: '{st5_value}'") return data def send_to_user(self, official_openid, template_data, order_id=None): """发送模板消息给单个用户 - 【调试关键点2】""" # 🟡 调试点3:打印每次发送前的数据 logger.info(f"🟡【调试-发送前】准备发送给 {official_openid[:10]}... 的数据:") logger.info(f" 模板ID: {self.template_id}") logger.info( f" pagepath: pages/dingdan-xiangqing/dingdan-xiangqing?id={order_id}" if order_id else " 无pagepath") for key, content in template_data.items(): logger.info(f" 字段 '{key}': '{content.get('value', '')}'") access_token = self.get_access_token() if not access_token: return {'success': False, 'msg': '获取access_token失败', 'retry': True} request_data = { "touser": official_openid, "template_id": self.template_id, "data": template_data } if order_id and hasattr(settings, 'WEIXIN_APPID'): request_data["miniprogram"] = { "appid": settings.WEIXIN_APPID, "pagepath": f"pages/jiedan/jiedan?id={order_id}" } send_url = f'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={access_token}' try: response = requests.post(send_url, json=request_data, timeout=10) result = response.json() logger.info(f"🟡【调试-微信响应】{official_openid[:10]}... 的发送结果: {result}") if result.get('errcode') == 0: return {'success': True, 'msg': '发送成功', 'msgid': result.get('msgid')} else: # 如果是未关注或拒收,更新本地状态 if result.get('errcode') in [43004, 43101]: try: OfficialAccountUser.objects.filter(official_openid=official_openid).update(is_subscribed=False) except: pass return {'success': False, 'msg': f"[{result.get('errcode')}]{result.get('errmsg')}", 'retry': False} except Exception as e: logger.error(f"❌ 发送给用户异常: {official_openid[:10]}..., 错误: {str(e)}") return {'success': False, 'msg': f'请求异常: {str(e)}', 'retry': True} def broadcast_order(self, order_info): """广播订单消息给所有关注用户 - 【调试入口】""" logger.info(f"🚀【开始广播】订单: {order_info.get('dingdan_id')}") # 1. 获取所有关注用户 try: subscribed_users = OfficialAccountUser.objects.filter(is_subscribed=True).values_list('official_openid', flat=True) total_users = subscribed_users.count() logger.info(f"📊【广播统计】将从数据库查询到 {total_users} 个订阅用户") if total_users == 0: return {'success': True, 'msg': '无订阅用户', 'total': 0} except Exception as e: logger.error(f"❌ 获取订阅用户列表失败: {str(e)}") return {'success': False, 'msg': f'获取用户失败: {str(e)}'} # 2. 构建模板数据(这里会触发上面的调试打印) template_data = self.build_order_message_data(order_info) order_id = order_info.get('dingdan_id') # 3. 分批发送 success_count = 0 fail_count = 0 current_batch = 0 openid_list = list(subscribed_users) logger.info(f"🟡【调试-广播循环】开始遍历 {len(openid_list)} 个用户进行发送...") for i, openid in enumerate(openid_list): try: result = self.send_to_user(openid, template_data, order_id) if result['success']: success_count += 1 else: fail_count += 1 if fail_count <= 5: # 只打印前几个失败原因 logger.warning(f"⚠️ 发送失败 {openid[:10]}...: {result.get('msg')}") # 分批控制 current_batch += 1 if current_batch >= self.batch_size: time.sleep(self.delay_between_batches) current_batch = 0 except Exception as e: fail_count += 1 logger.error(f"❌ 处理用户 {openid[:10]}... 异常: {str(e)}") logger.info(f"🏁【广播完成】订单 {order_id},总计: {total_users},成功: {success_count},失败: {fail_count}") return { 'success': True, 'msg': f'广播完成: 成功{success_count}, 失败{fail_count}', 'total': total_users, 'success_count': success_count, 'fail_count': fail_count }