RONGYAN IMAGE,Typecho博客日记,NAS技术,网站SEO优化 - RONGYAN
给客户安装了一台服务器，主板是联想RD450X.安装好之后才发现这板子的散热风扇每一个都是100%全速运转，那噪音感人。于是查了资料，发现可以通过BMC通道用IPMITOOL工具进行微调。然后就写了这么个方便使用的小工具（MacOS和Windows兼支持，演示的是MacOS）：通过界面截图可以直观查看到当前风扇的运转情况，并且实时显示系统每一个温度采集点的温度信息。并且程序可以整体控制所有的风扇，也可以专一控制某一个风扇，具体风扇信息见联想说明书此页文档说明：程序和文档风扇对应说明：System Fan1——8System Fan2——4System Fan3——3System Fan4——11CPU Fan1——27CPU Fan2——41下载地址：稍后放出。
剪映字幕识别要花钱？我开发了FCPX字幕自动AI识别工具AiFcpxSrt
为什么做这个工具？剪辑师都懂：给视频加字幕是一件耗时且枯燥的事。用 SRT 导入 FCPX？文字样式没法调，想改字体、大小、位置、颜色？抱歉，不行。一条条手动打字幕？几十条字幕输完，半天过去了。外包给语音转文字服务？音频文件要上传，隐私不保障，还要花钱。AiFcpxSrt 就是为解决这些问题做的。它能做什么把音频文件拖进去，点一下，字幕就出来了。不是简单的 SRT 文件，是原生 FCPX 字幕——导入后直接出现在时间线上，字体、大小、颜色、位置、效果，全部可以在 FCPX 里自由调整，和手动加的字幕一模一样。支持双语模式（中文 + 英文双行字幕），英文原生 FCPX 样式。核心流程音频 → Whisper 语音识别 → 文案对齐校正 → LLM 翻译 → 原生 FCPXML 字幕全程本地运行，不需要上传任何文件。Apple Silicon M 芯片 GPU 加速。开发历程（按时间）第一阶段：核心功能Whisper 语音识别 + Qwen LLM 纠错 pipeline原生 FCPXML 字幕导出（多行 title 元素）双语模式（CN lane 1 / EN lane 2）一键导入 FCPX第二阶段：交互优化双语预览/编辑（CN 可编辑，EN 灰色不可编辑状态）右击「重新翻译」单条字幕保存/打开会话进度（.aifcpxsession JSON 文件）HSplitView 布局，黑线分隔工具栏和内容区深色模式第三阶段：授权系统LicenseManager 单例状态机OnlineVerifier 服务端通信 + HMAC 验签设备指纹生成（DeviceFingerprint）LicenseView 输入界面激活成功弹窗确认第四阶段：稳定性修复指纹计算去掉 MAC 地址（解决网络接口顺序导致的指纹不稳定）指纹结果缓存到默认设置（解决算法变更导致的设备身份变化）启动状态同步判定（不再依赖异步 cache 校验，消除界面闪烁）后台校验不降级状态（只显示错误，不踢回授权页）第五阶段：产品化SVG/PNG 图标制作，替换为 Final Cut Pro 风格图标正式将软件名称定为「AiFcpxSrt」关于界面（开发链接、邮箱、版本信息、发行说明）构建脚本自动打包（build.sh）ffmpeg 捆绑第六阶段：真实进度 + LLM 翻译ASR 进度从假脉冲（线性 2%/2s）改为截获 mlx_whisper 内部 tqdm 的真实进度中+英双语的英文通道从双次 ASR 改为 LLM 翻译（Qwen 2.5）引入音频语言选择器（中文 / English），分离 ASR 语种和目标字幕语种"自动校正"按钮扩展为两步：校正（有文案时）→ 翻译（需翻译时）翻译在校正之后执行，保证英文翻译质量技术栈语言: Swift 6.2 (SwiftUI + Combine) + Python 3AI 模型: Whisper medium (MLX 4bit) + Qwen 2.5 1.5B (4bit)语音识别: mlx-whisper（Apple GPU 加速）LLM 翻译: Mlx-lm + Qwen 2.5（离线本地推理）加密: CryptoKit (AES-GCM, HMAC-SHA256)构建: Swift Package Manager + PyInstaller最低系统: 理论上MAC M1以上系统都能运行，不限版本架构: Apple Silicon (arm64)补充说明：站长的开发环境为MacBook Pro M4 16+512 2024 Sequoia 15.7.5 在此系统上测试了全流程通过，所以环境仅供参考。由于没有更多系统版本的机器、系统可供测试，拿到软件的，请自行测试。 Intel 版的Mac 机器就不要试了，跑不起来的，架构不一样。Final Cut Pro: 我测试了两个版本 11.2和 12.3，均导入通过。硬件要求Apple Silicon Mac（M1 / M2 / M3 / M4 /M5）macOS 11+(编译环境系统为15.7.5)建议 16GB 内存当前状态（2026-07-11）核心功能稳定，授权系统运行正常双语模式：中文 ASR + LLM 翻译为英文，英文音频同理反向构建系统完成：一次脚本生成两种 app（Dev / Release）待做：服务端解除设备绑定 API、在线校验周期性定时器持续优化：更详细的错误信息透传、用户体验打磨一点感受做这个工具的初衷其实很简单——我懒得一条条打字幕。又不想给剪映充钱，我就是想白嫖。但做着做着发现，很多剪辑师都在忍受同样的事情。FCPX 文件格式晦涩难懂，SRT 与 FCPX 之间存在巨大的功能鸿沟。本地的 AI 模型越来越强，跑在 M 芯片上又快又省电，为什么不利用起来？这么好用的工具，我收个打包费，不过份吧？它还有一个更豪华的展示链接： AiFcpxSrt Fincal Cut Pro 字幕自动识别下载地址：AiFcpxSrt Fincal Cut Pro 字幕自动识别下载地址图片展示：这是AI根据加载的音频刚生成的初始字幕，断句准确，可能会有错别字经过加入文案文档后，用算法重新校正后的效果，可以发现错别字已全部纠正：这是点击一键导入Final Cut Pro后提示把字幕放到哪个事件中：导入过度条导入成功：{lamp/}
从零到一：Linux 服务器安全加固 + 日报监控完整部署指南
一、前言我的服务器半个月前中过木马，具体见这篇文章： 服务器被植入流量劫持后门(跳转世界杯赌球网站)的排查与解决方案 后门连着海外的C2服务器，Docker里还藏着挖矿程序。清理干净之后我意识到一件事：没有防护的服务器，在互联网上就像没锁门的房子，是条狗都能进来撒泡尿。于是我试着弄了这么一个日报程序，这套每日汇报方案是经过我自己实操用了多台服务器布署后的实践总结，囊括了Ubuntu、CentOS、Debain等市面上主流Linux服务器系统，从系统加固、SSH安全、fail2ban防暴力破解，到每天自动发邮件汇报安全状况。从程序写出来到今天为止，三天了，经过不断优化、更迭，差不多能达到我理想中的状态了，所以我把它分享出来，希望能帮到同样在维护服务器的你。2026-07-22 更新说明macOS 客户端多服务器数据隔离 — 按 serverID 分目录缓存，切换服务器后不显示旧服务器数据打开配置目录 — 界面增加按钮，一键打开 ~/.config/server-report/ 目录服务器 IP 默认隐藏 — 列表 IP 以 *** 显示，👁 按钮可切换显示/隐藏Windows 客户端多服务器数据隔离 — 按 serverID 分目录缓存，切换服务器后不显示旧服务器数据服务器 IP 默认隐藏 — 列表 IP 以 *** 显示，👁 按钮可切换显示/隐藏2026-07-08 更新说明服务端新增 /api/v1/dates 接口 — 返回服务器所有存档日期列表，客户端据此按需同步缺失数据SSH 审计支持日期过滤 — `归档时按指定日期查询，不再显示实时数据fail2ban "本日封禁" — 实时报告和归档统一按日期过滤 IP 列表，只显示当日新增封禁数，不显示历史累计值封禁时间零"未知" — 同时提取 IP 和时间，消除封禁时间显示未知的问题实时报告日期策略 — 默认显示当日数据API 响应精简 — server 字段返回空字符串（服务器名称已由客户端侧边栏显示）macOS windows 客户端数据同步 — 同步对应服务器新增接口，对比本地日期，缺失的自动下载；开屏、刷新、切换服务器均触发同步标题居中 —实现日历按钮右侧覆盖，标题绝对居中日期格式 — 仅显示当前数据显示的日期时间戳标签 — 加 查询时间： 前缀界面精简 — 标题去掉服务器名称行和图标；左侧已有服务器信息，没必要多次显示运行时间本地化 — 直接中文显示如“15天1小时19分钟”，更直观网络超时 — 配置 30s 请求超时、60s 资源超时，不无限卡 loading防重入 — 客户端同步数据优化，自动跳过已有数据，避免数据错乱或闪烁列宽优化 —优化显示，解决封禁时间显示不全的问题邮件发送优化`改用新版数据采集 — 邮件发送不再使用旧版文本采集函数，容易导致卡死），改为调用新函数获取结构化数据，再格式化为邮件正文新增 构建邮件文本 — 从 JSON 报告生成邮件文本，按模块独立格式化函数，类型安全依赖变更客户端新增依赖：服务端 /api/v1/dates 接口（2026-07-08 后更新的服务端版本）中国 IP 市县细化依赖：ip-api.com（geoLookupDetail 接口），需服务器能访问外网2026-06-30 更新说明服务端增加api key功能，供客户端调用增加 Windows、macOS 客户端(付费)服务端改为定时采集 + 文件缓存策略，客户端查询毫秒级响应数据保留 180 天，每天自动清理超期数据服务端源代码、预编译二进制同步更新二、系统初始化2.1 更新系统刚买的服务器系统包可能比较旧，先更新到最新版本，避免因为旧版本的已知漏洞被入侵。# ------------------- Ubuntu / Debian 系统 ------------------- # apt 是 Ubuntu/Debian 的包管理器 # update = 更新软件源列表（检查有哪些新版本可用） # upgrade -y = 升级所有已安装的软件包（-y 表示自动确认，不弹询问） apt update && apt upgrade -y # ------------------- CentOS 系统 ------------------- # yum 是 CentOS 的包管理器，update 同时更新源和升级包 yum update -y2.2 安装基础工具安装一些后续操作会用到的常用命令行工具。工具作用curl用来下载文件、调用 API、测试网络请求wget下载文件（和 curl 类似，但更专注于文件下载）git用来克隆代码仓库（如果需要从 GitHub 下载源码时用到）vim一个命令行文本编辑器（编辑配置文件用）# ------------------- Ubuntu / Debian ------------------- apt install -y curl wget git vim # ------------------- CentOS ------------------- yum install -y curl wget git vim2.3 设置时区把服务器的时间设成北京时间（CST，东八区）。否则定时任务（比如每天 00:01 发日报）会按 UTC 时间执行，和你的预期差 8 个小时。# 设置时区为亚洲/上海（北京时间） timedatectl set-timezone Asia/Shanghai # 查看当前时间，确认是否生效 date # 正常应该输出类似：Thu Jun 25 00:01:00 CST 2026 # 注意末尾的 CST 表示中国标准时间2.4 设置主机名给服务器起一个容易辨识的名字，这样在终端提示符、邮件标题里都能看到，方便区分多台服务器。# 把 your-server-name 换成你想取的名字，例如： # hostnamectl set-hostname web-server # hostnamectl set-hostname mail-server hostnamectl set-hostname your-server-name三、SSH 安全加固（最重要的一步 ⭐）3.1 修改默认 SSH 端口SSH 默认端口 22 是互联网上扫描器的首选目标，新服务器上线几分钟内就会被扫描。改成一个不常用的高位端口，能过滤掉 99% 的自动扫描。# 用 vim 打开 SSH 配置文件（vim 是命令行文本编辑器） # 如果不会用 vim，也可以用 nano：nano /etc/ssh/sshd_config vim /etc/ssh/sshd_config在文件中找到这一行（一般在第 15 行左右）：#Port 22把 # 删掉（去掉注释），把 22 改成你选的高位端口：Port 32958选端口的建议：✅ 推荐：32958、23254、18957、42196（4-5 位数字，好记就行）❌ 避免：2222、22222、8822（这些常见替代端口一样会被扫描）❌ 避免：22 附近的端口（21、23 等也会被批量扫描）三种系统注意： SSH 配置文件都在 /etc/ssh/sshd_config，位置一样，没有区别。3.2 生成密钥对（在自己电脑上操作，不是服务器）服务器登录方式有两种：密码（会被暴力破解）和密钥（几乎不可能被破解）。密钥登录的原理是生成一对文件——私钥和公钥。私钥你留着，公钥放服务器上，登录时服务器验证你是否有对应的私钥。没有私钥的人就算知道密码也登录不了。# 在你自己电脑的终端执行（Mac 的「终端」、Windows 的 PowerShell） # -t ed25519 = 指定密钥加密算法（ed25519 是目前最安全的） # -f ~/.ssh/... = 指定密钥保存路径，后面的名字改成你的服务器名方便管理 ssh-keygen -t ed25519 -f ~/.ssh/服务器名称_密钥执行后会问两次密码短语（passphrase），直接按两下回车跳过——这样登录时不需要额外输入。完成后，在 ~/.ssh/ 目录下会生成两个文件：~/.ssh/服务器名称_密钥 ← 私钥（相当于你的钥匙，绝对不能给别人看） ~/.ssh/服务器名称_密钥.pub ← 公钥（相当于锁芯，放在服务器上）⚠️ 私钥必须妥善保管，谁拿到它就能登录你的服务器，不要上传到公共代码仓库，不要复制到不安全的设备上。3.3 上传公钥到服务器把上一步生成的公钥（.pub 文件）放到服务器的 authorized_keys 文件中。有两种方式：方式一：用 ssh-copy-id（推荐，最简单）# -p 32743 = 指定 SSH 端口（改成你上一步设置的新端口） # -i ~/.ssh/xxx.pub = 指定公钥文件 # root@你的服务器IP = 以 root 用户登录你的服务器 ssh-copy-id -p 32743 -i ~/.ssh/服务器名称_密钥.pub root@你的服务器IP按提示输入当前 root 密码即可。命令会自动把公钥追加到服务器的正确位置。方式二：手动上传（如果方式一不可用）# 第一步：在服务器上创建 .ssh 目录（设置正确权限） ssh -p 32743 root@你的服务器IP "mkdir -p ~/.ssh && chmod 700 ~/.ssh" # 第二步：把本地公钥内容追加到服务器上的 authorized_keys 文件 cat ~/.ssh/服务器名称_密钥.pub | ssh -p 32743 root@你的服务器IP \ "cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"3.4 禁用密码登录，锁定 SSH 配置密钥上传成功后，就可以关闭不安全的密码登录方式了。再次编辑 SSH 配置文件：vim /etc/ssh/sshd_config把以下几项逐一修改（可以直接找到对应行改，也可以直接替换整个文件内容）：# ===== 端口（改成你前面设置的值）===== Port 32743 # ===== 只允许 SSH 协议版本 2（版本 1 有已知漏洞）===== Protocol 2 # ===== 允许 root 登录，但只接受密钥方式（prohibit-password = 禁止密码）===== PermitRootLogin prohibit-password # ===== 启用公钥认证（刚才上传的密钥就是靠这个选项生效）===== PubkeyAuthentication yes # ===== 禁止密码认证（这步做完后，没有密钥就登录不了）===== PasswordAuthentication no # ===== 也禁止其他不安全的认证方式===== ChallengeResponseAuthentication no # ===== 只接受公钥认证===== AuthenticationMethods publickey # ===== 最多允许 3 次认证失败===== MaxAuthTries 3 # ===== 最多 5 个同时连接===== MaxSessions 5 # ===== 每 5 分钟检测客户端是否还活着===== ClientAliveInterval 300 # ===== 连续 2 次没响应就断开===== ClientAliveCountMax 2 # ===== 30 秒内必须完成登录===== LoginGraceTime 30 # ===== 只允许 root 用户通过 SSH 登录===== AllowUsers root # ===== 日志记录到系统日志===== SyslogFacility AUTH LogLevel VERBOSE # ===== 禁止 X11 转发（一般用不上）===== X11Forwarding no # ===== 只允许用安全的加密算法（防止算法攻击）===== KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com3.5 重启 SSH 并验证配置改完后，需要重启 SSH 服务才能生效。# ------------------- Ubuntu / Debian ------------------- # Ubuntu 和 Debian 的 SSH 服务名是 ssh（注意不是 sshd） systemctl restart ssh # ------------------- CentOS ------------------- # CentOS 的 SSH 服务名是 sshd systemctl restart sshd最关键的一步：验证密钥登录是否正常# 新开一个终端窗口（千万不要关当前这个窗口），测试密钥登录 ssh -p 32743 -i ~/.ssh/服务器名称_密钥 root@你的服务器IP如果成功登录了，恭喜你，SSH 安全加固完成。⚠️ 重要：在确定新窗口能登录之前，绝对不要关闭当前的 SSH 窗口。 万一密钥配置有问题，你还能通过当前窗口修复。确认新窗口正常后，再关闭旧窗口。🔑 之后的登录方式——只能用密钥登录：# 每次连接都需要指定端口和密钥文件 ssh -p 32743 -i ~/.ssh/服务器名称_密钥 root@你的服务器IP为了方便，可以把配置写进 ~/.ssh/config（这样就不用每次敲端口和密钥了）：# 在本地电脑执行 echo " Host my-server HostName 你的服务器IP Port 32743 User root IdentityFile ~/.ssh/服务器名称_密钥 " >> ~/.ssh/config之后只需要 ssh my-server 就能连上了。四、防火墙防火墙的作用是控制哪些端口对外开放，防止不必要的服务暴露在公网上。原则是：默认拒绝所有入站流量，只放行需要的端口。Ubuntu / Debian 用 UFWUFW是 Ubuntu/Debian 自带的防火墙管理工具，用起来很简单。# ------------------- 安装 UFW ------------------- apt install -y ufw # ------------------- 设置默认策略 ------------------- # 默认拒绝所有入站连接（别人连不上你的服务器） ufw default deny incoming # 默认允许所有出站连接（你的服务器可以正常访问外网） ufw default allow outgoing # ------------------- 放行必要的端口 ------------------- # 注意：32958 要改成你前面设置的 SSH 端口 ufw allow 32958/tcp comment 'SSH' # 如果你要跑网站，需要放行 80（HTTP）和 443（HTTPS） ufw allow 80/tcp comment 'HTTP' ufw allow 443/tcp comment 'HTTPS' # 如果后续部署了其他服务（如数据库、VPN 等），按需增加 # ufw allow 8080/tcp comment '自定义服务' # ------------------- 启用防火墙 ------------------- # --force 表示直接启用，不弹确认询问 ufw --force enable # ------------------- 查看规则 ------------------- ufw status numbered后续管理命令：# 放行新端口 ufw allow 12345/tcp comment '新服务' # 删除规则 ufw delete allow 12345/tcp # 查看日志（看哪些 IP 被拦截了） tail -f /var/log/ufw.logCentOS 用 firewalldCentOS 默认使用 firewalld 管理防火墙，用法和 UFW 不同，但效果一样。# ------------------- 安装 firewalld（通常已预装）------------------- yum install -y firewalld # 启动并设置开机自启 systemctl enable --now firewalld # ------------------- 放行必要的端口 ------------------- # --zone=public = 应用到公共区域 # --add-port=... = 允许的端口/协议 # --permanent = 永久生效（重启后保留） # 注意：32958 要改成你的 SSH 端口 firewall-cmd --zone=public --add-port= 32958/tcp --permanent firewall-cmd --zone=public --add-port=80/tcp --permanent firewall-cmd --zone=public --add-port=443/tcp --permanent # 重新加载使规则生效 firewall-cmd --reload # ------------------- 查看规则 ------------------- firewall-cmd --list-all五、fail2ban 防暴力破解（核心 ⭐）虽然前面已经禁用了密码登录，但攻击者仍然会尝试连接你的 SSH 端口。fail2ban 会监控系统日志，发现有人尝试登录失败就自动封禁它的IP，连尝试的机会都不给。5.1 安装# ------------------- Ubuntu / Debian ------------------- # fail2ban = 暴力破解防护工具 # geoip-bin = IP 归属地查询工具（让日报能显示攻击者来自哪个国家） apt install -y fail2ban geoip-bin # ------------------- CentOS ------------------- yum install -y fail2ban geoip5.2 配置策略创建一个自定义配置文件，覆盖默认的 SSH 防护规则：# 创建配置目录 mkdir -p /etc/fail2ban/jail.d # 写入配置文件（cat > 文件 << 'EOF' 的意思是：将下面几行内容写入到文件中） cat > /etc/fail2ban/jail.d/sshd-custom.conf << 'EOF' [sshd] enabled = true port = 32958 maxretry = 1 bantime = -1 findtime = 600 backend = systemd journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd EOF配置参数详解：参数值含义enabledtrue启用这个规则port32958监控你的 SSH 端口（改成你自己设置的）maxretry11 次失败就封禁。因为我们已经禁用了密码登录，任何密码尝试都是恶意的bantime-1永久封禁（-1 表示永远，不解封）findtime600在 10 分钟窗口内累计失败次数backendsystemd通过 systemd 的 journal 读取 SSH 日志journalmatch_COMM=sshd匹配 SSH 进程的日志条目（不同系统有差异）5.3 journalmatch 适配（不同系统有区别）journalmatch 的值取决于你的系统 SSH 服务名，配置错了 fail2ban 就检测不到登录尝试。# ----- 查看你的 SSH 服务名（用这个命令确认）----- systemctl list-units --type=service | grep ssh # 根据输出结果选择对应的配置： # # 如果输出 ssh.service（Ubuntu/Debian 常见）： journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd # 如果输出 sshd.service（CentOS 常见）： journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd systemctl list-units --type=service | grep ssh # 确认 fail2ban 能正确匹配日志 journalctl -u ssh.service --since "-5 min" | grep "Invalid user\|Failed password"5.4 启动并验证# 重启 fail2ban 使配置生效 systemctl restart fail2ban # 等待 2 秒让服务完全启动 sleep 2 # 查看 SSH 防护状态 # 如果显示 Active: active (running) 说明启动成功 fail2ban-client status sshd输出示例：Status for the jail: sshd |- Filter | |- Currently failed: 0 ← 当前累计失败次数 | |- Total failed: 0 ← 总失败次数 `- Actions |- Currently banned: 0 ← 当前被封禁的 IP 数 |- Total banned: 0 ← 累计封禁的 IP 数测试 fail2ban 是否真的能封禁：# 打开一个新终端，故意输错密码登录一次 # 注意：这里用密码方式尝试，你的密钥不会触发封禁 ssh -p 32958 root@你的服务器IP # 随便输入一个错误密码 # 回到原来终端，再次查看封禁状态 fail2ban-client status sshd如果看到 Currently banned: 1，说明 fail2ban 生效了——这个 IP 永远不能再用 SSH 连接你的服务器了。5.5 子网自动封段（可选但推荐）如果你留意日报数据，会发现攻击 IP 经常来自同一个 C 段（比如 213.209.159.x 有十几个不同 IP 在换着尝试）。这时只封单个 IP 不够，应该把整个 /24 段一起封了。这个功能内置在日报程序里，每 30 分钟自动扫描一次 fail2ban 的封禁列表，如果发现同一个 C 段有 ≥2 个 IP 被封，就自动通过 iptables 封禁整个子网。# 在 config.yml 中启用： # subnet_ban: # enabled: true # threshold: 2 # 同 C 段 ≥2 个 IP 触发封段 # check_interval: 30 # 每 30 分钟扫描一次具体配置在下一章「部署日报程序」中详细说明。六、部署日报程序6.1、项目结构server-report/ ├── go/ # 服务端 Go 源码 │ ├── main.go # 入口（--serve 启动API） │ ├── config.go # 配置管理 │ ├── serve.go # HTTP API 服务器 │ ├── report_api.go # 日报数据构建 │ ├── report_json.go # JSON 数据模型 │ ├── fail2ban.go # fail2ban 管理 + 地理位置 │ ├── system.go # 系统信息采集 │ ├── ssh_auth.go # SSH 审计 │ ├── network.go # 网络连接分析 │ ├── firewall.go # 防火墙统计 │ ├── services.go # 服务列表 │ ├── changes.go # 系统变更追踪 │ ├── security.go # 安全检测 │ ├── mail.go # 邮件发送 │ ├── subnet.go # 子网封禁 │ └── util.go # 工具函数 ├── gosrc/ # 编译好的服务端二进制程序 │ ├── server-report │ └── config.yml # 示例配置 ├── Client/ # 客户端 │ ├── ServerReport.app # macOS，付费 │ └── ServerReport.exe # Windows 付费 └── README.md# 创建目录 mkdir -p /data/server-report cd /data/server-report # 下载二进制（替换为实际下载地址） 下载日报程序压缩包后，上传到/data/server-report目录并解压 chmod 755 server-report6.2、服务器端自行编译cd go GOOS=linux GOARCH=amd64 go build -o ../data/server-report .或者直接使用预编译的二进制 /data/server-report6.3、上传到服务器scp server-report root@你的服务器IP:/data/server-report/server-report scp config.sample.yml root@你的服务器IP:/data/server-report/config.sample.yml ssh root@你的服务器IP "mv /data/server-report/config.sample.yml /data/server-report/config.yml" ssh root@你的服务器IP "chmod +x /data/server-report/server-report"6.4 修改配置文件# 编辑配置文件 ssh root@你的服务器IP "vi /data/server-report/config.yml"server: name: "myserver" # 你的服务器名称（邮件标题中用） ip: "" # 留空自动获取公网 IP ssh_port: 32958 # 改成你自己的SSH登陆端口 api_key: "your-api-key-here" # API 密钥（建议32位大小写字母加数字） api_port: 8080 smtp: server: "mail.example.com" # SMTP 服务器地址 port: 465 # 465=SSL / 587=STARTTLS ssl: true user: "user@example.com" # SMTP帐号 password: "your-password" # SMTP密码 mail: to: "admin@example.com" # 接收日报的邮箱 from: "user@example.com" # 发件人地址 subject: "服务器日报 - {name} / {ip} - {date}" # 发件邮箱标题 schedule: report_at: "00:01" # 每天发送时间，24小时制 modules: system: true # 系统健康 ssh_auth: true # SSH 登录审计 fail2ban: true # fail2ban 封禁统计 network: true # 网络连接 firewall: false # 防火墙（UWF/firewalld 统计） services: true # 服务列表 changes: true # 系统变更 security: true # 安全检测 security: fail2ban: enabled: true maxretry: 1 bantime: -1 findtime: 600 journalmatch: "_SYSTEMD_UNIT=ssh.service + _COMM=sshd" subnet_ban: enabled: true # 是否启用子网自动封段 threshold: 2 # 同 C 段 ≥2 个 IP 被封时封整个段 check_interval: 30 # 每 30 分钟扫描一次6.4.1、配置文件说明字段说明默认值server.name服务器名称，用于邮件标题必填server.ip固定 IP，留空自动获取自动server.ssh_portSSH 端口22server.api_keyAPI 认证密钥（建议32位随机字符串）必填server.api_portAPI 监听端口8080smtp.*邮件发送配置必填mail.*邮件接收配置必填modules.*各功能模块开关truesecurity.fail2ban.maxretry封禁阈值1security.fail2ban.bantime封禁时长（秒，-1=永久）-1security.subnet_ban.threshold封段阈值26.4.2、API Key 说明项目说明长度要求建议 32 位 随机字符串（如 a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6）字符范围建议使用大小写字母 + 数字（a-z, A-Z, 0-9），不支持中文或特殊符号生成方式在服务端 config.yml 中手动设置，客户端填相同的值用途认证请求来源，防止未授权访问 API安全性api_key 通过 URL 参数传输，建议搭配防火墙 IP 白名单使用6.4.3、生成示例命令：# Linux 生成32位随机API Key tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 32 | head -1 # macOS 生成32位随机API Key openssl rand -hex 166.5、预编译的二进制目录结构最终 /data/server-report/ 目录下应该只有两个文件：/data/server-report/ ├── server-report ← 二进制文件（7.8MB） └── config.yml ← 配置文件整个系统就这两个文件，迁移到新服务器时整个目录拷走就行。6.6、手动测试/data/server-report/server-report --run-once如果配置正确，你的邮箱会收到一封测试日报邮件。6.7、配置定时任务注：第6.7-6.8条为旧版定时发送邮件服务使用，新版仍可使用。# 每天 00:01 发送日报 cat > /etc/cron.d/server-report << 'EOF' 1 0 * * * root /data/server-report/server-report EOF # 每 30 分钟检查一次子网封禁（如果启用了 subnet_ban） cat > /etc/cron.d/ban-subnet << 'EOF' */30 * * * * root /data/server-report/server-report --subnet-scan EOF chmod 644 /etc/cron.d/server-report /etc/cron.d/ban-subnet关于 /etc/cron.d/ 的注意事项：文件必须有一个空行结尾（否则 cron 可能不执行）文件名不要包含 . 以外的特殊字符如果定时任务不执行，可以改用 crontab -e 方式：(crontab -l 2>/dev/null; echo "1 0 * * * /data/server-report/server-report") | crontab -6.8、验证定时任务# 查看已安装的 cron 任务 cat /etc/cron.d/server-report # 或查看用户 crontab（如果用 crontab 方式） crontab -l6.9、创建 systemd 服务cat > /etc/systemd/system/server-report-api.service << 'EOF' [Unit] Description=Server Report API Service After=network.target [Service] Type=simple User=root ExecStart=/data/server-report/server-report --serve Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable server-report-api.service systemctl start server-report-api.service6.10、验证服务# 查看服务状态 systemctl status server-report-api.service # 测试 API curl "http://IP:8080/api/v1/report?key=your-api-key-here"6.11、API 端口放行如果使用客户端需要远程连接API，需在防火墙放行端口：ufw allow 8080/tcp # Ubuntu/Debian firewall-cmd --add-port=8080/tcp # CentOS/RHEL6.12、更新服务端# 停止服务 systemctl stop server-report-api.service # 备份旧版本 mv /data/server-report/server-report /data/server-report/server-report.bak # 上传新版本 scp server-report-linux root@服务器IP:/data/server-report/server-report ssh root@服务器IP "chmod +x /data/server-report/server-report" # 启动服务 systemctl start server-report-api.service6.13、定时任务（数据采集策略）服务端每小时自动采集一次系统数据，保存为 today.json，客户端查询时直接读文件返回（毫秒级响应）。每天凌晨将前一天数据归档为 YYYY-MM-DD.json，保留 180 天。时间任务说明每小时（0分）--save-report采集系统数据更新 today.json每天 00:01--save-report --archive将前一天数据归档为 YYYY-MM-DD.json每天 03:00journalctl 清理清理 180 天前的系统日志每天 03:00报告文件清理删除 reports/ 中超过 180 天的 .json 文件设置方式（已配置好，无需手动操作）：crontab -l # 输出： # 0 * * * * /data/server-report/server-report --save-report # 1 0 * * * /data/server-report/server-report --save-report --archive # 0 3 * * * /usr/bin/journalctl --vacuum-time=180d --quiet # 0 3 * * * find /data/server-report/reports -name "*.json" -mtime +180 -delete6.14、日志管理服务状态和系统错误日志通过 journalctl 管理。每天凌晨 3 点自动清理 180 天前的日志和归档报告文件。手动清理：# 清理 180 天前的日志 journalctl --vacuum-time=180d # 删除旧归档报告 find /data/server-report/reports -name "*.json" -mtime +180 -delete6.15、命令说明命令说明server-report --serve启动 API 服务（供客户端调用）server-report --save-report立即采集一次数据并保存到 today.jsonserver-report --save-report --archive将前一天数据归档为 YYYY-MM-DD.jsonserver-report --run-once立即发送一次邮件日报server-report --subnet-scan手动扫描并封禁恶意子网server-report --version查看版本server-report --help查看帮助七、日报内容解读7.1、服务器安全日志客户端主界面7.2、SSH登陆审计封禁列表7.3、fail2ban封禁列表7.4、针对中国IP单独进行了市级显示7.5、IP-C段封禁效果以及其它模块八、常见问题排查Q1：收不到邮件检查步骤：# 1. 手动触发发送，看有没有报错 /data/server-report/server-report --run-once # 2. 确认 SMTP 配置是否正确 cat /data/server-report/config.yml | grep -E "smtp:|server:|port:|user:|password:|mail:" # 3. 确认 SSL 端口可连通 echo | openssl s_client -connect mail.example.com:465 -servername mail.example.com 2>/dev/null | head -5 # 4. 如果 SSL 证书验证失败，程序会自动降级，但需要确认 SMTP 账号密码正确常见问题：密码包含特殊字符（'、;、$ 等），需要用双引号包裹：password: "your'pass;word"465端口被运营商屏蔽（尤其是家庭宽带），可以试试587（STARTTLS）SMTP服务商要求开启「允许低安全性应用访问」Q2：IP 归属地显示英文，没有中文# 检查是否安装了 geoip-bin geoiplookup 8.8.8.8 # 如果没安装 apt install -y geoip-bin # Ubuntu/Debian yum install -y geoip # CentOSQ3：成功登录记录出现 @ 方式: key这是新版 OpenSSH 日志格式变化导致的用户名解析问题，不影响功能。如果介意，可以升级到最新版二进制。Q4：系统错误日志里很多看不懂的提示日报已经内置了白噪音过滤列表，以下类型的错误会被自动过滤：错误类型原因是否影响安全no more sessionsSSH 并发限制❌ 正常AF_VSOCK CID虚拟机不支持 VSOCK❌ 正常systemd-ssh-generator同上❌ 正常soft lockupCPU 瞬时过载❌ 正常kdump / Crash recovery kernel内核崩溃转储服务未启动❌ 正常sendmail 相关sendmail 已卸载❌ 正常如果还有其他看不懂的错误，检查是否是服务器上的业务程序（如 MySQL、Nginx、PHP-FPM）的正常日志，也可以发给AI查询。也非常欢迎大家在文章下方给我留言，把系统错误日志贴给我（无伤大雅的并影响每日汇报内容需要去掉的），我将一一进行汇总，对程序进行更新并重新布发升级版。Q5：定时任务不执行# 1. 检查 cron 服务状态 systemctl is-active cron # Ubuntu/Debian systemctl is-active crond # CentOS # 2. 检查 /etc/cron.d/ 文件格式 cat -A /etc/cron.d/server-report # 正常输出应以 $ 结尾（表示有新行） # 3. 如果还不行，改用 crontab 方式： (crontab -l 2>/dev/null; echo "1 0 * * * /data/server-report/server-report") | crontab -Q6：不同系统版本 SSH 日志格式不同日报程序已经兼容了以下格式：系统版本SSH 日志格式Debian 12 (OpenSSH < 9.8)Failed password for invalid user xxxDebian 13 (OpenSSH ≥ 9.8)Invalid user xxx from IP + 旧格式并存Ubuntu 20.04+Failed password for invalid user xxxCentOS 7+Failed password for invalid user xxxQ7：如何更新日报程序# 下载新版二进制 经常访问本地址查看站长有无更新,若有新版重新下载上传到服务器即可。 # 设置权限 chmod 755 /data/server-report/server-report # 配置文件和之前的任务不受影响Q8：客户端提示"连接失败"或"获取日报失败"确认服务器 API 端口（默认 8080）在防火墙上已放行确认客户端填写的 API Key 与服务端 config.yml 中的 api_key 一致确认能从客户端网络访问服务器 IP 和端口：telnet 服务器IP 8080服务端每小时自动采集数据，客户端查询为毫秒级响应；如果刚好在第一小时部署还未生成缓存，会实时采集一次（约20-30秒），之后即可秒回Q9：客户端找不到配置文件macOS：~/.config/server-report/servers.jsonWindows：程序目录下的 config/servers.json使用"导入数据"功能（📂 按钮）可从备份目录恢复配置和日报缓存。九、API 接口获取日报GET /api/v1/report?key=你的API密钥响应格式：{ "status": "ok", "server": "服务器名称", "timestamp": "2026-06-29 10:30:00", "date": "2026-06-28 → 2026-06-29", "sections": [ { "id": "system", "title": "系统健康", "type": "system", "data": { "hostname": "...", "ip": "...", "uptime": "...", "load": "...", "cpu": "...", "memory": { "used": "...", "total": "...", "percent": 0 }, "disks": [{ "mount": "/", "used": "...", "total": "...", "percent": 0 }] } }, { "id": "ssh_auth", "title": "SSH 登录审计", "type": "ssh_auth", "data": { "failed_total": 0, "failed_ips": 0, "failed_detail": [{ "ip": "1.2.3.4", "count": 5, "users": ["root"], "first": "...", "last": "...", "location_cn": "中国", "location_en": "China", "location_detail": "广东省广州市" }], "success_total": 0, "success_list": [{ "ip": "1.2.3.4", "count": 1, "method": "publickey" }] } }, { "id": "fail2ban", "title": "fail2ban 封禁统计", "type": "fail2ban", "data": { "total_banned": 0, "current_banned": 0, "banned_ips": [{ "ip": "1.2.3.4", "location_cn": "美国", "location_en": "USA", "ban_time": "...", "users": ["root"], "attempts": 10, "subnet": "1.2.3.0/24" }], "banned_subnets": [{ "subnet": "1.2.3.0/24", "ip_count": 3 }] } }, { "id": "network", "title": "网络连接与流量", "type": "network", "data": { "established": 0, "time_wait": 0, "total": 0, "top_conns": [], "listening_ports": [], "traffic": [] } }, { "id": "firewall", "title": "防火墙拦截统计", "type": "firewall", "data": { "total_blocks": 0, "top_src": [], "top_dst": [] } }, { "id": "services", "title": "运行中的系统服务", "type": "services", "data": { "total": 0, "running": [] } }, { "id": "changes", "title": "系统变更记录", "type": "changes", "data": { "packages": [] } }, { "id": "security", "title": "安全检测", "type": "security", "data": { "suid_ok": true, "new_users": [], "suspicious": [], "online_users": [], "errors": [] } } ] }健康检查GET /api/v1/ping?key=你的API密钥十、客户端（付费）10.1 macOS 客户端运行： 拷贝 ServerReport.app 到应用程序目录直接打开即可使用。数据存储： ~/.config/server-report/servers.json — 服务器配置列表reports/*.json — 历史日报缓存10.2 Windows 客户端运行： 直接双击 ServerReport.exe数据存储： 程序所在目录下的 config/ 文件夹config/servers.json — 服务器配置列表config/reports/*.json — 历史日报缓存10.3、客户端功能列表：功能列表：功能说明多服务器管理左侧列表添加/编辑/删除多台服务器，右键菜单操作日报展示8 个功能区块，数据结构与服务端 API 完全对应SSH 审计表格展示：IP、国家（归属地）、封禁时间、尝试用户、次数，次数超 10 标红fail2ban累计/当前封禁数，国家下拉筛选，表头可排序（IP/国家/封禁时间/尝试用户/次数），C段子网点击展开日历📅 按钮弹出日历，可切换年月，有数据的日期加重颜色背景手动刷新↻ 按钮重新拉取日报导入数据📂 按钮从目录选择器导入配置和缓存显示切换👁 按钮（在 show/hide 间切换）控制服务器列表IP显示导出配置右键服务器 → 导出，下载 servers.json关于页面关于窗口，含版本、开发信息、GitHub 链接10.4、日报服务器程序下载，免费开放本项目已开源，您也可以到Github下载(下载地址回复可见)：{hide}https://github.com/rongyan-cc/server-report/{/hide}10.5、客户端下载（付费）10.6、项目反馈如果你在使用过程中遇到问题，或者有功能建议，下方留言吧，或者给我发邮件。本项目不提供技术支持，若需技术支持，可以有尝代部署，收费60元/台,B站或抖音粉丝半价。mail:me@rongyan.cc{lamp/}
WINRAR-30年的行为艺术，我们都欠它一个付款记录
WinRAR 收到一笔付款就全网狂欢，这出戏演了 30 年还没停2026 年 6 月 18 日，WinRAR 的官方 X 账号突然发了一条帖子，语气兴奋得像中了彩票："We got paid... AGAIN! We'll try not to let all this cash go to our heads! 🤑🤑🤑👑💎"（"我们又收到钱了！我们会努力不让这些现金冲昏头脑！"）短短几天，这条帖子就收获了 15 万点赞、347 万次浏览 和 904 条评论。但真正让这条帖子爆火的，不是帖子本身，而是背后那件事的荒诞感——一个软件收到了一笔付款，官方像中了彩票一样公开庆祝。如果你用过电脑，你大概知道为什么。全世界最著名的"40 天试用"WinRAR 的许可证页面上白纸黑字写着："The software is distributed as try before you buy. This means that anyone may use the software during a test period of a maximum of 40 days at no charge."（"本软件以先试后买的方式分发。任何人可在最长 40 天的试用期内免费使用。"）听起来很正常，但所有用过 WinRAR 的人都知道一个公开的秘密：40 天到期之后，软件照常运行，一个功能都不锁。它只做一件事——弹出一个窗口，礼貌地提醒你："嘿，试用期过了，要不要买个许可证？"你点"关闭"，继续用。明天打开，再弹。你再关，再用。就这样，一弹就是十年、二十年、三十年。WinRAR 1995 年诞生，到 2026 年已经 31 岁了。这个弹窗陪着无数人从学生时代走到了职场，成为数字世界里最持久的"温柔催款"。维基百科对这种模式有精确的描述：非企业用户的功能始终可用（"the non-enterprise functionalities remain available"）。翻译过来就是——官方明知道你不会付钱，但选择不卡你。所以当真的有人掏钱买了许可证，WinRAR 官方的反应就像见到了独角兽。两周之内，"又"收到钱了这次帖子里的"AGAIN"大有来头。就在两周前，6 月 4 日，一个用户发帖 @ 了 WinRAR 官方，配文："I PAID MY DEBT"（我还债了）。官方立刻发帖庆祝："Yay! We finally got paid! 🤑🤑🤑😎😍🥳🤣"（"耶！我们终于收到钱了！"）那条帖子也拿了 7.1 万赞。然后仅仅 14 天后，又一个用户 @ParenteRamon 晒出了自己的 WinRAR 付款截图，官方再次"狂喜"——这次直接冲到 14.5 万赞，翻了一倍。两周内两次"收到钱了"，对于 WinRAR 来说可能已经算是史无前例的"营收爆发期"了。一个玩了三年以上的固定节目翻一下 WinRAR 官方的历史帖子，你会发现这个"庆祝有人付费"的套路至少从 2023 年就开始了。2023 年 6 月 6 日，官方发帖："To all of you who have paid for a license... Cheers to you! #IBoughtWinRAR"（"致所有为许可证付费的用户……为你们干杯！"）从 2023 年的"为你们干杯"，到 2026 年的"我们又收到钱了！"——官方的语气越来越放飞，emoji 越来越密集，自嘲越来越纯熟。三年下来，这个"梗"已经进化成了一套成熟的品牌运营策略。而且它真的在驱动销售。评论区：大型集体忏悔现场主帖下面的评论区，简直就是互联网内疚大会。有人终于来"赎罪"了："Hey! I bought it too! (after like 25 years... but I bought it!!)"（"嘿！我也买了！用了大概 25 年之后……但我买了！！"）官方回复："Better late than never!"（迟到总比不到好！）有人做出了人生规划："Ever since I was a child, I've dreamt that the first thing I'll do when I get rich is buy a WinRAR license."（"从小我就梦想，等我发财了，第一件事就是买一份 WinRAR 许可证。"）有人试图打听商业机密："How is this business strategy called?"官方只回了一个词："Genius"（天才）。还有人直接开始忏悔，求付款链接："WinRAR, I'm sorry. My 40-day trial turns 22 this year... DM me the payment link and I'll pay up."（"WinRAR 对不起，我的 40 天试用今年已经 22 岁了……把付款链接私信给我，我付。"）官方真的给了折扣链接。甚至有人问：你们收到的该不会是 30 年来的第一笔个人付款吧？官方答："The second... 🤣"（第二笔……）。评论里还有一类声音特别值得注意——真诚的敬意："WinRAR really respects your peace — no aggressive ads or payment walls, just one polite tab."（"WinRAR 真的尊重你的空间——没有侵略性广告或付费墙，就一个礼貌的标签页。"）"Not exaggerating... the world would heal faster if all companies ran with this much integrity to its users."（"不夸张……如果所有公司都以这样的诚信对待用户，世界会好得更快。"）一家活了 30 年的德国小公司很多人可能从没想过 WinRAR 背后是谁。开发者叫 Eugene Roshal，俄罗斯人，1993 年发布了 RAR 命令行工具。他的哥哥 Alexander L. Roshal 负责版权和分发。实际的发行公司是 win.rar GmbH，注册在德国柏林。一个小团队。没有硅谷背景，没有几轮融资，没有上市敲钟。到 2026 年还在安安静静地发布新版本——最新版 7.22。这家公司的收入结构值得细看：企业端是真正的现金牛。公司采购有合规需求——审计要过、许可证要齐，所以批量采购跑不掉。这块收入支撑着公司的运营。个人端才是社交媒体上的主角。每一个真的掏钱买了许可证的个人用户，官方都当成宝贝来庆祝。因为对这样一个小团队来说，每一笔个人订单都意味着两件事：真金白银的现金流，以及一次"有人真的认可我们"的确认。这解释了为什么官方帖子里的狂喜语气不像在演——因为可能真的没在演。WinRAR 的许可证还有一个细节：终身买断制。买一次，永远用，包括未来的版本升级。2026 年，当几乎所有软件都在搞月付订阅、年费会员、功能分级、强制更新的时候，WinRAR 还在坚持三十年前定下的规矩——付一次钱，管一辈子。最成功的催款方式，就是不催回过头来看，WinRAR 的"无限试用"模式能存活 30 年，靠的是一个违反常理的逻辑：压力越小，付费意愿反而越真实。它不锁功能、不插广告、不用倒计时恐吓你，只在你每次打开时弹一个对话框，语气温和到像邻居问你借盐——"要不要考虑一下？"你可以无限期点"关闭"，没人会怪你。但正因为这份松弛，当你某一天终于决定付费的时候，那个动作变成了一个主动选择。用户发帖 @ 官方说 "I PAID MY DEBT"（我还债了），那个措辞就是证据——没人逼他，但他觉得自己"欠着"。WinRAR 把潜在的盗版者变成了潜在的付费者，默默等候，无怨无悔。这跟 2026 年主流软件公司的做法正好相反。你随便打开一个订阅制软件——7 天试用到期，功能立刻锁死；升级弹窗每隔五分钟出现一次；想导出你自己的数据？对不起，先给钱。WinRAR 的策略在今天看来几乎像行为艺术：我知道你不会付，但我不卡你。如果你付了，我像中了彩票一样替你庆祝。结果是——每次庆祝都带来新一轮付费。有人看到帖子，突然想起自己用了 20 年没付过钱，于是冲动下单。然后晒出来。然后官方再庆祝。然后更多人看到，更多人下单。一个靠内疚和好感驱动的正循环，不需要一分钱广告预算。最后说一件小事。有用户问官方：许可证会不会过期，换电脑后要不要重买？官方回："Nope. As long as you keep your license key safe, you can use it FOREVER!"（"不用。只要你保管好许可证密钥，就能永远使用！"）FOREVER，全大写。一个卖了 30 年软件的公司，在 2026 年还敢对用户说出这个词。你上次听到哪个软件承诺"永久"，是什么时候？附： WINRAR官方下载链接 注意：此链接为英文官网汉语界面，非中国官网。请认准.com而非.com.cn，若想使用舒心，请远离.cn的版本（中国特供版,俗称“广告特供版 😂 ”）。{lamp/}
这是一个轻量级的 Typecho 插件，通过邮箱、域名、关键词黑名单自动拦截垃圾评论。开发此插件的由来 写这个插件是有原因的，连续两天，一直有个鸟人来我博客发留言（最新两篇文章）,他的主要目的其实是发他的算命网站链接。我后台删了他的留言他第二天又来发了。首先不能用禁IP的方式，因为我发现他IP是广西联通（一般都是家庭宽带，这个IP只是他临时的，封了也没用，而且还可能会造成整个广西联通的网都来不了）。一般他会发两条留言，一篇文章一条，留的QQ邮箱都是一样5652xx5524@qq.com，但是在两篇文章中的留言名字改了，配套的网址也不一样，一个叫做免费算命，留的网址是https://www.resoxxxg.cn/，一个叫做姓名测试打分，留的网址是https://www.tiatixxxxxiao.com/。于是针对这个垃圾玩艺，这套插件应运而生。功能环境要求 — 在 Typecho 1.3 上测试通过，其他版本请自行测试邮箱拦截 — 支持部分匹配，如 qq.com 可拦截所有 QQ 邮箱域名拦截 — 拦截评论者填写的网站域名关键词拦截 — 拦截昵称或正文中的关键词两种拦截方式：直接拦截 — 匹配规则时拒绝评论，前台提示错误标记为垃圾 — 匹配规则时标记为 spam 状态，前台不报错也不显示，后台可审查安装将 SpamBlocker 文件夹上传至 /usr/plugins/登录 Typecho 后台，进入 控制台 → 插件找到 SpamBlocker，点击 启用点击 设置，填写拦截规则使用方法在插件设置页面填入需要拦截的内容，每行一条：# 拦截邮箱（部分匹配） spammer@example.com qq.com # 拦截域名（部分匹配） spam-site.cn # 拦截关键词 免费算命选择拦截方式后保存即可生效。数据存储配置存储在 typecho_options 表中，name = 'plugin:SpamBlocker'，值为序列化数组。无需手动建表或改表结构。拦截原理通过 Typecho_Plugin::factory('Widget_Feedback')->comment 钩子，在评论提交前对邮箱、网址、昵称、正文进行匹配检查。开源协议MIT已在github上开源， Github开源地址 {lamp/}
服务器被植入流量劫持后门(跳转世界杯赌球网站)的排查与解决方案
前言 今天早上遇到一个非常头疼的问题：我的一台云服务器上部署的网站，访问时会有很大概率跳转到赌球网站（刚好世界杯期间，什么牛鬼蛇神都出来了）。更诡异的是，哪怕我放一个纯空白 HTML 页面上去，依然会被劫持。折腾了半天，最终定位到是一套精心伪装的博彩木马。本文记录完整的排查、分析和清理过程。一、症状描述先说环境：三台云服务器，同一家服务商出问题的这台：Ubuntu 系统另外两台正常的：CentOS 系统所有网站运行在 Nginx 上，通过 宝塔面板 管理症状：访问网站（无论哪个域名），浏览器有很高概率跳转到 https://dd0a000h4orvt.cloudfxxxt.net/?cid=8214805 （域名已做了无害处理）这个赌球网站即使是刚上传的空白 test.html（内容就一句 hello），依然会跳转；开启 Cloudflare 小黄云（代理模式）无效，关闭小黄云（DNS only）也无效；从服务器上 curl 访问页面，返回正常，没有任何跳转；另外两台 CentOS 服务器完全正常跳转链（之前没有捕捉到前面那个xyz跳转链接所以走了一些弯路）：先是跳转faradfdsa.anfxxc.xyz，接着很快跳转至dd0a000h4orvt.cloudfxxxt.net/?cid=8214805二、初步分析2.1 排除 ISP/DNS 层面因为只有这一台服务器有问题，而三台机器用的是同一家云服务商，所以排除了"运营商线路劫持"的可能。如果是 ISP 层面的劫持，三台服务器应该都会中招。2.2 为什么 Cloudflare 无效Cloudflare 的代理模式会将用户流量先经过 Cloudflare 节点再转发到源服务器。如果劫持发生在源服务器发出响应之后（比如浏览器执行时），那么 Cloudflare 也救不了。2.3 核心疑点：curl 正常但浏览器跳转这说明劫持不是发生在 Nginx 配置层面（因为 curl 返回的是原始响应），而是在更上层——要么是浏览器端被注入 JavaScript，要么是网络传输层被篡改。三、第一轮排查：发现 defunct / iptable6 rootkit3.1 先看 Nginx 配置nginx -T 2>/dev/null | grep -i "redirect\|rewrite\|return 302\|sub_filter"输出中除了正常的 rewrite 规则外，没有发现明显的恶意配置。3.2 检查进程——发现重大线索lsof -i :443输出赫然出现了三个可疑进程：defunct 812 root 3u TCP spk.laws.ms:33018->ec2.us-east-2.amazonaws.com:https (ESTABLISHED) iptable6 817 root 4u TCP spk.laws.ms:54498->ec2-13-57-38-185.us-west-1.compute.amazonaws.com:https (ESTABLISHED) iptable6 28789 root 4u TCP spk.laws.ms:54498->ec2-13-57-38-185.us-west-1.compute.amazonaws.com:https (ESTABLISHED)defunct——这个名字看起来像是进程状态（<defunct> 表示僵尸进程），实则是恶意程序iptable6——伪装成系统工具（正确名称是 ip6tables），拼写故意只差一点点这两个进程都在向外部的 AWS EC2 服务器建立 HTTPS 连接。3.3 定位二进制文件file /usr/bin/defunct /usr/bin/iptable6 ls -la /usr/bin/defunct /usr/bin/iptable6/usr/bin/defunct: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, stripped /usr/bin/iptable6: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, no section header两个文件都是静态链接、strip 去符号，显然是恶意软件标准做法。defunct 的文件日期被伪造为 2003-09-08，而 iptable6 的日期是 2026-04-28，说明是近期才植入的。lsof -i -P -n | grep -E "defunct|iptable6"defunct 812 root TCP 127.0.0.1:33018->212.132.98.170:443 (ESTABLISHED) iptable6 817 root TCP 127.0.0.1:54498->13.57.38.185:443 (ESTABLISHED)两个二进制分别连接不同的 C2 服务器，形成两条指挥通道。3.4 找到自启动持久化机制grep -r "iptable6\|defunct" /etc/init.d/ /lib/systemd/ 2>/dev/null结果令人震惊——几乎每个 /etc/init.d/ 下的脚本都被注入了 /bin/iptable6：/etc/init.d/keyboard-setup.sh:/bin/iptable6 /etc/init.d/apparmor:/bin/iptable6 /etc/init.d/docker:/bin/iptable6 /etc/init.d/open-vm-tools:/bin/iptable6 /etc/init.d/postfix:/bin/iptable6 ...（共 20+ 个文件）更隐蔽的是 systemd 层面：/lib/systemd/system/defunct.service: ExecStart=/bin/bash -c "GS_ARGS='-k /lib/systemd/system/defunct.dat -ilq' exec -a '[kswapd0]' '/usr/bin/defunct'"这里使用了 exec -a '[kswapd0]' 技巧——将进程名伪装成 Linux 内核线程 [kswapd0]，用 ps aux 根本看不到它。3.5 查看登录记录——找到攻击来源last -10root pts/2 212.132.98.170 Mon Jun 1 (02:01)6月1日凌晨，一个来自 212.132.98.170 的 IP 通过 SSH 登录了这台服务器——这就是攻击者。3.6 Docker 容器的问题还发现服务器上运行了两个可疑的 Docker 容器：docker ps -atraffmonetizer/cli_v2 ← 带宽贩卖工具 earnfm/earnfm-client:latest ← 同类流量变现服务这两个容器将服务器作为代理出口节点，虽然不是导致劫跳转的直接原因，但它们进一步消耗服务器资源且可能引来更多攻击。第一轮清理操作详情停止并删除 defunct.service systemd 服务✅ rm -f /lib/systemd/system/defunct.service /defunct.dat杀掉 defunct / iptable6 / [kswapd0] 进程✅删除 /usr/bin/defunct / /usr/bin/iptable6✅清理所有 /etc/init.d/* 中的注入命令✅ sed -i '/\/bin\/iptable6/d' /etc/init.d/*删除 traffmonetizer / earnfm Docker 容器✅修改 root / 面板 / 数据库密码✅重启服务器验证✅ 跳转消失第一阶段判断当时以为是 defunct/iptable6 这个 rootkit 做的中间人攻击（MITM），在 HTTP 响应中注入 JavaScript 跳转代码。重启后确认跳转消失，认为是彻底解决了。四、第二轮排查：跳转复发 —— 发现 Nginx Lua 后门4.1 复发清理后不到一个小时，手机端再次出现跳转到赌博网站的问题。PC 端访问正常，手机端（Android/iPhone）依然会 302 跳转。这意味着：不是 defunct/iptable6 导致的（它们已被清除）还有一套独立的、更隐蔽的后门在运行这个后门只针对移动设备——说明攻击者很清楚用户群体4.2 深入排查 Nginx 层再次检查 Nginx 配置，这次更仔细地看有没有加载 Lua 模块：nginx -V 2>&1 | grep -i lua发现 Nginx 编译了 ngx_http_lua_module，说明支持 Lua 处理请求。检查 nginx.conf，发现一行关键配置：lua_package_path "/www/server/nginx/lib/lua/?.lua;;";在 Lua 模块路径中找到一个可疑文件：ls -la /www/server/nginx/lib/lua/-rw-r--r-- 1 www www 20887 Mar 19 12:10 ngxd.lua文件属主是 www（Web 用户），且被 chattr +i 锁定（不可删除）。4.3 ngxd.lua 后门分析这是整个攻击中最精巧的部分。ngxd.lua（20887 字节）是一个完整的 Nginx Lua 后门模块，具备以下功能：功能说明User-Agent 检测根据 UA 判断设备类型（Android / iPhone / PC）并区分处理比例跳转Android 10%、iPhone 10%、PC 0% 的概率触发跳转，控制感染面不易被察觉远程配置从 pull.969a.xyz 拉取配置，可动态调整跳转比例和跳转域名RSA 签名命令执行通过 X-Nginx-Authorize 请求头传递命令，RSA 验签后执行任意 shellBot 伪装Google/Bing/Baidu 爬虫不跳转，避免 SEO 处罚路径例外/admin/、/api/、/login/、/static/ 等路径不跳转跳转参数跳转时在 URL 中携带 Base64 编码的 host\machine_id\device_type核心跳转逻辑：-- 伪代码示意 function process_request() local ua = get_headers("User-Agent") if ua matches "Android" then if math.random() < 0.10 then -- 10% 概率 if not has_cookie("k") then set_cookie("k", "1", 86400) redirect("https://faradfdsa.anfdnc.xyz/...") end end elseif ua matches "iPhone" then -- 相同逻辑，10% 概率 end end4.4 后门加载机制第一次排查后，恢复 nginx.conf、注释掉 #include luawaf.conf;、删除 waf/ 目录，但没有 reload nginx，所以旧 worker 进程继续运行恶意逻辑。这就是为什么 nginx -T 看到的是干净配置，但实际请求仍然被劫持。执行 nginx -s reload 后，新 worker 加载干净配置，跳转停止——导致误判为"已经完全修复"。4.5 真正的持久化机制清理 ngxd.lua 并 reload nginx 后不久跳转复发。这次检查了 /etc/init.d/nginx 启动脚本，发现被篡改：# 在 /etc/init.d/nginx 中，攻击者插入了 prepare_nginx_conf() 函数 # 该函数在每次 nginx start/reload 时自动执行 prepare_nginx_conf(): 1. 检测 nginx.conf 中是否有 lua_package_path 2. 有 → 将 DIR 设为 /www/server/nginx/lib/lua/ngxd.lua 3. 通过 curl 从 https://pull.969a.xyz/ngxd.lua 下载后门 4. chattr +i 锁定、chown www:www 伪造 Web 上传痕迹 5. 向 VHOST_PATH 写入 _.conf，写入内容： rewrite_by_lua_block { local diversion = require "ngxd" diversion.process_request() } 6. nginx 加载配置（_.conf 被 include 进来） 7. **立刻删除 _.conf** ← 灭迹关键为什么第一次没发现：原因说明注意力集中在 nginx 配置层nginx -T、vhost conf、Lua 钩子，没检查 /etc/init.d/_.conf 转瞬即逝只在 nginx 启动时存在几毫秒，加载完立刻被删nginx -T 看不到运行时 _.conf 已不存在，dump 的配置是干净的启动脚本不常被审计相比 nginx.conf 和 PHP 文件，init.d 脚本容易被忽视正确做法：找到可疑文件后，立即全系统检索其名称或相关字符串：grep -rn "ngxd\|pull.969a" /etc/ /root/ /usr/local/ 2>/dev/null4.6 攻击链还原宝塔面板 RCE（root 权限） → 第一阶段：植入 defunct/iptable6 rootkit + 带宽贩卖 Docker 容器 → 2026-06-13 第一次排查，清除 rootkit → 第二阶段：通过 init 脚本注入防删除机制 → 篡改 /etc/init.d/nginx，插入 prepare_nginx_conf() 函数 → 上传 ngxd.lua 到模块路径，chown www:www 伪造成 Web 漏洞 → 第一次排查时，木马恢复 nginx.conf、删除 waf/ 目录 → 但未执行 nginx reload，旧 worker 继续跳转 → nginx -s reload 后跳转消失，导致我误以为修复 → 接着我把宝塔面板自动升级触发 nginx restart → init 脚本自动下载 ngxd.lua + _.conf → 跳转复发 → 第二次排查发现 init 脚本被篡改 → 彻底清除已知恶意域名域名用途212.132.98.170攻击者 SSH 来源 + defunct C213.57.38.185iptable6 C2（AWS EC2）faradfdsa.anfXXc.xyz跳转中间站dd0a000h4orvt.cloudfXXXt.net最终赌博站（CloudFront CDN）pull.969a.xyzngxd.lua C2 配置拉取五、第二轮清理5.1 清除 Lua 后门操作状态删除 /www/server/nginx/lib/lua/ngxd.lua✅同时检查并删除其他路径（btwaf / free_waf）✅删除 /tmp/.machine_id / timestamp.txt / ngxd_*✅5.2 修复 nginx 启动脚本# 将 /etc/init.d/nginx 替换为干净的系统版本 # 用 chattr +i 锁定，防止再次被篡改 chattr +i /etc/init.d/nginx5.3 完全重启 nginx/etc/init.d/nginx stop sleep 2 /etc/init.d/nginx start注意：必须 restart（先 stop 再 start），因为 init 脚本的注入点只在 start 阶段执行。用 reload 不会触发 prepare_nginx_conf()。5.4 验证# 手机 UA 测试本地 curl -A "Mozilla/5.0 (Linux; Android 13)" http://127.0.0.1 # → 200 OK（无跳转） # 手机 UA 测试外网 curl -A "Mozilla/5.0 (Linux; Android 13)" http://223.254.130.86 # → 200 OK（无跳转） # 多轮确认 ngxd.lua 未复现 for i in 1 2 3; do [ -f /www/server/nginx/lib/lua/ngxd.lua ] && echo "WARNING" || echo "Check $i: clean" sleep 10 done最终验证结果检查项结果rootkit 扫描（chkrootkit / unhide）✅ 无 rootkit隐藏进程✅ 无SSH 密钥✅ 仅授权密钥系统 cron✅ 无恶意任务面板插件✅ 仅标准插件ngxd.lua 30 秒内复现检测（3 轮）✅ 未复现手机 UA 跳转（3 轮）✅ 均返回 200六、其他安全检查检查项结果SSH 配置✅ 密钥登录，禁用密码Cron 任务✅ 仅宝塔面板 SSL 续签任务PHP .user.ini✅ 无 auto_prepend_file.htaccess✅ 无恶意重写规则PHP 扩展✅ 无可疑 .soLD_PRELOAD✅ 未设置Systemd 服务✅ 无恶意服务PHP webshell✅ 未发现SUID 二进制✅ 标准系统文件Docker 容器✅ 已全部清理iptables NAT✅ 仅 Docker MASQUERADE七、投毒入口分析证据链证据结论ngxd.lua 目录属主 rootwww 用户无法写入，文件不可能是 Web 漏洞上传nginx.conf / enable-php-00.conf 均属主 root需 root 权限才能修改ngxd.lua 被 chown www:www人为伪造成 Web 上传痕迹，误导排查方向宝塔面板版本 11.3.0（存在已知 RCE）宝塔面板 RCE 是最可能的初始入口结论：宝塔面板 RCE 是最可能的入侵入口本次感染的攻击面是 Nginx 层面的 Lua 后门 + init 脚本持久化，不涉及 PHP 代码层面。服务端已清理完毕后，本地源码确认干净。八、经验和教训不能只看运行时配置：nginx -T 看到的配置可能和实际运行的不一致——_.conf 只在启动瞬间存在查文件来源比删文件重要：找到文件后应追溯"谁写进来的"，否则会复发。grep -rn "关键词" /etc/ /root/ 应在第一时间执行持久化机制常藏在启动脚本：/etc/init.d/、systemd units、cron、rc.local 都是需要重点检查的位置chattr +i 要双向使用：不仅要锁定恶意文件，更要锁定关键系统脚本本身进程伪装越来越高级：exec -a '[kswapd0]' 把进程名伪装成内核线程，ps 发现不了，必须用 lsof 查网络连接curl 正常 ≠ 网站正常：服务端本地 curl 看到的是原始响应，但浏览器请求可能被 Lua 层的 302 劫持多维度排查顺序：Nginx 配置 → 系统进程 → 网络连接 → 启动脚本 → 登录日志，缺一不可宝塔面板是最薄弱环节：面板以 root 运行、暴露在公网、历史漏洞多，安全问题任重道远。九、建议措施立即执行# 修改所有数据库密码、Redis 密码 # 修改宝塔面板密码和密钥 # 更新宝塔面板至最新版 bt upgrade # 安装 Fail2Ban 防止爆破 apt install fail2ban -y systemctl enable fail2ban systemctl start fail2ban # 锁定关键配置文件 chattr +i /www/server/nginx/conf/nginx.conf长期建议定期扫描：安装 rkhunter 和 chkrootkit，每周扫描文件完整性：对 /www/server/nginx/conf/、/usr/bin/ 等关键目录启用 tripwire 或 AIDE日志监控：启用访问日志，监控异常重定向和 SSH 登录最小权限：检查 www 用户对系统非必要目录的写权限宝塔面板：限制面板仅内网访问，或绑定域名 + HTTPS 访问SSH 安全：确认 authorized_keys 已配置，关闭密码登录，修改端口记录于 2026 年 6 月 13 日PS:真羡慕韩国球迷啊！祝国足本期世界杯“夺冠”！！！
一、VisitorLoggerPro介绍本站用的访客统计插件名称叫VisitorLoggerPro： 作者开源地址 并且作者还专门做了一个前端访客统计独立页，但是只适配handsome主题，我试了在JOE主题下是无法正常使用的。于是我对其进行了一些修改，以使其能够在Joe主题下可以正常使用，可以在我的网站导航菜单点击访客统计查看。二、VisitorLoggerPro 适配 Joe主题修改主要的一些修改：1、改变前端样式结构，使其适应joe主题；2、改变读取数据库方式，使其在Joe主题下能够正常读取数据并显示；3、原插件默认显示最近7天数据，改为默认显示当天数据；三、在Joe主题下的使用方法正常安装插件后，将插件目录下的visitor-stats.php拷贝到主题joe目录，在后台创建独立页，选择访客统计模板即可。四、下载地址百度网盘，回复可见：{hide}https://pan.baidu.com/s/16YPMUE6YitYgoucFLN8eOQ?pwd=6tvc{/hide}
经常来看我博客的访客都会知道，我的博客文章中的图片都会有一个域名文字的水印。所以也经常在后台有人私信问我这个水印是怎么弄的。其实这是一个插件，原作者插件github开源地址（感谢！）： 图片水印插件 然后根据你自己的需求，弄两张水印图（一大一小，覆盖大的图片和小的图片，Png格式）如下图，看上去似乎一样，实际上分辨率是不同的，就是同一张图我在PS中导出为两种分辨率的而已。我这里一开始就是随便测试的两张图，没想到效果出奇的符合我的味口，所以就一直沿用至今：后台的插件设置也非常简单直接：这样子你在编写文章的时候，上传的图片会自动进行水印处理。但是作者的这个插件好久没有更新了，我测试了在我的php8.2下有一些问题。我的博客之前用的老服务器是php7.4，这个插件使用一切正常。昨天我的博客进行了搬家，现在新的服务器是php8.2，这个插件就不正常了，症状是上传图片会显示上传失败，但实际上图片已经上传成功了，水印也附着了，就是需要刷新网页才能显示出来。后来查了日志才知道是这个水印插件的问题，waterMark插件在返回JSON前输出了PHP8.2弃用警告，导致整个响应不是合法的JSON，JavaScript解析失败。说人话就是imgfunc.php参数顺序不符合PHP8.2，必选参数在可选参数之后，输出Deprecated 警告污染了JSON响应。上传其实是成功的（文件已保存），但浏览器解析JSON时报错，所以显示"上传失败"。修复方法：修正 waterMark 插件的函数参数顺序。改动的地方有两处：imgfunc.php:6 — 把 $w_pct, $w_quality 移到可选参数前面 Plugin.php:79 — 同步调整调用顺序为了以后更新方便，我直接把作者的github项目fork之后更新了：本次更新日志（2026-05-31）：兼容 PHP 8.0+修复 ImgWaterMark() 函数参数顺序：将必选参数 $w_pct、$w_quality 移至可选参数之前解决 PHP 8.0+ 报 Deprecated: Optional parameter declared before required parameter 警告的问题修复因该警告导致 Typecho 上传图片后 JSON 响应被污染、前端提示"上传失败"的 bug下载地址（回复可见）：{hide}[...]
换了新服务器&彻底解决Typecho博客头像源显示问题
序言：服务器搬家了~今天是个里程碑的日子，因为网站正式搬迁到新家落户了！虽然都是香港的服务器，但是这个是CN2，懂的都懂。为何又是香港而不是内地的服务器，懂的也都懂。和nobbcc的站长认识，他给了我JOE主题的魔改包，可惜原来的服务器上面有太多的兼容性问题，一直没能够用上。旧务器是Nginx 1.28+PHP7.4+MySQL 5.7,新服务器是Nginx 1.3+PHP8.2+MySQL8.0，刚好乘这个机会直接把主题用新版的了。有兴趣的可以去nobcc站长的博客去下载：JOE魔改主题下载地址（基于JOE 7.7.1魔改优化而来）Typecho 的头像源问题一番测试下来，感觉良好，就是有一个打从Typecho娘胎里带来的怪病（中国大陆地区水土不服），访客头像破图问题依然存在。相信用Typecho做博客的站长都知道，网上一堆的说法就是换Cravatar的解析地址。虽然我换了国内的cravatar.cn地址，可是我测试换过了依然有的访客的头像是破的，或者是灰色的（无头像，加载不了QQ邮箱头像+没有注册Cravatar头像的邮箱地址大有人在）。于是我试着解决这个问题。我用好几个邮件地址作了测试，诸如123@qq.com(这个能正常加载头像)、abc@qq.com(QQ别名邮箱地址，很难加载头像，显示破图)、abc@163\sina\gmail\msn等等，有一半的机率是灰图以及破图，很不好看。通过分析代码分析可能的原因：1、头像走的是library/avatar.php代理（服务器去下载头像再返回），服务器连不上外部源就会破图。2、QQ邮箱头像接口只能通过纯数字QQ号查询头像。诸如abc@qq.com这样的邮箱前缀不是数字，所以走不到QQ头像通道，降级到Cravatar查头像，而该邮箱没有注册 Cravatar/Gravatar，就返回了默认灰图（其它非qq.com邮箱同理）。所以我现在解决的方式是这样：1、头像函数从本地代理avatar.php改为直链cravatar.cn（QQ邮箱自动优先用QQ头像）；2、非QQ邮箱同步头像源改为从cravatar.cn获取；3、在JOE主题目录下新建avatar头像目录并存放了100多张随机头像，若以上都失败（非QQ邮箱，也没在Cravatar注册头像），根据邮箱的MD5哈希值取模，从本地头像目录里选一张固定分配给该邮箱——同一个邮箱始终显示同一张图。解决方法一、先直接切回直链方式，改为直接输出 CDN 地址：1、编辑 function.php 位于/usr/themes/joe/core//* 头像函数从本地代理 avatar.php 改为直链 cravatar.cn（QQ 邮箱自动用 QQ 头像），从第311行开始 */ echo $timeTotal < 1 ? $timeTotal * 1000 . 'ms' : $timeTotal . 's'; } function _getAvatarByMail($mail) { $themeUrl = Helper::options()->themeUrl; $encodedMail = base64_encode($mail); $proxyUrl = $themeUrl . '/library/avatar.php?data=' . $encodedMail; echo $proxyUrl; } /*function _getAvatarByMail($mail) { $gravatarsUrl = Helper::options()->JCustomAvatarSource ? Helper::options()->JCustomAvatarSource : 'https://cn.cravatar.com/avatar/'; $gravatarsUrl = Helper::options()->JCustomAvatarSource ? Helper::options()->JCustomAvatarSource : 'https://cravatar.cn/avatar/'; $mailLower = strtolower($mail); $md5MailLower = md5($mailLower); $qqMail = str_replace('@qq.com', '', $mailLower); if (strstr($mailLower, "qq.com") && is_numeric($qqMail) && strlen($qqMail) < 11 && strlen($qqMail) > 4) { echo 'https://thirdqq.qlogo.cn/g?b=qq&nk=' . $qqMail . '&s=100'; } else { echo $gravatarsUrl . $md5MailLower . '?d=mm'; } };*/ }; /* 评论名片*/ function get_comment_card_info($comments) { // 静态变量缓存：确保单次请求中，相同数据只计算一次2、编辑 core.php 位于/usr/themes/joe/core//* 同步头像源默认地址改为 cravatar.cn，从第271行开始 */ function _getAvatarUrlByMail($mail) { if (empty($mail)) {$mail = '';} $gravatarsUrl = Helper::options()->JCustomAvatarSource ? Helper::options()->JCustomAvatarSource : 'https://cn.cravatar.com/avatar/'; $gravatarsUrl = Helper::options()->JCustomAvatarSource ? Helper::options()->JCustomAvatarSource : 'https://cravatar.cn/avatar/'; $mailLower = strtolower($mail); $md5MailLower = md5($mailLower); $qqMail = str_replace('@qq.com', '', $mailLower); if (strstr($mailLower, "qq.com") && is_numeric($qqMail) && strlen($qqMail) < 11 && strlen($qqMail) > 4) {3、编辑 DyUtils.php 位于/usr/themes/joe/times//* 动态评论头像源同步，从第48行开始 */ function ParseAvatar($mail) { if (function_exists('_getAvatarByMail')) return _getAvatarByMail($mail); echo "https://cn.cravatar.com/avatar/".md5(strtolower(trim($mail)))."?d=mm"; echo "https://cravatar.cn/avatar/".md5(strtolower(trim($mail)))."?d=mm"; }4、编辑 global.php 位于/usr/themes/joe/public/seting//* 后台设置说明文案同步，从第164行开始 */ NULL, NULL, '【11】自定义头像源（非必填）', '介绍：用于修改全站头像源地址 例如：https://cn.cravatar.com/avatar/ 其他：非必填，默认头像源为https://cn.cravatar.com/avatar/ 例如：https://cravatar.cn/avatar/ 其他：非必填，默认头像源为https://cravatar.cn/avatar/ https://dn-qiniu-avatar.qbox.me/avatar/ 注意：填写时，务必保证最后有一个/字符，否则不起作用！' ); $JCustomAvatarSource->setAttribute('class', 'joe_content joe_global');二、建立本地头像源建立joe\avatar目录，并将头像图片全部存放到此目录;三、变更头像获取机制1、编辑 function.php 位于/usr/themes/joe/core//* 通过邮箱生成头像地址，从第316行开始 */ function _getAvatarByMail($mail) { $gravatarsUrl = Helper::options()->JCustomAvatarSource ? Helper::options()->JCustomAvatarSource : 'https://cravatar.cn/avatar/'; $mailLower = strtolower($mail); $md5MailLower = md5($mailLower); $qqMail = str_replace('@qq.com', '', $mailLower); if (strstr($mailLower, "qq.com") && is_numeric($qqMail) && strlen($qqMail) < 11 && strlen($qqMail) > 4) { echo 'https://thirdqq.qlogo.cn/g?b=qq&nk=' . $qqMail . '&s=100'; } else { echo $gravatarsUrl . $md5MailLower . '?d=mm'; $index = abs(crc32($mailLower)) % 139 + 1; echo Helper::options()->themeUrl . '/avatar/avatar' . $index . '.jpg'; } }; /* 评论名片*/2、编辑 core.php 位于/usr/themes/joe/core//* 通过邮箱生成头像地址（字符串），从第272行开始 */ function _getAvatarUrlByMail($mail) { if (empty($mail)) {$mail = '';} $gravatarsUrl = Helper::options()->JCustomAvatarSource ? Helper::options()->JCustomAvatarSource : 'https://cravatar.cn/avatar/'; $mailLower = strtolower($mail); $md5MailLower = md5($mailLower); $qqMail = str_replace('@qq.com', '', $mailLower); if (strstr($mailLower, "qq.com") && is_numeric($qqMail) && strlen($qqMail) < 11 && strlen($qqMail) > 4) { return 'https://thirdqq.qlogo.cn/g?b=qq&nk=' . $qqMail . '&s=100'; } else { return $gravatarsUrl . $md5MailLower . '?d=mm'; $index = abs(crc32($mailLower)) % 139 + 1; return Helper::options()->themeUrl . '/avatar/avatar' . $index . '.jpg'; } } /*逻辑说明：1、QQ 数字号邮箱 → QQ 头像（不变）2、其他邮箱 → 有avatar显示avatar头像，没有则邮箱 CRC32哈希值对本地头像取模，固定分配一张本地头像；并且同一个邮箱永远只显示同一张头像。前后对比：四、修改好的文件包下载（注意这是基于nobb站长的Joe魔改主题包而作的修改，其它版本的joe主题请自行测试）,回复可见[...]
一、系统介绍 我的零售业务有好多的IT设备，销售给客户的数据原来都是表格管理，不仅查询不方便，客户也会有想要知道设备质保期的信息。由于市面上一直没能够找到符合我预期的产品，所以一直也就这样将就着用着。 前两天心血来潮下载了Cursor，结果在这AI编程器的配合下，竟然花了两天时间手搓出了个不错的资产管理系统，不管是界面、还是功能上，都是非常符合我原先的预期了。 所以这是一套面向 IT 服务商的设备与资产管理平台，包含前端客户自助查询系统和后端资产管理系统两部分。20260525:为简化布署，系统已改为php语言，以下描述是初版，不再更新。稍后有时间会重新编辑功能说明。有兴趣的可以直接点击新版宣传页进行查看： IT设备销售管理系统 ，有购买意向或者定制功能的的可以直接联系站长。二、技术栈层级技术后端Express 5 + Node.js数据库MySQL（mysql2 连接池）Sessionexpress-session + express-mysql-session（MySQL 持久化，24h 有效期）安全helmet（安全响应头）、cors、express-rate-limit（限流）日志winston + winston-daily-rotate-file（按天轮转，保留 30 天）HTTP 日志morgan压缩compression（Gzip）文件上传自定义 multipart 解析数据导出exceljs（Excel）、pdfkit（PDF）前端框架Vue 3 + Vite + Element Plus路由vue-router 4（createWebHistory，懒加载）状态管理PiniaHTTP 客户端Axios图表Chart.js 4 + vue-chartjs三、项目结构├── server/ # 后端 │ ├── index.js # Express 入口，路由注册 │ ├── schema.sql # 数据库建表语句 │ ├── .env # 环境变量配置 │ ├── lib/ # 业务模块 │ │ ├── db.js # 数据库连接池与初始化 │ │ ├── env.js # .env 加载器 │ │ ├── auth.js # 认证（登录/登出/改密/会话校验） │ │ ├── session.js # Session 持久化配置 │ │ ├── rate-limiter.js # 请求限流 │ │ ├── devices.js # 设备 CRUD、公开查询、图片上传 │ │ ├── customers.js # 客户 CRUD、联系人管理 │ │ ├── assets.js # 资产 CRUD（配件/软件/耗材/服务） │ │ ├── todos.js # 维护记录 CRUD │ │ ├── dashboard.js # 仪表盘统计与图表 │ │ ├── config.js # 站点配置读写、LOGO 上传 │ │ ├── audit.js # 操作审计日志 │ │ ├── export.js # Excel / PDF 导出 │ │ ├── faults.js # 故障记录管理（JSON 文件存储） │ │ ├── logger.js # Winston 日志配置 │ │ ├── pagination.js # 分页工具 │ │ └── utils.js # 通用工具函数 │ └── package.json ├── public-vue/ # 前端构建产物（Vite build 输出） │ └── assets/ # 代码分割后的 JS/CSS 文件 └── data/ # 数据文件 └── faults.json # 故障记录（JSON 存储）四、环境要求Node.js >= 18MySQL >= 8.0五、快速开始# 1. 进入服务端目录 cd server # 2. 安装依赖 npm install # 3. 配置环境变量 cp .env.example .env # 编辑 .env，填入数据库连接信息和管理员账号 # 4. 启动服务 npm start # 或指定端口 PORT=8020 npm start启动后访问：前台查询页：http://localhost:3000/管理后台：http://localhost:3000/login六、环境变量配置在 server/.env 文件中配置：变量名说明默认值PORT服务监听端口3000DB_HOSTMySQL 主机地址—DB_PORTMySQL 端口3306DB_NAME数据库名称support_itDB_USER数据库用户名support_itDB_PASSWORD数据库密码自己设置ADMIN_USERNAME管理员用户名adminADMIN_PASSWORD管理员密码admin123SESSION_SECRETSession 加密密钥随机生成七、功能说明1、前台 — 客户自助查询前台面向客户，提供设备信息自助查询功能，如购买日期、质保期、脱保期、设备图片等。访问地址： /功能：前台设备查询输入客户编码（必填，精确匹配,防止有闲得蛋疼的访客手痒）输入设备序列号（必填，支持部分模糊匹配）单条结果直接展示详情卡片；多条结果展示列表，点击可展开，图片可以点击浮动放大查看详情展示：设备名称、序列号、客户信息、销售日期、质保期、质保截止日期、质保状态（在保/已过保/已报废）、多张设备图片（缩略图 + 灯箱预览）、备注多图灯箱支持左右箭头切换和圆点指示维护模式后台开启维护模式后，前台自动跳转至维护提示页面，暂停查询服务2、后台 — 管理系统访问地址： /login（未登录时自动跳转）默认账号： admin / admin1232.1、 业务看板后台首页，展示关键业务数据概览。统计卡片（8 个）：设备总数、客户总数、过保设备数、在保设备数、设备总值、配件总值、耗材总值、资产总值趋势图表（Chart.js 折线图）：展示各类资产价值随时间的变化趋势支持按月 / 按年切换统计粒度系统每月自动记录快照，历史数据持久化存储2.2、 维护记录管理设备的维护/维修/巡检记录。列表展示：标题、关联设备、维护日期、优先级（高/中/低）、状态（待处理/进行中/已完成）新增 / 编辑 / 删除维护记录关联设备：下拉选择已有设备，自动填充设备名称和序列号关键词搜索：支持按标题、设备名、备注模糊搜索状态标签：已完成（绿色）、待处理（红色）、进行中（蓝色）2.3、 客户管理管理客户信息及联系人。列表展示：客户编码、名称、地址、网址、联系人列表新增 / 编辑 / 删除客户多联系人：每个客户支持添加多个联系人（姓名、邮箱、电话），可动态增删唯一性校验：客户编码和名称不可重复删除保护：存在关联设备或资产时禁止删除自动同步：修改客户名称时，自动同步更新所有关联设备和资产的客户名称字段客户联动选择器：在设备/资产表单中输入编码自动匹配名称，反之亦然2.4、 设备管理管理客户设备信息，每台设备最多支持 3 张图片。列表展示：图片缩略图（+N 角标）、序列号、设备名称、客户编码/名称、销售日期、质保期、质保截止日期、质保状态（在保/过保/已报废）、价格新增 / 编辑 / 删除设备设备图片上传：每台设备最多 3 张，支持 JPG/PNG/GIF/WEBP，最大 5MB图片预览：表格内单击预览，支持灯箱大图浏览设备报废：标记设备为报废状态质保自动计算：根据销售日期 + 质保月数自动计算质保截止日期和状态客户联动：输入客户编码或名称自动匹配另一项唯一性校验：同一客户下序列号不可重复关键词搜索：支持按序列号、设备名称、客户名称搜索2.5、 资产管理（配件 / 软件 / 耗材 / 服务）四类资产统一管理，通过路由参数 :type 区分。2.5.1、配件列表展示：图片缩略图、名称、编码、所属设备、客户编码/名称、单价、数量、总价、质保状态、备注新增 / 编辑 / 删除配件质保状态自动计算（在保/已过保）报废功能归属设备：可选关联到已有设备，选择设备后自动匹配客户编码和名称客户联动选择：输入客户编码自动匹配客户名称质保期输入：支持设置质保月数2.5.2、软件功能与配件管理类似，支持质保状态自动计算、报废、归属设备关联、质保期输入。2.5.3、耗材列表展示：图片缩略图、名称、编码、所属设备、客户编码/名称、单价、数量、总价、购买日期、备注新增 / 编辑 / 删除耗材归属设备：可选关联到已有设备，选择后自动匹配客户编码和名称购买日期：支持记录购买日期无质保期字段2.5.4、服务列表展示：名称、编码、客户编码/名称、价格、起始日期、服务时间、服务状态、备注新增 / 编辑 / 删除服务服务期限自动计算：起始日期 + 服务月数脱保状态自动判断无数量字段（服务按项计费）无归属设备字段2.5.5、资产管理通用功能图片上传：每项资产最多 3 张，支持预览和删除Excel 导入/导出客户联动：所有资产模块均支持客户编码/名称自动匹配2.6、 系统配置管理系统全局设置。基础配置系统名称（同步前台标题和后台侧边栏）站点域名文字 LOGO（侧边栏显示）图片 LOGO 上传（支持 JPG / PNG / GIF / WEBP / SVG，最大 2MB）调试/维护模式开关（开启后前台跳转维护页）版权信息：支持 HTML 内容，显示在前台和后台页脚SEO / TDK：保存后服务端注入 HTML，立即生效页面标题、SEO 元信息从后台配置读取，服务端直接注入 HTML系统名称、LOGO、版权信息动态渲染2.7、 数据导出支持将各模块数据导出为 Excel 或 PDF 文件。可导出模块：设备列表、资产列表（配件/软件/耗材/服务）、客户列表、维护记录、审计日志导出格式：Excel（.xlsx）/ PDF导出字段包含各模块的核心信息2.8、 审计日志系统自动记录所有关键操作，并提供后台查询界面。记录内容：操作人、操作类型、目标类型、目标 ID、操作详情、IP 地址、浏览器标识、操作时间自动记录的操作：登录/登出、设备的增删改/报废、客户的增删改、资产的增删改/报废、维护记录的增删改、修改密码、更新系统配置、上传 LOGO 等查询筛选：操作人、操作类型、日期时间范围分页浏览：每页 20 条2.9、 界面主题暗色主题：全后台采用暗色主题配色，Element Plus 组件（表格、输入框、按钮、对话框、下拉框、分页、标签、气泡提示等）全局适配暗色统一圆角：表格 12px 圆角，与卡片风格一致多级菜单：资产侧边栏按类型分类八、数据库表结构系统共使用 10 张数据表：表名说明admin_users管理员账号（用户名、密码哈希、盐值）site_settings站点配置（KV 键值对结构）customers客户信息（编码、名称、地址、网址）customer_contacts客户联系人（姓名、邮箱、电话，关联客户）devices设备信息（序列号、名称、客户、质保、价格、图片 url×3、状态）assets资产信息（配件/软件/耗材/服务统一存储，含类型、质保、服务、归属设备等字段）todos维护记录（标题、状态、优先级、关联设备）value_history资产价值历史快照（按月/年记录各类资产价值）sessionsSession 持久化（express-mysql-session 自动管理）audit_logs操作审计日志（操作人、类型、详情、IP、时间）完整建表语句见 server/schema.sql。九、部署说明1、前端/public-vue/2、使用 PM2 守护进程（推荐）npm install -g pm2 cd server pm2 start index.js --name "support-it" pm2 save pm2 startup3、使用 Nginx 反向代理server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }4、数据库初始化系统启动时会自动执行 server/schema.sql 创建所需表结构，无需手动导入。5、宝塔面板布署教程5.1、资产管理系统的文件列表说明show 目录：系统功能说明图片及演示网页database.sql.zip 示例数据库文件public-vue.zip 前端程序readme.md 系统功能说明server.zip 后端程序5.2、建立数据库在宝塔的数据库中建立如下数据库：记得把这些信息拷贝备用：数据库名称、帐号、密码数据库建立完成后，上传示例数据库文件并导入database.sql.zip5.3、上传前后端程序在宝塔文件管理中建立系统专用的目录Asset-Management-System并把前后端程序上传如下：public-vue为前端程序目录，server为后端程序目录5.4、修改数据库连接信息server目录下有个.env文件，打开它：把你刚才建立的数据库里的信息填入，如实填就行，不要和我的一样。5.5、建立前端网站网站建立的时候选择纯静态即可，网站目录要选择前端目录public-vue5.6、安装及运行服务端在宝塔的--网站--Node项目中，打开Node项目管理器，安装好Node.js依赖再添加一个Node项目，目录要选择后端目录server，其它的都是自动匹配的，不需要手动去填，最下面的包管理器要手动选择npm顺利的话确定之后后端程序就应该跑起来了后端面板查看一下运行日志，确定没问题，成功在跑这时候我们就可以通过以下链接进行使用了：管理后台: http://localhost:3000/admin/前台访问: http://192.168.64.13:30005.7、做反代去端口访问程序为了和其它项目共生，默认并没有直接跑80端口，我设置了3000端口使用。为了去端口访问，我们可以在宝塔做个反向代理即可在网站前台项目属性找到反向代理，进行如下设置即可：如此，我们的资产管理系统就算正式上线了！十、安全说明管理员密码使用 scrypt 算法加盐哈希存储登录接口限流 5 次/分钟/IP，防止暴力破解API 接口限流 100 次/分钟/IP（含前台查询和后台管理接口）Session 有效期 24 小时，存储在 MySQL 中使用 helmet 设置安全响应头所有管理接口需登录认证关键操作自动记录审计日志（含 IP 和 User-Agent）十一、管理员密码找回若忘记管理员密码，可通过以下方式重置：1、直接修改数据库# 进入 MySQL mysql -u 数据库用户名 -p # 使用系统数据库 USE support_it; # 删除管理员账号，系统重启后会自动使用 .env 中的配置重新创建默认账号 DELETE FROM admin_users WHERE username = 'admin';重启服务后，系统会自动使用 .env 中配置的 ADMIN_USERNAME 和 ADMIN_PASSWORD 创建管理员账号。2、默认账号用户名：admin（可在 .env 中配置 ADMIN_USERNAME）密码：admin123（可在 .env 中配置 ADMIN_PASSWORD）十二、下载地址百度网盘下载地址，说明：此价格仅是程序包价格，不提供技术支持，请自行研究。若需要代布署至服务器，或者功能需要定制，请联系站长，提供有偿服务。