Compare commits

..

12 Commits

Author SHA1 Message Date
bfe6b1bc5e 修复 LogInterceptor获取不到用户真实IP的问题 2026-03-30 11:31:22 +08:00
5fec4cb020 refactor(core): 升级时间字段为 LocalDateTime
- 将 SystemTenant 及相关 VO 的过期时间字段由 java.util.Date 替换为 java.time.LocalDateTime
- 同步修改 DateUtils.dateBetween 签名并适配 LocalDateTime 计算
- 调整 FeatureAuthController 与 LoginService 中的时间比较逻辑
- 保持功能不变,提升类型安全与时区清晰度
2026-03-30 09:04:36 +08:00
abe20c99fe feat(host-info): 添加主播AI操作状态更新功能
- 新增 `/update_ai_operation` 接口,支持根据主播ID和租户ID更新AI操作状态
- 添加请求参数校验和幂等性检查逻辑
- 删除旧日志文件并添加新日志文件
2026-03-26 10:56:47 +08:00
dae4696224 feat(host-info): 添加主播AI操作状态更新功能
- 新增 `/update_ai_operation` 接口,支持根据主播ID和租户ID更新AI操作状态
- 添加请求参数校验和幂等性检查逻辑
- 删除旧日志文件并添加新日志文件
2026-03-26 10:56:38 +08:00
3f0014c9b4 添加获取当前用户信息接口 2026-03-16 17:16:42 +08:00
e56fc03099 feat(auth): 新增登录日志模块并记录登录失败
- 新增 SystemLoginLog 实体、Mapper、Service 及 XML 配置
- UserController 登录接口补充异常捕获与失败日志记录
- 删除旧压缩日志,新增最新日志文件
- 补充 AGENTS.md 使用说明文档
2026-03-10 11:49:45 +08:00
e754fef4da feat(custom-service): 新增客服信息查询接口及实体 2026-03-03 21:39:40 +08:00
6839ee7de3 feat(user): 新增用户积分字段
- SystemUsers 实体添加 points 字段及注解
- SystemUsersVO 视图对象同步 points 字段
- 生产环境配置切换为线上数据库地址
2026-03-02 16:14:23 +08:00
e75fae2ad7 fix(auth): 调整放行路径并补充租户筛选条件 2026-02-24 19:19:49 +08:00
edd78aa0ca feat(notice): 新增系统公告接口及后台实现 2026-02-11 19:14:07 +08:00
553eadfaa6 feat(core): 新增 Feature Ticket 鉴权体系与任务执行接口 2026-02-05 14:42:41 +08:00
89fe1c2f66 refactor(login): 精简登录逻辑并移除冗余角色校验 2026-02-03 19:25:32 +08:00
50 changed files with 1957 additions and 128 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.gitignore vendored
View File

@@ -147,3 +147,6 @@ fabric.properties
/CLAUDE.md
/API_USAGE.md
/.omc/
/src/test/
/docs/Feature-Ticket-API-Guide.md

195
AGENTS.md Normal file
View File

