Deprecated: Creation of dynamic property Typecho\Widget\Request::$feed is deprecated in /www/wwwroot/blog.iletter.top/var/Widget/Archive.php on line 253
白荼日记 - 技术笔记 2025-08-07T13:59:32+08:00 Typecho https://blog.iletter.top/feed/atom/tag/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/ <![CDATA[typecho博客Hello world插件增强]]> https://blog.iletter.top/archives/424.html 2025-08-07T13:59:32+08:00 2025-08-07T13:59:32+08:00 DelLevin https://blog.iletter.top 后台点击用户名称旁边的hello word会出现提示。

企业微信截图_17545462641024.png#B #S #R #60%

直接将该代码替换掉原本的代码即可:

<?php

namespace TypechoPlugin\HelloWorld;

use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Form\Element\Text;
use Widget\Options;
use Widget\Stat;
use Typecho\Db;

if (!defined('__TYPECHO_ROOT_DIR__')) {
    exit;
}

/**
 * Hello World
 *
 * @package HelloWorld
 * @author 白荼
 * @version 1.0.0
 */
class Plugin implements PluginInterface
{
    /**
     * 激活插件方法,如果激活失败,直接抛出异常
     */
    public static function activate()
    {
        \Typecho\Plugin::factory('admin/menu.php')->navBar = __CLASS__ . '::render';
        // 添加CSS和JS
        \Typecho\Plugin::factory('admin/header.php')->header = __CLASS__ . '::header';
    }

    /**
     * 禁用插件方法,如果禁用失败,直接抛出异常
     */
    public static function deactivate()
    {
    }

    /**
     * 获取插件配置面板
     *
     * @param Form $form 配置面板
     */
    public static function config(Form $form)
    {
        /** 分类名称 */
        $name = new Text('word', null, 'Hello World', _t('说点什么'));
        $form->addInput($name);
    }

    /**
     * 个人用户的配置面板
     *
     * @param Form $form
     */
    public static function personalConfig(Form $form)
    {
    }

    /**
     * 在header中添加CSS和JS
     */
    public static function header($header)
    {
        $customCssJs = '
<!-- HelloWorld Plugin CSS -->
<style>
.helloworld-tooltip {
    position: absolute;
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 15px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    z-index: 9999;
    min-width: 280px;
    display: none;
    font-size: 12px;
}
.helloworld-tooltip h4 {
    margin: 0 0 10px 0;
    padding: 0;
    color: #333;
    border-bottom: 1px solid #eee;
    padding-bottom: 8px;
    font-size: 14px;
}
.helloworld-tooltip .comparison {
    background: #f8f9fa;
    border-left: 3px solid #007cba;
    padding: 8px 12px;
    margin: 10px 0;
    border-radius: 0 4px 4px 0;
    font-size: 13px;
}
.helloworld-tooltip ul {
    list-style: none;
    padding: 0;
    margin: 0;
}
.helloworld-tooltip li {
    padding: 5px 0;
    border-bottom: 1px solid #f5f5f5;
    line-height: 1.5;
}
.helloworld-tooltip li:last-child {
    border-bottom: none;
}
.helloworld-tooltip strong {
    color: #666;
    font-weight: normal;
}
.helloworld-trigger {
    cursor: pointer;
    border-bottom: 1px dotted #BBBBBB;
}
.helloworld-trigger:hover {
    color: #666 !important;
    border-bottom-color: #666;
}
</style>
<!-- HelloWorld Plugin JS -->
<script>
(function() {
    function initHelloWorldTooltip() {
        var trigger = document.querySelector(".helloworld-trigger");
        var tooltip = document.querySelector(".helloworld-tooltip");
        
        if (trigger && tooltip) {
            trigger.addEventListener("click", function(e) {
                e.preventDefault();
                e.stopPropagation();
                
                // 计算位置
                var rect = trigger.getBoundingClientRect();
                var tooltipWidth = tooltip.offsetWidth || 280;
                var tooltipHeight = tooltip.offsetHeight || 250;
                
                // 默认位置
                var left = rect.left + window.scrollX;
                var top = rect.bottom + window.scrollY + 5;
                
                // 边界检测
                if (left + tooltipWidth > window.innerWidth + window.scrollX) {
                    left = window.innerWidth + window.scrollX - tooltipWidth - 10;
                }
                if (top + tooltipHeight > window.innerHeight + window.scrollY) {
                    top = rect.top + window.scrollY - tooltipHeight - 5;
                }
                
                tooltip.style.left = left + "px";
                tooltip.style.top = top + "px";
                tooltip.style.display = "block";
            });
            
            // 点击其他地方关闭
            document.addEventListener("click", function(e) {
                if (!tooltip.contains(e.target) && e.target !== trigger) {
                    tooltip.style.display = "none";
                }
            });
        }
    }
    
    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", initHelloWorldTooltip);
    } else {
        initHelloWorldTooltip();
    }
})();
</script>';

