Browse Source

ly app版本管理

ly 2 weeks ago
parent
commit
052cac7223

+ 3 - 1
master/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java

@@ -36,7 +36,9 @@ public class MimeTypeUtils
             //视频
             "mp4","swf","flv","avi","rm","3gp","mkv",
             //"图纸"
-            "dwg"
+            "dwg",
+            // APP安装包
+            "apk", "wgt"
     };
 
     public static String getExtension(String prefix)

+ 1 - 0
master/src/main/java/com/ruoyi/framework/config/SecurityConfig.java

@@ -136,6 +136,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/safecheck/weekcheck/downloadWeekcheck").anonymous()
                 .antMatchers("/safecheck/weekcheck/downloadWeekcheckPdf").anonymous()
                 .antMatchers("/production/quality/export").anonymous()
+                .antMatchers("/system/appVersion/check").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证
                 .anyRequest().authenticated()
                 .and()

+ 202 - 0
master/src/main/java/com/ruoyi/project/system/controller/SysAppVersionController.java

@@ -0,0 +1,202 @@
+package com.ruoyi.project.system.controller;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import javax.servlet.http.HttpServletRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.framework.aspectj.lang.annotation.Log;
+import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
+import com.ruoyi.framework.web.controller.BaseController;
+import com.ruoyi.framework.web.domain.AjaxResult;
+import com.ruoyi.framework.web.page.TableDataInfo;
+import com.ruoyi.project.system.domain.SysAppVersion;
+import com.ruoyi.project.system.service.ISysAppVersionService;
+
+/**
+ * APP版本管理 信息操作处理
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/appVersion")
+public class SysAppVersionController extends BaseController
+{
+    private static final Logger log = LoggerFactory.getLogger(SysAppVersionController.class);
+
+    /** 支持的平台类型 */
+    private static final String[] SUPPORTED_PLATFORMS = {"android", "ios", "all"};
+
+    /** 版本号正则:支持 x.x.x 格式 */
+    private static final String VERSION_PATTERN = "^\\d+(\\.\\d+){0,2}$";
+
+    @Autowired
+    private ISysAppVersionService appVersionService;
+
+    /**
+     * 检查APP版本更新(供APP端调用,无需登录)
+     */
+    @GetMapping("/check")
+    public AjaxResult checkVersion(
+            @RequestParam(value = "version", required = false) String version,
+            @RequestParam(value = "appVersion", required = false) String appVersion,
+            @RequestParam(value = "platform", required = false) String platform,
+            HttpServletRequest request)
+    {
+        // 生成请求追踪ID,便于日志排查
+        String traceId = UUID.randomUUID().toString().substring(0, 8);
+        String clientIp = getClientIp(request);
+        
+        log.info("[{}] APP版本检查请求 - IP:{}, version:{}, appVersion:{}, platform:{}", 
+                traceId, clientIp, version, appVersion, platform);
+        
+        try {
+            // 参数校验
+            if (version == null || version.trim().isEmpty()) {
+                log.warn("[{}] 参数校验失败: version为空", traceId);
+                return AjaxResult.error("参数错误: version不能为空");
+            }
+            if (platform == null || platform.trim().isEmpty()) {
+                log.warn("[{}] 参数校验失败: platform为空", traceId);
+                return AjaxResult.error("参数错误: platform不能为空");
+            }
+            
+            // 版本号格式校验
+            version = version.trim();
+            if (!version.matches(VERSION_PATTERN)) {
+                log.warn("[{}] 参数校验失败: version格式错误 - {}", traceId, version);
+                return AjaxResult.error("参数错误: version格式应为x.x.x");
+            }
+            
+            // 平台校验
+            platform = platform.trim().toLowerCase();
+            if (!isSupportedPlatform(platform)) {
+                log.warn("[{}] 参数校验失败: platform不支持 - {}", traceId, platform);
+                return AjaxResult.error("参数错误: platform仅支持android/ios");
+            }
+            
+            // appVersion 格式校验(可选参数)
+            if (appVersion != null && !appVersion.trim().isEmpty()) {
+                appVersion = appVersion.trim();
+                if (!appVersion.matches(VERSION_PATTERN)) {
+                    log.warn("[{}] 参数校验失败: appVersion格式错误 - {}", traceId, appVersion);
+                    return AjaxResult.error("参数错误: appVersion格式应为x.x.x");
+                }
+            }
+            
+            // 执行版本检查
+            Map<String, Object> result = appVersionService.checkVersion(version, appVersion, platform);
+            
+            boolean needUpdate = (boolean) result.getOrDefault("needUpdate", false);
+            log.info("[{}] APP版本检查完成 - needUpdate:{}, latestVersion:{}", 
+                    traceId, needUpdate, result.get("version"));
+            
+            return AjaxResult.success(result);
+            
+        } catch (Exception e) {
+            log.error("[{}] APP版本检查异常: {}", traceId, e.getMessage(), e);
+            return AjaxResult.error("服务异常,请稍后重试");
+        }
+    }
+    
+    /**
+     * 判断是否为支持的平台
+     */
+    private boolean isSupportedPlatform(String platform) {
+        for (String p : SUPPORTED_PLATFORMS) {
+            if (p.equals(platform)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * 获取客户端IP
+     */
+    private String getClientIp(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("X-Real-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        // 多个代理时取第一个IP
+        if (ip != null && ip.contains(",")) {
+            ip = ip.split(",")[0].trim();
+        }
+        return ip;
+    }
+
+    /**
+     * 获取APP版本列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:appVersion:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysAppVersion appVersion)
+    {
+        startPage();
+        List<SysAppVersion> list = appVersionService.selectAppVersionList(appVersion);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据版本ID获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:appVersion:query')")
+    @GetMapping(value = "/{versionId}")
+    public AjaxResult getInfo(@PathVariable Long versionId)
+    {
+        return AjaxResult.success(appVersionService.selectAppVersionById(versionId));
+    }
+
+    /**
+     * 新增APP版本
+     */
+    @PreAuthorize("@ss.hasPermi('system:appVersion:add')")
+    @Log(title = "APP版本管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysAppVersion appVersion)
+    {
+        appVersion.setCreateBy(SecurityUtils.getUsername());
+        return toAjax(appVersionService.insertAppVersion(appVersion));
+    }
+
+    /**
+     * 修改APP版本
+     */
+    @PreAuthorize("@ss.hasPermi('system:appVersion:edit')")
+    @Log(title = "APP版本管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysAppVersion appVersion)
+    {
+        appVersion.setUpdateBy(SecurityUtils.getUsername());
+        return toAjax(appVersionService.updateAppVersion(appVersion));
+    }
+
+    /**
+     * 删除APP版本
+     */
+    @PreAuthorize("@ss.hasPermi('system:appVersion:remove')")
+    @Log(title = "APP版本管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{versionIds}")
+    public AjaxResult remove(@PathVariable Long[] versionIds)
+    {
+        return toAjax(appVersionService.deleteAppVersionByIds(versionIds));
+    }
+}

+ 177 - 0
master/src/main/java/com/ruoyi/project/system/domain/SysAppVersion.java

@@ -0,0 +1,177 @@
+package com.ruoyi.project.system.domain;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.framework.web.domain.BaseEntity;
+
+/**
+ * APP版本管理表 sys_app_version
+ *
+ * @author ruoyi
+ */
+public class SysAppVersion extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 版本ID */
+    private Long versionId;
+
+    /** 版本号(如 2.6.0) */
+    private String version;
+
+    /** 原生包版本号(如 1.0.0) */
+    private String appVersion;
+
+    /** 平台(android/ios/all) */
+    private String platform;
+
+    /** 更新类型(wgt热更新/apk整包更新) */
+    private String updateType;
+
+    /** 更新标题 */
+    private String title;
+
+    /** 更新内容说明 */
+    private String content;
+
+    /** 下载地址 */
+    private String downloadUrl;
+
+    /** 是否强制更新(0否 1是) */
+    private String forceUpdate;
+
+    /** 状态(0正常 1停用) */
+    private String status;
+
+    public Long getVersionId()
+    {
+        return versionId;
+    }
+
+    public void setVersionId(Long versionId)
+    {
+        this.versionId = versionId;
+    }
+
+    @NotBlank(message = "版本号不能为空")
+    @Size(min = 0, max = 50, message = "版本号长度不能超过50个字符")
+    public String getVersion()
+    {
+        return version;
+    }
+
+    public void setVersion(String version)
+    {
+        this.version = version;
+    }
+
+    @Size(min = 0, max = 50, message = "原生包版本号长度不能超过50个字符")
+    public String getAppVersion()
+    {
+        return appVersion;
+    }
+
+    public void setAppVersion(String appVersion)
+    {
+        this.appVersion = appVersion;
+    }
+
+    @NotBlank(message = "平台不能为空")
+    public String getPlatform()
+    {
+        return platform;
+    }
+
+    public void setPlatform(String platform)
+    {
+        this.platform = platform;
+    }
+
+    @NotBlank(message = "更新类型不能为空")
+    public String getUpdateType()
+    {
+        return updateType;
+    }
+
+    public void setUpdateType(String updateType)
+    {
+        this.updateType = updateType;
+    }
+
+    @Size(min = 0, max = 200, message = "更新标题长度不能超过200个字符")
+    public String getTitle()
+    {
+        return title;
+    }
+
+    public void setTitle(String title)
+    {
+        this.title = title;
+    }
+
+    @Size(min = 0, max = 2000, message = "更新内容长度不能超过2000个字符")
+    public String getContent()
+    {
+        return content;
+    }
+
+    public void setContent(String content)
+    {
+        this.content = content;
+    }
+
+    @NotBlank(message = "下载地址不能为空")
+    @Size(min = 0, max = 500, message = "下载地址长度不能超过500个字符")
+    public String getDownloadUrl()
+    {
+        return downloadUrl;
+    }
+
+    public void setDownloadUrl(String downloadUrl)
+    {
+        this.downloadUrl = downloadUrl;
+    }
+
+    public String getForceUpdate()
+    {
+        return forceUpdate;
+    }
+
+    public void setForceUpdate(String forceUpdate)
+    {
+        this.forceUpdate = forceUpdate;
+    }
+
+    public String getStatus()
+    {
+        return status;
+    }
+
+    public void setStatus(String status)
+    {
+        this.status = status;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("versionId", getVersionId())
+            .append("version", getVersion())
+            .append("appVersion", getAppVersion())
+            .append("platform", getPlatform())
+            .append("updateType", getUpdateType())
+            .append("title", getTitle())
+            .append("content", getContent())
+            .append("downloadUrl", getDownloadUrl())
+            .append("forceUpdate", getForceUpdate())
+            .append("status", getStatus())
+            .append("createBy", getCreateBy())
+            .append("createTime", getCreateTime())
+            .append("updateBy", getUpdateBy())
+            .append("updateTime", getUpdateTime())
+            .append("remark", getRemark())
+            .toString();
+    }
+}

+ 68 - 0
master/src/main/java/com/ruoyi/project/system/mapper/SysAppVersionMapper.java

@@ -0,0 +1,68 @@
+package com.ruoyi.project.system.mapper;
+
+import java.util.List;
+import com.ruoyi.project.system.domain.SysAppVersion;
+
+/**
+ * APP版本管理 数据层
+ *
+ * @author ruoyi
+ */
+public interface SysAppVersionMapper
+{
+    /**
+     * 查询APP版本信息
+     *
+     * @param versionId 版本ID
+     * @return APP版本信息
+     */
+    public SysAppVersion selectAppVersionById(Long versionId);
+
+    /**
+     * 查询APP版本列表
+     *
+     * @param appVersion APP版本信息
+     * @return APP版本集合
+     */
+    public List<SysAppVersion> selectAppVersionList(SysAppVersion appVersion);
+
+    /**
+     * 查询最新版本(根据平台)
+     *
+     * @param platform 平台
+     * @return APP版本信息
+     */
+    public SysAppVersion selectLatestVersion(String platform);
+
+    /**
+     * 新增APP版本
+     *
+     * @param appVersion APP版本信息
+     * @return 结果
+     */
+    public int insertAppVersion(SysAppVersion appVersion);
+
+    /**
+     * 修改APP版本
+     *
+     * @param appVersion APP版本信息
+     * @return 结果
+     */
+    public int updateAppVersion(SysAppVersion appVersion);
+
+    /**
+     * 删除APP版本
+     *
+     * @param versionId 版本ID
+     * @return 结果
+     */
+    public int deleteAppVersionById(Long versionId);
+
+    /**
+     * 批量删除APP版本
+     *
+     * @param versionIds 需要删除的版本ID
+     * @return 结果
+     */
+    public int deleteAppVersionByIds(Long[] versionIds);
+}

+ 71 - 0
master/src/main/java/com/ruoyi/project/system/service/ISysAppVersionService.java

@@ -0,0 +1,71 @@
+package com.ruoyi.project.system.service;
+
+import java.util.List;
+import java.util.Map;
+import com.ruoyi.project.system.domain.SysAppVersion;
+
+/**
+ * APP版本管理 服务层
+ *
+ * @author ruoyi
+ */
+public interface ISysAppVersionService
+{
+    /**
+     * 查询APP版本信息
+     *
+     * @param versionId 版本ID
+     * @return APP版本信息
+     */
+    public SysAppVersion selectAppVersionById(Long versionId);
+
+    /**
+     * 查询APP版本列表
+     *
+     * @param appVersion APP版本信息
+     * @return APP版本集合
+     */
+    public List<SysAppVersion> selectAppVersionList(SysAppVersion appVersion);
+
+    /**
+     * 检查版本更新
+     *
+     * @param version 当前wgt版本
+     * @param appVersion 原生包版本
+     * @param platform 平台
+     * @return 版本检查结果
+     */
+    public Map<String, Object> checkVersion(String version, String appVersion, String platform);
+
+    /**
+     * 新增APP版本
+     *
+     * @param appVersion APP版本信息
+     * @return 结果
+     */
+    public int insertAppVersion(SysAppVersion appVersion);
+
+    /**
+     * 修改APP版本
+     *
+     * @param appVersion APP版本信息
+     * @return 结果
+     */
+    public int updateAppVersion(SysAppVersion appVersion);
+
+    /**
+     * 删除APP版本信息
+     *
+     * @param versionId 版本ID
+     * @return 结果
+     */
+    public int deleteAppVersionById(Long versionId);
+
+    /**
+     * 批量删除APP版本信息
+     *
+     * @param versionIds 需要删除的版本ID
+     * @return 结果
+     */
+    public int deleteAppVersionByIds(Long[] versionIds);
+}

+ 194 - 0
master/src/main/java/com/ruoyi/project/system/service/impl/SysAppVersionServiceImpl.java

@@ -0,0 +1,194 @@
+package com.ruoyi.project.system.service.impl;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.project.system.domain.SysAppVersion;
+import com.ruoyi.project.system.mapper.SysAppVersionMapper;
+import com.ruoyi.project.system.service.ISysAppVersionService;
+
+/**
+ * APP版本管理 服务层实现
+ *
+ * @author ruoyi
+ */
+@Service
+public class SysAppVersionServiceImpl implements ISysAppVersionService
+{
+    @Autowired
+    private SysAppVersionMapper appVersionMapper;
+
+    /**
+     * 查询APP版本信息
+     *
+     * @param versionId 版本ID
+     * @return APP版本信息
+     */
+    @Override
+    public SysAppVersion selectAppVersionById(Long versionId)
+    {
+        return appVersionMapper.selectAppVersionById(versionId);
+    }
+
+    /**
+     * 查询APP版本列表
+     *
+     * @param appVersion APP版本信息
+     * @return APP版本集合
+     */
+    @Override
+    public List<SysAppVersion> selectAppVersionList(SysAppVersion appVersion)
+    {
+        return appVersionMapper.selectAppVersionList(appVersion);
+    }
+
+    /**
+     * 检查版本更新
+     *
+     * @param version 当前wgt版本
+     * @param appVersion 原生包版本
+     * @param platform 平台
+     * @return 版本检查结果
+     */
+    @Override
+    public Map<String, Object> checkVersion(String version, String appVersion, String platform)
+    {
+        Map<String, Object> result = new HashMap<>();
+        result.put("needUpdate", false);
+
+        // 查询最新版本
+        SysAppVersion latestVersion = appVersionMapper.selectLatestVersion(platform);
+        if (latestVersion == null)
+        {
+            return result;
+        }
+
+        // 比较版本号
+        boolean needUpdate = compareVersion(version, latestVersion.getVersion()) < 0;
+
+        if (needUpdate)
+        {
+            result.put("needUpdate", true);
+            result.put("version", latestVersion.getVersion());
+            result.put("title", latestVersion.getTitle());
+            result.put("content", latestVersion.getContent());
+            result.put("downloadUrl", latestVersion.getDownloadUrl());
+            result.put("updateType", latestVersion.getUpdateType());
+            result.put("forceUpdate", "1".equals(latestVersion.getForceUpdate()));
+        }
+
+        return result;
+    }
+
+    /**
+     * 比较版本号
+     *
+     * @param v1 版本1
+     * @param v2 版本2
+     * @return 负数表示v1<v2,0表示相等,正数表示v1>v2
+     */
+    private int compareVersion(String v1, String v2)
+    {
+        if (StringUtils.isEmpty(v1) || StringUtils.isEmpty(v2))
+        {
+            return 0;
+        }
+
+        try
+        {
+            String[] arr1 = v1.split("\\.");
+            String[] arr2 = v2.split("\\.");
+
+            int length = Math.max(arr1.length, arr2.length);
+            for (int i = 0; i < length; i++)
+            {
+                int num1 = i < arr1.length ? parseVersionNumber(arr1[i]) : 0;
+                int num2 = i < arr2.length ? parseVersionNumber(arr2[i]) : 0;
+
+                if (num1 < num2)
+                {
+                    return -1;
+                }
+                else if (num1 > num2)
+                {
+                    return 1;
+                }
+            }
+            return 0;
+        }
+        catch (Exception e)
+        {
+            // 版本号解析异常,返回0表示相等(不更新)
+            return 0;
+        }
+    }
+    
+    /**
+     * 解析版本号数字,处理非数字字符
+     */
+    private int parseVersionNumber(String str)
+    {
+        if (StringUtils.isEmpty(str))
+        {
+            return 0;
+        }
+        // 移除非数字字符(如 1.0.0-beta 中的 -beta)
+        String numStr = str.replaceAll("[^0-9]", "");
+        if (numStr.isEmpty())
+        {
+            return 0;
+        }
+        return Integer.parseInt(numStr);
+    }
+
+    /**
+     * 新增APP版本
+     *
+     * @param appVersion APP版本信息
+     * @return 结果
+     */
+    @Override
+    public int insertAppVersion(SysAppVersion appVersion)
+    {
+        return appVersionMapper.insertAppVersion(appVersion);
+    }
+
+    /**
+     * 修改APP版本
+     *
+     * @param appVersion APP版本信息
+     * @return 结果
+     */
+    @Override
+    public int updateAppVersion(SysAppVersion appVersion)
+    {
+        return appVersionMapper.updateAppVersion(appVersion);
+    }
+
+    /**
+     * 删除APP版本信息
+     *
+     * @param versionId 版本ID
+     * @return 结果
+     */
+    @Override
+    public int deleteAppVersionById(Long versionId)
+    {
+        return appVersionMapper.deleteAppVersionById(versionId);
+    }
+
+    /**
+     * 批量删除APP版本信息
+     *
+     * @param versionIds 需要删除的版本ID
+     * @return 结果
+     */
+    @Override
+    public int deleteAppVersionByIds(Long[] versionIds)
+    {
+        return appVersionMapper.deleteAppVersionByIds(versionIds);
+    }
+}

+ 129 - 0
master/src/main/resources/mybatis/system/SysAppVersionMapper.xml

@@ -0,0 +1,129 @@
+<?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.ruoyi.project.system.mapper.SysAppVersionMapper">
+
+    <resultMap type="SysAppVersion" id="SysAppVersionResult">
+        <result property="versionId"    column="version_id"    />
+        <result property="version"      column="version"       />
+        <result property="appVersion"   column="app_version"   />
+        <result property="platform"     column="platform"      />
+        <result property="updateType"   column="update_type"   />
+        <result property="title"        column="title"         />
+        <result property="content"      column="content"       />
+        <result property="downloadUrl"  column="download_url"  />
+        <result property="forceUpdate"  column="force_update"  />
+        <result property="status"       column="status"        />
+        <result property="createBy"     column="create_by"     />
+        <result property="createTime"   column="create_time"   />
+        <result property="updateBy"     column="update_by"     />
+        <result property="updateTime"   column="update_time"   />
+        <result property="remark"       column="remark"        />
+    </resultMap>
+
+    <sql id="selectAppVersionVo">
+        select version_id, version, app_version, platform, update_type, title, content, 
+               download_url, force_update, status, create_by, create_time, update_by, update_time, remark
+        from sys_app_version
+    </sql>
+
+    <select id="selectAppVersionById" parameterType="Long" resultMap="SysAppVersionResult">
+        <include refid="selectAppVersionVo"/>
+        where version_id = #{versionId}
+    </select>
+
+    <select id="selectAppVersionList" parameterType="SysAppVersion" resultMap="SysAppVersionResult">
+        <include refid="selectAppVersionVo"/>
+        <where>
+            <if test="version != null and version != ''">
+                AND version like concat(concat('%',#{version}),'%')
+            </if>
+            <if test="platform != null and platform != ''">
+                AND platform = #{platform}
+            </if>
+            <if test="updateType != null and updateType != ''">
+                AND update_type = #{updateType}
+            </if>
+            <if test="status != null and status != ''">
+                AND status = #{status}
+            </if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectLatestVersion" parameterType="String" resultMap="SysAppVersionResult">
+        select * from (
+            <include refid="selectAppVersionVo"/>
+            where status = '0'
+            and (platform = #{platform} or platform = 'all')
+            order by create_time desc
+        ) where rownum = 1
+    </select>
+
+    <insert id="insertAppVersion" parameterType="SysAppVersion">
+        <selectKey keyProperty="versionId" order="BEFORE" resultType="long">
+            select seq_sys_app_version.nextval as versionId from DUAL
+        </selectKey>
+        insert into sys_app_version (
+            <if test="versionId != null">version_id,</if>
+            <if test="version != null and version != ''">version,</if>
+            <if test="appVersion != null and appVersion != ''">app_version,</if>
+            <if test="platform != null and platform != ''">platform,</if>
+            <if test="updateType != null and updateType != ''">update_type,</if>
+            <if test="title != null and title != ''">title,</if>
+            <if test="content != null and content != ''">content,</if>
+            <if test="downloadUrl != null and downloadUrl != ''">download_url,</if>
+            <if test="forceUpdate != null and forceUpdate != ''">force_update,</if>
+            <if test="status != null and status != ''">status,</if>
+            <if test="remark != null and remark != ''">remark,</if>
+            <if test="createBy != null and createBy != ''">create_by,</if>
+            create_time
+        )values(
+            <if test="versionId != null">#{versionId},</if>
+            <if test="version != null and version != ''">#{version},</if>
+            <if test="appVersion != null and appVersion != ''">#{appVersion},</if>
+            <if test="platform != null and platform != ''">#{platform},</if>
+            <if test="updateType != null and updateType != ''">#{updateType},</if>
+            <if test="title != null and title != ''">#{title},</if>
+            <if test="content != null and content != ''">#{content},</if>
+            <if test="downloadUrl != null and downloadUrl != ''">#{downloadUrl},</if>
+            <if test="forceUpdate != null and forceUpdate != ''">#{forceUpdate},</if>
+            <if test="status != null and status != ''">#{status},</if>
+            <if test="remark != null and remark != ''">#{remark},</if>
+            <if test="createBy != null and createBy != ''">#{createBy},</if>
+            sysdate
+        )
+    </insert>
+
+    <update id="updateAppVersion" parameterType="SysAppVersion">
+        update sys_app_version
+        <set>
+            <if test="version != null and version != ''">version = #{version},</if>
+            <if test="appVersion != null">app_version = #{appVersion},</if>
+            <if test="platform != null and platform != ''">platform = #{platform},</if>
+            <if test="updateType != null and updateType != ''">update_type = #{updateType},</if>
+            <if test="title != null">title = #{title},</if>
+            <if test="content != null">content = #{content},</if>
+            <if test="downloadUrl != null and downloadUrl != ''">download_url = #{downloadUrl},</if>
+            <if test="forceUpdate != null and forceUpdate != ''">force_update = #{forceUpdate},</if>
+            <if test="status != null and status != ''">status = #{status},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            update_time = sysdate
+        </set>
+        where version_id = #{versionId}
+    </update>
+
+    <delete id="deleteAppVersionById" parameterType="Long">
+        delete from sys_app_version where version_id = #{versionId}
+    </delete>
+
+    <delete id="deleteAppVersionByIds" parameterType="Long">
+        delete from sys_app_version where version_id in
+        <foreach item="versionId" collection="array" open="(" separator="," close=")">
+            #{versionId}
+        </foreach>
+    </delete>
+
+</mapper>

+ 57 - 0
ui/src/api/system/appVersion.js

@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+
+// 查询APP版本列表
+export function listAppVersion(query) {
+  return request({
+    url: '/system/appVersion/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询APP版本详细
+export function getAppVersion(versionId) {
+  return request({
+    url: '/system/appVersion/' + versionId,
+    method: 'get'
+  })
+}
+
+// 新增APP版本
+export function addAppVersion(data) {
+  return request({
+    url: '/system/appVersion',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改APP版本
+export function updateAppVersion(data) {
+  return request({
+    url: '/system/appVersion',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除APP版本
+export function delAppVersion(versionId) {
+  return request({
+    url: '/system/appVersion/' + versionId,
+    method: 'delete'
+  })
+}
+
+// 检查APP版本更新
+export function checkAppVersion(version, appVersion, platform) {
+  return request({
+    url: '/system/appVersion/check',
+    method: 'get',
+    params: {
+      version: version,
+      appVersion: appVersion,
+      platform: platform
+    }
+  })
+}

BIN
ui/src/assets/logo/qrcode.png


+ 12 - 0
ui/src/layout/components/Navbar.vue

@@ -9,6 +9,17 @@
 
         <search id="header-search" class="right-menu-item" />
 
+        <el-popover placement="bottom" trigger="hover" width="240">
+          <div style="text-align: center;">
+            <img src="@/assets/logo/qrcode.png" alt="APP下载二维码" style="width: 180px; height: 180px;" />
+            <p style="color: #666; margin: 10px 0 0 0;">{{ $t('扫码下载APP') }}</p>
+          </div>
+          <span slot="reference" class="right-menu-item hover-effect">
+            <i class="el-icon-mobile-phone" style="font-size: 20px; vertical-align: middle;"></i>
+            <span style="font-size: 12px; vertical-align: middle;">APP</span>
+          </span>
+        </el-popover>
+
 <!--          <router-link class="right-menu-item" to="/notice/details">-->
         <div class="right-menu-item" style="cursor:pointer" @click="getNotice" >
             <el-popover
@@ -79,6 +90,7 @@
         </el-dropdown-menu>
       </el-dropdown>
     </div>
+
   </div>
 </template>
 

+ 399 - 0
ui/src/views/system/appVersion/index.vue

@@ -0,0 +1,399 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item :label="$t('版本号')" prop="version">
+        <el-input
+          v-model="queryParams.version"
+          :placeholder="$t('请输入') + $t('版本号')"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item :label="$t('平台')" prop="platform">
+        <el-select v-model="queryParams.platform" :placeholder="$t('请选择') + $t('平台')" clearable size="small">
+          <el-option label="Android" value="android" />
+          <el-option label="iOS" value="ios" />
+          <el-option :label="$t('全部平台')" value="all" />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="$t('更新类型')" prop="updateType">
+        <el-select v-model="queryParams.updateType" :placeholder="$t('请选择') + $t('更新类型')" clearable size="small">
+          <el-option :label="$t('热更新')" value="wgt" />
+          <el-option :label="$t('整包更新')" value="apk" />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="$t('状态')" prop="status">
+        <el-select v-model="queryParams.status" :placeholder="$t('请选择') + $t('状态')" clearable size="small">
+          <el-option :label="$t('正常')" value="0" />
+          <el-option :label="$t('停用')" value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('搜索') }}</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('重置') }}</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['system:appVersion:add']"
+        >{{ $t('新增') }}</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          @click="handleUpdate"
+          v-hasPermi="['system:appVersion:edit']"
+        >{{ $t('修改') }}</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['system:appVersion:remove']"
+        >{{ $t('删除') }}</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="versionList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column :label="$t('版本ID')" align="center" prop="versionId" width="80" />
+      <el-table-column :label="$t('版本号')" align="center" prop="version" width="120" />
+      <el-table-column :label="$t('原生包版本')" align="center" prop="appVersion" width="120" />
+      <el-table-column :label="$t('平台')" align="center" prop="platform" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.platform === 'android'" type="success">Android</el-tag>
+          <el-tag v-else-if="scope.row.platform === 'ios'" type="warning">iOS</el-tag>
+          <el-tag v-else type="info">{{ $t('全部') }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('更新类型')" align="center" prop="updateType" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.updateType === 'wgt'" type="primary">{{ $t('热更新') }}</el-tag>
+          <el-tag v-else type="danger">{{ $t('整包更新') }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('更新标题')" align="center" prop="title" :show-overflow-tooltip="true" />
+      <el-table-column :label="$t('强制更新')" align="center" prop="forceUpdate" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.forceUpdate === '1'" type="danger">{{ $t('是') }}</el-tag>
+          <el-tag v-else type="info">{{ $t('否') }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('状态')" align="center" prop="status" width="80">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === '0'" type="success">{{ $t('正常') }}</el-tag>
+          <el-tag v-else type="danger">{{ $t('停用') }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('创建时间')" align="center" prop="createTime" width="160">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('操作')" align="center" width="150" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['system:appVersion:edit']"
+          >{{ $t('修改') }}</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['system:appVersion:remove']"
+          >{{ $t('删除') }}</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item :label="$t('版本号')" prop="version">
+          <el-input v-model="form.version" :placeholder="$t('请输入') + $t('版本号') + '(如 2.6.0)'" />
+        </el-form-item>
+        <el-form-item :label="$t('原生包版本')" prop="appVersion">
+          <el-input v-model="form.appVersion" :placeholder="$t('请输入') + $t('原生包版本') + '(如 1.0.0)'" />
+        </el-form-item>
+        <el-form-item :label="$t('平台')" prop="platform">
+          <el-select v-model="form.platform" :placeholder="$t('请选择') + $t('平台')" style="width: 100%">
+            <el-option label="Android" value="android" />
+            <el-option label="iOS" value="ios" />
+            <el-option :label="$t('全部平台')" value="all" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="$t('更新类型')" prop="updateType">
+          <el-select v-model="form.updateType" :placeholder="$t('请选择') + $t('更新类型')" style="width: 100%">
+            <el-option :label="$t('热更新') + ' (wgt)'" value="wgt" />
+            <el-option :label="$t('整包更新') + ' (apk)'" value="apk" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="$t('更新标题')" prop="title">
+          <el-input v-model="form.title" :placeholder="$t('请输入') + $t('更新标题')" />
+        </el-form-item>
+        <el-form-item :label="$t('更新内容')" prop="content">
+          <el-input v-model="form.content" type="textarea" :rows="4" :placeholder="$t('请输入') + $t('更新内容')" />
+        </el-form-item>
+        <el-form-item :label="$t('安装包')" prop="downloadUrl">
+          <el-upload
+            class="upload-demo"
+            :action="uploadUrl"
+            :headers="headers"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+            :before-upload="beforeUpload"
+            :show-file-list="false"
+            :accept="uploadAccept"
+          >
+            <el-button size="small" type="primary" icon="el-icon-upload">{{ $t('上传安装包') }}</el-button>
+            <span style="margin-left: 10px; color: #909399; font-size: 12px;">{{ $t('支持 .apk / .wgt 文件') }}</span>
+          </el-upload>
+          <el-input v-model="form.downloadUrl" :placeholder="$t('上传后自动填充')" style="margin-top: 10px;" readonly>
+            <template slot="prepend">{{ $t('下载地址') }}</template>
+          </el-input>
+        </el-form-item>
+        <el-form-item :label="$t('强制更新')" prop="forceUpdate">
+          <el-radio-group v-model="form.forceUpdate">
+            <el-radio label="0">{{ $t('否') }}</el-radio>
+            <el-radio label="1">{{ $t('是') }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item :label="$t('状态')" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio label="0">{{ $t('正常') }}</el-radio>
+            <el-radio label="1">{{ $t('停用') }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item :label="$t('备注')" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :placeholder="$t('请输入') + $t('备注')" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">{{ $t('确 定') }}</el-button>
+        <el-button @click="cancel">{{ $t('取 消') }}</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listAppVersion, getAppVersion, addAppVersion, updateAppVersion, delAppVersion } from "@/api/system/appVersion";
+import { getToken } from "@/utils/auth";
+
+export default {
+  name: "AppVersion",
+  data() {
+    return {
+      // 上传地址(上传到独立的 /app 文件夹)
+      uploadUrl: process.env.VUE_APP_BASE_API + "/common/uploadWithPath?path=/app",
+      // 上传请求头
+      headers: {
+        Authorization: "Bearer " + getToken()
+      },
+      // 允许上传的文件类型
+      uploadAccept: ".apk,.wgt",
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 版本列表
+      versionList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        version: undefined,
+        platform: undefined,
+        updateType: undefined,
+        status: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        version: [
+          { required: true, message: this.$t('版本号') + this.$t('不能为空'), trigger: "blur" }
+        ],
+        platform: [
+          { required: true, message: this.$t('平台') + this.$t('不能为空'), trigger: "change" }
+        ],
+        updateType: [
+          { required: true, message: this.$t('更新类型') + this.$t('不能为空'), trigger: "change" }
+        ],
+        downloadUrl: [
+          { required: true, message: this.$t('下载地址') + this.$t('不能为空'), trigger: "blur" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      listAppVersion(this.queryParams).then(response => {
+        this.versionList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        versionId: undefined,
+        version: undefined,
+        appVersion: undefined,
+        platform: "android",
+        updateType: "wgt",
+        title: undefined,
+        content: undefined,
+        downloadUrl: undefined,
+        forceUpdate: "0",
+        status: "0",
+        remark: undefined
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.versionId);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = this.$t('新增') + " APP " + this.$t('版本');
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const versionId = row.versionId || this.ids[0];
+      getAppVersion(versionId).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = this.$t('修改') + " APP " + this.$t('版本');
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.versionId !== undefined) {
+            updateAppVersion(this.form).then(response => {
+              this.msgSuccess(this.$t('修改成功'));
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addAppVersion(this.form).then(response => {
+              this.msgSuccess(this.$t('新增成功'));
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const versionIds = row.versionId || this.ids;
+      this.$confirm(this.$t('是否确认删除版本ID为"') + versionIds + this.$t('"的数据项?'), this.$t('警告'), {
+        confirmButtonText: this.$t('确定'),
+        cancelButtonText: this.$t('取消'),
+        type: "warning"
+      }).then(() => {
+        return delAppVersion(versionIds);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess(this.$t('删除成功'));
+      });
+    },
+    /** 上传前校验 */
+    beforeUpload(file) {
+      const fileName = file.name.toLowerCase();
+      const isValidType = fileName.endsWith('.apk') || fileName.endsWith('.wgt');
+      if (!isValidType) {
+        this.$message.error(this.$t('只能上传 .apk 或 .wgt 格式的文件'));
+        return false;
+      }
+      // 文件大小限制 200MB
+      const isLt200M = file.size / 1024 / 1024 < 200;
+      if (!isLt200M) {
+        this.$message.error(this.$t('上传文件大小不能超过 200MB'));
+        return false;
+      }
+      return true;
+    },
+    /** 上传成功回调 */
+    handleUploadSuccess(response, file) {
+      if (response.code === 200) {
+        this.form.downloadUrl = response.fileName;
+        this.$message.success(this.$t('上传成功'));
+      } else {
+        this.$message.error(response.msg || this.$t('上传失败'));
+      }
+    },
+    /** 上传失败回调 */
+    handleUploadError(err) {
+      this.$message.error(this.$t('上传失败'));
+    }
+  }
+};
+</script>