Compare commits
12 Commits
1d1e45174c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bfe6b1bc5e | |||
| 5fec4cb020 | |||
| abe20c99fe | |||
| dae4696224 | |||
| 3f0014c9b4 | |||
| e56fc03099 | |||
| e754fef4da | |||
| 6839ee7de3 | |||
| e75fae2ad7 | |||
| 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
|
||||
|
||||
195
AGENTS.md
Normal file
195
AGENTS.md
Normal 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
|
||||
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>
|
||||
|
||||
BIN
src/.DS_Store
vendored
Normal file
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
src/main/java/com/yupi/springbootinit/.DS_Store
vendored
Normal file
BIN
src/main/java/com/yupi/springbootinit/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -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";
|
||||
}
|
||||
@@ -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 start,id: {}, path: {}, ip: {}, params: {}", requestId, url,
|
||||
httpServletRequest.getRemoteHost(), reqParam);
|
||||
clientIp, reqParam);
|
||||
// 执行原方法
|
||||
Object result = point.proceed();
|
||||
// 输出响应日志
|
||||
@@ -53,4 +55,3 @@ public class LogInterceptor {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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,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;
|
||||
}
|
||||
@@ -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/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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 是否删除
|
||||
|
||||
@@ -195,4 +195,8 @@ public class SystemUsers {
|
||||
@TableField(value = "web_ai")
|
||||
@ApiModelProperty(value = "能否登录智能回复客户端")
|
||||
private Byte webAi;
|
||||
|
||||
@TableField(value = "points")
|
||||
@ApiModelProperty(value = "用户积分")
|
||||
private Integer points;
|
||||
}
|
||||
@@ -11,5 +11,5 @@ import java.util.Date;
|
||||
*/
|
||||
@Data
|
||||
public class SystemTenantVO {
|
||||
private Date expiredTime;
|
||||
private LocalDateTime expiredTime;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户登录信息
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/ServerCustomServiceInfoMapper.xml
Normal file
25
src/main/resources/mapper/ServerCustomServiceInfoMapper.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.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>
|
||||
28
src/main/resources/mapper/SystemLoginLogMapper.xml
Normal file
28
src/main/resources/mapper/SystemLoginLogMapper.xml
Normal 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>
|
||||
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>
|
||||
Reference in New Issue
Block a user