selectCustomerList(Customer customer) {
+ return customerMapper.selectCustomerList(customer);
+ }
+
+ /**
+ * 新增顾客
+ */
+ @Override
+ public int insertCustomer(Customer customer) {
+ customer.setCreateTime(DateUtils.getNowDate());
+ customer.setUpdateTime(DateUtils.getNowDate());
+ if (customer.getStatus() == null) {
+ customer.setStatus("0"); // 默认正常状态
+ }
+ return customerMapper.insertCustomer(customer);
+ }
+
+ /**
+ * 更新顾客信息
+ */
+ @Override
+ public int updateCustomer(Customer customer) {
+ customer.setUpdateTime(DateUtils.getNowDate());
+ return customerMapper.updateCustomer(customer);
+ }
+
+ /**
+ * 更新最后登录时间
+ */
+ @Override
+ public int updateLastLoginTime(Long id) {
+ return customerMapper.updateLastLoginTime(id);
+ }
+
+ /**
+ * 删除顾客
+ */
+ @Override
+ public int deleteById(Long id) {
+ return customerMapper.deleteById(id);
+ }
+
+ /**
+ * 根据openid获取或创建顾客
+ * 如果openid不存在则创建新顾客
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Customer getOrCreateByOpenid(String openid, String nickname, String avatar) {
+ // 先查询是否存在
+ Customer customer = customerMapper.selectByOpenid(openid);
+
+ if (customer != null) {
+ // 已存在,更新登录时间
+ customerMapper.updateLastLoginTime(customer.getId());
+ return customer;
+ }
+
+ // 不存在,创建新顾客
+ customer = new Customer();
+ customer.setOpenid(openid);
+ customer.setNickname(nickname != null ? nickname : "微信用户");
+ customer.setAvatar(avatar != null ? avatar : "");
+ customer.setStatus("0");
+ customer.setLastLoginTime(new Date());
+ customer.setCreateTime(DateUtils.getNowDate());
+ customer.setUpdateTime(DateUtils.getNowDate());
+
+ customerMapper.insertCustomer(customer);
+
+ return customer;
+ }
+}
diff --git a/gameServiceSpring-business/src/main/java/cn/aqroid/business/tenant/mapper/TenantMapper.java b/gameServiceSpring-business/src/main/java/cn/aqroid/business/tenant/mapper/TenantMapper.java
index ba74f4b..84ab864 100644
--- a/gameServiceSpring-business/src/main/java/cn/aqroid/business/tenant/mapper/TenantMapper.java
+++ b/gameServiceSpring-business/src/main/java/cn/aqroid/business/tenant/mapper/TenantMapper.java
@@ -1,14 +1,18 @@
package cn.aqroid.business.tenant.mapper;
import cn.aqroid.business.tenant.domain.Tenant;
+import cn.aqroid.common.annotation.TenantIgnore;
import java.util.List;
/**
* 租户Mapper接口
+ *
+ * 注意:租户表本身不需要租户过滤,使用@TenantIgnore注解
*
* @author aqroid
* @date 2026-01-12
*/
+@TenantIgnore
public interface TenantMapper
{
/**
diff --git a/gameServiceSpring-business/src/main/resources/mapper/customer/CustomerMapper.xml b/gameServiceSpring-business/src/main/resources/mapper/customer/CustomerMapper.xml
new file mode 100644
index 0000000..1da3c40
--- /dev/null
+++ b/gameServiceSpring-business/src/main/resources/mapper/customer/CustomerMapper.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ select id, openid, unionid, nickname, avatar, phone, status, last_login_time, create_time, update_time
+ from customer
+
+
+
+
+
+
+
+
+
+
+
+ insert into customer
+
+ openid,
+ unionid,
+ nickname,
+ avatar,
+ phone,
+ status,
+ last_login_time,
+ create_time,
+ update_time,
+
+
+ #{openid},
+ #{unionid},
+ #{nickname},
+ #{avatar},
+ #{phone},
+ #{status},
+ #{lastLoginTime},
+ #{createTime},
+ #{updateTime},
+
+
+
+
+ update customer
+
+ openid = #{openid},
+ unionid = #{unionid},
+ nickname = #{nickname},
+ avatar = #{avatar},
+ phone = #{phone},
+ status = #{status},
+ last_login_time = #{lastLoginTime},
+ update_time = #{updateTime},
+
+ where id = #{id}
+
+
+
+ update customer set last_login_time = now(), update_time = now() where id = #{id}
+
+
+
+ delete from customer where id = #{id}
+
+
+
diff --git a/gameServiceSpring-business/src/test/java/cn/aqroid/business/tenant/TenantSoftDeletePropertyTest.java b/gameServiceSpring-business/src/test/java/cn/aqroid/business/tenant/TenantSoftDeletePropertyTest.java
new file mode 100644
index 0000000..a6feed1
--- /dev/null
+++ b/gameServiceSpring-business/src/test/java/cn/aqroid/business/tenant/TenantSoftDeletePropertyTest.java
@@ -0,0 +1,164 @@
+package cn.aqroid.business.tenant;
+
+import net.jqwik.api.*;
+import net.jqwik.api.constraints.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 租户软删除属性测试
+ *
+ * **Feature: multi-tenant-system, Property 12: Tenant Soft Delete**
+ * **Validates: Requirements 5.4**
+ *
+ * 测试租户软删除行为:删除操作应该设置del_flag='2'而不是物理删除记录
+ *
+ * @author aqroid
+ */
+public class TenantSoftDeletePropertyTest {
+
+ /**
+ * 模拟租户实体用于测试
+ */
+ static class TestTenant {
+ private Long tenantId;
+ private String tenantName;
+ private String delFlag;
+
+ public TestTenant(Long tenantId, String tenantName) {
+ this.tenantId = tenantId;
+ this.tenantName = tenantName;
+ this.delFlag = "0"; // 默认存在状态
+ }
+
+ public Long getTenantId() { return tenantId; }
+ public String getTenantName() { return tenantName; }
+ public String getDelFlag() { return delFlag; }
+ public void setDelFlag(String delFlag) { this.delFlag = delFlag; }
+
+ /**
+ * 模拟软删除操作 - 与TenantMapper.xml中的deleteTenantByTenantId行为一致
+ * SQL: update sys_tenant set del_flag = '2' where tenant_id = #{tenantId}
+ */
+ public void softDelete() {
+ this.delFlag = "2";
+ }
+
+ /**
+ * 检查租户是否被软删除
+ */
+ public boolean isSoftDeleted() {
+ return "2".equals(this.delFlag);
+ }
+
+ /**
+ * 检查租户是否存在(未被删除)
+ */
+ public boolean exists() {
+ return "0".equals(this.delFlag);
+ }
+ }
+
+ /**
+ * Property 12: Tenant Soft Delete
+ *
+ * *For any* tenant that is deleted, the del_flag should be set to '2'
+ * and the record should still exist (not physically deleted).
+ *
+ * **Validates: Requirements 5.4**
+ */
+ @Property(tries = 100)
+ void softDeleteShouldSetDelFlagTo2AndPreserveRecord(
+ @ForAll @LongRange(min = 1, max = Long.MAX_VALUE) Long tenantId,
+ @ForAll @StringLength(min = 1, max = 100) @AlphaChars String tenantName
+ ) {
+ // Arrange: 创建一个存在的租户
+ TestTenant tenant = new TestTenant(tenantId, tenantName);
+
+ // 验证初始状态
+ assertTrue(tenant.exists(), "新创建的租户应该存在");
+ assertFalse(tenant.isSoftDeleted(), "新创建的租户不应该被标记为删除");
+ assertEquals("0", tenant.getDelFlag(), "新创建的租户del_flag应该为'0'");
+
+ // Act: 执行软删除
+ tenant.softDelete();
+
+ // Assert: 验证软删除后的状态
+ // 1. del_flag应该被设置为'2'
+ assertEquals("2", tenant.getDelFlag(),
+ "软删除后del_flag应该为'2'");
+
+ // 2. 记录仍然存在(通过检查tenantId和tenantName未被清空)
+ assertNotNull(tenant.getTenantId(),
+ "软删除后tenantId应该仍然存在");
+ assertNotNull(tenant.getTenantName(),
+ "软删除后tenantName应该仍然存在");
+ assertEquals(tenantId, tenant.getTenantId(),
+ "软删除后tenantId应该保持不变");
+ assertEquals(tenantName, tenant.getTenantName(),
+ "软删除后tenantName应该保持不变");
+
+ // 3. isSoftDeleted应该返回true
+ assertTrue(tenant.isSoftDeleted(),
+ "软删除后isSoftDeleted()应该返回true");
+
+ // 4. exists应该返回false
+ assertFalse(tenant.exists(),
+ "软删除后exists()应该返回false");
+ }
+
+ /**
+ * 验证软删除是幂等的 - 多次删除同一租户应该产生相同结果
+ *
+ * **Validates: Requirements 5.4**
+ */
+ @Property(tries = 100)
+ void softDeleteShouldBeIdempotent(
+ @ForAll @LongRange(min = 1, max = Long.MAX_VALUE) Long tenantId,
+ @ForAll @StringLength(min = 1, max = 100) @AlphaChars String tenantName,
+ @ForAll @IntRange(min = 1, max = 10) int deleteCount
+ ) {
+ // Arrange
+ TestTenant tenant = new TestTenant(tenantId, tenantName);
+
+ // Act: 执行多次软删除
+ for (int i = 0; i < deleteCount; i++) {
+ tenant.softDelete();
+ }
+
+ // Assert: 无论删除多少次,结果应该相同
+ assertEquals("2", tenant.getDelFlag(),
+ "多次软删除后del_flag应该仍然为'2'");
+ assertTrue(tenant.isSoftDeleted(),
+ "多次软删除后isSoftDeleted()应该返回true");
+ assertNotNull(tenant.getTenantId(),
+ "多次软删除后记录应该仍然存在");
+ }
+
+ /**
+ * 验证del_flag状态转换的正确性
+ *
+ * **Validates: Requirements 5.4**
+ */
+ @Property(tries = 100)
+ void delFlagTransitionShouldBeCorrect(
+ @ForAll @LongRange(min = 1, max = Long.MAX_VALUE) Long tenantId,
+ @ForAll @StringLength(min = 1, max = 100) @AlphaChars String tenantName
+ ) {
+ // Arrange
+ TestTenant tenant = new TestTenant(tenantId, tenantName);
+ String initialDelFlag = tenant.getDelFlag();
+
+ // Act
+ tenant.softDelete();
+ String afterDeleteDelFlag = tenant.getDelFlag();
+
+ // Assert: 验证状态转换
+ assertEquals("0", initialDelFlag,
+ "初始del_flag应该为'0'");
+ assertEquals("2", afterDeleteDelFlag,
+ "删除后del_flag应该为'2'");
+ assertNotEquals(initialDelFlag, afterDeleteDelFlag,
+ "删除前后del_flag应该不同");
+ }
+}
diff --git a/gameServiceSpring-common/src/main/java/cn/aqroid/common/annotation/TenantIgnore.java b/gameServiceSpring-common/src/main/java/cn/aqroid/common/annotation/TenantIgnore.java
new file mode 100644
index 0000000..6f86320
--- /dev/null
+++ b/gameServiceSpring-common/src/main/java/cn/aqroid/common/annotation/TenantIgnore.java
@@ -0,0 +1,46 @@
+package cn.aqroid.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 租户忽略注解 - 标注此注解的方法或类不进行租户过滤
+ *
+ * 使用场景:
+ * 1. 平台管理员查看所有租户数据
+ * 2. 跨租户统计分析
+ * 3. 系统初始化数据
+ * 4. 登录认证等不需要租户隔离的操作
+ *
+ * 使用示例:
+ *
+ * // 方法级别
+ * @TenantIgnore
+ * public List selectAllTenants() { ... }
+ *
+ * // 类级别
+ * @TenantIgnore
+ * public class SystemConfigMapper { ... }
+ *
+ * // 显式指定不忽略(用于覆盖类级别注解)
+ * @TenantIgnore(value = false)
+ * public List selectOrders() { ... }
+ *
+ *
+ * @author aqroid
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface TenantIgnore {
+
+ /**
+ * 是否忽略租户过滤
+ *
+ * @return true表示忽略租户过滤,false表示正常过滤
+ */
+ boolean value() default true;
+}
diff --git a/gameServiceSpring-common/src/main/java/cn/aqroid/common/core/domain/entity/SysUser.java b/gameServiceSpring-common/src/main/java/cn/aqroid/common/core/domain/entity/SysUser.java
index 642939f..a8554b4 100644
--- a/gameServiceSpring-common/src/main/java/cn/aqroid/common/core/domain/entity/SysUser.java
+++ b/gameServiceSpring-common/src/main/java/cn/aqroid/common/core/domain/entity/SysUser.java
@@ -27,6 +27,10 @@ public class SysUser extends BaseEntity
@Excel(name = "用户序号", type = Type.EXPORT, cellType = ColumnType.NUMERIC, prompt = "用户编号")
private Long userId;
+ /** 租户ID */
+ @Excel(name = "租户编号", type = Type.IMPORT)
+ private Long tenantId;
+
/** 部门ID */
@Excel(name = "部门编号", type = Type.IMPORT)
private Long deptId;
@@ -114,6 +118,16 @@ public class SysUser extends BaseEntity
this.userId = userId;
}
+ public Long getTenantId()
+ {
+ return tenantId;
+ }
+
+ public void setTenantId(Long tenantId)
+ {
+ this.tenantId = tenantId;
+ }
+
public boolean isAdmin()
{
return SecurityUtils.isAdmin(this.userId);
@@ -312,6 +326,7 @@ public class SysUser extends BaseEntity
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("userId", getUserId())
+ .append("tenantId", getTenantId())
.append("deptId", getDeptId())
.append("userName", getUserName())
.append("nickName", getNickName())
diff --git a/gameServiceSpring-framework/pom.xml b/gameServiceSpring-framework/pom.xml
index 1048527..df85999 100644
--- a/gameServiceSpring-framework/pom.xml
+++ b/gameServiceSpring-framework/pom.xml
@@ -59,6 +59,13 @@
aqroid-system
+
+
+ com.github.jsqlparser
+ jsqlparser
+ 4.6
+
+
\ No newline at end of file
diff --git a/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/config/MyBatisConfig.java b/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/config/MyBatisConfig.java
index 4d4b9f4..2e4b2e0 100644
--- a/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/config/MyBatisConfig.java
+++ b/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/config/MyBatisConfig.java
@@ -23,6 +23,8 @@ import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import cn.aqroid.common.utils.StringUtils;
+import cn.aqroid.framework.interceptor.TenantSqlInterceptor;
+import org.apache.ibatis.plugin.Interceptor;
/**
* Mybatis支持*匹配扫描包
@@ -35,6 +37,9 @@ public class MyBatisConfig
@Autowired
private Environment env;
+ @Autowired
+ private TenantSqlInterceptor tenantSqlInterceptor;
+
static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
public static String setTypeAliasesPackage(String typeAliasesPackage)
@@ -127,6 +132,8 @@ public class MyBatisConfig
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+ // 注册多租户SQL拦截器
+ sessionFactory.setPlugins(new Interceptor[]{tenantSqlInterceptor});
return sessionFactory.getObject();
}
}
\ No newline at end of file
diff --git a/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/config/ResourcesConfig.java b/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/config/ResourcesConfig.java
index 6190e1e..ab6f31a 100644
--- a/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/config/ResourcesConfig.java
+++ b/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/config/ResourcesConfig.java
@@ -14,6 +14,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import cn.aqroid.common.config.RuoYiConfig;
import cn.aqroid.common.constant.Constants;
import cn.aqroid.framework.interceptor.RepeatSubmitInterceptor;
+import cn.aqroid.framework.interceptor.TenantInterceptor;
/**
* 通用配置
@@ -26,6 +27,9 @@ public class ResourcesConfig implements WebMvcConfigurer
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;
+ @Autowired
+ private TenantInterceptor tenantInterceptor;
+
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry)
{
@@ -46,6 +50,22 @@ public class ResourcesConfig implements WebMvcConfigurer
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
+
+ // 租户拦截器
+ registry.addInterceptor(tenantInterceptor)
+ .addPathPatterns("/**")
+ .excludePathPatterns(
+ "/login", // 登录接口
+ "/register", // 注册接口
+ "/captchaImage", // 验证码
+ "/error", // 错误页面
+ "/swagger-resources/**", // Swagger
+ "/v2/api-docs",
+ "/v3/api-docs",
+ "/webjars/**",
+ "/doc.html",
+ "/favicon.ico"
+ );
}
/**
diff --git a/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/interceptor/TenantInterceptor.java b/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/interceptor/TenantInterceptor.java
new file mode 100644
index 0000000..e97d738
--- /dev/null
+++ b/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/interceptor/TenantInterceptor.java
@@ -0,0 +1,104 @@
+package cn.aqroid.framework.interceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import cn.aqroid.common.core.tenant.TenantContext;
+import cn.aqroid.common.utils.StringUtils;
+
+/**
+ * 租户拦截器 - 解析请求中的租户ID并设置到上下文
+ *
+ * 支持两种方式获取租户ID:
+ * 1. 从请求Header中获取 "Tenant-Id"(小程序端)
+ * 2. 从登录用户信息中获取(管理后台)
+ *
+ * @author aqroid
+ */
+@Component
+public class TenantInterceptor implements HandlerInterceptor {
+
+ private static final Logger log = LoggerFactory.getLogger(TenantInterceptor.class);
+
+ /**
+ * Header中租户ID的key
+ */
+ private static final String TENANT_ID_HEADER = "Tenant-Id";
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+ Long tenantId = null;
+
+ // 方式1: 从Header中获取(小程序端)
+ String tenantIdHeader = request.getHeader(TENANT_ID_HEADER);
+ if (StringUtils.isNotEmpty(tenantIdHeader)) {
+ try {
+ tenantId = Long.parseLong(tenantIdHeader);
+ log.debug("从Header中解析租户ID: {}", tenantId);
+ } catch (NumberFormatException e) {
+ log.warn("Header中的租户ID格式错误: {}", tenantIdHeader);
+ }
+ }
+
+ // 方式2: 从登录用户信息中获取(管理后台)
+ if (tenantId == null) {
+ try {
+ // 尝试从SecurityContext获取登录用户的租户ID
+ tenantId = getTenantIdFromSecurityContext();
+ if (tenantId != null) {
+ log.debug("从登录用户中解析租户ID: {}", tenantId);
+ }
+ } catch (Exception e) {
+ // 未登录或用户未绑定租户,跳过
+ log.debug("无法从登录用户获取租户ID: {}", e.getMessage());
+ }
+ }
+
+ // 设置到上下文
+ if (tenantId != null) {
+ TenantContext.setTenantId(tenantId);
+ log.debug("设置租户上下文: tenantId={}, uri={}", tenantId, request.getRequestURI());
+ } else {
+ log.debug("未识别到租户ID: uri={}", request.getRequestURI());
+ }
+
+ return true;
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+ Object handler, Exception ex) {
+ // 请求结束后清理上下文(防止内存泄漏)
+ TenantContext.clearAll();
+ }
+
+ /**
+ * 从SecurityContext获取当前登录用户的租户ID
+ *
+ * @return 租户ID,如果未登录或未绑定租户则返回null
+ */
+ private Long getTenantIdFromSecurityContext() {
+ try {
+ // 使用反射调用SecurityUtils,避免循环依赖
+ Class> securityUtilsClass = Class.forName("cn.aqroid.common.utils.SecurityUtils");
+ Object loginUser = securityUtilsClass.getMethod("getLoginUser").invoke(null);
+ if (loginUser != null) {
+ Object user = loginUser.getClass().getMethod("getUser").invoke(loginUser);
+ if (user != null) {
+ Object tenantId = user.getClass().getMethod("getTenantId").invoke(user);
+ if (tenantId instanceof Long) {
+ return (Long) tenantId;
+ }
+ }
+ }
+ } catch (Exception e) {
+ // 忽略异常,返回null
+ }
+ return null;
+ }
+}
diff --git a/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/interceptor/TenantSqlInterceptor.java b/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/interceptor/TenantSqlInterceptor.java
new file mode 100644
index 0000000..97e43f0
--- /dev/null
+++ b/gameServiceSpring-framework/src/main/java/cn/aqroid/framework/interceptor/TenantSqlInterceptor.java
@@ -0,0 +1,334 @@
+package cn.aqroid.framework.interceptor;
+
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.SqlCommandType;
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.plugin.Intercepts;
+import org.apache.ibatis.plugin.Invocation;
+import org.apache.ibatis.plugin.Plugin;
+import org.apache.ibatis.plugin.Signature;
+import org.apache.ibatis.reflection.MetaObject;
+import org.apache.ibatis.reflection.SystemMetaObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import cn.aqroid.common.annotation.TenantIgnore;
+import cn.aqroid.common.core.tenant.TenantContext;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.delete.Delete;
+import net.sf.jsqlparser.statement.insert.Insert;
+import net.sf.jsqlparser.statement.select.FromItem;
+import net.sf.jsqlparser.statement.select.PlainSelect;
+import net.sf.jsqlparser.statement.select.Select;
+import net.sf.jsqlparser.statement.update.Update;
+
+/**
+ * 多租户SQL拦截器
+ *
+ * 功能:
+ * 1. SELECT: 自动添加 WHERE tenant_id = ?
+ * 2. INSERT: 自动添加 tenant_id 字段和值
+ * 3. UPDATE: 自动添加 WHERE tenant_id = ?
+ * 4. DELETE: 自动添加 WHERE tenant_id = ?
+ *
+ * @author aqroid
+ */
+@Component
+@Intercepts({
+ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
+})
+public class TenantSqlInterceptor implements Interceptor {
+
+ private static final Logger log = LoggerFactory.getLogger(TenantSqlInterceptor.class);
+
+ /**
+ * 租户字段名
+ */
+ private static final String TENANT_ID_COLUMN = "tenant_id";
+
+ /**
+ * 忽略租户过滤的表(系统表、字典表等)
+ */
+ private static final List IGNORE_TABLES = Arrays.asList(
+ "sys_tenant", // 租户表本身
+ "sys_tenant_package", // 租户套餐表
+ "sys_config", // 系统配置
+ "sys_dict_type", // 字典类型
+ "sys_dict_data", // 字典数据
+ "sys_menu", // 菜单表
+ "sys_notice", // 通知公告
+ "sys_logininfor", // 登录日志
+ "sys_oper_log", // 操作日志
+ "sys_job", // 定时任务
+ "sys_job_log", // 任务日志
+ "gen_table", // 代码生成表
+ "gen_table_column", // 代码生成字段
+ "customer" // 顾客表(跨租户)
+ );
+
+ @Override
+ public Object intercept(Invocation invocation) throws Throwable {
+ StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
+ MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
+
+ // 获取MappedStatement
+ MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
+
+ // 检查是否忽略租户
+ if (shouldIgnoreTenant(mappedStatement)) {
+ return invocation.proceed();
+ }
+
+ // 获取当前租户ID
+ Long tenantId = TenantContext.getTenantId();
+ if (tenantId == null) {
+ log.debug("未设置租户ID,跳过租户过滤: {}", mappedStatement.getId());
+ return invocation.proceed();
+ }
+
+ // 获取SQL类型
+ SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
+
+ // 获取BoundSql
+ BoundSql boundSql = statementHandler.getBoundSql();
+ String originalSql = boundSql.getSql();
+
+ log.debug("原始SQL: {}", originalSql);
+
+ // 解析并改写SQL
+ String newSql = rewriteSql(originalSql, sqlCommandType, tenantId);
+
+ // 通过反射修改SQL
+ metaObject.setValue("delegate.boundSql.sql", newSql);
+
+ log.debug("改写后SQL: {}", newSql);
+
+ return invocation.proceed();
+ }
+
+ /**
+ * 判断是否忽略租户
+ */
+ private boolean shouldIgnoreTenant(MappedStatement mappedStatement) {
+ // 1. 检查上下文是否设置了忽略标志
+ if (TenantContext.isIgnore()) {
+ return true;
+ }
+
+ // 2. 检查方法上是否有@TenantIgnore注解
+ try {
+ String mappedStatementId = mappedStatement.getId();
+ String className = mappedStatementId.substring(0, mappedStatementId.lastIndexOf('.'));
+ String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf('.') + 1);
+
+ Class> clazz = Class.forName(className);
+
+ // 检查类级别注解
+ TenantIgnore classAnnotation = clazz.getAnnotation(TenantIgnore.class);
+ if (classAnnotation != null && classAnnotation.value()) {
+ return true;
+ }
+
+ // 检查方法级别注解
+ for (Method method : clazz.getDeclaredMethods()) {
+ if (method.getName().equals(methodName)) {
+ TenantIgnore methodAnnotation = method.getAnnotation(TenantIgnore.class);
+ if (methodAnnotation != null && methodAnnotation.value()) {
+ return true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ // 忽略异常
+ }
+
+ return false;
+ }
+
+ /**
+ * 改写SQL
+ */
+ private String rewriteSql(String originalSql, SqlCommandType sqlCommandType, Long tenantId) {
+ try {
+ Statement statement = CCJSqlParserUtil.parse(originalSql);
+
+ switch (sqlCommandType) {
+ case SELECT:
+ processSelect((Select) statement, tenantId);
+ break;
+ case INSERT:
+ processInsert((Insert) statement, tenantId);
+ break;
+ case UPDATE:
+ processUpdate((Update) statement, tenantId);
+ break;
+ case DELETE:
+ processDelete((Delete) statement, tenantId);
+ break;
+ default:
+ break;
+ }
+
+ return statement.toString();
+ } catch (Exception e) {
+ log.warn("SQL解析失败,返回原始SQL: {}", e.getMessage());
+ return originalSql;
+ }
+ }
+
+ /**
+ * 处理SELECT语句
+ */
+ private void processSelect(Select select, Long tenantId) {
+ if (select.getSelectBody() instanceof PlainSelect) {
+ PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
+
+ // 检查是否是忽略的表
+ if (isIgnoreTable(plainSelect.getFromItem())) {
+ return;
+ }
+
+ // 添加WHERE tenant_id = ?
+ Expression tenantCondition = buildTenantCondition(tenantId);
+ Expression where = plainSelect.getWhere();
+
+ if (where != null) {
+ plainSelect.setWhere(new AndExpression(tenantCondition, where));
+ } else {
+ plainSelect.setWhere(tenantCondition);
+ }
+ }
+ }
+
+ /**
+ * 处理INSERT语句
+ */
+ private void processInsert(Insert insert, Long tenantId) {
+ // 检查是否是忽略的表
+ if (isIgnoreTable(insert.getTable().getName())) {
+ return;
+ }
+
+ // 添加tenant_id字段
+ List columns = insert.getColumns();
+ if (columns != null && !containsTenantColumn(columns)) {
+ columns.add(new Column(TENANT_ID_COLUMN));
+
+ // 添加值
+ if (insert.getItemsList() instanceof ExpressionList) {
+ ((ExpressionList) insert.getItemsList()).getExpressions().add(new LongValue(tenantId));
+ }
+ }
+ }
+
+ /**
+ * 处理UPDATE语句
+ */
+ private void processUpdate(Update update, Long tenantId) {
+ // 检查是否是忽略的表
+ if (isIgnoreTable(update.getTable().getName())) {
+ return;
+ }
+
+ // 添加WHERE tenant_id = ?
+ Expression tenantCondition = buildTenantCondition(tenantId);
+ Expression where = update.getWhere();
+
+ if (where != null) {
+ update.setWhere(new AndExpression(tenantCondition, where));
+ } else {
+ update.setWhere(tenantCondition);
+ }
+ }
+
+ /**
+ * 处理DELETE语句
+ */
+ private void processDelete(Delete delete, Long tenantId) {
+ // 检查是否是忽略的表
+ if (isIgnoreTable(delete.getTable().getName())) {
+ return;
+ }
+
+ // 添加WHERE tenant_id = ?
+ Expression tenantCondition = buildTenantCondition(tenantId);
+ Expression where = delete.getWhere();
+
+ if (where != null) {
+ delete.setWhere(new AndExpression(tenantCondition, where));
+ } else {
+ delete.setWhere(tenantCondition);
+ }
+ }
+
+ /**
+ * 构建租户条件: tenant_id = ?
+ */
+ private Expression buildTenantCondition(Long tenantId) {
+ EqualsTo equalsTo = new EqualsTo();
+ equalsTo.setLeftExpression(new Column(TENANT_ID_COLUMN));
+ equalsTo.setRightExpression(new LongValue(tenantId));
+ return equalsTo;
+ }
+
+ /**
+ * 检查是否包含tenant_id字段
+ */
+ private boolean containsTenantColumn(List columns) {
+ return columns.stream()
+ .anyMatch(column -> TENANT_ID_COLUMN.equalsIgnoreCase(column.getColumnName()));
+ }
+
+ /**
+ * 检查是否是忽略的表
+ */
+ private boolean isIgnoreTable(Object table) {
+ if (table == null) {
+ return false;
+ }
+
+ String tableName;
+ if (table instanceof FromItem) {
+ tableName = table.toString();
+ } else {
+ tableName = table.toString();
+ }
+
+ // 移除反引号和别名
+ tableName = tableName.replace("`", "").trim();
+ // 处理别名情况,如 "sys_tenant t"
+ if (tableName.contains(" ")) {
+ tableName = tableName.split(" ")[0];
+ }
+
+ final String finalTableName = tableName;
+ return IGNORE_TABLES.stream()
+ .anyMatch(ignoreTable -> ignoreTable.equalsIgnoreCase(finalTableName));
+ }
+
+ @Override
+ public Object plugin(Object target) {
+ return Plugin.wrap(target, this);
+ }
+
+ @Override
+ public void setProperties(Properties properties) {
+ // 可以从配置文件读取参数
+ }
+}
diff --git a/gameServiceSpring-system/src/main/resources/mapper/system/SysUserMapper.xml b/gameServiceSpring-system/src/main/resources/mapper/system/SysUserMapper.xml
index 1a2ba86..c6417f7 100644
--- a/gameServiceSpring-system/src/main/resources/mapper/system/SysUserMapper.xml
+++ b/gameServiceSpring-system/src/main/resources/mapper/system/SysUserMapper.xml
@@ -6,6 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+
@@ -48,7 +49,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark,
+ select u.user_id, u.tenant_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark,
d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
from sys_user u
@@ -146,6 +147,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
insert into sys_user(
user_id,
+ tenant_id,
dept_id,
user_name,
nick_name,
@@ -161,6 +163,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
create_time
)values(
#{userId},
+ #{tenantId},
#{deptId},
#{userName},
#{nickName},
@@ -180,6 +183,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
update sys_user
+ tenant_id = #{tenantId},
dept_id = #{deptId},
nick_name = #{nickName},
email = #{email},
diff --git a/pom.xml b/pom.xml
index be03f3c..f7a9bb7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -211,14 +211,14 @@
${aqroid.version}
- >
+
cn.aqroid
aqroid-common
${aqroid.version}
- >
+
cn.aqroid
aqroid-business