今天我们来讲一下移动端版本自动更新。
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} 删除成功!`);
}
};
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. 优化策略
- 安全校验机制
- 安装包MD5校验
// 下载完成后校验MD5
const fileMd5 = await calculateFileMD5(res.tempFilePath);
if (fileMd5 !== version.md5) {
throw new Error('文件校验失败');
}
- 增量更新支持
- 使用bsdiff算法生成差分包
- 移动端合并增量包实现小体积更新
本文源码已上传Gitee 开源项目地址:
欢迎在评论区分享你的技术选型经验,或对本文方案的改进建议!
关注公众号「慧工云创」