CONTEXT = new ThreadLocal<>();
+
+ /**
+ * 获取当前登录会话信息
+ */
+ public static UserSession getLoginInfo() {
+ return CONTEXT.get();
+ }
+
+ /**
+ * 设置当前登录会话信息(由 LoginAspect 调用)
+ */
+ public static void setLoginInfo(UserSession session) {
+ CONTEXT.set(session);
+ }
+
+ /**
+ * 清除当前线程的登录会话(由 DataSourceHeaderAspect 调用)
+ */
+ public static void clean() {
+ CONTEXT.remove();
+ }
+}
diff --git a/src/main/java/cn/apes/commons/auth/Login.java b/src/main/java/cn/apes/commons/auth/Login.java
new file mode 100644
index 0000000..21d8762
--- /dev/null
+++ b/src/main/java/cn/apes/commons/auth/Login.java
@@ -0,0 +1,18 @@
+package cn.apes.commons.auth;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 登录校验注解
+ * 标注在 Controller 类或方法上,表示需要登录后才能访问
+ * 由 LoginAspect 拦截处理:从请求头读取 token,查 Redis 组装 UserSession,写入 AuthContext
+ *
+ * 从 saas-service 的 apes-commons 0.0.7-SNAPSHOT 提取内联
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Login {
+}
diff --git a/src/main/java/cn/apes/commons/auth/LoginAspect.java b/src/main/java/cn/apes/commons/auth/LoginAspect.java
new file mode 100644
index 0000000..98bdbb0
--- /dev/null
+++ b/src/main/java/cn/apes/commons/auth/LoginAspect.java
@@ -0,0 +1,109 @@
+package cn.apes.commons.auth;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.redisson.api.RBucket;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 登录认证 AOP 切面
+ * 从 saas-service 的 apes-commons 0.0.7-SNAPSHOT 提取内联
+ *
+ * 拦截 @Login 注解的 Controller 类/方法:
+ * 1. 从请求头读取 token
+ * 2. 查 Redis 获取用户信息 (key=token) 和客户信息 (key=token:customer)
+ * 3. 组装 UserSession 写入 AuthContext ThreadLocal
+ * 4. 业务方法执行完毕后清理 AuthContext
+ *
+ * Order(2) — 在 DataSourceHeaderAspect (Order 1) 之后执行
+ */
+@Slf4j
+@Aspect
+@Component
+@Order(2)
+public class LoginAspect {
+
+ @Autowired
+ private RedissonClient redissonClient;
+
+ @Around("@within(cn.apes.commons.auth.Login)")
+ public Object aroundClass(ProceedingJoinPoint pjp) throws Throwable {
+ return process(pjp);
+ }
+
+ @Around("@annotation(cn.apes.commons.auth.Login)")
+ public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
+ return process(pjp);
+ }
+
+ private Object process(ProceedingJoinPoint pjp) throws Throwable {
+ try {
+ HttpServletRequest request = ((ServletRequestAttributes)
+ RequestContextHolder.getRequestAttributes()).getRequest();
+
+ String token = request.getHeader("token");
+ if (token == null || token.isEmpty()) {
+ token = request.getHeader("Token");
+ }
+
+ if (token != null && !token.isEmpty()) {
+ UserSession session = new UserSession();
+ session.setToken(token);
+
+ // 读取用户信息
+ RBucket userBucket = redissonClient.getBucket(token);
+ if (userBucket.isExists()) {
+ JSONObject userJson = userBucket.get();
+ if (userJson != null) {
+ LoginUser loginUser = new LoginUser();
+ loginUser.setId(userJson.getLong("id"));
+ loginUser.setNickName(userJson.getString("nickName"));
+ loginUser.setPhone(userJson.getString("phone"));
+ loginUser.setPortrait(userJson.getString("portrait"));
+ loginUser.setEmail(userJson.getString("email"));
+ session.setUser(loginUser);
+ }
+ }
+
+ // 读取客户信息
+ RBucket customerBucket = redissonClient.getBucket(token + ":customer");
+ if (customerBucket.isExists()) {
+ JSONObject customerJson = customerBucket.get();
+ if (customerJson != null) {
+ LoginCustomer customer = new LoginCustomer();
+ customer.setId(customerJson.getLong("id"));
+ customer.setCustomerType(customerJson.getInteger("customerType"));
+ customer.setCustomerName(customerJson.getString("customerName"));
+ customer.setShortName(customerJson.getString("shortName"));
+ customer.setLogo(customerJson.getString("logo"));
+ customer.setCustomerStatus(customerJson.getInteger("customerStatus"));
+ customer.setAuditRemark(customerJson.getString("auditRemark"));
+ customer.setAdminName(customerJson.getString("adminName"));
+ customer.setAdminPhone(customerJson.getString("adminPhone"));
+ customer.setProvince(customerJson.getString("province"));
+ customer.setCity(customerJson.getString("city"));
+ customer.setIndustry(customerJson.getString("industry"));
+ customer.setIndustrySub(customerJson.getString("industrySub"));
+ session.setCustomer(customer);
+ }
+ }
+
+ AuthContext.setLoginInfo(session);
+ }
+
+ return pjp.proceed();
+ } finally {
+ AuthContext.clean();
+ }
+ }
+}
diff --git a/src/main/java/cn/apes/commons/auth/LoginCustomer.java b/src/main/java/cn/apes/commons/auth/LoginCustomer.java
new file mode 100644
index 0000000..f2420e3
--- /dev/null
+++ b/src/main/java/cn/apes/commons/auth/LoginCustomer.java
@@ -0,0 +1,39 @@
+package cn.apes.commons.auth;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 登录客户(企业)信息(从 Redis 会话中反序列化)
+ * 从 saas-service 的 apes-commons 0.0.7-SNAPSHOT 提取内联
+ */
+@Data
+public class LoginCustomer implements Serializable {
+
+ private Long id;
+
+ private Integer customerType;
+
+ private String customerName;
+
+ private String shortName;
+
+ private String logo;
+
+ private Integer customerStatus;
+
+ private String auditRemark;
+
+ private String adminName;
+
+ private String adminPhone;
+
+ private String province;
+
+ private String city;
+
+ private String industry;
+
+ private String industrySub;
+}
diff --git a/src/main/java/cn/apes/commons/auth/LoginUser.java b/src/main/java/cn/apes/commons/auth/LoginUser.java
new file mode 100644
index 0000000..f757308
--- /dev/null
+++ b/src/main/java/cn/apes/commons/auth/LoginUser.java
@@ -0,0 +1,23 @@
+package cn.apes.commons.auth;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 登录用户信息(从 Redis 会话中反序列化)
+ * 从 saas-service 的 apes-commons 0.0.7-SNAPSHOT 提取内联
+ */
+@Data
+public class LoginUser implements Serializable {
+
+ private Long id;
+
+ private String nickName;
+
+ private String phone;
+
+ private String portrait;
+
+ private String email;
+}
diff --git a/src/main/java/cn/apes/commons/auth/UserSession.java b/src/main/java/cn/apes/commons/auth/UserSession.java
new file mode 100644
index 0000000..18622a8
--- /dev/null
+++ b/src/main/java/cn/apes/commons/auth/UserSession.java
@@ -0,0 +1,19 @@
+package cn.apes.commons.auth;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 用户会话(AuthContext ThreadLocal 中存储的对象)
+ * 从 saas-service 的 apes-commons 0.0.7-SNAPSHOT 提取内联
+ */
+@Data
+public class UserSession implements Serializable {
+
+ private LoginUser user;
+
+ private LoginCustomer customer;
+
+ private String token;
+}
diff --git a/src/main/java/cn/apes/commons/domain/DbEntity.java b/src/main/java/cn/apes/commons/domain/DbEntity.java
new file mode 100644
index 0000000..5bebcab
--- /dev/null
+++ b/src/main/java/cn/apes/commons/domain/DbEntity.java
@@ -0,0 +1,30 @@
+package cn.apes.commons.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 基础数据库实体
+ * 从 saas-service 的 apes-commons 0.0.7-SNAPSHOT 提取内联
+ *
+ * 提供主键 id、创建时间、更新时间三个公共字段
+ */
+@Data
+public class DbEntity implements Serializable {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Date createTime;
+
+ private Date updateTime;
+
+ /**
+ * 逻辑删除标记(0=未删除, 1=已删除)
+ */
+ private Integer isDel;
+}