add:pda新版本发布在线更新
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package org.nl.wms.system_manage.controller.appversion;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import org.nl.common.base.ResponseData;
|
||||
import org.nl.common.domain.query.PageQuery;
|
||||
import org.nl.wms.system_manage.service.appversion.IAppVersionService;
|
||||
import org.nl.wms.system_manage.service.appversion.dao.AppVersion;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/appVersion")
|
||||
public class AppVersionController {
|
||||
|
||||
@Autowired
|
||||
private IAppVersionService appVersionService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Object> query(@RequestParam(required = false, name = "app_name") String appName, PageQuery page) {
|
||||
return ResponseData.build(appVersionService.pageQuery(appName, page), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PostMapping("/publish")
|
||||
public ResponseEntity<Object> publish(@RequestParam("app_name") String appName,
|
||||
@RequestParam("version_name") String versionName,
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
return ResponseData.build(appVersionService.publish(appName, versionName, file), HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@SaIgnore
|
||||
@GetMapping("/check")
|
||||
public ResponseEntity<Object> check(@RequestParam("app_name") String appName,
|
||||
@RequestParam(required = false, name = "version_name") String versionName) {
|
||||
return ResponseData.build(appVersionService.checkLatest(appName, versionName), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@SaIgnore
|
||||
@GetMapping("/download/{appVersionId}")
|
||||
public void download(@PathVariable String appVersionId, HttpServletResponse response) throws IOException {
|
||||
AppVersion version = appVersionService.getDownloadVersion(appVersionId);
|
||||
File apk = new File(version.getApk_path());
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
response.setContentLengthLong(apk.length());
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" +
|
||||
URLEncoder.encode(version.getApk_name(), StandardCharsets.UTF_8.name()).replace("+", "%20"));
|
||||
try (FileInputStream input = new FileInputStream(apk)) {
|
||||
StreamUtils.copy(input, response.getOutputStream());
|
||||
response.flushBuffer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.nl.wms.system_manage.service.appversion;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.nl.common.domain.query.PageQuery;
|
||||
import org.nl.wms.system_manage.service.appversion.dao.AppVersion;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface IAppVersionService extends IService<AppVersion> {
|
||||
|
||||
IPage<AppVersion> pageQuery(String appName, PageQuery page);
|
||||
|
||||
AppVersion publish(String appName, String versionName, MultipartFile apk);
|
||||
|
||||
Map<String, Object> checkLatest(String appName, String versionName);
|
||||
|
||||
AppVersion getDownloadVersion(String appVersionId);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.nl.wms.system_manage.service.appversion.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@TableName("sys_app_version")
|
||||
public class AppVersion implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId("app_version_id")
|
||||
private String app_version_id;
|
||||
|
||||
private String app_name;
|
||||
|
||||
private String version_name;
|
||||
|
||||
private String apk_name;
|
||||
|
||||
@JsonIgnore
|
||||
private String apk_path;
|
||||
|
||||
private Long apk_size;
|
||||
|
||||
private String create_id;
|
||||
|
||||
private String create_name;
|
||||
|
||||
private String create_time;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.nl.wms.system_manage.service.appversion.dao.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.nl.wms.system_manage.service.appversion.dao.AppVersion;
|
||||
|
||||
@Mapper
|
||||
public interface AppVersionMapper extends BaseMapper<AppVersion> {
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package org.nl.wms.system_manage.service.appversion.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.nl.common.domain.query.PageQuery;
|
||||
import org.nl.common.exception.BadRequestException;
|
||||
import org.nl.common.utils.SecurityUtils;
|
||||
import org.nl.config.FileProperties;
|
||||
import org.nl.wms.system_manage.service.appversion.IAppVersionService;
|
||||
import org.nl.wms.system_manage.service.appversion.dao.AppVersion;
|
||||
import org.nl.wms.system_manage.service.appversion.dao.mapper.AppVersionMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class AppVersionServiceImpl extends ServiceImpl<AppVersionMapper, AppVersion> implements IAppVersionService {
|
||||
|
||||
@Autowired
|
||||
private FileProperties fileProperties;
|
||||
|
||||
@Override
|
||||
public IPage<AppVersion> pageQuery(String appName, PageQuery page) {
|
||||
return page(page.build(AppVersion.class), new LambdaQueryWrapper<AppVersion>()
|
||||
.like(StrUtil.isNotBlank(appName), AppVersion::getApp_name, appName)
|
||||
.orderByDesc(AppVersion::getCreate_time));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AppVersion publish(String appName, String versionName, MultipartFile apk) {
|
||||
if (StrUtil.isBlank(appName) || StrUtil.isBlank(versionName)) {
|
||||
throw new BadRequestException("应用名称和版本号不能为空");
|
||||
}
|
||||
if (apk == null || apk.isEmpty() || !"apk".equalsIgnoreCase(getExtension(apk.getOriginalFilename()))) {
|
||||
throw new BadRequestException("只能上传 APK 文件");
|
||||
}
|
||||
if (apk.getSize() > fileProperties.getMaxSize() * 1024 * 1024) {
|
||||
throw new BadRequestException("APK 文件超过允许大小");
|
||||
}
|
||||
if (count(new LambdaQueryWrapper<AppVersion>()
|
||||
.eq(AppVersion::getApp_name, appName)
|
||||
.eq(AppVersion::getVersion_name, versionName)) > 0) {
|
||||
throw new BadRequestException("该应用版本已发布");
|
||||
}
|
||||
|
||||
String apkName = safeFilePart(appName) + "-" + safeFilePart(versionName) + ".apk";
|
||||
File directory = new File(fileProperties.getPath().getPath(), "apk");
|
||||
File destination = new File(directory, apkName);
|
||||
if (destination.exists()) {
|
||||
throw new BadRequestException("同名 APK 文件已存在");
|
||||
}
|
||||
if (!directory.exists() && !directory.mkdirs()) {
|
||||
throw new BadRequestException("APK 存储目录创建失败");
|
||||
}
|
||||
|
||||
try {
|
||||
apk.transferTo(destination);
|
||||
AppVersion appVersion = new AppVersion();
|
||||
appVersion.setApp_version_id(IdUtil.getSnowflake(1, 1).nextIdStr());
|
||||
appVersion.setApp_name(appName.trim());
|
||||
appVersion.setVersion_name(versionName.trim());
|
||||
appVersion.setApk_name(apkName);
|
||||
appVersion.setApk_path(destination.getCanonicalPath());
|
||||
appVersion.setApk_size(apk.getSize());
|
||||
appVersion.setCreate_id(SecurityUtils.getCurrentUserId());
|
||||
appVersion.setCreate_name(SecurityUtils.getCurrentNickName());
|
||||
appVersion.setCreate_time(DateUtil.now());
|
||||
save(appVersion);
|
||||
return appVersion;
|
||||
} catch (Exception e) {
|
||||
if (destination.exists()) {
|
||||
destination.delete();
|
||||
}
|
||||
if (e instanceof BadRequestException) {
|
||||
throw (BadRequestException) e;
|
||||
}
|
||||
throw new BadRequestException("APK 保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> checkLatest(String appName, String versionName) {
|
||||
if (StrUtil.isBlank(appName)) {
|
||||
throw new BadRequestException("应用名称不能为空");
|
||||
}
|
||||
List<AppVersion> versions = list(new LambdaQueryWrapper<AppVersion>()
|
||||
.eq(AppVersion::getApp_name, appName.trim()));
|
||||
AppVersion latest = null;
|
||||
for (AppVersion version : versions) {
|
||||
if (latest == null || compareVersion(version.getVersion_name(), latest.getVersion_name()) > 0) {
|
||||
latest = version;
|
||||
}
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("has_update", latest != null && compareVersion(latest.getVersion_name(), versionName) > 0);
|
||||
if (latest != null) {
|
||||
Map<String, Object> latestInfo = new HashMap<>();
|
||||
latestInfo.put("app_version_id", latest.getApp_version_id());
|
||||
latestInfo.put("app_name", latest.getApp_name());
|
||||
latestInfo.put("version_name", latest.getVersion_name());
|
||||
latestInfo.put("apk_name", latest.getApk_name());
|
||||
latestInfo.put("apk_size", latest.getApk_size());
|
||||
latestInfo.put("create_time", latest.getCreate_time());
|
||||
result.put("latest", latestInfo);
|
||||
} else {
|
||||
result.put("latest", null);
|
||||
}
|
||||
result.put("download_url", latest == null ? null : "/api/appVersion/download/" + latest.getApp_version_id());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppVersion getDownloadVersion(String appVersionId) {
|
||||
AppVersion version = getById(appVersionId);
|
||||
if (version == null || !new File(version.getApk_path()).isFile()) {
|
||||
throw new BadRequestException("APK 文件不存在");
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
private String getExtension(String name) {
|
||||
if (name == null || name.lastIndexOf('.') < 0) {
|
||||
return "";
|
||||
}
|
||||
return name.substring(name.lastIndexOf('.') + 1);
|
||||
}
|
||||
|
||||
private String safeFilePart(String value) {
|
||||
String safeValue = value.trim().replaceAll("[\\\\/:*?\"<>|\\s]+", "_");
|
||||
if (safeValue.isEmpty() || ".".equals(safeValue) || "..".equals(safeValue)) {
|
||||
throw new BadRequestException("应用名称或版本号不能用于文件名");
|
||||
}
|
||||
return safeValue;
|
||||
}
|
||||
|
||||
private int compareVersion(String first, String second) {
|
||||
String[] firstParts = normalizeVersion(first).split("\\.");
|
||||
String[] secondParts = normalizeVersion(second).split("\\.");
|
||||
int length = Math.max(firstParts.length, secondParts.length);
|
||||
for (int index = 0; index < length; index++) {
|
||||
int firstPart = index < firstParts.length ? parseVersionPart(firstParts[index]) : 0;
|
||||
int secondPart = index < secondParts.length ? parseVersionPart(secondParts[index]) : 0;
|
||||
if (firstPart != secondPart) {
|
||||
return firstPart > secondPart ? 1 : -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private String normalizeVersion(String version) {
|
||||
return version == null ? "0" : version.trim().replaceFirst("^[vV]", "");
|
||||
}
|
||||
|
||||
private int parseVersionPart(String value) {
|
||||
try {
|
||||
return Integer.parseInt(value.replaceAll("\\D.*", ""));
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
180
nladmin-ui/src/views/system/appVersion/index.vue
Normal file
180
nladmin-ui/src/views/system/appVersion/index.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="head-container">
|
||||
<div v-if="crud.props.searchToggle">
|
||||
<el-input
|
||||
v-model="query.app_name"
|
||||
clearable
|
||||
size="mini"
|
||||
placeholder="应用名称"
|
||||
style="width: 200px;"
|
||||
@keyup.enter.native="crud.toQuery"
|
||||
/>
|
||||
<rrOperation />
|
||||
</div>
|
||||
<crudOperation :permission="permission">
|
||||
<el-button
|
||||
slot="left"
|
||||
class="filter-item"
|
||||
size="mini"
|
||||
type="primary"
|
||||
icon="el-icon-upload"
|
||||
@click="dialogVisible = true"
|
||||
>发布版本</el-button>
|
||||
</crudOperation>
|
||||
<el-table v-loading="crud.loading" :data="crud.data" size="mini" style="width: 100%;">
|
||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||
<el-table-column prop="app_name" label="应用名称" min-width="140" />
|
||||
<el-table-column prop="version_name" label="版本号" width="120" />
|
||||
<el-table-column prop="apk_name" label="APK 文件名" min-width="200" />
|
||||
<el-table-column :formatter="formatSize" prop="apk_size" label="文件大小" width="110" align="right" />
|
||||
<el-table-column prop="create_name" label="发布人" width="100" />
|
||||
<el-table-column prop="create_time" label="发布时间" width="160" />
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-link type="primary" :href="downloadUrl(scope.row.app_version_id)">下载</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination />
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
:close-on-click-modal="false"
|
||||
title="发布 APP 版本"
|
||||
:visible.sync="dialogVisible"
|
||||
width="480px"
|
||||
@close="resetForm"
|
||||
>
|
||||
<el-form ref="form" :model="form" :rules="rules" size="small" label-width="90px">
|
||||
<el-form-item label="应用名称" prop="app_name">
|
||||
<el-input v-model="form.app_name" maxlength="100" placeholder="例如:WMS PDA" />
|
||||
</el-form-item>
|
||||
<el-form-item label="版本号" prop="version_name">
|
||||
<el-input v-model="form.version_name" maxlength="50" placeholder="例如:1.2.0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="APK 文件" prop="file">
|
||||
<el-upload
|
||||
ref="upload"
|
||||
:action="publishUrl"
|
||||
:auto-upload="false"
|
||||
:data="form"
|
||||
:headers="headers"
|
||||
:limit="1"
|
||||
accept=".apk,application/vnd.android.package-archive"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
:on-exceed="handleExceed"
|
||||
>
|
||||
<el-button size="small" type="primary">选择 APK</el-button>
|
||||
<div slot="tip" class="el-upload__tip">仅支持 APK,文件名将保存为“应用名称-版本号.apk”。</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button :loading="uploading" type="primary" @click="publish">发布</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import CRUD, { crud, header, presenter } from '@crud/crud'
|
||||
import rrOperation from '@crud/RR.operation'
|
||||
import crudOperation from '@crud/CRUD.operation'
|
||||
import pagination from '@crud/Pagination'
|
||||
|
||||
const defaultForm = { app_name: '', version_name: '' }
|
||||
|
||||
export default {
|
||||
name: 'AppVersion',
|
||||
components: { crudOperation, rrOperation, pagination },
|
||||
cruds() {
|
||||
return CRUD({
|
||||
title: 'APP版本',
|
||||
idField: 'app_version_id',
|
||||
url: 'api/appVersion',
|
||||
optShow: { add: false, edit: false, del: false, reset: true, download: false }
|
||||
})
|
||||
},
|
||||
mixins: [presenter(), header(), crud()],
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
uploading: false,
|
||||
headers: { Authorization: getToken() },
|
||||
form: { ...defaultForm },
|
||||
permission: {},
|
||||
rules: {
|
||||
app_name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
|
||||
version_name: [{ required: true, message: '请输入版本号', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['baseApi']),
|
||||
publishUrl() {
|
||||
return this.baseApi + '/api/appVersion/publish'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
publish() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return
|
||||
if (this.$refs.upload.uploadFiles.length === 0) {
|
||||
this.$message.warning('请选择 APK 文件')
|
||||
return
|
||||
}
|
||||
this.uploading = true
|
||||
this.$refs.upload.submit()
|
||||
})
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const isApk = file.name.toLowerCase().endsWith('.apk')
|
||||
if (!isApk) {
|
||||
this.$message.error('只能上传 APK 文件')
|
||||
this.uploading = false
|
||||
}
|
||||
return isApk
|
||||
},
|
||||
uploadSuccess() {
|
||||
this.$message.success('版本发布成功')
|
||||
this.dialogVisible = false
|
||||
this.crud.toQuery()
|
||||
},
|
||||
uploadError(error) {
|
||||
let message = '版本发布失败'
|
||||
try {
|
||||
message = JSON.parse(error.message).message || message
|
||||
} catch (e) {
|
||||
// Element UI does not always expose a JSON response body.
|
||||
}
|
||||
this.$message.error(message)
|
||||
this.uploading = false
|
||||
},
|
||||
handleExceed() {
|
||||
this.$message.warning('一次只能上传一个 APK 文件')
|
||||
},
|
||||
resetForm() {
|
||||
this.uploading = false
|
||||
this.form = { ...defaultForm }
|
||||
if (this.$refs.form) this.$refs.form.resetFields()
|
||||
if (this.$refs.upload) this.$refs.upload.clearFiles()
|
||||
},
|
||||
formatSize(row) {
|
||||
if (!row.apk_size) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
const index = Math.min(Math.floor(Math.log(row.apk_size) / Math.log(1024)), units.length - 1)
|
||||
const value = row.apk_size / Math.pow(1024, index)
|
||||
return value.toFixed(index === 0 ? 0 : 2) + ' ' + units[index]
|
||||
},
|
||||
downloadUrl(id) {
|
||||
return this.baseApi + '/api/appVersion/download/' + id
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
14
sql/sys_app_version.sql
Normal file
14
sql/sys_app_version.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE IF NOT EXISTS `sys_app_version` (
|
||||
`app_version_id` VARCHAR(32) NOT NULL COMMENT '版本记录标识',
|
||||
`app_name` VARCHAR(100) NOT NULL COMMENT '应用名称',
|
||||
`version_name` VARCHAR(50) NOT NULL COMMENT '版本号',
|
||||
`apk_name` VARCHAR(255) NOT NULL COMMENT 'APK 存储文件名,格式:应用名称-版本号.apk',
|
||||
`apk_path` VARCHAR(500) NOT NULL COMMENT 'APK 绝对存储路径',
|
||||
`apk_size` BIGINT NOT NULL COMMENT 'APK 文件字节数',
|
||||
`create_id` VARCHAR(32) DEFAULT NULL COMMENT '发布人标识',
|
||||
`create_name` VARCHAR(100) DEFAULT NULL COMMENT '发布人名称',
|
||||
`create_time` VARCHAR(25) NOT NULL COMMENT '发布时间',
|
||||
PRIMARY KEY (`app_version_id`),
|
||||
UNIQUE KEY `uk_sys_app_version_name_version` (`app_name`, `version_name`),
|
||||
KEY `idx_sys_app_version_name` (`app_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='APP 版本发布记录';
|
||||
Reference in New Issue
Block a user