Deprecated: Creation of dynamic property Typecho\Widget\Request::$feed is deprecated in /www/wwwroot/blog.iletter.top/var/Widget/Archive.php on line 246
白荼日记 - 技术笔记
https://blog.iletter.top/index.php/category/ji-shu-bi-ji/
技术笔记,我的思考,bug还有一些奇奇怪怪的问题
-
typecho给后台admin界面添加图标分析
https://blog.iletter.top/index.php/archives/406.html
2025-07-27T02:32:00+08:00
觉得后台admin空荡荡的,索性加个图表自己魔改一下下。以下是完整的index.php<?php
include 'common.php';
include 'header.php';
include 'menu.php';
$stat = \Widget\Stat::alloc();
?>
<div class="main">
<div class="container typecho-dashboard">
<?php include 'page-title.php'; ?>
<div class="row typecho-page-main">
<div class="col-mb-12 welcome-board" role="main">
<p><?php _e('目前有 <em>%s</em> 篇文章, 并有 <em>%s</em> 条关于你的评论在 <em>%s</em> 个分类中.',
$stat->myPublishedPostsNum, $stat->myPublishedCommentsNum, $stat->categoriesNum); ?>
<!--<br><?php _e('点击下面的链接快速开始:'); ?></p>-->
<ul id="start-link" class="clearfix">
<?php if ($user->pass('contributor', true)): ?>
<li><a href="<?php $options->adminUrl('write-post.php'); ?>"><?php _e('撰写新文章'); ?></a></li>
<?php if ($user->pass('editor', true) && 'on' == $request->get('__typecho_all_comments') && $stat->waitingCommentsNum > 0): ?>
<li>
<a href="<?php $options->adminUrl('manage-comments.php?status=waiting'); ?>"><?php _e('待审核的评论'); ?></a>
<span class="balloon"><?php $stat->waitingCommentsNum(); ?></span>
</li>
<?php elseif ($stat->myWaitingCommentsNum > 0): ?>
<li>
<a href="<?php $options->adminUrl('manage-comments.php?status=waiting'); ?>"><?php _e('待审核评论'); ?></a>
<span class="balloon"><?php $stat->myWaitingCommentsNum(); ?></span>
</li>
<?php endif; ?>
<?php if ($user->pass('editor', true) && 'on' == $request->get('__typecho_all_comments') && $stat->spamCommentsNum > 0): ?>
<li>
<a href="<?php $options->adminUrl('manage-comments.php?status=spam'); ?>"><?php _e('垃圾评论'); ?></a>
<span class="balloon"><?php $stat->spamCommentsNum(); ?></span>
</li>
<?php elseif ($stat->mySpamCommentsNum > 0): ?>
<li>
<a href="<?php $options->adminUrl('manage-comments.php?status=spam'); ?>"><?php _e('垃圾评论'); ?></a>
<span class="balloon"><?php $stat->mySpamCommentsNum(); ?></span>
</li>
<?php endif; ?>
<?php if ($user->pass('administrator', true)): ?>
<li><a href="<?php $options->adminUrl('manage-posts.php'); ?>"><?php _e('文章管理'); ?></a></li>
<!--<li><a href="<?php $options->adminUrl('themes.php'); ?>"><?php _e('更换外观'); ?></a></li>-->
<li><a href="<?php $options->adminUrl('plugins.php'); ?>"><?php _e('插件管理'); ?></a></li>
<li><a href="<?php $options->adminUrl('options-general.php'); ?>"><?php _e('系统设置'); ?></a>
</li>
<?php endif; ?>
<?php endif; ?>
<!--<li><a href="<?php $options->adminUrl('profile.php'); ?>"><?php _e('更新我的资料'); ?></a></li>-->
</ul>
</div>
<div class="col-mb-12 col-tb-4" role="complementary">
<section class="latest-link">
<h3><?php _e('最近发布的文章'); ?></h3>
<?php \Widget\Contents\Post\Recent::alloc('pageSize=10')->to($posts); ?>
<ul>
<?php if ($posts->have()): ?>
<?php while ($posts->next()): ?>
<li>
<span><?php $posts->date('n.j'); ?></span>
<a href="<?php $posts->permalink(); ?>" class="title"><?php $posts->title(); ?></a>
</li>
<?php endwhile; ?>
<?php else: ?>
<li><em><?php _e('暂时没有文章'); ?></em></li>
<?php endif; ?>
</ul>
</section>
</div>
<div class="col-mb-12 col-tb-4" role="complementary">
<section class="latest-link">
<h3><?php _e('最近得到的回复'); ?></h3>
<ul>
<?php \Widget\Comments\Recent::alloc('pageSize=10')->to($comments); ?>
<?php if ($comments->have()): ?>
<?php while ($comments->next()): ?>
<li>
<span><?php $comments->date('n.j'); ?></span>
<a href="<?php $comments->permalink(); ?>"
class="title"><?php $comments->author(false); ?></a>:
<?php $comments->excerpt(35, '...'); ?>
</li>
<?php endwhile; ?>
<?php else: ?>
<li><?php _e('暂时没有回复'); ?></li>
<?php endif; ?>
</ul>
</section>
</div>
<!--<div class="col-mb-12 col-tb-4" role="complementary">-->
<!-- <section class="latest-link">-->
<!-- <h3><?php _e('官方最新日志'); ?></h3>-->
<!-- <div id="typecho-message">-->
<!-- <ul>-->
<!-- <li><?php _e('读取中...'); ?></li>-->
<!-- </ul>-->
<!-- </div>-->
<!-- </section>-->
<!--</div>-->
<!-- 图表显示区域 -->
<div class="col-mb-12 welcome-board">
<h3 style="color:black"><?php _e('文章统计数据'); ?></h3>
<div style="display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px;">
<!-- 月度发布图表 -->
<div style="flex: 1; min-width: 400px;">
<canvas id="monthlyPostChart" height="300"></canvas>
</div>
<!-- 状态分布图表 -->
<div style="flex: 1; min-width: 300px;">
<canvas id="statusChart" height="300"></canvas>
</div>
</div>
<div style="display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px;">
<!-- 分类文章统计 -->
<div style="flex: 1; min-width: 400px;">
<canvas id="categoryChart" height="300"></canvas>
</div>
<!-- 标签文章统计 -->
<div style="flex: 1; min-width: 400px;">
<canvas id="tagChart" height="300"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
<?php
include 'copyright.php';
include 'common-js.php';
?>
<script src="https://blog.iletter.top/usr/blog_img/chart.js"></script>
<script>
$(document).ready(function() {
<?php
// ==================== PHP 数据计算部分 ====================
// 1. 月度文章统计数据
$db = Typecho_Db::get();
// 月度文章统计
$select = $db->select()
->from('table.contents')
->where('type = ?', 'post')
->where('status = ?', 'publish');
$posts = $db->fetchAll($select);
$monthlyData = [];
$monthLabels = [];
$currentYear = date('Y');
$currentMonth = date('n');
// 初始化最近12个月的数据
for ($i = 11; $i >= 0; $i--) {
$year = $currentYear;
$month = $currentMonth - $i;
if ($month <= 0) {
$month += 12;
$year--;
}
$key = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT);
$monthlyData[$key] = 0;
$monthLabels[$key] = $year . '年' . $month . '月';
}
// 统计实际数据
foreach ($posts as $post) {
$created = $post['created'];
$year = date('Y', $created);
$month = date('m', $created);
$key = $year . '-' . $month;
if (isset($monthlyData[$key])) {
$monthlyData[$key]++;
}
}
// 2. 文章状态统计
$published = $db->fetchObject($db->select(['COUNT(*)' => 'count'])
->from('table.contents')
->where('type = ?', 'post')
->where('status = ?', 'publish'))->count;
$draft = $db->fetchObject($db->select(['COUNT(*)' => 'count'])
->from('table.contents')
->where('type = ?', 'post_draft')
->where('status = ?', 'publish'))->count;
// 3. 分类文章统计
$categories = $db->fetchAll($db->select()->from('table.metas')->where('type = ?', 'category'));
$categoryData = [];
$categoryLabels = [];
foreach ($categories as $category) {
$count = $db->fetchObject($db->select(['COUNT(*)' => 'count'])
->from('table.relationships')
->where('mid = ?', $category['mid']))->count;
if ($count > 0) { // 只显示有文章的分类
$categoryLabels[] = $category['name'];
$categoryData[] = (int)$count;
}
}
// 4. 标签文章统计 (取前10个)
$tags = $db->fetchAll($db->select()->from('table.metas')->where('type = ?', 'tag'));
$tagData = [];
$tagLabels = [];
foreach ($tags as $tag) {
$count = $db->fetchObject($db->select(['COUNT(*)' => 'count'])
->from('table.relationships')
->where('mid = ?', $tag['mid']))->count;
if ($count > 0) {
$tagLabels[] = $tag['name'];
$tagData[] = (int)$count;
}
}
// 按数量排序,取前10个
array_multisort($tagData, SORT_DESC, $tagLabels);
$topTagLabels = array_slice($tagLabels, 0, 10);
$topTagData = array_slice($tagData, 0, 10);
// 准备图表数据
$chartMonthlyData = [
'labels' => array_values($monthLabels),
'values' => array_values($monthlyData)
];
$chartStatusData = [
'published' => (int)$published,
'draft' => (int)$draft
];
$chartCategoryData = [
'labels' => $categoryLabels,
'values' => $categoryData
];
$chartTagData = [
'labels' => $topTagLabels,
'values' => $topTagData
];
?>
// ==================== JavaScript 图表渲染部分 ====================
// 1. 月度文章图表
var monthlyData = <?php echo json_encode($chartMonthlyData); ?>;
if (monthlyData.labels && monthlyData.labels.length > 0) {
var ctx1 = document.getElementById('monthlyPostChart').getContext('2d');
new Chart(ctx1, {
type: 'line',
data: {
labels: monthlyData.labels,
datasets: [{
label: '每月发文数',
data: monthlyData.values,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
},
plugins: {
title: {
display: true,
text: '月度文章发布趋势'
}
}
}
});
}
// 2. 文章状态图表
var statusData = <?php echo json_encode($chartStatusData); ?>;
if (statusData) {
var ctx2 = document.getElementById('statusChart').getContext('2d');
new Chart(ctx2, {
type: 'doughnut',
data: {
labels: ['已发布', '草稿'],
datasets: [{
data: [statusData.published, statusData.draft],
backgroundColor: [
'rgba(75, 192, 192, 0.8)',
'rgba(255, 205, 86, 0.8)',
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '文章状态分布'
},
legend: {
position: 'bottom'
}
}
}
});
}
// 3. 分类文章图表
var categoryData = <?php echo json_encode($chartCategoryData); ?>;
if (categoryData.labels && categoryData.labels.length > 0) {
var ctx3 = document.getElementById('categoryChart').getContext('2d');
new Chart(ctx3, {
type: 'bar',
data: {
labels: categoryData.labels,
datasets: [{
label: '文章数量',
data: categoryData.values,
backgroundColor: 'rgba(153, 102, 255, 0.8)',
borderColor: 'rgba(153, 102, 255, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
},
plugins: {
title: {
display: true,
text: '各分类文章数量'
}
}
}
});
}
// 4. 标签文章图表
var tagData = <?php echo json_encode($chartTagData); ?>;
if (tagData.labels && tagData.labels.length > 0) {
var ctx4 = document.getElementById('tagChart').getContext('2d');
new Chart(ctx4, {
type: 'bar',
data: {
labels: tagData.labels,
datasets: [{
label: '文章数量',
data: tagData.values,
backgroundColor: 'rgba(255, 159, 64, 0.8)',
borderColor: 'rgba(255, 159, 64, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
},
plugins: {
title: {
display: true,
text: '热门标签文章数量 (前10)'
}
}
}
});
}
});
</script>
<script>
$(document).ready(function () {
var ul = $('#typecho-message ul'), cache = window.sessionStorage,
html = cache ? cache.getItem('feed') : '',
update = cache ? cache.getItem('update') : '';
if (!!html) {
ul.html(html);
} else {
html = '';
$.get('<?php $options->index('/action/ajax?do=feed'); ?>', function (o) {
for (var i = 0; i < o.length; i++) {
var item = o[i];
html += '<li><span>' + item.date + '</span> <a href="' + item.link + '" target="_blank">' + item.title
+ '</a></li>';
}
ul.html(html);
cache.setItem('feed', html);
}, 'json');
}
function applyUpdate(update) {
if (update.available) {
$('<div class="update-check message error"><p>'
+ '<?php _e('您当前使用的版本是 %s'); ?>'.replace('%s', update.current) + '<br />'
+ '<strong><a href="' + update.link + '" target="_blank">'
+ '<?php _e('官方最新版本是 %s'); ?>'.replace('%s', update.latest) + '</a></strong></p></div>')
.insertAfter('.typecho-page-title').effect('highlight');
}
}
if (!!update) {
applyUpdate($.parseJSON(update));
} else {
$.get('<?php $options->index('/action/ajax?do=checkVersion'); ?>', function (o, status, resp) {
applyUpdate(o);
cache.setItem('update', resp.responseText);
}, 'json');
}
});
</script>
<?php include 'footer.php'; ?>
其他插件推荐:[post cid="405" /]
-
为typecho博客添加Gotify插件通知
https://blog.iletter.top/index.php/archives/405.html
2025-07-27T02:05:00+08:00
受限于typecho博客没有通知,自己写了一个博客有评论就通知的gotify插件,脚本使用之前的python改的。这是效果<?php
namespace TypechoPlugin\GotifyNotify;
use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Form\Element\Text;
// use Typecho\Widget\Helper\Form\Element\Radio; // 不再需要 Radio 元素
use Widget\Options;
use Typecho\Plugin\Exception as PluginException;
use Typecho\Widget\Exception as WidgetException;
use Typecho\Db\Exception as DbException;
use Utils;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 当用户评论时通过 Gotify 发送通知
*
* @package GotifyNotify
* @author 白荼
* @version 1.3.0
* @link https://gotify.net
*/
class Plugin implements PluginInterface
{
/**
* 激活插件方法
*
* @access public
* @return void
* @throws PluginException
*/
public static function activate()
{
// 监听评论完成事件 - 触发服务调用
\Typecho\Plugin::factory('Widget_Feedback')->finishComment = __CLASS__ . '::requestService';
\Typecho\Plugin::factory('Widget_Comments_Edit')->finishComment = __CLASS__ . '::requestService';
// 注册异步调用服务
\Typecho\Plugin::factory('Widget_Service')->sendGotify = __CLASS__ . '::sendGotify';
}
/**
* 禁用插件方法
*
* @access public
* @return void
* @throws PluginException
*/
public static function deactivate()
{
// 通常不需要显式移除
}
/**
* 获取插件配置面板
*
* @param Form $form 配置面板
* @access public
* @return void
*/
public static function config(Form $form)
{
// Gotify 服务器地址
$serverUrl = new Text('serverUrl', NULL, '', _t('Gotify 服务器地址'), _t('例如: http://your-gotify-server.com'));
$form->addInput($serverUrl->addRule('required', _t('Gotify 服务器地址不能为空')));
// 应用 Token
$appToken = new Text('appToken', NULL, '', _t('应用 Token'), _t('在 Gotify 中创建应用后获得的 Token'));
$form->addInput($appToken->addRule('required', _t('应用 Token 不能为空')));
// 通知标题
$title = new Text('title', NULL, '博客有新评论', _t('通知标题'), _t('收到新评论时的推送标题'));
$form->addInput($title);
// 消息优先级 (Priority) - 使用 Text 输入框
$priority = new Text('priority', NULL, '1', _t('消息优先级'), _t('设置 Gotify 消息的优先级 (自己在Gorify的定义)'));
$form->addInput($priority);
// 移除了 "是否启用" 的配置选项,插件默认启用
}
/**
* 个人用户的配置面板
*
* @param Form $form
* @access public
* @return void
*/
public static function personalConfig(Form $form)
{
// 个人配置通常为空
}
/**
* 评论通知回调 - 触发异步服务
* 这个方法在评论完成后被调用
*
* @access public
* @param $comment (Widget_Comments_Edit 或 Widget_Feedback 的实例)
* @return void
* @throws PluginException
*/
public static function requestService($comment)
{
// 检查评论对象是否有效
if (!isset($comment->coid) || !$comment->have()) {
error_log("GotifyNotify: Invalid comment object received in requestService.");
return;
}
$coid = $comment->coid;
$options = Options::alloc()->plugin('GotifyNotify');
// 移除了对 $options->enable 的检查,插件默认启用
// 检查必要配置
if (empty($options->serverUrl) || empty($options->appToken)) {
error_log("GotifyNotify: Missing server URL or app token, skipping notification for comment ID {$coid}.");
return;
}
// 调用 Widget_Service 中注册的异步方法
// 将评论 ID 传递给异步服务
try {
error_log("GotifyNotify: Calling async service for comment ID {$coid}.");
Utils\Helper::requestService('sendGotify', $coid);
} catch (\Exception $e) {
error_log("GotifyNotify: Failed to call async service for comment ID {$coid}. Error: " . $e->getMessage());
}
}
/**
* 异步发送 Gotify 通知
* 这个方法由 Widget_Service 调用
*
* @param integer $coid 评论ID
* @access public
* @return void
* @throws WidgetException
* @throws DbException
*/
public static function sendGotify(int $coid)
{
error_log("GotifyNotify Service: Starting to process comment ID {$coid}.");
// 获取插件配置
$options = Options::alloc()->plugin('GotifyNotify');
// 移除了对 $options->enable 的检查,插件默认启用
// 重新获取评论对象
try {
$commentWidget = Utils\Helper::widgetById('comments', $coid);
} catch (WidgetException $e) {
error_log("GotifyNotify Service: Failed to get comment widget for ID {$coid}. Error: " . $e->getMessage());
return;
}
if (!$commentWidget->have()) {
error_log("GotifyNotify Service: Comment widget is empty for ID {$coid}.");
return;
}
// 检查必要配置
if (empty($options->serverUrl) || empty($options->appToken)) {
error_log("GotifyNotify Service: Missing configuration for comment ID {$coid}.");
return;
}
try {
// 构造通知内容
$title = $options->title ?: '博客有新评论';
$message = self::buildMessage($commentWidget); // 传入 widget 对象
// 获取优先级设置
$priority = $options->priority ?? '1'; // 默认优先级为 1
error_log("GotifyNotify Service: Prepared message for comment ID {$coid}. Title: {$title}, Priority: {$priority}");
// 发送通知,传入优先级
self::sendGotifyMessage($options->serverUrl, $options->appToken, $title, $message, $priority);
error_log("GotifyNotify Service: Successfully sent notification for comment ID {$coid}.");
} catch (\Exception $e) {
// 记录详细错误信息
error_log("GotifyNotify Service: Failed to send notification for comment ID {$coid}. Error: " . $e->getMessage());
error_log("GotifyNotify Service: Stack trace: " . $e->getTraceAsString());
}
}
/**
* 构造通知消息内容
*
* @param \Widget\Base\Comments $comment 评论对象
* @return string 消息内容
*/
private static function buildMessage($comment)
{
$content = '';
if (is_object($comment) && $comment->have()) {
// 从评论对象获取信息
$author = $comment->author ?? '匿名用户';
$mail = $comment->mail ?? '';
$url = $comment->url ?? '';
$text = $comment->text ?? '';
// $ip = $comment->ip ?? ''; // Widget\Base\Comments 通常不直接暴露 IP
$content = "作者: {$author}\n";
if (!empty($mail)) {
$content .= "邮箱: {$mail}\n";
}
if (!empty($url)) {
$content .= "网站: {$url}\n";
}
// if (!empty($ip)) {
// $content .= "IP: {$ip}\n";
// }
$content .= "内容: {$text}\n";
$content .= "时间: " . date('Y-m-d H:i:s', $comment->created) . "\n"; // 使用评论创建时间
$content .= "文章: " . $comment->title . "\n"; // 文章标题
$content .= "链接: " . $comment->permalink . "\n"; // 评论链接
} else {
// 兼容其他情况
$content = "收到新评论,请登录后台查看 (评论ID: " . ($comment->coid ?? 'unknown') . ")";
}
return $content;
}
/**
* 发送 Gotify 消息
*
* @param string $serverUrl Gotify 服务器地址
* @param string $appToken 应用 Token
* @param string $title 消息标题
* @param string $message 消息内容
* @param string $priority 消息优先级
* @throws \Exception
*/
private static function sendGotifyMessage($serverUrl, $appToken, $title, $message, $priority = '1')
{
// 清理 URL
$serverUrl = rtrim($serverUrl, '/');
// 构造请求 URL (使用查询参数传递 token)
$url = $serverUrl . '/message';
// 准备表单数据,包含 priority
$data = [
'title' => $title,
'message' => $message,
'priority' => $priority // 使用传入的优先级
];
// 准备查询参数
$params = ['token' => $appToken];
$fullUrl = $url . '?' . http_build_query($params);
error_log("GotifyNotify: Sending request to {$fullUrl}");
error_log("GotifyNotify: POST data: " . print_r($data, true));
// 使用 cURL 发送请求
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $fullUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data); // 发送表单数据
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 根据你的服务器 SSL 配置调整
curl_setopt($ch, CURLOPT_USERAGENT, 'Typecho GotifyNotify Plugin/1.3.0');
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
error_log("GotifyNotify: cURL Response Code: {$httpCode}");
error_log("GotifyNotify: cURL Response Body: {$response}");
error_log("GotifyNotify: cURL Error (if any): {$error}");
// 检查响应
if ($response === false) {
throw new \Exception('cURL error: ' . $error);
}
if ($httpCode >= 200 && $httpCode < 300) {
// 成功
error_log("GotifyNotify: Message sent successfully.");
// 可以进一步检查返回的 JSON
$result = json_decode($response, true);
if (!$result) {
error_log("GotifyNotify: Warning - Could not decode JSON response, but HTTP status was OK.");
}
return; // 成功返回
} else {
// 失败
throw new \Exception('HTTP error: ' . $httpCode . ', response: ' . $response);
}
}
}
-
python每日检查网站ssl证书是否过期
https://blog.iletter.top/index.php/archives/402.html
2025-07-26T23:58:48+08:00
目的是检查网站是否过期,过期前几天进行通知import subprocess
from datetime import datetime, timedelta, timezone
import requests
from datetime import datetime
from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler()
# 发送通知请求
def send_msg_to_gotify(title, msg):
url = "http://152.136.153.72:8385/message"
params = {"token": "AI.53prwavAZsoC"}
current_time = datetime.now()
# 表单数据
data = {
"title": title,
"message": msg,
"priority": "0"
}
try:
response = requests.post(
url,
params=params,
data=data
)
print("Response Body:", response.text)
except requests.exceptions.RequestException as e:
print("请求失败:", e)
def check_ssl_certificate_expiration(web_site, out_date):
# 执行 openssl 命令获取证书信息
try:
result = subprocess.run(
["openssl", "x509", "-in", "fullchain.pem", "-noout", "-dates"],
capture_output=True,
text=True,
check=True,
cwd=f"C:\\Certbot\\live\\{web_site}"
)
except subprocess.CalledProcessError as e:
print("执行 openssl 命令失败:", e)
return
# 解析输出,提取 notAfter 日期
output = result.stdout
not_after_str = None
for line in output.splitlines():
if line.startswith("notAfter="):
not_after_str = line.split("=", 1)[1].strip()
break
if not not_after_str:
print("未找到 notAfter 信息")
return
# 解析日期字符串为 datetime 对象(使用 GMT 时间)
try:
date_format = "%b %d %H:%M:%S %Y GMT"
not_after_date = datetime.strptime(not_after_str, date_format).replace(tzinfo=timezone.utc)
except ValueError as e:
print("日期解析失败:", e)
return
# 获取当前 UTC 时间
current_date = datetime.now(timezone.utc)
# 计算时间差
delta = not_after_date - current_date
# 判断是否在 15 天内且未过期
if 0 <= delta.days <= out_date:
print(f"⚠️ SSL 证书({web_site})将在 {delta.days} 天后过期,请及时续期!")
send_msg_to_gotify('SSL即将过期', f'SSL 证书({web_site})将在 {delta.days} 天后过期,请及时更新并重启nginx服务')
elif delta.days < 0:
print(f"❌ SSL 证书({web_site})已过期!")
send_msg_to_gotify('SSL即将过期', f'SSL 证书({web_site})已过期,请及时更新并重启nginx服务')
else:
print(f"✅ SSL 证书({web_site})有效期超过 {out_date} 天,无需处理。")
# 执行检查
@scheduler.scheduled_job('cron', hour=8, minute=30, misfire_grace_time=3600)
def tick():
check_ssl_certificate_expiration('cx.sdasinfo.org.cn', 15)
try:
scheduler.start()
print('定时任务成功执行')
except Exception as e:
scheduler.shutdown()
print('定时任务执行失败')
finally:
exit()
-
去除宝塔界面一些烦人的东西
https://blog.iletter.top/index.php/archives/400.html
2025-07-25T13:18:02+08:00
更新之后越来越难用了,自己懒得配置nginx,mysql,这些环境,开心版的又怕后门,自己改后端的js文件看的脑瓜子疼,反正都是界面,直接js删掉一些dom元素就行了。有需要的可以拿走。// ==UserScript==
// @name 宝塔面板优化 - 界面美化
// @namespace http://tampermonkey.net/
// @version 1.3
// @description 移除指定 class 元素、替换特定 span 文字内容,并删除包含“需求反馈”的 span
// @match http://152.136.153.72:9999/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
console.log('⚠ 脚本准备加载');
// 定义需要移除的 class 组合
const selectorsToRemove = [
'.h-auto.mt-\\[1\\.2rem\\]', // h-auto mt-[1.2rem]
'.svgtofont-desired.\\!mr-4px.\\!text-\\[16px\\]', // svgtofont-desired !mr-4px !text-[16px]
'.icon-end-time-free.icon-unpaid-ltd', // icon-end-time-free icon-unpaid-ltd
'.el-card.is-always-shadow.is-always-shadow.\\!border-\\[\\#efefef\\]', // el-card is-always-shadow is-always-shadow !border-[#efefef]
'.icon-end-time.icon-unpaid-ltd' // icon-end-time icon-unpaid-ltd
];
// 移除指定 class 的元素
const removeElementsByClass = () => {
selectorsToRemove.forEach(selector => {
document.querySelectorAll(selector).forEach(el => el.remove());
});
};
// 替换指定 span 的文字内容
const replaceSpanText = () => {
const spanSelector = 'span.mr-1rem.ml-\\[-1\\.5rem\\]';
document.querySelectorAll(spanSelector).forEach(span => {
span.textContent = '精简版';
});
};
// 删除 span/button 包含 指定字符 的 span 元素
const removeSpansWithText = () => {
const spans = document.querySelectorAll('span');
spans.forEach(span => {
if (span.textContent.trim() == '需求反馈') {
span.remove();
}
});
const buttons = document.querySelectorAll('button');
buttons.forEach(span => {
if (span.textContent.trim() == '立即体验') {
span.remove();
}
});
};
// 删除指定class 包含置顶字符的元素
const removeClassWithText =()=>{
const parents = document.querySelectorAll('.header-child-tab');
parents.forEach(span => {
if (
span.textContent.trim() == '安全检测' ||
span.textContent.trim() == '违规词检测' ||
span.textContent.trim() == 'PHP网站安全' ||
span.textContent.trim() == '入侵防御' ||
span.textContent.trim() == '系统加固' ||
span.textContent.trim() == '扫描感知' ||
span.textContent.trim() == '日志审计' ||
span.textContent.trim() == 'SSH登录日志'
) {
span.remove();
}
});
const classparents = document.querySelectorAll('.el-collapse-item');
classparents.forEach(span => {
if (
span.textContent.trim().includes('top5')
) {
span.remove();
}
});
}
// 删除包含特定子元素的 el-col 元素
const removeParentIfChildExists = () => {
// 获取所有 el-col.el-col-6.wrapper-item 元素
const parents = document.querySelectorAll('.el-col.el-col-6.wrapper-item');
parents.forEach(parent => {
// 检查子元素是否包含指定的 class
const child = parent.querySelector('.absolute.-left-1\\.5.-top-1');
if (child) {
parent.remove(); // 移除父元素
}
});
};
// 综合执行函数
const removeElements = () => {
removeElementsByClass();
replaceSpanText();
removeSpansWithText();
removeParentIfChildExists();
removeClassWithText();
};
// 等待dom加载完毕后执行综合函数
let timeoutId;
const debouncedRemove = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(removeElements, 200);
};
let runCount = 0;
const maxRuns = 5;
const observer = new MutationObserver((mutations, obs) => {
if (runCount >= maxRuns) {
obs.disconnect();
return;
}
obs.disconnect();
debouncedRemove();
runCount++;
obs.observe(document.body, { childList: true, subtree: true });
});
observer.observe(document.body, { childList: true, subtree: true });
// 点击一次执行一次综合函数
let clickTimeout;
document.addEventListener('click', () => {
clearTimeout(clickTimeout);
clickTimeout = setTimeout(removeElements, 200);
});
// 初始执行(延迟 1 秒,等待 DOM 加载)
setTimeout(() => {
removeElements();
}, 1000);
})();
-
ubuntu server设置samba共享服务
https://blog.iletter.top/index.php/archives/399.html
2025-07-25T13:17:00+08:00
在 Ubuntu Server 上搭建 Samba 文件共享服务 是一个非常常见的需求,适用于局域网内的文件共享、打印机共享等场景。下面是一个完整教程 ,帮助你快速搭建一个基础的 Samba 服务,并允许其他用户访问你的共享目录。第一步:安装 Sambasudo apt update
sudo apt install samba -y安装完成后,Samba 会自动启动。第二步:备份默认配置文件(可选)Samba 的主配置文件位于:/etc/samba/smb.conf建议先做一个备份:sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.bak第三步:添加共享目录配置你可以选择共享一个现有目录,比如 /home/dellevin/shared。1. 创建共享目录(如果还没有)mkdir -p /home/dellevin/shared2. 编辑 Samba 配置文件sudo nano /etc/samba/smb.conf在文件末尾添加如下内容(以共享 shared 目录为例):[Shared]
comment = Shared Folder
path = /home/dellevin/shared
browseable = yes
read only = no
writable = yes
valid users = dellevin[Shared]:共享名称(客户端看到的名字)path:要共享的目录路径read only = no 和 writable = yes 表示允许写入valid users:允许访问该共享的用户第四步:设置 Samba 用户密码你需要为允许访问的用户设置 Samba 密码(即使该用户已存在 Linux 系统中):sudo smbpasswd -a dellevin系统会提示你输入并确认 Samba 密码。⚠️ 注意:这个密码可以和系统密码不同。第五步:重启 Samba 服务sudo systemctl restart smbd第六步:设置开机自启sudo systemctl enable smbd第七步:检查是否运行正常systemctl status smbd确保服务状态是 active (running)。第八步:从 Windows 或 Linux 客户端访问Windows 访问方式:打开“此电脑”或资源管理器,在地址栏输入:\\你的Ubuntu服务器IP地址例如:\\192.168.1.100然后输入用户名 dellevin 和你在 smbpasswd 中设置的密码即可访问。Linux 访问方式(如 Ubuntu 桌面):打开文件管理器(如 Nautilus),按下 Ctrl + L 输入:smb://192.168.1.100或者使用命令行挂载:sudo mount -t cifs //192.168.1.100/Shared /mnt/shared -o user=dellevin可选:配置防火墙(UFW)如果你启用了防火墙 UFW,需要开放 Samba 所需端口:sudo ufw allow 'Samba'总结:常用命令一览表操作命令安装 Sambasudo apt install samba配置文件位置/etc/samba/smb.conf添加共享目录在配置文件中添加[ShareName]块设置 Samba 用户密码sudo smbpasswd -a username重启 Sambasudo systemctl restart smbd开机自启sudo systemctl enable smbd查看服务状态systemctl status smbd客户端访问地址\\IP地址或smb://IP地址
-
linux注册服务为系统服务并开机自启
https://blog.iletter.top/index.php/archives/398.html
2025-07-15T22:22:23+08:00
因为安装了frp穿透服务,每次开启都要手动启动, 所以索性注册为系统服务,并实现开机自启。编写配置文件sudo vim /etc/systemd/system/frp-panel.service[Unit]
Description=FRP Panel Service
After=network.target
[Service]
User=root
WorkingDirectory=/home/dellevin/frp
ExecStart=/home/dellevin/frp/frp-panel client -s 4f448b4a-1c0f-486d-9f42-aed90f288da5 -i admin.c.home-services --api-url http://152.136.153.72:9000 --rpc-url grpc://152.136.153.72:9001
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target说明:User=:运行该服务的用户(这里是 root)WorkingDirectory=:程序运行时的工作目录ExecStart=:要执行的完整命令Restart=always:异常退出后自动重启RestartSec=10s:重启前等待 10 秒重新加载 systemd 配置sudo systemctl daemon-reexec
sudo systemctl daemon-reload启用服务开机自启sudo systemctl enable frp-panel.service启动服务(或重启系统测试)sudo systemctl start frp-panel.service查看运行状态:sudo systemctl status frp-panel.service如果显示 active (running),说明服务已成功运行。日志查看方式你可以使用 journalctl 查看服务日志:journalctl -u frp-panel.service -f取消开机自启的命令sudo systemctl disable 服务名称查看当前是否已取消成功你可以运行以下命令查看该服务的状态:systemctl is-enabled frp-panel如果输出是:disabled说明已经成功取消开机启动了
-
ubuntu-services挂载外置硬盘
https://blog.iletter.top/index.php/archives/397.html
2025-07-15T22:20:55+08:00
软件包升级sudo apt upgradelsblk命令用于列出所有可用的存储设备及其分区信息df -h 查看挂载点、文件系统类型、已用空间、可用空间等。root@dellevin-ubuntu:/# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 63.9M 1 loop /snap/core20/2318
loop1 7:1 0 87M 1 loop /snap/lxd/29351
loop2 7:2 0 38.8M 1 loop /snap/snapd/21759
sda 8:0 0 119.2G 0 disk
├─sda1 8:1 0 1M 0 part
├─sda2 8:2 0 2G 0 part /boot
└─sda3 8:3 0 117.2G 0 part
└─ubuntu--vg-ubuntu--lv 253:0 0 117.2G 0 lvm /
sdb 8:16 0 465.8G 0 disk
└─sdb1 8:17 0 465.8G 0 part
root@dellevin-ubuntu:/# df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 578M 1.2M 577M 1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv 115G 7.1G 102G 7% /
tmpfs 2.9G 0 2.9G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
/dev/sda2 2.0G 130M 1.7G 8% /boot
tmpfs 578M 4.0K 578M 1% /run/user/1000可以看到sdb并没有挂载sudo mkfs.ext4 /dev/sdb1 将这块硬盘格式化为 ext4 文件系统root@dellevin-ubuntu:/mnt# sudo mkfs.ext4 /dev/sdb1
mke2fs 1.46.5 (30-Dec-2021)
/dev/sdb1 contains a ntfs file system labelled '系统'
Proceed anyway? (y,N) y
Creating filesystem with 122096000 4k blocks and 30531584 inodes
Filesystem UUID: 86424462-d372-4894-89b2-3186bb52b237
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
102400000
Allocating group tables: done
Writing inode tables: done
Creating journal (262144 blocks):
done
Writing superblocks and filesystem accounting information: donesudo mount /dev/sdb1 /mnt/500Data 创建挂载点并挂载硬盘root@dellevin-ubuntu:/mnt# mkdir 500Data
root@dellevin-ubuntu:/mnt# ls
500Data
root@dellevin-ubuntu:/mnt# sudo mount /dev/sdb1 /mnt/500Data
root@dellevin-ubuntu:/mnt# df -h | grep sdb1
/dev/sdb1 458G 28K 435G 1% /mnt/500Data
root@dellevin-ubuntu:/mnt# df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 578M 1.2M 577M 1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv 115G 7.1G 102G 7% /
tmpfs 2.9G 0 2.9G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
/dev/sda2 2.0G 130M 1.7G 8% /boot
tmpfs 578M 4.0K 578M 1% /run/user/1000
/dev/sdb1 458G 28K 435G 1% /mnt/500Data设置开机自动挂载sudo blkid | grep /dev/sdb1 查看硬盘UUIDroot@dellevin-ubuntu:/mnt/500Data# sudo blkid | grep /dev/sdb1
/dev/sdb1: UUID="86424462-d372-4894-89b2-3186bb52b237" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="1ea79eea-01"vim /etc/fstab 然后编辑 fstab 添加一行:UUID=86424462-d372-4894-89b2-3186bb52b237 /mnt/500Data ext4 defaults 0 2
-
windows server 2019 docker安装
https://blog.iletter.top/index.php/archives/365.html
2025-07-14T17:11:31+08:00
windows server 2019 docker安装最近把家里的旧服务器搬到出租屋里面来了,准备当服务器用来着,二十年前的老机器了。想着用微信hook的功能,所以选择的windows server。配置好一些环境之后开始配置docker环境除了一些问题。启用Hyper-V和Containers功能运行管理员权限powershellInstall-WindowsFeature -Name Hyper-V,Containers -IncludeAllSubFeature -IncludeManagementTools -Verbose配置安装源Install-Module -Name DockerMsftProvider -Repository PSGallery -Verbose安装Docker运行管理员权限powershell,国内安装可能会因为网络原因失败,可以尝试手动安装,如果按照成功这下面步骤不用进行了Install-Package -Name docker -ProviderName DockerMsftProvider -Verbose下载文件PS C:\Users\Administrator\Desktop> Invoke-WebRequest -UseBasicParsing -OutFile D:\docker-28.3.2.zip https://download.docker.com/win/static/stable/x86_64/docker-28.3.2.zip文件下载地址https://download.docker.com/win/static/stable/x86_64/配置系统环境变量Path注册为系统服务dockerd --register-service -H npipe:// -H tcp://0.0.0.0:2375 --config-file "D:\Env\docker\config\daemon.json"配置文件内容D:\Env\docker\config\daemon.json{
"dns": ["114.114.114.114", "8.8.8.8"],
"data-root": "D:\\Env\\docker\\data",
"registry-mirrors": ["https://registry.docker-cn.com"]
}其他命令设置Docker开机启动Set-Service -Name docker -StartupType Automatic启动 Docker 服务Start-Service docker重启 Docker 服务Restart-Service Docker -Force停止 Docker 服务Stop-Service Dockerdocker-compose下载https://github.com/docker/compose/releases1.下载适用于 Windows 的 docker-compose-Windows-x86_64.exe 文件。2.将文件重命名为 docker-compose.exe 并移动到 Docker 安装目录(如 D:\Env\docker)ps:安装完了我才发现,windows server 2019的docker不支持linux的容器,要想弄,还要一个wsl支持,或者升级机器配置。想想还是算了,直接换ubuntu 的server版了
-
建筑类网站爬虫
https://blog.iletter.top/index.php/archives/364.html
2025-07-04T20:26:41+08:00
最近帮我同学写相关建筑类网站的爬虫以及前后端搜索界面功能,其实技术要点一个没有,涉及到加密的网站我也是放弃爬虫,解密太麻烦了。简单的网站都是一套的逻辑爬虫,大家可以参考一下。有兴趣的话帮忙点个start支持一下前后端系统以及数据库https://gitee.com/wonder19991209/mohurd\_search\_sys爬虫脚本https://gitee.com/wonder19991209/mohurd-spider
-
河北住建厅公告提取油猴脚本
https://blog.iletter.top/index.php/archives/361.html
2025-06-28T17:21:27+08:00
// ==UserScript==
// @name 河北住建厅公告提取
// @version 1.0
// @description 点击按钮提取河北住建厅信息,根据页面 URL 自动判断逻辑
// @author YourName
// @match https://zfcxjst.hebei.gov.cn/*
// @require https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function () {
'use strict';
// 创建按钮
const button = document.createElement('button');
button.innerText = '提取公告';
button.style.position = 'fixed';
button.style.top = '10px';
button.style.right = '10px';
button.style.zIndex = '99999';
button.style.padding = '8px 12px';
button.style.backgroundColor = '#007bff';
button.style.color = 'white';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
document.body.appendChild(button);
// 函数:提取河北住建厅公告数据
function extractHBZJTData_GongGaoGongShi(types) {
console.log("页面标题:", document.title);
const result = [];
const contentDiv = document.querySelector('div.pson_listWenhao');
if (!contentDiv) {
console.log("未找到目标 div 元素");
return;
}
contentDiv.querySelectorAll('li').forEach(li => {
const aTag = li.querySelector('a');
const dateSpan = li.querySelector('span.date');
const wenhaoSpan = li.querySelector('span.wenhao');
if (!aTag || !dateSpan) return;
let href = aTag.getAttribute('href');
let title = aTag.textContent.trim();
let wenhao = '';
if (wenhaoSpan) {
wenhao = wenhaoSpan.textContent.trim();
}
let fullUrl = href;
if (href.startsWith('/hbzjt')) {
fullUrl = new URL(href, 'https://zfcxjst.hebei.gov.cn').href;
}
result.push({
title: wenhao ? `[${wenhao}] ${title}` : title,
url: fullUrl,
date: dateSpan.textContent.trim(),
type:types
});
});
// result.forEach(item => {
// console.log(`${item.date}|${item.type} | ${item.title} -> ${item.url}`);
// });
// 发送数据到 API
sendToAPI(result);
}
function sendToAPI(dataArray) {
GM_xmlhttpRequest({
method: 'POST',
url: 'http://192.168.196.81:8081/sys_api/api/buildingspider/batch',
data: JSON.stringify(dataArray),
headers: {
'Content-Type': 'application/json'
},
onload: function(response) {
console.log('数据发送成功:', response.responseText);
alert('数据已成功发送到服务器!');
},
onerror: function(error) {
console.error('数据发送失败:', error);
alert('数据发送失败,请检查网络或服务器状态!');
}
});
}
// 函数:提取河北住建厅公告数据
function extractHBZJTData_XinWenZiXun(types) {
console.log("页面标题:", document.title);
const result = [];
const contentDiv = document.querySelector('div.pson_list');
if (!contentDiv) {
console.log("未找到目标 div 元素");
return;
}
contentDiv.querySelectorAll('li').forEach(li => {
const aTag = li.querySelector('a');
const dateSpan = li.querySelector('span.date');
if (!aTag || !dateSpan) return;
let href = aTag.getAttribute('href');
let title = aTag.textContent.trim();
let fullUrl = href;
if (href.startsWith('/hbzjt')) {
fullUrl = new URL(href, 'https://zfcxjst.hebei.gov.cn').href;
}
result.push({
title: title,
url: fullUrl,
date: dateSpan.textContent.trim(),
type:types
});
});
// result.forEach(item => {
// console.log(`${item.date}|${item.type} | ${item.title} -> ${item.url}`);
// });
// 发送数据到 API
sendToAPI(result);
}
// 备用函数(可根据需要自定义)
function fallbackFunction() {
console.log("不爬虫,页面标题:", document.title);
}
// 按钮点击事件
button.addEventListener('click', () => {
const currentUrl = window.location.href;
if ( currentUrl.includes('https://zfcxjst.hebei.gov.cn/hbzjt/zcwj/gggs/') ){
extractHBZJTData_GongGaoGongShi("河北省住房和城乡建设厅,公告公示");
}else if( currentUrl.includes('https://zfcxjst.hebei.gov.cn/hbzjt/zcwj/tfwj/') ){
extractHBZJTData_GongGaoGongShi("河北省住房和城乡建设厅,厅发文件");
}else if( currentUrl.includes('https://zfcxjst.hebei.gov.cn/hbzjt/zcwj/gfxwj/') ){
extractHBZJTData_GongGaoGongShi("河北省住房和城乡建设厅,厅发规范性文件");
}else if( currentUrl.includes('https://zfcxjst.hebei.gov.cn/hbzjt/xwzx/szyw/') ) {
extractHBZJTData_XinWenZiXun("河北省住房和城乡建设厅,时政要闻")
}else if( currentUrl.includes('https://zfcxjst.hebei.gov.cn/hbzjt/xwzx/jsyw/') ) {
extractHBZJTData_XinWenZiXun("河北省住房和城乡建设厅,建设要闻")
} else if( currentUrl.includes('https://zfcxjst.hebei.gov.cn/hbzjt/xwzx/sxdt/') ) {
extractHBZJTData_XinWenZiXun("河北省住房和城乡建设厅,市县动态")
}else if( currentUrl.includes('https://zfcxjst.hebei.gov.cn/hbzjt/xwzx/mtgz/') ) {
extractHBZJTData_GongGaoGongShi("河北省住房和城乡建设厅,媒体关注")
}else if( currentUrl.includes('https://zfcxjst.hebei.gov.cn/hbzjt/xwzx/zcjd/') ) {
extractHBZJTData_XinWenZiXun("河北省住房和城乡建设厅,政策解读")
} else {
fallbackFunction();
}
});
})();