Skip to content

Commit cd49e0d

Browse files
committed
feat: 登录日志 + 审计日志独立Controller + 流程设计节点工具栏移至左侧
1 parent 67acf0b commit cd49e0d

10 files changed

Lines changed: 444 additions & 14 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using Juggle.Domain.Entities;
2+
using Juggle.Infrastructure.Persistence;
3+
using Juggle.Application.Models.Request;
4+
using Juggle.Application.Models.Response;
5+
using Juggle.Application.Services;
6+
using Microsoft.AspNetCore.Authorization;
7+
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.EntityFrameworkCore;
9+
10+
namespace Juggle.Api.Controllers.Api;
11+
12+
[ApiController]
13+
[Route("api/audit-log")]
14+
public class AuditLogController : ControllerBase
15+
{
16+
private readonly JuggleDbContext _db;
17+
private readonly ITenantAccessor _tenant;
18+
19+
public AuditLogController(JuggleDbContext db, ITenantAccessor tenant)
20+
{
21+
_db = db;
22+
_tenant = tenant;
23+
}
24+
25+
/// <summary>审计日志分页列表(超级管理员)</summary>
26+
[HttpPost("page"), Authorize]
27+
public async Task<ApiResult> Page([FromBody] AuditLogPageRequest req)
28+
{
29+
if (!_tenant.IsSuperAdmin)
30+
return ApiResult.Fail("仅超级管理员可查看", 403);
31+
32+
var query = _db.AuditLogs.Where(a => a.Deleted == 0);
33+
if (!string.IsNullOrEmpty(req.Module))
34+
query = query.Where(a => a.Module == req.Module);
35+
if (!string.IsNullOrEmpty(req.ActionType))
36+
query = query.Where(a => a.ActionType == req.ActionType);
37+
if (!string.IsNullOrEmpty(req.Keyword))
38+
query = query.Where(a => a.ChangeContent.Contains(req.Keyword)
39+
|| (a.OperatorName != null && a.OperatorName.Contains(req.Keyword)));
40+
41+
var total = await query.CountAsync();
42+
var records = await query.OrderByDescending(a => a.Id)
43+
.Skip((req.PageNum - 1) * req.PageSize)
44+
.Take(req.PageSize)
45+
.Select(a => new
46+
{
47+
a.Id, a.Module, a.ActionType, a.TargetId, a.ChangeContent,
48+
a.OperatorName, a.OperatorId, a.OperatorTenantId, a.CreatedAt
49+
})
50+
.ToListAsync();
51+
52+
return ApiResult.Success(new { total, records });
53+
}
54+
}

Juggle.Api/Controllers/Api/RoleController.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ public async Task<ApiResult> Add([FromBody] RoleAddRequest req)
181181
await _db.SaveChangesAsync();
182182
}
183183

184+
_db.AuditLogs.Add(new AuditLogEntity
185+
{
186+
Module = "role", ActionType = "add", TargetId = role.Id,
187+
ChangeContent = $"新增角色:{req.RoleName}",
188+
OperatorName = _tenant.UserName, OperatorId = _tenant.UserId,
189+
OperatorTenantId = _tenant.TenantId,
190+
Deleted = 0, CreatedAt = DateTime.Now.ToString("o")
191+
});
192+
await _db.SaveChangesAsync();
193+
184194
return ApiResult.Success(new { id = role.Id });
185195
}
186196

@@ -240,6 +250,17 @@ public async Task<ApiResult> Update([FromBody] RoleUpdateRequest req)
240250
}
241251

242252
await _db.SaveChangesAsync();
253+
254+
_db.AuditLogs.Add(new AuditLogEntity
255+
{
256+
Module = "role", ActionType = "update", TargetId = req.Id,
257+
ChangeContent = $"更新角色:{req.RoleName}",
258+
OperatorName = _tenant.UserName, OperatorId = _tenant.UserId,
259+
OperatorTenantId = _tenant.TenantId,
260+
Deleted = 0, CreatedAt = DateTime.Now.ToString("o")
261+
});
262+
await _db.SaveChangesAsync();
263+
243264
return ApiResult.Success();
244265
}
245266

@@ -267,6 +288,17 @@ public async Task<ApiResult> Delete(long id)
267288
_db.RoleMenus.UpdateRange(menus);
268289

269290
await _db.SaveChangesAsync();
291+
292+
_db.AuditLogs.Add(new AuditLogEntity
293+
{
294+
Module = "role", ActionType = "delete", TargetId = id,
295+
ChangeContent = $"删除角色:{role.RoleName}",
296+
OperatorName = _tenant.UserName, OperatorId = _tenant.UserId,
297+
OperatorTenantId = _tenant.TenantId,
298+
Deleted = 0, CreatedAt = DateTime.Now.ToString("o")
299+
});
300+
await _db.SaveChangesAsync();
301+
270302
return ApiResult.Success();
271303
}
272304
}