        return $header . $customCssJs;
    }

    /**
     * 根据字数获取对应的文学著作比较
     */
    private static function getLiteraryComparison($wordCount)
    {
        // 文学著作字数对照表(字数 => 著作名称)
        $literaryWorks = [
            // 百字级
            50 => '几行诗句',
            100 => '一段朋友圈文字',
            200 => '一条微博内容',
            500 => '一篇微博长文',
            
            // 千字级
            1000 => '一页A4纸手写内容',
            1500 => '一页打印稿',
            2000 => '一篇标准作文',
            3000 => '一篇博客文章',
            5000 => '一篇短篇小说',
            8000 => '一篇中篇小说开头',
            
            // 万字级(中国现代文学)
            10000 => '《呐喊》',
            12000 => '《彷徨》',
            15000 => '《朝花夕拾》',
            20000 => '《野草》',
            25000 => '《故事新编》',
            30000 => '《热风》',
            35000 => '《坟》',
            40000 => '《而已集》',
            45000 => '《三闲集》',
            
            // 万字级(其他现代文学)
            50000 => '《沉沦》(郁达夫)',
            55000 => '《春风沉醉的晚上》',
            60000 => '《迟桂花》',
            65000 => '《茑萝集》',
            70000 => '《过去》',
            75000 => '《薄奠》',
            80000 => '《微雪的早晨》',
            
            // 万字级(长篇小说选段)
            85000 => '《骆驼祥子》前半部分',
            90000 => '《二马》',
            95000 => '《离婚》',
            100000 => '《骆驼祥子》',
            110000 => '《四世同堂》第一部分',
            120000 => '《边城》',
            130000 => '《湘行散记》',
            140000 => '《长河》',
            150000 => '《家》',
            160000 => '《憩园》',
            170000 => '《第四病室》',
            180000 => '《春》',
            190000 => '《秋》',
            200000 => '《寒夜》',
            
            // 20万字级
            210000 => '《激流三部曲》选段',
            220000 => '《爱情三部曲》选段',
            230000 => '《抗战三部曲》选段',
            240000 => '《人间词话》',
            250000 => '《人间喜剧》选段',
            260000 => '《巴黎圣母院》',
            270000 => '《悲惨世界》选段',
            280000 => '《九三年》',
            290000 => '《海上劳工》',
            300000 => '《笑面人》',
            
            // 30万字级
            310000 => '《约翰·克利斯朵夫》选段',
            320000 => '《红与黑》',
            330000 => '《巴马修道院》',
            340000 => '《吕西安·娄凡》',
            350000 => '《阿尔芒斯》',
            360000 => '《法尼娜·法尼尼》',
            370000 => '《瓦尼娜·瓦尼尼》',
            380000 => '《红屋骑士》',
            390000 => '《卡斯特罗修道院院长》',
            400000 => '《意大利遗事》',
            
            // 40万字级
            410000 => '《三个火枪手》',
            420000 => '《基督山伯爵》',
            430000 => '《黑郁金香》',
            440000 => '《二十年后》',
            450000 => '《布拉热洛纳子爵》',
            460000 => '《玛戈王后》',
            470000 => '《蒙梭罗夫人》',
            480000 => '《四十五卫士》',
            490000 => '《侠隐记》',
            500000 => '《红楼梦》(精简版)',
            
            // 50万字级
            520000 => '《水浒传》选段',
            540000 => '《西游记》选段',
            560000 => '《三国演义》选段',
            580000 => '《儒林外史》',
            600000 => '《聊斋志异》选段',
            620000 => '《阅微草堂笔记》',
            640000 => '《子不语》',
            660000 => '《夜雨秋灯录》',
            680000 => '《萤窗异草》',
            700000 => '《耳食录》',
            
            // 70万字级
            720000 => '《浮生六记》',
            740000 => '《影梅庵忆语》',
            760000 => '《秋灯琐忆》',
            780000 => '《香艳丛书》选段',
            800000 => '《太平广记》选段',
            820000 => '《夷坚志》选段',
            840000 => '《剪灯新话》',
            860000 => '《剪灯余话》',
            880000 => '《觅灯因话》',
            900000 => '《喻世明言》',
            
            // 百万字级
            920000 => '《警世通言》',
            940000 => '《醒世恒言》',
            960000 => '《初刻拍案惊奇》',
            980000 => '《二刻拍案惊奇》',
            1000000 => '《红楼梦》',
            1050000 => '《镜花缘》',
            1100000 => '《老残游记》',
            1150000 => '《孽海花》',
            1200000 => '《官场现形记》',
            1250000 => '《二十年目睹之怪现状》',
            
            // 百万字级以上
            1300000 => '《资本论》第一卷',
            1400000 => '《莎士比亚十四行诗集》',
            1500000 => '《战争与和平》',
            1600000 => '《安娜·卡列尼娜》',
            1700000 => '《复活》',
            1800000 => '《静静的顿河》',
            1900000 => '《被开垦的处女地》',
            2000000 => '《一个人的遭遇》',
            2100000 => '《静静的顿河》全本',
            2200000 => '《钢铁是怎样炼成的》',
            2300000 => '《青年近卫军》',
            2400000 => '《日瓦戈医生》',
            2500000 => '《追忆似水年华》第一卷',
            2600000 => '《约翰·克利斯朵夫》',
            2700000 => '《马丁·伊登》',
            2800000 => '《美国悲剧》',
            2900000 => '《嘉莉妹妹》',
            3000000 => '《资本论》全三卷',
            3100000 => '《莎士比亚全集》悲剧部分',
            3200000 => '《莎士比亚全集》喜剧部分',
            3300000 => '《莎士比亚全集》历史剧部分',
            3400000 => '《莎士比亚十四行诗全集》',
            3500000 => '《莎士比亚全集》',
            3600000 => '《追忆似水年华》第二卷',
            3700000 => '《追忆似水年华》第三卷',
            3800000 => '《追忆似水年华》第四卷',
            3900000 => '《追忆似水年华》第五卷',
            4000000 => '《追忆似水年华》',
            4200000 => '《堂吉诃德》',
            4400000 => '《神曲》',
            4600000 => '《荷马史诗》',
            4800000 => '《失乐园》',
            5000000 => '《大英百科全书》一卷',
            5500000 => '《大英百科全书》两卷',
            6000000 => '《牛津英语词典》一卷',
            6500000 => '《不列颠百科全书》',
            7000000 => '《中国大百科全书》一卷',
            7500000 => '《辞海》',
            8000000 => '《康熙字典》',
            9000000 => '《中华大字典》',
            10000000 => '《大英百科全书》全套',
            12000000 => '《四库全书》选段',
            15000000 => '《永乐大典》残卷',
            20000000 => '《古今图书集成》选段'
        ];
    
        // 找到最接近的著作
        $closestWork = '几行文字';
        $closestWords = 0;
        
        foreach ($literaryWorks as $words => $work) {
            if ($wordCount >= $words) {
                $closestWork = $work;
                $closestWords = $words;
            } else {
                break;
            }
        }
    
        // 计算倍数
        if ($closestWords > 0) {
            $multiple = round($wordCount / $closestWords, 1);
            if ($multiple >= 2) {
                return "约等于 {$multiple} 本 {$closestWork} 的字数";
            } else {
                return "约等于 1 本 {$closestWork} 的字数";
            }
        } else {
            return "约等于几行文字的字数";
        }
    }

    /**
     * 获取统计信息
     */
    private static function getStats()
    {
        $db = Db::get();
        $user = \Typecho\Widget::widget('Widget_User');
        
        // 最近登录时间
        $lastLogin = $user->logged;
        $lastLoginText = $lastLogin ? date('Y-m-d H:i:s', $lastLogin) : '未知';
        
        // 文章数量
        $postCount = $db->fetchObject($db->select(['COUNT(cid)' => 'num'])
            ->from('table.contents')
            ->where('type = ?', 'post')
            ->where('authorId = ?', $user->uid))->num;
        
        // 字数统计(简单统计标题和内容长度)
        $posts = $db->fetchAll($db->select('title', 'text')
            ->from('table.contents')
            ->where('type = ?', 'post')
            ->where('authorId = ?', $user->uid));
        
        $totalWords = 0;
        foreach ($posts as $post) {
            $totalWords += mb_strlen($post['title'], 'UTF-8');
            $totalWords += mb_strlen(strip_tags($post['text']), 'UTF-8');
        }
        
        // 最近文章时间
        $lastPost = $db->fetchRow($db->select('created')
            ->from('table.contents')
            ->where('type = ?', 'post')
            ->where('authorId = ?', $user->uid)
            ->order('created', Db::SORT_DESC)
            ->limit(1));
        
        $lastPostTime = $lastPost ? date('Y-m-d H:i:s', $lastPost['created']) : '暂无文章';
        
        // 获取文学著作比较
        $literaryComparison = self::getLiteraryComparison($totalWords);
        
        return [
            'lastLogin' => $lastLoginText,
            'postCount' => $postCount,
            'totalWords' => $totalWords,
            'lastPostTime' => $lastPostTime,
            'literaryComparison' => $literaryComparison
        ];
    }

    /**
     * 插件实现方法
     *
     * @access public
     * @return void
     */
    public static function render()
    {
        $stats = self::getStats();
        $word = Options::alloc()->plugin('HelloWorld')->word;
        
        // 获取当前登录用户的信息
        $user = \Typecho\Widget::widget('Widget_User');
        $userName = $user->screenName ?: $user->name; // 优先使用昵称,如果没有则使用登录名

        echo '<span class="helloworld-trigger" style="color:#BBBBBB">' . 
             htmlspecialchars($word) . 
             '</span>';
        
        echo '<div class="helloworld-tooltip">
                <h4>欢迎回来,' . htmlspecialchars($userName) . '</h4>
                <div style="margin-bottom:20px;">
                    您已经写了 ' . $stats['postCount'] . ' 篇文章,一共 ' . $stats['totalWords'] . ' 字,' . $stats['literaryComparison'] . '。
                </div>
                <ul>
                    <li><strong>最近登录:</strong>' . $stats['lastLogin'] . '</li>
                    <li><strong>最近发文:</strong>' . $stats['lastPostTime'] . '</li>
                </ul>
              </div>';
    }
}
]]>
<![CDATA[typecho给后台admin界面添加图表分析]]> https://blog.iletter.top/archives/406.html 2025-07-27T02:32:00+08:00 2025-07-27T02:32:00+08:00 DelLevin https://blog.iletter.top 觉得后台admin空荡荡的,索性加个图表自己魔改一下下。以下是完整的index.php

