当前位置:首页>python>Python插件化架构设计:事件总线 + 运行时热插拔

Python插件化架构设计:事件总线 + 运行时热插拔

  • 2026-08-20 19:53:25
Python插件化架构设计:事件总线 + 运行时热插拔

阅读收益:掌握插件间解耦通信的标准方案、理解热插拔的实现原理与踩坑规避,直接应用于 Tkinter 桌面工具、上位机软件或任何需要动态扩展的 Python 项目。


🧩 从"加载插件"到"插件协作"

前两篇我们解决了插件的发现与加载问题——用 importlib 动态导入模块,用 PluginManager 统一管理生命周期。但实际项目跑起来之后,新的麻烦接踵而至:

插件 A 处理完数据,插件 B 怎么知道?

最直接的写法是让 A 直接调用 B 的方法。这玩意儿一开始看着没问题,但随着插件数量增加,依赖关系会变成一张蜘蛛网。改一个插件,另外五个跟着崩。更难受的是,如果某个插件没加载,直接调用就会抛 AttributeError,你还得到处加 try-except 防御。

另一个问题是运行时更新。开发阶段调试一个插件,改完代码得重启整个程序,Tkinter 的主窗口重新初始化一遍,所有状态全丢。在工控、上位机场景里,这个代价更大——设备连接、采集线程都得重建。

方案三正是针对这两个痛点:用事件总线解耦插件通信,用热插拔机制实现运行时替换


🔌 事件总线:插件间通信的标准解法

核心思想

事件总线的本质是发布-订阅模式的一个集中式实现。插件 A 不认识插件 B,只管向总线喊一句"数据更新了";插件 B 提前告诉总线"我对这类消息感兴趣";总线负责居中转发。双方完全不知道对方的存在。

这个模式在前端框架(Vue 的 EventBus)、消息队列(RabbitMQ)里都是标配,放到 Python 桌面应用里同样好用。

完整实现

from typing importCallableDictList
import logging  

logger = logging.getLogger(__name__)  


classEventBus:  
"""  
    轻量级事件总线,支持插件间解耦通信  
    发布者和订阅者互不知晓对方的存在  
    """

def__init__(self):  
self._handlers: Dict[strList[Callable]] = {}  

defsubscribe(self, event: str, handler: Callable):  
"""订阅事件(含防重复注册)"""
if event notinself._handlers:  
self._handlers[event] = []  
if handler notinself._handlers[event]:  
self._handlers[event].append(handler)  
            logger.debug(f"订阅事件: {event} -> {handler.__qualname__}")  

defunsubscribe(self, event: str, handler: Callable):  
"""取消订阅,插件卸载时务必调用"""
if event inself._handlers:  
self._handlers[event] = [  
                h for h inself._handlers[event] if h != handler  
            ]  
            logger.debug(f"取消订阅: {event} -> {handler.__qualname__}")  

defpublish(self, event: str, **kwargs):  
"""发布事件,所有订阅者都会收到通知"""
if event notinself._handlers:  
return
for handler inlist(self._handlers[event]):  
try:  
                handler(**kwargs)  
except Exception as e:  
                logger.error(  
f"事件处理器出错 [{event}]: {e}", exc_info=True
                )  

defget_subscriber_count(self, event: str) -> int:  
"""查询某事件当前订阅者数量(调试用)"""
returnlen(self._handlers.get(event, []))

几个设计细节值得关注:

  • • _handlers 用字典存列表:同一个事件可以有多个订阅者,顺序按订阅先后执行。
  • • publish 里的 try-except:某个处理器出错不会影响其他订阅者,这在多插件场景里非常重要。
  • • unsubscribe 用列表推导重建:比 remove() 更安全,不会因为重复订阅导致只删一个的问题。

插件如何接入

把 EventBus 实例放进 app_context,所有插件共享同一个总线:

# 宿主初始化时
from event_bus import EventBus

app_context = {
"event_bus": EventBus(),
# 其他共享资源...
}

插件 A 负责发布数据更新事件:

import random  
import threading  
import logging  

logger = logging.getLogger(__name__)  