Juggle.Api/Controllers/Api/UserController.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,61 @@ public async Task<ApiResult> Login([FromBody] LoginRequest req)
3535
.FirstOrDefaultAsync(u => u.UserName == req.UserName
3636
&& u.Password == pwdMd5
3737
&& u.Deleted == 0);
38+
39+
// 记录登录日志
40+
var loginIp = HttpContext.Connection?.RemoteIpAddress?.ToString();
41+
var loginUA = HttpContext.Request.Headers.UserAgent.ToString();
42+
3843
if (user == null)
44+
{
45+
// 记录失败日志
46+
_db.LoginLogs.Add(new LoginLogEntity
47+
{
48+
UserName = req.UserName, LoginType = "login", Result = "fail",
49+
IpAddress = loginIp, UserAgent = loginUA,
50+
Deleted = 0, CreatedAt = DateTime.Now.ToString("o")
51+
});
52+
await _db.SaveChangesAsync();
3953
return ApiResult.Fail("用户名或密码错误", 401);
54+
}
4055

4156
// 检查租户是否禁用或过期
4257
if (user.TenantId.HasValue)
4358
{
4459
var tenant = await _db.Tenants.FindAsync(user.TenantId.Value);
4560
if (tenant == null || tenant.Deleted == 1 || tenant.Status == 0)
61+
{
62+
_db.LoginLogs.Add(new LoginLogEntity
63+
{
64+
UserId = user.Id, UserName = user.UserName, LoginType = "login", Result = "fail",
65+
IpAddress = loginIp, UserAgent = loginUA, TenantId = user.TenantId,
66+
Deleted = 0, CreatedAt = DateTime.Now.ToString("o")
67+
});
68+
await _db.SaveChangesAsync();
4669
return ApiResult.Fail("租户已被禁用,无法登录", 403);
70+
}
4771
if (tenant.ExpiredAt.HasValue && tenant.ExpiredAt.Value < DateTime.Now)
72+
{
73+
_db.LoginLogs.Add(new LoginLogEntity
74+
{
75+
UserId = user.Id, UserName = user.UserName, LoginType = "login", Result = "fail",
76+
IpAddress = loginIp, UserAgent = loginUA, TenantId = user.TenantId,
77+
Deleted = 0, CreatedAt = DateTime.Now.ToString("o")
78+
});
79+
await _db.SaveChangesAsync();
4880
return ApiResult.Fail("租户已过期,无法登录", 403);
81+
}
4982
}
5083

84+
// 记录成功登录日志
85+
_db.LoginLogs.Add(new LoginLogEntity
86+
{
87+
UserId = user.Id, UserName = user.UserName, LoginType = "login", Result = "success",
88+
IpAddress = loginIp, UserAgent = loginUA, TenantId = user.TenantId,
89+
Deleted = 0, CreatedAt = DateTime.Now.ToString("o")
90+
});
91+
await _db.SaveChangesAsync();
92+
5193
var tokenStr = _jwtService.GenerateToken(user);
5294

5395
// 查询菜单权限(角色权限 ∩ 租户权限)
@@ -266,4 +308,30 @@ public async Task<ApiResult> Delete(long id)
266308
await _db.SaveChangesAsync();
267309
return ApiResult.Success();
268310
}
311+
312+
/// <summary>登录日志分页列表</summary>
313+
[HttpPost("login-log/page"), Authorize]
314+
public async Task<ApiResult> LoginLogPage([FromBody] PageRequest req)
315+
{
316+
if (!_tenant.IsSuperAdmin)
317+
return ApiResult.Fail("仅超级管理员可查看", 403);
318+
319+
var query = _db.LoginLogs.Where(l => l.Deleted == 0);
320+
if (!string.IsNullOrEmpty(req.Keyword))
321+
query = query.Where(l => (l.UserName != null && l.UserName.Contains(req.Keyword))
322+
|| (l.IpAddress != null && l.IpAddress.Contains(req.Keyword)));
323+
324+
var total = await query.CountAsync();
325+
var records = await query.OrderByDescending(l => l.Id)
326+
.Skip((req.PageNum - 1) * req.PageSize)
327+
.Take(req.PageSize)
328+
.Select(l => new
329+
{
330+
l.Id, l.UserId, l.UserName, l.LoginType, l.Result,
331+
l.IpAddress, l.TenantId, l.CreatedAt
332+
})
333+
.ToListAsync();
334+
335+
return ApiResult.Success(new { total, records });
336+
}
269337
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace Juggle.Domain.Entities;
2+
3+
/// <summary>登录访问日志</summary>
4+
public class LoginLogEntity : BaseEntity
5+
{
6+
/// <summary>用户ID</summary>
7+
public long? UserId { get; set; }
8+
/// <summary>用户名</summary>
9+
public string? UserName { get; set; }
10+
/// <summary>登录类型:login / logout</summary>
11+
public string LoginType { get; set; } = "login";
12+
/// <summary>登录结果:success / fail</summary>
13+
public string Result { get; set; } = "success";
14+
/// <summary>登录IP</summary>
15+
public string? IpAddress { get; set; }
16+
/// <summary>User-Agent</summary>
17+
public string? UserAgent { get; set; }
18+
/// <summary>租户ID</summary>
19+
public long? TenantId { get; set; }
20+
}

