feat(warning): 新增按语言环境查询键盘警告消息接口

This commit is contained in:
2026-03-04 21:33:51 +08:00
parent 506e1e0192
commit c3768caae6
10 changed files with 442 additions and 2 deletions

View File

@@ -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, "未找到可用的版本配置");
/**
* 状态码

View File

@@ -80,7 +80,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/character/listWithNotLogin",
"/character/listByTagWithNotLogin",
"/ai-companion/report",
"/apple/notification"
"/apple/notification",
"/appVersions/checkUpdate"
};
}
@Bean

View File

@@ -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<KeyboardAppUpdateCheckRespVO> checkUpdate(@RequestBody KeyboardAppUpdateCheckReq req) {
final KeyboardAppUpdateCheckRespVO resp = keyboardAppVersionsService.checkUpdate(req);
return ResultUtils.success(resp);
}
}

View File

@@ -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<KeyboardAppVersions> {
}

View File

@@ -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=versionCodeiOS 建议维护同样的递增值", 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;
}

View File

@@ -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 对应 versionCodeiOS 建议维护同样的递增值以便比较。
*/
@TableField(value = "version_code")
@Schema(description="比较用版本号整数递增Android 对应 versionCodeiOS 建议维护同样的递增值以便比较。")
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 直链/市场 schemeiOS 通常为 App Store 链接或统一跳转页。
*/
@TableField(value = "download_url")
@Schema(description="下载链接Android 可为 apk 直链/市场 schemeiOS 通常为 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;
}

View File

@@ -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;
}

View File

@@ -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<KeyboardAppVersions>{
/**
* 检查客户端是否需要更新/强制更新。
*/
KeyboardAppUpdateCheckRespVO checkUpdate(KeyboardAppUpdateCheckReq req);
}

View File

@@ -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<KeyboardAppVersionsMapper, KeyboardAppVersions> 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<KeyboardAppVersions> wrapper = new QueryWrapper<KeyboardAppVersions>()
.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;
}
}

View File

@@ -0,0 +1,32 @@
<?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.yolo.keyborad.mapper.KeyboardAppVersionsMapper">
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardAppVersions">
<!--@mbg.generated-->
<!--@Table keyboard_app_versions-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="app_id" jdbcType="VARCHAR" property="appId" />
<result column="platform" jdbcType="VARCHAR" property="platform" />
<result column="channel" jdbcType="VARCHAR" property="channel" />
<result column="version_name" jdbcType="VARCHAR" property="versionName" />
<result column="version_code" jdbcType="BIGINT" property="versionCode" />
<result column="build_number" jdbcType="VARCHAR" property="buildNumber" />
<result column="min_supported_code" jdbcType="BIGINT" property="minSupportedCode" />
<result column="is_force_update" jdbcType="BOOLEAN" property="isForceUpdate" />
<result column="is_active" jdbcType="BOOLEAN" property="isActive" />
<result column="release_notes" jdbcType="VARCHAR" property="releaseNotes" />
<result column="download_url" jdbcType="VARCHAR" property="downloadUrl" />
<result column="store_url" jdbcType="VARCHAR" property="storeUrl" />
<result column="metadata" jdbcType="OTHER" property="metadata" />
<result column="released_at" jdbcType="TIMESTAMP" property="releasedAt" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
<result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
<result column="deleted" jdbcType="BOOLEAN" property="deleted" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
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
</sql>
</mapper>