feat: 新增heritage-modules业务模块

- 新增heritage-system系统管理模块
  - 用户管理、角色管理、菜单管理
  - 字典管理、文件管理、图片管理
  - 登录日志、操作日志管理
  - 仪表盘统计功能
- 新增heritage-monitor系统监控模块
  - 服务器监控(CPU、内存、JVM、磁盘)
  - Redis缓存监控
  - 在线用户管理
This commit is contained in:
Leo 2025-10-08 02:08:48 +08:00
parent 9c1937b4fb
commit f507bdea83
48 changed files with 5778 additions and 0 deletions

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-backend</artifactId>
<version>${revision}</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<name>heritage-monitor</name>
<artifactId>heritage-monitor</artifactId>
<description>系统监控模块服务器资源监控、Redis监控、缓存管理等功能</description>
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据统一返回、全局异常以及限流、数据脱敏、重复提交等插件 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-resultex</artifactId>
<version>${revision}</version>
</dependency>
<!-- Sa-Token组件 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-sa-token</artifactId>
<version>${revision}</version>
</dependency>
<!-- SpringDoc OpenAPI 3.0 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- OSHI - 系统硬件信息库 -->
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.4.10</version>
</dependency>
<!-- Apache Commons Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,134 @@
package org.leocoder.heritage.monitor.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.monitor.pojo.server.SysCache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author Leocoder
* @description [缓存管理]
*/
@Tag(name = "缓存管理", description = "Redis缓存管理包括缓存查看、删除、清空等操作")
@RestController
@RequestMapping("/coder/monitor")
@RequiredArgsConstructor
public class CacheController {
private final RedisTemplate<String, String> redisTemplate;
private final static List<SysCache> cacheList = new ArrayList<>();
static {
cacheList.add(new SysCache("Authorization:login:session:", "用户登录信息"));
cacheList.add(new SysCache("coderDict:", "数据字典"));
cacheList.add(new SysCache("coderCaptchaCodes:", "验证码"));
cacheList.add(new SysCache("repeat_submit:", "防重提交"));
cacheList.add(new SysCache("rate_limit:", "限流处理"));
cacheList.add(new SysCache("pwd_error:", "密码错误次数"));
cacheList.add(new SysCache("coderBlacklistIp:", "黑名单IP"));
}
/**
* @description [查询Redis缓存所有Key]
* @author Leocoder
*/
@Operation(summary = "查询Redis缓存所有Key", description = "获取系统中所有缓存分类的Key列表")
@SaCheckPermission("monitor:cache:list")
@GetMapping("/cache/getRedisCache")
public List<SysCache> getRedisInformation() {
return cacheList;
}
/**
* @description [查询Redis缓存键名列表]
* @author Leocoder
*/
@Operation(summary = "查询Redis缓存键名列表", description = "根据缓存名称获取对应的键名列表")
@SaCheckPermission("monitor:cache:list")
@GetMapping("/cache/getCacheKeys/{cacheName}")
public TreeSet<Object> getCacheKeys(@PathVariable("cacheName") String cacheName) {
Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
if (ObjectUtils.isEmpty(cacheKeys)) {
return new TreeSet<>();
}
return new TreeSet<>(cacheKeys);
}
/**
* @description [获取Redis缓存内容]
* @author Leocoder
*/
@Operation(summary = "获取Redis缓存内容", description = "根据缓存键名获取缓存内容和过期时间")
@SaCheckPermission("monitor:cache:list")
@PostMapping("/cache/getValue")
public SysCache getCacheValue(@RequestBody SysCache sysCache) {
Boolean hasKey = redisTemplate.hasKey(sysCache.getCacheKey());
if (!hasKey) {
return new SysCache(sysCache.getCacheName(), sysCache.getCacheKey(), "", "");
}
String cacheValue = redisTemplate.opsForValue().get(sysCache.getCacheKey());
Long cacheTime = redisTemplate.getExpire(sysCache.getCacheKey(), TimeUnit.SECONDS);
String formatCacheTime = "";
if (cacheTime != null) {
if (cacheTime == -1L) {
formatCacheTime = "不过期";
} else {
int hours = (int) (cacheTime / 3600);
int minutes = (int) ((cacheTime % 3600) / 60);
int seconds = (int) (cacheTime % 60);
formatCacheTime = String.format("%02d小时%02d分钟%02d秒", hours, minutes, seconds);
}
}
return new SysCache(sysCache.getCacheName(), sysCache.getCacheKey(), cacheValue, formatCacheTime);
}
/**
* @description [删除Redis指定名称缓存]
* @author Leocoder
*/
@Operation(summary = "删除Redis指定名称缓存", description = "根据缓存名称前缀删除所有相关缓存")
@SaCheckPermission("monitor:cache:delete")
@PostMapping("/cache/deleteCacheName/{cacheName}")
public void deleteCacheName(@PathVariable("cacheName") String cacheName) {
Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
if (ObjectUtils.isNotEmpty(cacheKeys)) {
redisTemplate.delete(cacheKeys);
}
}
/**
* @description [删除Redis指定键名缓存]
* @author Leocoder
*/
@Operation(summary = "删除Redis指定键名缓存", description = "根据具体的缓存键名删除单个缓存")
@SaCheckPermission("monitor:cache:delete")
@PostMapping("/cache/deleteCacheKey")
public void deleteCacheKey(@RequestBody SysCache sysCache) {
if (StringUtils.isNotBlank(sysCache.getCacheKey())) {
redisTemplate.delete(sysCache.getCacheKey());
}
}
/**
* @description [删除Redis所有信息]
* @author Leocoder
*/
@Operation(summary = "删除Redis所有信息", description = "清空Redis中的所有缓存数据谨慎操作")
@SaCheckPermission("monitor:cache:clear")
@PostMapping("/cache/deleteCacheAll")
public void deleteCacheAll() {
Collection<String> cacheKeys = redisTemplate.keys("*");
if (ObjectUtils.isNotEmpty(cacheKeys)) {
redisTemplate.delete(cacheKeys);
}
}
}

View File

@ -0,0 +1,63 @@
package org.leocoder.heritage.monitor.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
/**
* @author Leocoder
* @description [Redis监控]
*/
@Tag(name = "Redis监控", description = "Redis数据库监控包括连接信息、内存使用、命令统计等")
@RestController
@RequestMapping("/coder/monitor")
@RequiredArgsConstructor
public class RedisController {
private final RedisTemplate<String, String> redisTemplate;
/**
* @description [获取Redis监控信息]
* @author Leocoder
*/
@Operation(summary = "获取Redis监控信息", description = "获取Redis服务器监控数据包括服务信息、内存使用、连接数、命令统计等")
@SaCheckPermission("monitor:redis:list")
@GetMapping("/redis/getRedisInformation")
public Map<String, Object> getRedisInformation() {
try {
Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
Map<String, Object> result = new HashMap<>(3);
result.put("info", info);
result.put("dbSize", dbSize);
List<Map<String, String>> pieList = new ArrayList<>();
if (commandStats != null) {
commandStats.stringPropertyNames().forEach(key -> {
Map<String, String> data = new HashMap<>(2);
String propertyValue = commandStats.getProperty(key);
data.put("name", StringUtils.removeStart(key, "cmdstat_"));
data.put("value", StringUtils.substringBetween(propertyValue, "calls=", ",usec"));
pieList.add(data);
});
}
result.put("commandStats", pieList);
return result;
} catch (Exception e) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("error", "获取Redis监控信息失败: " + e.getMessage());
return errorResult;
}
}
}

View File

@ -0,0 +1,34 @@
package org.leocoder.heritage.monitor.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.leocoder.heritage.monitor.pojo.server.Server;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Leocoder
* @description [服务器监控]
*/
@Tag(name = "服务器监控", description = "系统服务器资源监控包括CPU、内存、磁盘、JVM等信息")
@RestController
@RequestMapping("/coder/monitor")
@RequiredArgsConstructor
public class ServerController {
/**
* @description [获取服务器监控信息]
* @author Leocoder
*/
@Operation(summary = "获取服务器监控信息", description = "获取服务器实时监控数据包括CPU使用率、内存使用情况、磁盘空间、JVM状态等")
@SaCheckPermission("monitor:server:list")
@GetMapping("/server/getServerInformation")
public Server getServerInformation() {
Server server = new Server();
server.copyTo();
return server;
}
}

View File

@ -0,0 +1,120 @@
package org.leocoder.heritage.monitor.pojo.server;
import lombok.Data;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* @author Leocoder
* @description [CPU相关信息]
*/
@Data
public class Cpu {
/**
* 核心数
*/
private int cpuNum;
/**
* CPU总的使用率
*/
private double total;
/**
* CPU系统使用率
*/
private double sys;
/**
* CPU用户使用率
*/
private double used;
/**
* CPU当前等待率
*/
private double wait;
/**
* CPU当前空闲率
*/
private double free;
/**
* @description [获取CPU使用率]
* @author Leocoder
*/
public double getCpuUsage() {
if (total > 0) {
return multiply(divide(total - free, total, 4), 100);
}
return 0.0;
}
/**
* @description [获取CPU系统使用率]
* @author Leocoder
*/
public double getSysUsage() {
if (total > 0) {
return multiply(divide(sys, total, 4), 100);
}
return 0.0;
}
/**
* @description [获取CPU用户使用率]
* @author Leocoder
*/
public double getUserUsage() {
if (total > 0) {
return multiply(divide(used, total, 4), 100);
}
return 0.0;
}
/**
* @description [获取CPU等待率]
* @author Leocoder
*/
public double getWaitUsage() {
if (total > 0) {
return multiply(divide(wait, total, 4), 100);
}
return 0.0;
}
/**
* @description [获取CPU空闲率]
* @author Leocoder
*/
public double getFreeUsage() {
if (total > 0) {
return multiply(divide(free, total, 4), 100);
}
return 0.0;
}
/**
* 精确的除法运算
*/
private static double divide(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("精确度不能小于0");
}
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();
}
/**
* 精确的乘法运算
*/
private static double multiply(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}
}

View File

@ -0,0 +1,155 @@
package org.leocoder.heritage.monitor.pojo.server;
import lombok.Data;
import java.lang.management.ManagementFactory;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
/**
* @author Leocoder
* @description [JVM相关信息]
*/
@Data
public class Jvm {
/**
* 当前JVM占用的内存总数(M)
*/
private long total;
/**
* JVM最大可用内存总数(M)
*/
private long max;
/**
* JVM空闲内存(M)
*/
private long free;
/**
* JDK版本
*/
private String version;
/**
* JDK路径
*/
private String home;
/**
* @description [获取JVM已用内存]
* @author Leocoder
*/
public long getUsed() {
return total - free;
}
/**
* @description [获取JVM内存使用率]
* @author Leocoder
*/
public double getUsage() {
if (total > 0) {
return multiply(divide(getUsed(), total, 4), 100);
}
return 0.0;
}
/**
* @description [获取总内存格式化]
* @author Leocoder
*/
public String getTotalStr() {
return convertFileSize(total);
}
/**
* @description [获取已用内存格式化]
* @author Leocoder
*/
public String getUsedStr() {
return convertFileSize(getUsed());
}
/**
* @description [获取剩余内存格式化]
* @author Leocoder
*/
public String getFreeStr() {
return convertFileSize(free);
}
/**
* @description [获取最大内存格式化]
* @author Leocoder
*/
public String getMaxStr() {
return convertFileSize(max);
}
/**
* @description [获取JVM启动时间]
* @author Leocoder
*/
public String getStartTime() {
long startTime = ManagementFactory.getRuntimeMXBean().getStartTime();
return new Date(startTime).toString();
}
/**
* @description [获取JVM运行时间]
* @author Leocoder
*/
public String getRunTime() {
long runTime = ManagementFactory.getRuntimeMXBean().getUptime();
long day = runTime / (24 * 60 * 60 * 1000);
long hour = (runTime / (60 * 60 * 1000)) - (day * 24);
long minute = (runTime / (60 * 1000)) - (day * 24 * 60) - (hour * 60);
long second = (runTime / 1000) - (day * 24 * 60 * 60) - (hour * 60 * 60) - (minute * 60);
return String.format("%d天%d小时%d分钟%d秒", day, hour, minute, second);
}
/**
* 字节转换
*/
private String convertFileSize(long size) {
long kb = 1024;
long mb = kb * 1024;
long gb = mb * 1024;
if (size >= gb) {
return String.format("%.1f GB", (float) size / gb);
} else if (size >= mb) {
float f = (float) size / mb;
return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f);
} else if (size >= kb) {
float f = (float) size / kb;
return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f);
} else {
return String.format("%d B", size);
}
}
/**
* 精确的除法运算
*/
private static double divide(long v1, long v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("精确度不能小于0");
}
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();
}
/**
* 精确的乘法运算
*/
private static double multiply(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}
}

View File

@ -0,0 +1,105 @@
package org.leocoder.heritage.monitor.pojo.server;
import lombok.Data;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* @author Leocoder
* @description [内存相关信息]
*/
@Data
public class Mem {
/**
* 内存总量
*/
private long total;
/**
* 已用内存
*/
private long used;
/**
* 剩余内存
*/
private long free;
/**
* @description [获取内存使用率]
* @author Leocoder
*/
public double getUsage() {
if (total > 0) {
return multiply(divide(used, total, 4), 100);
}
return 0.0;
}
/**
* @description [获取总内存格式化]
* @author Leocoder
*/
public String getTotalStr() {
return convertFileSize(total);
}
/**
* @description [获取已用内存格式化]
* @author Leocoder
*/
public String getUsedStr() {
return convertFileSize(used);
}
/**
* @description [获取剩余内存格式化]
* @author Leocoder
*/
public String getFreeStr() {
return convertFileSize(free);
}
/**
* 字节转换
*/
private String convertFileSize(long size) {
long kb = 1024;
long mb = kb * 1024;
long gb = mb * 1024;
if (size >= gb) {
return String.format("%.1f GB", (float) size / gb);
} else if (size >= mb) {
float f = (float) size / mb;
return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f);
} else if (size >= kb) {
float f = (float) size / kb;
return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f);
} else {
return String.format("%d B", size);
}
}
/**
* 精确的除法运算
*/
private static double divide(long v1, long v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("精确度不能小于0");
}
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();
}
/**
* 精确的乘法运算
*/
private static double multiply(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}
}

View File

@ -0,0 +1,202 @@
package org.leocoder.heritage.monitor.pojo.server;
import lombok.Data;
import org.leocoder.heritage.common.utils.ip.IpUtil;
import oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
import oshi.hardware.CentralProcessor.TickType;
import oshi.hardware.GlobalMemory;
import oshi.hardware.HardwareAbstractionLayer;
import oshi.software.os.FileSystem;
import oshi.software.os.OSFileStore;
import oshi.software.os.OperatingSystem;
import oshi.util.Util;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
/**
* @author Leocoder
* @description [服务器相关信息]
*/
@Data
public class Server {
private static final int OSHI_WAIT_SECOND = 1000;
/**
* CPU相关信息
*/
private Cpu cpu = new Cpu();
/**
* 内存相关信息
*/
private Mem mem = new Mem();
/**
* JVM相关信息
*/
private Jvm jvm = new Jvm();
/**
* 服务器相关信息
*/
private Sys sys = new Sys();
/**
* 磁盘相关信息
*/
private List<SysFile> sysFiles = new LinkedList<>();
/**
* @description [获取服务器信息]
* @author Leocoder
*/
public void copyTo() {
try {
SystemInfo si = new SystemInfo();
HardwareAbstractionLayer hal = si.getHardware();
setCpuInfo(hal.getProcessor());
setMemInfo(hal.getMemory());
setSysInfo();
setJvmInfo();
setSysFiles(si.getOperatingSystem());
} catch (Exception e) {
System.err.println("获取服务器信息失败: " + e.getMessage());
}
}
/**
* 设置CPU信息
*/
private void setCpuInfo(CentralProcessor processor) {
// CPU信息
long[] prevTicks = processor.getSystemCpuLoadTicks();
Util.sleep(OSHI_WAIT_SECOND);
long[] ticks = processor.getSystemCpuLoadTicks();
long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()];
long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()];
long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()];
long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()];
long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()];
long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()];
long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()];
long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()];
long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;
cpu.setCpuNum(processor.getLogicalProcessorCount());
cpu.setTotal(totalCpu);
cpu.setSys(cSys);
cpu.setUsed(user);
cpu.setWait(iowait);
cpu.setFree(idle);
}
/**
* 设置内存信息
*/
private void setMemInfo(GlobalMemory memory) {
mem.setTotal(memory.getTotal());
mem.setUsed(memory.getTotal() - memory.getAvailable());
mem.setFree(memory.getAvailable());
}
/**
* 设置服务器信息
*/
private void setSysInfo() {
Properties props = System.getProperties();
sys.setComputerName(IpUtil.getHostName());
sys.setComputerIp(IpUtil.getHostIp());
sys.setOsName(props.getProperty("os.name"));
sys.setOsArch(props.getProperty("os.arch"));
sys.setUserDir(props.getProperty("user.dir"));
}
/**
* 设置Java虚拟机
*/
private void setJvmInfo() {
Properties props = System.getProperties();
jvm.setTotal(Runtime.getRuntime().totalMemory());
jvm.setMax(Runtime.getRuntime().maxMemory());
jvm.setFree(Runtime.getRuntime().freeMemory());
jvm.setVersion(props.getProperty("java.version"));
jvm.setHome(props.getProperty("java.home"));
}
/**
* 设置磁盘信息
*/
private void setSysFiles(OperatingSystem os) {
FileSystem fileSystem = os.getFileSystem();
List<OSFileStore> fsArray = fileSystem.getFileStores();
for (OSFileStore fs : fsArray) {
long free = fs.getUsableSpace();
long total = fs.getTotalSpace();
long used = total - free;
SysFile sysFile = new SysFile();
sysFile.setDirName(fs.getMount());
sysFile.setSysTypeName(fs.getType());
sysFile.setTypeName(fs.getName());
sysFile.setTotal(convertFileSize(total));
sysFile.setFree(convertFileSize(free));
sysFile.setUsed(convertFileSize(used));
if (total > 0) {
sysFile.setUsage(multiply(divide(used, total, 4), 100));
} else {
sysFile.setUsage(0.0);
}
sysFiles.add(sysFile);
}
}
/**
* 字节转换
*
* @param size 字节大小
* @return 转换后值
*/
public String convertFileSize(long size) {
long kb = 1024;
long mb = kb * 1024;
long gb = mb * 1024;
if (size >= gb) {
return String.format("%.1f GB", (float) size / gb);
} else if (size >= mb) {
float f = (float) size / mb;
return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f);
} else if (size >= kb) {
float f = (float) size / kb;
return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f);
} else {
return String.format("%d B", size);
}
}
/**
* 精确的除法运算
*/
private static double divide(long v1, long v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("精确度不能小于0");
}
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();
}
/**
* 精确的乘法运算
*/
private static double multiply(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}
}

View File

@ -0,0 +1,36 @@
package org.leocoder.heritage.monitor.pojo.server;
import lombok.Data;
/**
* @author Leocoder
* @description [系统相关信息]
*/
@Data
public class Sys {
/**
* 服务器名称
*/
private String computerName;
/**
* 服务器IP
*/
private String computerIp;
/**
* 项目路径
*/
private String userDir;
/**
* 操作系统
*/
private String osName;
/**
* 系统架构
*/
private String osArch;
}

View File

@ -0,0 +1,52 @@
package org.leocoder.heritage.monitor.pojo.server;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
/**
* @author Leocoder
* @description [缓存信息]
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysCache {
/**
* 缓存名称
*/
private String cacheName;
/**
* 缓存键名
*/
private String cacheKey;
/**
* 缓存内容
*/
private String cacheValue;
/**
* 缓存过期时间
*/
private String expireTime;
/**
* 备注信息
*/
private String remark;
public SysCache(String cacheName, String remark) {
this.cacheName = cacheName;
this.remark = remark;
}
public SysCache(String cacheName, String cacheKey, String cacheValue, String expireTime) {
this.cacheName = cacheName;
this.cacheKey = cacheKey;
this.cacheValue = cacheValue;
this.expireTime = expireTime;
}
}

View File

@ -0,0 +1,46 @@
package org.leocoder.heritage.monitor.pojo.server;
import lombok.Data;
/**
* @author Leocoder
* @description [系统文件相关信息]
*/
@Data
public class SysFile {
/**
* 盘符路径
*/
private String dirName;
/**
* 盘符类型
*/
private String sysTypeName;
/**
* 文件类型
*/
private String typeName;
/**
* 总大小
*/
private String total;
/**
* 剩余大小
*/
private String free;
/**
* 已经使用量
*/
private String used;
/**
* 资源的使用率
*/
private double usage;
}

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-backend</artifactId>
<version>${revision}</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<name>heritage-system</name>
<artifactId>heritage-system</artifactId>
<description>系统模块</description>
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- 数据统一返回、全局异常以及限流、数据脱敏、重复提交等插件 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-resultex</artifactId>
<version>${revision}</version>
</dependency>
<!-- Sa-Token组件 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-sa-token</artifactId>
<version>${revision}</version>
</dependency>
<!-- 操作日志组件 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-oper-logs</artifactId>
<version>${revision}</version>
</dependency>
<!-- SpringDoc OpenAPI 3.0 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- OSS对象存储模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-oss</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,31 @@
package org.leocoder.heritage.system.config.init;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.system.service.dictdata.SysDictDataService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 平常开发中有可能需要实现在项目启动后执行的功能SpringBoot提供的一种简单的实现方案就是添加一个model并实现CommandLineRunner接口
* 实现功能的代码放在实现的run方法中也就是项目一启动之后就立即需要执行的动作
* 多实现类执行顺序可以通过注解@Order控制实现类执行顺序,其中Order的值越小越先被执行
* @author Leocoder
*/
@Slf4j
@Order(1)
@Component
public class ServerCommandLineRunner implements CommandLineRunner {
@Resource
private SysDictDataService sysDictDataService;
@Override
public void run(String... args) {
// 逻辑代码[字典数据缓存]
sysDictDataService.listDictCacheRedis();
log.info("CommandLineRunner项目启动后立即执行重新获取缓存 => [推荐使用]");
}
}

View File

