feat(config): 接入 Nacos 配置中心

- 新增 AppConfig、NacosAppConfigCenter 动态配置类
- 将 userRegisterProperties 的默认值改为运行时从 Nacos 读取
- 注册/创建用户时免费配额改为动态配置获取
- 增加 nacos-client 依赖并配置 dev 环境连接信息
This commit is contained in:
2025-12-16 21:50:00 +08:00
parent f95762138b
commit 8e26488738
9 changed files with 195 additions and 69 deletions

View File

@@ -55,6 +55,13 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>3.1.1</version>
</dependency>
<!-- qdrant向量数据库 sdk -->
<dependency>
<groupId>io.qdrant</groupId>

View File

@@ -0,0 +1,24 @@
package com.yolo.keyborad.config;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/12/16 21:18
*/
@Data
public class AppConfig {
private UserRegisterProperties userRegisterProperties = new UserRegisterProperties();
@Data
public static class UserRegisterProperties {
/**
* 新用户注册时的免费使用次数
*/
private Integer freeTrialQuota = 5;
}
}

View File

@@ -0,0 +1,82 @@
package com.yolo.keyborad.config;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
@Configuration
public class NacosAppConfigCenter {
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Bean
public ConfigService nacosConfigService(
@Value("${nacos.config.server-addr}") String serverAddr
) throws NacosException {
Properties p = new Properties();
p.put("serverAddr", serverAddr);
return NacosFactory.createConfigService(p);
}
@Bean
public DynamicAppConfig dynamicAppConfig(
ConfigService configService,
@Value("${nacos.config.group}") String group,
@Value("${nacos.config.data-id}") String dataId
) throws Exception {
DynamicAppConfig holder = new DynamicAppConfig();
// 启动先拉一次
String content = configService.getConfig(dataId, group, 3000);
if (content != null && !content.isBlank()) {
holder.ref.set(parse(content));
log.info("Loaded nacos config: dataId={}, group={}", dataId, group);
} else {
log.warn("Empty nacos config: dataId={}, group={}", dataId, group);
}
// 监听热更新
configService.addListener(dataId, group, new Listener() {
@Override public Executor getExecutor() { return null; }
@Override public void receiveConfigInfo(String configInfo) {
try {
AppConfig newCfg = parse(configInfo);
holder.ref.set(newCfg);
log.info("Refreshed nacos config: dataId={}, group={}", dataId, group);
log.info("New config: {}", newCfg.toString());
} catch (Exception e) {
// 解析失败不覆盖旧配置
log.error("Failed to refresh nacos config: dataId={}, keep old config.", dataId, e);
}
}
});
return holder;
}
private AppConfig parse(String yaml) throws Exception {
if (yaml == null || yaml.isBlank()) return new AppConfig();
return yamlMapper.readValue(yaml, AppConfig.class);
}
@Getter
public static class DynamicAppConfig {
private final AtomicReference<AppConfig> ref = new AtomicReference<>(new AppConfig());
}
}

View File

@@ -15,6 +15,6 @@ public class UserRegisterProperties {
/**
* 新用户注册时的免费使用次数
*/
private Integer freeTrialQuota = 5;
private Integer freeTrialQuota;
}

View File

