APP版本管理全链路(后端设计)

发布时间:2026/7/30 4:05:27

APP版本管理全链路(后端设计) APP版本管理全链路 — 从零到一实现若依框架下的APP更新系统一、背景与需求在若依RuoYi框架下为APP实现版本管理功能包括APP端自动检查更新提示用户下载新版本Web管理端超管在后台管理版本信息、上传APK二、方案选型数据库配置 vs 硬编码方案优点缺点硬编码简单每次更新需改代码、重启服务数据库配置✅动态管理无需重启需要建表、写CRUD选择数据库方案建表适配 PostgreSQLSQLCREATE TABLE sys_app_version ( id BIGSERIAL PRIMARY KEY, version_code INT NOT NULL, version_name VARCHAR(50) NOT NULL, apk_url VARCHAR(500), wgt_url VARCHAR(500), update_type VARCHAR(20) DEFAULT suggest, update_log TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );三、踩坑记录坑1静态资源访问 401上传APK到static/app/后浏览器访问返回401。原因Spring Security 拦截了/static/**路径。修复在SecurityConfig中放行Java.antMatchers(/static/**).permitAll()坑2静态资源访问 404放行后仍404。原因Spring Boot 默认不映射/static/**到classpath:/static/。修复在ResourcesConfig中显式配置Javaregistry.addResourceHandler(/static/**) .addResourceLocations(classpath:/static/);坑3返回的 apkUrl 中IP/端口不对上传后返回的URL里IP是客户端IP而非服务器IP。原因误用了request.getRemoteHost()/getRemotePort()。修复改用request.getServerName()/getServerPort()。坑4补录旧版本导致返回错误版本/api/app/version用ORDER BY id DESC取最新版本补录旧版本时id更大导致返回旧版本。修复改为ORDER BY version_code DESC。XMLselect idselectLatestVersion resultMapSysAppVersionResult include refidselectAppVersionVo/ order by version_code desc limit 1 /select坑5apk-parser 依赖 NoClassDefFoundErrorapk-parser加在ruoyi-common模块但运行时找不到类报NoClassDefFoundError: net/dongliu/apk/parser/ApkFile。原因依赖只在ruoyi-common声明但运行时打包在ruoyi-admin模块依赖未传递进最终JAR。修复在ruoyi-admin/pom.xml也显式声明依赖确保打包进最终JARXML!-- APK文件解析 -- dependency groupIdnet.dongliu/groupId artifactIdapk-parser/artifactId version2.6.10/version /dependency四、方案二服务端自动解析APK真实versionCode问题前端手动填写versionCode容易出错比如填101实际却是102。解决引入net.dongliu:apk-parser:2.6.10上传APK时自动解析真实版本信息。① 添加Maven依赖ruoyi-admin/pom.xmlXML!-- APK文件解析 -- dependency groupIdnet.dongliu/groupId artifactIdapk-parser/artifactId version2.6.10/version /dependency如果工具类放在ruoyi-common模块ruoyi-common/pom.xml也需要一份确保编译通过。② 工具类ApkInfoUtil.javaJavapackage com.ruoyi.common.utils; import net.dongliu.apk.parser.ApkFile; import net.dongliu.apk.parser.bean.ApkMeta; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; public class ApkInfoUtil { private static final Logger log LoggerFactory.getLogger(ApkInfoUtil.class); public static ApkVersionInfo extractVersion(String apkFilePath) throws IOException { try (ApkFile apkFile new ApkFile(new File(apkFilePath))) { ApkMeta meta apkFile.getApkMeta(); ApkVersionInfo info new ApkVersionInfo(); info.setVersionCode(meta.getVersionCode().intValue()); info.setVersionName(meta.getVersionName()); return info; } catch (Exception e) { log.error(APK文件解析失败: {}, apkFilePath, e); throw new IOException(APK文件解析失败: e.getMessage(), e); } } public static class ApkVersionInfo { private int versionCode; private String versionName; // getter / setter 省略... } }③ 上传接口保存文件后立即解析返回真实版本信息JSON{ code: 200, msg: 上传成功, apkUrl: /static/app/2026/07/29/patrolApp-1.0.2.apk, versionCode: 102, versionName: 1.0.2 }④ 新增/编辑接口兜底覆盖——即使前端传错也以APK解析值为准Javaprivate void fillVersionFromApk(SysAppVersion appVersion) { if (StringUtils.isEmpty(appVersion.getApkUrl())) return; String absolutePath System.getProperty(user.dir) /ruoyi-admin/src/main/resources appVersion.getApkUrl(); ApkInfoUtil.ApkVersionInfo info ApkInfoUtil.extractVersion(absolutePath); appVersion.setVersionCode(info.getVersionCode()); appVersion.setVersionName(info.getVersionName()); }流程图Plain Text前端上传 APK │ ▼ POST /upload │ ① 保存至 static/app/ │ ② apk-parser 解析真实 versionCode / versionName │ ③ 返回 apkUrl versionCode versionName ▼ 前端拿到 versionCode填入表单只读展示 │ ▼ POST /appVersion新增/ PUT /appVersion编辑 │ fillVersionFromApk() 再次解析 APK 兜底 │ 确保数据库存的永远 APK 真实值 ▼ 数据库: versionCode 102 ✓五、前端防御方案同版本不反复弹窗问题用户点了暂不更新后每次打开APP都弹窗。方案localStorage记录已提示过的 versionCode同版本跳过。JavaScript_shouldPrompt(remote, isManual) { const key APP_LAST_PROMPTED_VERSION_CODE; const lastPrompted uni.getStorageSync(key); const localVC plus.runtime.versionCode; if (remote.versionCode localVC) return false; // 无需更新 if (remote.versionCode lastPrompted) { if (isManual) uni.showToast({ title: 当前已是最新版本 }); return false; // 已提示过跳过 } uni.setStorageSync(key, remote.versionCode); // 记录本次 return true; }效果时间线本地VC服务端VClastPrompted结果T1 自动检查100101无弹窗 ✅T2 用户安装实际VC100100101101—T3 下次自动检查100101101101101→ 跳过 ✅T4 真正新版本100102101102≠101→ 弹窗 ✅六、最终架构Plain Text┌──────────────────────────────────────────────────────┐ │ Web管理端超管 │ │ │ │ 上传APK → apk-parser解析versionCode/versionName │ │ → 写入数据库 sys_app_version │ │ 列表/新增/编辑/删除 → CRUD │ └──────────────────────┬─────────────────────────────────┘ │ sys_app_version 表 │ ┌──────────────────────┴─────────────────────────────────┐ │ APP端 │ │ │ │ GET /api/app/version → ORDER BY version_code DESC │ │ → 对比本地 versionCode │ │ → localStorage 防重复弹窗 │ │ → 下载 APK 覆盖安装 │ └──────────────────────────────────────────────────────┘七、关键设计决策APK存static/app/而非profile/upload/利用静态资源映射直接提供下载无需额外接口versionCode 以服务端解析为准不信任前端传值保证数据一致性ORDER BY version_code DESC而非id DESC防止补录旧版本干扰前端 localStorage 防重复弹窗同一版本只在首次提示用户体验友好apk-parser 依赖需在ruoyi-admin模块显式声明否则运行时打包不进去报NoClassDefFoundError

相关新闻