1. 背景
1.2 菜单效果
根据微信开放文档 介绍,我们可通过WebAPI修改服务号的菜单。实现的效果如下:
1.3 微信接口
微信服务号提供了接口用来创建、删除、查询菜单等,这里主要是维护菜单,这几个接口足够使用了。
1.3.1 创建菜单
因为限定了三个菜单,每个菜单最多五个子菜单。基本就是一个对象数组的json发过去就好。
1.3.2 删除菜单
当菜单数据有变化时,先删除菜单,再创建。
1.3.3 查询菜单
可用来验证是否创建成功。或者删除成功。
2. 实现
基于以上材料,计划写一段Python代码,基于MySQL的数据库表来更新菜单。管理员通过配置页面修改MySQL的数据库表,微信服务号的菜单就自动更新。大体实现方案如下:
SE : 软件工程师。 OM : 运营经理。
SE创建管理服务号菜单的数据库表,编写基于库表更新菜单的python代码,然后通过airflow调度起来。实现菜单的自动更新。
OM使用浏览器访问运营管理系统,修改数据库表配置。并查看更新结果。
2.1 数据库设计
首先创建记录菜单的数据库,支持多个不同服务号的菜单设定。并
DROP TABLE IF EXISTS t_wechat_menu;
CREATE TABLE `t_wechat_menu` (
`f_id` int(11) NOT NULL AUTO_INCREMENT,
`f_parent_id` int(11) DEFAULT NULL COMMENT '父菜单ID,一级菜单为NULL',
`f_name` varchar(64) COLLATE utf8mb4_bin NOT NULL COMMENT '菜单标题',
`f_type` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '菜单类型: click-点击事件, view-网页链接, miniprogram-小程序, media_id-素材ID, view_limited-图文消息',
`f_key` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '菜单KEY值,用于消息接口推送',
`f_url` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '网页链接',
`f_appid` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '小程序的appid',
`f_pagepath` varchar(128) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '小程序的页面路径',
`f_media_id` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '素材ID',
`f_sort_order` int(11) DEFAULT '0' COMMENT '菜单排序',
`f_state` tinyint(1) DEFAULT '1' COMMENT '状态: 0-禁用, 1-启用',
`f_crt_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`f_upload_time` datetime DEFAULT NULL,
`f_wechat_id` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一标识一个微信服务号:例如jygt bdm xlz',
PRIMARY KEY (`f_id`) USING BTREE,
KEY `idx_parent_id` (`f_parent_id`),
KEY `idx_sort_order` (`f_sort_order`),
KEY `idx_wechat_id` (`f_wechat_id`),
KEY `idx_wechat_id_and_sort_corder` (`f_sort_order`,`f_state`,`f_wechat_id`),
KEY `idx_state` (`f_state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='微信服务号菜单配置表';
-- 检查 t_wechat_menu_sync_log 表是否存在,如果存在则删除
DROP TABLE IF EXISTS t_wechat_menu_sync_log;
-- 创建 t_wechat_menu_sync_log 表
CREATE TABLE `t_wechat_menu_sync_log` (
`f_id` int(11) NOT NULL AUTO_INCREMENT,
`f_sync_time` datetime NOT NULL COMMENT '同步时间',
`f_status` tinyint(1) NOT NULL COMMENT '0-失败, 1-成功',
`f_error_msg` text COMMENT '错误信息',
`f_operator` varchar(64) DEFAULT NULL COMMENT '操作人',
`f_wechat_id` varchar(64) DEFAULT NULL COMMENT '唯一标识一个微信服务号:例如jygt bdm xlz',
PRIMARY KEY (`f_id`),
KEY `idx_sync_time` (`f_sync_time`),
KEY `idx_wechat_id` (`f_wechat_id`) -- 这里将索引名修正,避免重复
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信菜单同步日志';
2.2 Python代码
基于SQLAlchemy实现数据库实体类和服务类。然后从数据库获取数据,打包成微信服务号菜单创建的请求报文。
2.2.1 TWechatMenuService
#
# 此代码为自动化代码工具【JYGT-CODER】自动生成的文件,请勿手动编辑。
#
# 作者: 修炼者 7457222@qq.com
# 日期: 2025-03-25 11:54:08
#
from interface.db.db_jygt.connection.MydbPool import mydb_pool_session, mydb_transaction, mydb_copy
from interface.db.db_jygt.entity.TWechatMenuEntity import TWechatMenuEntity
import re
from sqlalchemy import or_, and_
from public.jygt_coder_loger import get_logger
logger = get_logger(__name__)
class TWechatMenuService:
@mydb_pool_session
def getAll(self, page=0, per_page=10):
"""
获取所有 TWechatMenuService 记录,支持分页。
:param page: 当前页码,默认为 1。
:param per_page: 每页记录数,默认为 10。
:return: 包含总记录数、总页数、当前页码和记录列表的字典。
"""
try:
logger.info(f"{self.__class__.__name__}.getAll")
items = self.session.query(TWechatMenuEntity
).limit(per_page).offset(page * per_page).all()
total = self.session.query(TWechatMenuEntity).count()
pages = (total + per_page - 1) // per_page
return {
'total': total,
'pages': pages,
'current_page': page,
'items': items
}
except Exception as e:
logger.error(f"Error fetching all TWechatMenuEntity records: {e}")
raise
@mydb_pool_session
def _get_by_filters(self, filters: list) -> list:
"""
根据给定的过滤条件查询 TWechatMenuEntity 记录。
:param filters: 过滤条件列表。
:return: 查询结果列表。
"""
try:
query = self.session.query(TWechatMenuEntity).filter(and_(*filters))
return query.all()
except Exception as e:
logger.error(f"Error fetching TWechatMenuEntity records with filters {filters}: {e}")
raise
@mydb_pool_session
def getByPrimary(self, f_id):
"""
根据 表t_wechat_menu索引 PRIMARY 获取记录。
输入参数:
f_id
输出参数:
TWechatMenuEntity对象或 None
"""
logger.info(f"{self.__class__.__name__}.getByPrimary")
filters = [
TWechatMenuEntity.f_id == f_id,
1==1
]
results = self._get_by_filters(filters)
return results[0] if results else None
@mydb_pool_session
def getByIdxParentId(self, f_parent_id):
"""
根据 表t_wechat_menu索引 idx_parent_id 获取记录。
输入参数:
f_parent_id
输出参数:
TWechatMenuEntity列表或空
"""
logger.info(f"{self.__class__.__name__}.getByIdxParentId")
filters = [
TWechatMenuEntity.f_parent_id == f_parent_id,
1==1
]
return self._get_by_filters(filters)
@mydb_pool_session
def getByIdxSortOrder(self, f_sort_order):
"""
根据 表t_wechat_menu索引 idx_sort_order 获取记录。
输入参数:
f_sort_order
输出参数:
TWechatMenuEntity列表或空
"""
logger.info(f"{self.__class__.__name__}.getByIdxSortOrder")
filters = [
TWechatMenuEntity.f_sort_order == f_sort_order,
1==1
]
return self._get_by_filters(filters)
@mydb_pool_session
def getByIdxWechatId(self, f_wechat_id):
"""
根据 表t_wechat_menu索引 idx_wechat_id 获取记录。
输入参数:
f_wechat_id
输出参数:
TWechatMenuEntity列表或空
"""
logger.info(f"{self.__class__.__name__}.getByIdxWechatId")
filters = [
TWechatMenuEntity.f_wechat_id == f_wechat_id,
1==1
]
return self._get_by_filters(filters)
@mydb_pool_session
def getByIdxWechatIdAndSortCorder(self, f_sort_order, f_state, f_wechat_id):
"""
根据 表t_wechat_menu索引 idx_wechat_id_and_sort_corder 获取记录。
输入参数:
f_sort_order
f_state
f_wechat_id
输出参数:
TWechatMenuEntity列表或空
"""
logger.info(f"{self.__class__.__name__}.getByIdxWechatIdAndSortCorder")
filters = [
TWechatMenuEntity.f_sort_order == f_sort_order,
TWechatMenuEntity.f_state == f_state,
TWechatMenuEntity.f_wechat_id == f_wechat_id,
1==1
]
return self._get_by_filters(filters)
@mydb_pool_session
def getByIdxState(self, f_state):
"""
根据 表t_wechat_menu索引 idx_state 获取记录。
输入参数:
f_state
输出参数:
TWechatMenuEntity列表或空
"""
logger.info(f"{self.__class__.__name__}.getByIdxState")
filters = [
TWechatMenuEntity.f_state == f_state,
1==1
]
return self._get_by_filters(filters)
@mydb_pool_session
@mydb_transaction
def replace(self, records):
"""
批量替换数据,当数据存在时更新,当数据不存在时增加。
:param records: 要替换的数据列表。
:return: 替换后的数据列表。
"""
if not isinstance(records, list):
records = [records]
requests = []
for record in records:
obj = TWechatMenuEntity(**record)
if record.get('f_id'):
objOld = self.getByPrimary(f_id=record['f_id'])
if objOld is None:
self.session.add(obj)
requests.append(obj)
else:
mydb_copy(obj, objOld)
self.session.merge(objOld)
requests.append(objOld)
else:
self.session.add(obj)
requests.append(obj)
return requests
@mydb_pool_session
@mydb_transaction
def add(self, records):
"""
批量增加数据,当数据不存在时增加。
:param records: 要增加的数据列表。
:return: 增加后的数据列表。
"""
if not isinstance(records, list):
records = [records]
requests = []
for record in records:
obj = TWechatMenuEntity(**record)
if record.get('f_id'):
objOld = self.getByPrimary(f_id=record['f_id'])
if objOld is None:
self.session.add(obj)
requests.append(obj)
else:
self.session.add(obj)
requests.append(obj)
return requests
@mydb_pool_session
@mydb_transaction
def update(self, records):
"""
批量修改数据,当数据存在时修改。
:param records: 要修改的数据列表。
:return: 修改后的数据列表。
"""
if not isinstance(records, list):
records = [records]
requests = []
for record in records:
obj = TWechatMenuEntity(**record)
if record.get('f_id'):
objOld = self.getByPrimary(f_id=record['f_id'])
if objOld :
mydb_copy(obj, objOld)
self.session.merge(objOld)
requests.append(objOld)
return requests
@mydb_pool_session
def delete(self, f_ids):
"""
删除指定主键集合的记录。
输入参数:
单值 f_id
集合 [f_id,...]
输出参数:
删除的实体对象列表 [TWechatMenuEntity]
"""
if not isinstance(f_ids, list):
f_ids = [f_ids]
deleted_items = []
for f_id in f_ids:
try:
oldRecord = self.getByPrimary(f_id=f_id)
if oldRecord is None:
continue
self.session.delete(oldRecord)
deleted_items.append(oldRecord)
except Exception as exp:
logger.error(exp)
self.session.rollback()
self.session.commit()
return deleted_items
@mydb_pool_session
def getAllByWechatidAndState(self, f_wechat_id, f_state):
"""
根据 f_wechat_id 和 f_state 过滤数据,并按照 f_wechat_id, f_parent_id, f_sort_order 排序。
:param f_wechat_id: 微信 ID
:param f_state: 状态
:return: 过滤并排序后的 TWechatMenuEntity 列表
"""
try:
logger.info(f"{self.__class__.__name__}.get_by_wechat_id_and_state")
filters = [
TWechatMenuEntity.f_wechat_id == f_wechat_id,
TWechatMenuEntity.f_state == f_state
]
query = self.session.query(TWechatMenuEntity).filter(and_(*filters))
query = query.order_by(
TWechatMenuEntity.f_wechat_id,
TWechatMenuEntity.f_parent_id,
TWechatMenuEntity.f_sort_order
)
return query.all()
except Exception as e:
logger.error(f"Error fetching TWechatMenuEntity records by f_wechat_id and f_state: {e}")
raise
@mydb_pool_session
def getAllByState(self, f_state):
"""
根据 f_state 过滤数据,并按照 f_wechat_id, f_parent_id, f_sort_order 排序。
:param f_wechat_id: 微信 ID
:param f_state: 状态
:return: 过滤并排序后的 TWechatMenuEntity 列表
"""
try:
logger.info(f"{self.__class__.__name__}.get_by_wechat_id_and_state")
filters = [
TWechatMenuEntity.f_state == f_state
#,
#or_(
# TWechatMenuEntity.f_upload_time == None, # 从未上传过的记录
# TWechatMenuEntity.f_upload_time < TWechatMenuEntity.f_crt_time # 上传时间早于创建时间的记录
#)
]
query = self.session.query(TWechatMenuEntity).filter(and_(*filters))
query = query.order_by(
TWechatMenuEntity.f_wechat_id,
TWechatMenuEntity.f_parent_id,
TWechatMenuEntity.f_sort_order
)
return query.all()
except Exception as e:
logger.error(f"Error fetching TWechatMenuEntity records by f_wechat_id and f_state: {e}")
raise
@mydb_pool_session
def isUpdated(self, f_wechat_id=None):
"""
检查是否存在需要上传的新记录(state=1 且 f_crt_time > f_upload_time)
输入参数:
f_wechat_id: 可选,微信ID,如果提供则只检查该微信ID的记录
输出参数:
bool: True表示存在需要上传的新记录,False表示没有
"""
try:
logger.info(f"{self.__class__.__name__}.has_new_records_to_upload")
filters = [
TWechatMenuEntity.f_state == 1,
TWechatMenuEntity.f_crt_time > TWechatMenuEntity.f_upload_time
]
if f_wechat_id is not None:
filters.append(TWechatMenuEntity.f_wechat_id == f_wechat_id)
query = self.session.query(TWechatMenuEntity).filter(and_(*filters))
# 使用exists()提高查询效率,只要找到一条记录就返回True
return self.session.query(query.exists()).scalar()
except Exception as e:
logger.error(f"Error checking for new records to upload: {e}")
raise
@mydb_pool_session
@mydb_transaction
def updateLastSyncTime(self, f_wechat_id=None):
"""
更新最后同步时间(f_upload_time字段为当前时间)
输入参数:
f_wechat_id: 可选,微信ID,如果提供则只更新该微信ID的记录
输出参数:
int: 更新的记录数
"""
try:
logger.info(f"{self.__class__.__name__}.update_last_sync_time")
from datetime import datetime
current_time = datetime.now()
# 构建更新条件
filters = [
TWechatMenuEntity.f_state == 1
]
if f_wechat_id is not None:
filters.append(TWechatMenuEntity.f_wechat_id == f_wechat_id)
# 执行更新
result = self.session.query(TWechatMenuEntity)\
.filter(and_(*filters))\
.update({"f_upload_time": current_time},
synchronize_session=False)
self.session.commit()
logger.info(f"成功更新了 {result} 条记录的同步时间")
return result
except Exception as e:
logger.error(f"更新同步时间失败: {e}")
self.session.rollback()
raise
2.2.2 TWechatMenuSyncLogService
#
# 此代码为自动化代码工具【JYGT-CODER】自动生成的文件,请勿手动编辑。
#
# 作者: 修炼者 7457222@qq.com
# 日期: 2025-03-25 11:54:08
#
from interface.db.db_jygt.connection.MydbPool import mydb_pool_session, mydb_transaction, mydb_copy
from interface.db.db_jygt.entity.TWechatMenuSyncLogEntity import TWechatMenuSyncLogEntity
import re
from sqlalchemy import or_, and_
from public.jygt_coder_loger import get_logger
logger = get_logger(__name__)
class TWechatMenuSyncLogService:
@mydb_pool_session
def getAll(self, page=0, per_page=10):
"""
获取所有 TWechatMenuSyncLogService 记录,支持分页。
:param page: 当前页码,默认为 1。
:param per_page: 每页记录数,默认为 10。
:return: 包含总记录数、总页数、当前页码和记录列表的字典。
"""
try:
logger.info(f"{self.__class__.__name__}.getAll")
items = self.session.query(TWechatMenuSyncLogEntity
).limit(per_page).offset(page * per_page).all()
total = self.session.query(TWechatMenuSyncLogEntity).count()
pages = (total + per_page - 1) // per_page
return {
'total': total,
'pages': pages,
'current_page': page,
'items': items
}
except Exception as e:
logger.error(f"Error fetching all TWechatMenuSyncLogEntity records: {e}")
raise
@mydb_pool_session
def _get_by_filters(self, filters: list) -> list:
"""
根据给定的过滤条件查询 TWechatMenuSyncLogEntity 记录。
:param filters: 过滤条件列表。
:return: 查询结果列表。
"""
try:
query = self.session.query(TWechatMenuSyncLogEntity).filter(and_(*filters))
return query.all()
except Exception as e:
logger.error(f"Error fetching TWechatMenuSyncLogEntity records with filters {filters}: {e}")
raise
@mydb_pool_session
def getByPrimary(self, f_id):
"""
根据 表t_wechat_menu_sync_log索引 PRIMARY 获取记录。
输入参数:
f_id
输出参数:
TWechatMenuSyncLogEntity对象或 None
"""
logger.info(f"{self.__class__.__name__}.getByPrimary")
filters = [
TWechatMenuSyncLogEntity.f_id == f_id,
1==1
]
results = self._get_by_filters(filters)
return results[0] if results else None
@mydb_pool_session
def getByIdxSyncTime(self, f_sync_time):
"""
根据 表t_wechat_menu_sync_log索引 idx_sync_time 获取记录。
输入参数:
f_sync_time
输出参数:
TWechatMenuSyncLogEntity列表或空
"""
logger.info(f"{self.__class__.__name__}.getByIdxSyncTime")
filters = [
TWechatMenuSyncLogEntity.f_sync_time == f_sync_time,
1==1
]
return self._get_by_filters(filters)
@mydb_pool_session
def getByIdxWechatId(self, f_wechat_id):
"""
根据 表t_wechat_menu_sync_log索引 idx_wechat_id 获取记录。
输入参数:
f_wechat_id
输出参数:
TWechatMenuSyncLogEntity列表或空
"""
logger.info(f"{self.__class__.__name__}.getByIdxWechatId")
filters = [
TWechatMenuSyncLogEntity.f_wechat_id == f_wechat_id,
1==1
]
return self._get_by_filters(filters)
@mydb_pool_session
@mydb_transaction
def replace(self, records):
"""
批量替换数据,当数据存在时更新,当数据不存在时增加。
:param records: 要替换的数据列表。
:return: 替换后的数据列表。
"""
if not isinstance(records, list):
records = [records]
requests = []
for record in records:
obj = TWechatMenuSyncLogEntity(**record)
if record.get('f_id'):
objOld = self.getByPrimary(f_id=record['f_id'])
if objOld is None:
self.session.add(obj)
requests.append(obj)
else:
mydb_copy(obj, objOld)
self.session.merge(objOld)
requests.append(objOld)
else:
self.session.add(obj)
requests.append(obj)
return requests
@mydb_pool_session
@mydb_transaction
def add(self, records):
"""
批量增加数据,当数据不存在时增加。
:param records: 要增加的数据列表。
:return: 增加后的数据列表。
"""
if not isinstance(records, list):
records = [records]
requests = []
for record in records:
obj = TWechatMenuSyncLogEntity(**record)
if record.get('f_id'):
objOld = self.getByPrimary(f_id=record['f_id'])
if objOld is None:
self.session.add(obj)
requests.append(obj)
else:
self.session.add(obj)
requests.append(obj)
return requests
@mydb_pool_session
@mydb_transaction
def update(self, records):
"""
批量修改数据,当数据存在时修改。
:param records: 要修改的数据列表。
:return: 修改后的数据列表。
"""
if not isinstance(records, list):
records = [records]
requests = []
for record in records:
obj = TWechatMenuSyncLogEntity(**record)
if record.get('f_id'):
objOld = self.getByPrimary(f_id=record['f_id'])
if objOld :
mydb_copy(obj, objOld)
self.session.merge(objOld)
requests.append(objOld)
return requests
@mydb_pool_session
def delete(self, f_ids):
"""
删除指定主键集合的记录。
输入参数:
单值 f_id
集合 [f_id,...]
输出参数:
删除的实体对象列表 [TWechatMenuSyncLogEntity]
"""
if not isinstance(f_ids, list):
f_ids = [f_ids]
deleted_items = []
for f_id in f_ids:
try:
oldRecord = self.getByPrimary(f_id=f_id)
if oldRecord is None:
continue
self.session.delete(oldRecord)
deleted_items.append(oldRecord)
except Exception as exp:
logger.error(exp)
self.session.rollback()
self.session.commit()
return deleted_items
2.2.3 TWechatMenuEntity
#
# 此代码为自动化代码工具【JYGT-CODER】自动生成的文件,请勿手动编辑。
#
# 作者: 修炼者 7457222@qq.com
# 日期: 2025-03-25 11:54:08
#
from sqlalchemy import Column, Integer, String, DateTime,Float,Text,BigInteger,Numeric,Date,Time,Boolean,LargeBinary,func
from sqlalchemy.orm import declarative_base
from datetime import datetime
Base = declarative_base()
class TWechatMenuEntity(Base):
__tablename__ = 't_wechat_menu'
f_id = Column(Integer, primary_key=True, nullable=False)
f_parent_id = Column(Integer, nullable=True)
f_name = Column(String(64), nullable=False)
f_type = Column(String(64), nullable=True)
f_key = Column(String(64), nullable=True)
f_url = Column(String(255), nullable=True)
f_appid = Column(String(64), nullable=True)
f_pagepath = Column(String(128), nullable=True)
f_media_id = Column(String(64), nullable=True)
f_sort_order = Column(Integer, nullable=True)
f_state = Column(String(255), nullable=True)
f_crt_time = Column(DateTime, nullable=True)
f_upload_time = Column(DateTime, nullable=True)
f_wechat_id = Column(String(64), nullable=True)
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
def __repr__(self):
attrs = ', '.join(f"{key}={value}" for key, value in vars(self).items() if not key.startswith('_'))
return f"TWechatMenuEntity({attrs})"
2.2.4 TWechatMenuSyncLogEntity
#
# 此代码为自动化代码工具【JYGT-CODER】自动生成的文件,请勿手动编辑。
#
# 作者: 修炼者 7457222@qq.com
# 日期: 2025-03-25 11:54:08
#
from sqlalchemy import Column, Integer, String, DateTime,Float,Text,BigInteger,Numeric,Date,Time,Boolean,LargeBinary,func
from sqlalchemy.orm import declarative_base
from datetime import datetime
Base = declarative_base()
class TWechatMenuSyncLogEntity(Base):
__tablename__ = 't_wechat_menu_sync_log'
f_id = Column(Integer, primary_key=True, nullable=False)
f_sync_time = Column(DateTime, nullable=False)
f_status = Column(String(255), nullable=False)
f_error_msg = Column(Text, nullable=True)
f_operator = Column(String(64), nullable=True)
f_wechat_id = Column(String(64), nullable=True)
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
def __repr__(self):
attrs = ', '.join(f"{key}={value}" for key, value in vars(self).items() if not key.startswith('_'))
return f"TWechatMenuSyncLogEntity({attrs})"
2.2.5 微信服务接口类
import json
import requests
from datetime import datetime
from interface.db.db_jygt.service.TWechatMenuService import TWechatMenuService
from interface.db.db_jygt.service.TWechatMenuSyncLogService import TWechatMenuSyncLogService
from config.config_auto import CONST_JYGT_CONFIG as JygtConfig
from interface.redis.SessionRedis import SessionRedis
from public.jygt_coder_loger import get_logger
logger = get_logger(log_file_path="log/wechat-menu.log",name=__name__)
class WeChatMenuService:
def __init__(self):
self.objSessionRedis = SessionRedis()
def _get_access_token(self, wechat_id):
wechatConfig = JygtConfig.get("wechat",{}).get(wechat_id,{})
ACCESS_TOKEN = self.objSessionRedis.get(wechatConfig["access_token_name"])
if ACCESS_TOKEN is None:
logger.error("WECHAT_ACCESS_TOKEN had lost")
return False
# 不加上这句貌似就不正确
ACCESS_TOKEN = ACCESS_TOKEN.decode("utf-8")
return ACCESS_TOKEN
def _build_menu_structure(self):
objTWechatMenuService = TWechatMenuService()
rows = objTWechatMenuService.getAllByState( 1)
# 构建菜单树
res_menu_map = {"jygt":{0: []},"bdm":{0: []},"xlz":{0: []}}
for row in rows:
menu_map = res_menu_map.get(row.f_wechat_id,None)
# 跳过不支持的wx服务号菜单
if(menu_map is None):
continue
item = {
key: value
for key, value in {
'name': row.f_name,
'type': row.f_type,
'key': row.f_key,
'url': row.f_url,
'appid': row.f_appid,
'pagepath': row.f_pagepath,
'media_id': row.f_media_id
}.items()
if value is not None # 或者 if value,根据你的"空"的定义
}
if row.f_parent_id == 0:
item['sub_button'] = [] # 确保sub_button总是存在
menu_map.setdefault(row.f_id, item)
if row.f_parent_id == 0:
menu_map[row.f_parent_id].append(menu_map.get(row.f_id))
else:
menu_map[row.f_parent_id]["sub_button"].append(item)
result ={}
for key,val in res_menu_map.items():
if val.get(0) is None or (len(val.get(0)) == 0): continue
result.setdefault(key, val.get(0))
return result
def _call_wechat_api(self, endpoint, wx_id, data=None):
"""调用微信API(强制使用UTF-8编码)"""
token = self._get_access_token(wx_id)
url = f"https://api.weixin.qq.com/cgi-bin/{endpoint}?access_token={token}"
logger.info(f"调用微信API: {url}")
headers = {
"Content-Type": "application/json; charset=utf-8", # 明确指定UTF-8编码
#"User-Agent": "Your-App/1.0" # 建议添加User-Agent
}
try:
# 手动序列化JSON,禁用ensure_ascii
json_data = json.dumps({"button":data}, ensure_ascii=False).encode('utf-8')
logger.info(f"发送的JSON数据: {json_data}")
response = requests.post(
url,
data=json_data, # 注意这里用data=而不是json=
headers=headers,
timeout=10
)
response.raise_for_status() # 检查HTTP错误
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"WeChat API请求失败: {e}")
return {"errcode": -1, "errmsg": str(e)}
def _log_sync_result(self,wechat_id, status=0, error_msg=None, operator='system'):
logger.info(f"开始记录同步结果 {wechat_id} {status} {error_msg} {operator}")
objLog = TWechatMenuSyncLogService()
objLog.add({"f_sync_time":datetime.now(),"f_status":status,"f_error_msg":error_msg,"f_operator":operator,"f_wechat_id":wechat_id})
def sync_menu(self):
"""执行完整同步流程"""
try:
# 1. 检查是否有未同步的变更
objTWechatMenuService = TWechatMenuService()
if not objTWechatMenuService.isUpdated() :
logger.info("没有未同步的变更,跳过同步菜单")
return
logger.info("有未同步的变更,开始同步菜单")
# 2. 构建菜单数据
menu_data = self._build_menu_structure()
for key,val in menu_data.items():
logger.info(f"开始同步菜单 {key} {val}")
# 3. 调用微信API
delete_result = self._call_wechat_api('menu/delete',key)
if delete_result.get('errcode') != 0:
logger.error(f"删除菜单失败: {delete_result}")
return
create_result = self._call_wechat_api('menu/create', key,val)
self._log_sync_result(key, operator='auto_sync',error_msg=str(create_result))
if create_result.get('errcode') != 0:
logger.error(f"创建菜单失败: {create_result}")
return
# 4. 记录成功日志
logger.info (f"创建菜单成功 {create_result}")
objTWechatMenuService.updateLastSyncTime(key)
except Exception as e:
logger.info(f"创建菜单异常 {str(e)}")
2.2.6 命令行测试函数
def wechat_menu():
from interface.wechat.wechat_menu import WeChatMenuService
wechatMenuService = WeChatMenuService()
wechatMenuService.sync_menu()
wechat_menu()
可执行验证下,是否正确更新菜单。
2.3 Airflow配置
Airflow可参考如下配置文件配置,然后编写DAGS,即可。
2.3.1 安装与配置
# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate
# 安装最新版本airflow
pip install apache-airflow
# 修改airflow.cfg配置
vi data/airflow.cfg
# 设定自己的工作目录 AIRFLOW_HOME
export AIRFLOW_HOME=/home/ubuntu/code/py-task-svr/
# 设定mysql的安装路径
export MYSQLCLIENT_CFLAGS="-I/usr/include/mysql/"
export MYSQLCLIENT_LDFLAGS="-L/usr/lib/x86_64-linux-gnu/ -lmysqlclient"
pip install mysqlclient
# 创建db_task_... 空库即可
airflow db migrate
# 创建用户
airflow users create \
--username xingming \
--firstname xing \
--lastname ming \
--role Admin \
--email co@aigrow.space
# 未用过
airflow users set-password --username xm --password xm1234
# 测试
airflow webserver --port 5001 &
# 配置nginx代理,详细参考nginx-agent-...
```
2.3.2 编写DAGS
from airflow import DAG
from airflow.operators.bash import BashOperator
from datetime import datetime, timedelta
default_args = {
'owner': 'airflow',
'depends_on_past': False,
'start_date': datetime(2024, 3, 15),
'retries': 1,
'retry_delay': timedelta(hours=1),
}
dag = DAG(
'fkx_task_of_bash_cmd',
default_args=default_args,
description='每小时执行一次本脚本,除了同样周期的任务',
schedule_interval='@hourly', # Run every hour
)
# Define the three Bash commands as tasks
wak_task = BashOperator(
task_id='wechat-access-token',
bash_command='/home/ubuntu/code/pywebsvr/bin/airflow_cmd.sh wak',
dag=dag,
)
# Set the task dependencies
wak_task