diff --git a/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java index 40c88f50..9c0207fd 100644 --- a/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java +++ b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java @@ -1,8 +1,6 @@ package com.youlai.boot.app.packages.component; -import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; +import com.android.aapt.Resources; import com.android.tools.build.bundletool.commands.DumpCommand; import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.AppBundle; @@ -11,6 +9,8 @@ import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; import com.fasterxml.jackson.databind.ObjectMapper; import com.youlai.boot.app.packages.model.dto.ApkmJsonInfo; +import com.youlai.boot.app.packages.model.dto.SplitApks; +import com.youlai.boot.app.packages.model.dto.XapkJsonInfo; import net.dongliu.apk.parser.ApkFile; import net.dongliu.apk.parser.bean.ApkMeta; import net.dongliu.apk.parser.bean.Icon; @@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.*; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; @@ -44,7 +45,8 @@ import java.util.zip.ZipInputStream; */ public class ApkMetaParser { private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android"; - private Logger logger = LoggerFactory.getLogger(ApkMetaParser.class); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final Logger LOGGER = LoggerFactory.getLogger(ApkMetaParser.class); /** * 统一解析入口:根据文件扩展名分发到对应解析策略。 @@ -189,6 +191,32 @@ public class ApkMetaParser { return apkMetaResult; } + /** + * 在资源表中按资源 ID(packageId/typeId/entryId)定位到目标条目。 + */ + private static Resources.Entry findResourceEntry( + Resources.ResourceTable table, int resId) { + int packageId = (resId >>> 24) & 0xFF; + int typeId = (resId >>> 16) & 0xFF; + int entryId = resId & 0xFFFF; + for (Resources.Package pkg : table.getPackageList()) { + if (pkg.getPackageId().getId() != packageId) { + continue; + } + for (Resources.Type type : pkg.getTypeList()) { + if (type.getTypeId().getId() != typeId) { + continue; + } + for (Resources.Entry entry : type.getEntryList()) { + if (entry.getEntryId().getId() == entryId) { + return entry; + } + } + } + } + return null; + } + /** * 从 AAB base 模块的资源表中,按资源 ID 解析字符串资源(如 @string/app_name)。 *

@@ -198,34 +226,20 @@ public class ApkMetaParser { */ private static String resolveStringResource(BundleModule baseModule, int resId) { try { - Optional tableOpt = + Optional tableOpt = baseModule.getResourceTable(); if (!tableOpt.isPresent()) { return null; } - com.android.aapt.Resources.ResourceTable table = tableOpt.get(); - int packageId = (resId >>> 24) & 0xFF; - int typeId = (resId >>> 16) & 0xFF; - int entryId = resId & 0xFFFF; - for (com.android.aapt.Resources.Package pkg : table.getPackageList()) { - if (pkg.getPackageId().getId() != packageId) { - continue; - } - for (com.android.aapt.Resources.Type type : pkg.getTypeList()) { - if (type.getTypeId().getId() != typeId) { - continue; - } - for (com.android.aapt.Resources.Entry entry : type.getEntryList()) { - if (entry.getEntryId().getId() != entryId) { - continue; - } - for (com.android.aapt.Resources.ConfigValue cv : entry.getConfigValueList()) { - com.android.aapt.Resources.Item item = cv.getValue().getItem(); - if (item != null && item.getValueCase() == com.android.aapt.Resources.Item.ValueCase.STR) { - return item.getStr().getValue(); - } - } - } + Resources.ResourceTable table = tableOpt.get(); + Resources.Entry entry = findResourceEntry(table, resId); + if (entry == null) { + return null; + } + for (Resources.ConfigValue cv : entry.getConfigValueList()) { + Resources.Item item = cv.getValue().getItem(); + if (item != null && item.getValueCase() == Resources.Item.ValueCase.STR) { + return item.getStr().getValue(); } } } catch (Exception ignored) { @@ -243,61 +257,47 @@ public class ApkMetaParser { */ private static String resolveFileResource(BundleModule baseModule, int resId) { try { - Optional tableOpt = + Optional tableOpt = baseModule.getResourceTable(); if (!tableOpt.isPresent()) { return null; } - com.android.aapt.Resources.ResourceTable table = tableOpt.get(); - int packageId = (resId >>> 24) & 0xFF; - int typeId = (resId >>> 16) & 0xFF; - int entryId = resId & 0xFFFF; - for (com.android.aapt.Resources.Package pkg : table.getPackageList()) { - if (pkg.getPackageId().getId() != packageId) { + Resources.ResourceTable table = tableOpt.get(); + Resources.Entry entry = findResourceEntry(table, resId); + if (entry == null) { + return null; + } + String bestPath = null; + int bestDensity = -1; + boolean hasDensity = false; + for (Resources.ConfigValue cv : entry.getConfigValueList()) { + Resources.Item item = cv.getValue().getItem(); + if (item == null + || item.getValueCase() != Resources.Item.ValueCase.FILE) { continue; } - for (com.android.aapt.Resources.Type type : pkg.getTypeList()) { - if (type.getTypeId().getId() != typeId) { - continue; - } - for (com.android.aapt.Resources.Entry entry : type.getEntryList()) { - if (entry.getEntryId().getId() != entryId) { - continue; - } - String bestPath = null; - int bestDensity = -1; - boolean hasDensity = false; - for (com.android.aapt.Resources.ConfigValue cv : entry.getConfigValueList()) { - com.android.aapt.Resources.Item item = cv.getValue().getItem(); - if (item == null - || item.getValueCase() != com.android.aapt.Resources.Item.ValueCase.FILE) { - continue; - } - String path = item.getFile().getPath(); - if (path == null || path.isBlank()) { - continue; - } - int density = cv.getConfig().getDensity(); - if (density > 0) { - hasDensity = true; - if (density > bestDensity) { - bestDensity = density; - bestPath = path; - } - } else if (bestPath == null) { - // 无 density 信息的配置值,作为兜底 - bestPath = path; - } - } - if (hasDensity && bestPath != null) { - return bestPath; - } - // 存在带 density 的配置时走上面的分支;否则返回无 density 的第一个兜底路径 - if (bestPath != null) { - return bestPath; - } - } + String path = item.getFile().getPath(); + if (path == null || path.isBlank()) { + continue; } + int density = cv.getConfig().getDensity(); + if (density > 0) { + hasDensity = true; + if (density > bestDensity) { + bestDensity = density; + bestPath = path; + } + } else if (bestPath == null) { + // 无 density 信息的配置值,作为兜底 + bestPath = path; + } + } + if (hasDensity && bestPath != null) { + return bestPath; + } + // 存在带 density 的配置时走上面的分支;否则返回无 density 的第一个兜底路径 + if (bestPath != null) { + return bestPath; } } catch (Exception ignored) { // 资源解析失败不影响其它字段 @@ -355,113 +355,70 @@ public class ApkMetaParser { //以上3个应该都存在,否则解析失败 - byte[] infoData; - byte[] iconBytes; + ApkmContent content = readApkmContent(apkmFile); + byte[] infoData = content.infoJson; + List apks = content.apks; + List splitApkList = apks.stream().map(apk -> apk.name).collect(Collectors.toList()); + apkMetaResult.setSplitApkList(splitApkList); - if (infoExists) { - infoData = ZipUtil.unpackEntry(apkmFile, "info.json"); - ObjectMapper objectMapper = new ObjectMapper(); - ApkmJsonInfo info = objectMapper.readValue(infoData, ApkmJsonInfo.class); - apkMetaResult.setAppLabel(info.getApp_name()); - apkMetaResult.setPackageName(info.getPname()); - apkMetaResult.setVersionName(info.getRelease_version()); - apkMetaResult.setVersionCode(info.getVersioncode()); - apkMetaResult.setMinSdk(info.getMin_api()); - apkMetaResult.setAbiList(info.getArches()); + ApkmJsonInfo info = OBJECT_MAPPER.readValue(infoData, ApkmJsonInfo.class); + apkMetaResult.setAppLabel(info.getApp_name()); + apkMetaResult.setPackageName(info.getPname()); + apkMetaResult.setVersionName(info.getRelease_version()); + apkMetaResult.setVersionCode(info.getVersioncode()); + apkMetaResult.setMinSdk(info.getMin_api()); + apkMetaResult.setAbiList(info.getArches()); - //info.json 无法获取到 targetSdk,compileSdk,permissions,iconPath - //先加载icon.png兜底,后续再加载base.apk,有些base.apk无法获取到图标 - iconBytes = ZipUtil.unpackEntry(apkmFile, "icon.png"); - apkMetaResult.setIconBytes(iconBytes); - apkMetaResult.setIconPath("icon.png"); + //info.json 无法获取到 targetSdk,compileSdk,permissions,iconPath + //先加载icon.png兜底,后续再加载base.apk,有些base.apk无法获取到图标 + byte[] iconBytes = ZipUtil.unpackEntry(apkmFile, "icon.png"); + apkMetaResult.setIconPath("icon.png"); + apkMetaResult.setIconBytes(iconBytes); - byte[] baseApkbytes = ZipUtil.unpackEntry(apkmFile, "base.apk"); - ApkMetaResult result = tryParseApkBytes(baseApkbytes); - if (result != null) { - if (!Objects.equals(apkMetaResult.getAppLabel(),result.getAppLabel())){ - apkMetaResult.setAppLabel(result.getAppLabel()); - } - if (!Objects.equals(apkMetaResult.getPackageName(),result.getPackageName())){ - apkMetaResult.setPackageName(result.getPackageName()); - } - if (!Objects.equals(apkMetaResult.getVersionName(),result.getVersionName())){ - apkMetaResult.setVersionName(result.getVersionName()); - } - if (!Objects.equals(apkMetaResult.getVersionCode(), result.getVersionCode())){ - apkMetaResult.setVersionCode(result.getVersionCode()); - } + byte[] baseApkbytes = ZipUtil.unpackEntry(apkmFile, "base.apk"); + ApkMetaResult result = tryParseApkBytes(baseApkbytes); - //这三个属性无法从info.json中获取,只能从base.apk中获取 - apkMetaResult.setTargetSdk(result.getTargetSdk()); - apkMetaResult.setCompileSdk(result.getCompileSdk()); - apkMetaResult.setPermissions(result.getPermissions()); - - if (result.getAbiList()!= null && !result.getAbiList().isEmpty()){ - apkMetaResult.setAbiList(result.getAbiList()); - } - if(result.getSplitApkList()!= null && !result.getSplitApkList().isEmpty()){ - apkMetaResult.setSplitApkList(result.getSplitApkList()); - } - - if (StringUtils.isNotBlank(result.getIconPath())) { - apkMetaResult.setIconPath(result.getIconPath()); - } - if (result.getIconBytes() != null) { - apkMetaResult.setIconBytes(result.getIconBytes()); - } + if (result != null) { + if (!Objects.equals(apkMetaResult.getAppLabel(), result.getAppLabel())) { + apkMetaResult.setAppLabel(result.getAppLabel()); + } + if (!Objects.equals(apkMetaResult.getPackageName(), result.getPackageName())) { + apkMetaResult.setPackageName(result.getPackageName()); + } + if (!Objects.equals(apkMetaResult.getVersionName(), result.getVersionName())) { + apkMetaResult.setVersionName(result.getVersionName()); + } + if (!Objects.equals(apkMetaResult.getVersionCode(), result.getVersionCode())) { + apkMetaResult.setVersionCode(result.getVersionCode()); } + //这三个属性无法从info.json中获取,只能从base.apk中获取 + apkMetaResult.setTargetSdk(result.getTargetSdk()); + apkMetaResult.setCompileSdk(result.getCompileSdk()); + apkMetaResult.setPermissions(result.getPermissions()); - return apkMetaResult; - } else { - - ApkmContent content = readApkmContent(apkmFile); - List apks = content.apks; - byte[] infoJson = content.infoJson; - - if (apks.isEmpty()) { - throw new IOException("容器内未找到任何 .apk 文件: " + apkmFile.getName()); + if (isNotEmpty(result.getAbiList())) { + apkMetaResult.setAbiList(result.getAbiList()); + } + if (isNotEmpty(result.getSplitApkList())) { + apkMetaResult.setSplitApkList(result.getSplitApkList()); } - - byte[] baseApkbytes = ZipUtil.unpackEntry(apkmFile, "base.apk"); - - // 1) 按经验法则优先定位主 APK - ContainerEntry mainApk = locateApkmMainApk(apks); - - // 2) 若经验法则未命中,兜底:逐个尝试解析,第一个拿到 packageName 的即为主包 - ApkMetaResult result = tryParseApkBytes(baseApkbytes); - if (result == null) { - for (ContainerEntry entry : apks) { - ApkMetaResult r = tryParseApkBytes(entry.data); - if (r != null && r.getPackageName() != null && !r.getPackageName().isBlank()) { - result = r; - mainApk = entry; - break; - } - } + if (StringUtils.isNotBlank(result.getIconPath())) { + apkMetaResult.setIconPath(result.getIconPath()); } - if (result == null) { - throw new IOException("容器内所有 apk 均无法解析出有效包名: " + apkmFile.getName()); + if (result.getIconBytes() != null) { + apkMetaResult.setIconBytes(result.getIconBytes()); } - - // 3) 记录 split apk 列表 - for (ContainerEntry entry : apks) { - result.getSplitApkList().add(entry.name); - } - - if (infoJson != null) { - supplementFromJson(result, infoJson); - } - - return result; } + + return apkMetaResult; } /** * 解包 ZIP 容器,收集内部所有 .apk 及描述文件(info.json)。 * APKM固定为info.json - *

+ * 只获取splitApkList和info.json信息 * */ private static ApkmContent readApkmContent(File apkmFile) throws IOException { @@ -475,7 +432,7 @@ public class ApkMetaParser { continue; } String entryName = entry.getName(); - if (entryName.toLowerCase(Locale.ROOT).endsWith(".apk")) { + if (entryName.toLowerCase(Locale.ROOT).endsWith(".apk") && !entryName.equalsIgnoreCase("base.apk")) { try (InputStream in = zf.getInputStream(entry)) { apks.add(new ContainerEntry(entryName, in.readAllBytes())); } @@ -505,44 +462,63 @@ public class ApkMetaParser { * 可能额外包含大型 obb 扩展文件(非 apk,直接忽略)。 */ public static ApkMetaResult parseXapk(File xapkFile) throws IOException { + ApkMetaResult apkMetaResult = new ApkMetaResult(); + + boolean manifestExists = ZipUtil.containsEntry(xapkFile, "manifest.json"); + + if (!manifestExists) { + throw new IOException("容器内未找到 manifest.json 文件: " + xapkFile.getName()); + } + XapkContent content = readXapkContent(xapkFile); List apks = content.apks; byte[] manifestJson = content.manifestJson; - if (apks.isEmpty()) { - throw new IOException("容器内未找到任何 .apk 文件: " + xapkFile.getName()); + XapkJsonInfo xapkJsonInfo = OBJECT_MAPPER.readValue(manifestJson, XapkJsonInfo.class); + + apkMetaResult.setAppLabel(xapkJsonInfo.getName()); + apkMetaResult.setPackageName(xapkJsonInfo.getPackage_name()); + apkMetaResult.setVersionName(xapkJsonInfo.getVersion_name()); + apkMetaResult.setVersionCode(xapkJsonInfo.getVersion_code()); + apkMetaResult.setMinSdk(xapkJsonInfo.getMin_sdk_version()); + apkMetaResult.setTargetSdk(xapkJsonInfo.getTarget_sdk_version()); + apkMetaResult.setPermissions(xapkJsonInfo.getPermissions()); + + byte[] iconBytes = ZipUtil.unpackEntry(xapkFile, xapkJsonInfo.getIcon()); + apkMetaResult.setIconPath(xapkJsonInfo.getIcon()); + apkMetaResult.setIconBytes(iconBytes); + + List splitConfigs = xapkJsonInfo.getSplit_configs(); + List splitApksList = xapkJsonInfo.getSplit_apks(); + Set splitConfigSet = new HashSet<>(splitConfigs); + List fileList = xapkJsonInfo.getSplit_apks().stream() + .filter(apk -> splitConfigSet.contains(apk.getId())) + .map(SplitApks::getFile) + .collect(Collectors.toList()); + apkMetaResult.setSplitApkList(fileList); + + SplitApks baseApk = splitApksList.stream().filter(splitApks -> splitApks.getId().equals("base")).findFirst().orElse(null); + if (baseApk == null) { + throw new IOException("容器内未找到 base apk 文件: " + xapkFile.getName()); } - // 1) XAPK 主包优先按 manifest.json 的 split_apks(id == "base") 定位,失败再走经验法则 - ContainerEntry mainApk = locateXapkMainApk(apks, manifestJson); + ContainerEntry baseApkEntry = apks.stream().filter(containerEntry -> containerEntry.name.equals(baseApk.getFile())).findFirst().orElse(null); + if (baseApkEntry == null) { + throw new IOException("容器内未找到 base apk 文件: " + xapkFile.getName()); + } // 2) 若经验法则未命中,兜底:逐个尝试解析,第一个拿到 packageName 的即为主包 - ApkMetaResult result = mainApk != null ? tryParseApkBytes(mainApk.data) : null; + ApkMetaResult result = tryParseApkBytes(baseApkEntry.data); if (result == null) { - for (ContainerEntry entry : apks) { - ApkMetaResult r = tryParseApkBytes(entry.data); - if (r != null && r.getPackageName() != null && !r.getPackageName().isBlank()) { - result = r; - mainApk = entry; - break; - } - } - } - if (result == null) { - throw new IOException("容器内所有 apk 均无法解析出有效包名: " + xapkFile.getName()); + throw new IOException("容器内未找到 base apk 文件: " + xapkFile.getName()); } - // 3) 记录 split apk 列表 - for (ContainerEntry entry : apks) { - result.getSplitApkList().add(entry.name); - } + // 主 APK 解析出的字段优先(覆盖容器 JSON 中的值) + overrideIfDifferent(apkMetaResult, result); + apkMetaResult.setCompileSdk(result.getCompileSdk()); + apkMetaResult.setAbiList(result.getAbiList()); - // 4) 补充 JSON 描述信息(XAPK 通常只有 manifest.json) - if (manifestJson != null) { - supplementFromJson(result, manifestJson); - } - - return result; + return apkMetaResult; } private static XapkContent readXapkContent(File xapkFile) throws IOException { @@ -570,64 +546,6 @@ public class ApkMetaParser { return new XapkContent(apks, manifestJson); } - /** - * 经验法则定位主 APK: - *

- */ - private static ContainerEntry locateApkmMainApk(List apks) { - // 优先 base-master.apk / base.apk - for (ContainerEntry e : apks) { - if (e.name.toLowerCase(Locale.ROOT).equalsIgnoreCase("base.apk")) { - return e; - } - } - // 再找不含 split / config / density 等关键字的主包 - for (ContainerEntry e : apks) { - String lower = e.name.toLowerCase(Locale.ROOT); - if (!lower.contains("split") && !lower.contains("config") && !lower.contains("density")) { - return e; - } - } - return null; - } - - /** - * XAPK 专属定位:解析 manifest.json 的 {@code split_apks} 数组,返回 - * {@code id == "base"} 项对应的 apk 条目(即主包)。 - */ - private static ContainerEntry locateXapkMainApk(List apks, byte[] manifestJson) { - if (manifestJson == null) { - return null; - } - String baseFile = null; - try { - JSONArray splits = JSONUtil.parseObj(new String(manifestJson, StandardCharsets.UTF_8)) - .getJSONArray("split_apks"); - if (splits != null) { - for (int i = 0; i < splits.size(); i++) { - JSONObject split = splits.getJSONObject(i); - if ("base".equals(split.getStr("id"))) { - baseFile = split.getStr("file"); - break; - } - } - } - } catch (Exception ignored) { - // JSON 解析失败则回退到经验法则 - } - if (baseFile == null) { - return null; - } - for (ContainerEntry e : apks) { - if (e.name.equalsIgnoreCase(baseFile)) { - return e; - } - } - return null; - } - private record XapkContent(List apks, byte[] manifestJson) { } @@ -870,6 +788,34 @@ public class ApkMetaParser { } } + /** + * 将主 APK 解析结果覆盖到容器解析结果上,仅当两者不一致时才覆盖。 + */ + private static void overrideIfDifferent(ApkMetaResult target, ApkMetaResult source) { + if (!Objects.equals(target.getAppLabel(), source.getAppLabel())) { + target.setAppLabel(source.getAppLabel()); + } + if (!Objects.equals(target.getPackageName(), source.getPackageName())) { + target.setPackageName(source.getPackageName()); + } + if (!Objects.equals(target.getVersionName(), source.getVersionName())) { + target.setVersionName(source.getVersionName()); + } + if (!Objects.equals(target.getVersionCode(), source.getVersionCode())) { + target.setVersionCode(source.getVersionCode()); + } + if (!Objects.equals(target.getMinSdk(), source.getMinSdk())) { + target.setMinSdk(source.getMinSdk()); + } + if (!Objects.equals(target.getTargetSdk(), source.getTargetSdk())) { + target.setTargetSdk(source.getTargetSdk()); + } + } + + private static boolean isNotEmpty(List list) { + return list != null && !list.isEmpty(); + } + private static void fillIcon(ApkMetaResult result, ApkFile apkFileParser) { try { List icons = apkFileParser.getIconFiles(); diff --git a/src/main/java/com/youlai/boot/app/packages/model/dto/SplitApks.java b/src/main/java/com/youlai/boot/app/packages/model/dto/SplitApks.java new file mode 100644 index 00000000..abeaa516 --- /dev/null +++ b/src/main/java/com/youlai/boot/app/packages/model/dto/SplitApks.java @@ -0,0 +1,25 @@ +package com.youlai.boot.app.packages.model.dto; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class SplitApks implements Serializable { + + private static final long serialVersionUID = 4207274362982520838L; + + private String file; + private String id; + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException e) { + return super.toString(); + } + } +} diff --git a/src/main/java/com/youlai/boot/app/packages/model/dto/XapkJsonInfo.java b/src/main/java/com/youlai/boot/app/packages/model/dto/XapkJsonInfo.java new file mode 100644 index 00000000..26c480d7 --- /dev/null +++ b/src/main/java/com/youlai/boot/app/packages/model/dto/XapkJsonInfo.java @@ -0,0 +1,37 @@ +package com.youlai.boot.app.packages.model.dto; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +@Data + +public class XapkJsonInfo implements Serializable { + + private static final long serialVersionUID = -5080591968988248631L; + + private int xapk_version; + private String package_name; + private String name; + private long version_code; + private String version_name; + private int min_sdk_version; + private int target_sdk_version; + private List permissions; + private List split_configs; + private long total_size; + private String icon; + private List split_apks; + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException e) { + return super.toString(); + } + } +} diff --git a/src/main/java/com/youlai/boot/app/packages/model/vo/AppPackageVO.java b/src/main/java/com/youlai/boot/app/packages/model/vo/AppPackageVO.java index cb5f9356..723d6fe5 100644 --- a/src/main/java/com/youlai/boot/app/packages/model/vo/AppPackageVO.java +++ b/src/main/java/com/youlai/boot/app/packages/model/vo/AppPackageVO.java @@ -44,7 +44,7 @@ public class AppPackageVO { private String versionName; @Schema(description = "版本号") - private Integer versionCode; + private Long versionCode; @Schema(description = "最低SDK版本") private Integer minSdk;