用Airflow基于MySQL数据驱动Python代码更新服务号菜单

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 

2.3.3 测试验证

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值