feat(log): 完善系统日志功能并优化租户权限控制

- 新增租户ID字段到SysLog实体并调整日志记录逻辑
- 实现平台管理员跨租户查看日志的功能
- 重构登录日志记录方法,添加执行时长统计
- 优化JWT过滤器中的租户验证逻辑
- 调整MyBatis-Plus配置以支持布尔类型的逻辑删除
- 更新前端日志页面UI和数据展示逻辑
- 修复字典项和字典类型实体
master
chenhao 2026-02-13 11:06:36 +08:00
parent 69dc3e6788
commit a497deacfc
23 changed files with 284 additions and 161 deletions

View File

@ -69,13 +69,14 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 1. Validate User Status (Ignore Tenant isolation here) // 1. Validate User Status (Ignore Tenant isolation here)
SysUser user = sysUserMapper.selectByIdIgnoreTenant(userId); 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"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "User account is disabled or deleted");
return; return;
} }
// 2. Validate Tenant Status & Grace Period // 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); SysTenant tenant = sysTenantMapper.selectByIdIgnoreTenant(tenantId);
if (tenant == null || tenant.getStatus() != 1) { if (tenant == null || tenant.getStatus() != 1) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Tenant is disabled"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Tenant is disabled");

View File

@ -59,38 +59,28 @@ public class LogAspect {
Log logAnnotation = method.getAnnotation(Log.class); Log logAnnotation = method.getAnnotation(Log.class);
SysLog sysLog = new SysLog(); SysLog sysLog = new SysLog();
sysLog.setOperationType(request.getMethod()); sysLog.setLogType("OPERATION");
sysLog.setResourceType(logAnnotation.type().isEmpty() ? logAnnotation.value() : logAnnotation.type()); sysLog.setOperation(logAnnotation.value());
sysLog.setMethod(request.getMethod() + " " + request.getRequestURI());
// Capture request parameters sysLog.setDuration(duration);
String params = getArgsJson(joinPoint); sysLog.setIp(request.getRemoteAddr());
sysLog.setDetail(logAnnotation.value() + " | Params: " + params);
sysLog.setIpAddress(request.getRemoteAddr());
sysLog.setUserAgent(request.getHeader("User-Agent"));
sysLog.setCreatedAt(LocalDateTime.now()); sysLog.setCreatedAt(LocalDateTime.now());
// Get Current User // 仅保留请求参数,移除响应结果
sysLog.setParams(getArgsJson(joinPoint));
// 获取当前租户和用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof LoginUser) { if (auth != null && auth.getPrincipal() instanceof LoginUser) {
LoginUser user = (LoginUser) auth.getPrincipal(); LoginUser user = (LoginUser) auth.getPrincipal();
sysLog.setUserId(user.getUserId()); sysLog.setUserId(user.getUserId());
sysLog.setTenantId(user.getTenantId());
sysLog.setUsername(user.getUsername()); sysLog.setUsername(user.getUsername());
} }
if (e != null) { sysLog.setStatus(e != null ? 0 : 1);
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)");
sysLogService.recordLog(sysLog); sysLogService.recordLog(sysLog);
} catch (Exception ex) { } catch (Exception ex) {
// Log the logging error to console only
ex.printStackTrace(); ex.printStackTrace();
} }
} }
@ -98,19 +88,22 @@ public class LogAspect {
private String getArgsJson(ProceedingJoinPoint joinPoint) { private String getArgsJson(ProceedingJoinPoint joinPoint) {
try { try {
Object[] args = joinPoint.getArgs(); Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) { if (args == null || args.length == 0) return null;
return "[]";
} StringBuilder sb = new StringBuilder();
Object[] filterArgs = new Object[args.length]; for (Object arg : args) {
for (int i = 0; i < args.length; i++) { if (arg instanceof jakarta.servlet.ServletRequest
if (args[i] instanceof jakarta.servlet.ServletRequest || arg instanceof jakarta.servlet.ServletResponse
|| args[i] instanceof jakarta.servlet.ServletResponse || arg instanceof org.springframework.web.multipart.MultipartFile) {
|| args[i] instanceof org.springframework.web.multipart.MultipartFile) {
continue; 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) { } catch (Exception e) {
return "[Error capturing params]"; return "[Error capturing params]";
} }

View File

@ -48,12 +48,12 @@ public class MybatisPlusConfig {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof LoginUser) { if (auth != null && auth.getPrincipal() instanceof LoginUser) {
LoginUser user = (LoginUser) auth.getPrincipal(); 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 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; return interceptor;
@ -67,7 +67,7 @@ public class MybatisPlusConfig {
strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class); strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class);
strictInsertFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class); strictInsertFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
strictInsertFill(metaObject, "status", () -> 1, Integer.class); strictInsertFill(metaObject, "status", () -> 1, Integer.class);
strictInsertFill(metaObject, "isDeleted", () -> 0, Integer.class); strictInsertFill(metaObject, "isDeleted", () -> Boolean.FALSE, Boolean.class);
} }
@Override @Override

View File

@ -1,15 +1,17 @@
package com.imeeting.controller; 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.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imeeting.common.ApiResponse; import com.imeeting.common.ApiResponse;
import com.imeeting.entity.SysLog; import com.imeeting.entity.SysLog;
import com.imeeting.security.LoginUser;
import com.imeeting.service.SysLogService; import com.imeeting.service.SysLogService;
import org.springframework.security.access.prepost.PreAuthorize; 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 org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
@RestController @RestController
@RequestMapping("/api/logs") @RequestMapping("/api/logs")
public class SysLogController { public class SysLogController {
@ -21,39 +23,65 @@ public class SysLogController {
@GetMapping @GetMapping
@PreAuthorize("@ss.hasPermi('sys_log:list')") @PreAuthorize("@ss.hasPermi('sys_log:list')")
public ApiResponse<Page<SysLog>> list( public ApiResponse<IPage<SysLog>> list(
@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size, @RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String username, @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) Integer status,
@RequestParam(required = false) String startDate, @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<SysLog> query = new LambdaQueryWrapper<>(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) auth.getPrincipal();
if (username != null && !username.isEmpty()) { // 判定平台管理员: isPlatformAdmin=true 且 tenantId=0
query.like(SysLog::getUsername, username); boolean isPlatformAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0).equals(loginUser.getTenantId());
QueryWrapper<SysLog> query = new QueryWrapper<>();
// 只有联表查询才需要前缀 'l.'
String prefix = isPlatformAdmin ? "l." : "";
if (logType != null && !logType.isEmpty()) {
query.eq(prefix + "log_type", logType);
} }
if (operationType != null && !operationType.isEmpty()) { if (username != null && !username.isEmpty()) {
if ("LOGIN".equals(operationType)) { query.like(prefix + "username", username);
query.eq(SysLog::getOperationType, "LOGIN"); }
} else { if (operation != null && !operation.isEmpty()) {
query.ne(SysLog::getOperationType, "LOGIN"); query.like(prefix + "operation", operation);
}
} }
if (status != null) { if (status != null) {
query.eq(SysLog::getStatus, status); query.eq(prefix + "status", status);
} }
// Simplified date range filtering
if (startDate != null && !startDate.isEmpty()) { 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()) { 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));
}
} }
} }

View File

@ -10,6 +10,7 @@ import com.imeeting.entity.SysUser;
import com.imeeting.entity.SysUserRole; import com.imeeting.entity.SysUserRole;
import com.imeeting.mapper.SysUserRoleMapper; import com.imeeting.mapper.SysUserRoleMapper;
import com.imeeting.service.SysUserService; import com.imeeting.service.SysUserService;
import com.imeeting.common.annotation.Log;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -39,13 +40,9 @@ public class UserController {
@PreAuthorize("@ss.hasPermi('sys_user:list')") @PreAuthorize("@ss.hasPermi('sys_user:list')")
public ApiResponse<List<SysUser>> list(@RequestParam(required = false) Long tenantId, @RequestParam(required = false) Long orgId) { public ApiResponse<List<SysUser>> list(@RequestParam(required = false) Long tenantId, @RequestParam(required = false) Long orgId) {
LambdaQueryWrapper<SysUser> query = new LambdaQueryWrapper<>(); LambdaQueryWrapper<SysUser> query = new LambdaQueryWrapper<>();
if (tenantId != null) {
query.eq(SysUser::getTenantId, tenantId);
}
if (orgId != null) { if (orgId != null) {
query.eq(SysUser::getOrgId, orgId); query.eq(SysUser::getOrgId, orgId);
} }
query.eq(SysUser::getIsDeleted, 0);
return ApiResponse.ok(sysUserService.list(query)); return ApiResponse.ok(sysUserService.list(query));
} }
@ -82,7 +79,7 @@ public class UserController {
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermi('sys_user:create')") @PreAuthorize("@ss.hasPermi('sys_user:create')")
@com.imeeting.common.annotation.Log(value = "新增用户", type = "用户管理") @Log(value = "新增用户", type = "用户管理")
public ApiResponse<Boolean> create(@RequestBody SysUser user) { public ApiResponse<Boolean> create(@RequestBody SysUser user) {
if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) { if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) {
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash())); user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
@ -92,7 +89,7 @@ public class UserController {
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_user:update')") @PreAuthorize("@ss.hasPermi('sys_user:update')")
@com.imeeting.common.annotation.Log(value = "修改用户", type = "用户管理") @Log(value = "修改用户", type = "用户管理")
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysUser user) { public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysUser user) {
user.setUserId(id); user.setUserId(id);
if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) { if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) {
@ -103,7 +100,7 @@ public class UserController {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermi('sys_user:delete')") @PreAuthorize("@ss.hasPermi('sys_user:delete')")
@com.imeeting.common.annotation.Log(value = "删除用户", type = "用户管理") @Log(value = "删除用户", type = "用户管理")
public ApiResponse<Boolean> delete(@PathVariable Long id) { public ApiResponse<Boolean> delete(@PathVariable Long id) {
return ApiResponse.ok(sysUserService.removeById(id)); return ApiResponse.ok(sysUserService.removeById(id));
} }

View File

@ -13,7 +13,7 @@ public class BaseEntity {
private Integer status; private Integer status;
@TableLogic @TableLogic
private Integer isDeleted; private Boolean isDeleted;
@TableField(fill = FieldFill.INSERT) @TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt; private LocalDateTime createdAt;

View File

@ -23,5 +23,5 @@ public class SysDictItem extends BaseEntity {
private Long tenantId; private Long tenantId;
@TableField(exist = false) @TableField(exist = false)
private Integer isDeleted; private Boolean isDeleted;
} }

View File

@ -21,5 +21,5 @@ public class SysDictType extends BaseEntity {
private Long tenantId; private Long tenantId;
@TableField(exist = false) @TableField(exist = false)
private Integer isDeleted; private Boolean isDeleted;
} }

View File

@ -1,6 +1,7 @@
package com.imeeting.entity; package com.imeeting.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
@ -11,15 +12,18 @@ import java.time.LocalDateTime;
public class SysLog { public class SysLog {
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private Long id; private Long id;
private Long tenantId;
private Long userId; private Long userId;
private String username; private String username;
private String operationType; // LOGIN, LOGOUT, CREATE, UPDATE, DELETE, QUERY private String logType; // LOGIN, OPERATION
private String resourceType; // 所属模块/资源 private String operation;
private Long resourceId; private String method;
private String detail; // 操作详情(可以是 JSON private String params;
private String ipAddress; private Integer status;
private String userAgent; private String ip;
private Integer status; // 1-成功, 0-失败 private Long duration;
private String errorMessage;
private LocalDateTime createdAt; private LocalDateTime createdAt;
@TableField(exist = false)
private String tenantName;
} }

View File

@ -21,6 +21,4 @@ public class SysOrg extends BaseEntity {
private String orgPath; private String orgPath;
private Integer sortOrder; private Integer sortOrder;
@TableField(exist = false)
private Integer isDeleted;
} }

View File

@ -1,13 +1,13 @@
package com.imeeting.entity; package com.imeeting.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
@Data @Data
@TableName("sys_permission") @TableName("sys_permission")
public class SysPermission extends BaseEntity { public class SysPermission {
@TableId(value = "perm_id", type = IdType.AUTO) @TableId(value = "perm_id", type = IdType.AUTO)
private Long permId; private Long permId;
private Long parentId; private Long parentId;
@ -22,4 +22,14 @@ public class SysPermission extends BaseEntity {
private Integer isVisible; private Integer isVisible;
private String description; private String description;
private String meta; 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;
} }

View File

@ -24,7 +24,4 @@ public class SysTenant extends BaseEntity {
@TableField(exist = false) @TableField(exist = false)
private Long tenantId; private Long tenantId;
@TableField(exist = false)
private Integer isDeleted;
} }

View File

@ -16,7 +16,6 @@ public class SysUser extends BaseEntity {
private String phone; private String phone;
private String passwordHash; private String passwordHash;
private Long tenantId;
private Long orgId; private Long orgId;
private Boolean isPlatformAdmin; private Boolean isPlatformAdmin;
} }

View File

@ -1,9 +1,24 @@
package com.imeeting.mapper; package com.imeeting.mapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; 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 com.imeeting.entity.SysLog;
import org.apache.ibatis.annotations.Mapper; 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 @Mapper
public interface SysLogMapper extends BaseMapper<SysLog> { public interface SysLogMapper extends BaseMapper<SysLog> {
@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<SysLog> selectPageWithTenant(IPage<SysLog> page, @Param(Constants.WRAPPER) Wrapper<SysLog> queryWrapper);
} }

View File

@ -1,8 +1,12 @@
package com.imeeting.service; 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.baomidou.mybatisplus.extension.service.IService;
import com.imeeting.entity.SysLog; import com.imeeting.entity.SysLog;
public interface SysLogService extends IService<SysLog> { public interface SysLogService extends IService<SysLog> {
void recordLog(SysLog log); void recordLog(SysLog log);
IPage<SysLog> selectPageWithTenant(IPage<SysLog> page, Wrapper<SysLog> queryWrapper);
} }

View File

@ -65,6 +65,7 @@ public class AuthServiceImpl implements AuthService {
@Override @Override
public TokenResponse login(LoginRequest request) { public TokenResponse login(LoginRequest request) {
long start = System.currentTimeMillis();
try { try {
if (isCaptchaEnabled()) { if (isCaptchaEnabled()) {
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode()); validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
@ -109,24 +110,25 @@ public class AuthServiceImpl implements AuthService {
TokenResponse tokens = issueTokens(user, deviceCode, accessMinutes, refreshDays); TokenResponse tokens = issueTokens(user, deviceCode, accessMinutes, refreshDays);
cacheRefreshToken(user.getUserId(), deviceCode, tokens.getRefreshToken(), 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; return tokens;
} catch (Exception e) { } catch (Exception e) {
recordLoginLog(null, request.getUsername(), 0, e.getMessage()); recordLoginLog(null, null, request.getUsername(), 0, e.getMessage(), System.currentTimeMillis() - start);
throw e; 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 sysLog = new SysLog();
sysLog.setUserId(userId); sysLog.setUserId(userId);
sysLog.setTenantId(tenantId);
sysLog.setUsername(username); sysLog.setUsername(username);
sysLog.setOperationType("LOGIN"); sysLog.setLogType("LOGIN");
sysLog.setResourceType("认证模块"); sysLog.setOperation("用户登录: " + username);
sysLog.setDetail(msg); sysLog.setMethod("POST /api/auth/login");
sysLog.setDuration(duration);
sysLog.setStatus(status); sysLog.setStatus(status);
sysLog.setIpAddress(httpServletRequest.getRemoteAddr()); sysLog.setIp(httpServletRequest.getRemoteAddr());
sysLog.setUserAgent(httpServletRequest.getHeader("User-Agent"));
sysLog.setCreatedAt(LocalDateTime.now()); sysLog.setCreatedAt(LocalDateTime.now());
sysLogService.recordLog(sysLog); sysLogService.recordLog(sysLog);
} }

View File

@ -1,5 +1,7 @@
package com.imeeting.service.impl; 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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.imeeting.entity.SysLog; import com.imeeting.entity.SysLog;
import com.imeeting.mapper.SysLogMapper; import com.imeeting.mapper.SysLogMapper;
@ -15,4 +17,9 @@ public class SysLogServiceImpl extends ServiceImpl<SysLogMapper, SysLog> impleme
public void recordLog(SysLog log) { public void recordLog(SysLog log) {
save(log); save(log);
} }
@Override
public IPage<SysLog> selectPageWithTenant(IPage<SysLog> page, Wrapper<SysLog> queryWrapper) {
return baseMapper.selectPageWithTenant(page, queryWrapper);
}
} }

View File

@ -13,9 +13,6 @@ public class SysOrgServiceImpl extends ServiceImpl<SysOrgMapper, SysOrg> impleme
@Override @Override
public List<SysOrg> listTree(Long tenantId) { public List<SysOrg> listTree(Long tenantId) {
LambdaQueryWrapper<SysOrg> query = new LambdaQueryWrapper<>(); LambdaQueryWrapper<SysOrg> query = new LambdaQueryWrapper<>();
if (tenantId != null) {
query.eq(SysOrg::getTenantId, tenantId);
}
query.orderByAsc(SysOrg::getSortOrder); query.orderByAsc(SysOrg::getSortOrder);
return list(query); return list(query);
} }

View File

@ -28,13 +28,18 @@ public class SysPermissionServiceImpl extends ServiceImpl<SysPermissionMapper, S
return List.of(); return List.of();
} }
// Super admin or Platform Admin gets all permissions // Get current login user from Security Context
if (userId == 1L) { org.springframework.security.core.Authentication auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
return list(); if (auth != null && auth.getPrincipal() instanceof com.imeeting.security.LoginUser) {
com.imeeting.security.LoginUser loginUser = (com.imeeting.security.LoginUser) auth.getPrincipal();
// If current user is Platform Admin (tenantId=0 & isPlatformAdmin=true), return all permissions
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0).equals(loginUser.getTenantId())) {
return list();
}
} }
SysUser user = sysUserService.getById(userId); // Fallback or specific user logic (for tenant users or internal calls)
if (user != null && Boolean.TRUE.equals(user.getIsPlatformAdmin())) { if (userId == 1L) {
return list(); return list();
} }

View File

@ -33,9 +33,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
if (user.getUsername() == null) return; if (user.getUsername() == null) return;
LambdaQueryWrapper<SysUser> query = new LambdaQueryWrapper<SysUser>() LambdaQueryWrapper<SysUser> query = new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, user.getUsername()) .eq(SysUser::getUsername, user.getUsername());
.eq(SysUser::getTenantId, user.getTenantId())
.eq(SysUser::getIsDeleted, 0);
if (user.getUserId() != null) { if (user.getUserId() != null) {
query.ne(SysUser::getUserId, user.getUserId()); query.ne(SysUser::getUserId, user.getUserId());

View File

@ -18,6 +18,7 @@ spring:
mybatis-plus: mybatis-plus:
configuration: configuration:
map-underscore-to-camel-case: true map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config: global-config:
db-config: db-config:
logic-delete-field: isDeleted logic-delete-field: isDeleted

View File

@ -1,7 +1,8 @@
import { Card, Table, Tabs, Tag, Input, Space, Button, DatePicker, Select, Typography, Modal, Descriptions } from "antd"; 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 { 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"; import dayjs from "dayjs";
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
@ -10,7 +11,7 @@ const { Text, Title } = Typography;
export default function Logs() { export default function Logs() {
const [activeTab, setActiveTab] = useState("OPERATION"); const [activeTab, setActiveTab] = useState("OPERATION");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState([]); const [data, setData] = useState<SysLog[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [params, setParams] = useState({ const [params, setParams] = useState({
current: 1, current: 1,
@ -18,18 +19,34 @@ export default function Logs() {
username: "", username: "",
status: undefined, status: undefined,
startDate: "", 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 // Modal for detail view
const [detailModalVisible, setDetailModalVisible] = useState(false); const [detailModalVisible, setDetailModalVisible] = useState(false);
const [selectedLog, setSelectedLog] = useState<any>(null); const [selectedLog, setSelectedLog] = useState<SysLog | null>(null);
const loadData = async (currentParams = params) => { const loadData = async (currentParams = params) => {
setLoading(true); setLoading(true);
try { try {
const operationType = activeTab === "LOGIN" ? "LOGIN" : "OPERATION"; // Use logType for precise filtering
const result = await fetchLogs({ ...currentParams, operationType }); const result = await fetchLogs({ ...currentParams, logType: activeTab });
setData(result.records || []); setData(result.records || []);
setTotal(result.total || 0); setTotal(result.total || 0);
} finally { } finally {
@ -39,7 +56,17 @@ export default function Logs() {
useEffect(() => { useEffect(() => {
loadData(); 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 = () => { const handleSearch = () => {
setParams({ ...params, current: 1 }); setParams({ ...params, current: 1 });
@ -53,50 +80,76 @@ export default function Logs() {
username: "", username: "",
status: undefined, status: undefined,
startDate: "", startDate: "",
endDate: "" endDate: "",
operation: "",
sortField: "createdAt",
sortOrder: "descend" as any
}; };
setParams(resetParams); setParams(resetParams);
loadData(resetParams); loadData(resetParams);
}; };
const showDetail = (record: any) => { const showDetail = (record: SysLog) => {
setSelectedLog(record); setSelectedLog(record);
setDetailModalVisible(true); 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 (
<Text style={{ color, fontWeight: ms > 300 ? 600 : 400 }}>
{ms}ms
</Text>
);
};
const columns = [ const columns = [
...(isPlatformAdmin ? [{
title: "所属租户",
dataIndex: "tenantName",
key: "tenantName",
width: 150,
render: (text: string) => <Text type="warning">{text || "系统平台"}</Text>
}] : []),
{ {
title: "操作账号", title: "操作账号",
dataIndex: "username", dataIndex: "username",
key: "username", key: "username",
width: 140, width: 120,
render: (text: string) => <Text strong>{text || "系统/访客"}</Text> render: (text: string) => <Text strong>{text || "系统"}</Text>
}, },
{ {
title: activeTab === "LOGIN" ? "登录模块" : "业务模块", title: "操作详情",
dataIndex: "resourceType", dataIndex: "operation",
key: "resourceType", key: "operation",
width: 150
},
{
title: "操作描述",
dataIndex: "detail",
key: "detail",
ellipsis: true, ellipsis: true,
render: (text: string) => <Text type="secondary">{text}</Text> render: (text: string) => <Text type="secondary">{text}</Text>
}, },
{ {
title: "IP 地址", title: "IP 地址",
dataIndex: "ipAddress", dataIndex: "ip",
key: "ipAddress", key: "ip",
width: 140, width: 130,
className: "tabular-nums" className: "tabular-nums"
}, },
{
title: "耗时",
dataIndex: "duration",
key: "duration",
width: 100,
sorter: true,
sortOrder: params.sortField === 'duration' ? params.sortOrder : null,
render: renderDuration
},
{ {
title: "状态", title: "状态",
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
width: 100, width: 90,
render: (status: number) => ( render: (status: number) => (
<Tag color={status === 1 ? "green" : "red"} className="m-0"> <Tag color={status === 1 ? "green" : "red"} className="m-0">
{status === 1 ? "成功" : "失败"} {status === 1 ? "成功" : "失败"}
@ -108,15 +161,17 @@ export default function Logs() {
dataIndex: "createdAt", dataIndex: "createdAt",
key: "createdAt", key: "createdAt",
width: 180, width: 180,
sorter: true,
sortOrder: params.sortField === 'createdAt' ? params.sortOrder : null,
className: "tabular-nums", className: "tabular-nums",
render: (text: string) => text?.replace('T', ' ').substring(0, 19) render: (text: string) => text?.replace('T', ' ').substring(0, 19)
}, },
{ {
title: "详情", title: "详情",
key: "action", key: "action",
width: 80, width: 60,
fixed: "right" as const, fixed: "right" as const,
render: (_: any, record: any) => ( render: (_: any, record: SysLog) => (
<Button <Button
type="link" type="link"
size="small" size="small"
@ -129,12 +184,12 @@ export default function Logs() {
]; ];
if (activeTab === "OPERATION") { if (activeTab === "OPERATION") {
columns.splice(1, 0, { columns.splice(isPlatformAdmin ? 2 : 1, 0, {
title: "请求方", title: "请求方",
dataIndex: "operationType", dataIndex: "method",
key: "operationType", key: "method",
width: 100, width: 180,
render: (t: string) => <Tag color="blue">{t}</Tag> render: (t: string) => <Tag color="blue" style={{ fontSize: '11px' }}>{t}</Tag>
}); });
} }
@ -148,12 +203,11 @@ export default function Logs() {
<Card className="mb-4 shadow-sm"> <Card className="mb-4 shadow-sm">
<Space wrap size="middle"> <Space wrap size="middle">
<Input <Input
placeholder="搜索用户名…" placeholder="搜索操作内容…"
style={{ width: 180 }} style={{ width: 180 }}
value={params.username} value={params.operation}
onChange={e => setParams({ ...params, username: e.target.value })} onChange={e => setParams({ ...params, operation: e.target.value })}
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />} prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
aria-label="搜索用户名"
allowClear allowClear
/> />
<Select <Select
@ -212,12 +266,12 @@ export default function Logs() {
dataSource={data} dataSource={data}
loading={loading} loading={loading}
size="middle" size="middle"
onChange={handleTableChange}
pagination={{ pagination={{
current: params.current, current: params.current,
pageSize: params.size, pageSize: params.size,
total: total, total: total,
showSizeChanger: true, showSizeChanger: true,
onChange: (page, size) => setParams({ ...params, current: page, size }),
showTotal: (total) => `${total} 条数据` showTotal: (total) => `${total} 条数据`
}} }}
/> />
@ -236,40 +290,39 @@ export default function Logs() {
> >
{selectedLog && ( {selectedLog && (
<Descriptions bordered column={1} size="small"> <Descriptions bordered column={1} size="small">
<Descriptions.Item label="操作模块">{selectedLog.resourceType}</Descriptions.Item> {isPlatformAdmin && (
<Descriptions.Item label="请求方式"> <Descriptions.Item label="所属租户">
<Tag color="blue">{selectedLog.operationType}</Tag> <Text type="warning">{selectedLog.tenantName || "系统平台"}</Text>
</Descriptions.Item>
)}
<Descriptions.Item label="操作详情">{selectedLog.operation}</Descriptions.Item>
<Descriptions.Item label="请求方法">
<Tag color="blue">{selectedLog.method || "N/A"}</Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="操作账号">{selectedLog.username || "系统"}</Descriptions.Item> <Descriptions.Item label="操作账号">{selectedLog.username || "系统"}</Descriptions.Item>
<Descriptions.Item label="IP 地址" className="tabular-nums">{selectedLog.ipAddress}</Descriptions.Item> <Descriptions.Item label="IP 地址" className="tabular-nums">{selectedLog.ip}</Descriptions.Item>
<Descriptions.Item label="User Agent"> <Descriptions.Item label="耗时">{selectedLog.duration ? `${selectedLog.duration}ms` : "-"}</Descriptions.Item>
<Text type="secondary" style={{ fontSize: '12px' }}>{selectedLog.userAgent}</Text>
</Descriptions.Item>
<Descriptions.Item label="状态"> <Descriptions.Item label="状态">
<Tag color={selectedLog.status === 1 ? "green" : "red"}> <Tag color={selectedLog.status === 1 ? "green" : "red"}>
{selectedLog.status === 1 ? "成功" : "失败"} {selectedLog.status === 1 ? "成功" : "失败"}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="时间" className="tabular-nums">{selectedLog.createdAt?.replace('T', ' ')}</Descriptions.Item> <Descriptions.Item label="时间" className="tabular-nums">{selectedLog.createdAt?.replace('T', ' ')}</Descriptions.Item>
<Descriptions.Item label="详情/参数"> <Descriptions.Item label="请求参数">
<div style={{ <div style={{
background: '#f5f5f5', background: '#f5f5f5',
padding: '12px', padding: '12px',
borderRadius: '4px', borderRadius: '4px',
maxHeight: '200px', maxHeight: '150px',
overflowY: 'auto', overflowY: 'auto',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
wordBreak: 'break-all', wordBreak: 'break-all',
fontFamily: 'monospace' fontFamily: 'monospace',
fontSize: '12px'
}}> }}>
{selectedLog.detail} {selectedLog.params || "无"}
</div> </div>
</Descriptions.Item> </Descriptions.Item>
{selectedLog.errorMessage && (
<Descriptions.Item label="错误信息">
<Text type="danger">{selectedLog.errorMessage}</Text>
</Descriptions.Item>
)}
</Descriptions> </Descriptions>
)} )}
</Modal> </Modal>
@ -277,5 +330,3 @@ export default function Logs() {
); );
} }
// Ensure UserOutlined is imported
import { UserOutlined } from "@ant-design/icons";

View File

@ -100,6 +100,22 @@ export interface SysOrg extends BaseEntity {
sortOrder?: number; sortOrder?: number;
} }
export interface SysLog {
id: number;
tenantId?: number;
tenantName?: string;
userId?: number;
username?: string;
operation: string;
method?: string;
params?: string;
result?: string;
status: number;
ip?: string;
duration?: number;
createdAt: string;
}
export interface OrgNode extends SysOrg { export interface OrgNode extends SysOrg {
children: OrgNode[]; children: OrgNode[];
} }