@@ -0,0 +1,195 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## Project Overview
This is a Spring Boot 2.7.2 application serving as a multi-tenant system with AI integration capabilities. The application focuses on host/streamer data management with AI chat functionality, built on a layered architecture pattern.
**Key Technologies:**
- Spring Boot 2.7.2 with Java 17
- MyBatis-Plus 3.5.2 for data access
- SA-Token 1.44.0 for authentication
- Redis for distributed caching and session management
- RabbitMQ for message-driven architecture
- Knife4j 4.4.0 for API documentation
- MySQL database
## Build and Run Commands
### Development
```bash
# Run the application (dev profile is default)
mvn spring-boot:run
# Build the project
mvn clean package
# Run tests
mvn test
# Skip tests during build
mvn clean package -DskipTests
```
### Database Setup
```bash
# Initialize database using the SQL script
mysql -u root -p < sql/create_table.sql
```
**Important:** Update database credentials in `src/main/resources/application.yml` before running.
### Access Points
- Application: http://localhost:8101/api
- API Documentation: http://localhost:8101/api/doc.html
## Architecture
### Layered Structure
The codebase follows a standard layered architecture:
**Controller Layer** (`controller/`) → **Service Layer** (`service/`) → **Mapper Layer** (`mapper/`) → **Database**
### Key Packages
- `annotation/` - Custom annotations like `@AuthCheck` for role-based access control
- `aop/` - Aspect-oriented programming for cross-cutting concerns (logging, auth)
- `config/` - Spring configuration classes for SA-Token, MyBatis-Plus, Redis, RabbitMQ, CORS
- `model/entity/` - Database entities mapped with MyBatis-Plus
- `model/dto/` - Data Transfer Objects for API requests
- `model/vo/` - View Objects for API responses
- `model/enums/` - Enumerations including `LoginSceneEnum` for multi-scenario authentication
- `exception/` - Custom exception handling with `GlobalExceptionHandler`
- `utils/` - Utility classes for common operations
### Authentication System (SA-Token)
The application uses SA-Token with **multiple login scenarios**:
1. **HOST** - Host/streamer login (`/user/doLogin`)
2. **BIG_BROTHER** - Admin tenant login (`/user/bigbrother-doLogin`)
3. **AI_CHAT** - AI chat login (`/user/aiChat-doLogin`)
4. **WEB_AI** - Web AI login (`/user/webAi-doLogin`)
Each scenario uses a different SA-Token login mode (defined in `LoginSceneEnum`) and has its own role validation logic in `SystemUsersService`.
**Token Configuration:**
- Token name: `vvtoken`
- Timeout: 172800 seconds (2 days)
- Non-concurrent login (new login kicks out old session)
- Token style: random-128
**Public Endpoints** (no authentication required):
- All Swagger/Knife4j documentation endpoints
- Login endpoints for all scenarios
- `/tenant/get-id-by-name`
- `/error`
See `SaTokenConfigure.java:35` for the complete list of excluded paths.
### Database Layer (MyBatis-Plus)
**Configuration Notes:**
- Camel case to underscore mapping is **disabled** (`map-underscore-to-camel-case: false`)
- Field names in entities must match database column names exactly
- Logical deletion is enabled globally via `isDelete` field
- Pagination is configured via `PaginationInnerInterceptor`
**Custom Mappers:**
- Complex queries use XML mappers in `src/main/resources/mapper/`
- Simple CRUD operations use MyBatis-Plus built-in methods
### Message Queue (RabbitMQ)
The application uses **HeadersExchange** pattern for message routing. Configuration is in `RabbitMQConfig.java`.
**Key Queues:**
- Multiple business-specific queues with persistent messages
- Manual acknowledgment mode
- JSON message serialization with Jackson
### Redis Integration
Redis is used for:
- Distributed session storage (Spring Session)
- SA-Token session management
- Custom caching needs
**Configuration:** See `RedisConfig.java` for JSON serialization setup with Jackson.
### Multi-Tenant Architecture
The system supports multi-tenancy with:
- `SystemTenant` entity for tenant management
- `SystemUsers` entity with tenant associations
- Tenant-specific authentication flows (BIG_BROTHER scenario)
- Tenant ID retrieval endpoint: `/tenant/get-id-by-name`
## Development Guidelines
### Adding New Endpoints
1. Create/update DTO in `model/dto/` for request validation
2. Create/update VO in `model/vo/` for response formatting
3. Add service method in appropriate service interface and implementation
4. Add controller endpoint with proper `@AuthCheck` annotation if authentication is required
5. If endpoint should be public, add path to `SaTokenConfigure.getExcludePaths()`
### Working with Authentication
**Role-based Access Control:**
```java
@AuthCheck(mustRole = UserRoleEnum.ADMIN)
public BaseResponse<String> adminOnlyEndpoint() {
// Implementation
}
```
**Multi-scenario Login:**
- Use `LoginSceneEnum` to determine the login scenario
- Each scenario has its own SA-Token mode and role validation
- Logout endpoints are scenario-specific (e.g., `/user/aiChat-logout`)
### Database Entities
**Important:** When creating or modifying entities:
- Use `@TableName` to specify exact table name
- Use `@TableField` for fields that don't follow naming conventions
- Include `isDelete` field for logical deletion support
- Add `createTime` and `updateTime` with appropriate defaults
### Code Generation
The project includes FreeMarker-based code generation utilities in the `generate/` package for scaffolding new modules.
## Configuration Files
- `application.yml` - Main configuration with SA-Token, database, and server settings
- `application-dev.yml` - Development environment overrides
- `application-prod.yml` - Production environment overrides
**Active Profile:** Defaults to `dev` (see `application.yml:9`)
## Important Notes
- **Redis:** Currently disabled in `MainApplication.java`. To enable, remove `RedisAutoConfiguration` from the `exclude` list and uncomment session store-type in `application.yml:17`
- **MyBatis Logging:** Disabled by default in static block of `MainApplication.java:24-27`
- **Context Path:** All endpoints are prefixed with `/api` (see `application.yml:42`)
- **File Upload Limit:** 10MB (see `application.yml:37`)
- **CORS:** Configured to allow all origins in `SaTokenConfigure.corsHandle()`
## Testing
Tests are located in `src/test/java/`. The project uses Spring Boot Test framework.
Run specific test class:
```bash
mvn test -Dtest=ClassName
```
## Common Issues
1. **Authentication Errors:** Verify the correct login endpoint is being used for the scenario (HOST, BIG_BROTHER, AI_CHAT, WEB_AI)
2. **Database Connection:** Ensure MySQL is running and credentials in `application.yml` are correct
3. **Redis Connection:** If Redis features are needed, enable Redis in `MainApplication.java` and configure connection in `application.yml`
4. **Field Mapping Issues:** Remember that camel-to-underscore mapping is disabled; field names must match exactly

View File

@@ -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>

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -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 TicketURI: {}", request.getRequestURI());
writeErrorResponse(response, ErrorCode.TICKET_MISSING, "缺少 Feature Ticket");
return false;
}
// 校验设备 ID 是否存在
if (StrUtil.isBlank(machineId)) {
log.warn("请求缺少设备 IDURI: {}", 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();
}
}
}

View File

@@ -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";
}

View File

@@ -1,5 +1,6 @@
package com.yupi.springbootinit.aop;
import com.yupi.springbootinit.utils.NetUtils;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@@ -41,9 +42,10 @@ public class LogInterceptor {
// 获取请求参数
Object[] args = point.getArgs();
String reqParam = "[" + StringUtils.join(args, ", ") + "]";
String clientIp = NetUtils.getIpAddress(httpServletRequest);
// 输出请求日志
log.info("request startid: {}, path: {}, ip: {}, params: {}", requestId, url,
httpServletRequest.getRemoteHost(), reqParam);
clientIp, reqParam);
// 执行原方法
Object result = point.proceed();
// 输出响应日志
@@ -53,4 +55,3 @@ public class LogInterceptor {
return result;
}
}