@ -0,0 +1,173 @@
package org.leocoder.heritage.system.controller.dashboard;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.domain.model.vo.system.DashboardStatisticsVo;
import org.leocoder.heritage.domain.model.vo.system.LoginTrendVo;
import org.leocoder.heritage.system.service.dashboard.DashboardService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 仪表盘统计控制器
*
* @author Leocoder
*/
@Tag(name = "仪表盘管理", description = "仪表盘统计数据接口")
@Slf4j
@Validated
@RestController
@RequestMapping("/coder/dashboard")
@RequiredArgsConstructor
public class DashboardController {
private final DashboardService dashboardService;
/**
* @description 获取仪表盘统计数据
* @author Leocoder
*/
@Operation(
summary = "获取仪表盘统计数据",
description = "获取用户、登录、存储、活跃度等核心统计数据"
)
@GetMapping("/getStatistics")
@SaCheckPermission("dashboard:view")
public DashboardStatisticsVo getStatistics() {
DashboardStatisticsVo statistics = dashboardService.getStatistics();
YUtil.isTrue(ObjectUtils.isEmpty(statistics), "获取仪表盘统计数据失败");
return statistics;
}
/**
* @description 获取登录趋势数据
* @author Leocoder
*/
@Operation(
summary = "获取登录趋势数据",
description = "获取最近N天的登录趋势图表数据"
)
@GetMapping("/getLoginTrend")
@SaCheckPermission("dashboard:view")
public LoginTrendVo getLoginTrend(
@Parameter(description = "查询天数", example = "7") @RequestParam(
defaultValue = "7"
) Integer days
) {
LoginTrendVo loginTrend = dashboardService.getLoginTrend(days);
YUtil.isTrue(ObjectUtils.isEmpty(loginTrend), "获取登录趋势数据失败");
return loginTrend;
}
/**
* @description 获取完整仪表盘数据
* @author Leocoder
*/
@Operation(
summary = "获取完整仪表盘数据",
description = "一次性获取所有仪表盘数据,减少前端请求次数"
)
@GetMapping("/getAllData")
@SaCheckPermission("dashboard:view")
public DashboardStatisticsVo getAllData(
@Parameter(
description = "是否包含趋势数据",
example = "true"
) @RequestParam(defaultValue = "true") Boolean includeTrend,
@Parameter(description = "趋势数据天数", example = "7") @RequestParam(
defaultValue = "7"
) Integer trendDays
) {
DashboardStatisticsVo allData = dashboardService.getAllData(
includeTrend,
trendDays
);
YUtil.isTrue(ObjectUtils.isEmpty(allData), "获取完整仪表盘数据失败");
return allData;
}
/**
* @description 获取用户统计数据
* @author Leocoder
*/
@Operation(
summary = "获取用户统计数据",
description = "获取用户相关的统计信息"
)
@GetMapping("/getUserStats")
@SaCheckPermission("dashboard:view")
public DashboardStatisticsVo.UserStatsVo getUserStats() {
DashboardStatisticsVo.UserStatsVo userStats =
dashboardService.getUserStats();
YUtil.isTrue(ObjectUtils.isEmpty(userStats), "获取用户统计数据失败");
return userStats;
}
/**
* @description 获取登录统计数据
* @author Leocoder
*/
@Operation(
summary = "获取登录统计数据",
description = "获取登录相关的统计信息"
)
@GetMapping("/getLoginStats")
@SaCheckPermission("dashboard:view")
public DashboardStatisticsVo.LoginStatsVo getLoginStats(
@Parameter(
description = "是否包含趋势数据",
example = "false"
) @RequestParam(defaultValue = "false") Boolean includeTrend,
@Parameter(description = "趋势数据天数", example = "7") @RequestParam(
defaultValue = "7"
) Integer trendDays
) {
DashboardStatisticsVo.LoginStatsVo loginStats =
dashboardService.getLoginStats(includeTrend, trendDays);
YUtil.isTrue(ObjectUtils.isEmpty(loginStats), "获取登录统计数据失败");
return loginStats;
}
/**
* @description 获取存储统计数据
* @author Leocoder
*/
@Operation(
summary = "获取存储统计数据",
description = "获取存储相关的统计信息"
)
@GetMapping("/getStorageStats")
@SaCheckPermission("dashboard:view")
public DashboardStatisticsVo.StorageStatsVo getStorageStats() {
DashboardStatisticsVo.StorageStatsVo storageStats =
dashboardService.getStorageStats();
YUtil.isTrue(ObjectUtils.isEmpty(storageStats), "获取存储统计数据失败");
return storageStats;
}
/**
* @description 获取今日活跃统计数据
* @author Leocoder
*/
@Operation(
summary = "获取今日活跃统计数据",
description = "获取今日活跃相关的统计信息"
)
@GetMapping("/getDailyActivityStats")
@SaCheckPermission("dashboard:view")
public DashboardStatisticsVo.DailyActivityStatsVo getDailyActivityStats() {
DashboardStatisticsVo.DailyActivityStatsVo activityStats =
dashboardService.getDailyActivityStats();
YUtil.isTrue(
ObjectUtils.isEmpty(activityStats),
"获取今日活跃统计数据失败"
);
return activityStats;
}
}

View File

@ -0,0 +1,239 @@
package org.leocoder.heritage.system.controller.dictdata;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.constants.CoderCacheConstants;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.common.utils.cache.RedisUtil;
import org.leocoder.heritage.domain.enums.oper.OperType;
import org.leocoder.heritage.domain.model.vo.system.SysDictDataVo;
import org.leocoder.heritage.domain.pojo.system.SysDictData;
import org.leocoder.heritage.operlog.annotation.OperLog;
import org.leocoder.heritage.system.service.dictdata.SysDictDataService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
/**
* @author Leocoder
* @description [字典数据表-控制层]
*/
@Tag(name = "字典数据管理", description = "系统字典数据的增删改查操作")
@Validated
@RequestMapping("/coder")
@RequiredArgsConstructor
@RestController
public class SysDictDataController {
private final SysDictDataService sysDictDataService;
private final RedisUtil redisUtil;
/**
* @description [分页查询]
* @author Leocoder
*/
@Operation(summary = "分页查询字典数据列表", description = "根据查询条件分页获取字典数据信息")
@SaCheckPermission("system:dict:list")
@GetMapping("/sysDictData/listPage")
public IPage<SysDictData> listPage(SysDictDataVo vo) {
// 分页构造器
Page<SysDictData> page = new Page<>(vo.getPageNo(), vo.getPageSize());
// 条件构造器
LambdaQueryWrapper<SysDictData> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.isNotBlank(vo.getDictType()), SysDictData::getDictType, vo.getDictType());
wrapper.like(StringUtils.isNotBlank(vo.getDictLabel()), SysDictData::getDictLabel, vo.getDictLabel());
wrapper.eq(StringUtils.isNotBlank(vo.getDictStatus()), SysDictData::getDictStatus, vo.getDictStatus());
wrapper.orderByDesc(SysDictData::getDictType);
wrapper.orderByAsc(SysDictData::getSorted);
// 进行分页查询
page = sysDictDataService.page(page, wrapper);
return page;
}
/**
* @description [查询所有]
* @author Leocoder
*/
@Operation(summary = "查询所有字典数据", description = "获取所有字典数据信息")
@SaCheckPermission("system:dict:list")
@GetMapping("/sysDictData/list")
public List<SysDictData> list() {
return sysDictDataService.list();
}
/**
* @description [根据主键进行查询]
* @author Leocoder
*/
@Operation(summary = "根据ID查询字典数据", description = "通过ID获取字典数据详细信息")
@GetMapping("/sysDictData/getById/{id}")
public SysDictData getById(@PathVariable Long id) {
return sysDictDataService.getById(id);
}
/**
* @description [新增]
* @author Leocoder
*/
@Operation(summary = "新增字典数据", description = "创建新的字典数据")
@SaCheckPermission("system:dict:add")
@PostMapping("/sysDictData/add")
@OperLog(value = "新增字典数据", operType = OperType.INSERT)
public void add(@Validated @RequestBody SysDictData sysDictData) {
// 查询是否已经存在字典名称
LambdaQueryWrapper<SysDictData> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysDictData::getDictLabel, sysDictData.getDictLabel());
wrapper.eq(SysDictData::getDictType, sysDictData.getDictType());
long count = sysDictDataService.count(wrapper);
YUtil.isTrue(count > 0, "该字典名称已存在,请重新输入");
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
sysDictData.setCreateBy(CoderLoginUtil.getUserName());
}
YUtil.isTrue(!sysDictDataService.save(sysDictData), "新增失败,请稍后重试");
// 同步缓存
sysDictDataService.listDictCacheRedis();
}
/**
* @description [获取最新排序]
* @author Leocoder
*/
@Operation(summary = "获取最新排序", description = "根据字典类型获取最新的排序号")
@GetMapping("/sysDictData/getSorted/{dictType}")
public int getSorted(@PathVariable("dictType") String dictType) {
YUtil.isTrue(StringUtils.isBlank(dictType), "请传递参数");
LambdaQueryWrapper<SysDictData> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.select(SysDictData::getSorted);
lambdaQueryWrapper.eq(SysDictData::getDictType, dictType);
lambdaQueryWrapper.orderByDesc(SysDictData::getSorted);
lambdaQueryWrapper.last("LIMIT 1");
SysDictData sysDictData = sysDictDataService.getOne(lambdaQueryWrapper);
if (ObjectUtils.isEmpty(sysDictData)) return CoderConstants.ONE_NUMBER;
return sysDictData.getSorted() != null ? sysDictData.getSorted() + CoderConstants.ONE_NUMBER : CoderConstants.ONE_NUMBER;
}
/**
* @description [修改]
* @author Leocoder
*/
@Operation(summary = "修改字典数据", description = "更新字典数据信息")
@SaCheckPermission("system:dict:update")
@PostMapping("/sysDictData/update")
@OperLog(value = "修改字典数据", operType = OperType.UPDATE)
public void update(@Validated @RequestBody SysDictData sysDictData) {
// 查询是否已经存在字典名称
LambdaQueryWrapper<SysDictData> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysDictData::getDictLabel, sysDictData.getDictLabel());
wrapper.eq(SysDictData::getDictType, sysDictData.getDictType());
SysDictData dictData = sysDictDataService.getOne(wrapper);
YUtil.isTrue(ObjectUtils.isNotEmpty(dictData) && !dictData.getDictId().equals(sysDictData.getDictId()), "该字典名称已存在,请重新输入");
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
sysDictData.setUpdateBy(CoderLoginUtil.getUserName());
}
YUtil.isTrue(!sysDictDataService.updateById(sysDictData), "修改失败,请稍后重试");
// 同步缓存
sysDictDataService.listDictCacheRedis();
}
/**
* @description [删除]
* @author Leocoder
*/
@Operation(summary = "删除字典数据", description = "根据ID删除字典数据")
@SaCheckPermission("system:dict:delete")
@PostMapping("/sysDictData/deleteById/{id}")
@OperLog(value = "删除字典数据", operType = OperType.DELETE)
public void delete(@PathVariable Long id) {
YUtil.isTrue(!sysDictDataService.removeById(id), "删除失败,请稍后重试");
// 同步缓存
sysDictDataService.listDictCacheRedis();
}
/**
* @description [批量删除]
* @author Leocoder
*/
@Operation(summary = "批量删除字典数据", description = "根据ID列表批量删除字典数据")
@SaCheckPermission("system:dict:delete")
@PostMapping("/sysDictData/batchDelete")
@OperLog(value = "批量删除字典数据", operType = OperType.DELETE)
public void batchDelete(@NotNull(message = "请选择需要删除的数据") @RequestBody List<Long> ids) {
YUtil.isTrue(!sysDictDataService.removeBatchByIds(ids), "批量删除失败,请稍后重试");
// 同步缓存
sysDictDataService.listDictCacheRedis();
}
/**
* @description [修改状态]
* @author Leocoder
*/
@Operation(summary = "修改字典数据状态", description = "启用或停用字典数据")
@SaCheckPermission("system:dict:update")
@PostMapping("/sysDictData/updateStatus/{dictId}/{dictStatus}")
@OperLog(value = "修改字典数据状态", operType = OperType.UPDATE)
public void updateStatus(@PathVariable("dictId") Long dictId, @PathVariable("dictStatus") String dictStatus) {
UpdateWrapper<SysDictData> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("dict_status", dictStatus).eq("dict_id", dictId);
YUtil.isTrue(!sysDictDataService.update(updateWrapper), "修改失败,请稍后重试");
// 同步缓存
sysDictDataService.listDictCacheRedis();
}
/**
* @description [根据类型查询字典数据]
* @author Leocoder
*/
@Operation(summary = "根据类型查询字典数据", description = "通过字典类型获取对应的字典数据列表")
@GetMapping("/sysDictData/listDataByType/{dictType}")
public List<SysDictData> listDataByType(@PathVariable("dictType") String dictType) {
Boolean isExist = redisUtil.hasKey(CoderCacheConstants.DICT_REDIS_KEY + dictType);
if (!isExist) {
LambdaQueryWrapper<SysDictData> wrapper = new LambdaQueryWrapper<>();
// 查询指定字段
wrapper.select(SysDictData::getDictLabel, SysDictData::getDictValue, SysDictData::getDictType, SysDictData::getDictTag, SysDictData::getDictColor);
wrapper.eq(StringUtils.isNotBlank(dictType), SysDictData::getDictType, dictType);
wrapper.eq(SysDictData::getDictStatus, CoderConstants.ZERO_STRING);
List<SysDictData> dictDataList = sysDictDataService.list(wrapper);
if (CollectionUtils.isNotEmpty(dictDataList)) {
return dictDataList;
} else {
return Collections.emptyList();
}
} else {
List<SysDictData> redisDictData = redisUtil.getKey(CoderCacheConstants.DICT_REDIS_KEY + dictType);
if (CollectionUtils.isNotEmpty(redisDictData)) {
return redisDictData;
} else {
return Collections.emptyList();
}
}
}
/**
* @description [字典数据同步Redis进行缓存]
* @author Leocoder
*/
@Operation(summary = "同步字典缓存", description = "手动同步所有字典数据到Redis缓存")
@SaCheckPermission("system:dict:update")
@GetMapping("/sysDictData/listDictCacheRedis")
@OperLog(value = "字典数据同步缓存", operType = OperType.UPDATE)
public void listDictCacheRedis() {
sysDictDataService.listDictCacheRedis();
}
}

View File

@ -0,0 +1,234 @@
package org.leocoder.heritage.system.controller.dicttype;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.constants.CoderCacheConstants;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.common.utils.cache.RedisUtil;
import org.leocoder.heritage.domain.enums.oper.OperType;
import org.leocoder.heritage.domain.model.vo.system.SysDictTypeVo;
import org.leocoder.heritage.domain.pojo.system.SysDictData;
import org.leocoder.heritage.domain.pojo.system.SysDictType;
import org.leocoder.heritage.operlog.annotation.OperLog;
import org.leocoder.heritage.system.service.dictdata.SysDictDataService;
import org.leocoder.heritage.system.service.dicttype.SysDictTypeService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
/**
* @author Leocoder
* @description [字典类型表-控制层]
*/
@Tag(name = "字典类型管理", description = "系统字典类型的增删改查操作")
@Validated
@RequestMapping("/coder")
@RequiredArgsConstructor
@RestController
public class SysDictTypeController {
private final SysDictTypeService sysDictTypeService;
private final SysDictDataService sysDictDataService;
private final RedisUtil redisUtil;
/**
* @description [分页查询]
* @author Leocoder
*/
@Operation(summary = "分页查询字典类型列表", description = "根据查询条件分页获取字典类型信息")
@SaCheckPermission("system:dict:list")
@GetMapping("/sysDictType/listPage")
public IPage<SysDictType> listPage(SysDictTypeVo vo) {
// 分页构造器
Page<SysDictType> page = new Page<>(vo.getPageNo(), vo.getPageSize());
// 条件构造器
LambdaQueryWrapper<SysDictType> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(vo.getDictName()), SysDictType::getDictName, vo.getDictName());
wrapper.like(StringUtils.isNotBlank(vo.getDictType()), SysDictType::getDictType, vo.getDictType());
wrapper.eq(StringUtils.isNotBlank(vo.getDictStatus()), SysDictType::getDictStatus, vo.getDictStatus());
wrapper.orderByDesc(SysDictType::getCreateTime);
// 进行分页查询
page = sysDictTypeService.page(page, wrapper);
return page;
}
/**
* @description [查询所有]
* @author Leocoder
*/
@Operation(summary = "查询所有字典类型", description = "获取所有字典类型信息")
@SaCheckPermission("system:dict:list")
@GetMapping("/sysDictType/list")
public List<SysDictType> list() {
return sysDictTypeService.list();
}
/**
* @description [根据主键查询]
* @author Leocoder
*/
@Operation(summary = "根据ID查询字典类型", description = "通过ID获取字典类型详细信息")
@GetMapping("/sysDictType/getById/{id}")
public SysDictType getById(@PathVariable Long id) {
return sysDictTypeService.getById(id);
}
/**
* @description [新增]
* @author Leocoder
*/
@Operation(summary = "新增字典类型", description = "创建新的字典类型")
@SaCheckPermission("system:dict:add")
@PostMapping("/sysDictType/add")
@OperLog(value = "新增字典类型", operType = OperType.INSERT)
public void add(@Validated @RequestBody SysDictType sysDictType) {
// 查询是否已经存在字典名称
LambdaQueryWrapper<SysDictType> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysDictType::getDictType, sysDictType.getDictType());
long count = sysDictTypeService.count(wrapper);
YUtil.isTrue(count > 0, "该字典类型已存在,请重新输入");
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
sysDictType.setCreateBy(CoderLoginUtil.getUserName());
}
YUtil.isTrue(!sysDictTypeService.save(sysDictType), "新增失败,请稍后重试");
// 同步缓存
sysDictDataService.listDictCacheRedis();
}
/**
* @description [修改]
* @author Leocoder
*/
@Operation(summary = "修改字典类型", description = "更新字典类型信息")
@SaCheckPermission("system:dict:update")
@PostMapping("/sysDictType/update")
@OperLog(value = "修改字典类型", operType = OperType.UPDATE)
public void update(@Validated @RequestBody SysDictType sysDictType) {
// 根据ID进行查询用来同步修改字典数据类型
SysDictType sysDictTypeModel = sysDictTypeService.getById(sysDictType.getDictId());
// 查询是否已经存在字典名称
LambdaQueryWrapper<SysDictType> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysDictType::getDictType, sysDictType.getDictType());
SysDictType dictType = sysDictTypeService.getOne(wrapper);
YUtil.isTrue(ObjectUtils.isNotEmpty(dictType) && !dictType.getDictId().equals(sysDictType.getDictId()), "该字典类型已存在,请重新输入");
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
sysDictType.setUpdateBy(CoderLoginUtil.getUserName());
}
YUtil.isTrue(!sysDictTypeService.updateById(sysDictType), "修改失败,请稍后重试");
if(!sysDictTypeModel.getDictType().equals(sysDictType.getDictType())) {
// 将字典数据的类型也同步修改
LambdaUpdateWrapper<SysDictData> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysDictData::getDictType, sysDictType.getDictType());
updateWrapper.eq(SysDictData::getDictType, sysDictTypeModel.getDictType());
sysDictDataService.update(updateWrapper);
}
// 先删除该缓存
redisUtil.deleteKey(CoderCacheConstants.DICT_REDIS_KEY + sysDictTypeModel.getDictType());
// 同步缓存
sysDictDataService.listDictCacheRedis();
}
/**
* @description [删除]
* @author Leocoder
*/
@Operation(summary = "删除字典类型", description = "根据ID删除字典类型及其关联的字典数据")
@SaCheckPermission("system:dict:delete")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/sysDictType/deleteById/{id}")
@OperLog(value = "删除字典类型", operType = OperType.DELETE)
public void delete(@PathVariable("id") Long id) {
SysDictType sysDictType = sysDictTypeService.getById(id);
YUtil.isTrue(ObjectUtils.isEmpty(sysDictType) || StringUtils.isBlank(sysDictType.getDictType()), "请检查该数据是否存在");
LambdaQueryWrapper<SysDictData> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysDictData::getDictType, sysDictType.getDictType());
// 删除字典详情
sysDictDataService.remove(wrapper);
YUtil.isTrue(!sysDictTypeService.removeById(id), "删除失败,请稍后重试");
// 删除缓存
redisUtil.deleteKey(CoderCacheConstants.DICT_REDIS_KEY + sysDictType.getDictType());
// 同步缓存
sysDictDataService.listDictCacheRedis();
}
/**
* @description [批量删除]
* @author Leocoder
*/
@Operation(summary = "批量删除字典类型", description = "根据ID列表批量删除字典类型")
@SaCheckPermission("system:dict:delete")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/sysDictType/batchDelete")
@OperLog(value = "批量删除字典类型", operType = OperType.DELETE)
public void batchDelete(@NotNull(message = "请选择需要删除的数据") @RequestBody List<Long> ids) {
if (CollectionUtil.isNotEmpty(ids)) {
for (Long id : ids) {
SysDictType sysDictType = sysDictTypeService.getById(id);
YUtil.isTrue(ObjectUtils.isEmpty(sysDictType) || StringUtils.isBlank(sysDictType.getDictType()), "请检查该数据是否存在");
LambdaQueryWrapper<SysDictData> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysDictData::getDictType, sysDictType.getDictType());
YUtil.isTrue(!sysDictDataService.remove(wrapper), "删除失败,请稍后重试");
// 删除缓存
redisUtil.deleteKey(CoderCacheConstants.DICT_REDIS_KEY + sysDictType.getDictType());
}
}
YUtil.isTrue(!sysDictTypeService.removeBatchByIds(ids), "批量删除失败,请稍后重试");
// 同步缓存
sysDictDataService.listDictCacheRedis();
}
/**
* @description [修改状态]
* @author Leocoder
*/
@Operation(summary = "修改字典类型状态", description = "启用或停用字典类型")
@SaCheckPermission("system:dict:update")
@PostMapping("/sysDictType/updateStatus/{dictId}/{dictStatus}")
@OperLog(value = "修改字典类型状态", operType = OperType.UPDATE)
public void updateStatus(@PathVariable("dictId") Long dictId, @PathVariable("dictStatus") String dictStatus) {
UpdateWrapper<SysDictType> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("dict_status", dictStatus).eq("dict_id", dictId);
YUtil.isTrue(!sysDictTypeService.update(updateWrapper), "修改失败,请稍后重试");
// 同步缓存
sysDictDataService.listDictCacheRedis();
}
/**
* @description [查询字典类型下拉框]
* @author Leocoder
*/
@Operation(summary = "查询字典类型下拉框", description = "获取启用状态的字典类型列表,用于下拉选择")
@GetMapping("/sysDictType/listDictType")
public List<SysDictType> listDictType() {
LambdaQueryWrapper<SysDictType> wrapper = new LambdaQueryWrapper<>();
// 按需加载
wrapper.select(SysDictType::getDictType, SysDictType::getDictName);
wrapper.eq(SysDictType::getDictStatus, CoderConstants.ZERO_STRING);
List<SysDictType> dictTypeList = sysDictTypeService.list(wrapper);
if (CollectionUtils.isNotEmpty(dictTypeList)) {
return dictTypeList;
} else {
return Collections.emptyList();
}
}
}

View File

@ -0,0 +1,436 @@
package org.leocoder.heritage.system.controller.file;
import cn.dev33.satoken.annotation.SaIgnore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.exception.BusinessException;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.common.utils.file.FileTypeUtil;
import org.leocoder.heritage.common.utils.ip.IpUtil;
import org.leocoder.heritage.common.utils.ip.ServletUtil;
import org.leocoder.heritage.domain.pojo.system.SysFile;
import org.leocoder.heritage.domain.pojo.system.SysPicture;
import org.leocoder.heritage.oss.service.StorageService;
import org.leocoder.heritage.oss.service.StorageServiceFactory;
import org.leocoder.heritage.oss.utils.OssUtil;
import org.leocoder.heritage.system.service.file.SysFileService;
import org.leocoder.heritage.system.service.picture.SysPictureService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [FileController]
*/
@Tag(name = "文件管理", description = "文件上传、下载、删除等操作")
@Slf4j
@RequestMapping("/coder")
@RequiredArgsConstructor
@RestController
public class FileController {
@Value("${coder.filePath}")
private String basePath;
@Value("${coder.storage.type:local}")
private String storageType;
// 允许的图片文件扩展名
private static final List<String> ALLOWED_IMAGE_EXTENSIONS = Arrays.asList(
"jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"
);
// 允许的文档文件扩展名
private static final List<String> ALLOWED_DOCUMENT_EXTENSIONS = Arrays.asList(
"doc", "docx", "pdf", "txt", "xls", "xlsx", "ppt", "md", "zip", "rar", "7z"
);
// 文件大小限制字节
// 1MB
private static final long MAX_FILE_SIZE_1MB = 1048576L;
// 2MB
private static final long MAX_FILE_SIZE_2MB = 2097152L;
// 5MB
private static final long MAX_FILE_SIZE_5MB = 5242880L;
// 10MB
private static final long MAX_FILE_SIZE_10MB = 10485760L;
private final Environment env;
private final SysFileService sysFileService;
private final SysPictureService sysPictureService;
private final StorageServiceFactory storageServiceFactory;
/**
* @param fileSize 文件大小
* @param folderName 上传指定文件夹名称
* @description [上传单文件-需在WebConfig配置静态资源]
* @author Leocoder
*/
@Operation(summary = "上传文件", description = "上传单个文件到服务器")
@PostMapping("/file/uploadFile/{fileSize}/{folderName}/{fileParam}")
public Map<String, Object> uploadSingleFile(
@RequestParam("file") MultipartFile file,
@PathVariable("fileSize") Integer fileSize,
@PathVariable("folderName") String folderName,
@PathVariable("fileParam") String fileParam,
@RequestParam(value = "storageType", required = false) String requestStorageType) {
// 文件预检查
validateUploadFile(file, fileSize, folderName);
// 确定要使用的存储类型
String requestedStorageType = determineStorageType(requestStorageType);
Map<String, Object> fileMap;
try {
// 根据确定的存储类型选择存储服务
StorageService storageService = storageServiceFactory.getStorageService(requestedStorageType);
// 生成唯一文件名
String fileName = OssUtil.generateUniqueFileName(file.getOriginalFilename());
// 构建文件夹路径
String folderPath = OssUtil.buildFolderPath(folderName, CoderLoginUtil.getLoginName());
// 上传文件
fileMap = storageService.uploadFile(file, fileName, folderPath);
log.info("文件上传成功: requestedStorageType={}, fileName={}, filePath={}",
requestedStorageType, fileName, fileMap.get("filePath"));
} catch (Exception e) {
// 降级到本地存储
try {
StorageService localService = storageServiceFactory.getStorageService("local");
String fileName = OssUtil.generateUniqueFileName(file.getOriginalFilename());
String folderPath = OssUtil.buildFolderPath(folderName, CoderLoginUtil.getLoginName());
fileMap = localService.uploadFile(file, fileName, folderPath);
log.warn("使用本地存储降级成功: fileName={}", fileName);
} catch (Exception ex) {
log.error("本地存储降级也失败", ex);
throw new BusinessException(500, "文件上传失败: " + ex.getMessage());
}
}
// 获取实际使用的存储服务类型
String actualStorageType = determineActualStorageType(fileMap);
// 统一保存到文件表便于文件管理页面统一显示
saveUploadFilesInformation(fileMap, actualStorageType, CoderConstants.TRUE);
// 如果是图片同时保存到图库表保持图库管理功能
if (CoderConstants.PICTURES.equals(folderName)) {
saveUploadPicturesInformation(fileMap, fileParam, actualStorageType, CoderConstants.TRUE);
}
return fileMap;
}
/**
* @description [保存上传图片信息]
* @author Leocoder
*/
private void saveUploadPicturesInformation(Map<String, Object> fileMap, String fileParam, String storageServiceType, boolean isCreateBy) {
log.info("图库上传 ->");
// 新增文件信息
SysPicture sysPicture = new SysPicture();
sysPicture.setPictureName(fileMap.get("fileName").toString());
sysPicture.setNewName(fileMap.get("newName").toString());
sysPicture.setPictureSize(fileMap.get("fileSize").toString());
sysPicture.setPictureSuffix(fileMap.get("suffixName").toString());
sysPicture.setPictureUpload(fileMap.get("filePath").toString());
sysPicture.setPictureService(storageServiceType);
// 设置文件访问路径
String fileUploadPath = (String) fileMap.get("fileUploadPath");
if (isFullUrl(fileUploadPath)) {
// 如果已经是完整URL如OSSMinIO直接使用
sysPicture.setPicturePath(fileUploadPath);
} else {
// 如果是相对路径如本地存储构建完整URL
String protocol = IpUtil.getProtocol(ServletUtil.getRequest());
if (StringUtils.isBlank(protocol)) {
protocol = "http";
}
String hostIp = IpUtil.getHostIp(ServletUtil.getRequest());
String hostPort = StringUtils.isNotBlank(env.getProperty("server.port")) ? env.getProperty("server.port") : "18099";
String fullUrl = protocol + "://" + hostIp + ":" + hostPort + fileUploadPath;
sysPicture.setPicturePath(fullUrl);
}
log.info("图片回显地址:{}", sysPicture.getPicturePath());
if (CoderConstants.MINUS_ONE_STRING.equals(fileParam)) {
sysPicture.setPictureType("9");
} else {
sysPicture.setPictureType(fileParam);
}
if (isCreateBy) {
sysPicture.setCreateBy(CoderLoginUtil.getUserName());
}
sysPictureService.save(sysPicture);
}
/**
* 保存文件信息到数据库
*/
private void saveUploadFilesInformation(Map<String, Object> fileMap, String storageType, Boolean isCreateBy) {
SysFile sysFile = new SysFile();
sysFile.setFileName(fileMap.get("fileName").toString());
sysFile.setNewName(fileMap.get("newName").toString());
sysFile.setFileSize(fileMap.get("fileSize").toString());
sysFile.setFileSuffix(fileMap.get("suffixName").toString());
sysFile.setFileUpload(fileMap.get("filePath").toString());
sysFile.setFileService(storageType);
// 判断是否为完整URL如OSSMinIO存储
String fileUploadPath = (String) fileMap.get("fileUploadPath");
if (isFullUrl(fileUploadPath)) {
// 如果已经是完整URL如OSSMinIO直接使用
sysFile.setFilePath(fileUploadPath);
} else {
// 如果是相对路径如本地存储构建完整URL
String protocol = IpUtil.getProtocol(ServletUtil.getRequest());
if (StringUtils.isBlank(protocol)) {
protocol = "http";
}
String hostIp = IpUtil.getHostIp(ServletUtil.getRequest());
String hostPort = StringUtils.isNotBlank(env.getProperty("server.port")) ? env.getProperty("server.port") : "18099";
String fullUrl = protocol + "://" + hostIp + ":" + hostPort + fileUploadPath;
sysFile.setFilePath(fullUrl);
}
log.info("文件回显地址:{}", sysFile.getFilePath());
String fileType = FileTypeUtil.checkFileExtension(fileMap.get("suffixName").toString());
sysFile.setFileType(fileType);
if (isCreateBy) {
sysFile.setCreateBy(CoderLoginUtil.getUserName());
}
sysFileService.save(sysFile);
}
/**
* 判断字符串是否为完整URL
*/
private boolean isFullUrl(String url) {
return StringUtils.isNotBlank(url) && (url.startsWith("http://") || url.startsWith("https://"));
}
/**
* 确定要使用的存储类型
*/
private String determineStorageType(String requestStorageType) {
// 如果前端传递了存储类型优先使用前端选择的类型
if (StringUtils.isNotBlank(requestStorageType)) {
String normalizedType = requestStorageType.trim().toUpperCase();
// 将前端选择的类型转换为实际的存储服务类型
switch (normalizedType) {
case "LOCAL":
return "local";
case "OSS":
return "oss";
case "MINIO":
return "minio";
default:
log.warn("未知的存储类型: {}, 使用默认配置", requestStorageType);
break;
}
}
// 如果前端没有传递存储类型或类型无效使用配置文件中的默认值
return storageType;
}
/**
* 确定实际使用的存储服务类型
*/
private String determineActualStorageType(Map<String, Object> fileMap) {
String fileUploadPath = (String) fileMap.get("fileUploadPath");
// 根据返回的文件路径判断实际使用的存储类型
if (isFullUrl(fileUploadPath)) {
// 如果是完整URL检查具体的存储类型
if (fileUploadPath.contains(".aliyuncs.com") || fileUploadPath.contains("oss-")) {
// 阿里云OSS
return "3";
} else if (fileUploadPath.contains("minio.leocoder.cn") ||
fileUploadPath.contains("/coder-files/") ||
fileUploadPath.contains("X-Amz-Algorithm")) {
// MinIO存储支持多种识别方式
return "2";
}
}
// 默认为本地存储
return "1";
}
/**
* @param fileSize 文件大小
* @param folderName 上传指定文件夹名称
* @description [上传单文件-需在WebConfig配置静态资源]
* @author Leocoder
*/
@Operation(summary = "匿名上传文件", description = "匿名上传单个文件到服务器,无需登录认证")
@SaIgnore
@PostMapping("/file/uploadAnyFile/{fileSize}/{folderName}/{fileParam}")
public Map<String, Object> uploadAnyFile(
@RequestParam("file") MultipartFile file,
@PathVariable("fileSize") Integer fileSize,
@PathVariable("folderName") String folderName,
@PathVariable("fileParam") String fileParam,
@RequestParam(value = "storageType", required = false) String requestStorageType) {
// 文件预检查
validateUploadFile(file, fileSize, folderName);
// 确定要使用的存储类型
String requestedStorageType = determineStorageType(requestStorageType);
Map<String, Object> fileMap;
try {
// 根据确定的存储类型选择存储服务
StorageService storageService = storageServiceFactory.getStorageService(requestedStorageType);
// 生成唯一文件名
String fileName = OssUtil.generateUniqueFileName(file.getOriginalFilename());
// 构建文件夹路径匿名上传不包含用户名
String folderPath = folderName;
// 上传文件
fileMap = storageService.uploadFile(file, fileName, folderPath);
log.info("匿名文件上传成功: requestedStorageType={}, fileName={}, filePath={}",
requestedStorageType, fileName, fileMap.get("filePath"));
} catch (Exception e) {
log.error("匿名文件上传失败,尝试使用本地存储降级", e);
// 降级到本地存储
try {
StorageService localService = storageServiceFactory.getStorageService("local");
String fileName = OssUtil.generateUniqueFileName(file.getOriginalFilename());
String folderPath = folderName;
fileMap = localService.uploadFile(file, fileName, folderPath);
log.warn("匿名上传使用本地存储降级成功: fileName={}", fileName);
} catch (Exception ex) {
log.error("本地存储降级也失败", ex);
throw new BusinessException(500, "文件上传失败: " + ex.getMessage());
}
}
// 获取实际使用的存储服务类型
String actualStorageType = determineActualStorageType(fileMap);
// 统一保存到文件表
saveUploadFilesInformation(fileMap, actualStorageType, false);
// 如果是图片同时保存到图库表
if (CoderConstants.PICTURES.equals(folderName)) {
saveUploadPicturesInformation(fileMap, fileParam, actualStorageType, false);
}
return fileMap;
}
/**
* @description [文件上传预检查]
* @author Leocoder
*/
private void validateUploadFile(MultipartFile file, Integer fileSizeLimit, String folderName) {
// 检查文件是否为空
if (file == null || file.isEmpty()) {
throw new BusinessException(400, "请选择要上传的文件");
}
// 检查文件大小是否超限
long fileSize = file.getSize();
long maxSizeBytes = convertMBToBytes(fileSizeLimit);
if (fileSize > maxSizeBytes) {
String sizeMsg = fileSizeLimit + "MB";
throw new BusinessException(413, "文件大小超出限制,最大允许上传" + sizeMsg + "的文件");
}
// 获取文件扩展名
String originalFilename = file.getOriginalFilename();
if (StringUtils.isBlank(originalFilename)) {
throw new BusinessException(400, "文件名不能为空");
}
String fileExtension = getFileExtension(originalFilename).toLowerCase();
if (StringUtils.isBlank(fileExtension)) {
throw new BusinessException(400, "文件必须有扩展名");
}
// 根据文件夹类型检查文件格式
if (CoderConstants.PICTURES.equals(folderName)) {
// 图片文件检查
if (!ALLOWED_IMAGE_EXTENSIONS.contains(fileExtension)) {
throw new BusinessException(400, "不支持的图片格式,仅支持:" + String.join(", ", ALLOWED_IMAGE_EXTENSIONS));
}
} else {
// 文档文件检查 - 允许图片和文档类型
if (!ALLOWED_IMAGE_EXTENSIONS.contains(fileExtension) && !ALLOWED_DOCUMENT_EXTENSIONS.contains(fileExtension)) {
List<String> allAllowedExtensions = new ArrayList<>();
allAllowedExtensions.addAll(ALLOWED_IMAGE_EXTENSIONS);
allAllowedExtensions.addAll(ALLOWED_DOCUMENT_EXTENSIONS);
throw new BusinessException(400, "不支持的文件格式,仅支持:" + String.join(", ", allAllowedExtensions));
}
}
log.info("文件验证通过:文件名={}, 大小={}bytes, 类型={}", originalFilename, fileSize, fileExtension);
}
/**
* @description [将MB转换为字节]
* @author Leocoder
*/
private long convertMBToBytes(Integer fileSizeMB) {
if (fileSizeMB == null || fileSizeMB <= 0) {
// 默认2MB
return MAX_FILE_SIZE_2MB;
}
return fileSizeMB * 1024L * 1024L;
}
/**
* @description [获取文件扩展名]
* @author Leocoder
*/
private String getFileExtension(String filename) {
if (StringUtils.isBlank(filename)) {
return "";
}
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
return "";
}
return filename.substring(lastDotIndex + 1);
}
}

View File

@ -0,0 +1,203 @@
package org.leocoder.heritage.system.controller.file;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.common.utils.file.FileUtil;
import org.leocoder.heritage.domain.enums.oper.OperType;
import org.leocoder.heritage.domain.model.vo.system.SysFileVo;
import org.leocoder.heritage.domain.pojo.system.SysFile;
import org.leocoder.heritage.operlog.annotation.OperLog;
import org.leocoder.heritage.oss.service.StorageService;
import org.leocoder.heritage.oss.service.StorageServiceFactory;
import org.leocoder.heritage.system.service.file.SysFileService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author Leocoder
* @description [文件资源表-控制层]
*/
@Tag(name = "系统文件管理", description = "系统文件资源的增删改查操作")
@Validated
@RequestMapping("/coder")
@RequiredArgsConstructor
@RestController
@Slf4j
public class SysFileController {
private final SysFileService sysFileService;
private final StorageServiceFactory storageServiceFactory;
/**
* @description [分页查询]
* @author Leocoder
*/
@Operation(summary = "分页查询文件列表", description = "根据查询条件分页获取系统文件信息")
@SaCheckPermission("system:file:list")
@GetMapping("/sysFile/listPage")
public IPage<SysFile> listPage(SysFileVo vo) {
// 分页构造器
Page<SysFile> page = new Page<>(vo.getPageNo(), vo.getPageSize());
// 条件构造器
LambdaQueryWrapper<SysFile> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(vo.getFileName()), SysFile::getFileName, vo.getFileName());
wrapper.like(StringUtils.isNotBlank(vo.getFileSuffix()), SysFile::getFileSuffix, vo.getFileSuffix());
wrapper.eq(StringUtils.isNotBlank(vo.getFileService()), SysFile::getFileService, vo.getFileService());
wrapper.eq(StringUtils.isNotBlank(vo.getFileType()) && !CoderConstants.ZERO_STRING.equals(vo.getFileType()), SysFile::getFileType, vo.getFileType());
wrapper.orderByDesc(SysFile::getFileId);
// 进行分页查询
page = sysFileService.page(page, wrapper);
return page;
}
/**
* @description [查询所有]
* @author Leocoder
*/
@Operation(summary = "查询所有文件", description = "获取系统中所有文件信息")
@SaCheckPermission("system:file:list")
@GetMapping("/sysFile/list")
public List<SysFile> list(SysFileVo vo) {
// 条件构造器
LambdaQueryWrapper<SysFile> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(vo.getFileName()), SysFile::getFileName, vo.getFileName());
wrapper.like(StringUtils.isNotBlank(vo.getFileSuffix()), SysFile::getFileSuffix, vo.getFileSuffix());
wrapper.eq(StringUtils.isNotBlank(vo.getFileService()), SysFile::getFileService, vo.getFileService());
return sysFileService.list(wrapper);
}
/**
* @description [查询一个]
* @author Leocoder
*/
@Operation(summary = "根据ID查询文件", description = "根据文件ID获取文件详细信息")
@GetMapping("/sysFile/getById/{id}")
public SysFile getById(@PathVariable Long id) {
return sysFileService.getById(id);
}
/**
* @description [新增]
* @author Leocoder
*/
@Operation(summary = "新增文件记录", description = "添加新的文件资源记录")
@SaCheckPermission("system:file:add")
@PostMapping("/sysFile/add")
@OperLog(value = "新增文件资源", operType = OperType.INSERT)
public void add(@Validated @RequestBody SysFile sysFile) {
sysFile.setCreateBy(CoderLoginUtil.getUserName());
YUtil.isTrue(!sysFileService.save(sysFile), "新增失败,请稍后重试");
}
/**
* @description [修改]
* @author Leocoder
*/
@Operation(summary = "修改文件信息", description = "更新系统文件的基本信息")
@SaCheckPermission("system:file:update")
@PostMapping("/sysFile/update")
@OperLog(value = "修改文件资源", operType = OperType.UPDATE)
public void update(@Validated @RequestBody SysFile sysFile) {
sysFile.setUpdateBy(CoderLoginUtil.getUserName());
YUtil.isTrue(!sysFileService.updateById(sysFile), "修改失败,请稍后重试");
}
/**
* @description [删除]
* @author Leocoder
*/
@Operation(summary = "删除文件", description = "根据文件ID删除指定文件及其记录")
@SaCheckPermission("system:file:delete")
@PostMapping("/sysFile/deleteById/{id}")
@OperLog(value = "删除文件资源", operType = OperType.DELETE)
public void delete(@PathVariable("id") Long id) {
SysFile sysFile = sysFileService.getById(id);
if (!ObjectUtil.isEmpty(sysFile)) {
if (StringUtils.isNotBlank(sysFile.getFileUpload())) {
// 根据存储服务类型删除文件
deletePhysicalFile(sysFile.getFileUpload(), sysFile.getFileService());
}
}
YUtil.isTrue(!sysFileService.removeById(id), "删除失败,请稍后重试");
}
/**
* @description [批量删除]
* @author Leocoder
*/
@Operation(summary = "批量删除文件", description = "批量删除多个文件及其记录")
@SaCheckPermission("system:file:delete")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/sysFile/batchDelete")
@OperLog(value = "批量删除文件资源", operType = OperType.DELETE)
public void batchDelete(@RequestBody List<Long> ids) {
LambdaQueryWrapper<SysFile> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SysFile::getFileId, ids);
List<SysFile> sysFileList = sysFileService.list(lambdaQueryWrapper);
if (!sysFileList.isEmpty()) {
for (SysFile sysFile : sysFileList) {
if (StringUtils.isNotBlank(sysFile.getFileUpload())) {
// 根据存储服务类型删除文件
deletePhysicalFile(sysFile.getFileUpload(), sysFile.getFileService());
}
}
}
YUtil.isTrue(!sysFileService.removeBatchByIds(ids), "删除失败,请稍后重试");
}
/**
* 根据存储服务类型删除物理文件
*/
private void deletePhysicalFile(String filePath, String fileService) {
try {
boolean deleteResult = false;
switch (fileService) {
// 本地存储
case "1":
deleteResult = FileUtil.deleteFile(filePath);
break;
// OSS存储
case "3":
try {
StorageService ossService = storageServiceFactory.getStorageService("oss");
deleteResult = ossService.deleteFile(filePath);
} catch (Exception e) {
log.error("OSS存储服务不可用尝试本地删除", e);
deleteResult = FileUtil.deleteFile(filePath);
}
break;
default:
log.warn("未知的存储服务类型: {},使用本地删除", fileService);
deleteResult = FileUtil.deleteFile(filePath);
break;
}
if (deleteResult) {
log.info("文件删除成功: filePath={}, fileService={}", filePath, fileService);
} else {
log.warn("文件删除失败: filePath={}, fileService={}", filePath, fileService);
}
} catch (Exception e) {
log.error("删除物理文件时发生异常: filePath={}, fileService={}", filePath, fileService, e);
}
}
}

View File

