新增 :演示模式

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()); SysUserVo sysUser = sysUserService.getUserRoleAnfFunctionInfo(LoginObject.getLoginId());
RedisUtil.setCacheObject(RedisConstant.ADMIN_TENANT_KEY+sysUser.getUserId(),sysUser.getTenantId(), Duration.ofHours(24)); 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_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)); ajax.put(Constants.USERINFO, sysUserService.getUserInfo(sysUser));
if(LoginObject.isLogin()){ if(LoginObject.isLogin()){
//给浏览器端设置一个 Cookie clientKey值 3天 //给浏览器端设置一个 Cookie clientKey值 3天

View File

@ -26,5 +26,6 @@ public interface SysUserMapper extends BaseMapperPlus<SysUser, SysUserVo> {
@InterceptorIgnore(tenantLine = "true") @InterceptorIgnore(tenantLine = "true")
int updateRsa(@Param("tenantId") String tenantId, @Param("userId") String userId, @Param("publicKey") String publicKey,@Param("priverKey") String priverKey); 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) { public void updateSysUser(HttpServletRequest request,SysUser sysUser) {
sysUser.setLoginIp(IPUtil.getIp(request)); sysUser.setLoginIp(IPUtil.getIp(request));
sysUser.setLoginDate(new Date()); sysUser.setLoginDate(new Date());
sysUserMapper.updateById(sysUser); sysUserMapper.updateIpById(sysUser);
} }

View File

@ -15,6 +15,16 @@ sxpcwlkj:
# 开启验证码: true开启 false 关闭 # 开启验证码: true开启 false 关闭
isOpenCaptcha: false isOpenCaptcha: false
# 演示模式配置
demo:
mode:
enabled: false # 默认开启演示模式
message-template: "演示模式下禁止{operation}操作 [实体: {entity}, 方法: {method}]"
allowed-users: # 白名单用户
- admin
allowed-methods:
- updateIpById
--- # 验证码配置 --- # 验证码配置
captcha: captcha:
# 是否开启验证码: true 开启 false 关闭 # 是否开启验证码: 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 `sys_user` SET `public_key`=#{publicKey} , `private_key`=#{priverKey} WHERE `tenant_id`=#{tenantId} AND `user_id`=#{userId}
</update> </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 id="selectByUserName" resultType="com.sxpcwlkj.system.entity.SysUser">
SELECT * FROM `sys_user` WHERE `user_name`=#{userName} LIMIT 1 SELECT * FROM `sys_user` WHERE `user_name`=#{userName} LIMIT 1
</select> </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; package com.sxpcwlkj.common.exception;
/** import lombok.Getter;
* @ClassName DemoModeException
* @Description TODO
* @Author 西决
* @Date 2022/12/25 11:42
*/
public class DemoModeException extends RuntimeException {
private static final long serialVersionUID = 1L;
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; package com.sxpcwlkj.common.exception;
import java.io.Serial;
/**
* @author xijue
*/
public class LoginException extends RuntimeException{ public class LoginException extends RuntimeException{
@Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
public LoginException(String message) { public LoginException(String message) {

View File

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

View File

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

View File

@ -1,11 +1,12 @@
package com.sxpcwlkj.common.exception; package com.sxpcwlkj.common.exception;
import java.io.Serial;
/** /**
* @ClassName UtilException * @author xijue
* @Description TODO
* @Author 西决
*/ */
public class UtilException extends RuntimeException { public class UtilException extends RuntimeException {
@Serial
private static final long serialVersionUID = 8247610319171014183L; private static final long serialVersionUID = 8247610319171014183L;
public UtilException(Throwable e) { 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.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.sxpcwlkj.authority.LoginObject; import com.sxpcwlkj.authority.LoginObject;
import com.sxpcwlkj.common.properties.TenantProperties; import com.sxpcwlkj.common.properties.TenantProperties;
import com.sxpcwlkj.datasource.handler.DemoModeInterceptor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.StringValue;
@ -35,12 +36,14 @@ import java.util.Objects;
@Configuration @Configuration
public class MybatisPlusConfig { public class MybatisPlusConfig {
private final TenantProperties tenantProperties; private final TenantProperties tenantProperties;
private final DemoModeInterceptor demoModeInterceptor;
//插件 //插件
@Bean @Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加演示模式拦截器
interceptor.addInnerInterceptor(demoModeInterceptor);
//多租户插件 //多租户插件
if (tenantProperties.getEnable()) { 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); throw new MmsException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
} }
} }
/**
* 获取租户号
*/
private String getValidTenantId() { private String getValidTenantId() {
String tenantId = LoginObject.getLoginTenant(); String tenantId = LoginObject.getLoginTenant();
return tenantId != null ? tenantId : "default_tenant"; 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.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException; import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.exceptions.PersistenceException;
import org.mybatis.spring.MyBatisSystemException; import org.mybatis.spring.MyBatisSystemException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -34,7 +36,14 @@ import java.util.Objects;
@Slf4j @Slf4j
@RestControllerAdvice @RestControllerAdvice
public class GlobalException { 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; 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) @ExceptionHandler(AccessDeniedException.class)
public R<Void> handleAccessDeniedException(AccessDeniedException e, public R<Void> handleAccessDeniedException(AccessDeniedException e,
HttpServletRequest request) { HttpServletRequest request) {
@ -138,9 +155,14 @@ public class GlobalException {
*/ */
@ExceptionHandler(MyBatisSystemException.class) @ExceptionHandler(MyBatisSystemException.class)
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) { 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 requestUrl = request.getRequestURI();
String message = e.getMessage(); String message = e.getMessage();
if (message.contains("CannotFindDataSourceException")) { if (message!=null && message.contains("CannotFindDataSourceException")) {
log.error("请求地址'{}', 未找到数据源", requestUrl); log.error("请求地址'{}', 未找到数据源", requestUrl);
return R.fail("未找到数据源,请联系管理员确认"); return R.fail("未找到数据源,请联系管理员确认");
} }
@ -148,6 +170,38 @@ public class GlobalException {
return R.fail(message); 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语句异常 * 错误SQL语句异常

View File

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

View File

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