View File

@@ -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, "功能代码不匹配");
/**
* 状态码

View File

@@ -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);
}
}

View File

@@ -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"
);
}
}

View File

@@ -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;
}

View File

@@ -50,7 +50,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/user/bigbrother-doLogin",
"/user/aiChat-doLogin",
"/user/aiChat-logout",
"/user/webAi-doLogin",
"/user/webAi-doLogin",
"/common/notice",
"/error",
};
}

View File

@@ -4,12 +4,16 @@ import com.yupi.springbootinit.common.BaseResponse;
import com.yupi.springbootinit.common.ResultUtils;
import com.yupi.springbootinit.model.entity.AiComment;
import com.yupi.springbootinit.model.entity.AiTemplate;
import com.yupi.springbootinit.model.entity.ServerCustomServiceInfo;
import com.yupi.springbootinit.model.vo.common.AccountCrawlCount;
import com.yupi.springbootinit.model.vo.country.CountryInfoVO;
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.ServerCustomServiceInfoService;
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 +43,12 @@ public class CommonController {
@Resource
private AiCommentService aiCommentService;
@Resource
private SystemNoticeService systemNoticeService;
@Resource
private ServerCustomServiceInfoService serverCustomServiceInfoService;
@PostMapping("country_info")
public BaseResponse<List<CountryInfoVO>> countryInfo() {
@@ -74,4 +84,17 @@ public class CommonController {
public BaseResponse<String> health(){
return ResultUtils.success("ok");
}
@GetMapping("notice")
public BaseResponse<List<SystemNotice>> getActiveNotice(){
return ResultUtils.success(systemNoticeService.getActiveNoticeList());
}
@GetMapping("custom_service_info")
public BaseResponse<List<ServerCustomServiceInfo>> listCustomServiceInfo() {
return ResultUtils.success(serverCustomServiceInfoService.listNotDeleted());
}
}

View File

@@ -0,0 +1,168 @@
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.time.LocalDateTime;
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();
LocalDateTime expireTime = getFeatureExpireTime(tenant, featureCode);
if (expireTime != null && expireTime.isBefore(LocalDateTime.now())) {
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 LocalDateTime 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;
}
}
}

View File

@@ -3,6 +3,7 @@ package com.yupi.springbootinit.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yupi.springbootinit.common.BaseResponse;
import com.yupi.springbootinit.common.ResultUtils;
import com.yupi.springbootinit.model.dto.host.HostAiOperationUpdateRequest;
import com.yupi.springbootinit.model.dto.host.HostInfoDTO;
import com.yupi.springbootinit.model.dto.host.ServerLiveHostDetailDTO;
import com.yupi.springbootinit.model.entity.NewHosts;
@@ -58,6 +59,14 @@ public class HostInfoController {
}
@PostMapping("/update_ai_operation")
public BaseResponse<Boolean> updateAiOperation(@RequestBody HostAiOperationUpdateRequest request) {
String hostsId = request == null ? null : request.getHostsId();
Long tenantId = request == null ? null : request.getTenantId();
log.info("更新主播AI操作状态hostsId: {}, tenantId: {}", hostsId, tenantId);
return ResultUtils.success(hostInfoService.updateAiOperation(request));
}
/**
* 根据主播ID和租户ID查询直播明细
* @param detailDTO 查询条件包含hostsId和tenantId

View File

@@ -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());
}
}

View File

@@ -1,17 +1,11 @@
package com.yupi.springbootinit.controller;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import com.yupi.springbootinit.common.BaseResponse;
import com.yupi.springbootinit.common.ErrorCode;
import com.yupi.springbootinit.common.ResultUtils;
import com.yupi.springbootinit.exception.BusinessException;
import com.yupi.springbootinit.model.dto.user.SystemUsersDTO;
import com.yupi.springbootinit.model.entity.SystemUsers;
import com.yupi.springbootinit.model.enums.CommonStatusEnum;
import com.yupi.springbootinit.model.enums.LoginSceneEnum;
import com.yupi.springbootinit.model.vo.user.SystemUsersVO;
import com.yupi.springbootinit.service.SystemLoginLogService;
import com.yupi.springbootinit.service.SystemUsersService;
import com.yupi.springbootinit.service.impl.LoginService;
import lombok.extern.slf4j.Slf4j;
@@ -34,10 +28,23 @@ public class UserController {
@Resource
private LoginService loginService;
@Resource
private SystemLoginLogService systemLoginLogService;
@Resource
private SystemUsersService systemUsersService;
// 用户登陆接口
@PostMapping("doLogin")
public BaseResponse<SystemUsersVO> doLogin(@RequestBody SystemUsersDTO usersDTO) {
return ResultUtils.success(loginService.login(LoginSceneEnum.HOST, usersDTO));
try {
SystemUsersVO usersVO = loginService.login(LoginSceneEnum.HOST, usersDTO);
systemLoginLogService.recordDoLoginLog(usersDTO, usersVO, true);
return ResultUtils.success(usersVO);
} catch (RuntimeException exception) {
recordFailedDoLoginLog(usersDTO, exception);
throw exception;
}
}
@@ -68,9 +75,25 @@ public class UserController {
return ResultUtils.success(loginService.logout());
}
@GetMapping("/current")
public BaseResponse<SystemUsersVO> getCurrentUser() {
return ResultUtils.success(systemUsersService.getCurrentUserInfo());
}
@PostMapping("/bigbrother-logout")
public BaseResponse<Boolean> bigBrotherLogout(@RequestBody SystemUsersDTO usersDTO){
return ResultUtils.success(loginService.bigBrotherLogout(usersDTO));
}
private void recordFailedDoLoginLog(SystemUsersDTO usersDTO, RuntimeException exception) {
try {
systemLoginLogService.recordDoLoginLog(usersDTO, null, false);
} catch (RuntimeException logException) {
logException.addSuppressed(exception);
throw logException;
}
}
}

View File

@@ -0,0 +1,12 @@
package com.yupi.springbootinit.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yupi.springbootinit.model.entity.ServerCustomServiceInfo;
/*
* @author: ziin
* @date: 2026/3/3 21:32
*/
public interface ServerCustomServiceInfoMapper extends BaseMapper<ServerCustomServiceInfo> {
}

