从0到1构建MES系统13-App版本更新

89 阅读4分钟

今天我们来讲一下移动端版本自动更新。

1. 概述

本文介绍如何构建一个使用uniapp作为移动端自动更新的功能,web端管理App版本和安装包上传下载,移动端打开应用会自动获取最新版本判断是否需要升级,如果版本低于最新版本则自动下载安装。

2. 技术栈说明

  • 移动端:UniApp

  • 后端:Spring Boot + MyBatis Plus

  • Web管理端:React + Ant Design

  • 存储:MinIO、本地存储

  • 数据库:MySQL

3. 数据库设计

create table app_version  
(  
    id          bigint       not null  
        primary key,  
    version     varchar(128) null comment '版本号',  
    file_name   varchar(128) null comment '文件名',  
    file_path   varchar(255) null comment '文件路径',  
    update_log  text         null comment '更新说明',  
    create_by   bigint       null,  
    create_time datetime     null,  
    update_by   bigint       null,  
    update_time datetime     null  
)  
    comment 'app版本管理';

如有需要还可以添加字段

platform VARCHAR(10) NOT NULL COMMENT '平台 android/ios',
version_code INT NOT NULL COMMENT '内部版本号',
version_name VARCHAR(20) NOT NULL COMMENT '显示版本号',
is_force TINYINT(1) DEFAULT 0 COMMENT '是否强制更新',
file_size BIGINT COMMENT '文件大小(字节)',
md5 VARCHAR(32) COMMENT '文件MD5',
publish_time DATETIME DEFAULT CURRENT_TIMESTAMP,
status TINYINT(1) DEFAULT 1 COMMENT '1启用 0禁用'

4. 后端接口实现

/**
 * app版本管理Controller
 *
 * @author fwj
 * @since 2025-07-28
 */
@Tag(name = "app版本管理控制器", description = "app版本管理控制器")
@RestController
@RequestMapping("/tool/app/version")
public class AppVersionController {
    @Autowired
    private AppVersionService appVersionService;

    @Autowired
    private FileServiceContext fileServiceContext;

    @Operation(summary = "查询版本管理列表", description = "查询版本管理列表")
    @PostMapping("/pageList")
    public Result<IPage<AppVersion>> pageList(@Parameter(description = "查询参数") @RequestBody AppVersionParam appVersionParam) {
        Page<AppVersion> page = new Page<>(appVersionParam.getPage(), appVersionParam.getPageSize());
        AppVersion appVersion = new AppVersion();
        BeanUtils.copyProperties(appVersionParam, appVersion);
        QueryWrapper<AppVersion> wrapper = QueryWrapperUtil.build(appVersion);
        IPage<AppVersion> list = AppVersionService.page(page, wrapper);
        return Result.success(list);
    }


    @Operation(summary = "获取版本管理详细信息", description = "获取版本管理详细信息")
    @GetMapping(value = "/{id}")
    public Result getInfo(@Parameter(description = "版本管理ID") @PathVariable("id") Long id) {
        return Result.success(appVersionService.getById(id));
    }
    @Operation(summary = "新增版本管理", description = "新增版本管理")
    @PostMapping
    public Result<?> save(@Parameter(description = "版本管理") @RequestBody AppVersion appVersion) {
        appVersionService.saveVersion(appVersion);
        return Result.success();
    }

    @Operation(summary = "修改版本管理", description = "修改版本管理")
    @PutMapping
    public Result<?> update(@Parameter(description = "版本管理") @RequestBody AppVersion appVersion) {
        appVersionService.updateVersion(appVersion);
        return Result.success();
    }

    @Operation(summary = "删除版本管理", description = "删除版本管理")
    @DeleteMapping
    public Result<?> remove(@Parameter(description = "删除ID") @RequestParam("ids") Long[] ids) {
        appVersionService.deleteApp(Arrays.asList(ids));
        return Result.success();
    }

    /**
     * 通用上传请求(单个)
     */
    @PostMapping("/upload")
    public Result<String> uploadFile(@RequestParam("file") MultipartFile file) throws Exception
    {
        // 上传并返回新文件名称
        String filePath = fileServiceContext.uploadFile(file, "app");

        return Result.success("上传成功!",filePath);
    }

    @Operation(summary = "删除附件", description = "删除附件")
    @DeleteMapping("/file")
    public Result remove(@Parameter(description = "删除ID") @RequestParam("filePath") String filePath)
    {
        fileServiceContext.deleteFile(filePath);
        return Result.success();
    }

    @Operation(summary = "下载安装包", description = "下载安装包")
    @GetMapping("/download/{id}")
    public void download(@PathVariable("id") Long id, HttpServletResponse response) throws UnsupportedEncodingException {
        AppVersion appVersion = appVersionService.getById(id);
        byte[] file = fileServiceContext.downloadFile(appVersion.getFilePath());
        String fileName = java.net.URLEncoder.encode(appVersion.getFileName(), "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename="+fileName);
        HttpUtils.writeData(response, file, "application/octet-stream");
    }