网站概要-白荼日记-Powered-by-Typecho.png#B #S #R #60%

感兴趣的可以直接下载index.php文件覆盖/admin 下面的index.php文件

下载地址:

https://wonder1999.lanzouu.com/ikRiy32sfnah

<?php
include 'common.php';
include 'header.php';
include 'menu.php';
$stat = \Widget\Stat::alloc();
?>
<script src="https://blog.iletter.top/usr/blog_img/chart.js"></script>
<!-- 引入 Font Awesome 图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
/* 自定义美化样式 */
.typecho-dashboard {
    padding: 20px 0;
}
.card {
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    padding: 20px;
    margin-bottom: 20px;
}
.card-title {
    margin-top: 0;
    margin-bottom: 15px;
    padding-bottom: 10px;
    border-bottom: 1px solid #eee;
    color: #333;
    font-size: 1.2em;
}
/* 欢迎区域 */
.welcome-card {
    background: linear-gradient(135deg, #1f1f20 0%, #0076bb 100%);
    color: white;
}
.welcome-card h3 {
    color: white;
    margin-top: 0;
}
.welcome-card p {
    font-size: 1.1em;
    margin-bottom: 20px;
}
.welcome-card a {
    color: #fff;
    text-decoration: underline;
}
.welcome-card a:hover {
    text-decoration: none;
    opacity: 0.9;
}
/* 快捷链接 */
#start-link {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}
#start-link li {
    margin: 0;
}
#start-link a {
    display: inline-block;
    padding: 8px 16px;
    background-color: rgba(255, 255, 255, 0.2);
    border-radius: 4px;
    color: white !important;
    text-decoration: none;
    transition: background-color 0.3s ease;
}
#start-link a:hover {
    background-color: rgba(255, 255, 255, 0.3);
}
#start-link .balloon {
    background-color: #ff6b6b;
    color: white;
    border-radius: 10px;
    padding: 2px 6px;
    font-size: 0.8em;
    margin-left: 5px;
}
/* 统计卡片 */
.stat-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 15px;
    margin-bottom: 20px;
}
.stat-card {
    background-color: #f8f9fa;
    border: 1px solid #e9ecef;
    border-radius: 8px;
    padding: 15px;
    text-align: center;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.stat-icon {
    font-size: 2em;
    margin-bottom: 10px;
    color: #000000; /* 可根据不同类型调整颜色 */
}
.stat-number {
    font-size: 1.8em;
    font-weight: bold;
    margin: 5px 0;
    color: #495057;
}
.stat-label {
    font-size: 0.9em;
    color: #6c757d;
}
/* 最近列表 */
.latest-link ul {
    list-style: none;
    padding: 0;
    margin: 0;
}
.latest-link li {
    padding: 8px 0;
    border-bottom: 1px solid #eee;
}
.latest-link li:last-child {
    border-bottom: none;
}
.latest-link .title {
    margin-left: 5px;
}
.latest-link span {
    color: #6c757d;
    font-size: 0.9em;
}
/* 图表区域 - 重新规划 */
.chart-container {
    max-width: 1200px; /* 限制图表区域最大宽度 */
    margin: 0 auto; /* 水平居中 */
    display: flex;
    flex-direction: column;
    gap: 20px;
}
.chart-row {
    display: flex;
    gap: 20px;
    flex-wrap: wrap;
}
.chart-box {
    flex: 1;
    min-width: 250px; /* 调整最小宽度以适应更多屏幕 */
    background-color: #fff;
    border-radius: 8px;
    /*box-shadow: 0 2px 4px rgba(0,0,0,0.1);*/
    padding: 15px;
    display: flex;
    flex-direction: column; /* 确保内部元素垂直排列 */
}
.chart-box h4 {
    margin: 0 0 10px 0; /* 调整标题边距 */
    text-align: center;
    color: #333;
    flex-shrink: 0; /* 防止标题被压缩 */
}
.chart-box canvas {
    width: 100% !important; /* 确保画布宽度占满容器 */
    max-height: 300px; /* 限制图表最大高度 */
    flex-grow: 1; /* 画布占据剩余空间 */
}
/* 响应式调整 */
@media (max-width: 768px) {
    .stat-cards {
        grid-template-columns: repeat(2, 1fr);
    }
    .chart-row {
        flex-direction: column; /* 小屏幕时堆叠 */
    }
    .chart-box {
        min-width: 100%; /* 小屏幕时占满宽度 */
    }
    #start-link {
        flex-direction: column;
    }
}
/* 中等屏幕调整 - 让图表在中等屏幕也能两列显示 */
@media (min-width: 769px) and (max-width: 1100px) {
    .chart-row {
        /* 在这个范围内,允许换行,但.chart-box的flex行为会使其尽可能并排 */
    }
    .chart-box {
        /* 可以微调 min-width 来控制换行点 */
        min-width: calc(50% - 10px);
    }
}
</style>
<div class="main">
    <div class="container typecho-dashboard">
        <?php include 'page-title.php'; ?>
        <div class="row typecho-page-main">
            <!-- 欢迎卡片 -->
            <div class="col-mb-12" role="main">
                <div class="card welcome-card">
                    <h3><?php _e('欢迎使用 Typecho'); ?></h3>
                    <p><?php _e('目前有 <em>%s</em> 篇文章, 并有 <em>%s</em> 条关于你的评论在 <em>%s</em> 个分类中.', $stat->myPublishedPostsNum, $stat->myPublishedCommentsNum, $stat->categoriesNum); ?></p>
                    <ul id="start-link" class="clearfix">
                        <?php if ($user->pass('contributor', true)): ?>
                            <li><a href="<?php $options->adminUrl('write-post.php'); ?>"><i class="fas fa-pen"></i> <?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'); ?>"><i class="fas fa-comments"></i> <?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'); ?>"><i class="fas fa-comments"></i> <?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'); ?>"><i class="fas fa-trash-alt"></i> <?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'); ?>"><i class="fas fa-trash-alt"></i> <?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'); ?>"><i class="fas fa-list"></i> <?php _e('文章管理'); ?></a></li>
                                <li><a href="<?php $options->adminUrl('plugins.php'); ?>"><i class="fas fa-plug"></i> <?php _e('插件管理'); ?></a></li>
                                <li><a href="<?php $options->adminUrl('options-general.php'); ?>"><i class="fas fa-cog"></i> <?php _e('系统设置'); ?></a></li>
                            <?php endif; ?>
                        <?php endif; ?>
                    </ul>
                </div>
            </div>
            <!-- 统计卡片 -->
            <div class="col-mb-12">
                 <div class="stat-cards">
                    <div class="stat-card">
                        <a href="<?php $options->adminUrl('manage-posts.php'); ?>">
                            <div class="stat-icon"><i class="fas fa-file-alt"></i></div>
                            <div class="stat-number"><?php echo $stat->myPublishedPostsNum; ?></div>
                            <div class="stat-label"><?php _e('文章'); ?></div>
                        </a>
                    </div>
                    <div class="stat-card">
                        <a href="<?php $options->adminUrl('manage-comments.php'); ?>">
                            <div class="stat-icon"><i class="fas fa-comment"></i></div>
                            <div class="stat-number"><?php echo $stat->myPublishedCommentsNum; ?></div>
                            <div class="stat-label"><?php _e('评论'); ?></div>
                        </a>
                    </div>
                    <div class="stat-card">
                        <a href="<?php $options->adminUrl('manage-categories.php'); ?>">
                            <div class="stat-icon"><i class="fas fa-folder"></i></div>
                            <div class="stat-number"><?php echo $stat->categoriesNum; ?></div>
                            <div class="stat-label"><?php _e('分类'); ?></div>
                        </a>
                    </div>
                    <div class="stat-card">
                        <a href="<?php $options->adminUrl('manage-tags.php'); ?>">
                            <div class="stat-icon"><i class="fas fa-tags"></i></div>
                            <div class="stat-number"><?php echo $stat->tagsNum; ?></div>
                            <div class="stat-label"><?php _e('标签'); ?></div>
                         </a>
                    </div>
                    <!-- 可根据需要添加更多统计 -->
                 </div>
            </div>
            <!-- 左侧内容:最近文章 -->
            <div class="col-mb-12 col-tb-6" role="complementary">
                <div class="card">
                    <h3 class="card-title"><?php _e('最近发布的文章'); ?></h3>
                    <?php \Widget\Contents\Post\Recent::alloc('pageSize=10')->to($posts); ?>
                    <ul class="latest-link">
                        <?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>
                </div>
            </div>
            <!-- 右侧内容:最近评论 -->
            <div class="col-mb-12 col-tb-6" role="complementary">
                <div class="card">
                    <h3 class="card-title"><?php _e('最近得到的回复'); ?></h3>
                    <ul class="latest-link">
                        <?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>
                </div>
            </div>
            <!-- 图表显示区域 -->
            <div class="col-mb-12" role="complementary">
                <div class="card">
                    <h3 class="card-title" style="color: #333;"><?php _e('文章统计数据'); ?></h3>
                    <div class="chart-container">
                        <div class="chart-row">
                            <!-- 月度发布图表 -->
                            <div class="chart-box">
                                <h4><?php _e('月度文章发布趋势'); ?></h4>
                                <canvas id="monthlyPostChart" height="250"></canvas>
                            </div>
                            <!-- 状态分布图表 -->
                            <div class="chart-box">
                                <h4><?php _e('文章状态分布'); ?></h4>
                                <canvas id="statusChart" height="250"></canvas>
                            </div>
                        </div>
                        <div class="chart-row">
                            <!-- 分类文章统计 -->
                            <div class="chart-box">
                                <h4><?php _e('各分类文章数量'); ?></h4>
                                <canvas id="categoryChart" height="250"></canvas>
                            </div>
                            <!-- 标签文章统计 -->
                            <div class="chart-box">
                                <h4><?php _e('热门标签文章数量 (Top10)'); ?></h4>
                                <canvas id="tagChart" height="250"></canvas>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