View File

@@ -0,0 +1,12 @@
package com.yupi.springbootinit.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yupi.springbootinit.model.entity.SystemLoginLog;
/*
* @author: ziin
* @date: 2026/3/10 11:32
*/
public interface SystemLoginLogMapper extends BaseMapper<SystemLoginLog> {
}

View File

@@ -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> {
}

View File

@@ -0,0 +1,23 @@
package com.yupi.springbootinit.model.dto.host;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 主播 AI 操作状态更新请求
*/
@Data
@ApiModel("主播 AI 操作状态更新请求")
public class HostAiOperationUpdateRequest implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主播ID", required = true, example = "host123")
private String hostsId;
@ApiModelProperty(value = "租户ID", required = true, example = "1001")
private Long tenantId;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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/3/3 21:32
*/
/**
* 客服信息表
*/
@ApiModel(description="客服信息表")
@Data
@TableName(value = "server_custom_service_info")
public class ServerCustomServiceInfo {
/**
* 主键id
*/
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty(value="主键id")
private Long id;
/**
* 姓名
*/
@TableField(value = "`name`")
@ApiModelProperty(value="姓名")
private String name;
/**
* 头像
*/
@TableField(value = "avater")
@ApiModelProperty(value="头像")
private String avater;
/**
* 简介
*/
@TableField(value = "description")
@ApiModelProperty(value="简介")
private String description;
/**
* 微信
*/
@TableField(value = "`concat`")
@ApiModelProperty(value="微信")
private String concat;
/**
* 手机号
*/
@TableField(value = "phone")
@ApiModelProperty(value="手机号")
private String phone;
/**
* 创建人
*/
@TableField(value = "creator")
@ApiModelProperty(value="创建人")
private String creator;
/**
* 创建时间
*/
@TableField(value = "create_time")
@ApiModelProperty(value="创建时间")
private Date createTime;
/**
* 更新人
*/
@TableField(value = "updater")
@ApiModelProperty(value="更新人")
private Long updater;
/**
* 更新时间
*/
@TableField(value = "update_time")
@ApiModelProperty(value="更新时间")
private Date updateTime;
/**
* 是否删除
*/
@TableField(value = "deleted")
@ApiModelProperty(value="是否删除")
private Boolean deleted;
/**
* 租户Id
*/
@TableField(value = "tenant_id")
@ApiModelProperty(value="租户Id")
private Long tenantId;
}

View File

@@ -0,0 +1,129 @@
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/3/10 11:32
*/
/**
* 系统访问记录
*/
@ApiModel(description="系统访问记录")
@Data
@TableName(value = "system_login_log")
public class SystemLoginLog {
/**
* 访问ID
*/
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty(value="访问ID")
private Long id;
/**
* 日志类型
*/
@TableField(value = "log_type")
@ApiModelProperty(value="日志类型")
private Long logType;
/**
* 链路追踪编号
*/
@TableField(value = "trace_id")
@ApiModelProperty(value="链路追踪编号")
private String traceId;
/**
* 用户编号
*/
@TableField(value = "user_id")
@ApiModelProperty(value="用户编号")
private Long userId;
/**
* 用户类型
*/
@TableField(value = "user_type")
@ApiModelProperty(value="用户类型")
private Byte userType;
/**
* 用户账号
*/
@TableField(value = "username")
@ApiModelProperty(value="用户账号")
private String username;
/**
* 登陆结果
*/
@TableField(value = "`result`")
@ApiModelProperty(value="登陆结果")
private Byte result;
/**
* 用户 IP
*/
@TableField(value = "user_ip")
@ApiModelProperty(value="用户 IP")
private String userIp;
/**
* 浏览器 UA
*/
@TableField(value = "user_agent")
@ApiModelProperty(value="浏览器 UA")
private String userAgent;
/**
* 创建者
*/
@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;
}

View File

@@ -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;
}

View File