classPlugin:  
"""  
    数据生产者插件  
    - 每隔一段时间生成一条模拟传感器数据  
    - 通过事件总线广播 "data.updated" 事件  
    - 不直接依赖任何消费者插件  
    """

    name = "DataProducer"

def__init__(self, ctx: dict, master):  
self.ctx = ctx  
self.master = master  
self._stop_event = threading.Event()  
self._thread: threading.Thread | None = None
self._after_id = None
self._interval_ms = 2000# 发布间隔(毫秒)  
self._counter = 0

# 构建 UI        
self._build_ui()  


def_build_ui(self):  
import tkinter as tk  
from tkinter import ttk  

self.frame = tk.LabelFrame(  
self.master,  
            text="📡  插件 A — 数据生产者 (DataProducer)",  
            padx=10, pady=8,  
            bg="
#1e2a38", fg="#7ecfff",  
            font=("Consolas"10"bold"),  
            relief="groove", bd=2
        )  

        tk.Label(  
self.frame,  
            text="模拟传感器数据发生器,每 2 秒向事件总线广播一次 data.updated",  
            bg="#1e2a38", fg="#adb5bd", font=("Consolas"9)  
        ).pack(anchor="w", pady=(06))  

        ctrl = tk.Frame(self.frame, bg="#1e2a38")  
        ctrl.pack(fill="x")  

self._btn_start = ttk.Button(ctrl, text="▶ 开始生产", command=self._start)  
self._btn_start.pack(side="left", padx=(06))  

self._btn_stop = ttk.Button(ctrl, text="⏹ 停止", command=self._stop_producing, state="disabled")  
self._btn_stop.pack(side="left")  

self._status_var = tk.StringVar(value="状态:待机")  
        tk.Label(  
self.frame,  
            textvariable=self._status_var,  
            bg="#1e2a38", fg="#52c41a", font=("Consolas"9)  
        ).pack(anchor="w", pady=(60))  

self._log_var = tk.StringVar(value="—")  
        tk.Label(  
self.frame,  
            textvariable=self._log_var,  
            bg="#1e2a38", fg="#ffd666", font=("Consolas"9)  
        ).pack(anchor="w")  


#  生命周期  
defload(self):  
        logger.info(f"[{self.name}] 已加载")  
self.frame.pack(fill="x", padx=12, pady=(104))  

defshow(self):  
self.frame.pack(fill="x", padx=12, pady=(104))  

defunload(self):  
"""卸载时停止所有后台任务"""
self._stop_producing()  
        logger.info(f"[{self.name}] 已卸载")  


#  数据生产逻辑                                                        
#    def _start(self):  
ifself._after_id isnotNone:  
return
self._btn_start.config(state="disabled")  
self._btn_stop.config(state="normal")  
self._status_var.set("状态:生产中 ●")  
self._schedule_next()  

def_stop_producing(self):  
ifself._after_id:  
try:  
self.frame.after_cancel(self._after_id)  
except Exception:  
pass
self._after_id = None
self._status_var.set("状态:已停止")  
try:  
self._btn_start.config(state="normal")  
self._btn_stop.config(state="disabled")  
except Exception:  
pass

def_schedule_next(self):  
"""用 after() 替代裸线程,天然线程安全"""
self._after_id = self.frame.after(self._interval_ms, self._produce_once)  

def_produce_once(self):  
self._after_id = None
self._counter += 1

        data = {  
"seq"self._counter,  
"temperature"round(random.uniform(20.085.0), 2),  
"humidity"round(random.uniform(30.095.0), 2),  
"voltage"round(random.uniform(3.05.5), 3),  
        }  

        msg = (f"[#{self._counter:04d}] "
f"T={data['temperature']}°C  "
f"H={data['humidity']}%  "
f"V={data['voltage']}V")  
self._log_var.set(f"最新发布 → {msg}")  
        logger.debug(f"[{self.name}] 发布: {data}")  

# 向事件总线广播,完全不知道谁在监听  
self.ctx["event_bus"].publish(  
"data.updated",  
            data=data,  
            source=self.name  
        )  