<?php
include 'copyright.php';
include 'common-js.php';
?>

<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') // 注意:Typecho 草稿通常在 contents 表中 type 为 post_draft
        // ->where('status = ?', 'publish') // 草稿状态通常不是 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(54, 162, 235)', // 调整为蓝色系
                    backgroundColor: 'rgba(54, 162, 235, 0.1)',
                    tension: 0.3, // 稍微增加曲线张力
                    fill: true
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: true,
                        ticks: {
                            precision: 0
                        },
                        grid: {
                            color: 'rgba(0, 0, 0, 0.05)' // 网格线颜色
                        }
                    },
                    x: {
                        grid: {
                            color: 'rgba(0, 0, 0, 0.05)'
                        }
                    }
                },
                plugins: {
                    legend: {
                        display: false // 隐藏图例,因为只有一个数据集
                    },
                    tooltip: {
                        mode: 'index',
                        intersect: false
                    }
                }
            }
        });
    }
    // 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,
                    borderColor: '#fff'
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                plugins: {
                    legend: {
                        position: 'bottom',
                        labels: {
                            padding: 15,
                            usePointStyle: true
                        }
                    },
                    tooltip: {
                        callbacks: {
                            label: function(context) {
                                return context.label + ': ' + context.raw;
                            }
                        }
                    }
                },
                cutout: '60%' // 增加中间空心部分
            }
        });
    }
    // 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.7)', // 紫色系
                    borderColor: 'rgba(153, 102, 255, 1)',
                    borderWidth: 1,
                    borderRadius: 4, // 条形圆角
                    barPercentage: 0.7, // 调整条形宽度
                    categoryPercentage: 0.8
                }]
            },
            options: {
                indexAxis: 'x', // 保持为垂直柱状图
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: true,
                        ticks: {
                            precision: 0
                        },
                        grid: {
                            color: 'rgba(0, 0, 0, 0.05)'
                        }
                    },
                    x: {
                        grid: {
                            display: false // 隐藏 X 轴网格线
                        }
                    }
                },
                plugins: {
                    legend: {
                        display: false
                    },
                    tooltip: {
                        mode: 'index',
                        intersect: false
                    }
                }
            }
        });
    }
    // 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.7)', // 橙色系
                    borderColor: 'rgba(255, 159, 64, 1)',
                    borderWidth: 1,
                    borderRadius: 4,
                    barPercentage: 0.7,
                    categoryPercentage: 0.8
                }]
            },
            options: {
                indexAxis: 'x',
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: true,
                        ticks: {
                            precision: 0
                        },
                        grid: {
                            color: 'rgba(0, 0, 0, 0.05)'
                        }
                    },
                    x: {
                        grid: {
                            display: false
                        }
                    }
                },
                plugins: {
                     legend: {
                        display: false
                    },
                    tooltip: {
                        mode: 'index',
                        intersect: false
                    }
                }
            }
        });
    }
});
</script>
<script>
$(document).ready(function () {
    });
