新增答辩用后台管理系统

This commit is contained in:
2026-06-26 13:57:13 +08:00
parent 35e7532f1b
commit 1a5aa9b3ef
98 changed files with 16022 additions and 419 deletions
@@ -9,24 +9,17 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
@@ -37,19 +30,76 @@ public class OperationLogWebFilter implements WebFilter {
private final IOperationLogService operationLogService;
private final ObjectMapper objectMapper;
private static final Map<String, OperationInfo> OPERATION_MAPPING = new ConcurrentHashMap<>();
/** 精确匹配的操作映射(method:path → module, operation */
private static final Map<String, OperationInfo> PRECISE_MAPPING = new LinkedHashMap<>();
/** 前缀匹配的操作映射(按声明顺序) */
private static final Map<String, OperationInfo> PREFIX_MAPPING = new LinkedHashMap<>();
/** URL模块名称映射 */
private static final Map<String, String> MODULE_NAMES = new LinkedHashMap<>();
static {
OPERATION_MAPPING.put("POST:/api/roles", new OperationInfo("角色管理", "创建角色"));
OPERATION_MAPPING.put("PUT:/api/roles/", new OperationInfo("角色管理", "更新角色"));
OPERATION_MAPPING.put("DELETE:/api/roles/", new OperationInfo("角色管理", "删除角色"));
OPERATION_MAPPING.put("POST:/api/users", new OperationInfo("用户管理", "创建用户"));
OPERATION_MAPPING.put("PUT:/api/users/", new OperationInfo("用户管理", "更新用户"));
OPERATION_MAPPING.put("DELETE:/api/users/", new OperationInfo("用户管理", "删除用户"));
OPERATION_MAPPING.put("POST:/api/users/", new OperationInfo("用户管理", "用户操作"));
OPERATION_MAPPING.put("POST:/api/menus", new OperationInfo("菜单管理", "创建菜单"));
OPERATION_MAPPING.put("PUT:/api/menus/", new OperationInfo("菜单管理", "更新菜单"));
OPERATION_MAPPING.put("DELETE:/api/menus/", new OperationInfo("菜单管理", "删除菜单"));
// ===== 精确路径匹配 =====
PRECISE_MAPPING.put("POST:/api/roles", new OperationInfo("角色管理", "创建角色"));
PRECISE_MAPPING.put("POST:/api/users", new OperationInfo("用户管理", "创建用户"));
PRECISE_MAPPING.put("POST:/api/menus", new OperationInfo("菜单管理", "创建菜单"));
PRECISE_MAPPING.put("POST:/api/auth/login", new OperationInfo("认证", "用户登录"));
PRECISE_MAPPING.put("GET:/api/groupCourse/types/categories", new OperationInfo("团课类型", "查询分类"));
PRECISE_MAPPING.put("POST:/api/groupCourse/types", new OperationInfo("团课类型", "创建类型"));
PRECISE_MAPPING.put("POST:/api/groupCourse", new OperationInfo("团课管理", "创建团课"));
PRECISE_MAPPING.put("POST:/api/member", new OperationInfo("会员管理", "创建会员"));
PRECISE_MAPPING.put("POST:/api/member-cards", new OperationInfo("会员卡管理", "创建会员卡"));
PRECISE_MAPPING.put("POST:/api/groupCourse/recommend", new OperationInfo("推荐管理", "创建推荐"));
PRECISE_MAPPING.put("POST:/api/checkIn", new OperationInfo("签到管理", "签到"));
PRECISE_MAPPING.put("POST:/api/payment/create", new OperationInfo("支付管理", "创建支付"));
PRECISE_MAPPING.put("POST:/api/upload/image", new OperationInfo("文件管理", "上传图片"));
// ===== 前缀匹配 =====
PREFIX_MAPPING.put("PUT:/api/roles/", new OperationInfo("角色管理", "更新角色"));
PREFIX_MAPPING.put("DELETE:/api/roles/", new OperationInfo("角色管理", "删除角色"));
PREFIX_MAPPING.put("PUT:/api/users/", new OperationInfo("用户管理", "更新用户"));
PREFIX_MAPPING.put("DELETE:/api/users/", new OperationInfo("用户管理", "删除用户"));
PREFIX_MAPPING.put("PUT:/api/menus/", new OperationInfo("菜单管理", "更新菜单"));
PREFIX_MAPPING.put("DELETE:/api/menus/", new OperationInfo("菜单管理", "删除菜单"));
PREFIX_MAPPING.put("PUT:/api/groupCourse/types/", new OperationInfo("团课类型", "更新类型"));
PREFIX_MAPPING.put("DELETE:/api/groupCourse/types/", new OperationInfo("团课类型", "删除类型"));
PREFIX_MAPPING.put("PUT:/api/groupCourse/", new OperationInfo("团课管理", "更新团课"));
PREFIX_MAPPING.put("DELETE:/api/groupCourse/", new OperationInfo("团课管理", "删除团课"));
PREFIX_MAPPING.put("POST:/api/groupCourse/", new OperationInfo("团课管理", "操作团课"));
PREFIX_MAPPING.put("PUT:/api/member/", new OperationInfo("会员管理", "编辑会员"));
PREFIX_MAPPING.put("DELETE:/api/member/", new OperationInfo("会员管理", "删除会员"));
PREFIX_MAPPING.put("PUT:/api/member-cards/", new OperationInfo("会员卡管理", "编辑会员卡"));
PREFIX_MAPPING.put("DELETE:/api/member-cards/", new OperationInfo("会员卡管理", "删除会员卡"));
PREFIX_MAPPING.put("PUT:/api/groupCourse/recommend/", new OperationInfo("推荐管理", "更新推荐"));
PREFIX_MAPPING.put("DELETE:/api/groupCourse/recommend/", new OperationInfo("推荐管理", "删除推荐"));
PREFIX_MAPPING.put("POST:/api/groupCourse/recommend/", new OperationInfo("推荐管理", "操作推荐"));
PREFIX_MAPPING.put("POST:/api/member-card-transactions/", new OperationInfo("会员卡", "交易操作"));
PREFIX_MAPPING.put("PUT:/api/payment/", new OperationInfo("支付管理", "更新支付"));
PREFIX_MAPPING.put("POST:/api/payment/", new OperationInfo("支付管理", "支付操作"));
PREFIX_MAPPING.put("PUT:/api/admin/member/", new OperationInfo("会员管理", "管理员编辑会员"));
PREFIX_MAPPING.put("DELETE:/api/admin/member/", new OperationInfo("会员管理", "管理员删除会员"));
PREFIX_MAPPING.put("DELETE:/api/groupCourse/labels/", new OperationInfo("标签管理", "删除标签"));
// ===== URL模块名映射(用于未匹配写操作自动生成) =====
MODULE_NAMES.put("roles", "角色管理");
MODULE_NAMES.put("users", "用户管理");
MODULE_NAMES.put("menus", "菜单管理");
MODULE_NAMES.put("auth", "认证");
MODULE_NAMES.put("groupCourse", "团课管理");
MODULE_NAMES.put("member", "会员管理");
MODULE_NAMES.put("member-cards", "会员卡管理");
MODULE_NAMES.put("member-card-records", "会员卡记录");
MODULE_NAMES.put("member-card-transactions", "会员卡交易");
MODULE_NAMES.put("checkIn", "签到管理");
MODULE_NAMES.put("payment", "支付管理");
MODULE_NAMES.put("dictionaries", "字典管理");
MODULE_NAMES.put("config", "系统配置");
MODULE_NAMES.put("upload", "文件管理");
MODULE_NAMES.put("logs", "日志管理");
MODULE_NAMES.put("datacount", "数据统计");
MODULE_NAMES.put("diagnostic", "诊断");
MODULE_NAMES.put("stats", "统计");
}
public OperationLogWebFilter(IOperationLogService operationLogService, ObjectMapper objectMapper) {
@@ -61,10 +111,8 @@ public class OperationLogWebFilter implements WebFilter {
@PostConstruct
public void init() {
logger.info("=== OperationLogWebFilter 初始化 ===");
logger.info("操作日志映射配置数量: {}", OPERATION_MAPPING.size());
OPERATION_MAPPING.forEach((key, value) -> {
logger.info(" {} -> {}:{}", key, value.module, value.operation);
});
logger.info("精确匹配配置数量: {}, 前缀匹配配置数量: {}, 模块映射数量: {}",
PRECISE_MAPPING.size(), PREFIX_MAPPING.size(), MODULE_NAMES.size());
}
@Override
@@ -72,103 +120,135 @@ public class OperationLogWebFilter implements WebFilter {
ServerHttpRequest request = exchange.getRequest();
String method = request.getMethod().name();
String path = request.getPath().value();
String key = method + ":" + path;
logger.info("WebFilter 拦截请求: {} {}", method, path);
// 先尝试精确匹配
OperationInfo operationInfo = PRECISE_MAPPING.get(key);
OperationInfo operationInfo = findOperationInfo(method, path);
if (operationInfo == null) {
// 尝试前缀匹配
operationInfo = findPrefixMatch(key);
}
if (operationInfo == null) {
// 未匹配:如果是写操作(POST/PUT/DELETE),自动生成记录
if (isWriteOperation(method)) {
operationInfo = buildAutoOperationInfo(method, path);
logger.info("自动生成操作日志: {} {} -> {}:{}", method, path, operationInfo.module, operationInfo.operation);
}
} else {
logger.info("匹配到操作日志配置: {} {} -> {}:{}", method, path, operationInfo.module, operationInfo.operation);
}
if (operationInfo == null) {
logger.info("未匹配到操作日志配置,跳过: {} {}", method, path);
return chain.filter(exchange);
}
logger.info("匹配到操作日志配置: {} {} -> {}:{}", method, path, operationInfo.module, operationInfo.operation);
long startTime = System.currentTimeMillis();
String ip = IpUtils.getClientIp(request);
final OperationInfo finalInfo = operationInfo;
return Mono.deferContextual(contextView -> {
return chain.filter(exchange)
.then(Mono.defer(() -> {
long duration = System.currentTimeMillis() - startTime;
logger.info("请求处理完成,准备保存操作日志: {} {}, 耗时: {}ms", method, path, duration);
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
String username = principal instanceof String ? (String) principal : "system";
logger.info("获取到用户名: {}", username);
return Mono.just(username);
})
.defaultIfEmpty("system")
.flatMap(username -> {
logger.info("开始保存操作日志: 用户={}, 操作={}", username,
operationInfo.module + " - " + operationInfo.operation);
OperationLog log = new OperationLog();
log.setUsername(username);
log.setOperation(operationInfo.module + " - " + operationInfo.operation);
log.setMethod(method + " " + path);
log.setParams(null);
log.setIp(ip);
log.setDuration(duration);
log.setStatus("0");
return operationLogService.save(log)
.doOnSuccess(saved -> logger.info("操作日志保存成功: {} - {}",
operationInfo.module, operationInfo.operation))
.doOnError(e -> logger.error("操作日志保存失败: {}", e.getMessage(), e))
.onErrorResume(e -> Mono.empty());
})
.then();
}))
.onErrorResume(error -> {
long duration = System.currentTimeMillis() - startTime;
logger.error("请求处理失败: {} {}, 错误: {}", method, path, error.getMessage());
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
String username = principal instanceof String ? (String) principal : "system";
return Mono.just(username);
})
.defaultIfEmpty("system")
.flatMap(username -> {
OperationLog log = new OperationLog();
log.setUsername(username);
log.setOperation(operationInfo.module + " - " + operationInfo.operation);
log.setMethod(method + " " + path);
log.setParams(null);
log.setIp(ip);
log.setDuration(duration);
log.setStatus("1");
log.setErrorMsg(error.getMessage());
return operationLogService.save(log)
.doOnError(e -> logger.error("错误日志保存失败: {}", e.getMessage()))
.onErrorResume(e -> Mono.empty());
})
.then(Mono.error(error));
});
});
return chain.filter(exchange)
.then(Mono.defer(() -> {
long duration = System.currentTimeMillis() - startTime;
return getCurrentUsername()
.flatMap(username -> saveOperationLog(username, method, path, ip, duration, "0", null, finalInfo));
}))
.onErrorResume(error -> {
long duration = System.currentTimeMillis() - startTime;
logger.error("请求处理失败: {} {}, 错误: {}", method, path, error.getMessage());
return getCurrentUsername()
.flatMap(username -> saveOperationLog(username, method, path, ip, duration, "1",
error.getMessage().substring(0, Math.min(error.getMessage().length(), 500)), finalInfo))
.then(Mono.error(error));
});
}
private OperationInfo findOperationInfo(String method, String path) {
String key = method + ":" + path;
if (OPERATION_MAPPING.containsKey(key)) {
return OPERATION_MAPPING.get(key);
}
private boolean isWriteOperation(String method) {
return HttpMethod.POST.name().equals(method) ||
HttpMethod.PUT.name().equals(method) ||
HttpMethod.DELETE.name().equals(method) ||
HttpMethod.PATCH.name().equals(method);
}
for (Map.Entry<String, OperationInfo> entry : OPERATION_MAPPING.entrySet()) {
String mappingKey = entry.getKey();
if (key.startsWith(mappingKey)) {
private OperationInfo findPrefixMatch(String key) {
for (Map.Entry<String, OperationInfo> entry : PREFIX_MAPPING.entrySet()) {
if (key.startsWith(entry.getKey())) {
return entry.getValue();
}
}
return null;
}
/**
* 根据URL路径自动生成操作信息
* 例如: DELETE:/api/groupCourse/types/5 → 团课管理 - 删除操作
*/
private OperationInfo buildAutoOperationInfo(String method, String path) {
String module = extractModuleFromPath(path);
String operation = methodToOperationName(method);
return new OperationInfo(module, operation);
}
private String extractModuleFromPath(String path) {
// 去掉 /api/ 前缀,取第一段作为模块名
if (path.startsWith("/api/")) {
String subPath = path.substring(5); // remove "/api/"
int slashIdx = subPath.indexOf('/');
String moduleKey = slashIdx > 0 ? subPath.substring(0, slashIdx) : subPath;
// 尝试复合模块名 (如 member-cards)
if (slashIdx > 0) {
String rest = subPath.substring(slashIdx + 1);
int nextSlash = rest.indexOf('/');
String secondPart = nextSlash > 0 ? rest.substring(0, nextSlash) : rest;
String compositeKey = moduleKey + "/" + secondPart;
if (MODULE_NAMES.containsKey(compositeKey)) {
return MODULE_NAMES.get(compositeKey);
}
}
return MODULE_NAMES.getOrDefault(moduleKey, moduleKey);
}
return path;
}
private String methodToOperationName(String method) {
switch (method.toUpperCase()) {
case "POST": return "创建/新增操作";
case "PUT": return "编辑/更新操作";
case "DELETE": return "删除操作";
case "PATCH": return "修改操作";
default: return "操作";
}
}
private Mono<String> getCurrentUsername() {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal())
.map(principal -> principal instanceof String ? (String) principal : "system")
.defaultIfEmpty("system")
.onErrorReturn("system");
}
private Mono<Void> saveOperationLog(String username, String method, String path, String ip,
long duration, String status, String errorMsg, OperationInfo info) {
OperationLog log = new OperationLog();
log.setUsername(username);
log.setOperation(info.module + " - " + info.operation);
log.setMethod(method + " " + path);
log.setIp(ip);
log.setDuration(duration);
log.setStatus(status);
log.setErrorMsg(errorMsg);
return operationLogService.save(log)
.doOnSuccess(saved -> logger.debug("操作日志保存成功: {} - {}", info.module, info.operation))
.doOnError(e -> logger.error("操作日志保存失败: {}", e.getMessage(), e))
.onErrorResume(e -> Mono.empty())
.then();
}
private static class OperationInfo {
final String module;
final String operation;
@@ -11,6 +11,11 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFlux
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebFluxSecurity
@@ -29,6 +34,20 @@ public class SecurityConfig {
this.environment = environment;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
String[] activeProfiles = environment.getActiveProfiles();
@@ -41,6 +60,7 @@ public class SecurityConfig {
activeProfiles.length > 0 ? String.join(",", activeProfiles) : "default", isDevOrTest);
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
@@ -53,6 +53,8 @@ public interface ISysUserService {
Mono<SysUser> changePassword(Long userId, String oldPassword, String newPassword);
Mono<Boolean> verifyPassword(Long userId, String password);
Mono<Void> updateRoleIdToNullByRoleId(Long roleId);
Mono<Void> assignRolesToUser(Long userId, java.util.List<Long> roleIds);
@@ -120,6 +120,9 @@ public class SysPermissionService implements ISysPermissionService {
@Override
public Flux<SysPermission> findByRoleIds(List<Long> roleIds) {
if (roleIds == null || roleIds.isEmpty()) {
return Flux.empty();
}
return permissionRepository.findByRoleIds(roleIds);
}
@@ -83,7 +83,6 @@ public class SysRoleService implements ISysRoleService {
@Override
public Mono<SysRole> createRole(CreateRoleCommand command) {
SysRole role = new SysRole();
role.generateId();
role.setRoleName(command.roleName());
role.setRoleKey(command.roleKey());
role.setRoleSort(command.roleSort());
@@ -97,7 +97,6 @@ public class SysUserService implements ISysUserService {
logger.info("SysUserService.createUser - 用户名: {}, 密码前缀: {}",
user.getUsername(),
user.getPassword() != null ? user.getPassword().substring(0, 7) : "null");
user.generateId();
if (user.getPassword() != null && !user.getPassword().startsWith("$2a$")
&& !user.getPassword().startsWith("$2b$")) {
logger.info("密码不以$2a$或$2b$开头,重新编码");
@@ -117,7 +116,6 @@ public class SysUserService implements ISysUserService {
@Override
public Mono<SysUser> createUser(CreateUserCommand command) {
SysUser user = new SysUser();
user.generateId();
user.setUsername(command.username().getValue());
user.setPassword(passwordEncoder.encode(command.password().getValue()));
user.setEmail(command.email().getValue());
@@ -204,6 +202,13 @@ public class SysUserService implements ISysUserService {
return userRepository.updateRoleIdToNullByRoleId(roleId);
}
@Override
public Mono<Boolean> verifyPassword(Long userId, String password) {
return userRepository.findById(userId)
.map(user -> passwordEncoder.matches(password, user.getPassword()))
.defaultIfEmpty(false);
}
@Override
public Mono<SysUser> changePassword(Long userId, String oldPassword, String newPassword) {
return userRepository.findById(userId)
@@ -2,6 +2,8 @@ package cn.novalon.gym.manage.sys.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
/**
* 认证响应DTO
*
@@ -20,13 +22,21 @@ public class AuthResponse {
@Schema(description = "用户名", example = "admin")
private String username;
@Schema(description = "角色标识列表", example = "[\"admin\"]")
private List<String> roles;
@Schema(description = "权限码列表", example = "[\"system:user:view\", \"system:user:create\"]")
private List<String> permissions;
public AuthResponse() {
}
public AuthResponse(String token, Long userId, String username) {
public AuthResponse(String token, Long userId, String username, List<String> roles, List<String> permissions) {
this.token = token;
this.userId = userId;
this.username = username;
this.roles = roles;
this.permissions = permissions;
}
public String getToken() {
@@ -52,4 +62,20 @@ public class AuthResponse {
public void setUsername(String username) {
this.username = username;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public List<String> getPermissions() {
return permissions;
}
public void setPermissions(List<String> permissions) {
this.permissions = permissions;
}
}
@@ -8,6 +8,7 @@ import cn.novalon.gym.manage.sys.core.domain.SysUser;
import cn.novalon.gym.manage.sys.core.domain.SysLoginLog;
import cn.novalon.gym.manage.sys.core.service.ISysUserService;
import cn.novalon.gym.manage.sys.core.service.ISysLoginLogService;
import cn.novalon.gym.manage.sys.core.service.ISysPermissionService;
import cn.novalon.gym.manage.sys.util.UserAgentParser;
import cn.novalon.gym.manage.sys.util.IpLocationParser;
import cn.novalon.gym.manage.common.util.StatusConstants;
@@ -28,6 +29,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -50,6 +52,7 @@ public class SysAuthHandler {
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final ISysLoginLogService loginLogService;
private final ISysPermissionService permissionService;
private final UserAgentParser userAgentParser;
private final IpLocationParser ipLocationParser;
@@ -60,11 +63,13 @@ public class SysAuthHandler {
public SysAuthHandler(ISysUserService userService,
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider, ISysLoginLogService loginLogService,
ISysPermissionService permissionService,
UserAgentParser userAgentParser, IpLocationParser ipLocationParser) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
this.loginLogService = loginLogService;
this.permissionService = permissionService;
this.userAgentParser = userAgentParser;
this.ipLocationParser = ipLocationParser;
@@ -126,29 +131,49 @@ public class SysAuthHandler {
}
return userService.getUserRoles(user.getId())
.map(role -> role.getRoleKey())
.collectList()
.flatMap(roleKeys -> {
String token = jwtTokenProvider
.generateToken(
.flatMap(roles -> {
List<String> roleKeys = roles.stream()
.map(r -> r.getRoleKey())
.collect(Collectors.toList());
List<Long> roleIds = roles.stream()
.map(r -> r.getId())
.collect(Collectors.toList());
Mono<List<String>> permCodesMono;
if (roleIds.isEmpty()) {
permCodesMono = Mono.just(java.util.Collections.<String>emptyList());
} else {
permCodesMono = permissionService.findByRoleIds(roleIds)
.map(p -> p.getPermissionCode())
.collectList();
}
return permCodesMono
.flatMap(permCodes -> {
String token = jwtTokenProvider
.generateToken(
user.getUsername(),
user.getId(),
roleKeys);
logger.info("用户登录成功: username={}, userId={}, roles={}",
user.getUsername(),
user.getId(),
roleKeys);
recordLoginLog(loginRequest
.getUsername(),
clientIp,
"0", "登录成功",
userAgent);
AuthResponse response = new AuthResponse(
token,
user.getId(),
user.getUsername());
return ServerResponse.ok()
.bodyValue(response);
logger.info("用户登录成功: username={}, userId={}, roles={}, permissions={}",
user.getUsername(),
user.getId(),
roleKeys,
permCodes.size());
recordLoginLog(loginRequest.getUsername(),
clientIp,
"0", "登录成功",
userAgent);
AuthResponse response = new AuthResponse(
token,
user.getId(),
user.getUsername(),
roleKeys,
permCodes);
return ServerResponse.ok()
.bodyValue(response);
});
});
})
.switchIfEmpty(Mono.defer(() -> {
@@ -190,6 +215,50 @@ public class SysAuthHandler {
});
}
@Operation(summary = "获取当前用户信息", description = "根据Token获取当前登录用户的详细信息和权限")
public Mono<ServerResponse> me(ServerRequest request) {
String authHeader = request.headers().firstHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
}
String token = authHeader.substring(7);
if (!jwtTokenProvider.validateToken(token)) {
return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
}
Long userId = jwtTokenProvider.getUserIdFromToken(token);
return userService.findById(userId)
.flatMap(user -> userService.getUserRoles(user.getId())
.collectList()
.flatMap(roles -> {
List<String> roleKeys = roles.stream()
.map(r -> r.getRoleKey())
.collect(Collectors.toList());
List<Long> roleIds = roles.stream()
.map(r -> r.getId())
.collect(Collectors.toList());
Mono<List<String>> permCodesMono;
if (roleIds.isEmpty()) {
permCodesMono = Mono.just(java.util.Collections.<String>emptyList());
} else {
permCodesMono = permissionService.findByRoleIds(roleIds)
.map(p -> p.getPermissionCode())
.collectList();
}
return permCodesMono
.flatMap(permCodes -> {
AuthResponse response = new AuthResponse(
token,
user.getId(),
user.getUsername(),
roleKeys,
permCodes);
return ServerResponse.ok().bodyValue(response);
});
}));
}
private void recordLoginLog(String username, String ip, String status, String message, String userAgent) {
try {
SysLoginLog loginLog = new SysLoginLog();
@@ -1,7 +1,11 @@
package cn.novalon.gym.manage.sys.handler.permission;
import cn.novalon.gym.manage.sys.core.domain.SysPermission;
import cn.novalon.gym.manage.sys.core.domain.SysRole;
import cn.novalon.gym.manage.sys.core.service.ISysPermissionService;
import cn.novalon.gym.manage.sys.core.service.ISysRoleService;
import cn.novalon.gym.manage.sys.core.service.ISysUserService;
import cn.novalon.gym.manage.sys.util.AuthUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
@@ -22,10 +26,19 @@ import java.util.List;
@Tag(name = "权限管理", description = "权限相关操作")
public class SysPermissionHandler {
private final ISysPermissionService permissionService;
private static final Long BUILTIN_ROLE_ID = 1L;
public SysPermissionHandler(ISysPermissionService permissionService) {
private final ISysPermissionService permissionService;
private final ISysRoleService roleService;
private final ISysUserService userService;
private final AuthUtil authUtil;
public SysPermissionHandler(ISysPermissionService permissionService, ISysRoleService roleService,
ISysUserService userService, AuthUtil authUtil) {
this.permissionService = permissionService;
this.roleService = roleService;
this.userService = userService;
this.authUtil = authUtil;
}
@Operation(summary = "获取所有权限", description = "获取系统中所有权限列表")
@@ -97,12 +110,27 @@ public class SysPermissionHandler {
.body(permissionService.getPermissionsByRoleId(roleId), SysPermission.class);
}
@Operation(summary = "为角色分配权限", description = "为指定角色分配权限列表")
@Operation(summary = "为角色分配权限", description = "为指定角色分配权限列表,需验证管理员密码,超级管理员角色不可被分配")
public Mono<ServerResponse> assignPermissionsToRole(ServerRequest request) {
Long roleId = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(AssignPermissionsRequest.class)
.flatMap(req -> permissionService.assignPermissionsToRole(roleId, req.permissionIds()))
.then(ServerResponse.ok().build());
return verifyAdminPassword(request)
.flatMap(valid -> {
if (!valid) return ServerResponse.badRequest().bodyValue("管理员密码不能为空或错误");
if (BUILTIN_ROLE_ID.equals(roleId)) return ServerResponse.badRequest().bodyValue("超级管理员角色权限不可被修改");
return request.bodyToMono(AssignPermissionsRequest.class)
.flatMap(req -> permissionService.assignPermissionsToRole(roleId, req.permissionIds()))
.then(ServerResponse.ok().build());
});
}
private Mono<Boolean> verifyAdminPassword(ServerRequest request) {
String adminPassword = request.queryParam("adminPassword").orElse(null);
if (adminPassword == null || adminPassword.isBlank()) {
return Mono.just(false);
}
Long adminId = authUtil.getMemberIdOrThrow(request);
return userService.verifyPassword(adminId, adminPassword);
}
private record AssignPermissionsRequest(List<Long> permissionIds) {}
@@ -2,6 +2,8 @@ package cn.novalon.gym.manage.sys.handler.role;
import cn.novalon.gym.manage.sys.core.domain.SysRole;
import cn.novalon.gym.manage.sys.core.service.ISysRoleService;
import cn.novalon.gym.manage.sys.core.service.ISysUserService;
import cn.novalon.gym.manage.sys.util.AuthUtil;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.sys.dto.request.RoleCreateRequest;
import cn.novalon.gym.manage.sys.dto.request.RoleUpdateRequest;
@@ -30,12 +32,18 @@ import java.util.Map;
@Tag(name = "角色管理", description = "角色相关操作")
public class SysRoleHandler {
private static final Long BUILTIN_ROLE_ID = 1L;
private final ISysRoleService roleService;
private final Validator validator;
private final AuthUtil authUtil;
private final ISysUserService userService;
public SysRoleHandler(ISysRoleService roleService, Validator validator) {
public SysRoleHandler(ISysRoleService roleService, Validator validator, AuthUtil authUtil, ISysUserService userService) {
this.roleService = roleService;
this.validator = validator;
this.authUtil = authUtil;
this.userService = userService;
}
@Operation(summary = "获取所有角色", description = "获取系统中所有角色列表")
@@ -115,30 +123,39 @@ public class SysRoleHandler {
});
}
@Operation(summary = "更新角色", description = "更新角色信息")
@Operation(summary = "更新角色", description = "更新角色信息需验证管理员密码超级管理员角色不可被编辑")
@OperationLog(operation = "更新角色", module = "角色管理")
public Mono<ServerResponse> updateRole(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(RoleUpdateRequest.class)
.map(req -> UpdateRoleCommand.of(
id,
req.getRoleName(),
req.getRoleKey(),
req.getRoleSort(),
req.getStatus()
))
.flatMap(roleService::updateRole)
.flatMap(updatedRole -> ServerResponse.ok().bodyValue(updatedRole))
.switchIfEmpty(ServerResponse.notFound().build());
return verifyAdminPassword(request)
.flatMap(valid -> {
if (!valid) return ServerResponse.badRequest().bodyValue("管理员密码不能为空或错误");
if (BUILTIN_ROLE_ID.equals(id)) return ServerResponse.badRequest().bodyValue("超级管理员角色不可编辑");
return request.bodyToMono(RoleUpdateRequest.class)
.map(req -> UpdateRoleCommand.of(
id, req.getRoleName(), req.getRoleKey(),
req.getRoleSort(), req.getStatus()
))
.flatMap(roleService::updateRole)
.flatMap(updatedRole -> ServerResponse.ok().bodyValue(updatedRole))
.switchIfEmpty(ServerResponse.notFound().build());
});
}
@Operation(summary = "删除角色", description = "逻辑删除角色")
@Operation(summary = "删除角色", description = "逻辑删除角色需验证管理员密码超级管理员角色不可被删除")
@OperationLog(operation = "删除角色", module = "角色管理")
public Mono<ServerResponse> deleteRole(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return roleService.logicalDeleteRole(id)
.flatMap(role -> ServerResponse.ok().bodyValue(role))
.switchIfEmpty(ServerResponse.notFound().build());
return verifyAdminPassword(request)
.flatMap(valid -> {
if (!valid) return ServerResponse.badRequest().bodyValue("管理员密码不能为空或错误");
if (BUILTIN_ROLE_ID.equals(id)) return ServerResponse.badRequest().bodyValue("超级管理员角色不可删除");
return roleService.logicalDeleteRole(id)
.flatMap(role -> ServerResponse.ok().bodyValue(role))
.switchIfEmpty(ServerResponse.notFound().build());
});
}
@Operation(summary = "恢复角色", description = "恢复被逻辑删除的角色")
@@ -148,4 +165,13 @@ public class SysRoleHandler {
.flatMap(role -> ServerResponse.ok().bodyValue(role))
.switchIfEmpty(ServerResponse.notFound().build());
}
private Mono<Boolean> verifyAdminPassword(ServerRequest request) {
String adminPassword = request.queryParam("adminPassword").orElse(null);
if (adminPassword == null || adminPassword.isBlank()) {
return Mono.just(false);
}
Long adminId = authUtil.getMemberIdOrThrow(request);
return userService.verifyPassword(adminId, adminPassword);
}
}
@@ -2,6 +2,7 @@ package cn.novalon.gym.manage.sys.handler.user;
import cn.novalon.gym.manage.sys.core.domain.SysUser;
import cn.novalon.gym.manage.sys.core.service.ISysUserService;
import cn.novalon.gym.manage.sys.util.AuthUtil;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.sys.dto.request.AssignRolesRequest;
import cn.novalon.gym.manage.sys.dto.request.PasswordChangeRequest;
@@ -42,10 +43,12 @@ public class SysUserHandler {
private static final Logger logger = LoggerFactory.getLogger(SysUserHandler.class);
private final ISysUserService userService;
private final Validator validator;
private final AuthUtil authUtil;
public SysUserHandler(ISysUserService userService, Validator validator) {
public SysUserHandler(ISysUserService userService, Validator validator, AuthUtil authUtil) {
this.userService = userService;
this.validator = validator;
this.authUtil = authUtil;
}
@Operation(summary = "获取所有用户", description = "获取系统中所有用户列表")
@@ -152,39 +155,63 @@ public class SysUserHandler {
});
}
@Operation(summary = "更新用户", description = "更新用户信息")
@Operation(summary = "更新用户", description = "更新用户信息需验证管理员密码超级管理员不可编辑")
@OperationLog(operation = "更新用户", module = "用户管理")
public Mono<ServerResponse> updateUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(UserUpdateRequest.class)
.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());
return verifyAdminPassword(request)
.flatMap(valid -> {
if (!valid) {
return ServerResponse.badRequest().bodyValue("管理员密码不能为空或错误");
}
return userService.findById(id)
.flatMap(user -> {
if ("admin".equals(user.getUsername())) {
return Mono.<ServerResponse>error(new RuntimeException("超级管理员不可编辑"));
}
return request.bodyToMono(UserUpdateRequest.class)
.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(updated -> ServerResponse.ok().bodyValue(updated));
})
.switchIfEmpty(ServerResponse.notFound().build());
});
}
@Operation(summary = "删除用户", description = "物理删除用户")
@Operation(summary = "删除用户", description = "物理删除用户需验证管理员密码超级管理员不可删除")
@OperationLog(operation = "删除用户", module = "用户管理")
public Mono<ServerResponse> deleteUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.findById(id)
.flatMap(user -> userService.deleteUser(id)
.then(ServerResponse.noContent().build()))
.switchIfEmpty(Mono.error(new RuntimeException("User not found")))
return verifyAdminPassword(request)
.flatMap(valid -> {
if (!valid) {
return ServerResponse.badRequest().bodyValue("管理员密码不能为空或错误");
}
return userService.findById(id)
.flatMap(user -> {
if ("admin".equals(user.getUsername())) {
return Mono.<ServerResponse>error(new RuntimeException("超级管理员不可删除"));
}
return userService.deleteUser(id)
.then(ServerResponse.noContent().build());
})
.switchIfEmpty(Mono.error(new RuntimeException("User not found")));
})
.onErrorResume(RuntimeException.class, ex -> {
if (ex.getMessage().contains("not found")) {
String msg = ex.getMessage();
if ("超级管理员不可删除".equals(msg) || "超级管理员不可编辑".equals(msg)) {
return ServerResponse.badRequest().bodyValue(msg);
}
if ("User not found".equals(msg)) {
return ServerResponse.notFound().build();
}
return Mono.error(ex);
@@ -258,16 +285,37 @@ public class SysUserHandler {
.flatMap(exists -> ServerResponse.ok().bodyValue(exists));
}
@Operation(summary = "为用户分配角色", description = "为指定用户分配角色列表")
@Operation(summary = "为用户分配角色", description = "为指定用户分配角色列表需验证管理员密码超级管理员不可分配")
@OperationLog(operation = "分配角色", module = "用户管理")
public Mono<ServerResponse> assignRoles(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(AssignRolesRequest.class)
.flatMap(req -> userService.assignRolesToUser(id, req.getRoleIdsAsLong()))
.then(ServerResponse.ok().build())
.onErrorResume(error -> {
logger.error("分配角色失败", error);
return ServerResponse.status(500).bodyValue("分配角色失败: " + error.getMessage());
return verifyAdminPassword(request)
.flatMap(valid -> {
if (!valid) {
return ServerResponse.badRequest().bodyValue("管理员密码不能为空或错误");
}
return userService.findById(id)
.flatMap(user -> {
if ("admin".equals(user.getUsername())) {
return Mono.<ServerResponse>error(new RuntimeException("超级管理员不可被分配角色"));
}
return request.bodyToMono(AssignRolesRequest.class)
.flatMap(req -> userService.assignRolesToUser(id, req.getRoleIdsAsLong()))
.then(ServerResponse.ok().build());
})
.switchIfEmpty(Mono.error(new RuntimeException("User not found")));
})
.onErrorResume(RuntimeException.class, ex -> {
String msg = ex.getMessage();
if ("超级管理员不可被分配角色".equals(msg)) {
return ServerResponse.badRequest().bodyValue(msg);
}
if ("User not found".equals(msg)) {
return ServerResponse.notFound().build();
}
logger.error("分配角色失败", ex);
return ServerResponse.status(500).bodyValue("分配角色失败: " + msg);
});
}
@@ -277,4 +325,16 @@ public class SysUserHandler {
return ServerResponse.ok()
.body(userService.getUserRoles(id), cn.novalon.gym.manage.sys.core.domain.SysRole.class);
}
/**
* 验证管理员密码
*/
private Mono<Boolean> verifyAdminPassword(ServerRequest request) {
String adminPassword = request.queryParam("adminPassword").orElse(null);
if (adminPassword == null || adminPassword.isBlank()) {
return Mono.just(false);
}
Long adminId = authUtil.getMemberIdOrThrow(request);
return userService.verifyPassword(adminId, adminPassword);
}
}