feat(log): 完善系统日志功能并优化租户权限控制
- 新增租户ID字段到SysLog实体并调整日志记录逻辑 - 实现平台管理员跨租户查看日志的功能 - 重构登录日志记录方法,添加执行时长统计 - 优化JWT过滤器中的租户验证逻辑 - 调整MyBatis-Plus配置以支持布尔类型的逻辑删除 - 更新前端日志页面UI和数据展示逻辑 - 修复字典项和字典类型实体master
parent
69dc3e6788
commit
a497deacfc
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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]";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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";
|
|
||||||
|
|
|
||||||
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue