Jenkins+Maven+Git自动化部署实战:从脆弱脚本到稳定流水线
最近在帮一个团队做持续集成流程优化发现一个挺有意思的现象很多人把 Jenkins、Maven、Git 这几个工具都装上了脚本也跑起来了但整个自动化部署流程依然脆弱得像纸糊的一样。一次代码提交构建成功部署到测试环境看起来一切顺利。可一旦换台机器、换个分支或者只是加了个新的依赖整个流程就可能卡在某个莫名其妙的环节报错信息让人摸不着头脑最后还得手动登录服务器去救火。这让我意识到真正的“自动化部署指南”重点从来不是把命令和插件列表堆砌出来。那只是说明书。真正的价值在于如何把一次性的、依赖个人经验的部署动作转化成一个稳定、可重复、可追溯的工程化流程。它解决的不仅是“省几分钟”的问题而是把“构建-测试-部署”这个链条从黑盒变成白盒让每次发布的风险可控让团队协作的基线清晰。所以今天我们不聊那些随处可见的安装命令。我们来聊聊如何用 Jenkins、Maven、Git 搭建一个真正能扛事的自动化部署流水线。这套方案的核心不是“全”而是“稳”和“可演进”。你会看到从单次手动触发到全自动流水线中间需要跨越的远不止几个插件。1. 自动化部署的真正目标从“能跑通”到“敢交付”在开始敲任何命令之前我们必须先对齐认知我们为什么要做自动化部署很多人会脱口而出为了节省时间为了减少人为失误。这没错但这是表层价值。更深层的目标是建立一种可靠的、标准化的交付能力。想象一下这个场景开发人员 A 在本地功能分支上完成了开发自测通过。现在他需要把代码合并到主分支并部署到测试环境供 QA 验证。在没有自动化流程时他可能需要手动合并代码解决冲突。在本地执行mvn clean package祈祷不要有本地环境特有的问题。通过 SCP 或 FTP 把打包好的 JAR/WAR 文件传到测试服务器。SSH 登录服务器备份旧版本停止服务替换文件重启服务。观察日志确认服务启动成功。这个过程充满了不确定性本地 Maven 仓库的缓存是否干净服务器上的 JDK 版本是否一致服务停止时是否有处理中的请求被强制中断任何一个环节出问题都可能让部署卡住半小时更糟的是可能引入难以排查的线上问题。自动化部署就是要消灭这些不确定性。它的核心产出不是一个“构建成功的绿色图标”而是一条从代码提交到服务上线的、全程可观测、可回滚的标准化路径。这条路径上每个环节编译、测试、打包、部署都应该是独立、可验证的。Jenkins 在这里扮演的角色是“流程编排器”和“状态记录员”而不是一个简单的脚本执行器。因此搭建流水线的第一步不是安装 Jenkins而是设计流程。一个健壮的 CI/CD 流水线至少应该包含以下几个阶段并且每个阶段失败都应能自动停止流程并通知负责人代码质量门禁在合并前或构建前通过 Git Hook 或 Jenkins 插件检查代码规范如 SonarQube、基础语法等。依赖与编译在一个干净的、可控的环境中进行确保产物只依赖于版本库中的声明而非构建者的本地状态。自动化测试运行单元测试、集成测试并且测试覆盖率需要达到预设门槛。构建与打包生成最终可部署的制品如 Docker 镜像、JAR 文件并上传到制品库如 Nexus, Jfrog Artifactory。部署到测试环境将制品自动部署到集成测试或预发布环境。人工验证/自动化验收在测试环境进行手动或自动化的验收测试。部署到生产环境通常需要手动触发或审批后自动进行。我们的指南将围绕如何用 Jenkins Pipeline声明式或脚本式来串联这些阶段并确保每个环节的稳定。2. 环境准备为“稳定”打下地基而非仅仅“可用”很多教程会告诉你“安装 JDK、Maven、Git、Jenkins然后就可以开始了。”这就像盖房子只说了“需要砖头和水泥”。我们得知道用什么标号的水泥砖头怎么砌地基要打多深。2.1 基础设施与版本锁定首先强烈建议将所有构建环境容器化。无论是使用 Jenkins 的 Docker Agent还是直接在 Docker 里运行 Maven 构建容器化能保证每次构建的环境绝对一致。这是消除“在我本地是好的”这类问题的最有效手段。如果暂时无法全面容器化那么必须严格锁定版本JDK明确使用某个特定版本如 OpenJDK 11.0.xx并在 Jenkins 全局工具配置和服务器上保持一致。Maven同样指定版本并使用项目自带的maven-wrapper或通过 Jenkins 工具配置统一管理。更重要的是需要配置一个稳定、高速的中央仓库镜像如阿里云镜像并在settings.xml中明确避免因网络问题导致构建失败。Git版本影响不大但需要确保认证方式SSH 密钥或用户名密码在 Jenkins 中配置正确。2.2 Jenkins 的“正确”安装与关键配置安装 Jenkins 本身很简单但初始配置决定了后续的维护成本。用户与权限不要永远使用admin账户。根据团队角色创建用户并利用Role-Based Strategy插件进行细粒度权限控制。比如开发人员只能触发自己项目的构建运维人员可以管理节点和凭据。凭据管理这是安全核心。将访问 Git 仓库的 SSH 私钥、访问制品库的用户名密码、服务器 SSH 密钥等全部存入 Jenkins 的“凭据”系统。在 Pipeline 中通过credentials()函数引用绝对不要将密码明文写在脚本里。节点管理如果构建任务繁重需要配置 Agent 节点。确保主节点与 Agent 节点之间的通信稳定并且 Agent 节点的环境尤其是 Docker、JDK是清洁和一致的。可以为不同项目如前端、后端、移动端配置不同标签的 Agent。2.3 Maven 与 Git 的工程化配置Maven除了settings.xml项目本身的pom.xml是重中之重。必须规范所有依赖的版本号通过dependencyManagement或properties统一管理。使用maven-enforcer-plugin等插件强制约定 JDK 版本、Maven 版本、依赖版本冲突规则。配置好maven-surefire-plugin以正确收集单元测试报告并集成jacoco-maven-plugin收集覆盖率报告供 Jenkins 展示。Git规范分支模型是关键。推荐使用GitFlow或GitHub Flow等简化模型。在 Jenkins Pipeline 中可以通过BRANCH_NAME环境变量来区分不同分支的构建行为例如仅对main和develop分支进行自动化部署到测试环境。3. 构建 Pipeline 骨架声明式 Pipeline 的最佳实践Jenkins Pipeline 有两种语法声明式Declarative和脚本式Scripted。对于大多数项目声明式 Pipeline 更清晰、更结构化是首选。下面是一个包含了核心阶段的 Pipeline 骨架模板。pipeline { agent any // 或指定 label如 agent { label java-agent } tools { // 在Jenkins全局工具配置中定义好的工具 maven Maven-3.8.6 jdk OpenJDK-11 } environment { // 定义全局环境变量如制品版本、仓库地址等 ARTIFACT_ID readMavenPom().getArtifactId() VERSION readMavenPom().getVersion() NEXUS_URL https://nexus.yourcompany.com/repository/maven-releases/ } options { // 管道级别的配置 timeout(time: 1, unit: HOURS) // 构建超时设置 buildDiscarder(logRotator(numToKeepStr: 10)) // 保留最近10次构建记录 disableConcurrentBuilds() // 禁止并行构建避免竞争 } stages { stage(代码检出与准备) { steps { checkout scm // 检出触发此次构建的代码 script { // 可以在这里进行一些预检查如判断分支 currentBranch env.GIT_BRANCH ?: sh(script: git rev-parse --abbrev-ref HEAD, returnStdout: true).trim() } } } stage(编译与单元测试) { steps { sh mvn clean compile test // 建议将参数放在pom.xml或settings.xml中 } post { always { junit **/target/surefire-reports/*.xml // 收集测试报告 jacoco() // 收集代码覆盖率报告需安装Jacoco插件 } } } stage(代码质量分析) { when { // 例如只在主干分支或定时构建时运行因为比较耗时 branch main } steps { sh mvn sonar:sonar // 需要预先配置SonarQube服务器和令牌 } } stage(打包与上传制品) { steps { sh mvn package -DskipTests // 跳过测试因为上一步已执行 script { // 将打好的包如target/*.jar上传到制品库 // 示例使用Nexus插件或curl命令 def jarFile sh(script: find target -name ${ARTIFACT_ID}-*.jar -not -name *sources* -not -name *javadoc*, returnStdout:).trim() nexusArtifactUploader( nexusVersion: nexus3, protocol: https, nexusUrl: NEXUS_URL, groupId: com.yourcompany, version: VERSION, repository: maven-releases, credentialsId: nexus-credential, artifacts: [ [artifactId: ARTIFACT_ID, classifier: , file: jarFile, type: jar] ] ) } } } stage(部署到测试环境) { when { // 例如当分支是develop或main且上阶段成功时 expression { currentBranch develop || currentBranch main } } steps { script { // 这里可以是SSH到服务器执行命令或调用K8s API或使用Ansible等 // 示例通过SSH插件执行远程命令 sshPublisher( publishers: [ sshPublisherDesc( configName: test-server-ssh, // Jenkins中配置的SSH服务器 transfers: [ sshTransfer( sourceFiles: target/${ARTIFACT_ID}-*.jar, removePrefix: target, remoteDirectory: /opt/app/, execCommand: cd /opt/app/ ./stop.sh || true ./start.sh ) ] ) ] ) } } } stage(集成测试) { when { // 部署到测试环境成功后执行 expression { currentBranch develop || currentBranch main } } steps { // 可以运行一些针对测试环境的API自动化测试 // 例如使用Postman、RestAssured或Selenium echo 运行集成测试... // sh mvn verify -Pintegration-test } post { always { // 收集集成测试报告 // junit **/target/failsafe-reports/*.xml } } } } post { // 整个Pipeline执行后的处理 always { echo Pipeline ${currentBuild.fullDisplayName} 执行完成。 // 清理工作空间可选有时需要保留日志 // cleanWs() } success { echo 构建成功 // 可以发送成功通知到钉钉/企业微信等 } failure { echo 构建失败 // 必须发送失败通知并附上构建日志链接 // emailext to: teamyourcompany.com, subject: 构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}, body: 请检查: ${env.BUILD_URL} } unstable { echo 构建状态为不稳定如测试失败。 } } }这个模板是一个坚实的起点。它包含了从代码检出到部署测试环境的核心阶段并集成了测试报告、代码覆盖率、制品管理。但请注意这仅仅是骨架。真正的血肉在于每个阶段内部的异常处理、日志记录和状态反馈。4. 填平那些让你“翻车”的坑从构建到部署的实战细节有了骨架我们来看看哪些细节会让你的流水线从“演示成功”变成“生产可用”。4.1 依赖下载与缓存问题问题Maven 构建最常因网络超时下载依赖失败导致整个构建失败。解决使用可靠的镜像仓库在 Jenkins Agent 的settings.xml或项目级settings.xml中配置阿里云等国内镜像。启用依赖缓存对于非容器化 Agent可以配置 Maven 的本地仓库为共享目录避免每次构建都重新下载全部依赖。对于 Docker Agent可以在 Dockerfile 中预先下载基础依赖层或者使用volume挂载一个持久的 Maven 仓库。重试机制在mvn命令中加入-Dmaven.wagon.http.retryHandler.count3等参数或在 Pipeline 的retry块中包装下载步骤。4.2 测试的稳定性与报告集成问题单元测试偶尔失败可能是数据或环境问题导致整个 Pipeline 变红。解决隔离测试环境单元测试应该是无状态的、不依赖外部服务的。使用内存数据库如 H2和 Mock 框架。区分单元测试与集成测试使用maven-failsafe-plugin运行集成测试并将其放在package阶段之后。这样即使集成测试失败至少制品已经生成。处理不稳定测试对于已知的、暂时难以修复的不稳定测试可以用Flaky注解标记或在 Jenkins 中配置“测试结果分析”插件忽略特定失败。报告可视化确保junit和jacoco插件正确配置失败时能快速定位到是哪条测试用例、哪行代码出了问题。4.3 制品管理与版本号问题打出来的包版本混乱无法追溯对应代码版本。解决版本号自动化不要手动改pom.xml里的版本。对于发布版本可以使用maven-release-plugin或 Jenkins 的Version Number插件自动生成版本号如1.0.${BUILD_NUMBER}。更现代的做法是使用git-commit-id插件将 Git Commit SHA 嵌入到 JAR 的MANIFEST.MF中。上传制品库如模板所示必须将最终产物JAR/WAR上传到 Nexus、Artifactory 等制品库。这不仅是为了存档更是为了部署环节能拉取到唯一的、经过验证的二进制文件实现“一次构建多处部署”。Docker 化进阶最佳实践是将应用打包成 Docker 镜像并推送到镜像仓库如 Harbor。镜像标签同样与构建号或 Git Commit 关联。这样部署就变成了拉取指定镜像并运行环境一致性得到终极保障。4.4 部署环节的可靠性与回滚问题部署脚本不健壮服务启动失败后无法自动回滚。解决脚本原子化与幂等性部署脚本如start.sh,stop.sh需要精心编写。停止服务前检查进程是否存在启动服务后检查健康端点如/actuator/health并设置超时和重试。蓝绿部署或滚动更新对于有多个实例的服务采用蓝绿部署或滚动更新策略可以做到无缝切换和快速回滚。这通常需要与 Kubernetes 或云平台集成。在 Pipeline 中实现回滚在部署阶段后增加一个“健康检查”阶段。如果健康检查失败自动触发回滚操作。回滚可以是重新部署上一个版本的制品从制品库拉取或者调用基础设施的 API 进行流量切换。日志与通知部署过程中的所有关键操作开始部署、停止服务、启动服务、健康检查结果都应通过日志输出并在失败时通过邮件、即时通讯工具通知相关人员。日志需要包含足够上下文如构建编号、Git Commit、部署目标等。4.5 Pipeline 本身的维护与优化问题Pipeline 脚本越来越长难以维护多个项目间存在重复代码。解决使用共享库将通用的步骤如构建、部署、通知封装成 Jenkins Shared Library。各个项目的 Jenkinsfile 只需要调用这些库函数极大减少重复和错误。参数化构建允许手动触发构建时选择分支、部署环境、版本等参数增加灵活性。并行执行如果某些阶段互不依赖如单元测试和静态代码分析可以放在parallel块中执行缩短整体构建时间。定期清理配置 Jenkins 的buildDiscarder和定期清理工作空间防止磁盘被占满。5. 从流水线到平台建立团队的持续交付文化工具和流程搭建好了但这只是开始。自动化部署要真正发挥作用需要融入团队的工作习惯形成文化。“流水线即代码”将Jenkinsfile和部署脚本与应用代码一起存放在 Git 仓库中。任何对流程的修改都需要经过代码评审保证了流程的可追溯性和一致性。质量门禁前置不要等到 Jenkins 构建失败了才发现问题。利用 Git 的pre-commit或pre-pushhook 在本地运行基础检查如代码格式化、静态分析。将 SonarQube 质量阈Quality Gate与 Pipeline 集成不达标则无法合并。可视化与反馈将 Jenkins 构建状态通过插件集成到 Git 仓库界面如 GitHub 的 Commit Status。让团队成员一目了然地看到每次提交的状态。构建失败的通知要及时、准确并附带直接的问题链接。持续优化定期回顾 Pipeline 的执行效率。哪些阶段最耗时哪些失败最频繁根据数据驱动优化比如引入更快的构建机器拆分巨型单体应用优化测试用例。最终一个优秀的自动化部署系统会让“发布”变成一个平淡无奇、低风险的常规操作。开发人员可以专注于创造功能而不是担忧部署的泥潭。当你发现团队不再需要专门的“发布日”当任何一位成员都可以自信地点击“部署生产”按钮时这套流程的价值才真正显现出来。它不再只是一份技术指南的堆砌而是成为了团队研发效能和工程可靠性的坚实底座。

相关新闻

PHP版本迁移实战:从PHP 5/6遗留代码到PHP 8.2的现代化重构指南

PHP版本迁移实战:从PHP 5/6遗留代码到PHP 8.2的现代化重构指南

在实际项目中,PHP 6 是一个从未正式发布的版本,但围绕其概念、特性传闻以及从 PHP 5 到 PHP 7 的跨越式升级,开发者社区积累了大量的讨论、误解和实际迁移经验。许多遗留系统或教程中可能仍会提及“PHP 6”,这通常指的是一个已被放…

2026/10/11 21:35:21 阅读更多 →
Vue CLI架构解析与迁移Vite实战指南

Vue CLI架构解析与迁移Vite实战指南

1. Vue CLI 的前世今生与现状定位Vue CLI 作为 Vue.js 官方提供的标准脚手架工具,自2016年诞生以来经历了多次重大迭代。当前最新稳定版本为v5.x,但官方已在文档顶部明确标注"Maintenance Mode"(维护模式)。这意味着&am…

2026/10/4 5:04:07 阅读更多 →
C++与QML实战:从零构建桌面井字棋游戏

C++与QML实战:从零构建桌面井字棋游戏

1. 项目概述:为什么选择C与QML来构建桌面游戏?最近在整理一些给团队新人练手的小项目,发现“井字棋”这个看似简单的游戏,其实是一个绝佳的入门沙盒。它规则清晰,逻辑不复杂,但足以串联起现代桌面应用开发的…

2026/10/14 10:25:33 阅读更多 →

最新新闻

SSM+Django混合架构在鲜花电商系统的实践与优化

SSM+Django混合架构在鲜花电商系统的实践与优化

1. 项目概述:当鲜花遇上全栈技术 这个看似传统的网上花店系统,实际上是一个融合了Java企业级开发与Python快速迭代优势的复合型项目。SSM(SpringSpringMVCMyBatis)作为后端核心框架,提供了稳定的订单处理和库存管理能力…

2026/10/14 12:55:56 阅读更多 →
智能数据分析平台有哪些?五个主流平台的数据模型与选型对比

智能数据分析平台有哪些?五个主流平台的数据模型与选型对比

摘要: 面对多样化的企业数据分析诉求,不同分析平台在底层数据组织、模型设计与分析颗粒度上存在显著差异。本文对比 GrowingIO、FineBI、Tableau、Power BI 与 Quick BI 五个平台的技术架构特征、核心计算模型、协同方式及部署要求,结合多源数…

2026/10/14 12:55:56 阅读更多 →
视觉重定位:层级场景坐标回归如何拆解分类与回归难题

视觉重定位:层级场景坐标回归如何拆解分类与回归难题

1. 视觉重定位到底在解决什么问题视觉重定位这个词,第一次接触的人容易把它和SLAM搞混。SLAM是"一边走一边建图,同时知道自己在哪里",而重定位是"我手里有一张图,或者我正站在某个位置拍了一张照片,我要…

2026/10/14 12:55:56 阅读更多 →
SR-IOV网卡虚拟化:硬件级VF分流原理与生产落地实践

SR-IOV网卡虚拟化:硬件级VF分流原理与生产落地实践

1. 项目概述:为什么一张网卡要“分身”成几十个独立网口?你有没有遇到过这种场景:一台服务器上跑着十几个虚拟机,每个都要直连物理网络,低延迟、高吞吐、零丢包——结果发现宿主机的网卡成了瓶颈?CPU软中断…

2026/10/14 12:55:56 阅读更多 →
SpringBoot+Vue党员教育管理系统开发实践

SpringBoot+Vue党员教育管理系统开发实践

1. 项目背景与核心价值 党员教育和管理系统是当前基层党组织数字化转型的重要工具。这个基于SpringBootVue的全栈项目,完美契合高校计算机专业毕业设计的选题要求,同时具备实际应用价值。我在参与某机关单位党建系统升级时,深刻体会到这类系统…

2026/10/14 12:55:56 阅读更多 →
podofo-0.9.5-vs2013-x86:老项目里PDF解析库的集成与避坑指南

podofo-0.9.5-vs2013-x86:老项目里PDF解析库的集成与避坑指南

简介:Podofo 0.9.5 在 Visual Studio 2013 下编译生成的 x86 预编译库,面向需要在 Windows 平台读写、编辑 PDF 文件的 C 开发者。Podofo 是一套开源 PDF 处理库,支持元数据读取、页面渲染、文本抽取、文档生成等常用能力,本包省去…

2026/10/14 12:54:56 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →