diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..de8550a --- /dev/null +++ b/AGENTS.md @@ -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 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 diff --git a/src/main/java/com/yupi/springbootinit/controller/UserController.java b/src/main/java/com/yupi/springbootinit/controller/UserController.java index b0a4b90..9af6952 100644 --- a/src/main/java/com/yupi/springbootinit/controller/UserController.java +++ b/src/main/java/com/yupi/springbootinit/controller/UserController.java @@ -1,18 +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.SystemUsersService; +import com.yupi.springbootinit.service.SystemLoginLogService; import com.yupi.springbootinit.service.impl.LoginService; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -34,10 +27,20 @@ public class UserController { @Resource private LoginService loginService; + @Resource + private SystemLoginLogService systemLoginLogService; + // 用户登陆接口 @PostMapping("doLogin") public BaseResponse 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; + } } @@ -73,4 +76,12 @@ public class UserController { 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; + } + } } diff --git a/src/main/java/com/yupi/springbootinit/mapper/SystemLoginLogMapper.java b/src/main/java/com/yupi/springbootinit/mapper/SystemLoginLogMapper.java new file mode 100644 index 0000000..8087272 --- /dev/null +++ b/src/main/java/com/yupi/springbootinit/mapper/SystemLoginLogMapper.java @@ -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 { +} \ No newline at end of file diff --git a/src/main/java/com/yupi/springbootinit/model/entity/SystemLoginLog.java b/src/main/java/com/yupi/springbootinit/model/entity/SystemLoginLog.java new file mode 100644 index 0000000..52aeeb3 --- /dev/null +++ b/src/main/java/com/yupi/springbootinit/model/entity/SystemLoginLog.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/yupi/springbootinit/service/SystemLoginLogService.java b/src/main/java/com/yupi/springbootinit/service/SystemLoginLogService.java new file mode 100644 index 0000000..b376f20 --- /dev/null +++ b/src/main/java/com/yupi/springbootinit/service/SystemLoginLogService.java @@ -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{ + + /** + * 记录主播端 doLogin 登录日志 + * + * @param usersDTO 登录请求参数 + * @param usersVO 登录成功后的用户信息,失败时为 null + * @param success 是否登录成功 + */ + void recordDoLoginLog(SystemUsersDTO usersDTO, SystemUsersVO usersVO, boolean success); +} diff --git a/src/main/java/com/yupi/springbootinit/service/impl/SystemLoginLogServiceImpl.java b/src/main/java/com/yupi/springbootinit/service/impl/SystemLoginLogServiceImpl.java new file mode 100644 index 0000000..6d6876a --- /dev/null +++ b/src/main/java/com/yupi/springbootinit/service/impl/SystemLoginLogServiceImpl.java @@ -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 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(); + } +} diff --git a/src/main/resources/mapper/SystemLoginLogMapper.xml b/src/main/resources/mapper/SystemLoginLogMapper.xml new file mode 100644 index 0000000..ebfcfb1 --- /dev/null +++ b/src/main/resources/mapper/SystemLoginLogMapper.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + id, log_type, trace_id, user_id, user_type, username, `result`, user_ip, user_agent, + creator, create_time, updater, update_time, deleted, tenant_id + + \ No newline at end of file diff --git a/tk-data-user.2026-02-10.0.gz b/tk-data-user.2026-02-10.0.gz deleted file mode 100644 index afe1765..0000000 Binary files a/tk-data-user.2026-02-10.0.gz and /dev/null differ