Compare commits
3 Commits
1d1e45174c
...
edd78aa0ca
| Author | SHA1 | Date | |
|---|---|---|---|
| edd78aa0ca | |||
| 553eadfaa6 | |||
| 89fe1c2f66 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -147,3 +147,6 @@ fabric.properties
|
||||
|
||||
/CLAUDE.md
|
||||
/API_USAGE.md
|
||||
/.omc/
|
||||
/src/test/
|
||||
/docs/Feature-Ticket-API-Guide.md
|
||||
|
||||
8
pom.xml
8
pom.xml
@@ -16,7 +16,7 @@
|
||||
<version>1.0</version>
|
||||
<name>springboot-init</name>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -129,7 +129,11 @@
|
||||
<artifactId>x-file-storage-spring</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
<version>4.5.1</version> <scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -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<Object> errorResponse = new BaseResponse<>(code, null, message);
|
||||
|
||||
try (PrintWriter writer = response.getWriter()) {
|
||||
writer.write(JsonUtils.toJsonString(errorResponse));
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.yupi.springbootinit.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Feature Ticket 校验注解
|
||||
* 标注在需要 Ticket 校验的接口方法上
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface RequireFeatureTicket {
|
||||
|
||||
/**
|
||||
* 需要的功能代码,为空时不校验功能代码
|
||||
*/
|
||||
String featureCode() default "";
|
||||
|
||||
/**
|
||||
* 设备 ID 的请求头名称
|
||||
*/
|
||||
String machineIdHeader() default "X-Machine-Id";
|
||||
|
||||
/**
|
||||
* Ticket 的请求头名称
|
||||
*/
|
||||
String ticketHeader() default "X-Feature-Ticket";
|
||||
}
|
||||
@@ -23,7 +23,14 @@ public enum ErrorCode {
|
||||
SYSTEM_ERROR(50000, "系统内部异常"),
|
||||
OPERATION_ERROR(50001, "操作失败"),
|
||||
QUEUE_ERROR(60001, "队列消息添加失败"),
|
||||
QUEUE_CONSUMPTION_FAILURE(60001, "队列消息消费失败");
|
||||
QUEUE_CONSUMPTION_FAILURE(60001, "队列消息消费失败"),
|
||||
|
||||
// Feature Ticket 相关错误码
|
||||
TICKET_MISSING(40901, "缺少 Feature Ticket"),
|
||||
TICKET_INVALID(40902, "Ticket 无效或已损坏"),
|
||||
TICKET_EXPIRED(40903, "Ticket 已过期"),
|
||||
TICKET_MACHINE_MISMATCH(40904, "设备 ID 不匹配"),
|
||||
TICKET_FEATURE_MISMATCH(40905, "功能代码不匹配");
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.yupi.springbootinit.component;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import cn.hutool.crypto.symmetric.AES;
|
||||
import com.yupi.springbootinit.common.ErrorCode;
|
||||
import com.yupi.springbootinit.config.FeatureTicketProperties;
|
||||
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 javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Feature Ticket 认证组件
|
||||
* 负责 Ticket 的生成、加密、解密和校验
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Component
|
||||
public class FeatureAuthComponent {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FeatureAuthComponent.class);
|
||||
|
||||
@Resource
|
||||
private FeatureTicketProperties ticketProperties;
|
||||
|
||||
private AES aes;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 使用配置的 AES 密钥初始化加密器
|
||||
byte[] keyBytes = ticketProperties.getAesKey().getBytes(StandardCharsets.UTF_8);
|
||||
// 确保密钥长度为 16 字节(AES-128)
|
||||
if (keyBytes.length < 16) {
|
||||
byte[] paddedKey = new byte[16];
|
||||
System.arraycopy(keyBytes, 0, paddedKey, 0, keyBytes.length);
|
||||
keyBytes = paddedKey;
|
||||
} else if (keyBytes.length > 16 && keyBytes.length < 24) {
|
||||
byte[] truncatedKey = new byte[16];
|
||||
System.arraycopy(keyBytes, 0, truncatedKey, 0, 16);
|
||||
keyBytes = truncatedKey;
|
||||
} else if (keyBytes.length > 24 && keyBytes.length < 32) {
|
||||
byte[] truncatedKey = new byte[24];
|
||||
System.arraycopy(keyBytes, 0, truncatedKey, 0, 24);
|
||||
keyBytes = truncatedKey;
|
||||
} else if (keyBytes.length > 32) {
|
||||
byte[] truncatedKey = new byte[32];
|
||||
System.arraycopy(keyBytes, 0, truncatedKey, 0, 32);
|
||||
keyBytes = truncatedKey;
|
||||
}
|
||||
this.aes = SecureUtil.aes(keyBytes);
|
||||
log.info("FeatureAuthComponent 初始化完成,AES 密钥长度: {} 字节", keyBytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Feature Ticket
|
||||
*
|
||||
* @param tenantId 租户 ID
|
||||
* @param userId 用户 ID
|
||||
* @param machineId 设备 ID
|
||||
* @param featureCode 功能代码
|
||||
* @param expireSeconds 有效期(秒)
|
||||
* @return 加密后的 Base64 字符串
|
||||
*/
|
||||
public String generateTicket(Long tenantId, Long userId, String machineId,
|
||||
String featureCode, Long expireSeconds) {
|
||||
if (tenantId == null || userId == null || StrUtil.isBlank(machineId) || StrUtil.isBlank(featureCode)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "生成 Ticket 参数不完整");
|
||||
}
|
||||
|
||||
// 计算过期时间
|
||||
long expireTime = expireSeconds != null ? expireSeconds : ticketProperties.getDefaultExpireSeconds();
|
||||
long expiryTimestamp = System.currentTimeMillis() + (expireTime * 1000);
|
||||
|
||||
// 构建载荷
|
||||
FeatureTicketPayload payload = FeatureTicketPayload.builder()
|
||||
.tenantId(tenantId)
|
||||
.userId(userId)
|
||||
.machineId(machineId)
|
||||
.featureCode(featureCode)
|
||||
.expiryTimestamp(expiryTimestamp)
|
||||
.build();
|
||||
|
||||
// 序列化为 JSON
|
||||
String jsonPayload = JsonUtils.toJsonString(payload);
|
||||
|
||||
// AES 加密
|
||||
byte[] encryptedBytes = aes.encrypt(jsonPayload.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Base64 编码(URL 安全)
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(encryptedBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密 Ticket
|
||||
*
|
||||
* @param encryptedTicket 加密的 Ticket 字符串
|
||||
* @return 解密后的载荷
|
||||
*/
|
||||
public FeatureTicketPayload decryptTicket(String encryptedTicket) {
|
||||
if (StrUtil.isBlank(encryptedTicket)) {
|
||||
throw new BusinessException(ErrorCode.TICKET_INVALID, "Ticket 不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// Base64 解码
|
||||
byte[] encryptedBytes = Base64.getUrlDecoder().decode(encryptedTicket);
|
||||
|
||||
// AES 解密
|
||||
byte[] decryptedBytes = aes.decrypt(encryptedBytes);
|
||||
String jsonPayload = new String(decryptedBytes, StandardCharsets.UTF_8);
|
||||
|
||||
// 反序列化
|
||||
return JsonUtils.parseObject(jsonPayload, FeatureTicketPayload.class);
|
||||
} catch (Exception e) {
|
||||
log.error("Ticket 解密失败: {}", e.getMessage());
|
||||
throw new BusinessException(ErrorCode.TICKET_INVALID, "Ticket 无效或已损坏");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Ticket
|
||||
*
|
||||
* @param encryptedTicket 加密的 Ticket
|
||||
* @param requestMachineId 请求中的设备 ID
|
||||
* @param requiredFeatureCode 需要的功能代码(可选,为 null 时不校验)
|
||||
* @return 校验通过的载荷
|
||||
*/
|
||||
public FeatureTicketPayload validateTicket(String encryptedTicket, String requestMachineId,
|
||||
String requiredFeatureCode) {
|
||||
// 解密 Ticket
|
||||
FeatureTicketPayload payload = decryptTicket(encryptedTicket);
|
||||
|
||||
// 校验过期时间
|
||||
if (payload.getExpiryTimestamp() == null || System.currentTimeMillis() > payload.getExpiryTimestamp()) {
|
||||
throw new BusinessException(ErrorCode.TICKET_EXPIRED, "Ticket 已过期");
|
||||
}
|
||||
|
||||
// 校验设备 ID
|
||||
if (StrUtil.isBlank(requestMachineId)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求中缺少设备 ID");
|
||||
}
|
||||
if (!requestMachineId.equals(payload.getMachineId())) {
|
||||
log.warn("设备 ID 不匹配,请求: {}, Ticket: {}", requestMachineId, payload.getMachineId());
|
||||
throw new BusinessException(ErrorCode.TICKET_MACHINE_MISMATCH, "设备 ID 不匹配");
|
||||
}
|
||||
|
||||
// 校验功能代码(如果指定)
|
||||
if (StrUtil.isNotBlank(requiredFeatureCode) && !requiredFeatureCode.equals(payload.getFeatureCode())) {
|
||||
log.warn("功能代码不匹配,需要: {}, Ticket: {}", requiredFeatureCode, payload.getFeatureCode());
|
||||
throw new BusinessException(ErrorCode.TICKET_FEATURE_MISMATCH, "功能代码不匹配");
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化校验(仅校验有效性和设备 ID)
|
||||
*
|
||||
* @param encryptedTicket 加密的 Ticket
|
||||
* @param requestMachineId 请求中的设备 ID
|
||||
* @return 校验通过的载荷
|
||||
*/
|
||||
public FeatureTicketPayload validateTicket(String encryptedTicket, String requestMachineId) {
|
||||
return validateTicket(encryptedTicket, requestMachineId, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.yupi.springbootinit.config;
|
||||
|
||||
import com.yupi.springbootinit.Interceptor.FeatureTicketInterceptor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* Feature Ticket 拦截器配置
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Configuration
|
||||
public class FeatureTicketConfig implements WebMvcConfigurer {
|
||||
|
||||
@Resource
|
||||
private FeatureTicketInterceptor featureTicketInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册 Feature Ticket 拦截器
|
||||
// 拦截所有请求,但只有带 @RequireFeatureTicket 注解的接口才会进行校验
|
||||
registry.addInterceptor(featureTicketInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
// 排除静态资源
|
||||
"/static/**",
|
||||
"/webjars/**",
|
||||
// 排除 Swagger 文档
|
||||
"/doc.html",
|
||||
"/swagger-resources/**",
|
||||
"/v2/api-docs/**",
|
||||
"/v3/api-docs/**",
|
||||
// 排除错误页面
|
||||
"/error"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.yupi.springbootinit.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Feature Ticket 配置属性
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "feature-ticket")
|
||||
public class FeatureTicketProperties {
|
||||
|
||||
/**
|
||||
* AES 加密密钥(必须是 16/24/32 字节)
|
||||
*/
|
||||
private String aesKey = "Ehq8aoQcJRXpgMbfZOt2ms7USgT5zGVr";
|
||||
|
||||
/**
|
||||
* Ticket 默认有效期(秒),默认 5 分钟
|
||||
*/
|
||||
private Long defaultExpireSeconds = 300L;
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import com.yupi.springbootinit.service.AiCommentService;
|
||||
import com.yupi.springbootinit.service.AiTemplateService;
|
||||
import com.yupi.springbootinit.service.CommonService;
|
||||
import com.yupi.springbootinit.service.CountryInfoService;
|
||||
import com.yupi.springbootinit.service.SystemNoticeService;
|
||||
import com.yupi.springbootinit.model.entity.SystemNotice;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -39,6 +41,9 @@ public class CommonController {
|
||||
@Resource
|
||||
private AiCommentService aiCommentService;
|
||||
|
||||
@Resource
|
||||
private SystemNoticeService systemNoticeService;
|
||||
|
||||
@PostMapping("country_info")
|
||||
public BaseResponse<List<CountryInfoVO>> countryInfo() {
|
||||
|
||||
@@ -74,4 +79,11 @@ public class CommonController {
|
||||
public BaseResponse<String> health(){
|
||||
return ResultUtils.success("ok");
|
||||
}
|
||||
|
||||
@GetMapping("notice")
|
||||
public BaseResponse<List<SystemNotice>> getActiveNotice(){
|
||||
return ResultUtils.success(systemNoticeService.getActiveNoticeList());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.yupi.springbootinit.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.yupi.springbootinit.common.BaseResponse;
|
||||
import com.yupi.springbootinit.common.ErrorCode;
|
||||
import com.yupi.springbootinit.common.ResultUtils;
|
||||
import com.yupi.springbootinit.component.FeatureAuthComponent;
|
||||
import com.yupi.springbootinit.exception.BusinessException;
|
||||
import com.yupi.springbootinit.model.dto.ticket.FeatureTicketRequest;
|
||||
import com.yupi.springbootinit.model.entity.SystemTenant;
|
||||
import com.yupi.springbootinit.model.entity.SystemUsers;
|
||||
import com.yupi.springbootinit.service.SystemTenantService;
|
||||
import com.yupi.springbootinit.service.SystemUsersService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Feature Ticket 认证控制器
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
@Api(tags = "Feature Ticket 认证")
|
||||
public class FeatureAuthController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FeatureAuthController.class);
|
||||
|
||||
@Resource
|
||||
private FeatureAuthComponent featureAuthComponent;
|
||||
|
||||
@Resource
|
||||
private SystemUsersService systemUsersService;
|
||||
|
||||
@Resource
|
||||
private SystemTenantService systemTenantService;
|
||||
|
||||
/**
|
||||
* 请求 Feature Ticket
|
||||
* 根据当前登录用户,校验其所属租户和权限后生成 Ticket
|
||||
*/
|
||||
@PostMapping("/request-ticket")
|
||||
@ApiOperation(value = "请求 Feature Ticket", notes = "根据当前登录用户生成功能准考证")
|
||||
public BaseResponse<String> requestTicket(@RequestBody FeatureTicketRequest request) {
|
||||
// 参数校验
|
||||
if (request == null || StrUtil.isBlank(request.getMachineId()) || StrUtil.isBlank(request.getFeatureCode())) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "设备ID和功能代码不能为空");
|
||||
}
|
||||
|
||||
// 1. 获取当前登录用户
|
||||
if (!StpUtil.isLogin()) {
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
|
||||
}
|
||||
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
SystemUsers user = systemUsersService.getById(userId);
|
||||
if (user == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在");
|
||||
}
|
||||
|
||||
// 2. 校验用户状态
|
||||
if (user.getStatus() != null && user.getStatus() == 1) {
|
||||
throw new BusinessException(ErrorCode.USER_DISABLE);
|
||||
}
|
||||
|
||||
// 3. 获取租户信息
|
||||
Long tenantId = user.getTenantId();
|
||||
if (tenantId == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户未关联租户");
|
||||
}
|
||||
|
||||
SystemTenant tenant = systemTenantService.getById(tenantId);
|
||||
if (tenant == null) {
|
||||
throw new BusinessException(ErrorCode.TENANT_NAME_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 4. 校验租户状态
|
||||
if (tenant.getStatus() != null && tenant.getStatus() == 1) {
|
||||
throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "租户已停用");
|
||||
}
|
||||
|
||||
// 5. 校验租户是否过期(根据功能代码选择不同的过期时间字段)
|
||||
String featureCode = request.getFeatureCode();
|
||||
Date expireTime = getFeatureExpireTime(tenant, featureCode);
|
||||
if (expireTime != null && expireTime.before(new Date())) {
|
||||
throw new BusinessException(ErrorCode.PACKAGE_EXPIRED, "该功能套餐已过期");
|
||||
}
|
||||
|
||||
// 6. 校验用户是否有该功能的权限
|
||||
if (!checkUserFeaturePermission(user, featureCode)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无该功能权限");
|
||||
}
|
||||
|
||||
// 7. 生成 Ticket
|
||||
String ticket = featureAuthComponent.generateTicket(
|
||||
tenantId,
|
||||
userId,
|
||||
request.getMachineId(),
|
||||
featureCode,
|
||||
request.getExpireSeconds()
|
||||
);
|
||||
|
||||
log.info("用户 {} 成功获取 Feature Ticket,功能: {}, 设备: {}",
|
||||
userId, featureCode, request.getMachineId());
|
||||
|
||||
return ResultUtils.success(ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据功能代码获取对应的过期时间
|
||||
*/
|
||||
private Date getFeatureExpireTime(SystemTenant tenant, String featureCode) {
|
||||
if (featureCode == null) {
|
||||
return tenant.getExpireTime();
|
||||
}
|
||||
|
||||
switch (featureCode.toUpperCase()) {
|
||||
case "CRAWL":
|
||||
case "HOST_CRAWL":
|
||||
return tenant.getCrawlExpireTime();
|
||||
case "AI":
|
||||
case "AI_CHAT":
|
||||
return tenant.getAiExpireTime();
|
||||
case "BROTHER":
|
||||
case "BIG_BROTHER":
|
||||
return tenant.getBrotherExpireTime();
|
||||
default:
|
||||
return tenant.getExpireTime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户是否有指定功能的权限
|
||||
*/
|
||||
private boolean checkUserFeaturePermission(SystemUsers user, String featureCode) {
|
||||
if (featureCode == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (featureCode.toUpperCase()) {
|
||||
case "CRAWL":
|
||||
case "HOST_CRAWL":
|
||||
return user.getCrawl() != null && user.getCrawl() == 1;
|
||||
case "AI":
|
||||
case "AI_CHAT":
|
||||
return user.getAiChat() != null && user.getAiChat() == 1;
|
||||
case "BROTHER":
|
||||
case "BIG_BROTHER":
|
||||
return user.getBigBrother() != null && user.getBigBrother() == 1;
|
||||
case "WEB_AI":
|
||||
return user.getWebAi() != null && user.getWebAi() == 1;
|
||||
default:
|
||||
// 未知功能代码,默认不允许
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.yupi.springbootinit.controller;
|
||||
|
||||
import com.yupi.springbootinit.Interceptor.FeatureTicketInterceptor;
|
||||
import com.yupi.springbootinit.annotation.RequireFeatureTicket;
|
||||
import com.yupi.springbootinit.common.BaseResponse;
|
||||
import com.yupi.springbootinit.common.ResultUtils;
|
||||
import com.yupi.springbootinit.model.dto.ticket.FeatureTicketPayload;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 任务执行控制器(示例)
|
||||
* 展示如何使用 @RequireFeatureTicket 注解保护接口
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/v1")
|
||||
@Api(tags = "任务执行(示例)")
|
||||
public class TaskExecuteController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TaskExecuteController.class);
|
||||
|
||||
/**
|
||||
* 执行任务接口(受 Feature Ticket 保护)
|
||||
*
|
||||
* 调用此接口需要:
|
||||
* 1. 请求头 X-Feature-Ticket: 有效的 Ticket
|
||||
* 2. 请求头 X-Machine-Id: 与 Ticket 中一致的设备 ID
|
||||
*/
|
||||
@PostMapping("/execute-task")
|
||||
@RequireFeatureTicket(featureCode = "CRAWL")
|
||||
@ApiOperation(value = "执行任务", notes = "需要 Feature Ticket 校验,功能代码: CRAWL")
|
||||
public BaseResponse<Map<String, Object>> executeTask(
|
||||
@RequestBody Map<String, Object> taskParams,
|
||||
HttpServletRequest request) {
|
||||
|
||||
// 从请求属性中获取已校验的 Ticket 载荷
|
||||
FeatureTicketPayload payload = (FeatureTicketPayload) request.getAttribute(
|
||||
FeatureTicketInterceptor.TICKET_PAYLOAD_ATTR);
|
||||
|
||||
log.info("执行任务,用户: {}, 租户: {}, 设备: {}",
|
||||
payload.getUserId(), payload.getTenantId(), payload.getMachineId());
|
||||
|
||||
// 业务逻辑处理
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("taskId", System.currentTimeMillis());
|
||||
result.put("tenantId", payload.getTenantId());
|
||||
result.put("userId", payload.getUserId());
|
||||
result.put("message", "任务已提交执行");
|
||||
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 另一个受保护的接口示例(不限定功能代码)
|
||||
*
|
||||
* 只校验 Ticket 有效性和设备 ID,不校验功能代码
|
||||
*/
|
||||
@PostMapping("/query-status")
|
||||
@RequireFeatureTicket
|
||||
@ApiOperation(value = "查询状态", notes = "需要 Feature Ticket 校验,不限定功能代码")
|
||||
public BaseResponse<Map<String, Object>> queryStatus(HttpServletRequest request) {
|
||||
|
||||
FeatureTicketPayload payload = (FeatureTicketPayload) request.getAttribute(
|
||||
FeatureTicketInterceptor.TICKET_PAYLOAD_ATTR);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "running");
|
||||
result.put("featureCode", payload.getFeatureCode());
|
||||
result.put("tenantId", payload.getTenantId());
|
||||
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义请求头名称的示例
|
||||
*/
|
||||
@PostMapping("/custom-header-task")
|
||||
@RequireFeatureTicket(
|
||||
featureCode = "AI_CHAT",
|
||||
ticketHeader = "Authorization-Ticket",
|
||||
machineIdHeader = "Device-Id"
|
||||
)
|
||||
@ApiOperation(value = "自定义请求头任务", notes = "使用自定义请求头名称")
|
||||
public BaseResponse<String> customHeaderTask(HttpServletRequest request) {
|
||||
|
||||
FeatureTicketPayload payload = (FeatureTicketPayload) request.getAttribute(
|
||||
FeatureTicketInterceptor.TICKET_PAYLOAD_ATTR);
|
||||
|
||||
return ResultUtils.success("任务执行成功,用户: " + payload.getUserId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yupi.springbootinit.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yupi.springbootinit.model.entity.SystemNotice;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/2/10 20:25
|
||||
*/
|
||||
|
||||
public interface SystemNoticeMapper extends BaseMapper<SystemNotice> {
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.yupi.springbootinit.model.dto.ticket;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Feature Ticket 载荷
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FeatureTicketPayload implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 租户 ID
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 设备 ID(机器码)
|
||||
*/
|
||||
private String machineId;
|
||||
|
||||
/**
|
||||
* 功能代码
|
||||
*/
|
||||
private String featureCode;
|
||||
|
||||
/**
|
||||
* 过期时间戳(毫秒)
|
||||
*/
|
||||
private Long expiryTimestamp;
|
||||
|
||||
/**
|
||||
* 用户 ID
|
||||
*/
|
||||
private Long userId;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.yupi.springbootinit.model.dto.ticket;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 请求 Feature Ticket 的参数
|
||||
*
|
||||
* @author ziin
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "请求 Feature Ticket 的参数")
|
||||
public class FeatureTicketRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 设备 ID(机器码)
|
||||
*/
|
||||
@ApiModelProperty(value = "设备ID(机器码)", required = true)
|
||||
private String machineId;
|
||||
|
||||
/**
|
||||
* 功能代码
|
||||
*/
|
||||
@ApiModelProperty(value = "功能代码", required = true)
|
||||
private String featureCode;
|
||||
|
||||
/**
|
||||
* 自定义有效期(秒),可选
|
||||
*/
|
||||
@ApiModelProperty(value = "自定义有效期(秒),可选")
|
||||
private Long expireSeconds;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.yupi.springbootinit.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/2/10 20:25
|
||||
*/
|
||||
|
||||
/**
|
||||
* 通知公告表
|
||||
*/
|
||||
@ApiModel(description="通知公告表")
|
||||
@Data
|
||||
@TableName(value = "system_notice")
|
||||
public class SystemNotice {
|
||||
/**
|
||||
* 公告ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@ApiModelProperty(value="公告ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 公告标题
|
||||
*/
|
||||
@TableField(value = "title")
|
||||
@ApiModelProperty(value="公告标题")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 公告内容
|
||||
*/
|
||||
@TableField(value = "content")
|
||||
@ApiModelProperty(value="公告内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 公告类型(1通知 2公告)
|
||||
*/
|
||||
@TableField(value = "`type`")
|
||||
@ApiModelProperty(value="公告类型(1通知 2公告)")
|
||||
private Byte type;
|
||||
|
||||
/**
|
||||
* 公告状态(0正常 1关闭)
|
||||
*/
|
||||
@TableField(value = "`status`")
|
||||
@ApiModelProperty(value="公告状态(0正常 1关闭)")
|
||||
private Byte status;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(value = "creator")
|
||||
@ApiModelProperty(value="创建者")
|
||||
private String creator;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time")
|
||||
@ApiModelProperty(value="创建时间")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(value = "updater")
|
||||
@ApiModelProperty(value="更新者")
|
||||
private String updater;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "update_time")
|
||||
@ApiModelProperty(value="更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 是否删除
|
||||
*/
|
||||
@TableField(value = "deleted")
|
||||
@ApiModelProperty(value="是否删除")
|
||||
private Boolean deleted;
|
||||
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
@TableField(value = "tenant_id")
|
||||
@ApiModelProperty(value="租户编号")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 分类
|
||||
*/
|
||||
@TableField(value = "category")
|
||||
@ApiModelProperty(value="分类")
|
||||
private String category;
|
||||
}
|
||||
@@ -86,6 +86,13 @@ public class SystemTenant {
|
||||
@ApiModelProperty(value="过期时间")
|
||||
private Date expireTime;
|
||||
|
||||
/**
|
||||
* 爬主播过期时间
|
||||
*/
|
||||
@TableField(value = "crawl_expire_time")
|
||||
@ApiModelProperty(value="爬主播过期时间")
|
||||
private Date crawlExpireTime;
|
||||
|
||||
/**
|
||||
* ai过期时间
|
||||
*/
|
||||
@@ -100,6 +107,7 @@ public class SystemTenant {
|
||||
@ApiModelProperty(value="大哥过期时间")
|
||||
private Date brotherExpireTime;
|
||||
|
||||
|
||||
/**
|
||||
* 账号数量
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.yupi.springbootinit.model.vo.user;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
@@ -36,5 +38,16 @@ public class SystemUsersVO {
|
||||
|
||||
private Date aiExpireTime;
|
||||
|
||||
private Date crawlExpireTime;
|
||||
|
||||
private Byte aiReplay;
|
||||
|
||||
private Byte crawl;
|
||||
|
||||
private Byte bigBrother;
|
||||
|
||||
private Byte aiChat;
|
||||
|
||||
private Byte webAi;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yupi.springbootinit.service;
|
||||
|
||||
import com.yupi.springbootinit.model.entity.SystemNotice;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/2/10 20:25
|
||||
*/
|
||||
|
||||
public interface SystemNoticeService extends IService<SystemNotice>{
|
||||
|
||||
/**
|
||||
* 查询当前状态为开启的公告列表
|
||||
*/
|
||||
List<SystemNotice> getActiveNoticeList();
|
||||
|
||||
}
|
||||
@@ -76,9 +76,7 @@ public class LoginService {
|
||||
// 1. 校验用户名、密码、状态、租户过期
|
||||
SystemUsers user = validateUser(dto);
|
||||
// 2. 按场景校验角色权限
|
||||
checkRole(scene, user.getId());
|
||||
|
||||
|
||||
// checkRole(scene, user.getId());
|
||||
// 3. AI_CHAT 场景专属逻辑:缓存登录状态并动态创建 RabbitMQ 队列
|
||||
if (scene.equals(LoginSceneEnum.AI_CHAT)) {
|
||||
// 记录该用户已登录 AI_CHAT
|
||||
@@ -95,44 +93,11 @@ public class LoginService {
|
||||
.match();
|
||||
rabbitAdmin.declareBinding(binding);
|
||||
}
|
||||
// 3. 大哥场景专属逻辑:缓存登录状态并动态创建 RabbitMQ 队列
|
||||
if (scene.equals(LoginSceneEnum.BIG_BROTHER)) {
|
||||
// 记录该用户已登录 BIG_BROTHER
|
||||
redisTemplate.opsForValue().set("bigbrother_login:" + user.getTenantId() + ":" + user.getId(), true);
|
||||
String queueName = "b.tenant." + user.getTenantId();
|
||||
// 若该租户队列尚未创建,则创建队列并绑定到 HeadersExchange
|
||||
Queue queue = QueueBuilder.durable(queueName).build();
|
||||
rabbitAdmin.declareQueue(queue);
|
||||
Map<String, Object> headers = Map.of("tenantId", user.getTenantId(), "x-match", "all");
|
||||
Binding binding = BindingBuilder
|
||||
.bind(queue)
|
||||
.to(bigBrotherHeadersExchange) // 使用大哥专用交换机
|
||||
.whereAll(headers)
|
||||
.match();
|
||||
rabbitAdmin.declareBinding(binding);
|
||||
}
|
||||
if (scene.equals(LoginSceneEnum.WEB_AI)) {
|
||||
redisTemplate.opsForValue().set("webAI_login:" + user.getTenantId() + ":" + user.getId(), true);
|
||||
String queueName = "w.tenant." + user.getTenantId();
|
||||
// 若该租户队列尚未创建,则创建队列并绑定到 HeadersExchange
|
||||
Queue queue = QueueBuilder.durable(queueName).build();
|
||||
rabbitAdmin.declareQueue(queue);
|
||||
Map<String, Object> headers = Map.of("tenantId", user.getTenantId(), "x-match", "all");
|
||||
Binding binding = BindingBuilder
|
||||
.bind(queue)
|
||||
.to(webAiHeadersExchange) // 使用webAi专用交换机
|
||||
.whereAll(headers)
|
||||
.match();
|
||||
rabbitAdmin.declareBinding(binding);
|
||||
}
|
||||
|
||||
SystemTenant systemTenant = tenantMapper.selectById(user.getTenantId());
|
||||
// 封装返回数据
|
||||
SystemUsersVO vo = new SystemUsersVO();
|
||||
BeanUtil.copyProperties(user, vo);
|
||||
vo.setTokenName(StpUtil.getTokenName());
|
||||
vo.setTokenValue(StpUtil.getTokenValue());
|
||||
|
||||
// 5. Sa-Token 登录
|
||||
StpUtil.login(user.getId(), scene.getSaMode());
|
||||
switch (scene) {
|
||||
@@ -144,25 +109,15 @@ public class LoginService {
|
||||
vo.setAiExpireTime(systemTenant.getAiExpireTime());
|
||||
return vo;
|
||||
case HOST:
|
||||
StpUtil.renewTimeout(DateUtils.dateBetween(systemTenant.getExpireTime(),DateUtil.date()));
|
||||
BeanUtil.copyProperties(user, vo);
|
||||
vo.setTokenName(StpUtil.getTokenName());
|
||||
vo.setTokenValue(StpUtil.getTokenValue());
|
||||
vo.setExpireTime(systemTenant.getExpireTime());
|
||||
return vo;
|
||||
case BIG_BROTHER:
|
||||
StpUtil.renewTimeout(DateUtils.dateBetween(systemTenant.getBrotherExpireTime(),DateUtil.date()));
|
||||
BeanUtil.copyProperties(user, vo);
|
||||
vo.setTokenName(StpUtil.getTokenName());
|
||||
vo.setTokenValue(StpUtil.getTokenValue());
|
||||
vo.setBrotherExpireTime(systemTenant.getBrotherExpireTime());
|
||||
return vo;
|
||||
case WEB_AI:
|
||||
StpUtil.renewTimeout(DateUtils.dateBetween(systemTenant.getAiExpireTime(),DateUtil.date()));
|
||||
BeanUtil.copyProperties(user, vo);
|
||||
vo.setTokenName(StpUtil.getTokenName());
|
||||
vo.setTokenValue(StpUtil.getTokenValue());
|
||||
vo.setAiExpireTime(systemTenant.getAiExpireTime());
|
||||
vo.setCrawl(user.getCrawl());
|
||||
vo.setAiChat(user.getAiChat());
|
||||
vo.setBigBrother(user.getBigBrother());
|
||||
return vo;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.yupi.springbootinit.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.yupi.springbootinit.model.entity.SystemNotice;
|
||||
import com.yupi.springbootinit.mapper.SystemNoticeMapper;
|
||||
import com.yupi.springbootinit.service.SystemNoticeService;
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/2/10 20:25
|
||||
*/
|
||||
|
||||
@Service
|
||||
public class SystemNoticeServiceImpl extends ServiceImpl<SystemNoticeMapper, SystemNotice> implements SystemNoticeService{
|
||||
|
||||
@Override
|
||||
public List<SystemNotice> getActiveNoticeList() {
|
||||
LambdaQueryWrapper<SystemNotice> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SystemNotice::getStatus, 0);
|
||||
return this.list(queryWrapper);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -107,4 +107,11 @@ sa-token:
|
||||
md5:
|
||||
salt: (-FhqvXO,wMz
|
||||
|
||||
ai_log_path: /test/ai_log
|
||||
ai_log_path: /test/ai_log
|
||||
|
||||
# Feature Ticket 配置
|
||||
feature-ticket:
|
||||
# AES 加密密钥(必须是 16/24/32 字节,生产环境请修改)
|
||||
aes-key: Ehq8aoQcJRXpgMbfZOt2ms7USgT5zGVr
|
||||
# Ticket 默认有效期(秒),默认 5 分钟
|
||||
default-expire-seconds: 300
|
||||
25
src/main/resources/mapper/SystemNoticeMapper.xml
Normal file
25
src/main/resources/mapper/SystemNoticeMapper.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.yupi.springbootinit.mapper.SystemNoticeMapper">
|
||||
<resultMap id="BaseResultMap" type="com.yupi.springbootinit.model.entity.SystemNotice">
|
||||
<!--@mbg.generated-->
|
||||
<!--@Table system_notice-->
|
||||
<id column="id" jdbcType="BIGINT" property="id" />
|
||||
<result column="title" jdbcType="VARCHAR" property="title" />
|
||||
<result column="content" jdbcType="LONGVARCHAR" property="content" />
|
||||
<result column="type" jdbcType="TINYINT" property="type" />
|
||||
<result column="status" jdbcType="TINYINT" property="status" />
|
||||
<result column="creator" jdbcType="VARCHAR" property="creator" />
|
||||
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
|
||||
<result column="updater" jdbcType="VARCHAR" property="updater" />
|
||||
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
|
||||
<result column="deleted" jdbcType="BIT" property="deleted" />
|
||||
<result column="tenant_id" jdbcType="BIGINT" property="tenantId" />
|
||||
<result column="category" jdbcType="VARCHAR" property="category" />
|
||||
</resultMap>
|
||||
<sql id="Base_Column_List">
|
||||
<!--@mbg.generated-->
|
||||
id, title, content, `type`, `status`, creator, create_time, updater, update_time,
|
||||
deleted, tenant_id, category
|
||||
</sql>
|
||||
</mapper>
|
||||
BIN
tk-data-user.2026-02-10.0.gz
Normal file
BIN
tk-data-user.2026-02-10.0.gz
Normal file
Binary file not shown.
Reference in New Issue
Block a user