feat(logs): 添加操作日志功能支持推送和授权模块
- 在JwtAuthFilter中设置认证详情到claims - 为license-service添加LicenseOperationLog相关实体、仓库和服务 - 为push-service添加PushOperationLog相关实体、仓库和服务 - 在LicenseAdminController中注入并使用操作日志记录授权变更 - 在PushManagementController中注入并使用操作日志记录推送操作 - 更新OperationLogService以支持从JWT claims获取用户信息 - 扩展OperationLogService支持推送和授权操作日志查询 - 在前端OperationLogView中添加推送和授权日志选项卡 - 添加LicenseOperationLog和PushOperationLog接口定义 - 实现推送和授权日志的数据加载和分页功能 - 添加操作类型和资源类型的标签映射支持
这个提交包含在:
父节点
f9ad40cb98
当前提交
73dd4814f2
@ -37,6 +37,7 @@ public class JwtAuthFilter extends OncePerRequestFilter {
|
||||
: List.of();
|
||||
UsernamePasswordAuthenticationToken auth =
|
||||
new UsernamePasswordAuthenticationToken(subject, null, authorities);
|
||||
auth.setDetails(claims);
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import com.xuqm.license.entity.AppLicenseEntity;
|
||||
import com.xuqm.license.entity.DeviceEntity;
|
||||
import com.xuqm.license.service.AppLicenseService;
|
||||
import com.xuqm.license.service.DeviceService;
|
||||
import com.xuqm.license.service.LicenseOperationLogService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@ -17,10 +18,13 @@ public class LicenseAdminController {
|
||||
|
||||
private final AppLicenseService appLicenseService;
|
||||
private final DeviceService deviceService;
|
||||
private final LicenseOperationLogService opLogService;
|
||||
|
||||
public LicenseAdminController(AppLicenseService appLicenseService, DeviceService deviceService) {
|
||||
public LicenseAdminController(AppLicenseService appLicenseService, DeviceService deviceService,
|
||||
LicenseOperationLogService opLogService) {
|
||||
this.appLicenseService = appLicenseService;
|
||||
this.deviceService = deviceService;
|
||||
this.opLogService = opLogService;
|
||||
}
|
||||
|
||||
@GetMapping("/apps/{appKey}")
|
||||
@ -52,18 +56,21 @@ public class LicenseAdminController {
|
||||
}
|
||||
AppLicenseEntity updated = appLicenseService.update(
|
||||
appKey, null, req.maxDevices(), newExpiresAt, clearExpiresAt, req.isActive(), req.remark(), null, null, null);
|
||||
opLogService.record(appKey, "UPDATE_LICENSE", "LICENSE", appKey, "更新 " + appKey + " 的授权配置");
|
||||
return ResponseEntity.ok(ApiResponse.success(updated));
|
||||
}
|
||||
|
||||
@DeleteMapping("/devices/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> revokeDevice(@PathVariable String id) {
|
||||
deviceService.revoke(id);
|
||||
opLogService.record(null, "REVOKE_DEVICE", "DEVICE", id, "吊销设备 " + id + " 的授权");
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@PutMapping("/devices/{id}/reactivate")
|
||||
public ResponseEntity<ApiResponse<Void>> reactivateDevice(@PathVariable String id) {
|
||||
deviceService.reactivate(id);
|
||||
opLogService.record(null, "REACTIVATE_DEVICE", "DEVICE", id, "重新激活设备 " + id);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package com.xuqm.license.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.license.entity.LicenseOperationLogEntity;
|
||||
import com.xuqm.license.service.LicenseOperationLogService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/license/admin/operation-logs")
|
||||
public class LicenseOperationLogController {
|
||||
|
||||
private final LicenseOperationLogService logService;
|
||||
|
||||
public LicenseOperationLogController(LicenseOperationLogService logService) {
|
||||
this.logService = logService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> list(
|
||||
@RequestParam String appKey,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
Page<LicenseOperationLogEntity> result = logService.list(appKey, page, size);
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
||||
"content", result.getContent(),
|
||||
"total", result.getTotalElements(),
|
||||
"totalPages", result.getTotalPages()
|
||||
)));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.xuqm.license.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "license_operation_log", indexes = {
|
||||
@Index(name = "idx_license_op_log_app_time", columnList = "appKey,createdAt")
|
||||
})
|
||||
public class LicenseOperationLogEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appKey;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String operator;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String action;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String resourceType;
|
||||
|
||||
@Column(length = 128)
|
||||
private String resourceId;
|
||||
|
||||
@Column(length = 255)
|
||||
private String summary;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String detailJson;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppKey() { return appKey; }
|
||||
public void setAppKey(String appKey) { this.appKey = appKey; }
|
||||
|
||||
public String getOperator() { return operator; }
|
||||
public void setOperator(String operator) { this.operator = operator; }
|
||||
|
||||
public String getAction() { return action; }
|
||||
public void setAction(String action) { this.action = action; }
|
||||
|
||||
public String getResourceType() { return resourceType; }
|
||||
public void setResourceType(String resourceType) { this.resourceType = resourceType; }
|
||||
|
||||
public String getResourceId() { return resourceId; }
|
||||
public void setResourceId(String resourceId) { this.resourceId = resourceId; }
|
||||
|
||||
public String getSummary() { return summary; }
|
||||
public void setSummary(String summary) { this.summary = summary; }
|
||||
|
||||
public String getDetailJson() { return detailJson; }
|
||||
public void setDetailJson(String detailJson) { this.detailJson = detailJson; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.xuqm.license.repository;
|
||||
|
||||
import com.xuqm.license.entity.LicenseOperationLogEntity;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface LicenseOperationLogRepository extends JpaRepository<LicenseOperationLogEntity, String> {
|
||||
Page<LicenseOperationLogEntity> findByAppKeyOrderByCreatedAtDesc(String appKey, Pageable pageable);
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.xuqm.license.service;
|
||||
|
||||
import com.xuqm.license.entity.LicenseOperationLogEntity;
|
||||
import com.xuqm.license.repository.LicenseOperationLogRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class LicenseOperationLogService {
|
||||
|
||||
private final LicenseOperationLogRepository repository;
|
||||
|
||||
public LicenseOperationLogService(LicenseOperationLogRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public void record(String appKey, String action, String resourceType,
|
||||
String resourceId, String summary) {
|
||||
LicenseOperationLogEntity entity = new LicenseOperationLogEntity();
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setAppKey(appKey);
|
||||
entity.setOperator(currentOperator());
|
||||
entity.setAction(action);
|
||||
entity.setResourceType(resourceType);
|
||||
entity.setResourceId(resourceId);
|
||||
entity.setSummary(summary);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
repository.save(entity);
|
||||
}
|
||||
|
||||
public Page<LicenseOperationLogEntity> list(String appKey, int page, int size) {
|
||||
int safePage = Math.max(page, 0);
|
||||
int safeSize = Math.min(Math.max(size, 1), 200);
|
||||
return repository.findByAppKeyOrderByCreatedAtDesc(appKey, PageRequest.of(safePage, safeSize));
|
||||
}
|
||||
|
||||
private String currentOperator() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || auth.getName() == null || auth.getName().isBlank()) {
|
||||
return "system";
|
||||
}
|
||||
if (auth.getDetails() instanceof io.jsonwebtoken.Claims claims) {
|
||||
String nickname = claims.get("nickname", String.class);
|
||||
if (nickname != null && !nickname.isBlank()) return nickname;
|
||||
String username = claims.get("username", String.class);
|
||||
if (username != null && !username.isBlank()) return username;
|
||||
}
|
||||
return auth.getName();
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,8 @@ import com.xuqm.push.entity.DeviceLoginLogEntity;
|
||||
import com.xuqm.push.entity.PushUserEntity;
|
||||
import com.xuqm.push.service.PushAccountService;
|
||||
import com.xuqm.push.service.PushDiagnosticsService;
|
||||
import com.xuqm.push.service.PushOperationLogService;
|
||||
import java.util.LinkedHashMap;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@ -28,11 +30,14 @@ public class PushManagementController {
|
||||
|
||||
private final PushDiagnosticsService diagnosticsService;
|
||||
private final PushAccountService accountService;
|
||||
private final PushOperationLogService opLogService;
|
||||
|
||||
public PushManagementController(PushDiagnosticsService diagnosticsService,
|
||||
PushAccountService accountService) {
|
||||
PushAccountService accountService,
|
||||
PushOperationLogService opLogService) {
|
||||
this.diagnosticsService = diagnosticsService;
|
||||
this.accountService = accountService;
|
||||
this.opLogService = opLogService;
|
||||
}
|
||||
|
||||
// ---- user account management ----
|
||||
@ -68,6 +73,8 @@ public class PushManagementController {
|
||||
}
|
||||
PushUserEntity updated = accountService.updateAccount(request.appKey(), userId,
|
||||
request.nickname(), request.avatar(), gender);
|
||||
opLogService.record(request.appKey(), "UPDATE_USER", "ACCOUNT", userId,
|
||||
"编辑用户 " + userId + " 的信息", null);
|
||||
return ResponseEntity.ok(ApiResponse.success(updated));
|
||||
}
|
||||
|
||||
@ -81,7 +88,11 @@ public class PushManagementController {
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().body(ApiResponse.error(400, "无效的状态值,可选:ACTIVE, BANNED"));
|
||||
}
|
||||
return ResponseEntity.ok(ApiResponse.success(accountService.setUserStatus(request.appKey(), userId, status)));
|
||||
PushUserEntity result = accountService.setUserStatus(request.appKey(), userId, status);
|
||||
String statusLabel = status == PushUserEntity.Status.BANNED ? "禁用" : "启用";
|
||||
opLogService.record(request.appKey(), "UPDATE_USER_STATUS", "ACCOUNT", userId,
|
||||
statusLabel + "用户 " + userId, null);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
@DeleteMapping("/users/{userId}")
|
||||
@ -89,6 +100,8 @@ public class PushManagementController {
|
||||
@PathVariable String userId,
|
||||
@RequestParam String appKey) {
|
||||
accountService.deleteAccount(appKey, userId);
|
||||
opLogService.record(appKey, "DELETE_USER", "ACCOUNT", userId,
|
||||
"删除用户 " + userId, null);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@ -104,6 +117,8 @@ public class PushManagementController {
|
||||
}
|
||||
PushUserEntity user = accountService.importAccount(request.appKey(), request.userId(),
|
||||
request.nickname(), request.avatar(), gender, status);
|
||||
opLogService.record(request.appKey(), "IMPORT_USER", "ACCOUNT", request.userId(),
|
||||
"导入用户 " + request.userId(), null);
|
||||
return ResponseEntity.ok(ApiResponse.success(user));
|
||||
}
|
||||
|
||||
@ -139,6 +154,8 @@ public class PushManagementController {
|
||||
request.title(),
|
||||
request.body(),
|
||||
request.payload());
|
||||
opLogService.record(request.appKey(), "TEST_PUSH", "PUSH", request.userId(),
|
||||
"向 " + request.userId() + " 发送测试推送", null);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
package com.xuqm.push.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.push.entity.PushOperationLogEntity;
|
||||
import com.xuqm.push.service.PushOperationLogService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/push/admin/operation-logs")
|
||||
@PreAuthorize("hasAnyAuthority('ROLE_OPS', 'ROLE_TENANT', 'ROLE_ADMIN')")
|
||||
public class PushOperationLogController {
|
||||
|
||||
private final PushOperationLogService logService;
|
||||
|
||||
public PushOperationLogController(PushOperationLogService logService) {
|
||||
this.logService = logService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> list(
|
||||
@RequestParam String appKey,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
Page<PushOperationLogEntity> result = logService.list(appKey, page, size);
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
||||
"content", result.getContent(),
|
||||
"total", result.getTotalElements(),
|
||||
"totalPages", result.getTotalPages()
|
||||
)));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.xuqm.push.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "push_operation_log", indexes = {
|
||||
@Index(name = "idx_push_op_log_app_time", columnList = "appKey,createdAt")
|
||||
})
|
||||
public class PushOperationLogEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appKey;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String operator;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String action;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String resourceType;
|
||||
|
||||
@Column(length = 128)
|
||||
private String resourceId;
|
||||
|
||||
@Column(length = 255)
|
||||
private String summary;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String detail;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppKey() { return appKey; }
|
||||
public void setAppKey(String appKey) { this.appKey = appKey; }
|
||||
|
||||
public String getOperator() { return operator; }
|
||||
public void setOperator(String operator) { this.operator = operator; }
|
||||
|
||||
public String getAction() { return action; }
|
||||
public void setAction(String action) { this.action = action; }
|
||||
|
||||
public String getResourceType() { return resourceType; }
|
||||
public void setResourceType(String resourceType) { this.resourceType = resourceType; }
|
||||
|
||||
public String getResourceId() { return resourceId; }
|
||||
public void setResourceId(String resourceId) { this.resourceId = resourceId; }
|
||||
|
||||
public String getSummary() { return summary; }
|
||||
public void setSummary(String summary) { this.summary = summary; }
|
||||
|
||||
public String getDetail() { return detail; }
|
||||
public void setDetail(String detail) { this.detail = detail; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.xuqm.push.repository;
|
||||
|
||||
import com.xuqm.push.entity.PushOperationLogEntity;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface PushOperationLogRepository extends JpaRepository<PushOperationLogEntity, String> {
|
||||
Page<PushOperationLogEntity> findByAppKeyOrderByCreatedAtDesc(String appKey, Pageable pageable);
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.xuqm.push.service;
|
||||
|
||||
import com.xuqm.push.entity.PushOperationLogEntity;
|
||||
import com.xuqm.push.repository.PushOperationLogRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class PushOperationLogService {
|
||||
|
||||
private final PushOperationLogRepository repository;
|
||||
|
||||
public PushOperationLogService(PushOperationLogRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public void record(String appKey, String action, String resourceType,
|
||||
String resourceId, String summary, Map<String, Object> detail) {
|
||||
PushOperationLogEntity entity = new PushOperationLogEntity();
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setAppKey(appKey);
|
||||
entity.setOperator(currentOperator());
|
||||
entity.setAction(action);
|
||||
entity.setResourceType(resourceType);
|
||||
entity.setResourceId(resourceId);
|
||||
entity.setSummary(summary);
|
||||
entity.setDetail(detail == null ? null : detail.toString());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
repository.save(entity);
|
||||
}
|
||||
|
||||
public Page<PushOperationLogEntity> list(String appKey, int page, int size) {
|
||||
int safePage = Math.max(page, 0);
|
||||
int safeSize = Math.min(Math.max(size, 1), 200);
|
||||
return repository.findByAppKeyOrderByCreatedAtDesc(appKey, PageRequest.of(safePage, safeSize));
|
||||
}
|
||||
|
||||
private String currentOperator() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || auth.getName() == null || auth.getName().isBlank()) {
|
||||
return "system";
|
||||
}
|
||||
if (auth.getDetails() instanceof io.jsonwebtoken.Claims claims) {
|
||||
String nickname = claims.get("nickname", String.class);
|
||||
if (nickname != null && !nickname.isBlank()) return nickname;
|
||||
String username = claims.get("username", String.class);
|
||||
if (username != null && !username.isBlank()) return username;
|
||||
}
|
||||
return auth.getName();
|
||||
}
|
||||
}
|
||||
@ -77,6 +77,12 @@ public class OperationLogService {
|
||||
if (auth == null || auth.getName() == null || auth.getName().isBlank()) {
|
||||
return "system";
|
||||
}
|
||||
if (auth.getDetails() instanceof io.jsonwebtoken.Claims claims) {
|
||||
String nickname = claims.get("nickname", String.class);
|
||||
if (nickname != null && !nickname.isBlank()) return nickname;
|
||||
String username = claims.get("username", String.class);
|
||||
if (username != null && !username.isBlank()) return username;
|
||||
}
|
||||
return auth.getName();
|
||||
}
|
||||
|
||||
|
||||
@ -56,6 +56,12 @@ public class UpdateOperationLogService {
|
||||
if (auth == null || auth.getName() == null || auth.getName().isBlank()) {
|
||||
return "system";
|
||||
}
|
||||
if (auth.getDetails() instanceof io.jsonwebtoken.Claims claims) {
|
||||
String nickname = claims.get("nickname", String.class);
|
||||
if (nickname != null && !nickname.isBlank()) return nickname;
|
||||
String username = claims.get("username", String.class);
|
||||
if (username != null && !username.isBlank()) return username;
|
||||
}
|
||||
return auth.getName();
|
||||
}
|
||||
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户