@@ -84,21 +84,29 @@ public class SystemTenant {
*/
@TableField(value = "expire_time")
@ApiModelProperty(value="过期时间")
private Date expireTime;
private LocalDateTime expireTime;
/**
* 爬主播过期时间
*/
@TableField(value = "crawl_expire_time")
@ApiModelProperty(value="爬主播过期时间")
private LocalDateTime crawlExpireTime;
/**
* ai过期时间
*/
@TableField(value = "expire_time")
@ApiModelProperty(value="ai过期时间")
private Date aiExpireTime;
@TableField(value = "ai_expire_time")
private LocalDateTime aiExpireTime;
/**
* 大哥过期时间
*/
@TableField(value = "expire_time")
@ApiModelProperty(value="大哥过期时间")
private Date brotherExpireTime;
@TableField(value = "brother_expire_time")
private LocalDateTime brotherExpireTime;
/**
* 账号数量
@@ -117,9 +125,9 @@ public class SystemTenant {
/**
* 创建时间
*/
@TableField(value = "create_time")
@ApiModelProperty(value="创建时间")
private Date createTime;
@TableField(value = "create_time")
private LocalDateTime createTime;
/**
* 更新者
@@ -133,7 +141,7 @@ public class SystemTenant {
*/
@TableField(value = "update_time")
@ApiModelProperty(value="更新时间")
private Date updateTime;
private LocalDateTime updateTime;
/**
* 是否删除

View File

@@ -195,4 +195,8 @@ public class SystemUsers {
@TableField(value = "web_ai")
@ApiModelProperty(value = "能否登录智能回复客户端")
private Byte webAi;
@TableField(value = "points")
@ApiModelProperty(value = "用户积分")
private Integer points;
}

View File

@@ -11,5 +11,5 @@ import java.util.Date;
*/
@Data
public class SystemTenantVO {
private Date expiredTime;
private LocalDateTime expiredTime;
}

View File

@@ -1,7 +1,10 @@
package com.yupi.springbootinit.model.vo.user;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Date;
/*
@@ -30,11 +33,33 @@ public class SystemUsersVO {
private String tokenValue;
private Date expireTime;
private LocalDateTime expireTime;
private Date brotherExpireTime;
private LocalDateTime brotherExpireTime;
private Date aiExpireTime;
private LocalDateTime aiExpireTime;
private LocalDateTime crawlExpireTime;
private Byte aiReplay;
}
private Byte crawl;
private Byte bigBrother;
private Byte aiChat;
private Byte webAi;
private Boolean aiReplayEnabled;
private Boolean crawlEnabled;
private Boolean bigBrotherEnabled;
private Boolean aiChatEnabled;
private Boolean webAiEnabled;
private Integer points;
}

View File

@@ -3,14 +3,10 @@ package com.yupi.springbootinit.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yupi.springbootinit.model.dto.host.HistoryDataDTO;
import com.yupi.springbootinit.model.dto.host.HostAiOperationUpdateRequest;
import com.yupi.springbootinit.model.dto.host.HostInfoDTO;
import com.yupi.springbootinit.model.entity.NewHosts;
import com.yupi.springbootinit.model.vo.hosts.NewHostsVO;
import com.yupi.springbootinit.model.vo.hosts.SevenDaysData;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/*
* @author: ziin
@@ -21,4 +17,6 @@ public interface HostInfoService extends IService<NewHosts> {
Page<NewHostsVO> getConditionHosts(HostInfoDTO hostInfoDTO);
boolean updateAiOperation(HostAiOperationUpdateRequest request);
}

View File

@@ -0,0 +1,20 @@
package com.yupi.springbootinit.service;
import com.yupi.springbootinit.model.entity.ServerCustomServiceInfo;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/*
* @author: ziin
* @date: 2026/3/3 21:32
*/
public interface ServerCustomServiceInfoService extends IService<ServerCustomServiceInfo>{
/**
* 查询所有未删除的客服信息
*
* @return 未删除的客服信息列表
*/
List<ServerCustomServiceInfo> listNotDeleted();
}

View File

@@ -0,0 +1,22 @@
package com.yupi.springbootinit.service;
import com.yupi.springbootinit.model.entity.SystemLoginLog;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yupi.springbootinit.model.dto.user.SystemUsersDTO;
import com.yupi.springbootinit.model.vo.user.SystemUsersVO;
/*
* @author: ziin
* @date: 2026/3/10 11:32
*/
public interface SystemLoginLogService extends IService<SystemLoginLog>{
/**
* 记录主播端 doLogin 登录日志
*
* @param usersDTO 登录请求参数
* @param usersVO 登录成功后的用户信息,失败时为 null
* @param success 是否登录成功
*/
void recordDoLoginLog(SystemUsersDTO usersDTO, SystemUsersVO usersVO, boolean success);
}

View File

@@ -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();
}

View File