@@ -94,8 +94,7 @@ public class UserController {
@PostMapping("/register")
@Operation(summary = "用户注册",description = "用户注册接口")
public BaseResponse<Boolean> register(@RequestBody UserRegisterDTO userRegisterDTO) {
userService.userRegister(userRegisterDTO);
return ResultUtils.success(true);
return ResultUtils.success(userService.userRegister(userRegisterDTO));
}
@PostMapping("/sendVerifyMail")

View File

@@ -8,16 +8,16 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yolo.keyborad.common.ErrorCode;
import com.yolo.keyborad.config.AppConfig;
import com.yolo.keyborad.config.NacosAppConfigCenter;
import com.yolo.keyborad.config.UserRegisterProperties;
import com.yolo.keyborad.exception.BusinessException;
import com.yolo.keyborad.mapper.KeyboardUserMapper;
import com.yolo.keyborad.model.dto.user.*;
import com.yolo.keyborad.model.entity.KeyboardUser;
import com.yolo.keyborad.model.entity.KeyboardUserWallet;
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
import com.yolo.keyborad.service.KeyboardCharacterService;
import com.yolo.keyborad.service.KeyboardUserLoginLogService;
import com.yolo.keyborad.service.KeyboardUserWalletService;
import com.yolo.keyborad.service.UserService;
import com.yolo.keyborad.service.*;
import jakarta.servlet.http.HttpServletRequest;
import com.yolo.keyborad.utils.RedisUtil;
import com.yolo.keyborad.utils.SendMailUtils;
@@ -61,10 +61,16 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
private KeyboardUserLoginLogService loginLogService;
@Resource
private com.yolo.keyborad.service.KeyboardUserQuotaTotalService quotaTotalService;
private KeyboardUserQuotaTotalService quotaTotalService;
@Resource
private com.yolo.keyborad.config.UserRegisterProperties userRegisterProperties;
private UserRegisterProperties userRegisterProperties;
private final NacosAppConfigCenter.DynamicAppConfig cfgHolder;
public UserServiceImpl(NacosAppConfigCenter.DynamicAppConfig cfgHolder) {
this.cfgHolder = cfgHolder;
}
@Override
public KeyboardUser selectUserWithSubjectId(String sub) {
@@ -97,7 +103,8 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
com.yolo.keyborad.model.entity.KeyboardUserQuotaTotal quotaTotal =
new com.yolo.keyborad.model.entity.KeyboardUserQuotaTotal();
quotaTotal.setUserId(keyboardUser.getId());
quotaTotal.setTotalQuota(userRegisterProperties.getFreeTrialQuota());
AppConfig appConfig = cfgHolder.getRef().get();
quotaTotal.setTotalQuota(appConfig.getUserRegisterProperties().getFreeTrialQuota());
quotaTotal.setUsedQuota(0);
quotaTotal.setVersion(0);
quotaTotal.setCreatedAt(new Date());
@@ -105,7 +112,7 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
quotaTotalService.save(quotaTotal);
log.info("User registered with Apple Sign-In, userId={}, freeQuota={}",
keyboardUser.getId(), userRegisterProperties.getFreeTrialQuota());
keyboardUser.getId(), appConfig.getUserRegisterProperties().getFreeTrialQuota());
return keyboardUser;
}
@@ -235,7 +242,8 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
com.yolo.keyborad.model.entity.KeyboardUserQuotaTotal quotaTotal =
new com.yolo.keyborad.model.entity.KeyboardUserQuotaTotal();
quotaTotal.setUserId(keyboardUser.getId());
quotaTotal.setTotalQuota(userRegisterProperties.getFreeTrialQuota());
AppConfig appConfig = cfgHolder.getRef().get();
quotaTotal.setTotalQuota(appConfig.getUserRegisterProperties().getFreeTrialQuota());
quotaTotal.setUsedQuota(0);
quotaTotal.setVersion(0);
quotaTotal.setCreatedAt(new Date());
@@ -243,7 +251,7 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
quotaTotalService.save(quotaTotal);
log.info("User registered with email, userId={}, email={}, freeQuota={}",
keyboardUser.getId(), keyboardUser.getEmail(), userRegisterProperties.getFreeTrialQuota());
keyboardUser.getId(), keyboardUser.getEmail(), appConfig.getUserRegisterProperties().getFreeTrialQuota());
}
return insertCount > 0;
}

View File

