Initial commit

This commit is contained in:
2025-09-22 11:51:16 +08:00
commit c32381f8ed
1191 changed files with 130140 additions and 0 deletions

1
ruoyi-admin/data.json Normal file

File diff suppressed because one or more lines are too long

151
ruoyi-admin/pom.xml Normal file
View File

@@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>ruoyi-admin</artifactId>
<description>
web服务入口
</description>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.34.0</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-grid</artifactId>
<version>4.34.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.36</version>
</dependency>
<!-- &lt;!&ndash; https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-chrome-driver &ndash;&gt;
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-chrome-driver</artifactId>
<version>4.34.0</version>
</dependency>-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.13.3</version>
<scope>test</scope>
</dependency>
<!-- spring-boot-devtools -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional> &lt;!&ndash; 表示依赖不会传递 &ndash;&gt;
</dependency>-->
<!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-core -->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>1.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-extension -->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>1.0.3</version>
</dependency>
<!-- swagger3-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
</dependency>
<!-- 防止进入swagger页面报类型转换错误排除3.0.0中的引用手动增加1.6.2版本 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.6.2</version>
</dependency>
<!-- Mysql驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 核心模块-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
</dependency>
<!-- 定时任务-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-quartz</artifactId>
</dependency>
<!-- 代码生成-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-generator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.15</version>
<configuration>
<fork>true</fork> <!-- 如果没有该配置devtools不会生效 -->
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<warName>${project.artifactId}</warName>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
<finalName>${project.artifactId}</finalName>
</build>
</project>

View File