</script>
<?php include 'footer.php'; ?>

其他插件推荐:
[post cid="405" /]

]]>
<![CDATA[为typecho博客添加Gotify插件通知]]> https://blog.iletter.top/archives/405.html 2025-07-27T02:05:00+08:00 2025-07-27T02:05:00+08:00 DelLevin https://blog.iletter.top 受限于typecho博客没有通知,自己写了一个博客有评论就通知的gotify插件,脚本使用之前的python改的。这是效果

下载地址

插件+app+docker部署

https://wonder1999.lanzouu.com/iB6DK32mdwcf

代码


<?php

namespace TypechoPlugin\GotifyNotify;

use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Form\Element\Text;
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定义的Priority)'));
        $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');


        // 检查必要配置
        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);
        }
    }
}
]]>
<![CDATA[python每日检查网站ssl证书是否过期]]> https://blog.iletter.top/archives/402.html 2025-07-26T23:58:48+08:00 2025-07-26T23:58:48+08:00 DelLevin https://blog.iletter.top 目的是检查网站是否过期,过期前几天进行通知

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()
]]>
<![CDATA[去除宝塔界面一些烦人的东西]]> https://blog.iletter.top/archives/400.html 2025-07-25T13:18:00+08:00 2025-07-25T13:18:00+08:00 DelLevin https://blog.iletter.top 更新之后越来越难用了,自己懒得配置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);
})();
]]>
<![CDATA[ubuntu server设置samba共享服务]]> https://blog.iletter.top/archives/399.html 2025-07-25T13:17:00+08:00 2025-07-25T13:17:00+08:00 DelLevin https://blog.iletter.top 在 Ubuntu Server 上搭建 Samba 文件共享服务 是一个非常常见的需求,适用于局域网内的文件共享、打印机共享等场景。

