From c3768caae6bf19a93d58f821aab7b32059851dcd Mon Sep 17 00:00:00 2001 From: ziin Date: Wed, 4 Mar 2026 21:33:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(warning):=20=E6=96=B0=E5=A2=9E=E6=8C=89?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E7=8E=AF=E5=A2=83=E6=9F=A5=E8=AF=A2=E9=94=AE?= =?UTF-8?q?=E7=9B=98=E8=AD=A6=E5=91=8A=E6=B6=88=E6=81=AF=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yolo/keyborad/common/ErrorCode.java | 3 +- .../keyborad/config/SaTokenConfigure.java | 3 +- .../KeyboardAppVersionsController.java | 36 +++++ .../mapper/KeyboardAppVersionsMapper.java | 12 ++ .../appversion/KeyboardAppUpdateCheckReq.java | 31 ++++ .../model/entity/KeyboardAppVersions.java | 148 ++++++++++++++++++ .../KeyboardAppUpdateCheckRespVO.java | 37 +++++ .../service/KeyboardAppVersionsService.java | 18 +++ .../impl/KeyboardAppVersionsServiceImpl.java | 124 +++++++++++++++ .../mapper/KeyboardAppVersionsMapper.xml | 32 ++++ 10 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/yolo/keyborad/controller/KeyboardAppVersionsController.java create mode 100644 src/main/java/com/yolo/keyborad/mapper/KeyboardAppVersionsMapper.java create mode 100644 src/main/java/com/yolo/keyborad/model/dto/appversion/KeyboardAppUpdateCheckReq.java create mode 100644 src/main/java/com/yolo/keyborad/model/entity/KeyboardAppVersions.java create mode 100644 src/main/java/com/yolo/keyborad/model/vo/appversion/KeyboardAppUpdateCheckRespVO.java create mode 100644 src/main/java/com/yolo/keyborad/service/KeyboardAppVersionsService.java create mode 100644 src/main/java/com/yolo/keyborad/service/impl/KeyboardAppVersionsServiceImpl.java create mode 100644 src/main/resources/mapper/KeyboardAppVersionsMapper.xml diff --git a/src/main/java/com/yolo/keyborad/common/ErrorCode.java b/src/main/java/com/yolo/keyborad/common/ErrorCode.java index 94d28c2..e91836f 100644 --- a/src/main/java/com/yolo/keyborad/common/ErrorCode.java +++ b/src/main/java/com/yolo/keyborad/common/ErrorCode.java @@ -78,7 +78,8 @@ public enum ErrorCode { STT_SERVICE_ERROR(50031, "语音转文字服务异常"), REPORT_TYPE_INVALID(40020, "举报类型无效"), REPORT_COMPANION_ID_EMPTY(40021, "被举报的AI角色ID不能为空"), - REPORT_TYPE_EMPTY(40022, "举报类型不能为空"); + REPORT_TYPE_EMPTY(40022, "举报类型不能为空"), + VERSION_NOT_FOUND(40022, "未找到可用的版本配置"); /** * 状态码 diff --git a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java index 35802b5..74fabcc 100644 --- a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java +++ b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java @@ -80,7 +80,8 @@ public class SaTokenConfigure implements WebMvcConfigurer { "/character/listWithNotLogin", "/character/listByTagWithNotLogin", "/ai-companion/report", - "/apple/notification" + "/apple/notification", + "/appVersions/checkUpdate" }; } @Bean diff --git a/src/main/java/com/yolo/keyborad/controller/KeyboardAppVersionsController.java b/src/main/java/com/yolo/keyborad/controller/KeyboardAppVersionsController.java new file mode 100644 index 0000000..d431f45 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/controller/KeyboardAppVersionsController.java @@ -0,0 +1,36 @@ +package com.yolo.keyborad.controller; + +import com.yolo.keyborad.common.BaseResponse; +import com.yolo.keyborad.common.ResultUtils; +import com.yolo.keyborad.model.dto.appversion.KeyboardAppUpdateCheckReq; +import com.yolo.keyborad.model.vo.appversion.KeyboardAppUpdateCheckRespVO; +import com.yolo.keyborad.service.KeyboardAppVersionsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestBody; + +/* + * @author: ziin + * @date: 2026/3/4 + */ +@RestController +@Slf4j +@RequestMapping("/appVersions") +@Tag(name = "App版本", description = "App 更新检查") +public class KeyboardAppVersionsController { + + @Resource + private KeyboardAppVersionsService keyboardAppVersionsService; + + @PostMapping("/checkUpdate") + @Operation(summary = "检查是否需要更新", description = "根据平台/渠道/客户端版本号,返回是否需要更新与是否强更") + public BaseResponse checkUpdate(@RequestBody KeyboardAppUpdateCheckReq req) { + final KeyboardAppUpdateCheckRespVO resp = keyboardAppVersionsService.checkUpdate(req); + return ResultUtils.success(resp); + } +} diff --git a/src/main/java/com/yolo/keyborad/mapper/KeyboardAppVersionsMapper.java b/src/main/java/com/yolo/keyborad/mapper/KeyboardAppVersionsMapper.java new file mode 100644 index 0000000..c8bdd52 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/mapper/KeyboardAppVersionsMapper.java @@ -0,0 +1,12 @@ +package com.yolo.keyborad.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yolo.keyborad.model.entity.KeyboardAppVersions; + +/* +* @author: ziin +* @date: 2026/3/4 16:36 +*/ + +public interface KeyboardAppVersionsMapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/yolo/keyborad/model/dto/appversion/KeyboardAppUpdateCheckReq.java b/src/main/java/com/yolo/keyborad/model/dto/appversion/KeyboardAppUpdateCheckReq.java new file mode 100644 index 0000000..c42e0df --- /dev/null +++ b/src/main/java/com/yolo/keyborad/model/dto/appversion/KeyboardAppUpdateCheckReq.java @@ -0,0 +1,31 @@ +package com.yolo.keyborad.model.dto.appversion; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/* + * @author: ziin + * @date: 2026/3/4 + */ +@Data +@Schema(description = "App 更新检查请求") +public class KeyboardAppUpdateCheckReq { + + @Schema(description = "应用标识;单 App 可固定为 main", example = "main") + private String appId; + + @Schema(description = "平台:android/ios", example = "ios") + private String platform; + + @Schema(description = "渠道:official/testflight/...", example = "official") + private String channel; + + @Schema(description = "客户端版本号(整数递增):Android=versionCode,iOS 建议维护同样的递增值", example = "100") + private Long clientVersionCode; + + @Schema(description = "客户端展示版本号(可选):如 1.2.3", example = "1.2.3") + private String clientVersionName; + + @Schema(description = "客户端构建号(可选):iOS CFBundleVersion 等", example = "2026030401") + private String buildNumber; +} diff --git a/src/main/java/com/yolo/keyborad/model/entity/KeyboardAppVersions.java b/src/main/java/com/yolo/keyborad/model/entity/KeyboardAppVersions.java new file mode 100644 index 0000000..0458e3a --- /dev/null +++ b/src/main/java/com/yolo/keyborad/model/entity/KeyboardAppVersions.java @@ -0,0 +1,148 @@ +package com.yolo.keyborad.model.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Date; +import lombok.Data; + +/* +* @author: ziin +* @date: 2026/3/4 16:36 +*/ + +/** + * App 版本发布与更新检查表:区分 Android/iOS、渠道,支持最低支持版本与强更策略。 + */ +@Schema(description="App 版本发布与更新检查表:区分 Android/iOS、渠道,支持最低支持版本与强更策略。") +@Data +@TableName(value = "keyboard_app_versions") +public class KeyboardAppVersions { + /** + * 主键,自增版本记录ID。 + */ + @TableId(value = "id", type = IdType.AUTO) + @Schema(description="主键,自增版本记录ID。") + private Long id; + + /** + * 应用标识(支持多App/多包名场景);单App可固定为 main。 + */ + @TableField(value = "app_id") + @Schema(description="应用标识(支持多App/多包名场景);单App可固定为 main。") + private String appId; + + /** + * 平台:android 或 ios(用 CHECK 约束限制取值)。 + */ + @TableField(value = "platform") + @Schema(description="平台:android 或 ios(用 CHECK 约束限制取值)。") + private String platform; + + /** + * 渠道标识:如 official / huawei / xiaomi / testflight 等,用于区分不同分发包。 + */ + @TableField(value = "channel") + @Schema(description="渠道标识:如 official / huawei / xiaomi / testflight 等,用于区分不同分发包。") + private String channel; + + /** + * 展示用版本号(语义版本字符串),如 1.2.3。 + */ + @TableField(value = "version_name") + @Schema(description="展示用版本号(语义版本字符串),如 1.2.3。") + private String versionName; + + /** + * 比较用版本号(整数递增):Android 对应 versionCode;iOS 建议维护同样的递增值以便比较。 + */ + @TableField(value = "version_code") + @Schema(description="比较用版本号(整数递增):Android 对应 versionCode;iOS 建议维护同样的递增值以便比较。") + private Long versionCode; + + /** + * iOS 可选构建号(例如 CFBundleVersion),通常为字符串;用于追溯构建或与CI编号对齐。 + */ + @TableField(value = "build_number") + @Schema(description="iOS 可选构建号(例如 CFBundleVersion),通常为字符串;用于追溯构建或与CI编号对齐。") + private String buildNumber; + + /** + * 最低支持版本号(整数):客户端 version_code 低于该值必须更新/可拒绝继续使用。 + */ + @TableField(value = "min_supported_code") + @Schema(description="最低支持版本号(整数):客户端 version_code 低于该值必须更新/可拒绝继续使用。") + private Long minSupportedCode; + + /** + * 是否强制更新:当客户端未达到最新版本且此字段为 true,可要求强更(即使 >= min_supported_code)。 + */ + @TableField(value = "is_force_update") + @Schema(description="是否强制更新:当客户端未达到最新版本且此字段为 true,可要求强更(即使 >= min_supported_code)。") + private Boolean isForceUpdate; + + /** + * 是否生效:true 表示该版本记录可用于对外更新检查;false 用于下架/撤回。 + */ + @TableField(value = "is_active") + @Schema(description="是否生效:true 表示该版本记录可用于对外更新检查;false 用于下架/撤回。") + private Boolean isActive; + + /** + * 更新说明(展示给用户的版本更新内容)。 + */ + @TableField(value = "release_notes") + @Schema(description="更新说明(展示给用户的版本更新内容)。") + private String releaseNotes; + + /** + * 下载链接:Android 可为 apk 直链/市场 scheme;iOS 通常为 App Store 链接或统一跳转页。 + */ + @TableField(value = "download_url") + @Schema(description="下载链接:Android 可为 apk 直链/市场 scheme;iOS 通常为 App Store 链接或统一跳转页。") + private String downloadUrl; + + /** + * 应用市场/商店页面链接(可选,若 download_url 已覆盖可不填)。 + */ + @TableField(value = "store_url") + @Schema(description="应用市场/商店页面链接(可选,若 download_url 已覆盖可不填)。") + private String storeUrl; + + /** + * 扩展元数据(JSON):如包大小、md5、签名信息、最低系统版本等。 + */ + @TableField(value = "metadata") + @Schema(description="扩展元数据(JSON):如包大小、md5、签名信息、最低系统版本等。") + private Object metadata; + + /** + * 发布时间(对外宣布/上线时间),用于展示与排序。 + */ + @TableField(value = "released_at") + @Schema(description="发布时间(对外宣布/上线时间),用于展示与排序。") + private Date releasedAt; + + /** + * 记录创建时间。 + */ + @TableField(value = "created_at") + @Schema(description="记录创建时间。") + private Date createdAt; + + /** + * 记录更新时间(建议配合触发器自动维护)。 + */ + @TableField(value = "updated_at") + @Schema(description="记录更新时间(建议配合触发器自动维护)。") + private Date updatedAt; + + /** + * 是否删除 + */ + @TableField(value = "deleted") + @Schema(description="是否删除") + private Boolean deleted; +} \ No newline at end of file diff --git a/src/main/java/com/yolo/keyborad/model/vo/appversion/KeyboardAppUpdateCheckRespVO.java b/src/main/java/com/yolo/keyborad/model/vo/appversion/KeyboardAppUpdateCheckRespVO.java new file mode 100644 index 0000000..2b1dde8 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/model/vo/appversion/KeyboardAppUpdateCheckRespVO.java @@ -0,0 +1,37 @@ +package com.yolo.keyborad.model.vo.appversion; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/* + * @author: ziin + * @date: 2026/3/4 + */ +@Data +@Schema(description = "App 更新检查响应") +public class KeyboardAppUpdateCheckRespVO { + + @Schema(description = "是否需要更新(客户端版本 < 最新版本)") + private Boolean needUpdate; + + @Schema(description = "是否强制更新(客户端版本 < 最低支持版本 或 最新版本标记为强更)") + private Boolean forceUpdate; + + @Schema(description = "最新版本展示号,如 1.2.3") + private String latestVersionName; + + @Schema(description = "最新版本比较号(整数递增)") + private Long latestVersionCode; + + @Schema(description = "最低支持版本比较号(整数);客户端低于该值必须更新") + private Long minSupportedCode; + + @Schema(description = "更新说明") + private String releaseNotes; + + @Schema(description = "下载链接/市场 scheme") + private String downloadUrl; + + @Schema(description = "商店页面链接") + private String storeUrl; +} diff --git a/src/main/java/com/yolo/keyborad/service/KeyboardAppVersionsService.java b/src/main/java/com/yolo/keyborad/service/KeyboardAppVersionsService.java new file mode 100644 index 0000000..73e9eb1 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/service/KeyboardAppVersionsService.java @@ -0,0 +1,18 @@ +package com.yolo.keyborad.service; + +import com.yolo.keyborad.model.entity.KeyboardAppVersions; +import com.baomidou.mybatisplus.extension.service.IService; +import com.yolo.keyborad.model.dto.appversion.KeyboardAppUpdateCheckReq; +import com.yolo.keyborad.model.vo.appversion.KeyboardAppUpdateCheckRespVO; + /* +* @author: ziin +* @date: 2026/3/4 16:36 +*/ + +public interface KeyboardAppVersionsService extends IService{ + + /** + * 检查客户端是否需要更新/强制更新。 + */ + KeyboardAppUpdateCheckRespVO checkUpdate(KeyboardAppUpdateCheckReq req); +} diff --git a/src/main/java/com/yolo/keyborad/service/impl/KeyboardAppVersionsServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/KeyboardAppVersionsServiceImpl.java new file mode 100644 index 0000000..b6f6162 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/service/impl/KeyboardAppVersionsServiceImpl.java @@ -0,0 +1,124 @@ +package com.yolo.keyborad.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yolo.keyborad.common.ErrorCode; +import com.yolo.keyborad.exception.BusinessException; +import com.yolo.keyborad.model.dto.appversion.KeyboardAppUpdateCheckReq; +import com.yolo.keyborad.model.entity.KeyboardAppVersions; +import com.yolo.keyborad.model.vo.appversion.KeyboardAppUpdateCheckRespVO; +import com.yolo.keyborad.mapper.KeyboardAppVersionsMapper; +import com.yolo.keyborad.service.KeyboardAppVersionsService; +/* +* @author: ziin +* @date: 2026/3/4 16:36 +*/ + +@Service +public class KeyboardAppVersionsServiceImpl extends ServiceImpl implements KeyboardAppVersionsService{ + + private static final String DEFAULT_APP_ID = "main"; + private static final String DEFAULT_CHANNEL = "official"; + private static final String PLATFORM_ANDROID = "android"; + private static final String PLATFORM_IOS = "ios"; + private static final String LIMIT_1 = "LIMIT 1"; + + private record UpdateCheckQuery(String appId, String platform, String channel, long clientVersionCode) { + } + + @Override + public KeyboardAppUpdateCheckRespVO checkUpdate(KeyboardAppUpdateCheckReq req) { + final UpdateCheckQuery query = parseAndValidate(req); + final KeyboardAppVersions latest = findLatestActiveVersion(query); + if (latest == null) { + throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到可用的版本配置"); + } + if (latest.getVersionCode() == null) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "版本配置缺少 versionCode"); + } + + final long clientCode = query.clientVersionCode(); + final long latestCode = latest.getVersionCode(); + + final boolean needUpdate = clientCode < latestCode; + final Long minSupportedCode = latest.getMinSupportedCode(); + final boolean belowMinSupported = minSupportedCode != null && clientCode < minSupportedCode; + final boolean forceUpdate = belowMinSupported + || (needUpdate && Boolean.TRUE.equals(latest.getIsForceUpdate())); + + final KeyboardAppUpdateCheckRespVO resp = new KeyboardAppUpdateCheckRespVO(); + resp.setNeedUpdate(needUpdate); + resp.setForceUpdate(forceUpdate); + resp.setLatestVersionName(latest.getVersionName()); + resp.setLatestVersionCode(latest.getVersionCode()); + resp.setMinSupportedCode(minSupportedCode); + resp.setReleaseNotes(latest.getReleaseNotes()); + resp.setDownloadUrl(latest.getDownloadUrl()); + resp.setStoreUrl(latest.getStoreUrl()); + return resp; + } + + private UpdateCheckQuery parseAndValidate(KeyboardAppUpdateCheckReq req) { + if (req == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空"); + } + final String platform = normalizePlatform(req.getPlatform()); + if (platform == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "platform 不能为空"); + } + if (!PLATFORM_ANDROID.equals(platform) && !PLATFORM_IOS.equals(platform)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "platform 仅支持 android/ios"); + } + + final String appId = defaultIfBlank(normalizeId(req.getAppId()), DEFAULT_APP_ID); + final String channel = defaultIfBlank(normalizeId(req.getChannel()), DEFAULT_CHANNEL); + + if (req.getClientVersionCode() == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "versionCode 不能为空"); + } + final long clientVersionCode = req.getClientVersionCode(); + if (clientVersionCode <= 0) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "versionCode 必须大于 0"); + } + return new UpdateCheckQuery(appId, platform, channel, clientVersionCode); + } + + private KeyboardAppVersions findLatestActiveVersion(UpdateCheckQuery query) { + final QueryWrapper wrapper = new QueryWrapper() + .eq("app_id", query.appId()) + .eq("platform", query.platform()) + .eq("channel", query.channel()) + .eq("is_active", true) + .eq("deleted", false) + .orderByDesc("version_code") + .last(LIMIT_1); + return this.getOne(wrapper, false); + } + + private static String normalizePlatform(String value) { + if (value == null) { + return null; + } + final String trimmed = value.trim(); + if (trimmed.isBlank()) { + return null; + } + return trimmed.toLowerCase(); + } + + private static String normalizeId(String value) { + if (value == null) { + return null; + } + final String trimmed = value.trim(); + if (trimmed.isBlank()) { + return null; + } + return trimmed; + } + + private static String defaultIfBlank(String value, String defaultValue) { + return value == null ? defaultValue : value; + } +} diff --git a/src/main/resources/mapper/KeyboardAppVersionsMapper.xml b/src/main/resources/mapper/KeyboardAppVersionsMapper.xml new file mode 100644 index 0000000..ae046ae --- /dev/null +++ b/src/main/resources/mapper/KeyboardAppVersionsMapper.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + id, app_id, platform, channel, version_name, version_code, build_number, min_supported_code, + is_force_update, is_active, release_notes, download_url, store_url, metadata, released_at, + created_at, updated_at, deleted + + \ No newline at end of file