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

APP版本管理全链路 --- 从零到一实现若依框架下的APP更新系统


一、背景与需求

在若依(RuoYi)框架下,为APP实现版本管理功能,包括:

  • APP端:自动检查更新,提示用户下载新版本
  • Web管理端:超管在后台管理版本信息、上传APK

二、方案选型:数据库配置 vs 硬编码

方案 优点 缺点
硬编码 简单 每次更新需改代码、重启服务
数据库配置 动态管理,无需重启 需要建表、写CRUD

选择数据库方案,建表适配 PostgreSQL:

SQL

sql 复制代码
CREATE 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

scss 复制代码
.antMatchers("/static/**").permitAll()

坑2:静态资源访问 404

放行后仍404。

原因 :Spring Boot 默认不映射 /static/**classpath:/static/

修复 :在 ResourcesConfig 中显式配置:

Java

arduino 复制代码
registry.addResourceHandler("/static/**")
        .addResourceLocations("classpath:/static/");

坑3:返回的 apkUrl 中IP/端口不对

上传后返回的URL里IP是客户端IP而非服务器IP。

原因 :误用了 request.getRemoteHost() / getRemotePort()

修复 :改用 request.getServerName() / getServerPort()

坑4:补录旧版本导致返回错误版本

/api/app/versionORDER BY id DESC 取最新版本,补录旧版本时id更大,导致返回旧版本。

修复 :改为 ORDER BY version_code DESC

XML

ini 复制代码
<select id="selectLatestVersion" resultMap="SysAppVersionResult">
    <include refid="selectAppVersionVo"/>
    order by version_code desc limit 1
</select>

坑5:apk-parser 依赖 NoClassDefFoundError

apk-parser 加在 ruoyi-common 模块,但运行时找不到类,报 NoClassDefFoundError: net/dongliu/apk/parser/ApkFile

原因 :依赖只在 ruoyi-common 声明,但运行时打包在 ruoyi-admin 模块,依赖未传递进最终JAR。

修复 :在 ruoyi-admin/pom.xml 也显式声明依赖,确保打包进最终JAR:

XML

xml 复制代码
<!-- APK文件解析 -->
<dependency>
    <groupId>net.dongliu</groupId>
    <artifactId>apk-parser</artifactId>
    <version>2.6.10</version>
</dependency>

四、方案二:服务端自动解析APK真实versionCode

问题

前端手动填写 versionCode 容易出错,比如填101实际却是102。

解决

引入 net.dongliu:apk-parser:2.6.10,上传APK时自动解析真实版本信息。

① 添加Maven依赖ruoyi-admin/pom.xml):

XML

xml 复制代码
<!-- APK文件解析 -->
<dependency>
    <groupId>net.dongliu</groupId>
    <artifactId>apk-parser</artifactId>
    <version>2.6.10</version>
</dependency>

如果工具类放在 ruoyi-common 模块,ruoyi-common/pom.xml 也需要一份,确保编译通过。

② 工具类 ApkInfoUtil.java

Java

java 复制代码
package 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

css 复制代码
{
  "code": 200,
  "msg": "上传成功",
  "apkUrl": "/static/app/2026/07/29/patrolApp-1.0.2.apk",
  "versionCode": 102,
  "versionName": "1.0.2"
}

④ 新增/编辑接口 兜底覆盖------即使前端传错,也以APK解析值为准:

Java

ini 复制代码
private 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

bash 复制代码
前端上传 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

ini 复制代码
_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 服务端VC lastPrompted 结果
T1 自动检查 100 101 弹窗 ✅
T2 用户安装(实际VC=100) 100 101 101 ---
T3 下次自动检查 100 101 101 101==101 → 跳过 ✅
T4 真正新版本 100 102 101 102≠101 → 弹窗 ✅

六、最终架构

Plain Text

sql 复制代码
┌──────────────────────────────────────────────────────┐
│  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 覆盖安装                                    │
└──────────────────────────────────────────────────────┘

七、关键设计决策

  1. APK存 static/app/ 而非 profile/upload/,利用静态资源映射直接提供下载,无需额外接口
  2. versionCode 以服务端解析为准,不信任前端传值,保证数据一致性
  3. ORDER BY version_code DESC 而非 id DESC,防止补录旧版本干扰
  4. 前端 localStorage 防重复弹窗,同一版本只在首次提示,用户体验友好
  5. apk-parser 依赖需在 ruoyi-admin 模块显式声明 ,否则运行时打包不进去,报 NoClassDefFoundError
相关推荐
suconnect1 小时前
Spring Boot接入企业RAG:文档切分、向量检索、权限过滤和答案溯源
java·spring boot·后端
神奇小汤圆1 小时前
别再用 nohup java -jar 了:Spring Boot 生产环境该怎么守护?
后端
JavaGuide1 小时前
GitHub 9.8 万 Star!把整个代码仓库变成知识图谱,这个 AI Coding 工具太适合 Claude Code / Codex 了
前端·后端·ai编程
旺仔学长 哈哈2 小时前
Spring Boot 智能停车场管理系统---附源码+数据库文档
数据库·spring boot·后端·智能停车场
用户8356290780512 小时前
使用 Python 在 PDF 中绘制线条、矩形和自定义图形
后端·python
进击的丸子2 小时前
虹软人脸服务器SDK-C++语言Demo实操指南
后端·算法
程序员鱼皮3 小时前
Claude Opus 5 全新发布,7 大项目实测,夯还是拉?半价吊打 Fable 5?
前端·后端·ai编程
Conan在掘金3 小时前
ArkTS 进阶之道(11):build() 重渲边界——状态变为啥只刷依赖组件
后端
xiaobaoyu3 小时前
mac+idea快捷键
后端