Initial commit
This commit is contained in:
31
ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
Normal file
31
ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
Normal 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" +
|
||||
" ''-' `'-' `-..-' ");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 < version2,0表示相等,正数表示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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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("上传图片异常,请联系管理员");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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刷新请求已执行");
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) ;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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("\"", "\\\"") + "\"";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
restart.include.json=/com.alibaba.fastjson2.*.jar
|
||||
64
ruoyi-admin/src/main/resources/application-druid.yml
Normal file
64
ruoyi-admin/src/main/resources/application-druid.yml
Normal 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
|
||||
142
ruoyi-admin/src/main/resources/application.yml
Normal file
142
ruoyi-admin/src/main/resources/application.yml
Normal 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/*
|
||||
24
ruoyi-admin/src/main/resources/banner.txt
Normal file
24
ruoyi-admin/src/main/resources/banner.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
Application Version: ${ruoyi.version}
|
||||
Spring Boot Version: ${spring-boot.version}
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// _ooOoo_ //
|
||||
// o8888888o //
|
||||
// 88" . "88 //
|
||||
// (| ^_^ |) //
|
||||
// O\ = /O //
|
||||
// ____/`---'\____ //
|
||||
// .' \\| |// `. //
|
||||
// / \\||| : |||// \ //
|
||||
// / _||||| -:- |||||- \ //
|
||||
// | | \\\ - /// | | //
|
||||
// | \_| ''\---/'' | | //
|
||||
// \ .-\__ `-` ___/-. / //
|
||||
// ___`. .' /--.--\ `. . ___ //
|
||||
// ."" '< `.___\_<|>_/___.' >'"". //
|
||||
// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
|
||||
// \ \ `-. \_ __\ /__ _/ .-` / / //
|
||||
// ========`-.____`-.___\_____/___.-`____.-'======== //
|
||||
// `=---=' //
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
|
||||
// 佛祖保佑 永不宕机 永无BUG //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
38
ruoyi-admin/src/main/resources/i18n/messages.properties
Normal file
38
ruoyi-admin/src/main/resources/i18n/messages.properties
Normal 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}]
|
||||
97
ruoyi-admin/src/main/resources/logback.xml
Normal file
97
ruoyi-admin/src/main/resources/logback.xml
Normal 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>
|
||||
20
ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml
Normal file
20
ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml
Normal 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>
|
||||
BIN
ruoyi-admin/src/main/resources/test_proxy.yml
Normal file
BIN
ruoyi-admin/src/main/resources/test_proxy.yml
Normal file
Binary file not shown.
Reference in New Issue
Block a user