@@ -7,6 +7,7 @@ package com.yupi.springbootinit.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yupi.springbootinit.model.entity.SystemUsers;
import com.yupi.springbootinit.model.vo.user.SystemUsersVO;
public interface SystemUsersService extends IService<SystemUsers> {
@@ -26,4 +27,6 @@ public interface SystemUsersService extends IService<SystemUsers> {
boolean checkAiCHatLoginRole(Long userId);
boolean checkWebAILoginRole(Long userId);
SystemUsersVO getCurrentUserInfo();
}

View File

@@ -1,27 +1,34 @@
package com.yupi.springbootinit.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yupi.springbootinit.common.ErrorCode;
import com.yupi.springbootinit.exception.BusinessException;
import com.yupi.springbootinit.mapper.NewHostsMapper;
import com.yupi.springbootinit.model.dto.host.HostAiOperationUpdateRequest;
import com.yupi.springbootinit.model.dto.host.HostInfoDTO;
import com.yupi.springbootinit.model.entity.NewHosts;
import com.yupi.springbootinit.model.vo.hosts.NewHostsVO;
import com.yupi.springbootinit.service.HostInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
/*
* @author: ziin
* @date: 2025/6/10 19:04
*/
@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class HostInfoServiceImpl extends ServiceImpl<NewHostsMapper, NewHosts> implements HostInfoService {
private static final byte AI_OPERATION_ENABLED = 1;
private static final byte NOT_DELETED = 0;
@Resource
private NewHostsMapper newHostsMapper;
@@ -31,5 +38,34 @@ public class HostInfoServiceImpl extends ServiceImpl<NewHostsMapper, NewHosts> i
return newHostsMapper.selectPageByCondition(page, hostInfoDTO);
}
@Override
public boolean updateAiOperation(HostAiOperationUpdateRequest request) {
validateAiOperationUpdateRequest(request);
List<NewHosts> matchedHosts = lambdaQuery()
.eq(NewHosts::getHostsId, request.getHostsId())
.eq(NewHosts::getTenantId, request.getTenantId())
.eq(NewHosts::getDeleted, NOT_DELETED)
.list();
if (matchedHosts.isEmpty()) {
return false;
}
boolean allAiOperationEnabled = matchedHosts.stream()
.allMatch(host -> Objects.equals(host.getAiOperation(), AI_OPERATION_ENABLED));
if (allAiOperationEnabled) {
return true;
}
return lambdaUpdate()
.eq(NewHosts::getHostsId, request.getHostsId())
.eq(NewHosts::getTenantId, request.getTenantId())
.eq(NewHosts::getDeleted, NOT_DELETED)
.set(NewHosts::getAiOperation, AI_OPERATION_ENABLED)
.update();
}
private void validateAiOperationUpdateRequest(HostAiOperationUpdateRequest request) {
if (request == null || StrUtil.isBlank(request.getHostsId()) || request.getTenantId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "hostsId和tenantId不能为空");
}
}
}

View File

@@ -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,79 +93,47 @@ 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) {
case AI_CHAT:
StpUtil.renewTimeout(DateUtils.dateBetween(systemTenant.getAiExpireTime(),DateUtil.date()));
StpUtil.renewTimeout(DateUtils.dateBetween(systemTenant.getAiExpireTime(), DateUtil.date().toLocalDateTime()));
BeanUtil.copyProperties(user, vo);
vo.setTokenName(StpUtil.getTokenName());
vo.setTokenValue(StpUtil.getTokenValue());
vo.setAiExpireTime(systemTenant.getAiExpireTime());
fillFeatureInfo(vo, user, systemTenant);
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());
fillFeatureInfo(vo, user, systemTenant);
return vo;
}
return null;
}
private void fillFeatureInfo(SystemUsersVO vo, SystemUsers user, SystemTenant tenant) {
vo.setExpireTime(tenant.getExpireTime());
vo.setCrawlExpireTime(tenant.getCrawlExpireTime());
vo.setBrotherExpireTime(tenant.getBrotherExpireTime());
vo.setAiExpireTime(tenant.getAiExpireTime());
vo.setAiReplayEnabled(isEnabled(user.getAiReplay()));
vo.setCrawlEnabled(isEnabled(user.getCrawl()));
vo.setBigBrotherEnabled(isEnabled(user.getBigBrother()));
vo.setAiChatEnabled(isEnabled(user.getAiChat()));
vo.setWebAiEnabled(isEnabled(user.getWebAi()));
}
private boolean isEnabled(Byte value) {
return value != null && value == 1;
}
/**
* 校验用户登录信息
*

View File

@@ -0,0 +1,26 @@
package com.yupi.springbootinit.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yupi.springbootinit.model.entity.ServerCustomServiceInfo;
import com.yupi.springbootinit.mapper.ServerCustomServiceInfoMapper;
import com.yupi.springbootinit.service.ServerCustomServiceInfoService;
import java.util.List;
/*
* @author: ziin
* @date: 2026/3/3 21:32
*/
@Service
public class ServerCustomServiceInfoServiceImpl extends ServiceImpl<ServerCustomServiceInfoMapper, ServerCustomServiceInfo> implements ServerCustomServiceInfoService{
@Override
public List<ServerCustomServiceInfo> listNotDeleted() {
QueryWrapper<ServerCustomServiceInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("deleted", false);
return this.list(queryWrapper);
}
}

View File

