remove(@PathVariable String id) {
+ categoryService.remove(id);
+ return Result.success(true);
+ }
+}
diff --git a/src/main/java/com/youlai/boot/app/category/model/document/AppCategoryDocument.java b/src/main/java/com/youlai/boot/app/category/model/document/AppCategoryDocument.java
new file mode 100644
index 00000000..0acc2e36
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/category/model/document/AppCategoryDocument.java
@@ -0,0 +1,52 @@
+package com.youlai.boot.app.category.model.document;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+import java.time.LocalDateTime;
+
+/**
+ * 应用分类文档(MongoDB)
+ *
+ * code 为业务唯一编码,前端上传安装包时携带该编码;name/icon/description 用于展示。
+ */
+@Schema(description = "应用分类")
+@Data
+@Document(collection = "app_category")
+public class AppCategoryDocument {
+
+ @Schema(description = "分类ID")
+ @Id
+ private String id;
+
+ @Schema(description = "分类编码(唯一,前端上传时携带,如 game/tools)")
+ @Field("code")
+ private String code;
+
+ @Schema(description = "分类名称(展示用,如 游戏/工具)")
+ @Field("name")
+ private String name;
+
+ @Schema(description = "分类图标(手动上传图片的访问 URL,如 /static/category_icon/{md5}.png)")
+ @Field("icon")
+ private String icon;
+
+ @Schema(description = "分类描述")
+ @Field("description")
+ private String description;
+
+ @Schema(description = "排序(越小越靠前)")
+ @Field("sort")
+ private Integer sort;
+
+ @Schema(description = "创建时间")
+ @Field("create_time")
+ private LocalDateTime createTime;
+
+ @Schema(description = "更新时间")
+ @Field("update_time")
+ private LocalDateTime updateTime;
+}
diff --git a/src/main/java/com/youlai/boot/app/category/model/form/AppCategoryForm.java b/src/main/java/com/youlai/boot/app/category/model/form/AppCategoryForm.java
new file mode 100644
index 00000000..b5c9cdfe
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/category/model/form/AppCategoryForm.java
@@ -0,0 +1,27 @@
+package com.youlai.boot.app.category.model.form;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 应用分类表单(新增/更新)
+ */
+@Schema(description = "应用分类表单")
+@Data
+public class AppCategoryForm {
+
+ @Schema(description = "分类编码(唯一,如 game/tools)")
+ private String code;
+
+ @Schema(description = "分类名称(展示用)")
+ private String name;
+
+ @Schema(description = "分类图标(手动上传图片的访问 URL)")
+ private String icon;
+
+ @Schema(description = "分类描述")
+ private String description;
+
+ @Schema(description = "排序(越小越靠前)")
+ private Integer sort;
+}
diff --git a/src/main/java/com/youlai/boot/app/category/model/vo/AppCategoryVO.java b/src/main/java/com/youlai/boot/app/category/model/vo/AppCategoryVO.java
new file mode 100644
index 00000000..3c1999e3
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/category/model/vo/AppCategoryVO.java
@@ -0,0 +1,38 @@
+package com.youlai.boot.app.category.model.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 应用分类视图对象
+ */
+@Schema(description = "应用分类视图对象")
+@Data
+public class AppCategoryVO {
+
+ @Schema(description = "分类ID")
+ private String id;
+
+ @Schema(description = "分类编码(唯一)")
+ private String code;
+
+ @Schema(description = "分类名称")
+ private String name;
+
+ @Schema(description = "分类图标(图片 URL)")
+ private String icon;
+
+ @Schema(description = "分类描述")
+ private String description;
+
+ @Schema(description = "排序")
+ private Integer sort;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+
+ @Schema(description = "更新时间")
+ private LocalDateTime updateTime;
+}
diff --git a/src/main/java/com/youlai/boot/app/category/repository/AppCategoryRepository.java b/src/main/java/com/youlai/boot/app/category/repository/AppCategoryRepository.java
new file mode 100644
index 00000000..699e2435
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/category/repository/AppCategoryRepository.java
@@ -0,0 +1,37 @@
+package com.youlai.boot.app.category.repository;
+
+import com.youlai.boot.app.category.model.document.AppCategoryDocument;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 应用分类仓储
+ */
+public interface AppCategoryRepository {
+
+ /**
+ * 查询全部分类(按 sort 升序、createTime 降序)
+ */
+ List listAll();
+
+ /**
+ * 根据编码查询(用于唯一性校验)
+ */
+ Optional findByCode(String code);
+
+ /**
+ * 根据ID查询
+ */
+ Optional findById(String id);
+
+ /**
+ * 保存
+ */
+ AppCategoryDocument save(AppCategoryDocument document);
+
+ /**
+ * 根据ID删除
+ */
+ void deleteById(String id);
+}
diff --git a/src/main/java/com/youlai/boot/app/category/repository/impl/AppCategoryRepositoryImpl.java b/src/main/java/com/youlai/boot/app/category/repository/impl/AppCategoryRepositoryImpl.java
new file mode 100644
index 00000000..7df51fc8
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/category/repository/impl/AppCategoryRepositoryImpl.java
@@ -0,0 +1,55 @@
+package com.youlai.boot.app.category.repository.impl;
+
+import com.youlai.boot.app.category.model.document.AppCategoryDocument;
+import com.youlai.boot.app.category.repository.AppCategoryRepository;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 应用分类仓储实现
+ */
+@Repository
+public class AppCategoryRepositoryImpl implements AppCategoryRepository {
+
+ private final MongoTemplate mongoTemplate;
+
+ public AppCategoryRepositoryImpl(MongoTemplate mongoTemplate) {
+ this.mongoTemplate = mongoTemplate;
+ }
+
+ @Override
+ public List listAll() {
+ Query query = new Query()
+ .with(Sort.by(Sort.Direction.ASC, "sort"))
+ .with(Sort.by(Sort.Direction.DESC, "createTime"));
+ return mongoTemplate.find(query, AppCategoryDocument.class);
+ }
+
+ @Override
+ public Optional findByCode(String code) {
+ Query query = new Query(Criteria.where("code").is(code));
+ return Optional.ofNullable(mongoTemplate.findOne(query, AppCategoryDocument.class));
+ }
+
+ @Override
+ public Optional findById(String id) {
+ return Optional.ofNullable(mongoTemplate.findById(id, AppCategoryDocument.class));
+ }
+
+ @Override
+ public AppCategoryDocument save(AppCategoryDocument document) {
+ return mongoTemplate.save(document);
+ }
+
+ @Override
+ public void deleteById(String id) {
+ Query query = new Query(Criteria.where("_id").is(id));
+ mongoTemplate.remove(query, AppCategoryDocument.class);
+ }
+}
diff --git a/src/main/java/com/youlai/boot/app/category/service/AppCategoryService.java b/src/main/java/com/youlai/boot/app/category/service/AppCategoryService.java
new file mode 100644
index 00000000..ac26dd36
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/category/service/AppCategoryService.java
@@ -0,0 +1,37 @@
+package com.youlai.boot.app.category.service;
+
+import com.youlai.boot.app.category.model.form.AppCategoryForm;
+import com.youlai.boot.app.category.model.vo.AppCategoryVO;
+
+import java.util.List;
+
+/**
+ * 应用分类服务
+ */
+public interface AppCategoryService {
+
+ /**
+ * 查询全部分类
+ */
+ List listCategories();
+
+ /**
+ * 分类详情
+ */
+ AppCategoryVO getById(String id);
+
+ /**
+ * 新增分类(code 唯一)
+ */
+ AppCategoryVO create(AppCategoryForm form);
+
+ /**
+ * 更新分类(code 唯一,排除自身)
+ */
+ AppCategoryVO update(String id, AppCategoryForm form);
+
+ /**
+ * 删除分类
+ */
+ void remove(String id);
+}
diff --git a/src/main/java/com/youlai/boot/app/category/service/impl/AppCategoryServiceImpl.java b/src/main/java/com/youlai/boot/app/category/service/impl/AppCategoryServiceImpl.java
new file mode 100644
index 00000000..c77e8203
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/category/service/impl/AppCategoryServiceImpl.java
@@ -0,0 +1,98 @@
+package com.youlai.boot.app.category.service.impl;
+
+import com.youlai.boot.app.category.model.document.AppCategoryDocument;
+import com.youlai.boot.app.category.model.form.AppCategoryForm;
+import com.youlai.boot.app.category.model.vo.AppCategoryVO;
+import com.youlai.boot.app.category.repository.AppCategoryRepository;
+import com.youlai.boot.app.category.service.AppCategoryService;
+import com.youlai.boot.common.exception.BusinessException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * 应用分类服务实现
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AppCategoryServiceImpl implements AppCategoryService {
+
+ private final AppCategoryRepository categoryRepository;
+
+ @Override
+ public List listCategories() {
+ return categoryRepository.listAll().stream()
+ .map(this::toVO)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public AppCategoryVO getById(String id) {
+ AppCategoryDocument doc = categoryRepository.findById(id)
+ .orElseThrow(() -> new BusinessException("分类不存在"));
+ return toVO(doc);
+ }
+
+ @Override
+ public AppCategoryVO create(AppCategoryForm form) {
+ if (!StringUtils.hasText(form.getCode())) {
+ throw new BusinessException("分类编码不能为空");
+ }
+ if (!StringUtils.hasText(form.getName())) {
+ throw new BusinessException("分类名称不能为空");
+ }
+ if (categoryRepository.findByCode(form.getCode()).isPresent()) {
+ throw new BusinessException("分类编码已存在:" + form.getCode());
+ }
+
+ AppCategoryDocument doc = new AppCategoryDocument();
+ BeanUtils.copyProperties(form, doc);
+ if (doc.getSort() == null) {
+ doc.setSort(0);
+ }
+ doc.setId(UUID.randomUUID().toString());
+ doc.setCreateTime(LocalDateTime.now());
+ doc.setUpdateTime(LocalDateTime.now());
+ return toVO(categoryRepository.save(doc));
+ }
+
+ @Override
+ public AppCategoryVO update(String id, AppCategoryForm form) {
+ AppCategoryDocument doc = categoryRepository.findById(id)
+ .orElseThrow(() -> new BusinessException("分类不存在"));
+
+ if (StringUtils.hasText(form.getCode()) && !form.getCode().equals(doc.getCode())) {
+ categoryRepository.findByCode(form.getCode()).ifPresent(existing -> {
+ if (!existing.getId().equals(id)) {
+ throw new BusinessException("分类编码已存在:" + form.getCode());
+ }
+ });
+ }
+
+ BeanUtils.copyProperties(form, doc, "id", "createTime");
+ doc.setUpdateTime(LocalDateTime.now());
+ return toVO(categoryRepository.save(doc));
+ }
+
+ @Override
+ public void remove(String id) {
+ if (categoryRepository.findById(id).isEmpty()) {
+ throw new BusinessException("分类不存在");
+ }
+ categoryRepository.deleteById(id);
+ }
+
+ private AppCategoryVO toVO(AppCategoryDocument doc) {
+ AppCategoryVO vo = new AppCategoryVO();
+ BeanUtils.copyProperties(doc, vo);
+ return vo;
+ }
+}
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
new file mode 100644
index 00000000..7f10730f
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java
@@ -0,0 +1,117 @@
+package com.youlai.boot.app.packages.component;
+
+import lombok.Data;
+import net.dongliu.apk.parser.ApkFile;
+import net.dongliu.apk.parser.bean.ApkMeta;
+import net.dongliu.apk.parser.bean.Icon;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * APK 安装包元数据解析器(基于 apk-parser)
+ *
+ * 用于从 apk 文件中尽可能详细地读取包名、版本、SDK 版本、权限、CPU 架构、图标等信息。
+ * aab / xapk / apks / apkm 为其他格式,本解析器不适用,由上传流程按文件基础信息处理。
+ */
+@Component
+public class ApkMetaParser {
+
+ @Data
+ public static class ApkMetaResult {
+ private String packageName;
+ private String appLabel;
+ private String versionName;
+ private Integer versionCode;
+ private Integer minSdk;
+ private Integer targetSdk;
+ private Integer compileSdk;
+ private Integer maxSdk;
+ private List abiList = new ArrayList<>();
+ private List permissions = new ArrayList<>();
+ private String iconPath;
+ private byte[] iconBytes;
+ }
+
+ /**
+ * 解析 apk 文件元数据
+ */
+ public ApkMetaResult parse(File apkFile) throws IOException {
+ ApkMetaResult result = new ApkMetaResult();
+ try (ApkFile apkFileParser = new ApkFile(apkFile)) {
+ ApkMeta meta = apkFileParser.getApkMeta();
+
+ result.setPackageName(meta.getPackageName());
+ result.setAppLabel(meta.getLabel());
+ result.setVersionName(meta.getVersionName());
+ result.setVersionCode(toInt(meta.getVersionCode()));
+ result.setMinSdk(toInt(meta.getMinSdkVersion()));
+ result.setTargetSdk(toInt(meta.getTargetSdkVersion()));
+ result.setCompileSdk(toInt(meta.getCompileSdkVersion()));
+ result.setMaxSdk(toInt(meta.getMaxSdkVersion()));
+
+ if (meta.getUsesPermissions() != null) {
+ result.setPermissions(new ArrayList<>(meta.getUsesPermissions()));
+ }
+
+ // CPU 架构:扫描 lib// 目录
+ result.setAbiList(extractAbiList(apkFile));
+
+ // 图标:优先 launcher 图标,取首个图标的字节
+ try {
+ List icons = apkFileParser.getIconFiles();
+ if (icons != null && !icons.isEmpty()) {
+ Icon icon = icons.get(0);
+ result.setIconPath(icon.getPath());
+ byte[] data = icon.getData();
+ if (data != null && data.length > 0) {
+ result.setIconBytes(data);
+ }
+ }
+ } catch (Exception ignored) {
+ // 图标解析失败不影响其它字段
+ }
+ }
+ return result;
+ }
+
+ private List extractAbiList(File apkFile) {
+ Set abis = new LinkedHashSet<>();
+ try (ZipFile zf = new ZipFile(apkFile)) {
+ java.util.Enumeration extends ZipEntry> entries = zf.entries();
+ while (entries.hasMoreElements()) {
+ String name = entries.nextElement().getName();
+ if (name.startsWith("lib/") && name.length() > 4) {
+ String abi = name.substring(4).split("/")[0];
+ if (!abi.isBlank()) {
+ abis.add(abi);
+ }
+ }
+ }
+ } catch (IOException ignored) {
+ // 忽略
+ }
+ return new ArrayList<>(abis);
+ }
+
+ private Integer toInt(Object value) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ try {
+ return Integer.parseInt(value.toString().trim());
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/youlai/boot/app/packages/controller/AppPackageController.java b/src/main/java/com/youlai/boot/app/packages/controller/AppPackageController.java
new file mode 100644
index 00000000..12cb78df
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/packages/controller/AppPackageController.java
@@ -0,0 +1,88 @@
+package com.youlai.boot.app.packages.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.youlai.boot.app.packages.model.query.AppPackagePageQuery;
+import com.youlai.boot.app.packages.model.vo.AppPackageVO;
+import com.youlai.boot.app.packages.service.AppPackageService;
+import com.youlai.boot.common.result.PageResult;
+import com.youlai.boot.common.result.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+/**
+ * 应用安装包资源库控制层
+ *
+ * 提供 apk / apks / aab / apkm / xapk 安装包的上传、分页列表、详情与删除能力。
+ * 移动端(设备侧)无需调用本接口。
+ */
+@Tag(name = "应用安装包库")
+@RestController
+@RequestMapping("/api/v1/app/packages")
+@RequiredArgsConstructor
+public class AppPackageController {
+
+ private final AppPackageService appPackageService;
+
+ @Operation(summary = "上传安装包(apk/aab/xapk/apks/apkm),自动解析详细字段")
+ @PostMapping("/upload")
+ public Result upload(
+ @Parameter(description = "安装包文件(apk/aab/xapk/apks/apkm)") @RequestPart("file") MultipartFile file,
+ @Parameter(description = "应用包名(aab/xapk/apks/apkm 建议填写,apk 可省略)") @RequestParam(required = false) String packageName,
+ @Parameter(description = "版本名称(可省略,apk 自动解析)") @RequestParam(required = false) String versionName,
+ @Parameter(description = "版本号(可省略,apk 自动解析)") @RequestParam(required = false) String versionCode,
+ @Parameter(description = "备注说明") @RequestParam(required = false) String remark,
+ @Parameter(description = "应用分类ID列表(多选)") @RequestParam(required = false) List categories,
+ @Parameter(description = "应用截图文件列表(png/jpg,分目录存储)") @RequestPart(required = false) MultipartFile[] screenshots
+ ) {
+ return Result.success(appPackageService.upload(file, packageName, versionName, versionCode, remark, categories, screenshots));
+ }
+
+ @Operation(summary = "分页查询安装包列表")
+ @GetMapping("/page")
+ public PageResult page(
+ @Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") Integer pageNum,
+ @Parameter(description = "每页条数") @RequestParam(defaultValue = "10") Integer pageSize,
+ @Parameter(description = "关键字(包名/应用名/文件名)") @RequestParam(required = false) String keywords,
+ @Parameter(description = "类型过滤:apk/aab/xapk/apks/apkm") @RequestParam(required = false) String packageType
+ ) {
+ AppPackagePageQuery query = new AppPackagePageQuery();
+ query.setKeywords(keywords);
+ query.setPackageType(packageType);
+ org.springframework.data.domain.Page springPage =
+ appPackageService.page(query, pageNum, pageSize);
+
+ IPage iPage = new Page<>(pageNum, pageSize, springPage.getTotalElements());
+ iPage.setRecords(springPage.getContent());
+ return PageResult.success(iPage);
+ }
+
+ @Operation(summary = "安装包详情")
+ @GetMapping("/{id}")
+ public Result detail(@PathVariable String id) {
+ return Result.success(appPackageService.detail(id));
+ }
+
+ @Operation(summary = "删除安装包(同时删除本地文件)")
+ @DeleteMapping("/{id}")
+ public Result remove(@PathVariable String id) {
+ appPackageService.removeById(id);
+ return Result.success(true);
+ }
+
+ @Operation(summary = "更新安装包分类(覆盖写)")
+ @PutMapping("/{id}/categories")
+ public Result updateCategories(
+ @Parameter(description = "安装包ID") @PathVariable String id,
+ @Parameter(description = "分类编码列表") @RequestBody List categories
+ ) {
+ appPackageService.updateCategories(id, categories);
+ return Result.success(true);
+ }
+}
diff --git a/src/main/java/com/youlai/boot/app/packages/model/document/AppPackageDocument.java b/src/main/java/com/youlai/boot/app/packages/model/document/AppPackageDocument.java
new file mode 100644
index 00000000..86ee75bf
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/packages/model/document/AppPackageDocument.java
@@ -0,0 +1,122 @@
+package com.youlai.boot.app.packages.model.document;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Data;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 应用安装包资源库文档(apk / apks / aab / apkm / xapk)
+ *
+ * 区别于设备已安装应用记录(ApkInstallDocument),本集合用于管理上传的应用安装包资源,
+ * 支持从安装包中解析并存储尽可能详细的元数据字段。
+ */
+@Schema(description = "应用安装包资源")
+@Data
+@Builder
+@Document(collection = "app_package")
+public class AppPackageDocument {
+
+ @Schema(description = "主键ID")
+ @Id
+ private String id;
+
+ @Schema(description = "安装包类型:apk / apks / aab / apkm / xapk")
+ @Field("package_type")
+ private String packageType;
+
+ @Schema(description = "存储文件名(含后缀)")
+ @Field("file_name")
+ private String fileName;
+
+ @Schema(description = "原始上传文件名")
+ @Field("original_name")
+ private String originalName;
+
+ @Schema(description = "文件访问URL")
+ @Field("file_url")
+ private String fileUrl;
+
+ @Schema(description = "文件大小(字节)")
+ @Field("file_size")
+ private Long fileSize;
+
+ @Schema(description = "文件大小(人类可读,如 12.3 MB)")
+ @Field("file_size_text")
+ private String fileSizeText;
+
+ @Schema(description = "文件MD5")
+ @Field("md5")
+ private String md5;
+
+ @Schema(description = "应用包名(Android applicationId)")
+ @Field("package_name")
+ private String packageName;
+
+ @Schema(description = "应用名称/标签")
+ @Field("app_label")
+ private String appLabel;
+
+ @Schema(description = "版本名称(versionName)")
+ @Field("version_name")
+ private String versionName;
+
+ @Schema(description = "版本号(versionCode)")
+ @Field("version_code")
+ private Integer versionCode;
+
+ @Schema(description = "最低SDK版本(minSdkVersion)")
+ @Field("min_sdk")
+ private Integer minSdk;
+
+ @Schema(description = "目标SDK版本(targetSdkVersion)")
+ @Field("target_sdk")
+ private Integer targetSdk;
+
+ @Schema(description = "编译SDK版本(compileSdkVersion)")
+ @Field("compile_sdk")
+ private Integer compileSdk;
+
+ @Schema(description = "支持的CPU架构列表(如 arm64-v8a, armeabi-v7a)")
+ @Field("abi_list")
+ private List abiList;
+
+ @Schema(description = "权限列表")
+ @Field("permissions")
+ private List permissions;
+
+ @Schema(description = "应用图标URL")
+ @Field("icon_url")
+ private String iconUrl;
+
+ @Schema(description = "应用分类ID列表(多选),如 [\"game\", \"tools\"]")
+ @Field("categories")
+ private List categories;
+
+ @Schema(description = "应用截图URL列表(与安装包、图标分目录存储)")
+ @Field("screenshots")
+ private List screenshots;
+
+ @Schema(description = "是否系统应用")
+ @Field("system_app")
+ private Boolean systemApp;
+
+ @Schema(description = "是否可调试(debuggable)")
+ @Field("debuggable")
+ private Boolean debuggable;
+
+ @Schema(description = "安装包说明/备注")
+ @Field("remark")
+ private String remark;
+
+ @Schema(description = "上传时间")
+ @CreatedDate
+ @Field("create_time")
+ private LocalDateTime createTime;
+}
diff --git a/src/main/java/com/youlai/boot/app/packages/model/query/AppPackagePageQuery.java b/src/main/java/com/youlai/boot/app/packages/model/query/AppPackagePageQuery.java
new file mode 100644
index 00000000..671e8b51
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/packages/model/query/AppPackagePageQuery.java
@@ -0,0 +1,18 @@
+package com.youlai.boot.app.packages.model.query;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 应用安装包分页查询对象
+ */
+@Schema(description = "应用安装包分页查询")
+@Data
+public class AppPackagePageQuery {
+
+ @Schema(description = "关键字(包名/应用名/文件名模糊匹配)")
+ private String keywords;
+
+ @Schema(description = "安装包类型过滤:apk / apks / aab / apkm / xapk")
+ private String packageType;
+}
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
new file mode 100644
index 00000000..cb5f9356
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/packages/model/vo/AppPackageVO.java
@@ -0,0 +1,84 @@
+package com.youlai.boot.app.packages.model.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 应用安装包资源视图对象
+ */
+@Schema(description = "应用安装包资源视图对象")
+@Data
+public class AppPackageVO {
+
+ @Schema(description = "主键ID")
+ private String id;
+
+ @Schema(description = "安装包类型:apk / apks / aab / apkm / xapk")
+ private String packageType;
+
+ @Schema(description = "原始上传文件名")
+ private String originalName;
+
+ @Schema(description = "文件访问URL")
+ private String fileUrl;
+
+ @Schema(description = "文件大小(字节)")
+ private Long fileSize;
+
+ @Schema(description = "文件大小(人类可读)")
+ private String fileSizeText;
+
+ @Schema(description = "文件MD5")
+ private String md5;
+
+ @Schema(description = "应用包名")
+ private String packageName;
+
+ @Schema(description = "应用名称/标签")
+ private String appLabel;
+
+ @Schema(description = "版本名称")
+ private String versionName;
+
+ @Schema(description = "版本号")
+ private Integer versionCode;
+
+ @Schema(description = "最低SDK版本")
+ private Integer minSdk;
+
+ @Schema(description = "目标SDK版本")
+ private Integer targetSdk;
+
+ @Schema(description = "编译SDK版本")
+ private Integer compileSdk;
+
+ @Schema(description = "支持的CPU架构")
+ private List abiList;
+
+ @Schema(description = "权限列表")
+ private List permissions;
+
+ @Schema(description = "应用图标URL")
+ private String iconUrl;
+
+ @Schema(description = "应用分类ID列表(多选)")
+ private List categories;
+
+ @Schema(description = "应用截图URL列表")
+ private List screenshots;
+
+ @Schema(description = "是否系统应用")
+ private Boolean systemApp;
+
+ @Schema(description = "是否可调试")
+ private Boolean debuggable;
+
+ @Schema(description = "安装包说明/备注")
+ private String remark;
+
+ @Schema(description = "上传时间")
+ private LocalDateTime createTime;
+}
diff --git a/src/main/java/com/youlai/boot/app/packages/repository/AppPackageRepository.java b/src/main/java/com/youlai/boot/app/packages/repository/AppPackageRepository.java
new file mode 100644
index 00000000..dd031511
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/packages/repository/AppPackageRepository.java
@@ -0,0 +1,41 @@
+package com.youlai.boot.app.packages.repository;
+
+import com.youlai.boot.app.packages.model.document.AppPackageDocument;
+import com.youlai.boot.app.packages.model.query.AppPackagePageQuery;
+import org.springframework.data.domain.Page;
+
+import java.util.List;
+
+/**
+ * 应用安装包资源库仓储
+ */
+public interface AppPackageRepository {
+
+ /**
+ * 分页查询安装包列表
+ */
+ Page page(AppPackagePageQuery query, int pageNum, int pageSize);
+
+ /**
+ * 根据ID查询
+ */
+ AppPackageDocument findById(String id);
+
+ /**
+ * 保存
+ */
+ AppPackageDocument save(AppPackageDocument document);
+
+ /**
+ * 根据ID删除
+ */
+ void deleteById(String id);
+
+ /**
+ * 更新指定安装包的分类(覆盖写)
+ *
+ * @param id 安装包ID
+ * @param categories 分类编码列表
+ */
+ void updateCategories(String id, List categories);
+}
diff --git a/src/main/java/com/youlai/boot/app/packages/repository/impl/AppPackageRepositoryImpl.java b/src/main/java/com/youlai/boot/app/packages/repository/impl/AppPackageRepositoryImpl.java
new file mode 100644
index 00000000..7d86c866
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/packages/repository/impl/AppPackageRepositoryImpl.java
@@ -0,0 +1,84 @@
+package com.youlai.boot.app.packages.repository.impl;
+
+import com.youlai.boot.app.packages.model.document.AppPackageDocument;
+import com.youlai.boot.app.packages.model.query.AppPackagePageQuery;
+import com.youlai.boot.app.packages.repository.AppPackageRepository;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.stereotype.Repository;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 应用安装包资源库仓储实现
+ */
+@Repository
+public class AppPackageRepositoryImpl implements AppPackageRepository {
+
+ private final MongoTemplate mongoTemplate;
+
+ public AppPackageRepositoryImpl(MongoTemplate mongoTemplate) {
+ this.mongoTemplate = mongoTemplate;
+ }
+
+ @Override
+ public Page page(AppPackagePageQuery query, int pageNum, int pageSize) {
+ List andCriteria = new ArrayList<>();
+
+ if (query.getKeywords() != null && !query.getKeywords().isBlank()) {
+ String kw = query.getKeywords().trim();
+ andCriteria.add(new Criteria().orOperator(
+ Criteria.where("package_name").regex(kw, "i"),
+ Criteria.where("app_label").regex(kw, "i"),
+ Criteria.where("original_name").regex(kw, "i"),
+ Criteria.where("version_name").regex(kw, "i")
+ ));
+ }
+ if (query.getPackageType() != null && !query.getPackageType().isBlank()) {
+ andCriteria.add(Criteria.where("package_type").is(query.getPackageType().toLowerCase()));
+ }
+
+ Criteria criteria = new Criteria();
+ if (!andCriteria.isEmpty()) {
+ criteria.andOperator(andCriteria.toArray(new Criteria[0]));
+ }
+
+ Query pageQuery = new Query(criteria)
+ .with(Sort.by(Sort.Direction.DESC, "createTime"))
+ .with(PageRequest.of(pageNum - 1, pageSize));
+ List list = mongoTemplate.find(pageQuery, AppPackageDocument.class);
+ long total = mongoTemplate.count(new Query(criteria), AppPackageDocument.class);
+
+ return new PageImpl<>(list, PageRequest.of(pageNum - 1, pageSize), total);
+ }
+
+ @Override
+ public AppPackageDocument findById(String id) {
+ return mongoTemplate.findById(id, AppPackageDocument.class);
+ }
+
+ @Override
+ public AppPackageDocument save(AppPackageDocument document) {
+ return mongoTemplate.save(document);
+ }
+
+ @Override
+ public void deleteById(String id) {
+ Query query = new Query(Criteria.where("_id").is(id));
+ mongoTemplate.remove(query, AppPackageDocument.class);
+ }
+
+ @Override
+ public void updateCategories(String id, List categories) {
+ Query query = new Query(Criteria.where("_id").is(id));
+ Update update = new Update().set("categories", categories == null ? List.of() : categories);
+ mongoTemplate.updateFirst(query, update, AppPackageDocument.class);
+ }
+}
diff --git a/src/main/java/com/youlai/boot/app/packages/service/AppPackageService.java b/src/main/java/com/youlai/boot/app/packages/service/AppPackageService.java
new file mode 100644
index 00000000..b9e899d8
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/packages/service/AppPackageService.java
@@ -0,0 +1,52 @@
+package com.youlai.boot.app.packages.service;
+
+import com.youlai.boot.app.packages.model.query.AppPackagePageQuery;
+import com.youlai.boot.app.packages.model.vo.AppPackageVO;
+import org.springframework.data.domain.Page;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+/**
+ * 应用安装包资源库服务
+ */
+public interface AppPackageService {
+
+ /**
+ * 上传安装包(保存文件 + 解析元数据 + 入库)
+ *
+ * @param file 安装包文件(apk / apks / aab / apkm / xapk)
+ * @param packageName 用户补充的包名(apk可自动解析,aab/xapk/apks/apkm建议填写)
+ * @param versionName 用户补充的版本名称
+ * @param versionCode 用户补充的版本号
+ * @param remark 备注说明
+ * @param categories 应用分类ID列表(多选)
+ * @param screenshots 应用截图文件列表(与安装包、图标分目录存储)
+ */
+ AppPackageVO upload(MultipartFile file, String packageName, String versionName,
+ String versionCode, String remark, List categories,
+ MultipartFile[] screenshots);
+
+ /**
+ * 分页查询
+ */
+ Page page(AppPackagePageQuery query, int pageNum, int pageSize);
+
+ /**
+ * 详情
+ */
+ AppPackageVO detail(String id);
+
+ /**
+ * 删除(同时删除本地文件)
+ */
+ void removeById(String id);
+
+ /**
+ * 更新指定安装包的分类(覆盖写)
+ *
+ * @param id 安装包ID
+ * @param categories 分类编码列表
+ */
+ void updateCategories(String id, List categories);
+}
diff --git a/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java b/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java
new file mode 100644
index 00000000..81a9eed0
--- /dev/null
+++ b/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java
@@ -0,0 +1,254 @@
+package com.youlai.boot.app.packages.service.impl;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import com.youlai.boot.app.packages.component.ApkMetaParser;
+import com.youlai.boot.app.packages.model.document.AppPackageDocument;
+import com.youlai.boot.app.packages.model.query.AppPackagePageQuery;
+import com.youlai.boot.app.packages.model.vo.AppPackageVO;
+import com.youlai.boot.app.packages.repository.AppPackageRepository;
+import com.youlai.boot.app.packages.service.AppPackageService;
+import com.youlai.boot.common.config.FilePath;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.data.domain.Page;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.nio.file.Files;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 应用安装包资源库服务实现
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AppPackageServiceImpl implements AppPackageService {
+
+ private final AppPackageRepository appPackageRepository;
+ private final ApkMetaParser apkMetaParser;
+
+ private static final List ALLOWED_EXT = List.of("apk", "apks", "aab", "apkm", "xapk");
+
+ @Override
+ public AppPackageVO upload(MultipartFile file, String packageName, String versionName,
+ String versionCode, String remark, List categories,
+ MultipartFile[] screenshots) {
+ String originalName = file.getOriginalFilename();
+ String ext = FileUtil.getSuffix(originalName);
+ if (ext == null || !ALLOWED_EXT.contains(ext.toLowerCase())) {
+ throw new IllegalArgumentException("仅支持上传 apk / apks / aab / apkm / xapk 安装包");
+ }
+
+ // 1. 落盘安装包文件(内容寻址:md5 作为文件名前缀,避免冲突与重复)
+ String md5;
+ File packageDir = new File(FilePath.getPackagePath());
+ if (!packageDir.exists()) {
+ packageDir.mkdirs();
+ }
+ String storedName = IdUtil.simpleUUID() + "." + ext;
+ File storedFile = new File(packageDir, storedName);
+ try {
+ md5 = DigestUtil.md5Hex(file.getInputStream());
+ file.transferTo(storedFile);
+ } catch (IOException e) {
+ log.error("安装包保存失败", e);
+ throw new RuntimeException("安装包保存失败");
+ }
+
+ long fileSize = storedFile.length();
+ String fileUrl = "/static/packages/" + storedName;
+
+ // 2. 解析元数据(apk 解析详细字段;aab/xapk/apks/apkm 仅文件基础信息)
+ AppPackageDocument.AppPackageDocumentBuilder builder = AppPackageDocument.builder()
+ .packageType(ext.toLowerCase())
+ .fileName(storedName)
+ .originalName(originalName)
+ .fileUrl(fileUrl)
+ .fileSize(fileSize)
+ .fileSizeText(formatSize(fileSize))
+ .md5(md5)
+ .createTime(LocalDateTime.now())
+ .remark(remark);
+
+ // 3. 保存应用截图到独立的 screenshot 目录(与安装包、图标分目录存储)
+ List screenshotUrls = saveScreenshots(screenshots);
+
+ if ("apk".equalsIgnoreCase(ext)) {
+ try {
+ ApkMetaParser.ApkMetaResult meta = apkMetaParser.parse(storedFile);
+ builder.packageName(firstNonBlank(meta.getPackageName(), packageName))
+ .appLabel(meta.getAppLabel())
+ .versionName(firstNonBlank(meta.getVersionName(), versionName))
+ .versionCode(orNull(meta.getVersionCode(), versionCode))
+ .minSdk(meta.getMinSdk())
+ .targetSdk(meta.getTargetSdk())
+ .compileSdk(meta.getCompileSdk())
+ .abiList(meta.getAbiList())
+ .permissions(meta.getPermissions());
+ if (meta.getIconPath() != null && meta.getIconBytes() != null && meta.getIconBytes().length > 0) {
+ String iconMd5 = DigestUtil.md5Hex(meta.getIconBytes());
+ String iconName = iconMd5 + ".png";
+ File iconDir = new File(FilePath.getApkIconPath());
+ if (!iconDir.exists()) {
+ iconDir.mkdirs();
+ }
+ File iconTarget = new File(iconDir, iconName);
+ if (!iconTarget.exists()) {
+ Files.write(iconTarget.toPath(), meta.getIconBytes());
+ }
+ builder.iconUrl("/static/app_icon/" + iconName);
+ }
+ } catch (Exception e) {
+ log.warn("APK 元数据解析失败,使用表单补充信息:{}", e.getMessage());
+ fillFallback(builder, packageName, versionName, versionCode);
+ }
+ } else {
+ // aab / xapk / apks / apkm 暂不支持二进制解析,使用表单补充信息
+ fillFallback(builder, packageName, versionName, versionCode);
+ }
+
+ List categoryList = (categories != null) ? categories : List.of();
+ builder.categories(categoryList).screenshots(screenshotUrls);
+
+ AppPackageDocument saved = appPackageRepository.save(builder.build());
+ return toVO(saved);
+ }
+
+ /**
+ * 保存应用截图到独立的 screenshot 目录(与安装包、图标分目录存储)
+ */
+ private List saveScreenshots(MultipartFile[] screenshots) {
+ List urls = new java.util.ArrayList<>();
+ if (screenshots == null || screenshots.length == 0) {
+ return urls;
+ }
+ File dir = new File(FilePath.getScreenshotPath());
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ List allowedImgExt = List.of("png", "jpg", "jpeg", "webp");
+ for (MultipartFile sf : screenshots) {
+ if (sf == null || sf.isEmpty()) {
+ continue;
+ }
+ String suffix = FileUtil.getSuffix(sf.getOriginalFilename());
+ if (suffix == null || !allowedImgExt.contains(suffix.toLowerCase())) {
+ throw new IllegalArgumentException("应用截图仅支持 png / jpg / jpeg / webp 格式");
+ }
+ String stored = IdUtil.simpleUUID() + "." + suffix;
+ try {
+ sf.transferTo(new File(dir, stored));
+ } catch (IOException e) {
+ log.error("应用截图保存失败", e);
+ throw new RuntimeException("应用截图保存失败");
+ }
+ urls.add("/static/screenshot/" + stored);
+ }
+ return urls;
+ }
+
+ private void fillFallback(AppPackageDocument.AppPackageDocumentBuilder builder,
+ String packageName, String versionName, String versionCode) {
+ builder.packageName(packageName)
+ .versionName(versionName)
+ .versionCode(parseIntSafe(versionCode));
+ }
+
+ @Override
+ public Page page(AppPackagePageQuery query, int pageNum, int pageSize) {
+ Page docPage = appPackageRepository.page(query, pageNum, pageSize);
+ return docPage.map(this::toVO);
+ }
+
+ @Override
+ public AppPackageVO detail(String id) {
+ AppPackageDocument doc = appPackageRepository.findById(id);
+ return doc == null ? null : toVO(doc);
+ }
+
+ @Override
+ public void removeById(String id) {
+ AppPackageDocument doc = appPackageRepository.findById(id);
+ if (doc == null) {
+ return;
+ }
+ // 删除本地安装包文件
+ if (doc.getFileName() != null) {
+ FileUtil.del(FilePath.getPackagePath() + doc.getFileName());
+ }
+ // 删除本地图标文件
+ if (doc.getIconUrl() != null && doc.getIconUrl().startsWith("/static/app_icon/")) {
+ String iconName = doc.getIconUrl().substring("/static/app_icon/".length());
+ FileUtil.del(FilePath.getApkIconPath() + iconName);
+ }
+ // 删除本地截图文件(独立 screenshot 目录)
+ if (doc.getScreenshots() != null) {
+ for (String shotUrl : doc.getScreenshots()) {
+ if (shotUrl != null && shotUrl.startsWith("/static/screenshot/")) {
+ String shotName = shotUrl.substring("/static/screenshot/".length());
+ FileUtil.del(FilePath.getScreenshotPath() + shotName);
+ }
+ }
+ }
+ appPackageRepository.deleteById(id);
+ }
+
+ @Override
+ public void updateCategories(String id, List categories) {
+ appPackageRepository.updateCategories(id, categories);
+ }
+
+ private AppPackageVO toVO(AppPackageDocument doc) {
+ AppPackageVO vo = new AppPackageVO();
+ BeanUtils.copyProperties(doc, vo);
+ return vo;
+ }
+
+ private String firstNonBlank(String... values) {
+ for (String v : values) {
+ if (v != null && !v.isBlank()) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ private Integer orNull(Integer parsed, String fallback) {
+ if (parsed != null) {
+ return parsed;
+ }
+ return parseIntSafe(fallback);
+ }
+
+ private Integer parseIntSafe(String value) {
+ if (value == null || value.isBlank()) {
+ return null;
+ }
+ try {
+ return Integer.parseInt(value.trim());
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ private String formatSize(long bytes) {
+ if (bytes <= 0) {
+ return "0 B";
+ }
+ String[] units = {"B", "KB", "MB", "GB"};
+ int digitGroups = (int) (Math.log10(bytes) / Math.log10(1024));
+ digitGroups = Math.min(digitGroups, units.length - 1);
+ BigDecimal size = new BigDecimal(bytes)
+ .divide(new BigDecimal(Math.pow(1024, digitGroups)), 2, RoundingMode.HALF_UP);
+ return size.toPlainString() + " " + units[digitGroups];
+ }
+}
diff --git a/src/main/java/com/youlai/boot/common/config/FilePath.java b/src/main/java/com/youlai/boot/common/config/FilePath.java
index 00617815..8c356222 100644
--- a/src/main/java/com/youlai/boot/common/config/FilePath.java
+++ b/src/main/java/com/youlai/boot/common/config/FilePath.java
@@ -19,6 +19,8 @@ public class FilePath {
public static final String AVATAR_PATH = "avatar";
public static final String APK_ICON_PATH = "apkIcon";
public static final String SCREENSHOT_PATH = "screenshot";
+ public static final String PACKAGE_PATH = "packages";
+ public static final String CATEGORY_ICON_PATH = "category_icon";
public static String getRootPath() {
String osName = System.getProperty("os.name");
@@ -43,4 +45,12 @@ public class FilePath {
public static String getScreenshotPath() {
return getRootPath() + File.separator + TABLET_PATH + File.separator + SCREENSHOT_PATH + File.separator;
}
+
+ public static String getPackagePath() {
+ return getRootPath() + File.separator + TABLET_PATH + File.separator + PACKAGE_PATH + File.separator;
+ }
+
+ public static String getCategoryIconPath() {
+ return getRootPath() + File.separator + TABLET_PATH + File.separator + CATEGORY_ICON_PATH + File.separator;
+ }
}
diff --git a/src/main/java/com/youlai/boot/common/config/WebMvcConfig.java b/src/main/java/com/youlai/boot/common/config/WebMvcConfig.java
index 864b101a..f653b2b4 100644
--- a/src/main/java/com/youlai/boot/common/config/WebMvcConfig.java
+++ b/src/main/java/com/youlai/boot/common/config/WebMvcConfig.java
@@ -41,5 +41,19 @@ public class WebMvcConfig implements WebMvcConfigurer {
registry.addResourceHandler("/static/screenshot/**")
.addResourceLocations(screenshotLocation)
.setCacheControl(CacheControl.noCache());
+
+ // 应用安装包静态资源映射:/static/packages/{fileName} -> 磁盘 tablet/packages 目录
+ // 安装包内容为内容寻址命名(含 md5),内容变更文件名必变,可安全使用长缓存。
+ String packageLocation = "file:" + FilePath.getPackagePath().replace("\\", "/");
+ registry.addResourceHandler("/static/packages/**")
+ .addResourceLocations(packageLocation)
+ .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic().immutable());
+
+ // 分类图标静态资源映射:/static/category_icon/{fileName} -> 磁盘 tablet/category_icon 目录
+ // 分类图标为手动上传图片,文件名内容寻址(含 md5),变更必变文件名,可安全使用长缓存。
+ String categoryIconLocation = "file:" + FilePath.getCategoryIconPath().replace("\\", "/");
+ registry.addResourceHandler("/static/category_icon/**")
+ .addResourceLocations(categoryIconLocation)
+ .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic().immutable());
}
}
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 1d0dfd4a..dec45643 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -127,7 +127,7 @@ file-storage:
# 上传限制
upload:
# 单文件大小上限(同时作为 spring.servlet.multipart 的上限,单一来源)
- max-file-size: 50MB
+ max-file-size: 500MB
# 允许的文件扩展名白名单(置空表示不限制)
allowed-extensions:
- jpg
diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml
index 4c81dcaf..08f16050 100644
--- a/src/main/resources/application-prod.yml
+++ b/src/main/resources/application-prod.yml
@@ -121,7 +121,7 @@ file-storage:
# 上传限制
upload:
# 单文件大小上限(同时作为 spring.servlet.multipart 的上限,单一来源)
- max-file-size: 50MB
+ max-file-size: 500MB
# 允许的文件扩展名白名单(置空表示不限制)
allowed-extensions:
- jpg