diff --git a/heritage-modules/heritage-monitor/pom.xml b/heritage-modules/heritage-monitor/pom.xml new file mode 100644 index 0000000..5f23631 --- /dev/null +++ b/heritage-modules/heritage-monitor/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + org.leocoder.heritage + heritage-backend + ${revision} + ../../pom.xml + + + heritage-monitor + heritage-monitor + 系统监控模块:服务器资源监控、Redis监控、缓存管理等功能 + + + + + org.leocoder.heritage + heritage-common + ${revision} + + + + + org.leocoder.heritage + heritage-resultex + ${revision} + + + + + org.leocoder.heritage + heritage-sa-token + ${revision} + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + com.github.oshi + oshi-core + 6.4.10 + + + + + org.apache.commons + commons-lang3 + + + + \ No newline at end of file diff --git a/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/controller/CacheController.java b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/controller/CacheController.java new file mode 100644 index 0000000..5124966 --- /dev/null +++ b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/controller/CacheController.java @@ -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 redisTemplate; + + private final static List 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 getRedisInformation() { + return cacheList; + } + + /** + * @description [查询Redis缓存键名列表] + * @author Leocoder + */ + @Operation(summary = "查询Redis缓存键名列表", description = "根据缓存名称获取对应的键名列表") + @SaCheckPermission("monitor:cache:list") + @GetMapping("/cache/getCacheKeys/{cacheName}") + public TreeSet getCacheKeys(@PathVariable("cacheName") String cacheName) { + Set 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 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 cacheKeys = redisTemplate.keys("*"); + if (ObjectUtils.isNotEmpty(cacheKeys)) { + redisTemplate.delete(cacheKeys); + } + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/controller/RedisController.java b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/controller/RedisController.java new file mode 100644 index 0000000..d2a92a8 --- /dev/null +++ b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/controller/RedisController.java @@ -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 redisTemplate; + + /** + * @description [获取Redis监控信息] + * @author Leocoder + */ + @Operation(summary = "获取Redis监控信息", description = "获取Redis服务器监控数据,包括服务信息、内存使用、连接数、命令统计等") + @SaCheckPermission("monitor:redis:list") + @GetMapping("/redis/getRedisInformation") + public Map getRedisInformation() { + try { + Properties info = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info()); + Properties commandStats = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info("commandstats")); + Object dbSize = redisTemplate.execute((RedisCallback) connection -> connection.dbSize()); + + Map result = new HashMap<>(3); + result.put("info", info); + result.put("dbSize", dbSize); + + List> pieList = new ArrayList<>(); + if (commandStats != null) { + commandStats.stringPropertyNames().forEach(key -> { + Map 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 errorResult = new HashMap<>(); + errorResult.put("error", "获取Redis监控信息失败: " + e.getMessage()); + return errorResult; + } + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/controller/ServerController.java b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/controller/ServerController.java new file mode 100644 index 0000000..249dfeb --- /dev/null +++ b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/controller/ServerController.java @@ -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; + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Cpu.java b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Cpu.java new file mode 100644 index 0000000..2a64b49 --- /dev/null +++ b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Cpu.java @@ -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(); + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Jvm.java b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Jvm.java new file mode 100644 index 0000000..363f1ba --- /dev/null +++ b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Jvm.java @@ -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(); + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Mem.java b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Mem.java new file mode 100644 index 0000000..ab80824 --- /dev/null +++ b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Mem.java @@ -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(); + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Server.java b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Server.java new file mode 100644 index 0000000..2643f10 --- /dev/null +++ b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Server.java @@ -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 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 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(); + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Sys.java b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Sys.java new file mode 100644 index 0000000..f822ec5 --- /dev/null +++ b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/Sys.java @@ -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; +} \ No newline at end of file diff --git a/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/SysCache.java b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/SysCache.java new file mode 100644 index 0000000..9615f37 --- /dev/null +++ b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/SysCache.java @@ -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; + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/SysFile.java b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/SysFile.java new file mode 100644 index 0000000..b61fd28 --- /dev/null +++ b/heritage-modules/heritage-monitor/src/main/java/org/leocoder/heritage/monitor/pojo/server/SysFile.java @@ -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; +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/pom.xml b/heritage-modules/heritage-system/pom.xml new file mode 100644 index 0000000..e09039c --- /dev/null +++ b/heritage-modules/heritage-system/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + org.leocoder.heritage + heritage-backend + ${revision} + ../../pom.xml + + + + heritage-system + heritage-system + 系统模块 + + + + + org.leocoder.heritage + heritage-common + ${revision} + + + + org.leocoder.heritage + heritage-resultex + ${revision} + + + + org.leocoder.heritage + heritage-sa-token + ${revision} + + + + org.leocoder.heritage + heritage-oper-logs + ${revision} + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + org.leocoder.heritage + heritage-oss + ${revision} + + + + \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/config/init/ServerCommandLineRunner.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/config/init/ServerCommandLineRunner.java new file mode 100755 index 0000000..a5d1aaa --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/config/init/ServerCommandLineRunner.java @@ -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项目启动后立即执行,重新获取缓存 => [推荐使用]"); + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/dashboard/DashboardController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/dashboard/DashboardController.java new file mode 100644 index 0000000..6235b2f --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/dashboard/DashboardController.java @@ -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; + } +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/dictdata/SysDictDataController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/dictdata/SysDictDataController.java new file mode 100644 index 0000000..191ec43 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/dictdata/SysDictDataController.java @@ -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 listPage(SysDictDataVo vo) { + // 分页构造器 + Page page = new Page<>(vo.getPageNo(), vo.getPageSize()); + // 条件构造器 + LambdaQueryWrapper 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 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 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 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 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 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 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 listDataByType(@PathVariable("dictType") String dictType) { + Boolean isExist = redisUtil.hasKey(CoderCacheConstants.DICT_REDIS_KEY + dictType); + if (!isExist) { + LambdaQueryWrapper 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 dictDataList = sysDictDataService.list(wrapper); + if (CollectionUtils.isNotEmpty(dictDataList)) { + return dictDataList; + } else { + return Collections.emptyList(); + } + } else { + List 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(); + } + +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/dicttype/SysDictTypeController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/dicttype/SysDictTypeController.java new file mode 100644 index 0000000..6b0fc6b --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/dicttype/SysDictTypeController.java @@ -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 listPage(SysDictTypeVo vo) { + // 分页构造器 + Page page = new Page<>(vo.getPageNo(), vo.getPageSize()); + // 条件构造器 + LambdaQueryWrapper 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 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 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 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 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 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 ids) { + if (CollectionUtil.isNotEmpty(ids)) { + for (Long id : ids) { + SysDictType sysDictType = sysDictTypeService.getById(id); + YUtil.isTrue(ObjectUtils.isEmpty(sysDictType) || StringUtils.isBlank(sysDictType.getDictType()), "请检查该数据是否存在"); + LambdaQueryWrapper 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 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 listDictType() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + // 按需加载 + wrapper.select(SysDictType::getDictType, SysDictType::getDictName); + wrapper.eq(SysDictType::getDictStatus, CoderConstants.ZERO_STRING); + List dictTypeList = sysDictTypeService.list(wrapper); + if (CollectionUtils.isNotEmpty(dictTypeList)) { + return dictTypeList; + } else { + return Collections.emptyList(); + } + } + +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/file/FileController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/file/FileController.java new file mode 100755 index 0000000..945096f --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/file/FileController.java @@ -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 ALLOWED_IMAGE_EXTENSIONS = Arrays.asList( + "jpg", "jpeg", "png", "gif", "bmp", "webp", "svg" + ); + + // 允许的文档文件扩展名 + private static final List 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 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 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 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(如OSS、MinIO),直接使用 + 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 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(如OSS、MinIO存储) + String fileUploadPath = (String) fileMap.get("fileUploadPath"); + + if (isFullUrl(fileUploadPath)) { + // 如果已经是完整URL(如OSS、MinIO),直接使用 + 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 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 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 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 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); + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/file/SysFileController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/file/SysFileController.java new file mode 100755 index 0000000..f646c0b --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/file/SysFileController.java @@ -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 listPage(SysFileVo vo) { + // 分页构造器 + Page page = new Page<>(vo.getPageNo(), vo.getPageSize()); + // 条件构造器 + LambdaQueryWrapper 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 list(SysFileVo vo) { + // 条件构造器 + LambdaQueryWrapper 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 ids) { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.in(SysFile::getFileId, ids); + List 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); + } + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/login/CaptchaController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/login/CaptchaController.java new file mode 100755 index 0000000..8941952 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/login/CaptchaController.java @@ -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 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 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 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 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 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 map = new HashMap<>(); + // map.put("codeKey", codeKey); + // map.put("captchaPicture", arithmeticCaptcha.toBase64()); + // return map; + // } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/login/SysLoginController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/login/SysLoginController.java new file mode 100755 index 0000000..8d7c54e --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/login/SysLoginController.java @@ -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[这个key,sa-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); + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/loginlog/SysLoginLogController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/loginlog/SysLoginLogController.java new file mode 100755 index 0000000..3e4051e --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/loginlog/SysLoginLogController.java @@ -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 listPage(SysLoginLogVo vo) { + // 分页构造器 + Page page = new Page<>(vo.getPageNo(), vo.getPageSize()); + // 条件构造器 + LambdaQueryWrapper 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 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 ids) { + YUtil.isTrue(!sysLoginLogService.removeByIds(ids), "删除失败,请稍后重试"); + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/menu/SysMenuController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/menu/SysMenuController.java new file mode 100755 index 0000000..69c462e --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/menu/SysMenuController.java @@ -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 listPage(SysMenuVo vo) { + // 分页构造器 + Page page = new Page<>(vo.getPageNo(), vo.getPageSize()); + // 条件构造器 + LambdaQueryWrapper 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 list(SysMenuVo vo) { + // 条件构造器 + LambdaQueryWrapper 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 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 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 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 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 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 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 cascaderList() { + return sysMenuService.cascaderList(); + } + + + /** + * @description [生成当前用户所拥有菜单路由] + * @author Leocoder + */ + @Operation(summary = "生成用户菜单路由", description = "根据当前用户权限生成可访问的菜单路由") + @GetMapping("/sysMenu/listRouters") + public List generatorRouters() { + return sysMenuService.generatorRouters(); + } + + /** + * @description [查询所有正常的路由 AND 展开节点(角色分配菜单权限使用)] + * @author Leocoder + */ + @Operation(summary = "查询正常菜单列表", description = "查询所有正常状态的菜单和展开节点,用于角色分配权限") + @GetMapping("/sysMenu/listMenuNormal") + public Map listMenuNormal(SysMenuVo sysMenuVo) { + Map map = new HashMap<>(); + List menuList = sysMenuService.listMenuNormal(sysMenuVo); + /* 如果不进行权限数据筛选,则上边一行注释,下边注释解开 */ + // 菜单正常数据 + // LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + // wrapper.select(SysMenu::getMenuId, SysMenu::getMenuName, SysMenu::getParentId); + // wrapper.eq(SysMenu::getMenuStatus, CoderConstants.ZERO_STRING); + // List menuList = sysMenuService.list(wrapper); + map.put("menuList", menuList); + + // 菜单是否展开数据 + LambdaQueryWrapper lambdaWrapper = new LambdaQueryWrapper<>(); + lambdaWrapper.select(SysMenu::getMenuId); + lambdaWrapper.eq(SysMenu::getIsSpread, CoderConstants.ZERO_STRING); + lambdaWrapper.eq(SysMenu::getMenuStatus, CoderConstants.ZERO_STRING); + List menuSpreadList = sysMenuService.list(lambdaWrapper); + List 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 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()); + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/online/SysUserOnlineController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/online/SysUserOnlineController.java new file mode 100644 index 0000000..598bebb --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/online/SysUserOnlineController.java @@ -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 listPage(SysUserOnlineVo vo) { + // 使用Sa-Token官方API获取所有已登录的会话ID + // keyword: 查询关键字,只有包括这个字符串的 token 值才会被查询出来。 + // start: 数据开始处索引。 + // size: 要获取的数据条数 (值为-1代表一直获取到末尾)。 + // sortType: 排序方式(true=正序:先登录的在前,false=反序:后登录的在前)。 + List sessionKeyList = StpUtil.searchSessionId("", 0, -1, false); + List loginUserList = null; + Map 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 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 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 getOnlineUserCount() { + List sessionKeyList = StpUtil.searchSessionId("", 0, -1, false); + int onlineCount = sessionKeyList != null ? sessionKeyList.size() : 0; + + Map result = new HashMap<>(); + result.put("onlineCount", onlineCount); + result.put("timestamp", System.currentTimeMillis()); + + log.info("当前在线用户数: {}", onlineCount); + return result; + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/operlog/SysOperLogController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/operlog/SysOperLogController.java new file mode 100644 index 0000000..482f873 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/operlog/SysOperLogController.java @@ -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 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 operIds = List.of(operId); + sysOperLogService.deleteByIds(operIds); + } + + /** + * @description [批量删除操作日志] + * @author Leocoder + */ + @PostMapping("/sysOperLog/batchDelete") + @SaCheckPermission("system:operlog:delete") + public void batchDelete(@RequestBody List 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 statistics() { + return sysOperLogService.getStatistics(); + } + + /** + * @description [获取仪表盘统计] + * @author Leocoder + */ + @GetMapping("/sysOperLog/dashboard") + @SaCheckPermission("system:operlog:search") + public Map dashboard() { + return sysOperLogService.getDashboardStats(); + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/role/SysRoleController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/role/SysRoleController.java new file mode 100755 index 0000000..fc3a843 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/role/SysRoleController.java @@ -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 listPage(SysRoleVo vo) { + return sysRoleService.listPage(vo); + } + + /** + * @description [查询所有] + * @author Leocoder + */ + @Operation(summary = "查询所有角色", description = "获取系统中所有角色信息") + @SaCheckPermission("system:role:list") + @GetMapping("/sysRole/list") + public List 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 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 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 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 listNormalRole(@PathVariable("userId") Long userId) { + Map 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 roleIds) { + sysRoleService.assignUserRole(userId, roleIds); + } + + /** + * @description [获取当前用户分配得角色下拉框(不需要可以删除注释和vo)] + * @author Leocoder + */ + @Operation(summary = "获取角色下拉框", description = "获取当前用户可分配的角色下拉选项") + @GetMapping("/sysRole/listRoleElSelect") + public List listRoleElSelect(BaseVo vo) { + return sysRoleService.listRoleElSelect(vo); + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/user/SysLoginUserController.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/user/SysLoginUserController.java new file mode 100755 index 0000000..802b052 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/controller/user/SysLoginUserController.java @@ -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 listPage(SysLoginUserVo vo) { + return sysLoginUserService.listPage(vo); + } + + /** + * @description [查询所有] + * @author Leocoder + */ + @Operation(summary = "查询所有用户", description = "获取系统中所有用户信息") + @SaCheckPermission("system:user:list") + @GetMapping("/sysLoginUser/list") + public List list() { + return sysLoginUserService.list(); + } + + /** + * @description [查询一个] + * @author Leocoder + */ + @Operation(summary = "根据ID查询用户", description = "根据用户ID获取用户详细信息") + @GetMapping("/sysLoginUser/getById/{id}") + public SysLoginUser getById(@PathVariable("id") Long id) { + // 条件构造器 + LambdaQueryWrapper 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 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 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 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 getLoginUserInformation() { + return sysLoginUserService.getLoginUserInformation(); + } + + /** + * @description [个人中心-左侧卡片资料数据] + * @author Leocoder + */ + @Operation(summary = "获取个人资料", description = "获取个人中心卡片显示数据") + @GetMapping("/sysLoginUser/getPersonalData") + public Map 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 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 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 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 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 list = easyExcelUtil.importExcel(multipartFile, SysLoginUser.class, null); + if (CollectionUtils.isNotEmpty(list)) { + System.out.println("本次成功导入:" + list.size() + "条"); + List 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, "导入失败"); + } + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dashboard/DashboardService.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dashboard/DashboardService.java new file mode 100644 index 0000000..6e13153 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dashboard/DashboardService.java @@ -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(); +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dashboard/DashboardServiceImpl.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dashboard/DashboardServiceImpl.java new file mode 100644 index 0000000..c9743e2 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dashboard/DashboardServiceImpl.java @@ -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 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 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 todayWrapper = + new LambdaQueryWrapper<>(); + todayWrapper.apply("DATE(create_time) = CURDATE()"); + Long todayNewUsers = sysLoginUserMapper.selectCount(todayWrapper); + userStats.setTodayNewUsers( + todayNewUsers != null ? todayNewUsers.intValue() : 0 + ); + + // 活跃用户数(最近30天有登录记录的用户) + // 使用原生SQL统计不重复的登录用户数 + LambdaQueryWrapper activeWrapper = + new LambdaQueryWrapper<>(); + activeWrapper + .apply("login_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)") + .eq(SysLoginLog::getLoginStatus, "0"); + + // 获取符合条件的登录记录,然后统计不重复的用户名 + List loginLogs = sysLoginLogMapper.selectList(activeWrapper); + long activeUsers = loginLogs.stream() + .map(SysLoginLog::getLoginName) + .distinct() + .count(); + userStats.setActiveUsers((int) activeUsers); + + // 当前在线用户数(通过Sa-Token获取) + List 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 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 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 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 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 todayWrapper = + new LambdaQueryWrapper<>(); + todayWrapper.apply("DATE(oper_time) = CURDATE()"); + Long todayVisits = sysOperLogMapper.selectCount(todayWrapper); + activityStats.setTodayVisits( + todayVisits != null ? todayVisits.intValue() : 0 + ); + + // 今日操作数(排除查询操作) + LambdaQueryWrapper 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 fileWrapper = + new LambdaQueryWrapper<>(); + fileWrapper.apply("DATE(create_time) = CURDATE()"); + Long newFiles = sysFileMapper.selectCount(fileWrapper); + + LambdaQueryWrapper 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; + } +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dictdata/SysDictDataService.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dictdata/SysDictDataService.java new file mode 100644 index 0000000..3f2e98f --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dictdata/SysDictDataService.java @@ -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 { + + /** + * @description [字典数据同步Redis进行缓存] + * @author Leocoder + */ + void listDictCacheRedis(); +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dictdata/SysDictDataServiceImpl.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dictdata/SysDictDataServiceImpl.java new file mode 100644 index 0000000..c529615 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dictdata/SysDictDataServiceImpl.java @@ -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 implements SysDictDataService { + + private final SysDictDataMapper sysDictDataMapper; + + private final SysDictTypeMapper sysDictTypeMapper; + + private final RedisUtil redisUtil; + + /** + * @description [字典数据同步Redis进行缓存] + * @author Leocoder + */ + @Override + public void listDictCacheRedis() { + // 1、查询所有可用的字典数据 + LambdaQueryWrapper dictTypeWrapper = new LambdaQueryWrapper<>(); + dictTypeWrapper.select(SysDictType::getDictType, SysDictType::getDictStatus); + List dictTypeList = sysDictTypeMapper.selectList(dictTypeWrapper); + if (CollectionUtil.isNotEmpty(dictTypeList)) { + // 2、根据字典类型查询字典数据 + for (SysDictType sysDictType : dictTypeList) { + if (sysDictType.getDictStatus().equals(CoderConstants.ZERO_STRING)) { + LambdaQueryWrapper dictDataWrapper = new LambdaQueryWrapper<>(); + dictDataWrapper.eq(SysDictData::getDictStatus, CoderConstants.ZERO_STRING); + dictDataWrapper.eq(SysDictData::getDictType, sysDictType.getDictType()); + dictDataWrapper.orderByAsc(SysDictData::getSorted); + List dictDataList = sysDictDataMapper.selectList(dictDataWrapper); + // 3、把字典数据循环存到Redis,设计Redis的key:CoderDict:dictType -> [{},{},{}] + redisUtil.setCacheObject(CoderCacheConstants.DICT_REDIS_KEY + sysDictType.getDictType(), dictDataList); + } else { + redisUtil.deleteKey(CoderCacheConstants.DICT_REDIS_KEY + sysDictType.getDictType()); + } + } + } + } + +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dicttype/SysDictTypeService.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dicttype/SysDictTypeService.java new file mode 100644 index 0000000..69ca4ec --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dicttype/SysDictTypeService.java @@ -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 { + +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dicttype/SysDictTypeServiceImpl.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dicttype/SysDictTypeServiceImpl.java new file mode 100644 index 0000000..4e9d1ae --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/dicttype/SysDictTypeServiceImpl.java @@ -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 implements SysDictTypeService { + + private final SysDictTypeMapper sysDictTypeMapper; + +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/file/SysFileService.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/file/SysFileService.java new file mode 100755 index 0000000..d3d4d52 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/file/SysFileService.java @@ -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 { + + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/file/SysFileServiceImpl.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/file/SysFileServiceImpl.java new file mode 100755 index 0000000..cd9b5c9 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/file/SysFileServiceImpl.java @@ -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 implements SysFileService { + + private final SysFileMapper sysFileMapper; + + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/login/SysLoginService.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/login/SysLoginService.java new file mode 100755 index 0000000..bdba842 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/login/SysLoginService.java @@ -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); + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/login/SysLoginServiceImpl.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/login/SysLoginServiceImpl.java new file mode 100755 index 0000000..2834023 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/login/SysLoginServiceImpl.java @@ -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 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 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); + // 2、IP地址 + CoderLoginUser.setLoginIp(loginUser.getLoginIp()); + // 3、登录地址 + CoderLoginUser.setLoginAddress(ipAddressUtil.getAddress(loginUser.getLoginIp())); + // 4、UserAgent信息 + 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 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 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("系统暂未配置注册用户数据[角色]"); + } + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/loginlog/SysLoginLogService.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/loginlog/SysLoginLogService.java new file mode 100755 index 0000000..7a027b5 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/loginlog/SysLoginLogService.java @@ -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 { + + /** + * @description [保存登录日志] + * @author Leocoder + */ + void addLoginLog(String loginName, String loginStatus, String message); + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/loginlog/SysLoginLogServiceImpl.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/loginlog/SysLoginLogServiceImpl.java new file mode 100755 index 0000000..b1640dc --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/loginlog/SysLoginLogServiceImpl.java @@ -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 implements SysLoginLogService { + + private final IpAddressUtil ipAddressUtil; + + /** + * @description [保存登录日志] + * @author Leocoder + */ + @Override + public void addLoginLog(String loginName, String loginStatus, String message) { + // 1、new一个登录日志对象,用来装载信息 + 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())); + // 4、UserAgent信息 + 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); + } + + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/menu/SysMenuService.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/menu/SysMenuService.java new file mode 100755 index 0000000..931a556 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/menu/SysMenuService.java @@ -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 { + + /** + * @description [查询菜单] + * @author Leocoder + */ + List listSysMenu(SysMenuVo vo); + + /** + * @description [菜单级联下拉框] + * @author Leocoder + */ + List cascaderList(); + + /** + * @description [生成当前用户所拥有菜单路由] + * @author Leocoder + */ + List generatorRouters(); + + /** + * @description [根据用户拥有的角色ID查询权限菜单(不包含父节点)] + * @author Leocoder + */ + List listMenuIdsByRoleId(Long roleId); + + /** + * @description [保存角色和菜单权限之间的关系] + * @author Leocoder + */ + void saveRoleMenu(Long roleId, List menuIds); + + /** + * @description [查询所有正常的路由 AND 展开节点(角色分配菜单权限使用)] + * @author Leocoder + */ + List listMenuNormal(SysMenuVo sysMenuVo); + + /** + * @description [注册-根据角色ID查询菜单(包含父节点)] + * @author Leocoder + */ + List listMenuNodesByRoleId(Long roleId); + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/menu/SysMenuServiceImpl.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/menu/SysMenuServiceImpl.java new file mode 100755 index 0000000..19eb858 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/menu/SysMenuServiceImpl.java @@ -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 implements SysMenuService { + + private final SysMenuMapper sysMenuMapper; + + private final SysLoginUserMapper sysLoginUserMapper; + + /** + * @description [查询菜单] + * @author Leocoder + */ + @Override + public List listSysMenu(SysMenuVo vo) { + return sysMenuMapper.listSysMenu(vo); + } + + /** + * @description [生成当前用户所拥有菜单路由] + * @author Leocoder + */ + @Override + public List generatorRouters() { + // 1、根据用户ID查询用户信息 + SysLoginUser loginUser = sysLoginUserMapper.selectById(CoderLoginUtil.getUserId()); + YUtil.isTrue(ObjectUtils.isEmpty(loginUser), "请先进行登录"); + // 2、查询菜单数据 + // 超级管理员,全部菜单权限 + if (loginUser != null && CoderLoginUtil.getIsCoderAdmin()) { + List sysMenuBos = sysMenuMapper.listMenuAdmin(); + YUtil.isTrue(CollectionUtil.isEmpty(sysMenuBos), "菜单权限全部被删除,禁止登录"); + List menuList = sysMenuBos.parallelStream().filter(x -> { + LambdaQueryWrapper 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 sysMenuBos = sysMenuMapper.listMenuByUserId(CoderLoginUtil.getUserId()); + YUtil.isTrue(CollectionUtil.isEmpty(sysMenuBos), "该用户角色未分配菜单权限,禁止登录"); + List menuList = sysMenuBos.parallelStream().filter(x -> { + LambdaQueryWrapper 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 cascaderList() { + return sysMenuMapper.cascaderList(); + } + + /** + * @description [根据用户拥有的角色ID查询权限菜单(不包含父节点)] + * @author Leocoder + */ + @Override + public List listMenuIdsByRoleId(Long roleId) { + // 菜单反选数据[菜单反选只注重子节点,父节点不用返回,子节点都有父节点自然会选中] + List menuList = sysMenuMapper.listMenuIdsByRoleId(roleId); + if(CollectionUtils.isEmpty(menuList)){ + return Collections.emptyList(); + } + // 剔除父节点数据 + List collectMenuList = menuList.parallelStream().filter(x -> { + LambdaQueryWrapper 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 menuIds) { + // 1、删除该角色拥有的权限 + sysMenuMapper.deleteMenuIdsByRoleId(roleId); + // 取消当前角色所有权限,传递空数组或包含-1不进行保存操作 + if (CollectionUtil.isNotEmpty(menuIds) && !menuIds.contains(-1L)) { + // 过滤掉无效的菜单ID + List 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 listMenuNormal(SysMenuVo sysMenuVo) { + sysMenuVo.setMenuStatus(CoderConstants.ZERO_STRING); + return sysMenuMapper.listSysMenu(sysMenuVo); + } + + /** + * @description [注册-根据角色ID查询菜单(包含父节点)] + * @author Leocoder + */ + @Override + public List listMenuNodesByRoleId(Long roleId) { + List menuList = sysMenuMapper.listMenuIdsByRoleId(roleId); + if(CollectionUtils.isEmpty(menuList)){ + return Collections.emptyList(); + } + return menuList.parallelStream().map(SysMenu::getMenuId).collect(Collectors.toList()); + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/operlog/SysOperLogService.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/operlog/SysOperLogService.java new file mode 100644 index 0000000..925f338 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/operlog/SysOperLogService.java @@ -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 { + + /** + * @description [分页查询操作日志] + * @author Leocoder + */ + Page listPage(SysOperLogVo vo); + + /** + * @description [根据ID查询操作日志详情] + * @author Leocoder + */ + SysOperLog getDetail(Long operId); + + /** + * @description [批量删除操作日志] + * @author Leocoder + */ + void deleteByIds(List operIds); + + /** + * @description [清空操作日志] + * @author Leocoder + */ + void clear(); + + /** + * @description [导出操作日志] + * @author Leocoder + */ + void export(SysOperLogVo vo, HttpServletResponse response); + + /** + * @description [获取操作统计] + * @author Leocoder + */ + Map getStatistics(); + + /** + * @description [清理过期日志] + * @author Leocoder + */ + void cleanExpiredLogs(int days); + + /** + * @description [获取仪表盘统计] + * @author Leocoder + */ + Map getDashboardStats(); +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/operlog/SysOperLogServiceImpl.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/operlog/SysOperLogServiceImpl.java new file mode 100644 index 0000000..e66ae06 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/operlog/SysOperLogServiceImpl.java @@ -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 implements SysOperLogService { + + private final RedisUtil redisUtil; + + /** + * @description [分页查询操作日志] + * @author Leocoder + */ + @Override + public Page listPage(SysOperLogVo vo) { + Page page = Page.of(vo.getPageNo(), vo.getPageSize()); + + LambdaQueryWrapper 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 operIds) { + log.info("开始删除操作日志,ID列表: {}", operIds); + + YUtil.isNull(operIds, "操作日志ID列表不能为空"); + YUtil.isTrue(operIds.isEmpty(), "操作日志ID列表不能为空"); + + try { + // 检查要删除的记录是否存在 + long existCount = this.count(new LambdaQueryWrapper() + .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 wrapper = buildQueryWrapper(vo); + + // 查询所有匹配的记录 + List 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 getStatistics() { + Map 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> typeStats = baseMapper.getOperTypeStats(); + stats.put("typeStats", typeStats); + + // 获取每日统计 + List> dailyStats = baseMapper.getDailyStats(); + stats.put("dailyStats", dailyStats); + + // 获取用户统计 + List> 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 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 getDashboardStats() { + Map stats = new HashMap<>(); + + // 今日统计 + String today = DateUtil.format(new Date(), "yyyy-MM-dd"); + Long todayCount = this.count(new LambdaQueryWrapper() + .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() + .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() + .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 buildQueryWrapper(SysOperLogVo vo) { + LambdaQueryWrapper 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 keys = redisUtil.keys("oper:stat:*"); + if (keys != null && !keys.isEmpty()) { + redisUtil.deleteKeys(keys); + } + log.info("清空操作日志统计缓存成功"); + } catch (Exception e) { + log.warn("清空操作日志统计缓存失败", e); + } + } +} \ No newline at end of file diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/role/SysRoleService.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/role/SysRoleService.java new file mode 100755 index 0000000..698670e --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/role/SysRoleService.java @@ -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 { + + /** + * @description [多条件分页查询] + * @author Leocoder + */ + IPage listPage(SysRoleVo vo); + + /** + * @description [查询是否拥有超级管理员角色-AOP] + * @author Leocoder + */ + boolean getIsCoderAdmin(Long userId); + + /** + * @description [查询所有正常角色] + * @author Leocoder + */ + List listLeftRole(); + + /** + * @description [删除当前用户拥有的角色] + * @author Leocoder + */ + void deleteUserRole(Long userId); + + /** + * @description [查询用户拥有正常角色-穿梭框右侧] + * @author Leocoder + */ + List listRightRole(Long userId); + + /** + * @description [根据用户ID分配角色-穿梭框] + * @author Leocoder + */ + void assignUserRole(Long userId, List roleIds); + + /** + * @description [获取角色下拉框] + * @author Leocoder + */ + List listRoleElSelect(BaseVo vo); + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/role/SysRoleServiceImpl.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/role/SysRoleServiceImpl.java new file mode 100755 index 0000000..c0f7a26 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/role/SysRoleServiceImpl.java @@ -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 implements SysRoleService { + + private final SysRoleMapper sysRoleMapper; + + /** + * @description [多条件分页查询] + * @author Leocoder + */ + @Override + public IPage listPage(SysRoleVo vo) { + // 分页构造器 + Page 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 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 listLeftRole() { + return sysRoleMapper.listLeftRole(); + } + + /** + * @description [查询当前用户拥有角色-穿梭框右侧] + * @author Leocoder + */ + @Override + public List 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 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 listRoleElSelect(BaseVo vo) { + return sysRoleMapper.listRoleElSelect(vo); + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/user/SysLoginUserService.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/user/SysLoginUserService.java new file mode 100755 index 0000000..961c54c --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/user/SysLoginUserService.java @@ -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 { + + + /** + * @description [用户表、部门表左连接查询] + * @author Leocoder + */ + IPage listPage(SysLoginUserVo vo); + + /** + * @description [获取用户信息、角色、按钮等权限] + * @author Leocoder + */ + Map getLoginUserInformation(); + + /** + * @description [个人中心-左侧卡片数据] + * @author Leocoder + */ + Map getPersonalData(); + + /** + * @description [多条件数据导出] + * @author Leocoder + */ + List listLoginUser(SysLoginUserVo vo); + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/user/SysLoginUserServiceImpl.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/user/SysLoginUserServiceImpl.java new file mode 100755 index 0000000..93193b6 --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/service/user/SysLoginUserServiceImpl.java @@ -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 implements SysLoginUserService { + + private final StpInterface stpInterface; + + private final SysLoginUserMapper sysLoginUserMapper; + + + private final SysRoleService sysRoleService; + + /** + * @description [用户表、部门表左连接查询] + * @author Leocoder + */ + @Override + public IPage listPage(SysLoginUserVo vo) { + // 分页构造器 + Page page = new Page<>(vo.getPageNo(), vo.getPageSize()); + // 进行分页查询 + page = sysLoginUserMapper.listPage(page, vo); + return page; + } + + /** + * @description [获取用户信息、角色、按钮等权限、左侧菜单栏数据(另外一个接口)] + * @author Leocoder + */ + @Override + public Map getLoginUserInformation() { + YUtil.isTrue(ObjectUtils.isEmpty(StpUtil.getLoginIdAsLong()), "登录认证失效,请重新登录"); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(SysLoginUser::getUserId, SysLoginUser::getUserName, SysLoginUser::getAvatar); + wrapper.eq(SysLoginUser::getUserId, StpUtil.getLoginIdAsLong()); + SysLoginUser sysLoginUser = sysLoginUserMapper.selectOne(wrapper); + Map 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 getPersonalData() { + SysLoginUser sysLoginUser = this.getById(CoderLoginUtil.getUserId()); + Map 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 roleIdList = sysRoleService.listRightRole(CoderLoginUtil.getUserId()); + if (CollectionUtil.isEmpty(roleIdList)) { + map.put("roleName", "无名角色"); + } else { + LambdaQueryWrapper roleWrapper = new LambdaQueryWrapper<>(); + roleWrapper.select(SysRole::getRoleName); + roleWrapper.eq(SysRole::getRoleStatus, CoderConstants.ZERO_STRING); + roleWrapper.in(SysRole::getRoleId, roleIdList); + roleWrapper.orderByAsc(SysRole::getRoleId); + List roleList = sysRoleService.list(roleWrapper); + List 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 listLoginUser(SysLoginUserVo vo) { + return sysLoginUserMapper.listLoginUser(vo); + } + +} diff --git a/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/task/OperLogCleanTask.java b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/task/OperLogCleanTask.java new file mode 100644 index 0000000..21b79af --- /dev/null +++ b/heritage-modules/heritage-system/src/main/java/org/leocoder/heritage/system/task/OperLogCleanTask.java @@ -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); + } + } +} \ No newline at end of file diff --git a/heritage-modules/pom.xml b/heritage-modules/pom.xml new file mode 100644 index 0000000..737df02 --- /dev/null +++ b/heritage-modules/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + org.leocoder.heritage + heritage-backend + ${revision} + + + + heritage-modules + pom + 业务模块 + + + + heritage-system + heritage-monitor + + + \ No newline at end of file