新增 :演示模式

This commit is contained in:
MMS 2025-06-19 01:15:13 +08:00
parent 61e32cc129
commit 5fb03face8
20 changed files with 345 additions and 27 deletions

View File

@ -61,6 +61,7 @@ public class AuthController extends BaseController {
SysUserVo sysUser = sysUserService.getUserRoleAnfFunctionInfo(LoginObject.getLoginId());
RedisUtil.setCacheObject(RedisConstant.ADMIN_TENANT_KEY+sysUser.getUserId(),sysUser.getTenantId(), Duration.ofHours(24));
RedisUtil.setCacheObject(RedisConstant.ADMIN_KEY+sysUser.getUserId(),sysUser, Duration.ofHours(24));
RedisUtil.setCacheObject(RedisConstant.ADMIN_NAME+sysUser.getUserId(),sysUser.getUserName(), Duration.ofHours(24));
ajax.put(Constants.USERINFO, sysUserService.getUserInfo(sysUser));
if(LoginObject.isLogin()){
//给浏览器端设置一个 Cookie clientKey值 3天

View File

@ -26,5 +26,6 @@ public interface SysUserMapper extends BaseMapperPlus<SysUser, SysUserVo> {
@InterceptorIgnore(tenantLine = "true")
int updateRsa(@Param("tenantId") String tenantId, @Param("userId") String userId, @Param("publicKey") String publicKey,@Param("priverKey") String priverKey);
@InterceptorIgnore(tenantLine = "true")
int updateIpById(SysUser sysUser);
}

View File

@ -147,7 +147,7 @@ public class SysLoginServiceImpl implements SysLoginService {
public void updateSysUser(HttpServletRequest request,SysUser sysUser) {
sysUser.setLoginIp(IPUtil.getIp(request));
sysUser.setLoginDate(new Date());
sysUserMapper.updateById(sysUser);
sysUserMapper.updateIpById(sysUser);
}

View File

@ -15,6 +15,16 @@ sxpcwlkj:
# 开启验证码: true开启 false 关闭
isOpenCaptcha: false
# 演示模式配置
demo:
mode:
enabled: false # 默认开启演示模式
message-template: "演示模式下禁止{operation}操作 [实体: {entity}, 方法: {method}]"
allowed-users: # 白名单用户
- admin
allowed-methods:
- updateIpById
--- # 验证码配置
captcha:
# 是否开启验证码: true 开启 false 关闭

View File

@ -6,6 +6,10 @@
UPDATE `sys_user` SET `public_key`=#{publicKey} , `private_key`=#{priverKey} WHERE `tenant_id`=#{tenantId} AND `user_id`=#{userId}
</update>
<update id="updateIpById">
UPDATE `sys_user` SET `login_ip`=#{loginIp} , `login_date`=#{loginDate} WHERE `tenant_id`=#{tenantId} AND `user_id`=#{userId}
</update>
<select id="selectByUserName" resultType="com.sxpcwlkj.system.entity.SysUser">
SELECT * FROM `sys_user` WHERE `user_name`=#{userName} LIMIT 1
</select>

View File

@ -170,4 +170,15 @@ public class LoginObject<T> {
}
public static String getLoginUserName() {
try {
String id=getLoginId();
if(id==null) {
return null;
}
return RedisUtil.getCacheObject(RedisConstant.ADMIN_NAME + id);
}catch (NotWebContextException e){
return null;
}
}
}

View File

@ -1,14 +1,27 @@
package com.sxpcwlkj.common.exception;
/**
* @ClassName DemoModeException
* @Description TODO
* @Author 西决
* @Date 2022/12/25 11:42
*/
public class DemoModeException extends RuntimeException {
private static final long serialVersionUID = 1L;
import lombok.Getter;
public DemoModeException() {
import java.io.Serial;
/**
* DemoModeException
* @Author 西决
*/
@Getter
public class DemoModeException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
// 添加错误代码字段
private final int errorCode;
public DemoModeException(String message) {
this(403100, message);
}
public DemoModeException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}

View File

@ -1,7 +1,13 @@
package com.sxpcwlkj.common.exception;
import java.io.Serial;
/**
* @author xijue
*/
public class LoginException extends RuntimeException{
@Serial
private static final long serialVersionUID = 1L;
public LoginException(String message) {

View File

@ -1,6 +1,11 @@
package com.sxpcwlkj.common.exception;
import java.io.Serial;
/**
* @author xijue
*/
public class TenantException extends RuntimeException{
@Serial
private static final long serialVersionUID = 1L;
public TenantException(Throwable e) {

View File

@ -3,6 +3,8 @@ package com.sxpcwlkj.common.exception;
import com.sxpcwlkj.common.enums.ErrorCodeEnum;
import lombok.Setter;
import java.io.Serial;
/**
* 用户登陆信息过期异常
* @author xijue
@ -10,6 +12,7 @@ import lombok.Setter;
@Setter
public class TokenExpireException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private String message;

View File

@ -1,11 +1,12 @@
package com.sxpcwlkj.common.exception;
import java.io.Serial;
/**
* @ClassName UtilException
* @Description TODO
* @Author 西决
* @author xijue
*/
public class UtilException extends RuntimeException {
@Serial
private static final long serialVersionUID = 8247610319171014183L;
public UtilException(Throwable e) {

View File

@ -0,0 +1,30 @@
package com.sxpcwlkj.common.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
* @author xijue
*/
@Data
@Component
@ConfigurationProperties(prefix = "demo.mode")
public class DemoModeProperties {
private boolean enabled;
private String messageTemplate;
// 白名单用户允许在演示模式下修改
private Set<String> allowedUsers;
// 白名单方法允许在演示模式下修改
private Set<String> allowedMethods;
public boolean isAllowedUser(String username) {
return allowedUsers.contains(username);
}
public boolean isAllowedMethod(String methodName) {
return allowedMethods.contains(methodName);
}
}

View File

@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerIntercept
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.sxpcwlkj.authority.LoginObject;
import com.sxpcwlkj.common.properties.TenantProperties;
import com.sxpcwlkj.datasource.handler.DemoModeInterceptor;
import lombok.RequiredArgsConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
@ -35,12 +36,14 @@ import java.util.Objects;
@Configuration
public class MybatisPlusConfig {
private final TenantProperties tenantProperties;
private final DemoModeInterceptor demoModeInterceptor;
//插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加演示模式拦截器
interceptor.addInnerInterceptor(demoModeInterceptor);
//多租户插件
if (tenantProperties.getEnable()) {

View File

@ -0,0 +1,21 @@
package com.sxpcwlkj.datasource.handler;
/**
* 创建上下文持有者
* @author xijue
*/
public class DemoModeContextHolder {
private static final ThreadLocal<Boolean> DEMO_MODE_DISABLED = new ThreadLocal<>();
public static void setDemoModeDisabled(boolean disabled) {
DEMO_MODE_DISABLED.set(disabled);
}
public static Boolean isDemoModeDisabled() {
return DEMO_MODE_DISABLED.get();
}
public static void clear() {
DEMO_MODE_DISABLED.remove();
}
}

View File

@ -0,0 +1,149 @@
package com.sxpcwlkj.datasource.handler;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.sxpcwlkj.authority.LoginObject;
import com.sxpcwlkj.common.exception.DemoModeException;
import com.sxpcwlkj.common.properties.DemoModeProperties;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* @author xijue
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
public class DemoModeInterceptor implements InnerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(DemoModeInterceptor.class);
private final DemoModeProperties demoModeProperties;
public DemoModeInterceptor(DemoModeProperties demoModeProperties) {
this.demoModeProperties = demoModeProperties;
}
@Override
public boolean willDoUpdate(Executor executor, MappedStatement ms, Object parameter) {
// 1. 检查是否演示模式
if (!demoModeProperties.isEnabled()) {
return true;
}
// 2. 检查超级管理员放行
if(LoginObject.isLogin()){
if(LoginObject.getLoginSuper()){
return true;
}
if(demoModeProperties.isAllowedUser(LoginObject.getLoginUserName())){
return true;
}
}
// 3.如果是白名单方法允许执行
if (demoModeProperties.isAllowedMethod(getMethodName(ms.getId()))) {
return true;
}
// 4. 检查是否是修改操作
if (!isModifyOperation(ms)) {
return true;
}
// 5. 生成详细错误信息
String errorMessage = generateErrorMessage(ms);
// 6. 记录拦截日志
logger.warn("拦截演示模式下的修改操作: [{}] {}",
ms.getSqlCommandType(),
ms.getId());
// 7. 抛出自定义异常
throw new DemoModeException(403100, errorMessage);
}
// 判断是否是修改操作
private boolean isModifyOperation(MappedStatement ms) {
SqlCommandType commandType = ms.getSqlCommandType();
return commandType == SqlCommandType.INSERT ||
commandType == SqlCommandType.UPDATE ||
commandType == SqlCommandType.DELETE;
}
// 生成详细的错误消息
private String generateErrorMessage(MappedStatement ms) {
// 获取操作类型
String operation = getOperationType(ms.getSqlCommandType());
// 获取实体名称
String entity = getEntityName(ms.getId());
// 获取方法名称
String method = getMethodName(ms.getId());
// 使用配置中的消息模板
String template = demoModeProperties.getMessageTemplate();
if (template == null || template.isEmpty()) {
template = "演示模式下禁止{operation}操作 [实体: {entity}, 方法: {method}]";
}
// 替换占位符
return template
.replace("{operation}", operation)
.replace("{entity}", entity)
.replace("{method}", method);
}
// 获取操作类型中文描述
private String getOperationType(SqlCommandType commandType) {
switch (commandType) {
case INSERT: return "新增";
case UPDATE: return "更新";
case DELETE: return "删除";
default: return "修改";
}
}
// 从Mapper方法名提取实体名
private String getEntityName(String statementId) {
if (statementId == null) return "未知实体";
// 示例: com.sxpcwlkj.system.mapper.SysNoticeMapper.updateById
String[] parts = statementId.split("\\.");
if (parts.length > 1) {
String className = parts[parts.length - 2];
if (className.endsWith("Mapper")) {
return className.substring(0, className.length() - 6);
}
return className;
}
return statementId;
}
// 获取方法名
private String getMethodName(String statementId) {
if (statementId == null) return "未知方法";
int lastDot = statementId.lastIndexOf('.');
if (lastDot > 0 && lastDot < statementId.length() - 1) {
return statementId.substring(lastDot + 1);
}
return statementId;
}
}

View File

@ -55,6 +55,9 @@ public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
throw new MmsException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
}
}
/**
* 获取租户号
*/
private String getValidTenantId() {
String tenantId = LoginObject.getLoginTenant();
return tenantId != null ? tenantId : "default_tenant";
@ -76,8 +79,6 @@ public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
}
}
/**
* 获取登录用户名
*/
}

View File

@ -9,7 +9,9 @@ import com.sxpcwlkj.common.utils.R;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.exceptions.PersistenceException;
import org.mybatis.spring.MyBatisSystemException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
@ -34,7 +36,14 @@ import java.util.Objects;
@Slf4j
@RestControllerAdvice
public class GlobalException {
// 获取异常的根本原因
private Throwable getRootCause(Throwable throwable) {
Throwable rootCause = throwable;
while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
rootCause = rootCause.getCause();
}
return rootCause;
}
/**
* 根据错误码匹配
*/
@ -46,6 +55,14 @@ public class GlobalException {
return map;
}
// 专门处理演示模式异常
@ExceptionHandler(DemoModeException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public R<Void> handleDemoModeException(DemoModeException e) {
log.error("演示模式拦截操作: {}", e.getMessage());
return R.fail(e.getErrorCode(), e.getMessage());
}
@ExceptionHandler(AccessDeniedException.class)
public R<Void> handleAccessDeniedException(AccessDeniedException e,
HttpServletRequest request) {
@ -138,9 +155,14 @@ public class GlobalException {
*/
@ExceptionHandler(MyBatisSystemException.class)
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
Throwable rootCause = getRootCause(e);
// 2.1 如果是演示模式异常按演示模式异常处理
if (rootCause instanceof DemoModeException) {
return handleDemoModeException((DemoModeException) rootCause);
}
String requestUrl = request.getRequestURI();
String message = e.getMessage();
if (message.contains("CannotFindDataSourceException")) {
if (message!=null && message.contains("CannotFindDataSourceException")) {
log.error("请求地址'{}', 未找到数据源", requestUrl);
return R.fail("未找到数据源,请联系管理员确认");
}
@ -148,6 +170,38 @@ public class GlobalException {
return R.fail(message);
}
// 3. 处理数据访问异常
@ExceptionHandler(DataAccessException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R<Void> handleDataAccessException(DataAccessException e) {
Throwable rootCause = getRootCause(e);
// 3.1 如果是演示模式异常按演示模式异常处理
if (rootCause instanceof DemoModeException) {
return handleDemoModeException((DemoModeException) rootCause);
}
// 3.2 其他数据访问异常
log.error("数据访问异常", e);
return R.fail(500200, "数据库访问错误");
}
// 4. 处理PersistenceException
@ExceptionHandler(PersistenceException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R<?> handlePersistenceException(PersistenceException e) {
Throwable rootCause = getRootCause(e);
// 4.1 如果是演示模式异常按演示模式异常处理
if (rootCause instanceof DemoModeException) {
return handleDemoModeException((DemoModeException) rootCause);
}
// 4.2 其他持久化异常
log.error("MyBatis持久化异常", e);
return R.fail(500300, "持久化操作失败");
}
/**
* 拦截未知的运行时异常
@ -209,13 +263,7 @@ public class GlobalException {
}
/**
* 演示模式异常
*/
@ExceptionHandler(DemoModeException.class)
public R<Void> handleDemoModeException(DemoModeException e) {
return R.fail("演示模式,不允许操作");
}
/**
* 错误SQL语句异常

View File

@ -7,11 +7,17 @@ import com.alibaba.ttl.TransmittableThreadLocal;
import com.sxpcwlkj.common.utils.JsonUtil;
import com.sxpcwlkj.common.utils.SpringUtil;
import com.sxpcwlkj.common.utils.StringUtil;
import com.sxpcwlkj.common.properties.DemoModeProperties;
import com.sxpcwlkj.datasource.handler.DemoModeContextHolder;
import com.sxpcwlkj.datasource.handler.DemoModeInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
@ -20,11 +26,13 @@ import org.springframework.web.servlet.ModelAndView;
import java.io.BufferedReader;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
/**
* @author xijue
*/
@Slf4j
public class WebHandlerInterceptorHandler implements HandlerInterceptor {
private final String prodProfile = "prod";
@ -68,6 +76,7 @@ public class WebHandlerInterceptorHandler implements HandlerInterceptor {
invokeTimeTL.set(stopWatch);
stopWatch.start();
}
// 获取处理method
if (!(handler instanceof HandlerMethod)) {
return true;
@ -111,6 +120,7 @@ public class WebHandlerInterceptorHandler implements HandlerInterceptor {
invokeTimeTL.remove();
}
}
DemoModeContextHolder.clear();
}
/**

View File

@ -9,6 +9,7 @@ public class RedisConstant {
// user换成前缀key
public static final String ADMIN_KEY="admin:";
public static final String ADMIN_NAME="admin:name:";
public static final String MOBILE_KEY="mobile:member:";
public static final String PC_KEY="pc:member:";
public static final String ENCRYPTION_SERVER_PORT="encryption:server:";