@ -0,0 +1,117 @@
package org.leocoder.heritage.system.controller.login;
import com.wf.captcha.GifCaptcha;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.common.constants.CoderCacheConstants;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.utils.cache.RedisUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author Leocoder
* @description [三种类型验证码]
*/
@Tag(name = "验证码管理", description = "生成各种类型的验证码")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
private final RedisUtil redisUtil;
/**
* easy-captcha
* png类型 验证码 - 6分钟过期
*
* @author Leocoder
*/
@Operation(summary = "生成PNG验证码", description = "生成PNG格式的验证码6分钟过期")
@GetMapping("/png")
public Map<String, String> pngCaptcha() {
// 三个参数分别为宽位数
SpecCaptcha specCaptcha = new SpecCaptcha(100, 30, 5);
// 获取验证码
String securityCode = specCaptcha.text();
// 存放Redis验证码的key
String codeKey = UUID.randomUUID().toString();
log.info("png验证码Key{}png验证码{}", codeKey, securityCode);
// 放入redis中并设置过期时间
redisUtil.setCacheObject(CoderCacheConstants.CAPTCHA_CODE_KEY + codeKey, securityCode);
redisUtil.expire(CoderCacheConstants.CAPTCHA_CODE_KEY + codeKey, CoderConstants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
Map<String, String> map = new HashMap<>();
map.put("codeKey", codeKey);
map.put("captchaPicture", specCaptcha.toBase64());
return map;
}
/**
* easy-captcha
* gif验证码 - 6分钟过期
*
* @author Leocoder
*/
@Operation(summary = "生成GIF验证码", description = "生成GIF动画格式的验证码6分钟过期")
@GetMapping("/gif")
public Map<String, Object> gifCaptcha() {
// 三个参数分别为宽位数
GifCaptcha gifCaptcha = new GifCaptcha(100, 30, 5);
// 设置类型字母数字混合
gifCaptcha.setCharType(Captcha.TYPE_DEFAULT);
// 获取验证码
String securityCode = gifCaptcha.text();
// 存放Redis验证码的key
String codeKey = UUID.randomUUID().toString();
log.info("gif验证码Key{}gif验证码{}", codeKey, securityCode);
// 放入redis中并设置过期时间
redisUtil.setCacheObject(CoderCacheConstants.CAPTCHA_CODE_KEY + codeKey, securityCode);
redisUtil.expire(CoderCacheConstants.CAPTCHA_CODE_KEY + codeKey, CoderConstants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
Map<String, Object> map = new HashMap<>();
map.put("codeKey", codeKey);
map.put("captchaPicture", gifCaptcha.toBase64());
return map;
}
/**
* easy-captcha
* 算术类型验证码[不支持SpringBoot3需要新增依赖这里未添加] - 6分钟过期
*
* @author Leocoder
*/
// @SneakyThrows
// @GetMapping("/arithmetic")
// public Map<String, Object> arithmeticCaptcha() {
// // 算术类型
// ArithmeticCaptcha arithmeticCaptcha = new ArithmeticCaptcha(100, 30);
// 几位数运算默认是两位
// arithmeticCaptcha.setLen(3);
// 获取运算的公式4-9+1=?
// arithmeticCaptcha.getArithmeticString();
// // 获取验证码
// 获取运算的结果-4
// String securityCode = arithmeticCaptcha.text();
// // 存放Redis验证码的key
// String codeKey = UUID.randomUUID().toString();
// log.info("算术验证码Key{},算术验证码:{}", codeKey, securityCode);
// // 放入redis中并设置过期时间
// redisUtil.setCacheObject(CoderCacheConstants.CAPTCHA_CODE_KEY + codeKey, securityCode);
// redisUtil.expire(CoderCacheConstants.CAPTCHA_CODE_KEY + codeKey, CoderConstants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// Map<String, Object> map = new HashMap<>();
// map.put("codeKey", codeKey);
// map.put("captchaPicture", arithmeticCaptcha.toBase64());
// return map;
// }
}

View File

@ -0,0 +1,64 @@
package org.leocoder.heritage.system.controller.login;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.domain.model.bo.system.SysLoginBo;
import org.leocoder.heritage.domain.model.vo.system.SysLoginVo;
import org.leocoder.heritage.domain.model.vo.system.SysRegisterVo;
import org.leocoder.heritage.system.service.login.SysLoginService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Leocoder
* @description [SysLoginController]
*/
@Tag(name = "登录认证", description = "用户登录、退出、注册等认证相关操作")
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
public class SysLoginController {
private final SysLoginService loginService;
/**
* @description [PC登录]
* @author Leocoder
*/
@Operation(summary = "用户登录", description = "PC端用户登录接口")
@PostMapping("/auth/login")
public SysLoginBo login(@Validated @RequestBody SysLoginVo loginVo) {
return loginService.login(loginVo);
}
/**
* 退出登录调用此后端接口的时候默认直接从携带Headers请求头中获取token值
* Authorization[这个keysa-token的yml可进行配置] Bearer D0_TfFaChPxkvxghK_tabvTy5IRB9DdvQR__
*
* @description [退出登录]
* @author Leocoder
*/
@Operation(summary = "用户退出", description = "用户退出登录接口")
@GetMapping("/auth/logout")
public String logout() {
loginService.logout();
return "退出成功";
}
/**
* @description [PC注册]
* @author Leocoder
*/
@Operation(summary = "用户注册", description = "PC端用户注册接口")
@PostMapping("/auth/register")
public void register(@Validated @RequestBody SysRegisterVo registerVo) {
loginService.register(registerVo);
}
}

View File

@ -0,0 +1,122 @@
package org.leocoder.heritage.system.controller.loginlog;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.domain.model.vo.system.SysLoginLogVo;
import org.leocoder.heritage.domain.pojo.system.SysLoginLog;
import org.leocoder.heritage.system.service.loginlog.SysLoginLogService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author Leocoder
* @description [系统访问记录-控制层]
*/
@Tag(name = "登录日志", description = "系统登录记录的查询和管理")
@Validated
@RequestMapping("/coder")
@RequiredArgsConstructor
@RestController
public class SysLoginLogController {
private final SysLoginLogService sysLoginLogService;
/**
* @description [分页查询]
* @author Leocoder
*/
@Operation(summary = "分页查询登录日志", description = "根据查询条件分页获取系统登录记录")
@SaCheckPermission("system:loginlog:list")
@GetMapping("/sysLoginLog/listPage")
public IPage<SysLoginLog> listPage(SysLoginLogVo vo) {
// 分页构造器
Page<SysLoginLog> page = new Page<>(vo.getPageNo(), vo.getPageSize());
// 条件构造器
LambdaQueryWrapper<SysLoginLog> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(vo.getLoginName()), SysLoginLog::getLoginName, vo.getLoginName());
wrapper.like(StringUtils.isNotBlank(vo.getLoginIp()), SysLoginLog::getLoginIp, vo.getLoginIp());
wrapper.eq(StringUtils.isNotBlank(vo.getLoginStatus()), SysLoginLog::getLoginStatus, vo.getLoginStatus());
wrapper.ge(StringUtils.isNotBlank(vo.getBeginTime()), SysLoginLog::getLoginTime, vo.getBeginTime());
wrapper.le(StringUtils.isNotBlank(vo.getEndTime()), SysLoginLog::getLoginTime, vo.getEndTime());
wrapper.orderByDesc(SysLoginLog::getLoginTime);
// 进行分页查询
page = sysLoginLogService.page(page, wrapper);
return page;
}
/**
* @description [查询所有]
* @author Leocoder
*/
@Operation(summary = "查询所有登录日志", description = "获取系统中所有登录记录")
@SaCheckPermission("system:loginlog:list")
@GetMapping("/sysLoginLog/list")
public List<SysLoginLog> list() {
return sysLoginLogService.list();
}
/**
* @description [查询一个]
* @author Leocoder
*/
@Operation(summary = "根据ID查询登录日志", description = "根据日志ID获取登录记录详细信息")
@GetMapping("/sysLoginLog/getById/{id}")
public SysLoginLog getById(@PathVariable Long id) {
return sysLoginLogService.getById(id);
}
/**
* @description [新增]
* @author Leocoder
*/
@Operation(summary = "新增登录日志", description = "添加新的登录记录")
@SaCheckPermission("system:loginlog:add")
@PostMapping("/sysLoginLog/add")
public void add(@Validated @RequestBody SysLoginLog sysLoginLog) {
YUtil.isTrue(!sysLoginLogService.save(sysLoginLog), "新增失败,请稍后重试");
}
/**
* @description [修改]
* @author Leocoder
*/
@Operation(summary = "修改登录日志", description = "更新登录记录信息")
@SaCheckPermission("system:loginlog:update")
@PostMapping("/sysLoginLog/update")
public void update(@Validated @RequestBody SysLoginLog sysLoginLog) {
YUtil.isTrue(!sysLoginLogService.updateById(sysLoginLog), "修改失败,请稍后重试");
}
/**
* @description [删除]
* @author Leocoder
*/
@Operation(summary = "删除登录日志", description = "根据日志ID删除指定登录记录")
@SaCheckPermission("system:loginlog:delete")
@PostMapping("/sysLoginLog/deleteById/{id}")
public void delete(@PathVariable("id") Long id) {
YUtil.isTrue(!sysLoginLogService.removeById(id), "删除失败,请稍后重试");
}
/**
* @description [批量删除]
* @author Leocoder
*/
@Operation(summary = "批量删除登录日志", description = "批量删除多个登录记录")
@SaCheckPermission("system:loginlog:delete")
@PostMapping("/sysLoginLog/batchDelete")
public void batchDelete(@NotNull(message = "请选择需要删除的数据") @RequestBody List<Long> ids) {
YUtil.isTrue(!sysLoginLogService.removeByIds(ids), "删除失败,请稍后重试");
}
}

View File

@ -0,0 +1,284 @@
package org.leocoder.heritage.system.controller.menu;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.domain.enums.menu.MenuTypeEnum;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.domain.enums.oper.OperType;
import org.leocoder.heritage.domain.model.bo.element.CascaderLongBo;
import org.leocoder.heritage.domain.model.bo.system.SysMenuBo;
import org.leocoder.heritage.domain.model.bo.system.SysRoleMenuBo;
import org.leocoder.heritage.domain.model.vo.system.SysMenuVo;
import org.leocoder.heritage.domain.pojo.system.SysMenu;
import org.leocoder.heritage.operlog.annotation.OperLog;
import org.leocoder.heritage.system.service.menu.SysMenuService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Leocoder
* @description [菜单权限表-控制层]
*/
@Tag(name = "菜单管理", description = "系统菜单权限的增删改查操作")
@Validated
@RequestMapping("/coder")
@RequiredArgsConstructor
@RestController
public class SysMenuController {
private final SysMenuService sysMenuService;
/**
* @description [分页查询]
* @author Leocoder
*/
@Operation(summary = "分页查询菜单列表", description = "根据查询条件分页获取系统菜单信息")
@SaCheckPermission("system:menu:list")
@GetMapping("/sysMenu/listPage")
public IPage<SysMenu> listPage(SysMenuVo vo) {
// 分页构造器
Page<SysMenu> page = new Page<>(vo.getPageNo(), vo.getPageSize());
// 条件构造器
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(vo.getMenuName()), SysMenu::getMenuName, vo.getMenuName());
wrapper.eq(StringUtils.isNotBlank(vo.getMenuStatus()), SysMenu::getMenuStatus, vo.getMenuStatus());
wrapper.like(StringUtils.isNotBlank(vo.getAuth()), SysMenu::getAuth, vo.getAuth());
// 进行分页查询
page = sysMenuService.page(page, wrapper);
return page;
}
/**
* @description [查询菜单]
* @author Leocoder
*/
@Operation(summary = "查询菜单列表", description = "根据条件查询系统菜单信息")
@SaCheckPermission("system:menu:list")
@GetMapping("/sysMenu/list")
public List<SysMenu> list(SysMenuVo vo) {
// 条件构造器
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(vo.getMenuName()), SysMenu::getMenuName, vo.getMenuName());
wrapper.eq(StringUtils.isNotBlank(vo.getMenuStatus()), SysMenu::getMenuStatus, vo.getMenuStatus());
wrapper.like(StringUtils.isNotBlank(vo.getAuth()), SysMenu::getAuth, vo.getAuth());
wrapper.orderByAsc(SysMenu::getSorted);
return sysMenuService.listSysMenu(vo);
}
/**
* @description [查询一个]
* @author Leocoder
*/
@Operation(summary = "根据ID查询菜单", description = "根据菜单ID获取菜单详细信息")
@GetMapping("/sysMenu/getById/{id}")
public SysMenu getById(@PathVariable Long id) {
return sysMenuService.getById(id);
}
/**
* @description [新增]
* @author Leocoder
*/
@Operation(summary = "新增菜单", description = "添加新的系统菜单")
@SaCheckPermission("system:menu:add")
@PostMapping("/sysMenu/add")
@OperLog(value = "新增菜单", operType = OperType.INSERT)
public void add(@Validated @RequestBody SysMenu sysMenu) {
// 路由Path不能重复
if (StringUtils.isNotBlank(sysMenu.getPath())) {
// 条件构造器
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getPath, sysMenu.getPath());
long count = sysMenuService.count(wrapper);
YUtil.isTrue(count > CoderConstants.ONE_LONG, "请联系管理员检查路由path重复多次");
if (count == CoderConstants.ONE_LONG) {
SysMenu menu = sysMenuService.getOne(wrapper);
YUtil.isTrue(!Objects.equals(menu.getMenuId(), sysMenu.getMenuId()), "该路由path已存在");
}
}
// 如果是按钮类型并且非外链必须得隐藏
if (MenuTypeEnum.isButton(sysMenu.getMenuType()) && StringUtils.isBlank(sysMenu.getIsLink())) {
sysMenu.setIsHide(CoderConstants.ZERO_STRING);
}
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
sysMenu.setCreateBy(CoderLoginUtil.getUserName());
}
YUtil.isTrue(!sysMenuService.save(sysMenu), "新增失败,请稍后重试");
}
/**
* @description [修改]
* @author Leocoder
*/
@Operation(summary = "修改菜单信息", description = "更新系统菜单的基本信息")
@SaCheckPermission("system:menu:update")
@PostMapping("/sysMenu/update")
@OperLog(value = "修改菜单", operType = OperType.UPDATE)
public void update(@Validated @RequestBody SysMenu sysMenu) {
// 路由Path不能重复
if (StringUtils.isNotBlank(sysMenu.getPath())) {
// 条件构造器
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getPath, sysMenu.getPath());
long count = sysMenuService.count(wrapper);
YUtil.isTrue(count > CoderConstants.ONE_LONG, "请联系管理员检查路由path重复多次");
if (count == CoderConstants.ONE_LONG) {
SysMenu menu = sysMenuService.getOne(wrapper);
YUtil.isTrue(!Objects.equals(menu.getMenuId(), sysMenu.getMenuId()), "该路由path已存在");
}
}
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
sysMenu.setUpdateBy(CoderLoginUtil.getUserName());
}
YUtil.isTrue(!sysMenuService.updateById(sysMenu), "修改失败,请稍后重试");
}
/**
* @description [删除]
* @author Leocoder
*/
@Operation(summary = "删除菜单", description = "根据菜单ID删除指定菜单")
@SaCheckPermission("system:menu:delete")
@PostMapping("/sysMenu/deleteById/{id}")
@OperLog(value = "删除菜单", operType = OperType.DELETE)
public void delete(@PathVariable Long id) {
SysMenu menu = sysMenuService.getById(id);
YUtil.isTrue(ObjectUtils.isEmpty(menu) || menu.getMenuId() == null, "请选择需要删除的数据");
// 条件构造器
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getParentId, menu.getMenuId());
long count = sysMenuService.count(wrapper);
YUtil.isTrue(count > CoderConstants.ZERO_LONG, "请先删除该节点下的子节点");
YUtil.isTrue(!sysMenuService.removeById(id), "删除失败,请稍后重试");
}
/**
* @description [批量删除]
* @author Leocoder
*/
@Operation(summary = "批量删除菜单", description = "批量删除多个系统菜单")
@SaCheckPermission("system:menu:delete")
@PostMapping("/sysMenu/batchDelete")
@OperLog(value = "批量删除菜单", operType = OperType.DELETE)
public void batchDelete(@RequestBody List<Long> ids) {
YUtil.isTrue(!sysMenuService.removeBatchByIds(ids), "删除失败,请稍后重试");
}
/**
* @description [修改状态]
* @author Leocoder
*/
@Operation(summary = "修改菜单状态", description = "启用或禁用系统菜单")
@SaCheckPermission("system:menu:update")
@PostMapping("/sysMenu/updateStatus/{id}/{menuStatus}")
@OperLog(value = "修改菜单状态", operType = OperType.UPDATE)
public void updateStatus(@PathVariable("id") Long id, @PathVariable("menuStatus") String menuStatus) {
UpdateWrapper<SysMenu> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("menu_status", menuStatus).eq("menu_id", id);
YUtil.isTrue(!sysMenuService.update(updateWrapper), "修改失败,请稍后重试");
}
/**
* @description [是否展开]
* @author Leocoder
*/
@Operation(summary = "修改菜单展开状态", description = "设置菜单是否默认展开")
@PostMapping("/sysMenu/updateSpread/{id}/{isSpread}")
@OperLog(value = "修改菜单展开状态", operType = OperType.UPDATE)
public void updateSpread(@PathVariable("id") Long id, @PathVariable("isSpread") String isSpread) {
LambdaUpdateWrapper<SysMenu> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(StringUtils.isNotBlank(isSpread), SysMenu::getIsSpread, isSpread).eq(SysMenu::getMenuId, id);
YUtil.isTrue(!sysMenuService.update(updateWrapper), "修改失败,请稍后重试");
}
/**
* @description [菜单级联下拉框]
* @author Leocoder
*/
@Operation(summary = "菜单级联下拉框", description = "获取菜单的树形级联选择器数据")
@GetMapping("/sysMenu/cascaderList")
public List<CascaderLongBo> cascaderList() {
return sysMenuService.cascaderList();
}
/**
* @description [生成当前用户所拥有菜单路由]
* @author Leocoder
*/
@Operation(summary = "生成用户菜单路由", description = "根据当前用户权限生成可访问的菜单路由")
@GetMapping("/sysMenu/listRouters")
public List<SysMenuBo> generatorRouters() {
return sysMenuService.generatorRouters();
}
/**
* @description [查询所有正常的路由 AND 展开节点角色分配菜单权限使用]
* @author Leocoder
*/
@Operation(summary = "查询正常菜单列表", description = "查询所有正常状态的菜单和展开节点,用于角色分配权限")
@GetMapping("/sysMenu/listMenuNormal")
public Map<String, Object> listMenuNormal(SysMenuVo sysMenuVo) {
Map<String, Object> map = new HashMap<>();
List<SysMenu> menuList = sysMenuService.listMenuNormal(sysMenuVo);
/* 如果不进行权限数据筛选,则上边一行注释,下边注释解开 */
// 菜单正常数据
// LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
// wrapper.select(SysMenu::getMenuId, SysMenu::getMenuName, SysMenu::getParentId);
// wrapper.eq(SysMenu::getMenuStatus, CoderConstants.ZERO_STRING);
// List<SysMenu> menuList = sysMenuService.list(wrapper);
map.put("menuList", menuList);
// 菜单是否展开数据
LambdaQueryWrapper<SysMenu> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.select(SysMenu::getMenuId);
lambdaWrapper.eq(SysMenu::getIsSpread, CoderConstants.ZERO_STRING);
lambdaWrapper.eq(SysMenu::getMenuStatus, CoderConstants.ZERO_STRING);
List<SysMenu> menuSpreadList = sysMenuService.list(lambdaWrapper);
List<Long> spreadList = menuSpreadList.parallelStream().map(SysMenu::getMenuId).collect(Collectors.toList());
if (CollectionUtil.isEmpty(spreadList)) {
spreadList = new ArrayList<>();
}
map.put("spreadList", spreadList);
return map;
}
/**
* @description [根据用户拥有的角色ID查询权限菜单]
* @author Leocoder
*/
@Operation(summary = "根据角色ID查询菜单", description = "根据角色ID获取该角色拥有的菜单权限ID列表")
@GetMapping("/sysMenu/listMenuIdsByRoleId/{roleId}")
public List<Long> listMenuIdsByRoleId(@PathVariable("roleId") Long roleId) {
return sysMenuService.listMenuIdsByRoleId(roleId);
}
/**
* @description [保存角色和菜单权限之间的关系]
* @author Leocoder
*/
@Operation(summary = "保存角色菜单权限", description = "保存角色和菜单权限之间的关联关系")
@SaCheckPermission("system:role:menu")
@PostMapping("/sysMenu/saveRoleMenu")
@OperLog(value = "保存角色菜单权限", operType = OperType.UPDATE)
public void saveRoleMenu(@Validated @RequestBody SysRoleMenuBo roleMenuBo) {
sysMenuService.saveRoleMenu(roleMenuBo.getRoleIdAsLong(), roleMenuBo.getMenuIdsAsLong());
}
}

View File

