背景:由于项目需要,需要将apk包加入服务端返回的静态资源文件到apk中,形成离线apk包供下载安装。经过调查研究,决定使用apktool实现。关于apktool的资料可以参考
https://blog.csdn.net/quantum7/article/details/124060620
https://blog.csdn.net/qq_20451879/article/details/117300056
1.JDK环境
2.下载apktool.jar
打包流程:
apktool下载地址:https://ibotpeaches.github.io/Apktool/
3.解压apk包
java -jar apktool_2.6.1.jar d app-release.apk
4删除签名文件
签名文件在解压文件后的\original\META-INF目录下
C:\Users***\Downloads\app-release1111\original\META-INF
5.添加要替换的文件到
C:\Users***\Downloads\app-release\assets\assets下
6.生成签名文件
.keystore 签名方式:
keytool -genkey -alias test.keystore -keyalg RSA -validity 20000 -keystore test.keystore
.jks方式:
keytool -genkey -v -keystore test.jks -alias test-keyalg RSA -keysize 2048 -validity 20000
keytool -importkeystore -srckeystore test.jks -destkeystore test.jks -deststoretype pkcs12
7.重新打包
java -jar apktool_2.6.1.jar b app-release
8.使用重新打包后的apk和签名文件打包
.keystore重新签名打包方式:
jarsigner -verbose -keystore test.keystore -signedjar app-release-1-0224.apk app-release-1.apk test.keystore
.jks重新签名打包方式:
jarsigner -verbose -keystore test.jks -signedjar 222.apk test.apk test
可参考下面网站
https://ibotpeaches.github.io/Apktool/install/
构建脚本
bulidApk.bat
@echo off
start cmd /k "cd C:\Users\aipingh\Downloads && java -jar C:\Users\aipingh\Downloads\apktool.jar b C:\Users\***\Downloads\app-release8"
rebuildKeystoreApk.bat
@echo off
start cmd /k "cd C:\Users\aipingh\Downloads && jarsigner -verbose -keystore tinnove.keystore -storepass 123456 -signedjar C:\Users\***\Downloads\app-release8\dist\app-release.apk C:\Users\aipingh\Downloads\app-release8\dist\app-release8.apk test.keystore"
代码:
import ch.qos.logback.core.util.FileUtil;
import cn.hutool.core.io.FileUtil;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ApkUtil {
private String outPth;
// windows版下载
public void downloadWindowsOfflineApk(InfoReqVO reqVO, HttpServletResponse response) {
try {
// apk解压包路径
String apkOriginalPath = "C:\\Users\\***\\Downloads\\app-release8\\";
// 下载离线js文件到目标apk的资源文件路径中
String fullPath = apkOriginalPath + "original\\META-INF\\";
downloadJsFile(reqVO);
//删除签名文件
File mkdir = FileUtil.mkdir(fullPath);
//去掉签名
FileUtils.deleteTempFiles(mkdir, fullPath);
//重新打包
try {
String commandStr = "cmd /c C:\\Users\\***\\Downloads\\buildApk.bat";
Runtime.getRuntime().exec(commandStr);
} catch (IOException e) {
}
// 下载签名文件到dist目录中
String packagePath = "dist";
File packagePathFile = FileUtil.mkdir(apkOriginalPath + packagePath);
String keystorePath = "https://***/apk/keystore/tinnove.keystore";
ImageInfo appDesignDetailImageInfo = new ImageInfo();
appDesignDetailImageInfo.setFilename("tinnove.keystore");
appDesignDetailImageInfo.setPathUrl(keystorePath);
downloadFile(packagePathFile, appDesignDetailImageInfo);
// 加签名后打包APK
try {
String commandStr = "cmd /c C:\\Users\\***\\Downloads\\rebuildKeystoreApk.bat";
Runtime.getRuntime().exec(commandStr).waitFor(30, TimeUnit.MILLISECONDS);
} catch (IOException e) {
} catch (InterruptedException e) {
e.printStackTrace();
}
// 重新构建后的apk文件地址
String newApkName = "app-release.apk";
String newApkPath = apkOriginalPath + "dist\\" + newApkName;
// 将apk包返回给前端
File file = new File(newApkPath);
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("app-release.apk", "UTF-8"));
//获取文件的输入流
InputStream fis = new FileInputStream(file);
byte[] buffer = new byte[1024 * 5];
int r;
while ((r = fis.read(buffer)) != -1) {
response.getOutputStream().write(buffer, 0, r);
}
// 删除build目录,方便下次打包
FileUtils.deleteTempFiles(null, apkOriginalPath + "build");
// 删除dist及目录下的apk包
FileUtils.deleteTempFiles(null, apkOriginalPath + "dist");
} catch (IOException e) {
}
}
//liunx版下载
public void apkShellDownload(InfoReqVO reqVO, HttpServletResponse response) {
InputStream fis = null;
try {
String apkOriginalPath = apkPath;
String apkDistPath = apkPath + "dist/";
String newApkName = "app-release-" + reqVO.getDesignId() + "-" + reqVO.getCount() + ".apk";
FileUtils.execSh("java -jar " + designOfflinePath + "apktool.jar d " + designOfflinePath + "app-release.apk");
// 下载离线js文件到目标apk的资源文件路径中
String fullPath = apkOriginalPath + "original/META-INF/";
downloadJsFile(reqVO);
//删除签名文件 去掉签名
FileUtil.clean(fullPath);
FileUtil.del(fullPath);
FileUtils.execSh(" java -jar " + designOfflinePath + "apktool.jar b " + apkOriginalPath
+ " && " + "cp " + designOfflinePath + "test.keystore " + apkDistPath + " && "
+ "cp " + designOfflinePath + "test.keystore " + apkDistPath + " && "
+ "cd app-release/dist/" + " && "
+ "jarsigner -verbose -keystore test.keystore -storepass *****-signedjar " + newApkName + " app-release.apk tinnove.keystore", 10, TimeUnit.SECONDS);
// 将签名文件移动到到dist目录中
log.info("将签名文件移动到到dist目录中===");
try {
log.info("thread start sleep={} ==== ", System.currentTimeMillis());
// 抛出异常后,同时清除中断信号
Thread.sleep(15 * 1000);
// 将apk包返回给前端
File parentFile = new File(apkDistPath);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
File file = FileUtil.file(parentFile, newApkName);
log.info("apk file ={}", file.exists());
if (file.exists()) {
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + newApkName);
//获取文件的输入流
fis = new FileInputStream(file);
byte[] buffer = new byte[1024 * 5];
int r;
while ((r = fis.read(buffer)) != -1) {
response.getOutputStream().write(buffer, 0, r);
}
}
// 删除临时目录
FileUtil.clean(apkOriginalPath);
} catch (InterruptedException e) {
log.error("重新设置线程中断状态失败,原因是e={}", e.getMessage());
//重新设置线程中断状态为true
Thread.currentThread().interrupt();
}
} catch (IOException e) {
log.error("执行打包命令失败e={}", e.getMessage());
} finally {
if (null != fis) {
try {
fis.close();
} catch (IOException e) {
log.error("fis close 失败e={}", e.getMessage());
}
}
}
}
private void downloadFile(File target, ImageInfo detailImageInfo) throws IOException {
File file = org.apache.commons.io.FileUtils.getFile(target, detailImageInfo.getFilename());
FileOutputStream outputStream = new FileOutputStream(file);
//获取文件的网络输入流
byte[] bytes = cn.hutool.http.HttpUtil.downloadBytes(detailImageInfo.getPathUrl());
InputStream fis = new ByteArrayInputStream(bytes);
byte[] buffer = new byte[1024 * 5];
int r;
while ((r = fis.read(buffer)) != -1) {
outputStream.write(buffer, 0, r);
}
fis.close();
outputStream.close();
}
private void downloadJsFile(InfoReqVO reqVO) {
try {
//apk包所在的服务器路径
String fullPath = outPth + "/app-release/" + "/assets/app-data/";
//本地路径
// String fullPath = "C:\\Users\\xxx\\Downloads\\app-release\\assets\\app-data";
// String fullPath = designOfflinePath + "/" + reqVO.getDesignId() + "/" + reqVO.getCount() + "/";
File target = cn.hutool.core.io.FileUtil.mkdir(new File(fullPath));
// File target = new File(fullPath + "preview");
//
// // 返回图片文件夹
List<ImageInfo> designDetailImages = new ArrayList<>();
downloadFiles(designDetailImages, target);
// 返回逻辑连线 json文件
List<Object> designLogicWiring = new ArrayList<>();
String logicWiringListJs = "let logicWiringList = " + cn.hutool.json.JSONUtil.toJsonStr(designLogicWiring);
FileUtils.object2JsonFile(fullPath + "logicWiring.js", logicWiringListJs);
// 返回图片url json文件
// List designDetailImageUrls = getImagesInfo(reqVO);
// String previewJs = "let previewImageUrls = " + JSONUtil.toJsonStr(designDetailImageUrls);
// FileUtils.object2JsonFile(fullPath + "preview.js", previewJs);
// 返回分组 json文件
List<Object> groupList = new ArrayList<>();
String groupListJs = "let groupList = " + cn.hutool.json.JSONUtil.toJsonStr(groupList);
FileUtils.object2JsonFile(fullPath + "group.js", groupListJs);
} catch (Exception e) {
}
}
private void downloadFiles(List<ImageInfo> designDetailImages, File target) {
try {
//将输出流转换成Zip输出流
for (ImageInfo detailImageInfo : designDetailImages) {
downloadFile(target, detailImageInfo);
}
} catch (IOException e) {
}
}
}
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;
/**
* 文件处理工具类
*
* @author
*/
@Slf4j
public class FileUtils {
/**
* 字符常量:斜杠 {@code '/'}
*/
public static final char SLASH = '/';
/**
* 字符常量:反斜杠 {@code '\\'}
*/
public static final char BACKSLASH = '\\';
public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+";
/**
* 输出指定文件的byte数组
*
* @param filePath 文件路径
* @param os 输出流
* @return
*/
public static void writeBytes(String filePath, OutputStream os) throws IOException {
FileInputStream fis = null;
try {
File file = new File(filePath);
if (!file.exists()) {
throw new FileNotFoundException(filePath);
}
fis = new FileInputStream(file);
byte[] b = new byte[1024];
int length;
while ((length = fis.read(b)) > 0) {
os.write(b, 0, length);
}
} catch (IOException e) {
throw e;
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
/**
* 删除文件
*
* @param filePath 文件
* @return
*/
public static boolean deleteFile(String filePath) {
boolean flag = false;
File file = new File(filePath);
// 路径为文件且不为空则进行删除
if (file.isFile() && file.exists()) {
file.delete();
flag = true;
}
return flag;
}
/**
* 文件名称验证
*
* @param filename 文件名称
* @return true 正常 false 非法
*/
public static boolean isValidFilename(String filename) {
return filename.matches(FILENAME_PATTERN);
}
/**
* 检查文件是否可下载
*
* @param resource 需要下载的文件
* @return true 正常 false 非法
*/
public static boolean checkAllowDownload(String resource) {
// 禁止目录上跳级别
if (StringUtils.contains(resource, "..")) {
return false;
}
// 检查允许下载的文件规则
if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) {
return true;
}
// 不在允许下载的文件规则
return false;
}
/**
* 下载文件名重新编码
*
* @param request 请求对象
* @param fileName 文件名
* @return 编码后的文件名
*/
public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException {
final String agent = request.getHeader("USER-AGENT");
String filename = fileName;
if (agent.contains("MSIE")) {
// IE浏览器
filename = URLEncoder.encode(filename, "utf-8");
filename = filename.replace("+", " ");
} else if (agent.contains("Firefox")) {
// 火狐浏览器
filename = new String(fileName.getBytes(), "ISO8859-1");
} else if (agent.contains("Chrome")) {
// google浏览器
filename = URLEncoder.encode(filename, "utf-8");
} else {
// 其它浏览器
filename = URLEncoder.encode(filename, "utf-8");
}
return filename;
}
/**
* 返回文件名
*
* @param filePath 文件
* @return 文件名
*/
public static String getName(String filePath) {
if (null == filePath) {
return null;
}
int len = filePath.length();
if (0 == len) {
return filePath;
}
if (isFileSeparator(filePath.charAt(len - 1))) {
// 以分隔符结尾的去掉结尾分隔符
len--;
}
int begin = 0;
char c;
for (int i = len - 1; i > -1; i--) {
c = filePath.charAt(i);
if (isFileSeparator(c)) {
// 查找最后一个路径分隔符(/或者\)
begin = i + 1;
break;
}
}
return filePath.substring(begin, len);
}
/**
* 获取文件名,不带后缀
*
* @param filePath
* @return
*/
public static String getFilename(String filePath) {
String name = getName(filePath);
if (null == name) {
return null;
}
if (name.contains(".")) {
int end = name.indexOf(".");
return name.substring(0, end);
}
return name;
}
/**
* 获取文件后缀 如:.zip
*
* @param filePath
* @return
*/
public static String getFilenameSuffix(String filePath) {
String name = getName(filePath);
if (null == name) {
return null;
}
if (name.contains(".")) {
int end = name.indexOf(".");
return name.substring(end);
}
return name;
}
/**
* 是否为Windows或者Linux(Unix)文件分隔符
* Windows平台下分隔符为\,Linux(Unix)为/
*
* @param c 字符
* @return 是否为Windows或者Linux(Unix)文件分隔符
*/
public static boolean isFileSeparator(char c) {
return SLASH == c || BACKSLASH == c;
}
/**
* 下载文件名重新编码
*
* @param response 响应对象
* @param realFileName 真实文件名
* @return
*/
public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException {
String percentEncodedFileName = percentEncode(realFileName);
StringBuilder contentDispositionValue = new StringBuilder();
contentDispositionValue.append("attachment; filename=")
.append(percentEncodedFileName)
.append(";")
.append("filename*=")
.append("utf-8''")
.append(percentEncodedFileName);
response.setHeader("Content-disposition", contentDispositionValue.toString());
response.setHeader("download-filename", percentEncodedFileName);
}
/**
* 百分号编码工具方法
*
* @param s 需要百分号编码的字符串
* @return 百分号编码后的字符串
*/
public static String percentEncode(String s) throws UnsupportedEncodingException {
String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString());
return encode.replaceAll("\\+", "%20");
}
/**
* 获取路径下所有文件名和文件路径
* 以,分割
*
* @param dirPath 目录路径
* @return hashMap name和url
*/
public static HashMap<String, String> getMapPath(String dirPath) {
HashMap<String, String> pathMap = new HashMap<String, String>();
File dirFile = new File(dirPath);
String[] fileName = dirFile.list();
StringJoiner joiner = new StringJoiner(",");
for (String name : fileName) {
joiner.add(dirPath + name);
}
pathMap.put("name", String.join(",", fileName));
pathMap.put("url", joiner.toString());
return pathMap;
}
public static boolean deleteAllFile(String dir) {
File dirFile = new File(dir);
// 如果dir对应的文件不存在,或者不是一个目录,则退出
if ((!dirFile.exists()) || (!dirFile.isDirectory())) {
return false;
}
boolean flag = true;
// 删除文件夹中的所有文件包括子文件夹
File[] files = dirFile.listFiles();
for (int i = 0; i < files.length; i++) {
// 删除子文件
if (files[i].isFile()) {
flag = deleteFileFlag(files[i].getAbsolutePath());
if (!flag) {
break;
}
}
// 删除子文件夹
else if (files[i].isDirectory()) {
flag = deleteAllFile(files[i].getAbsolutePath());
if (!flag) {
break;
}
}
}
if (!flag) {
return false;
}
// 删除当前文件夹
if (dirFile.delete()) {
return true;
} else {
return false;
}
}
/**
* 删除文件返回bool
*
* @param fileName
* @return boolean
*/
public static boolean deleteFileFlag(String fileName) {
File file = new File(fileName);
// 如果文件路径只有单个文件
if (file.exists() && file.isFile()) {
if (file.delete()) {
return true;
} else {
return false;
}
} else {
return false;
}
}
/**
* json文件转json对象
*
* @param data 文件流
* @return json对象
*/
public static Map readJsonFile(byte[] data) {
Gson gson = new Gson();
String json = "";
try {
Reader reader = new InputStreamReader(new ByteArrayInputStream(data), StandardCharsets.UTF_8);
int ch = 0;
StringBuilder buffer = new StringBuilder(1024);
while ((ch = reader.read()) != -1) {
buffer.append((char) ch);
}
reader.close();
json = buffer.toString();
return gson.fromJson(json, Map.class);
} catch (IOException e) {
log.error("json文件转json对象失败,原因是e={}", e.getMessage());
return Collections.emptyMap();
}
}
/**
* Object 转换为 json 文件
*
* @param finalPath finalPath 是绝对路径 + 文件名,请确保欲生成的文件所在目录已创建好
* @param content 需要被转换的 content
*/
public static void object2JsonFile(String finalPath, String content) {
try {
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(finalPath), StandardCharsets.UTF_8);
osw.write(content);
osw.flush();
osw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将java对象转成json文件返回给前端
*
* @param object 转换为 json
* @param fileName json文件名称
* @param response 结果
*/
public static void object2JsonFile(Object object, String fileName, HttpServletResponse response) {
try {
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
//获取文件的网络输入流
byte[] bytes = JSONUtil.toJsonStr(object).getBytes(StandardCharsets.UTF_8);
InputStream fis = new ByteArrayInputStream(bytes);
byte[] buffer = new byte[1024 * 5];
int r;
while ((r = fis.read(buffer)) != -1) {
response.getOutputStream().write(buffer, 0, r);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取封装得MultipartFile
*
* @param inputStream inputStream
* @param fileName fileName
* @return MultipartFile
*/
private MultipartFile getMultipartFile(InputStream inputStream, String fileName) {
FileItem fileItem = createFileItem(inputStream, fileName);
//CommonsMultipartFile是feign对multipartFile的封装,但是要FileItem类对象
return new CommonsMultipartFile(fileItem);
}
/**
* FileItem类对象创建
*
* @param inputStream inputStream
* @param fileName fileName
* @return FileItem
*/
public FileItem createFileItem(InputStream inputStream, String fileName) {
FileItemFactory factory = new DiskFileItemFactory(16, null);
String textFieldName = "file";
FileItem item = factory.createItem(textFieldName, MediaType.MULTIPART_FORM_DATA_VALUE, true, fileName);
int bytesRead = 0;
byte[] buffer = new byte[8192];
OutputStream os = null;
//使用输出流输出输入流的字节
try {
os = item.getOutputStream();
while ((bytesRead = inputStream.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
inputStream.close();
} catch (IOException e) {
log.error("Stream copy exception", e);
throw new IllegalArgumentException("文件上传失败");
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
log.error("Stream close exception", e);
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error("Stream close exception", e);
}
}
}
return item;
}
public static int execSh(String bashCommand) {
log.info("开始执行shell命令bashCommand={}", bashCommand);
int status = 0;
try {
Runtime runtime = Runtime.getRuntime();
String[] bash = {"/bin/bash", "-c", bashCommand};
Process exec = runtime.exec(bash);
status = exec.waitFor();
if (status != 0) {
return 1;
}
} catch (IOException | InterruptedException e) {
log.error("执行shell命令bashCommand={}失败,原因是e={}", bashCommand, e.getMessage());
}
return status;
}
/**
* 执行Shell脚本 0成功 1失败
*/
public static boolean execSh(String bashCommand, long time, TimeUnit timeUnit) {
try {
log.info("开始执行shell命令bashCommand={}", bashCommand);
Runtime runtime = Runtime.getRuntime();
String[] bash = {"/bin/bash", "-c", bashCommand};
Process exec = runtime.exec(bash);
return exec.waitFor(time, timeUnit);
} catch (IOException | InterruptedException e) {
log.error("执行shell命令bashCommand={}失败,原因是e={}", bashCommand, e.getMessage());
}
return false;
}
public static void deleteTempFiles(File file2, String descDir) {
File file1 = new File(descDir);
//删除zip解压的数据
if (ObjectUtil.isNotEmpty(file1) && file1.exists()) {
log.info("file1={}", file1.getPath());
deleteFile(file1);
}
//删除zip文件
//删除zip文件
if (ObjectUtil.isNotEmpty(file2) && file2.exists()) {
log.info("file2={}", file2.getPath());
deleteFile(file2);
}
}
public static void deleteFile(File file) {
if (file == null) {
log.info("deleteFile结果file=null");
return;
}
if (file.isFile()) {
boolean delete = file.delete();
log.info("删除结果file={},result={}", file.getPath(), delete);
} else if (file.isDirectory()) {
for (File sub : file.listFiles()) {
deleteFile(sub);
}
file.delete();
}
}
/**
* 根据byte数组,生成文件
*
* @param bfile 文件数组
* @param filePath 文件存放路径
* @param fileName 文件名称
*/
public static File byte2File(byte[] bfile, String filePath, String fileName) {
BufferedOutputStream bos = null;
FileOutputStream fos = null;
File file = null;
try {
File dir = new File(filePath);
if (!dir.exists() && !dir.isDirectory()) {//判断文件目录是否存在
dir.mkdirs();
}
file = new File(filePath + fileName);
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
bos.write(bfile);
} catch (Exception e) {
log.error("byte数组,生成文件失败,原因是e={}", e.getMessage());
} finally {
try {
if (bos != null) {
bos.close();
}
if (fos != null) {
fos.close();
}
} catch (Exception e) {
log.error(">>>> byte2File error" + e.getMessage());
e.printStackTrace();
}
}
return file;
}
public static byte[] base64StrToBytes(String base64Str) {
byte[] bts = org.apache.tomcat.util.codec.binary.Base64.decodeBase64(base64Str);
for (int k = 0; k < bts.length; ++k) {
//调整异常数据
if (bts[k] < 0) {
bts[k] += 256;
}
}
return bts;
}
}
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @Author:
* @Description: 校验分享链接入参
*/
@Data
@ApiModel("入参")
public class ImageInfo {
@ApiModelProperty(name = "id", value = "id", required = true)
private String id;
@ApiModelProperty(value = "资源路径")
private String pathUrl;
@ApiModelProperty(value = "文件名称")
private String filename;
}
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 入参
*
* @author ***
* @since 2023/02/16
*/
@Data
@ApiModel(入参")
public class InfoReqVO {
@ApiModelProperty(value = "id")
private Long id;
@ApiModelProperty(value = "版本号")
private Integer version;
}