@@ -5,27 +5,27 @@ spring:
username: root
password: 123asd
# 日志配置
# ????
logging:
level:
# 设置 mapper 包的日志级别为 DEBUG,打印 SQL 语句
# ?? mapper ??????? DEBUG??? SQL ??
com.yolo.keyborad.mapper: DEBUG
# 设置根日志级别
# ???????
root: INFO
# Spring 框架日志
# Spring ????
org.springframework: INFO
# MyBatis 日志
# MyBatis ??
org.mybatis: DEBUG
pattern:
# 彩色控制台日志格式
# 时间-无颜色,日志级别-根据级别变色进程ID-品红,线程-黄色,类名-青色,消息-默认色
# ?????????
# ??-????????-?????????ID-?????-?????-?????-???
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} | %clr(%-5level){highlight} %clr(${PID:- }){magenta} | %clr(%-15thread){yellow} %clr(%-50logger{50}){cyan} | %msg%n"
knife4j:
enable: true
openapi:
title: "接口文档"
title: "????"
version: 1.0
group:
default:
@@ -38,65 +38,71 @@ apple:
issuer-id: "178b442e-b7be-4526-bd13-ab293d019df0"
key-id: "Y7TF7BV74G"
bundle-id: "com.loveKey.nyx"
# app App Store Apple ID(数值),生产环境必填
# app ? App Store ? Apple ID???????????
app-apple-id: 1234567890
# p8 私钥文件路径(你可以放在 resources 下)
# p8 ???????????? resources ??
private-key-path: "classpath:SubscriptionKey_Y7TF7BV74G.p8"
# SANDBOX PRODUCTION
# SANDBOX ? PRODUCTION
environment: "SANDBOX"
# 根证书路径(从 Apple PKI 下载)
# ??????? Apple PKI ???
root-certificates:
- "classpath:AppleRootCA-G2.cer"
- "classpath:AppleRootCA-G3.cer"
dromara:
x-file-storage: #文件存储配置
default-platform: cloudflare-r2 #默认使用的存储平台
thumbnail-suffix: ".min.jpg" #缩略图后缀,例如【.min.jpg】【.png
x-file-storage: #??????
default-platform: cloudflare-r2 #?????????
thumbnail-suffix: ".min.jpg" #?????????.min.jpg??.png?
enable-byte-file-wrapper: false
#对应平台的配置写在这里,注意缩进要对齐
#???????????????????
amazon-s3-v2: # Amazon S3 V2
- platform: cloudflare-r2 # 存储平台标识
enable-storage: true # 启用存储
- platform: cloudflare-r2 # ??????
enable-storage: true # ????
access-key: 550b33cc4d53e05c2e438601f8a0e209
secret-key: df4d529cdae44e6f614ca04f4dc0f1f9a299e57367181243e8abdc7f7c28e99a
region: ENAM # 必填
end-point: https://b632a61caa85401f63c9b32eef3a74c8.r2.cloudflarestorage.com # 必填
bucket-name: keyborad-resource #桶名称
domain: https://resource.loveamorkey.com/ # 访问域名,注意“/”结尾,例如:https://abcd.s3.ap-east-1.amazonaws.com/
base-path: avatar/ # 基础路径
region: ENAM # ??
end-point: https://b632a61caa85401f63c9b32eef3a74c8.r2.cloudflarestorage.com # ??
bucket-name: keyborad-resource #???
domain: https://resource.loveamorkey.com/ # ????????/???????https://abcd.s3.ap-east-1.amazonaws.com/
base-path: avatar/ # ????
mailgun:
api-key: ${MAILGUN_API_KEY} # 你的 Private API Key
domain: sandboxxxxxxx.mailgun.org # 或你自己的业务域名
from-email: no-reply@yourdomain.com # 发件人邮箱
from-name: Key Of Love # 发件人名称(可选)
api-key: ${MAILGUN_API_KEY} # ?? Private API Key
domain: sandboxxxxxxx.mailgun.org # ?????????
from-email: no-reply@yourdomain.com # ?????
from-name: Key Of Love # ?????????
# 用户注册配置
# ??????
user:
register:
# 新用户注册时的免费使用次数
# ?????????????
free-trial-quota: 5
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
############## Sa-Token ?? (??: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
# token ??????? cookie ???
token-name: auth-token
# token 有效期(单位:秒) 默认30天-1 代表永久有效
# token ????????? ??30??-1 ??????
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
# token ??????????????? token ???????????????????-1 ??????????
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, false 时新登录挤掉旧登录)
# ?????????????? ?? true ???????, ? false ??????????
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, false 时每次登录新建一个 token
# ????????????????? token ?? true ????????? token, ? false ????????? token?
is-share: false
# token 风格(默认可取值:uuidsimple-uuidrandom-32random-64random-128tik
# token ?????????uuid?simple-uuid?random-32?random-64?random-128?tik?
token-style: random-128
# 是否输出操作日志
# ????????
is-log: true
nacos:
config:
server-addr: 127.0.0.1:8848
group: DEFAULT_GROUP
data-id: keyboard_default-config.yaml

View File

@@ -5,32 +5,32 @@ spring:
username: root
password: 123asd
# 生产环境日志配置
# ????????
logging:
level:
# 生产环境不打印 SQL 日志
# ??????? SQL ??
com.yolo.keyborad.mapper: INFO
# 设置根日志级别
# ???????
root: INFO
# Spring 框架日志
# Spring ????
org.springframework: WARN
# MyBatis 日志
# MyBatis ??
org.mybatis: WARN
pattern:
# 生产环境控制台日志格式
# ???????????
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} | %clr(%-5level){highlight} %clr(${PID:- }){magenta} | %clr(%-15thread){yellow} %clr(%-50logger{50}){cyan} | %msg%n"
# 文件日志格式(无颜色代码)
# ?????????????
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} | %-5level ${PID:- } | %-15thread %-50logger{50} | %msg%n"
file:
# 生产环境日志文件路径
# ??????????
name: logs/keyborad-backend.log
# 日志文件大小限制
# ????????
max-size: 10MB
# 保留的日志文件数量
# ?????????
max-history: 30
# 用户注册配置
# ??????
user:
register:
# 新用户注册时的免费使用次数
# ?????????????
free-trial-quota: 5

View File

@@ -18,17 +18,17 @@ spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
# session 失效时间(分钟)
# session ????????
session:
timeout: 86400
store-type: redis
# redis 配置
# redis ??
data:
redis:
port: 6379
host: localhost
database: 0
# 启用 ANSI 彩色输出
# ?? ANSI ????
output:
ansi:
enabled: always
@@ -47,10 +47,10 @@ mybatis-plus:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# 扫描 TypeHandler
logic-delete-field: isDelete # ????????????(since 3.3.0,????????????2)
logic-delete-value: 1 # ??????(??? 1)
logic-not-delete-value: 0 # ??????(??? 0)
# ?? TypeHandler ?
type-handlers-package: com.yolo.keyborad.typehandler
appid: loveKeyboard