@@ -0,0 +1,83 @@
package com.yupi.springbootinit.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yupi.springbootinit.mapper.SystemLoginLogMapper;
import com.yupi.springbootinit.model.dto.user.SystemUsersDTO;
import com.yupi.springbootinit.model.entity.SystemLoginLog;
import com.yupi.springbootinit.model.vo.user.SystemUsersVO;
import com.yupi.springbootinit.service.SystemLoginLogService;
import com.yupi.springbootinit.utils.NetUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.UUID;
/*
* @author: ziin
* @date: 2026/3/10 11:32
*/
@Service
public class SystemLoginLogServiceImpl extends ServiceImpl<SystemLoginLogMapper, SystemLoginLog> implements SystemLoginLogService{
private static final Long HOST_LOGIN_LOG_TYPE = 1L;
private static final Byte LOGIN_SUCCESS = 1;
private static final Byte LOGIN_FAILURE = 0;
private static final String USER_AGENT_HEADER = "User-Agent";
private static final String TRACE_ID_HEADER = "X-Trace-Id";
private static final String SYSTEM_OPERATOR = "system";
@Override
public void recordDoLoginLog(SystemUsersDTO usersDTO, SystemUsersVO usersVO, boolean success) {
HttpServletRequest request = getCurrentRequest();
Date now = new Date();
SystemLoginLog loginLog = new SystemLoginLog();
loginLog.setLogType(HOST_LOGIN_LOG_TYPE);
loginLog.setTraceId(resolveTraceId(request));
loginLog.setUserId(usersVO == null ? null : usersVO.getId());
loginLog.setUsername(resolveUsername(usersDTO, usersVO));
loginLog.setResult(success ? LOGIN_SUCCESS : LOGIN_FAILURE);
loginLog.setUserIp(NetUtils.getIpAddress(request));
loginLog.setUserAgent(request.getHeader(USER_AGENT_HEADER));
loginLog.setCreator(SYSTEM_OPERATOR);
loginLog.setCreateTime(now);
loginLog.setUpdater(SYSTEM_OPERATOR);
loginLog.setUpdateTime(now);
loginLog.setDeleted(Boolean.FALSE);
loginLog.setTenantId(resolveTenantId(usersDTO, usersVO));
boolean saved = save(loginLog);
if (!saved) {
throw new IllegalStateException("保存 doLogin 登录日志失败");
}
}
private HttpServletRequest getCurrentRequest() {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
return attributes.getRequest();
}
private String resolveTraceId(HttpServletRequest request) {
String traceId = request.getHeader(TRACE_ID_HEADER);
if (traceId == null || traceId.isBlank()) {
return UUID.randomUUID().toString();
}
return traceId;
}
private String resolveUsername(SystemUsersDTO usersDTO, SystemUsersVO usersVO) {
if (usersVO != null && usersVO.getUsername() != null) {
return usersVO.getUsername();
}
return usersDTO.getUsername();
}
private Long resolveTenantId(SystemUsersDTO usersDTO, SystemUsersVO usersVO) {
if (usersVO != null && usersVO.getTenantId() != null) {
return usersVO.getTenantId();
}
return usersDTO.getTenantId();
}
}

View File

@@ -0,0 +1,26 @@
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)
.eq(SystemNotice::getTenantId, 1);
return this.list(queryWrapper);
}
}

View File