@@ -0,0 +1,31 @@
package com.ruoyi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* 启动程序
*
* @author ruoyi
*/
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class RuoYiApplication
{
public static void main(String[] args)
{
// System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(RuoYiApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \n" +
" | ( ' ) | \\ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\\ \\ | || |(_,_)' \n" +
" | | \\ `' /| `-' / \n" +
" | | \\ / \\ / \n" +
" ''-' `'-' `-..-' ");
}
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
* web容器中进行部署
*
* @author ruoyi
*/
public class RuoYiServletInitializer extends SpringBootServletInitializer
{
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
{
return application.sources(RuoYiApplication.class);
}
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.framework.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* 程序注解配置
*
* @author ruoyi
*/
@Configuration
public class BeanRestConfig
{
/**
* 创建RestTemplate Bean
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.framework.config;
import lombok.Data;
/**
* @Author liwq
* @Date 2025年03月14日 14:52
* @Desc
*/
@Data
public class FileDto {
private String fileUrl;
private String fileName;
public FileDto(String fileUrl, String fileName) {
this.fileUrl =fileUrl;
this.fileName = fileName;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2018-2999 广州市蓝海创新科技有限公司 All rights reserved.
*
* https://www.mall4j.com/
*
* 未经允许,不可做商业用途!
*
* 版权所有,侵权必究!
*/
package com.ruoyi.framework.config;
import lombok.Data;
/**
* 本地存储配置信息
* @author lgh
*/
@Data
public class ImgUpload {
/**
* 本地文件上传文件夹
*/
private String imagePath;
/**
* 文件上传方式 1.本地文件上传 2.七牛云
*/
private Integer uploadType;
/**
* 网站url
*/
private String resourceUrl;
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2018-2999 广州市蓝海创新科技有限公司 All rights reserved.
*
* https://www.mall4j.com/
*
* 未经允许,不可做商业用途!
*
* 版权所有,侵权必究!
*/
package com.ruoyi.web.config;
import com.ruoyi.common.config.Qiniu;
import com.ruoyi.framework.config.ImgUpload;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
/**
* 商城配置文件
* @author lgh
*/
@Data
@Component
@PropertySource("classpath:shop.properties")
@ConfigurationProperties(prefix = "shop")
public class ShopBasicConfig {
/**
* 七牛云的配置信息
*/
private Qiniu qiniu;
/**
* 用于加解密token的密钥
*/
private String tokenAesKey;
/**
* 本地文件上传配置
*/
private ImgUpload imgUpload;
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2018-2999 广州市蓝海创新科技有限公司 All rights reserved.
*
* https://www.mall4j.com/
*
* 未经允许,不可做商业用途!
*
* 版权所有,侵权必究!
*/
package com.ruoyi.web.config;
import cn.hutool.crypto.symmetric.AES;
import com.ruoyi.framework.config.ImgUpload;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author lanhai
*/
@Configuration
@AllArgsConstructor
public class ShopBeanConfig {
private final ShopBasicConfig shopBasicConfig;
@Bean
public ImgUpload imgUpload() {
return shopBasicConfig.getImgUpload();
}
}

View File

@@ -0,0 +1,394 @@
package com.ruoyi.web.controller.common;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
import java.util.List;
import java.util.Random;
/**
* 高级滑块验证码处理器
* 支持多种滑块类型和重试机制
*/
public class AdvancedSliderCaptchaHandler {
private final WebDriver driver;
private final WebDriverWait wait;
private final Actions actions;
private final Random random;
private static final int MAX_RETRY_ATTEMPTS = 3;
private static final Duration WAIT_TIMEOUT = Duration.ofSeconds(15);
// 人类行为参数
private static final int MIN_CLICK_HOLD_DURATION = 300;
private static final int MAX_CLICK_HOLD_DURATION = 800;
private static final int MIN_MOVE_DURATION = 1000;
private static final int MAX_MOVE_DURATION = 2000;
private static final double OVERSHOOT_FACTOR = 1.05; // 5%的过冲概率
public AdvancedSliderCaptchaHandler(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, WAIT_TIMEOUT.toSeconds());
this.actions = new Actions(driver);
this.random = new Random();
}
public boolean handleAnyCaptcha() {
for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
System.out.println("" + attempt + " 次尝试处理验证码");
CaptchaType captchaType = detectCaptchaType();
if (captchaType == CaptchaType.NONE) {
System.out.println("未检测到验证码");
return true;
}
boolean success = processCaptchaByType(captchaType);
if (success) {
System.out.println("验证码处理成功");
return true;
}
if (attempt < MAX_RETRY_ATTEMPTS) {
waitBeforeRetry();
}
}
System.err.println("验证码处理失败,已达到最大重试次数");
return false;
}
/**
* 检测验证码类型
*/
private CaptchaType detectCaptchaType() {
try {
if (isElementPresent(By.id("nc_1_n1z"))) {
return CaptchaType.ALIBABA_SLIDER;
}
// 检测通用滑块
if (isElementPresent(By.className("btn_slide")) ||
isElementPresent(By.className("slider-btn"))) {
return CaptchaType.GENERIC_SLIDER;
}
// 检测其他常见滑块选择器
String[] commonSelectors = {
".captcha-slider-btn",
".slide-verify-slider-mask-item",
".slider_bg .slider_btn"
};
for (String selector : commonSelectors) {
if (isElementPresent(By.cssSelector(selector))) {
return CaptchaType.GENERIC_SLIDER;
}
}
return CaptchaType.NONE;
} catch (Exception e) {
System.err.println("检测验证码类型时发生错误: " + e.getMessage());
return CaptchaType.NONE;
}
}
/**
* 根据验证码类型进行处理
*/
private boolean processCaptchaByType(CaptchaType type) {
switch (type) {
case ALIBABA_SLIDER:
return handleAlibabaSlider();
case GENERIC_SLIDER:
return handleGenericSlider();
default:
return false;
}
}
/**
* 处理阿里云滑块验证码
*/
private boolean handleAlibabaSlider() {
try {
WebElement sliderButton = wait.until(
ExpectedConditions.elementToBeClickable(By.id("nc_1_n1z"))
);
WebElement sliderTrack = driver.findElement(By.id("nc_1_n1t"));
int moveDistance = calculateMoveDistance(sliderTrack, sliderButton);
return performSmartSlide(sliderButton, moveDistance);
} catch (Exception e) {
System.err.println("处理阿里云滑块失败: " + e.getMessage());
return false;
}
}
/**
* 处理通用滑块验证码
*/
private boolean handleGenericSlider() {
try {
List<WebElement> sliderButtons = driver.findElements(By.className("btn_slide"));
if (sliderButtons.isEmpty()) {
sliderButtons = driver.findElements(By.className("slider-btn"));
}
if (sliderButtons.isEmpty()) {
return false;
}
WebElement sliderButton = sliderButtons.get(0);
WebElement sliderTrack = findSliderTrack(sliderButton);
if (sliderTrack == null) {
return false;
}
int moveDistance = calculateMoveDistance(sliderTrack, sliderButton);
System.out.println("处理通用滑块失败: ");
return performSmartSlide(sliderButton, moveDistance);
} catch (Exception e) {
System.err.println("处理通用滑块失败: " + e.getMessage());
return false;
}
}
/**
* 智能滑动算法
*/
// private boolean performSmartSlide(WebElement sliderButton, int totalDistance) {
// try {
// actions.clickAndHold(sliderButton).perform();
// Thread.sleep(50 + random.nextInt(200));
//
// int moved = 0;
// int segments = 20 + random.nextInt(5);
//
// for (int i = 0; i < segments && moved < totalDistance; i++) {
// double progress = (double) i / segments;
// int stepSize = (int) (totalDistance * getBezierValue(progress) - moved);
//
// if (stepSize > 0 && moved + stepSize <= totalDistance) {
// int yOffset = (int) (Math.sin(progress * Math.PI * 2) * 2);
// actions.moveByOffset(stepSize, yOffset).perform();
// moved += stepSize;
//
// }
// }
// if (moved < totalDistance) {
// actions.moveByOffset(totalDistance - moved, 0).perform();
// }
//
// Thread.sleep(50 + random.nextInt(200));
// actions.release().perform();
//
// return waitForVerificationResult();
//
// } catch (Exception e) {
// System.err.println("智能滑动失败: " + e.getMessage());
// return false;
// }
// }
/**
* 贝塞尔曲线值计算
*/
private double getBezierValue(double t) {
return t * t * (3.0 - 2.0 * t);
}
/**
* 计算移动距离
*/
private int calculateMoveDistance(WebElement track, WebElement button) {
int trackWidth = track.getSize().getWidth() + 100;
int buttonWidth = button.getSize().getWidth();
return Math.max(trackWidth - buttonWidth - 5, 0);
}
/**
* 查找滑块轨道
*/
private WebElement findSliderTrack(WebElement button) {
try {
return button.findElement(By.xpath("./parent::*"));
} catch (Exception e) {
return null;
}
}
/**
* 等待验证结果
*/
private boolean waitForVerificationResult() {
try {
Thread.sleep(2000);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 检查元素是否存在
*/
private boolean isElementPresent(By locator) {
try {
driver.findElement(locator);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 重试前等待
*/
private void waitBeforeRetry() {
try {
Thread.sleep(2000 + random.nextInt(3000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 验证码类型枚举
*/
private enum CaptchaType {
NONE,
ALIBABA_SLIDER,
GENERIC_SLIDER
}
private boolean performSmartSlide(WebElement sliderButton, int totalDistance) {
try {
// 模拟人类点击前的短暂思考时间
randomSleep(MIN_CLICK_HOLD_DURATION, MAX_CLICK_HOLD_DURATION);
// 点击并按住滑块
actions.clickAndHold(sliderButton).perform();
// 生成更自然的移动轨迹
List<MoveStep> moveSteps = generateHumanLikeTrajectory(totalDistance);
// 执行移动步骤
long startTime = System.currentTimeMillis();
long currentTime = startTime;
long endTime = startTime + randomLong(MIN_MOVE_DURATION, MAX_MOVE_DURATION);
for (MoveStep step : moveSteps) {
// 根据时间进度执行步骤,使整体移动速度更自然
while (currentTime < endTime * step.timeProgress) {
actions.moveByOffset(step.xOffset, step.yOffset).perform();
currentTime = System.currentTimeMillis();
// 微小的随机停顿
if (random.nextDouble() < 0.1) {
Thread.sleep(random.nextInt(50));
}
}
}
// 随机过冲然后回退,更像人类操作
if (random.nextDouble() < OVERSHOOT_FACTOR) {
int overshoot = random.nextInt(5) + 5;
actions.moveByOffset(overshoot, 0).perform();
Thread.sleep(random.nextInt(100) + 50);
actions.moveByOffset(-overshoot, 0).perform();
}
// 释放前的随机延迟
randomSleep(100, 300);
actions.release().perform();
// 模拟验证过程中的等待
randomSleep(500, 1500);
return waitForVerificationResult();
} catch (Exception e) {
System.err.println("智能滑动失败: " + e.getMessage());
return false;
}
}
/**
* 生成更接近人类行为的滑动轨迹
*/
private List<MoveStep> generateHumanLikeTrajectory(int totalDistance) {
java.util.List<MoveStep> steps = new java.util.ArrayList<>();
// 总步数 - 随机但合理的范围
int totalSteps = random.nextInt(15) + 25;
// 计算每个时间点的进度0-1之间
double[] timeProgress = new double[totalSteps];
for (int i = 0; i < totalSteps; i++) {
timeProgress[i] = (double) i / totalSteps;
}
// 生成更自然的S型速度曲线开始和结束较慢中间较快
double[] distanceProgress = new double[totalSteps];
for (int i = 0; i < totalSteps; i++) {
double t = timeProgress[i];
// 使用改进的S型曲线函数
distanceProgress[i] = t * t * t * (t * (t * 6 - 15) + 10);
}
// 计算每一步的偏移量
for (int i = 0; i < totalSteps; i++) {
double currentDistance = i == 0 ?
distanceProgress[i] * totalDistance :
(distanceProgress[i] - distanceProgress[i-1]) * totalDistance;
// 添加小幅度的垂直偏移,模拟人类手部颤抖
int yOffset = random.nextInt(3) - 1; // -1, 0, 1
steps.add(new MoveStep(
(int) Math.round(currentDistance),
yOffset,
timeProgress[i]
));
}
return steps;
}
/**
* 随机睡眠一段时间
*/
private void randomSleep(int min, int max) throws InterruptedException {
Thread.sleep(random.nextInt(max - min + 1) + min);
}
/**
* 生成随机长整型数
*/
private long randomLong(long min, long max) {
return min + (long) (random.nextDouble() * (max - min));
}
/**
* 移动步骤类 - 封装每一步的移动信息
*/
private static class MoveStep {
final int xOffset;
final int yOffset;
final double timeProgress;
MoveStep(int xOffset, int yOffset, double timeProgress) {
this.xOffset = xOffset;
this.yOffset = yOffset;
this.timeProgress = timeProgress;
}
}
}

View File

@@ -0,0 +1,48 @@
package com.ruoyi.web.controller.common;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Component
public class ApiIdleMonitor {
private static final long IDLE_THRESHOLD = 5 * 60 * 1000;
// private static final long IDLE_THRESHOLD = 10000;
private static final ConcurrentHashMap<String, Long> lastRequestTimeMap = new ConcurrentHashMap<>();
private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
@PostConstruct
public void init() {
scheduler.scheduleAtFixedRate(this::checkIdleApis, 1, 5, TimeUnit.MINUTES);//TimeUnit.MINUTES)
}
public static void updateRequestTime(String apiPath) {
lastRequestTimeMap.put(apiPath, System.currentTimeMillis());
}
private void checkIdleApis() {
long currentTime = System.currentTimeMillis();
lastRequestTimeMap.forEach((apiPath, lastTime) -> {
if (currentTime - lastTime > IDLE_THRESHOLD) {
try {
lastRequestTimeMap.remove(apiPath);
ProcessBuilder pb = new ProcessBuilder("systemctl", "stop", "clash");
pb.redirectErrorStream(true);
Process process = pb.start();
process.waitFor();
lastRequestTimeMap.remove(apiPath);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}

View File

@@ -0,0 +1,94 @@
package com.ruoyi.web.controller.common;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.google.code.kaptcha.Producer;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.sign.Base64;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.system.service.ISysConfigService;
/**
* 验证码操作处理
*
* @author ruoyi
*/
@RestController
public class CaptchaController
{
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
@Autowired
private RedisCache redisCache;
@Autowired
private ISysConfigService configService;
/**
* 生成验证码
*/
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException
{
AjaxResult ajax = AjaxResult.success();
boolean captchaEnabled = configService.selectCaptchaEnabled();
ajax.put("captchaEnabled", captchaEnabled);
if (!captchaEnabled)
{
return ajax;
}
// 保存验证码信息
String uuid = IdUtils.simpleUUID();
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
String captchaType = RuoYiConfig.getCaptchaType();
if ("math".equals(captchaType))
{
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os);
}
catch (IOException e)
{
return AjaxResult.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
}

View File

@@ -0,0 +1,162 @@
package com.ruoyi.web.controller.common;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.framework.config.ServerConfig;
/**
* 通用请求处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/common")
public class CommonController
{
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
@Autowired
private ServerConfig serverConfig;
private static final String FILE_DELIMETER = ",";
/**
* 通用下载请求
*
* @param fileName 文件名称
* @param delete 是否删除
*/
@GetMapping("/download")
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
{
try
{
if (!FileUtils.checkAllowDownload(fileName))
{
throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
}
String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
String filePath = RuoYiConfig.getDownloadPath() + fileName;
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, realFileName);
FileUtils.writeBytes(filePath, response.getOutputStream());
if (delete)
{
FileUtils.deleteFile(filePath);
}
}
catch (Exception e)
{
log.error("下载文件失败", e);
}
}
/**
* 通用上传请求(单个)
*/
@PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file) throws Exception
{
try
{
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
AjaxResult ajax = AjaxResult.success();
ajax.put("url", url);
ajax.put("fileName", fileName);
ajax.put("newFileName", FileUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
/**
* 通用上传请求(多个)
*/
@PostMapping("/uploads")
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
{
try
{
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
List<String> urls = new ArrayList<String>();
List<String> fileNames = new ArrayList<String>();
List<String> newFileNames = new ArrayList<String>();
List<String> originalFilenames = new ArrayList<String>();
for (MultipartFile file : files)
{
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
urls.add(url);
fileNames.add(fileName);
newFileNames.add(FileUtils.getName(fileName));
originalFilenames.add(file.getOriginalFilename());
}
AjaxResult ajax = AjaxResult.success();
ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
/**
* 本地资源通用下载
*/
@GetMapping("/download/resource")
public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
throws Exception
{
try
{
if (!FileUtils.checkAllowDownload(resource))
{
throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
}
// 本地资源路径
String localPath = RuoYiConfig.getProfile();
// 数据库资源地址
String downloadPath = localPath + FileUtils.stripPrefix(resource);
// 下载名称
String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, downloadName);
FileUtils.writeBytes(downloadPath, response.getOutputStream());
}
catch (Exception e)
{
log.error("下载文件失败", e);
}
}
}

View File

@@ -0,0 +1,23 @@
package com.ruoyi.web.controller.common;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.web.security.JwtRsaKeyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class JwksController {
@Autowired
private JwtRsaKeyService jwtRsaKeyService;
@Anonymous
@GetMapping(value = "/.well-known/jwks.json", produces = MediaType.APPLICATION_JSON_VALUE)
public String jwks() {
return jwtRsaKeyService.getJwksJson();
}
}

View File

@@ -0,0 +1,285 @@
package com.ruoyi.web.controller.common;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
import java.util.List;
import java.util.Random;
/**
* 滑块验证码处理器
* 遵循单一职责原则,专门处理滑块验证码的自动化操作
*/
public class SliderCaptchaHandler {
private final WebDriver driver;
private final WebDriverWait wait;
private final Actions actions;
private final Random random;
// 滑块相关的CSS选择器
private static final String SLIDER_BUTTON_ID = "nc_1_n1z";
private static final String SLIDER_TRACK_ID = "nc_1_n1t";
private static final String CAPTCHA_CONTAINER_ID = "nocaptcha";
// 人类行为参数
private static final int MIN_CLICK_HOLD_DURATION = 200;
private static final int MAX_CLICK_HOLD_DURATION = 500;
private static final int MIN_MOVE_DURATION = 800;
private static final int MAX_MOVE_DURATION = 1500;
private static final double OVERSHOOT_PROBABILITY = 0.7;
private static final int OVERSHOOT_DISTANCE = 5;
private static final double JITTER_FACTOR = 0.1; // 10%的抖动概率
public SliderCaptchaHandler(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10).toSeconds());
this.actions = new Actions(driver);
this.random = new Random();
}
/**
* 处理滑块验证码的主要方法
*/
public boolean handleSliderCaptcha() {
try {
if (!waitForCaptchaToAppear()) {
System.out.println("未检测到滑块验证码");
return true;
}
return performSliderAction();
} catch (Exception e) {
System.err.println("处理滑块验证码失败: " + e.getMessage());
return false;
}
}
/**
* 等待验证码出现
*/
private boolean waitForCaptchaToAppear() {
try {
wait.until(ExpectedConditions.presenceOfElementLocated(By.id(CAPTCHA_CONTAINER_ID)));
Thread.sleep(1000); // 等待动画完成
return true;
} catch (Exception e) {
return false;
}
}
/**
* 执行滑块拖拽操作
*/
private boolean performSliderAction() {
WebElement sliderButton = driver.findElement(By.id(SLIDER_BUTTON_ID));
WebElement sliderTrack = driver.findElement(By.id(SLIDER_TRACK_ID));
int trackWidth = sliderTrack.getSize().getWidth();
int buttonWidth = sliderButton.getSize().getWidth();
int moveDistance = trackWidth - buttonWidth + 100;
return executeHumanLikeSlide(sliderButton, moveDistance);
}
/**
* 极简仿人类滑动算法 - 回归本质
*/
private boolean executeHumanLikeSlide(WebElement sliderButton, int totalDistance) {
try {
// 等待页面加载完成
randomSleep(800, 1200);
// 随机移动到页面其他位置,再移回滑块
simulateRandomMouseMovement();
// 移动到滑块,模拟人类的不精确瞄准
actions.moveToElement(sliderButton,
random.nextInt(10) - 5, // x偏移-5到5像素
random.nextInt(10) - 5 // y偏移-5到5像素
).perform();
// 随机延迟,模拟思考时间
randomSleep(MIN_CLICK_HOLD_DURATION, MAX_CLICK_HOLD_DURATION);
// 按下鼠标,添加微小的垂直抖动
actions.clickAndHold().perform();
randomSleep(30, 70);
// 生成人类化的移动轨迹
List<MoveStep> moveSteps = generateHumanLikeTrajectory(totalDistance);
// 执行移动步骤
long startTime = System.currentTimeMillis();
long endTime = startTime + randomLong(MIN_MOVE_DURATION, MAX_MOVE_DURATION);
for (MoveStep step : moveSteps) {
actions.moveByOffset(step.xOffset, step.yOffset).perform();
// 随机停顿,模拟人类的不流畅操作
if (random.nextDouble() < 0.15) {
Thread.sleep(random.nextInt(30) + 10);
}
// 确保整体移动时间符合预期
long currentTime = System.currentTimeMillis();
long expectedTime = (long) (startTime + step.timeProgress * (endTime - startTime));
if (currentTime < expectedTime) {
Thread.sleep(expectedTime - currentTime);
}
}
// 随机过冲然后回退
if (random.nextDouble() < OVERSHOOT_PROBABILITY) {
int overshootAmount = random.nextInt(OVERSHOOT_DISTANCE) + 2;
actions.moveByOffset(overshootAmount, 0).perform();
randomSleep(100, 200);
actions.moveByOffset(-overshootAmount, 0).perform();
}
// 释放前的微小抖动
actions.moveByOffset(random.nextInt(2) - 1, random.nextInt(2) - 1).perform();
// 随机延迟后释放
randomSleep(50, 150);
actions.release().perform();
// 验证前的等待
randomSleep(500, 1000);
return verifyCaptchaSuccess();
} catch (Exception e) {
System.err.println("滑动操作失败: " + e.getMessage());
return false;
}
}
private void randomSleep(int min, int max) throws InterruptedException {
Thread.sleep(random.nextInt(max - min + 1) + min);
}
/**
* 生成随机长整型数
*/
private long randomLong(long min, long max) {
return min + (long) (random.nextDouble() * (max - min));
}
private void simulateRandomMouseMovement() throws InterruptedException {
WebElement body = driver.findElement(By.tagName("body"));
// 随机移动到页面其他位置
actions.moveToElement(body,
random.nextInt(200) + 50,
random.nextInt(100) + 50
).perform();
Thread.sleep(random.nextInt(300) + 200);
// 再随机移动一次
actions.moveToElement(body,
random.nextInt(100) + 100,
random.nextInt(50) + 100
).perform();
Thread.sleep(random.nextInt(200) + 100);
}
private List<MoveStep> generateHumanLikeTrajectory(int totalDistance) {
java.util.List<MoveStep> steps = new java.util.ArrayList<>();
// 总步数 - 随机但合理的范围
int totalSteps = random.nextInt(15) + 25;
// 计算每个时间点的进度0-1之间
double[] timeProgress = new double[totalSteps];
for (int i = 0; i < totalSteps; i++) {
timeProgress[i] = (double) i / totalSteps;
}
// 使用更自然的S型曲线基于五阶多项式
double[] distanceProgress = new double[totalSteps];
for (int i = 0; i < totalSteps; i++) {
double t = timeProgress[i];
// 五阶多项式S曲线f(t) = 6t⁵ - 15t⁴ + 10t³
distanceProgress[i] = t * t * t * (t * (t * 6 - 15) + 10);
}
// 计算每一步的偏移量
for (int i = 0; i < totalSteps; i++) {
double currentDistance = i == 0 ?
distanceProgress[i] * totalDistance :
(distanceProgress[i] - distanceProgress[i - 1]) * totalDistance;
// 添加小幅度的垂直偏移,模拟人类手部颤抖
int yOffset = random.nextInt(3) - 1; // -1, 0, 1
// 偶尔添加较大的垂直抖动,更像人类操作
if (random.nextDouble() < JITTER_FACTOR) {
yOffset += random.nextInt(5) - 2; // -2到2的额外抖动
}
steps.add(new MoveStep(
(int) Math.round(currentDistance),
yOffset,
timeProgress[i]
));
}
return steps;
}
/**
* 验证滑块验证码是否成功
*/
private boolean verifyCaptchaSuccess() {
try {
WebElement captchaContainer = driver.findElement(By.id(CAPTCHA_CONTAINER_ID));
String displayStyle = captchaContainer.getCssValue("display");
if ("none".equals(displayStyle)) {
System.out.println("滑块验证码验证成功");
return true;
}
// 检查是否有成功标识
try {
WebElement successElement = driver.findElement(By.className("nc-lang-cnt"));
String text = successElement.getText();
if (text.contains("验证通过") || text.contains("成功")) {
System.out.println("滑块验证码验证成功");
return true;
}
} catch (Exception ignored) {
// 忽略找不到成功元素的异常
}
System.out.println("滑块验证码验证可能失败,需要重试");
return false;
} catch (Exception e) {
System.err.println("验证结果检查失败: " + e.getMessage());
return false;
}
}
private static class MoveStep {
final int xOffset;
final int yOffset;
final double timeProgress;
MoveStep(int xOffset, int yOffset, double timeProgress) {
this.xOffset = xOffset;
this.yOffset = yOffset;
this.timeProgress = timeProgress;
}
}
}

View File

@@ -0,0 +1,121 @@
package com.ruoyi.web.controller.monitor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
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 com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysCache;
/**
* 缓存监控
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/cache")
public class CacheController
{
@Autowired
private RedisTemplate<String, String> redisTemplate;
private final static List<SysCache> caches = new ArrayList<SysCache>();
{
caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping()
public AjaxResult getInfo() throws Exception
{
Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
Map<String, Object> result = new HashMap<>(3);
result.put("info", info);
result.put("dbSize", dbSize);
List<Map<String, String>> pieList = new ArrayList<>();
commandStats.stringPropertyNames().forEach(key -> {
Map<String, String> data = new HashMap<>(2);
String property = commandStats.getProperty(key);
data.put("name", StringUtils.removeStart(key, "cmdstat_"));
data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
pieList.add(data);
});
result.put("commandStats", pieList);
return AjaxResult.success(result);
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping("/getNames")
public AjaxResult cache()
{
return AjaxResult.success(caches);
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping("/getKeys/{cacheName}")
public AjaxResult getCacheKeys(@PathVariable String cacheName)
{
Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
return AjaxResult.success(new TreeSet<>(cacheKeys));
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping("/getValue/{cacheName}/{cacheKey}")
public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
{
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
return AjaxResult.success(sysCache);
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@DeleteMapping("/clearCacheName/{cacheName}")
public AjaxResult clearCacheName(@PathVariable String cacheName)
{
Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
redisTemplate.delete(cacheKeys);
return AjaxResult.success();
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@DeleteMapping("/clearCacheKey/{cacheKey}")
public AjaxResult clearCacheKey(@PathVariable String cacheKey)
{
redisTemplate.delete(cacheKey);
return AjaxResult.success();
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@DeleteMapping("/clearCacheAll")
public AjaxResult clearCacheAll()
{
Collection<String> cacheKeys = redisTemplate.keys("*");
redisTemplate.delete(cacheKeys);
return AjaxResult.success();
}
}

View File

@@ -0,0 +1,265 @@
package com.ruoyi.web.controller.monitor;
import java.util.Date;
import java.util.Map;
import java.util.HashMap;
import com.ruoyi.common.annotation.Anonymous;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.ClientAccount;
import com.ruoyi.web.service.IClientAccountService;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import com.ruoyi.web.security.JwtRsaKeyService;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.ruoyi.web.sse.SseHubService;
import com.ruoyi.system.mapper.ClientDeviceMapper;
import com.ruoyi.system.domain.ClientDevice;
/**
* 客户端账号控制器
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/account")
@Anonymous
public class ClientAccountController extends BaseController {
@Autowired
private IClientAccountService clientAccountService;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 访问令牌3天
private final long JWT_EXPIRATION = 3L * 24 * 60 * 60 * 1000;
@Autowired
private JwtRsaKeyService jwtRsaKeyService;
@Autowired
private ClientDeviceMapper clientDeviceMapper;
@Autowired
private SseHubService sseHubService;
/**
* 查询账号列表
*/
@PreAuthorize("@ss.hasPermi('monitor:account:list')")
@GetMapping("/list")
public TableDataInfo list(ClientAccount clientAccount) {
startPage();
return getDataTable(clientAccountService.selectClientAccountList(clientAccount));
}
/**
* 获取账号详细信息
*/
@PreAuthorize("@ss.hasPermi('monitor:account:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
ClientAccount account = clientAccountService.selectClientAccountById(id);
if (account != null) {
account.setPassword(null);
}
return AjaxResult.success(account);
}
/**
* 新增账号
*/
@PreAuthorize("@ss.hasPermi('monitor:account:add')")
@Log(title = "客户端账号", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody ClientAccount clientAccount) {
if (StringUtils.isEmpty(clientAccount.getUsername()) || StringUtils.isEmpty(clientAccount.getPassword())) {
return AjaxResult.error("用户名和密码不能为空");
}
if (clientAccountService.selectClientAccountByUsername(clientAccount.getUsername()) != null) {
return AjaxResult.error("用户名已存在");
}
clientAccount.setCreateBy(getUsername());
clientAccount.setPassword(passwordEncoder.encode(clientAccount.getPassword()));
if (clientAccount.getExpireTime() == null) {
Date expireDate = new Date(System.currentTimeMillis() + 90L * 24 * 60 * 60 * 1000);
clientAccount.setExpireTime(expireDate);
}
return toAjax(clientAccountService.insertClientAccount(clientAccount));
}
@PreAuthorize("@ss.hasPermi('monitor:account:edit')")
@Log(title = "客户端账号", businessType = BusinessType.UPDATE)
@PostMapping("/update")
public AjaxResult edit(@RequestBody ClientAccount clientAccount) {
clientAccount.setUpdateBy(getUsername());
if (StringUtils.isNotEmpty(clientAccount.getPassword())) {
clientAccount.setPassword(passwordEncoder.encode(clientAccount.getPassword()));
} else {
clientAccount.setPassword(null);
}
return toAjax(clientAccountService.updateClientAccount(clientAccount));
}
@PreAuthorize("@ss.hasPermi('monitor:account:remove')")
@Log(title = "客户端账号", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(clientAccountService.deleteClientAccountByIds(ids));
}
/**
* 客户端登录认证
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody Map<String, String> loginData) {
String username = loginData.get("username");
String password = loginData.get("password");
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
return AjaxResult.error("用户名和密码不能为空");
}
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
if (account == null || !passwordEncoder.matches(password, account.getPassword())) {
return AjaxResult.error("用户名或密码错误");
}
if (!"0".equals(account.getStatus())) {
return AjaxResult.error("账号已被停用");
}
if (account.getExpireTime() != null && account.getExpireTime().before(new Date())) {
return AjaxResult.error("账号已过期");
}
String clientId = loginData.get("clientId");
String accessToken = Jwts.builder()
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
.claim("accountId", account.getId())
.claim("username", username)
.claim("clientId", clientId)
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
.compact();
Map<String, Object> result = new HashMap<>();
result.put("accessToken", accessToken);
result.put("permissions", account.getPermissions());
result.put("accountName", account.getAccountName());
result.put("expireTime", account.getExpireTime());
return AjaxResult.success("登录成功", result);
}
/**
* 验证token
*/
@PostMapping("/verify")
public AjaxResult verifyToken(@RequestBody Map<String, String> data) {
String token = data.get("token");
if (StringUtils.isEmpty(token)) {
return AjaxResult.error("token不能为空");
}
Map<String, Object> claims = Jwts.parser().setSigningKey(jwtRsaKeyService.getPublicKey()).parseClaimsJws(token).getBody();
String username = (String) claims.get("sub");
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
if (account == null || !"0".equals(account.getStatus())) {
return AjaxResult.error("token无效");
}
Map<String, Object> result = new HashMap<>();
result.put("username", username);
result.put("permissions", account.getPermissions());
result.put("accountName", account.getAccountName());
return AjaxResult.success("验证成功", result);
}
/**
* 账号会话事件SSE
*/
@GetMapping("/events")
public SseEmitter events(@RequestParam("clientId") String clientId, @RequestParam("token") String token) {
Map<String, Object> claims = Jwts.parser().setSigningKey(jwtRsaKeyService.getPublicKey()).parseClaimsJws(token).getBody();
String username = (String) claims.getOrDefault("sub", claims.get("subject"));
String tokenClientId = (String) claims.get("clientId");
if (username == null || tokenClientId == null || !tokenClientId.equals(clientId)) {
throw new RuntimeException("会话不匹配");
}
SseEmitter emitter = sseHubService.register(username, clientId, 0L);
try { emitter.send(SseEmitter.event().name("ready").data("ok")); } catch (Exception ignored) {}
return emitter;
}
/**
* 客户端账号注册
*/
@PostMapping("/register")
public AjaxResult register(@RequestBody ClientAccount clientAccount) {
if (StringUtils.isEmpty(clientAccount.getUsername()) || StringUtils.isEmpty(clientAccount.getPassword())) {
return AjaxResult.error("用户名和密码不能为空");
}
if (clientAccount.getPassword().length() < 6) {
return AjaxResult.error("密码长度不能少于6位");
}
if (clientAccountService.selectClientAccountByUsername(clientAccount.getUsername()) != null) {
return AjaxResult.error("用户名已存在");
}
clientAccount.setCreateBy("system");
clientAccount.setStatus("0");
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
clientAccount.setPassword(passwordEncoder.encode(clientAccount.getPassword()));
if (clientAccount.getExpireTime() == null) {
Date expireDate = new Date(System.currentTimeMillis() + 90L * 24 * 60 * 60 * 1000);
clientAccount.setExpireTime(expireDate);
}
int result = clientAccountService.insertClientAccount(clientAccount);
if (result <= 0) {
return AjaxResult.error("注册失败");
}
String accessToken = Jwts.builder()
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
.setSubject(clientAccount.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
.claim("accountId", clientAccount.getId())
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
.compact();
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("accessToken", accessToken);
dataMap.put("permissions", clientAccount.getPermissions());
dataMap.put("accountName", clientAccount.getAccountName());
dataMap.put("expireTime", clientAccount.getExpireTime());
return AjaxResult.success("注册成功", dataMap);
}
/**
* 检查用户名是否可用
*/
@GetMapping("/check-username")
public AjaxResult checkUsername(@RequestParam("username") String username) {
if (StringUtils.isEmpty(username)) {
return AjaxResult.error("用户名不能为空");
}
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
return AjaxResult.success(account == null);
}
}

View File

@@ -0,0 +1,248 @@
package com.ruoyi.web.controller.monitor;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import com.ruoyi.common.annotation.Anonymous;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.domain.*;
import com.ruoyi.web.service.IClientMonitorService;
import com.ruoyi.common.utils.poi.ExcelUtil;
/**
* 客户端监控控制器
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/client")
public class ClientMonitorController extends BaseController {
@Autowired
private IClientMonitorService clientMonitorService;
/**
* 获取客户端信息列表
*/
@GetMapping("/info/list")
public TableDataInfo infoList(ClientInfo clientInfo) {
startPage();
return getDataTable(clientMonitorService.selectClientInfoList(clientInfo));
}
/**
* 获取客户端错误报告列表
*/
@GetMapping("/error/list")
public TableDataInfo errorList(ClientErrorReport clientErrorReport) {
startPage();
return getDataTable(clientMonitorService.selectClientErrorList(clientErrorReport));
}
/**
* 获取客户端事件日志列表
*/
@PreAuthorize("@ss.hasPermi('monitor:client:list')")
@GetMapping("/eventlog/list")
public TableDataInfo eventLogList(ClientEventLog clientEventLog) {
startPage();
return getDataTable(clientMonitorService.selectClientEventLogList(clientEventLog));
}
/**
* 获取客户端数据采集报告列表
*/
@GetMapping("/data/list")
public TableDataInfo dataList(ClientDataReport clientDataReport) {
startPage();
return getDataTable(clientMonitorService.selectClientDataReportList(clientDataReport));
}
/**
* 导出客户端信息列表
*/
@PreAuthorize("@ss.hasPermi('monitor:client:export')")
@GetMapping("/info/export")
public AjaxResult exportInfo(ClientInfo clientInfo) {
List<ClientInfo> list = clientMonitorService.selectClientInfoList(clientInfo);
return new ExcelUtil<ClientInfo>(ClientInfo.class).exportExcel(list, "客户端信息数据");
}
/**
* 获取客户端数据统计
*/
@GetMapping("/statistics")
public AjaxResult getClientStatistics() {
return AjaxResult.success(clientMonitorService.getClientStatistics());
}
/**
* 获取近7天在线客户端趋势
*/
@GetMapping("/online/trend")
public AjaxResult getOnlineClientTrend() {
return AjaxResult.success(clientMonitorService.getOnlineClientTrend());
}
/**
* 获取数据采集类型分布
*/
@GetMapping("/data/distribution")
public AjaxResult getDataTypeDistribution() {
return AjaxResult.success(clientMonitorService.getDataTypeDistribution());
}
/**
* 客户端认证API
*/
@PostMapping("/api/auth")
public AjaxResult clientAuth(@RequestBody Map<String, Object> authData) {
try {
String authKey = (String) authData.get("authKey");
return AjaxResult.success("认证成功", clientMonitorService.authenticateClient(authKey, authData));
} catch (Exception e) {
return AjaxResult.error("认证失败:" + e.getMessage());
}
}
/**
* 客户端心跳API
*/
@PostMapping("/api/heartbeat")
public AjaxResult clientHeartbeat(@RequestBody Map<String, Object> heartbeatData) {
try {
clientMonitorService.recordHeartbeat(heartbeatData);
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
/**
* 客户端错误上报API
*/
@PostMapping("/api/error")
public AjaxResult clientError(@RequestBody Map<String, Object> errorData) {
try {
clientMonitorService.recordErrorReport(errorData);
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
/**
* 客户端数据上报API
*/
@PostMapping("/api/data")
public AjaxResult clientDataReport(@RequestBody Map<String, Object> dataReport) {
try {
clientMonitorService.recordDataReport(dataReport);
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
/**
* 获取特定客户端的详细信息
*/
@GetMapping("/detail/{clientId}")
public AjaxResult getClientDetail(@PathVariable String clientId) {
Map<String, Object> clientDetail = clientMonitorService.getClientDetail(clientId);
return clientDetail != null ? AjaxResult.success(clientDetail) : AjaxResult.error("未找到客户端信息");
}
/**
* 获取客户端版本分布
*/
@GetMapping("/version/distribution")
public AjaxResult getVersionDistribution() {
return AjaxResult.success(clientMonitorService.getVersionDistribution());
}
/**
* 获取客户端日志内容
*/
@GetMapping("/logs/{clientId}")
public AjaxResult getClientLogs(@PathVariable String clientId) {
try {
return AjaxResult.success(clientMonitorService.getClientLogs(clientId));
} catch (Exception e) {
return AjaxResult.error("获取客户端日志失败: " + e.getMessage());
}
}
/**
* 下载客户端日志文件
*/
@PreAuthorize("@ss.hasPermi('monitor:client:list')")
@GetMapping("/logs/{clientId}/download")
public void downloadClientLogs(@PathVariable String clientId, HttpServletResponse response) {
try {
clientMonitorService.downloadClientLogs(clientId, response);
} catch (Exception e) {
logger.error("下载客户端日志失败: {}", e.getMessage());
}
}
/**
* 客户端日志上传接口
*/
@PostMapping("/logs/upload")
public AjaxResult uploadClientLogs(@RequestBody Map<String, Object> logData) {
try {
clientMonitorService.saveClientLogs(logData);
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error("保存客户端日志失败: " + e.getMessage());
}
}
/**
* 客户端日志批量上传接口
*/
@PostMapping("/logs/batchUpload")
public AjaxResult batchUploadClientLogs(@RequestBody Map<String, Object> batchLogData) {
try {
String clientId = (String) batchLogData.get("clientId");
List<String> logEntries = (List<String>) batchLogData.get("logEntries");
if (clientId == null || logEntries == null || logEntries.isEmpty()) {
return AjaxResult.error("无效的批量日志数据");
}
clientMonitorService.saveBatchClientLogs(clientId, logEntries);
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error("批量保存客户端日志失败: " + e.getMessage());
}
}
/**
* 清理过期数据
*/
@PreAuthorize("@ss.hasPermi('monitor:client:export')")
@PostMapping("/cleanup")
public AjaxResult cleanupExpiredData() {
try {
clientMonitorService.cleanExpiredData();
return AjaxResult.success("过期数据清理完成");
} catch (Exception e) {
return AjaxResult.error("清理过期数据失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.web.controller.monitor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.framework.web.domain.Server;
/**
* 服务器监控
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/server")
public class ServerController
{
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
@GetMapping()
public AjaxResult getInfo() throws Exception
{
Server server = new Server();
server.copyTo();
return AjaxResult.success(server);
}
}

View File

@@ -0,0 +1,82 @@
package com.ruoyi.web.controller.monitor;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.service.SysPasswordService;
import com.ruoyi.system.domain.SysLogininfor;
import com.ruoyi.system.service.ISysLogininforService;
/**
* 系统访问记录
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/logininfor")
public class SysLogininforController extends BaseController
{
@Autowired
private ISysLogininforService logininforService;
@Autowired
private SysPasswordService passwordService;
@PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
@GetMapping("/list")
public TableDataInfo list(SysLogininfor logininfor)
{
startPage();
List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
return getDataTable(list);
}
@Log(title = "登录日志", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysLogininfor logininfor)
{
List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class);
util.exportExcel(response, list, "登录日志");
}
@PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
@Log(title = "登录日志", businessType = BusinessType.DELETE)
@DeleteMapping("/{infoIds}")
public AjaxResult remove(@PathVariable Long[] infoIds)
{
return toAjax(logininforService.deleteLogininforByIds(infoIds));
}
@PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
@Log(title = "登录日志", businessType = BusinessType.CLEAN)
@DeleteMapping("/clean")
public AjaxResult clean()
{
logininforService.cleanLogininfor();
return success();
}
@PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
@Log(title = "账户解锁", businessType = BusinessType.OTHER)
@GetMapping("/unlock/{userName}")
public AjaxResult unlock(@PathVariable("userName") String userName)
{
passwordService.clearLoginRecordCache(userName);
return success();
}
}

View File

@@ -0,0 +1,69 @@
package com.ruoyi.web.controller.monitor;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.SysOperLog;
import com.ruoyi.system.service.ISysOperLogService;
/**
* 操作日志记录
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/operlog")
public class SysOperlogController extends BaseController
{
@Autowired
private ISysOperLogService operLogService;
@PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
@GetMapping("/list")
public TableDataInfo list(SysOperLog operLog)
{
startPage();
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
return getDataTable(list);
}
@Log(title = "操作日志", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysOperLog operLog)
{
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
util.exportExcel(response, list, "操作日志");
}
@Log(title = "操作日志", businessType = BusinessType.DELETE)
@PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
@DeleteMapping("/{operIds}")
public AjaxResult remove(@PathVariable Long[] operIds)
{
return toAjax(operLogService.deleteOperLogByIds(operIds));
}
@Log(title = "操作日志", businessType = BusinessType.CLEAN)
@PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
@DeleteMapping("/clean")
public AjaxResult clean()
{
operLogService.cleanOperLog();
return success();
}
}

View File

@@ -0,0 +1,83 @@
package com.ruoyi.web.controller.monitor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
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 com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysUserOnline;
import com.ruoyi.system.service.ISysUserOnlineService;
/**
* 在线用户监控
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/online")
public class SysUserOnlineController extends BaseController
{
@Autowired
private ISysUserOnlineService userOnlineService;
@Autowired
private RedisCache redisCache;
@PreAuthorize("@ss.hasPermi('monitor:online:list')")
@GetMapping("/list")
public TableDataInfo list(String ipaddr, String userName)
{
Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
for (String key : keys)
{
LoginUser user = redisCache.getCacheObject(key);
if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
{
userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
}
else if (StringUtils.isNotEmpty(ipaddr))
{
userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
}
else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser()))
{
userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
}
else
{
userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
}
}
Collections.reverse(userOnlineList);
userOnlineList.removeAll(Collections.singleton(null));
return getDataTable(userOnlineList);
}
/**
* 强退用户
*/
@PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
@Log(title = "在线用户", businessType = BusinessType.FORCE)
@DeleteMapping("/{tokenId}")
public AjaxResult forceLogout(@PathVariable String tokenId)
{
redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
return success();
}
}

View File

@@ -0,0 +1,143 @@
package com.ruoyi.web.controller.monitor;
import com.ruoyi.common.annotation.Anonymous;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.core.controller.BaseController;
import java.util.HashMap;
import java.util.Map;
/**
* 版本管理控制器
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/version")
@Anonymous
public class VersionController extends BaseController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String VERSION_REDIS_KEY = "erp:client:version";
private static final String DOWNLOAD_URL_REDIS_KEY = "erp:client:download:url";
/**
* 检查版本更新
*/
@GetMapping("/check")
public AjaxResult checkVersion(@RequestParam String currentVersion) {
try {
// 从Redis获取最新版本信息
String latestVersion = redisTemplate.opsForValue().get(VERSION_REDIS_KEY);
if (StringUtils.isEmpty(latestVersion)) {
latestVersion = "2.0.0"; // 默认版本
}
// 比较版本号
boolean needUpdate = compareVersions(currentVersion, latestVersion) < 0;
Map<String, Object> result = new HashMap<>();
result.put("currentVersion", currentVersion);
result.put("latestVersion", latestVersion);
result.put("needUpdate", needUpdate);
// 从Redis获取下载链接
String downloadUrl = redisTemplate.opsForValue().get(DOWNLOAD_URL_REDIS_KEY);
result.put("downloadUrl", downloadUrl);
return AjaxResult.success(result);
} catch (Exception e) {
return AjaxResult.error("版本检查失败: " + e.getMessage());
}
}
/**
* 获取当前版本信息
*/
@PreAuthorize("@ss.hasPermi('system:version:query')")
@GetMapping("/info")
public AjaxResult getVersionInfo() {
try {
String currentVersion = redisTemplate.opsForValue().get(VERSION_REDIS_KEY);
if (StringUtils.isEmpty(currentVersion)) {
currentVersion = "2.0.0";
}
String downloadUrl = redisTemplate.opsForValue().get(DOWNLOAD_URL_REDIS_KEY);
Map<String, Object> result = new HashMap<>();
result.put("currentVersion", currentVersion);
result.put("downloadUrl", downloadUrl);
result.put("updateTime", System.currentTimeMillis());
return AjaxResult.success(result);
} catch (Exception e) {
return AjaxResult.error("获取版本信息失败: " + e.getMessage());
}
}
/**
* 设置版本信息和下载链接
*/
@Log(title = "版本信息设置", businessType = BusinessType.UPDATE)
@PreAuthorize("@ss.hasPermi('system:version:update')")
@PostMapping("/update")
public AjaxResult updateVersionInfo(@RequestParam("version") String version,
@RequestParam("downloadUrl") String downloadUrl) {
try {
if (StringUtils.isEmpty(version)) {
return AjaxResult.error("版本号不能为空");
}
if (StringUtils.isEmpty(downloadUrl)) {
return AjaxResult.error("下载链接不能为空");
}
// 更新Redis中的版本信息和下载链接
redisTemplate.opsForValue().set(VERSION_REDIS_KEY, version);
redisTemplate.opsForValue().set(DOWNLOAD_URL_REDIS_KEY, downloadUrl);
Map<String, Object> result = new HashMap<>();
result.put("version", version);
result.put("downloadUrl", downloadUrl);
result.put("updateTime", System.currentTimeMillis());
return AjaxResult.success("版本信息更新成功", result);
} catch (Exception e) {
return AjaxResult.error("版本信息更新失败: " + e.getMessage());
}
}
/**
* 比较版本号
* @param version1 版本1
* @param version2 版本2
* @return 负数表示version1 < version20表示相等正数表示version1 > version2
*/
private int compareVersions(String version1, String version2) {
if (StringUtils.isEmpty(version1) || StringUtils.isEmpty(version2)) {
return 0;
}
String[] v1Parts = version1.split("\\.");
String[] v2Parts = version2.split("\\.");
int maxLength = Math.max(v1Parts.length, v2Parts.length);
for (int i = 0; i < maxLength; i++) {
int v1Part = i < v1Parts.length ? Integer.parseInt(v1Parts[i]) : 0;
int v2Part = i < v2Parts.length ? Integer.parseInt(v2Parts[i]) : 0;
if (v1Part != v2Part) {
return Integer.compare(v1Part, v2Part);
}
}
return 0;
}
}

View File

@@ -0,0 +1,158 @@
package com.ruoyi.web.controller.system;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.system.domain.ClientDevice;
import com.ruoyi.system.mapper.ClientDeviceMapper;
import com.ruoyi.web.sse.SseHubService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/monitor/device")
@Anonymous
public class ClientDeviceController {
@Autowired
private ClientDeviceMapper clientDeviceMapper;
@Autowired
private SseHubService sseHubService;
private static final int DEFAULT_LIMIT = 3;
/**
* 查询设备配额与已使用数量
*
* @param username 用户名为空时返回0
* @return 配额信息
*/
@GetMapping("/quota")
public AjaxResult quota(@RequestParam(value = "username", required = false) String username) {
List<ClientDevice> all = clientDeviceMapper.selectByUsername(username);
int used = 0;
for (ClientDevice d : all) {
if (!"removed".equals(d.getStatus())) used++;
}
Map<String, Object> map = new HashMap<>();
map.put("limit", DEFAULT_LIMIT);
map.put("used", used);
return AjaxResult.success(map);
}
/**
* 按用户名查询设备列表(最近活动优先)
* @param username 用户名,必需参数
* @return 设备列表
*/
@GetMapping("/list")
public AjaxResult list(@RequestParam("username") String username) {
List<ClientDevice> list = clientDeviceMapper.selectByUsername(username);
java.util.ArrayList<ClientDevice> active = new java.util.ArrayList<>();
for (ClientDevice d : list) {
if (!"removed".equals(d.getStatus())) active.add(d);
}
return AjaxResult.success(active);
}
/**
* 设备注册(幂等)
*
* 根据 deviceId 判断:
* - 不存在插入新记录后端生成设备名称、IP等信息
* - 已存在:更新设备信息
*/
@PostMapping("/register")
public AjaxResult register(@RequestBody ClientDevice device, HttpServletRequest request) {
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
String ip = IpUtils.getIpAddr(request);
String deviceName = request.getHeader("X-Client-User") + "@" + ip + " (" + request.getHeader("X-Client-OS") + ")";
if (exists == null) {
device.setIp(ip);
device.setStatus("online");
device.setLastActiveAt(new java.util.Date());
device.setName(deviceName);
clientDeviceMapper.insert(device);
} else {
exists.setUsername(device.getUsername());
exists.setName(deviceName);
exists.setOs(device.getOs());
exists.setStatus("online");
exists.setIp(ip);
exists.setLocation(device.getLocation());
exists.setLastActiveAt(new java.util.Date());
clientDeviceMapper.updateByDeviceId(exists);
}
return AjaxResult.success();
}
/**
* 重命名设备
*
* 根据 deviceId 更新 name。
*/
@PostMapping("/rename")
public AjaxResult rename(@RequestBody ClientDevice device) {
clientDeviceMapper.updateByDeviceId(device);
return AjaxResult.success();
}
/**
* 移除设备
*
* 根据 deviceId 删除设备绑定记录。
*/
@PostMapping("/remove")
public AjaxResult remove(@RequestBody Map<String, String> body) {
String deviceId = body.get("deviceId");
if (deviceId == null || deviceId.isEmpty()) {
return AjaxResult.error("deviceId不能为空");
}
ClientDevice exists = clientDeviceMapper.selectByDeviceId(deviceId);
if (exists == null) {
return AjaxResult.success();
}
if (!"removed".equals(exists.getStatus())) {
exists.setStatus("removed");
exists.setLastActiveAt(new java.util.Date());
clientDeviceMapper.updateByDeviceId(exists);
// 推送SSE下线事件
try { sseHubService.sendEvent(exists.getUsername(), deviceId, "DEVICE_REMOVED", "{}"); } catch (Exception ignored) {}
}
return AjaxResult.success();
}
/**
* 设备心跳
* 若设备未注册则按注册逻辑插入;已注册则更新在线状态和设备信息
*/
@PostMapping("/heartbeat")
public AjaxResult heartbeat(@RequestBody ClientDevice device, HttpServletRequest request) {
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
String ip = IpUtils.getIpAddr(request);
String deviceName = request.getHeader("X-Client-User") + "@" + ip + " (" + request.getHeader("X-Client-OS") + ")";
if (exists == null) {
device.setIp(ip);
device.setStatus("online");
device.setLastActiveAt(new java.util.Date());
device.setName(deviceName);
clientDeviceMapper.insert(device);
} else if ("removed".equals(exists.getStatus())) {
AjaxResult res = AjaxResult.error("设备已被移除");
res.put("bizCode", "DEVICE_REMOVED");
return res;
} else {
exists.setUsername(device.getUsername());
exists.setStatus("online");
exists.setIp(ip);
exists.setLastActiveAt(new java.util.Date());
exists.setName(deviceName);
clientDeviceMapper.updateByDeviceId(exists);
}
return AjaxResult.success();
}
}

View File

@@ -0,0 +1,133 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.service.ISysConfigService;
/**
* 参数配置 信息操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/config")
public class SysConfigController extends BaseController
{
@Autowired
private ISysConfigService configService;
/**
* 获取参数配置列表
*/
@PreAuthorize("@ss.hasPermi('system:config:list')")
@GetMapping("/list")
public TableDataInfo list(SysConfig config)
{
startPage();
List<SysConfig> list = configService.selectConfigList(config);
return getDataTable(list);
}
@Log(title = "参数管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:config:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysConfig config)
{
List<SysConfig> list = configService.selectConfigList(config);
ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
util.exportExcel(response, list, "参数数据");
}
/**
* 根据参数编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:config:query')")
@GetMapping(value = "/{configId}")
public AjaxResult getInfo(@PathVariable Long configId)
{
return success(configService.selectConfigById(configId));
}
/**
* 根据参数键名查询参数值
*/
@GetMapping(value = "/configKey/{configKey}")
public AjaxResult getConfigKey(@PathVariable String configKey)
{
return success(configService.selectConfigByKey(configKey));
}
/**
* 新增参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:add')")
@Log(title = "参数管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysConfig config)
{
if (!configService.checkConfigKeyUnique(config))
{
return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
}
config.setCreateBy(getUsername());
return toAjax(configService.insertConfig(config));
}
/**
* 修改参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:edit')")
@Log(title = "参数管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysConfig config)
{
if (!configService.checkConfigKeyUnique(config))
{
return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
}
config.setUpdateBy(getUsername());
return toAjax(configService.updateConfig(config));
}
/**
* 删除参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:remove')")
@Log(title = "参数管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{configIds}")
public AjaxResult remove(@PathVariable Long[] configIds)
{
configService.deleteConfigByIds(configIds);
return success();
}
/**
* 刷新参数缓存
*/
@PreAuthorize("@ss.hasPermi('system:config:remove')")
@Log(title = "参数管理", businessType = BusinessType.CLEAN)
@DeleteMapping("/refreshCache")
public AjaxResult refreshCache()
{
configService.resetConfigCache();
return success();
}
}

View File

@@ -0,0 +1,132 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDeptService;
/**
* 部门信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dept")
public class SysDeptController extends BaseController
{
@Autowired
private ISysDeptService deptService;
/**
* 获取部门列表
*/
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list")
public AjaxResult list(SysDept dept)
{
List<SysDept> depts = deptService.selectDeptList(dept);
return success(depts);
}
/**
* 查询部门列表(排除节点)
*/
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list/exclude/{deptId}")
public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
{
List<SysDept> depts = deptService.selectDeptList(new SysDept());
depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
return success(depts);
}
/**
* 根据部门编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:dept:query')")
@GetMapping(value = "/{deptId}")
public AjaxResult getInfo(@PathVariable Long deptId)
{
deptService.checkDeptDataScope(deptId);
return success(deptService.selectDeptById(deptId));
}
/**
* 新增部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:add')")
@Log(title = "部门管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDept dept)
{
if (!deptService.checkDeptNameUnique(dept))
{
return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
dept.setCreateBy(getUsername());
return toAjax(deptService.insertDept(dept));
}
/**
* 修改部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:edit')")
@Log(title = "部门管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDept dept)
{
Long deptId = dept.getDeptId();
deptService.checkDeptDataScope(deptId);
if (!deptService.checkDeptNameUnique(dept))
{
return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
else if (dept.getParentId().equals(deptId))
{
return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
}
else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
{
return error("该部门包含未停用的子部门!");
}
dept.setUpdateBy(getUsername());
return toAjax(deptService.updateDept(dept));
}
/**
* 删除部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:remove')")
@Log(title = "部门管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{deptId}")
public AjaxResult remove(@PathVariable Long deptId)
{
if (deptService.hasChildByDeptId(deptId))
{
return warn("存在下级部门,不允许删除");
}
if (deptService.checkDeptExistUser(deptId))
{
return warn("部门存在用户,不允许删除");
}
deptService.checkDeptDataScope(deptId);
return toAjax(deptService.deleteDeptById(deptId));
}
}

View File

@@ -0,0 +1,121 @@
package com.ruoyi.web.controller.system;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.service.ISysDictDataService;
import com.ruoyi.system.service.ISysDictTypeService;
/**
* 数据字典信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dict/data")
public class SysDictDataController extends BaseController
{
@Autowired
private ISysDictDataService dictDataService;
@Autowired
private ISysDictTypeService dictTypeService;
@PreAuthorize("@ss.hasPermi('system:dict:list')")
@GetMapping("/list")
public TableDataInfo list(SysDictData dictData)
{
startPage();
List<SysDictData> list = dictDataService.selectDictDataList(dictData);
return getDataTable(list);
}
@Log(title = "字典数据", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:dict:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysDictData dictData)
{
List<SysDictData> list = dictDataService.selectDictDataList(dictData);
ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
util.exportExcel(response, list, "字典数据");
}
/**
* 查询字典数据详细
*/
@PreAuthorize("@ss.hasPermi('system:dict:query')")
@GetMapping(value = "/{dictCode}")
public AjaxResult getInfo(@PathVariable Long dictCode)
{
return success(dictDataService.selectDictDataById(dictCode));
}
/**
* 根据字典类型查询字典数据信息
*/
@GetMapping(value = "/type/{dictType}")
public AjaxResult dictType(@PathVariable String dictType)
{
List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
if (StringUtils.isNull(data))
{
data = new ArrayList<SysDictData>();
}
return success(data);
}
/**
* 新增字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:add')")
@Log(title = "字典数据", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDictData dict)
{
dict.setCreateBy(getUsername());
return toAjax(dictDataService.insertDictData(dict));
}
/**
* 修改保存字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:edit')")
@Log(title = "字典数据", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDictData dict)
{
dict.setUpdateBy(getUsername());
return toAjax(dictDataService.updateDictData(dict));
}
/**
* 删除字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.DELETE)
@DeleteMapping("/{dictCodes}")
public AjaxResult remove(@PathVariable Long[] dictCodes)
{
dictDataService.deleteDictDataByIds(dictCodes);
return success();
}
}

View File

@@ -0,0 +1,131 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDictType;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.service.ISysDictTypeService;
/**
* 数据字典信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dict/type")
public class SysDictTypeController extends BaseController
{
@Autowired
private ISysDictTypeService dictTypeService;
@PreAuthorize("@ss.hasPermi('system:dict:list')")
@GetMapping("/list")
public TableDataInfo list(SysDictType dictType)
{
startPage();
List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
return getDataTable(list);
}
@Log(title = "字典类型", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:dict:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysDictType dictType)
{
List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
util.exportExcel(response, list, "字典类型");
}
/**
* 查询字典类型详细
*/
@PreAuthorize("@ss.hasPermi('system:dict:query')")
@GetMapping(value = "/{dictId}")
public AjaxResult getInfo(@PathVariable Long dictId)
{
return success(dictTypeService.selectDictTypeById(dictId));
}
/**
* 新增字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:add')")
@Log(title = "字典类型", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDictType dict)
{
if (!dictTypeService.checkDictTypeUnique(dict))
{
return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
}
dict.setCreateBy(getUsername());
return toAjax(dictTypeService.insertDictType(dict));
}
/**
* 修改字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:edit')")
@Log(title = "字典类型", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDictType dict)
{
if (!dictTypeService.checkDictTypeUnique(dict))
{
return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
}
dict.setUpdateBy(getUsername());
return toAjax(dictTypeService.updateDictType(dict));
}
/**
* 删除字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.DELETE)
@DeleteMapping("/{dictIds}")
public AjaxResult remove(@PathVariable Long[] dictIds)
{
dictTypeService.deleteDictTypeByIds(dictIds);
return success();
}
/**
* 刷新字典缓存
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.CLEAN)
@DeleteMapping("/refreshCache")
public AjaxResult refreshCache()
{
dictTypeService.resetDictCache();
return success();
}
/**
* 获取字典选择框列表
*/
@GetMapping("/optionselect")
public AjaxResult optionselect()
{
List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
return success(dictTypes);
}
}

View File

@@ -0,0 +1,29 @@
package com.ruoyi.web.controller.system;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.StringUtils;
/**
* 首页
*
* @author ruoyi
*/
@RestController
public class SysIndexController
{
/** 系统基础配置 */
@Autowired
private RuoYiConfig ruoyiConfig;
/**
* 访问首页,提示语
*/
@RequestMapping("/")
public String index()
{
return StringUtils.format("欢迎使用{}后台管理框架当前版本v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
}
}

View File

@@ -0,0 +1,131 @@
package com.ruoyi.web.controller.system;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
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;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysMenuService;
/**
* 登录验证
*
* @author ruoyi
*/
@RestController
public class SysLoginController
{
@Autowired
private SysLoginService loginService;
@Autowired
private ISysMenuService menuService;
@Autowired
private SysPermissionService permissionService;
@Autowired
private TokenService tokenService;
@Autowired
private ISysConfigService configService;
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
LoginUser loginUser = SecurityUtils.getLoginUser();
SysUser user = loginUser.getUser();
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user);
if (!loginUser.getPermissions().equals(permissions))
{
loginUser.setPermissions(permissions);
tokenService.refreshToken(loginUser);
}
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
return ajax;
}
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
// 检查初始密码是否提醒修改
public boolean initPasswordIsModify(Date pwdUpdateDate)
{
Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify"));
return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null;
}
// 检查密码是否过期
public boolean passwordIsExpiration(Date pwdUpdateDate)
{
Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays"));
if (passwordValidateDays != null && passwordValidateDays > 0)
{
if (StringUtils.isNull(pwdUpdateDate))
{
// 如果从未修改过初始密码,直接提醒过期
return true;
}
Date nowDate = DateUtils.getNowDate();
return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays;
}
return false;
}
}

View File

@@ -0,0 +1,142 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysMenuService;
/**
* 菜单信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/menu")
public class SysMenuController extends BaseController
{
@Autowired
private ISysMenuService menuService;
/**
* 获取菜单列表
*/
@PreAuthorize("@ss.hasPermi('system:menu:list')")
@GetMapping("/list")
public AjaxResult list(SysMenu menu)
{
List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
return success(menus);
}
/**
* 根据菜单编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:menu:query')")
@GetMapping(value = "/{menuId}")
public AjaxResult getInfo(@PathVariable Long menuId)
{
return success(menuService.selectMenuById(menuId));
}
/**
* 获取菜单下拉树列表
*/
@GetMapping("/treeselect")
public AjaxResult treeselect(SysMenu menu)
{
List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
return success(menuService.buildMenuTreeSelect(menus));
}
/**
* 加载对应角色菜单列表树
*/
@GetMapping(value = "/roleMenuTreeselect/{roleId}")
public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId)
{
List<SysMenu> menus = menuService.selectMenuList(getUserId());
AjaxResult ajax = AjaxResult.success();
ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId));
ajax.put("menus", menuService.buildMenuTreeSelect(menus));
return ajax;
}
/**
* 新增菜单
*/
@PreAuthorize("@ss.hasPermi('system:menu:add')")
@Log(title = "菜单管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysMenu menu)
{
if (!menuService.checkMenuNameUnique(menu))
{
return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
}
else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
{
return error("新增菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
}
menu.setCreateBy(getUsername());
return toAjax(menuService.insertMenu(menu));
}
/**
* 修改菜单
*/
@PreAuthorize("@ss.hasPermi('system:menu:edit')")
@Log(title = "菜单管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysMenu menu)
{
if (!menuService.checkMenuNameUnique(menu))
{
return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
}
else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
{
return error("修改菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
}
else if (menu.getMenuId().equals(menu.getParentId()))
{
return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
}
menu.setUpdateBy(getUsername());
return toAjax(menuService.updateMenu(menu));
}
/**
* 删除菜单
*/
@PreAuthorize("@ss.hasPermi('system:menu:remove')")
@Log(title = "菜单管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{menuId}")
public AjaxResult remove(@PathVariable("menuId") Long menuId)
{
if (menuService.hasChildByMenuId(menuId))
{
return warn("存在子菜单,不允许删除");
}
if (menuService.checkMenuExistRole(menuId))
{
return warn("菜单已分配,不允许删除");
}
return toAjax(menuService.deleteMenuById(menuId));
}
}

View File

@@ -0,0 +1,91 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.SysNotice;
import com.ruoyi.system.service.ISysNoticeService;
/**
* 公告 信息操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/notice")
public class SysNoticeController extends BaseController
{
@Autowired
private ISysNoticeService noticeService;
/**
* 获取通知公告列表
*/
@PreAuthorize("@ss.hasPermi('system:notice:list')")
@GetMapping("/list")
public TableDataInfo list(SysNotice notice)
{
startPage();
List<SysNotice> list = noticeService.selectNoticeList(notice);
return getDataTable(list);
}
/**
* 根据通知公告编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:notice:query')")
@GetMapping(value = "/{noticeId}")
public AjaxResult getInfo(@PathVariable Long noticeId)
{
return success(noticeService.selectNoticeById(noticeId));
}
/**
* 新增通知公告
*/
@PreAuthorize("@ss.hasPermi('system:notice:add')")
@Log(title = "通知公告", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysNotice notice)
{
notice.setCreateBy(getUsername());
return toAjax(noticeService.insertNotice(notice));
}
/**
* 修改通知公告
*/
@PreAuthorize("@ss.hasPermi('system:notice:edit')")
@Log(title = "通知公告", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysNotice notice)
{
notice.setUpdateBy(getUsername());
return toAjax(noticeService.updateNotice(notice));
}
/**
* 删除通知公告
*/
@PreAuthorize("@ss.hasPermi('system:notice:remove')")
@Log(title = "通知公告", businessType = BusinessType.DELETE)
@DeleteMapping("/{noticeIds}")
public AjaxResult remove(@PathVariable Long[] noticeIds)
{
return toAjax(noticeService.deleteNoticeByIds(noticeIds));
}
}

View File

@@ -0,0 +1,129 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.SysPost;
import com.ruoyi.system.service.ISysPostService;
/**
* 岗位信息操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/post")
public class SysPostController extends BaseController
{
@Autowired
private ISysPostService postService;
/**
* 获取岗位列表
*/
@PreAuthorize("@ss.hasPermi('system:post:list')")
@GetMapping("/list")
public TableDataInfo list(SysPost post)
{
startPage();
List<SysPost> list = postService.selectPostList(post);
return getDataTable(list);
}
@Log(title = "岗位管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:post:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysPost post)
{
List<SysPost> list = postService.selectPostList(post);
ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class);
util.exportExcel(response, list, "岗位数据");
}
/**
* 根据岗位编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:post:query')")
@GetMapping(value = "/{postId}")
public AjaxResult getInfo(@PathVariable Long postId)
{
return success(postService.selectPostById(postId));
}
/**
* 新增岗位
*/
@PreAuthorize("@ss.hasPermi('system:post:add')")
@Log(title = "岗位管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysPost post)
{
if (!postService.checkPostNameUnique(post))
{
return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在");
}
else if (!postService.checkPostCodeUnique(post))
{
return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在");
}
post.setCreateBy(getUsername());
return toAjax(postService.insertPost(post));
}
/**
* 修改岗位
*/
@PreAuthorize("@ss.hasPermi('system:post:edit')")
@Log(title = "岗位管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysPost post)
{
if (!postService.checkPostNameUnique(post))
{
return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在");
}
else if (!postService.checkPostCodeUnique(post))
{
return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在");
}
post.setUpdateBy(getUsername());
return toAjax(postService.updatePost(post));
}
/**
* 删除岗位
*/
@PreAuthorize("@ss.hasPermi('system:post:remove')")
@Log(title = "岗位管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{postIds}")
public AjaxResult remove(@PathVariable Long[] postIds)
{
return toAjax(postService.deletePostByIds(postIds));
}
/**
* 获取岗位选择框列表
*/
@GetMapping("/optionselect")
public AjaxResult optionselect()
{
List<SysPost> posts = postService.selectPostAll();
return success(posts);
}
}

View File

@@ -0,0 +1,148 @@
package com.ruoyi.web.controller.system;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.common.utils.file.MimeTypeUtils;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysUserService;
/**
* 个人信息 业务处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/user/profile")
public class SysProfileController extends BaseController
{
@Autowired
private ISysUserService userService;
@Autowired
private TokenService tokenService;
/**
* 个人信息
*/
@GetMapping
public AjaxResult profile()
{
LoginUser loginUser = getLoginUser();
SysUser user = loginUser.getUser();
AjaxResult ajax = AjaxResult.success(user);
ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
return ajax;
}
/**
* 修改用户
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult updateProfile(@RequestBody SysUser user)
{
LoginUser loginUser = getLoginUser();
SysUser currentUser = loginUser.getUser();
currentUser.setNickName(user.getNickName());
currentUser.setEmail(user.getEmail());
currentUser.setPhonenumber(user.getPhonenumber());
currentUser.setSex(user.getSex());
if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser))
{
return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在");
}
if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser))
{
return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在");
}
if (userService.updateUserProfile(currentUser) > 0)
{
// 更新缓存用户信息
tokenService.setLoginUser(loginUser);
return success();
}
return error("修改个人信息异常,请联系管理员");
}
/**
* 重置密码
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping("/updatePwd")
public AjaxResult updatePwd(@RequestBody Map<String, String> params)
{
String oldPassword = params.get("oldPassword");
String newPassword = params.get("newPassword");
LoginUser loginUser = getLoginUser();
Long userId = loginUser.getUserId();
String password = loginUser.getPassword();
if (!SecurityUtils.matchesPassword(oldPassword, password))
{
return error("修改密码失败,旧密码错误");
}
if (SecurityUtils.matchesPassword(newPassword, password))
{
return error("新密码不能与旧密码相同");
}
newPassword = SecurityUtils.encryptPassword(newPassword);
if (userService.resetUserPwd(userId, newPassword) > 0)
{
// 更新缓存用户密码&密码最后更新时间
loginUser.getUser().setPwdUpdateDate(DateUtils.getNowDate());
loginUser.getUser().setPassword(newPassword);
tokenService.setLoginUser(loginUser);
return success();
}
return error("修改密码异常,请联系管理员");
}
/**
* 头像上传
*/
@Log(title = "用户头像", businessType = BusinessType.UPDATE)
@PostMapping("/avatar")
public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception
{
if (!file.isEmpty())
{
LoginUser loginUser = getLoginUser();
String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION, true);
if (userService.updateUserAvatar(loginUser.getUserId(), avatar))
{
String oldAvatar = loginUser.getUser().getAvatar();
if (StringUtils.isNotEmpty(oldAvatar))
{
FileUtils.deleteFile(RuoYiConfig.getProfile() + FileUtils.stripPrefix(oldAvatar));
}
AjaxResult ajax = AjaxResult.success();
ajax.put("imgUrl", avatar);
// 更新缓存用户头像
loginUser.getUser().setAvatar(avatar);
tokenService.setLoginUser(loginUser);
return ajax;
}
}
return error("上传图片异常,请联系管理员");
}
}

View File

@@ -0,0 +1,38 @@
package com.ruoyi.web.controller.system;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.RegisterBody;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.SysRegisterService;
import com.ruoyi.system.service.ISysConfigService;
/**
* 注册验证
*
* @author ruoyi
*/
@RestController
public class SysRegisterController extends BaseController
{
@Autowired
private SysRegisterService registerService;
@Autowired
private ISysConfigService configService;
@PostMapping("/register")
public AjaxResult register(@RequestBody RegisterBody user)
{
if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser"))))
{
return error("当前系统没有开启注册功能!");
}
String msg = registerService.register(user);
return StringUtils.isEmpty(msg) ? success() : error(msg);
}
}

View File

@@ -0,0 +1,262 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.domain.SysUserRole;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysRoleService;
import com.ruoyi.system.service.ISysUserService;
/**
* 角色信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/role")
public class SysRoleController extends BaseController
{
@Autowired
private ISysRoleService roleService;
@Autowired
private TokenService tokenService;
@Autowired
private SysPermissionService permissionService;
@Autowired
private ISysUserService userService;
@Autowired
private ISysDeptService deptService;
@PreAuthorize("@ss.hasPermi('system:role:list')")
@GetMapping("/list")
public TableDataInfo list(SysRole role)
{
startPage();
List<SysRole> list = roleService.selectRoleList(role);
return getDataTable(list);
}
@Log(title = "角色管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:role:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysRole role)
{
List<SysRole> list = roleService.selectRoleList(role);
ExcelUtil<SysRole> util = new ExcelUtil<SysRole>(SysRole.class);
util.exportExcel(response, list, "角色数据");
}
/**
* 根据角色编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:role:query')")
@GetMapping(value = "/{roleId}")
public AjaxResult getInfo(@PathVariable Long roleId)
{
roleService.checkRoleDataScope(roleId);
return success(roleService.selectRoleById(roleId));
}
/**
* 新增角色
*/
@PreAuthorize("@ss.hasPermi('system:role:add')")
@Log(title = "角色管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysRole role)
{
if (!roleService.checkRoleNameUnique(role))
{
return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
}
else if (!roleService.checkRoleKeyUnique(role))
{
return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在");
}
role.setCreateBy(getUsername());
return toAjax(roleService.insertRole(role));
}
/**
* 修改保存角色
*/
@PreAuthorize("@ss.hasPermi('system:role:edit')")
@Log(title = "角色管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysRole role)
{
roleService.checkRoleAllowed(role);
roleService.checkRoleDataScope(role.getRoleId());
if (!roleService.checkRoleNameUnique(role))
{
return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在");
}
else if (!roleService.checkRoleKeyUnique(role))
{
return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在");
}
role.setUpdateBy(getUsername());
if (roleService.updateRole(role) > 0)
{
// 更新缓存用户权限
LoginUser loginUser = getLoginUser();
if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin())
{
loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName()));
loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser()));
tokenService.setLoginUser(loginUser);
}
return success();
}
return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员");
}
/**
* 修改保存数据权限
*/
@PreAuthorize("@ss.hasPermi('system:role:edit')")
@Log(title = "角色管理", businessType = BusinessType.UPDATE)
@PutMapping("/dataScope")
public AjaxResult dataScope(@RequestBody SysRole role)
{
roleService.checkRoleAllowed(role);
roleService.checkRoleDataScope(role.getRoleId());
return toAjax(roleService.authDataScope(role));
}
/**
* 状态修改
*/
@PreAuthorize("@ss.hasPermi('system:role:edit')")
@Log(title = "角色管理", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody SysRole role)
{
roleService.checkRoleAllowed(role);
roleService.checkRoleDataScope(role.getRoleId());
role.setUpdateBy(getUsername());
return toAjax(roleService.updateRoleStatus(role));
}
/**
* 删除角色
*/
@PreAuthorize("@ss.hasPermi('system:role:remove')")
@Log(title = "角色管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{roleIds}")
public AjaxResult remove(@PathVariable Long[] roleIds)
{
return toAjax(roleService.deleteRoleByIds(roleIds));
}
/**
* 获取角色选择框列表
*/
@PreAuthorize("@ss.hasPermi('system:role:query')")
@GetMapping("/optionselect")
public AjaxResult optionselect()
{
return success(roleService.selectRoleAll());
}
/**
* 查询已分配用户角色列表
*/
@PreAuthorize("@ss.hasPermi('system:role:list')")
@GetMapping("/authUser/allocatedList")
public TableDataInfo allocatedList(SysUser user)
{
startPage();
List<SysUser> list = userService.selectAllocatedList(user);
return getDataTable(list);
}
/**
* 查询未分配用户角色列表
*/
@PreAuthorize("@ss.hasPermi('system:role:list')")
@GetMapping("/authUser/unallocatedList")
public TableDataInfo unallocatedList(SysUser user)
{
startPage();
List<SysUser> list = userService.selectUnallocatedList(user);
return getDataTable(list);
}
/**
* 取消授权用户
*/
@PreAuthorize("@ss.hasPermi('system:role:edit')")
@Log(title = "角色管理", businessType = BusinessType.GRANT)
@PutMapping("/authUser/cancel")
public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole)
{
return toAjax(roleService.deleteAuthUser(userRole));
}
/**
* 批量取消授权用户
*/
@PreAuthorize("@ss.hasPermi('system:role:edit')")
@Log(title = "角色管理", businessType = BusinessType.GRANT)
@PutMapping("/authUser/cancelAll")
public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds)
{
return toAjax(roleService.deleteAuthUsers(roleId, userIds));
}
/**
* 批量选择用户授权
*/
@PreAuthorize("@ss.hasPermi('system:role:edit')")
@Log(title = "角色管理", businessType = BusinessType.GRANT)
@PutMapping("/authUser/selectAll")
public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds)
{
roleService.checkRoleDataScope(roleId);
return toAjax(roleService.insertAuthUsers(roleId, userIds));
}
/**
* 获取对应角色部门树列表
*/
@PreAuthorize("@ss.hasPermi('system:role:query')")
@GetMapping(value = "/deptTree/{roleId}")
public AjaxResult deptTree(@PathVariable("roleId") Long roleId)
{
AjaxResult ajax = AjaxResult.success();
ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId));
ajax.put("depts", deptService.selectDeptTreeList(new SysDept()));
return ajax;
}
}

View File

@@ -0,0 +1,256 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysPostService;
import com.ruoyi.system.service.ISysRoleService;
import com.ruoyi.system.service.ISysUserService;
/**
* 用户信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController
{
@Autowired
private ISysUserService userService;
@Autowired
private ISysRoleService roleService;
@Autowired
private ISysDeptService deptService;
@Autowired
private ISysPostService postService;
/**
* 获取用户列表
*/
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
{
startPage();
List<SysUser> list = userService.selectUserList(user);
return getDataTable(list);
}
@Log(title = "用户管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:user:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysUser user)
{
List<SysUser> list = userService.selectUserList(user);
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
util.exportExcel(response, list, "用户数据");
}
@Log(title = "用户管理", businessType = BusinessType.IMPORT)
@PreAuthorize("@ss.hasPermi('system:user:import')")
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
{
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
List<SysUser> userList = util.importExcel(file.getInputStream());
String operName = getUsername();
String message = userService.importUser(userList, updateSupport, operName);
return success(message);
}
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response)
{
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
util.importTemplateExcel(response, "用户数据");
}
/**
* 根据用户编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:user:query')")
@GetMapping(value = { "/", "/{userId}" })
public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId)
{
AjaxResult ajax = AjaxResult.success();
if (StringUtils.isNotNull(userId))
{
userService.checkUserDataScope(userId);
SysUser sysUser = userService.selectUserById(userId);
ajax.put(AjaxResult.DATA_TAG, sysUser);
ajax.put("postIds", postService.selectPostListByUserId(userId));
ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList()));
}
List<SysRole> roles = roleService.selectRoleAll();
ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
ajax.put("posts", postService.selectPostAll());
return ajax;
}
/**
* 新增用户
*/
@PreAuthorize("@ss.hasPermi('system:user:add')")
@Log(title = "用户管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysUser user)
{
deptService.checkDeptDataScope(user.getDeptId());
roleService.checkRoleDataScope(user.getRoleIds());
if (!userService.checkUserNameUnique(user))
{
return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
}
else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
{
return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
}
else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
{
return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
user.setCreateBy(getUsername());
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
return toAjax(userService.insertUser(user));
}
/**
* 修改用户
*/
@PreAuthorize("@ss.hasPermi('system:user:edit')")
@Log(title = "用户管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysUser user)
{
userService.checkUserAllowed(user);
userService.checkUserDataScope(user.getUserId());
deptService.checkDeptDataScope(user.getDeptId());
roleService.checkRoleDataScope(user.getRoleIds());
if (!userService.checkUserNameUnique(user))
{
return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在");
}
else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
{
return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
}
else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
{
return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
user.setUpdateBy(getUsername());
return toAjax(userService.updateUser(user));
}
/**
* 删除用户
*/
@PreAuthorize("@ss.hasPermi('system:user:remove')")
@Log(title = "用户管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds)
{
if (ArrayUtils.contains(userIds, getUserId()))
{
return error("当前用户不能删除");
}
return toAjax(userService.deleteUserByIds(userIds));
}
/**
* 重置密码
*/
@PreAuthorize("@ss.hasPermi('system:user:resetPwd')")
@Log(title = "用户管理", businessType = BusinessType.UPDATE)
@PutMapping("/resetPwd")
public AjaxResult resetPwd(@RequestBody SysUser user)
{
userService.checkUserAllowed(user);
userService.checkUserDataScope(user.getUserId());
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
user.setUpdateBy(getUsername());
return toAjax(userService.resetPwd(user));
}
/**
* 状态修改
*/
@PreAuthorize("@ss.hasPermi('system:user:edit')")
@Log(title = "用户管理", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody SysUser user)
{
userService.checkUserAllowed(user);
userService.checkUserDataScope(user.getUserId());
user.setUpdateBy(getUsername());
return toAjax(userService.updateUserStatus(user));
}
/**
* 根据用户编号获取授权角色
*/
@PreAuthorize("@ss.hasPermi('system:user:query')")
@GetMapping("/authRole/{userId}")
public AjaxResult authRole(@PathVariable("userId") Long userId)
{
AjaxResult ajax = AjaxResult.success();
SysUser user = userService.selectUserById(userId);
List<SysRole> roles = roleService.selectRolesByUserId(userId);
ajax.put("user", user);
ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
return ajax;
}
/**
* 用户授权角色
*/
@PreAuthorize("@ss.hasPermi('system:user:edit')")
@Log(title = "用户管理", businessType = BusinessType.GRANT)
@PutMapping("/authRole")
public AjaxResult insertAuthRole(Long userId, Long[] roleIds)
{
userService.checkUserDataScope(userId);
roleService.checkRoleDataScope(roleIds);
userService.insertUserAuth(userId, roleIds);
return success();
}
/**
* 获取部门树列表
*/
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/deptTree")
public AjaxResult deptTree(SysDept dept)
{
return success(deptService.selectDeptTreeList(dept));
}
}

View File

@@ -0,0 +1,430 @@
package com.ruoyi.web.controller.tool;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.scheduling.annotation.Scheduled;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.async.DeferredResult;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.StringUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* 斑马订单控制器
*
* @author ruoyi
*/
@Api("斑马订单接口")
@RestController
@RequestMapping("/tool/banma")
@Anonymous
public class BanmaOrderController extends BaseController {
private static String AUTH_TOKEN = "Bearer e5V8Vlaf9xh5i31xaI300wbdXEE3iLtgip+JXfzZsb7GShP2XCGhoVzTEVxyc8LH";
private static final String LOGIN_URL = "https://banma365.cn/api/login";
private static final String LOGIN_USERNAME = "大赢家网络科技(主账号)";
private static final String LOGIN_PASSWORD = "banma123456";
private static final String API_URL = "https://banma365.cn/api/order/list?recipientName=&page=%d&size=%d&markFlag=0&state=4&_t=%d";
private static final String API_URL_WITH_TIME = "https://banma365.cn/api/order/list?recipientName=&page=%d&size=%d&markFlag=0&state=4&orderedAtStart=%s&orderedAtEnd=%s&_t=%d";
private static final String TRACKING_URL = "https://banma365.cn/zebraExpressHub/web/tracking/getByExpressNumber/%s";
private static final int CONNECTION_TIMEOUT = 999999999;
private static final int READ_TIMEOUT = 999999999;
private static final int DEFAULT_PAGE_SIZE = 20;
@Autowired
private RestTemplate restTemplate;
@Autowired
private SagawaExpressController sagawaExpressController;
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
public BanmaOrderController() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(CONNECTION_TIMEOUT);
factory.setReadTimeout(READ_TIMEOUT);
restTemplate = new RestTemplate(factory);
}
/**
* 初始化方法启动时刷新token
*/
@PostConstruct
public void init() {
refreshToken();
}
/**
* 关闭线程池
*/
@PreDestroy
public void destroy() {
executorService.shutdownNow();
}
@Scheduled(fixedRate = 86400000 * 3)
public void refreshToken() {
try {
// 1. 输入准备:构建请求参数
Map<String, String> loginParams = new HashMap<>();
loginParams.put("username", LOGIN_USERNAME);
loginParams.put("password", LOGIN_PASSWORD);
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
ResponseEntity<Map> response = restTemplate.postForEntity(
LOGIN_URL,
new HttpEntity<>(loginParams, headers),
Map.class
);
Optional.ofNullable(response.getBody())
.filter(body -> Integer.valueOf(0).equals(body.get("code")))
.map(body -> (Map<String, Object>) body.get("data"))
.map(data -> (String) data.get("token"))
.filter(StringUtils::isNotEmpty)
.ifPresent(token -> {
AUTH_TOKEN = "Bearer " + token;
logger.info("斑马token刷新成功: {}", token);
});
} catch (Exception e) {
logger.error("斑马token刷新异常: {}", e.getMessage());
}
}
/**
* 创建HTTP请求实体
*/
private HttpEntity<String> createHttpEntity() {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", AUTH_TOKEN);
return new HttpEntity<>(headers);
}
/**
* 处理订单数据
*/
@SuppressWarnings("unchecked")
private CompletableFuture<Map<String, Object>> processOrderDataAsync(Map<String, Object> order) {
return CompletableFuture.supplyAsync(() -> {
if (order == null) return null;
Map<String, Object> simplifiedOrder = new HashMap<>();
// 提取国际运单号和运费
String trackingNumber = (String) order.get("internationalTrackingNumber");
simplifiedOrder.put("internationalTrackingNumber", trackingNumber);
simplifiedOrder.put("internationalShippingFee", order.get("internationalShippingFee"));
// 获取物流轨迹信息
if (StringUtils.isNotEmpty(trackingNumber)) {
simplifiedOrder.put("trackInfo", getTrackingInfo(trackingNumber));
}
// 处理子订单信息
Optional.ofNullable(order.get("subOrders"))
.map(subOrders -> (List<Map<String, Object>>) subOrders)
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.ifPresent(subOrder -> extractSubOrderFields(simplifiedOrder, subOrder));
return simplifiedOrder;
}, executorService);
}
/**
* 提取子订单字段
*/
private void extractSubOrderFields(Map<String, Object> simplifiedOrder, Map<String, Object> subOrder) {
// 基础信息
simplifiedOrder.put("orderedAt", subOrder.get("orderedAt"));
simplifiedOrder.put("timeSinceOrder", subOrder.get("timeSinceOrder"));
simplifiedOrder.put("productImage", subOrder.get("productImage"));
simplifiedOrder.put("createdAt", subOrder.get("createdAt"));
simplifiedOrder.put("poTrackingNumber", subOrder.get("poTrackingNumber"));
// 商品信息
simplifiedOrder.put("productTitle", subOrder.get("productTitle"));
simplifiedOrder.put("shopOrderNumber", subOrder.get("shopOrderNumber"));
simplifiedOrder.put("priceJpy", subOrder.get("priceJpy"));
simplifiedOrder.put("productQuantity", subOrder.get("productQuantity"));
simplifiedOrder.put("shippingFeeJpy", subOrder.get("shippingFeeJpy"));
simplifiedOrder.put("productNumber", subOrder.get("productNumber"));
// 采购信息
simplifiedOrder.put("poNumber", subOrder.get("poNumber"));
simplifiedOrder.put("shippingFeeCny", subOrder.get("shippingFeeCny"));
simplifiedOrder.put("poLogisticsCompany", subOrder.get("poLogisticsCompany"));
}
/**
* 获取斑马订单数据 - 异步方法
*/
@SuppressWarnings("unchecked")
private CompletableFuture<List<Map<String, Object>>> fetchOrdersFromApiAsync(int page, int size, String startDate, String endDate) {
return CompletableFuture.supplyAsync(() -> {
try {
HttpEntity<String> entity = createHttpEntity();
String url = buildApiUrl(page, size, startDate, endDate);
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);
Map<String, Object> responseBody = response.getBody();
if (responseBody == null || !responseBody.containsKey("data")) {
return Collections.emptyList();
}
Map<String, Object> dataMap = (Map<String, Object>) responseBody.get("data");
List<Map<String, Object>> orders = Optional.ofNullable(dataMap.get("list"))
.map(list -> (List<Map<String, Object>>) list)
.orElse(Collections.emptyList());
return orders;
} catch (Exception e) {
logger.error("获取订单数据失败: {}", e.getMessage());
return Collections.emptyList();
}
}, executorService);
}
/**
* 构建API URL
*/
private String buildApiUrl(int page, int size, String startDate, String endDate) {
if (StringUtils.isNotEmpty(startDate) && StringUtils.isNotEmpty(endDate)) {
String startTime = startDate + " 00:00:00";
String endTime = endDate + " 23:59:59";
return String.format(API_URL_WITH_TIME, page, size, startTime, endTime, System.currentTimeMillis());
}
return String.format(API_URL, page, size, System.currentTimeMillis());
}
/**
* 获取物流轨迹信息
*/
@SuppressWarnings("unchecked")
private String getTrackingInfo(String trackingNumber) {
try {
R<Map<String, Object>> sagawaResult = sagawaExpressController.getTrackingInfo(trackingNumber);
if (sagawaResult != null && sagawaResult.getCode() == 200) {
Map<String, Object> sagawaData = sagawaResult.getData();
if (sagawaData != null && "success".equals(sagawaData.get("status"))) {
Map<String, String> trackInfo = (Map<String, String>) sagawaData.get("trackInfo");
if (trackInfo != null) {
return String.format("%s - %s - %s",
trackInfo.get("status"),
trackInfo.get("dateTime"),
trackInfo.get("office"));
}
}
}
try {
String url = String.format(TRACKING_URL, trackingNumber);
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
Map<String, Object> responseBody = response.getBody();
if (responseBody != null && Integer.valueOf(0).equals(responseBody.get("code"))) {
return Optional.ofNullable(responseBody.get("data"))
.map(data -> (List<Map<String, Object>>) data)
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.map(track -> (String) track.get("track"))
.orElse(null);
}
} catch (Exception e) {
logger.error("从斑马API获取物流信息失败: {}", e.getMessage());
}
} catch (Exception e) {
logger.error("获取物流信息失败: {}", e.getMessage());
}
return "暂无物流信息";
}
/**
* 获取物流轨迹信息
*/
@ApiOperation("获取物流轨迹信息")
@GetMapping("/tracking/{trackingNumber}")
public R<String> getTracking(@PathVariable("trackingNumber") String trackingNumber) {
try {
String trackInfo = getTrackingInfo(trackingNumber);
return trackInfo != null ? R.ok(trackInfo) : R.fail("未找到物流信息");
} catch (Exception e) {
return R.fail("获取物流信息失败: " + e.getMessage());
}
}
/**
* 获取所有页的斑马订单 - 优化版本
*/
@ApiOperation("获取所有页的斑马订单")
@GetMapping("/orders/all")
@SuppressWarnings("unchecked")
public DeferredResult<R<Map<String, Object>>> getAllOrders(
@ApiParam("开始日期(yyyy-MM-dd)") @RequestParam(required = false) String startDate,
@ApiParam("结束日期(yyyy-MM-dd)") @RequestParam(required = false) String endDate) {
DeferredResult<R<Map<String, Object>>> deferredResult = new DeferredResult<>(9999000L);
CompletableFuture.runAsync(() -> {
try {
HttpEntity<String> entity = createHttpEntity();
String url = buildApiUrl(1, DEFAULT_PAGE_SIZE, startDate, endDate);
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);
Map<String, Object> responseBody = response.getBody();
Map<String, Object> dataMap = (Map<String, Object>) responseBody.get("data");
int totalCount = ((Number) dataMap.getOrDefault("total", 0)).intValue();
List<Map<String, Object>> orders = Optional.ofNullable(dataMap.get("list"))
.map(list -> (List<Map<String, Object>>) list)
.orElse(Collections.emptyList());
List<CompletableFuture<Map<String, Object>>> futures = orders.stream()
.map(this::processOrderDataAsync)
.toList();
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 收集所有处理结果
CompletableFuture<List<Map<String, Object>>> resultsFuture = allFutures.thenApply(v ->
futures.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.collect(Collectors.toList())
);
List<Map<String, Object>> processedOrders = resultsFuture.get();
int totalPages = (int) Math.ceil((double) totalCount / DEFAULT_PAGE_SIZE);
boolean hasMore = totalCount > 0 && 1 < totalPages;
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("orders", processedOrders);
resultMap.put("total", totalCount);
resultMap.put("totalPages", totalPages);
resultMap.put("hasMore", hasMore);
resultMap.put("nextPage", 2);
deferredResult.setResult(R.ok(resultMap));
} catch (Exception e) {
logger.error("获取订单数据失败: {}", e.getMessage());
deferredResult.setResult(R.fail("获取订单失败: " + e.getMessage()));
}
}, executorService);
return deferredResult;
}
/**
* 获取下一页斑马订单 - 优化版本
*/
@ApiOperation("获取下一页斑马订单")
@GetMapping("/orders/next")
public DeferredResult<R<Map<String, Object>>> getNextPageOrders(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@ApiParam("开始日期(yyyy-MM-dd)") @RequestParam(required = false) String startDate,
@ApiParam("结束日期(yyyy-MM-dd)") @RequestParam(required = false) String endDate) {
DeferredResult<R<Map<String, Object>>> deferredResult = new DeferredResult<>(999999999L);
CompletableFuture.runAsync(() -> {
try {
// 获取总页数信息
HttpEntity<String> entity = createHttpEntity();
String url = buildApiUrl(1, DEFAULT_PAGE_SIZE, startDate, endDate);
ResponseEntity<Map> countResponse = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);
Map<String, Object> countResponseBody = countResponse.getBody();
int totalPages = 1;
if (countResponseBody != null && countResponseBody.containsKey("data")) {
Map<String, Object> dataMap = (Map<String, Object>) countResponseBody.get("data");
int totalCount = ((Number) dataMap.getOrDefault("total", 0)).intValue();
totalPages = (int) Math.ceil((double) totalCount / DEFAULT_PAGE_SIZE);
}
// 获取当前页数据
CompletableFuture<List<Map<String, Object>>> ordersFuture = fetchOrdersFromApiAsync(page, DEFAULT_PAGE_SIZE, startDate, endDate);
List<Map<String, Object>> orders = ordersFuture.get();
// 并行处理订单数据
List<CompletableFuture<Map<String, Object>>> processFutures = orders.stream()
.map(this::processOrderDataAsync)
.collect(Collectors.toList());
// 等待所有处理完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
processFutures.toArray(new CompletableFuture[0])
);
CompletableFuture<List<Map<String, Object>>> resultsFuture = allFutures.thenApply(v ->
processFutures.stream()
.map(CompletableFuture::join)
.filter(order -> order != null)
.collect(Collectors.toList())
);
List<Map<String, Object>> processedOrders = resultsFuture.get();
// 修改hasMore判断逻辑根据当前页数和总页数判断
boolean hasMore = page < totalPages;
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("orders", processedOrders);
resultMap.put("hasMore", hasMore);
resultMap.put("nextPage", page + 1);
resultMap.put("totalPages", totalPages);
deferredResult.setResult(R.ok(resultMap));
} catch (Exception e) {
logger.error("获取下一页订单失败: {}", e.getMessage());
deferredResult.setResult(R.fail("获取订单失败: " + e.getMessage()));
}
}, executorService);
return deferredResult;
}
/**
* 图片代理接口
*/
@ApiOperation("图片代理接口")
@GetMapping("/image-proxy")
public void imageProxy(@RequestParam("url") String imageUrl, javax.servlet.http.HttpServletResponse response) {
if (StringUtils.isEmpty(imageUrl)) {
return;
}
try {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(999999999);
factory.setReadTimeout(999999999);
RestTemplate proxyTemplate = new RestTemplate(factory);
ResponseEntity<byte[]> imageResponse = proxyTemplate.getForEntity(imageUrl, byte[].class);
byte[] imageBytes = imageResponse.getBody();
if (imageBytes != null) {
String contentType = Optional.ofNullable(imageResponse.getHeaders().getContentType())
.map(Object::toString)
.orElse("image/jpeg");
response.setContentType(contentType);
response.setContentLength(imageBytes.length);
response.getOutputStream().write(imageBytes);
response.getOutputStream().flush();
}
} catch (Exception e) {
logger.error("图片代理请求失败: {}", e.getMessage());
}
}
/**
* 手动刷新token接口
*/
@GetMapping("/refresh-token")
public R<String> manualRefreshToken() {
refreshToken();
return R.ok("Token刷新请求已执行");
}
}

View File

@@ -0,0 +1,211 @@
package com.ruoyi.web.controller.tool;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.web.util.Alibaba1688CookieUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PreDestroy;
import javax.xml.bind.DatatypeConverter;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
/**
* 1688图像搜索API
*/
@RestController
@RequestMapping("/figre")
@Anonymous
public class FigureTransmissionController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(FigureTransmissionController.class);
private static final String APP_KEY = "12574478";
private static final int MAX_BATCH_SIZE = 30;
private static final int REQUEST_TIMEOUT = 30;
@Autowired
private RestTemplate restTemplate;
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
@PreDestroy
public void shutdown() {
executorService.shutdown();
}
public static class BatchImageUrlRequest {
private List<String> imageUrls;
public List<String> getImageUrls() {
return imageUrls;
}
public void setImageUrls(List<String> imageUrls) {
this.imageUrls = imageUrls;
}
}
@PostMapping("/batchUpload1688Images")
public AjaxResult batchUpload1688Images(@RequestBody BatchImageUrlRequest request) {
List<String> batchUrls = request.getImageUrls().stream().limit(MAX_BATCH_SIZE).collect(Collectors.toList());
List<CompletableFuture<Map<String, Object>>> futures = batchUrls.stream().map(imageUrl -> CompletableFuture.supplyAsync(() -> {
Map<String, Object> result = new HashMap<>();
result.put("imageUrl", imageUrl);
try {
ResponseEntity<byte[]> response = restTemplate.getForEntity(imageUrl, byte[].class);
String base64Image = Base64.getEncoder().encodeToString(response.getBody());
AjaxResult uploadResult = uploadImageBase64(base64Image);
String responseBody = (String) uploadResult.get("data");
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> apiResponse = objectMapper.readValue(responseBody, Map.class);
Map<String, Object> data = (Map<String, Object>) apiResponse.get("data");
String imageId = (String) data.get("imageId");
result.put("success", true);
result.put("imageId", imageId);
result.put("searchUrl", "https://s.1688.com/youyuan/index.html?tab=imageSearch&imageType=spider&imageId=" + imageId);
} catch (Exception e) {
result.put("success", false);
result.put("error", "处理图片失败: " + e.getMessage());
}
return result;
}, executorService)).collect(Collectors.toList());
List<Map<String, Object>> results = futures.stream().map(future -> {
try {
return future.get(REQUEST_TIMEOUT, TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
}).collect(Collectors.toList());
return AjaxResult.success("批量处理完成", results);
}
@PostMapping("/upload1688ImageMobile")
public AjaxResult upload1688ImageMobile(@RequestParam String imageUrl) {
try {
String token = Alibaba1688CookieUtil.getToken(restTemplate);
long timestamp = System.currentTimeMillis();
String jsonData = "{\"appId\":\"32517\",\"params\":\"{\\\"categoryId\\\":-1,\\\"imageAddress\\\":\\\"" + imageUrl + "\\\",\\\"interfaceName\\\":\\\"imageOfferSearchService\\\",\\\"needYolocrop\\\":false,\\\"pageIndex\\\":\\\"1\\\",\\\"pageSize\\\":\\\"40\\\",\\\"searchScene\\\":\\\"image\\\",\\\"snAppAb\\\":true,\\\"appName\\\":\\\"ios\\\",\\\"scene\\\":\\\"seoSearch\\\"}\"}";
String sign = generateSign(token, String.valueOf(timestamp), jsonData);
String url = "https://h5api.m.1688.com/h5/mtop.relationrecommend.wirelessrecommend.recommend/2.0/" + "?jsv=2.6.1" + "&appKey=" + APP_KEY + "&t=" + timestamp + "&sign=" + sign + "&v=2.0" + "&type=originaljson" + "&isSec=0" + "&timeout=20000" + "&api=mtop.relationrecommend.WirelessRecommend.recommend" + "&ignoreLogin=true" + "&prefix=h5api" + "&dataType=jsonp";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.set("Cookie", Alibaba1688CookieUtil.getCookieString(restTemplate));
headers.set("authority", "h5api.m.1688.com");
headers.set("accept", "application/json");
headers.set("origin", "https://m.1688.com");
headers.set("referer", "https://m.1688.com/");
headers.set("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1");
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("data", jsonData);
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
updateCookiesFromResponse(response);
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> originalResponse = objectMapper.readValue(response.getBody(), Map.class);
List<String> detailUrls = new ArrayList<>();
try {
Map<String, Object> offerList = (Map<String, Object>) ((Map<String, Object>) originalResponse.get("data")).get("offerList");
for (Map<String, Object> offer : (List<Map<String, Object>>) offerList.get("offers")) {
String detailUrl = (String) offer.get("detailUrl");
if (StringUtils.isNotEmpty(detailUrl)) {
detailUrls.add(detailUrl);
}
}
} catch (Exception e) {
log.error("提取detailUrls失败", e);
}
Map<String, Object> result = new HashMap<>();
List<String> modifiedUrls = detailUrls.stream().map(detailUrl -> {
String baseUrl = detailUrl.split("\\?")[0];
return baseUrl.replace("detail.1688.com", "m.1688.com") + "?ptow=113d26e7c9a&ptow=113d26e&callByHgJs=1&__removesafearea__=1&src_cna=cPb9IGgLcDgBASQOA3xQ3mUM";
}).collect(Collectors.toList());
result.put("detailUrls", modifiedUrls);
return AjaxResult.success(result);
} catch (Exception e) {
log.error("图片搜索失败", e);
return AjaxResult.error("图片搜索失败: " + e.getMessage());
}
}
@PostMapping("/refreshCookie")
public AjaxResult refreshCookie() {
try {
Alibaba1688CookieUtil.refreshCookies(restTemplate);
String token = Alibaba1688CookieUtil.getToken(restTemplate);
return StringUtils.isNotEmpty(token) ? AjaxResult.success("刷新Cookie成功", token) : AjaxResult.error("获取1688 API token失败请稍后重试");
} catch (Exception e) {
log.error("刷新Cookie失败", e);
return AjaxResult.error("刷新Cookie失败: " + e.getMessage());
}
}
private String generateSign(String token, String timestamp, String data) {
try {
String signStr = token + "&" + timestamp + "&" + APP_KEY + "&" + data;
MessageDigest md = MessageDigest.getInstance("MD5");
return DatatypeConverter.printHexBinary(md.digest(signStr.getBytes("UTF-8"))).toLowerCase();
} catch (Exception e) {
log.error("生成签名失败", e);
return "";
}
}
private AjaxResult uploadImageBase64(String base64Image) {
try {
String token = Alibaba1688CookieUtil.getToken(restTemplate);
long timestamp = System.currentTimeMillis();
String jsonData = "{\"appId\":32517,\"params\":\"{\\\"searchScene\\\":\\\"imageEx\\\",\\\"interfaceName\\\":\\\"imageBase64ToImageId\\\",\\\"serviceParam.extendParam[imageBase64]\\\":\\\"" + base64Image + "\\\",\\\"subChannel\\\":\\\"pc_image_search_image_id\\\"}\"}";
String sign = generateSign(token, String.valueOf(timestamp), jsonData);
String url = "https://h5api.m.1688.com/h5/mtop.relationrecommend.wirelessrecommend.recommend/2.0/?jsv=2.7.4&appKey=" + APP_KEY + "&t=" + timestamp + "&sign=" + sign + "&api=mtop.relationrecommend.wirelessrecommend.recommend&v=2.0&type=originaljson&timeout=20000&dataType=jsonp";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.set("Cookie", Alibaba1688CookieUtil.getCookieString(restTemplate));
headers.set("authority", "h5api.m.1688.com");
headers.set("accept", "application/json");
headers.set("origin", "https://www.1688.com");
headers.set("referer", "https://www.1688.com/");
headers.set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36");
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("data", jsonData);
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
updateCookiesFromResponse(response);
return AjaxResult.success("上传成功", response.getBody());
} catch (Exception e) {
log.error("上传图片失败", e);
return AjaxResult.error("上传失败: " + e.getMessage());
}
}
private void updateCookiesFromResponse(ResponseEntity<String> response) {
List<String> newCookies = response.getHeaders().get("Set-Cookie");
if (newCookies != null && !newCookies.isEmpty()) {
for (String cookie : newCookies) {
String[] parts = cookie.split(";")[0].split("=", 2);
if (parts.length == 2) {
Alibaba1688CookieUtil.setCookie(parts[0], parts[1]);
}
}
}
}
}

View File

@@ -0,0 +1,65 @@
package com.ruoyi.web.controller.tool;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import com.qiniu.http.Response;
import com.qiniu.storage.UploadManager;
import com.qiniu.util.Auth;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.config.Qiniu;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.framework.config.FileDto;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author liwq
* @Date 2025年03月14日 14:38
* @Desc
*/
@RestController
@RequestMapping("file")
@AllArgsConstructor
@Anonymous
@Slf4j
public class FileController {
public final static String NORM_MONTH_PATTERN = "yyyy/MM/";
private final Qiniu qiniu;
private final UploadManager uploadManager;
private final Auth auth;
@PostMapping("/uploads")
public AjaxResult uploadFiles(List<MultipartFile> files) {
List<FileDto> fileDtoS = new ArrayList<>();
for (MultipartFile file : files) {
String extName = FileUtil.extName(file.getOriginalFilename());
String fileName = DateUtil.format(new Date(), NORM_MONTH_PATTERN) + IdUtil.simpleUUID() + "." + extName;
String uploadToken = auth.uploadToken(qiniu.getBucket());
try (InputStream inputStream = file.getInputStream()) {
Response response = uploadManager.put(inputStream, fileName, uploadToken,null,"");
if (response.isOK()) {
String fileUrl = qiniu.getResourcesUrl() + fileName;
fileDtoS.add(new FileDto(fileUrl, fileName));
log.info("文件上传成功:{}", fileName);
} else {
log.error("文件上传失败:{},响应:{}", file.getOriginalFilename(), response.bodyString());
}
} catch (IOException e) {
e.printStackTrace();
}
}
return AjaxResult.success(fileDtoS) ;
}
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.web.controller.tool;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.redis.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
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;
@RestController
@Anonymous
public class GenmaijlController {
@Autowired
RedisCache redisCache;
@GetMapping("/getToken")
public String getToken(){
return redisCache.getCacheObject("genmaijlToken");
}
@PostMapping("/saveToken")
public int saveToken(@RequestBody String token){
redisCache.setCacheObject("genmaijlToken", token);
return 1;
}
}

View File

@@ -0,0 +1,57 @@
package com.ruoyi.web.controller.tool;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import java.util.ArrayList;
import java.util.List;
/**
* 1688Controller
*
* @author ruoyi
* @date 2025-07-15
*/
@RestController
@RequestMapping("/prod/ozon")
public class ProductsController extends BaseController
{
@GetMapping("/scrapeImages")
public AjaxResult scrapeImages()
{
String url = "https://www.ozon.ru/highlight/ozon-global/?currency_price=14.000%3B500.000";
List<String> imageUrls = new ArrayList<>();
Site site = Site.me()
.setRetryTimes(3)
.setTimeOut(10000);
site.addHeader("cookie", "xcid=b30f6db523d4aee734a822aa0a230b3f; __Secure-ext_xcid=b30f6db523d4aee734a822aa0a230b3f; __Secure-ab-group=92; rfuid=NjkyNDcyNDUyLDEyNC4wNDM0NzUyNzUxNjA3NCwxMDI4MjM3MjIzLC0xLC05ODc0NjQ3MjQsVzNzaWJtRnRaU0k2SWtOb2NtOXRhWFZ0SUZCRVJpQlFiSFZuYVc0aUxDSmtaWE5qY21sd2RHbHZiaUk2SWxCdmNuUmhZbXhsSUVSdlkzVnRaVzUwSUVadmNtMWhkQ0lzSW0xcGJXVlVlWEJsY3lJNlczc2lkSGx3WlNJNkltRndjR3hwWTJGMGFXOXVMM2d0WjI5dloyeGxMV05vY205dFpTMXdaR1lpTENKemRXWm1hWGhsY3lJNkluQmtaaUo5WFgwc2V5SnVZVzFsSWpvaVEyaHliMjFwZFcwZ1VFUkdJRlpwWlhkbGNpSXNJbVJsYzJOeWFYQjBhVzl1SWpvaUlpd2liV2x0WlZSNWNHVnpJanBiZXlKMGVYQmxJam9pWVhCd2JHbGpZWFJwYjI0dmNHUm1JaXdpYzNWbVptbDRaWE1pT2lKd1pHWWlmVjE5WFE9PSxXeUo2YUMxRFRpSmQsMCwxLDAsMjQsMjM3NDE1OTMwLDgsMjI3MTI2NTIwLDAsMSwwLC00OTEyNzU1MjMsUjI5dloyeGxJRWx1WXk0Z1RtVjBjMk5oY0dVZ1IyVmphMjhnVjJsdU16SWdOUzR3SUNoWGFXNWtiM2R6SUU1VUlERXdMakE3SUZkcGJqWTBPeUI0TmpRcElFRndjR3hsVjJWaVMybDBMelV6Tnk0ek5pQW9TMGhVVFV3c0lHeHBhMlVnUjJWamEyOHBJRU5vY205dFpTOHhNamN1TUM0d0xqQWdVMkZtWVhKcEx6VXpOeTR6TmlBeU1EQXpNREV3TnlCTmIzcHBiR3hoLGV5SmphSEp2YldVaU9uc2lZWEJ3SWpwN0ltbHpTVzV6ZEdGc2JHVmtJanBtWVd4elpTd2lTVzV6ZEdGc2JGTjBZWFJsSWpwN0lrUkpVMEZDVEVWRUlqb2laR2x6WVdKc1pXUWlMQ0pKVGxOVVFVeE1SVVFpT2lKcGJuTjBZV3hzWldRaUxDSk9UMVJmU1U1VFZFRk1URVZFSWpvaWJtOTBYMmx1YzNSaGJHeGxaQ0o5TENKU2RXNXVhVzVuVTNSaGRHVWlPbnNpUTBGT1RrOVVYMUpWVGlJNkltTmhibTV2ZEY5eWRXNGlMQ0pTUlVGRVdWOVVUMTlTVlU0aU9pSnlaV0ZrZVY5MGIxOXlkVzRpTENKU1ZVNU9TVTVISWpvaWNuVnVibWx1WnlKOWZYMTksNjUsLTExODM0MTA3MiwxLDEsLTEsMTY5OTk1NDg4NywxNjk5OTU0ODg3LDMzNjAwNzkzMyw4; guest=true; x-hng=lang=zh-CN&domain=www.ozon.ru; __Secure-user-id=210183128; is_adult_confirmed=; is_alco_adult_confirmed=; ozonIdAuthResponseToken=eyJhbGciOiJIUzI1NiIsIm96b25pZCI6Im5vdHNlbnNpdGl2ZSIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyMTAxODMxMjgsImlzX3JlZ2lzdHJhdGlvbiI6ZmFsc2UsInJldHVybl91cmwiOiIiLCJwYXlsb2FkIjpudWxsLCJleHAiOjE3NTI1NDczNzksImlhdCI6MTc1MjU0NzM2OSwiaXNzIjoib3pvbmlkIn0.DX0nfNf9rcuPdt9lzE5fn1en5yqiD7Aw1vqpRt13OiU; abt_data=7.iE3bYFZ2m7yMc8mn5Rm8V9pI_ELHBH8eHNcM1w0kxMd8-HXap37uTEk6E_nAsmdWsO5pYQKhwamysCHZexl_YPPpWOWk7wgfKSuP8pTEdlDlXwLOy-sokLKLOdHyTFxcxNx5yRfpmNqFoQP8D5KccoiDh5U5kU8x7rJDLpqixSah6TFKKYsTiZrokn5Tb5aJu5lMAZBOkhr7CkYTFd_4j9wtALKnFM-oZGxCX0qTgUP5kIf9MfDSGI0U0pZ6igW6aSGirFb5ZVNmCV2D4NImCGn00K_Sn8ZX0vR6krWW1cixLrCnKp0rO7JJEFi9c7-4FL54ZvaJ-tKg8ALwlPmRIr-aI156iSlHQU6lULo8oKmBL13eLI9d_d8fp50BJe7oA6PNylYhI5DdV81WGI2EFnZtR3sVYF8O4qrS7gXwsMCoku51prrYZG1pLJJiD8HfAPTnFLvxpURZ7x1-lsAinljoh8_N1oVP89PVPJjd-tKQyRiUnleAnwaeF9kQmGvIMqYSYi506QWf-KHqfdEnEA; __Secure-ETC=90ef0679199c55cdec8592a7e480c3c5; __Secure-access-token=8.210183128.eH-AgbboQkWnqX9a7XeDqQ.92.Aa5lCeHJOXOoA90AsaTtX6OsppswLMDuxLGNHW98_K79BKfyceiX8mpea_qxY5qoaYUOO7_5hsQ6ndRa9tCxUzOCvERd7f056ZaoOw8W9lnUgQn_q5TX-7X2WK8ejP7OZg.20250715024249.20250715073104.M4j1mEB0PD4T55SZ4iQqCG6lZytjYmwG50Q28F-3q_s.1a7857bd2a7e95575; __Secure-refresh-token=8.210183128.eH-AgbboQkWnqX9a7XeDqQ.92.Aa5lCeHJOXOoA90AsaTtX6OsppswLMDuxLGNHW98_K79BKfyceiX8mpea_qxY5qoaYUOO7_5hsQ6ndRa9tCxUzOCvERd7f056ZaoOw8W9lnUgQn_q5TX-7X2WK8ejP7OZg.20250715024249.20250715073104.Ip-CdBv1mEGilz15LZCTwFmfoxEL8RrayFNa90r_XLI.197047eb6a54a7041; is_cookies_accepted=1; token=eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjJiMDM5ODBiLWYzZDQtNDI2Ny04NTQ2LWYwZjQxOTczNTAyNiJ9.oIF-bHh7pubdNWzETutNcoc_Nu-A7zgqBIJwcHFgF0V2s-xfnZVbs_EbvyJSBYYkUjqrBlJP_1qBl3vB1mQ_ow");
Spider.create(new PageProcessor() {
@Override
public void process(Page page) {
List<String> imgs = page.getHtml()
.css("div.ip7_24.p7i_24 img.i7p_24.b95_3_1-a", "src").all();
page.putField("imageUrls", imgs);
}
@Override
public Site getSite() {
return site;
}
}).addUrl(url)
.addPipeline((resultItems, task) -> {
List<String> imgs = resultItems.get("imageUrls");
if (imgs != null) {
imageUrls.addAll(imgs);
}
})
.thread(1)
.run();
return AjaxResult.success(imageUrls);
}
}

View File

@@ -0,0 +1,430 @@
package com.ruoyi.web.controller.tool;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.annotation.Anonymous;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import com.ruoyi.web.util.WebMagicProxyUtil;
import com.ruoyi.web.util.Alibaba1688CookieUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import javax.xml.bind.DatatypeConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.codecraft.webmagic.downloader.HttpClientDownloader;
import java.net.*;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@RestController
@RequestMapping("/prod/rakuten")
public class RakutenController extends BaseController {
private static final Logger logger = LoggerFactory.getLogger(RakutenController.class);
private final Random random = new Random();
private static final String APP_KEY = "12574478";
@Autowired
private RestTemplate restTemplate;
// @GetMapping("test")
// public String test(){
// final StringBuilder aa = new StringBuilder();
// String targetUrl = "https://ranking.rakuten.co.jp/?l-id=top_normal_grayheader02";
// logger.info("=== 开始爬取测试 ===");
// Proxy systemProxy = detectSystemProxy(targetUrl);
// if (systemProxy != null) {
// logger.info("成功检测到代理,准备配置下载器");
// } else {
// logger.info("未检测到代理,使用直连模式");
// }
// HttpClientDownloader downloader = createProxyDownloader(systemProxy);
// Spider.create(new PageProcessor() {
// @Override
// public void process(Page page) {
// aa.append(page.getHtml().toString());
// }
// @Override
// public Site getSite() {
// Site site= Site.me();
// site.addHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36");
// site.addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
// site.addHeader("accept-encoding", "gzip, deflate, br, zstd");
// site.addHeader("accept-language", "zh-CN,zh;q=0.9");
// site.addHeader("cache-control", "max-age=0");
// site.addHeader("priority", "u=0, i");
// site.addHeader("sec-ch-ua", "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"");
// site.addHeader("sec-ch-ua-mobile", "?0");
// site.addHeader("sec-ch-ua-platform", "\"Windows\"");
// site.addHeader("sec-fetch-dest", "document");
// site.addHeader("sec-fetch-mode", "navigate");
// site.addHeader("sec-fetch-site", "same-origin");
// site.addHeader("sec-fetch-user", "?1");
// site.addHeader("upgrade-insecure-requests", "1");
// site.addHeader("cookie", "_ra=1750472997398|0ff0eb32-5d9f-4c27-a7ca-c9ff1149e90b; Rp=779873a7b6c0e87edcf6f39cf2368561928309c8; rcx=136377ca-334f-4305-b45e-ad43e9538d2e; rcxGlobal=136377ca-334f-4305-b45e-ad43e9538d2e;");
// return site;
// }
// }).addUrl(targetUrl).setDownloader(downloader).thread(1).run();
// return aa.toString();
// }
//
/**
* 根据imageUrl爬取1688商品价格和重量信息返回中位数价格及对应的重量
*
* @param imageUrl 图片URL
* @return 商品中位数价格和对应重量
*/
@GetMapping("/scrape1688Products")
public AjaxResult scrape1688Products(@RequestParam String imageUrl) {
try {
List<ProductInfo> productInfoList = new ArrayList<>();
Site site = createOptimizedSite();
List<String> detailUrls = get1688DetailUrls(imageUrl);
scrapeProductDetailsSequential(detailUrls, site, productInfoList);
if (productInfoList.isEmpty()) {
logger.error("爬取1688商品数据失败");
return AjaxResult.error("未找到商品信息");
}
return buildResult(productInfoList);
} catch (Exception e) {
logger.error("爬取1688商品数据失败: {}", e.getMessage(), e);
return AjaxResult.error("爬取1688商品数据失败: " + e.getMessage());
} finally {
WebMagicProxyUtil.clearSystemProxy();
}
}
private Site createOptimizedSite() {
Site site = Site.me().setRetryTimes(2).setTimeOut(10000).setSleepTime(500 + random.nextInt(1000));
site.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36");
site.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
site.addHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
site.addHeader("Accept-Encoding", "gzip, deflate, br");
site.addHeader("Referer", "https://s.1688.com/");
site.addHeader("Connection", "keep-alive");
site.addHeader("Upgrade-Insecure-Requests", "1");
site.addHeader(" sec-ch-ua-platform", "\"Windows\"");
site.addHeader("cookie", "arms_uid=c609390d-9eec-4d96-8402-705e85eec143; taklid=60a276649e864914a72a034bf895d586; _bl_uid=C2m9kdwR3URv2h9XU1zLv7ep0wL0; tracknick=; trackId=8f5f1443ebaa40a5b878a0a8c847e248; x-hng=lang=zh-CN&domain=detail.1688.com; union={\"amug_biz\":\"oneself\",\"amug_fl_src\":\"awakeId_982\",\"creative_url\":\"https%3A%2F%2Fdetail.1688.com%2Foffer%2F650296798072.html%3Ffromkv%3DcbuPcPlugin%3AimageSearchDrawerCard%26spm%3Da2639h.29135425.offerlist.i0%26source%3Daction%2523offerItem%253Borigin%2523s.1688.com%26amug_biz%3Doneself%26amug_fl_src%3DawakeId_982\",\"creative_time\":1752544005175}; cookie2=13f737f8500b45917b248310f133fd85; t=3bce9026cffba4d4cb7f7707b055a37b; _tb_token_=e6b5e3e75b136; lid=tb242078004181; __last_loginid__=b2b-2215893137000702f0; __last_memberid__=b2b-2215893137000702f0; ali_apache_track=c_mid=b2b-2215893137000702f0|c_lid=tb242078004181|c_ms=1; token=; cookie1=U%2BbMNkTCG0pSsj5xAGEoX6SKc5SIUjqNO8PwGVfgBd0%3D; cookie17=UUpgQyFSrJGiDvwfPw%3D%3D; sgcookie=E100xeguhPGg55wTvssYju665JLiOpV0Uo%2Fg4P60bQVtc4M7xRIpk5LNActuqoYy5dtVMTm89tkdXDt7XgRS%2B%2BfE5j5bZ4iD4feBA8XC5P2OCjs%3D; sg=104; csg=3eac1f8c; unb=2215893137000; uc4=nk4=0%40FY4Mt4wfbfnv2ZVPeOSXt4Tr3gwwfHbyww%3D%3D&id4=0%40U2gqzJfuijLPEdau%2FJG20lxehlz4ZAg5; _nk_=tb242078004181; last_mid=b2b-2215893137000702f0; __cn_logon__=true; __cn_logon_id__=tb242078004181; cna=ip/8IIvOgXABASQOA3z+hOcs; plugin_home_downLoad_cookie=%E5%AE%89%E8%A3%85%E6%8F%92%E4%BB%B6; keywordsHistory=%E5%A5%B3%E6%AC%BE%E5%86%85%E8%A1%A3%E8%96%84%E6%AC%BE%3B%E8%96%84%E6%AC%BE%E5%86%85%E8%A1%A3%3B%E5%81%8F%E5%85%89%E5%A2%A8%E9%95%9C%3B%E5%BE%95%E8%8A%AC%E7%94%B5%E5%90%B9%E9%A3%8E%E6%9C%BA%3B%E5%A5%B3%E7%AB%A5%E8%A2%9C%E5%AD%90; isg=BC4udCqKymJkBz6IOd_5ftKDf4TwL_IpL98Yxlj3mjHsO86VwL9COdS686fX4-pB; mtop_partitioned_detect=1; _m_h5_tk=480ae7820698a739a9733b209cba6c5c_1753070964344; _m_h5_tk_enc=ea1ad31e4aceb42009dd55ecbc385446; xlly_s=1; _csrf_token=1753063846774; _user_vitals_session_data_={\"user_line_track\":true,\"ul_session_id\":\"stdvffkk9zs\",\"last_page_id\":\"detail.1688.com%2Fytcik8fiacs\"}; tfstk=grCjjLqRZmmbw5At5K4rOOFnlBO_zzPeMVTO-NhqWIdv5f_hfZRNWKH-yExPgn72MGM1AMTwDxd9wbL2SmKtMxp-y3sx_KSN1Tc1-Nf4oCzDiZAM6krUT1_coCfkyKFg4aU9SCTxf5rrBZAM6uur6JPCoib59jb9WUUW7FovXGKvy4Kk7jhOXnp-2eTH6CI9HUhJJe-tMCLvezTM2hd96Gd8PF-JXjqtRFwXmZaI0hq2r8KFk3Gt6_FMhH_xQfhONE9f6ZK5uZ5WlKtpBtQs6sIPWs5DE-kDsaWCfOIgLYAfWtdv8ZVjNBQAU1TP_Pk6Da5WIspxDxLBGn9dML3t9QJOF_OVMlDhzatvdQWzEoJwGi6HxKeuqG_W0GCXemZyb97PMLsL4bsMC9bw5iNjMMIzZX-Q9zHsPpc6PHz7PADMSdViVT7hrw9vrE9zPziAIKLkPHz7PmBMHUYX8zaSDOf..");
return site;
}
/**
* 一站式获取1688商品详情URL列表
*
* @param imageUrl 图片URL从七牛云上传后获取的URL
* @return 商品详情URL列表
*/
private List<String> get1688DetailUrls(String imageUrl) {
try {
String token = Alibaba1688CookieUtil.getToken(restTemplate);
long timestamp = System.currentTimeMillis();
String jsonData = "{\"appId\":\"32517\",\"params\":\"{\\\"categoryId\\\":-1,\\\"imageAddress\\\":\\\"" + imageUrl + "\\\",\\\"interfaceName\\\":\\\"imageOfferSearchService\\\",\\\"needYolocrop\\\":false,\\\"pageIndex\\\":\\\"1\\\",\\\"pageSize\\\":\\\"40\\\",\\\"searchScene\\\":\\\"image\\\",\\\"snAppAb\\\":true,\\\"appName\\\":\\\"ios\\\",\\\"scene\\\":\\\"seoSearch\\\"}\"}";
String sign = generateSign(token, String.valueOf(timestamp), jsonData);
String url = "https://h5api.m.1688.com/h5/mtop.relationrecommend.wirelessrecommend.recommend/2.0/" + "?jsv=2.6.1" + "&appKey=" + APP_KEY + "&t=" + timestamp + "&sign=" + sign + "&v=2.0" + "&type=originaljson" + "&isSec=0" + "&timeout=20000" + "&api=mtop.relationrecommend.WirelessRecommend.recommend" + "&ignoreLogin=true" + "&prefix=h5api" + "&dataType=jsonp";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.set("Cookie", Alibaba1688CookieUtil.getCookieString(restTemplate));
headers.set("authority", "h5api.m.1688.com");
headers.set("accept", "application/json");
headers.set("origin", "https://m.1688.com");
headers.set("referer", "https://m.1688.com/");
//这个乐天导入还是有bug,为什么
// headers.set(" sec-ch-ua-platform", " \"Windows\"");
headers.set("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1");
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("data", jsonData);
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
List<String> newCookies = response.getHeaders().get("Set-Cookie");
if (newCookies != null && !newCookies.isEmpty()) {
for (String cookie : newCookies) {
String[] parts = cookie.split(";")[0].split("=", 2);
if (parts.length == 2) {
Alibaba1688CookieUtil.setCookie(parts[0], parts[1]);
}
}
}
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> responseData = objectMapper.readValue(response.getBody(), Map.class);
List<String> detailUrls = new ArrayList<>();
Map<String, Object> data = (Map<String, Object>) responseData.get("data");
Map<String, Object> offerList = (Map<String, Object>) data.get("offerList");
List<Map<String, Object>> offers = (List<Map<String, Object>>) offerList.get("offers");
for (Map<String, Object> offer : offers) {
String detailUrl = (String) offer.get("detailUrl");
String baseUrl = detailUrl.split("\\?")[0];
SecureRandom secureRandom = new SecureRandom();
StringBuilder randomString = new StringBuilder(20);
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (int i = 0; i < 20; i++) {
randomString.append(chars.charAt(secureRandom.nextInt(chars.length())));
}
String formattedUrl = baseUrl.replace("detail.1688.com", "m.1688.com") + "?callByHgJs=1&__removesafearea__=1&src_cna=" + randomString.toString();
detailUrls.add(formattedUrl);
}
return detailUrls.stream().limit(10).collect(Collectors.toList());
} catch (Exception e) {
logger.error("通过1688 API获取商品详情链接失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
private String generateSign(String token, String timestamp, String data) {
try {
String signStr = token + "&" + timestamp + "&" + APP_KEY + "&" + data;
MessageDigest md = MessageDigest.getInstance("MD5");
return DatatypeConverter.printHexBinary(md.digest(signStr.getBytes("UTF-8"))).toLowerCase();
} catch (Exception e) {
logger.error("生成签名失败", e);
return "";
}
}
/**
* 串行爬取商品详情信息
*
* @param detailUrls 详情页URL列表
* @param site 爬虫配置
* @param productInfoList 商品信息列表
*/
private void scrapeProductDetailsSequential(List<String> detailUrls, Site site, List<ProductInfo> productInfoList) {
for (String detailUrl : detailUrls) {
try {
int initialSize = productInfoList.size();
scrapeProductDetail(detailUrl, site, productInfoList);
boolean hasValidPrice = false;
for (int i = initialSize; i < productInfoList.size(); i++) {
ProductInfo info = productInfoList.get(i);
if (info.getPrice() != null && !info.getPrice().trim().isEmpty()) {
hasValidPrice = true;
break;
}
}
if (!hasValidPrice) {
try {
int waitTime = 50000 + random.nextInt(30000);
Thread.sleep(waitTime);
logger.info("等待{}秒后开始重试", waitTime / 1000);
scrapeProductDetail(detailUrl, site, productInfoList);
} catch (Exception retryException) {
logger.error("重试爬取失败: {}", retryException.getMessage());
}
}
} catch (Exception e) {
logger.error("爬取商品详情失败: {}", e.getMessage());
}
}
}
private void scrapeProductDetail(String detailUrl, Site site, List<ProductInfo> productInfoList) {
try {
Spider.create(new PageProcessor() {
@Override
public void process(Page page) {
try {
System.out.println("正在爬取商品详情: " + page.getHtml());
String htmlContent = page.getRawText();
Pattern pricePattern = Pattern.compile("\"price\":\\s*\"([^\"]+)\"");
Matcher priceMatcher = pricePattern.matcher(htmlContent);
String price = null;
if (priceMatcher.find()) {
String fullPrice = priceMatcher.group(1);
price = fullPrice.split("-")[0].trim();
}
String weight = null;
Pattern weightPattern = Pattern.compile("\"weight\":(\\d+)");
Matcher weightMatcher = weightPattern.matcher(htmlContent);
if (weightMatcher.find()) {
weight = weightMatcher.group(1) + "g";
} else {
Pattern unitWeightPattern = Pattern.compile("\"unitWeight\":(\\d+(?:\\.\\d+)?)");
Matcher unitWeightMatcher = unitWeightPattern.matcher(htmlContent);
if (unitWeightMatcher.find()) {
double weightValue = Double.parseDouble(unitWeightMatcher.group(1));
if (weightValue > 0) {
weight = (int) (weightValue * 1000) + "g";
}
}
}
System.out.println("价格重量:" + price + "-" + weight);
Thread.sleep(WebMagicProxyUtil.getRandomSleepTime(1000, 3000));
productInfoList.add(new ProductInfo(price, weight));
} catch (Exception e) {
logger.error("解析商品详情页面失败: {}", e.getMessage());
}
}
@Override
public Site getSite() {
return site;
}
}).addUrl(detailUrl).setDownloader(WebMagicProxyUtil.getDefaultProxyDownloader()).thread(1).run();
} catch (Exception e) {
logger.error("爬取商品详情失败: {}", e.getMessage());
}
}
private AjaxResult buildResult(List<ProductInfo> productInfoList) {
// 分别计算价格和重量的中位数
List<Float> validPrices = new ArrayList<>();
List<Float> validWeights = new ArrayList<>();
for (ProductInfo info : productInfoList) {
try {
if (info.getPrice() != null) {
validPrices.add(Float.parseFloat(info.getPrice()));
}
} catch (NumberFormatException e) {
// 忽略无效价格
}
try {
if (info.getWeight() != null) {
String weightStr = info.getWeight().replace("g", "");
validWeights.add(Float.parseFloat(weightStr));
}
} catch (NumberFormatException e) {
// 忽略无效重量
}
}
Map<String, Object> result = new HashMap<>();
// 计算价格中位数
if (!validPrices.isEmpty()) {
Collections.sort(validPrices);
float medianPrice = validPrices.get(validPrices.size() / 2);
result.put("priceList", Collections.singletonList(String.valueOf(medianPrice)));
} else {
result.put("priceList", Collections.emptyList());
}
// 计算重量中位数
if (!validWeights.isEmpty()) {
Collections.sort(validWeights);
float medianWeight = validWeights.get(validWeights.size() / 2);
result.put("weightList", Collections.singletonList(medianWeight + "g"));
} else {
result.put("weightList", Collections.emptyList());
}
return AjaxResult.success(result);
}
private static class ProductInfo {
private final String price;
private final String weight;
public ProductInfo(String price, String weight) {
this.price = price;
this.weight = weight;
}
public String getPrice() {
return price;
}
public String getWeight() {
return weight;
}
}
@GetMapping("/scrapeProducts")
public AjaxResult scrapeProducts(@RequestParam String shopName) {
try {
String url = "https://ranking.rakuten.co.jp/search?stx=" + URLEncoder.encode(shopName, StandardCharsets.UTF_8.toString());
List<Map<String, String>> products = new ArrayList<>();
Site site = Site.me().setRetryTimes(3).setTimeOut(10000).setSleepTime(1000 + random.nextInt(2000));
site.addHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36");
site.addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
site.addHeader("accept-encoding", "gzip, deflate, br, zstd");
site.addHeader("accept-language", "zh-CN,zh;q=0.9");
site.addHeader("cache-control", "max-age=0");
site.addHeader("priority", "u=0, i");
site.addHeader("sec-ch-ua", "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"");
site.addHeader("sec-ch-ua-mobile", "?0");
site.addHeader("sec-ch-ua-platform", "\"Windows\"");
site.addHeader("sec-fetch-dest", "document");
site.addHeader("sec-fetch-mode", "navigate");
site.addHeader("sec-fetch-site", "same-origin");
site.addHeader("sec-fetch-user", "?1");
site.addHeader("upgrade-insecure-requests", "1");
site.addHeader("cookie", "_ra=1750472997398|0ff0eb32-5d9f-4c27-a7ca-c9ff1149e90b; Rp=779873a7b6c0e87edcf6f39cf2368561928309c8; rcx=136377ca-334f-4305-b45e-ad43e9538d2e; rcxGlobal=136377ca-334f-4305-b45e-ad43e9538d2e;");
Spider.create(new PageProcessor() {
@Override
public void process(Page page) {
try {
List<String> rankings = page.getHtml().xpath("//div[@class='srhRnk']/span[@class='icon']/text()").all();
List<String> productUrls = page.getHtml().xpath("//div[@class='srhPic']//div[@class='rnkRanking_bigImageBox']/a/@href").all();
List<String> imageUrls = page.getHtml().xpath("//div[@class='srhPic']//div[@class='rnkRanking_bigImageBox']/a/img/@src").all();
List<String> titles = page.getHtml().xpath("//div[@class='srhItm']/a/text()").all();
List<String> prices = page.getHtml().xpath("//div[@class='srhPri']/text()").all();
int count = Math.min(productUrls.size(), Math.min(imageUrls.size(), Math.min(titles.size(), Math.min(prices.size(), rankings.size()))));
for (int i = 0; i < count; i++) {
Map<String, String> product = new HashMap<>();
product.put("ranking", rankings.get(i).trim());
String productUrl = productUrls.get(i);
product.put("productUrl", productUrl);
String[] parts = productUrl.split("/");
if (parts.length > 3) {
product.put("shopName", parts[3]);
}
product.put("imageUrl", imageUrls.get(i));
product.put("productTitle", titles.get(i).trim());
String price = prices.get(i).replaceAll("[^0-9]", "").trim();
if (!price.isEmpty()) {
product.put("price", price);
}
product.put("imageId", null);
product.put("image1688Url", null);
products.add(product);
}
} catch (Exception e) {
logger.error("解析页面时发生错误: {}", e.getMessage());
}
}
@Override
public Site getSite() {
return site;
}
}).addUrl(url).setDownloader(WebMagicProxyUtil.getDefaultProxyDownloader()).thread(1).run();
return AjaxResult.success(products);
} catch (Exception e) {
logger.error("抓取商品数据失败: {}", e.getMessage());
return AjaxResult.error("抓取商品数据失败");
} finally {
WebMagicProxyUtil.clearSystemProxy();
}
}
}

View File

@@ -0,0 +1,179 @@
package com.ruoyi.web.controller.tool;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.PreDestroy;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.pipeline.Pipeline;
import us.codecraft.webmagic.ResultItems;
/**
* 佐川急便物流查询控制器
*/
@Api("佐川急便物流查询接口")
@RestController
@RequestMapping("/tool/sagawa")
@Anonymous
public class SagawaExpressController extends BaseController implements PageProcessor {
private static final Logger logger = LoggerFactory.getLogger(SagawaExpressController.class);
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
private final Site site = Site.me()
.setRetryTimes(2)
.setSleepTime(1000)
.setTimeOut(10000)
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
/**
* 关闭线程池
*/
@PreDestroy
public void destroy() {
executorService.shutdownNow();
}
@Override
public void process(Page page) {
try {
String pageContent = page.getHtml().toString();
Map<String, Object> result = new HashMap<>();
if (pageContent.contains("お荷物データが登録されておりません")) {
result.put("status", "notFound");
result.put("message", "没有找到对应的包裹信息");
page.putField("result", result);
return;
}
// 提取物流表格
String trackingTableRegex = "<table[^>]*class=\"table_basic table_okurijo_detail2\"[^>]*>\\s*<tbody>\\s*(?:<tr>.*?<th[^>]*>荷物状況</th>.*?</tr>.*?<tr>.*?</tr>.*?)+\\s*</tbody>\\s*</table>";
Pattern pattern = Pattern.compile(trackingTableRegex, Pattern.DOTALL);
Matcher matcher = pattern.matcher(pageContent);
if (matcher.find()) {
String trackingTable = matcher.group(0);
String rowRegex = "<tr>\\s*<td>\\s*([^<]*?)\\s*</td>\\s*<td>\\s*([^<]*?)\\s*</td>\\s*<td>\\s*([^<]*?)\\s*</td>\\s*</tr>";
Pattern rowPattern = Pattern.compile(rowRegex, Pattern.DOTALL);
Matcher rowMatcher = rowPattern.matcher(trackingTable);
String status = "";
String dateTime = "";
String office = "";
while (rowMatcher.find()) {
status = rowMatcher.group(1).trim();
dateTime = rowMatcher.group(2).trim();
office = rowMatcher.group(3).trim();
}
if (!status.isEmpty()) {
Map<String, String> trackInfo = new HashMap<>();
trackInfo.put("status", status);
trackInfo.put("dateTime", dateTime);
trackInfo.put("office", office);
result.put("status", "success");
result.put("trackInfo", trackInfo);
} else {
result.put("status", "noRecords");
result.put("message", "没有物流记录");
}
} else {
result.put("status", "noTable");
result.put("message", "未找到物流跟踪表格");
}
page.putField("result", result);
} catch (Exception e) {
logger.error("解析页面失败", e);
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
result.put("message", "解析页面失败: " + e.getMessage());
page.putField("result", result);
}
}
@Override
public Site getSite() {
return site;
}
/**
* 构建佐川急便查询URL
*/
private String buildSagawaUrl(String trackingNumber) {
return "https://k2k.sagawa-exp.co.jp/p/web/okurijosearch.do?okurijoNo=" + trackingNumber.trim();
}
/**
* 自定义结果收集Pipeline
*/
private static class ResultCollectorPipeline implements Pipeline {
private Map<String, Object> result = null;
@Override
public void process(ResultItems resultItems, us.codecraft.webmagic.Task task) {
this.result = resultItems.get("result");
}
public Map<String, Object> getResult() {
return result;
}
}
/**
* 查询佐川急便物流信息 - API入口
*/
@ApiOperation("查询佐川急便物流信息")
@GetMapping("/tracking/{trackingNumber}")
public R<Map<String, Object>> getTrackingInfo(@PathVariable("trackingNumber") String trackingNumber) {
try {
if (trackingNumber == null || trackingNumber.trim().isEmpty()) {
return R.fail("运单号不能为空");
}
String url = buildSagawaUrl(trackingNumber);
ResultCollectorPipeline pipeline = new ResultCollectorPipeline();
Spider.create(this)
.addUrl(url)
.addPipeline(pipeline)
.thread(executorService, 1)
.run();
Map<String, Object> result = pipeline.getResult();
// 如果没有获取到结果,设置默认值
if (result == null) {
Map<String, String> defaultTrackInfo = new HashMap<>();
defaultTrackInfo.put("status", "处理中");
defaultTrackInfo.put("dateTime", "");
defaultTrackInfo.put("office", "");
result = new HashMap<>();
result.put("status", "success");
result.put("trackInfo", defaultTrackInfo);
}
return R.ok(result);
} catch (Exception e) {
logger.error("查询物流信息失败", e);
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("status", "error");
errorResult.put("message", "查询物流信息失败: " + e.getMessage());
return R.ok(errorResult);
}
}
}

View File

@@ -0,0 +1,594 @@
package com.ruoyi.web.controller.tool;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.PreDestroy;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.http.ResponseEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.web.util.WebMagicProxyUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Html;
import us.codecraft.webmagic.downloader.HttpClientDownloader;
import us.codecraft.webmagic.proxy.Proxy;
import us.codecraft.webmagic.proxy.SimpleProxyProvider;
import us.codecraft.webmagic.scheduler.QueueScheduler;
import java.util.Objects;
import java.util.concurrent.TimeoutException;
/**
* 亚马逊爬虫控制器 - 爬取价格和卖家信息
* 性能优化版本
*
* @author ruoyi
*/
@Api("亚马逊爬虫功能")
@RestController
@RequestMapping("/tool/webmagic")
@Anonymous
public class WebMagicController extends BaseController implements PageProcessor {
private static final Logger logger = LoggerFactory.getLogger(WebMagicController.class);
private final Random random = new Random();
private static volatile Spider activeSpider = null;
private static final Object spiderLock = new Object();
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
private final AtomicInteger activeTasks = new AtomicInteger(0);
private final int MAX_CONCURRENT_TASKS = 5;
private final Map<String, Map<String, Object>> resultCache = new ConcurrentHashMap<>();
private final Site site = Site.me()
.setRetryTimes(3)
.setSleepTime(1000 + random.nextInt(2000))
.setTimeOut(15000)
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.addCookie("session-id", "358-1261309-0483141")
.addCookie("session-id-time", "2082787201l")
.addCookie("i18n-prefs", "JPY")
.addCookie("lc-acbjp", "zh_CN")
.addCookie("ubid-acbjp", "357-8224002-9668932");
/**
* 关闭线程池
*/
@PreDestroy
public void destroy() {
try {
logger.info("正在关闭爬虫线程池...");
executorService.shutdown();
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
@Override
public void process(Page page) {
try {
Html html = page.getHtml();
String priceSymbol = html.xpath("//span[@class='a-price-symbol']/text()").toString();
String priceWhole = html.xpath("//span[@class='a-price-whole']/text()").toString();
Map<String, Object> resultMap = new HashMap<>();
if (priceSymbol != null && !priceSymbol.isEmpty() && priceWhole != null && !priceWhole.isEmpty()) {
resultMap.put("price", priceSymbol + priceWhole);
}
// 提取卖家信息
resultMap.put("seller", html.xpath("//a[@id='sellerProfileTriggerId']/text()").toString());
String asin = html.xpath("//input[@id='ASIN']/@value").toString();
resultMap.put("asin", asin);
String price = (String) resultMap.get("price");
String seller = (String) resultMap.get("seller");
Object retriesObj = page.getRequest().getExtra("retries");
int retries = retriesObj == null ? 0 : (int) retriesObj;
if ((price == null || price.isEmpty() || seller == null || seller.isEmpty()) && retries < 3) {
String url = page.getUrl().toString();
us.codecraft.webmagic.Request request = new us.codecraft.webmagic.Request(url);
request.putExtra("retries", retries + 1);
page.addTargetRequest(request);
int backoffTime = (int) Math.pow(2, retries) * 1000 + random.nextInt(1000);
logger.info("数据不完整,准备进行第{}次重试ASIN: {}, 等待: {}ms", retries + 1, asin, backoffTime);
} else {
if (asin != null && !asin.isEmpty()) {
resultCache.put(asin, resultMap);
}
}
page.putField("resultMap", resultMap);
} catch (Exception e) {
logger.error("解析页面失败", e);
}
}
@Override
public Site getSite() {
return site;
}
/**
* 构建亚马逊产品URL
*/
private String buildAmazonUrl(String asin) {
asin = asin.replaceAll("[^a-zA-Z0-9]", "");
return "https://www.amazon.co.jp/dp/" + asin;
}
/**
* 获取所有可用代理节点
*/
@ApiOperation("获取所有可用代理节点")
@GetMapping("/proxies")
public R<List<Map<String, String>>> getProxies() {
return R.ok(WebMagicProxyUtil.getAllProxies());
}
/**
* 设置当前使用的代理节点
*/
@ApiOperation("设置当前使用的代理节点")
@PostMapping("/proxy/set")
public R<Map<String, String>> setCurrentProxy(@RequestParam String proxyName) {
boolean success = WebMagicProxyUtil.setCurrentProxy(proxyName);
if (success) {
List<Map<String, String>> proxies = WebMagicProxyUtil.getAllProxies();
Map<String, String> currentProxy = proxies.stream()
.filter(p -> p.get("name").equals(proxyName))
.findFirst()
.orElse(new HashMap<>());
return R.ok(currentProxy);
} else {
return R.fail("未找到指定的代理节点: " + proxyName);
}
}
/**
* 获取 Clash 运行状态
*/
@ApiOperation("获取Clash运行状态")
@GetMapping("/clash/status")
public R<Map<String, Object>> getClashStatus() {
Map<String, Object> result = new HashMap<>();
boolean running = WebMagicProxyUtil.isProxyRunning();
result.put("running", running);
result.put("status", running ? "运行中" : "已停止");
return R.ok(result);
}
/**
* 手动控制 Clash 启动
*/
@ApiOperation("启动Clash")
@PostMapping("/clash/start")
public R<Map<String, Object>> startClashManually() {
Map<String, Object> result = new HashMap<>();
boolean success = WebMagicProxyUtil.startProxy();
result.put("success", success);
result.put("message", success ? "Clash 启动成功" : "Clash 启动失败");
try {
// 添加延迟确保Clash完全启动
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.warn("Clash启动等待被中断", e);
}
return R.ok(result);
}
/**
* 手动控制 Clash 停止
*/
@ApiOperation("停止Clash")
@PostMapping("/clash/stop")
public R<Map<String, Object>> stopClashManually() {
Map<String, Object> result = new HashMap<>();
boolean success = WebMagicProxyUtil.stopProxy();
result.put("success", success);
result.put("message", success ? "Clash 停止成功" : "Clash 停止失败");
return R.ok(result);
}
/**
* 批量爬取亚马逊产品信息 - 优化版本
*/
@ApiOperation("批量爬取亚马逊产品信息")
@PostMapping("/batch")
public DeferredResult<ResponseEntity<R<List<Map<String, Object>>>>> batchGetAmazonProductInfo(@RequestBody List<String> asinList) {
DeferredResult<ResponseEntity<R<List<Map<String, Object>>>>> deferredResult = new DeferredResult<>(500000L);
// 清理输入数据
List<String> cleanedAsinList = asinList.stream()
.filter(asin -> asin != null && !asin.trim().isEmpty())
.map(asin -> asin.trim().replaceAll("[^a-zA-Z0-9]", ""))
.distinct() // 去重
.collect(Collectors.toList());
if (cleanedAsinList.isEmpty()) {
deferredResult.setResult(ResponseEntity.ok(R.ok(new ArrayList<>())));
return deferredResult;
}
CompletableFuture.runAsync(() -> {
try {
WebMagicProxyUtil.startProxy();
logger.info("开始批量爬取 {} 个ASIN", cleanedAsinList.size());
List<CompletableFuture<Map<String, Object>>> futures = new ArrayList<>();
Map<String, Map<String, Object>> results = new ConcurrentHashMap<>();
int batchSize = 3;
for (int i = 0; i < cleanedAsinList.size(); i += batchSize) {
final int startIndex = i;
final int endIndex = Math.min(i + batchSize, cleanedAsinList.size());
List<String> batch = cleanedAsinList.subList(startIndex, endIndex);
Thread.sleep(500 + random.nextInt(1500));
CompletableFuture<Void> batchFuture = CompletableFuture.runAsync(() -> {
try {
processBatch(batch, results);
} catch (Exception e) {
logger.error("处理批次 {}~{} 失败: {}", startIndex, endIndex - 1, e.getMessage());
}
}, executorService);
// 添加延迟,避免同时发送太多请求
Thread.sleep(2000 + random.nextInt(3000));
}
Thread.sleep(cleanedAsinList.size() * 1000);
List<Map<String, Object>> resultList = new ArrayList<>();
for (String asin : cleanedAsinList) {
Map<String, Object> result = resultCache.getOrDefault(asin, new HashMap<>());
if (result.isEmpty() || !result.containsKey("price") || !result.containsKey("seller")) {
Map<String, Object> defaultResult = new HashMap<>();
defaultResult.put("asin", asin);
defaultResult.put("price", "未获取");
defaultResult.put("seller", "未获取");
resultList.add(defaultResult);
} else {
resultList.add(result);
}
}
resultCache.clear();
retryFailedItems(resultList).thenAccept(finalResults -> {
try {
WebMagicProxyUtil.stopProxy();
deferredResult.setResult(ResponseEntity.ok(R.ok(finalResults)));
logger.info("批量爬取完成,成功获取 {} 个产品信息", finalResults.size());
} catch (Exception e) {
logger.error("处理最终结果失败", e);
deferredResult.setResult(ResponseEntity.ok(R.fail("处理最终结果失败: " + e.getMessage())));
}
});
} catch (Exception e) {
logger.error("批量爬取失败: {}", e.getMessage());
WebMagicProxyUtil.stopProxy();
deferredResult.setResult(ResponseEntity.ok(R.fail("批量爬取失败: " + e.getMessage())));
}
}, executorService);
return deferredResult;
}
/**
* 处理一个批次的ASIN
*/
private void processBatch(List<String> asinBatch, Map<String, Map<String, Object>> results) {
for (String asin : asinBatch) {
try {
String url = buildAmazonUrl(asin);
synchronized (spiderLock) {
if (activeTasks.get() >= MAX_CONCURRENT_TASKS) {
Thread.sleep(1000);
}
activeTasks.incrementAndGet();
}
HttpClientDownloader downloader = WebMagicProxyUtil.getProxyDownloader();
Spider spider = Spider.create(this)
.addUrl(url)
.setDownloader(downloader)
.thread(1);
try {
synchronized (spiderLock) {
activeSpider = spider;
}
spider.run();
Thread.sleep(1000 + random.nextInt(2000));
} finally {
WebMagicProxyUtil.clearSystemProxy();
synchronized (spiderLock) {
if (activeSpider == spider) {
activeSpider = null;
}
}
activeTasks.decrementAndGet();
}
} catch (Exception e) {
logger.error("处理ASIN: {} 失败: {}", asin, e.getMessage());
}
}
}
/**
* 重试失败的项目
*/
private CompletableFuture<List<Map<String, Object>>> retryFailedItems(List<Map<String, Object>> results) {
return CompletableFuture.supplyAsync(() -> {
try {
List<String> failedAsins = results.stream()
.filter(item -> "未获取".equals(item.get("seller")) || "未获取".equals(item.get("price")))
.map(item -> (String) item.get("asin"))
.collect(Collectors.toList());
if (failedAsins.isEmpty()) {
return results;
}
logger.info("开始重试 {} 个失败的ASIN", failedAsins.size());
Map<String, Map<String, Object>> retryResults = new ConcurrentHashMap<>();
for (String asin : failedAsins) {
try {
String url = buildAmazonUrl(asin);
HttpClientDownloader downloader = WebMagicProxyUtil.getProxyDownloader();
Spider spider = Spider.create(this)
.addUrl(url)
.setDownloader(downloader)
.thread(1);
synchronized (spiderLock) {
activeSpider = spider;
}
spider.run();
Thread.sleep(2000 + random.nextInt(3000));
Map<String, Object> result = resultCache.get(asin);
if (result != null && result.get("seller") != null && result.get("price") != null) {
for (Map<String, Object> item : results) {
if (asin.equals(item.get("asin"))) {
item.put("seller", result.get("seller"));
item.put("price", result.get("price"));
break;
}
}
logger.info("重试成功: ASIN={}", asin);
} else {
logger.warn("重试失败: ASIN={}", asin);
}
} catch (Exception e) {
logger.error("重试ASIN: {} 失败: {}", asin, e.getMessage());
} finally {
WebMagicProxyUtil.clearSystemProxy();
synchronized (spiderLock) {
activeSpider = null;
}
}
Thread.sleep(3000 + random.nextInt(2000));
}
return results;
} catch (Exception e) {
logger.error("重试失败项目时出错: {}", e.getMessage());
return results;
}
}, executorService);
}
/**
* 测试代理节点延迟
*/
@ApiOperation("测试代理节点延迟")
@PostMapping("/proxy/test")
public DeferredResult<ResponseEntity<R<List<Map<String, Object>>>>> testProxyDelay(@RequestBody List<String> proxyNames) {
DeferredResult<ResponseEntity<R<List<Map<String, Object>>>>> deferredResult = new DeferredResult<>(600000L);
// 测试前启动 Clash
boolean clashStarted = WebMagicProxyUtil.startProxy();
if (!clashStarted) {
logger.warn("启动 Clash 失败,将尝试继续测试...");
}
CompletableFuture.runAsync(() -> {
List<Map<String, Object>> resultList = new ArrayList<>();
try {
int batchSize = 5; // 一次测试5个代理
for (int i = 0; i < proxyNames.size(); i += batchSize) {
// 获取当前批次
final int startIndex = i;
final int endIndex = Math.min(i + batchSize, proxyNames.size());
List<String> batch = proxyNames.subList(startIndex, endIndex);
// 并行测试当前批次的代理
List<CompletableFuture<Map<String, Object>>> futures = batch.stream()
.map(proxyName -> CompletableFuture.supplyAsync(
() -> testSingleProxy(proxyName), executorService))
.collect(Collectors.toList());
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.get(5, TimeUnit.SECONDS);
} catch (TimeoutException te) {
}
// 收集结果
for (CompletableFuture<Map<String, Object>> future : futures) {
try {
if (future.isDone()) {
resultList.add(future.join());
} else {
future.cancel(true);
Map<String, Object> timeoutResult = new HashMap<>();
timeoutResult.put("name", "unknown");
timeoutResult.put("status", "fail");
timeoutResult.put("message", "测试超时");
timeoutResult.put("delay", -1);
resultList.add(timeoutResult);
}
} catch (Exception e) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("name", "error");
errorResult.put("status", "fail");
errorResult.put("message", "测试异常: " + e.getMessage());
errorResult.put("delay", -1);
resultList.add(errorResult);
}
}
// 批次之间增加延迟
Thread.sleep(1000);
}
deferredResult.setResult(ResponseEntity.ok(R.ok(resultList)));
} catch (Exception e) {
logger.error("测试代理延迟失败", e);
deferredResult.setResult(ResponseEntity.ok(R.fail("测试代理延迟失败")));
} finally {
boolean clashStopped = WebMagicProxyUtil.stopProxy();
if (!clashStopped) {
logger.warn("停止 Clash 失败,请手动检查 Clash 状态");
}
}
}, executorService);
return deferredResult;
}
/**
* 测试单个代理节点延迟
*/
private Map<String, Object> testSingleProxy(String proxyName) {
Map<String, Object> result = new HashMap<>();
result.put("name", proxyName);
long startTime = System.currentTimeMillis();
try {
// 查找匹配的代理节点
List<Map<String, String>> proxyNodes = WebMagicProxyUtil.getAllProxies();
Map<String, String> targetProxy = proxyNodes.stream()
.filter(node -> proxyName.equals(node.get("name")))
.findFirst()
.orElse(null);
if (targetProxy == null) {
result.put("status", "fail");
result.put("message", "未找到指定的代理节点");
result.put("delay", -1);
return result;
}
String proxyHost = targetProxy.get("server");
int proxyPort = Integer.parseInt(targetProxy.get("port"));
HttpClientDownloader testDownloader = new HttpClientDownloader();
WebMagicProxyUtil.clearSystemProxy();
testDownloader.setProxyProvider(SimpleProxyProvider.from(new Proxy(proxyHost, proxyPort)));
Spider.create(new PageProcessor() {
@Override
public void process(Page page) {
// 只是测试连接,不做实际处理
}
@Override
public Site getSite() {
return Site.me()
.setRetryTimes(0)
.setSleepTime(100)
.setTimeOut(3000);
}
})
.setDownloader(testDownloader)
.addUrl("http://www.gstatic.com/generate_204")
.thread(1)
.run();
long endTime = System.currentTimeMillis();
result.put("status", "success");
result.put("delay", endTime - startTime);
} catch (Exception e) {
result.put("status", "fail");
result.put("message", "连接超时或失败");
result.put("delay", -1);
} finally {
WebMagicProxyUtil.clearSystemProxy();
}
return result;
}
/**
* 停止所有爬虫活动
*/
@ApiOperation("停止所有爬虫活动")
@PostMapping("/stop-crawling")
public R<Map<String, Object>> stopAllCrawling() {
Map<String, Object> result = new HashMap<>();
boolean spiderStopped = false;
synchronized (spiderLock) {
if (activeSpider != null) {
try {
activeSpider.stop();
activeSpider = null;
spiderStopped = true;
logger.info("爬虫任务已终止");
} catch (Exception e) {
logger.error("停止爬虫时出错", e);
}
}
}
boolean clashStopped = WebMagicProxyUtil.stopProxy();
result.put("success", spiderStopped || clashStopped);
result.put("message", "爬虫和Clash已停止");
return R.ok(result);
}
}

View File

@@ -0,0 +1,125 @@
package com.ruoyi.web.core.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.ruoyi.common.config.RuoYiConfig;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
/**
* Swagger2的接口配置
*
* @author ruoyi
*/
@Configuration
public class SwaggerConfig
{
/** 系统基础配置 */
@Autowired
private RuoYiConfig ruoyiConfig;
/** 是否开启swagger */
@Value("${swagger.enabled}")
private boolean enabled;
/** 设置请求的统一前缀 */
@Value("${swagger.pathMapping}")
private String pathMapping;
/**
* 创建API
*/
@Bean
public Docket createRestApi()
{
return new Docket(DocumentationType.OAS_30)
// 是否启用Swagger
.enable(enabled)
// 用来创建该API的基本信息展示在文档的页面中自定义展示的信息
.apiInfo(apiInfo())
// 设置哪些接口暴露给Swagger展示
.select()
// 扫描所有有注解的api用这种方式更灵活
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
// 扫描指定包中的swagger注解
// .apis(RequestHandlerSelectors.basePackage("com.ruoyi.project.tool.swagger"))
// 扫描所有 .apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
/* 设置安全模式swagger可以设置访问token */
.securitySchemes(securitySchemes())
.securityContexts(securityContexts())
.pathMapping(pathMapping);
}
/**
* 安全模式这里指定token通过Authorization头请求头传递
*/
private List<SecurityScheme> securitySchemes()
{
List<SecurityScheme> apiKeyList = new ArrayList<SecurityScheme>();
apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue()));
return apiKeyList;
}
/**
* 安全上下文
*/
private List<SecurityContext> securityContexts()
{
List<SecurityContext> securityContexts = new ArrayList<>();
securityContexts.add(
SecurityContext.builder()
.securityReferences(defaultAuth())
.operationSelector(o -> o.requestMappingPattern().matches("/.*"))
.build());
return securityContexts;
}
/**
* 默认的安全上引用
*/
private List<SecurityReference> defaultAuth()
{
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
List<SecurityReference> securityReferences = new ArrayList<>();
securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
return securityReferences;
}
/**
* 添加摘要信息
*/
private ApiInfo apiInfo()
{
// 用ApiInfoBuilder进行定制
return new ApiInfoBuilder()
// 设置标题
.title("标题若依管理系统_接口文档")
// 描述
.description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...")
// 作者信息
.contact(new Contact(ruoyiConfig.getName(), null, null))
// 版本
.version("版本号:" + ruoyiConfig.getVersion())
.build();
}
}

View File

@@ -0,0 +1,120 @@
package com.ruoyi.web.security;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@Component
public class JwtRsaKeyService {
private RSAPrivateKey privateKey;
private RSAPublicKey publicKey;
private String keyId;
@PostConstruct
public void init() {
try {
if (!loadKeysFromFiles()) {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
this.privateKey = (RSAPrivateKey) keyPair.getPrivate();
this.publicKey = (RSAPublicKey) keyPair.getPublic();
saveKeysToFiles();
}
this.keyId = computeKeyId(this.publicKey);
} catch (Exception e) {
throw new IllegalStateException("Failed to initialize RSA key pair", e);
}
}
public RSAPrivateKey getPrivateKey() {
return privateKey;
}
public RSAPublicKey getPublicKey() {
return publicKey;
}
public String getKeyId() {
return keyId;
}
public String getJwksJson() {
String n = base64Url(publicKey.getModulus());
String e = base64Url(publicKey.getPublicExponent());
return "{\"keys\":[{\"kty\":\"RSA\",\"use\":\"sig\",\"kid\":\"" + keyId + "\",\"alg\":\"RS256\",\"n\":\"" + n + "\",\"e\":\"" + e + "\"}]}";
}
private boolean loadKeysFromFiles() {
try {
Path dir = Paths.get("data");
Path priv = dir.resolve("jwt_rsa_private.pem");
Path pub = dir.resolve("jwt_rsa_public.pem");
if (!Files.exists(priv) || !Files.exists(pub)) return false;
String privPem = new String(Files.readAllBytes(priv), StandardCharsets.UTF_8);
String pubPem = new String(Files.readAllBytes(pub), StandardCharsets.UTF_8);
byte[] privDer = Base64.getMimeDecoder().decode(privPem.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replaceAll("\r?\n", ""));
byte[] pubDer = Base64.getMimeDecoder().decode(pubPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replaceAll("\r?\n", ""));
KeyFactory kf = KeyFactory.getInstance("RSA");
this.privateKey = (RSAPrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(privDer));
this.publicKey = (RSAPublicKey) kf.generatePublic(new X509EncodedKeySpec(pubDer));
return true;
} catch (Exception e) {
return false;
}
}
private void saveKeysToFiles() {
try {
Path dir = Paths.get("data");
if (!Files.exists(dir)) Files.createDirectories(dir);
Path priv = dir.resolve("jwt_rsa_private.pem");
Path pub = dir.resolve("jwt_rsa_public.pem");
String privPem = "-----BEGIN PRIVATE KEY-----\n" + Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(privateKey.getEncoded()) + "\n-----END PRIVATE KEY-----\n";
String pubPem = "-----BEGIN PUBLIC KEY-----\n" + Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(publicKey.getEncoded()) + "\n-----END PUBLIC KEY-----\n";
Files.write(priv, privPem.getBytes(StandardCharsets.UTF_8));
Files.write(pub, pubPem.getBytes(StandardCharsets.UTF_8));
} catch (Exception ignored) {}
}
private static String computeKeyId(RSAPublicKey key) {
try {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
sha256.update(key.getModulus().toByteArray());
sha256.update(key.getPublicExponent().toByteArray());
byte[] digest = sha256.digest();
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest).substring(0, 16);
} catch (Exception e) {
return "rsa-key";
}
}
private static String base64Url(BigInteger value) {
byte[] bytes = toUnsignedBytes(value);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
private static byte[] toUnsignedBytes(BigInteger value) {
byte[] bytes = value.toByteArray();
if (bytes.length > 1 && bytes[0] == 0) {
byte[] tmp = new byte[bytes.length - 1];
System.arraycopy(bytes, 1, tmp, 0, tmp.length);
return tmp;
}
return bytes;
}
}

View File

@@ -0,0 +1,47 @@
package com.ruoyi.web.service;
import java.util.List;
import com.ruoyi.system.domain.ClientAccount;
/**
* 客户端账号Service接口
*
* @author ruoyi
*/
public interface IClientAccountService
{
/**
* 查询客户端账号
*/
public ClientAccount selectClientAccountById(Long id);
/**
* 通过用户名查询客户端账号
*/
public ClientAccount selectClientAccountByUsername(String username);
/**
* 查询客户端账号列表
*/
public List<ClientAccount> selectClientAccountList(ClientAccount clientAccount);
/**
* 新增客户端账号
*/
public int insertClientAccount(ClientAccount clientAccount);
/**
* 修改客户端账号
*/
public int updateClientAccount(ClientAccount clientAccount);
/**
* 批量删除客户端账号
*/
public int deleteClientAccountByIds(Long[] ids);
/**
* 删除客户端账号信息
*/
public int deleteClientAccountById(Long id);
}

View File

@@ -0,0 +1,159 @@
package com.ruoyi.web.service;
import java.util.List;
import java.util.Map;
import com.ruoyi.system.domain.ClientInfo;
import com.ruoyi.system.domain.ClientErrorReport;
import com.ruoyi.system.domain.ClientDataReport;
import com.ruoyi.system.domain.ClientEventLog;
/**
* 客户端监控服务接口
*
* @author ruoyi
*/
public interface IClientMonitorService {
/**
* 查询客户端信息列表
*/
public List<ClientInfo> selectClientInfoList(ClientInfo clientInfo);
/**
* 查询客户端错误报告列表
*/
public List<Map<String, Object>> selectClientErrorList(ClientErrorReport clientErrorReport);
/**
* 查询客户端事件日志列表
*/
public List<ClientEventLog> selectClientEventLogList(ClientEventLog clientEventLog);
/**
* 查询客户端数据采集报告列表
*/
public List<ClientDataReport> selectClientDataReportList(ClientDataReport clientDataReport);
/**
* 查询在线客户端数量
*/
public int selectOnlineClientCount();
/**
* 查询客户端总数
*/
public int selectTotalClientCount();
/**
* 新增客户端信息
*/
public int insertClientInfo(ClientInfo clientInfo);
/**
* 新增客户端错误报告
*/
public int insertClientError(ClientErrorReport clientErrorReport);
/**
* 新增客户端事件日志
*/
public int insertClientEventLog(ClientEventLog clientEventLog);
/**
* 新增客户端数据采集报告
*/
public int insertDataReport(ClientDataReport clientDataReport);
/**
* 获取客户端统计数据
*/
public Map<String, Object> getClientStatistics();
/**
* 获取客户端活跃趋势
*/
public Map<String, Object> getClientActiveTrend();
/**
* 获取数据采集类型分布
*/
public Map<String, Object> getDataTypeDistribution();
/**
* 获取近7天在线客户端趋势
*/
public Map<String, Object> getOnlineClientTrend();
/**
* 客户端认证
*/
public Map<String, Object> authenticateClient(String authKey, Map<String, Object> clientInfo);
/**
* 记录客户端心跳
*/
public void recordHeartbeat(Map<String, Object> heartbeatData);
/**
* 记录客户端错误
*/
public void recordErrorReport(Map<String, Object> errorData);
/**
* 记录客户端数据采集报告
*/
public void recordDataReport(Map<String, Object> dataReport);
/**
* 获取客户端详细信息
*/
public Map<String, Object> getClientDetail(String clientId);
/**
* 获取客户端版本分布
*/
public List<Map<String, Object>> getVersionDistribution();
/**
* 记录1688风控监控数据
*/
public void recordAlibaba1688MonitorData(Map<String, Object> monitorData);
/**
* 查询1688风控监控数据列表
*/
public List<Map<String, Object>> selectAlibaba1688MonitorList(Map<String, Object> params);
/**
* 获取1688风控数据统计
*/
public Map<String, Object> getAlibaba1688Statistics();
/**
* 获取客户端日志内容
*/
public Map<String, Object> getClientLogs(String clientId);
/**
* 下载客户端日志文件
*/
public void downloadClientLogs(String clientId, javax.servlet.http.HttpServletResponse response);
/**
* 保存客户端日志
*/
public void saveClientLogs(Map<String, Object> logData);
/**
* 批量保存客户端日志
*
* @param clientId 客户端ID
* @param logEntries 日志条目列表
*/
public void saveBatchClientLogs(String clientId, List<String> logEntries);
/**
* 清理过期数据
*/
public void cleanExpiredData();
}

View File

@@ -0,0 +1,83 @@
package com.ruoyi.web.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.ClientAccount;
import com.ruoyi.system.mapper.ClientAccountMapper;
import com.ruoyi.web.service.IClientAccountService;
/**
* 客户端账号Service业务层处理
*
* @author ruoyi
*/
@Service
public class ClientAccountServiceImpl implements IClientAccountService
{
@Autowired
private ClientAccountMapper clientAccountMapper;
/**
* 查询客户端账号
*/
@Override
public ClientAccount selectClientAccountById(Long id)
{
return clientAccountMapper.selectClientAccountById(id);
}
/**
* 通过用户名查询客户端账号
*/
@Override
public ClientAccount selectClientAccountByUsername(String username)
{
return clientAccountMapper.selectClientAccountByUsername(username);
}
/**
* 查询客户端账号列表
*/
@Override
public List<ClientAccount> selectClientAccountList(ClientAccount clientAccount)
{
return clientAccountMapper.selectClientAccountList(clientAccount);
}
/**
* 新增客户端账号
*/
@Override
public int insertClientAccount(ClientAccount clientAccount)
{
return clientAccountMapper.insertClientAccount(clientAccount);
}
/**
* 修改客户端账号
*/
@Override
public int updateClientAccount(ClientAccount clientAccount)
{
return clientAccountMapper.updateClientAccount(clientAccount);
}
/**
* 批量删除客户端账号
*/
@Override
public int deleteClientAccountByIds(Long[] ids)
{
return clientAccountMapper.deleteClientAccountByIds(ids);
}
/**
* 删除客户端账号信息
*/
@Override
public int deleteClientAccountById(Long id)
{
return clientAccountMapper.deleteClientAccountById(id);
}
}

View File

@@ -0,0 +1,64 @@
package com.ruoyi.web.sse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class SseHubService {
private final Map<String, SseEmitter> sessionEmitters = new ConcurrentHashMap<>();
public String buildSessionKey(String username, String clientId) {
return (username == null ? "" : username) + ":" + (clientId == null ? "" : clientId);
}
public SseEmitter register(String username, String clientId, Long timeoutMs) {
String key = buildSessionKey(username, clientId);
SseEmitter emitter = new SseEmitter(timeoutMs != null ? timeoutMs : 0L);
sessionEmitters.put(key, emitter);
emitter.onCompletion(() -> sessionEmitters.remove(key));
emitter.onTimeout(() -> sessionEmitters.remove(key));
return emitter;
}
public void sendEvent(String username, String clientId, String type, String message) {
String key = buildSessionKey(username, clientId);
SseEmitter emitter = sessionEmitters.get(key);
if (emitter == null) return;
try {
String data = message != null ? message : "{}";
emitter.send(SseEmitter.event().name("event").data("{\"type\":\"" + type + "\",\"message\":" + escapeJson(data) + "}"));
} catch (IOException e) {
sessionEmitters.remove(key);
try { emitter.complete(); } catch (Exception ignored) {}
}
}
public void sendPing(String username, String clientId) {
String key = buildSessionKey(username, clientId);
SseEmitter emitter = sessionEmitters.get(key);
if (emitter == null) return;
try {
emitter.send(SseEmitter.event().name("ping").data(String.valueOf(System.currentTimeMillis())));
} catch (IOException e) {
sessionEmitters.remove(key);
try { emitter.complete(); } catch (Exception ignored) {}
}
}
private String escapeJson(String raw) {
return "\"" + raw.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
}

View File

@@ -0,0 +1,168 @@
package com.ruoyi.web.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 阿里巴巴1688 API Cookie工具类
*/
public class Alibaba1688CookieUtil {
private static final Logger log = LoggerFactory.getLogger(Alibaba1688CookieUtil.class);
private static Map<String, String> cookieCache = new HashMap<>();
private static final long COOKIE_EXPIRE_TIME = 10 * 60 * 1000;
private static long lastUpdateTime = 0;
private static final String M_H5_TK = "_m_h5_tk";
private static final String M_H5_TK_ENC = "_m_h5_tk_enc";
/**
* 获取1688 API的Token (从_m_h5_tk cookie中提取)
*
* @param restTemplate RestTemplate实例
* @return Token字符串失败返回空字符串
*/
public static String getToken(RestTemplate restTemplate) {
String tokenCookie = getCookie(M_H5_TK, restTemplate);
if (tokenCookie != null && !tokenCookie.isEmpty()) {
return tokenCookie.split("_")[0];
}
return "";
}
/**
* 获取指定名称的Cookie值
*
* @param cookieName Cookie名称
* @param restTemplate RestTemplate实例
* @return Cookie值未找到返回null
*/
public static String getCookie(String cookieName, RestTemplate restTemplate) {
if (System.currentTimeMillis() - lastUpdateTime > COOKIE_EXPIRE_TIME) {
refreshCookies(restTemplate);
}
return cookieCache.get(cookieName);
}
/**
* 获取完整的Cookie字符串用于HTTP请求头
*
* @param restTemplate RestTemplate实例
* @return 完整的Cookie字符串
*/
public static String getCookieString(RestTemplate restTemplate) {
if (System.currentTimeMillis() - lastUpdateTime > COOKIE_EXPIRE_TIME) {
refreshCookies(restTemplate);
}
StringBuilder cookieBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : cookieCache.entrySet()) {
if (cookieBuilder.length() > 0) {
cookieBuilder.append("; ");
}
cookieBuilder.append(entry.getKey()).append("=").append(entry.getValue());
}
return cookieBuilder.toString();
}
/**
* 刷新Cookie
*
* @param restTemplate RestTemplate实例
*/
public static synchronized void refreshCookies(RestTemplate restTemplate) {
try {
HttpHeaders headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36");
headers.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange("https://www.1688.com/", HttpMethod.GET, entity, String.class);
List<String> cookies = response.getHeaders().get("Set-Cookie");
if (cookies != null) {
for (String cookie : cookies) {
parseCookie(cookie);
}
}
headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36");
headers.set("Accept", "application/json");
headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
headers.set("Referer", "https://www.1688.com/");
if (!cookieCache.isEmpty()) {
StringBuilder cookieBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : cookieCache.entrySet()) {
if (cookieBuilder.length() > 0) {
cookieBuilder.append("; ");
}
cookieBuilder.append(entry.getKey()).append("=").append(entry.getValue());
}
headers.set("Cookie", cookieBuilder.toString());
}
entity = new HttpEntity<>(headers);
response = restTemplate.exchange(
"https://h5api.m.1688.com/h5/mtop.relationrecommend.wirelessrecommend.recommend/2.0/?jsv=2.7.4&appKey=12574478&api=mtop.relationrecommend.wirelessrecommend.recommend&v=2.0&type=originaljson&dataType=json",
HttpMethod.GET, entity, String.class);
// 解析新的Set-Cookie头
cookies = response.getHeaders().get("Set-Cookie");
if (cookies != null) {
for (String cookie : cookies) {
parseCookie(cookie);
}
}
// 打印获取到的关键Cookie
log.info("获取到的1688 API token: {}", cookieCache.getOrDefault(M_H5_TK, "未获取到"));
log.info("获取到的1688 API token_enc: {}", cookieCache.getOrDefault(M_H5_TK_ENC, "未获取到"));
lastUpdateTime = System.currentTimeMillis();
} catch (Exception e) {
log.error("刷新1688 API Cookie失败", e);
}
}
/**
* 解析Cookie字符串并更新缓存
*
* @param cookieStr Cookie字符串
*/
private static void parseCookie(String cookieStr) {
if (cookieStr == null || cookieStr.isEmpty()) {
return;
}
String[] parts = cookieStr.split(";")[0].split("=", 2);
if (parts.length == 2) {
String name = parts[0].trim();
String value = parts[1].trim();
cookieCache.put(name, value);
}
}
/**
* 手动设置Cookie
*
* @param name Cookie名称
* @param value Cookie值
*/
public static void setCookie(String name, String value) {
cookieCache.put(name, value);
lastUpdateTime = System.currentTimeMillis();
}
/**
* 清除所有Cookie缓存
*/
public static void clearCookies() {
cookieCache.clear();
lastUpdateTime = 0;
}
}

View File

@@ -0,0 +1,69 @@
package com.ruoyi.web.util;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.framework.config.ImgUpload;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
/**
* 本地文件上传与删除
* @author TRACK
*/
@Component
public class ImgUploadUtil {
@Resource(name = "imgUpload")
private ImgUpload imgUpload;
public Integer getUploadType() {
Integer uploadType = imgUpload.getUploadType();
if (Objects.isNull(uploadType)) {
throw new ServiceException("请配置图片存储方式!");
}
return uploadType;
}
public String getUploadPath() {
String imagePath = imgUpload.getImagePath();
if (Objects.isNull(imagePath) || StringUtils.isBlank(imagePath)) {
throw new ServiceException("请配置图片存储路径");
}
return imagePath;
}
public String getResourceUrl() {
String resourceUrl = imgUpload.getResourceUrl();
if (Objects.isNull(resourceUrl) || StringUtils.isBlank(resourceUrl)) {
throw new ServiceException("请配置图片路径");
}
return resourceUrl;
}
public String upload(MultipartFile img, String fileName) {
String filePath = imgUpload.getImagePath();
File file = new File(filePath + fileName);
if (!file.exists()) {
boolean result = file.mkdirs();
if (!result) {
throw new ServiceException("创建目录:" + filePath + "失败");
}
}
try {
img.transferTo(file);
} catch (IOException e) {
throw new ServiceException("图片上传失败");
}
return fileName;
}
public void delete(String fileName) {
String filePath = imgUpload.getImagePath();
File file = new File(filePath + fileName);
file.deleteOnExit();
}
}

View File

@@ -0,0 +1,346 @@
package com.ruoyi.web.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import com.ruoyi.common.core.redis.RedisCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.Yaml;
import us.codecraft.webmagic.downloader.HttpClientDownloader;
import us.codecraft.webmagic.proxy.Proxy;
import us.codecraft.webmagic.proxy.SimpleProxyProvider;
/**
* WebMagic爬虫代理工具类
*
* @author ruoyi
*/
@Component
public class WebMagicProxyUtil {
private static final Logger logger = LoggerFactory.getLogger(WebMagicProxyUtil.class);
// 随机数生成器
private static final Random random = new Random();
// 默认代理配置
private static final String DEFAULT_PROXY_HOST = "127.0.0.1";
private static final String DEFAULT_PROXY_PORT = "7890";
// 存储所有代理节点信息
private static List<Map<String, Object>> proxyNodes = new ArrayList<>();
// 当前使用的代理信息
private static String currentProxyName = "";
private static String currentProxyHost = DEFAULT_PROXY_HOST;
private static int currentProxyPort = Integer.parseInt(DEFAULT_PROXY_PORT);
// Clash状态标志
private static boolean clashRunning = false;
// Redis缓存相关
@Autowired
private RedisCache redisCache;
private static RedisCache staticRedisCache;
private static final String PROXY_POOL_CACHE_KEY = "proxy:pool:cache";
private static final Integer CACHE_DURATION = 30 * 60; // 30分钟缓存时间
static {
loadProxyConfig();
}
/**
* 获取代理IP池
* @return 代理IP列表
*/
public static List<String> fetchProxyPool() {
List<String> proxyList = new ArrayList<>();
try {
List<String> cachedProxyPool = staticRedisCache.getCacheObject(PROXY_POOL_CACHE_KEY);
if (cachedProxyPool != null) {
return new ArrayList<>(cachedProxyPool);
}
URL url = new URL("https://www.proxy-list.download/api/v2/get?l=en&t=http");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && line.contains(":")) {
proxyList.add(line);
}
}
}
staticRedisCache.setCacheObject(PROXY_POOL_CACHE_KEY, proxyList, CACHE_DURATION, TimeUnit.SECONDS);
logger.info("成功获取{}个代理IP", proxyList.size());
} catch (Exception e) {
logger.error("获取代理IP池失败: {}", e.getMessage());
}
return proxyList;
}
/**
* 加载代理配置文件
*/
@SuppressWarnings("unchecked")
private static void loadProxyConfig() {
try {
File configFile = new File("/www/java_mall/erp/config/test_proxy.yml");
if (configFile.exists()) {
Yaml yaml = new Yaml();
try (InputStream inputStream = new FileInputStream(configFile)) {
Map<String, Object> config = yaml.load(inputStream);
if (config != null && config.containsKey("proxies")) {
List<Map<String, Object>> proxies = (List<Map<String, Object>>) config.get("proxies");
proxyNodes = proxies.stream()
.filter(proxy -> proxy != null && proxy.containsKey("name") && proxy.containsKey("server") && proxy.containsKey("port"))
.collect(Collectors.toList());
logger.info("成功加载{}个代理节点配置", proxyNodes.size());
}
}
} else {
logger.warn("未找到代理配置文件,将使用默认代理设置");
}
} catch (Exception e) {
logger.error("加载代理配置失败", e);
}
}
/**
* 获取所有可用代理节点
*/
public static List<Map<String, String>> getAllProxies() {
if (proxyNodes.isEmpty()) {
loadProxyConfig();
}
if (proxyNodes.isEmpty()) {
List<Map<String, String>> defaultProxyList = new ArrayList<>();
Map<String, String> defaultProxy = new HashMap<>();
defaultProxy.put("name", "默认代理");
defaultProxy.put("server", DEFAULT_PROXY_HOST);
defaultProxy.put("port", DEFAULT_PROXY_PORT);
defaultProxy.put("type", "http");
defaultProxyList.add(defaultProxy);
return defaultProxyList;
}
return proxyNodes.stream()
.map(node -> {
Map<String, String> proxyInfo = new HashMap<>();
proxyInfo.put("name", (String) node.get("name"));
proxyInfo.put("server", (String) node.get("server"));
proxyInfo.put("port", String.valueOf(node.get("port")));
proxyInfo.put("type", (String) node.get("type"));
return proxyInfo;
})
.collect(Collectors.toList());
}
/**
* 设置当前使用的代理节点
*/
public static boolean setCurrentProxy(String proxyName) {
// 查找匹配的代理节点
Map<String, Object> targetProxy = proxyNodes.stream()
.filter(node -> proxyName.equals(node.get("name")))
.findFirst()
.orElse(null);
if (targetProxy != null) {
currentProxyName = (String) targetProxy.get("name");
currentProxyHost = (String) targetProxy.get("server");
currentProxyPort = ((Number) targetProxy.get("port")).intValue();
logger.info("已设置代理节点: {}, 地址: {}:{}", currentProxyName, currentProxyHost, currentProxyPort);
return true;
} else if ("默认代理".equals(proxyName)) {
// 使用默认代理
currentProxyName = "默认代理";
currentProxyHost = DEFAULT_PROXY_HOST;
currentProxyPort = Integer.parseInt(DEFAULT_PROXY_PORT);
logger.info("已设置默认代理: {}:{}", DEFAULT_PROXY_HOST, DEFAULT_PROXY_PORT);
return true;
} else {
logger.warn("未找到指定的代理节点: {}, 将使用默认代理", proxyName);
return false;
}
}
/**
* 获取配置了代理的下载器
*/
public static HttpClientDownloader getProxyDownloader() {
return getProxyDownloader(currentProxyHost, currentProxyPort);
}
/**
* 获取配置了指定代理的下载器
*/
public static HttpClientDownloader getProxyDownloader(String host, int port) {
HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
clearSystemProxy();
try {
// 设置系统代理
System.setProperty("http.proxyHost", host);
System.setProperty("http.proxyPort", String.valueOf(port));
System.setProperty("https.proxyHost", host);
System.setProperty("https.proxyPort", String.valueOf(port));
System.setProperty("http.nonProxyHosts", "localhost|127.0.0.1");
// 设置WebMagic代理
httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(
new Proxy(host, port)
));
} catch (Exception e) {
logger.error("设置代理失败", e);
}
return httpClientDownloader;
}
/**
* 获取配置了默认代理的下载器
*/
public static HttpClientDownloader getDefaultProxyDownloader() {
return getProxyDownloader(DEFAULT_PROXY_HOST, Integer.parseInt(DEFAULT_PROXY_PORT));
}
/**
* 清除系统代理设置
*/
public static void clearSystemProxy() {
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
}
/**
* 获取随机休眠时间
*/
public static int getRandomSleepTime(int min, int max) {
return min + random.nextInt(max - min);
}
/**
* 启动代理服务
*
* @return 是否成功启动
*/
public static boolean startProxy() {
logger.info("正在启动代理服务...");
try {
if (isProxyRunning()) {
logger.info("代理服务已经在运行中,无需重复启动");
clashRunning = true;
return true;
}
ProcessBuilder pb = new ProcessBuilder("systemctl", "start", "clash");
pb.redirectErrorStream(true);
Process process = pb.start();
process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
Thread.sleep(3000);
if (isProxyRunning()) {
logger.info("代理服务启动成功");
clashRunning = true;
return true;
} else {
logger.warn("代理服务启动失败或超时");
return false;
}
} catch (Exception e) {
logger.error("启动代理服务时发生异常", e);
return false;
}
}
/**
* 停止代理服务
*
* @return 是否成功停止
*/
public static boolean stopProxy() {
logger.info("正在停止代理服务...");
try {
if (!isProxyRunning()) {
logger.info("代理服务未在运行,无需停止");
clashRunning = false;
return true;
}
// 在Linux环境下使用systemctl
ProcessBuilder pb = new ProcessBuilder("systemctl", "stop", "clash");
pb.redirectErrorStream(true);
Process process = pb.start();
process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
clearSystemProxy();
Thread.sleep(1000);
if (!isProxyRunning()) {
logger.info("代理服务停止成功");
clashRunning = false;
return true;
} else {
logger.warn("代理服务停止失败或超时");
return false;
}
} catch (Exception e) {
logger.error("停止代理服务时发生异常", e);
return false;
} finally {
clearSystemProxy();
}
}
/**
* 检查代理服务是否在运行
*
* @return 是否在运行
*/
public static boolean isProxyRunning() {
try {
java.net.Socket socket = new java.net.Socket();
socket.connect(new java.net.InetSocketAddress(DEFAULT_PROXY_HOST, Integer.parseInt(DEFAULT_PROXY_PORT)), 1000);
socket.close();
return true;
} catch (Exception e) {
return false;
}
}
}

View File

@@ -0,0 +1 @@
restart.include.json=/com.alibaba.fastjson2.*.jar

View File

@@ -0,0 +1,64 @@
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://8.138.23.49:8896/erp?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: jaz7fMSiCrQK48nK
# url: jdbc:mysql://localhost:3306/erp?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
# username: root
# password: 123123
# 从库数据源
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

View File

@@ -0,0 +1,142 @@
# 项目相关配置
ruoyi:
# 名称
name: RuoYi
# 版本
version: 3.9.0
# 版权年份
copyrightYear: 2025
profile: D:/ruoyi/uploadPath
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
captchaType: math
# 七牛云配置
qiniu:
# 七牛密钥
accessKey: M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF
secretKey: Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl
# 七牛空间名
bucket: pxdj-prod
# 资源地址
resourcesUrl: https://qiniu.pxdj.tashowz.com/
# 七牛云机房
zone: HUA_NAN
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 8080
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
threads:
# tomcat最大线程数默认为200
max: 800
# Tomcat启动初始化的线程数默认值10
min-spare: 100
# 日志配置
logging:
level:
com.ruoyi: debug
org.springframework: warn
# 用户配置
user:
password:
# 密码最大错误次数
maxRetryCount: 5
# 密码锁定时间默认10分钟
lockTime: 10
# Spring配置
spring:
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
profiles:
active: druid
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 500MB
# 设置总上传的文件大小
max-request-size: 500MB
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
# redis 配置
redis:
# 地址
host: 8.138.23.49
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
# password:
password: 123123
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# 令牌有效期默认30分钟
expireTime: 30
# MyBatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.ruoyi.**.domain
# 配置mapper的扫描找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 加载全局的配置文件
configLocation: classpath:mybatis/mybatis-config.xml
# PageHelper分页插件
pagehelper:
helperDialect: mysql
supportMethodsArguments: true
params: count=countSql
# Swagger配置
swagger:
# 是否开启swagger
enabled: true
# 请求前缀
pathMapping: /dev-api
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice,/tool/webmagic
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*

View File

@@ -0,0 +1,24 @@
Application Version: ${ruoyi.version}
Spring Boot Version: ${spring-boot.version}
////////////////////////////////////////////////////////////////////
// _ooOoo_ //
// o8888888o //
// 88" . "88 //
// (| ^_^ |) //
// O\ = /O //
// ____/`---'\____ //
// .' \\| |// `. //
// / \\||| : |||// \ //
// / _||||| -:- |||||- \ //
// | | \\\ - /// | | //
// | \_| ''\---/'' | | //
// \ .-\__ `-` ___/-. / //
// ___`. .' /--.--\ `. . ___ //
// ."" '< `.___\_<|>_/___.' >'"". //
// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
// \ \ `-. \_ __\ /__ _/ .-` / / //
// ========`-.____`-.___\_____/___.-`____.-'======== //
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永不宕机 永无BUG //
////////////////////////////////////////////////////////////////////

View File

@@ -0,0 +1,38 @@
#错误消息
not.null=* 必须填写
user.jcaptcha.error=验证码错误
user.jcaptcha.expire=验证码已失效
user.not.exists=用户不存在/密码错误
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
user.password.delete=对不起,您的账号已被删除
user.blocked=用户已封禁,请联系管理员
role.blocked=角色已封禁,请联系管理员
login.blocked=很遗憾访问IP已被列入系统黑名单
user.logout.success=退出成功
length.not.valid=长度必须在{min}到{max}个字符之间
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成且必须以非数字开头
user.password.not.valid=* 5-50个字符
user.email.not.valid=邮箱格式错误
user.mobile.phone.number.not.valid=手机号格式错误
user.login.success=登录成功
user.register.success=注册成功
user.notfound=请重新登录
user.forcelogout=管理员强制退出,请重新登录
user.unknown.error=未知错误,请重新登录
##文件上传消息
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB
upload.filename.exceed.length=上传的文件名最长{0}个字符
##权限
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property name="log.path" value="/home/ruoyi/logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 用户访问日志输出 -->
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-user.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.ruoyi" level="info" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<!-- Apache HTTP客户端日志级别控制避免输出过多DEBUG日志 -->
<logger name="org.apache.http" level="warn" />
<logger name="org.apache.http.wire" level="warn" />
<logger name="org.apache.http.headers" level="warn" />
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
<!--系统用户操作日志-->
<logger name="sys-user" level="info">
<appender-ref ref="sys-user"/>
</logger>
</configuration>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局参数 -->
<settings>
<!-- 使全局的映射器启用或禁用缓存 -->
<setting name="cacheEnabled" value="true" />
<!-- 允许JDBC 支持自动生成主键 -->
<setting name="useGeneratedKeys" value="true" />
<!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
<setting name="defaultExecutorType" value="SIMPLE" />
<!-- 指定 MyBatis 所用日志的具体实现 -->
<setting name="logImpl" value="SLF4J" />
<!-- 使用驼峰命名法转换字段 -->
<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
</settings>
</configuration>

Binary file not shown.