Эх сурвалжийг харах

feat:smsb ota package manager

lihao16 7 сар өмнө
parent
commit
a7a451e930
22 өөрчлөгдсөн 1446 нэмэгдсэн , 8 устгасан
  1. 2 2
      smsb-admin/src/main/resources/application-dev.yml
  2. 1 0
      smsb-admin/src/main/resources/application.yml
  3. 1 1
      smsb-extend/smsb-snailjob-server/src/main/resources/application-dev.yml
  4. 5 0
      smsb-modules/smsb-device/pom.xml
  5. 105 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/controller/SmsbOtaPackageController.java
  6. 114 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/SmsbOtaPackage.java
  7. 111 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/bo/SmsbOtaPackageBo.java
  8. 98 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/SmsbOtaPackageVo.java
  9. 15 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/mapper/SmsbOtaPackageMapper.java
  10. 68 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbOtaPackageService.java
  11. 151 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbOtaPackageServiceImpl.java
  12. 7 0
      smsb-modules/smsb-device/src/main/resources/mapper/device/SmsbOtaPackageMapper.xml
  13. 7 0
      smsb-modules/smsb-system/pom.xml
  14. 20 0
      smsb-modules/smsb-system/src/main/java/org/dromara/system/controller/system/SysOssController.java
  15. 6 1
      smsb-modules/smsb-system/src/main/java/org/dromara/system/domain/SysOss.java
  16. 5 0
      smsb-modules/smsb-system/src/main/java/org/dromara/system/domain/vo/SysOssVo.java
  17. 7 0
      smsb-modules/smsb-system/src/main/java/org/dromara/system/service/ISysOssService.java
  18. 66 4
      smsb-modules/smsb-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java
  19. 63 0
      smsb-plus-ui/src/api/smsb/device/otaPackage.ts
  20. 134 0
      smsb-plus-ui/src/api/smsb/device/otaPackage_type.ts
  21. 229 0
      smsb-plus-ui/src/components/OtaFileUpload/index.vue
  22. 231 0
      smsb-plus-ui/src/views/smsb/otaPackage/index.vue

+ 2 - 2
smsb-admin/src/main/resources/application-dev.yml

@@ -23,7 +23,7 @@ snail-job:
   # token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
   token: "SJ_Wyz3dmsdbDOkDujOTSSoBjGQP1BMsVnj"
   server:
-    host: 127.0.0.1
+    host: 117.73.13.40
     port: 17888
   # 详见 script/sql/snail_job.sql `sj_namespace` 表
   #namespace: ${spring.profiles.active}
@@ -32,7 +32,7 @@ snail-job:
   # port: 2${server.port}
   port: 28080
   # 客户端ip指定
-  host: 127.0.0.1
+  host:
 
 --- # 数据源配置
 spring:

+ 1 - 0
smsb-admin/src/main/resources/application.yml

@@ -140,6 +140,7 @@ tenant:
     - sys_client
     - sys_oss_config
     - smsb_source_tree_rel
+    - smsb_ota_package
 
 # MyBatisPlus配置
 # https://baomidou.com/config/

+ 1 - 1
smsb-extend/smsb-snailjob-server/src/main/resources/application-dev.yml

@@ -2,7 +2,7 @@ spring:
   datasource:
     type: com.zaxxer.hikari.HikariDataSource
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://127.0.0.1:3306/smsb-plus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+    url: jdbc:mysql://117.73.3.135:3306/smsb-plus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
     username: root
     # password: password
     password: Hycpb@123

+ 5 - 0
smsb-modules/smsb-device/pom.xml

@@ -103,6 +103,11 @@
             <artifactId>smsb-common-websocket</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.inspur</groupId>
+            <artifactId>smsb-system</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>

+ 105 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/controller/SmsbOtaPackageController.java