@ -0,0 +1,163 @@
package org.leocoder.heritage.system.controller.online;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.common.constants.SaTokenSessionConstants;
import org.leocoder.heritage.common.satoken.CoderLoginUser;
import org.leocoder.heritage.domain.model.vo.system.SysUserOnlineVo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Leocoder
* @description [在线用户管理]
*/
@Tag(name = "在线用户管理", description = "实时查看在线用户、强制注销等功能")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/coder")
public class SysUserOnlineController {
/**
* @description [分页查询在线用户列表]
* @author Leocoder
*/
@Operation(summary = "分页查询在线用户", description = "实时获取所有在线用户信息支持按登录名、用户名、IP地址过滤")
@SaCheckPermission("monitor:online:list")
@GetMapping("/sysUserOnline/listPage")
public Map<String, Object> listPage(SysUserOnlineVo vo) {
// 使用Sa-Token官方API获取所有已登录的会话ID
// keyword: 查询关键字只有包括这个字符串的 token 值才会被查询出来
// start: 数据开始处索引
// size: 要获取的数据条数 值为-1代表一直获取到末尾
// sortType: 排序方式true=正序先登录的在前false=反序后登录的在前
List<String> sessionKeyList = StpUtil.searchSessionId("", 0, -1, false);
List<CoderLoginUser> loginUserList = null;
Map<String, Object> result = new HashMap<>();
if (CollectionUtil.isNotEmpty(sessionKeyList)) {
loginUserList = new ArrayList<>();
// 遍历所有会话获取登录用户信息
for (String sessionKey : sessionKeyList) {
try {
// 使用sessionKey获取SaSession对象
SaSession saSession = StpUtil.getSessionBySessionId(sessionKey);
if (saSession != null) {
// 获取登录用户对象
CoderLoginUser coderLoginUser = saSession.getModel(
SaTokenSessionConstants.LOGIN_USER,
CoderLoginUser.class
);
if (coderLoginUser != null) {
loginUserList.add(coderLoginUser);
}
}
} catch (Exception e) {
// 处理可能的会话过期或异常避免影响整体查询
log.warn("获取会话[{}]用户信息失败: {}", sessionKey, e.getMessage());
}
}
// 获取查询参数
Integer pageNo = vo.getPageNo() != null ? vo.getPageNo() : 1;
Integer pageSize = vo.getPageSize() != null ? vo.getPageSize() : 10;
String loginName = vo.getLoginName();
String userName = vo.getUserName();
String loginIp = vo.getLoginIp();
// 根据条件过滤用户列表
List<CoderLoginUser> filteredList = loginUserList.stream()
.filter(loginUser -> userName == null || userName.isEmpty() ||
(loginUser.getUserName() != null && loginUser.getUserName().contains(userName)))
.filter(loginUser -> loginName == null || loginName.isEmpty() ||
(loginUser.getLoginName() != null && loginUser.getLoginName().contains(loginName)))
.filter(loginUser -> loginIp == null || loginIp.isEmpty() ||
(loginUser.getLoginIp() != null && loginUser.getLoginIp().contains(loginIp)))
.collect(Collectors.toList());
// 计算分页信息
int totalCount = filteredList.size();
int totalPages = (int) Math.ceil((double) totalCount / pageSize);
int startIndex = (pageNo - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize, totalCount);
// 执行分页
List<CoderLoginUser> pagedList = filteredList.stream()
.skip(startIndex)
.limit(pageSize)
.collect(Collectors.toList());
// 构建返回结果
result.put("total", totalCount);
result.put("current", pageNo);
result.put("size", pageSize);
result.put("pages", totalPages);
result.put("records", pagedList);
log.info("查询在线用户成功,总数: {}, 过滤后: {}, 当前页: {}",
loginUserList.size(), totalCount, pagedList.size());
} else {
// 没有在线用户
result.put("total", 0);
result.put("current", vo.getPageNo() != null ? vo.getPageNo() : 1);
result.put("size", vo.getPageSize() != null ? vo.getPageSize() : 10);
result.put("pages", 0);
result.put("records", Collections.emptyList());
}
return result;
}
/**
* @description [强制注销]
* @author Leocoder
*/
@Operation(summary = "强制注销", description = "强制指定用户注销登录清除Token信息")
@SaCheckPermission("monitor:online:logout")
@GetMapping("/sysUserOnline/logout/{userId}")
public String logout(@PathVariable("userId") Long userId) {
try {
// 使用Sa-Token强制指定账号注销下线
// 注销会清除该用户的所有Token信息
StpUtil.logout(userId);
log.info("用户[{}]已被强制注销", userId);
return "强制注销成功";
} catch (Exception e) {
log.error("强制注销失败用户ID: {}, 错误: {}", userId, e.getMessage());
throw new RuntimeException("强制注销失败: " + e.getMessage());
}
}
/**
* @description [获取在线用户统计信息]
* @author Leocoder
*/
@Operation(summary = "获取在线用户统计", description = "获取当前在线用户总数等统计信息")
@SaCheckPermission("monitor:online:list")
@GetMapping("/sysUserOnline/count")
public Map<String, Object> getOnlineUserCount() {
List<String> sessionKeyList = StpUtil.searchSessionId("", 0, -1, false);
int onlineCount = sessionKeyList != null ? sessionKeyList.size() : 0;
Map<String, Object> result = new HashMap<>();
result.put("onlineCount", onlineCount);
result.put("timestamp", System.currentTimeMillis());
log.info("当前在线用户数: {}", onlineCount);
return result;
}
}

View File

@ -0,0 +1,108 @@
package org.leocoder.heritage.system.controller.operlog;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.domain.model.vo.system.SysOperLogVo;
import org.leocoder.heritage.domain.pojo.system.SysOperLog;
import org.leocoder.heritage.system.service.operlog.SysOperLogService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [操作日志控制器]
*/
@RestController
@RequestMapping("/coder")
@Slf4j
@RequiredArgsConstructor
public class SysOperLogController {
private final SysOperLogService sysOperLogService;
/**
* @description [分页查询操作日志]
* @author Leocoder
*/
@GetMapping("/sysOperLog/listPage")
@SaCheckPermission("system:operlog:search")
public IPage<SysOperLog> listPage(SysOperLogVo vo) {
return sysOperLogService.listPage(vo);
}
/**
* @description [根据ID查询操作日志详情]
* @author Leocoder
*/
@GetMapping("/sysOperLog/getById/{operId}")
@SaCheckPermission("system:operlog:search")
public SysOperLog getById(@PathVariable Long operId) {
return sysOperLogService.getDetail(operId);
}
/**
* @description [查询操作日志详情]
* @author Leocoder
*/
@GetMapping("/sysOperLog/getDetailById/{operId}")
@SaCheckPermission("system:operlog:search")
public SysOperLog getDetailById(@PathVariable Long operId) {
return sysOperLogService.getDetail(operId);
}
/**
* @description [删除操作日志]
* @author Leocoder
*/
@PostMapping("/sysOperLog/deleteById/{operId}")
@SaCheckPermission("system:operlog:delete")
public void deleteById(@PathVariable Long operId) {
List<Long> operIds = List.of(operId);
sysOperLogService.deleteByIds(operIds);
}
/**
* @description [批量删除操作日志]
* @author Leocoder
*/
@PostMapping("/sysOperLog/batchDelete")
@SaCheckPermission("system:operlog:delete")
public void batchDelete(@RequestBody List<Long> operIds) {
sysOperLogService.deleteByIds(operIds);
}
/**
* @description [清空操作日志]
* @author Leocoder
*/
@PostMapping("/sysOperLog/clear")
@SaCheckPermission("system:operlog:delete")
public void clear() {
sysOperLogService.clear();
}
/**
* @description [获取操作统计]
* @author Leocoder
*/
@GetMapping("/sysOperLog/statistics")
@SaCheckPermission("system:operlog:search")
public Map<String, Object> statistics() {
return sysOperLogService.getStatistics();
}
/**
* @description [获取仪表盘统计]
* @author Leocoder
*/
@GetMapping("/sysOperLog/dashboard")
@SaCheckPermission("system:operlog:search")
public Map<String, Object> dashboard() {
return sysOperLogService.getDashboardStats();
}
}

View File

@ -0,0 +1,198 @@
package org.leocoder.heritage.system.controller.role;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.domain.enums.oper.OperType;
import org.leocoder.heritage.domain.model.bo.element.SelectLongBo;
import org.leocoder.heritage.domain.model.vo.base.BaseVo;
import org.leocoder.heritage.domain.model.vo.system.SysRoleVo;
import org.leocoder.heritage.domain.pojo.system.SysRole;
import org.leocoder.heritage.operlog.annotation.OperLog;
import org.leocoder.heritage.system.service.role.SysRoleService;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @author Leocoder
* @description [角色信息表-控制层]
*/
@Tag(name = "角色管理", description = "系统角色权限的增删改查操作")
@Validated
@RequestMapping("/coder")
@RequiredArgsConstructor
@RestController
public class SysRoleController {
private final SysRoleService sysRoleService;
/**
* @description [分页查询]
* @author Leocoder
*/
@Operation(summary = "分页查询角色列表", description = "根据查询条件分页获取系统角色信息")
@SaCheckPermission("system:role:list")
@GetMapping("/sysRole/listPage")
public IPage<SysRole> listPage(SysRoleVo vo) {
return sysRoleService.listPage(vo);
}
/**
* @description [查询所有]
* @author Leocoder
*/
@Operation(summary = "查询所有角色", description = "获取系统中所有角色信息")
@SaCheckPermission("system:role:list")
@GetMapping("/sysRole/list")
public List<SysRole> list() {
return sysRoleService.list();
}
/**
* @description [根据主键查询]
* @author Leocoder
*/
@Operation(summary = "根据ID查询角色", description = "根据角色ID获取角色详细信息")
@GetMapping("/sysRole/getById/{id}")
public SysRole getById(@PathVariable("id") Long id) {
return sysRoleService.getById(id);
}
/**
* @description [新增]
* @author Leocoder
*/
@Operation(summary = "新增角色", description = "添加新的系统角色")
@SaCheckPermission("system:role:add")
@PostMapping("/sysRole/add")
@OperLog(value = "新增角色", operType = OperType.INSERT)
public void add(@Validated @RequestBody SysRole sysRole) {
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
sysRole.setCreateBy(CoderLoginUtil.getUserName());
}
YUtil.isTrue(!sysRoleService.save(sysRole), "新增失败,请稍后重试");
}
/**
* @description [获取最新排序]
* @author Leocoder
*/
@Operation(summary = "获取最新排序号", description = "获取当前最大的角色排序号用于新增")
@GetMapping("/sysRole/getSorted")
public int getSorted() {
LambdaQueryWrapper<SysRole> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.select(SysRole::getSorted);
lambdaQueryWrapper.orderByDesc(SysRole::getSorted);
lambdaQueryWrapper.last("LIMIT 1");
SysRole sysRole = sysRoleService.getOne(lambdaQueryWrapper);
if (ObjectUtils.isEmpty(sysRole)) return CoderConstants.ONE_NUMBER;
return sysRole.getSorted() != null ? sysRole.getSorted() + CoderConstants.ONE_NUMBER : CoderConstants.SIX_INFINITE_NUMBER;
}
/**
* @description [修改]
* @author Leocoder
*/
@Operation(summary = "修改角色信息", description = "更新系统角色的基本信息")
@SaCheckPermission("system:role:update")
@PostMapping("/sysRole/update")
@OperLog(value = "修改角色", operType = OperType.UPDATE)
public void update(@Validated @RequestBody SysRole sysRole) {
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
sysRole.setUpdateBy(CoderLoginUtil.getUserName());
}
YUtil.isTrue(!sysRoleService.updateById(sysRole), "修改失败,请稍后重试");
}
/**
* @description [删除]
* @author Leocoder
*/
@Operation(summary = "删除角色", description = "根据角色ID删除指定角色")
@SaCheckPermission("system:role:delete")
@PostMapping("/sysRole/deleteById/{id}")
@OperLog(value = "删除角色", operType = OperType.DELETE)
public void delete(@PathVariable("id") Long id) {
YUtil.isTrue(Objects.equals(id, 1L), "超级管理员不可删除");
YUtil.isTrue(!sysRoleService.removeById(id), "删除失败,请稍后重试");
}
/**
* @description [批量删除]
* @author Leocoder
*/
@Operation(summary = "批量删除角色", description = "批量删除多个系统角色")
@SaCheckPermission("system:role:delete")
@PostMapping("/sysRole/batchDelete")
@OperLog(value = "批量删除角色", operType = OperType.DELETE)
public void batchDelete(@RequestBody List<Long> ids) {
for (Long id : ids) {
YUtil.isTrue(Objects.equals(id, 1L), "超级管理员不可删除");
}
YUtil.isTrue(!sysRoleService.removeBatchByIds(ids), "批量删除失败,请稍后重试");
}
/**
* @description [修改状态]
* @author Leocoder
*/
@Operation(summary = "修改角色状态", description = "启用或禁用系统角色")
@PostMapping("/sysRole/updateStatus/{roleId}/{roleStatus}")
@OperLog(value = "修改角色状态", operType = OperType.UPDATE)
public void update(@PathVariable("roleId") Long roleId, @PathVariable("roleStatus") String roleStatus) {
UpdateWrapper<SysRole> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("role_status", roleStatus).eq("role_id", roleId);
YUtil.isTrue(!sysRoleService.update(updateWrapper), "修改失败,请稍后重试");
}
/**
* @description [查询所有正常角色-穿梭框]
* @author Leocoder
*/
@Operation(summary = "查询正常角色穿梭框", description = "查询所有正常状态的角色,用于穿梭框组件")
@SaCheckPermission("system:user:role")
@GetMapping("/sysRole/listNormalRole/{userId}")
public Map<String, Object> listNormalRole(@PathVariable("userId") Long userId) {
Map<String, Object> map = new HashMap<>();
map.put("data1", sysRoleService.listLeftRole());
map.put("data2", sysRoleService.listRightRole(userId));
return map;
}
/**
* @description [根据当前用户ID分配角色-穿梭框]
* @author Leocoder
*/
@Operation(summary = "分配用户角色", description = "为指定用户分配角色权限")
@SaCheckPermission("system:user:role")
@GetMapping("/sysRole/assignUserRole/{userId}/{roleIds}")
@OperLog(value = "分配用户角色", operType = OperType.UPDATE)
public void assignUserRole(@PathVariable("userId") Long userId, @PathVariable("roleIds") List<Long> roleIds) {
sysRoleService.assignUserRole(userId, roleIds);
}
/**
* @description [获取当前用户分配得角色下拉框(不需要可以删除注释和vo)]
* @author Leocoder
*/
@Operation(summary = "获取角色下拉框", description = "获取当前用户可分配的角色下拉选项")
@GetMapping("/sysRole/listRoleElSelect")
public List<SelectLongBo> listRoleElSelect(BaseVo vo) {
return sysRoleService.listRoleElSelect(vo);
}
}

View File

@ -0,0 +1,358 @@
package org.leocoder.heritage.system.controller.user;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.common.satoken.CoderPasswordUtil;
import org.leocoder.heritage.common.utils.file.FileTypeUtil;
import org.leocoder.heritage.common.utils.number.VerifyCodeUtil;
import org.leocoder.heritage.domain.enums.oper.OperType;
import org.leocoder.heritage.domain.model.vo.system.SysLoginUserVo;
import org.leocoder.heritage.domain.model.vo.system.SysPwdVo;
import org.leocoder.heritage.domain.pojo.system.SysLoginUser;
import org.leocoder.heritage.easyexcel.core.utils.EasyExcelUtil;
import org.leocoder.heritage.operlog.annotation.OperLog;
import org.leocoder.heritage.system.service.role.SysRoleService;
import org.leocoder.heritage.system.service.user.SysLoginUserService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [用户信息表-控制层]
*/
@Tag(name = "用户管理", description = "系统用户信息的增删改查操作")
@Slf4j
@Validated
@RequestMapping("/coder")
@RequiredArgsConstructor
@RestController
public class SysLoginUserController {
private final EasyExcelUtil easyExcelUtil;
private final SysLoginUserService sysLoginUserService;
private final SysRoleService sysRoleService;
/**
* @description [分页查询]
* @author Leocoder
*/
@Operation(summary = "分页查询用户列表", description = "根据查询条件分页获取系统用户信息")
@SaCheckPermission("system:user:list")
@GetMapping("/sysLoginUser/listPage")
public IPage<SysLoginUser> listPage(SysLoginUserVo vo) {
return sysLoginUserService.listPage(vo);
}
/**
* @description [查询所有]
* @author Leocoder
*/
@Operation(summary = "查询所有用户", description = "获取系统中所有用户信息")
@SaCheckPermission("system:user:list")
@GetMapping("/sysLoginUser/list")
public List<SysLoginUser> list() {
return sysLoginUserService.list();
}
/**
* @description [查询一个]
* @author Leocoder
*/
@Operation(summary = "根据ID查询用户", description = "根据用户ID获取用户详细信息")
@GetMapping("/sysLoginUser/getById/{id}")
public SysLoginUser getById(@PathVariable("id") Long id) {
// 条件构造器
LambdaQueryWrapper<SysLoginUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(null != id, SysLoginUser::getUserId, id);
wrapper.select(SysLoginUser.class,
column -> !column.getColumn().equals("password")
&& !column.getColumn().equals("salt")
&& !column.getColumn().equals("login_ip")
&& !column.getColumn().equals("login_time")
&& !column.getColumn().equals("pwd_update_time")
&& !column.getColumn().equals("create_by")
&& !column.getColumn().equals("update_by")
&& !column.getColumn().equals("update_time")
);
SysLoginUser loginUser = sysLoginUserService.getOne(wrapper);
loginUser.setRoleIds(sysRoleService.listRightRole(id));
return loginUser;
}
/**
* @description [新增]
* @author Leocoder
*/
@Operation(summary = "新增用户", description = "添加新的系统用户")
@SaCheckPermission("system:user:add")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/sysLoginUser/add")
@OperLog(value = "新增用户", operType = OperType.INSERT, excludeFields = {"password", "salt"})
public void add(@Validated @RequestBody SysLoginUser sysLoginUser) {
// 1先查询是否已经存在登录名
LambdaQueryWrapper<SysLoginUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysLoginUser::getLoginName, sysLoginUser.getLoginName());
long count = sysLoginUserService.count(wrapper);
YUtil.isTrue(count > 0, "该登录名已存在,请重新输入");
// 2设置盐值和密码
sysLoginUser.setSalt(VerifyCodeUtil.generateMixedLetters(6));
sysLoginUser.setPassword(CoderPasswordUtil.getPassword(sysLoginUser.getPassword(), sysLoginUser.getSalt()));
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
sysLoginUser.setCreateBy(CoderLoginUtil.getUserName());
}
YUtil.isTrue(!sysLoginUserService.save(sysLoginUser), "新增失败,请稍后重试");
SysLoginUser loginUser = sysLoginUserService.getOne(wrapper);
// 3添加角色
sysRoleService.assignUserRole(loginUser.getUserId(), sysLoginUser.getRoleIds());
}
/**
* @description [修改]
* @author Leocoder
*/
@Operation(summary = "修改用户信息", description = "更新系统用户的基本信息")
@SaCheckPermission("system:user:update")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/sysLoginUser/update")
@OperLog(value = "修改用户", operType = OperType.UPDATE, excludeFields = {"password", "salt"})
public void update(@Validated @RequestBody SysLoginUser sysLoginUser) {
// 1修改用户基本信息
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
sysLoginUser.setUpdateBy(CoderLoginUtil.getUserName());
}
YUtil.isTrue(!sysLoginUserService.updateById(sysLoginUser), "修改失败,请稍后重试");
// 2添加角色
sysRoleService.assignUserRole(sysLoginUser.getUserId(), sysLoginUser.getRoleIds());
}
/**
* @description [删除]
* @author Leocoder
*/
@Operation(summary = "删除用户", description = "根据用户ID删除指定用户")
@SaCheckPermission("system:user:delete")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/sysLoginUser/deleteById/{id}")
@OperLog(value = "删除用户", operType = OperType.DELETE)
public void delete(@PathVariable("id") Long id) {
// 1超级管理员不可删除
YUtil.isTrue(id == 1L, "超级管理员不可删除");
// 2删除用户数据
YUtil.isTrue(!sysLoginUserService.removeById(id), "删除失败,请稍后重试");
// 4删除当前用户拥有的角色
sysRoleService.deleteUserRole(id);
}
/**
* @description [批量删除]
* @author Leocoder
*/
@Operation(summary = "批量删除用户", description = "批量删除多个系统用户")
@SaCheckPermission("system:user:delete")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/sysLoginUser/batchDelete")
@OperLog(value = "批量删除用户", operType = OperType.DELETE)
public void deleteBatch(@NotNull(message = "请选择需要删除的数据") @RequestBody List<Long> ids) {
for (Long id : ids) {
// 1超级管理员不可删除
YUtil.isTrue(id == 1L, "超级管理员不可删除");
// 3删除当前用户拥有的角色
sysRoleService.deleteUserRole(id);
}
// 4删除用户
YUtil.isTrue(!sysLoginUserService.removeBatchByIds(ids), "删除失败,请稍后重试");
}
/**
* @description [修改状态]
* @author Leocoder
*/
@Operation(summary = "修改用户状态", description = "启用或禁用系统用户")
@PostMapping("/sysLoginUser/updateStatus/{userId}/{userStatus}")
@OperLog(value = "修改用户状态", operType = OperType.UPDATE)
public void update(@PathVariable("userId") Long userId, @PathVariable("userStatus") String userStatus) {
UpdateWrapper<SysLoginUser> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("user_status", userStatus).eq("user_id", userId);
YUtil.isTrue(!sysLoginUserService.update(updateWrapper), "修改失败,请稍后重试");
}
/**
* @description [获取登录用户信息角色左侧菜单栏(另外一个接口)按钮等权限]
* @author Leocoder
*/
@Operation(summary = "获取登录用户信息", description = "获取当前登录用户的基本信息、角色和权限")
@GetMapping("/sysLoginUser/getLoginUserInformation")
public Map<String, Object> getLoginUserInformation() {
return sysLoginUserService.getLoginUserInformation();
}
/**
* @description [个人中心-左侧卡片资料数据]
* @author Leocoder
*/
@Operation(summary = "获取个人资料", description = "获取个人中心卡片显示数据")
@GetMapping("/sysLoginUser/getPersonalData")
public Map<String, Object> getPersonalData() {
return sysLoginUserService.getPersonalData();
}
/**
* @description [个人中心-修改基本资料 头像]
* @author Leocoder
*/
@Operation(summary = "修改个人资料", description = "修改个人基本信息和头像")
@PostMapping("/sysLoginUser/updateBasicData")
@OperLog(value = "修改个人资料", operType = OperType.UPDATE)
public void updateBasicData(@RequestBody SysLoginUser sysLoginUser) {
sysLoginUser.setUserId(CoderLoginUtil.getUserId());
YUtil.isTrue(!sysLoginUserService.updateById(sysLoginUser), "操作失败");
}
/**
* @description [修改密码]
* @author Leocoder
*/
@Operation(summary = "修改登录密码", description = "用户修改自己的登录密码")
@PostMapping("/sysLoginUser/updateUserPwd")
@OperLog(value = "修改密码", operType = OperType.UPDATE, saveRequestData = false, saveResponseData = false)
public void updateUserPwd(@Validated @RequestBody SysPwdVo vo) {
SysLoginUser sysLoginUser = sysLoginUserService.getById(CoderLoginUtil.getUserId());
YUtil.isTrue(ObjectUtils.isEmpty(sysLoginUser), "用户信息发生改变,请重新登录");
// 1输入的密码是否和数据库密码一致
String password = vo.getPassword();
String inputPwd = CoderPasswordUtil.getPassword(password, sysLoginUser.getSalt());
String databasePwd = sysLoginUser.getPassword();
YUtil.isTrue(!inputPwd.equals(databasePwd), "密码输入错误");
// 2密码是否和新密码一致
String newPassword = vo.getNewPassword();
YUtil.isTrue(password.equals(newPassword), "新密码不能与旧密码相同");
// 3新密码是否和确认密码一致
String confirmPassword = vo.getConfirmPassword();
YUtil.isTrue(!newPassword.equals(confirmPassword), "新密码与确认密码不一致");
// 4保存新密码至数据库中
String finshPwd = CoderPasswordUtil.getPassword(newPassword, sysLoginUser.getSalt());
LambdaUpdateWrapper<SysLoginUser> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysLoginUser::getPassword, finshPwd);
updateWrapper.set(SysLoginUser::getPwdUpdateTime, new Date());
updateWrapper.eq(SysLoginUser::getUserId, CoderLoginUtil.getUserId());
YUtil.isTrue(!sysLoginUserService.update(updateWrapper), "修改失败,请稍后重试");
}
/**
* @PathVariable上使用@NotBlank或@NotNull时这些验证注解实际上不会生效
*
* @description [重置密码]
* @author Leocoder
*/
@Operation(summary = "重置用户密码", description = "管理员重置指定用户的登录密码")
@SaCheckPermission("system:user:resetPwd")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/sysLoginUser/resetPwd/{id}/{password}")
@OperLog(value = "重置用户密码", operType = OperType.UPDATE, saveRequestData = false, saveResponseData = false)
public void resetPwd(@PathVariable("id") Long id, @PathVariable("password") String password) {
// 1参数校验
YUtil.isTrue(id == null, "用户ID不能为空");
YUtil.isNull(password, "用户密码不能为空");
YUtil.isTrue(password.length() < 6, "密码长度不能小于6位");
SysLoginUser sysLoginUser = sysLoginUserService.getById(id);
YUtil.isTrue(ObjectUtils.isEmpty(sysLoginUser), "用户信息发生改变,请重新登录");
// 2密码加密
String encryptPwd = CoderPasswordUtil.getPassword(password, sysLoginUser.getSalt());
// 3修改密码
LambdaUpdateWrapper<SysLoginUser> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysLoginUser::getPassword, encryptPwd);
updateWrapper.set(SysLoginUser::getPwdUpdateTime, new Date());
updateWrapper.eq(SysLoginUser::getUserId, id);
boolean update = sysLoginUserService.update(updateWrapper);
YUtil.isTrue(!update, "重置密码失败,请重试");
}
/**
* @description [用户下载模版]
* @author Leocoder
*/
@Operation(summary = "下载用户导入模版", description = "下载用户数据导入的Excel模版文件")
@GetMapping("/sysLoginUser/downloadExcelTemplate")
@OperLog(value = "下载用户导入模板", operType = OperType.EXPORT)
public void downloadExcelTemplate(HttpServletResponse response) {
List<SysLoginUser> list = new ArrayList<>();
easyExcelUtil.exportExcel(response, list, SysLoginUser.class, "下载模版", "下载模版");
}
/**
* @description [多条件用户数据导出]
* @author Leocoder
*/
@Operation(summary = "导出用户数据", description = "根据查询条件导出用户数据到Excel文件")
@SaCheckPermission("system:user:export")
@GetMapping("/sysLoginUser/exportExcelData")
@OperLog(value = "导出用户数据", operType = OperType.EXPORT)
public void exportExcelData(SysLoginUserVo vo, HttpServletResponse response) {
List<SysLoginUser> list = sysLoginUserService.listLoginUser(vo);
easyExcelUtil.exportExcel(response, list, SysLoginUser.class, "导出", "用户信息表");
}
/**
* @description [用户数据导入]
* @author Leocoder
*/
@Operation(summary = "导入用户数据", description = "从 Excel文件中批量导入用户数据")
@SaCheckPermission("system:user:import")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/sysLoginUser/importExcelData")
@OperLog(value = "导入用户数据", operType = OperType.IMPORT, saveRequestData = false, saveResponseData = true)
public void importExcelData(@RequestParam("file") MultipartFile multipartFile) {
// 判断是否符合excel文件格式
FileTypeUtil.decideIsExcel(multipartFile);
// 导入数据的字段类型和实体类要保持一致
List<SysLoginUser> list = easyExcelUtil.importExcel(multipartFile, SysLoginUser.class, null);
if (CollectionUtils.isNotEmpty(list)) {
System.out.println("本次成功导入:" + list.size() + "");
List<SysLoginUser> importList = new ArrayList<>();
list.forEach(yu -> {
SysLoginUser sysLoginUser = new SysLoginUser();
sysLoginUser.setLoginName(yu.getLoginName());
sysLoginUser.setUserName(yu.getUserName());
sysLoginUser.setSalt(VerifyCodeUtil.generateMixedLetters(6));
sysLoginUser.setPassword(CoderPasswordUtil.getPassword(sysLoginUser.getSalt(), "123456"));
sysLoginUser.setUserType(CoderConstants.TWO_STRING);
sysLoginUser.setEmail(yu.getEmail());
sysLoginUser.setPhone(yu.getPhone());
sysLoginUser.setSex(yu.getSex());
sysLoginUser.setAvatar(yu.getAvatar());
sysLoginUser.setUserStatus(CoderConstants.ZERO_STRING);
sysLoginUser.setCreateTime(LocalDateTime.now());
importList.add(sysLoginUser);
});
boolean saveBatch = sysLoginUserService.saveBatch(importList);
YUtil.isTrue(!saveBatch, "导入失败");
}
}
}

