diff --git a/.gitignore b/.gitignore
index 61ecd18..76e877c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -148,3 +148,5 @@ fabric.properties
/CLAUDE.md
/API_USAGE.md
/.omc/
+/src/test/
+/docs/Feature-Ticket-API-Guide.md
diff --git a/pom.xml b/pom.xml
index ff0f0e0..7e0b949 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,7 @@
1.0
springboot-init
- 1.8
+ 17
@@ -129,7 +129,11 @@
x-file-storage-spring
2.3.0
-
+
+ org.mockito
+ mockito-inline
+ 4.5.1 test
+
diff --git a/src/main/java/com/yupi/springbootinit/Interceptor/FeatureTicketInterceptor.java b/src/main/java/com/yupi/springbootinit/Interceptor/FeatureTicketInterceptor.java
new file mode 100644
index 0000000..2973bad
--- /dev/null
+++ b/src/main/java/com/yupi/springbootinit/Interceptor/FeatureTicketInterceptor.java
@@ -0,0 +1,132 @@
+package com.yupi.springbootinit.Interceptor;
+
+import cn.hutool.core.util.StrUtil;
+import com.yupi.springbootinit.annotation.RequireFeatureTicket;
+import com.yupi.springbootinit.common.BaseResponse;
+import com.yupi.springbootinit.common.ErrorCode;
+import com.yupi.springbootinit.component.FeatureAuthComponent;
+import com.yupi.springbootinit.exception.BusinessException;
+import com.yupi.springbootinit.model.dto.ticket.FeatureTicketPayload;
+import com.yupi.springbootinit.utils.JsonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * Feature Ticket 拦截器
+ * 拦截带有 @RequireFeatureTicket 注解的接口
+ *
+ * @author ziin
+ */
+@Component
+public class FeatureTicketInterceptor implements HandlerInterceptor {
+
+ private static final Logger log = LoggerFactory.getLogger(FeatureTicketInterceptor.class);
+
+ @Resource
+ private FeatureAuthComponent featureAuthComponent;
+
+ /**
+ * 请求属性 Key:存储解析后的 Ticket 载荷
+ */
+ public static final String TICKET_PAYLOAD_ATTR = "FEATURE_TICKET_PAYLOAD";
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ // 仅处理 Controller 方法
+ if (!(handler instanceof HandlerMethod)) {
+ return true;
+ }
+
+ HandlerMethod handlerMethod = (HandlerMethod) handler;
+
+ // 获取方法或类上的注解
+ RequireFeatureTicket annotation = handlerMethod.getMethodAnnotation(RequireFeatureTicket.class);
+ if (annotation == null) {
+ annotation = handlerMethod.getBeanType().getAnnotation(RequireFeatureTicket.class);
+ }
+
+ // 没有注解,放行
+ if (annotation == null) {
+ return true;
+ }
+
+ // 获取请求头
+ String ticketHeader = annotation.ticketHeader();
+ String machineIdHeader = annotation.machineIdHeader();
+ String requiredFeatureCode = annotation.featureCode();
+
+ String ticket = request.getHeader(ticketHeader);
+ String machineId = request.getHeader(machineIdHeader);
+
+ // 校验 Ticket 是否存在
+ if (StrUtil.isBlank(ticket)) {
+ log.warn("请求缺少 Feature Ticket,URI: {}", request.getRequestURI());
+ writeErrorResponse(response, ErrorCode.TICKET_MISSING, "缺少 Feature Ticket");
+ return false;
+ }
+
+ // 校验设备 ID 是否存在
+ if (StrUtil.isBlank(machineId)) {
+ log.warn("请求缺少设备 ID,URI: {}", request.getRequestURI());
+ writeErrorResponse(response, ErrorCode.PARAMS_ERROR, "缺少设备 ID");
+ return false;
+ }
+
+ try {
+ // 校验 Ticket
+ FeatureTicketPayload payload;
+ if (StrUtil.isNotBlank(requiredFeatureCode)) {
+ payload = featureAuthComponent.validateTicket(ticket, machineId, requiredFeatureCode);
+ } else {
+ payload = featureAuthComponent.validateTicket(ticket, machineId);
+ }
+
+ // 将载荷存入请求属性,供后续业务使用
+ request.setAttribute(TICKET_PAYLOAD_ATTR, payload);
+
+ log.debug("Feature Ticket 校验通过,用户: {}, 租户: {}, 功能: {}",
+ payload.getUserId(), payload.getTenantId(), payload.getFeatureCode());
+
+ return true;
+ } catch (BusinessException e) {
+ log.warn("Feature Ticket 校验失败: {}", e.getMessage());
+ writeErrorResponse(response, e.getCode(), e.getMessage());
+ return false;
+ } catch (Exception e) {
+ log.error("Feature Ticket 校验异常", e);
+ writeErrorResponse(response, ErrorCode.SYSTEM_ERROR.getCode(), "Ticket 校验异常");
+ return false;
+ }
+ }
+
+ /**
+ * 写入错误响应
+ */
+ private void writeErrorResponse(HttpServletResponse response, ErrorCode errorCode, String message) throws IOException {
+ writeErrorResponse(response, errorCode.getCode(), message);
+ }
+
+ /**
+ * 写入错误响应
+ */
+ private void writeErrorResponse(HttpServletResponse response, int code, String message) throws IOException {
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ response.setContentType("application/json;charset=UTF-8");
+
+ BaseResponse