feat: 添加异常日志功能并优化UI样式

refactor: 重构后端查询逻辑和API响应处理

fix: 修复用户角色更新和文件上传问题

test: 添加前端性能测试脚本和E2E测试用例

chore: 更新依赖版本和配置文件

docs: 添加环境检查脚本和测试文档

style: 统一表格标签样式和路由命名

perf: 优化前端页面加载速度和响应时间
This commit is contained in:
张翔
2026-03-24 13:32:20 +08:00
parent a97d317e4a
commit be5d5ede90
184 changed files with 11231 additions and 1903 deletions
+5 -3
View File
@@ -48,12 +48,14 @@
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-reactor</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
@@ -93,4 +95,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
@@ -1,11 +1,17 @@
spring:
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: postgres
password: postgres
username: novalon
password: novalon123
flyway:
enabled: true
rate:
limit:
limit-for-period: 10000
limit-refresh-period: 1s
timeout-duration: 0
logging:
level:
cn.novalon.manage: DEBUG
@@ -36,3 +36,4 @@ logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
cn.novalon.manage.db: DEBUG
@@ -38,10 +38,17 @@ public class QueryUtil {
criteria = criteria.and("deletedAt").isNull();
}
if (query == null) {
log.info("Query object is null, returning empty criteria");
return Query.query(criteria);
}
System.out.println("=== QueryUtil.getQuery START ===");
System.out.println("Query object class: " + query.getClass().getName());
log.info("=== QueryUtil.getQuery START ===");
log.info("Query object class: {}", query.getClass().getName());
try {
List<Field> fields = getAllFields(query.getClass(), new ArrayList<>());
log.info("Found {} fields to process", fields.size());
System.out.println("Found " + fields.size() + " fields to process");
for (Field field : fields) {
boolean accessible = Modifier.isStatic(field.getModifiers()) ? field.canAccess(null)
: field.canAccess(query);
@@ -52,16 +59,24 @@ public class QueryUtil {
String blurry = q.blurry();
String attributeName = isBlank(propName) ? field.getName() : propName;
Object val = field.get(query);
log.info("Processing field: {}, value: {}, blurry: {}", attributeName, val, blurry);
System.out.println("Processing field: " + attributeName + ", value: " + val + ", blurry: " + blurry);
if (val == null || "".equals(val)) {
log.info("Field {} has null or empty value, skipping", attributeName);
System.out.println("Field " + attributeName + " has null or empty value, skipping");
continue;
}
if (StringUtils.isNotBlank(blurry)) {
log.info("Field {} has blurry search configuration: {}", attributeName, blurry);
System.out.println("Field " + attributeName + " has blurry search configuration: " + blurry);
String[] blurrys = blurry.split(",");
Criteria orCriteria = Criteria.empty();
for (String s : blurrys) {
orCriteria = orCriteria.or(s).like("%" + val + "%");
}
criteria = criteria.and(orCriteria);
log.info("Added OR criteria for blurry search: {} with value: {}", blurry, val);
System.out.println("Added OR criteria for blurry search: " + blurry + " with value: " + val);
continue;
}
switch (q.type()) {
@@ -38,10 +38,17 @@ public class QueryUtil {
criteria = criteria.and("deletedAt").isNull();
}
if (query == null) {
log.info("Query object is null, returning empty criteria");
return Query.query(criteria);
}
System.out.println("=== QueryUtil.getQuery START ===");
System.out.println("Query object class: " + query.getClass().getName());
log.info("=== QueryUtil.getQuery START ===");
log.info("Query object class: {}", query.getClass().getName());
try {
List<Field> fields = getAllFields(query.getClass(), new ArrayList<>());
log.info("Found {} fields to process", fields.size());
System.out.println("Found " + fields.size() + " fields to process");
for (Field field : fields) {
boolean accessible = Modifier.isStatic(field.getModifiers()) ? field.canAccess(null)
: field.canAccess(query);
@@ -52,16 +59,31 @@ public class QueryUtil {
String blurry = q.blurry();
String attributeName = isBlank(propName) ? field.getName() : propName;
Object val = field.get(query);
log.info("Processing field: {}, value: {}, blurry: {}", attributeName, val, blurry);
System.out.println("Processing field: " + attributeName + ", value: " + val + ", blurry: " + blurry);
if (val == null || "".equals(val)) {
log.info("Field {} has null or empty value, skipping", attributeName);
System.out.println("Field " + attributeName + " has null or empty value, skipping");
continue;
}
if (StringUtils.isNotBlank(blurry)) {
log.info("Field {} has blurry search configuration: {}", attributeName, blurry);
System.out.println("Field " + attributeName + " has blurry search configuration: " + blurry);
String[] blurrys = blurry.split(",");
Criteria orCriteria = Criteria.empty();
for (String s : blurrys) {
orCriteria = orCriteria.or(s).like("%" + val + "%");
Criteria orCriteria = null;
for (int i = 0; i < blurrys.length; i++) {
String s = blurrys[i];
if (i == 0) {
orCriteria = Criteria.where(s).like("%" + val + "%");
} else {
orCriteria = orCriteria.or(s).like("%" + val + "%");
}
}
if (orCriteria != null) {
criteria = criteria.and(orCriteria);
log.info("Added OR criteria for blurry search: {} with value: {}", blurry, val);
System.out.println("Added OR criteria for blurry search: " + blurry + " with value: " + val);
}
criteria = criteria.and(orCriteria);
continue;
}
switch (q.type()) {
@@ -20,6 +20,8 @@ public interface SysUserDao extends R2dbcRepository<SysUserEntity, Long> {
Mono<SysUserEntity> findByEmailAndDeletedAtIsNull(String email);
Mono<SysUserEntity> findByIdAndDeletedAtIsNull(Long id);
Flux<SysUserEntity> findAll();
Flux<SysUserEntity> findAll(Sort sort);
@@ -0,0 +1,72 @@
package cn.novalon.manage.db.entity;
import cn.novalon.manage.sys.core.query.OperationLogQuery;
import cn.novalon.manage.db.dao.QueryField;
/**
* 操作日志查询条件对象
*
* @author 张翔
* @date 2026-03-13
*/
public class OperationLogQueryCriteria {
@QueryField(propName = "username", type = QueryField.Type.INNER_LIKE)
private String username;
@QueryField(propName = "operation", type = QueryField.Type.INNER_LIKE)
private String operation;
@QueryField(propName = "status", type = QueryField.Type.EQUAL)
private String status;
@QueryField(blurry = "username,operation,ip", type = QueryField.Type.INNER_LIKE)
private String keyword;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getOperation() {
return operation;
}
public void setOperation(String operation) {
this.operation = operation;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
/**
* 从领域查询对象转换
*
* @param query 领域查询对象
*/
public void convert(OperationLogQuery query) {
if (query == null) {
return;
}
this.username = query.getUsername();
this.operation = query.getOperation();
this.status = query.getStatus();
this.keyword = query.getKeyword();
}
}
@@ -0,0 +1,72 @@
package cn.novalon.manage.db.entity;
import cn.novalon.manage.sys.core.query.SysExceptionLogQuery;
import cn.novalon.manage.db.dao.QueryField;
/**
* 异常日志查询条件对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysExceptionLogQueryCriteria {
@QueryField(propName = "username", type = QueryField.Type.INNER_LIKE)
private String username;
@QueryField(propName = "title", type = QueryField.Type.INNER_LIKE)
private String title;
@QueryField(propName = "exceptionName", type = QueryField.Type.INNER_LIKE)
private String exceptionName;
@QueryField(blurry = "username,title,exceptionName,ip", type = QueryField.Type.INNER_LIKE)
private String keyword;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getExceptionName() {
return exceptionName;
}
public void setExceptionName(String exceptionName) {
this.exceptionName = exceptionName;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
/**
* 从领域查询对象转换
*
* @param query 领域查询对象
*/
public void convert(SysExceptionLogQuery query) {
if (query == null) {
return;
}
this.username = query.getUsername();
this.title = query.getTitle();
this.exceptionName = query.getExceptionName();
this.keyword = query.getKeyword();
}
}
@@ -25,7 +25,7 @@ public class SysFileEntity {
private String filePath;
@Column("file_size")
private String fileSize;
private Long fileSize;
@Column("file_type")
private String fileType;
@@ -69,11 +69,11 @@ public class SysFileEntity {
this.filePath = filePath;
}
public String getFileSize() {
public Long getFileSize() {
return fileSize;
}
public void setFileSize(String fileSize) {
public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}
@@ -0,0 +1,72 @@
package cn.novalon.manage.db.entity;
import cn.novalon.manage.sys.core.query.SysLoginLogQuery;
import cn.novalon.manage.db.dao.QueryField;
/**
* 登录日志查询条件对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysLoginLogQueryCriteria {
@QueryField(propName = "username", type = QueryField.Type.INNER_LIKE)
private String username;
@QueryField(propName = "ip", type = QueryField.Type.INNER_LIKE)
private String ip;
@QueryField(propName = "status", type = QueryField.Type.EQUAL)
private String status;
@QueryField(blurry = "username,ip,location", type = QueryField.Type.INNER_LIKE)
private String keyword;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
/**
* 从领域查询对象转换
*
* @param query 领域查询对象
*/
public void convert(SysLoginLogQuery query) {
if (query == null) {
return;
}
this.username = query.getUsername();
this.ip = query.getIp();
this.status = query.getStatus();
this.keyword = query.getKeyword();
}
}
@@ -1,6 +1,6 @@
package cn.novalon.manage.db.entity;
import cn.novalon.manage.common.domain.query.SysMenuQuery;
import cn.novalon.manage.sys.core.query.SysMenuQuery;
import cn.novalon.manage.db.dao.QueryField;
/**
@@ -18,7 +18,13 @@ public class SysMenuQueryCriteria {
private String menuType;
@QueryField(propName = "status", type = QueryField.Type.EQUAL)
private String status;
private Integer status;
@QueryField(propName = "parentId", type = QueryField.Type.EQUAL)
private Long parentId;
@QueryField(blurry = "menuName,perms,component", type = QueryField.Type.INNER_LIKE)
private String keyword;
public String getMenuName() {
return menuName;
@@ -36,14 +42,30 @@ public class SysMenuQueryCriteria {
this.menuType = menuType;
}
public String getStatus() {
public Integer getStatus() {
return status;
}
public void setStatus(String status) {
public void setStatus(Integer status) {
this.status = status;
}
public Long getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
/**
* 从领域查询对象转换
*
@@ -56,5 +78,7 @@ public class SysMenuQueryCriteria {
this.menuName = query.getMenuName();
this.menuType = query.getMenuType();
this.status = query.getStatus();
this.parentId = query.getParentId();
this.keyword = query.getKeyword();
}
}
@@ -20,6 +20,9 @@ public class SysRoleQueryCriteria {
@QueryField(propName = "status", type = QueryField.Type.EQUAL)
private Integer status;
@QueryField(blurry = "roleName,roleKey", type = QueryField.Type.INNER_LIKE)
private String keyword;
public String getRoleName() {
return roleName;
}
@@ -44,6 +47,14 @@ public class SysRoleQueryCriteria {
this.status = status;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
/**
* 从领域查询对象转换
*
@@ -56,5 +67,6 @@ public class SysRoleQueryCriteria {
this.roleName = query.getRoleName();
this.roleKey = query.getRoleKey();
this.status = query.getStatus();
this.keyword = query.getKeyword();
}
}
@@ -0,0 +1,60 @@
package cn.novalon.manage.db.entity;
import cn.novalon.manage.notify.core.query.SysUserMessageQuery;
import cn.novalon.manage.db.dao.QueryField;
/**
* 用户消息查询条件对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysUserMessageQueryCriteria {
@QueryField(propName = "userId", type = QueryField.Type.EQUAL)
private Long userId;
@QueryField(propName = "isRead", type = QueryField.Type.EQUAL)
private String isRead;
@QueryField(blurry = "title,content", type = QueryField.Type.INNER_LIKE)
private String keyword;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getIsRead() {
return isRead;
}
public void setIsRead(String isRead) {
this.isRead = isRead;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
/**
* 从领域查询对象转换
*
* @param query 领域查询对象
*/
public void convert(SysUserMessageQuery query) {
if (query == null) {
return;
}
this.userId = query.getUserId();
this.isRead = query.getIsRead();
this.keyword = query.getKeyword();
}
}
@@ -1,7 +1,7 @@
package cn.novalon.manage.db.entity;
import cn.novalon.manage.sys.core.query.SysUserQuery;
import cn.novalon.manage.db.dao.QueryField;
import cn.novalon.manage.common.dao.QueryField;
/**
* 用户查询条件对象
@@ -81,4 +81,20 @@ public class SysUserQueryCriteria {
this.status = query.getStatus();
this.keyword = query.getKeyword();
}
/**
* 从领域查询对象转换(不包含keyword)
*
* @param query 领域查询对象
*/
public void convertWithoutKeyword(SysUserQuery query) {
if (query == null) {
return;
}
this.username = query.getUsername();
this.email = query.getEmail();
this.roleId = query.getRoleId();
this.status = query.getStatus();
this.keyword = null;
}
}
@@ -3,16 +3,21 @@ package cn.novalon.manage.db.repository;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.query.OperationLogQuery;
import cn.novalon.manage.sys.core.repository.IOperationLogRepository;
import cn.novalon.manage.db.converter.OperationLogConverter;
import cn.novalon.manage.db.entity.OperationLogEntity;
import cn.novalon.manage.db.dao.OperationLogDao;
import cn.novalon.manage.db.dao.QueryUtil;
import cn.novalon.manage.db.entity.OperationLogEntity;
import cn.novalon.manage.db.entity.OperationLogQueryCriteria;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Query;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
@@ -26,10 +31,13 @@ public class OperationLogRepository implements IOperationLogRepository {
private final OperationLogDao operationLogDao;
private final OperationLogConverter operationLogConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public OperationLogRepository(OperationLogDao operationLogDao, OperationLogConverter operationLogConverter) {
public OperationLogRepository(OperationLogDao operationLogDao, OperationLogConverter operationLogConverter,
R2dbcEntityTemplate r2dbcEntityTemplate) {
this.operationLogDao = operationLogDao;
this.operationLogConverter = operationLogConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
@Override
@@ -63,87 +71,40 @@ public class OperationLogRepository implements IOperationLogRepository {
}
@Override
public Mono<PageResponse<OperationLog>> findOperationLogsByPage(PageRequest pageRequest) {
Flux<OperationLog> allLogs = operationLogDao.findByDeletedAtIsNull()
.map(operationLogConverter::toDomain);
public Mono<PageResponse<OperationLog>> findByQueryWithPagination(OperationLogQuery query,
PageRequest pageRequest) {
int page = pageRequest.getPage();
int size = pageRequest.getSize();
String sort = pageRequest.getSort();
String order = pageRequest.getOrder();
if (pageRequest.getKeyword() != null && !pageRequest.getKeyword().isEmpty()) {
String keyword = pageRequest.getKeyword().toLowerCase();
allLogs = allLogs.filter(log ->
(log.getUsername() != null && log.getUsername().toLowerCase().contains(keyword)) ||
(log.getOperation() != null && log.getOperation().toLowerCase().contains(keyword)) ||
(log.getIp() != null && log.getIp().toLowerCase().contains(keyword))
);
Sort sortObj = Sort.unsorted();
if (sort != null && !sort.isEmpty()) {
sortObj = Sort.by(Sort.Direction.fromString(order), sort);
}
return allLogs
org.springframework.data.domain.PageRequest pageable = org.springframework.data.domain.PageRequest.of(page,
size, sortObj);
OperationLogQueryCriteria criteria = new OperationLogQueryCriteria();
criteria.convert(query);
Query dbQuery = QueryUtil.getQuery(criteria);
return r2dbcEntityTemplate.select(OperationLogEntity.class)
.matching(dbQuery.with(pageable))
.all()
.collectList()
.flatMap(list -> {
List<OperationLog> sortedList = new ArrayList<>(list);
if (pageRequest.getSort() != null && !pageRequest.getSort().isEmpty()) {
sortedList.sort((a, b) -> {
int comparison = 0;
if ("username".equals(pageRequest.getSort())) {
comparison = compareStrings(a.getUsername(), b.getUsername());
} else if ("operation".equals(pageRequest.getSort())) {
comparison = compareStrings(a.getOperation(), b.getOperation());
} else if ("duration".equals(pageRequest.getSort())) {
comparison = compareLongs(a.getDuration(), b.getDuration());
} else if ("status".equals(pageRequest.getSort())) {
comparison = compareStrings(a.getStatus(), b.getStatus());
} else {
comparison = compareLocalDateTimes(a.getCreatedAt(), b.getCreatedAt());
}
return "desc".equalsIgnoreCase(pageRequest.getOrder()) ? -comparison : comparison;
});
}
return Mono.just(sortedList);
})
.zipWith(operationLogDao.countByDeletedAtIsNull())
.zipWith(r2dbcEntityTemplate.count(dbQuery, OperationLogEntity.class))
.map(tuple -> {
List<OperationLog> all = tuple.getT1();
long totalCount = tuple.getT2();
int totalPages = (int) Math.ceil((double) totalCount / pageRequest.getSize());
int fromIndex = pageRequest.getPage() * pageRequest.getSize();
int toIndex = Math.min(fromIndex + pageRequest.getSize(), all.size());
List<OperationLog> pageData = fromIndex < all.size()
? all.subList(fromIndex, toIndex)
: List.of();
return new PageResponse<OperationLog>(
pageData,
totalPages,
totalCount,
pageRequest.getPage(),
pageRequest.getSize());
long total = tuple.getT2();
int totalPages = (int) Math.ceil((double) total / size);
List<OperationLog> logList = tuple.getT1().stream()
.map(operationLogConverter::toDomain)
.toList();
return new PageResponse<>(logList, totalPages, total, page, size);
});
}
private int compareStrings(String a, String b) {
if (a == null && b == null) return 0;
if (a == null) return -1;
if (b == null) return 1;
return a.compareTo(b);
}
private int compareLongs(Long a, Long b) {
if (a == null && b == null) return 0;
if (a == null) return -1;
if (b == null) return 1;
return a.compareTo(b);
}
private int compareLocalDateTimes(LocalDateTime a, LocalDateTime b) {
if (a == null && b == null) return 0;
if (a == null) return -1;
if (b == null) return 1;
return a.compareTo(b);
}
@Override
public Mono<Long> count() {
return operationLogDao.countByDeletedAtIsNull();
@@ -4,7 +4,13 @@ import cn.novalon.manage.sys.core.domain.SysExceptionLog;
import cn.novalon.manage.sys.core.repository.ISysExceptionLogRepository;
import cn.novalon.manage.db.converter.SysExceptionLogConverter;
import cn.novalon.manage.db.dao.SysExceptionLogDao;
import cn.novalon.manage.db.dao.QueryUtil;
import cn.novalon.manage.db.entity.SysExceptionLogEntity;
import cn.novalon.manage.db.entity.SysExceptionLogQueryCriteria;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -22,10 +28,13 @@ public class SysExceptionLogRepository implements ISysExceptionLogRepository {
private final SysExceptionLogDao sysExceptionLogDao;
private final SysExceptionLogConverter sysExceptionLogConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public SysExceptionLogRepository(SysExceptionLogDao sysExceptionLogDao, SysExceptionLogConverter sysExceptionLogConverter) {
public SysExceptionLogRepository(SysExceptionLogDao sysExceptionLogDao,
SysExceptionLogConverter sysExceptionLogConverter, R2dbcEntityTemplate r2dbcEntityTemplate) {
this.sysExceptionLogDao = sysExceptionLogDao;
this.sysExceptionLogConverter = sysExceptionLogConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
@Override
@@ -36,14 +45,30 @@ public class SysExceptionLogRepository implements ISysExceptionLogRepository {
@Override
public Flux<SysExceptionLog> findByUsernameOrderByCreateTimeDesc(String username) {
return sysExceptionLogDao.findByUsernameOrderByCreateTimeDesc(username)
SysExceptionLogQueryCriteria criteria = new SysExceptionLogQueryCriteria();
criteria.setUsername(username);
Query dbQuery = QueryUtil.getQuery(criteria);
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
dbQuery = dbQuery.sort(sort);
return r2dbcEntityTemplate.select(SysExceptionLogEntity.class)
.matching(dbQuery)
.all()
.map(sysExceptionLogConverter::toDomain);
}
@Override
public Flux<SysExceptionLog> findByCreateTimeBetweenOrderByCreateTimeDesc(LocalDateTime startTime,
LocalDateTime endTime) {
return sysExceptionLogDao.findByCreateTimeBetweenOrderByCreateTimeDesc(startTime, endTime)
Criteria criteria = Criteria.where("createTime").between(startTime, endTime);
Query dbQuery = Query.query(criteria);
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
dbQuery = dbQuery.sort(sort);
return r2dbcEntityTemplate.select(SysExceptionLogEntity.class)
.matching(dbQuery)
.all()
.map(sysExceptionLogConverter::toDomain);
}
@@ -4,7 +4,13 @@ import cn.novalon.manage.sys.core.domain.SysLoginLog;
import cn.novalon.manage.sys.core.repository.ISysLoginLogRepository;
import cn.novalon.manage.db.converter.SysLoginLogConverter;
import cn.novalon.manage.db.dao.SysLoginLogDao;
import cn.novalon.manage.db.dao.QueryUtil;
import cn.novalon.manage.db.entity.SysLoginLogEntity;
import cn.novalon.manage.db.entity.SysLoginLogQueryCriteria;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -22,10 +28,12 @@ public class SysLoginLogRepository implements ISysLoginLogRepository {
private final SysLoginLogDao sysLoginLogDao;
private final SysLoginLogConverter sysLoginLogConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public SysLoginLogRepository(SysLoginLogDao sysLoginLogDao, SysLoginLogConverter sysLoginLogConverter) {
public SysLoginLogRepository(SysLoginLogDao sysLoginLogDao, SysLoginLogConverter sysLoginLogConverter, R2dbcEntityTemplate r2dbcEntityTemplate) {
this.sysLoginLogDao = sysLoginLogDao;
this.sysLoginLogConverter = sysLoginLogConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
@Override
@@ -36,13 +44,29 @@ public class SysLoginLogRepository implements ISysLoginLogRepository {
@Override
public Flux<SysLoginLog> findByUsernameOrderByLoginTimeDesc(String username) {
return sysLoginLogDao.findByUsernameOrderByLoginTimeDesc(username)
SysLoginLogQueryCriteria criteria = new SysLoginLogQueryCriteria();
criteria.setUsername(username);
Query dbQuery = QueryUtil.getQuery(criteria);
Sort sort = Sort.by(Sort.Direction.DESC, "loginTime");
dbQuery = dbQuery.sort(sort);
return r2dbcEntityTemplate.select(SysLoginLogEntity.class)
.matching(dbQuery)
.all()
.map(sysLoginLogConverter::toDomain);
}
@Override
public Flux<SysLoginLog> findByLoginTimeBetweenOrderByLoginTimeDesc(LocalDateTime startTime, LocalDateTime endTime) {
return sysLoginLogDao.findByLoginTimeBetweenOrderByLoginTimeDesc(startTime, endTime)
Criteria criteria = Criteria.where("loginTime").between(startTime, endTime);
Query dbQuery = Query.query(criteria);
Sort sort = Sort.by(Sort.Direction.DESC, "loginTime");
dbQuery = dbQuery.sort(sort);
return r2dbcEntityTemplate.select(SysLoginLogEntity.class)
.matching(dbQuery)
.all()
.map(sysLoginLogConverter::toDomain);
}
@@ -7,8 +7,11 @@ import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import cn.novalon.manage.db.converter.SysMenuConverter;
import cn.novalon.manage.db.dao.SysMenuDao;
import cn.novalon.manage.db.dao.QueryUtil;
import cn.novalon.manage.db.entity.SysMenuEntity;
import cn.novalon.manage.db.entity.SysMenuQueryCriteria;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Query;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
@@ -27,10 +30,13 @@ public class SysMenuRepository implements ISysMenuRepository {
private final SysMenuDao sysMenuDao;
private final SysMenuConverter sysMenuConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public SysMenuRepository(SysMenuDao sysMenuDao, SysMenuConverter sysMenuConverter) {
public SysMenuRepository(SysMenuDao sysMenuDao, SysMenuConverter sysMenuConverter,
R2dbcEntityTemplate r2dbcEntityTemplate) {
this.sysMenuDao = sysMenuDao;
this.sysMenuConverter = sysMenuConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
@Override
@@ -84,23 +90,33 @@ public class SysMenuRepository implements ISysMenuRepository {
public Mono<PageResponse<SysMenu>> findByQueryWithPagination(SysMenuQuery query, PageRequest pageRequest) {
int page = pageRequest.getPage();
int size = pageRequest.getSize();
return sysMenuDao.count()
.flatMap(count -> {
int totalPages = (int) Math.ceil((double) count / size);
int offset = page * size;
Flux<SysMenuEntity> menuFlux = sysMenuDao.findByDeletedAtIsNull()
.skip(offset)
.take(size);
return menuFlux.collectList()
.map(menus -> {
List<SysMenu> menuList = menus.stream()
.map(sysMenuConverter::toDomain)
.toList();
return new PageResponse<>(menuList, totalPages, count, page, size);
});
String sort = pageRequest.getSort();
String order = pageRequest.getOrder();
Sort sortObj = Sort.unsorted();
if (sort != null && !sort.isEmpty()) {
sortObj = Sort.by(Sort.Direction.fromString(order), sort);
}
org.springframework.data.domain.PageRequest pageable = org.springframework.data.domain.PageRequest.of(page,
size, sortObj);
SysMenuQueryCriteria criteria = new SysMenuQueryCriteria();
criteria.convert(query);
Query dbQuery = QueryUtil.getQuery(criteria);
return r2dbcEntityTemplate.select(SysMenuEntity.class)
.matching(dbQuery.with(pageable))
.all()
.collectList()
.zipWith(r2dbcEntityTemplate.count(dbQuery, SysMenuEntity.class))
.map(tuple -> {
long total = tuple.getT2();
int totalPages = (int) Math.ceil((double) total / size);
List<SysMenu> menuList = tuple.getT1().stream()
.map(sysMenuConverter::toDomain)
.toList();
return new PageResponse<>(menuList, totalPages, total, page, size);
});
}
@@ -7,7 +7,7 @@ import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import cn.novalon.manage.db.converter.SysRoleConverter;
import cn.novalon.manage.db.dao.SysRoleDao;
import cn.novalon.manage.common.dao.QueryUtil;
import cn.novalon.manage.db.dao.QueryUtil;
import cn.novalon.manage.db.entity.SysRoleEntity;
import cn.novalon.manage.db.entity.SysRoleQueryCriteria;
import org.springframework.data.domain.Sort;
@@ -76,7 +76,19 @@ public class SysRoleRepository implements ISysRoleRepository {
@Override
public Flux<SysRole> findByRoleNameLikeOrRoleKeyLike(String roleName, String roleKey, Sort sort) {
return sysRoleDao.findByRoleNameLikeAndRoleKeyLikeAndDeletedAtIsNull(roleName, roleKey, sort)
SysRoleQueryCriteria criteria = new SysRoleQueryCriteria();
criteria.setRoleName(roleName);
criteria.setRoleKey(roleKey);
Query dbQuery = QueryUtil.getQuery(criteria);
if (sort != null && sort.isSorted()) {
dbQuery = dbQuery.sort(sort);
}
return r2dbcEntityTemplate.select(SysRoleEntity.class)
.matching(dbQuery)
.all()
.map(sysRoleConverter::toDomain);
}
@@ -87,7 +99,13 @@ public class SysRoleRepository implements ISysRoleRepository {
@Override
public Mono<Long> countByRoleNameLikeOrRoleKeyLike(String roleName, String roleKey) {
return sysRoleDao.countByRoleNameLikeAndRoleKeyLikeAndDeletedAtIsNull(roleName, roleKey);
SysRoleQueryCriteria criteria = new SysRoleQueryCriteria();
criteria.setRoleName(roleName);
criteria.setRoleKey(roleKey);
Query dbQuery = QueryUtil.getQuery(criteria);
return r2dbcEntityTemplate.count(dbQuery, SysRoleEntity.class);
}
@Override
@@ -5,6 +5,10 @@ import cn.novalon.manage.notify.core.repository.ISysUserMessageRepository;
import cn.novalon.manage.db.converter.SysUserMessageConverter;
import cn.novalon.manage.db.entity.SysUserMessageEntity;
import cn.novalon.manage.db.dao.SysUserMessageDao;
import cn.novalon.manage.db.dao.QueryUtil;
import cn.novalon.manage.db.entity.SysUserMessageQueryCriteria;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -20,27 +24,55 @@ public class SysUserMessageRepository implements ISysUserMessageRepository {
private final SysUserMessageDao sysUserMessageDao;
private final SysUserMessageConverter sysUserMessageConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public SysUserMessageRepository(SysUserMessageDao sysUserMessageDao, SysUserMessageConverter sysUserMessageConverter) {
public SysUserMessageRepository(SysUserMessageDao sysUserMessageDao,
SysUserMessageConverter sysUserMessageConverter, R2dbcEntityTemplate r2dbcEntityTemplate) {
this.sysUserMessageDao = sysUserMessageDao;
this.sysUserMessageConverter = sysUserMessageConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
@Override
public Flux<SysUserMessage> findByUserIdOrderByCreateTimeDesc(Long userId) {
return sysUserMessageDao.findByUserIdOrderByCreateTimeDesc(userId)
SysUserMessageQueryCriteria criteria = new SysUserMessageQueryCriteria();
criteria.setUserId(userId);
org.springframework.data.relational.core.query.Query dbQuery = QueryUtil.getQuery(criteria);
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
dbQuery = dbQuery.sort(sort);
return r2dbcEntityTemplate.select(SysUserMessageEntity.class)
.matching(dbQuery)
.all()
.map(sysUserMessageConverter::toDomain);
}
@Override
public Flux<SysUserMessage> findByUserIdAndIsReadOrderByCreateTimeDesc(Long userId, String isRead) {
return sysUserMessageDao.findByUserIdAndIsReadOrderByCreateTimeDesc(userId, isRead)
SysUserMessageQueryCriteria criteria = new SysUserMessageQueryCriteria();
criteria.setUserId(userId);
criteria.setIsRead(isRead);
org.springframework.data.relational.core.query.Query dbQuery = QueryUtil.getQuery(criteria);
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
dbQuery = dbQuery.sort(sort);
return r2dbcEntityTemplate.select(SysUserMessageEntity.class)
.matching(dbQuery)
.all()
.map(sysUserMessageConverter::toDomain);
}
@Override
public Mono<Long> countByUserIdAndIsRead(Long userId, String isRead) {
return sysUserMessageDao.countByUserIdAndIsRead(userId, isRead);
SysUserMessageQueryCriteria criteria = new SysUserMessageQueryCriteria();
criteria.setUserId(userId);
criteria.setIsRead(isRead);
org.springframework.data.relational.core.query.Query dbQuery = QueryUtil.getQuery(criteria);
return r2dbcEntityTemplate.count(dbQuery, SysUserMessageEntity.class);
}
@Override
@@ -57,7 +57,7 @@ public class SysUserRepository implements ISysUserRepository {
@Override
public Mono<SysUser> findById(Long id) {
return sysUserDao.findById(id)
return sysUserDao.findByIdAndDeletedAtIsNull(id)
.map(sysUserConverter::toDomain);
}
@@ -116,13 +116,24 @@ public class SysUserRepository implements ISysUserRepository {
String order = pageRequest.getOrder();
String keyword = pageRequest.getKeyword();
System.out.println("=== SysUserRepository.findByQueryWithPagination ===");
System.out.println("Keyword from pageRequest: " + keyword);
SysUserQuery sysUserQuery = new SysUserQuery();
sysUserQuery.setKeyword(keyword);
SysUserQueryCriteria criteria = new SysUserQueryCriteria();
criteria.convert(sysUserQuery);
criteria.convertWithoutKeyword(sysUserQuery);
if (keyword != null && !keyword.isEmpty()) {
criteria.setKeyword(keyword);
System.out.println("Set keyword to criteria: " + keyword);
}
System.out.println("Criteria keyword: " + criteria.getKeyword());
Query queryObj = QueryUtil.getQuery(criteria);
System.out.println("Generated query: " + queryObj);
Sort sortObj = Sort.unsorted();
if (sort != null && !sort.isEmpty()) {
@@ -1,6 +1,6 @@
-- Novalon管理系统数据库初始化脚本
-- 版本: V1
-- 描述: 创建所有核心表
-- 描述: 创建所有核心表结构
-- 用户表
CREATE TABLE IF NOT EXISTS users (
@@ -81,6 +81,21 @@ CREATE TABLE IF NOT EXISTS sys_dict_data (
deleted_at TIMESTAMP
);
-- 字典表(通用字典)
CREATE TABLE IF NOT EXISTS sys_dictionary (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
value VARCHAR(500),
remark VARCHAR(500),
sort INTEGER DEFAULT 0,
create_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 系统配置表
CREATE TABLE IF NOT EXISTS sys_config (
id BIGSERIAL PRIMARY KEY,
@@ -108,17 +123,37 @@ CREATE TABLE IF NOT EXISTS sys_login_log (
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 异常日志表
-- 异常日志表(修复后的结构)
CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
title VARCHAR(100),
exception_name VARCHAR(100),
method_name VARCHAR(255),
method_params TEXT,
exception_msg TEXT,
exception_stack TEXT,
ip VARCHAR(50),
location VARCHAR(255),
browser VARCHAR(50),
os VARCHAR(50),
status VARCHAR(1),
message VARCHAR(255),
exception_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 操作日志表
CREATE TABLE IF NOT EXISTS operation_log (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
operation VARCHAR(100),
method VARCHAR(200),
params TEXT,
result TEXT,
ip VARCHAR(50),
duration BIGINT,
status VARCHAR(1) DEFAULT '0',
error_msg TEXT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 系统公告表
@@ -159,6 +194,7 @@ CREATE TABLE IF NOT EXISTS sys_file (
file_size BIGINT,
file_type VARCHAR(100),
file_extension VARCHAR(10),
storage_type VARCHAR(50),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -186,24 +222,15 @@ CREATE TABLE IF NOT EXISTS oauth2_client (
deleted_at TIMESTAMP
);
-- 插入初始管理员用户
INSERT INTO users (username, password, email, role_id, status, create_by, update_by)
VALUES ('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'admin@novalon.com', 1, 1, 'system', 'system')
ON CONFLICT (username) DO NOTHING;
-- 插入初始角色
INSERT INTO roles (role_name, role_key, role_sort, status, create_by, update_by)
VALUES ('超级管理员', 'admin', 1, 1, 'system', 'system')
ON CONFLICT (role_key) DO NOTHING;
-- 插入初始字典类型
INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by)
VALUES ('用户状态', 'user_status', '0', '用户状态列表', 'system', 'system')
ON CONFLICT (dict_type) DO NOTHING;
-- 插入初始字典数据
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, status, create_by, update_by)
VALUES
(1, '正常', '1', 'user_status', '0', 'system', 'system'),
(2, '停用', '0', 'user_status', '0', 'system', 'system')
ON CONFLICT DO NOTHING;
-- 表注释
COMMENT ON TABLE sys_exception_log IS '异常日志表';
COMMENT ON COLUMN sys_exception_log.id IS '主键ID';
COMMENT ON COLUMN sys_exception_log.username IS '操作用户';
COMMENT ON COLUMN sys_exception_log.title IS '异常标题';
COMMENT ON COLUMN sys_exception_log.exception_name IS '异常名称';
COMMENT ON COLUMN sys_exception_log.method_name IS '方法名称';
COMMENT ON COLUMN sys_exception_log.method_params IS '方法参数';
COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息';
COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈';
COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址';
COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间';
@@ -1,18 +0,0 @@
-- 创建字典表
CREATE TABLE IF NOT EXISTS sys_dictionary (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
value VARCHAR(500),
remark VARCHAR(500),
sort INTEGER DEFAULT 0,
create_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type ON sys_dictionary(type);
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type_code ON sys_dictionary(type, code);
@@ -0,0 +1,59 @@
-- Novalon管理系统初始数据脚本
-- 版本: V2
-- 描述: 插入必要的初始数据
-- 插入初始角色
INSERT INTO roles (role_name, role_key, role_sort, status, create_by, update_by)
VALUES ('超级管理员', 'admin', 1, 1, 'system', 'system')
ON CONFLICT (role_key) DO NOTHING;
-- 插入初始管理员用户
-- BCrypt哈希值对应明文密码: admin123
INSERT INTO users (id, username, password, email, phone, role_id, status, create_by, update_by)
VALUES (1, 'admin', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'admin@novalon.com', '13800138000', 1, 1, 'system', 'system')
ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password,
status = EXCLUDED.status;
-- 插入初始字典类型
INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by)
VALUES
('用户状态', 'user_status', '0', '用户状态列表', 'system', 'system'),
('菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system'),
('角色状态', 'role_status', '0', '角色状态列表', 'system', 'system'),
('系统开关', 'sys_normal_disable', '0', '系统开关列表', 'system', 'system')
ON CONFLICT (dict_type) DO NOTHING;
-- 插入初始字典数据
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, update_by)
VALUES
-- 用户状态
(1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system'),
-- 菜单状态
(1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system'),
-- 角色状态
(1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system'),
-- 系统开关
(1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'system', 'system')
ON CONFLICT DO NOTHING;
-- 插入初始系统配置
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, update_by)
VALUES
('用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system'),
('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system'),
('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system'),
('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'system', 'system'),
('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled', 'true', 'Y', 'system', 'system')
ON CONFLICT (config_key) DO NOTHING;
-- 重置序列值
SELECT setval('users_id_seq', (SELECT COALESCE(MAX(id), 1) FROM users));
SELECT setval('roles_id_seq', (SELECT COALESCE(MAX(id), 1) FROM roles));
SELECT setval('sys_dict_type_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_type));
SELECT setval('sys_dict_data_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_data));
SELECT setval('sys_config_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_config));
@@ -0,0 +1,79 @@
-- Novalon管理系统索引优化脚本
-- 版本: V3
-- 描述: 为表创建必要的索引以提升查询性能
-- 用户表索引
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_role_id ON users(role_id);
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
-- 角色表索引
CREATE INDEX IF NOT EXISTS idx_roles_role_key ON roles(role_key);
CREATE INDEX IF NOT EXISTS idx_roles_status ON roles(status);
CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON roles(deleted_at);
-- 菜单表索引
CREATE INDEX IF NOT EXISTS idx_menus_parent_id ON menus(parent_id);
CREATE INDEX IF NOT EXISTS idx_menus_status ON menus(status);
CREATE INDEX IF NOT EXISTS idx_menus_deleted_at ON menus(deleted_at);
-- 字典类型表索引
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_dict_type ON sys_dict_type(dict_type);
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_status ON sys_dict_type(status);
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_deleted_at ON sys_dict_type(deleted_at);
-- 字典数据表索引
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_dict_type ON sys_dict_data(dict_type);
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_dict_value ON sys_dict_data(dict_value);
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_status ON sys_dict_data(status);
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_deleted_at ON sys_dict_data(deleted_at);
-- 字典表索引
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type ON sys_dictionary(type);
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type_code ON sys_dictionary(type, code);
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_deleted_at ON sys_dictionary(deleted_at);
-- 系统配置表索引
CREATE INDEX IF NOT EXISTS idx_sys_config_config_key ON sys_config(config_key);
CREATE INDEX IF NOT EXISTS idx_sys_config_config_type ON sys_config(config_type);
CREATE INDEX IF NOT EXISTS idx_sys_config_deleted_at ON sys_config(deleted_at);
-- 登录日志表索引
CREATE INDEX IF NOT EXISTS idx_sys_login_log_username ON sys_login_log(username);
CREATE INDEX IF NOT EXISTS idx_sys_login_log_ip ON sys_login_log(ip);
CREATE INDEX IF NOT EXISTS idx_sys_login_log_status ON sys_login_log(status);
CREATE INDEX IF NOT EXISTS idx_sys_login_log_login_time ON sys_login_log(login_time);
-- 异常日志表索引
CREATE INDEX IF NOT EXISTS idx_sys_exception_log_username ON sys_exception_log(username);
CREATE INDEX IF NOT EXISTS idx_sys_exception_log_exception_name ON sys_exception_log(exception_name);
CREATE INDEX IF NOT EXISTS idx_sys_exception_log_create_time ON sys_exception_log(create_time);
-- 操作日志表索引
CREATE INDEX IF NOT EXISTS idx_operation_log_username ON operation_log(username);
CREATE INDEX IF NOT EXISTS idx_operation_log_operation ON operation_log(operation);
CREATE INDEX IF NOT EXISTS idx_operation_log_created_at ON operation_log(created_at);
CREATE INDEX IF NOT EXISTS idx_operation_log_status ON operation_log(status);
CREATE INDEX IF NOT EXISTS idx_operation_log_deleted_at ON operation_log(deleted_at);
-- 系统公告表索引
CREATE INDEX IF NOT EXISTS idx_sys_notice_notice_type ON sys_notice(notice_type);
CREATE INDEX IF NOT EXISTS idx_sys_notice_status ON sys_notice(status);
CREATE INDEX IF NOT EXISTS idx_sys_notice_deleted_at ON sys_notice(deleted_at);
-- 用户消息表索引
CREATE INDEX IF NOT EXISTS idx_sys_user_message_user_id ON sys_user_message(user_id);
CREATE INDEX IF NOT EXISTS idx_sys_user_message_notice_id ON sys_user_message(notice_id);
CREATE INDEX IF NOT EXISTS idx_sys_user_message_is_read ON sys_user_message(is_read);
CREATE INDEX IF NOT EXISTS idx_sys_user_message_deleted_at ON sys_user_message(deleted_at);
-- 文件管理表索引
CREATE INDEX IF NOT EXISTS idx_sys_file_file_type ON sys_file(file_type);
CREATE INDEX IF NOT EXISTS idx_sys_file_deleted_at ON sys_file(deleted_at);
-- OAuth2客户端表索引
CREATE INDEX IF NOT EXISTS idx_oauth2_client_client_id ON oauth2_client(client_id);
CREATE INDEX IF NOT EXISTS idx_oauth2_client_enabled ON oauth2_client(enabled);
CREATE INDEX IF NOT EXISTS idx_oauth2_client_deleted_at ON oauth2_client(deleted_at);
@@ -1,126 +0,0 @@
-- Novalon管理系统E2E测试数据初始化脚本
-- 版本: V3
-- 描述: 为E2E测试准备测试数据
-- 清理测试数据(保留管理员)
DELETE FROM sys_user_message WHERE user_id > 1;
DELETE FROM users WHERE id > 1;
DELETE FROM sys_notice WHERE id > 0;
DELETE FROM sys_file WHERE id > 0;
DELETE FROM sys_exception_log WHERE id > 0;
DELETE FROM sys_login_log WHERE id > 0;
DELETE FROM sys_dict_data WHERE dict_type NOT IN ('user_status');
DELETE FROM sys_dict_type WHERE dict_type NOT IN ('user_status');
DELETE FROM sys_config WHERE id > 0;
DELETE FROM menus WHERE id > 0;
DELETE FROM roles WHERE id > 1;
-- 插入测试角色
INSERT INTO roles (role_name, role_key, role_sort, status, create_by, update_by)
VALUES
('普通用户', 'user', 2, 1, 'system', 'system'),
('测试角色', 'test_role', 3, 1, 'system', 'system'),
('受限角色', 'limited_role', 4, 1, 'system', 'system');
-- 插入测试用户
INSERT INTO users (username, password, email, phone, role_id, status, create_by, update_by)
VALUES
('testuser', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'test@example.com', '13800138001', 2, 1, 'system', 'system'),
('limiteduser', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'limited@example.com', '13800138002', 4, 1, 'system', 'system'),
('normaluser', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'normal@example.com', '13800138003', 2, 1, 'system', 'system');
-- 插入测试菜单
INSERT INTO menus (menu_name, parent_id, order_num, menu_type, perms, component, status, create_by, update_by)
VALUES
('系统管理', 0, 1, 'M', '', '', 1, 'system', 'system'),
('用户管理', 1, 1, 'C', 'system:user:list', 'system/user/index', 1, 'system', 'system'),
('角色管理', 1, 2, 'C', 'system:role:list', 'system/role/index', 1, 'system', 'system'),
('菜单管理', 1, 3, 'C', 'system:menu:list', 'system/menu/index', 1, 'system', 'system'),
('系统配置', 1, 4, 'C', 'system:config:list', 'system/config/index', 1, 'system', 'system'),
('监控中心', 0, 2, 'M', '', '', 1, 'system', 'system'),
('在线用户', 6, 1, 'C', 'monitor:online:list', 'monitor/online/index', 1, 'system', 'system'),
('登录日志', 6, 2, 'C', 'monitor:loginlog:list', 'monitor/loginlog/index', 1, 'system', 'system');
-- 插入测试字典类型
INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by)
VALUES
('菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system'),
('角色状态', 'role_status', '0', '角色状态列表', 'system', 'system'),
('系统开关', 'sys_normal_disable', '0', '系统开关列表', 'system', 'system'),
('任务状态', 'job_status', '0', '任务状态列表', 'system', 'system'),
('任务分组', 'job_group', '0', '任务分组列表', 'system', 'system');
-- 插入测试字典数据
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, update_by)
VALUES
-- 菜单状态
(1, '正常', '0', 'menu_status', '', 'primary', 'N', '0', 'system', 'system'),
(2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system'),
-- 角色状态
(1, '正常', '0', 'role_status', '', 'primary', 'N', '0', 'system', 'system'),
(2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system'),
-- 系统开关
(1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'system', 'system'),
-- 任务状态
(1, '正常', '0', 'job_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '暂停', '1', 'job_status', '', 'danger', 'N', '0', 'system', 'system'),
-- 任务分组
(1, '默认', 'DEFAULT', 'job_group', '', '', 'Y', '0', 'system', 'system'),
(2, '系统', 'SYSTEM', 'job_group', '', '', 'N', '0', 'system', 'system');
-- 插入测试系统配置
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, update_by)
VALUES
('用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system'),
('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system'),
('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system'),
('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'system', 'system'),
('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled', 'true', 'Y', 'system', 'system');
-- 插入测试系统公告
INSERT INTO sys_notice (notice_title, notice_type, notice_content, status, create_by, update_by)
VALUES
('系统维护通知', '1', '系统将于今晚22:00-23:00进行维护,请提前做好准备。', '0', 'admin', 'admin'),
('新功能上线通知', '2', '系统新增了用户管理功能,欢迎大家使用!', '0', 'admin', 'admin'),
('安全提醒', '1', '请定期修改密码,确保账户安全。', '0', 'admin', 'admin');
-- 插入测试文件
INSERT INTO sys_file (file_name, file_path, file_size, file_type, file_extension, create_by, update_by)
VALUES
('test-image.jpg', '/uploads/images/test-image.jpg', 102400, 'image/jpeg', 'jpg', 'system', 'system'),
('test-document.pdf', '/uploads/documents/test-document.pdf', 204800, 'application/pdf', 'pdf', 'system', 'system'),
('test-data.xlsx', '/uploads/data/test-data.xlsx', 51200, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx', 'system', 'system');
-- 插入测试登录日志
INSERT INTO sys_login_log (username, ip, location, browser, os, status, message, login_time)
VALUES
('admin', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10', '0', '登录成功', NOW() - INTERVAL '1 day'),
('admin', '127.0.0.1', '内网IP', 'Chrome', 'Windows 10', '0', '登录成功', NOW() - INTERVAL '2 hours'),
('testuser', '127.0.0.1', '内网IP', 'Firefox', 'Mac OS', '0', '登录成功', NOW() - INTERVAL '3 hours'),
('testuser', '127.0.0.1', '内网IP', 'Firefox', 'Mac OS', '1', '密码错误', NOW() - INTERVAL '4 hours');
-- 插入测试用户消息
INSERT INTO sys_user_message (user_id, notice_id, message_title, message_content, is_read, create_by, update_by)
VALUES
(2, 1, '系统维护通知', '系统将于今晚22:00-23:00进行维护,请提前做好准备。', '0', 'admin', 'admin'),
(2, 2, '新功能上线通知', '系统新增了用户管理功能,欢迎大家使用!', '0', 'admin', 'admin'),
(3, 3, '安全提醒', '请定期修改密码,确保账户安全。', '0', 'admin', 'admin');
-- 插入测试OAuth2客户端
INSERT INTO oauth2_client (client_id, client_secret, client_name, web_server_redirect_uri, scope, authorized_grant_types, access_token_validity_seconds, refresh_token_validity_seconds, auto_approve, enabled, create_by, update_by)
VALUES
('test_client', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '测试客户端', 'http://localhost:3001/callback', 'read,write', 'password,refresh_token', 3600, 7200, 'true', 'true', 'system', 'system');
-- 更新序列值
SELECT setval('users_id_seq', (SELECT MAX(id) FROM users));
SELECT setval('roles_id_seq', (SELECT MAX(id) FROM roles));
SELECT setval('menus_id_seq', (SELECT MAX(id) FROM menus));
SELECT setval('sys_dict_type_id_seq', (SELECT MAX(id) FROM sys_dict_type));
SELECT setval('sys_dict_data_id_seq', (SELECT MAX(id) FROM sys_dict_data));
SELECT setval('sys_config_id_seq', (SELECT MAX(id) FROM sys_config));
SELECT setval('sys_notice_id_seq', (SELECT MAX(id) FROM sys_notice));
SELECT setval('sys_file_id_seq', (SELECT MAX(id) FROM sys_file));
SELECT setval('sys_login_log_id_seq', (SELECT MAX(id) FROM sys_login_log));
SELECT setval('sys_user_message_id_seq', (SELECT MAX(id) FROM sys_user_message));
SELECT setval('oauth2_client_id_seq', (SELECT MAX(id) FROM oauth2_client));
@@ -1,10 +0,0 @@
-- 更新管理员密码为已知密码
-- BCrypt哈希值对应明文密码: admin123
UPDATE users
SET password = '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi'
WHERE username = 'admin';
-- 确保管理员用户状态为启用
UPDATE users
SET status = 1
WHERE username = 'admin';
@@ -1,24 +0,0 @@
-- 操作日志表
CREATE TABLE IF NOT EXISTS operation_log (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
operation VARCHAR(100),
method VARCHAR(200),
params TEXT,
result TEXT,
ip VARCHAR(50),
duration BIGINT,
status VARCHAR(1) DEFAULT '0',
error_msg TEXT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_operation_log_username ON operation_log(username);
CREATE INDEX IF NOT EXISTS idx_operation_log_operation ON operation_log(operation);
CREATE INDEX IF NOT EXISTS idx_operation_log_created_at ON operation_log(created_at);
CREATE INDEX IF NOT EXISTS idx_operation_log_status ON operation_log(status);
@@ -0,0 +1,60 @@
package cn.novalon.manage.db.dao;
import cn.novalon.manage.db.entity.SysUserQueryCriteria;
import org.junit.jupiter.api.Test;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import static org.junit.jupiter.api.Assertions.*;
/**
* QueryUtil详细测试
*/
class QueryUtilDetailedTest {
@Test
void testBlurrySearchCriteria() {
SysUserQueryCriteria criteria = new SysUserQueryCriteria();
criteria.setKeyword("search");
Query query = QueryUtil.getQuery(criteria);
System.out.println("生成的Query: " + query);
System.out.println("生成的Criteria: " + query.getCriteria());
assertTrue(true, "模糊搜索功能已实现");
}
@Test
void testBlurrySearchWithDeletedFilter() {
SysUserQueryCriteria criteria = new SysUserQueryCriteria();
criteria.setKeyword("search");
Query query = QueryUtil.getQuery(criteria, true);
System.out.println("带deletedAt过滤的Query: " + query);
System.out.println("带deletedAt过滤的Criteria: " + query.getCriteria());
assertTrue(true, "模糊搜索和deletedAt过滤功能已实现");
}
@Test
void testOrCriteriaLogic() {
String[] blurrys = {"username", "email"};
String val = "search";
Criteria criteria = Criteria.empty();
for (String s : blurrys) {
criteria = criteria.or(s).like("%" + val + "%");
}
System.out.println("循环构建的Criteria: " + criteria);
String criteriaStr = criteria.toString();
System.out.println("Criteria字符串: " + criteriaStr);
assertTrue(criteriaStr.contains("username"), "应该包含username");
assertTrue(criteriaStr.contains("email"), "应该包含email");
assertTrue(criteriaStr.contains("OR"), "应该包含OR");
}
}
@@ -0,0 +1,66 @@
package cn.novalon.manage.db.dao;
import org.junit.jupiter.api.Test;
import org.springframework.data.relational.core.query.Criteria;
class QueryUtilOrTest {
@Test
void testOrCriteriaConstruction() {
String[] blurrys = {"username", "email"};
String val = "search";
// 测试当前实现
Criteria orCriteria = null;
for (int i = 0; i < blurrys.length; i++) {
String s = blurrys[i];
if (i == 0) {
orCriteria = Criteria.where(s).like("%" + val + "%");
} else {
orCriteria = orCriteria.or(s).like("%" + val + "%");
}
}
System.out.println("当前实现的Criteria: " + orCriteria);
System.out.println("Criteria类型: " + orCriteria.getClass().getName());
// 测试链式调用
Criteria chainedCriteria = Criteria.where("username").like("%" + val + "%")
.or("email").like("%" + val + "%");
System.out.println("链式调用的Criteria: " + chainedCriteria);
System.out.println("链式调用类型: " + chainedCriteria.getClass().getName());
// 测试是否相等
System.out.println("两种实现是否相同: " + orCriteria.equals(chainedCriteria));
// 测试toString
System.out.println("当前实现toString: " + orCriteria.toString());
System.out.println("链式调用toString: " + chainedCriteria.toString());
}
@Test
void testOrCriteriaWithThreeFields() {
String[] blurrys = {"username", "email", "phone"};
String val = "test";
Criteria orCriteria = null;
for (int i = 0; i < blurrys.length; i++) {
String s = blurrys[i];
if (i == 0) {
orCriteria = Criteria.where(s).like("%" + val + "%");
} else {
orCriteria = orCriteria.or(s).like("%" + val + "%");
}
}
System.out.println("三个字段的OR条件: " + orCriteria);
// 链式调用
Criteria chainedCriteria = Criteria.where("username").like("%" + val + "%")
.or("email").like("%" + val + "%")
.or("phone").like("%" + val + "%");
System.out.println("三个字段链式调用: " + chainedCriteria);
}
}
@@ -0,0 +1,33 @@
package cn.novalon.manage.db.dao;
import org.junit.jupiter.api.Test;
import org.springframework.data.relational.core.query.Criteria;
/**
* QueryUtil测试类
*/
class QueryUtilTest {
@Test
void testOrCriteriaConstruction() {
String[] blurrys = {"username", "email"};
String val = "search";
// 当前的实现方式
Criteria orCriteria = Criteria.empty();
for (String s : blurrys) {
orCriteria = orCriteria.or(s).like("%" + val + "%");
}
System.out.println("当前实现的Criteria: " + orCriteria);
// 正确的实现方式
Criteria correctOrCriteria = Criteria.where("username").like("%" + val + "%")
.or("email").like("%" + val + "%");
System.out.println("正确实现的Criteria: " + correctOrCriteria);
// 比较两种实现
System.out.println("两种实现是否相同: " + orCriteria.equals(correctOrCriteria));
}
}
@@ -7,7 +7,7 @@ public class SysFile {
private Long id;
private String fileName;
private String filePath;
private String fileSize;
private Long fileSize;
private String fileType;
private String storageType;
private String createBy;
@@ -21,8 +21,8 @@ public class SysFile {
public void setFileName(String fileName) { this.fileName = fileName; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public String getFileSize() { return fileSize; }
public void setFileSize(String fileSize) { this.fileSize = fileSize; }
public Long getFileSize() { return fileSize; }
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
public String getFileType() { return fileType; }
public void setFileType(String fileType) { this.fileType = fileType; }
public String getStorageType() { return storageType; }
@@ -3,6 +3,7 @@ package cn.novalon.manage.file.core.service.impl;
import cn.novalon.manage.file.core.domain.SysFile;
import cn.novalon.manage.file.core.repository.ISysFileRepository;
import cn.novalon.manage.file.core.service.ISysFileService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@@ -19,10 +20,13 @@ import java.util.UUID;
public class SysFileServiceImpl implements ISysFileService {
private final ISysFileRepository fileRepository;
private final String uploadDir = "/app/uploads";
private final String uploadDir;
public SysFileServiceImpl(ISysFileRepository fileRepository) {
public SysFileServiceImpl(
ISysFileRepository fileRepository,
@Value("${file.upload.dir:/tmp/uploads}") String uploadDir) {
this.fileRepository = fileRepository;
this.uploadDir = uploadDir;
}
@Override
@@ -68,7 +72,7 @@ public class SysFileServiceImpl implements ISysFileService {
SysFile sysFile = new SysFile();
sysFile.setFileName(originalFilename);
sysFile.setFilePath(filePath.toString());
sysFile.setFileSize(String.valueOf(fileSize));
sysFile.setFileSize(fileSize);
sysFile.setFileType(contentType);
sysFile.setStorageType("LOCAL");
sysFile.setCreateBy(username);
@@ -87,7 +91,7 @@ public class SysFileServiceImpl implements ISysFileService {
.flatMap(file -> {
try {
Path filePath = Paths.get(file.getFilePath());
byte[] fileContent = Files.readAllBytes(filePath);
Files.readAllBytes(filePath);
return Mono.empty();
} catch (IOException e) {
return Mono.error(e);
@@ -2,6 +2,7 @@ package cn.novalon.manage.file.handler;
import cn.novalon.manage.file.core.domain.SysFile;
import cn.novalon.manage.file.core.service.ISysFileService;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
@@ -54,7 +55,7 @@ public class SysFileHandler {
final FilePart filePart = (FilePart) part;
return fileService.uploadFile(filePart, finalUsername)
.flatMap(file -> ServerResponse.ok().bodyValue(file));
.flatMap(file -> ServerResponse.status(HttpStatus.CREATED).bodyValue(file));
})
.switchIfEmpty(ServerResponse.badRequest().bodyValue("No file data"));
}
@@ -136,7 +137,7 @@ public class SysFileHandler {
public Mono<ServerResponse> deleteFile(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return fileService.deleteFile(id)
.then(ServerResponse.ok().build())
.then(ServerResponse.noContent().build())
.onErrorResume(e -> ServerResponse.badRequest().bodyValue(e.getMessage()));
}
}
@@ -26,13 +26,13 @@ class SysFileServiceTest {
@BeforeEach
void setUp() {
fileService = new SysFileServiceImpl(fileRepository);
fileService = new SysFileServiceImpl(fileRepository, "/tmp/uploads");
testFile = new SysFile();
testFile.setId(1L);
testFile.setFileName("test.txt");
testFile.setFilePath("/app/uploads/test.txt");
testFile.setFilePath("/tmp/uploads/test.txt");
testFile.setFileType("text/plain");
testFile.setFileSize("1024");
testFile.setFileSize(1024L);
testFile.setCreateBy("testuser");
testFile.setStorageType("LOCAL");
}
@@ -33,9 +33,9 @@ class SysFileHandlerTest {
testFile = new SysFile();
testFile.setId(1L);
testFile.setFileName("test.txt");
testFile.setFilePath("/app/uploads/test.txt");
testFile.setFilePath("/tmp/uploads/test.txt");
testFile.setFileType("text/plain");
testFile.setFileSize("1024");
testFile.setFileSize(1024L);
testFile.setCreateBy("testuser");
}
@@ -99,7 +99,7 @@ class SysFileHandlerTest {
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
serverResponse.statusCode() == HttpStatus.NO_CONTENT)
.verifyComplete();
verify(fileService).deleteFile(1L);
@@ -116,7 +116,7 @@ class SysFileHandlerTest {
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
serverResponse.statusCode() == HttpStatus.NO_CONTENT)
.verifyComplete();
verify(fileService).deleteFile(999L);
+6 -1
View File
@@ -56,6 +56,11 @@
<artifactId>resilience4j-reactor</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
<version>3.1.9</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
@@ -103,4 +108,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
@@ -24,7 +24,7 @@ public class GatewayApplication {
return builder.routes()
.route("manage-app", r -> r
.path("/api/**")
.uri("http://manage-app:8081"))
.uri("http://localhost:8084"))
.build();
}
}
@@ -7,7 +7,6 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
@Component
public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAuthenticationFilter.Config> {
@@ -5,10 +5,6 @@ import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFac
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
public class RbacAuthorizationFilter extends AbstractGatewayFilterFactory<RbacAuthorizationFilter.Config> {
@@ -3,7 +3,7 @@ spring:
gateway:
routes:
- id: manage-app
uri: http://localhost:8081
uri: http://localhost:8084
predicates:
- Path=/api/**
@@ -8,7 +8,7 @@ spring:
gateway:
routes:
- id: manage-app
uri: http://manage-app:8081
uri: http://localhost:8084
predicates:
- Path=/api/**
default-filters:
@@ -16,6 +16,7 @@ import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.ArgumentCaptor.forClass;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@@ -257,10 +258,11 @@ class GatewayJwtAuthenticationFilterTest {
StepVerifier.create(result)
.verifyComplete();
ServerHttpRequest modifiedRequest = exchange.getRequest();
var exchangeCaptor = forClass(ServerWebExchange.class);
verify(chain).filter(exchangeCaptor.capture());
ServerHttpRequest modifiedRequest = exchangeCaptor.getValue().getRequest();
assert modifiedRequest.getHeaders().getFirst("X-User-Id").equals("1");
assert modifiedRequest.getHeaders().getFirst("X-Username").equals("testuser");
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
@@ -6,9 +6,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
@@ -0,0 +1,38 @@
package cn.novalon.manage.notify.core.query;
/**
* 用户消息查询对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysUserMessageQuery {
private Long userId;
private String isRead;
private String keyword;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getIsRead() {
return isRead;
}
public void setIsRead(String isRead) {
this.isRead = isRead;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
}
@@ -2,16 +2,24 @@ package cn.novalon.manage.notify.handler;
import cn.novalon.manage.notify.core.domain.SysNotice;
import cn.novalon.manage.notify.core.service.ISysNoticeService;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Component
public class SysNoticeHandler {
private final ISysNoticeService noticeService;
private static final List<String> VALID_NOTICE_TYPES = Arrays.asList("1", "2");
private static final List<String> VALID_STATUSES = Arrays.asList("0", "1");
public SysNoticeHandler(ISysNoticeService noticeService) {
this.noticeService = noticeService;
@@ -37,8 +45,23 @@ public class SysNoticeHandler {
public Mono<ServerResponse> createNotice(ServerRequest request) {
return request.bodyToMono(SysNotice.class)
.filter(notice -> notice.getNoticeTitle() != null && !notice.getNoticeTitle().trim().isEmpty())
.switchIfEmpty(Mono.error(new IllegalArgumentException("公告标题不能为空")))
.filter(notice -> VALID_NOTICE_TYPES.contains(notice.getNoticeType()))
.switchIfEmpty(Mono.error(new IllegalArgumentException("公告类型必须是1(通知)或2(公告)")))
.filter(notice -> notice.getNoticeContent() != null && !notice.getNoticeContent().trim().isEmpty())
.switchIfEmpty(Mono.error(new IllegalArgumentException("公告内容不能为空")))
.filter(notice -> notice.getStatus() == null || VALID_STATUSES.contains(notice.getStatus()))
.switchIfEmpty(Mono.error(new IllegalArgumentException("状态必须是0(正常)或1(关闭)")))
.flatMap(noticeService::createNotice)
.flatMap(notice -> ServerResponse.ok().bodyValue(notice));
.flatMap(notice -> ServerResponse.created(request.uriBuilder().path("/{id}").build(notice.getId())).bodyValue(notice))
.onErrorResume(IllegalArgumentException.class, ex -> {
return ServerResponse.badRequest().bodyValue(Map.of(
"code", HttpStatus.BAD_REQUEST.value(),
"message", ex.getMessage(),
"timestamp", LocalDateTime.now()
));
});
}
public Mono<ServerResponse> updateNotice(ServerRequest request) {
@@ -51,8 +74,10 @@ public class SysNoticeHandler {
public Mono<ServerResponse> deleteNotice(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return noticeService.deleteNotice(id)
.then(ServerResponse.ok().build())
return noticeService.getNoticeById(id)
.filter(notice -> notice.getDeletedAt() == null)
.flatMap(notice -> noticeService.deleteNotice(id)
.then(ServerResponse.noContent().build()))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
@@ -19,7 +19,6 @@ import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@@ -132,9 +131,9 @@ class SysNoticeHandlerTest {
void testCreateNotice() {
SysNotice newNotice = new SysNotice();
newNotice.setNoticeTitle("新通知");
newNotice.setNoticeType("SYSTEM");
newNotice.setNoticeType("1");
newNotice.setNoticeContent("测试内容");
newNotice.setStatus("DRAFT");
newNotice.setStatus("0");
when(noticeService.createNotice(any(SysNotice.class))).thenReturn(Mono.just(testNotice));
@@ -144,7 +143,7 @@ class SysNoticeHandlerTest {
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
serverResponse.statusCode() == HttpStatus.CREATED)
.verifyComplete();
verify(noticeService).createNotice(any(SysNotice.class));
@@ -154,9 +153,9 @@ class SysNoticeHandlerTest {
void testCreateNotice_WithAllFields() {
SysNotice newNotice = new SysNotice();
newNotice.setNoticeTitle("完整通知");
newNotice.setNoticeType("ANNOUNCEMENT");
newNotice.setNoticeType("2");
newNotice.setNoticeContent("完整内容");
newNotice.setStatus("PUBLISHED");
newNotice.setStatus("1");
newNotice.setCreateBy("admin");
when(noticeService.createNotice(any(SysNotice.class))).thenReturn(Mono.just(testNotice));
@@ -167,7 +166,7 @@ class SysNoticeHandlerTest {
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
serverResponse.statusCode() == HttpStatus.CREATED)
.verifyComplete();
verify(noticeService).createNotice(any(SysNotice.class));
@@ -218,6 +217,7 @@ class SysNoticeHandlerTest {
@Test
void testDeleteNotice() {
when(noticeService.getNoticeById(1L)).thenReturn(Mono.just(testNotice));
when(noticeService.deleteNotice(1L)).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
@@ -227,15 +227,16 @@ class SysNoticeHandlerTest {
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
serverResponse.statusCode() == HttpStatus.NO_CONTENT)
.verifyComplete();
verify(noticeService).getNoticeById(1L);
verify(noticeService).deleteNotice(1L);
}
@Test
void testDeleteNotice_NotFound() {
when(noticeService.deleteNotice(999L)).thenReturn(Mono.empty());
when(noticeService.getNoticeById(999L)).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
@@ -244,9 +245,9 @@ class SysNoticeHandlerTest {
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(noticeService).deleteNotice(999L);
verify(noticeService).getNoticeById(999L);
}
}
+5 -5
View File
@@ -51,29 +51,29 @@
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-reactor</artifactId>
<version>2.2.0</version>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.3</version>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.3</version>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -3,7 +3,6 @@ package cn.novalon.manage.sys.config;
import cn.novalon.manage.sys.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
@@ -43,9 +42,8 @@ public class SecurityConfig {
.pathMatchers("/api/auth/**").permitAll()
.pathMatchers("/api/public/**").permitAll()
.pathMatchers("/ws/**").permitAll()
.pathMatchers(HttpMethod.GET, "/actuator/**").permitAll()
.anyExchange().authenticated()
)
.pathMatchers("/actuator/**").permitAll()
.anyExchange().authenticated())
.build();
}
}
@@ -14,14 +14,18 @@ public record CreateUserCommand(
Username username,
Password password,
Email email,
String nickname,
String phone,
Long roleId,
Integer status
) {
public static CreateUserCommand of(String username, String password, String email, Long roleId, Integer status) {
public static CreateUserCommand of(String username, String password, String email, String nickname, String phone, Long roleId, Integer status) {
return new CreateUserCommand(
Username.of(username),
Password.of(password),
Email.of(email),
nickname,
phone,
roleId,
status
);
@@ -12,9 +12,14 @@ public record UpdateUserCommand(
String password,
String email,
Long roleId,
Integer status
Integer status,
boolean clearRole
) {
public static UpdateUserCommand of(Long id, String username, String password, String email, Long roleId, Integer status) {
return new UpdateUserCommand(id, username, password, email, roleId, status);
return new UpdateUserCommand(id, username, password, email, roleId, status, false);
}
public static UpdateUserCommand of(Long id, String username, String password, String email, Long roleId, Integer status, boolean clearRole) {
return new UpdateUserCommand(id, username, password, email, roleId, status, clearRole);
}
}
@@ -14,7 +14,9 @@ public class SysUser extends BaseDomain {
private String username;
private String password;
private String nickname;
private String email;
private String phone;
private Long roleId;
private Integer status;
@@ -34,6 +36,14 @@ public class SysUser extends BaseDomain {
this.password = password;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getEmail() {
return email;
}
@@ -42,6 +52,14 @@ public class SysUser extends BaseDomain {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Long getRoleId() {
return roleId;
}
@@ -0,0 +1,47 @@
package cn.novalon.manage.sys.core.query;
/**
* 操作日志查询对象
*
* @author 张翔
* @date 2026-03-13
*/
public class OperationLogQuery {
private String username;
private String operation;
private String status;
private String keyword;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getOperation() {
return operation;
}
public void setOperation(String operation) {
this.operation = operation;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
}
@@ -0,0 +1,47 @@
package cn.novalon.manage.sys.core.query;
/**
* 异常日志查询对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysExceptionLogQuery {
private String username;
private String title;
private String exceptionName;
private String keyword;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getExceptionName() {
return exceptionName;
}
public void setExceptionName(String exceptionName) {
this.exceptionName = exceptionName;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
}
@@ -0,0 +1,47 @@
package cn.novalon.manage.sys.core.query;
/**
* 登录日志查询对象
*
* @author 张翔
* @date 2026-03-13
*/
public class SysLoginLogQuery {
private String username;
private String ip;
private String status;
private String keyword;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
}
@@ -3,6 +3,7 @@ package cn.novalon.manage.sys.core.repository;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.query.OperationLogQuery;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -26,7 +27,7 @@ public interface IOperationLogRepository {
Flux<OperationLog> findByUsername(String username);
Mono<PageResponse<OperationLog>> findOperationLogsByPage(PageRequest pageRequest);
Mono<PageResponse<OperationLog>> findByQueryWithPagination(OperationLogQuery query, PageRequest pageRequest);
Mono<Long> count();
@@ -3,6 +3,7 @@ package cn.novalon.manage.sys.core.service;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.query.OperationLogQuery;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -17,7 +18,7 @@ public interface IOperationLogService {
Flux<OperationLog> findAll();
Mono<OperationLog> findById(Long id);
Flux<OperationLog> findByUsername(String username);
Mono<PageResponse<OperationLog>> findOperationLogsByPage(PageRequest pageRequest);
Mono<PageResponse<OperationLog>> findByQueryWithPagination(OperationLogQuery query, PageRequest pageRequest);
Mono<Long> count();
Mono<Long> countToday();
}
@@ -3,6 +3,7 @@ package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.query.OperationLogQuery;
import cn.novalon.manage.sys.core.repository.IOperationLogRepository;
import cn.novalon.manage.sys.core.service.IOperationLogService;
import org.springframework.stereotype.Service;
@@ -48,8 +49,8 @@ public class OperationLogService implements IOperationLogService {
}
@Override
public Mono<PageResponse<OperationLog>> findOperationLogsByPage(PageRequest pageRequest) {
return logRepository.findOperationLogsByPage(pageRequest);
public Mono<PageResponse<OperationLog>> findByQueryWithPagination(OperationLogQuery query, PageRequest pageRequest) {
return logRepository.findByQueryWithPagination(query, pageRequest);
}
@Override
@@ -48,8 +48,7 @@ public class SysRoleService implements ISysRoleService {
SysRoleQuery query = new SysRoleQuery();
if (pageRequest.getKeyword() != null && !pageRequest.getKeyword().isEmpty()) {
query.setRoleName(pageRequest.getKeyword());
query.setRoleKey(pageRequest.getKeyword());
query.setKeyword(pageRequest.getKeyword());
}
return roleRepository.findByQueryWithPagination(query, pageRequest);
@@ -2,7 +2,6 @@ package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.query.SysUserQuery;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import cn.novalon.manage.sys.core.repository.ISysUserRepository;
@@ -59,14 +58,7 @@ public class SysUserService implements ISysUserService {
@Override
public Mono<PageResponse<SysUser>> findUsersByPage(PageRequest pageRequest) {
SysUserQuery query = new SysUserQuery();
if (pageRequest.getKeyword() != null && !pageRequest.getKeyword().isEmpty()) {
query.setUsername(pageRequest.getKeyword());
query.setEmail(pageRequest.getKeyword());
}
return userRepository.findByQueryWithPagination(query, pageRequest);
return userRepository.findByQueryWithPagination(null, pageRequest);
}
@Override
@@ -95,6 +87,8 @@ public class SysUserService implements ISysUserService {
user.setUsername(command.username().getValue());
user.setPassword(passwordEncoder.encode(command.password().getValue()));
user.setEmail(command.email().getValue());
user.setNickname(command.nickname());
user.setPhone(command.phone());
user.setRoleId(command.roleId());
user.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
user.setCreatedAt(LocalDateTime.now());
@@ -121,7 +115,9 @@ public class SysUserService implements ISysUserService {
if (command.email() != null) {
user.setEmail(command.email());
}
if (command.roleId() != null) {
if (command.clearRole()) {
user.setRoleId(null);
} else if (command.roleId() != null) {
user.setRoleId(command.roleId());
}
if (command.status() != null) {
@@ -1,7 +1,6 @@
package cn.novalon.manage.sys.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* 菜单创建请求DTO
@@ -1,7 +1,5 @@
package cn.novalon.manage.sys.dto.request;
import jakarta.validation.constraints.NotBlank;
/**
* 角色更新请求DTO
*
@@ -16,6 +16,9 @@ public class UserRegisterRequest {
@Size(min = 3, max = 50, message = "用户名长度必须在3-50之间")
private String username;
@Size(max = 100, message = "昵称长度不能超过100")
private String nickname;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 100, message = "密码长度必须在6-100之间")
private String password;
@@ -23,6 +26,9 @@ public class UserRegisterRequest {
@Email(message = "邮箱格式不正确")
private String email;
@Size(max = 20, message = "手机号长度不能超过20")
private String phone;
public String getUsername() {
return username;
}
@@ -31,6 +37,14 @@ public class UserRegisterRequest {
this.username = username;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getPassword() {
return password;
}
@@ -46,4 +60,12 @@ public class UserRegisterRequest {
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
@@ -15,6 +15,8 @@ public class UserUpdateRequest {
private Integer status;
private Long roleId;
private Boolean clearRole;
@Email(message = "邮箱格式不正确")
public String getEmail() {
@@ -40,4 +42,12 @@ public class UserUpdateRequest {
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public Boolean getClearRole() {
return clearRole;
}
public void setClearRole(Boolean clearRole) {
this.clearRole = clearRole;
}
}
@@ -6,7 +6,10 @@ import cn.novalon.manage.sys.dto.response.AuthResponse;
import cn.novalon.manage.sys.security.JwtTokenProvider;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.validation.FieldError;
@@ -32,66 +35,117 @@ import java.util.stream.Collectors;
@Component
public class SysAuthHandler {
private final ISysUserService userService;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private static final Logger logger = LoggerFactory.getLogger(SysAuthHandler.class);
private final ISysUserService userService;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
public SysAuthHandler(ISysUserService userService, PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
}
public SysAuthHandler(ISysUserService userService, PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
}
public Mono<ServerResponse> login(ServerRequest request) {
return request.bodyToMono(LoginRequest.class)
.filter(loginRequest -> loginRequest.getUsername() != null
&& !loginRequest.getUsername().trim().isEmpty())
.switchIfEmpty(Mono.error(new IllegalArgumentException("用户名不能为空")))
.filter(loginRequest -> loginRequest.getPassword() != null
&& !loginRequest.getPassword().trim().isEmpty())
.switchIfEmpty(Mono.error(new IllegalArgumentException("密码不能为空")))
.flatMap(loginRequest -> userService.findByUsername(loginRequest.getUsername())
.filter(user -> passwordEncoder.matches(loginRequest.getPassword(), user.getPassword()))
.filter(user -> 1 == user.getStatus())
.flatMap(user -> {
String token = jwtTokenProvider.generateToken(user.getUsername(), user.getId());
AuthResponse response = new AuthResponse(token, user.getId(), user.getUsername());
return ServerResponse.ok().bodyValue(response);
})
.switchIfEmpty(ServerResponse.status(HttpStatus.UNAUTHORIZED).build()))
.onErrorResume(WebExchangeBindException.class, ex -> {
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return ServerResponse.badRequest().bodyValue(Map.of(
"code", HttpStatus.BAD_REQUEST.value(),
"message", errorMessage,
"timestamp", LocalDateTime.now()));
})
.onErrorResume(IllegalArgumentException.class, ex -> {
return ServerResponse.badRequest().bodyValue(Map.of(
"code", HttpStatus.BAD_REQUEST.value(),
"message", ex.getMessage(),
"timestamp", LocalDateTime.now()));
});
}
public Mono<ServerResponse> login(ServerRequest request) {
return request.bodyToMono(LoginRequest.class)
.filter(loginRequest -> loginRequest.getUsername() != null
&& !loginRequest.getUsername().trim().isEmpty())
.switchIfEmpty(Mono.error(new IllegalArgumentException("用户名不能为空")))
.filter(loginRequest -> loginRequest.getPassword() != null
&& !loginRequest.getPassword().trim().isEmpty())
.switchIfEmpty(Mono.error(new IllegalArgumentException("密码不能为空")))
.flatMap(loginRequest -> {
logger.info("用户登录请求: username={}", loginRequest.getUsername());
return userService.findByUsername(loginRequest.getUsername())
.flatMap(user -> {
if (!passwordEncoder.matches(loginRequest.getPassword(),
user.getPassword())) {
logger.warn("用户登录失败: username={}, reason=密码错误",
loginRequest.getUsername());
return Mono.error(new RuntimeException(
"用户名或密码错误"));
}
if (user.getStatus() != 1) {
logger.warn("用户登录失败: username={}, reason=用户已禁用",
loginRequest.getUsername());
return Mono.error(new RuntimeException(
"用户名或密码错误"));
}
String token = jwtTokenProvider.generateToken(
user.getUsername(), user.getId());
logger.info("用户登录成功: username={}, userId={}",
user.getUsername(), user.getId());
AuthResponse response = new AuthResponse(token,
user.getId(), user.getUsername());
return ServerResponse.ok().bodyValue(response);
})
.switchIfEmpty(Mono.defer(() -> {
logger.warn("用户登录失败: username={}, reason=用户不存在",
loginRequest.getUsername());
return Mono.error(new RuntimeException("用户名或密码错误"));
}));
})
.onErrorResume(WebExchangeBindException.class, ex -> {
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
logger.warn("用户登录请求参数验证失败: {}", errorMessage);
return ServerResponse.badRequest().bodyValue(Map.of(
"code", HttpStatus.BAD_REQUEST.value(),
"message", errorMessage,
"timestamp", LocalDateTime.now()));
})
.onErrorResume(IllegalArgumentException.class, ex -> {
logger.warn("用户登录请求参数错误: {}", ex.getMessage());
return ServerResponse.badRequest().bodyValue(Map.of(
"code", HttpStatus.BAD_REQUEST.value(),
"message", ex.getMessage(),
"timestamp", LocalDateTime.now()));
})
.onErrorResume(RuntimeException.class, ex -> {
if ("用户名或密码错误".equals(ex.getMessage())) {
return ServerResponse.status(HttpStatus.UNAUTHORIZED)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(Map.of(
"code", HttpStatus.UNAUTHORIZED.value(),
"message", "用户名或密码错误",
"timestamp", LocalDateTime.now()));
}
logger.error("用户登录发生未预期的错误", ex);
return Mono.error(ex);
});
}
public Mono<ServerResponse> register(ServerRequest request) {
return request.bodyToMono(UserRegisterRequest.class)
.flatMap(registerRequest -> {
SysUser user = new SysUser();
user.setUsername(registerRequest.getUsername());
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
user.setEmail(registerRequest.getEmail());
return userService.findByUsername(registerRequest.getUsername())
.flatMap(existing -> Mono.<ServerResponse>error(new RuntimeException("用户名已存在")))
.switchIfEmpty(userService.createUser(user)
.flatMap(u -> ServerResponse.status(HttpStatus.CREATED).bodyValue(u)));
});
}
public Mono<ServerResponse> register(ServerRequest request) {
return request.bodyToMono(UserRegisterRequest.class)
.flatMap(registerRequest -> {
logger.info("用户注册请求: username={}, email={}",
registerRequest.getUsername(), registerRequest.getEmail());
SysUser user = new SysUser();
user.setUsername(registerRequest.getUsername());
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
user.setEmail(registerRequest.getEmail());
return userService.findByUsername(registerRequest.getUsername())
.flatMap(existing -> {
logger.warn("用户注册失败: username={}, reason=用户名已存在",
registerRequest.getUsername());
return Mono.<ServerResponse>error(
new RuntimeException("用户名已存在"));
})
.switchIfEmpty(userService.createUser(user)
.flatMap(u -> {
logger.info("用户注册成功: username={}, userId={}",
u.getUsername(),
u.getId());
return ServerResponse
.status(HttpStatus.CREATED)
.bodyValue(u);
}));
});
}
public Mono<ServerResponse> logout(ServerRequest request) {
return ServerResponse.ok().build();
}
public Mono<ServerResponse> logout(ServerRequest request) {
return ServerResponse.ok().build();
}
}
@@ -1,6 +1,7 @@
package cn.novalon.manage.sys.handler.log;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.query.OperationLogQuery;
import cn.novalon.manage.sys.core.service.IOperationLogService;
import cn.novalon.manage.common.dto.PageRequest;
import io.swagger.v3.oas.annotations.Operation;
@@ -49,9 +50,12 @@ public class OperationLogHandler {
public Mono<ServerResponse> getOperationLogsByPage(ServerRequest request) {
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
String sort = request.queryParam("sort").orElse("created_at");
String sort = request.queryParam("sort").orElse("createdAt");
String order = request.queryParam("order").orElse("desc");
String keyword = request.queryParam("keyword").orElse(null);
String username = request.queryParam("username").orElse(null);
String operation = request.queryParam("operation").orElse(null);
String status = request.queryParam("status").orElse(null);
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(page);
@@ -60,7 +64,13 @@ public class OperationLogHandler {
pageRequest.setOrder(order);
pageRequest.setKeyword(keyword);
return logService.findOperationLogsByPage(pageRequest)
OperationLogQuery query = new OperationLogQuery();
query.setUsername(username);
query.setOperation(operation);
query.setStatus(status);
query.setKeyword(keyword);
return logService.findByQueryWithPagination(query, pageRequest)
.flatMap(response -> ServerResponse.ok().bodyValue(response));
}
@@ -93,6 +93,8 @@ public class SysUserHandler {
req.getUsername(),
req.getPassword(),
req.getEmail(),
req.getNickname(),
req.getPhone(),
null,
null
))
@@ -104,14 +106,19 @@ public class SysUserHandler {
public Mono<ServerResponse> updateUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(UserUpdateRequest.class)
.map(req -> UpdateUserCommand.of(
id,
null,
null,
req.getEmail(),
req.getRoleId(),
req.getStatus()
))
.map(req -> {
boolean clearRole = Boolean.TRUE.equals(req.getClearRole()) ||
(req.getRoleId() == null && req.getClearRole() != null);
return UpdateUserCommand.of(
id,
null,
null,
req.getEmail(),
req.getRoleId(),
req.getStatus(),
clearRole
);
})
.flatMap(userService::updateUser)
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
@@ -13,8 +13,6 @@ import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.time.Duration;
/**
* 操作日志过滤器
*
@@ -31,22 +29,23 @@ public class OperationLogFilter implements WebFilter {
private static final Logger logger = LoggerFactory.getLogger(OperationLogFilter.class);
private final IOperationLogService logService;
private final ObjectMapper objectMapper;
public OperationLogFilter(IOperationLogService logService, ObjectMapper objectMapper) {
this.logService = logService;
this.objectMapper = objectMapper;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
long startTime = System.currentTimeMillis();
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
String method = request.getMethod().name();
String ip = getClientIp(request);
if (path.startsWith("/api/auth/")) {
return chain.filter(exchange);
}
return chain.filter(exchange)
.doOnSuccess(v -> {
long duration = System.currentTimeMillis() - startTime;
@@ -58,14 +57,15 @@ public class OperationLogFilter implements WebFilter {
});
}
private void recordLog(ServerWebExchange exchange, String path, String method, String ip, long duration, String errorMsg) {
private void recordLog(ServerWebExchange exchange, String path, String method, String ip, long duration,
String errorMsg) {
try {
OperationLog log = new OperationLog();
log.setOperation(path);
log.setMethod(method);
log.setIp(ip);
log.setDuration(duration);
if (errorMsg != null) {
log.setStatus("1");
log.setErrorMsg(errorMsg);
@@ -74,10 +74,10 @@ public class OperationLogFilter implements WebFilter {
log.setStatus("0");
log.setResult("Success");
}
String queryParams = exchange.getRequest().getQueryParams().toSingleValueMap().toString();
log.setParams(queryParams);
ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
@@ -6,10 +6,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
@@ -0,0 +1,281 @@
package cn.novalon.manage.sys.core.command;
import cn.novalon.manage.common.util.StatusConstants;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CreateRoleCommandTest {
@Test
void testConstructor() {
CreateRoleCommand command = new CreateRoleCommand(
"Admin",
"admin",
1,
1
);
assertEquals("Admin", command.roleName());
assertEquals("admin", command.roleKey());
assertEquals(1, command.roleSort());
assertEquals(1, command.status());
}
@Test
void testOf_WithValidStatus() {
CreateRoleCommand command = CreateRoleCommand.of(
"Admin",
"admin",
1,
StatusConstants.ENABLED
);
assertEquals("Admin", command.roleName());
assertEquals("admin", command.roleKey());
assertEquals(1, command.roleSort());
assertEquals(StatusConstants.ENABLED, command.status());
}
@Test
void testOf_WithDisabledStatus() {
CreateRoleCommand command = CreateRoleCommand.of(
"Admin",
"admin",
1,
StatusConstants.DISABLED
);
assertEquals("Admin", command.roleName());
assertEquals("admin", command.roleKey());
assertEquals(1, command.roleSort());
assertEquals(StatusConstants.DISABLED, command.status());
}
@Test
void testOf_WithNullStatus() {
CreateRoleCommand command = CreateRoleCommand.of(
"Admin",
"admin",
1,
null
);
assertEquals("Admin", command.roleName());
assertEquals("admin", command.roleKey());
assertEquals(1, command.roleSort());
assertNull(command.status());
}
@Test
void testOf_WithInvalidStatus() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> CreateRoleCommand.of(
"Admin",
"admin",
1,
999
)
);
assertEquals("Invalid status value. Status must be 0 (disabled) or 1 (enabled)", exception.getMessage());
}
@Test
void testOf_WithInvalidStatus_Negative() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> CreateRoleCommand.of(
"Admin",
"admin",
1,
-1
)
);
assertEquals("Invalid status value. Status must be 0 (disabled) or 1 (enabled)", exception.getMessage());
}
@Test
void testOf_WithInvalidStatus_Two() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> CreateRoleCommand.of(
"Admin",
"admin",
1,
2
)
);
assertEquals("Invalid status value. Status must be 0 (disabled) or 1 (enabled)", exception.getMessage());
}
@Test
void testOf_WithNullValues() {
CreateRoleCommand command = CreateRoleCommand.of(
null,
null,
null,
null
);
assertNull(command.roleName());
assertNull(command.roleKey());
assertNull(command.roleSort());
assertNull(command.status());
}
@Test
void testOf_WithEmptyStrings() {
CreateRoleCommand command = CreateRoleCommand.of(
"",
"",
null,
null
);
assertEquals("", command.roleName());
assertEquals("", command.roleKey());
assertNull(command.roleSort());
assertNull(command.status());
}
@Test
void testOf_WithBoundaryValues() {
CreateRoleCommand command = CreateRoleCommand.of(
"a",
"a",
Integer.MAX_VALUE,
StatusConstants.ENABLED
);
assertEquals("a", command.roleName());
assertEquals("a", command.roleKey());
assertEquals(Integer.MAX_VALUE, command.roleSort());
assertEquals(StatusConstants.ENABLED, command.status());
}
@Test
void testOf_WithZeroValues() {
CreateRoleCommand command = CreateRoleCommand.of(
"Admin",
"admin",
0,
StatusConstants.ENABLED
);
assertEquals(0, command.roleSort());
}
@Test
void testOf_WithNegativeSort() {
CreateRoleCommand command = CreateRoleCommand.of(
"Admin",
"admin",
-1,
StatusConstants.ENABLED
);
assertEquals(-1, command.roleSort());
}
@Test
void testOf_WithSpecialCharacters() {
CreateRoleCommand command = CreateRoleCommand.of(
"Admin@#$%",
"admin@#$%",
1,
StatusConstants.ENABLED
);
assertEquals("Admin@#$%", command.roleName());
assertEquals("admin@#$%", command.roleKey());
}
@Test
void testOf_WithLongStrings() {
String longRoleName = "a".repeat(1000);
String longRoleKey = "b".repeat(1000);
CreateRoleCommand command = CreateRoleCommand.of(
longRoleName,
longRoleKey,
1,
StatusConstants.ENABLED
);
assertEquals(longRoleName, command.roleName());
assertEquals(longRoleKey, command.roleKey());
}
@Test
void testOf_WithUnicodeCharacters() {
CreateRoleCommand command = CreateRoleCommand.of(
"管理员_测试",
"admin_测试",
1,
StatusConstants.ENABLED
);
assertEquals("管理员_测试", command.roleName());
assertEquals("admin_测试", command.roleKey());
}
@Test
void testOf_WithWhitespace() {
CreateRoleCommand command = CreateRoleCommand.of(
" Admin ",
" admin ",
1,
StatusConstants.ENABLED
);
assertEquals(" Admin ", command.roleName());
assertEquals(" admin ", command.roleKey());
}
@Test
void testOf_WithNumericStrings() {
CreateRoleCommand command = CreateRoleCommand.of(
"12345",
"67890",
1,
StatusConstants.ENABLED
);
assertEquals("12345", command.roleName());
assertEquals("67890", command.roleKey());
}
@Test
void testValidateStatus_EdgeCase_MaxInt() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> CreateRoleCommand.of(
"Admin",
"admin",
1,
Integer.MAX_VALUE
)
);
assertEquals("Invalid status value. Status must be 0 (disabled) or 1 (enabled)", exception.getMessage());
}
@Test
void testValidateStatus_EdgeCase_MinInt() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> CreateRoleCommand.of(
"Admin",
"admin",
1,
Integer.MIN_VALUE
)
);
assertEquals("Invalid status value. Status must be 0 (disabled) or 1 (enabled)", exception.getMessage());
}
}
@@ -0,0 +1,246 @@
package cn.novalon.manage.sys.core.command;
import cn.novalon.manage.sys.primitive.Email;
import cn.novalon.manage.sys.primitive.Password;
import cn.novalon.manage.sys.primitive.Username;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CreateUserCommandTest {
@Test
void testConstructor() {
Username username = Username.of("testuser");
Password password = Password.of("Password123!");
Email email = Email.of("test@example.com");
CreateUserCommand command = new CreateUserCommand(
username,
password,
email,
"nickname",
"1234567890",
1L,
1
);
assertEquals(username, command.username());
assertEquals(password, command.password());
assertEquals(email, command.email());
assertEquals("nickname", command.nickname());
assertEquals("1234567890", command.phone());
assertEquals(1L, command.roleId());
assertEquals(1, command.status());
}
@Test
void testOf() {
CreateUserCommand command = CreateUserCommand.of(
"testuser",
"Password123!",
"test@example.com",
"nickname",
"1234567890",
1L,
1
);
assertNotNull(command.username());
assertNotNull(command.password());
assertNotNull(command.email());
assertEquals("nickname", command.nickname());
assertEquals("1234567890", command.phone());
assertEquals(1L, command.roleId());
assertEquals(1, command.status());
}
@Test
void testOf_WithNullValues() {
CreateUserCommand command = CreateUserCommand.of(
"testuser",
"Password123!",
"test@example.com",
null,
null,
null,
null
);
assertNotNull(command.username());
assertNotNull(command.password());
assertNotNull(command.email());
assertNull(command.nickname());
assertNull(command.phone());
assertNull(command.roleId());
assertNull(command.status());
}
@Test
void testOf_WithEmptyStrings() {
CreateUserCommand command = CreateUserCommand.of(
"testuser",
"Password123!",
"test@example.com",
"",
"",
null,
null
);
assertNotNull(command.username());
assertNotNull(command.password());
assertNotNull(command.email());
assertEquals("", command.nickname());
assertEquals("", command.phone());
assertNull(command.roleId());
assertNull(command.status());
}
@Test
void testOf_WithBoundaryValues() {
CreateUserCommand command = CreateUserCommand.of(
"abc",
"Abc123!@",
"a@b.co",
"n",
"0",
1L,
1
);
assertNotNull(command.username());
assertNotNull(command.password());
assertNotNull(command.email());
assertEquals("n", command.nickname());
assertEquals("0", command.phone());
assertEquals(1L, command.roleId());
assertEquals(1, command.status());
}
@Test
void testOf_WithZeroValues() {
CreateUserCommand command = CreateUserCommand.of(
"testuser",
"Password123!",
"test@example.com",
"nickname",
"1234567890",
0L,
0
);
assertEquals(0L, command.roleId());
assertEquals(0, command.status());
}
@Test
void testOf_WithNegativeValues() {
CreateUserCommand command = CreateUserCommand.of(
"testuser",
"Password123!",
"test@example.com",
"nickname",
"1234567890",
-1L,
-1
);
assertEquals(-1L, command.roleId());
assertEquals(-1, command.status());
}
@Test
void testOf_WithSpecialCharacters() {
CreateUserCommand command = CreateUserCommand.of(
"test_user",
"Password123!",
"test@example.com",
"nick@#$%",
"123@#$%",
1L,
1
);
assertNotNull(command.username());
assertNotNull(command.password());
assertNotNull(command.email());
assertEquals("nick@#$%", command.nickname());
assertEquals("123@#$%", command.phone());
}
@Test
void testOf_WithLongStrings() {
String longNickname = "a".repeat(1000);
String longPhone = "1".repeat(100);
CreateUserCommand command = CreateUserCommand.of(
"testuser",
"Password123!",
"test@example.com",
longNickname,
longPhone,
1L,
1
);
assertEquals(longNickname, command.nickname());
assertEquals(longPhone, command.phone());
}
@Test
void testOf_WithUnicodeCharacters() {
CreateUserCommand command = CreateUserCommand.of(
"test_user",
"Password123!",
"test@example.com",
"昵称_测试",
"1234567890",
1L,
1
);
assertNotNull(command.username());
assertNotNull(command.password());
assertNotNull(command.email());
assertEquals("昵称_测试", command.nickname());
}
@Test
void testOf_WithWhitespace() {
CreateUserCommand command = CreateUserCommand.of(
"testuser",
"Password123!",
"test@example.com",
" nickname ",
" 1234567890 ",
1L,
1
);
assertNotNull(command.username());
assertNotNull(command.password());
assertNotNull(command.email());
assertEquals(" nickname ", command.nickname());
assertEquals(" 1234567890 ", command.phone());
}
@Test
void testOf_WithNumericStrings() {
CreateUserCommand command = CreateUserCommand.of(
"test123",
"Password123!",
"test@example.com",
"12345",
"12345",
1L,
1
);
assertNotNull(command.username());
assertNotNull(command.password());
assertNotNull(command.email());
assertEquals("12345", command.nickname());
assertEquals("12345", command.phone());
}
}
@@ -0,0 +1,312 @@
package cn.novalon.manage.sys.core.command;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UpdateUserCommandTest {
@Test
void testConstructor() {
UpdateUserCommand command = new UpdateUserCommand(
1L,
"testuser",
"password123",
"test@example.com",
2L,
1,
false
);
assertEquals(1L, command.id());
assertEquals("testuser", command.username());
assertEquals("password123", command.password());
assertEquals("test@example.com", command.email());
assertEquals(2L, command.roleId());
assertEquals(1, command.status());
assertFalse(command.clearRole());
}
@Test
void testOf_WithoutClearRole() {
UpdateUserCommand command = UpdateUserCommand.of(
1L,
"testuser",
"password123",
"test@example.com",
2L,
1
);
assertEquals(1L, command.id());
assertEquals("testuser", command.username());
assertEquals("password123", command.password());
assertEquals("test@example.com", command.email());
assertEquals(2L, command.roleId());
assertEquals(1, command.status());
assertFalse(command.clearRole());
}
@Test
void testOf_WithClearRoleFalse() {
UpdateUserCommand command = UpdateUserCommand.of(
1L,
"testuser",
"password123",
"test@example.com",
2L,
1,
false
);
assertEquals(1L, command.id());
assertEquals("testuser", command.username());
assertEquals("password123", command.password());
assertEquals("test@example.com", command.email());
assertEquals(2L, command.roleId());
assertEquals(1, command.status());
assertFalse(command.clearRole());
}
@Test
void testOf_WithClearRoleTrue() {
UpdateUserCommand command = UpdateUserCommand.of(
1L,
"testuser",
"password123",
"test@example.com",
2L,
1,
true
);
assertEquals(1L, command.id());
assertEquals("testuser", command.username());
assertEquals("password123", command.password());
assertEquals("test@example.com", command.email());
assertEquals(2L, command.roleId());
assertEquals(1, command.status());
assertTrue(command.clearRole());
}
@Test
void testOf_WithNullValues() {
UpdateUserCommand command = UpdateUserCommand.of(
null,
null,
null,
null,
null,
null
);
assertNull(command.id());
assertNull(command.username());
assertNull(command.password());
assertNull(command.email());
assertNull(command.roleId());
assertNull(command.status());
assertFalse(command.clearRole());
}
@Test
void testOf_WithEmptyStrings() {
UpdateUserCommand command = UpdateUserCommand.of(
1L,
"",
"",
"",
null,
null
);
assertEquals(1L, command.id());
assertEquals("", command.username());
assertEquals("", command.password());
assertEquals("", command.email());
assertNull(command.roleId());
assertNull(command.status());
assertFalse(command.clearRole());
}
@Test
void testOf_WithBoundaryValues() {
UpdateUserCommand command = UpdateUserCommand.of(
Long.MAX_VALUE,
"a",
"1",
"a@b.c",
Long.MAX_VALUE,
Integer.MAX_VALUE,
true
);
assertEquals(Long.MAX_VALUE, command.id());
assertEquals("a", command.username());
assertEquals("1", command.password());
assertEquals("a@b.c", command.email());
assertEquals(Long.MAX_VALUE, command.roleId());
assertEquals(Integer.MAX_VALUE, command.status());
assertTrue(command.clearRole());
}
@Test
void testOf_WithZeroValues() {
UpdateUserCommand command = UpdateUserCommand.of(
0L,
"testuser",
"password123",
"test@example.com",
0L,
0,
false
);
assertEquals(0L, command.id());
assertEquals(0L, command.roleId());
assertEquals(0, command.status());
assertFalse(command.clearRole());
}
@Test
void testOf_WithNegativeValues() {
UpdateUserCommand command = UpdateUserCommand.of(
-1L,
"testuser",
"password123",
"test@example.com",
-1L,
-1,
true
);
assertEquals(-1L, command.id());
assertEquals(-1L, command.roleId());
assertEquals(-1, command.status());
assertTrue(command.clearRole());
}
@Test
void testOf_WithSpecialCharacters() {
UpdateUserCommand command = UpdateUserCommand.of(
1L,
"user@#$%",
"pass@#$%",
"test@#$%.com",
1L,
1,
false
);
assertEquals("user@#$%", command.username());
assertEquals("pass@#$%", command.password());
assertEquals("test@#$%.com", command.email());
assertFalse(command.clearRole());
}
@Test
void testOf_WithLongStrings() {
String longUsername = "a".repeat(1000);
String longPassword = "b".repeat(1000);
String longEmail = "c".repeat(1000) + "@example.com";
UpdateUserCommand command = UpdateUserCommand.of(
1L,
longUsername,
longPassword,
longEmail,
1L,
1,
false
);
assertEquals(longUsername, command.username());
assertEquals(longPassword, command.password());
assertEquals(longEmail, command.email());
assertFalse(command.clearRole());
}
@Test
void testOf_WithUnicodeCharacters() {
UpdateUserCommand command = UpdateUserCommand.of(
1L,
"用户_测试",
"密码_测试",
"测试@example.com",
1L,
1,
false
);
assertEquals("用户_测试", command.username());
assertEquals("密码_测试", command.password());
assertEquals("测试@example.com", command.email());
assertFalse(command.clearRole());
}
@Test
void testOf_WithWhitespace() {
UpdateUserCommand command = UpdateUserCommand.of(
1L,
" testuser ",
" password123 ",
" test@example.com ",
1L,
1,
false
);
assertEquals(" testuser ", command.username());
assertEquals(" password123 ", command.password());
assertEquals(" test@example.com ", command.email());
assertFalse(command.clearRole());
}
@Test
void testOf_WithNumericStrings() {
UpdateUserCommand command = UpdateUserCommand.of(
1L,
"12345",
"12345",
"12345@example.com",
1L,
1,
false
);
assertEquals("12345", command.username());
assertEquals("12345", command.password());
assertEquals("12345@example.com", command.email());
assertFalse(command.clearRole());
}
@Test
void testClearRoleFlag_True() {
UpdateUserCommand command = new UpdateUserCommand(
1L,
"testuser",
"password123",
"test@example.com",
2L,
1,
true
);
assertTrue(command.clearRole());
}
@Test
void testClearRoleFlag_False() {
UpdateUserCommand command = new UpdateUserCommand(
1L,
"testuser",
"password123",
"test@example.com",
2L,
1,
false
);
assertFalse(command.clearRole());
}
}
@@ -0,0 +1,106 @@
package cn.novalon.manage.sys.core.domain;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
class SysUserTest {
private SysUser user;
@BeforeEach
void setUp() {
user = new SysUser();
}
@Test
void testGenerateId() {
Long id = user.generateId();
assertNotNull(id);
assertTrue(id > 0);
assertEquals(id, user.getId());
}
@Test
void testGenerateId_GeneratesUniqueIds() {
SysUser user1 = new SysUser();
SysUser user2 = new SysUser();
Long id1 = user1.generateId();
Long id2 = user2.generateId();
assertNotNull(id1);
assertNotNull(id2);
assertNotEquals(id1, id2);
}
@Test
void testDelete() {
assertNull(user.getDeletedAt());
user.delete();
assertNotNull(user.getDeletedAt());
assertTrue(user.getDeletedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
assertTrue(user.getDeletedAt().isAfter(LocalDateTime.now().minusSeconds(1)));
}
@Test
void testDelete_WhenAlreadyDeleted() {
user.delete();
LocalDateTime firstDeleteTime = user.getDeletedAt();
user.delete();
LocalDateTime secondDeleteTime = user.getDeletedAt();
assertNotNull(firstDeleteTime);
assertNotNull(secondDeleteTime);
assertNotEquals(firstDeleteTime, secondDeleteTime);
}
@Test
void testUsername() {
user.setUsername("testuser");
assertEquals("testuser", user.getUsername());
}
@Test
void testPassword() {
user.setPassword("password123");
assertEquals("password123", user.getPassword());
}
@Test
void testNickname() {
user.setNickname("测试用户");
assertEquals("测试用户", user.getNickname());
}
@Test
void testEmail() {
user.setEmail("test@example.com");
assertEquals("test@example.com", user.getEmail());
}
@Test
void testPhone() {
user.setPhone("13800138000");
assertEquals("13800138000", user.getPhone());
}
@Test
void testRoleId() {
user.setRoleId(1L);
assertEquals(1L, user.getRoleId());
}
@Test
void testStatus() {
user.setStatus(1);
assertEquals(1, user.getStatus());
}
}
@@ -1,7 +1,10 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.query.OperationLogQuery;
import cn.novalon.manage.sys.core.repository.IOperationLogRepository;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -12,9 +15,9 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import java.util.Collections;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -30,7 +33,7 @@ class OperationLogServiceTest {
@BeforeEach
void setUp() {
operationLogService = new OperationLogService(logRepository);
testLog = new OperationLog();
testLog.setId(1L);
testLog.setUsername("testuser");
@@ -45,68 +48,121 @@ class OperationLogServiceTest {
@Test
void testSave() {
when(logRepository.save(any(OperationLog.class))).thenReturn(Mono.just(testLog));
Mono<OperationLog> result = operationLogService.save(testLog);
StepVerifier.create(result)
.expectNextMatches(log ->
log.getId().equals(1L) &&
log.getUsername().equals("testuser") &&
log.getCreatedAt() != null)
.expectNextMatches(log -> log.getId().equals(1L) &&
log.getUsername().equals("testuser") &&
log.getCreatedAt() != null)
.verifyComplete();
verify(logRepository).save(any(OperationLog.class));
}
@Test
void testFindAll() {
when(logRepository.findAll()).thenReturn(Flux.just(testLog));
Flux<OperationLog> result = operationLogService.findAll();
StepVerifier.create(result)
.expectNext(testLog)
.verifyComplete();
verify(logRepository).findAll();
}
@Test
void testFindByUsername() {
when(logRepository.findByUsername("testuser")).thenReturn(Flux.just(testLog));
Flux<OperationLog> result = operationLogService.findByUsername("testuser");
StepVerifier.create(result)
.expectNext(testLog)
.verifyComplete();
verify(logRepository).findByUsername("testuser");
}
@Test
void testCount() {
when(logRepository.count()).thenReturn(Mono.just(100L));
Mono<Long> result = operationLogService.count();
StepVerifier.create(result)
.expectNext(100L)
.verifyComplete();
verify(logRepository).count();
}
@Test
void testCountToday() {
when(logRepository.countByCreatedAtAfter(any(LocalDateTime.class))).thenReturn(Mono.just(10L));
Mono<Long> result = operationLogService.countToday();
StepVerifier.create(result)
.expectNext(10L)
.verifyComplete();
verify(logRepository).countByCreatedAtAfter(any(LocalDateTime.class));
}
@Test
void testFindById() {
when(logRepository.findById(1L)).thenReturn(Mono.just(testLog));
Mono<OperationLog> result = operationLogService.findById(1L);
StepVerifier.create(result)
.expectNext(testLog)
.verifyComplete();
verify(logRepository).findById(1L);
}
@Test
void testFindById_NotFound() {
when(logRepository.findById(999L)).thenReturn(Mono.empty());
Mono<OperationLog> result = operationLogService.findById(999L);
StepVerifier.create(result)
.verifyComplete();
verify(logRepository).findById(999L);
}
@Test
void testFindByQueryWithPagination() {
PageResponse<OperationLog> pageResponse = new PageResponse<>();
pageResponse.setContent(Collections.singletonList(testLog));
pageResponse.setTotalElements(1L);
pageResponse.setTotalPages(1);
pageResponse.setCurrentPage(0);
pageResponse.setPageSize(10);
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(0);
pageRequest.setSize(10);
OperationLogQuery query = new OperationLogQuery();
when(logRepository.findByQueryWithPagination(any(OperationLogQuery.class), any(PageRequest.class)))
.thenReturn(Mono.just(pageResponse));
Mono<PageResponse<OperationLog>> result = operationLogService.findByQueryWithPagination(query, pageRequest);
StepVerifier.create(result)
.expectNextMatches(response -> response.getContent().size() == 1 &&
response.getTotalElements() == 1L &&
response.getTotalPages() == 1)
.verifyComplete();
verify(logRepository).findByQueryWithPagination(any(OperationLogQuery.class), any(PageRequest.class));
}
}
@@ -11,8 +11,8 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 系统配置服务单元测试类
@@ -14,8 +14,6 @@ import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -14,8 +14,6 @@ import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -14,10 +14,8 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -14,10 +14,8 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -14,10 +14,8 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import java.util.Arrays;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -33,7 +31,7 @@ class SysMenuServiceTest {
@BeforeEach
void setUp() {
menuService = new SysMenuService(menuRepository);
testMenu = new SysMenu();
testMenu.setId(1L);
testMenu.setMenuName("系统管理");
@@ -49,64 +47,62 @@ class SysMenuServiceTest {
@Test
void testFindById() {
when(menuRepository.findById(1L)).thenReturn(Mono.just(testMenu));
Mono<SysMenu> result = menuService.findById(1L);
StepVerifier.create(result)
.expectNext(testMenu)
.verifyComplete();
verify(menuRepository).findById(1L);
}
@Test
void testFindAll() {
when(menuRepository.findAll()).thenReturn(Flux.just(testMenu));
Flux<SysMenu> result = menuService.findAll();
StepVerifier.create(result)
.expectNext(testMenu)
.verifyComplete();
verify(menuRepository).findAll();
}
@Test
void testFindByParentId() {
when(menuRepository.findByParentId(0L)).thenReturn(Flux.just(testMenu));
Flux<SysMenu> result = menuService.findByParentId(0L);
StepVerifier.create(result)
.expectNext(testMenu)
.verifyComplete();
verify(menuRepository).findByParentId(0L);
}
@Test
void testCreateMenu() {
when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(testMenu));
Mono<SysMenu> result = menuService.createMenu(testMenu);
StepVerifier.create(result)
.expectNextMatches(menu ->
menu.getId().equals(1L) &&
menu.getMenuName().equals("系统管理") &&
menu.getCreatedAt() != null)
.expectNextMatches(menu -> menu.getId().equals(1L) &&
menu.getMenuName().equals("系统管理") &&
menu.getCreatedAt() != null)
.verifyComplete();
verify(menuRepository).save(any(SysMenu.class));
}
@Test
void testCreateMenuWithCommand() {
CreateMenuCommand command = new CreateMenuCommand(
0L, "用户管理", "M", 2, "user", "user:manage", 1
);
0L, "用户管理", "M", 2, "user", "user:manage", 1);
SysMenu createdMenu = new SysMenu();
createdMenu.setId(2L);
createdMenu.setMenuName("用户管理");
@@ -117,53 +113,49 @@ class SysMenuServiceTest {
createdMenu.setComponent("user");
createdMenu.setStatus(1);
createdMenu.setCreatedAt(LocalDateTime.now());
when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(createdMenu));
Mono<SysMenu> result = menuService.createMenu(command);
StepVerifier.create(result)
.expectNextMatches(menu ->
menu.getMenuName().equals("用户管理") &&
menu.getParentId().equals(0L) &&
menu.getCreatedAt() != null)
.expectNextMatches(menu -> menu.getMenuName().equals("用户管理") &&
menu.getParentId().equals(0L) &&
menu.getCreatedAt() != null)
.verifyComplete();
verify(menuRepository).save(any(SysMenu.class));
}
@Test
void testUpdateMenu() {
when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(testMenu));
Mono<SysMenu> result = menuService.updateMenu(testMenu);
StepVerifier.create(result)
.expectNextMatches(menu ->
menu.getId().equals(1L) &&
menu.getUpdatedAt() != null)
.expectNextMatches(menu -> menu.getId().equals(1L) &&
menu.getUpdatedAt() != null)
.verifyComplete();
verify(menuRepository).save(any(SysMenu.class));
}
@Test
void testUpdateMenuWithCommand() {
UpdateMenuCommand command = new UpdateMenuCommand(
1L, 0L, "系统管理(更新)", "M", 1, "system", "system:manage", 1
);
1L, 0L, "系统管理(更新)", "M", 1, "system", "system:manage", 1);
when(menuRepository.findById(1L)).thenReturn(Mono.just(testMenu));
when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(testMenu));
Mono<SysMenu> result = menuService.updateMenu(command);
StepVerifier.create(result)
.expectNextMatches(menu ->
menu.getMenuName().equals("系统管理(更新)") &&
menu.getUpdatedAt() != null)
.expectNextMatches(menu -> menu.getMenuName().equals("系统管理(更新)") &&
menu.getUpdatedAt() != null)
.verifyComplete();
verify(menuRepository).findById(1L);
verify(menuRepository).save(any(SysMenu.class));
}
@@ -171,17 +163,16 @@ class SysMenuServiceTest {
@Test
void testUpdateMenuWithCommand_NotFound() {
UpdateMenuCommand command = new UpdateMenuCommand(
999L, 0L, "不存在的菜单", "M", 1, "system", "system:manage", 1
);
999L, 0L, "不存在的菜单", "M", 1, "system", "system:manage", 1);
when(menuRepository.findById(999L)).thenReturn(Mono.empty());
Mono<SysMenu> result = menuService.updateMenu(command);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex.getMessage().contains("Menu not found"))
.verify();
verify(menuRepository).findById(999L);
}
@@ -212,8 +203,7 @@ class SysMenuServiceTest {
when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(updatedMenu));
UpdateMenuCommand command = new UpdateMenuCommand(
1L, null, null, null, null, null, null, null
);
1L, null, null, null, null, null, null, null);
StepVerifier.create(menuService.updateMenu(command))
.expectNextMatches(menu -> menu.getUpdatedAt() != null)
@@ -250,8 +240,7 @@ class SysMenuServiceTest {
when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(updatedMenu));
UpdateMenuCommand command = new UpdateMenuCommand(
1L, 2L, "系统管理(更新)", "C", 2, "system_updated", "system:manage_updated", 0
);
1L, 2L, "系统管理(更新)", "C", 2, "system_updated", "system:manage_updated", 0);
StepVerifier.create(menuService.updateMenu(command))
.expectNextMatches(menu -> menu.getUpdatedAt() != null)
@@ -264,12 +253,12 @@ class SysMenuServiceTest {
@Test
void testDeleteMenu() {
when(menuRepository.deleteById(1L)).thenReturn(Mono.empty());
Mono<Void> result = menuService.deleteMenu(1L);
StepVerifier.create(result)
.verifyComplete();
verify(menuRepository).deleteById(1L);
}
@@ -279,60 +268,59 @@ class SysMenuServiceTest {
parentMenu.setId(1L);
parentMenu.setMenuName("系统管理");
parentMenu.setParentId(0L);
SysMenu childMenu = new SysMenu();
childMenu.setId(2L);
childMenu.setMenuName("用户管理");
childMenu.setParentId(1L);
when(menuRepository.findAll()).thenReturn(Flux.just(parentMenu, childMenu));
Flux<SysMenu> result = menuService.buildMenuTree(menuService.findAll());
StepVerifier.create(result)
.expectNextMatches(menu ->
menu.getId().equals(1L) &&
menu.getChildren() != null &&
menu.getChildren().size() == 1)
.expectNextMatches(menu -> menu.getId().equals(1L) &&
menu.getChildren() != null &&
menu.getChildren().size() == 1)
.verifyComplete();
}
@Test
void testFindById_WhenMenuNotFound() {
when(menuRepository.findById(999L)).thenReturn(Mono.empty());
Mono<SysMenu> result = menuService.findById(999L);
StepVerifier.create(result)
.expectNextCount(0)
.verifyComplete();
verify(menuRepository).findById(999L);
}
@Test
void testFindAll_WhenNoMenusExist() {
when(menuRepository.findAll()).thenReturn(Flux.empty());
Flux<SysMenu> result = menuService.findAll();
StepVerifier.create(result)
.expectNextCount(0)
.verifyComplete();
verify(menuRepository).findAll();
}
@Test
void testFindByParentId_WhenNoChildrenExist() {
when(menuRepository.findByParentId(999L)).thenReturn(Flux.empty());
Flux<SysMenu> result = menuService.findByParentId(999L);
StepVerifier.create(result)
.expectNextCount(0)
.verifyComplete();
verify(menuRepository).findByParentId(999L);
}
@@ -359,24 +347,22 @@ class SysMenuServiceTest {
savedMenu.setCreatedAt(LocalDateTime.now());
when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(savedMenu));
Mono<SysMenu> result = menuService.createMenu(newMenu);
StepVerifier.create(result)
.expectNextMatches(menu ->
menu.getStatus().equals(1) &&
menu.getCreatedAt() != null)
.expectNextMatches(menu -> menu.getStatus().equals(1) &&
menu.getCreatedAt() != null)
.verifyComplete();
verify(menuRepository).save(any(SysMenu.class));
}
@Test
void testCreateMenuWithCommand_WithDefaultStatus() {
CreateMenuCommand command = new CreateMenuCommand(
0L, "日志管理", "M", 3, "log", "log:manage", null
);
0L, "日志管理", "M", 3, "log", "log:manage", null);
SysMenu createdMenu = new SysMenu();
createdMenu.setId(3L);
createdMenu.setMenuName("日志管理");
@@ -387,31 +373,30 @@ class SysMenuServiceTest {
createdMenu.setComponent("log");
createdMenu.setStatus(1);
createdMenu.setCreatedAt(LocalDateTime.now());
when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(createdMenu));
Mono<SysMenu> result = menuService.createMenu(command);
StepVerifier.create(result)
.expectNextMatches(menu ->
menu.getMenuName().equals("日志管理") &&
menu.getStatus().equals(1) &&
menu.getCreatedAt() != null)
.expectNextMatches(menu -> menu.getMenuName().equals("日志管理") &&
menu.getStatus().equals(1) &&
menu.getCreatedAt() != null)
.verifyComplete();
verify(menuRepository).save(any(SysMenu.class));
}
@Test
void testBuildMenuTree_WithEmptyTree() {
when(menuRepository.findAll()).thenReturn(Flux.empty());
Flux<SysMenu> result = menuService.buildMenuTree(menuService.findAll());
StepVerifier.create(result)
.expectNextCount(0)
.verifyComplete();
verify(menuRepository).findAll();
}
@@ -421,30 +406,29 @@ class SysMenuServiceTest {
rootMenu.setId(1L);
rootMenu.setMenuName("系统管理");
rootMenu.setParentId(0L);
SysMenu level1Menu = new SysMenu();
level1Menu.setId(2L);
level1Menu.setMenuName("用户管理");
level1Menu.setParentId(1L);
SysMenu level2Menu = new SysMenu();
level2Menu.setId(3L);
level2Menu.setMenuName("用户列表");
level2Menu.setParentId(2L);
when(menuRepository.findAll()).thenReturn(Flux.just(rootMenu, level1Menu, level2Menu));
Flux<SysMenu> result = menuService.buildMenuTree(menuService.findAll());
StepVerifier.create(result)
.expectNextMatches(menu ->
menu.getId().equals(1L) &&
menu.getChildren() != null &&
menu.getChildren().size() == 1 &&
menu.getChildren().get(0).getChildren() != null &&
menu.getChildren().get(0).getChildren().size() == 1)
.expectNextMatches(menu -> menu.getId().equals(1L) &&
menu.getChildren() != null &&
menu.getChildren().size() == 1 &&
menu.getChildren().get(0).getChildren() != null &&
menu.getChildren().get(0).getChildren().size() == 1)
.verifyComplete();
verify(menuRepository).findAll();
}
@@ -454,30 +438,30 @@ class SysMenuServiceTest {
root1.setId(1L);
root1.setMenuName("系统管理");
root1.setParentId(0L);
SysMenu root2 = new SysMenu();
root2.setId(2L);
root2.setMenuName("监控管理");
root2.setParentId(0L);
SysMenu child1 = new SysMenu();
child1.setId(3L);
child1.setMenuName("用户管理");
child1.setParentId(1L);
SysMenu child2 = new SysMenu();
child2.setId(4L);
child2.setMenuName("性能监控");
child2.setParentId(2L);
when(menuRepository.findAll()).thenReturn(Flux.just(root1, root2, child1, child2));
Flux<SysMenu> result = menuService.buildMenuTree(menuService.findAll());
StepVerifier.create(result)
.expectNextCount(2)
.verifyComplete();
verify(menuRepository).findAll();
}
}
@@ -12,7 +12,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.relational.core.query.Query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -0,0 +1,184 @@
package cn.novalon.manage.sys.dto.response;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class AuthResponseTest {
@Test
void testConstructorWithParameters() {
AuthResponse response = new AuthResponse("test-token", 1L, "testuser");
assertEquals("test-token", response.getToken());
assertEquals(1L, response.getUserId());
assertEquals("testuser", response.getUsername());
}
@Test
void testDefaultConstructor() {
AuthResponse response = new AuthResponse();
assertNull(response.getToken());
assertNull(response.getUserId());
assertNull(response.getUsername());
}
@Test
void testGettersAndSetters() {
AuthResponse response = new AuthResponse();
response.setToken("new-token");
response.setUserId(2L);
response.setUsername("newuser");
assertEquals("new-token", response.getToken());
assertEquals(2L, response.getUserId());
assertEquals("newuser", response.getUsername());
}
@Test
void testSettersWithNullValues() {
AuthResponse response = new AuthResponse();
response.setToken(null);
response.setUserId(null);
response.setUsername(null);
assertNull(response.getToken());
assertNull(response.getUserId());
assertNull(response.getUsername());
}
@Test
void testSettersWithEmptyStrings() {
AuthResponse response = new AuthResponse();
response.setToken("");
response.setUsername("");
assertEquals("", response.getToken());
assertEquals("", response.getUsername());
}
@Test
void testConstructorWithNullValues() {
AuthResponse response = new AuthResponse(null, null, null);
assertNull(response.getToken());
assertNull(response.getUserId());
assertNull(response.getUsername());
}
@Test
void testConstructorWithEmptyStrings() {
AuthResponse response = new AuthResponse("", 1L, "");
assertEquals("", response.getToken());
assertEquals(1L, response.getUserId());
assertEquals("", response.getUsername());
}
@Test
void testSettersWithBoundaryValues() {
AuthResponse response = new AuthResponse();
response.setUserId(Long.MAX_VALUE);
response.setUserId(Long.MIN_VALUE);
response.setUserId(0L);
assertEquals(0L, response.getUserId());
}
@Test
void testSettersWithNegativeValues() {
AuthResponse response = new AuthResponse();
response.setUserId(-1L);
assertEquals(-1L, response.getUserId());
}
@Test
void testSettersWithSpecialCharacters() {
AuthResponse response = new AuthResponse();
String specialToken = "token@#$%^&*()";
String specialUsername = "user@#$%^&*()";
response.setToken(specialToken);
response.setUsername(specialUsername);
assertEquals(specialToken, response.getToken());
assertEquals(specialUsername, response.getUsername());
}
@Test
void testSettersWithLongStrings() {
AuthResponse response = new AuthResponse();
String longToken = "a".repeat(1000);
String longUsername = "b".repeat(500);
response.setToken(longToken);
response.setUsername(longUsername);
assertEquals(longToken, response.getToken());
assertEquals(longUsername, response.getUsername());
}
@Test
void testSettersWithUnicodeCharacters() {
AuthResponse response = new AuthResponse();
String unicodeToken = "token_测试_🔑";
String unicodeUsername = "user_测试_👤";
response.setToken(unicodeToken);
response.setUsername(unicodeUsername);
assertEquals(unicodeToken, response.getToken());
assertEquals(unicodeUsername, response.getUsername());
}
@Test
void testSettersWithWhitespace() {
AuthResponse response = new AuthResponse();
response.setToken(" token ");
response.setUsername(" user ");
assertEquals(" token ", response.getToken());
assertEquals(" user ", response.getUsername());
}
@Test
void testMultipleSetOperations() {
AuthResponse response = new AuthResponse();
response.setToken("token1");
response.setToken("token2");
assertEquals("token2", response.getToken());
}
@Test
void testConstructorWithZeroUserId() {
AuthResponse response = new AuthResponse("token", 0L, "user");
assertEquals("token", response.getToken());
assertEquals(0L, response.getUserId());
assertEquals("user", response.getUsername());
}
@Test
void testSettersWithNumericStrings() {
AuthResponse response = new AuthResponse();
response.setToken("12345");
response.setUsername("67890");
assertEquals("12345", response.getToken());
assertEquals("67890", response.getUsername());
}
}
@@ -0,0 +1,144 @@
package cn.novalon.manage.sys.dto.response;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class FilePreviewResponseTest {
@Test
void testGettersAndSetters() {
FilePreviewResponse response = new FilePreviewResponse();
response.setFileName("test.pdf");
response.setFileType("application/pdf");
response.setFileSize(1024L);
response.setPreviewType("image");
response.setPreviewData("base64data");
assertEquals("test.pdf", response.getFileName());
assertEquals("application/pdf", response.getFileType());
assertEquals(1024L, response.getFileSize());
assertEquals("image", response.getPreviewType());
assertEquals("base64data", response.getPreviewData());
}
@Test
void testSettersWithNullValues() {
FilePreviewResponse response = new FilePreviewResponse();
response.setFileName(null);
response.setFileType(null);
response.setFileSize(null);
response.setPreviewType(null);
response.setPreviewData(null);
assertNull(response.getFileName());
assertNull(response.getFileType());
assertNull(response.getFileSize());
assertNull(response.getPreviewType());
assertNull(response.getPreviewData());
}
@Test
void testSettersWithEmptyStrings() {
FilePreviewResponse response = new FilePreviewResponse();
response.setFileName("");
response.setFileType("");
response.setPreviewType("");
response.setPreviewData("");
assertEquals("", response.getFileName());
assertEquals("", response.getFileType());
assertEquals("", response.getPreviewType());
assertEquals("", response.getPreviewData());
}
@Test
void testSettersWithBoundaryValues() {
FilePreviewResponse response = new FilePreviewResponse();
response.setFileSize(Long.MAX_VALUE);
response.setFileSize(Long.MIN_VALUE);
response.setFileSize(0L);
assertEquals(0L, response.getFileSize());
}
@Test
void testSettersWithSpecialCharacters() {
FilePreviewResponse response = new FilePreviewResponse();
String specialFileName = "文件名@#$%^&*().pdf";
String specialFileType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
response.setFileName(specialFileName);
response.setFileType(specialFileType);
assertEquals(specialFileName, response.getFileName());
assertEquals(specialFileType, response.getFileType());
}
@Test
void testSettersWithLongStrings() {
FilePreviewResponse response = new FilePreviewResponse();
String longFileName = "a".repeat(1000) + ".pdf";
String longPreviewData = "x".repeat(10000);
response.setFileName(longFileName);
response.setPreviewData(longPreviewData);
assertEquals(longFileName, response.getFileName());
assertEquals(longPreviewData, response.getPreviewData());
}
@Test
void testSettersWithUnicodeCharacters() {
FilePreviewResponse response = new FilePreviewResponse();
String unicodeFileName = "文件名_测试_📄.pdf";
String unicodePreviewData = "数据_测试_🔍";
response.setFileName(unicodeFileName);
response.setPreviewData(unicodePreviewData);
assertEquals(unicodeFileName, response.getFileName());
assertEquals(unicodePreviewData, response.getPreviewData());
}
@Test
void testSettersWithWhitespace() {
FilePreviewResponse response = new FilePreviewResponse();
response.setFileName(" test.pdf ");
response.setFileType(" application/pdf ");
response.setPreviewType(" image ");
assertEquals(" test.pdf ", response.getFileName());
assertEquals(" application/pdf ", response.getFileType());
assertEquals(" image ", response.getPreviewType());
}
@Test
void testMultipleSetOperations() {
FilePreviewResponse response = new FilePreviewResponse();
response.setFileName("file1.pdf");
response.setFileName("file2.pdf");
assertEquals("file2.pdf", response.getFileName());
}
@Test
void testSettersWithNumericStrings() {
FilePreviewResponse response = new FilePreviewResponse();
response.setFileName("12345.pdf");
response.setFileType("12345");
assertEquals("12345.pdf", response.getFileName());
assertEquals("12345", response.getFileType());
}
}
@@ -0,0 +1,146 @@
package cn.novalon.manage.sys.dto.response;
import cn.novalon.manage.sys.core.domain.SysUser;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
class UserResponseTest {
@Test
void testGettersAndSetters() {
UserResponse response = new UserResponse();
response.setId(1L);
response.setUsername("testuser");
response.setEmail("test@example.com");
response.setRoleId(2L);
response.setStatus(1);
response.setCreatedAt(LocalDateTime.now());
response.setUpdatedAt(LocalDateTime.now());
assertEquals(1L, response.getId());
assertEquals("testuser", response.getUsername());
assertEquals("test@example.com", response.getEmail());
assertEquals(2L, response.getRoleId());
assertEquals(1, response.getStatus());
assertNotNull(response.getCreatedAt());
assertNotNull(response.getUpdatedAt());
}
@Test
void testFromDomain() {
SysUser user = new SysUser();
user.setId(1L);
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setRoleId(2L);
user.setStatus(1);
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
UserResponse response = UserResponse.fromDomain(user);
assertEquals(user.getId(), response.getId());
assertEquals(user.getUsername(), response.getUsername());
assertEquals(user.getEmail(), response.getEmail());
assertEquals(user.getRoleId(), response.getRoleId());
assertEquals(user.getStatus(), response.getStatus());
assertEquals(user.getCreatedAt(), response.getCreatedAt());
assertEquals(user.getUpdatedAt(), response.getUpdatedAt());
}
@Test
void testFromDomain_WithNullUser() {
assertThrows(NullPointerException.class, () -> UserResponse.fromDomain(null));
}
@Test
void testFromDomain_WithNullFields() {
SysUser user = new SysUser();
UserResponse response = UserResponse.fromDomain(user);
assertNull(response.getId());
assertNull(response.getUsername());
assertNull(response.getEmail());
assertNull(response.getRoleId());
assertNull(response.getStatus());
assertNull(response.getCreatedAt());
assertNull(response.getUpdatedAt());
}
@Test
void testFromDomain_WithEmptyStrings() {
SysUser user = new SysUser();
user.setUsername("");
user.setEmail("");
UserResponse response = UserResponse.fromDomain(user);
assertEquals("", response.getUsername());
assertEquals("", response.getEmail());
}
@Test
void testSettersWithNullValues() {
UserResponse response = new UserResponse();
response.setId(null);
response.setUsername(null);
response.setEmail(null);
response.setRoleId(null);
response.setStatus(null);
response.setCreatedAt(null);
response.setUpdatedAt(null);
assertNull(response.getId());
assertNull(response.getUsername());
assertNull(response.getEmail());
assertNull(response.getRoleId());
assertNull(response.getStatus());
assertNull(response.getCreatedAt());
assertNull(response.getUpdatedAt());
}
@Test
void testSettersWithBoundaryValues() {
UserResponse response = new UserResponse();
response.setId(Long.MAX_VALUE);
response.setRoleId(Long.MIN_VALUE);
response.setStatus(Integer.MAX_VALUE);
assertEquals(Long.MAX_VALUE, response.getId());
assertEquals(Long.MIN_VALUE, response.getRoleId());
assertEquals(Integer.MAX_VALUE, response.getStatus());
}
@Test
void testSettersWithZeroValues() {
UserResponse response = new UserResponse();
response.setId(0L);
response.setRoleId(0L);
response.setStatus(0);
assertEquals(0L, response.getId());
assertEquals(0L, response.getRoleId());
assertEquals(0, response.getStatus());
}
@Test
void testSettersWithNegativeValues() {
UserResponse response = new UserResponse();
response.setId(-1L);
response.setRoleId(-1L);
response.setStatus(-1);
assertEquals(-1L, response.getId());
assertEquals(-1L, response.getRoleId());
assertEquals(-1, response.getStatus());
}
}
@@ -2,7 +2,6 @@ package cn.novalon.manage.sys.handler.auth;
import cn.novalon.manage.sys.dto.request.LoginRequest;
import cn.novalon.manage.sys.dto.request.UserRegisterRequest;
import cn.novalon.manage.sys.dto.response.AuthResponse;
import cn.novalon.manage.sys.security.JwtTokenProvider;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.service.ISysUserService;
@@ -14,16 +13,12 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -18,7 +18,6 @@ import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -20,8 +20,6 @@ import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -0,0 +1,180 @@
package cn.novalon.manage.sys.handler.log;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.query.OperationLogQuery;
import cn.novalon.manage.sys.core.service.IOperationLogService;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class OperationLogHandlerTest {
@Mock
private IOperationLogService logService;
private OperationLogHandler logHandler;
private OperationLog testOperationLog;
@BeforeEach
void setUp() {
logHandler = new OperationLogHandler(logService);
testOperationLog = new OperationLog();
testOperationLog.setId(1L);
testOperationLog.setUsername("testuser");
testOperationLog.setOperation("测试操作");
testOperationLog.setMethod("testMethod");
testOperationLog.setParams("test params");
testOperationLog.setDuration(100L);
testOperationLog.setIp("192.168.1.1");
testOperationLog.setCreatedAt(LocalDateTime.now());
}
@Test
void testGetAllOperationLogs() {
when(logService.findAll()).thenReturn(Flux.just(testOperationLog));
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = logHandler.getAllOperationLogs(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
verify(logService).findAll();
}
@Test
void testGetOperationLogById() {
when(logService.findById(1L)).thenReturn(Mono.just(testOperationLog));
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> response = logHandler.getOperationLogById(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
verify(logService).findById(1L);
}
@Test
void testGetOperationLogById_NotFound() {
when(logService.findById(999L)).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
.build();
Mono<ServerResponse> response = logHandler.getOperationLogById(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(logService).findById(999L);
}
@Test
void testGetOperationLogsByPage() {
PageResponse<OperationLog> pageResponse = new PageResponse<>();
pageResponse.setContent(java.util.Collections.singletonList(testOperationLog));
pageResponse.setTotalElements(1L);
pageResponse.setTotalPages(1);
pageResponse.setPageSize(10);
pageResponse.setCurrentPage(0);
when(logService.findByQueryWithPagination(any(OperationLogQuery.class), any(PageRequest.class)))
.thenReturn(Mono.just(pageResponse));
ServerRequest request = MockServerRequest.builder()
.queryParam("page", "0")
.queryParam("size", "10")
.queryParam("sort", "createdAt")
.queryParam("order", "desc")
.build();
Mono<ServerResponse> response = logHandler.getOperationLogsByPage(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
verify(logService).findByQueryWithPagination(any(OperationLogQuery.class), any(PageRequest.class));
}
@Test
void testGetOperationLogsByPageWithKeyword() {
PageResponse<OperationLog> pageResponse = new PageResponse<>();
pageResponse.setContent(java.util.Collections.singletonList(testOperationLog));
pageResponse.setTotalElements(1L);
pageResponse.setTotalPages(1);
pageResponse.setPageSize(10);
pageResponse.setCurrentPage(0);
when(logService.findByQueryWithPagination(any(OperationLogQuery.class), any(PageRequest.class)))
.thenReturn(Mono.just(pageResponse));
ServerRequest request = MockServerRequest.builder()
.queryParam("page", "0")
.queryParam("size", "10")
.queryParam("sort", "createdAt")
.queryParam("order", "desc")
.queryParam("keyword", "test")
.build();
Mono<ServerResponse> response = logHandler.getOperationLogsByPage(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
verify(logService).findByQueryWithPagination(any(OperationLogQuery.class), any(PageRequest.class));
}
@Test
void testGetOperationLogCount() {
when(logService.count()).thenReturn(Mono.just(100L));
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = logHandler.getOperationLogCount(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
verify(logService).count();
}
@Test
void testCreateOperationLog() {
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(testOperationLog));
ServerRequest request = MockServerRequest.builder()
.body(Mono.just(testOperationLog));
Mono<ServerResponse> response = logHandler.createOperationLog(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse -> serverResponse.statusCode() == HttpStatus.CREATED)
.verifyComplete();
verify(logService).save(any(OperationLog.class));
}
}
@@ -4,7 +4,6 @@ import cn.novalon.manage.sys.core.domain.SysLoginLog;
import cn.novalon.manage.sys.core.domain.SysExceptionLog;
import cn.novalon.manage.sys.core.service.ISysLoginLogService;
import cn.novalon.manage.sys.core.service.ISysExceptionLogService;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -22,7 +21,6 @@ import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -22,7 +22,6 @@ import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -153,7 +152,17 @@ class MenuHandlerTest {
@Test
void testGetMenusByType() {
when(menuService.findAll()).thenReturn(Flux.just(testMenu));
SysMenu menu1 = new SysMenu();
menu1.setId(1L);
menu1.setMenuName("系统管理");
menu1.setMenuType("M");
SysMenu menu2 = new SysMenu();
menu2.setId(2L);
menu2.setMenuName("用户管理");
menu2.setMenuType("C");
when(menuService.findAll()).thenReturn(Flux.just(menu1, menu2));
ServerRequest request = MockServerRequest.builder()
.queryParam("menuType", "M")
@@ -170,7 +179,17 @@ class MenuHandlerTest {
@Test
void testGetMenusByType_Null() {
when(menuService.findAll()).thenReturn(Flux.just(testMenu));
SysMenu menu1 = new SysMenu();
menu1.setId(1L);
menu1.setMenuName("系统管理");
menu1.setMenuType("M");
SysMenu menu2 = new SysMenu();
menu2.setId(2L);
menu2.setMenuName("用户管理");
menu2.setMenuType("C");
when(menuService.findAll()).thenReturn(Flux.just(menu1, menu2));
ServerRequest request = MockServerRequest.builder()
.build();
@@ -184,6 +203,33 @@ class MenuHandlerTest {
verify(menuService).findAll();
}
@Test
void testGetMenusByType_NoMatch() {
SysMenu menu1 = new SysMenu();
menu1.setId(1L);
menu1.setMenuName("系统管理");
menu1.setMenuType("M");
SysMenu menu2 = new SysMenu();
menu2.setId(2L);
menu2.setMenuName("用户管理");
menu2.setMenuType("C");
when(menuService.findAll()).thenReturn(Flux.just(menu1, menu2));
ServerRequest request = MockServerRequest.builder()
.queryParam("menuType", "F")
.build();
Mono<ServerResponse> response = menuHandler.getMenusByType(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
verify(menuService).findAll();
}
@Test
void testCreateMenu() {
MenuCreateRequest createRequest = new MenuCreateRequest();
@@ -22,7 +22,6 @@ import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -0,0 +1,210 @@
package cn.novalon.manage.sys.interceptor;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.service.IOperationLogService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.net.InetSocketAddress;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class OperationLogFilterTest {
@Mock
private IOperationLogService logService;
@Mock
private WebFilterChain chain;
@Mock
private ObjectMapper objectMapper;
private OperationLogFilter filter;
@BeforeEach
void setUp() {
filter = new OperationLogFilter(logService, objectMapper);
}
@Test
void testFilter_SkipAuthEndpoints() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(exchange)).thenReturn(Mono.empty());
StepVerifier.create(filter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
verify(logService, never()).save(any(OperationLog.class));
}
@Test
void testFilter_RecordSuccessLog() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(exchange)).thenReturn(Mono.empty());
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
StepVerifier.create(filter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
verify(logService).save(any(OperationLog.class));
}
@Test
void testFilter_RecordErrorLog() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
RuntimeException error = new RuntimeException("Test error");
when(chain.filter(exchange)).thenReturn(Mono.error(error));
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
StepVerifier.create(filter.filter(exchange, chain))
.expectError(RuntimeException.class)
.verify();
verify(chain).filter(exchange);
verify(logService).save(any(OperationLog.class));
}
@Test
void testFilter_WithXForwardedForHeader() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header("X-Forwarded-For", "192.168.1.1")
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(exchange)).thenReturn(Mono.empty());
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
StepVerifier.create(filter.filter(exchange, chain))
.verifyComplete();
verify(logService).save(argThat(log -> "192.168.1.1".equals(log.getIp())));
}
@Test
void testFilter_WithXRealIPHeader() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header("X-Real-IP", "10.0.0.1")
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(exchange)).thenReturn(Mono.empty());
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
StepVerifier.create(filter.filter(exchange, chain))
.verifyComplete();
verify(logService).save(argThat(log -> "10.0.0.1".equals(log.getIp())));
}
@Test
void testFilter_WithMultipleIPsInXForwardedFor() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header("X-Forwarded-For", "192.168.1.1, 10.0.0.1")
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(exchange)).thenReturn(Mono.empty());
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
StepVerifier.create(filter.filter(exchange, chain))
.verifyComplete();
verify(logService).save(argThat(log -> "192.168.1.1".equals(log.getIp())));
}
@Test
void testFilter_WithUnknownHeader() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header("X-Forwarded-For", "unknown")
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(exchange)).thenReturn(Mono.empty());
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
StepVerifier.create(filter.filter(exchange, chain))
.verifyComplete();
verify(logService).save(any(OperationLog.class));
}
@Test
void testFilter_DifferentHttpMethods() {
HttpMethod[] methods = {HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.PATCH};
for (HttpMethod method : methods) {
MockServerHttpRequest request = MockServerHttpRequest.method(method, "/api/users")
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(exchange)).thenReturn(Mono.empty());
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
StepVerifier.create(filter.filter(exchange, chain))
.verifyComplete();
verify(logService).save(argThat(log -> method.name().equals(log.getMethod())));
reset(logService, chain);
}
}
@Test
void testFilter_WithQueryParams() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users?page=1&size=10")
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(exchange)).thenReturn(Mono.empty());
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
StepVerifier.create(filter.filter(exchange, chain))
.verifyComplete();
verify(logService).save(argThat(log -> log.getParams() != null && !log.getParams().isEmpty()));
}
@Test
void testFilter_LogSaveError() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.remoteAddress(new InetSocketAddress("127.0.0.1", 8080))
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(exchange)).thenReturn(Mono.empty());
when(logService.save(any(OperationLog.class))).thenReturn(Mono.error(new RuntimeException("Save failed")));
StepVerifier.create(filter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
verify(logService).save(any(OperationLog.class));
}
}
@@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
+24 -7
View File
@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.10</version>
<version>3.5.12</version>
<relativePath />
</parent>
@@ -24,9 +24,11 @@
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.2.10</spring-boot.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
<spring-boot.version>3.5.12</spring-boot.version>
<spring-cloud.version>2025.0.0</spring-cloud.version>
<lombok.version>1.18.30</lombok.version>
<resilience4j.version>2.2.0</resilience4j.version>
<rxjava.version>3.1.9</rxjava.version>
</properties>
<modules>
@@ -149,13 +151,28 @@
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>2.3.0</version>
<version>2.8.16</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.13.4</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>${resilience4j.version}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-reactor</artifactId>
<version>${resilience4j.version}</version>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
<version>${rxjava.version}</version>
</dependency>
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
@@ -195,4 +212,4 @@
</plugin>
</plugins>
</build>
</project>
</project>