View File

@ -0,0 +1,54 @@
package org.leocoder.heritage.system.service.dashboard;
import org.leocoder.heritage.domain.model.vo.system.DashboardStatisticsVo;
import org.leocoder.heritage.domain.model.vo.system.LoginTrendVo;
/**
* 仪表盘统计服务接口
*
* @author Leocoder
*/
public interface DashboardService {
/**
* @description 获取仪表盘统计数据
* @author Leocoder
*/
DashboardStatisticsVo getStatistics();
/**
* @description 获取登录趋势数据
* @author Leocoder
*/
LoginTrendVo getLoginTrend(Integer days);
/**
* @description 获取完整仪表盘数据
* @author Leocoder
*/
DashboardStatisticsVo getAllData(Boolean includeTrend, Integer trendDays);
/**
* @description 获取用户统计数据
* @author Leocoder
*/
DashboardStatisticsVo.UserStatsVo getUserStats();
/**
* @description 获取登录统计数据
* @author Leocoder
*/
DashboardStatisticsVo.LoginStatsVo getLoginStats(Boolean includeTrend, Integer trendDays);
/**
* @description 获取存储统计数据
* @author Leocoder
*/
DashboardStatisticsVo.StorageStatsVo getStorageStats();
/**
* @description 获取今日活跃统计数据
* @author Leocoder
*/
DashboardStatisticsVo.DailyActivityStatsVo getDailyActivityStats();
}

View File

@ -0,0 +1,373 @@
package org.leocoder.heritage.system.service.dashboard;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.domain.model.vo.system.DashboardStatisticsVo;
import org.leocoder.heritage.domain.model.vo.system.LoginTrendVo;
import org.leocoder.heritage.domain.pojo.system.*;
import org.leocoder.heritage.mybatisplus.mapper.system.*;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
/**
* 仪表盘统计服务实现类
*
* @author Leocoder
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DashboardServiceImpl implements DashboardService {
private final SysLoginUserMapper sysLoginUserMapper;
private final SysLoginLogMapper sysLoginLogMapper;
private final SysOperLogMapper sysOperLogMapper;
private final SysFileMapper sysFileMapper;
private final SysPictureMapper sysPictureMapper;
/**
* @description 获取仪表盘统计数据
* @author Leocoder
*/
@Override
public DashboardStatisticsVo getStatistics() {
DashboardStatisticsVo statisticsVo = new DashboardStatisticsVo();
// 设置用户统计数据
statisticsVo.setUserStats(getUserStats());
// 设置登录统计数据不包含趋势
statisticsVo.setLoginStats(getLoginStats(false, null));
// 设置存储统计数据
statisticsVo.setStorageStats(getStorageStats());
// 设置今日活跃统计数据
statisticsVo.setDailyActivityStats(getDailyActivityStats());
return statisticsVo;
}
/**
* @description 获取登录趋势数据
* @author Leocoder
*/
@Override
public LoginTrendVo getLoginTrend(Integer days) {
if (days == null || days <= 0) {
days = 7;
}
LoginTrendVo loginTrendVo = new LoginTrendVo();
List<LoginTrendVo.LoginTrendItemVo> trendList = new ArrayList<>();
LocalDate today = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern(
"M月d日"
);
for (int i = days - 1; i >= 0; i--) {
LocalDate date = today.minusDays(i);
String dateStr = date.format(formatter);
String label = date.format(labelFormatter);
// 统计该日的登录次数
LambdaQueryWrapper<SysLoginLog> wrapper =
new LambdaQueryWrapper<>();
wrapper
.apply("DATE(login_time) = {0}", dateStr)
// 成功登录
.eq(SysLoginLog::getLoginStatus, "0");
Long count = sysLoginLogMapper.selectCount(wrapper);
LoginTrendVo.LoginTrendItemVo trendItem =
new LoginTrendVo.LoginTrendItemVo();
trendItem.setDate(dateStr);
trendItem.setCount(count != null ? count.intValue() : 0);
trendItem.setLabel(label);
trendList.add(trendItem);
}
loginTrendVo.setLoginTrend(trendList);
return loginTrendVo;
}
/**
* @description 获取完整仪表盘数据
* @author Leocoder
*/
@Override
public DashboardStatisticsVo getAllData(
Boolean includeTrend,
Integer trendDays
) {
DashboardStatisticsVo statisticsVo = new DashboardStatisticsVo();
// 设置用户统计数据
statisticsVo.setUserStats(getUserStats());
// 设置登录统计数据根据参数决定是否包含趋势
statisticsVo.setLoginStats(getLoginStats(includeTrend, trendDays));
// 设置存储统计数据
statisticsVo.setStorageStats(getStorageStats());
// 设置今日活跃统计数据
statisticsVo.setDailyActivityStats(getDailyActivityStats());
return statisticsVo;
}
/**
* @description 获取用户统计数据
* @author Leocoder
*/
@Override
public DashboardStatisticsVo.UserStatsVo getUserStats() {
DashboardStatisticsVo.UserStatsVo userStats =
new DashboardStatisticsVo.UserStatsVo();
try {
// 总用户数
Long totalUsers = sysLoginUserMapper.selectCount(null);
userStats.setTotalUsers(
totalUsers != null ? totalUsers.intValue() : 0
);
// 今日新增用户数
LambdaQueryWrapper<SysLoginUser> todayWrapper =
new LambdaQueryWrapper<>();
todayWrapper.apply("DATE(create_time) = CURDATE()");
Long todayNewUsers = sysLoginUserMapper.selectCount(todayWrapper);
userStats.setTodayNewUsers(
todayNewUsers != null ? todayNewUsers.intValue() : 0
);
// 活跃用户数最近30天有登录记录的用户
// 使用原生SQL统计不重复的登录用户数
LambdaQueryWrapper<SysLoginLog> activeWrapper =
new LambdaQueryWrapper<>();
activeWrapper
.apply("login_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)")
.eq(SysLoginLog::getLoginStatus, "0");
// 获取符合条件的登录记录然后统计不重复的用户名
List<SysLoginLog> loginLogs = sysLoginLogMapper.selectList(activeWrapper);
long activeUsers = loginLogs.stream()
.map(SysLoginLog::getLoginName)
.distinct()
.count();
userStats.setActiveUsers((int) activeUsers);
// 当前在线用户数通过Sa-Token获取
List<String> onlineTokens = StpUtil.searchTokenValue(
"",
0,
-1,
false
);
userStats.setOnlineUsers(
onlineTokens != null ? onlineTokens.size() : 0
);
} catch (Exception e) {
log.error("获取用户统计数据失败", e);
// 设置默认值
userStats.setTotalUsers(0);
userStats.setTodayNewUsers(0);
userStats.setActiveUsers(0);
userStats.setOnlineUsers(0);
}
return userStats;
}
/**
* @description 获取登录统计数据
* @author Leocoder
*/
@Override
public DashboardStatisticsVo.LoginStatsVo getLoginStats(
Boolean includeTrend,
Integer trendDays
) {
DashboardStatisticsVo.LoginStatsVo loginStats =
new DashboardStatisticsVo.LoginStatsVo();
try {
// 今日登录次数
LambdaQueryWrapper<SysLoginLog> todayWrapper =
new LambdaQueryWrapper<>();
todayWrapper
.apply("DATE(login_time) = CURDATE()")
.eq(SysLoginLog::getLoginStatus, "0"); // 成功登录
Long todayLogins = sysLoginLogMapper.selectCount(todayWrapper);
loginStats.setTodayLogins(
todayLogins != null ? todayLogins.intValue() : 0
);
// 累计登录次数
LambdaQueryWrapper<SysLoginLog> totalWrapper =
new LambdaQueryWrapper<>();
totalWrapper.eq(SysLoginLog::getLoginStatus, "0"); // 成功登录
Long totalLogins = sysLoginLogMapper.selectCount(totalWrapper);
loginStats.setTotalLogins(
totalLogins != null ? totalLogins.intValue() : 0
);
// 根据参数决定是否包含趋势数据
if (Boolean.TRUE.equals(includeTrend)) {
LoginTrendVo trendVo = getLoginTrend(trendDays);
// 转换LoginTrendItemVo类型
if (trendVo.getLoginTrend() != null) {
List<DashboardStatisticsVo.LoginTrendItemVo> trendItems =
new ArrayList<>();
for (LoginTrendVo.LoginTrendItemVo item : trendVo.getLoginTrend()) {
DashboardStatisticsVo.LoginTrendItemVo trendItem =
new DashboardStatisticsVo.LoginTrendItemVo();
trendItem.setDate(item.getDate());
trendItem.setCount(item.getCount());
trendItem.setLabel(item.getLabel());
trendItems.add(trendItem);
}
loginStats.setLoginTrend(trendItems);
}
}
} catch (Exception e) {
log.error("获取登录统计数据失败", e);
// 设置默认值
loginStats.setTodayLogins(0);
loginStats.setTotalLogins(0);
}
return loginStats;
}
/**
* @description 获取存储统计数据
* @author Leocoder
*/
@Override
public DashboardStatisticsVo.StorageStatsVo getStorageStats() {
DashboardStatisticsVo.StorageStatsVo storageStats =
new DashboardStatisticsVo.StorageStatsVo();
try {
// 总文件数
Long totalFiles = sysFileMapper.selectCount(null);
storageStats.setTotalFiles(
totalFiles != null ? totalFiles.intValue() : 0
);
// 总图片数
Long totalImages = sysPictureMapper.selectCount(null);
storageStats.setTotalImages(
totalImages != null ? totalImages.intValue() : 0
);
// 今日上传文件数
LambdaQueryWrapper<SysFile> todayWrapper =
new LambdaQueryWrapper<>();
todayWrapper.apply("DATE(create_time) = CURDATE()");
Long todayUploads = sysFileMapper.selectCount(todayWrapper);
storageStats.setTodayUploads(
todayUploads != null ? todayUploads.intValue() : 0
);
// 计算总存储大小这里可以根据实际情况优化
// 由于文件大小计算比较复杂这里先设置示例数据
storageStats.setTotalSize("2.3 GB");
storageStats.setStorageUsage(67.5);
storageStats.setAvailableSpace("1.2 GB");
} catch (Exception e) {
log.error("获取存储统计数据失败", e);
// 设置默认值
storageStats.setTotalFiles(0);
storageStats.setTotalImages(0);
storageStats.setTodayUploads(0);
storageStats.setTotalSize("0 B");
storageStats.setStorageUsage(0.0);
storageStats.setAvailableSpace("0 B");
}
return storageStats;
}
/**
* @description 获取今日活跃统计数据
* @author Leocoder
*/
@Override
public DashboardStatisticsVo.DailyActivityStatsVo getDailyActivityStats() {
DashboardStatisticsVo.DailyActivityStatsVo activityStats =
new DashboardStatisticsVo.DailyActivityStatsVo();
try {
// 今日访问量
LambdaQueryWrapper<SysOperLog> todayWrapper =
new LambdaQueryWrapper<>();
todayWrapper.apply("DATE(oper_time) = CURDATE()");
Long todayVisits = sysOperLogMapper.selectCount(todayWrapper);
activityStats.setTodayVisits(
todayVisits != null ? todayVisits.intValue() : 0
);
// 今日操作数排除查询操作
LambdaQueryWrapper<SysOperLog> operWrapper =
new LambdaQueryWrapper<>();
operWrapper
.apply("DATE(oper_time) = CURDATE()")
// 排除查询操作
.ne(SysOperLog::getOperType, "SELECT");
Long todayOperations = sysOperLogMapper.selectCount(operWrapper);
activityStats.setTodayOperations(
todayOperations != null ? todayOperations.intValue() : 0
);
// 活跃用户数取用户统计中的活跃用户数
DashboardStatisticsVo.UserStatsVo userStats = getUserStats();
activityStats.setActiveUsers(userStats.getActiveUsers());
// 新增内容数今日新增的文件和图片
LambdaQueryWrapper<SysFile> fileWrapper =
new LambdaQueryWrapper<>();
fileWrapper.apply("DATE(create_time) = CURDATE()");
Long newFiles = sysFileMapper.selectCount(fileWrapper);
LambdaQueryWrapper<SysPicture> pictureWrapper =
new LambdaQueryWrapper<>();
pictureWrapper.apply("DATE(create_time) = CURDATE()");
Long newPictures = sysPictureMapper.selectCount(pictureWrapper);
int newContent =
(newFiles != null ? newFiles.intValue() : 0) +
(newPictures != null ? newPictures.intValue() : 0);
activityStats.setNewContent(newContent);
// API调用次数今日操作日志总数
activityStats.setApiCalls(activityStats.getTodayVisits());
// 平均响应时间这里可以根据实际情况计算
// 示例数据
activityStats.setAvgResponseTime(235);
} catch (Exception e) {
log.error("获取今日活跃统计数据失败", e);
// 设置默认值
activityStats.setTodayVisits(0);
activityStats.setTodayOperations(0);
activityStats.setActiveUsers(0);
activityStats.setNewContent(0);
activityStats.setApiCalls(0);
activityStats.setAvgResponseTime(0);
}
return activityStats;
}
}

View File

@ -0,0 +1,17 @@
package org.leocoder.heritage.system.service.dictdata;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.pojo.system.SysDictData;
/**
* @author Leocoder
* @description [字典数据表-服务实现层接口]
*/
public interface SysDictDataService extends IService<SysDictData> {
/**
* @description [字典数据同步Redis进行缓存]
* @author Leocoder
*/
void listDictCacheRedis();
}

View File

@ -0,0 +1,62 @@
package org.leocoder.heritage.system.service.dictdata;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.common.constants.CoderCacheConstants;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.utils.cache.RedisUtil;
import org.leocoder.heritage.domain.pojo.system.SysDictData;
import org.leocoder.heritage.domain.pojo.system.SysDictType;
import org.leocoder.heritage.mybatisplus.mapper.system.SysDictDataMapper;
import org.leocoder.heritage.mybatisplus.mapper.system.SysDictTypeMapper;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Leocoder
* @description [字典数据表-服务实现层]
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysDictDataServiceImpl extends ServiceImpl<SysDictDataMapper, SysDictData> implements SysDictDataService {
private final SysDictDataMapper sysDictDataMapper;
private final SysDictTypeMapper sysDictTypeMapper;
private final RedisUtil redisUtil;
/**
* @description [字典数据同步Redis进行缓存]
* @author Leocoder
*/
@Override
public void listDictCacheRedis() {
// 1查询所有可用的字典数据
LambdaQueryWrapper<SysDictType> dictTypeWrapper = new LambdaQueryWrapper<>();
dictTypeWrapper.select(SysDictType::getDictType, SysDictType::getDictStatus);
List<SysDictType> dictTypeList = sysDictTypeMapper.selectList(dictTypeWrapper);
if (CollectionUtil.isNotEmpty(dictTypeList)) {
// 2根据字典类型查询字典数据
for (SysDictType sysDictType : dictTypeList) {
if (sysDictType.getDictStatus().equals(CoderConstants.ZERO_STRING)) {
LambdaQueryWrapper<SysDictData> dictDataWrapper = new LambdaQueryWrapper<>();
dictDataWrapper.eq(SysDictData::getDictStatus, CoderConstants.ZERO_STRING);
dictDataWrapper.eq(SysDictData::getDictType, sysDictType.getDictType());
dictDataWrapper.orderByAsc(SysDictData::getSorted);
List<SysDictData> dictDataList = sysDictDataMapper.selectList(dictDataWrapper);
// 3把字典数据循环存到Redis设计Redis的keyCoderDict:dictType -> [{},{},{}]
redisUtil.setCacheObject(CoderCacheConstants.DICT_REDIS_KEY + sysDictType.getDictType(), dictDataList);
} else {
redisUtil.deleteKey(CoderCacheConstants.DICT_REDIS_KEY + sysDictType.getDictType());
}
}
}
}
}

View File

@ -0,0 +1,12 @@
package org.leocoder.heritage.system.service.dicttype;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.pojo.system.SysDictType;
/**
* @author Leocoder
* @description [字典类型表-服务实现层接口]
*/
public interface SysDictTypeService extends IService<SysDictType> {
}

View File

@ -0,0 +1,21 @@
package org.leocoder.heritage.system.service.dicttype;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.domain.pojo.system.SysDictType;
import org.leocoder.heritage.mybatisplus.mapper.system.SysDictTypeMapper;
import org.springframework.stereotype.Service;
/**
* @author Leocoder
* @description [字典类型表-服务实现层]
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysDictTypeServiceImpl extends ServiceImpl<SysDictTypeMapper, SysDictType> implements SysDictTypeService {
private final SysDictTypeMapper sysDictTypeMapper;
}

View File

@ -0,0 +1,13 @@
package org.leocoder.heritage.system.service.file;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.pojo.system.SysFile;
/**
* @description [文件资源表-服务实现层接口]
* @author Leocoder
*/
public interface SysFileService extends IService<SysFile> {
}

View File

@ -0,0 +1,22 @@
package org.leocoder.heritage.system.service.file;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.domain.pojo.system.SysFile;
import org.leocoder.heritage.mybatisplus.mapper.system.SysFileMapper;
import org.springframework.stereotype.Service;
/**
* @description [文件资源表-服务实现层]
* @author Leocoder
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> implements SysFileService {
private final SysFileMapper sysFileMapper;
}

View File

@ -0,0 +1,32 @@
package org.leocoder.heritage.system.service.login;
import org.leocoder.heritage.domain.model.bo.system.SysLoginBo;
import org.leocoder.heritage.domain.model.vo.system.SysLoginVo;
import org.leocoder.heritage.domain.model.vo.system.SysRegisterVo;
/**
* @author Leocoder
* @description [SysLoginService]
*/
public interface SysLoginService {
/**
* @description [PC登录]
* @author Leocoder
*/
SysLoginBo login(SysLoginVo loginVo);
/**
* @description [退出]
* @author Leocoder
*/
void logout();
/**
* @description [PC注册]
* @author Leocoder
*/
void register(SysRegisterVo registerVo);
}

