在WordPress站点中展示阅读量等流量分析数据(超详细实现)
这篇文章也可以在我的博客中查看
关于本文
专业的流量统计系统能够相对真实地反应网站的访问情况。
这些数据可以在后台很好地进行分析统计,但有时我们希望在网站前端展示一些数据
最常见的情景就是:展示页面的浏览量
这简单的操作当然也可以通过简单的计数器实现,但可能会造成重复统计(比如同一个用户点击10次)
目标
流量分析工具所提供的准确性是不可比拟的
因此这篇文章我们就来实现如何将流量分析数据搬到网站展示,做到:
- 同步流量分析工具数据到网站前端
- 显示页面的阅读量
- 不影响页面加载
- 用户不会感知到同步任务进行
- 不频繁访问分析工具API
- 减少网络资源、API次数消耗
准备
为完成这些目标,需要一些前提准备:
- 配置好带有数据访问API的流量分析工具
- 如
Google Analytics
、Umami
(本文将以Umami为例) - 这是我们的真实数据来源
- 如
- 配置好WordPress后台进程(Background Process)支持
- 如Action-Scheduler(本文将以此为例)
- 这是我们非阻塞运行的基础
分析问题
Analytics类
分析问题
API访问频率
阅读量实时性并不强,我们无须(也不可能)每次页面访问都从远程分析工具获取数据
频繁访问很有可能会被禁止访问API,(自建的相当于DDoS攻击自己😅)
在获取数据后,应该在短时间内缓存起来
WordPress中的跨请求缓存API是
transient
处理缓存未命中
但如果缓存未命中怎么办?是立刻访问远程分析工具吗?
不可能,这样同步执行会使页面加载阻塞
特别是:如果你一次展示多篇文章,你需要等待它们全部完成才能加载出页面!
因此我们必须在本地数据库也持久化存储阅读量
这个冗余数据是缓存未命中时的唯一可行数据来源
在WordPress中,我们可以使用
post_meta
存储它
与此同时,这也可作为数据过时的标志:
我们应该触发更新阅读量的后台进程
非阻塞地将第三方分析工具的数据同步到本地上
小结
Analytics.php
的是用于页面获取数据的接口。它的数据来源是:
- 内存缓存
- 减少短期重复访问,减少服务器压力
- 本地数据库
- 缓存未命中时的保底数据
- 远程分析工具
- 数据更新的途径
它的职责是:
- 读写本地数据
- 发出更新请求
实现
注意组织文件结构,本文将
/App
文件夹作为根目录
在/App/Services/Analytics/
创建Analytics.php
文件
编写Analytics
类,它主要包含一些静态函数
namespace App\Services\Analytics {class Analytics{public static function getPageViews(WP_Post|int $post){}public static function setPageViews(WP_Post|int $postId, $newViews){}}
}
getPageViews
本文实现需要依赖$post->ID作为唯一标识符
如果你希望实现任何页面的阅读量展示,你需要:
- 使用
url[path]
的md5 hash
作为唯一标识符- 使用自定义数据库表存储阅读量:
(url_md5, page_view)
需要做什么?
当访客来访时,需要展示阅读量,此时:
- 我们需要获取目标地址的
WP_Post
实例- 以获取url等信息
- 有缓存读缓存
- 无缓存读数据库
- (不阻塞执行)请求第三方流量分析API,更新记录
- 马上使用旧数据刷新缓存
前面提到了缓存过期是发出数据同步请求的标志,但我们不希望重复发起请求,
因此缓存未命中时需要马上再次写入缓存。
虽然数据是旧的,但不急。我们可以在数据同步时强制刷新它
大部分都好处理,异步请求比较麻烦,先卖个关子
同时我们还为阅读量定义了缓存键值和在数据库的meta键值:
protected static string $pageViewMetaKey = 'page_views';
protected static int $pageViewCacheTime = HOUR_IN_SECONDS;
protected static function pageViewsCacheKey(int $postId)
{return static::$pageViewMetaKey . '_' . $postId;
}public static function getPageViews(WP_Post|int $post)
{if (!($post instanceof WP_Post))$post = get_post($post);if (empty($post)) return 0;// 尝试获取缓存$pageViews = get_transient(Analytics::pageViewsCacheKey($post->ID));if ($pageViews !== false) return $pageViews;// 记录更新请求// <-- ?? async call to update ?? -->// 读取数据库记录,这将是最后能够返回的值$pageViews = get_post_meta($post->ID, Analytics::$pageViewMetaKey, true) ?: 0;// 重写缓存set_transient(Analytics::pageViewsCacheKey($post->ID), $pageViews, static::$pageViewCacheTime);return $pageViews;
}
setPageViews
这个函数用于写入本地的数据存储,包括缓存和数据库
注意,它并不包含异步更新的过程,只是异步更新的结果需要借助它写入:
public static function setPageViews(WP_Post|int $postId, $newViews)
{if ($postId instanceof WP_Post)$postId = $postId->ID;// 更新缓存set_transient(Analytics::pageViewsCacheKey($postId), $newViews, static::$pageViewCacheTime);// 写到数据库update_post_meta($postId, Analytics::$pageViewMetaKey, $newViews);
}
Provider
好了,该想想怎么访问远程API了
Analytics
因为大多为固定操作,我们实现为静态
但是更新数据来源的逻辑呢?
不同的流量分析工具会提供不同的API,因此我们也需要为它们编写各自的处理逻辑
我们需要根据设置为Analytics
注入一个恰当的数据来源实例,这里称为Provider
先关注Analytics
类中需要如何支持注入Provider
没使用任何框架,我只能纯手工注入
以下代码是额外增加内容,需要与上文合并
class Analytics
{private static Closure|AnalyticsProvider $_provider;public static function setProvider(callable|AnalyticsProvider $provider){if (is_callable($provider))static::$_provider = Closure::fromCallable($provider);elsestatic::$_provider = $provider;}protected static function getProvider(): AnalyticsProvider{if (static::$_provider instanceof Closure)static::$_provider = (static::$_provider)();return static::$_provider;}
}
我们需要先setProvider
设置使用的数据源,后续使用getProvider
获取它
因为某些provider
可能会很沉重,这里支持传入一个返回AnalyticsProvider
的Closure
以实现懒加载,只有需要使用它的时候才会生成
接下来再看看provider
需要怎么编写
AnalyticsProvider类
不同的provider有不同的访问逻辑,但至少有没有些共性?
还真有!
需要未雨绸缪的问题
Provider负责组织后台任务,但每次请求更新都立刻组织一个后台任务还是很恐怖的。
比如:一个页面有100篇文章
每当Analytics::getPageViews
缓存未命中时,就组织后台任务
此时需要组织100个任务
因为php无守护进程,每个后台任务其实需要通过写数据库进行任务信息持久化
因此组织100个后台任务,意味着访问数据库上百次
而组织任务这个过程,是同步的、阻塞的
用户会看着页面转十秒加载不出来
但说到底,有没有必要把它视为100个任务?不能批处理一下吗?
当然可以,而且这就是不同AnalyticsProvider
的一个共性。
实现
在/App/Services/Analytics/
创建AnalyticsProvider.php
文件
编写Analytics
类
namespace App\Services\Analytics {abstract class AnalyticsProvider{}
}
pushUpdatePostViews
这是登记更新任务的逻辑
上文说了,我们不希望立刻生成后台任务,而是记录它:
protected array $updatesList = [];/*** 将目标加入浏览量更新任务队列* @param array $args 查询需要的参数,与具体实现有关*/
public function pushUpdatePostViews(WP_Post $post, array $args = [])
{$this->updatesList[$post->ID] = $args;
}
$args
主要是请求API时的参数,比如:时间段?目标地址?国家?……
这与具体数据源的实现有关,但总之,我们需要把这些可能用到的数据存到$updatesList
里
$updatesList
记录了本次请求中,所有需要请求阅读量更新的文章和相应参数
但我们如何把它加到后台任务?
submitTasks()
submitTasks由子类负责给出任务提交的逻辑
父类只需要给出约束
abstract public function submitTasks();
没完,我们需要有人在最后调用这个函数,才能完成所有任务一次性提交
可以利用WordPress的shutdown
hook
public function __construct()
{add_action('shutdown', [$this, 'submitTasks']);
}
因为shutdown是WordPress最后一个hook,因此不用担心之后还会有新的任务提交请求
注意,WordPress hook的回调必须是
public
函数
调用
还记得Analytics::getPageViews
的空缺位置吗?
它应该调用AnalyticsProvider
!
public static function getPageViews(WP_Post|int $post)
{// ...// <-- ?? async call to update ?? -->static::getProvider()->pushUpdatePostViews($post);// ...
}
注意:static
在上下文中就是Analytics
具体的AnalyticsProvider
主要完成两件事:
- 完成任务提交逻辑
- 封装处理参数
以下我以
Umami
为例
在/App/Services/Analytics/Umami
创建UmamiAnalyticsProvider.php
文件
编写UmamiAnalyticsProvider
类:
namespace App\Services\Analytics\Umami {use WP_Post;use App\Services\Analytics\AnalyticsProvider;class UmamiAnalyticsProvider extends AnalyticsProvider{public function submitTasks(){if ($this->updatesList) {// <-- ?? submit this background task ?? -->}}public function pushUpdatePostViews(WP_Post $post, array $args = []){$args['path'] = parse_url(get_permalink($post))['path'];parent::pushUpdatePostViews($post, $args);}}
}
Umami API
获取阅读量必须提供页面的path
,因此我重写pushUpdatePostViews
并按id
获取了它的path
submitTask
先检测了是否真有待提交任务数据,如有,提交
- 具体提交逻辑见下文
后台任务
万事俱备,只欠东风
我们只剩下后台任务需要解决了,但你先别急
这篇文章目前只到一半
本文将使用Action Scheduler
作为后台任务的驱动
但不管你是否使用它,后文的task
结构都可以给你一点灵感
Action-Scheduler
Action Scheduler
基本上是WordPress中支持后台进程的唯一选择了
它的官方例子如下:
require_once( plugin_dir_path( __FILE__ ) . '/libraries/action-scheduler/action-scheduler.php' );/*** Schedule an action with the hook 'eg_midnight_log' to run at midnight each day* so that our callback is run then.*/
function eg_schedule_midnight_log() {if ( false === as_has_scheduled_action( 'eg_midnight_log' ) ) {as_schedule_recurring_action( strtotime( 'tomorrow' ), DAY_IN_SECONDS, 'eg_midnight_log', array(), '', true );}
}
add_action( 'init', 'eg_schedule_midnight_log' );/*** A callback to run when the 'eg_midnight_log' scheduled action is run.*/
function eg_log_action_data() {error_log( 'It is just after midnight on ' . date( 'Y-m-d' ) );
}
add_action( 'eg_midnight_log', 'eg_log_action_data' );
这个例子将在每天午夜输出一个log
但这例子其实有个坑,Action Scheduler
的执行机制事实上跨越了2次php执行:
- 第一次,制定任务
- 使用
as_schedule_recurring_action
制定任务 - 此时
eg_midnight_log
hook无效
- 使用
- 第二次,午夜时执行任务(可能由cron或其它机制触发)
- 它从数据库中检测到预定的任务,生成
eg_midnight_log
hook - 执行
eg_midnight_log
hook的逻辑
- 它从数据库中检测到预定的任务,生成
所以坑点就在于add_action( 'eg_midnight_log', 'eg_log_action_data' );
必须在执行任务时加入,在制定任务时加入是无效的
而我们的目标,则是:
- 把2次php执行的代码尽可能地透明化,封装起来
- 使用面向对象的思想处理任务,使其模块化
TaskManager类
TaskManager
主要用于负责所有任务的提交和触发,我的实现主要针对Action Scheduler
,如果使用其它后台任务库,该类需要做对应修改。
在阅读前,建议先了解
Action Scheduler
的基本操作
实现
在/App/Services/Task
创建TaskManager.php
文件
编写TaskManager
类:
namespace App\Services\Task {class TaskManager{protected static array $taskList;public static function init(){}public static function registerTask($taskName){static::$taskList[] = $taskName;}public static function submitTask(string $handlerType, array $taskMeta, array $taskParams): int{}}
}
registerTask
用于记录所有需要管理的任务名,它的作用只是将名字加入$taskList
列表
submitTask
用于提交“保证任务触发时正常执行”所需的一切数据,包括:
- 交给谁处理(给谁处理)
- 执行处理的指引(怎么处理)
- 需要处理的数据(处理什么)
因此它需要传入3个参数:
$handlerType
: 承载任务处理逻辑的类名- 后文会详细介绍,它的基类是
Task
,包含一个handleTask
方法
- 后文会详细介绍,它的基类是
$taskMeta
: 承载任务处理的元数据- 比如任务时限?重试次数?
- 反正是与任务相关,但与任务执行主体无关的
$taskParams
: 任务执行所需的数据- 比如我们需要访问api,那可能就是api参数等等
因此可以写出这样的代码:
public static function submitTask(string $handlerType, array $taskMeta, array $taskParams): int
{if (!$handlerType) return 0;$args = ['handler' => $handlerType, 'meta' => $taskMeta, 'params' => $taskParams];return as_enqueue_async_action($handlerType::$taskName, $args, md5(json_encode($args)), true);
}
- 使用
Action Scheduler
提供的as_enqueue_async_action
,将任务数据移交至其托管。 - 所有
$args
参数将被Action Scheduler
存储于数据库,当执行时取出- 有点像序列化
$taskName
是Task
类的静态变量,表示任务名- 因为
Task
与任务直接关联,因此任务名就存在它那了
- 因为
- 防止完全重复任务
- 标记为唯一任务(第四个参数
unique:true
) - 计算参数的md5作为分组,用于识别重复任务
- 标记为唯一任务(第四个参数
init
init需要在每次执行、所有registerTask
调用结束后调用,它用于监听后台任务是否已触发,如果是,则分配到相应的处理函数
public static function init()
{require_once(get_template_directory() . '/vendor/woocommerce/action-scheduler/action-scheduler.php');/*** 监听事件触发并转交给handler*/foreach (static::$taskList as $taskName) {add_action($taskName, function (string $handlerType, array $meta, array $params) {$provider = new $handlerType();$provider->handleTask($meta, $params);}, 10, 3);}
}
首先需要引入Action Scheduler
文件,然后对每个注册的任务名,都使用监听函数(这里实现为匿名函数)订阅它的action hook
当事件触发时,这个函数将获得我们从TaskManager::submitTaask()
中传入的3个参数:
$handlerType
: 任务处理逻辑的类名- 用于动态生成负责处理事件的handler对象
$provider = new $handlerType();
- 调用它的
Task::handleTask
方法
- 用于动态生成负责处理事件的handler对象
$meta
: 承载任务处理的元数据- 将其转交给handler
$params
: 任务执行所需的数据- 将其转交给handler
当某个任务真正触发时,其对应的action hook
就会被触发,然后由监听函数转发至真正的执行逻辑
Task类-任务处理类
Task代表了一个任务,它包括:
任务名、任务提交逻辑、任务执行逻辑
实现
在/App/Services/Task
创建Task.php
文件
编写Task
类:
namespace App\Services\Task {use Exception;abstract class Task{public static string $taskName;/*** 提交一个该类型的任务,需要提供必要元数据和执行参数*/public static function submitTask(int $maxRetry, array $taskParams){}/*** 对应任务触发时的执行逻辑* @param mixed $taskMeta 任务元数据* @param mixed $taskParams 任务处理数据* @throws Exception 若任务未全部完成,抛出异常*/public function handleTask(array $taskMeta, array $taskParams){// ...$this->handle($taskParams);// ...}/*** 任务逻辑主体* @param mixed $taskParams 传入给该任务的参数* @return mixed */protected abstract function handle($taskParams);}
}
submitTask
submitTask()
是对TaskManager
提交函数的简单封装:
- 因为自身存储了
$taskName
,因此它可以省略TaskManager
的第一个参数 - 元数据可以明确限定
- 比如我只需要重试次数,我就只把它当做输入参数,然后封装成
meta
- 比如我只需要重试次数,我就只把它当做输入参数,然后封装成
具体编写为以下逻辑:
public static function submitTask(int $maxRetry, array $taskParams)
{$taskMeta = ['retry' => $maxRetry];TaskManager::submitTask(static::class, $taskMeta, $taskParams);
}
handleTask
前面也提到了,handleTask
是最终用于处理任务的逻辑
它其实有两个作用:
- 准备、善后处理
- 接受任务元数据,先进行准备
- 处理任务
- 接受任务参数,真正处理任务
在这里,“准备、善后”部分我只用作处理重试逻辑
处理任务的逻辑我把它分割到另一个handle
方法,由子类实现
handleTask
应在成功时返回假,失败时返回需要任务再次执行所需的参数
public function handleTask(array $taskMeta, array $taskParams)
{$pushBacks = $this->handle($taskParams);/*** 任务失败了,需要重新push任务:* 1. 有需要执行的东西* 2. 有retry的定义且不为0*/if (!empty($pushBacks)) {if (!empty($taskMeta['retry'])) {$taskMeta['retry'] -= 1;TaskManager::submitTask(static::class, $taskMeta, $pushBacks);throw new Exception("Retries have been scheduled for some uncompleted tasks. params are: " . var_export($pushBacks, true));} elsethrow new Exception("Some of tasks failed. params are: " . var_export($pushBacks, true));}
}
exception
将由Action Scheduler
处理并显示在控制台中
PageViewTask-具体的任务类
真正的功能类继承自Task类,这里需要编写访问远程分析工具,并返回页面浏览量的逻辑
因此命名为PageViewTask
同样地,具体的PageViewTask
依靠于具体的远程分析工具API
但在这层抽象中,我们只关注它们的共性:都需要失败重试
实现
在/App/Services/Analytics
创建PageViewTask.php
文件
编写PageViewTask
类:
namespace App\Services\Analytics {use App\Services\Task\Task;use Excecption;abstract class PageViewTask extends Task{public static string $taskName = 'nova_page_view_task';protected function handle($updatesList){foreach ($updatesList as $postId => $args) {try {$views = $this->getPostView($args);Analytics::setPageViews($postId, $views);// 删掉unset($updatesList[$postId]);} catch (\Exception $e) {// 无视}}return $updatesList;}abstract protected function getPostView($args): int;}
}
首先别忘了我们需要给任务起名$taskName
php的静态多态太爽了
C#什么时候能站起来()
handle()
这段逻辑呼应了我们远古时代实现的AnalyticsProvider::$updatesList
逻辑
我们为了节省开销,将多次阅读量更新捆绑成一次提交
因此$updatesList
包含的是一个列表的待更新文章
我们在foreach循环中分割成单个更新,再次踢皮球到getPostView
交给子类处理
然后更新过程中的try ctach
就有点秀了:
- 如果没出意外,我们把它从列表中移除,意为不再需要
- 如果出了意外,将被catch,并跳转到foreach下个循环
所以一顿操作后,最终执行失败的参数会保留在$updateList
中
将它返回,则会触发父类的重试逻辑,再次压入后台进程队列
妙妙妙妙妙
具体的PageViewTask
每个远程统计工具实现不同,所以这层是必须的
这里还是以Umami
为例,其它的也差不多,只是需要修改访问的参数
在/App/Services/Analytics/Umami
创建UmamiPageViewTask.php
文件
编写UmamiPageViewTask
类:
namespace App\Services\Analytics\Umami {use Exception;use App\Services\Analytics\PageViewTask;class UmamiPageViewTask extends PageViewTask{protected function getPostView($args): int{// 获取secret$baseUrl = of_get_option('analytics_api_domain', '');$authToken = of_get_option('analytics_api_token', '');// header$headers = array('Authorization' => "Bearer $authToken",'Content-Type' => 'application/json','Accept' => 'application/json',);// 向umami发送请求$umami_url = trailingslashit($baseUrl) . 'stats' . '?' . http_build_query(['startAt' => '0','endAt' => time() . '000','url' => $args['path'],]);$response = wp_remote_get($umami_url, ["headers" => $headers]);if (is_wp_error($response))throw new Exception($response->get_error_message());if (!empty($response['body']))$data = json_decode($response['body'], true);return \intval($data['uniques']['value']) ?? 0;}}
}
这段代码因为比较简单,也直接给出了
需要提醒的是:
- 重要数据不要硬编码在代码中,在WordPress中可以使用控制台的设置功能
- 不过这里用到的
of_get_option
是装了options framework
插件
- 不过这里用到的
- 大部分参数都可以自身构造而来,真正从外部接受的参数其实就只有:
$args['path']
- 我们在
$response
为WP_Error
时抛出异常,以示意出错- 出错的主要原因是网络连接不佳,因此我们需要抛出错误,并重试
- 返回401,404等不算出错,有返回的情况反而没有重试的必要
- 因为试几次都是一样的
- 返回的处理取决于返回数据,这里是顺着
Umami
的返回写的
化身为神的最后一块拼图!
ruaaaaaaaaaaaaaaaaaaaaa
还记得吗?之前的代码有一段空了一块
在UmamiAnalyticsProvider
提交任务时,没有给出具体的操作代码
因为当时还没引入后面的一堆
但现在,我们都是懂哥了
加入这句代码,让这个系统运作起来:
class UmamiAnalyticsProvider extends AnalyticsProvider
{public function submitTasks(){if ($this->updatesList) {// <-- ?? submit this background task ?? -->UmamiPageViewTask::submitTask(1, $this->updatesList);}}
}
调用UmamiPageViewTask::submitTask()
- 参数1:重试1次
- 参数2:更新若干文章的必要数据
初始化
最后,我们需要初始化TaskManager
,如果不初始化,没有任务会被监听
不管需不需要加入新任务,请确保每次php执行都会执行以下语句:
use App\Services\Analytics as Analytics;
use App\Services\Task\TaskManager;Analytics\Analytics::setProvider(new Analytics\Umami\UmamiAnalyticsProvider());
TaskManager::registerTask(Analytics\PageViewTask::$taskName);
TaskManager::init();
- 记得设置
Provider
,当然你也可以传入Closure
实现懒加载- e.g.
fn() => new UmamiAnalyticsProvider()
;
- e.g.
- 记得注册(
TaskManager::registerTask
)所有可能执行的任务- 注册开销并不大,不要省
- 省了任务绝对执行不了
- 在最后,记得调用
init()
,否则不会进行任何实质初始化操作
小结
花了好久,写了这么多
包括代码,包括文章
这过程中不止一次问自己,至于吗?
我最终的答案是肯定的
至于把东西封装到类里吗?多绕啊
确实绕,甚至是俄罗斯套娃
但在理解了绕之后,带来的是可拓展性、可维护性
当然也可以直接一步步写下来
实不相瞒,我第一个版本就是一步步写下去的,根本就没有一个类
但这样做,怎么进行拓展?
不同的代码混在一起,怎么维护?
所以就算是花更多时间,在把这坨屎跑起来之后,都要给它框架化、规则化
消化了这坨小屎,才能避免整个程序变成大屎
框架本身增加复杂性,但它也带来了规则性:
有了框架,就很容易借用相似的逻辑
有了框架,一切东西都井然有序
现在这个版本,你可以随意增加更多的Task,逻辑都是一样的
多舒服啊?
至于把问题想那么复杂吗?
至于访问远程统计工具获取精准数据吗?
至于搞缓存吗?
至于搞后台进程吗?
没错,要实现“显示浏览量”可以很简单
甚至不精准的统计数据,可以增加我网站的显示访问量(草,现在全是个位数)
但当把程序当做一种艺术,它就不能容忍凑合
精益求精,才是工匠精神
相关文章:
在WordPress站点中展示阅读量等流量分析数据(超详细实现)
这篇文章也可以在我的博客中查看 关于本文 专业的流量统计系统能够相对真实地反应网站的访问情况。 这些数据可以在后台很好地进行分析统计,但有时我们希望在网站前端展示一些数据 最常见的情景就是:展示页面的浏览量 这简单的操作当然也可以通过简单…...

学习 Iterator 迭代器
今天看到一个面试题, 让下面解构赋值成立。 let [a,b] {a:1,b:2} 如果我们直接在浏览器输出这行代码,会直接报错,说是 {a:1,b:2} 不能迭代。 看了es6文档后,具有迭代器的就一下几种类型,没有Object类型,…...
JVM---垃圾回收算法介绍
目录 分代收集理论 三种垃圾回收算法 标记-清除算法(最基础的、基本不用) 标记-复制算法 标记-整理算法 正式因为jvm有了垃圾回收机制,作为java开发者不会去特备关注内存,不像C和C。 优点:开发门槛低、安全 缺点…...

Ubuntu一直卡死的问题(20.04)
Ubuntu一直卡死的问题(18.04)_ubuntu频繁死机_Mr.Yi的博客-CSDN博客 我自己的解决方法: 1、首先强制关机重启后,直接打开命令行查看磁盘的使用: df -h发现/dev/loop都沾满了,我们能需要做的就是把他们清理干净 sud…...

自动化测试用例设计实例
在编写用例之间,笔者再次强调几点编写自动化测试用例的原则: 1、一个脚本是一个完整的场景,从用户登陆操作到用户退出系统关闭浏览器。 2、一个脚本脚本只验证一个功能点,不要试图用户登陆系统后把所有的功能都进行验证再退出系统…...

CSS3基础
CSS3在CSS2的基础上增加了很多功能,如圆角、多背景、透明度、阴影等,以帮助开发人员解决一些实际问题。 1、初次使用CSS 与HTML5一样,CSS3也是一种标识语言,可以使用任意文本编辑器编写代码。下面简单介绍CSS3的基本用法。 1.1…...
【栈】 735. 行星碰撞
735. 行星碰撞 解题思路 如果数组元素大于0 说明向右移动 那么不管 左边元素是不是大于0 都不会碰撞 如果数组元素小于0 说明想左边移动 那么判断左边元素 如果左边元素大于0 碰撞 那么遍历数组 当前元素大于0 直接入栈 如果当前元素小于0 判断栈顶元素是不是大于0 如果大…...

水库大坝安全监测MCU,提升大坝管理效率的利器!
水库大坝作为防洪度汛的重要设施,承担着防洪抗旱,节流发电的重要作用。大坝的安全直接关系到水库的安全和人民群众的生命财产安全。但因为水库大坝的隐患不易被察觉,发现时往往为时已晚。因此,必须加强对大坝的安全管理。其安全监…...

【vue2类型助手】vue2-cli 实现为 vue2 项目中的组件添加全局类型提示
实现 vue2 全局组件提示 vue2 项目全局注册组件直接使用没有提示 由于vue2中使用volar存在很大的性能问题,所以只能继续使用vetur,但是这样全局组件会没有提示,这对于开发来说,体验十分不友好,所以开发此cli并借助ve…...

mysql 索引 区分字符大小写
mysql 建立索引,特别是unique索引,是跟字符集、字符排序规则有关的。 对于utf8mb4_0900_ai_ci来说,0900代表Unicode 9.0的规范,ai表示accent insensitivity,也就是“不区分音调”,而ci表示case insensitiv…...

Stable Diffusion Webui源码剖析
1、关键python依赖 (1)xformers:优化加速方案。它可以对模型进行适当的优化来加速图片生成并降低显存占用。缺点是输出图像不稳定,有可能比不开Xformers略差。 (2)GFPGAN:它是腾讯开源的人脸修…...
为什么kafka 需要 subscribe 的 group.id?我们是否需要使用 commitSync 手动提交偏移量?
目录 一、为什么需要带有 subscribe 的 group.id二、我们需要使用commitSync手动提交偏移量吗?三、如果我想手动提交偏移量,该怎么做? 一、为什么需要带有 subscribe 的 group.id 消费概念: Kafka 使用消费者组的概念来实现主题的…...

什么是Web应用程序防火墙,WAF与其他网络安全工具差异在哪?
一、什么是Web 应用程序防火墙 (WAF) ? WAF软件产品被广泛应用于保护Web应用程序和网站免受威胁或攻击,它通过监控用户、应用程序和其他互联网来源之间的流量,有效防御跨站点伪造、跨站点脚本(XSS攻击)、SQL注入、DDo…...

打家劫舍 II——力扣213
动规 int robrange(vector<int>& nums, int start, int end){int first=nums[start]...

动手学深度学习—卷积神经网络LeNet(代码详解)
1. LeNet LeNet由两个部分组成: 卷积编码器:由两个卷积层组成;全连接层密集块:由三个全连接层组成。 每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均汇聚层;每个卷积层使用55卷积核和一个sigmoid激…...
腾讯面经总结
最近在准备面试,看了很多大厂的面经,抽空将腾讯面试的题目整理了一下,希望对大家有所帮助~ 一面 1、mysql索引结构? 2、redis持久化策略? 3、zookeeper节点类型说一下; 4、zookeeper选举机制ÿ…...
matlab机器人工具箱基础使用
资料:https://blog.csdn.net/huangjunsheng123/article/details/110630665 用vscode直接看工具箱api代码比较方便,代码说明很多 一、模型设置 1、基础效果 %采用机器人工具箱进行正逆运动学验证 a[0,-0.3,-0.3,0,0,0];%DH参数 d[0.05,0,0,0.06,0.05,…...
利用WonderLeak进行内存泄露检测【一】
1、下载地址: WonderLeak - Visual Studio Marketplace https://www.relyze.com/ 2、WonderLeak支持vs2017 2019扩展,或者单独启动 3、https://www.relyze.com/docs/wonderleak/help/w/overview/msvc_extension1.png 4、对于二进制程序来说支持以下…...
二刷LeetCode--155. 最小栈(C++版本),思维题
思路:本题需要使用两个栈,一个就是正常栈,执行出入操作,另一个栈只负责将对应的最小值进行保存即可.每次入栈的时候,最小值栈的栈顶也需要入栈元素,不过这个元素是最小值,那么就需要进行比较,因此在getmin()的时候只需要将最小值栈的栈顶元素弹出即可.初始化的时候只需要将最小…...
进程的状态与转换
进程在其生命周期内,由于系统中各进程之间的相互制约及系统的运行环境的变化,使得进程的状态也在不断地发生变化。通常进程有以下5种状态,前三种是基础讷航的基本状态 1)运行态。进程正在处理机上运行。在单处理机机中࿰…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1
每日一言 生活的美好,总是藏在那些你咬牙坚持的日子里。 硬件:OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写,"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...

群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...

Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
LangFlow技术架构分析
🔧 LangFlow 的可视化技术栈 前端节点编辑器 底层框架:基于 (一个现代化的 React 节点绘图库) 功能: 拖拽式构建 LangGraph 状态机 实时连线定义节点依赖关系 可视化调试循环和分支逻辑 与 LangGraph 的深…...
Oracle11g安装包
Oracle 11g安装包 适用于windows系统,64位 下载路径 oracle 11g 安装包...
用鸿蒙HarmonyOS5实现中国象棋小游戏的过程
下面是一个基于鸿蒙OS (HarmonyOS) 的中国象棋小游戏的实现代码。这个实现使用Java语言和鸿蒙的Ability框架。 1. 项目结构 /src/main/java/com/example/chinesechess/├── MainAbilitySlice.java // 主界面逻辑├── ChessView.java // 游戏视图和逻辑├──…...