【feat】 重构配置管理模块

This commit is contained in:
Kris 2025-12-25 13:41:53 +08:00
parent db27952e77
commit ff23864f16
18 changed files with 1211 additions and 164 deletions

View File

@ -33,10 +33,10 @@ public class DataiSfToken extends BaseEntity
@Excel(name = "用户名")
private String username;
/** 访问令牌 */
@Schema(title = "访问令牌")
@Excel(name = "访问令牌")
private String accessToken;
/** 会话ID */
@Schema(title = "会话ID")
@Excel(name = "会话ID")
private String sessionId;
/** 刷新令牌 */
@Schema(title = "刷新令牌")
@ -264,7 +264,7 @@ public class DataiSfToken extends BaseEntity
.append("id", getId())
.append("deptId", getDeptId())
.append("username", getUsername())
.append("accessToken", getAccessToken())
.append("sessionId", getSessionId())
.append("refreshToken", getRefreshToken())
.append("accessTokenExpire", getAccessTokenExpire())
.append("refreshTokenExpire", getRefreshTokenExpire())

View File

@ -59,8 +59,8 @@ public class SalesforceLoginResult implements Serializable {
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getAccessToken() { return accessToken; }
public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
public String getSessionId() { return sessionId; }
public void setSessionId(String sessionId) { this.sessionId = sessionId; }
public String getRefreshToken() { return refreshToken; }
public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; }

View File

@ -32,11 +32,11 @@ public interface ISalesforceLoginService {
/**
* 执行登出操作
*
* @param accessToken 访问令牌
* @param sessionId 会话ID
* @param loginType 登录类型
* @return 登出是否成功
*/
boolean logout(String accessToken, String loginType);
boolean logout(String sessionId, String loginType);
/**
* 获取当前登录状态

View File

@ -11,35 +11,35 @@ public interface ITokenManager {
/**
* 验证令牌有效性
*
* @param accessToken 访问令牌
* @param sessionId 会话ID
* @return 是否有效
*/
boolean validateToken(String accessToken);
boolean validateToken(String sessionId);
/**
* 吊销令牌
*
* @param accessToken 访问令牌
* @param sessionId 会话ID
* @return 是否吊销成功
*/
boolean revokeToken(String accessToken);
boolean revokeToken(String sessionId);
/**
* 绑定令牌到设备/IP
*
* @param accessToken 访问令牌
* @param sessionId 会话ID
* @param deviceId 设备ID
* @param ip IP地址
*/
void bindToken(String accessToken, String deviceId, String ip);
void bindToken(String sessionId, String deviceId, String ip);
/**
* 检查令牌绑定
*
* @param accessToken 访问令牌
* @param sessionId 会话ID
* @param deviceId 设备ID
* @param ip IP地址
* @return 是否绑定匹配
*/
boolean checkTokenBinding(String accessToken, String deviceId, String ip);
boolean checkTokenBinding(String sessionId, String deviceId, String ip);
}

View File

