This commit is contained in:
2026-08-02 15:22:39 +08:00
8 changed files with 163 additions and 17 deletions

View File

@@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.youlai.boot.framework.mybatis.handler.AutoFillMetaObjectHandler;
import com.youlai.boot.framework.mybatis.interceptor.MyDataPermissionHandler;
import com.youlai.boot.framework.mybatis.MetaFieldFillHandler;
import com.youlai.boot.framework.mybatis.MyDataPermissionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@@ -43,7 +43,7 @@ public class MybatisConfig {
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(new AutoFillMetaObjectHandler());
globalConfig.setMetaObjectHandler(new MetaFieldFillHandler());
return globalConfig;
}

View File

@@ -14,7 +14,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.util.AntPathMatcher;
@@ -32,8 +32,6 @@ import java.util.stream.Stream;
@Slf4j
public class OpenApiConfig {
private final Environment environment;
private final SecurityProperties securityProperties;
/**
@@ -42,13 +40,10 @@ public class OpenApiConfig {
@Bean
public OpenAPI openApi() {
String appVersion = environment.getProperty("project.version", "1.0.0");
return new OpenAPI()
.info(new Info()
.title("管理系统 API 文档")
.description("本文档涵盖管理系统的所有API接口包括登录认证、用户管理、角色管理、部门管理等功能模块提供详细的接口说明和使用指南。")
.version(appVersion)
.license(new License()
.name("Apache License 2.0")
.url("http://www.apache.org/licenses/LICENSE-2.0")

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.framework.mybatis.handler;
package com.youlai.boot.framework.mybatis;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.youlai.boot.framework.security.util.SecurityUtils;
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
* @since 3.0.0
*/
@Component
public class AutoFillMetaObjectHandler implements MetaObjectHandler {
public class MetaFieldFillHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.framework.mybatis.interceptor;
package com.youlai.boot.framework.mybatis;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
@@ -23,7 +23,7 @@ import java.lang.reflect.Method;
import java.util.List;
/**
* 数据权限控制
* 数据权限处理
* <p>
* 支持多角色数据权限合并并集策略
* - 如果任一角色是 ALL则跳过数据权限过滤

View File

@@ -4,12 +4,10 @@ spring:
profiles:
active: dev
config:
# 引入代码生成模块的配置(数据源、表前缀等),由 codegen.yml 提供
import: classpath:codegen.yml
servlet:
multipart:
# 单文件大小上限,取自 file-storage.upload.max-file-size单一来源
max-file-size: ${file-storage.upload.max-file-size:50MB}
max-request-size: ${file-storage.upload.max-file-size:50MB}
# 在 banner.txt 中显示项目版本,使用 @project.version@ 从 pom.xml 获取
project:
version: @project.version@

View File

@@ -7,7 +7,6 @@ ${AnsiColor.BRIGHT_BLUE}
|_|\___/ \__,_| |______\__,_|_|
${AnsiColor.BRIGHT_GREEN}
YouLai Boot Version: ${project.version}
Spring Boot Version: ${spring-boot.version}${spring-boot.formatted-version}
有来官网: https://www.youlai.tech/
版权所属: 有来开源组织

View File

@@ -0,0 +1,147 @@
package com.youlai.boot;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* 认证 + 当前用户接口单元测试
*
* @author Ray.Hao
* @since 4.6.0
*/
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("dev")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class AuthControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
private static String accessToken;
@Test
@Order(1)
@DisplayName("登录成功")
void loginSuccess() throws Exception {
String body = """
{
"username": "admin",
"password": "123456"
}
""";
MvcResult result = mockMvc.perform(post("/api/v1/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("00000"))
.andExpect(jsonPath("$.data.accessToken").isString())
.andExpect(jsonPath("$.data.refreshToken").isString())
.andExpect(jsonPath("$.data.tokenType").value("Bearer"))
.andExpect(jsonPath("$.data.expiresIn").isNumber())
.andReturn();
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
accessToken = root.path("data").path("accessToken").asText();
assertNotNull(accessToken, "登录后应返回 accessToken");
assertFalse(accessToken.isBlank(), "accessToken 不能为空");
}
@Test
@Order(2)
@DisplayName("密码错误登录失败")
void loginWithWrongPassword() throws Exception {
String body = """
{
"username": "admin",
"password": "wrong_password"
}
""";
mockMvc.perform(post("/api/v1/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("A0210"))
.andExpect(jsonPath("$.msg").value("密码错误"));
}
@Test
@Order(3)
@DisplayName("空用户名登录失败")
void loginWithEmptyUsername() throws Exception {
String body = """
{
"username": "",
"password": "123456"
}
""";
mockMvc.perform(post("/api/v1/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("B0001"));
}
@Test
@Order(4)
@DisplayName("登录后获取当前用户信息")
void getCurrentUserWithToken() throws Exception {
assertNotNull(accessToken, "accessToken 应由登录测试先行填充");
mockMvc.perform(get("/api/v1/users/me")
.header("Authorization", "Bearer " + accessToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("00000"))
.andExpect(jsonPath("$.data.userId").isNumber())
.andExpect(jsonPath("$.data.username").isString())
.andExpect(jsonPath("$.data.nickname").isString())
.andExpect(jsonPath("$.data.roles").isArray())
.andExpect(jsonPath("$.data.perms").isArray());
}
@Test
@Order(5)
@DisplayName("无 Token 请求 /me 返回令牌无效")
void getCurrentUserWithoutToken() throws Exception {
mockMvc.perform(get("/api/v1/users/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("A0230"))
.andExpect(jsonPath("$.msg").value("令牌无效或已过期"));
}
@Test
@Order(6)
@DisplayName("伪造 Token 请求 /me 返回令牌无效")
void getCurrentUserWithInvalidToken() throws Exception {
mockMvc.perform(get("/api/v1/users/me")
.header("Authorization", "Bearer invalid_token_xxx"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("A0230"));
}
}