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