# 安排下一次  
self._schedule_next()

插件 B 订阅并响应,注意卸载时一定要取消订阅

import logging  
from collections import deque  

logger = logging.getLogger(__name__)  

MAX_RECORDS = 50# 最多保留条数  


classPlugin:  
"""  
    数据消费者插件  
    - 订阅 "data.updated" 事件  
    - 将收到的数据展示在 Tkinter 列表框中  
    - 完全不知道数据来自哪个插件  
    """

    name = "DataConsumer"

def__init__(self, ctx: dict, master):  
self.ctx = ctx  
self.master = master  
self._records: deque = deque(maxlen=MAX_RECORDS)  
self._build_ui()  


#  UI        

def_build_ui(self):  
import tkinter as tk  
from tkinter import ttk  

self.frame = tk.LabelFrame(  
self.master,  
            text="📊  插件 B — 数据消费者 (DataConsumer)",  
            padx=10, pady=8,  
            bg="#1e2a38", fg="#95de64",  
            font=("Consolas"10"bold"),  
            relief="groove", bd=2
        )  

        tk.Label(  
self.frame,  
            text="订阅 data.updated 事件,实时展示收到的传感器数据(最近 50 条)",  
            bg="#1e2a38", fg="#adb5bd", font=("Consolas"9)  
        ).pack(anchor="w", pady=(04))  

# 统计行  
self._stat_var = tk.StringVar(value="已接收:0 条")  
        tk.Label(  
self.frame,  
            textvariable=self._stat_var,  
            bg="#1e2a38", fg="#52c41a", font=("Consolas"9"bold")  
        ).pack(anchor="w")  

# 列表框 + 滚动条  
        list_frame = tk.Frame(self.frame, bg="#1e2a38")  
        list_frame.pack(fill="both", expand=True, pady=(40))  

        scrollbar = ttk.Scrollbar(list_frame, orient="vertical")  
        scrollbar.pack(side="right", fill="y")  

self._listbox = tk.Listbox(  
            list_frame,  
            height=8,  
            bg="#0d1117", fg="#c9d1d9",  
            selectbackground="#264f78",  
            font=("Consolas"9),  
            relief="flat", bd=0,  
            yscrollcommand=scrollbar.set
        )  
self._listbox.pack(side="left", fill="both", expand=True)  
        scrollbar.config(command=self._listbox.yview)  

# 清空按钮  
        ttk.Button(  
self.frame, text="🗑  清空记录", command=self._clear  
        ).pack(anchor="e", pady=(40))  


#  生命周期  
defload(self):  
"""加载时订阅事件总线"""
self.ctx["event_bus"].subscribe("data.updated"self._handle_data)  
        logger.info(f"[{self.name}] 已加载,订阅 data.updated")  
self.frame.pack(fill="both", expand=True, padx=12, pady=(410))  

defshow(self):  
self.frame.pack(fill="both", expand=True, padx=12, pady=(410))  

defunload(self):  
"""卸载时取消订阅,防止内存泄漏与重复响应"""
self.ctx["event_bus"].unsubscribe("data.updated"self._handle_data)  
        logger.info(f"[{self.name}] 已卸载,取消订阅 data.updated")  


#  事件处理  
def_handle_data(self, data: dict, source: str):  
"""收到 data.updated 事件时调用"""
self._records.append((data, source))  
        count = len(self._records)  

        line = (f"[#{data['seq']:04d}] "
f"来源={source}  "
f"T={data['temperature']}°C  "
f"H={data['humidity']}%  "
f"V={data['voltage']}V")  

self._listbox.insert(0, line)    # 最新记录插入顶部  
# 超出上限时删除末尾  
ifself._listbox.size() > MAX_RECORDS:  
self._listbox.delete(MAX_RECORDS)  

self._stat_var.set(f"已接收:{count} 条(来自 {source})")  
        logger.debug(f"[{self.name}] 处理事件: {data}")  

def_clear(self):  
self._listbox.delete(0"end")  
self._records.clear()  
self._stat_var.set("已接收:0 条")