View File

@ -0,0 +1,283 @@
package org.leocoder.heritage.system.service.login;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.constants.CoderCacheConstants;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.constants.SaTokenSessionConstants;
import org.leocoder.heritage.common.enmus.log.ClientType;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.satoken.CoderLoginUser;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.common.satoken.CoderPasswordUtil;
import org.leocoder.heritage.common.utils.cache.RedisUtil;
import org.leocoder.heritage.common.utils.ip.IpAddressUtil;
import org.leocoder.heritage.common.utils.ip.IpUtil;
import org.leocoder.heritage.common.utils.ip.ServletUtil;
import org.leocoder.heritage.common.utils.number.VerifyCodeUtil;
import org.leocoder.heritage.domain.model.bo.system.SysLoginBo;
import org.leocoder.heritage.domain.model.vo.system.SysLoginVo;
import org.leocoder.heritage.domain.model.vo.system.SysRegisterVo;
import org.leocoder.heritage.domain.pojo.system.SysLoginUser;
import org.leocoder.heritage.domain.pojo.system.SysRole;
import org.leocoder.heritage.system.service.loginlog.SysLoginLogService;
import org.leocoder.heritage.system.service.role.SysRoleService;
import org.leocoder.heritage.system.service.user.SysLoginUserService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Date;
import java.util.List;
/**
* @author Leocoder
* @description [SysLoginServiceImpl]
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysLoginServiceImpl implements SysLoginService {
private final RedisUtil redisUtil;
private final IpAddressUtil ipAddressUtil;
private final SysLoginUserService sysLoginUserService;
private final SysLoginLogService sysLoginLogService;
private final SysRoleService sysRoleService;
/**
* @description [PC登录]
* @author Leocoder
*/
@Override
public SysLoginBo login(@Validated SysLoginVo loginVo) {
YUtil.isTrue(ObjectUtils.isEmpty(loginVo), "登录失败");
// 1登录后核实redis验证码数据
String securityCode = redisUtil.getKey(CoderCacheConstants.CAPTCHA_CODE_KEY + loginVo.getCodeKey());
YUtil.isTrue(StringUtils.isBlank(securityCode), "验证码已失效");
log.info("登录账号:{},验证码:{}", loginVo.getLoginName(), securityCode);
// 2前端登录输入验证码不区分大小写
YUtil.isTrue(!securityCode.equalsIgnoreCase(loginVo.getSecurityCode()), "验证码输入错误");
// 3验证账号是否存在
LambdaQueryWrapper<SysLoginUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysLoginUser::getLoginName, loginVo.getLoginName());
wrapper.last("LIMIT 1");
// 4根据登录名查询用户数据(失败不抛出异常影响登录日志记录)
SysLoginUser loginUser = sysLoginUserService.getOne(wrapper, false);
log.info("登录用户详细信息:{}", loginUser);
// 5用户不存在
if (ObjectUtils.isEmpty(loginUser)) {
// 保存登录日志(账户1(失败)提示信息客户端Request)
sysLoginLogService.addLoginLog(loginVo.getLoginName(), CoderConstants.ONE_STRING, "账户不存在");
YUtil.isTrue("账户或密码不正确");
}
// 6账户是否被冻结
if (loginUser.getUserStatus().equals("1")) {
// 保存登录日志
sysLoginLogService.addLoginLog(loginVo.getLoginName(), CoderConstants.ONE_STRING, "账户已被冻结");
YUtil.isTrue("账户已被冻结");
}
// 7账户是否输入错误超过指定次数
if (getPwdErrorNumber(loginVo.getLoginName()) >= 3) {
// 保存登录日志
sysLoginLogService.addLoginLog(loginVo.getLoginName(), CoderConstants.ONE_STRING, "十分钟内账号密码输错3次");
// 账号锁定过期时间
long expireTime = redisUtil.getExpire(CoderCacheConstants.PWD_ERROR_KEY + loginVo.getLoginName());
YUtil.isTrue("账号密码输错3次" + expireTime + "秒后重试");
}
// 8密码不正确进行处理
if (!CoderPasswordUtil.getPassword(loginVo.getPassword(), loginUser.getSalt()).equals(loginUser.getPassword())) {
// 保存登录日志
sysLoginLogService.addLoginLog(loginVo.getLoginName(), CoderConstants.ONE_STRING, "密码不正确");
// 仅仅配置十分钟内输错3次若想输入5次进行锁定请自行添加逻辑
setPwdErrorExpire(loginVo.getLoginName());
YUtil.isTrue("账户或密码不正确");
}
// 9登录成功保存登录日志[舍弃-Sa-Token监听其中进行保存登录日志也阔以]
sysLoginLogService.addLoginLog(loginVo.getLoginName(), CoderConstants.ZERO_STRING, "登录成功");
// 10执行登录
// 设置当前登录用户信息缓存有需要的其他全局获取信息的自行添加例如cityId
CoderLoginUser CoderLoginUser = setSaTokenLoginUserCache(loginUser);
SaLoginParameter loginParameter = new SaLoginParameter();
// 是否为持久Cookie临时Cookie在浏览器关闭时会自动删除持久Cookie在重新打开后依然存在当此值为false后关闭浏览器后再次打开需要重新登录
loginParameter.setIsLastingCookie(loginVo.getRememberMe());
loginParameter.setDeviceType(CoderConstants.SYSTEM_PC);
// 自定义分配不同用户体系的不同Token授权时间不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户7天过期
// 指定此次登录 token 的有效期, 单位:-1 = 永久有效
// loginParameter.setTimeout(30 * 60);
// 记录在 Token 上的扩展参数只在 jwt 模式下生效
// loginParameter.setExtra("key", "value")
// 登录 生成token
CoderLoginUtil.login(CoderLoginUser, loginParameter);
// 11修改登录时间 登录IP地址
LambdaUpdateWrapper<SysLoginUser> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysLoginUser::getLoginTime, new Date());
updateWrapper.set(SysLoginUser::getLoginIp, IpUtil.getIpAddr(ServletUtil.getRequest()));
updateWrapper.eq(SysLoginUser::getUserId, loginUser.getUserId());
sysLoginUserService.update(updateWrapper);
// 12返回token
SysLoginBo loginBo = new SysLoginBo();
loginBo.setTokenName(StpUtil.getTokenName());
loginBo.setTokenValue(StpUtil.getTokenValue());
log.info("登录验证-当前会话是否登录:{}", StpUtil.isLogin());
// 13返回字段少不用SysLoginBo实体类直接使用 JSONObject 也是可以的
// JSONObject jsonObject = new JSONObject();
// jsonObject.put("tokenName", StpUtil.getTokenName());
// jsonObject.put("tokenValue", StpUtil.getTokenValue());
// 13将用户数据放入缓存中后续方便获取当前用户数据使用
// 这个是以token当做key存入redis中
// StpUtil.getTokenSession().set(SaTokenSessionConstants.LOGIN_USER, CoderLoginUser);
// 这个是以sessionId[用户ID]当做key存入redis中
StpUtil.getSession().set(SaTokenSessionConstants.LOGIN_USER, CoderLoginUser);
// 14登录成功删除验证码
redisUtil.deleteKey(CoderCacheConstants.CAPTCHA_CODE_KEY + loginVo.getCodeKey());
// 15登录成功删除该用户的密码key
clearPwdLock(loginVo.getLoginName());
// 16登录成功返回Token信息
return loginBo;
}
/**
* @description [设置PC端当前登录用户信息缓存]
* @author Leocoder
*/
private CoderLoginUser setSaTokenLoginUserCache(SysLoginUser loginUser) {
CoderLoginUser CoderLoginUser = new CoderLoginUser();
// 1当前登录用户信息
CoderLoginUser.setUserId(loginUser.getUserId());
CoderLoginUser.setLoginName(loginUser.getLoginName());
CoderLoginUser.setUserName(loginUser.getUserName());
CoderLoginUser.setAvatar(loginUser.getAvatar());
CoderLoginUser.setSex(loginUser.getSex());
CoderLoginUser.setLoginTime(loginUser.getLoginTime());
CoderLoginUser.setPhone(loginUser.getPhone());
CoderLoginUser.setEmail(loginUser.getEmail());
CoderLoginUser.setUserType(loginUser.getUserType());
CoderLoginUser.setCreateTime(loginUser.getCreateTime());
// 登录时该用户必须拥有角色该角色可以不分配部门权限数据
boolean isCoderAdmin = sysRoleService.getIsCoderAdmin(CoderLoginUser.getUserId());
// 是否超级管理员
CoderLoginUser.setIsCoderAdmin(loginUser.getUserId().equals(CoderConstants.ONE_LONG) || isCoderAdmin);
// 2IP地址
CoderLoginUser.setLoginIp(loginUser.getLoginIp());
// 3登录地址
CoderLoginUser.setLoginAddress(ipAddressUtil.getAddress(loginUser.getLoginIp()));
// 4UserAgent信息
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtil.getRequest().getHeader("User-Agent"));
// 5登录浏览器
CoderLoginUser.setBrowser(userAgent.getBrowser().getName());
// 6登录操作系统
CoderLoginUser.setOs(userAgent.getOperatingSystem().getName());
// 7登录设备
String deviceName = userAgent.getOperatingSystem().getDeviceType().getName();
if (StringUtils.isBlank(deviceName)) {
deviceName = "Default";
} else if ("Computer".equals(deviceName)) {
deviceName = ClientType.PC.name();
} else if ("Mobile".equals(deviceName)) {
deviceName = ClientType.MOBILE.name();
}
CoderLoginUser.setDeviceName(deviceName);
return CoderLoginUser;
}
/**
* @description [清除用户锁定状态]
* @author Leocoder
*/
public void clearPwdLock(String loginName) {
redisUtil.deleteKey(CoderCacheConstants.PWD_ERROR_KEY + loginName);
}
/**
* @description [获取密码锁定次数]
* @author Leocoder
*/
public int getPwdErrorNumber(String loginName) {
Integer pwdErrorNumber = redisUtil.getKey(CoderCacheConstants.PWD_ERROR_KEY + loginName);
return pwdErrorNumber == null ? 0 : pwdErrorNumber;
}
/**
* @description [根据密码错误次数设置过期时间]
* @author Leocoder
*/
public void setPwdErrorExpire(String loginName) {
int pwdErrorNumber = getPwdErrorNumber(loginName);
// 密码错误次数加一
redisUtil.setCacheObjectMinutes(CoderCacheConstants.PWD_ERROR_KEY + loginName, pwdErrorNumber + 1, CoderConstants.TEN_NUMBER);
// 十分钟内输错3次
if (pwdErrorNumber >= 3) {
redisUtil.expireMinutes(CoderCacheConstants.PWD_ERROR_KEY + loginName, CoderConstants.TEN_NUMBER);
YUtil.isTrue("密码输入错误次数3次锁定10分钟");
}
}
/**
* @description [退出]
* @author Leocoder
*/
@Override
public void logout() {
StpUtil.logout();
}
/**
* @description [PC注册]
* @author Leocoder
*/
@Override
public void register(SysRegisterVo registerVo) {
// 1检测邀请码是否存在 密码是否一致
YUtil.isTrue(!registerVo.getRegCode().equals("Coder-ADMIN"), "邀请码输入错误");
YUtil.isTrue(!registerVo.getRegPwd().equals(registerVo.getRegConfirmPwd()), "密码和确认密码不一致");
// 2验证账号是否存在
LambdaQueryWrapper<SysLoginUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysLoginUser::getLoginName, registerVo.getRegLoginName());
wrapper.last("LIMIT 1");
// 3根据登录名查询用户数据
SysLoginUser sysLoginUser = sysLoginUserService.getOne(wrapper);
YUtil.isTrue(ObjectUtils.isNotEmpty(sysLoginUser), "当前登录名称已存在");
// 4新增注册用户默认密码
SysLoginUser loginUser = new SysLoginUser();
loginUser.setLoginName(registerVo.getRegLoginName());
loginUser.setUserName(registerVo.getRegLoginName());
loginUser.setSalt(VerifyCodeUtil.generateMixedLetters(6));
loginUser.setPassword(CoderPasswordUtil.getPassword(registerVo.getRegPwd(), loginUser.getSalt()));
loginUser.setUserType(CoderConstants.TWO_STRING);
loginUser.setUserStatus(CoderConstants.ZERO_STRING);
loginUser.setSex(CoderConstants.THREE_STRING);
loginUser.setPhone("18566666666");
loginUser.setEmail("Coder-ADMIN@163.com");
loginUser.setAvatar("https://pic1.zhimg.com/v2-60d16e77a47a49cfae8fb451ce514840_b.webp");
boolean save = sysLoginUserService.save(loginUser);
YUtil.isTrue(!save, "注册用户失败,请重试");
// 注意用户没有角色前端则会自动跳转登录页面不会进入管理平台主页
// 6配置默认角色[普通用户Coder_COMMON]
LambdaQueryWrapper<SysRole> roleWrapper = new LambdaQueryWrapper<>();
roleWrapper.eq(SysRole::getRoleCode, CoderConstants.CODER_COMMON);
roleWrapper.eq(SysRole::getRoleStatus, CoderConstants.ZERO_STRING);
long roleCount = sysRoleService.count(roleWrapper);
if (roleCount > CoderConstants.ZERO_LONG) {
SysRole commonRole = sysRoleService.getOne(roleWrapper);
Long roleId = commonRole.getRoleId();
sysRoleService.assignUserRole(loginUser.getUserId(), List.of(roleId));
} else {
YUtil.isTrue("系统暂未配置注册用户数据[角色]");
}
}
}

View File

@ -0,0 +1,18 @@
package org.leocoder.heritage.system.service.loginlog;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.pojo.system.SysLoginLog;
/**
* @author Leocoder
* @description [系统访问记录-服务实现层接口]
*/
public interface SysLoginLogService extends IService<SysLoginLog> {
/**
* @description [保存登录日志]
* @author Leocoder
*/
void addLoginLog(String loginName, String loginStatus, String message);
}

View File

@ -0,0 +1,75 @@
package org.leocoder.heritage.system.service.loginlog;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.enmus.log.ClientType;
import org.leocoder.heritage.common.utils.ip.IpAddressUtil;
import org.leocoder.heritage.common.utils.ip.IpUtil;
import org.leocoder.heritage.common.utils.ip.ServletUtil;
import org.leocoder.heritage.domain.pojo.system.SysLoginLog;
import org.leocoder.heritage.mybatisplus.mapper.system.SysLoginLogMapper;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* @author Leocoder
* @description [系统访问记录-服务实现层]
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysLoginLogServiceImpl extends ServiceImpl<SysLoginLogMapper, SysLoginLog> implements SysLoginLogService {
private final IpAddressUtil ipAddressUtil;
/**
* @description [保存登录日志]
* @author Leocoder
*/
@Override
public void addLoginLog(String loginName, String loginStatus, String message) {
// 1new一个登录日志对象用来装载信息
SysLoginLog sysLoginLog = new SysLoginLog();
try {
sysLoginLog.setLoginName(loginName);
sysLoginLog.setLoginStatus(loginStatus);
// LocalDateTime.now() 时间类型为 LocalDateTime类型
sysLoginLog.setLoginTime(LocalDateTime.now());
sysLoginLog.setMessage(message);
// 2登录IP地址
sysLoginLog.setLoginIp(IpUtil.getIpAddr(ServletUtil.getRequest()));
// 3登录地理位置
sysLoginLog.setLoginAddress(ipAddressUtil.getAddress(sysLoginLog.getLoginIp()));
// 4UserAgent信息
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtil.getRequest().getHeader("User-Agent"));
// 5登录浏览器
sysLoginLog.setBrowser(userAgent.getBrowser().getName());
// 6登录操作系统
sysLoginLog.setOs(userAgent.getOperatingSystem().getName());
// 7登录设备
String deviceName = userAgent.getOperatingSystem().getDeviceType().getName();
if (StringUtils.isBlank(deviceName)) {
deviceName = "Default";
} else if ("Computer".equals(deviceName)) {
deviceName = ClientType.PC.name();
} else if ("Mobile".equals(deviceName)) {
deviceName = ClientType.MOBILE.name();
}
sysLoginLog.setDeviceName(deviceName);
} catch (Exception e) {
sysLoginLog.setMessage(e.getMessage());
sysLoginLog.setLoginStatus("1");
log.error("登录日志异常信息:{}", e.getMessage());
e.printStackTrace();
} finally {
// 8登录日志保存
this.save(sysLoginLog);
}
}
}

View File

@ -0,0 +1,59 @@
package org.leocoder.heritage.system.service.menu;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.model.bo.element.CascaderLongBo;
import org.leocoder.heritage.domain.model.bo.system.SysMenuBo;
import org.leocoder.heritage.domain.model.vo.system.SysMenuVo;
import org.leocoder.heritage.domain.pojo.system.SysMenu;
import java.util.List;
/**
* @author Leocoder
* @description [菜单权限表-服务实现层接口]
*/
public interface SysMenuService extends IService<SysMenu> {
/**
* @description [查询菜单]
* @author Leocoder
*/
List<SysMenu> listSysMenu(SysMenuVo vo);
/**
* @description [菜单级联下拉框]
* @author Leocoder
*/
List<CascaderLongBo> cascaderList();
/**
* @description [生成当前用户所拥有菜单路由]
* @author Leocoder
*/
List<SysMenuBo> generatorRouters();
/**
* @description [根据用户拥有的角色ID查询权限菜单(不包含父节点)]
* @author Leocoder
*/
List<Long> listMenuIdsByRoleId(Long roleId);
/**
* @description [保存角色和菜单权限之间的关系]
* @author Leocoder
*/
void saveRoleMenu(Long roleId, List<Long> menuIds);
/**
* @description [查询所有正常的路由 AND 展开节点角色分配菜单权限使用]
* @author Leocoder
*/
List<SysMenu> listMenuNormal(SysMenuVo sysMenuVo);
/**
* @description [注册-根据角色ID查询菜单(包含父节点)]
* @author Leocoder
*/
List<Long> listMenuNodesByRoleId(Long roleId);
}

View File

