/** * 智能图书管理系统 (MCSLMS) - 单分支流水线 * 四端统一构建:CLI、GUI、Web、Android * 含单元测试、SonarQube 质量扫描、jpackage EXE、头歌平台发布 */ pipeline { agent any // 触发方式:Gitea Webhook 推送触发 (Generic Webhook Trigger) // Gitea 配置:仓库 -> 设置 -> Webhooks -> http://localhost:8084/generic-webhook-trigger/invoke?token=mcslms // Jenkins 配置:Token=mcslms, Token Credential=空 options { buildDiscarder(logRotator(numToKeepStr: '10')) timestamps() timeout(time: 60, unit: 'MINUTES') disableConcurrentBuilds() } environment { JAVA_HOME = 'E:\\2025-2026\\GitAIOps\\jdk' ANDROID_HOME = 'D:/development/Android' SONAR_HOST_URL = 'http://localhost:9000' SONAR_PROJECT_KEY = 'mcslms' SONAR_PROJECT_NAME = 'mcslms' RELEASE_VERSION = "v1.0.0.${BUILD_NUMBER}" } stages { stage('1. 环境信息') { steps { echo '==========================================' echo ' 智能图书管理系统 (MCSLMS) - 四端统一构建' echo '==========================================' echo "构建号: ${env.BUILD_NUMBER}" echo "版本号: ${env.RELEASE_VERSION}" bat 'java -version' } } stage('2. 构建 Core') { steps { echo '>>> [1/5] 构建核心模块' bat 'gradlew.bat :core:build -x test --no-daemon' } } stage('3. 运行单元测试') { steps { echo '>>> 运行单元测试并生成覆盖率报告' script { try { // 运行测试并生成JaCoCo覆盖率报告 bat 'gradlew.bat test jacocoTestReport --no-daemon' echo '✓ 测试执行完成,覆盖率报告已生成' } catch (Exception e) { echo "⚠️ 测试失败,但继续流水线: ${e.message}" currentBuild.result = 'UNSTABLE' } } } post { always { junit allowEmptyResults: true, testResults: '**/build/test-results/test/*.xml' publishHTML(target: [ allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'core/build/reports/tests/test', reportFiles: 'index.html', reportName: 'Core 测试报告' ]) // 发布JaCoCo覆盖率报告 publishHTML(target: [ allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'core/build/reports/jacoco/test/html', reportFiles: 'index.html', reportName: 'Core 覆盖率报告' ]) } } } stage('4. SonarQube 质量扫描') { steps { echo '>>> 执行 SonarQube 代码质量检测' withSonarQubeEnv('SonarQube') { bat ''' gradlew.bat sonar ^ -Dsonar.projectKey=%SONAR_PROJECT_KEY% ^ -Dsonar.projectName=%SONAR_PROJECT_NAME% ^ -Dsonar.host.url=%SONAR_HOST_URL% ^ -Dsonar.gradle.skipCompile=true ^ -Dsonar.java.binaries=core/build/classes/java/main ^ -Dsonar.coverage.jacoco.xmlReportPaths=core/build/reports/jacoco/test/jacocoTestReport.xml,cli/build/reports/jacoco/test/jacocoTestReport.xml,gui/build/reports/jacoco/test/jacocoTestReport.xml,backend/build/reports/jacoco/test/jacocoTestReport.xml ^ --no-daemon ''' } echo '✓ SonarQube 分析完成' } } stage('5. 构建四端') { parallel { stage('CLI') { steps { echo '>>> [2/5] 构建 CLI 命令行端 (fatJar)' bat 'gradlew.bat :cli:fatJar -x test -PbuildNumber=%BUILD_NUMBER% --no-daemon' } } stage('GUI') { steps { echo '>>> [3/5] 构建 GUI 桌面端 (fatJar)' bat 'gradlew.bat :gui:fatJar -x test -PbuildNumber=%BUILD_NUMBER% --no-daemon' } } stage('Backend JAR') { steps { echo '>>> [4/5] 构建 Web 后端 (bootJar)' bat 'gradlew.bat :backend:bootJar -x test -PbuildNumber=%BUILD_NUMBER% --no-daemon' } } stage('Backend WAR') { steps { echo '>>> [4/5] 构建 Web 后端 (bootWar)' bat 'gradlew.bat :backend:bootWar -x test -PbuildNumber=%BUILD_NUMBER% --no-daemon' } } stage('Android') { steps { echo '>>> [5/5] 构建 Android App' bat 'gradlew.bat :android:assembleDebug --no-daemon' } } } } stage('6. 创建 GUI EXE (jpackage)') { steps { echo '>>> 使用 jpackage 创建 Windows EXE' script { try { bat ''' @echo off setlocal EnableDelayedExpansion set JAVA_HOME=%JAVA_HOME% echo ============================================ echo jpackage 打包 GUI 应用 echo ============================================ REM 检查 jpackage echo [1/4] 检查 jpackage... jpackage --version >nul 2>&1 if errorlevel 1 ( echo ✗ jpackage 不可用,需要 JDK 14+ exit /b 1 ) for /f "tokens=*" %%i in ('jpackage --version') do echo ✓ jpackage: %%i REM 准备输入目录 echo [2/4] 准备 jpackage 输入... if exist jpackage-input rmdir /S /Q jpackage-input mkdir jpackage-input REM 查找 GUI fatJar (带 -all 后缀) set GUI_JAR= for %%f in (gui\\build\\libs\\mcslms-gui-v*.jar) do set GUI_JAR=%%f if "!GUI_JAR!"=="" ( echo ✗ 找不到 GUI JAR exit /b 1 ) copy /Y "!GUI_JAR!" jpackage-input\\mcslms-gui.jar >nul echo ✓ 复制 !GUI_JAR! REM 创建 app-image echo [3/4] 创建 app-image... if exist jpackage-output rmdir /S /Q jpackage-output jpackage ^ --type app-image ^ --input jpackage-input ^ --name MCSLMS ^ --main-jar mcslms-gui.jar ^ --main-class com.smartlibrary.gui.GUIApplication ^ --app-version 1.0.%BUILD_NUMBER% ^ --vendor "CHZU" ^ --description "智能图书管理系统 GUI 客户端" ^ --add-modules java.base,java.desktop,java.sql,java.logging,java.naming,java.management,java.prefs,java.xml,jdk.unsupported ^ --java-options "-Xmx512m" ^ --dest jpackage-output if errorlevel 1 ( echo ✗ app-image 创建失败 exit /b 1 ) echo ✓ app-image 创建成功 REM 创建 EXE 安装包 echo [4/4] 创建 EXE 安装包... jpackage ^ --type exe ^ --app-image jpackage-output\\MCSLMS ^ --name MCSLMS ^ --app-version 1.0.%BUILD_NUMBER% ^ --vendor "CHZU" ^ --description "智能图书管理系统 GUI 客户端" ^ --win-menu ^ --win-dir-chooser ^ --win-shortcut ^ --dest artifacts if errorlevel 1 ( echo ⚠️ EXE 创建失败,创建 ZIP 绿色版... powershell -Command "Compress-Archive -Path jpackage-output\\MCSLMS -DestinationPath artifacts\\mcslms-gui.zip -Force" if exist artifacts\\mcslms-gui.zip ( echo ✓ ZIP 绿色版创建成功 ) ) else ( REM 重命名 EXE if exist artifacts\\MCSLMS-1.0.%BUILD_NUMBER%.exe ( move /Y artifacts\\MCSLMS-1.0.%BUILD_NUMBER%.exe artifacts\\mcslms-gui-v1.0.0.%BUILD_NUMBER%.exe >nul echo ✓ EXE 创建成功: mcslms-gui-v1.0.0.%BUILD_NUMBER%.exe ) ) echo. echo ============================================ echo jpackage 打包完成 echo ============================================ ''' echo '✓ GUI EXE 创建成功' } catch (Exception e) { echo "⚠️ jpackage 打包失败: ${e.message}" currentBuild.result = 'UNSTABLE' } } } } stage('7. 归档制品') { steps { echo '>>> 归档构建产物' archiveArtifacts artifacts: '**/build/libs/*.jar, **/build/libs/*.war, **/build/outputs/apk/**/*.apk, artifacts/*.exe, artifacts/*.zip', fingerprint: true, allowEmptyArchive: true } } stage('8. 部署到 Tomcat') { steps { echo '========== 部署到本地 Tomcat ==========' script { def tomcatHome = 'E:\\2025-2026\\GitAIOps\\tomcat' def tomcatWebapps = "${tomcatHome}\\webapps" def tomcatBin = "${tomcatHome}\\bin" def appName = 'mcslms' try { // 防止 Jenkins 杀死衍生进程 env.BUILD_ID = "dontKillMe" bat """ @echo off setlocal EnableDelayedExpansion echo ============================================ echo MCSLMS Web 应用部署到 Tomcat echo ============================================ echo. REM 检查端口 8080 占用 echo [1/6] 检查端口 8080 占用... for /f "tokens=5" %%a in ('netstat -ano ^| findstr :8080 ^| findstr LISTENING') do ( if not "%%a"=="0" ( echo 发现占用端口 8080 的进程 PID: %%a taskkill /F /PID %%a 2>nul echo ✓ 已终止进程 ) ) echo. REM 停止 Tomcat echo [2/6] 停止 Tomcat... if exist "${tomcatBin}\\shutdown.bat" ( call "${tomcatBin}\\shutdown.bat" 2>nul timeout /t 5 /nobreak >nul echo ✓ Tomcat 已停止 ) echo. REM 清理旧应用 echo [3/6] 清理旧应用... if exist "${tomcatWebapps}\\${appName}.war" ( del /F /Q "${tomcatWebapps}\\${appName}.war" echo ✓ 删除旧 WAR 包 ) if exist "${tomcatWebapps}\\${appName}" ( rmdir /S /Q "${tomcatWebapps}\\${appName}" echo ✓ 删除旧应用目录 ) echo. REM 查找并部署新 WAR 包 echo [4/6] 部署新 WAR 包... set WAR_FOUND=0 for %%f in (backend\\build\\libs\\mcslms-web-*.war) do ( if !WAR_FOUND!==0 ( copy /Y "%%f" "${tomcatWebapps}\\${appName}.war" >nul echo ✓ 部署 WAR 包: %%f set WAR_FOUND=1 ) ) if !WAR_FOUND!==0 ( echo ✗ 错误: 找不到 WAR 文件 exit /b 1 ) echo. REM 复制数据库文件 echo [5/6] 复制数据库文件... if exist "data\\library.db" ( copy /Y "data\\library.db" "${tomcatBin}\\library.db" >nul echo ✓ 复制数据库: data\\library.db ) else ( echo ⚠️ 警告: 未找到 data\\library.db ) echo. REM 启动 Tomcat echo [6/6] 启动 Tomcat... set CATALINA_HOME=${tomcatHome} start "Tomcat-MCSLMS" /B "${tomcatBin}\\catalina.bat" run > "${tomcatHome}\\logs\\mcslms-tomcat.log" 2>&1 echo ✓ Tomcat 正在启动... echo. """ echo '等待 Tomcat 启动 (30秒)...' sleep 30 echo '验证应用访问...' bat """ @echo off echo. echo ============================================ echo 验证 MCSLMS 应用 echo ============================================ powershell -NoLogo -NoProfile -Command "try { \$response = Invoke-WebRequest -Uri 'http://localhost:8080/${appName}/' -UseBasicParsing -TimeoutSec 10; if (\$response.StatusCode -eq 200) { Write-Host '✓ 部署成功!应用已可访问'; Write-Host ' 访问地址: http://localhost:8080/${appName}/'; exit 0 } else { Write-Host '✗ 访问失败,状态码:' \$response.StatusCode; exit 1 } } catch { Write-Host '⚠️ 无法访问应用:' \$_.Exception.Message; Write-Host ' 请检查 Tomcat 日志'; exit 0 }" echo. echo 部署信息: echo - 应用名称: ${appName} echo - 访问地址: http://localhost:8080/${appName}/ echo - Tomcat目录: ${tomcatHome} echo - 日志文件: ${tomcatHome}\\logs\\mcslms-tomcat.log """ echo '✓ Tomcat 部署完成' } catch (Exception e) { echo "⚠️ Tomcat 部署失败: ${e.message}" echo "读取 Tomcat 启动日志..." bat """ @echo off powershell -NoLogo -NoProfile -Command "if (Test-Path '${tomcatHome}\\logs\\mcslms-tomcat.log') { Get-Content '${tomcatHome}\\logs\\mcslms-tomcat.log' -Tail 100 } else { Write-Host '找不到日志文件' }" """ currentBuild.result = 'UNSTABLE' } } } } stage('9. 推送到头歌平台') { parallel { stage('9.1 推送源码到 main') { steps { echo '========== 推送源代码到头歌 main 分支 ==========' script { try { withCredentials([usernamePassword( credentialsId: 'educoder-credentials', usernameVariable: 'EDUCODER_USER', passwordVariable: 'EDUCODER_PASS' )]) { bat ''' @echo off setlocal EnableExtensions EnableDelayedExpansion REM URL编码用户名和密码 for /f %%i in ('powershell -NoLogo -NoProfile -Command "[Console]::Out.Write([uri]::EscapeDataString($env:EDUCODER_USER))"') do set USER_ENC=%%i for /f %%i in ('powershell -NoLogo -NoProfile -Command "[Console]::Out.Write([uri]::EscapeDataString($env:EDUCODER_PASS))"') do set PASS_ENC=%%i git config user.name "Jenkins CI" git config user.email "jenkins@mcslms.local" git remote remove educoder 2>nul || echo Remote educoder does not exist git remote add educoder https://bdgit.educoder.net/pu6zrsfoy/mcslms.git git fetch --unshallow 2>nul || echo Already a complete repository REM 获取当前提交的 SHA for /f %%i in ('git rev-parse HEAD') do set CURRENT_SHA=%%i echo 当前提交 SHA: !CURRENT_SHA! REM 使用 SHA 推送到 main 分支 git push https://%USER_ENC%:%PASS_ENC%@bdgit.educoder.net/pu6zrsfoy/mcslms.git !CURRENT_SHA!:refs/heads/main --force ''' } echo '✓ 源代码推送到 main 成功' } catch (Exception e) { echo "⚠ 推送代码到 main 失败: ${e.message}" currentBuild.result = 'UNSTABLE' } } } } stage('9.2 推送制品到 release') { steps { echo '========== 推送制品到头歌 release 分支 ==========' script { try { withCredentials([usernamePassword( credentialsId: 'educoder-credentials', usernameVariable: 'EDUCODER_USER', passwordVariable: 'EDUCODER_PASS' )]) { bat ''' @echo off setlocal EnableExtensions EnableDelayedExpansion set "VERSION_TAG=v1.0.0.%BUILD_NUMBER%" set "ARTIFACTS_DIR=artifacts\\!VERSION_TAG!" echo ============================================ echo 准备制品目录: !VERSION_TAG! echo ============================================ REM 创建制品目录 if not exist artifacts mkdir artifacts if exist "!ARTIFACTS_DIR!" rmdir /S /Q "!ARTIFACTS_DIR!" mkdir "!ARTIFACTS_DIR!" REM 复制制品并重命名 (使用 fatJar) echo 复制制品... for %%f in (cli\\build\\libs\\mcslms-cli-*-all.jar) do ( copy /Y "%%f" "!ARTIFACTS_DIR!\\mcslms-cli-!VERSION_TAG!.jar" >nul echo ✓ CLI JAR (fatJar) ) for %%f in (gui\\build\\libs\\mcslms-gui-*-all.jar) do ( copy /Y "%%f" "!ARTIFACTS_DIR!\\mcslms-gui-!VERSION_TAG!.jar" >nul echo ✓ GUI JAR (fatJar) ) for %%f in (backend\\build\\libs\\mcslms-backend-*.jar) do ( copy /Y "%%f" "!ARTIFACTS_DIR!\\mcslms-backend-!VERSION_TAG!.jar" >nul echo ✓ Backend JAR ) for %%f in (backend\\build\\libs\\mcslms-web-*.war) do ( copy /Y "%%f" "!ARTIFACTS_DIR!\\mcslms-web-!VERSION_TAG!.war" >nul echo ✓ Web WAR ) REM 查找 APK for %%f in (android\\build\\outputs\\apk\\debug\\*.apk) do ( copy /Y "%%f" "!ARTIFACTS_DIR!\\mcslms-app-!VERSION_TAG!.apk" >nul echo ✓ Android APK ) REM 复制 GUI EXE/ZIP if exist artifacts\\mcslms-gui-!VERSION_TAG!.exe ( copy /Y artifacts\\mcslms-gui-!VERSION_TAG!.exe "!ARTIFACTS_DIR!\\\\" >nul echo ✓ GUI EXE ) if exist artifacts\\mcslms-gui.zip ( copy /Y artifacts\\mcslms-gui.zip "!ARTIFACTS_DIR!\\mcslms-gui-!VERSION_TAG!.zip" >nul echo ✓ GUI ZIP ) REM 生成清单文件 echo MCSLMS Release Manifest - !VERSION_TAG! > "!ARTIFACTS_DIR!\\manifest.txt" echo Build ID: %BUILD_NUMBER% >> "!ARTIFACTS_DIR!\\manifest.txt" echo 发布时间: %date% %time% >> "!ARTIFACTS_DIR!\\manifest.txt" echo. >> "!ARTIFACTS_DIR!\\manifest.txt" echo 制品列表: >> "!ARTIFACTS_DIR!\\manifest.txt" for %%F in (!ARTIFACTS_DIR!\\*) do echo %%~nxF >> "!ARTIFACTS_DIR!\\manifest.txt" echo. echo 制品目录内容: dir /B "!ARTIFACTS_DIR!" REM URL编码凭据 for /f %%i in ('powershell -NoLogo -NoProfile -Command "[Console]::Out.Write([uri]::EscapeDataString($env:EDUCODER_USER))"') do set USER_ENC=%%i for /f %%i in ('powershell -NoLogo -NoProfile -Command "[Console]::Out.Write([uri]::EscapeDataString($env:EDUCODER_PASS))"') do set PASS_ENC=%%i echo. echo ============================================ echo 推送到头歌 release 分支 echo ============================================ git config user.name "Jenkins CI" git config user.email "jenkins@mcslms.local" REM 创建 orphan 分支只包含制品 git checkout --orphan release-%BUILD_NUMBER% git rm -rf . --ignore-unmatch >nul 2>&1 git add artifacts/ git commit -m "release: 发布 !VERSION_TAG! (Build #%BUILD_NUMBER%)" echo 推送到头歌平台... git push https://%USER_ENC%:%PASS_ENC%@bdgit.educoder.net/pu6zrsfoy/mcslms.git HEAD:refs/heads/release --force REM 恢复原分支 git checkout -f develop 2>nul || git checkout -f main 2>nul echo ✓ 制品已推送到头歌 release 分支: !VERSION_TAG! ''' } echo '✓ 头歌平台发布成功' } catch (Exception e) { echo "⚠ 推送到头歌失败: ${e.message}" currentBuild.result = 'UNSTABLE' } } } } } } } post { success { echo '==========================================' echo " ✅ 四端构建成功!版本: ${env.RELEASE_VERSION}" echo ' - CLI: cli/build/libs/mcslms-cli-v1.0.0.X-all.jar' echo ' - GUI: gui/build/libs/mcslms-gui-v1.0.0.X-all.jar' echo ' - GUI EXE: artifacts/mcslms-gui-v1.0.0.X.exe' echo ' - Web WAR: backend/build/libs/mcslms-web-v1.0.0.X.war' echo ' - Backend: backend/build/libs/mcslms-backend-v1.0.0.X.jar' echo ' - Android: android/build/outputs/apk/' echo ' - Tomcat: http://localhost:8080/mcslms/' echo ' - 测试报告: Jenkins -> Core 测试报告' echo ' - SonarQube: http://localhost:9000/dashboard?id=mcslms' echo ' - 头歌源码: https://bdgit.educoder.net/pu6zrsfoy/mcslms/tree/main' echo ' - 头歌制品: https://bdgit.educoder.net/pu6zrsfoy/mcslms/tree/release' echo '==========================================' } failure { echo '❌ 构建失败,请检查日志' } always { script { echo "=== 构建完成: ${env.RELEASE_VERSION} ===" // 清理临时目录 bat 'if exist jpackage-input rmdir /S /Q jpackage-input 2>nul' bat 'if exist jpackage-output rmdir /S /Q jpackage-output 2>nul' // 发送邮件通知 echo '========== 发送邮件通知 ==========' def finalResult = currentBuild.result ?: 'SUCCESS' def emailSubject = "" def emailStatus = "" if (finalResult == 'SUCCESS') { emailSubject = "✅ MCSLMS 构建成功 - ${env.RELEASE_VERSION} (Build #${BUILD_NUMBER})" emailStatus = "成功" } else if (finalResult == 'FAILURE') { emailSubject = "❌ MCSLMS 构建失败 - ${env.RELEASE_VERSION} (Build #${BUILD_NUMBER})" emailStatus = "失败" } else if (finalResult == 'UNSTABLE') { emailSubject = "⚠️ MCSLMS 构建不稳定 - ${env.RELEASE_VERSION} (Build #${BUILD_NUMBER})" emailStatus = "不稳定" } else { emailSubject = "ℹ️ MCSLMS 构建完成 - ${env.RELEASE_VERSION} (Build #${BUILD_NUMBER})" emailStatus = "完成" } echo "准备发送邮件: ${emailSubject}" echo "收件人: 602924803@qq.com" try { mail to: '602924803@qq.com', subject: emailSubject, body: """MCSLMS 项目构建${emailStatus} 构建编号: #${BUILD_NUMBER} Release 版本: ${env.RELEASE_VERSION} 构建状态: ${emailStatus} 构建时间: ${new Date(currentBuild.startTimeInMillis)} Git 提交: ${env.GIT_COMMIT ?: 'N/A'} 制品列表: - mcslms-cli-${env.RELEASE_VERSION}.jar (命令行版本) - mcslms-gui-${env.RELEASE_VERSION}.jar (图形界面版本) - mcslms-gui-${env.RELEASE_VERSION}.exe (Windows安装包) - mcslms-web-${env.RELEASE_VERSION}.war (Web应用版本) - mcslms-backend-${env.RELEASE_VERSION}.jar (后端服务) - mcslms-app-${env.RELEASE_VERSION}.apk (Android版本) 访问地址: - Tomcat: http://localhost:8080/mcslms/ - SonarQube: http://localhost:9000/dashboard?id=mcslms - 头歌源码: https://bdgit.educoder.net/pu6zrsfoy/mcslms/tree/main - 头歌制品: https://bdgit.educoder.net/pu6zrsfoy/mcslms/tree/release 查看构建详情: ${BUILD_URL} """ echo '✓ 邮件发送成功' } catch (Exception e) { echo "⚠️ 邮件发送失败: ${e.message}" } } } } }