diff --git a/pom.xml b/pom.xml
index 3ac5aa1..86420a7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -82,6 +82,13 @@
3.5.9
+
+
+ com.baomidou
+ mybatis-plus-jsqlparser
+ 3.5.9
+
+
org.apache.shiro
diff --git a/src/main/java/com/label/common/config/MybatisPlusConfig.java b/src/main/java/com/label/common/config/MybatisPlusConfig.java
new file mode 100644
index 0000000..2b52882
--- /dev/null
+++ b/src/main/java/com/label/common/config/MybatisPlusConfig.java
@@ -0,0 +1,57 @@
+package com.label.common.config;
+
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import com.label.common.context.CompanyContext;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.NullValue;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Configuration
+public class MybatisPlusConfig {
+
+ // Tables that do NOT need tenant isolation (either global or tenant root tables)
+ private static final List IGNORED_TABLES = Arrays.asList(
+ "sys_company", // the tenant root table itself
+ "sys_config" // has company_id=NULL for global defaults; service handles this manually
+ );
+
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+
+ // 1. Tenant isolation - auto-injects WHERE company_id = ?
+ interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
+ @Override
+ public Expression getTenantId() {
+ Long companyId = CompanyContext.get();
+ if (companyId == null) {
+ return new NullValue();
+ }
+ return new LongValue(companyId);
+ }
+
+ @Override
+ public String getTenantIdColumn() {
+ return "company_id";
+ }
+
+ @Override
+ public boolean ignoreTable(String tableName) {
+ return IGNORED_TABLES.contains(tableName);
+ }
+ }));
+
+ // 2. Pagination interceptor (required for MyBatis Plus Page queries)
+ interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
+
+ return interceptor;
+ }
+}
diff --git a/src/main/java/com/label/common/statemachine/StateValidator.java b/src/main/java/com/label/common/statemachine/StateValidator.java
new file mode 100644
index 0000000..cf4385b
--- /dev/null
+++ b/src/main/java/com/label/common/statemachine/StateValidator.java
@@ -0,0 +1,36 @@
+package com.label.common.statemachine;
+
+import com.label.common.exception.BusinessException;
+import org.springframework.http.HttpStatus;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Generic state machine validator.
+ * Validates state transitions against a predefined transitions map.
+ */
+public final class StateValidator {
+
+ private StateValidator() {}
+
+ /**
+ * Assert that a state transition from {@code current} to {@code next} is valid.
+ *
+ * @param transitions the allowed transitions map
+ * @param current the current state
+ * @param next the desired next state
+ * @param the state type (enum)
+ * @throws BusinessException with code INVALID_STATE_TRANSITION if transition not allowed
+ */
+ public static void assertTransition(Map> transitions, S current, S next) {
+ Set allowed = transitions.get(current);
+ if (allowed == null || !allowed.contains(next)) {
+ throw new BusinessException(
+ "INVALID_STATE_TRANSITION",
+ String.format("不允许的状态转换: %s → %s", current, next),
+ HttpStatus.CONFLICT
+ );
+ }
+ }
+}