@ -0,0 +1,182 @@
package org.leocoder.heritage.system.service.menu;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.domain.model.bo.element.CascaderLongBo;
import org.leocoder.heritage.domain.model.bo.system.SysMenuBo;
import org.leocoder.heritage.domain.model.vo.system.SysMenuVo;
import org.leocoder.heritage.domain.pojo.system.SysLoginUser;
import org.leocoder.heritage.domain.pojo.system.SysMenu;
import org.leocoder.heritage.mybatisplus.mapper.system.SysLoginUserMapper;
import org.leocoder.heritage.mybatisplus.mapper.system.SysMenuMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Leocoder
* @description [菜单权限表-服务实现层]
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
private final SysMenuMapper sysMenuMapper;
private final SysLoginUserMapper sysLoginUserMapper;
/**
* @description [查询菜单]
* @author Leocoder
*/
@Override
public List<SysMenu> listSysMenu(SysMenuVo vo) {
return sysMenuMapper.listSysMenu(vo);
}
/**
* @description [生成当前用户所拥有菜单路由]
* @author Leocoder
*/
@Override
public List<SysMenuBo> generatorRouters() {
// 1根据用户ID查询用户信息
SysLoginUser loginUser = sysLoginUserMapper.selectById(CoderLoginUtil.getUserId());
YUtil.isTrue(ObjectUtils.isEmpty(loginUser), "请先进行登录");
// 2查询菜单数据
// 超级管理员全部菜单权限
if (loginUser != null && CoderLoginUtil.getIsCoderAdmin()) {
List<SysMenuBo> sysMenuBos = sysMenuMapper.listMenuAdmin();
YUtil.isTrue(CollectionUtil.isEmpty(sysMenuBos), "菜单权限全部被删除,禁止登录");
List<SysMenuBo> menuList = sysMenuBos.parallelStream().filter(x -> {
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getParentId, x.getMenuId());
wrapper.last("LIMIT 1");
SysMenu menu = this.getOne(wrapper);
if (ObjectUtil.isNotEmpty(menu)) {
x.setRedirect(menu.getPath());
}
return true;
}).collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(menuList)) {
return menuList;
} else {
return Collections.emptyList();
}
} else {
// 根据用户ID查询当前角色所拥有的菜单列表
List<SysMenuBo> sysMenuBos = sysMenuMapper.listMenuByUserId(CoderLoginUtil.getUserId());
YUtil.isTrue(CollectionUtil.isEmpty(sysMenuBos), "该用户角色未分配菜单权限,禁止登录");
List<SysMenuBo> menuList = sysMenuBos.parallelStream().filter(x -> {
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getParentId, x.getMenuId());
wrapper.last("LIMIT 1");
SysMenu menu = this.getOne(wrapper);
if (ObjectUtil.isNotEmpty(menu)) {
x.setRedirect(menu.getPath());
}
return true;
}).collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(menuList)) {
return menuList;
} else {
return Collections.emptyList();
}
}
}
/**
* @description [菜单级联下拉框]
* @author Leocoder
*/
@Override
public List<CascaderLongBo> cascaderList() {
return sysMenuMapper.cascaderList();
}
/**
* @description [根据用户拥有的角色ID查询权限菜单(不包含父节点)]
* @author Leocoder
*/
@Override
public List<Long> listMenuIdsByRoleId(Long roleId) {
// 菜单反选数据[菜单反选只注重子节点父节点不用返回子节点都有父节点自然会选中]
List<SysMenu> menuList = sysMenuMapper.listMenuIdsByRoleId(roleId);
if(CollectionUtils.isEmpty(menuList)){
return Collections.emptyList();
}
// 剔除父节点数据
List<SysMenu> collectMenuList = menuList.parallelStream().filter(x -> {
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getParentId, x.getMenuId());
long count = this.count(wrapper);
return count == 0L;
}).collect(Collectors.toList());
// 反显数据
if (CollectionUtil.isNotEmpty(collectMenuList)) {
return collectMenuList.parallelStream().map(SysMenu::getMenuId).collect(Collectors.toList());
} else {
return Collections.emptyList();
}
}
/**
* @description [保存角色和菜单权限之间的关系]
* @author Leocoder
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void saveRoleMenu(Long roleId, List<Long> menuIds) {
// 1删除该角色拥有的权限
sysMenuMapper.deleteMenuIdsByRoleId(roleId);
// 取消当前角色所有权限传递空数组或包含-1不进行保存操作
if (CollectionUtil.isNotEmpty(menuIds) && !menuIds.contains(-1L)) {
// 过滤掉无效的菜单ID
List<Long> validMenuIds = menuIds.stream()
.filter(id -> id != null && id > 0)
.collect(Collectors.toList());
if (CollectionUtil.isNotEmpty(validMenuIds)) {
// 2保存菜单权限
YUtil.isTrue(!sysMenuMapper.saveRoleMenu(roleId, validMenuIds), "分配失败,请重试");
}
}
}
/**
* @description [查询所有正常的路由 AND 展开节点角色分配菜单权限使用]
* @author Leocoder
*/
@Override
public List<SysMenu> listMenuNormal(SysMenuVo sysMenuVo) {
sysMenuVo.setMenuStatus(CoderConstants.ZERO_STRING);
return sysMenuMapper.listSysMenu(sysMenuVo);
}
/**
* @description [注册-根据角色ID查询菜单(包含父节点)]
* @author Leocoder
*/
@Override
public List<Long> listMenuNodesByRoleId(Long roleId) {
List<SysMenu> menuList = sysMenuMapper.listMenuIdsByRoleId(roleId);
if(CollectionUtils.isEmpty(menuList)){
return Collections.emptyList();
}
return menuList.parallelStream().map(SysMenu::getMenuId).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,65 @@
package org.leocoder.heritage.system.service.operlog;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
import org.leocoder.heritage.domain.model.vo.system.SysOperLogVo;
import org.leocoder.heritage.domain.pojo.system.SysOperLog;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [操作日志服务接口]
*/
public interface SysOperLogService extends IService<SysOperLog> {
/**
* @description [分页查询操作日志]
* @author Leocoder
*/
Page<SysOperLog> listPage(SysOperLogVo vo);
/**
* @description [根据ID查询操作日志详情]
* @author Leocoder
*/
SysOperLog getDetail(Long operId);
/**
* @description [批量删除操作日志]
* @author Leocoder
*/
void deleteByIds(List<Long> operIds);
/**
* @description [清空操作日志]
* @author Leocoder
*/
void clear();
/**
* @description [导出操作日志]
* @author Leocoder
*/
void export(SysOperLogVo vo, HttpServletResponse response);
/**
* @description [获取操作统计]
* @author Leocoder
*/
Map<String, Object> getStatistics();
/**
* @description [清理过期日志]
* @author Leocoder
*/
void cleanExpiredLogs(int days);
/**
* @description [获取仪表盘统计]
* @author Leocoder
*/
Map<String, Object> getDashboardStats();
}

View File

@ -0,0 +1,290 @@
package org.leocoder.heritage.system.service.operlog;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.utils.cache.RedisUtil;
import org.leocoder.heritage.common.utils.date.DateUtil;
import org.leocoder.heritage.domain.model.vo.system.SysOperLogVo;
import org.leocoder.heritage.domain.pojo.system.SysOperLog;
import org.leocoder.heritage.mybatisplus.mapper.system.SysOperLogMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
/**
* @author Leocoder
* @description [操作日志服务实现]
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class SysOperLogServiceImpl extends ServiceImpl<SysOperLogMapper, SysOperLog> implements SysOperLogService {
private final RedisUtil redisUtil;
/**
* @description [分页查询操作日志]
* @author Leocoder
*/
@Override
public Page<SysOperLog> listPage(SysOperLogVo vo) {
Page<SysOperLog> page = Page.of(vo.getPageNo(), vo.getPageSize());
LambdaQueryWrapper<SysOperLog> wrapper = buildQueryWrapper(vo);
return this.page(page, wrapper);
}
/**
* @description [根据ID查询操作日志详情]
* @author Leocoder
*/
@Override
public SysOperLog getDetail(Long operId) {
YUtil.isNull(operId, "操作日志ID不能为空");
SysOperLog operLog = this.getById(operId);
YUtil.isNull(operLog, "操作日志不存在");
return operLog;
}
/**
* @description [批量删除操作日志]
* @author Leocoder
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteByIds(List<Long> operIds) {
log.info("开始删除操作日志ID列表: {}", operIds);
YUtil.isNull(operIds, "操作日志ID列表不能为空");
YUtil.isTrue(operIds.isEmpty(), "操作日志ID列表不能为空");
try {
// 检查要删除的记录是否存在
long existCount = this.count(new LambdaQueryWrapper<SysOperLog>()
.in(SysOperLog::getOperId, operIds));
log.info("要删除的记录数量: {},实际存在数量: {}", operIds.size(), existCount);
// 执行删除
boolean result = this.removeByIds(operIds);
log.info("删除操作执行结果: {}删除ID数量: {}", result, operIds.size());
if (!result) {
throw new RuntimeException("删除操作失败");
}
log.info("批量删除操作日志成功,数量: {}", operIds.size());
} catch (Exception e) {
log.error("删除操作日志失败ID列表: {}", operIds, e);
throw e;
}
}
/**
* @description [清空操作日志]
* @author Leocoder
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void clear() {
// 清空所有操作日志
this.remove(new LambdaQueryWrapper<>());
// 清空相关缓存
clearAllCache();
log.info("清空操作日志成功");
}
/**
* @description [导出操作日志]
* @author Leocoder
*/
@Override
public void export(SysOperLogVo vo, HttpServletResponse response) {
// 构建查询条件
LambdaQueryWrapper<SysOperLog> wrapper = buildQueryWrapper(vo);
// 查询所有匹配的记录
List<SysOperLog> list = this.list(wrapper);
// 导出Excel
try {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = "操作日志_" + DateUtil.format(new Date(), "yyyyMMdd_HHmmss") + ".xlsx";
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
// 使用EasyExcel导出
// EasyExcel.write(response.getOutputStream(), SysOperLog.class)
// .sheet("操作日志")
// .doWrite(list);
log.info("导出操作日志成功,数量: {}", list.size());
} catch (Exception e) {
log.error("导出操作日志失败", e);
throw new RuntimeException("导出操作日志失败", e);
}
}
/**
* @description [获取操作统计]
* @author Leocoder
*/
@Override
public Map<String, Object> getStatistics() {
Map<String, Object> stats = new HashMap<>();
// 从缓存获取今日统计
String today = DateUtil.format(new Date(), "yyyy-MM-dd");
String totalKey = "oper:stat:total:" + today;
String errorKey = "oper:stat:error:" + today;
Object todayCountObj = redisUtil.getKey(totalKey);
Object errorCountObj = redisUtil.getKey(errorKey);
Long todayCount = todayCountObj != null ? Long.valueOf(todayCountObj.toString()) : 0L;
Long errorCount = errorCountObj != null ? Long.valueOf(errorCountObj.toString()) : 0L;
stats.put("todayCount", todayCount);
stats.put("errorCount", errorCount);
// 从数据库获取总统计
Long totalCount = this.count();
stats.put("totalCount", totalCount);
// 获取操作类型分布
List<Map<String, Object>> typeStats = baseMapper.getOperTypeStats();
stats.put("typeStats", typeStats);
// 获取每日统计
List<Map<String, Object>> dailyStats = baseMapper.getDailyStats();
stats.put("dailyStats", dailyStats);
// 获取用户统计
List<Map<String, Object>> userStats = baseMapper.getUserStats();
stats.put("userStats", userStats);
return stats;
}
/**
* @description [清理过期日志]
* @author Leocoder
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void cleanExpiredLogs(int days) {
YUtil.isTrue(days <= 0, "保留天数必须大于0");
LocalDateTime expireTime = LocalDateTime.now().minusDays(days);
LambdaQueryWrapper<SysOperLog> wrapper = new LambdaQueryWrapper<>();
wrapper.lt(SysOperLog::getOperTime, expireTime);
long deleteCount = this.count(wrapper);
this.remove(wrapper);
log.info("清理{}天前的操作日志成功,删除数量: {}", days, deleteCount);
}
/**
* @description [获取仪表盘统计]
* @author Leocoder
*/
@Override
public Map<String, Object> getDashboardStats() {
Map<String, Object> stats = new HashMap<>();
// 今日统计
String today = DateUtil.format(new Date(), "yyyy-MM-dd");
Long todayCount = this.count(new LambdaQueryWrapper<SysOperLog>()
.ge(SysOperLog::getOperTime, today + " 00:00:00")
.le(SysOperLog::getOperTime, today + " 23:59:59"));
// 昨日统计
Date yesterdayDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
String yesterday = DateUtil.format(yesterdayDate, "yyyy-MM-dd");
Long yesterdayCount = this.count(new LambdaQueryWrapper<SysOperLog>()
.ge(SysOperLog::getOperTime, yesterday + " 00:00:00")
.le(SysOperLog::getOperTime, yesterday + " 23:59:59"));
// 本月统计
// 获取本月第一天
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.DAY_OF_MONTH, 1);
String monthStart = DateUtil.format(calendar.getTime(), "yyyy-MM-dd");
Long monthCount = this.count(new LambdaQueryWrapper<SysOperLog>()
.ge(SysOperLog::getOperTime, monthStart + " 00:00:00"));
// 总统计
Long totalCount = this.count();
stats.put("todayCount", todayCount);
stats.put("yesterdayCount", yesterdayCount);
stats.put("monthCount", monthCount);
stats.put("totalCount", totalCount);
// 增长率计算
if (yesterdayCount > 0) {
double growthRate = ((double) (todayCount - yesterdayCount) / yesterdayCount) * 100;
stats.put("growthRate", String.format("%.2f", growthRate));
} else {
stats.put("growthRate", "0.00");
}
return stats;
}
/**
* @description [构建查询条件]
* @author Leocoder
*/
private LambdaQueryWrapper<SysOperLog> buildQueryWrapper(SysOperLogVo vo) {
LambdaQueryWrapper<SysOperLog> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(vo.getOperName()), SysOperLog::getOperName, vo.getOperName())
.eq(StringUtils.isNotBlank(vo.getOperType()), SysOperLog::getOperType, vo.getOperType())
.eq(StringUtils.isNotBlank(vo.getSystemType()), SysOperLog::getSystemType, vo.getSystemType())
.like(StringUtils.isNotBlank(vo.getOperMan()), SysOperLog::getOperMan, vo.getOperMan())
.eq(StringUtils.isNotBlank(vo.getOperStatus()), SysOperLog::getOperStatus, vo.getOperStatus())
.like(StringUtils.isNotBlank(vo.getOperIp()), SysOperLog::getOperIp, vo.getOperIp())
.like(StringUtils.isNotBlank(vo.getMethodName()), SysOperLog::getMethodName, vo.getMethodName())
.like(StringUtils.isNotBlank(vo.getOperUrl()), SysOperLog::getOperUrl, vo.getOperUrl())
.eq(StringUtils.isNotBlank(vo.getRequestMethod()), SysOperLog::getRequestMethod, vo.getRequestMethod())
.like(StringUtils.isNotBlank(vo.getOperLocation()), SysOperLog::getOperLocation, vo.getOperLocation())
.ge(vo.getOperTimeStart() != null, SysOperLog::getOperTime, vo.getOperTimeStart())
.le(vo.getOperTimeEnd() != null, SysOperLog::getOperTime, vo.getOperTimeEnd())
.orderByDesc(SysOperLog::getOperTime);
return wrapper;
}
/**
* @description [清空所有缓存]
* @author Leocoder
*/
private void clearAllCache() {
try {
// 清空统计缓存
Collection<String> keys = redisUtil.keys("oper:stat:*");
if (keys != null && !keys.isEmpty()) {
redisUtil.deleteKeys(keys);
}
log.info("清空操作日志统计缓存成功");
} catch (Exception e) {
log.warn("清空操作日志统计缓存失败", e);
}
}
}

View File

@ -0,0 +1,61 @@
package org.leocoder.heritage.system.service.role;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.model.bo.element.SelectLongBo;
import org.leocoder.heritage.domain.model.bo.element.TransferLongBo;
import org.leocoder.heritage.domain.model.vo.base.BaseVo;
import org.leocoder.heritage.domain.model.vo.system.SysRoleVo;
import org.leocoder.heritage.domain.pojo.system.SysRole;
import java.util.List;
/**
* @author Leocoder
* @description [角色信息表-服务实现层接口]
*/
public interface SysRoleService extends IService<SysRole> {
/**
* @description [多条件分页查询]
* @author Leocoder
*/
IPage<SysRole> listPage(SysRoleVo vo);
/**
* @description [查询是否拥有超级管理员角色-AOP]
* @author Leocoder
*/
boolean getIsCoderAdmin(Long userId);
/**
* @description [查询所有正常角色]
* @author Leocoder
*/
List<TransferLongBo> listLeftRole();
/**
* @description [删除当前用户拥有的角色]
* @author Leocoder
*/
void deleteUserRole(Long userId);
/**
* @description [查询用户拥有正常角色-穿梭框右侧]
* @author Leocoder
*/
List<Long> listRightRole(Long userId);
/**
* @description [根据用户ID分配角色-穿梭框]
* @author Leocoder
*/
void assignUserRole(Long userId, List<Long> roleIds);
/**
* @description [获取角色下拉框]
* @author Leocoder
*/
List<SelectLongBo> listRoleElSelect(BaseVo vo);
}

View File

@ -0,0 +1,113 @@
package org.leocoder.heritage.system.service.role;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.domain.model.bo.element.SelectLongBo;
import org.leocoder.heritage.domain.model.bo.element.TransferLongBo;
import org.leocoder.heritage.domain.model.vo.base.BaseVo;
import org.leocoder.heritage.domain.model.vo.system.SysRoleVo;
import org.leocoder.heritage.domain.pojo.system.SysRole;
import org.leocoder.heritage.domain.pojo.system.SysUserRole;
import org.leocoder.heritage.mybatisplus.mapper.system.SysRoleMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* @author Leocoder
* @description [角色信息表-服务实现层]
* 注意用户配置一个部门用户可以看当前部门的数据
* 注意角色配置多个部门一个角色可以看多个部门的数据然后根据用户ID查询角色再查询部门进行权限判断再使用AOP封装成注解
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {
private final SysRoleMapper sysRoleMapper;
/**
* @description [多条件分页查询]
* @author Leocoder
*/
@Override
public IPage<SysRole> listPage(SysRoleVo vo) {
// 分页构造器
Page<SysRole> page = new Page<>(vo.getPageNo(), vo.getPageSize());
// 进行分页查询
page = sysRoleMapper.listPage(page, vo);
return page;
}
/**
* @description [查询是否拥有超级管理员角色-AOP]
* @author Leocoder
*/
@Override
public boolean getIsCoderAdmin(Long userId) {
List<SysUserRole> userRoleList = sysRoleMapper.listSysUserRole(userId);
YUtil.isTrue(ObjectUtils.isEmpty(userRoleList), "该用户未分配角色,禁止登录");
return userRoleList.parallelStream().anyMatch(userRole -> userRole.getRoleId().equals(CoderConstants.ONE_LONG));
}
/**
* @description [查询所有正常角色]
* @author Leocoder
*/
@Override
public List<TransferLongBo> listLeftRole() {
return sysRoleMapper.listLeftRole();
}
/**
* @description [查询当前用户拥有角色-穿梭框右侧]
* @author Leocoder
*/
@Override
public List<Long> listRightRole(Long userId) {
return sysRoleMapper.listRightRole(userId);
}
/**
* @description [删除当前用户拥有的角色]
* @author Leocoder
*/
@Override
public void deleteUserRole(Long userId) {
sysRoleMapper.deleteUserRole(userId);
}
/**
* @description [根据用户ID分配角色-穿梭框]
* @author Leocoder
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void assignUserRole(Long userId, List<Long> roleIds) {
// 1删除当前用户拥有的角色
this.deleteUserRole(userId);
if (CollectionUtil.isNotEmpty(roleIds) && !roleIds.get(0).equals(-1L)) {
// 2添加当前用户选中的角色
boolean batchAdd = sysRoleMapper.batchAddUserRole(userId, roleIds);
YUtil.isTrue(!batchAdd, "分配角色失败,请重试");
}
}
/**
* @description [获取角色下拉框]
* @author Leocoder
*/
@Override
public List<SelectLongBo> listRoleElSelect(BaseVo vo) {
return sysRoleMapper.listRoleElSelect(vo);
}
}

View File

@ -0,0 +1,42 @@
package org.leocoder.heritage.system.service.user;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.model.vo.system.SysLoginUserVo;
import org.leocoder.heritage.domain.pojo.system.SysLoginUser;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [用户信息表-服务实现层接口]
*/
public interface SysLoginUserService extends IService<SysLoginUser> {
/**
* @description [用户表部门表左连接查询]
* @author Leocoder
*/
IPage<SysLoginUser> listPage(SysLoginUserVo vo);
/**
* @description [获取用户信息角色按钮等权限]
* @author Leocoder
*/
Map<String, Object> getLoginUserInformation();
/**
* @description [个人中心-左侧卡片数据]
* @author Leocoder
*/
Map<String, Object> getPersonalData();
/**
* @description [多条件数据导出]
* @author Leocoder
*/
List<SysLoginUser> listLoginUser(SysLoginUserVo vo);
}

View File

@ -0,0 +1,118 @@
package org.leocoder.heritage.system.service.user;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.leocoder.heritage.common.constants.CoderConstants;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.domain.model.vo.system.SysLoginUserVo;
import org.leocoder.heritage.domain.pojo.system.SysLoginUser;
import org.leocoder.heritage.domain.pojo.system.SysRole;
import org.leocoder.heritage.mybatisplus.mapper.system.SysLoginUserMapper;
import org.leocoder.heritage.system.service.role.SysRoleService;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author Leocoder
* @description [用户信息表-服务实现层]
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysLoginUserServiceImpl extends ServiceImpl<SysLoginUserMapper, SysLoginUser> implements SysLoginUserService {
private final StpInterface stpInterface;
private final SysLoginUserMapper sysLoginUserMapper;
private final SysRoleService sysRoleService;
/**
* @description [用户表部门表左连接查询]
* @author Leocoder
*/
@Override
public IPage<SysLoginUser> listPage(SysLoginUserVo vo) {
// 分页构造器
Page<SysLoginUser> page = new Page<>(vo.getPageNo(), vo.getPageSize());
// 进行分页查询
page = sysLoginUserMapper.listPage(page, vo);
return page;
}
/**
* @description [获取用户信息角色按钮等权限左侧菜单栏数据(另外一个接口)]
* @author Leocoder
*/
@Override
public Map<String, Object> getLoginUserInformation() {
YUtil.isTrue(ObjectUtils.isEmpty(StpUtil.getLoginIdAsLong()), "登录认证失效,请重新登录");
LambdaQueryWrapper<SysLoginUser> wrapper = new LambdaQueryWrapper<>();
wrapper.select(SysLoginUser::getUserId, SysLoginUser::getUserName, SysLoginUser::getAvatar);
wrapper.eq(SysLoginUser::getUserId, StpUtil.getLoginIdAsLong());
SysLoginUser sysLoginUser = sysLoginUserMapper.selectOne(wrapper);
Map<String, Object> map = new HashMap<>();
map.put("loginUser", sysLoginUser);
map.put("roles", stpInterface.getRoleList(StpUtil.getLoginId(), StpUtil.getLoginType()));
// 用户拥有的权限按钮
map.put("buttons", stpInterface.getPermissionList(StpUtil.getLoginIdAsLong(), StpUtil.getLoginType()));
return map;
}
/**
* @description [个人中心-左侧卡片数据]
* @author Leocoder
*/
@Override
public Map<String, Object> getPersonalData() {
SysLoginUser sysLoginUser = this.getById(CoderLoginUtil.getUserId());
Map<String, Object> map = new HashMap<>();
map.put("loginName", sysLoginUser.getLoginName());
map.put("userName", sysLoginUser.getUserName());
map.put("avatar", sysLoginUser.getAvatar());
map.put("phone", sysLoginUser.getPhone());
map.put("email", sysLoginUser.getEmail());
map.put("sex", sysLoginUser.getSex());
map.put("createTime", sysLoginUser.getCreateTime());
List<Long> roleIdList = sysRoleService.listRightRole(CoderLoginUtil.getUserId());
if (CollectionUtil.isEmpty(roleIdList)) {
map.put("roleName", "无名角色");
} else {
LambdaQueryWrapper<SysRole> roleWrapper = new LambdaQueryWrapper<>();
roleWrapper.select(SysRole::getRoleName);
roleWrapper.eq(SysRole::getRoleStatus, CoderConstants.ZERO_STRING);
roleWrapper.in(SysRole::getRoleId, roleIdList);
roleWrapper.orderByAsc(SysRole::getRoleId);
List<SysRole> roleList = sysRoleService.list(roleWrapper);
List<String> roleCollectList = roleList.stream().map(SysRole::getRoleName).collect(Collectors.toList());
String roleName = String.join("/", roleCollectList);
map.put("roleName", roleName);
}
return map;
}
/**
* @description [多条件数据导出]
* @author Leocoder
*/
@Override
public List<SysLoginUser> listLoginUser(SysLoginUserVo vo) {
return sysLoginUserMapper.listLoginUser(vo);
}
}

View File

@ -0,0 +1,45 @@
package org.leocoder.heritage.system.task;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.system.service.operlog.SysOperLogService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author Leocoder
* @description [操作日志清理任务]
*/
@Component
@ConditionalOnProperty(name = "coder.oper-log.clean.enabled", havingValue = "true", matchIfMissing = true)
@Slf4j
@RequiredArgsConstructor
public class OperLogCleanTask {
private final SysOperLogService sysOperLogService;
@Value("${coder.oper-log.clean.days:90}")
private int cleanDays;
/**
* @description [清理过期操作日志]
* @author Leocoder
*/
@Scheduled(cron = "${coder.oper-log.clean.cron:0 0 2 * * ?}")
public void cleanExpiredLogs() {
try {
log.info("开始清理{}天前的操作日志", cleanDays);
long startTime = System.currentTimeMillis();
sysOperLogService.cleanExpiredLogs(cleanDays);
long endTime = System.currentTimeMillis();
log.info("操作日志清理任务完成,耗时: {}ms", endTime - startTime);
} catch (Exception e) {
log.error("操作日志清理任务失败", e);
}
}
}

23
heritage-modules/pom.xml Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-backend</artifactId>
<version>${revision}</version>
</parent>
<artifactId>heritage-modules</artifactId>
<packaging>pom</packaging>
<description>业务模块</description>
<modules>
<module>heritage-system</module>
<module>heritage-monitor</module>
</modules>
</project>