@@ -0,0 +1,105 @@
+package com.inspur.device.controller;
+
+import java.util.List;
+
+import lombok.RequiredArgsConstructor;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.*;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.annotation.Validated;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.excel.utils.ExcelUtil;
+import com.inspur.device.domain.vo.SmsbOtaPackageVo;
+import com.inspur.device.domain.bo.SmsbOtaPackageBo;
+import com.inspur.device.service.ISmsbOtaPackageService;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+
+/**
+ * OTA版本管理
+ *
+ * @author Hao Li
+ * @date 2025-03-19
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/device/otaPackage")
+public class SmsbOtaPackageController extends BaseController {
+
+    private final ISmsbOtaPackageService smsbOtaPackageService;
+
+    /**
+     * 查询OTA版本管理列表
+     */
+    @SaCheckPermission("device:otaPackage:list")
+    @GetMapping("/list")
+    public TableDataInfo<SmsbOtaPackageVo> list(SmsbOtaPackageBo bo, PageQuery pageQuery) {
+        return smsbOtaPackageService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出OTA版本管理列表
+     */
+    @SaCheckPermission("device:otaPackage:export")
+    @Log(title = "OTA版本管理", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(SmsbOtaPackageBo bo, HttpServletResponse response) {
+        List<SmsbOtaPackageVo> list = smsbOtaPackageService.queryList(bo);
+        ExcelUtil.exportExcel(list, "OTA版本管理", SmsbOtaPackageVo.class, response);
+    }
+
+    /**
+     * 获取OTA版本管理详细信息
+     *
+     * @param id 主键
+     */
+    @SaCheckPermission("device:otaPackage:query")
+    @GetMapping("/{id}")
+    public R<SmsbOtaPackageVo> getInfo(@NotNull(message = "主键不能为空")
+                                       @PathVariable Long id) {
+        return R.ok(smsbOtaPackageService.queryById(id));
+    }
+
+    /**
+     * 新增OTA版本管理
+     */
+    @SaCheckPermission("device:otaPackage:add")
+    @Log(title = "OTA版本管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody SmsbOtaPackageBo bo) {
+        return toAjax(smsbOtaPackageService.insertByBo(bo));
+    }
+
+    /**
+     * 修改OTA版本管理
+     */
+    @SaCheckPermission("device:otaPackage:edit")
+    @Log(title = "OTA版本管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody SmsbOtaPackageBo bo) {
+        return toAjax(smsbOtaPackageService.updateByBo(bo));
+    }
+
+    /**
+     * 删除OTA版本管理
+     *
+     * @param ids 主键串
+     */
+    @SaCheckPermission("device:otaPackage:remove")
+    @Log(title = "OTA版本管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable Long[] ids) {
+        return toAjax(smsbOtaPackageService.deleteWithValidByIds(List.of(ids), true));
+    }
+}

+ 114 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/SmsbOtaPackage.java

@@ -0,0 +1,114 @@
+package com.inspur.device.domain;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * OTA版本管理对象 smsb_ota_package
+ *
+ * @author Hao Li
+ * @date 2025-03-19
+ */
+@Data
+@EqualsAndHashCode
+@TableName("smsb_ota_package")
+public class SmsbOtaPackage {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 版本名称
+     */
+    private String versionName;
+
+    /**
+     * 版本号
+     */
+    private String versionCode;
+
+    /**
+     * 文件大小KB
+     */
+    private Long fileSize;
+
+    /**
+     * 文件名称
+     */
+    private String fileName;
+
+    /**
+     * 下载路径
+     */
+    private String fileUrl;
+
+    /**
+     * apk MD5
+     */
+    private String md5;
+
+    /**
+     * 状态 0-禁用 1-启用
+     */
+    private Integer status;
+
+    /**
+     * 创建人
+     */
+    private String createUser;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+
+    /**
+     * 创建者
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /**
+     * 请求参数
+     */
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @TableField(exist = false)
+    private Map<String, Object> params = new HashMap<>();
+
+    /**
+     * 搜索值
+     */
+    @JsonIgnore
+    @TableField(exist = false)
+    private String searchValue;
+
+    private Long ossId;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Integer isDel;
+
+}

+ 111 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/bo/SmsbOtaPackageBo.java

@@ -0,0 +1,111 @@
+package com.inspur.device.domain.bo;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.inspur.device.domain.SmsbOtaPackage;
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * OTA版本管理业务对象 smsb_ota_package
+ *
+ * @author Hao Li
+ * @date 2025-03-19
+ */
+@Data
+@EqualsAndHashCode
+@AutoMapper(target = SmsbOtaPackage.class, reverseConvertGenerate = false)
+public class SmsbOtaPackageBo {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 版本名称
+     */
+    @NotBlank(message = "版本名称不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String versionName;
+
+    /**
+     * 版本号
+     */
+    private String versionCode;
+
+    /**
+     * 文件大小KB
+     */
+    private Long fileSize;
+
+    /**
+     * 文件名称
+     */
+    private String fileName;
+
+    /**
+     * 下载路径
+     */
+    private String fileUrl;
+
+    /**
+     * apk MD5
+     */
+    private String md5;
+
+    /**
+     * 状态 0-禁用 1-启用
+     */
+    private Long status;
+
+    /**
+     * 创建人
+     */
+    private String createUser;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 搜索值
+     */
+    @JsonIgnore
+    @TableField(exist = false)
+    private String searchValue;
+
+    /**
+     * 创建者
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /**
+     * 请求参数
+     */
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    @TableField(exist = false)
+    private Map<String, Object> params = new HashMap<>();
+
+    private Long ossId;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Integer isDel;
+}

+ 98 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/SmsbOtaPackageVo.java

@@ -0,0 +1,98 @@
+package com.inspur.device.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.inspur.device.domain.SmsbOtaPackage;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * OTA版本管理视图对象 smsb_ota_package
+ *
+ * @author Hao Li
+ * @date 2025-03-19
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = SmsbOtaPackage.class)
+public class SmsbOtaPackageVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @ExcelProperty(value = "主键ID")
+    private Long id;
+
+    /**
+     * 版本名称
+     */
+    @ExcelProperty(value = "版本名称")
+    private String versionName;
+
+    /**
+     * 版本号
+     */
+    @ExcelProperty(value = "版本号")
+    private String versionCode;
+
+    /**
+     * 文件大小KB
+     */
+    @ExcelProperty(value = "文件大小KB")
+    private Long fileSize;
+
+    /**
+     * 文件名称
+     */
+    @ExcelProperty(value = "文件名称")
+    private String fileName;
+
+    /**
+     * 下载路径
+     */
+    @ExcelProperty(value = "下载路径")
+    private String fileUrl;
+
+    /**
+     * apk MD5
+     */
+    @ExcelProperty(value = "apk MD5")
+    private String md5;
+
+    /**
+     * 状态 0-禁用 1-启用
+     */
+    @ExcelProperty(value = "状态 0-禁用 1-启用")
+    private Long status;
+
+    /**
+     * 创建时间
+     */
+    @ExcelProperty(value = "创建时间")
+    private Date createTime;
+
+    /**
+     * 创建人
+     */
+    @ExcelProperty(value = "创建人")
+    private String createUser;
+
+    /**
+     * 备注
+     */
+    @ExcelProperty(value = "备注")
+    private String remark;
+
+
+    private Long ossId;
+
+    private Integer isDel;
+}