    @Operation(summary = "下载安装包", description = "下载安装包")
    @GetMapping("/download/version/{version}")
    public void downloadByVersion(@PathVariable("version") String version, HttpServletResponse response) throws UnsupportedEncodingException {
        AppVersion appVersion = appVersionService.getByVersion(version);
        byte[] file = fileServiceContext.downloadFile(appVersion.getFilePath());
        String fileName = java.net.URLEncoder.encode(appVersion.getFileName(), "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename="+fileName);
        HttpUtils.writeData(response, file, "application/octet-stream");
    }

    @GetMapping("/lastVersion")
    public Result lastVersion() {
        String version = appVersionService.lastVersion();
        return Result.success("更新版本", version);
    }
}

5. WEB端代码

附件上传代码片段

<Form.Item name="filePath" label="文件">
	<Upload
	name="file"
	multiple={false}
	action="/api/tool/sop/version/upload"
	beforeUpload={beforeUpload}
	onChange={handleFileListChange}
	fileList={fileList}
	maxCount={1}>			
	
	<Button icon={<UploadOutlined />}>上传</Button>
	</Upload>
</Form.Item>

上传校验,文件列表变化方法

// 上传前检查

const beforeUpload = (file: File) => {
	const isApk = file.name.toLowerCase().endsWith(".apk");
	if (!isApk) {
		toast.error("请上传apk文件");
		return false;
	}
return true;
};

// 处理文件列表变化
const handleFileListChange: UploadProps["onChange"] = (info) => {
	const newFileList = info.fileList.map((file) => ({
		...file,
		// 尽量显式拷贝每个字段
		uid: file.uid,
		name: file.name,
		status: file.status,
		url: file.url,
		response: file.response,
	}));
	
	setFileList(newFileList);
	
	const { status } = info.file;
	
	if (status === "done") {
		const path = info.file.response?.data;
		toast.success(`${info.file.name} 上传成功!`);
		form.setFieldsValue({
			fileName: info.file.name,
			filePath: path,
		});
	} else if (status === "error") {
		toast.error(`${info.file.name} 上传失败!`);
	} else if (status === "removed") {
		const path = info.file.url;
	if (!path) {
		toast.error("文件路径不存在,无法删除!");
		return;
	}

	// 调用接口删除文件
	appVersionService.deleteFile(path);
		form.setFieldsValue({
			fileName: "",
			filePath: "",
		});
		toast.success(`${info.file.name} 删除成功!`);
	}

};

Pasted image 20250805174015.png

Pasted image 20250805174039.png

6. 移动端代码

版本校验检测工具

// utils/versionUtils.js

export async function checkForUpdates(getLastVersion, config, alertPush) {
	console.info("检测当前应用版本版本");
  if (typeof plus !== 'undefined') {
    try {
      // 获取当前应用信息
      const appInfo = await new Promise((resolve) => {
        plus.runtime.getProperty(plus.runtime.appid, resolve);
      });

      const appVersion = appInfo.version;

      // 获取最新版本信息
      const res = await getLastVersion({
        version: appVersion,
      });
      
      if (res.code === 200 && res.data) {
        console.log("当前版本:", res);
        const lastVersion = res.data;

        // 检查是否有新版本
        if (lastVersion !== appVersion) {
          const downPath = `${config.baseUrl}/tool/sop/version/download/version/${lastVersion}`;
          const platform = uni.getSystemInfoSync().platform; // 手机平台

          // 弹出版本更新提示框
          uni.showModal({
            title: '版本升级',
            content: '检测到新版本,请更新!',
            showCancel: false,
            success: function (response) {
              if (response.confirm) {
                if (platform === 'android') {
                  plus.runtime.openURL(downPath); // 打开下载链接
                }
              }
            }
          });
        }
      }
    } catch (error) {
      console.error("获取版本信息失败:", error);
    }
  } else {
    // 如果当前环境不支持版本检测,弹出提示
    alertPush({
      type: 'info',
      message: `当前环境不支持获取版本信息`,
      time: Date.now(),
    });
  }
}

应用启动调用版本检测

	onMounted(() => {
		...
		// 定义检查版本的函数
		checkForUpdates(getLastVersion, config, alertPush);
		...
	}

	//getLastVersion: 调用后端接口获取最新版本
	//config: 记录后端调用IP和端口
	//alertPush: 消息提示方法

安装兼容性处理

// AndroidManifest.xml 添加权限
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />

7. 优化策略

  1. 安全校验机制
    • 安装包MD5校验
    // 下载完成后校验MD5
    const fileMd5 = await calculateFileMD5(res.tempFilePath);
    if (fileMd5 !== version.md5) {
      throw new Error('文件校验失败');
    }
  1. 增量更新支持
    • 使用bsdiff算法生成差分包
    • 移动端合并增量包实现小体积更新

本文源码已上传Gitee 开源项目地址

欢迎在评论区分享你的技术选型经验,或对本文方案的改进建议!

关注公众号「慧工云创」

扫码_搜索联合传播样式-标准色版.png