Files
Django/utils/weixin_broadcast.py
XingQue 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
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
}