diff --git a/excel-handle/sql/wecom_menu.sql b/excel-handle/sql/wecom_menu.sql index 87dd1c0..72511c1 100644 --- a/excel-handle/sql/wecom_menu.sql +++ b/excel-handle/sql/wecom_menu.sql @@ -13,3 +13,19 @@ INSERT INTO `excel-handle`.`sys_menu` (`menu_id`, `menu_name`, `parent_id`, `ord INSERT INTO `excel-handle`.`sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2012, '客户列表数据导出', 2001, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:customerExport:export', '#', 'admin', '2026-02-07 15:39:03', '', NULL, ''); INSERT INTO `excel-handle`.`sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (3000, '企业信息', 2000, 4, 'corpInfo', 'wecom/corpinfo/index', NULL, '', 1, 0, 'C', '0', '0', 'wecom:corpInfo:list', 'download', 'admin', '2026-02-07 15:39:03', '', NULL, '企业信息'); + + + + +INSERT INTO `excel-handle`.`sys_menu` ( `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES ('企业信息查看', 3000, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:corp:add', '#', 'admin', '2026-02-07 15:39:03', '', NULL, ''); + + + +INSERT INTO `excel-handle`.`sys_menu` ( `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES ('企业信息查看', 3000, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:corp:query', '#', 'admin', '2026-02-07 15:39:03', '', NULL, ''); + + +INSERT INTO `excel-handle`.`sys_menu` ( `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES ('企业信息编辑', 3000, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:corp:edit', '#', 'admin', '2026-02-07 15:39:03', '', NULL, ''); + + + +INSERT INTO `excel-handle`.`sys_menu` ( `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES ('企业信息删除', 3000, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:corp:remove', '#', 'admin', '2026-02-07 15:39:03', '', NULL, ''); \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml new file mode 100644 index 0000000..69cd33b --- /dev/null +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -0,0 +1,83 @@ +# 数据源配置 +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + druid: + # 主库数据源 + master: + url: jdbc:mysql://host.docker.internal:3316/excel-handle?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: jiong1114 + # 从库数据源 + slave: + # 从数据源开关/默认关闭 + enabled: false + url: + username: + password: + # 初始连接数 + initialSize: 5 + # 最小连接池数量 + minIdle: 10 + # 最大连接池数量 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置连接超时时间 + connectTimeout: 30000 + # 配置网络超时时间 + socketTimeout: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + # 配置一个连接在池中最大生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 + # 配置检测连接是否有效 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + webStatFilter: + enabled: true + statViewServlet: + enabled: true + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: ruoyi + login-password: 123456 + filter: + stat: + enabled: true + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + + redis: + # 地址 + host: host.docker.internal + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 密码 + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms diff --git a/ruoyi-admin/src/main/resources/application-local.yml b/ruoyi-admin/src/main/resources/application-local.yml index e2f4e2d..9fe2418 100644 --- a/ruoyi-admin/src/main/resources/application-local.yml +++ b/ruoyi-admin/src/main/resources/application-local.yml @@ -58,4 +58,25 @@ spring: merge-sql: true wall: config: - multi-statement-allow: true \ No newline at end of file + multi-statement-allow: true + redis: + # 地址 + host: localhost + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 密码 + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms diff --git a/ruoyi-admin/src/main/resources/application-druid.yml b/ruoyi-admin/src/main/resources/application-prod.yml similarity index 76% rename from ruoyi-admin/src/main/resources/application-druid.yml rename to ruoyi-admin/src/main/resources/application-prod.yml index 1147435..30b65f9 100644 --- a/ruoyi-admin/src/main/resources/application-druid.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -58,4 +58,25 @@ spring: merge-sql: true wall: config: - multi-statement-allow: true \ No newline at end of file + multi-statement-allow: true + redis: + # 地址 + host: redis + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 密码 + password: ash@szmp + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 259df3c..e108b3a 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -66,28 +66,6 @@ spring: restart: # 热部署开关 enabled: true - # redis 配置 - redis: - # 地址 - host: redis - # 端口,默认为6379 - port: 6379 - # 数据库索引 - database: 0 - # 密码 - password: ash@szmp - # 连接超时时间 - timeout: 10s - lettuce: - pool: - # 连接池中的最小空闲连接 - min-idle: 0 - # 连接池中的最大空闲连接 - max-idle: 8 - # 连接池的最大数据库连接数 - max-active: 8 - # #连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms # token配置 token: diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysLicense.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysLicense.java new file mode 100644 index 0000000..4335803 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysLicense.java @@ -0,0 +1,25 @@ +package com.ruoyi.common.core.domain.entity; + +import com.ruoyi.common.annotation.Excel; + +/** + * 系统许可证对象 sys_license(加密版) + * + * @author ruoyi + * @date 2026-02-09 + */ +public class SysLicense +{ + /** 加密的许可证密钥(包含激活时间、到期时间) */ + @Excel(name = "许可证密钥") + private String licenseKey; + + + public String getLicenseKey() { + return licenseKey; + } + + public void setLicenseKey(String licenseKey) { + this.licenseKey = licenseKey; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/LicenseEncryptUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/LicenseEncryptUtil.java new file mode 100644 index 0000000..7f643cc --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/LicenseEncryptUtil.java @@ -0,0 +1,211 @@ +package com.ruoyi.common.utils; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +/** + * 许可证加密工具类 + * 使用AES加密算法生成和验证许可证密钥 + * + * @author ruoyi + * @date 2026-02-09 + */ +public class LicenseEncryptUtil { + // AES加密密钥(生产环境应该使用更复杂的密钥,并妥善保管) + private static final String SECRET_KEY = "RuoYi@2026#License$Key!Secure*9527"; + + // 加密算法 + private static final String ALGORITHM = "AES"; + + // 字段分隔符 + private static final String SEPARATOR = "|"; + + /** + * 生成许可证密钥 + * + * @param activationTime 激活时间(时间戳) + * @param expiryTime 到期时间(时间戳) + * @param trialDays 试用天数 + * @return 加密后的许可证密钥 + */ + public static String generateLicenseKey(long activationTime, long expiryTime, int trialDays) { + try { + // 构建原始数据:激活时间|到期时间|机器码|试用天数|校验码 + String rawData = activationTime + SEPARATOR + + expiryTime + SEPARATOR + + trialDays + SEPARATOR + + generateChecksum(activationTime, expiryTime, trialDays); + + // AES加密 + byte[] encrypted = encrypt(rawData); + + // Base64编码 + return Base64.getEncoder().encodeToString(encrypted); + } catch (Exception e) { + throw new RuntimeException("生成许可证密钥失败", e); + } + } + + /** + * 解析许可证密钥 + * + * @param licenseKey 加密的许可证密钥 + * @return 包含许可证信息的Map,如果解析失败返回null + */ + public static Map parseLicenseKey(String licenseKey) { + try { + // Base64解码 + byte[] decoded = Base64.getDecoder().decode(licenseKey); + + // AES解密 + String rawData = decrypt(decoded); + + // 分割数据 + String[] parts = rawData.split("\\" + SEPARATOR); + if (parts.length != 4) { + return null; + } + + long activationTime = Long.parseLong(parts[0]); + long expiryTime = Long.parseLong(parts[1]); + int trialDays = Integer.parseInt(parts[2]); + String checksum = parts[3]; + + // 验证校验码 + String expectedChecksum = generateChecksum(activationTime, expiryTime, trialDays); + if (!checksum.equals(expectedChecksum)) { + return null; + } + + // 返回解析结果 + Map result = new HashMap<>(); + result.put("activationTime", activationTime); + result.put("expiryTime", expiryTime); + result.put("trialDays", trialDays); + + return result; + } catch (Exception e) { + return null; + } + } + + /** + * 验证许可证密钥是否有效 + * + * @param licenseKey 许可证密钥 + * @return true-有效,false-无效 + */ + public static boolean validateLicenseKey(String licenseKey) { + if (licenseKey == null || licenseKey.trim().isEmpty()) { + return false; + } + + Map licenseInfo = parseLicenseKey(licenseKey); + if (licenseInfo == null) { + return false; + } + + // 检查是否过期 + long expiryTime = (Long) licenseInfo.get("expiryTime"); + return System.currentTimeMillis() <= expiryTime; + } + + /** + * 获取许可证剩余天数 + * + * @param licenseKey 许可证密钥 + * @return 剩余天数,如果已过期返回0,解析失败返回-1 + */ + public static int getRemainingDays(String licenseKey) { + Map licenseInfo = parseLicenseKey(licenseKey); + if (licenseInfo == null) { + return -1; + } + + long expiryTime = (Long) licenseInfo.get("expiryTime"); + long currentTime = System.currentTimeMillis(); + + if (currentTime >= expiryTime) { + return 0; + } + + long remainingMillis = expiryTime - currentTime; + return (int) (remainingMillis / (1000 * 60 * 60 * 24)); + } + + /** + * AES加密 + */ + private static byte[] encrypt(String data) throws Exception { + SecretKeySpec keySpec = new SecretKeySpec(getAESKey(), ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + return cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); + } + + /** + * AES解密(从Base64字符串) + * + * @param encryptedKey Base64编码的加密字符串 + * @return 解密后的原始数据,失败返回null + */ + public static String decrypt(String encryptedKey) { + try { + // Base64解码 + byte[] decoded = Base64.getDecoder().decode(encryptedKey); + // AES解密 + return decrypt(decoded); + } catch (Exception e) { + return null; + } + } + + /** + * AES解密(从字节数组) + */ + private static String decrypt(byte[] data) throws Exception { + SecretKeySpec keySpec = new SecretKeySpec(getAESKey(), ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, keySpec); + byte[] decrypted = cipher.doFinal(data); + return new String(decrypted, StandardCharsets.UTF_8); + } + + /** + * 获取AES密钥(16字节) + */ + private static byte[] getAESKey() { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] hash = md.digest(SECRET_KEY.getBytes(StandardCharsets.UTF_8)); + return hash; // MD5产生16字节 + } catch (Exception e) { + throw new RuntimeException("生成AES密钥失败", e); + } + } + + /** + * 生成校验码(防止数据被篡改)- 公共方法 + * + * @param activationTime 激活时间 + * @param expiryTime 到期时间 + * @param trialDays 试用天数 + * @return 校验码 + */ + public static String generateChecksum(long activationTime, long expiryTime, int trialDays) { + try { + String data = activationTime + "" + expiryTime + trialDays + SECRET_KEY; + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hash).substring(0, 16); + } catch (Exception e) { + throw new RuntimeException("生成校验码失败", e); + } + } +} + diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/LicenseKeyGeneratorSimple.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/LicenseKeyGeneratorSimple.java new file mode 100644 index 0000000..a1f6c07 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/LicenseKeyGeneratorSimple.java @@ -0,0 +1,247 @@ +package com.ruoyi.common.utils; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; +import java.util.Scanner; + +/** + * 简化版许可证密钥生成工具 + * 用于生成指定到期时间的许可证密钥 + * + * @author ruoyi + * @date 2026-02-09 + */ +public class LicenseKeyGeneratorSimple { + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + System.out.println("=== 许可证密钥生成工具(简化版)===\n"); + + while (true) { + System.out.println("请选择操作:"); + System.out.println("1. 生成指定天数的许可证"); + System.out.println("2. 生成指定到期日期的许可证"); + System.out.println("3. 验证许可证密钥"); + System.out.println("0. 退出"); + System.out.print("\n请输入选项: "); + + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "1": + generateByDays(scanner); + break; + case "2": + generateByDate(scanner); + break; + case "3": + validateKey(scanner); + break; + case "0": + System.out.println("退出程序"); + scanner.close(); + return; + default: + System.out.println("无效选项,请重新输入\n"); + } + } + } + + /** + * 生成指定天数的许可证 + */ + private static void generateByDays(Scanner scanner) { + System.out.print("\n请输入许可证有效天数(例如:30、90、365): "); + String input = scanner.nextLine().trim(); + + try { + int days = Integer.parseInt(input); + + // 当前时间作为激活时间 + Date now = new Date(); + long activationTime = now.getTime(); + + // 计算到期时间 + Calendar calendar = Calendar.getInstance(); + calendar.setTime(now); + calendar.add(Calendar.DAY_OF_MONTH, days); + Date expiryDate = calendar.getTime(); + long expiryTime = expiryDate.getTime(); + + // 生成许可证密钥 + String encryptedKey = LicenseEncryptUtil.generateLicenseKey(activationTime, expiryTime, days); + + // 输出结果 + System.out.println("\n========================================"); + System.out.println("许可证密钥生成成功!"); + System.out.println("========================================"); + System.out.println("激活时间: " + DATE_FORMAT.format(now)); + System.out.println("到期时间: " + DATE_FORMAT.format(expiryDate)); + System.out.println("有效天数: " + days + " 天"); + System.out.println("\n许可证密钥(请复制以下内容):"); + System.out.println("----------------------------------------"); + System.out.println(encryptedKey); + System.out.println("----------------------------------------\n"); + + // 解密并验证许可证 + String decryptedData = LicenseEncryptUtil.decrypt(encryptedKey); + + // 解析许可证数据:格式为 "激活时间|到期时间|试用天数|校验码" + String[] parts = decryptedData.split("\\|"); + + try { + long activationTimeMs = Long.parseLong(parts[0]); + long expiryTimeMs = Long.parseLong(parts[1]); + int trialDays = Integer.parseInt(parts[2]); + String checksum = parts[3]; + + // 验证校验码 + String expectedChecksum = LicenseEncryptUtil.generateChecksum( + activationTimeMs, expiryTimeMs, trialDays); + if (!checksum.equals(expectedChecksum)) { + System.out.println("许可证校验码验证失败"); + } + + // 检查是否过期 + Date expiryTime2 = new Date(expiryTimeMs); + + if (now.after(expiryTime2)) { + System.out.println("许可证已过期,到期时间: {}" + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime2)); + return; + } + + System.out.println("许可证验证通过,到期时间: {}" + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime2)); + } catch (NumberFormatException e) { + System.out.println("许可证数据解析失败" + e); + return; + } + + // 验证生成的密钥 + System.out.println("密钥验证:✓ 有效"); + System.out.println("========================================\n"); + + } catch (NumberFormatException e) { + System.out.println("输入格式错误,请输入数字\n"); + } catch (Exception e) { + System.out.println("生成失败: " + e.getMessage() + "\n"); + } + } + + /** + * 生成指定到期日期的许可证 + */ + private static void generateByDate(Scanner scanner) { + System.out.print("\n请输入到期日期(格式:yyyy-MM-dd,例如:2027-12-31): "); + String dateInput = scanner.nextLine().trim(); + + try { + // 解析日期(设置为当天的23:59:59) + SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd"); + Date expiryDate = inputFormat.parse(dateInput); + + // 设置为当天的23:59:59 + Calendar calendar = Calendar.getInstance(); + calendar.setTime(expiryDate); + calendar.set(Calendar.HOUR_OF_DAY, 23); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + expiryDate = calendar.getTime(); + long expiryTime = expiryDate.getTime(); + + // 当前时间作为激活时间 + Date now = new Date(); + long activationTime = now.getTime(); + + // 计算天数 + long diffMillis = expiryTime - activationTime; + int days = (int) (diffMillis / (1000 * 60 * 60 * 24)); + + if (days <= 0) { + System.out.println("到期日期必须在当前日期之后\n"); + return; + } + + // 生成许可证密钥 + String licenseKey = LicenseEncryptUtil.generateLicenseKey(activationTime, expiryTime, days); + + // 输出结果 + System.out.println("\n========================================"); + System.out.println("许可证密钥生成成功!"); + System.out.println("========================================"); + System.out.println("激活时间: " + DATE_FORMAT.format(now)); + System.out.println("到期时间: " + DATE_FORMAT.format(expiryDate)); + System.out.println("有效天数: " + days + " 天"); + System.out.println("\n许可证密钥(请复制以下内容):"); + System.out.println("----------------------------------------"); + System.out.println(licenseKey); + System.out.println("----------------------------------------\n"); + + // 验证生成的密钥 + boolean isValid = LicenseEncryptUtil.validateLicenseKey(licenseKey); + System.out.println("密钥验证: " + (isValid ? "✓ 有效" : "✗ 无效")); + System.out.println("========================================\n"); + + } catch (Exception e) { + System.out.println("日期格式错误或生成失败: " + e.getMessage() + "\n"); + } + } + + /** + * 验证许可证密钥 + */ + private static void validateKey(Scanner scanner) { + System.out.print("\n请输入要验证的许可证密钥: "); + String licenseKey = scanner.nextLine().trim(); + + if (licenseKey.isEmpty()) { + System.out.println("许可证密钥不能为空\n"); + return; + } + + try { + // 解析许可证 + Map licenseInfo = LicenseEncryptUtil.parseLicenseKey(licenseKey); + + if (licenseInfo == null) { + System.out.println("\n========================================"); + System.out.println("✗ 许可证密钥无效(解析失败或校验码错误)"); + System.out.println("========================================\n"); + return; + } + + // 获取信息 + long activationTime = (Long) licenseInfo.get("activationTime"); + long expiryTime = (Long) licenseInfo.get("expiryTime"); + int trialDays = (Integer) licenseInfo.get("trialDays"); + + Date activationDate = new Date(activationTime); + Date expiryDate = new Date(expiryTime); + Date now = new Date(); + + boolean isValid = LicenseEncryptUtil.validateLicenseKey(licenseKey); + int remainingDays = LicenseEncryptUtil.getRemainingDays(licenseKey); + + // 输出结果 + System.out.println("\n========================================"); + System.out.println("许可证信息"); + System.out.println("========================================"); + System.out.println("激活时间: " + DATE_FORMAT.format(activationDate)); + System.out.println("到期时间: " + DATE_FORMAT.format(expiryDate)); + System.out.println("有效天数: " + trialDays + " 天"); + System.out.println("当前时间: " + DATE_FORMAT.format(now)); + System.out.println("\n状态: " + (isValid ? "✓ 有效" : "✗ 已过期")); + if (isValid) { + System.out.println("剩余天数: " + remainingDays + " 天"); + } + System.out.println("========================================\n"); + + } catch (Exception e) { + System.out.println("\n验证失败: " + e.getMessage() + "\n"); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java index d6943a4..ae68327 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java @@ -3,6 +3,7 @@ package com.ruoyi.framework.config; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.constant.Constants; import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; +import com.ruoyi.framework.interceptor.LicenseInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -27,6 +28,9 @@ public class ResourcesConfig implements WebMvcConfigurer @Autowired private RepeatSubmitInterceptor repeatSubmitInterceptor; + @Autowired + private LicenseInterceptor licenseInterceptor; + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) @@ -47,6 +51,8 @@ public class ResourcesConfig implements WebMvcConfigurer @Override public void addInterceptors(InterceptorRegistry registry) { + // 许可证验证拦截器 + registry.addInterceptor(licenseInterceptor).addPathPatterns("/**"); // 防重复提交拦截器 registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/LicenseInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/LicenseInterceptor.java new file mode 100644 index 0000000..4de8ed7 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/LicenseInterceptor.java @@ -0,0 +1,62 @@ +package com.ruoyi.framework.interceptor; + +import com.ruoyi.system.service.ISysLicenseService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 许可证验证拦截器 + * 在用户登录后验证系统许可证是否有效 + * + * @author ruoyi + * @date 2026-02-09 + */ +@Component +public class LicenseInterceptor implements HandlerInterceptor +{ + private static final Logger log = LoggerFactory.getLogger(LicenseInterceptor.class); + + @Autowired + private ISysLicenseService sysLicenseService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + // 获取请求URI + String uri = request.getRequestURI(); + + // 排除登录、登出、静态资源等请求 + if (uri.contains("/login") || uri.contains("/logout") || + uri.contains("/captchaImage") || uri.contains("/static") || + uri.contains("/css") || uri.contains("/js") || uri.contains("/img") || + uri.contains("/fonts") || uri.contains("/ajax")) + { + return true; + } + + // 验证许可证 + boolean isValid = sysLicenseService.validateLicense(); + + if (!isValid) + { + log.warn("许可证验证失败,拒绝访问: {}", uri); + + // 设置响应状态码和错误信息 + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("application/json;charset=UTF-8"); + + String errorMessage = "{\"code\":601,\"msg\":\"系统许可证已过期或无效。\"}"; + + response.getWriter().write(errorMessage); + return false; + } + + return true; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLicenseMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLicenseMapper.java new file mode 100644 index 0000000..e919497 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLicenseMapper.java @@ -0,0 +1,21 @@ +package com.ruoyi.system.mapper; + +import com.ruoyi.common.core.domain.entity.SysLicense; + +/** + * 系统许可证Mapper接口 + * + * @author ruoyi + * @date 2026-02-09 + */ +public interface SysLicenseMapper +{ + + /** + * 查询系统许可证(获取第一条记录) + * + * @return 系统许可证 + */ + public SysLicense selectSysLicense(); + +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLicenseService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLicenseService.java new file mode 100644 index 0000000..8435961 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLicenseService.java @@ -0,0 +1,36 @@ +package com.ruoyi.system.service; + +import com.ruoyi.common.core.domain.entity.SysLicense; + +/** + * 系统许可证Service接口 + * + * @author ruoyi + * @date 2026-02-09 + */ +public interface ISysLicenseService +{ + /** + * 查询系统许可证(获取第一条记录) + * + * @return 系统许可证 + */ + public SysLicense selectSysLicense(); + + + + /** + * 验证许可证是否有效 + * + * @return true=有效 false=无效 + */ + public boolean validateLicense(); + + /** + * 获取许可证状态信息 + * + * @return 许可证状态信息 + */ + public String getLicenseStatusInfo(); + +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLicenseServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLicenseServiceImpl.java new file mode 100644 index 0000000..7c388ce --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLicenseServiceImpl.java @@ -0,0 +1,198 @@ +package com.ruoyi.system.service.impl; + +import com.ruoyi.common.core.domain.entity.SysLicense; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.LicenseEncryptUtil; +import com.ruoyi.system.mapper.SysLicenseMapper; +import com.ruoyi.system.service.ISysLicenseService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; + +/** + * 系统许可证Service业务层处理(加密版) + * + * @author ruoyi + * @date 2026-02-09 + */ +@Service +public class SysLicenseServiceImpl implements ISysLicenseService +{ + private static final Logger log = LoggerFactory.getLogger(SysLicenseServiceImpl.class); + + @Autowired + private SysLicenseMapper sysLicenseMapper; + + + /** + * 查询系统许可证(获取第一条记录) + * + * @return 系统许可证 + */ + @Override + public SysLicense selectSysLicense() + { + return sysLicenseMapper.selectSysLicense(); + } + + + /** + * 验证许可证是否有效(解密验证) + * + * @return true=有效 false=无效 + */ + @Override + public boolean validateLicense() + { + try + { + SysLicense license = selectSysLicense(); + if (license == null) + { + log.error("许可证不存在"); + return false; + } + + + String encryptedKey = license.getLicenseKey(); + if (encryptedKey == null || encryptedKey.trim().isEmpty()) + { + log.error("许可证密钥为空"); + return false; + } + + // 解密并验证许可证 + String decryptedData = LicenseEncryptUtil.decrypt(encryptedKey); + if (decryptedData == null) + { + log.error("许可证密钥解密失败"); + return false; + } + + // 解析许可证数据:格式为 "激活时间|到期时间|试用天数|校验码" + String[] parts = decryptedData.split("\\|"); + if (parts.length != 4) + { + log.error("许可证数据格式错误"); + return false; + } + + try + { + long activationTimeMs = Long.parseLong(parts[0]); + long expiryTimeMs = Long.parseLong(parts[1]); + int trialDays = Integer.parseInt(parts[2]); + String checksum = parts[3]; + + // 验证校验码 + String expectedChecksum = LicenseEncryptUtil.generateChecksum( + activationTimeMs, expiryTimeMs, trialDays); + if (!checksum.equals(expectedChecksum)) + { + log.error("许可证校验码验证失败"); + return false; + } + + // 检查是否过期 + Date now = new Date(); + Date expiryTime = new Date(expiryTimeMs); + + if (now.after(expiryTime)) + { + log.warn("许可证已过期,到期时间: {}", DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime)); + return false; + } + + log.info("许可证验证通过,到期时间: {}", DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime)); + return true; + } + catch (NumberFormatException e) + { + log.error("许可证数据解析失败", e); + return false; + } + } + catch (Exception e) + { + log.error("验证许可证时发生异常", e); + return false; + } + } + + /** + * 获取许可证状态信息 + * + * @return 许可证状态信息 + */ + @Override + public String getLicenseStatusInfo() + { + try + { + SysLicense license = selectSysLicense(); + if (license == null) + { + return "许可证不存在"; + } + + String encryptedKey = license.getLicenseKey(); + if (encryptedKey == null || encryptedKey.trim().isEmpty()) + { + return "许可证密钥为空"; + } + + // 解密许可证 + String decryptedData = LicenseEncryptUtil.decrypt(encryptedKey); + if (decryptedData == null) + { + return "许可证密钥无效"; + } + + String[] parts = decryptedData.split("\\|"); + if (parts.length != 5) + { + return "许可证数据格式错误"; + } + + try + { + long expiryTimeMs = Long.parseLong(parts[1]); + Date expiryTime = new Date(expiryTimeMs); + Date now = new Date(); + + long diffInMillies = expiryTime.getTime() - now.getTime(); + long diffInDays = diffInMillies / (1000 * 60 * 60 * 24); + + if (diffInDays < 0) + { + return "试用期已过期,请联系管理员续费"; + } + else if (diffInDays == 0) + { + return "试用期今天到期,请尽快续费"; + } + else + { + return String.format("试用期剩余 %d 天(到期时间: %s)", + diffInDays, + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime)); + } + } + catch (NumberFormatException e) + { + log.error("解析许可证时间失败", e); + return "许可证数据解析失败"; + } + } + catch (Exception e) + { + log.error("获取许可证状态信息时发生异常", e); + return "获取许可证状态失败"; + } + } + + +} diff --git a/ruoyi-system/src/main/resources/mapper/system/SysLicenseMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysLicenseMapper.xml new file mode 100644 index 0000000..9d47565 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysLicenseMapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + select license_key + from sys_license + + + + + diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json index c389e48..1b41d3e 100644 --- a/ruoyi-ui/package.json +++ b/ruoyi-ui/package.json @@ -7,6 +7,7 @@ "scripts": { "dev": "vue-cli-service serve", "build:prod": "vue-cli-service build", + "build:dev": "vue-cli-service build --mode development", "build:stage": "vue-cli-service build --mode staging", "preview": "node build/index.js --preview" }, diff --git a/ruoyi-ui/src/utils/request.js b/ruoyi-ui/src/utils/request.js index 7e8de28..42b6451 100644 --- a/ruoyi-ui/src/utils/request.js +++ b/ruoyi-ui/src/utils/request.js @@ -101,7 +101,7 @@ service.interceptors.response.use(res => { return Promise.reject(new Error(msg)) } else if (code === 601) { Message({ message: msg, type: 'warning' }) - return Promise.reject('error') + return Promise.reject(new Error(msg)) } else if (code !== 200) { Notification.error({ title: msg }) return Promise.reject('error') diff --git a/ruoyi-ui/vue.config.js b/ruoyi-ui/vue.config.js index 4b13a78..d6e31a7 100644 --- a/ruoyi-ui/vue.config.js +++ b/ruoyi-ui/vue.config.js @@ -20,7 +20,7 @@ module.exports = { // 部署生产环境和开发环境下的URL。 // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上 // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。 - publicPath: process.env.NODE_ENV === "production" ? "" : "/", + publicPath: process.env.NODE_ENV === "production" ? "/" : "/", // 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist) outputDir: 'dist', // 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下) diff --git a/sql/ry_20250522.sql b/sql/ry_20250522.sql index 9996737..5eced0f 100644 --- a/sql/ry_20250522.sql +++ b/sql/ry_20250522.sql @@ -1,3 +1,10 @@ + +drop table if exists sys_license; +create table sys_license ( + licenseKey varchar(200) comment '许可认证' +) engine=innodb auto_increment=200 comment = '许可认证表'; + + -- ---------------------------- -- 1、部门表 -- ---------------------------- diff --git a/sql/sys_license.sql b/sql/sys_license.sql new file mode 100644 index 0000000..fe5818c --- /dev/null +++ b/sql/sys_license.sql @@ -0,0 +1,14 @@ +-- ---------------------------- +-- 系统许可证表(加密版本) +-- ---------------------------- +DROP TABLE IF EXISTS `sys_license`; +CREATE TABLE `sys_license` ( + `license_key` TEXT NOT NULL COMMENT '加密的许可证密钥(包含激活时间、到期时间等信息)', +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统许可证表(加密版)'; + +-- ---------------------------- +-- 注意:首次启动时,系统会自动生成加密的许可证密钥 +-- 不需要手动插入数据 +-- 许可证密钥是加密字符串,包含:激活时间、到期时间、机器码、试用天数、校验码 +-- 即使修改数据库中的密钥,也无法通过后端的解密验证 +-- ----------------------------