@@ -1,22 +1,23 @@
package com.yupi.springbootinit.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yupi.springbootinit.common.ErrorCode;
import com.yupi.springbootinit.exception.BusinessException;
import com.yupi.springbootinit.mapper.SystemTenantMapper;
import com.yupi.springbootinit.model.entity.SystemTenant;
import com.yupi.springbootinit.model.entity.SystemUsers;
import com.yupi.springbootinit.service.SystemTenantService;
import com.yupi.springbootinit.mapper.SystemUsersMapper;
import com.yupi.springbootinit.model.vo.user.SystemUsersVO;
import com.yupi.springbootinit.service.SystemUsersService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import com.yupi.springbootinit.mapper.SystemUsersMapper;
import com.yupi.springbootinit.service.SystemUsersService;
import javax.annotation.Resource;
/*
* @author: ziin
@@ -50,14 +51,14 @@ public class SystemUsersServiceImpl extends ServiceImpl<SystemUsersMapper,System
@Override
public boolean isExpired(Long tendId) {
SystemTenant systemTenant = systemTenantMapper.selectById(tendId);
long between = DateUtil.between(systemTenant.getExpireTime(), DateUtil.date(), DateUnit.DAY);
long between = DateUtil.between(DateUtil.date(systemTenant.getExpireTime()), DateUtil.date(), DateUnit.DAY);
return between < 0;
}
@Override
public Long getTenantExpiredTime(Long tenantId) {
SystemTenant systemTenant = systemTenantMapper.selectById(tenantId);
long between = DateUtil.between(systemTenant.getExpireTime(), DateUtil.date(), DateUnit.SECOND);
long between = DateUtil.between(DateUtil.date(systemTenant.getExpireTime()), DateUtil.date(), DateUnit.SECOND);
if (between > 0 ) {
return between;
}
@@ -90,5 +91,43 @@ public class SystemUsersServiceImpl extends ServiceImpl<SystemUsersMapper,System
}
@Override
public SystemUsersVO getCurrentUserInfo() {
Long userId = StpUtil.getLoginIdAsLong();
SystemUsers user = baseMapper.selectById(userId);
if (user == null || Boolean.TRUE.equals(user.getDeleted())) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在");
}
SystemTenant tenant = systemTenantMapper.selectById(user.getTenantId());
if (tenant == null || Boolean.TRUE.equals(tenant.getDeleted())) {
throw new BusinessException(ErrorCode.TENANT_NAME_NOT_EXISTS);
}
return buildSystemUsersVO(user, tenant);
}
private SystemUsersVO buildSystemUsersVO(SystemUsers user, SystemTenant tenant) {
SystemUsersVO userVO = new SystemUsersVO();
BeanUtil.copyProperties(user, userVO);
userVO.setTokenName(StpUtil.getTokenName());
userVO.setTokenValue(StpUtil.getTokenValue());
userVO.setExpireTime(tenant.getExpireTime());
userVO.setCrawlExpireTime(tenant.getCrawlExpireTime());
userVO.setBrotherExpireTime(tenant.getBrotherExpireTime());
userVO.setAiExpireTime(tenant.getAiExpireTime());
fillFeatureEnabled(userVO, user);
return userVO;
}
private void fillFeatureEnabled(SystemUsersVO userVO, SystemUsers user) {
userVO.setAiReplayEnabled(isEnabled(user.getAiReplay()));
userVO.setCrawlEnabled(isEnabled(user.getCrawl()));
userVO.setBigBrotherEnabled(isEnabled(user.getBigBrother()));
userVO.setAiChatEnabled(isEnabled(user.getAiChat()));
userVO.setWebAiEnabled(isEnabled(user.getWebAi()));
}
private boolean isEnabled(Byte value) {
return value != null && value == 1;
}
}

View File

@@ -3,6 +3,7 @@ package com.yupi.springbootinit.utils;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import java.time.LocalDateTime;
import java.util.Date;
/*
@@ -11,7 +12,8 @@ import java.util.Date;
*/
public class DateUtils {
public static Long dateBetween(Date date1, Date date2) {
return DateUtil.between(date1, date2, DateUnit.SECOND);
public static Long dateBetween(LocalDateTime date1, LocalDateTime date2) {
return DateUtil.between(DateUtil.date(date1), DateUtil.date(date2), DateUnit.SECOND);
}
}

View File

@@ -1,7 +1,9 @@
package com.yupi.springbootinit.utils;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
/**
* 网络工具类
@@ -11,6 +13,23 @@ import javax.servlet.http.HttpServletRequest;
*/
public class NetUtils {
private static final String UNKNOWN = "unknown";
private static final String LOCALHOST_IPV4 = "127.0.0.1";
private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
private static final String LOCALHOST_IPV6_SHORT = "::1";
private static final String[] IP_HEADER_CANDIDATES = {
"x-forwarded-for",
"X-Forwarded-For",
"X-Real-IP",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR"
};
/**
* 获取客户端 IP 地址
*
@@ -18,38 +37,57 @@ public class NetUtils {
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
String headerIp = getIpFromHeaders(request);
if (StringUtils.isNotBlank(headerIp)) {
return headerIp;
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (ip.equals("127.0.0.1")) {
// 根据网卡取本机配置的 IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (Exception e) {
e.printStackTrace();
}
if (inet != null) {
ip = inet.getHostAddress();
}
return normalizeLocalIp(request.getRemoteAddr());
}
private static String getIpFromHeaders(HttpServletRequest request) {
for (String headerName : IP_HEADER_CANDIDATES) {
String headerValue = request.getHeader(headerName);
String ip = extractFirstValidIp(headerValue);
if (StringUtils.isNotBlank(ip)) {
return ip;
}
}
// 多个代理的情况第一个IP为客户端真实IP,多个IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
return null;
}
private static String extractFirstValidIp(String ipValue) {
if (StringUtils.isBlank(ipValue)) {
return null;
}
String[] ipList = ipValue.split(",");
for (String ip : ipList) {
String trimmedIp = StringUtils.trim(ip);
if (StringUtils.isNotBlank(trimmedIp) && !UNKNOWN.equalsIgnoreCase(trimmedIp)) {
return trimmedIp;
}
}
if (ip == null) {
return "127.0.0.1";
return null;
}
private static String normalizeLocalIp(String remoteAddr) {
if (StringUtils.isBlank(remoteAddr)) {
return LOCALHOST_IPV4;
}
return ip;
if (!isLocalhost(remoteAddr)) {
return remoteAddr;
}
try {
InetAddress inetAddress = InetAddress.getLocalHost();
return inetAddress.getHostAddress();
} catch (UnknownHostException e) {
return remoteAddr;
}
}
private static boolean isLocalhost(String ip) {
return LOCALHOST_IPV4.equals(ip)
|| LOCALHOST_IPV6.equals(ip)
|| LOCALHOST_IPV6_SHORT.equals(ip);
}
}

View File

@@ -8,19 +8,19 @@ spring:
# todo 需替换配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3326/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
url: jdbc:mysql://47.79.98.113:3326/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
username: root
password: wfn53400
# Redis 配置
# todo 需替换配置
redis:
database: 1
host: localhost
host: 47.79.98.113
port: 16379
timeout: 5000
password: ezyPM2UQkPO8O6i8s9
rabbitmq:
host: localhost
host: 47.79.98.113
port: 5672
username: tkdata
password: 6rARaRj8Z7UG3ahLzh

View File

@@ -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

View 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.ServerCustomServiceInfoMapper">
<resultMap id="BaseResultMap" type="com.yupi.springbootinit.model.entity.ServerCustomServiceInfo">
<!--@mbg.generated-->
<!--@Table server_custom_service_info-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="avater" jdbcType="VARCHAR" property="avater" />
<result column="description" jdbcType="VARCHAR" property="description" />
<result column="concat" jdbcType="VARCHAR" property="concat" />
<result column="phone" jdbcType="VARCHAR" property="phone" />
<result column="creator" jdbcType="VARCHAR" property="creator" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="updater" jdbcType="BIGINT" 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" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, `name`, avater, description, `concat`, phone, creator, create_time, updater,
update_time, deleted, tenant_id
</sql>
</mapper>

View File

@@ -0,0 +1,28 @@
<?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.SystemLoginLogMapper">
<resultMap id="BaseResultMap" type="com.yupi.springbootinit.model.entity.SystemLoginLog">
<!--@mbg.generated-->
<!--@Table system_login_log-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="log_type" jdbcType="BIGINT" property="logType" />
<result column="trace_id" jdbcType="VARCHAR" property="traceId" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="user_type" jdbcType="TINYINT" property="userType" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="result" jdbcType="TINYINT" property="result" />
<result column="user_ip" jdbcType="VARCHAR" property="userIp" />
<result column="user_agent" jdbcType="VARCHAR" property="userAgent" />
<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" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, log_type, trace_id, user_id, user_type, username, `result`, user_ip, user_agent,
creator, create_time, updater, update_time, deleted, tenant_id
</sql>
</mapper>

View 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>