这段代码有个容易被忽略的细节:unload() 里的 unsubscribe 调用。如果不清理,即便插件对象已经"卸载",它的方法引用仍然留在 _handlers 里,总线还会继续调用它,造成内存泄漏,甚至在插件重载后出现双重响应。


🔥 运行时热插拔:不重启、直接换

为什么需要热插拔

调试阶段改一行代码重启一次程序,这个成本在小工具里还能接受。但在以下场景里,重启代价就很高了:

  • • 上位机软件:设备连接、串口初始化需要几秒甚至更长
  • • 长时间运行的采集程序:重启会丢失当前采集状态
  • • 多人协作开发:某个模块更新不应该影响其他模块的运行

热插拔的目标是:在主程序不停止的情况下,卸载旧版插件、加载新版插件,并恢复之前的可见状态

核心实现

在 PluginManager 里增加 reload_plugin 方法:

import sys  
import json  
import logging  
import importlib  
from pathlib import Path  

logger = logging.getLogger(__name__)  

PLUGINS_DIR = Path(__file__).parent / "plugins"


classPluginManager:  
"""  
    插件生命周期管理器  
    负责发现、加载、卸载、热重载插件  
    """

def__init__(self, app_context: dict):  
self.ctx = app_context  
self._plugins: dict = {}          # name -> plugin instance  
self._plugin_meta: dict = {}      # name -> meta info (path, dir_name)  

#  发现插件  
defdiscover(self) -> list:  
"""扫描 plugins/ 目录,返回所有合法插件的元信息列表"""
        found = []  
ifnot PLUGINS_DIR.exists():  
            logger.warning(f"插件目录不存在: {PLUGINS_DIR}")  
return found  

for item insorted(PLUGINS_DIR.iterdir()):  
ifnot item.is_dir():  
continue
            plugin_file = item / "plugin.py"
            config_file = item / "config.json"
ifnot plugin_file.exists():  
continue

            config = {}  
if config_file.exists():  
try:  
                    config = json.loads(config_file.read_text(encoding="utf-8"))  
except Exception as e:  
                    logger.warning(f"读取 config.json 失败 [{item.name}]: {e}")  

            found.append({  
"dir_name": item.name,  
"path": item,  
"config": config,  
            })  
            logger.debug(f"发现插件: {item.name}")  

return found  


#  加载插件  
defload_plugin(self, dir_name: str, path: Path, master) -> object | None:  
"""动态加载单个插件,返回插件实例"""
        module_name = f"plugins.{dir_name}.plugin"
try:  
            spec = importlib.util.spec_from_file_location(  
                module_name, path / "plugin.py"
            )  
            module = importlib.util.module_from_spec(spec)  
            sys.modules[module_name] = module  
            spec.loader.exec_module(module)  

            plugin_cls = getattr(module, "Plugin")  
            instance = plugin_cls(self.ctx, master)  

ifhasattr(instance, "load"):  
                instance.load()  

            plugin_name = getattr(instance, "name", dir_name)  
self._plugins[plugin_name] = instance  
self._plugin_meta[plugin_name] = {  
"dir_name": dir_name,  
"path": path,  
            }  
            logger.info(f"插件已加载: {plugin_name}")  
return instance  

except Exception as e:  
            logger.error(f"加载插件失败 [{dir_name}]: {e}", exc_info=True)  
returnNone

#  卸载插件  
defunload_plugin(self, plugin_name: str) -> bool:  
"""安全卸载插件,调用其 unload() 并清理引用"""
        instance = self._plugins.get(plugin_name)  
ifnot instance:  
            logger.warning(f"找不到插件: {plugin_name}")  
returnFalse

try:  
ifhasattr(instance, "unload"):  
                instance.unload()  
except Exception as e:  
            logger.error(f"插件 unload() 出错 [{plugin_name}]: {e}", exc_info=True)  

# 销毁 Tkinter Frame(如果有)  
ifhasattr(instance, "frame"):  
try:  
                instance.frame.destroy()  
except Exception:  
pass