Juggle.Infrastructure/Persistence/JuggleDbContext.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public JuggleDbContext(DbContextOptions<JuggleDbContext> options) : base(options
3030
public DbSet<RoleMenuEntity> RoleMenus { get; set; } = null!;
3131
public DbSet<TenantEntity> Tenants { get; set; } = null!;
3232
public DbSet<AuditLogEntity> AuditLogs { get; set; } = null!;
33+
public DbSet<LoginLogEntity> LoginLogs { get; set; } = null!;
3334

3435
protected override void OnModelCreating(ModelBuilder modelBuilder)
3536
{
@@ -59,6 +60,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
5960
modelBuilder.Entity<RoleMenuEntity>().ToTable("t_role_menu");
6061
modelBuilder.Entity<TenantEntity>().ToTable("t_tenant");
6162
modelBuilder.Entity<AuditLogEntity>().ToTable("t_audit_log");
63+
modelBuilder.Entity<LoginLogEntity>().ToTable("t_login_log");
6264

6365
// 列名映射(snake_case)
6466
modelBuilder.Entity<UserEntity>(e => {
@@ -447,5 +449,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
447449
e.Property(p => p.OperatorId).HasColumnName("operator_id");
448450
e.Property(p => p.OperatorTenantId).HasColumnName("operator_tenant_id");
449451
});
452+
modelBuilder.Entity<LoginLogEntity>(e => {
453+
e.Property(p => p.Id).HasColumnName("id");
454+
e.Property(p => p.Deleted).HasColumnName("deleted");
455+
e.Property(p => p.CreatedAt).HasColumnName("created_at");
456+
e.Property(p => p.CreatedBy).HasColumnName("created_by");
457+
e.Property(p => p.UpdatedAt).HasColumnName("updated_at");
458+
e.Property(p => p.UpdatedBy).HasColumnName("updated_by");
459+
e.Property(p => p.UserId).HasColumnName("user_id");
460+
e.Property(p => p.UserName).HasColumnName("user_name");
461+
e.Property(p => p.LoginType).HasColumnName("login_type");
462+
e.Property(p => p.Result).HasColumnName("result");
463+
e.Property(p => p.IpAddress).HasColumnName("ip_address");
464+
e.Property(p => p.UserAgent).HasColumnName("user_agent").HasColumnType("text");
465+
e.Property(p => p.TenantId).HasColumnName("tenant_id");
466+
});
450467
}
451468
}

JuggleNet6.Frontend/src/router/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const routes = [
2828
{ path: 'system/role', component: () => import('../views/system/RoleManage.vue') },
2929
{ path: 'system/tenant', component: () => import('../views/system/TenantManage.vue') },
3030
{ path: 'system/config', component: () => import('../views/system/SystemConfig.vue') },
31+
{ path: 'system/login-log', component: () => import('../views/system/LoginLog.vue') },
32+
{ path: 'system/audit-log', component: () => import('../views/system/AuditLog.vue') },
3133
{ path: 'flow/log', component: () => import('../views/flow/FlowLog.vue') },
3234
{ path: 'flow/async-result', component: () => import('../views/flow/AsyncFlowResult.vue') },
3335
{ path: 'flow/testcase', component: () => import('../views/flow/FlowTestCase.vue') }

JuggleNet6.Frontend/src/views/Layout.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
<el-menu-item index="/system/role" v-if="hasMenu('/system/role')">角色管理</el-menu-item>
4545
<el-menu-item index="/system/tenant" v-if="hasMenu('/system/tenant')">租户管理</el-menu-item>
4646
<el-menu-item index="/system/config" v-if="hasMenu('/system/config')">系统配置</el-menu-item>
47+
<el-menu-item index="/system/login-log" v-if="hasMenu('/system/login-log')">登录日志</el-menu-item>
48+
<el-menu-item index="/system/audit-log" v-if="hasMenu('/system/audit-log')">审计日志</el-menu-item>
4749
</el-sub-menu>
4850
</el-menu>
4951
</el-aside>

0 commit comments

Comments
 (0)