@ -19,6 +19,8 @@ import com.datai.auth.cache.SalesforceAuthCacheManager;
import com.datai.common.utils.DateUtils;
import com.datai.common.utils.CacheUtils;
import com.datai.salesforce.common.exception.SalesforceAuthException;
import com.datai.setting.config.SalesforceConfigCacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
@ -71,6 +73,9 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
@Autowired
private SalesforceAuthCacheManager cacheManager;
@Autowired
private SalesforceConfigCacheManager configCacheManager;
/**
* 根据不同登录类型执行登录
*
@ -166,6 +171,9 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
// 清除登录失败记录
clearFailedLoginRecords(request.getUsername());
// 登录成功后触发配置加载到Redis
loadConfigToRedis();
// 记录登录审计日志
recordLoginAudit(request, result, "SUCCESS");
@ -290,11 +298,11 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
/**
* 执行登出操作
*
* @param accessToken 访问令牌
* @param sessionId 会话ID
* @param loginType 登录类型
* @return 登出是否成功
*/
public boolean logout(String accessToken, String loginType) {
public boolean logout(String sessionId, String loginType) {
// 添加跟踪ID到日志上下文
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
@ -305,7 +313,7 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
try {
// 清除相关缓存
cacheManager.evictAccessTokenCache(accessToken);
cacheManager.evictAccessTokenCache(sessionId);
// 1. 获取登录策略
LoginStrategy strategy;
@ -313,25 +321,25 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
strategy = loginStrategyFactory.getLoginStrategy(loginType);
} catch (Exception e) {
logger.error("获取登录策略失败,登录类型: {}", loginType, e);
recordLogoutAudit(accessToken, loginType, "FAILED");
recordLogoutAudit(sessionId, loginType, "FAILED");
return false;
}
// 2. 执行登出操作
boolean success;
try {
success = strategy.logout(accessToken, loginType);
success = strategy.logout(sessionId, loginType);
} catch (Exception e) {
logger.error("执行登出策略失败,登录类型: {}", loginType, e);
recordLogoutAudit(accessToken, loginType, "FAILED");
recordLogoutAudit(sessionId, loginType, "FAILED");
return false;
}
// 3. 清理登录状态
String tokenPrefix = accessToken != null ? accessToken.substring(0, Math.min(10, accessToken.length())) : "null";
String tokenPrefix = sessionId != null ? sessionId.substring(0, Math.min(10, sessionId.length())) : "null";
if (success) {
logger.info("登出操作成功,登录类型: {}, 令牌前缀: {}", loginType, tokenPrefix);
cleanupLoginStatus(accessToken);
cleanupLoginStatus(sessionId);
// 4. 更新登录统计令牌吊销
updateLoginStatisticsForRevoke(loginType);
@ -340,7 +348,7 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
}
// 5. 记录登出审计日志
recordLogoutAudit(accessToken, loginType, success ? "SUCCESS" : "FAILED");
recordLogoutAudit(sessionId, loginType, success ? "SUCCESS" : "FAILED");
return success;
} finally {
@ -389,12 +397,12 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
result.setTokenType(token.getTokenType());
// 计算剩余过期时间
// 注意这里假设accessTokenExpire存储的是日期实际应使用DateTime类型
// 注意这里假设sessionIdExpire存储的是日期实际应使用DateTime类型
long expiresIn = 7200; // 默认为2小时
result.setExpiresIn(expiresIn);
logger.info("获取登录状态成功,访问令牌前缀: {}",
result.getAccessToken() != null ? result.getAccessToken().substring(0, Math.min(10, result.getAccessToken().length())) : "null");
logger.info("获取登录状态成功,会话ID前缀: {}",
result.getSessionId() != null ? result.getSessionId().substring(0, Math.min(10, result.getSessionId().length())) : "null");
return result;
}
}
@ -425,9 +433,9 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
* @param result 登录结果
*/
public void saveLoginStatus(SalesforceLoginResult result) {
String accessToken = result.getAccessToken();
String tokenPrefix = accessToken != null ? accessToken.substring(0, Math.min(10, accessToken.length())) : "null";
logger.info("保存登录状态到缓存,访问令牌: {}", tokenPrefix + "...");
String sessionId = result.getSessionId();
String tokenPrefix = sessionId != null ? sessionId.substring(0, Math.min(10, sessionId.length())) : "null";
logger.info("保存登录状态到缓存,会话ID: {}", tokenPrefix + "...");
try {
// 实现保存登录状态到Redis缓存的逻辑
@ -436,7 +444,7 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
String cacheName = "salesforce_login_cache";
// 使用带有过期时间的put方法设置缓存
CacheUtils.put(cacheName, "current_access_token", result.getAccessToken(), expiresInSeconds, java.util.concurrent.TimeUnit.SECONDS);
CacheUtils.put(cacheName, "current_access_token", result.getSessionId(), expiresInSeconds, java.util.concurrent.TimeUnit.SECONDS);
CacheUtils.put(cacheName, "current_instance_url", result.getInstanceUrl(), expiresInSeconds, java.util.concurrent.TimeUnit.SECONDS);
CacheUtils.put(cacheName, "current_user_id", result.getUserId(), expiresInSeconds, java.util.concurrent.TimeUnit.SECONDS);
CacheUtils.put(cacheName, "current_organization_id", result.getOrganizationId(), expiresInSeconds, java.util.concurrent.TimeUnit.SECONDS);
@ -711,22 +719,22 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
* @param result 新的登录结果
*/
private void updateLoginStatus(SalesforceLoginResult result) {
String tokenPrefix = result.getAccessToken() != null ?
result.getAccessToken().substring(0, Math.min(10, result.getAccessToken().length())) : "null";
logger.info("更新登录状态,访问令牌: {}", tokenPrefix + "...");
String tokenPrefix = result.getSessionId() != null ?
result.getSessionId().substring(0, Math.min(10, result.getSessionId().length())) : "null";
logger.info("更新登录状态,会话ID: {}", tokenPrefix + "...");
try {
// 1. 根据旧的访问令牌查找令牌信息
// 1. 根据旧的会话ID查找令牌信息
// 这里简化处理实际应该从refreshToken查找
// 或者在refreshToken方法中传递旧的accessToken
// 或者在refreshToken方法中传递旧的sessionId
// 2. 创建新的令牌记录
DataiSfToken newToken = new DataiSfToken();
newToken.setUsername("unknown"); // 实际应该从用户上下文获取
newToken.setAccessToken(result.getAccessToken());
newToken.setSessionId(result.getSessionId());
newToken.setRefreshToken(result.getRefreshToken());
// 注意LocalDate只能保存日期不能保存时间实际应使用LocalDateTime类型
newToken.setAccessTokenExpire(LocalDateTime.now());
newToken.setSessionIdExpire(LocalDateTime.now());
if (result.getRefreshToken() != null) {
// 刷新令牌默认有效期30天
newToken.setRefreshTokenExpire(LocalDateTime.now().plusDays(30));
@ -771,18 +779,18 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
/**
* 清理登录状态
*
* @param accessToken 访问令牌
* @param sessionId 会话ID
*/
private void cleanupLoginStatus(String accessToken) {
String tokenPrefix = accessToken != null ? accessToken.substring(0, Math.min(10, accessToken.length())) : "null";
logger.info("清理登录状态,访问令牌: {}", tokenPrefix + "...");
private void cleanupLoginStatus(String sessionId) {
String tokenPrefix = sessionId != null ? sessionId.substring(0, Math.min(10, sessionId.length())) : "null";
logger.info("清理登录状态,会话ID: {}", tokenPrefix + "...");
try {
// 1. 查找令牌信息
// 注意这里需要根据accessToken查询令牌但当前接口只支持根据tokenId查询
// 注意这里需要根据sessionId查询令牌但当前接口只支持根据tokenId查询
// 这里简化处理查询所有令牌并遍历查找
DataiSfToken queryToken = new DataiSfToken();
queryToken.setAccessToken(accessToken);
queryToken.setSessionId(sessionId);
List<DataiSfToken> tokens = tokenService.selectDataiSfTokenList(queryToken);
if (!tokens.isEmpty()) {
@ -806,15 +814,15 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
}
// 清除相关缓存
cacheManager.evictAccessTokenCache(accessToken);
cacheManager.evictAccessTokenCache(sessionId);
if (token.getRefreshToken() != null) {
cacheManager.evictRefreshTokenCache(token.getRefreshToken());
}
}
logger.info("登录状态清理成功,访问令牌: {}", accessToken.substring(0, Math.min(10, accessToken.length())) + "...");
logger.info("登录状态清理成功,会话ID: {}", sessionId.substring(0, Math.min(10, sessionId.length())) + "...");
} catch (Exception e) {
logger.error("清理登录状态失败,访问令牌: {}", accessToken.substring(0, Math.min(10, accessToken.length())), e);
logger.error("清理登录状态失败,会话ID: {}", sessionId.substring(0, Math.min(10, sessionId.length())), e);
// 不抛出异常因为清理失败不应该影响主要流程
}
}
@ -873,11 +881,11 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
/**
* 记录登出审计日志
*
* @param accessToken 访问令牌
* @param sessionId 会话ID
* @param loginType 登录类型
* @param resultStr 操作结果
*/
private void recordLogoutAudit(String accessToken, String loginType, String resultStr) {
private void recordLogoutAudit(String sessionId, String loginType, String resultStr) {
try {
DataiSfLoginAudit audit = new DataiSfLoginAudit();
audit.setOperationType("LOGOUT");
@ -1135,6 +1143,25 @@ public class SalesforceLoginServiceImpl implements ISalesforceLoginService {
// 查询当前统计记录
DataiSfLoginStatistics query = new DataiSfLoginStatistics();
}
/**
* 登录成功后加载配置到Redis
*
* 该方法在Salesforce登录成功后调用确保最新的配置被加载到Redis缓存中
*/
private void loadConfigToRedis() {
logger.info("登录成功开始加载配置到Redis...");
try {
// 调用配置缓存管理器重置配置缓存触发配置加载到Redis
configCacheManager.resetConfigCache();
logger.info("配置加载到Redis成功");
} catch (Exception e) {
logger.error("配置加载到Redis失败: {}", e.getMessage(), e);
// 不抛出异常因为配置加载失败不应该影响登录流程
}
query.setStatDate(nowDate);
query.setStatHour(hour);
query.setLoginType(loginType);

View File

@ -37,22 +37,22 @@ public class TokenManagerImpl implements ITokenManager {
/**
* 验证令牌有效性
*
* @param accessToken 访问令牌
* @param sessionId 会话ID
* @return 是否有效
*/
@Override
public boolean validateToken(String accessToken) {
if (accessToken == null || accessToken.isEmpty()) {
public boolean validateToken(String sessionId) {
if (sessionId == null || sessionId.isEmpty()) {
logger.warn("令牌为空,无法验证");
return false;
}
String tokenPrefix = accessToken.substring(0, Math.min(10, accessToken.length()));
logger.info("验证令牌有效性,访问令牌: {}", tokenPrefix + "...");
String tokenPrefix = sessionId.substring(0, Math.min(10, sessionId.length()));
logger.info("验证令牌有效性,会话ID: {}", tokenPrefix + "...");
// 1. 根据访问令牌查找令牌信息
// 1. 根据会话ID查找令牌信息
DataiSfToken queryToken = new DataiSfToken();
queryToken.setAccessToken(accessToken);
queryToken.setSessionId(sessionId);
List<DataiSfToken> tokens = tokenService.selectDataiSfTokenList(queryToken);
// 2. 验证令牌是否存在且状态有效
@ -69,7 +69,7 @@ public class TokenManagerImpl implements ITokenManager {
}
// 3. 验证令牌是否过期
if (token.getAccessTokenExpire() != null && new Date().after(Date.from(token.getAccessTokenExpire().atZone(ZoneId.systemDefault()).toInstant()))) {
if (token.getSessionIdExpire() != null && new Date().after(Date.from(token.getSessionIdExpire().atZone(ZoneId.systemDefault()).toInstant()))) {
logger.warn("令牌已过期令牌ID: {}", token.getId());
// 更新令牌状态为过期
token.setStatus("EXPIRED");
@ -127,24 +127,24 @@ public class TokenManagerImpl implements ITokenManager {
/**
* 绑定令牌到设备/IP
*
* @param accessToken 访问令牌
* @param sessionId 会话ID
* @param deviceId 设备ID
* @param ip IP地址
*/
@Override
public void bindToken(String accessToken, String deviceId, String ip) {
if (accessToken == null || accessToken.isEmpty()) {
public void bindToken(String sessionId, String deviceId, String ip) {
if (sessionId == null || sessionId.isEmpty()) {
logger.warn("令牌为空,无法绑定");
return;
}
String tokenPrefix = accessToken.substring(0, Math.min(10, accessToken.length()));
logger.info("绑定令牌到设备/IP访问令牌: {}, 设备ID: {}, IP: {}",
String tokenPrefix = sessionId.substring(0, Math.min(10, sessionId.length()));
logger.info("绑定令牌到设备/IP会话ID: {}, 设备ID: {}, IP: {}",
tokenPrefix + "...", deviceId, ip);
// 1. 根据访问令牌查找令牌信息
// 1. 根据会话ID查找令牌信息
DataiSfToken queryToken = new DataiSfToken();
queryToken.setAccessToken(accessToken);
queryToken.setSessionId(sessionId);
List<DataiSfToken> tokens = tokenService.selectDataiSfTokenList(queryToken);
if (tokens.isEmpty()) {
@ -179,7 +179,7 @@ public class TokenManagerImpl implements ITokenManager {
binding.setStatus("ACTIVE");
binding.setBindingTime(LocalDateTime.now());
// 绑定有效期与令牌有效期一致
binding.setExpireTime(token.getAccessTokenExpire());
binding.setExpireTime(token.getSessionIdExpire());
binding.setCreateTime(new Date());
binding.setUpdateTime(new Date());

View File

@ -0,0 +1,594 @@
# datai-salesforce-auth 模块功能规划文档
## 1. 模块定位
### 1.1 模块概述
datai-salesforce-auth 是 Salesforce 认证中心模块,负责处理所有与 Salesforce 相关的身份认证、授权和会话管理功能。作为整个 Salesforce 集成体系的安全基石,为其他业务模块提供统一的认证服务。
### 1.2 设计理念
- **统一认证入口**:提供统一的认证接口,屏蔽底层认证细节
- **多策略支持**:采用策略模式,支持多种认证方式,易于扩展
- **安全第一**:多层次安全机制,保障系统安全
- **高性能**:利用 Redis 缓存,减少数据库访问
- **可观测性**:完善的日志和监控,便于问题排查
### 1.3 核心价值
- 降低其他模块的认证复杂度
- 提供统一的认证标准
- 保障系统安全性和可靠性
- 提升用户体验
---
## 2. 功能架构
### 2.1 功能分层架构
```
┌─────────────────────────────────────────────────────────┐
│ 接口层 (API Layer) │
│ RESTful API 接口、Controller 层 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 业务逻辑层 (Service Layer) │
│ 登录服务、令牌管理、会话管理、安全控制 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 策略层 (Strategy Layer) │
│ OAuth2 策略、CLI 策略、传统凭证策略、Session ID 策略 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 数据访问层 (Data Layer) │
│ 数据库访问、Redis 缓存、外部 API 调用 │
└─────────────────────────────────────────────────────────┘
```
### 2.2 功能模块划分
```
datai-salesforce-auth
├── 核心认证模块
│ ├── 登录认证
│ ├── 令牌管理
│ └── 会话管理
├── 安全控制模块
│ ├── 访问控制
│ ├── 风险检测
│ └── 审计日志
├── 管理功能模块
│ ├── 配置管理
│ ├── 用户管理
│ └── 策略管理
└── 监控分析模块
├── 登录统计
├── 性能监控
└── 健康检查
```
---
## 3. 核心功能模块
### 3.1 登录认证功能
#### 3.1.1 多种登录方式支持
| 登录方式 | 登录类型 | 适用场景 | 安全级别 |
|---------|---------|---------|---------|
| OAuth 2.0 | oauth2 | 现代应用集成、Web 应用 | 高 |
| Salesforce CLI | salesforce_cli | 开发环境、测试环境 | 中 |
| 传统凭证 | legacy_credential | 老系统集成、内部系统 | 中 |
| Session ID | session_id | 已有会话场景 | 中 |
| JWT Token | jwt_token | 微服务架构、分布式系统 | 高 |
| SAML SSO | saml_sso | 企业单点登录 | 高 |
| OAuth 2.0 JWT Bearer | oauth2_jwt_bearer | 服务间认证 | 高 |
#### 3.1.2 登录流程
```
客户端请求
参数验证
账号锁定检查
获取登录策略
执行登录认证
创建会话和令牌
缓存登录状态
记录审计日志
返回登录结果
```
#### 3.1.3 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 执行登录 | POST | /salesforce/login/execute | 执行登录操作 |
| 自动登录 | POST | /salesforce/login/auto | 使用上次登录参数自动登录 |
| 登出 | POST | /salesforce/login/logout | 退出登录 |
---
### 3.2 令牌管理功能
#### 3.2.1 令牌类型
| 令牌类型 | 用途 | 有效期 | 可刷新 |
|---------|------|--------|--------|
| 访问令牌 | 访问 Salesforce API | 短期2小时 | 是 |
| 刷新令牌 | 获取新的访问令牌 | 长期30天 | 否 |
| ID 令牌 | 获取用户信息 | 短期2小时 | 否 |
| JWT 令牌 | 服务间认证 | 可配置 | 否 |
#### 3.2.2 令牌生命周期管理
```
生成令牌 → 验证令牌 → 刷新令牌 → 吊销令牌
↓ ↓ ↓ ↓
存储 缓存 更新 清理
↓ ↓ ↓ ↓
绑定 过期检查 同步 审计
```
#### 3.2.3 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 刷新令牌 | POST | /salesforce/login/refresh-token | 刷新访问令牌 |
| 验证令牌 | GET | /salesforce/login/validate-token | 验证令牌有效性 |
| 吊销令牌 | POST | /salesforce/login/revoke-token | 吊销令牌 |
| 绑定令牌 | POST | /salesforce/login/bind-token | 绑定令牌到设备/IP |
| 检查绑定 | GET | /salesforce/login/check-binding | 检查令牌绑定状态 |
---
### 3.3 会话管理功能
#### 3.3.1 会话状态
| 状态 | 描述 | 可转换 |
|------|------|--------|
| ACTIVE | 活跃会话 | EXPIRED、REVOKED |
| EXPIRED | 已过期 | ACTIVE重新登录 |
| REVOKED | 已吊销 | ACTIVE重新登录 |
| INVALID | 无效 | ACTIVE重新登录 |
#### 3.3.2 会话管理功能
| 功能 | 描述 |
|------|------|
| 会话创建 | 登录成功后创建新会话 |
| 会话查询 | 查询当前活跃会话 |
| 会话更新 | 更新会话活动时间 |
| 会话过期 | 自动过期处理 |
| 会话清理 | 定期清理过期会话 |
| 多会话管理 | 支持用户多设备登录 |
#### 3.3.3 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 获取会话 | GET | /salesforce/login/session | 获取当前会话信息 |
| 获取登录状态 | GET | /salesforce/login/status | 获取当前登录状态 |
| 查询会话列表 | GET | /salesforce/login/sessions | 查询用户的所有会话 |
| 终止会话 | DELETE | /salesforce/login/session/{id} | 终止指定会话 |
| 终止所有会话 | DELETE | /salesforce/login/sessions | 终止用户的所有会话 |
---
## 4. 安全控制模块
### 4.1 访问控制功能
#### 4.1.1 账号锁定机制
| 锁定类型 | 触发条件 | 锁定时长 | 解锁方式 |
|---------|---------|---------|---------|
| 登录失败锁定 | 连续失败 N 次 | 可配置 | 等待过期/手动解锁 |
| 异常访问锁定 | 异常 IP/设备访问 | 可配置 | 管理员解锁 |
| 强制锁定 | 管理员操作 | 永久 | 管理员解锁 |
#### 4.1.2 访问限制
| 限制类型 | 描述 |
|---------|------|
| IP 白名单 | 仅允许特定 IP 访问 |
| IP 黑名单 | 拒绝特定 IP 访问 |
| 时间窗口限制 | 限制特定时间段访问 |
| 频率限制 | 限制单位时间内的请求次数 |
#### 4.1.3 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 检查账号锁定 | GET | /salesforce/login/check-lock | 检查账号是否被锁定 |
| 解锁账号 | POST | /salesforce/login/unlock | 解锁账号 |
| 获取失败记录 | GET | /salesforce/login/failed-logins | 获取登录失败记录 |
---
### 4.2 风险检测功能
#### 4.2.1 风险检测维度
| 检测维度 | 检测内容 | 风险等级 |
|---------|---------|---------|
| 异常登录 | 异常时间、异常地点、异常设备 | 高 |
| 暴力破解 | 短时间内多次失败尝试 | 高 |
| 令牌异常 | 令牌频繁刷新、异常使用 | 中 |
| 会话异常 | 多设备同时登录、异常活动 | 中 |
#### 4.2.2 风险处理策略
| 风险等级 | 处理策略 |
|---------|---------|
| 低 | 记录日志,告警 |
| 中 | 要求二次验证,限制操作 |
| 高 | 锁定账号,通知管理员 |
#### 4.2.3 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 风险评估 | POST | /salesforce/login/risk-assess | 评估登录风险 |
| 获取风险事件 | GET | /salesforce/login/risk-events | 获取风险事件列表 |
---
### 4.3 审计日志功能
#### 4.3.1 日志记录内容
| 日志类型 | 记录内容 |
|---------|---------|
| 登录日志 | 登录时间、IP、设备、结果 |
| 令牌日志 | 令牌创建、刷新、吊销 |
| 会话日志 | 会话创建、更新、终止 |
| 操作日志 | 关键操作记录 |
| 异常日志 | 异常事件记录 |
#### 4.3.2 日志查询功能
| 查询维度 | 支持条件 |
|---------|---------|
| 时间范围 | 起始时间、结束时间 |
| 用户 | 用户名、用户 ID |
| 操作类型 | 登录、登出、刷新等 |
| 操作结果 | 成功、失败 |
| IP 地址 | IP 地址段查询 |
#### 4.3.3 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 获取审计日志 | GET | /salesforce/login/audit-logs | 获取审计日志列表 |
| 导出审计日志 | GET | /salesforce/login/audit-logs/export | 导出审计日志 |
---
## 5. 管理功能模块
### 5.1 配置管理功能
#### 5.1.1 配置项
| 配置项 | 描述 | 默认值 |
|-------|------|--------|
| 登录超时时间 | 登录操作超时时间 | 30 秒 |
| 令牌过期时间 | 访问令牌过期时间 | 7200 秒 |
| 刷新令牌过期时间 | 刷新令牌过期时间 | 2592000 秒 |
| 会话过期时间 | 会话过期时间 | 86400 秒 |
| 失败锁定次数 | 连续失败锁定次数 | 5 次 |
| 锁定时长 | 账号锁定时长 | 1800 秒 |
| 缓存过期时间 | Redis 缓存过期时间 | 3600 秒 |
#### 5.1.2 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 获取配置 | GET | /salesforce/login/config | 获取登录配置 |
| 更新配置 | PUT | /salesforce/login/config | 更新登录配置 |
| 重置配置 | POST | /salesforce/login/config/reset | 重置为默认配置 |
---
### 5.2 用户管理功能
#### 5.2.1 用户信息管理
| 功能 | 描述 |
|------|------|
| 用户查询 | 查询 Salesforce 用户信息 |
| 用户同步 | 同步 Salesforce 用户信息 |
| 用户状态 | 管理用户登录状态 |
#### 5.2.2 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 获取用户信息 | GET | /salesforce/login/user-info | 获取当前用户信息 |
| 同步用户信息 | POST | /salesforce/login/sync-user | 同步 Salesforce 用户信息 |
---
### 5.3 策略管理功能
#### 5.3.1 策略管理
| 功能 | 描述 |
|------|------|
| 策略列表 | 获取支持的登录策略 |
| 策略启用/禁用 | 启用或禁用特定策略 |
| 策略配置 | 配置策略参数 |
#### 5.3.2 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 获取策略列表 | GET | /salesforce/login/strategies | 获取支持的登录策略 |
| 更新策略配置 | PUT | /salesforce/login/strategy/{type} | 更新策略配置 |
---
## 6. 监控分析模块
### 6.1 登录统计功能
#### 6.1.1 统计维度
| 统计维度 | 指标 |
|---------|------|
| 登录统计 | 登录次数、成功率、失败率 |
| 令牌统计 | 刷新次数、吊销次数 |
| 会话统计 | 活跃会话数、平均会话时长 |
| 用户统计 | 活跃用户数、新增用户数 |
#### 6.1.2 统计粒度
| 粒度 | 用途 |
|------|------|
| 实时统计 | 实时监控 |
| 小时统计 | 趋势分析 |
| 日统计 | 日报生成 |
| 月统计 | 月报生成 |
#### 6.1.3 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 获取登录统计 | GET | /salesforce/login/statistics | 获取登录统计数据 |
| 获取趋势数据 | GET | /salesforce/login/trends | 获取登录趋势数据 |
| 生成报表 | POST | /salesforce/login/report | 生成统计报表 |
---
### 6.2 性能监控功能
#### 6.2.1 监控指标
| 指标类型 | 监控项 |
|---------|--------|
| 性能指标 | 响应时间、吞吐量、错误率 |
| 资源指标 | CPU、内存、网络、磁盘 |
| 缓存指标 | 缓存命中率、缓存大小 |
| 数据库指标 | 连接数、查询时间、慢查询 |
#### 6.2.2 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 获取性能指标 | GET | /salesforce/login/metrics | 获取性能指标 |
| 获取健康状态 | GET | /salesforce/login/health | 获取模块健康状态 |
---
### 6.3 健康检查功能
#### 6.3.1 检查项
| 检查项 | 检查内容 |
|-------|---------|
| 数据库连接 | 数据库连接状态 |
| Redis 连接 | Redis 连接状态 |
| Salesforce API | Salesforce API 连接状态 |
| 外部依赖 | 外部服务依赖状态 |
#### 6.3.2 核心接口
| 接口名称 | 方法 | 路径 | 功能描述 |
|---------|------|------|---------|
| 健康检查 | GET | /salesforce/login/health | 执行健康检查 |
| 就绪检查 | GET | /salesforce/login/ready | 检查模块是否就绪 |
---
## 7. 数据模型
### 7.1 核心数据表
| 表名 | 用途 | 关键字段 |
|------|------|---------|
| datai_sf_login_session | 登录会话 | session_id, username, status, login_time |
| datai_sf_token | 令牌信息 | token_id, access_token, refresh_token, status |
| datai_sf_login_audit | 审计日志 | audit_id, operation_type, result, operation_time |
| datai_sf_token_binding | 令牌绑定 | binding_id, token_id, device_id, binding_ip |
| datai_sf_failed_login | 失败记录 | failed_id, username, failed_time, lock_status |
| datai_sf_login_statistics | 统计数据 | stat_id, stat_date, login_type, success_count |
### 7.2 数据流转
```
登录请求 → 参数验证 → 策略执行 → 令牌生成 → 会话创建 → 缓存更新 → 审计记录 → 统计更新
```
---
## 8. 技术架构
### 8.1 技术栈
| 层级 | 技术 |
|------|------|
| 开发语言 | Java 22 |
| 框架 | Spring Boot |
| ORM | MyBatis |
| 缓存 | Redis |
| 数据库 | MySQL |
| 安全 | Spring Security |
| 日志 | SLF4J + Logback |
### 8.2 设计模式
| 模式 | 应用场景 |
|------|---------|
| 策略模式 | 多种登录方式 |
| 工厂模式 | 登录策略创建 |
| 单例模式 | 缓存管理器 |
| 观察者模式 | 事件通知 |
| 模板方法模式 | 登录流程 |
---
## 9. 扩展性设计
### 9.1 扩展点
| 扩展点 | 扩展方式 |
|-------|---------|
| 登录策略 | 实现 LoginStrategy 接口 |
| 令牌验证 | 实现 TokenValidator 接口 |
| 风险检测 | 实现 RiskDetector 接口 |
| 审计日志 | 实现 AuditLogger 接口 |
| 统计报表 | 实现 StatisticsCollector 接口 |
### 9.2 插件机制
系统支持通过插件方式扩展功能:
- 登录策略插件
- 令牌管理插件
- 安全控制插件
- 监控分析插件
---
## 10. 性能优化
### 10.1 缓存策略
| 缓存项 | 缓存方式 | 过期时间 |
|-------|---------|---------|
| 登录状态 | Redis | 令牌过期时间 |
| 用户信息 | Redis | 1 小时 |
| 配置信息 | Redis | 30 分钟 |
| 统计数据 | Redis | 5 分钟 |
### 10.2 数据库优化
| 优化项 | 优化方式 |
|-------|---------|
| 索引优化 | 合理创建索引 |
| 查询优化 | 避免 N+1 查询 |
| 分表分库 | 按租户/时间分表 |
| 读写分离 | 主从复制 |
---
## 11. 安全设计
### 11.1 安全机制
| 安全机制 | 实现方式 |
|---------|---------|
| 数据加密 | AES 加密敏感数据 |
| 令牌签名 | JWT 签名验证 |
| HTTPS | 全链路加密传输 |
| SQL 注入防护 | 参数化查询 |
| XSS 防护 | 输入输出过滤 |
### 11.2 合规性
| 合规项 | 实现方式 |
|-------|---------|
| 数据隐私 | 数据脱敏、访问控制 |
| 审计追踪 | 完整的审计日志 |
| 数据保留 | 可配置的数据保留策略 |
---
## 12. 运维支持
### 12.1 监控告警
| 监控项 | 告警阈值 |
|-------|---------|
| 登录失败率 | > 10% |
| 响应时间 | > 3 秒 |
| 错误率 | > 5% |
| 缓存命中率 | < 80% |
### 12.2 日志管理
| 日志级别 | 用途 |
|---------|------|
| ERROR | 错误日志 |
| WARN | 警告日志 |
| INFO | 关键操作日志 |
| DEBUG | 调试日志 |
---
## 13. 版本规划
### 13.1 当前版本 (v1.0.0)
- 基础登录功能
- 令牌管理
- 会话管理
- 基础安全控制
- 审计日志
- 登录统计
### 13.2 后续版本规划
| 版本 | 计划功能 |
|------|---------|
| v1.1.0 | JWT Token 支持、SAML SSO 支持 |
| v1.2.0 | 高级风险检测、智能告警 |
| v1.3.0 | 多租户增强、权限管理 |
| v2.0.0 | 分布式认证、联邦认证 |
---
## 14. 附录
### 14.1 术语表
| 术语 | 定义 |
|------|------|
| 访问令牌 | 用于访问 Salesforce API 的短期令牌 |
| 刷新令牌 | 用于获取新的访问令牌的长期令牌 |
| 会话 | 用户登录后的活动上下文 |
| 策略 | 特定的登录认证方式 |
| 租户 | 系统的多租户隔离单位 |
### 14.2 参考资料
- Salesforce OAuth 2.0 文档
- Salesforce SOAP API 文档
- Spring Security 文档
- Redis 文档
---
**文档版本**: v1.0.0
**最后更新**: 2025-12-25
**维护者**: datai 开发团队

View File

@ -33,11 +33,10 @@ public class SalesforceConfigCacheManager {
@Autowired
private DataiConfigEnvironmentMapper environmentMapper;
/**
* 当前环境编码从配置文件或系统属性中获取
* 当前环境编码初始化时从数据库中获取
*/
@Value("${spring.profiles.active:dev}")
private String currentEnvironmentCode;
/**
@ -51,17 +50,27 @@ public class SalesforceConfigCacheManager {
@PostConstruct
public void init() {
long startTime = System.currentTimeMillis();
logger.info("开始初始化Salesforce配置...");
logger.info("[配置加载] 开始初始化Salesforce配置开始时间: {}", startTime);
try {
// 1. 初始化当前环境信息
long envInitStartTime = System.currentTimeMillis();
initCurrentEnvironment();
long envInitEndTime = System.currentTimeMillis();
logger.info("[配置加载] 环境信息初始化完成,耗时: {}ms", (envInitEndTime - envInitStartTime));
// 2. 加载配置到缓存
long configLoadStartTime = System.currentTimeMillis();
loadConfigToCache();
long configLoadEndTime = System.currentTimeMillis();
logger.info("[配置加载] 配置加载到缓存完成,耗时: {}ms", (configLoadEndTime - configLoadStartTime));
long endTime = System.currentTimeMillis();
logger.info("Salesforce配置初始化完成耗时: {}ms当前环境: {}(ID: {})",
logger.info("[配置加载] Salesforce配置初始化完成耗时: {}ms当前环境: {}(ID: {})",
(endTime - startTime), currentEnvironmentCode, currentEnvironmentId);
} catch (Exception e) {
logger.error("Salesforce配置初始化失败: {}", e.getMessage(), e);
long endTime = System.currentTimeMillis();
logger.error("[配置加载] Salesforce配置初始化失败开始时间: {}, 耗时: {}ms, 错误信息: {}",
startTime, (endTime - startTime), e.getMessage(), e);
throw new RuntimeException("Salesforce配置初始化失败", e);
}
}
@ -70,7 +79,7 @@ public class SalesforceConfigCacheManager {
* 初始化当前环境信息
*/
private void initCurrentEnvironment() {
logger.info("开始初始化当前环境信息,环境编码: {}", currentEnvironmentCode);
logger.info("[配置加载] 开始初始化当前环境信息,环境编码: {}", currentEnvironmentCode);
// 查询当前环境信息
DataiConfigEnvironment environment = new DataiConfigEnvironment();
@ -80,13 +89,15 @@ public class SalesforceConfigCacheManager {
List<DataiConfigEnvironment> environments = environmentMapper.selectDataiConfigEnvironmentList(environment);
if (environments == null || environments.isEmpty()) {
logger.warn("未找到环境编码为 {} 的激活环境使用默认环境ID: 1", currentEnvironmentCode);
logger.warn("[配置加载] 未找到环境编码为 {} 的激活环境使用默认环境ID: 1", currentEnvironmentCode);
// 默认环境ID为1确保系统能够正常启动
currentEnvironmentId = 1L;
currentEnvironmentCode = "dev";
} else {
DataiConfigEnvironment currentEnv = environments.get(0);
currentEnvironmentId = currentEnv.getId();
logger.info("成功获取当前环境信息: ID={}, 名称={}, 编码={}",
currentEnvironmentCode = currentEnv.getEnvironmentCode();
logger.info("[配置加载] 成功获取当前环境信息: ID={}, 名称={}, 编码={}",
currentEnv.getId(), currentEnv.getEnvironmentName(), currentEnv.getEnvironmentCode());
}
}
@ -95,40 +106,87 @@ public class SalesforceConfigCacheManager {
* 将配置加载到缓存
*/
public void loadConfigToCache() {
long loadStartTime = System.currentTimeMillis();
String cacheKey = getEnvironmentCacheKey();
logger.info("[配置加载] 开始加载配置到缓存,当前环境: {}(ID: {}), 缓存键: {}",
currentEnvironmentCode, currentEnvironmentId, cacheKey);
// 清空当前环境的缓存
CacheUtils.getCache(getEnvironmentCacheKey()).clear();
logger.info("开始加载配置,当前环境: {}(ID: {})", currentEnvironmentCode, currentEnvironmentId);
CacheUtils.getCache(cacheKey).clear();
logger.info("[配置加载] 已清空现有缓存: {}", cacheKey);
// 查询当前环境的所有激活配置
long dbQueryStartTime = System.currentTimeMillis();
DataiConfiguration query = new DataiConfiguration();
query.setIsActive(true); // 只加载激活状态的配置
query.setEnvironmentId(currentEnvironmentId); // 按当前环境过滤
List<DataiConfiguration> configs = configurationMapper.selectDataiConfigurationList(query);
long dbQueryEndTime = System.currentTimeMillis();
logger.info("[配置加载] 数据库查询完成,当前环境配置数量: {}, 查询耗时: {}ms",
configs.size(), (dbQueryEndTime - dbQueryStartTime));
logger.info("加载配置数量: {}", configs.size());
int successCount = 0;
int failCount = 0;
long cachePutStartTime = System.currentTimeMillis();
// 将配置存入缓存
for (DataiConfiguration config : configs) {
CacheUtils.getCache(getEnvironmentCacheKey()).put(config.getConfigKey(), config.getConfigValue());
logger.debug("配置加载到缓存: {} = {}", config.getConfigKey(), config.getConfigValue());
try {
CacheUtils.getCache(cacheKey).put(config.getConfigKey(), config.getConfigValue());
logger.debug("[配置加载] 配置加载到缓存成功: {} = {}", config.getConfigKey(), config.getConfigValue());
successCount++;
} catch (Exception e) {
logger.error("[配置加载] 配置处理异常,跳过加载: {} = {}, 错误: {}",
config.getConfigKey(), config.getConfigValue(), e.getMessage(), e);
failCount++;
}
}
long cachePutEndTime = System.currentTimeMillis();
logger.info("[配置加载] 当前环境配置处理完成,成功: {}个, 失败: {}个, 缓存写入耗时: {}ms",
successCount, failCount, (cachePutEndTime - cachePutStartTime));
// 如果当前环境没有配置加载默认环境(environmentId为null)的配置作为 fallback
if (configs.isEmpty()) {
logger.warn("当前环境 {} 没有配置,尝试加载默认环境配置", currentEnvironmentCode);
logger.warn("[配置加载] 当前环境 {} 没有配置尝试加载默认环境配置作为fallback", currentEnvironmentCode);
long defaultDbQueryStartTime = System.currentTimeMillis();
DataiConfiguration defaultQuery = new DataiConfiguration();
defaultQuery.setIsActive(true);
defaultQuery.setEnvironmentId(null); // 默认环境配置
List<DataiConfiguration> defaultConfigs = configurationMapper.selectDataiConfigurationList(defaultQuery);
long defaultDbQueryEndTime = System.currentTimeMillis();
logger.info("[配置加载] 默认环境配置查询完成,数量: {}, 查询耗时: {}ms",
defaultConfigs.size(), (defaultDbQueryEndTime - defaultDbQueryStartTime));
logger.info("加载默认环境配置数量: {}", defaultConfigs.size());
int defaultSuccessCount = 0;
int defaultFailCount = 0;
long defaultCachePutStartTime = System.currentTimeMillis();
for (DataiConfiguration config : defaultConfigs) {
CacheUtils.getCache(getEnvironmentCacheKey()).put(config.getConfigKey(), config.getConfigValue());
logger.debug("默认配置加载到缓存: {} = {}", config.getConfigKey(), config.getConfigValue());
try {
CacheUtils.getCache(cacheKey).put(config.getConfigKey(), config.getConfigValue());
logger.debug("[配置加载] 默认配置加载到缓存成功: {} = {}", config.getConfigKey(), config.getConfigValue());
defaultSuccessCount++;
} catch (Exception e) {
logger.error("[配置加载] 默认配置处理异常,跳过加载: {} = {}, 错误: {}",
config.getConfigKey(), config.getConfigValue(), e.getMessage(), e);
defaultFailCount++;
}
}
long defaultCachePutEndTime = System.currentTimeMillis();
logger.info("[配置加载] 默认环境配置处理完成,成功: {}个, 失败: {}个, 缓存写入耗时: {}ms",
defaultSuccessCount, defaultFailCount, (defaultCachePutEndTime - defaultCachePutStartTime));
successCount += defaultSuccessCount;
failCount += defaultFailCount;
}
// 统计缓存最终状态
long cacheSize = CacheUtils.getkeys(cacheKey).size();
long loadEndTime = System.currentTimeMillis();
logger.info("[配置加载] 配置加载到缓存完成,总耗时: {}ms, 总处理配置: {}个, 成功加载: {}个, 缓存最终大小: {}个, 缓存键: {}",
(loadEndTime - loadStartTime), (successCount + failCount), successCount, cacheSize, cacheKey);
}
/**
@ -143,16 +201,32 @@ public class SalesforceConfigCacheManager {
* 重置配置缓存
*/
public void resetConfigCache() {
logger.info("重置Salesforce配置缓存当前环境: {}", currentEnvironmentCode);
long resetStartTime = System.currentTimeMillis();
logger.info("[配置加载] 开始重置Salesforce配置缓存当前环境: {}, 开始时间: {}",
currentEnvironmentCode, resetStartTime);
loadConfigToCache();
long resetEndTime = System.currentTimeMillis();
logger.info("[配置加载] 配置缓存重置完成,耗时: {}ms, 当前环境: {}",
(resetEndTime - resetStartTime), currentEnvironmentCode);
}
/**
* 清空配置缓存
*/
public void clearConfigCache() {
logger.info("清空Salesforce配置缓存当前环境: {}", currentEnvironmentCode);
CacheUtils.getCache(getEnvironmentCacheKey()).clear();
long clearStartTime = System.currentTimeMillis();
String cacheKey = getEnvironmentCacheKey();
logger.info("[配置加载] 开始清空Salesforce配置缓存当前环境: {}, 缓存键: {}",
currentEnvironmentCode, cacheKey);
CacheUtils.getCache(cacheKey).clear();
long clearEndTime = System.currentTimeMillis();
long cacheSizeAfter = CacheUtils.getkeys(cacheKey).size();
logger.info("[配置加载] 配置缓存清空完成,耗时: {}ms, 当前环境: {}, 缓存键: {}, 清空后缓存大小: {}个",
(clearEndTime - clearStartTime), currentEnvironmentCode, cacheKey, cacheSizeAfter);
}
/**
@ -181,4 +255,66 @@ public class SalesforceConfigCacheManager {
public Long getCurrentEnvironmentId() {
return currentEnvironmentId;
}
/**
* 动态切换环境
*
* @param environmentCode 环境编码
* @return 切换结果
*/
public boolean switchEnvironment(String environmentCode) {
long startTime = System.currentTimeMillis();
String oldEnvironmentCode = this.currentEnvironmentCode;
Long oldEnvironmentId = this.currentEnvironmentId;
logger.info("[环境切换] 开始切换环境: {} -> {}, 开始时间: {}",
oldEnvironmentCode, environmentCode, startTime);
try {
DataiConfigEnvironment query = new DataiConfigEnvironment();
query.setEnvironmentCode(environmentCode);
query.setIsActive(true);
List<DataiConfigEnvironment> environments = environmentMapper.selectDataiConfigEnvironmentList(query);
if (environments == null || environments.isEmpty()) {
logger.error("[环境切换] 环境切换失败: 环境编码 {} 不存在或未激活", environmentCode);
return false;
}
DataiConfigEnvironment newEnvironment = environments.get(0);
if (environmentCode.equals(this.currentEnvironmentCode)) {
logger.info("[环境切换] 环境未发生变化,无需切换: {}", environmentCode);
return true;
}
String oldCacheKey = SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY + ":" + oldEnvironmentCode;
CacheUtils.getCache(oldCacheKey).clear();
logger.info("[环境切换] 已清理旧环境缓存: {}", oldCacheKey);
this.currentEnvironmentCode = environmentCode;
this.currentEnvironmentId = newEnvironment.getId();
logger.info("[环境切换] 已更新当前环境信息: 编码={}, ID={}",
this.currentEnvironmentCode, this.currentEnvironmentId);
loadConfigToCache();
long endTime = System.currentTimeMillis();
logger.info("[环境切换] 环境切换成功: {} -> {}, 耗时: {}ms",
oldEnvironmentCode, environmentCode, (endTime - startTime));
return true;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
logger.error("[环境切换] 环境切换失败: {} -> {}, 耗时: {}ms, 错误信息: {}",
oldEnvironmentCode, environmentCode, (endTime - startTime), e.getMessage(), e);
this.currentEnvironmentCode = oldEnvironmentCode;
this.currentEnvironmentId = oldEnvironmentId;
return false;
}
}
}

View File

@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.datai.common.annotation.Log;
import com.datai.common.core.controller.BaseController;
@ -22,6 +23,7 @@ import com.datai.common.utils.poi.ExcelUtil;
import com.datai.common.core.page.TableDataInfo;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
/**
* 配置环境Controller
@ -110,4 +112,44 @@ public class DataiConfigEnvironmentController extends BaseController
{
return toAjax(dataiConfigEnvironmentService.deleteDataiConfigEnvironmentByIds(ids));
}
/**
* 切换当前环境
*/
@Operation(summary = "切换当前环境")
@PreAuthorize("@ss.hasPermi('setting:environment:switch')")
@Log(title = "配置环境", businessType = BusinessType.UPDATE)
@PostMapping("/switch")
public AjaxResult switchEnvironment(
@Parameter(description = "环境编码", required = true) @RequestParam String environmentCode,
@Parameter(description = "切换原因", required = false) @RequestParam(required = false) String switchReason)
{
if (switchReason == null || switchReason.trim().isEmpty()) {
switchReason = "手动切换";
}
boolean result = dataiConfigEnvironmentService.switchEnvironment(environmentCode, switchReason);
if (result) {
DataiConfigEnvironment currentEnvironment = dataiConfigEnvironmentService.getCurrentActiveEnvironment();
return success("环境切换成功:" + currentEnvironment.getEnvironmentName());
} else {
return error("环境切换失败,请检查环境编码是否正确且已激活");
}
}
/**
* 获取当前激活的环境
*/
@Operation(summary = "获取当前激活的环境")
@PreAuthorize("@ss.hasPermi('setting:environment:query')")
@GetMapping("/current")
public AjaxResult getCurrentEnvironment()
{
DataiConfigEnvironment currentEnvironment = dataiConfigEnvironmentService.getCurrentActiveEnvironment();
if (currentEnvironment == null) {
return error("未找到当前激活的环境");
}
return success(currentEnvironment);
}
}

View File

@ -137,15 +137,5 @@ public class DataiConfigurationController extends BaseController
return success("配置缓存状态正常");
}
/**
* 验证配置值合法性
*/
@Operation(summary = "验证配置值合法性")
@PreAuthorize("@ss.hasPermi('setting:configuration:validate')")
@PostMapping("/validate")
public AjaxResult validateConfig(@RequestBody DataiConfiguration config)
{
boolean isValid = dataiConfigurationService.validateConfigValue(config);
return isValid ? success("配置值验证通过") : error("配置值验证失败");
}
}

View File

@ -60,6 +60,11 @@ public class DataiConfiguration extends BaseEntity
@Schema(title = "是否激活")
@Excel(name = "是否激活")
private Boolean isActive;
/** 配置版本号 */
@Schema(title = "配置版本号")
@Excel(name = "配置版本号")
private Integer version;
public void setId(Long id)
{
this.id = id;
@ -158,6 +163,16 @@ public class DataiConfiguration extends BaseEntity
return isActive;
}
public void setVersion(Integer version)
{
this.version = version;
}
public Integer getVersion()
{
return version;
}
@Override
@ -172,6 +187,7 @@ public class DataiConfiguration extends BaseEntity
.append("isEncrypted", getIsEncrypted())
.append("description", getDescription())
.append("isActive", getIsActive())
.append("version", getVersion())
.append("remark", getRemark())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())

View File

@ -0,0 +1,50 @@
package com.datai.setting.event;
import org.springframework.context.ApplicationEvent;
import com.datai.setting.domain.DataiConfigEnvironment;
/**
* 环境切换事件
* 当系统环境发生切换时触发此事件
*
* @author datai
* @date 2025-12-25
*/
public class EnvironmentSwitchEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private final DataiConfigEnvironment oldEnvironment;
private final DataiConfigEnvironment newEnvironment;
private final String switchReason;
public EnvironmentSwitchEvent(Object source, DataiConfigEnvironment oldEnvironment,
DataiConfigEnvironment newEnvironment, String switchReason) {
super(source);
this.oldEnvironment = oldEnvironment;
this.newEnvironment = newEnvironment;
this.switchReason = switchReason;
}
public DataiConfigEnvironment getOldEnvironment() {
return oldEnvironment;
}
public DataiConfigEnvironment getNewEnvironment() {
return newEnvironment;
}
public String getSwitchReason() {
return switchReason;
}
@Override
public String toString() {
return "EnvironmentSwitchEvent{" +
"oldEnvironment=" + (oldEnvironment != null ? oldEnvironment.getEnvironmentCode() : "null") +
", newEnvironment=" + (newEnvironment != null ? newEnvironment.getEnvironmentCode() : "null") +
", switchReason='" + switchReason + '\'' +
'}';
}
}

View File

@ -0,0 +1,100 @@
package com.datai.setting.event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.datai.common.utils.DateUtils;
import com.datai.common.utils.SecurityUtils;
import com.datai.setting.config.SalesforceConfigCacheManager;
import com.datai.setting.domain.DataiConfigEnvironment;
import com.datai.setting.domain.DataiConfiguration;
import com.datai.setting.service.IDataiConfigurationService;
import java.util.List;
/**
* 环境切换监听器
* 监听环境切换事件并执行相应的处理逻辑
*
* @author datai
* @date 2025-12-25
*/
@Component
public class EnvironmentSwitchListener {
private static final Logger logger = LoggerFactory.getLogger(EnvironmentSwitchListener.class);
@Autowired
private SalesforceConfigCacheManager cacheManager;
@Autowired
private IDataiConfigurationService configurationService;
@Autowired
private IDataiConfigurationService configService;
@Async
@EventListener
public void handleEnvironmentSwitch(EnvironmentSwitchEvent event) {
long startTime = System.currentTimeMillis();
DataiConfigEnvironment oldEnv = event.getOldEnvironment();
DataiConfigEnvironment newEnv = event.getNewEnvironment();
String switchReason = event.getSwitchReason();
logger.info("[环境切换] 开始处理环境切换事件: {} -> {}, 原因: {}, 开始时间: {}",
oldEnv != null ? oldEnv.getEnvironmentCode() : "null",
newEnv != null ? newEnv.getEnvironmentCode() : "null",
switchReason,
DateUtils.getNowDate());
try {
String username = SecurityUtils.getLoginUser() != null ?
SecurityUtils.getLoginUser().getUsername() : "system";
logger.info("[环境切换] 操作用户: {}", username);
logger.info("[环境切换] 切换原因: {}", switchReason);
if (oldEnv != null) {
logger.info("[环境切换] 旧环境信息: ID={}, 名称={}, 编码={}",
oldEnv.getId(), oldEnv.getEnvironmentName(), oldEnv.getEnvironmentCode());
}
if (newEnv != null) {
logger.info("[环境切换] 新环境信息: ID={}, 名称={}, 编码={}",
newEnv.getId(), newEnv.getEnvironmentName(), newEnv.getEnvironmentCode());
}
logger.info("[环境切换] 开始执行环境切换...");
if (newEnv != null) {
boolean switchSuccess = cacheManager.switchEnvironment(newEnv.getEnvironmentCode());
if (switchSuccess) {
logger.info("[环境切换] 环境切换成功: {}", newEnv.getEnvironmentCode());
DataiConfiguration query = new DataiConfiguration();
query.setEnvironmentId(newEnv.getId());
query.setIsActive(true);
List<DataiConfiguration> configs = configService.selectDataiConfigurationList(query);
logger.info("[环境切换] 新环境配置数量: {}", configs.size());
} else {
logger.error("[环境切换] 环境切换失败: {}", newEnv.getEnvironmentCode());
throw new RuntimeException("环境切换失败");
}
}
long endTime = System.currentTimeMillis();
logger.info("[环境切换] 环境切换处理完成,总耗时: {}ms", (endTime - startTime));
logger.info("[环境切换] 当前环境: {}", cacheManager.getCurrentEnvironmentCode());
} catch (Exception e) {
long endTime = System.currentTimeMillis();
logger.error("[环境切换] 环境切换处理失败,开始时间: {}, 耗时: {}ms, 错误信息: {}",
startTime, (endTime - startTime), e.getMessage(), e);
throw new RuntimeException("环境切换处理失败", e);
}
}
}

View File

@ -58,4 +58,20 @@ public interface IDataiConfigEnvironmentService
* @return 结果
*/
public int deleteDataiConfigEnvironmentById(Long id);
/**
* 切换当前环境
*
* @param environmentCode 环境编码
* @param switchReason 切换原因
* @return 切换结果
*/
public boolean switchEnvironment(String environmentCode, String switchReason);
/**
* 获取当前激活的环境
*
* @return 当前激活的环境
*/
public DataiConfigEnvironment getCurrentActiveEnvironment();
}

View File

@ -83,11 +83,4 @@ public interface IDataiConfigurationService
*/
public void resetConfigCache();
/**
* 验证配置值合法性
*
* @param dataiConfiguration 配置
* @return 验证结果
*/
public boolean validateConfigValue(DataiConfiguration dataiConfiguration);
}

View File

@ -1,14 +1,19 @@
package com.datai.setting.service.impl;
import java.util.List;
import com.datai.common.utils.DateUtils;
import com.datai.common.utils.SecurityUtils;
import com.datai.common.utils.DateUtils;
import com.datai.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import com.datai.setting.mapper.DataiConfigEnvironmentMapper;
import com.datai.setting.domain.DataiConfigEnvironment;
import com.datai.setting.service.IDataiConfigEnvironmentService;
import com.datai.setting.config.SalesforceConfigCacheManager;
import com.datai.setting.event.EnvironmentSwitchEvent;
import com.datai.common.core.domain.model.LoginUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
@ -19,9 +24,17 @@ import com.datai.common.core.domain.model.LoginUser;
*/
@Service
public class DataiConfigEnvironmentServiceImpl implements IDataiConfigEnvironmentService {
private static final Logger logger = LoggerFactory.getLogger(DataiConfigEnvironmentServiceImpl.class);
@Autowired
private DataiConfigEnvironmentMapper dataiConfigEnvironmentMapper;
@Autowired
private SalesforceConfigCacheManager cacheManager;
@Autowired
private ApplicationContext applicationContext;
/**
* 查询配置环境
*
@ -105,4 +118,55 @@ public class DataiConfigEnvironmentServiceImpl implements IDataiConfigEnvironmen
{
return dataiConfigEnvironmentMapper.deleteDataiConfigEnvironmentById(id);
}
@Override
public boolean switchEnvironment(String environmentCode, String switchReason) {
logger.info("[环境切换] 开始切换环境: {}, 原因: {}", environmentCode, switchReason);
try {
DataiConfigEnvironment oldEnvironment = getCurrentActiveEnvironment();
boolean switchResult = cacheManager.switchEnvironment(environmentCode);
if (switchResult) {
DataiConfigEnvironment newEnvironment = getCurrentActiveEnvironment();
applicationContext.publishEvent(
new EnvironmentSwitchEvent(this, oldEnvironment, newEnvironment, switchReason)
);
logger.info("[环境切换] 环境切换成功: {} -> {}",
oldEnvironment != null ? oldEnvironment.getEnvironmentCode() : "null",
newEnvironment != null ? newEnvironment.getEnvironmentCode() : "null");
} else {
logger.error("[环境切换] 环境切换失败: {}", environmentCode);
}
return switchResult;
} catch (Exception e) {
logger.error("[环境切换] 环境切换异常: {}, 错误信息: {}", environmentCode, e.getMessage(), e);
return false;
}
}
@Override
public DataiConfigEnvironment getCurrentActiveEnvironment() {
String currentEnvironmentCode = cacheManager.getCurrentEnvironmentCode();
if (currentEnvironmentCode == null) {
logger.warn("[环境查询] 当前环境编码为空");
return null;
}
DataiConfigEnvironment query = new DataiConfigEnvironment();
query.setEnvironmentCode(currentEnvironmentCode);
query.setIsActive(true);
List<DataiConfigEnvironment> environments = dataiConfigEnvironmentMapper.selectDataiConfigEnvironmentList(query);
if (environments != null && !environments.isEmpty()) {
return environments.get(0);
}
logger.warn("[环境查询] 未找到激活的环境: {}", currentEnvironmentCode);
return null;
}
}

View File

@ -63,8 +63,11 @@ public class DataiConfigurationServiceImpl implements IDataiConfigurationService
*/
@Override
public String selectConfigValueByKey(String configKey) {
// 先从缓存获取
String configValue = CacheUtils.get(SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY, configKey, String.class);
// 获取环境隔离的缓存键 (格式: SALESFORCE_CONFIG_CACHE_KEY:environmentCode)
String cacheKey = SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY + ":" + SalesforceConfigCacheManager.getCurrentEnvironmentCode();
// 先从缓存获取 - 缓存优先策略
String configValue = CacheUtils.get(cacheKey, configKey, String.class);
if (configValue != null) {
return configValue;
}
@ -73,12 +76,13 @@ public class DataiConfigurationServiceImpl implements IDataiConfigurationService
DataiConfiguration query = new DataiConfiguration();
query.setConfigKey(configKey);
query.setIsActive(true);
query.setEnvironmentId(SalesforceConfigCacheManager.getCurrentEnvironmentId()); // 按当前环境过滤
List<DataiConfiguration> configs = dataiConfigurationMapper.selectDataiConfigurationList(query);
if (!configs.isEmpty()) {
configValue = configs.get(0).getConfigValue();
// 更新缓存
CacheUtils.getCache(SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY).put(configKey, configValue);
// 更新缓存 - 写回到缓存
CacheUtils.getCache(cacheKey).put(configKey, configValue);
return configValue;
}
@ -106,12 +110,6 @@ public class DataiConfigurationServiceImpl implements IDataiConfigurationService
@Override
public int insertDataiConfiguration(DataiConfiguration dataiConfiguration)
{
// 验证配置值合法性
if (!validateConfigValue(dataiConfiguration)) {
logger.error("配置值验证失败: {}", dataiConfiguration.getConfigKey());
return 0;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
String username = loginUser.getUsername();
@ -120,14 +118,26 @@ public class DataiConfigurationServiceImpl implements IDataiConfigurationService
dataiConfiguration.setCreateBy(username);
dataiConfiguration.setUpdateBy(username);
// 设置当前环境ID
dataiConfiguration.setEnvironmentId(SalesforceConfigCacheManager.getCurrentEnvironmentId());
// 设置初始版本号为1
dataiConfiguration.setVersion(1);
// 1. 先写入数据库
int result = dataiConfigurationMapper.insertDataiConfiguration(dataiConfiguration);
if (result > 0) {
// 更新缓存
CacheUtils.getCache(SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY)
// 2. 再更新缓存 - 缓存优先策略数据库变更后立即更新缓存
String cacheKey = SalesforceConfigCacheManager.getCurrentEnvironmentCode() + ":" + SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY;
CacheUtils.getCache(cacheKey)
.put(dataiConfiguration.getConfigKey(), dataiConfiguration.getConfigValue());
// 发布配置变更事件
// 3. 发布配置变更事件
publishConfigChangeEvent(dataiConfiguration, "CREATE", null, dataiConfiguration.getConfigValue());
logger.info("[缓存更新] 新增配置后更新缓存成功: {} = {}, 版本: {}, 环境: {}",
dataiConfiguration.getConfigKey(), dataiConfiguration.getConfigValue(),
dataiConfiguration.getVersion(), SalesforceConfigCacheManager.getCurrentEnvironmentCode());
}
return result;
@ -142,12 +152,6 @@ public class DataiConfigurationServiceImpl implements IDataiConfigurationService
@Override
public int updateDataiConfiguration(DataiConfiguration dataiConfiguration)
{
// 验证配置值合法性
if (!validateConfigValue(dataiConfiguration)) {
logger.error("配置值验证失败: {}", dataiConfiguration.getConfigKey());
return 0;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
String username = loginUser.getUsername();
@ -157,21 +161,39 @@ public class DataiConfigurationServiceImpl implements IDataiConfigurationService
// 获取旧配置用于审计日志和缓存清理
DataiConfiguration oldConfig = selectDataiConfigurationById(dataiConfiguration.getId());
String oldValue = oldConfig != null ? oldConfig.getConfigValue() : null;
String oldConfigKey = oldConfig != null ? oldConfig.getConfigKey() : null;
// 版本自动递增如果是更新操作版本号+1
if (oldConfig != null) {
Integer currentVersion = oldConfig.getVersion();
dataiConfiguration.setVersion(currentVersion != null ? currentVersion + 1 : 1);
} else {
dataiConfiguration.setVersion(1); // 防止旧记录没有版本号
}
// 1. 先更新数据库
int result = dataiConfigurationMapper.updateDataiConfiguration(dataiConfiguration);
if (result > 0) {
// 如果配置键发生变化清理旧键的缓存
if (oldConfig != null && !oldConfig.getConfigKey().equals(dataiConfiguration.getConfigKey())) {
CacheUtils.getCache(SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY)
.evict(oldConfig.getConfigKey());
String cacheKey = SalesforceConfigCacheManager.getCurrentEnvironmentCode() + ":" + SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY;
// 2. 缓存更新策略先删除旧键再添加新键
if (oldConfig != null && !oldConfigKey.equals(dataiConfiguration.getConfigKey())) {
// 如果配置键发生变化清理旧键的缓存
CacheUtils.getCache(cacheKey).evict(oldConfigKey);
logger.info("[缓存更新] 配置键变更,已清理旧缓存键: {}, 环境: {}",
oldConfigKey, SalesforceConfigCacheManager.getCurrentEnvironmentCode());
}
// 更新缓存
CacheUtils.getCache(SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY)
// 3. 更新缓存 - 缓存优先策略数据库变更后立即更新缓存
CacheUtils.getCache(cacheKey)
.put(dataiConfiguration.getConfigKey(), dataiConfiguration.getConfigValue());
// 发布配置变更事件
// 4. 发布配置变更事件
publishConfigChangeEvent(dataiConfiguration, "UPDATE", oldValue, dataiConfiguration.getConfigValue());
logger.info("[缓存更新] 修改配置后更新缓存成功: {} -> {} = {}, 版本: {}, 环境: {}",
oldValue, dataiConfiguration.getConfigKey(), dataiConfiguration.getConfigValue(),
dataiConfiguration.getVersion(), SalesforceConfigCacheManager.getCurrentEnvironmentCode());
}
return result;
@ -187,16 +209,23 @@ public class DataiConfigurationServiceImpl implements IDataiConfigurationService
public int deleteDataiConfigurationByIds(Long[] configIds)
{
int result = 0;
String cacheKey = SalesforceConfigCacheManager.getCurrentEnvironmentCode() + ":" + SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY;
for (Long configId : configIds) {
DataiConfiguration config = selectDataiConfigurationById(configId);
if (config != null) {
String oldValue = config.getConfigValue();
String configKeyToDelete = config.getConfigKey();
// 1. 先删除数据库记录
result += dataiConfigurationMapper.deleteDataiConfigurationById(configId);
// 删除缓存
CacheUtils.getCache(SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY)
.evict(config.getConfigKey());
// 2. 再删除缓存 - 缓存优先策略数据库变更后立即清理缓存
CacheUtils.getCache(cacheKey).evict(configKeyToDelete);
logger.info("[缓存更新] 批量删除配置后清理缓存: {}, 环境: {}",
configKeyToDelete, SalesforceConfigCacheManager.getCurrentEnvironmentCode());
// 发布配置变更事件
// 3. 发布配置变更事件
publishConfigChangeEvent(config, "DELETE", oldValue, null);
}
}
@ -218,13 +247,18 @@ public class DataiConfigurationServiceImpl implements IDataiConfigurationService
}
String oldValue = config.getConfigValue();
String configKeyToDelete = config.getConfigKey();
String cacheKey = SalesforceConfigCacheManager.getCurrentEnvironmentCode() + ":" + SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY;
// 1. 先删除数据库记录
int result = dataiConfigurationMapper.deleteDataiConfigurationById(configId);
if (result > 0) {
// 删除缓存
CacheUtils.getCache(SalesforceConfigConstants.SALESFORCE_CONFIG_CACHE_KEY)
.evict(config.getConfigKey());
// 2. 再删除缓存 - 缓存优先策略数据库变更后立即清理缓存
CacheUtils.getCache(cacheKey).evict(configKeyToDelete);
logger.info("[缓存更新] 删除配置后清理缓存: {}, 环境: {}",
configKeyToDelete, SalesforceConfigCacheManager.getCurrentEnvironmentCode());
// 发布配置变更事件
// 3. 发布配置变更事件
publishConfigChangeEvent(config, "DELETE", oldValue, null);
}
@ -255,27 +289,7 @@ public class DataiConfigurationServiceImpl implements IDataiConfigurationService
SalesforceConfigCacheManager.resetConfigCache();
}
@Autowired
private IDataiConfigValidationRuleService configValidationRuleService;
/**
* 验证配置值合法性
*
* @param dataiConfiguration 配置
* @return 验证结果
*/
@Override
public boolean validateConfigValue(DataiConfiguration dataiConfiguration) {
// 1. 基础验证配置键不能为空
String configKey = dataiConfiguration.getConfigKey();
if (configKey == null || configKey.isEmpty()) {
logger.error("配置键不能为空");
return false;
}
// 2. 使用新的配置验证规则服务进行验证
return configValidationRuleService.validateConfigIntegrity(dataiConfiguration);
}
/**
* 发布配置变更事件

View File

@ -14,6 +14,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="isEncrypted" column="is_encrypted" />
<result property="description" column="description" />
<result property="isActive" column="is_active" />
<result property="version" column="version" />
<result property="remark" column="remark" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
@ -32,6 +33,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
dc.is_encrypted,
dc.description,
dc.is_active,
dc.version,
dc.remark,
dc.create_by,
dc.create_time,
@ -70,6 +72,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="isEncrypted != null">is_encrypted,</if>
<if test="description != null">description,</if>
<if test="isActive != null">is_active,</if>
<if test="version != null">version,</if>
<if test="remark != null">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="createTime != null">create_time,</if>
@ -85,6 +88,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="isEncrypted != null">#{isEncrypted},</if>
<if test="description != null">#{description},</if>
<if test="isActive != null">#{isActive},</if>
<if test="version != null">#{version},</if>
<if test="remark != null">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
@ -104,6 +108,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="isEncrypted != null">is_encrypted = #{isEncrypted},</if>
<if test="description != null">description = #{description},</if>
<if test="isActive != null">is_active = #{isActive},</if>
<if test="version != null">version = #{version},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="createBy != null and createBy != ''">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>