Files
Django/utils/weixin_broadcast.py
T
XingQueandCursor f36db77f6b fix: 新订单服务号模板改为两字段并换新模板ID
对齐「新订单待受理通知」:short_thing4 游戏类型、thing15 下单项目;不改动广播触发点。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 16:27:12 +08:00

186 lines
7.8 KiB
Python

# utils/weixin_broadcast.py - 强调试版本
import time
import logging
import json # 新增,用于格式化打印
import requests
from django.core.cache import cache
from django.conf import settings
from users.models import OfficialAccountUser
logger = logging.getLogger('weixin_broadcast')
# 服务号模板「新订单待受理通知」(类目:休闲娱乐)
# 字段:游戏类型 short_thing4、下单项目 thing15
ORDER_NOTIFY_TEMPLATE_ID = 'jZ73G_0KbmFK864WVUr0QABDc0EfsjeXXLPS86Vwo-4'
class WeixinBroadcastSender:
"""
微信模板消息广播发送器 - 调试版
"""
def __init__(self):
self.appid = getattr(settings, 'WEIXIN_OFFICIAL_APPID', '')
self.secret = getattr(settings, 'WEIXIN_OFFICIAL_SECRET', '')
self.template_id = ORDER_NOTIFY_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):
"""
构建「新订单待受理通知」模板数据。
short_thing4=游戏类型(最多5字),thing15=下单项目/描述(最多20字)。
"""
logger.info(
f"🟡【调试-输入】build_order_message_data 收到的 order_info: "
f"{json.dumps(order_info, ensure_ascii=False, indent=2)}"
)
game_type = str(order_info.get('game_type') or '游戏订单').strip() or '游戏订单'
order_desc = str(order_info.get('order_desc') or '新订单').strip() or '新订单'
data = {
"short_thing4": {
"value": game_type[:5],
"color": "#173177",
},
"thing15": {
"value": order_desc[:20],
"color": "#173177",
},
}
logger.info("🟡【调试-输出】构建完成的模板数据 data 字段:")
for key, content in data.items():
logger.info(f" 字段名: '{key}' -> 值: '{content.get('value', '')}'")
logger.info(f"🟡【调试-输出】完整 data 结构: {json.dumps(data, ensure_ascii=False)}")
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
}