From a497deacfc122d181dd3ed90d74069b045f24c48 Mon Sep 17 00:00:00 2001 From: chenhao Date: Fri, 13 Feb 2026 11:06:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(log):=20=E5=AE=8C=E5=96=84=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E6=97=A5=E5=BF=97=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=A7=9F=E6=88=B7=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增租户ID字段到SysLog实体并调整日志记录逻辑 - 实现平台管理员跨租户查看日志的功能 - 重构登录日志记录方法,添加执行时长统计 - 优化JWT过滤器中的租户验证逻辑 - 调整MyBatis-Plus配置以支持布尔类型的逻辑删除 - 更新前端日志页面UI和数据展示逻辑 - 修复字典项和字典类型实体 --- .../auth/JwtAuthenticationFilter.java | 5 +- .../com/imeeting/common/aspect/LogAspect.java | 55 +++--- .../imeeting/config/MybatisPlusConfig.java | 6 +- .../imeeting/controller/SysLogController.java | 70 +++++--- .../imeeting/controller/UserController.java | 11 +- .../java/com/imeeting/entity/BaseEntity.java | 2 +- .../java/com/imeeting/entity/SysDictItem.java | 2 +- .../java/com/imeeting/entity/SysDictType.java | 2 +- .../main/java/com/imeeting/entity/SysLog.java | 20 ++- .../main/java/com/imeeting/entity/SysOrg.java | 2 - .../com/imeeting/entity/SysPermission.java | 18 +- .../java/com/imeeting/entity/SysTenant.java | 3 - .../java/com/imeeting/entity/SysUser.java | 1 - .../com/imeeting/mapper/SysLogMapper.java | 15 ++ .../com/imeeting/service/SysLogService.java | 4 + .../service/impl/AuthServiceImpl.java | 18 +- .../service/impl/SysLogServiceImpl.java | 7 + .../service/impl/SysOrgServiceImpl.java | 3 - .../impl/SysPermissionServiceImpl.java | 17 +- .../service/impl/SysUserServiceImpl.java | 4 +- backend/src/main/resources/application.yml | 1 + frontend/src/pages/Logs.tsx | 163 ++++++++++++------ frontend/src/types/index.ts | 16 ++ 23 files changed, 284 insertions(+), 161 deletions(-) diff --git a/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java b/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java index 35ccff5..31bd821 100644 --- a/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java +++ b/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java @@ -69,13 +69,14 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { // 1. Validate User Status (Ignore Tenant isolation here) SysUser user = sysUserMapper.selectByIdIgnoreTenant(userId); - if (user == null || user.getStatus() != 1 || user.getIsDeleted() == 1) { + if (user == null || user.getStatus() != 1 || user.getIsDeleted() ) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "User account is disabled or deleted"); return; } // 2. Validate Tenant Status & Grace Period - if (tenantId != null) { + // Skip validation for system platform tenant (ID=0) + if (tenantId != null && !Long.valueOf(0).equals(tenantId)) { SysTenant tenant = sysTenantMapper.selectByIdIgnoreTenant(tenantId); if (tenant == null || tenant.getStatus() != 1) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Tenant is disabled"); diff --git a/backend/src/main/java/com/imeeting/common/aspect/LogAspect.java b/backend/src/main/java/com/imeeting/common/aspect/LogAspect.java index 28b5870..80cf8ba 100644 --- a/backend/src/main/java/com/imeeting/common/aspect/LogAspect.java +++ b/backend/src/main/java/com/imeeting/common/aspect/LogAspect.java @@ -59,38 +59,28 @@ public class LogAspect { Log logAnnotation = method.getAnnotation(Log.class); SysLog sysLog = new SysLog(); - sysLog.setOperationType(request.getMethod()); - sysLog.setResourceType(logAnnotation.type().isEmpty() ? logAnnotation.value() : logAnnotation.type()); - - // Capture request parameters - String params = getArgsJson(joinPoint); - sysLog.setDetail(logAnnotation.value() + " | Params: " + params); - - sysLog.setIpAddress(request.getRemoteAddr()); - sysLog.setUserAgent(request.getHeader("User-Agent")); + sysLog.setLogType("OPERATION"); + sysLog.setOperation(logAnnotation.value()); + sysLog.setMethod(request.getMethod() + " " + request.getRequestURI()); + sysLog.setDuration(duration); + sysLog.setIp(request.getRemoteAddr()); sysLog.setCreatedAt(LocalDateTime.now()); - // Get Current User + // 仅保留请求参数,移除响应结果 + sysLog.setParams(getArgsJson(joinPoint)); + + // 获取当前租户和用户信息 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getPrincipal() instanceof LoginUser) { LoginUser user = (LoginUser) auth.getPrincipal(); sysLog.setUserId(user.getUserId()); + sysLog.setTenantId(user.getTenantId()); sysLog.setUsername(user.getUsername()); } - if (e != null) { - sysLog.setStatus(0); - sysLog.setErrorMessage(e.getMessage()); - } else { - sysLog.setStatus(1); - } - - // Record duration in detail or a separate field if we want - // sysLog.setDetail(sysLog.getDetail() + " (Time: " + duration + "ms)"); - + sysLog.setStatus(e != null ? 0 : 1); sysLogService.recordLog(sysLog); } catch (Exception ex) { - // Log the logging error to console only ex.printStackTrace(); } } @@ -98,19 +88,22 @@ public class LogAspect { private String getArgsJson(ProceedingJoinPoint joinPoint) { try { Object[] args = joinPoint.getArgs(); - if (args == null || args.length == 0) { - return "[]"; - } - Object[] filterArgs = new Object[args.length]; - for (int i = 0; i < args.length; i++) { - if (args[i] instanceof jakarta.servlet.ServletRequest - || args[i] instanceof jakarta.servlet.ServletResponse - || args[i] instanceof org.springframework.web.multipart.MultipartFile) { + if (args == null || args.length == 0) return null; + + StringBuilder sb = new StringBuilder(); + for (Object arg : args) { + if (arg instanceof jakarta.servlet.ServletRequest + || arg instanceof jakarta.servlet.ServletResponse + || arg instanceof org.springframework.web.multipart.MultipartFile) { continue; } - filterArgs[i] = args[i]; + try { + sb.append(objectMapper.writeValueAsString(arg)).append(" "); + } catch (Exception e) { + sb.append("[Unserializable Argument] "); + } } - return objectMapper.writeValueAsString(filterArgs); + return sb.toString().trim(); } catch (Exception e) { return "[Error capturing params]"; } diff --git a/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java b/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java index 0b26466..35eb545 100644 --- a/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java +++ b/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java @@ -48,12 +48,12 @@ public class MybatisPlusConfig { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getPrincipal() instanceof LoginUser) { LoginUser user = (LoginUser) auth.getPrincipal(); - if (Boolean.TRUE.equals(user.getIsPlatformAdmin())) { + if (Boolean.TRUE.equals(user.getIsPlatformAdmin()) && Long.valueOf(0).equals(user.getTenantId())) { return true; } } - return List.of("sys_tenant", "sys_dict_type", "sys_dict_item", "sys_log").contains(tableName.toLowerCase()); + return List.of("sys_tenant", "sys_permission", "sys_role_permission", "sys_user_role", "sys_dict_type", "sys_dict_item", "sys_param").contains(tableName.toLowerCase()); } })); return interceptor; @@ -67,7 +67,7 @@ public class MybatisPlusConfig { strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class); strictInsertFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class); strictInsertFill(metaObject, "status", () -> 1, Integer.class); - strictInsertFill(metaObject, "isDeleted", () -> 0, Integer.class); + strictInsertFill(metaObject, "isDeleted", () -> Boolean.FALSE, Boolean.class); } @Override diff --git a/backend/src/main/java/com/imeeting/controller/SysLogController.java b/backend/src/main/java/com/imeeting/controller/SysLogController.java index d251499..88bdf08 100644 --- a/backend/src/main/java/com/imeeting/controller/SysLogController.java +++ b/backend/src/main/java/com/imeeting/controller/SysLogController.java @@ -1,15 +1,17 @@ package com.imeeting.controller; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.imeeting.common.ApiResponse; import com.imeeting.entity.SysLog; +import com.imeeting.security.LoginUser; import com.imeeting.service.SysLogService; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; - @RestController @RequestMapping("/api/logs") public class SysLogController { @@ -21,39 +23,65 @@ public class SysLogController { @GetMapping @PreAuthorize("@ss.hasPermi('sys_log:list')") - public ApiResponse> list( + public ApiResponse> list( @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String username, - @RequestParam(required = false) String operationType, // LOGIN or others + @RequestParam(required = false) String logType, + @RequestParam(required = false) String operation, @RequestParam(required = false) Integer status, @RequestParam(required = false) String startDate, - @RequestParam(required = false) String endDate + @RequestParam(required = false) String endDate, + @RequestParam(required = false) String sortField, + @RequestParam(required = false) String sortOrder ) { - LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + LoginUser loginUser = (LoginUser) auth.getPrincipal(); + + // 判定平台管理员: isPlatformAdmin=true 且 tenantId=0 + boolean isPlatformAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0).equals(loginUser.getTenantId()); + + QueryWrapper query = new QueryWrapper<>(); + // 只有联表查询才需要前缀 'l.' + String prefix = isPlatformAdmin ? "l." : ""; - if (username != null && !username.isEmpty()) { - query.like(SysLog::getUsername, username); + if (logType != null && !logType.isEmpty()) { + query.eq(prefix + "log_type", logType); } - if (operationType != null && !operationType.isEmpty()) { - if ("LOGIN".equals(operationType)) { - query.eq(SysLog::getOperationType, "LOGIN"); - } else { - query.ne(SysLog::getOperationType, "LOGIN"); - } + if (username != null && !username.isEmpty()) { + query.like(prefix + "username", username); + } + if (operation != null && !operation.isEmpty()) { + query.like(prefix + "operation", operation); } if (status != null) { - query.eq(SysLog::getStatus, status); + query.eq(prefix + "status", status); } - // Simplified date range filtering if (startDate != null && !startDate.isEmpty()) { - query.ge(SysLog::getCreatedAt, startDate + " 00:00:00"); + query.ge(prefix + "created_at", startDate + " 00:00:00"); } if (endDate != null && !endDate.isEmpty()) { - query.le(SysLog::getCreatedAt, endDate + " 23:59:59"); + query.le(prefix + "created_at", endDate + " 23:59:59"); } - query.orderByDesc(SysLog::getCreatedAt); - return ApiResponse.ok(sysLogService.page(new Page<>(current, size), query)); + // 动态排序逻辑 + if (sortField != null && !sortField.isEmpty()) { + String column = "created_at"; + if ("duration".equals(sortField)) column = "duration"; + + if ("ascend".equals(sortOrder)) { + query.orderByAsc(prefix + column); + } else { + query.orderByDesc(prefix + column); + } + } else { + query.orderByDesc(prefix + "created_at"); + } + + if (isPlatformAdmin) { + return ApiResponse.ok(sysLogService.selectPageWithTenant(new Page<>(current, size), query)); + } else { + return ApiResponse.ok(sysLogService.page(new Page<>(current, size), query)); + } } } diff --git a/backend/src/main/java/com/imeeting/controller/UserController.java b/backend/src/main/java/com/imeeting/controller/UserController.java index c6c8b1b..9519f86 100644 --- a/backend/src/main/java/com/imeeting/controller/UserController.java +++ b/backend/src/main/java/com/imeeting/controller/UserController.java @@ -10,6 +10,7 @@ import com.imeeting.entity.SysUser; import com.imeeting.entity.SysUserRole; import com.imeeting.mapper.SysUserRoleMapper; import com.imeeting.service.SysUserService; +import com.imeeting.common.annotation.Log; import io.jsonwebtoken.Claims; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; @@ -39,13 +40,9 @@ public class UserController { @PreAuthorize("@ss.hasPermi('sys_user:list')") public ApiResponse> list(@RequestParam(required = false) Long tenantId, @RequestParam(required = false) Long orgId) { LambdaQueryWrapper query = new LambdaQueryWrapper<>(); - if (tenantId != null) { - query.eq(SysUser::getTenantId, tenantId); - } if (orgId != null) { query.eq(SysUser::getOrgId, orgId); } - query.eq(SysUser::getIsDeleted, 0); return ApiResponse.ok(sysUserService.list(query)); } @@ -82,7 +79,7 @@ public class UserController { @PostMapping @PreAuthorize("@ss.hasPermi('sys_user:create')") - @com.imeeting.common.annotation.Log(value = "新增用户", type = "用户管理") + @Log(value = "新增用户", type = "用户管理") public ApiResponse create(@RequestBody SysUser user) { if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) { user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash())); @@ -92,7 +89,7 @@ public class UserController { @PutMapping("/{id}") @PreAuthorize("@ss.hasPermi('sys_user:update')") - @com.imeeting.common.annotation.Log(value = "修改用户", type = "用户管理") + @Log(value = "修改用户", type = "用户管理") public ApiResponse update(@PathVariable Long id, @RequestBody SysUser user) { user.setUserId(id); if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) { @@ -103,7 +100,7 @@ public class UserController { @DeleteMapping("/{id}") @PreAuthorize("@ss.hasPermi('sys_user:delete')") - @com.imeeting.common.annotation.Log(value = "删除用户", type = "用户管理") + @Log(value = "删除用户", type = "用户管理") public ApiResponse delete(@PathVariable Long id) { return ApiResponse.ok(sysUserService.removeById(id)); } diff --git a/backend/src/main/java/com/imeeting/entity/BaseEntity.java b/backend/src/main/java/com/imeeting/entity/BaseEntity.java index 252193f..7d475e7 100644 --- a/backend/src/main/java/com/imeeting/entity/BaseEntity.java +++ b/backend/src/main/java/com/imeeting/entity/BaseEntity.java @@ -13,7 +13,7 @@ public class BaseEntity { private Integer status; @TableLogic - private Integer isDeleted; + private Boolean isDeleted; @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; diff --git a/backend/src/main/java/com/imeeting/entity/SysDictItem.java b/backend/src/main/java/com/imeeting/entity/SysDictItem.java index 47da67b..ad895e4 100644 --- a/backend/src/main/java/com/imeeting/entity/SysDictItem.java +++ b/backend/src/main/java/com/imeeting/entity/SysDictItem.java @@ -23,5 +23,5 @@ public class SysDictItem extends BaseEntity { private Long tenantId; @TableField(exist = false) - private Integer isDeleted; + private Boolean isDeleted; } diff --git a/backend/src/main/java/com/imeeting/entity/SysDictType.java b/backend/src/main/java/com/imeeting/entity/SysDictType.java index 9600d2e..b8245c5 100644 --- a/backend/src/main/java/com/imeeting/entity/SysDictType.java +++ b/backend/src/main/java/com/imeeting/entity/SysDictType.java @@ -21,5 +21,5 @@ public class SysDictType extends BaseEntity { private Long tenantId; @TableField(exist = false) - private Integer isDeleted; + private Boolean isDeleted; } diff --git a/backend/src/main/java/com/imeeting/entity/SysLog.java b/backend/src/main/java/com/imeeting/entity/SysLog.java index 73307ab..a0c31d7 100644 --- a/backend/src/main/java/com/imeeting/entity/SysLog.java +++ b/backend/src/main/java/com/imeeting/entity/SysLog.java @@ -1,6 +1,7 @@ package com.imeeting.entity; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -11,15 +12,18 @@ import java.time.LocalDateTime; public class SysLog { @TableId(type = IdType.AUTO) private Long id; + private Long tenantId; private Long userId; private String username; - private String operationType; // LOGIN, LOGOUT, CREATE, UPDATE, DELETE, QUERY - private String resourceType; // 所属模块/资源 - private Long resourceId; - private String detail; // 操作详情(可以是 JSON) - private String ipAddress; - private String userAgent; - private Integer status; // 1-成功, 0-失败 - private String errorMessage; + private String logType; // LOGIN, OPERATION + private String operation; + private String method; + private String params; + private Integer status; + private String ip; + private Long duration; private LocalDateTime createdAt; + + @TableField(exist = false) + private String tenantName; } diff --git a/backend/src/main/java/com/imeeting/entity/SysOrg.java b/backend/src/main/java/com/imeeting/entity/SysOrg.java index d75c356..603048d 100644 --- a/backend/src/main/java/com/imeeting/entity/SysOrg.java +++ b/backend/src/main/java/com/imeeting/entity/SysOrg.java @@ -21,6 +21,4 @@ public class SysOrg extends BaseEntity { private String orgPath; private Integer sortOrder; - @TableField(exist = false) - private Integer isDeleted; } diff --git a/backend/src/main/java/com/imeeting/entity/SysPermission.java b/backend/src/main/java/com/imeeting/entity/SysPermission.java index ca9eeb7..a5b0d4c 100644 --- a/backend/src/main/java/com/imeeting/entity/SysPermission.java +++ b/backend/src/main/java/com/imeeting/entity/SysPermission.java @@ -1,13 +1,13 @@ package com.imeeting.entity; -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.*; import lombok.Data; +import java.time.LocalDateTime; + @Data @TableName("sys_permission") -public class SysPermission extends BaseEntity { +public class SysPermission { @TableId(value = "perm_id", type = IdType.AUTO) private Long permId; private Long parentId; @@ -22,4 +22,14 @@ public class SysPermission extends BaseEntity { private Integer isVisible; private String description; private String meta; + private Integer status; + + @TableLogic + private Boolean isDeleted; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createdAt; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedAt; } diff --git a/backend/src/main/java/com/imeeting/entity/SysTenant.java b/backend/src/main/java/com/imeeting/entity/SysTenant.java index a8e56a4..c6ff789 100644 --- a/backend/src/main/java/com/imeeting/entity/SysTenant.java +++ b/backend/src/main/java/com/imeeting/entity/SysTenant.java @@ -24,7 +24,4 @@ public class SysTenant extends BaseEntity { @TableField(exist = false) private Long tenantId; - - @TableField(exist = false) - private Integer isDeleted; } diff --git a/backend/src/main/java/com/imeeting/entity/SysUser.java b/backend/src/main/java/com/imeeting/entity/SysUser.java index fa3be0f..2eec747 100644 --- a/backend/src/main/java/com/imeeting/entity/SysUser.java +++ b/backend/src/main/java/com/imeeting/entity/SysUser.java @@ -16,7 +16,6 @@ public class SysUser extends BaseEntity { private String phone; private String passwordHash; - private Long tenantId; private Long orgId; private Boolean isPlatformAdmin; } diff --git a/backend/src/main/java/com/imeeting/mapper/SysLogMapper.java b/backend/src/main/java/com/imeeting/mapper/SysLogMapper.java index 319ecd8..f175016 100644 --- a/backend/src/main/java/com/imeeting/mapper/SysLogMapper.java +++ b/backend/src/main/java/com/imeeting/mapper/SysLogMapper.java @@ -1,9 +1,24 @@ package com.imeeting.mapper; +import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; import com.imeeting.entity.SysLog; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; @Mapper public interface SysLogMapper extends BaseMapper { + + @Override + @InterceptorIgnore(tenantLine = "true") + int insert(SysLog entity); + + @Select("SELECT l.*, t.tenant_name FROM sys_log l " + + "LEFT JOIN sys_tenant t ON l.tenant_id = t.id " + + "${ew.customSqlSegment}") + IPage selectPageWithTenant(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper); } diff --git a/backend/src/main/java/com/imeeting/service/SysLogService.java b/backend/src/main/java/com/imeeting/service/SysLogService.java index 55f0ad0..dc7525a 100644 --- a/backend/src/main/java/com/imeeting/service/SysLogService.java +++ b/backend/src/main/java/com/imeeting/service/SysLogService.java @@ -1,8 +1,12 @@ package com.imeeting.service; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; import com.imeeting.entity.SysLog; public interface SysLogService extends IService { void recordLog(SysLog log); + + IPage selectPageWithTenant(IPage page, Wrapper queryWrapper); } diff --git a/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java index 5f6ad8f..003d4e5 100644 --- a/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java @@ -65,6 +65,7 @@ public class AuthServiceImpl implements AuthService { @Override public TokenResponse login(LoginRequest request) { + long start = System.currentTimeMillis(); try { if (isCaptchaEnabled()) { validateCaptcha(request.getCaptchaId(), request.getCaptchaCode()); @@ -109,24 +110,25 @@ public class AuthServiceImpl implements AuthService { TokenResponse tokens = issueTokens(user, deviceCode, accessMinutes, refreshDays); cacheRefreshToken(user.getUserId(), deviceCode, tokens.getRefreshToken(), refreshDays); - recordLoginLog(user.getUserId(), user.getUsername(), 1, "登录成功"); + recordLoginLog(user.getUserId(), user.getTenantId(), user.getUsername(), 1, "登录成功", System.currentTimeMillis() - start); return tokens; } catch (Exception e) { - recordLoginLog(null, request.getUsername(), 0, e.getMessage()); + recordLoginLog(null, null, request.getUsername(), 0, e.getMessage(), System.currentTimeMillis() - start); throw e; } } - private void recordLoginLog(Long userId, String username, Integer status, String msg) { + private void recordLoginLog(Long userId, Long tenantId, String username, Integer status, String msg, long duration) { SysLog sysLog = new SysLog(); sysLog.setUserId(userId); + sysLog.setTenantId(tenantId); sysLog.setUsername(username); - sysLog.setOperationType("LOGIN"); - sysLog.setResourceType("认证模块"); - sysLog.setDetail(msg); + sysLog.setLogType("LOGIN"); + sysLog.setOperation("用户登录: " + username); + sysLog.setMethod("POST /api/auth/login"); + sysLog.setDuration(duration); sysLog.setStatus(status); - sysLog.setIpAddress(httpServletRequest.getRemoteAddr()); - sysLog.setUserAgent(httpServletRequest.getHeader("User-Agent")); + sysLog.setIp(httpServletRequest.getRemoteAddr()); sysLog.setCreatedAt(LocalDateTime.now()); sysLogService.recordLog(sysLog); } diff --git a/backend/src/main/java/com/imeeting/service/impl/SysLogServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysLogServiceImpl.java index 35004c2..6f3929d 100644 --- a/backend/src/main/java/com/imeeting/service/impl/SysLogServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/impl/SysLogServiceImpl.java @@ -1,5 +1,7 @@ package com.imeeting.service.impl; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.imeeting.entity.SysLog; import com.imeeting.mapper.SysLogMapper; @@ -15,4 +17,9 @@ public class SysLogServiceImpl extends ServiceImpl impleme public void recordLog(SysLog log) { save(log); } + + @Override + public IPage selectPageWithTenant(IPage page, Wrapper queryWrapper) { + return baseMapper.selectPageWithTenant(page, queryWrapper); + } } diff --git a/backend/src/main/java/com/imeeting/service/impl/SysOrgServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysOrgServiceImpl.java index 28d66d0..3537dbc 100644 --- a/backend/src/main/java/com/imeeting/service/impl/SysOrgServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/impl/SysOrgServiceImpl.java @@ -13,9 +13,6 @@ public class SysOrgServiceImpl extends ServiceImpl impleme @Override public List listTree(Long tenantId) { LambdaQueryWrapper query = new LambdaQueryWrapper<>(); - if (tenantId != null) { - query.eq(SysOrg::getTenantId, tenantId); - } query.orderByAsc(SysOrg::getSortOrder); return list(query); } diff --git a/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java index ab22258..d92b8ee 100644 --- a/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java @@ -27,14 +27,19 @@ public class SysPermissionServiceImpl extends ServiceImpl impl if (user.getUsername() == null) return; LambdaQueryWrapper query = new LambdaQueryWrapper() - .eq(SysUser::getUsername, user.getUsername()) - .eq(SysUser::getTenantId, user.getTenantId()) - .eq(SysUser::getIsDeleted, 0); + .eq(SysUser::getUsername, user.getUsername()); if (user.getUserId() != null) { query.ne(SysUser::getUserId, user.getUserId()); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index b6134ce..aaab273 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -18,6 +18,7 @@ spring: mybatis-plus: configuration: map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: isDeleted diff --git a/frontend/src/pages/Logs.tsx b/frontend/src/pages/Logs.tsx index c916094..f584ddb 100644 --- a/frontend/src/pages/Logs.tsx +++ b/frontend/src/pages/Logs.tsx @@ -1,7 +1,8 @@ import { Card, Table, Tabs, Tag, Input, Space, Button, DatePicker, Select, Typography, Modal, Descriptions } from "antd"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { fetchLogs } from "../api"; -import { SearchOutlined, ReloadOutlined, InfoCircleOutlined, EyeOutlined } from "@ant-design/icons"; +import { SearchOutlined, ReloadOutlined, InfoCircleOutlined, EyeOutlined, UserOutlined } from "@ant-design/icons"; +import { SysLog, UserProfile } from "../types"; import dayjs from "dayjs"; const { RangePicker } = DatePicker; @@ -10,7 +11,7 @@ const { Text, Title } = Typography; export default function Logs() { const [activeTab, setActiveTab] = useState("OPERATION"); const [loading, setLoading] = useState(false); - const [data, setData] = useState([]); + const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [params, setParams] = useState({ current: 1, @@ -18,18 +19,34 @@ export default function Logs() { username: "", status: undefined, startDate: "", - endDate: "" + endDate: "", + operation: "", + sortField: "createdAt", + sortOrder: "descend" as any }); + // Get user profile to check platform admin + const userProfile = useMemo(() => { + const stored = sessionStorage.getItem("userProfile"); + if (!stored) return null; + try { + return JSON.parse(stored) as UserProfile; + } catch (e) { + return null; + } + }, []); + + const isPlatformAdmin = userProfile?.isPlatformAdmin && userProfile?.tenantId === 0; + // Modal for detail view const [detailModalVisible, setDetailModalVisible] = useState(false); - const [selectedLog, setSelectedLog] = useState(null); + const [selectedLog, setSelectedLog] = useState(null); const loadData = async (currentParams = params) => { setLoading(true); try { - const operationType = activeTab === "LOGIN" ? "LOGIN" : "OPERATION"; - const result = await fetchLogs({ ...currentParams, operationType }); + // Use logType for precise filtering + const result = await fetchLogs({ ...currentParams, logType: activeTab }); setData(result.records || []); setTotal(result.total || 0); } finally { @@ -39,7 +56,17 @@ export default function Logs() { useEffect(() => { loadData(); - }, [activeTab, params.current, params.size]); + }, [activeTab, params.current, params.size, params.sortField, params.sortOrder]); + + const handleTableChange = (pagination: any, filters: any, sorter: any) => { + setParams({ + ...params, + current: pagination.current, + size: pagination.pageSize, + sortField: sorter.field || "createdAt", + sortOrder: sorter.order || "descend" + }); + }; const handleSearch = () => { setParams({ ...params, current: 1 }); @@ -53,50 +80,76 @@ export default function Logs() { username: "", status: undefined, startDate: "", - endDate: "" + endDate: "", + operation: "", + sortField: "createdAt", + sortOrder: "descend" as any }; setParams(resetParams); loadData(resetParams); }; - const showDetail = (record: any) => { + const showDetail = (record: SysLog) => { setSelectedLog(record); setDetailModalVisible(true); }; + const renderDuration = (ms: number) => { + if (!ms && ms !== 0) return "-"; + let color = ""; + if (ms > 1000) color = "#ff4d4f"; // 红色 (慢) + else if (ms > 300) color = "#faad14"; // 橘色 (中) + + return ( + 300 ? 600 : 400 }}> + {ms}ms + + ); + }; + const columns = [ + ...(isPlatformAdmin ? [{ + title: "所属租户", + dataIndex: "tenantName", + key: "tenantName", + width: 150, + render: (text: string) => {text || "系统平台"} + }] : []), { title: "操作账号", dataIndex: "username", key: "username", - width: 140, - render: (text: string) => {text || "系统/访客"} + width: 120, + render: (text: string) => {text || "系统"} }, { - title: activeTab === "LOGIN" ? "登录模块" : "业务模块", - dataIndex: "resourceType", - key: "resourceType", - width: 150 - }, - { - title: "操作描述", - dataIndex: "detail", - key: "detail", + title: "操作详情", + dataIndex: "operation", + key: "operation", ellipsis: true, render: (text: string) => {text} }, { title: "IP 地址", - dataIndex: "ipAddress", - key: "ipAddress", - width: 140, + dataIndex: "ip", + key: "ip", + width: 130, className: "tabular-nums" }, + { + title: "耗时", + dataIndex: "duration", + key: "duration", + width: 100, + sorter: true, + sortOrder: params.sortField === 'duration' ? params.sortOrder : null, + render: renderDuration + }, { title: "状态", dataIndex: "status", key: "status", - width: 100, + width: 90, render: (status: number) => ( {status === 1 ? "成功" : "失败"} @@ -108,15 +161,17 @@ export default function Logs() { dataIndex: "createdAt", key: "createdAt", width: 180, + sorter: true, + sortOrder: params.sortField === 'createdAt' ? params.sortOrder : null, className: "tabular-nums", render: (text: string) => text?.replace('T', ' ').substring(0, 19) }, { title: "详情", key: "action", - width: 80, + width: 60, fixed: "right" as const, - render: (_: any, record: any) => ( + render: (_: any, record: SysLog) => (