增加许可证认证

This commit is contained in:
MerCry 2026-02-09 14:48:02 +08:00
parent 023ea78d18
commit f577cbcbe5
19 changed files with 994 additions and 26 deletions

View File

@ -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, '');

View File

@ -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

View File

@ -58,4 +58,25 @@ spring:
merge-sql: true
wall:
config:
multi-statement-allow: true
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

View File

@ -58,4 +58,25 @@ spring:
merge-sql: true
wall:
config:
multi-statement-allow: true
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

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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);
}
}
}

View File

@ -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<String, Object> 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");
}
}
}

View File

@ -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("/**");
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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 "获取许可证状态失败";
}
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysLicenseMapper">
<resultMap type="SysLicense" id="SysLicenseResult">
<result property="licenseKey" column="license_key" />
</resultMap>
<sql id="selectSysLicenseVo">
select license_key
from sys_license
</sql>
<select id="selectSysLicense" resultMap="SysLicenseResult">
<include refid="selectSysLicenseVo"/>
limit 1
</select>
</mapper>

View File

@ -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"
},

View File

@ -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')

View File

@ -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) 的;(项目打包之后,静态资源会放在这个文件夹下)

View File

@ -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、部门表
-- ----------------------------

14
sql/sys_license.sql Normal file
View File

@ -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='系统许可证表(加密版)';
-- ----------------------------
-- 注意:首次启动时,系统会自动生成加密的许可证密钥
-- 不需要手动插入数据
-- 许可证密钥是加密字符串,包含:激活时间、到期时间、机器码、试用天数、校验码
-- 即使修改数据库中的密钥,也无法通过后端的解密验证
-- ----------------------------