下面是一个完整教程 ,帮助你快速搭建一个基础的 Samba 服务,并允许其他用户访问你的共享目录。

第一步:安装 Samba

sudo 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/shared

2. 编辑 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 = nowritable = 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地址
]]>
<![CDATA[linux注册服务为系统服务并开机自启]]> https://blog.iletter.top/archives/398.html 2025-07-15T22:22:23+08:00 2025-07-15T22:22:23+08:00 DelLevin https://blog.iletter.top 因为安装了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

说明已经成功取消开机启动了

]]>
<![CDATA[ubuntu-services挂载外置硬盘]]> https://blog.iletter.top/archives/397.html 2025-07-15T22:20:55+08:00 2025-07-15T22:20:55+08:00 DelLevin https://blog.iletter.top 软件包升级
sudo apt upgrade

lsblk命令用于列出所有可用的存储设备及其分区信息

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: done

sudo 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 查看硬盘UUID

root@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
]]>
<![CDATA[windows server 2019 docker安装]]> https://blog.iletter.top/archives/365.html 2025-07-14T17:11:31+08:00 2025-07-14T17:11:31+08:00 DelLevin https://blog.iletter.top windows server 2019 docker安装

最近把家里的旧服务器搬到出租屋里面来了,准备当服务器用来着,二十年前的老机器了。想着用微信hook的功能,所以选择的windows server。配置好一些环境之后开始配置docker环境除了一些问题。

启用Hyper-V和Containers功能

运行管理员权限powershell
Install-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 Docker

docker-compose下载

https://github.com/docker/compose/releases

1.下载适用于 Windows 的 docker-compose-Windows-x86_64.exe 文件。

2.将文件重命名为 docker-compose.exe 并移动到 Docker 安装目录(如 D:\Env\docker)

ps:安装完了我才发现,windows server 2019的docker不支持linux的容器,要想弄,还要一个wsl支持,或者升级机器配置。想想还是算了,直接换ubuntu 的server版了

]]>
<![CDATA[建筑类网站爬虫]]> https://blog.iletter.top/archives/364.html 2025-07-04T20:26:41+08:00 2025-07-04T20:26:41+08:00 DelLevin https://blog.iletter.top 最近帮我同学写相关建筑类网站的爬虫以及前后端搜索界面功能,其实技术要点一个没有,涉及到加密的网站我也是放弃爬虫,解密太麻烦了。

简单的网站都是一套的逻辑爬虫,大家可以参考一下。有兴趣的话帮忙点个start支持一下

前后端系统以及数据库

https://gitee.com/wonder19991209/mohurd\_search\_sys

爬虫脚本

https://gitee.com/wonder19991209/mohurd-spider

]]>