delself._plugins[plugin_name]  
        logger.info(f"插件已卸载: {plugin_name}")  
returnTrue

#  热重载    

defreload_plugin(self, plugin_name: str, master) -> bool:  
"""  
        热重载指定插件(不重启主程序)  
        核心步骤:卸载旧实例 → 清理 sys.modules 缓存 → 重新加载  
        """
        meta = self._plugin_meta.get(plugin_name)  
ifnot meta:  
            logger.warning(f"找不到插件元信息: {plugin_name}")  
returnFalse

# 记录可见状态  
        instance = self._plugins.get(plugin_name)  
        was_visible = False
if instance andhasattr(instance, "frame"):  
try:  
                was_visible = instance.frame.winfo_ismapped()  
except Exception:  
pass

        dir_name = meta["dir_name"]  
        path = meta["path"]  

# 卸载旧插件  
self.unload_plugin(plugin_name)  

# ★ 核心:清理 sys.modules 缓存,强制重新执行插件文件  
        keys_to_remove = [  
            k for k in sys.modules  
if k.startswith(f"plugins.{dir_name}")  
        ]  
for k in keys_to_remove:  
del sys.modules[k]  
            logger.debug(f"清理模块缓存: {k}")  

# 重新加载  
        new_instance = self.load_plugin(dir_name, path, master)  
if new_instance:  
if was_visible andhasattr(new_instance, "show"):  
                new_instance.show()  
            logger.info(f"插件热重载成功: {plugin_name}")  
returnTrue

        logger.error(f"插件热重载失败: {plugin_name}")  
returnFalse


defload_all(self, master) -> None:  
"""加载所有发现的插件"""
for meta inself.discover():  
self.load_plugin(meta["dir_name"], meta["path"], master)  

defget_plugin(self, name: str):  
returnself._plugins.get(name)  

deflist_plugins(self) -> list:  
returnlist(self._plugins.keys())

整个流程可以拆解为四步:

  1. 1. 查找并记录状态:拿到旧插件实例,记录它是否处于可见状态
  2. 2. 卸载旧插件:调用插件的 unload() 方法,清理事件订阅、停止线程
  3. 3. 清理模块缓存:从 sys.modules 删除旧模块,这是热重载的核心步骤
  4. 4. 重新加载:用 discover() 找到插件目录,重新执行 load_plugin(),并恢复可见状态

触发热重载

可以在宿主界面加一个"重载插件"按钮,也可以用文件监听自动触发:

# 手动触发示例(绑定到菜单或按钮)
defon_reload_plugin(plugin_name: str):
    success = plugin_manager.reload_plugin(plugin_name, root)
if success:
print(f"插件 {plugin_name} 热重载成功")
else:
print(f"插件 {plugin_name} 热重载失败,请检查日志")

🖼️运行效果

⚠️ 踩坑预警:这几个坑我替你踩过了

坑一:忘记清理 sys.modules

这是热重载里最常见的错误。Python 的模块导入有缓存机制,同一个模块路径第二次 import 时直接从 sys.modules 返回缓存,不会重新执行文件。

如果不清理,reload_plugin 里的 load_plugin 拿到的仍然是旧代码的模块对象,改了多少次文件都没用。

正确做法:用前缀匹配删掉所有相关模块,包括插件的子模块:

keys_to_remove = [
    k for k in sys.modules
if k.startswith(f"plugins.{plugin_name}")
]

坑二:旧线程没有停止

如果插件内部启动了后台线程或 after() 定时器,unload() 里必须把它们全部停掉。否则旧线程还在跑,新插件加载后会出现两个线程同时执行相同逻辑的诡异现象。

defunload(self):
# 停止后台线程
self._stop_event.set()
ifself._thread andself._thread.is_alive():
self._thread.join(timeout=2.0)

# 取消 Tkinter 定时器
ifself._after_id:
self.frame.after_cancel(self._after_id)

# 取消事件订阅
self.ctx["event_bus"].unsubscribe(
"data.updated"self._handle_data
    )

坑三:事件订阅重复注册