+ 15 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/mapper/SmsbOtaPackageMapper.java

@@ -0,0 +1,15 @@
+package com.inspur.device.mapper;
+
+import com.inspur.device.domain.SmsbOtaPackage;
+import com.inspur.device.domain.vo.SmsbOtaPackageVo;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+
+/**
+ * OTA版本管理Mapper接口
+ *
+ * @author Hao Li
+ * @date 2025-03-19
+ */
+public interface SmsbOtaPackageMapper extends BaseMapperPlus<SmsbOtaPackage, SmsbOtaPackageVo> {
+
+}

+ 68 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbOtaPackageService.java

@@ -0,0 +1,68 @@
+package com.inspur.device.service;
+
+import com.inspur.device.domain.bo.SmsbOtaPackageBo;
+import com.inspur.device.domain.vo.SmsbOtaPackageVo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * OTA版本管理Service接口
+ *
+ * @author Hao Li
+ * @date 2025-03-19
+ */
+public interface ISmsbOtaPackageService {
+
+    /**
+     * 查询OTA版本管理
+     *
+     * @param id 主键
+     * @return OTA版本管理
+     */
+    SmsbOtaPackageVo queryById(Long id);
+
+    /**
+     * 分页查询OTA版本管理列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return OTA版本管理分页列表
+     */
+    TableDataInfo<SmsbOtaPackageVo> queryPageList(SmsbOtaPackageBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询符合条件的OTA版本管理列表
+     *
+     * @param bo 查询条件
+     * @return OTA版本管理列表
+     */
+    List<SmsbOtaPackageVo> queryList(SmsbOtaPackageBo bo);
+
+    /**
+     * 新增OTA版本管理
+     *
+     * @param bo OTA版本管理
+     * @return 是否新增成功
+     */
+    Boolean insertByBo(SmsbOtaPackageBo bo);
+
+    /**
+     * 修改OTA版本管理
+     *
+     * @param bo OTA版本管理
+     * @return 是否修改成功
+     */
+    Boolean updateByBo(SmsbOtaPackageBo bo);
+
+    /**
+     * 校验并批量删除OTA版本管理信息
+     *
+     * @param ids     待删除的主键集合
+     * @param isValid 是否进行有效性校验
+     * @return 是否删除成功
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+}

+ 151 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbOtaPackageServiceImpl.java

@@ -0,0 +1,151 @@
+package com.inspur.device.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.inspur.device.domain.SmsbOtaPackage;
+import com.inspur.device.domain.bo.SmsbOtaPackageBo;
+import com.inspur.device.domain.vo.SmsbOtaPackageVo;
+import com.inspur.device.mapper.SmsbOtaPackageMapper;
+import com.inspur.device.service.ISmsbOtaPackageService;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.domain.vo.SysOssVo;
+import org.dromara.system.mapper.SysOssMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * OTA版本管理Service业务层处理
+ *
+ * @author Hao Li
+ * @date 2025-03-19
+ */
+@RequiredArgsConstructor
+@Service
+public class SmsbOtaPackageServiceImpl implements ISmsbOtaPackageService {
+
+    private final SmsbOtaPackageMapper baseMapper;
+
+    @Autowired
+    private SysOssMapper sysOssMapper;
+
+    /**
+     * 查询OTA版本管理
+     *
+     * @param id 主键
+     * @return OTA版本管理
+     */
+    @Override
+    public SmsbOtaPackageVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+
+    /**
+     * 分页查询OTA版本管理列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return OTA版本管理分页列表
+     */
+    @Override
+    public TableDataInfo<SmsbOtaPackageVo> queryPageList(SmsbOtaPackageBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<SmsbOtaPackage> lqw = buildQueryWrapper(bo);
+        Page<SmsbOtaPackageVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询符合条件的OTA版本管理列表
+     *
+     * @param bo 查询条件
+     * @return OTA版本管理列表
+     */
+    @Override
+    public List<SmsbOtaPackageVo> queryList(SmsbOtaPackageBo bo) {
+        LambdaQueryWrapper<SmsbOtaPackage> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<SmsbOtaPackage> buildQueryWrapper(SmsbOtaPackageBo bo) {
+        Map<String, Object> params = bo.getParams();
+        LambdaQueryWrapper<SmsbOtaPackage> lqw = Wrappers.lambdaQuery();
+        lqw.like(StringUtils.isNotBlank(bo.getVersionName()), SmsbOtaPackage::getVersionName, bo.getVersionName());
+        lqw.eq(StringUtils.isNotBlank(bo.getVersionCode()), SmsbOtaPackage::getVersionCode, bo.getVersionCode());
+        lqw.like(StringUtils.isNotBlank(bo.getCreateUser()), SmsbOtaPackage::getCreateUser, bo.getCreateUser());
+        lqw.eq(SmsbOtaPackage::getIsDel, 0);
+        return lqw;
+    }
+
+    /**
+     * 新增OTA版本管理
+     *
+     * @param bo OTA版本管理
+     * @return 是否新增成功
+     */
+    @Override
+    public Boolean insertByBo(SmsbOtaPackageBo bo) {
+        SmsbOtaPackage add = MapstructUtils.convert(bo, SmsbOtaPackage.class);
+
+        // sys oss
+        SysOssVo sysOssVo = sysOssMapper.selectVoById(bo.getOssId());
+        add.setVersionCode(sysOssVo.getVersionCode());
+        add.setFileSize(sysOssVo.getFileSize().longValue());
+        add.setFileName(sysOssVo.getOriginalName());
+        add.setFileUrl(sysOssVo.getUrl());
+        add.setMd5(sysOssVo.getFileMd5());
+        add.setStatus(1);
+        add.setCreateUser(LoginHelper.getUsername());
+        add.setOssId(bo.getOssId().longValue());
+        add.setIsDel(0);
+
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setId(add.getId());
+        }
+        return flag;
+    }
+
+    /**
+     * 修改OTA版本管理
+     *
+     * @param bo OTA版本管理
+     * @return 是否修改成功
+     */
+    @Override
+    public Boolean updateByBo(SmsbOtaPackageBo bo) {
+        SmsbOtaPackage update = MapstructUtils.convert(bo, SmsbOtaPackage.class);
+        validEntityBeforeSave(update);
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(SmsbOtaPackage entity) {
+        //TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 校验并批量删除OTA版本管理信息
+     *
+     * @param ids     待删除的主键集合
+     * @param isValid 是否进行有效性校验
+     * @return 是否删除成功
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
+        if (isValid) {
+            //TODO 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+}

+ 7 - 0
smsb-modules/smsb-device/src/main/resources/mapper/device/SmsbOtaPackageMapper.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.inspur.device.mapper.SmsbOtaPackageMapper">
+
+</mapper>

+ 7 - 0
smsb-modules/smsb-system/pom.xml

@@ -100,6 +100,13 @@
             <artifactId>smsb-common-sse</artifactId>
         </dependency>
 
+        <!-- apk解析 -->
+        <dependency>
+            <groupId>net.dongliu</groupId>
+            <artifactId>apk-parser</artifactId>
+            <version>2.5.3</version>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 20 - 0
smsb-modules/smsb-system/src/main/java/org/dromara/system/controller/system/SysOssController.java

@@ -103,6 +103,26 @@ public class SysOssController extends BaseController {
         return R.ok(uploadVo);
     }
 
+    /**
+     * 上传OSS OTA 文件
+     *
+     * @param file 文件
+     */
+    @Log(title = "OSS对象存储", businessType = BusinessType.INSERT)
+    @PostMapping(value = "/upload/ota", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<SysOssUploadVo> uploadOta(@RequestPart("file") MultipartFile file) {
+        if (ObjectUtil.isNull(file)) {
+            return R.fail("上传文件不能为空");
+        }
+        SysOssVo oss = ossService.uploadOta(file);
+
+        SysOssUploadVo uploadVo = new SysOssUploadVo();
+        uploadVo.setUrl(oss.getUrl());
+        uploadVo.setFileName(oss.getOriginalName());
+        uploadVo.setOssId(oss.getOssId().toString());
+        return R.ok(uploadVo);
+    }
+
     /**
      * 下载OSS对象
      *

+ 6 - 1
smsb-modules/smsb-system/src/main/java/org/dromara/system/domain/SysOss.java

@@ -52,7 +52,7 @@ public class SysOss extends TenantEntity {
     private Integer fileSize;
 
     /**
-     * 1图片 2视频 3音频
+     * 1图片 2视频 3音频 10-apk
      */
     private Integer fileType;
 
@@ -71,4 +71,9 @@ public class SysOss extends TenantEntity {
      */
     private Integer codeRate;
 
+    /**
+     * apk version code
+     */
+    private String versionCode;
+
 }

+ 5 - 0
smsb-modules/smsb-system/src/main/java/org/dromara/system/domain/vo/SysOssVo.java

@@ -92,5 +92,10 @@ public class SysOssVo implements Serializable {
      */
     private Integer codeRate;
 
+    /**
+     * apk version code
+     */
+    private String versionCode;
+
 
 }

+ 7 - 0
smsb-modules/smsb-system/src/main/java/org/dromara/system/service/ISysOssService.java

@@ -83,4 +83,11 @@ public interface ISysOssService {
      * @return
      */
     SysOssVo uploadSmsb(MultipartFile file);
+
+    /**
+     * smsb upload ota file
+     * @param file
+     * @return
+     */
+    SysOssVo uploadOta(MultipartFile file);
 }

+ 66 - 4
smsb-modules/smsb-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java

@@ -8,6 +8,9 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.dongliu.apk.parser.ApkFile;
+import net.dongliu.apk.parser.bean.ApkMeta;
 import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.domain.dto.OssDTO;
 import org.dromara.common.core.exception.ServiceException;
@@ -52,10 +55,13 @@ import java.util.Map;
  */
 @RequiredArgsConstructor
 @Service
+@Slf4j
 public class SysOssServiceImpl implements ISysOssService, OssService {
 
     private final SysOssMapper baseMapper;
 
+    private static final String APK_FILE_SUFFIX = ".apk";
+
     /**
      * 查询OSS对象存储列表
      *
@@ -236,6 +242,51 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
         return buildResultEntityV2(originalfileName, suffix, storage.getConfigKey(), uploadResult, md5, file);
     }
 
+    @Override
+    public SysOssVo uploadOta(MultipartFile file) {
+        // 计算文件的MD5
+        String md5 = getFileMD5(file);
+
+        String originalfileName = file.getOriginalFilename();
+        String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
+        OssClient storage = OssFactory.instance();
+        UploadResult uploadResult;
+        try {
+            uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
+        } catch (IOException e) {
+            throw new ServiceException(e.getMessage());
+        }
+        // 保存文件信息
+        return buildResultEntityV2(originalfileName, suffix, storage.getConfigKey(), uploadResult, md5, file);
+    }
+
+    private String getVersionCode(MultipartFile file) {
+        // 将 MultipartFile 转为临时文件
+        File tempFile = convertMultipartFileToFile(file);
+        //注释:apk所有信息都在apkMeta类里面。可以输出整个apkMeta来查看跟多详情信息
+        ApkFile apkFile = null;
+        if (null != tempFile && tempFile.exists() && tempFile.isFile()) {
+            try {
+                apkFile = new ApkFile(tempFile);
+                ApkMeta apkMeta = apkFile.getApkMeta();
+                return apkMeta.getVersionCode().toString();
+            } catch (IOException e) {
+                log.error("get apk file version code exception!");
+            } finally {
+                try {
+                    if (null != apkFile) {
+                        // 注意,此处一定要close,否则进行IO流操作时无法删除当前file
+                        apkFile.close();
+                    }
+                    tempFile.delete();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return null;
+    }
+
     private String getFileMD5(MultipartFile file) {
         try (InputStream inputStream = file.getInputStream()) {
             MessageDigest md = MessageDigest.getInstance("MD5");
@@ -276,15 +327,26 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
     private void getFileInfo(SysOss oss, MultipartFile file) {
         // 文件大小
         oss.setFileSize((int) (file.getSize() / 1024));
-        // 文件类型
-        Integer fileType = isImage(file) ? 1 : 2;
-        oss.setFileType(fileType);
         // 分辨率
         String resolution = "0x0";
         // 视频时长
         Integer duration = 0;
         // 视频码率
         Integer codeRate = 0;
+        // apk 文件
+        // 获取APK的version code
+        if (oss.getFileSuffix().equals(APK_FILE_SUFFIX)) {
+            String versionCode = getVersionCode(file);
+            oss.setVersionCode(versionCode);
+            oss.setResolution(resolution);
+            oss.setCodeRate(codeRate);
+            oss.setDuration(duration);
+            oss.setFileType(10);
+            return;
+        }
+        // 文件类型
+        Integer fileType = isImage(file) ? 1 : 2;
+        oss.setFileType(fileType);
         if (fileType == 1) {
             resolution = getImageResolution(file);
         }
@@ -300,6 +362,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
         oss.setResolution(resolution);
         oss.setCodeRate(codeRate);
         oss.setDuration(duration);
+
     }
 
     /**
@@ -314,7 +377,6 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
         if (tempFile == null) {
             return null;
         }
-
         try {
             // 执行 ffprobe 命令
             ProcessBuilder pb = new ProcessBuilder(

+ 63 - 0
smsb-plus-ui/src/api/smsb/device/otaPackage.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { OtaPackageVO, OtaPackageForm, OtaPackageQuery } from '@/api/smsb/device/otaPackage_type';
+
+/**
+ * 查询OTA版本管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listOtaPackage = (query?: OtaPackageQuery): AxiosPromise<OtaPackageVO[]> => {
+  return request({
+    url: '/device/otaPackage/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询OTA版本管理详细
+ * @param id
+ */
+export const getOtaPackage = (id: string | number): AxiosPromise<OtaPackageVO> => {
+  return request({
+    url: '/device/otaPackage/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增OTA版本管理
+ * @param data
+ */
+export const addOtaPackage = (data: OtaPackageForm) => {
+  return request({
+    url: '/device/otaPackage',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改OTA版本管理
+ * @param data
+ */
+export const updateOtaPackage = (data: OtaPackageForm) => {
+  return request({
+    url: '/device/otaPackage',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除OTA版本管理
+ * @param id
+ */
+export const delOtaPackage = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/device/otaPackage/' + id,
+    method: 'delete'
+  });
+};

+ 134 - 0
smsb-plus-ui/src/api/smsb/device/otaPackage_type.ts

@@ -0,0 +1,134 @@
+export interface OtaPackageVO {
+  /**
+   * 主键ID
+   */
+  id: string | number;
+
+  /**
+   * 版本名称
+   */
+  versionName: string;
+
+  /**
+   * 版本号
+   */
+  versionCode: string;
+
+  /**
+   * 文件大小KB
+   */
+  fileSize: number;
+
+  /**
+   * 文件名称
+   */
+  fileName: string;
+
+  /**
+   * 下载路径
+   */
+  fileUrl: string;
+
+  /**
+   * apk MD5
+   */
+  md5: string;
+
+  /**
+   * 状态 0-禁用 1-启用
+   */
+  status: number;
+
+  /**
+   * 创建时间
+   */
+  createTime: string;
+
+  /**
+   * 创建人
+   */
+  createUser: string;
+
+  /**
+   * 备注
+   */
+  remark: string;
+
+  ossId: number;
+}
+
+export interface OtaPackageForm extends BaseEntity {
+  /**
+   * 主键ID
+   */
+  id?: string | number;
+
+  /**
+   * 版本名称
+   */
+  versionName?: string;
+
+  /**
+   * 版本号
+   */
+  versionCode?: string;
+
+  /**
+   * 文件大小KB
+   */
+  fileSize?: number;
+
+  /**
+   * 文件名称
+   */
+  fileName?: string;
+
+  /**
+   * 下载路径
+   */
+  fileUrl?: string;
+
+  /**
+   * apk MD5
+   */
+  md5?: string;
+
+  /**
+   * 状态 0-禁用 1-启用
+   */
+  status?: number;
+
+  /**
+   * 创建人
+   */
+  createUser?: string;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+
+  ossId?: undefined | string;
+}
+
+export interface OtaPackageQuery extends PageQuery {
+  /**
+   * 版本名称
+   */
+  versionName?: string;
+
+  /**
+   * 版本号
+   */
+  versionCode?: string;
+
+  /**
+   * 创建人
+   */
+  createUser?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}

+ 229 - 0
smsb-plus-ui/src/components/OtaFileUpload/index.vue

@@ -0,0 +1,229 @@
+<template>
+  <div class="upload-file">
+    <el-upload
+      ref="fileUploadRef"
+      multiple
+      :action="uploadFileUrl"
+      :before-upload="handleBeforeUpload"
+      :file-list="fileList"
+      :limit="limit"
+      :on-error="handleUploadError"
+      :on-exceed="handleExceed"
+      :on-success="handleUploadSuccess"
+      :show-file-list="false"
+      :headers="headers"
+      class="upload-file-uploader"
+    >
+      <!-- 上传按钮 -->
+      <el-button type="primary">选取文件</el-button>
+    </el-upload>
+    <!-- 上传提示 -->
+    <div v-if="showTip" class="el-upload__tip">
+      请上传
+      <template v-if="fileSize">
+        大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
+      </template>
+      <template v-if="fileType">
+        格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
+      </template>
+      的文件
+    </div>
+    <!-- 文件列表 -->
+    <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
+      <li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
+        <el-link :href="`${file.url}`" :underline="false" target="_blank">
+          <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
+        </el-link>
+        <div class="ele-upload-list__item-content-action">
+          <el-button type="danger" link @click="handleDelete(index)">删除</el-button>
+        </div>
+      </li>
+    </transition-group>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes';
+import { delOss, listByIds } from '@/api/system/oss';
+import { globalHeaders } from '@/utils/request';
+
+const props = defineProps({
+  modelValue: {
+    type: [String, Object, Array],
+    default: () => []
+  },
+  // 数量限制
+  limit: propTypes.number.def(1),
+  // 大小限制(MB)
+  fileSize: propTypes.number.def(500),
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
+  fileType: propTypes.array.def(['apk']),
+  // 是否显示提示
+  isShowTip: propTypes.bool.def(true)
+});
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const emit = defineEmits(['update:modelValue']);
+const number = ref(0);
+const uploadList = ref<any[]>([]);
+
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const uploadFileUrl = ref(baseUrl + '/resource/oss/upload/ota'); // 上传文件服务器地址
+const headers = ref(globalHeaders());
+
+const fileList = ref<any[]>([]);
+const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
+
+const fileUploadRef = ref<ElUploadInstance>();
+
+watch(
+  () => props.modelValue,
+  async (val) => {
+    if (val) {
+      let temp = 1;
+      // 首先将值转为数组
+      let list: any[] = [];
+      if (Array.isArray(val)) {
+        list = val;
+      } else {
+        const res = await listByIds(val);
+        list = res.data.map((oss) => {
+          return {
+            name: oss.originalName,
+            url: oss.url,
+            ossId: oss.ossId
+          };
+        });
+      }
+      // 然后将数组转为对象数组
+      fileList.value = list.map((item) => {
+        item = { name: item.name, url: item.url, ossId: item.ossId };
+        item.uid = item.uid || new Date().getTime() + temp++;
+        return item;
+      });
+    } else {
+      fileList.value = [];
+      return [];
+    }
+  },
+  { deep: true, immediate: true }
+);
+
+// 上传前校检格式和大小
+const handleBeforeUpload = (file: any) => {
+  // 校检文件类型
+  if (props.fileType.length) {
+    const fileName = file.name.split('.');
+    const fileExt = fileName[fileName.length - 1];
+    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
+    if (!isTypeOk) {
+      proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
+      return false;
+    }
+  }
+  // 校检文件大小
+  if (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize;
+    if (!isLt) {
+      proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
+      return false;
+    }
+  }
+  proxy?.$modal.loading('正在上传文件,请稍候...');
+  number.value++;
+  return true;
+};
+
+// 文件个数超出
+const handleExceed = () => {
+  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
+};
+
+// 上传失败
+const handleUploadError = () => {
+  proxy?.$modal.msgError('上传文件失败');
+};
+
+// 上传成功回调
+const handleUploadSuccess = (res: any, file: UploadFile) => {
+  if (res.code === 200) {
+    uploadList.value.push({
+      name: res.data.fileName,
+      url: res.data.url,
+      ossId: res.data.ossId
+    });
+    uploadedSuccessfully();
+  } else {
+    number.value--;
+    proxy?.$modal.closeLoading();
+    proxy?.$modal.msgError(res.msg);
+    fileUploadRef.value?.handleRemove(file);
+    uploadedSuccessfully();
+  }
+};
+
+// 删除文件
+const handleDelete = (index: number) => {
+  let ossId = fileList.value[index].ossId;
+  delOss(ossId);
+  fileList.value.splice(index, 1);
+  emit('update:modelValue', listToString(fileList.value));
+};
+
+// 上传结束处理
+const uploadedSuccessfully = () => {
+  if (number.value > 0 && uploadList.value.length === number.value) {
+    fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
+    uploadList.value = [];
+    number.value = 0;
+    emit('update:modelValue', listToString(fileList.value));
+    proxy?.$modal.closeLoading();
+  }
+};
+
+// 获取文件名称
+const getFileName = (name: string) => {
+  // 如果是url那么取最后的名字 如果不是直接返回
+  if (name.lastIndexOf('/') > -1) {
+    return name.slice(name.lastIndexOf('/') + 1);
+  } else {
+    return name;
+  }
+};
+
+// 对象转成指定字符串分隔
+const listToString = (list: any[], separator?: string) => {
+  let strs = '';
+  separator = separator || ',';
+  list.forEach((item) => {
+    if (item.ossId) {
+      strs += item.ossId + separator;
+    }
+  });
+  return strs != '' ? strs.substring(0, strs.length - 1) : '';
+};
+</script>
+
+<style scoped lang="scss">
+.upload-file-uploader {
+  margin-bottom: 5px;
+}
+
+.upload-file-list .el-upload-list__item {
+  border: 1px solid #e4e7ed;
+  line-height: 2;
+  margin-bottom: 10px;
+  position: relative;
+}
+
+.upload-file-list .ele-upload-list__item-content {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  color: inherit;
+}
+
+.ele-upload-list__item-content-action .el-link {
+  margin-right: 10px;
+}
+</style>

+ 231 - 0
smsb-plus-ui/src/views/smsb/otaPackage/index.vue

@@ -0,0 +1,231 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover" :style="{ height: '60px' }">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="版本名称" prop="versionName">
+              <el-input v-model="queryParams.versionName" placeholder="请输入版本名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="版本号" prop="versionCode">
+              <el-input v-model="queryParams.versionCode" placeholder="请输入版本号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="创建人" prop="createUser">
+              <el-input v-model="queryParams.createUser" placeholder="请输入创建人" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+              <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['device:otaPackage:add']"> 新增 </el-button>
+              <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['device:otaPackage:remove']"
+                >删除
+              </el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <el-table v-loading="loading" :data="otaPackageList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="主键ID" align="left" prop="id" v-if="true" width="180" />
+        <el-table-column label="版本名称" align="center" prop="versionName" width="100" />
+        <el-table-column label="版本号" align="left" prop="versionCode" width="100" :show-overflow-tooltip="true" />
+        <el-table-column label="文件大小" align="left" prop="fileSize" width="100" :show-overflow-tooltip="true" />
+        <el-table-column label="文件名称" align="left" prop="fileName" width="200" :show-overflow-tooltip="true" />
+        <el-table-column label="下载路径" align="left" prop="fileUrl" :show-overflow-tooltip="true" width="200" />
+        <el-table-column label="MD5" align="left" prop="md5" width="250" :show-overflow-tooltip="true" />
+        <el-table-column label="状态" align="center" prop="status" width="80" />
+        <el-table-column label="创建时间" align="left" prop="createTime" width="160" />
+        <el-table-column label="创建人" align="center" prop="createUser" width="80" :show-overflow-tooltip="true" />
+        <el-table-column label="备注" align="left" prop="remark" :show-overflow-tooltip="true" />
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
+          <template #default="scope">
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['device:otaPackage:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改OTA版本管理对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+      <el-form ref="otaPackageFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="版本名称" prop="versionName">
+          <el-input v-model="form.versionName" placeholder="请输入版本名称" />
+        </el-form-item>
+
+        <el-form-item label="升级文件" prop="ossId">
+          <ota-file-upload v-model="form.ossId" />
+        </el-form-item>
+
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="4" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="OtaPackage" lang="ts">
+import { listOtaPackage, getOtaPackage, delOtaPackage, addOtaPackage, updateOtaPackage } from '@/api/smsb/device/otaPackage';
+import { OtaPackageVO, OtaPackageQuery, OtaPackageForm } from '@/api/smsb/device/otaPackage_type';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const otaPackageList = ref<OtaPackageVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const otaPackageFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: OtaPackageForm = {
+  id: undefined,
+  versionName: undefined,
+  versionCode: undefined,
+  fileSize: undefined,
+  fileName: undefined,
+  fileUrl: undefined,
+  md5: undefined,
+  status: undefined,
+  createUser: undefined,
+  remark: undefined
+};
+const data = reactive<PageData<OtaPackageForm, OtaPackageQuery>>({
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    versionName: undefined,
+    versionCode: undefined,
+    createUser: undefined,
+    params: {}
+  },
+  rules: {
+    versionName: [{ required: true, message: '版本名称不能为空', trigger: 'blur' }]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询OTA版本管理列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listOtaPackage(queryParams.value);
+  otaPackageList.value = res.rows;
+  otaPackageList.value.forEach((item) => {
+    item.fileSize = parseFloat(item.fileSize / 1024).toFixed(3) + 'MB';
+  });
+  total.value = res.total;
+  loading.value = false;
+};
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+};
+
+/** 表单重置 */
+const reset = () => {
+  form.value = { ...initFormData };
+  otaPackageFormRef.value?.resetFields();
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: OtaPackageVO[]) => {
+  ids.value = selection.map((item) => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = '添加OTA版本管理';
+};
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: OtaPackageVO) => {
+  reset();
+  const _id = row?.id || ids.value[0];
+  const res = await getOtaPackage(_id);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = '修改OTA版本管理';
+};
+
+/** 提交按钮 */
+const submitForm = () => {
+  otaPackageFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateOtaPackage(form.value).finally(() => (buttonLoading.value = false));
+      } else {
+        await addOtaPackage(form.value).finally(() => (buttonLoading.value = false));
+      }
+      proxy?.$modal.msgSuccess('操作成功');
+      dialog.visible = false;
+      await getList();
+    }
+  });
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: OtaPackageVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除OTA版本管理编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
+  await delOtaPackage(_ids);
+  proxy?.$modal.msgSuccess('删除成功');
+  await getList();
+};
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download(
+    'device/otaPackage/export',
+    {
+      ...queryParams.value
+    },
+    `otaPackage_${new Date().getTime()}.xlsx`
+  );
+};
+
+onMounted(() => {
+  getList();
+});
+</script>