热重载时,如果 load() 方法里有 subscribe 调用,每次重载都会新增一个订阅。如果 unload() 没有对应的 unsubscribe,同一个处理器会被调用多次。

建议:在 subscribe 之前先 unsubscribe 做防御性清理,或者在 EventBus 里增加去重逻辑:

defsubscribe(self, event: str, handler: Callable):
if event notinself._handlers:
self._handlers[event] = []
# 防止重复订阅
if handler notinself._handlers[event]:
self._handlers[event].append(handler)
        logger.debug(f"订阅事件: {event} -> {handler.__qualname__}")

🏗️ 完整架构回顾

三个方案组合在一起,构成了一套完整的 Tkinter 插件化架构:

宿主程序 (Host)
├── app_context          ← 共享上下文,注入给所有插件
│   ├── event_bus        ← 事件总线,插件间通信
│   └── shared_data      ← 其他共享资源
├── PluginManager        ← 插件生命周期管理
│   ├── discover()       ← 扫描插件目录
│   ├── load_plugin()    ← 动态加载
│   ├── unload_plugin()  ← 安全卸载
│   └── reload_plugin()  ← 热重载(方案三新增)
└── plugins/
    ├── plugin_a/        ← 独立插件目录
    │   ├── plugin.py    ← 插件主文件
    │   └── config.json  ← 插件配置
    └── plugin_b/
        ├── plugin.py
        └── config.json

三个方案的分工很清晰:方案一解决"插件怎么被发现",方案二解决"插件怎么被管理",方案三解决"插件怎么通信"和"怎么不重启更新"。三者叠加,才算是一个完整的插件化架构。


💡 三句话总结

事件总线让插件之间从"直接依赖"变成"广播订阅",任何一个插件挂掉都不会拖垮其他人。

热插拔的本质是清理 sys.modules 缓存,让 Python 重新执行插件文件——这一步不做,改再多代码也是白改。

unload() 方法的质量决定了热插拔的稳定性,线程、定时器、事件订阅,一个都不能漏。


📌 可复用代码模板

以下两段代码可以直接复制进你的项目:

模板一:带防重复的 EventBus

defsubscribe(self, event: str, handler: Callable):
if event notinself._handlers:
self._handlers[event] = []
if handler notinself._handlers[event]:
self._handlers[event].append(handler)

模板二:插件 unload 标准写法

defunload(self):
self._stop_event.set()              # 停止线程信号
ifself._thread:
self._thread.join(timeout=2.0)  # 等待线程退出
ifself._after_id:
self.frame.after_cancel(self._after_id)  # 取消定时器
self.ctx["event_bus"].unsubscribe(  # 清理事件订阅
"data.updated"self._handle_data
    )

💬 聊聊你的实践

你在项目里用过类似的插件化架构吗?有没有遇到过热重载失败、事件重复触发这类问题?欢迎在评论区聊聊你的解决思路,或者分享一下你遇到过的其他坑。


#Python#Tkinter#插件化架构#设计模式#上位机开发

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-08-21 17:47:19 HTTP/2.0 GET : https://f.mffb.com.cn/a/506344.html
  2. 运行时间 : 0.215347s [ 吞吐率:4.64req/s ] 内存消耗:4,546.02kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=714686fe6c0454a35f0b46b29ee0b76e
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.001130s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001989s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000785s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000733s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001680s ]
  6. SELECT * FROM `set` [ RunTime:0.000616s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001821s ]
  8. SELECT * FROM `article` WHERE `id` = 506344 LIMIT 1 [ RunTime:0.001511s ]
  9. UPDATE `article` SET `lasttime` = 1787305640 WHERE `id` = 506344 [ RunTime:0.029594s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000740s ]
  11. SELECT * FROM `article` WHERE `id` < 506344 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001390s ]
  12. SELECT * FROM `article` WHERE `id` > 506344 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001279s ]
  13. SELECT * FROM `article` WHERE `id` < 506344 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.002593s ]
  14. SELECT * FROM `article` WHERE `id` < 506344 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.005302s ]
  15. SELECT * FROM `article` WHERE `id` < 506344 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.006332s ]
0.219017s