Initial commit

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

View File

@@ -0,0 +1,19 @@
{
"permissions": {
"allow": [
"Bash(mvn:*)",
"Bash(del:*)",
"Bash(rm:*)",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller\\AuthController.java\")",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\resources\\static\\html\\login.html\")",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\java\\com\\tashow\\erp\\controller\\AuthProxyController.java\")",
"Bash(java:*)",
"Bash(del \"C:\\Users\\ZiJIe\\Desktop\\wox\\erp_client_sb\\src\\main\\resources\\static\\html\\shopee-platform.html\")",
"Bash(mkdir:*)",
"Bash(\"taskkill\" \"/f\" \"/pid\" \"9804\")",
"Bash(dir:*)",
"Bash(find:*)"
],
"deny": []
}
}

View File

@@ -0,0 +1 @@
DEVICE-CD31378598644EAA8C0E8153A9D80959

Binary file not shown.

185
erp_client_sb/pom.xml Normal file
View File

@@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/>
</parent>
<groupId>com.tashow.erp</groupId>
<artifactId>erp_client_sb</artifactId>
<version>2.4.7</version>
<name>erp_client_sb</name>
<description>erp客户端</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.release>${java.version}</maven.compiler.release>
<java.version>17</java.version>
<springboot.version>3.5.4</springboot.version>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Apache POI for Excel processing -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<!-- 已移除 JavaFX/FxWeaver 相关依赖,保留为纯 Spring Boot -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.12.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-core -->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>1.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-extension -->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>1.0.3</version>
</dependency>
<!-- JavaFX 相关依赖已移除 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.36</version>
</dependency>
<!-- SQLite数据库支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.42.0.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
<version>6.2.7.Final</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.23.0</version>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.python/jython-standalone -->
<dependency>
<groupId>org.python</groupId>
<artifactId>jython-standalone</artifactId>
<version>2.7.4</version>
</dependency>
<!-- JWT parsing for local RS256 verification -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>aliyun-repository</id>
<name>aliyun repository</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
<repository>
<id>jboss-repository</id>
<name>jboss repository</name>
<url>http://repository.jboss.org/nexus/content/groups/public-jboss/</url>
</repository>
</repositories>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>application*.yml</include>
<include>application*.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>application*.yml</exclude>
<exclude>application*.properties</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<mainClass>com.tashow.erp.ErpClientSbApplication</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<!-- 移除 OpenJFX 打包插件,采用纯 Spring Boot 运行 -->
</plugins>
</build>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View File

@@ -0,0 +1,37 @@
package com.tashow.erp;
import com.tashow.erp.utils.ErrorReporter;
import com.tashow.erp.utils.ResourcePreloader;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@Slf4j
@SpringBootApplication
public class ErpClientSbApplication {
public static void main(String[] args) {
// 纯 Spring Boot 启动,不再依赖 JavaFX
ConfigurableApplicationContext applicationContext = SpringApplication.run(ErpClientSbApplication.class, args);
try {
ErrorReporter errorReporter = applicationContext.getBean(ErrorReporter.class);
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
log.error("捕获到未处理异常: " + ex.getMessage(), ex);
errorReporter.reportSystemError("未捕获异常: " + thread.getName(), (Exception) ex);
});
log.info("全局异常处理器已设置");
} catch (Exception e) {
log.warn("未设置 ErrorReporter继续启动: {}", e.getMessage());
}
// 如需预加载资源,可按需保留
try {
ResourcePreloader.init();
ResourcePreloader.preloadErpDashboard();
ResourcePreloader.executePreloading();
} catch (Throwable t) {
log.warn("资源预加载失败: {}", t.getMessage());
}
}
}

View File

@@ -0,0 +1,85 @@
package com.tashow.erp.common;
import com.tashow.erp.utils.StringUtils;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* 字符集工具类
*
* @author ruoyi
*/
public class CharsetKit
{
/** ISO-8859-1 */
public static final String ISO_8859_1 = "ISO-8859-1";
/** UTF-8 */
public static final String UTF_8 = "UTF-8";
/** GBK */
public static final String GBK = "GBK";
/** ISO-8859-1 */
public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1);
/** UTF-8 */
public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8);
/** GBK */
public static final Charset CHARSET_GBK = Charset.forName(GBK);
/**
* 转换为Charset对象
*
* @param charset 字符集,为空则返回默认字符集
* @return Charset
*/
public static Charset charset(String charset)
{
return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset);
}
/**
* 转换字符串的字符集编码
*
* @param source 字符串
* @param srcCharset 源字符集默认ISO-8859-1
* @param destCharset 目标字符集默认UTF-8
* @return 转换后的字符集
*/
public static String convert(String source, String srcCharset, String destCharset)
{
return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset));
}
/**
* 转换字符串的字符集编码
*
* @param source 字符串
* @param srcCharset 源字符集默认ISO-8859-1
* @param destCharset 目标字符集默认UTF-8
* @return 转换后的字符集
*/
public static String convert(String source, Charset srcCharset, Charset destCharset)
{
if (null == srcCharset)
{
srcCharset = StandardCharsets.ISO_8859_1;
}
if (null == destCharset)
{
destCharset = StandardCharsets.UTF_8;
}
if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset))
{
return source;
}
return new String(source.getBytes(srcCharset), destCharset);
}
/**
* @return 系统字符集编码
*/
public static String systemCharset()
{
return Charset.defaultCharset().name();
}
}

View File

@@ -0,0 +1,169 @@
package com.tashow.erp.common;
import java.util.Locale;
/**
* 通用常量信息
*
* @author ruoyi
*/
public class Constants
{
/**
* UTF-8 字符集
*/
public static final String UTF8 = "UTF-8";
/**
* GBK 字符集
*/
public static final String GBK = "GBK";
/**
* 系统语言
*/
public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
/**
* www主域
*/
public static final String WWW = "www.";
/**
* http请求
*/
public static final String HTTP = "http://";
/**
* https请求
*/
public static final String HTTPS = "https://";
/**
* 通用成功标识
*/
public static final String SUCCESS = "0";
/**
* 通用失败标识
*/
public static final String FAIL = "1";
/**
* 登录成功
*/
public static final String LOGIN_SUCCESS = "Success";
/**
* 注销
*/
public static final String LOGOUT = "Logout";
/**
* 注册
*/
public static final String REGISTER = "Register";
/**
* 登录失败
*/
public static final String LOGIN_FAIL = "Error";
/**
* 所有权限标识
*/
public static final String ALL_PERMISSION = "*:*:*";
/**
* 管理员角色权限标识
*/
public static final String SUPER_ADMIN = "admin";
/**
* 角色权限分隔符
*/
public static final String ROLE_DELIMETER = ",";
/**
* 权限标识分隔符
*/
public static final String PERMISSION_DELIMETER = ",";
/**
* 验证码有效期(分钟)
*/
public static final Integer CAPTCHA_EXPIRATION = 2;
/**
* 令牌
*/
public static final String TOKEN = "token";
/**
* 令牌前缀
*/
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 令牌前缀
*/
public static final String LOGIN_USER_KEY = "login_user_key";
/**
* 用户ID
*/
public static final String JWT_USERID = "userid";
/**
* 用户头像
*/
public static final String JWT_AVATAR = "avatar";
/**
* 创建时间
*/
public static final String JWT_CREATED = "created";
/**
* 用户权限
*/
public static final String JWT_AUTHORITIES = "authorities";
/**
* 资源映射路径 前缀
*/
public static final String RESOURCE_PREFIX = "/profile";
/**
* RMI 远程方法调用
*/
public static final String LOOKUP_RMI = "rmi:";
/**
* LDAP 远程方法调用
*/
public static final String LOOKUP_LDAP = "ldap:";
/**
* LDAPS 远程方法调用
*/
public static final String LOOKUP_LDAPS = "ldaps:";
/**
* 自动识别json对象白名单配置仅允许解析的包名范围越小越安全
*/
public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.ruoyi" };
/**
* 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
*/
public static final String[] JOB_WHITELIST_STR = { "com.ruoyi.quartz.task" };
/**
* 定时任务违规的字符
*/
public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
"org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config", "com.ruoyi.generator" };
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
package com.tashow.erp.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 数据库配置,确保数据目录存在
*/
@Slf4j
@Component
@Order(1) // 确保在其他组件之前运行
public class DatabaseConfig implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 确保data目录存在
Path dataDir = Paths.get("data");
if (!Files.exists(dataDir)) {
try {
Files.createDirectories(dataDir);
log.info("创建数据目录: {}", dataDir.toAbsolutePath());
} catch (Exception e) {
log.error("创建数据目录失败: {}", e.getMessage());
throw e;
}
} else {
log.info("数据目录已存在: {}", dataDir.toAbsolutePath());
}
}
}

View File

@@ -0,0 +1,50 @@
package com.tashow.erp.config;
import com.tashow.erp.utils.ErrorReporter;
import com.tashow.erp.utils.JsonData;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 错误上报切面 - 自动捕获所有Controller异常并上报同时检测错误返回值
*/
@Aspect
@Component
public class ErrorReportAspect {
@Autowired
private ErrorReporter errorReporter;
/**
* 拦截所有Controller方法自动上报异常和错误返回值
*/
@Around("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)")
public Object aroundController(ProceedingJoinPoint point) throws Throwable {
String methodName = point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName();
try {
Object result = point.proceed();
// 检查返回值是否表示错误
if (result instanceof JsonData) {
JsonData jsonData = (JsonData) result;
// code != 0 表示失败根据JsonData注释0表示成功-1表示失败1表示处理中
if (jsonData.getCode() != null && jsonData.getCode() != 0) {
// 创建一个RuntimeException来包装错误信息
String errorMsg = jsonData.getMsg() != null ? jsonData.getMsg() : "未知错误";
Exception syntheticException = new RuntimeException("业务处理失败: " + errorMsg);
errorReporter.reportBusinessError(methodName + " 返回错误", syntheticException);
}
}
return result;
} catch (Exception e) {
// 自动上报未捕获的异常
errorReporter.reportBusinessError(methodName + " 抛出异常", e);
throw e;
}
}
}

View File

@@ -0,0 +1,18 @@
// package com.tashow.erp.config;
//
// 已移除 FxWeaver 相关配置(项目改为纯 Spring Boot
// 如需恢复 JavaFX 集成,请取消注释并恢复依赖。
//
// import net.rgielen.fxweaver.core.FxWeaver;
// import net.rgielen.fxweaver.spring.SpringFxWeaver;
// import org.springframework.context.ConfigurableApplicationContext;
// import org.springframework.context.annotation.Bean;
// import org.springframework.context.annotation.Configuration;
//
// @Configuration
// public class FxWeaverConfig {
// @Bean
// public FxWeaver fxWeaver(ConfigurableApplicationContext applicationContext) {
// return new SpringFxWeaver(applicationContext);
// }
// }

View File

@@ -0,0 +1,13 @@
package com.tashow.erp.config;
import com.tashow.erp.fx.controller.JavaBridge;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JavaBridgeConfig {
@Bean
public JavaBridge javaBridge() {
return new JavaBridge();
}
}

View File

@@ -0,0 +1,63 @@
// package com.tashow.erp.config;
//
// 已转为纯 Spring BootJavaFX 启动屏幕不再使用。如需恢复,可取消注释并补充 JavaFX 依赖。
//
// import javafx.application.Platform;
// import javafx.scene.Scene;
// import javafx.scene.control.ProgressBar;
// import javafx.scene.image.Image;
// import javafx.scene.image.ImageView;
// import javafx.scene.layout.StackPane;
// import javafx.scene.layout.VBox;
// import javafx.stage.Stage;
// import javafx.stage.StageStyle;
// import lombok.extern.slf4j.Slf4j;
// import org.springframework.stereotype.Component;
//
// /**
// * 自定义启动屏幕
// */
// @Slf4j
// @Component
// public class MySplashScreen {
// private static final String DEFAULT_IMAGE = "/static/image/splash_screen.png";
// private Stage splashStage;
// private ProgressBar progressBar;
//
// public void show() {
// Platform.runLater(() -> {
// try {
// splashStage = new Stage();
// splashStage.initStyle(StageStyle.UNDECORATED);
// Image splashImage = new Image(getClass().getResourceAsStream(DEFAULT_IMAGE));
// ImageView imageView = new ImageView(splashImage);
// ProgressBar progressBar = new ProgressBar();
// progressBar.setPrefWidth(splashImage.getWidth());
// progressBar.setMaxWidth(Double.MAX_VALUE);
// progressBar.setStyle("-fx-accent: #0078d4; -fx-background-color: transparent;");
// StackPane root = new StackPane();
// VBox progressContainer = new VBox();
// progressContainer.setAlignment(javafx.geometry.Pos.BOTTOM_CENTER);
// progressContainer.setSpacing(0);
// progressContainer.getChildren().add(progressBar);
// root.getChildren().addAll(imageView, progressContainer);
// Scene scene = new Scene(root);
// splashStage.setScene(scene);
// splashStage.setResizable(false);
// splashStage.centerOnScreen();
// splashStage.show();
// } catch (Exception e) {
// log.error("显示启动屏幕失败", e);
// }
// });
// }
//
// public void hide() {
// Platform.runLater(() -> {
// if (splashStage != null) {
// splashStage.hide();
// log.info("启动屏幕已隐藏");
// }
// });
// }
// }

View File

@@ -0,0 +1,19 @@
package com.tashow.erp.config;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
@Component
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplateBuilder builder = new RestTemplateBuilder();
builder.connectTimeout(Duration.ofSeconds(5));
builder.readTimeout(Duration.ofSeconds(10));
return builder.build();
}
}

View File

@@ -0,0 +1,23 @@
package com.tashow.erp.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置类
* 配置CORS以支持JavaFX WebView的网络请求
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8083", "http://127.0.0.1:8083", "file://")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

View File

@@ -0,0 +1,27 @@
package com.tashow.erp.config;
import com.tashow.erp.security.LocalJwtAuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LocalJwtAuthInterceptor localJwtAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localJwtAuthInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/login",
"/api/register",
"/api/verify",
"/api/check-username");
}
}

View File

@@ -0,0 +1,89 @@
package com.tashow.erp.controller;
import com.tashow.erp.repository.AmazonProductRepository;
import com.tashow.erp.service.IAmazonScrapingService;
import com.tashow.erp.utils.ExcelParseUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/amazon")
public class AmazonController {
private static final Logger logger = LoggerUtil.getLogger(AmazonController.class);
@Autowired
private IAmazonScrapingService amazonScrapingService;
@Autowired
private AmazonProductRepository amazonProductRepository;
/**
* 批量获取亚马逊产品信息
*/
@PostMapping("/products/batch")
public JsonData batchGetProducts(@RequestBody Object request) {
@SuppressWarnings("unchecked")
Map<String, Object> requestMap = (Map<String, Object>) request;
List<String> asinList = (List<String>) requestMap.get("asinList");
String batchId = (String) requestMap.get("batchId");
return JsonData.buildSuccess(amazonScrapingService.batchGetProductInfo(asinList, batchId));
}
/**
* 获取最新产品数据
*/
@GetMapping("/products/latest")
public JsonData getLatestProducts() {
List<Map<String, Object>> products = amazonProductRepository.findLatestProducts()
.parallelStream()
.map(entity -> {
Map<String, Object> map = new HashMap<>();
map.put("asin", entity.getAsin());
map.put("title", entity.getTitle());
map.put("price", entity.getPrice());
map.put("imageUrl", entity.getImageUrl());
map.put("productUrl", entity.getProductUrl());
map.put("brand", entity.getBrand());
map.put("category", entity.getCategory());
map.put("rating", entity.getRating());
map.put("reviewCount", entity.getReviewCount());
map.put("availability", entity.getAvailability());
map.put("seller", entity.getSeller());
map.put("shipper", entity.getSeller());
return map;
})
.collect(Collectors.toList());
Map<String, Object> result = new HashMap<>();
result.put("products", products);
result.put("total", products.size());
return JsonData.buildSuccess(result);
}
/**
* 解析Excel文件获取ASIN列表
*/
@PostMapping("/import/asin")
public JsonData importAsinFromExcel(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return JsonData.buildError("上传文件为空");
}
try {
List<String> asinList = ExcelParseUtil.parseFirstColumn(file);
if (asinList.isEmpty()) {
return JsonData.buildError("未从文件中解析到ASIN数据");
}
Map<String, Object> result = new HashMap<>();
result.put("asinList", asinList);
result.put("total", asinList.size());
return JsonData.buildSuccess(result);
} catch (Exception e) {
logger.error("解析文件失败: {}", e.getMessage(), e);
return JsonData.buildError("解析失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,193 @@
package com.tashow.erp.controller;
import com.tashow.erp.entity.AuthTokenEntity;
import com.tashow.erp.entity.CacheDataEntity;
import com.tashow.erp.repository.AuthTokenRepository;
import com.tashow.erp.repository.CacheDataRepository;
import com.tashow.erp.service.IAuthService;
import com.tashow.erp.utils.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api")
public class AuthController {
@Autowired
private IAuthService authService;
@Autowired
private AuthTokenRepository authTokenRepository;
@Autowired
private CacheDataRepository cacheDataRepository;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, Object> loginData) {
String username = (String) loginData.get("username");
String password = (String) loginData.get("password");
if (username == null || password == null) {
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名和密码不能为空"));
}
Map<String, Object> result = authService.login(username, password);
Object success = result.get("success");
Object tokenObj = result.get("token");
if (Boolean.TRUE.equals(success) && tokenObj instanceof String token && token != null && !token.isEmpty()) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
return ResponseEntity.ok().headers(headers).body(result);
}
return ResponseEntity.ok(result);
}
@PostMapping("/verify")
public ResponseEntity<?> verifyToken(@RequestBody Map<String, Object> data) {
String token = (String) data.get("token");
if (token == null) {
return ResponseEntity.ok(Map.of("code", 400, "message", "token不能为空"));
}
Map<String, Object> result = authService.verifyToken(token);
return ResponseEntity.ok(result);
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody Map<String, Object> registerData) {
String username = (String) registerData.get("username");
String password = (String) registerData.get("password");
if (username == null || password == null) {
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名和密码不能为空"));
}
Map<String, Object> result = authService.register(username, password);
Object success2 = result.get("success");
Object tokenObj2 = result.get("token");
if (Boolean.TRUE.equals(success2) && tokenObj2 instanceof String token && token != null && !token.isEmpty()) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
return ResponseEntity.ok().headers(headers).body(result);
}
return ResponseEntity.ok(result);
}
@GetMapping("/check-username")
public ResponseEntity<?> checkUsername(@RequestParam String username) {
if (username == null || username.trim().isEmpty()) {
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名不能为空"));
}
boolean available = authService.checkUsername(username);
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "检查成功",
"data", available
));
}
/**
* 保存认证密钥
*/
@PostMapping("/auth/save")
public JsonData saveAuth(@RequestBody Map<String, Object> data) {
String serviceName = (String) data.get("serviceName");
String authKey = (String) data.get("authKey");
if (serviceName == null || authKey == null) return JsonData.buildError("serviceName和authKey不能为空");
AuthTokenEntity entity = authTokenRepository.findByServiceName(serviceName).orElse(new AuthTokenEntity());
entity.setServiceName(serviceName);
entity.setToken(authKey);
authTokenRepository.save(entity);
return JsonData.buildSuccess("认证信息保存成功");
}
@GetMapping("/auth/get")
public JsonData getAuth(@RequestParam String serviceName) {
return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName).map(AuthTokenEntity::getToken).orElse(null));
}
/**
* 删除认证密钥
*/
@DeleteMapping("/auth/remove")
public JsonData removeAuth(@RequestParam String serviceName) {
authTokenRepository.findByServiceName(serviceName).ifPresent(authTokenRepository::delete);
return JsonData.buildSuccess("认证信息删除成功");
}
/**
* 保存缓存数据
*/
@PostMapping("/cache/save")
public JsonData saveCache(@RequestBody Map<String, Object> data) {
String key = (String) data.get("key");
String value = (String) data.get("value");
if (key == null || value == null) return JsonData.buildError("key和value不能为空");
CacheDataEntity entity = cacheDataRepository.findByCacheKey(key).orElse(new CacheDataEntity());
entity.setCacheKey(key);
entity.setCacheValue(value);
cacheDataRepository.save(entity);
return JsonData.buildSuccess("缓存数据保存成功");
}
/**
* 获取缓存数据
*/
@GetMapping("/cache/get")
public JsonData getCache(@RequestParam String key) {
return JsonData.buildSuccess(cacheDataRepository.findByCacheKey(key)
.map(CacheDataEntity::getCacheValue).orElse(null));
}
/**
* 删除缓存数据
*/
@DeleteMapping("/cache/remove")
public JsonData removeCache(@RequestParam String key) {
cacheDataRepository.findByCacheKey(key).ifPresent(cacheDataRepository::delete);
return JsonData.buildSuccess("缓存数据删除成功");
}
/**
* 删除缓存数据 - POST方式
*/
@PostMapping("/cache/delete")
public JsonData deleteCacheByPost(@RequestParam String key) {
if (key == null || key.trim().isEmpty()) {
return JsonData.buildError("key不能为空");
}
cacheDataRepository.deleteByCacheKey(key);
return JsonData.buildSuccess("缓存数据删除成功");
}
/**
* 会话引导检查SQLite中是否存在token
*/
@GetMapping("/session/bootstrap")
public ResponseEntity<?> sessionBootstrap() {
Optional<CacheDataEntity> tokenEntity = cacheDataRepository.findByCacheKey("token");
if (tokenEntity.isEmpty()) {
return ResponseEntity.status(401).body(Map.of("code", 401, "message", "无可用会话,请重新登录"));
}
String token = tokenEntity.get().getCacheValue();
if (token == null || token.isEmpty()) {
return ResponseEntity.status(401).body(Map.of("code", 401, "message", "无可用会话,请重新登录"));
}
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
return ResponseEntity.ok().headers(headers).body(Map.of("code", 200, "message", "会话已恢复"));
}
private String buildHttpOnlyCookie(String name, String value, int maxAgeSeconds) {
StringBuilder sb = new StringBuilder();
sb.append(name).append("=").append(value).append(";");
sb.append(" Path=/;");
sb.append(" HttpOnly;");
sb.append(" SameSite=Strict;");
if (maxAgeSeconds > 0) {
sb.append(" Max-Age=").append(maxAgeSeconds).append(";");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,116 @@
package com.tashow.erp.controller;
import com.tashow.erp.fx.controller.JavaBridge;
import com.tashow.erp.repository.BanmaOrderRepository;
import com.tashow.erp.service.IBanmaOrderService;
import com.tashow.erp.utils.ExcelExportUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/banma")
public class BanmaOrderController {
private static final Logger logger = LoggerUtil.getLogger(BanmaOrderController.class);
@Autowired
IBanmaOrderService banmaOrderService;
@Autowired
BanmaOrderRepository banmaOrderRepository;
@Autowired
JavaBridge javaBridge;
@Autowired
RestTemplate restTemplate;
@GetMapping("/orders")
public ResponseEntity<Map<String, Object>> getOrders(
@RequestParam(required = false, name = "startDate") String startDate,
@RequestParam(required = false, name = "endDate") String endDate,
@RequestParam(defaultValue = "1", name = "page") int page,
@RequestParam(defaultValue = "10", name = "pageSize") int pageSize,
@RequestParam(required = false, name = "batchId") String batchId,
@RequestParam(required = false, name = "shopIds") String shopIds) {
List<String> shopIdList = shopIds != null ? java.util.Arrays.asList(shopIds.split(",")) : null;
Map<String, Object> result = banmaOrderService.getOrdersByPage(startDate, endDate, page, pageSize, batchId, shopIdList);
return ResponseEntity.ok(result);
}
/**
* 获取店铺列表
*/
@GetMapping("/shops")
public JsonData getShops() {
try {
Map<String, Object> response = banmaOrderService.getShops();
return JsonData.buildSuccess(response);
} catch (Exception e) {
logger.error("获取店铺列表失败: {}", e.getMessage(), e);
return JsonData.buildError("获取店铺列表失败: " + e.getMessage());
}
}
/**
* 刷新斑马认证Token
*/
@PostMapping("/refresh-token")
public JsonData refreshToken(){
try {
banmaOrderService.refreshToken();
return JsonData.buildSuccess("Token刷新成功");
} catch (Exception e) {
logger.error("刷新Token失败: {}", e.getMessage(), e);
return JsonData.buildError("Token刷新失败: " + e.getMessage());
}
}
/**
* 获取最新订单数据
*/
@GetMapping("/orders/latest")
public JsonData getLatestOrders() {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
List<Map<String, Object>> orders = banmaOrderRepository.findLatestOrders()
.parallelStream()
.map(entity -> {
try {
@SuppressWarnings("unchecked")
Map<String, Object> data = mapper.readValue(entity.getOrderData(), Map.class);
return data;
} catch (Exception e) {
return new HashMap<String, Object>();
}
})
.filter(order -> !order.isEmpty())
.toList();
return JsonData.buildSuccess(Map.of("orders", orders, "total", orders.size()));
}
/**
* JavaFX专用导出并保存Excel文件到桌面
*/
@PostMapping("/export-and-save")
public JsonData exportAndSave(@RequestBody Map<String, Object> body) {
try {
@SuppressWarnings("unchecked")
List<Map<String, Object>> orders = (List<Map<String, Object>>) body.get("orders");
String[] headers = {"下单时间", "商品图片", "商品名称", "乐天订单号", "下单距今时间", "乐天订单金额/日元",
"购买数量", "税费/日元", "服务商回款抽点rmb", "商品番号", "1688采购订单号",
"采购金额/rmb", "国际运费/rmb", "国内物流公司", "国内物流单号", "日本物流单号", "地址状态"};
byte[] excelData = ExcelExportUtil.createExcelWithImages("斑马订单数据", headers, orders, 1, "productImage");
if (excelData.length == 0) return JsonData.buildError("生成Excel文件失败");
String fileName = String.format("斑马订单数据_%s.xlsx", java.time.LocalDate.now().toString());
String savedPath = javaBridge.saveExcelFileToDesktop(excelData, fileName);
return savedPath != null
? JsonData.buildSuccess(Map.of("filePath", savedPath, "fileName", fileName))
: JsonData.buildError("保存文件失败,请检查权限");
} catch (Exception e) {
logger.error("导出并保存斑马订单Excel失败: {}", e.getMessage(), e);
return JsonData.buildError("导出并保存Excel失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,30 @@
package com.tashow.erp.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 配置信息控制器
*/
@RestController
@RequestMapping("/api/config")
public class ConfigController {
@Value("${api.server.base-url}")
private String serverBaseUrl;
/**
* 获取服务器配置
*/
@GetMapping("/server")
public Map<String, Object> getServerConfig() {
return Map.of(
"baseUrl", serverBaseUrl,
"sseUrl", serverBaseUrl + "/monitor/account/events"
);
}
}

View File

@@ -0,0 +1,66 @@
package com.tashow.erp.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import com.tashow.erp.utils.ApiForwarder;
import com.tashow.erp.utils.DeviceUtils;
import java.util.Map;
import java.util.HashMap;
/**
* 设备管理代理控制器
* 简化职责:透传请求到后端服务
*/
@RestController
public class DeviceProxyController {
@Autowired
private RestTemplate restTemplate;
@Value("${api.server.base-url}")
private String serverBaseUrl;
/**
* 注册设备
*/
@Autowired
private ApiForwarder apiForwarder;
@PostMapping("/api/device/register")
public ResponseEntity<?> deviceRegister(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
Map<String, Object> deviceData = new HashMap<>(body);
deviceData.put("deviceId", DeviceUtils.generateDeviceId());
return apiForwarder.post("/monitor/device/register", deviceData, auth);
}
@PostMapping("/api/device/remove")
public ResponseEntity<?> deviceRemove(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
return apiForwarder.post("/monitor/device/remove", body, auth);
}
/**
* 设备心跳
*/
@PostMapping("/api/device/heartbeat")
public ResponseEntity<?> deviceHeartbeat(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
return apiForwarder.post("/monitor/device/heartbeat", body, auth);
}
@GetMapping("/api/device/quota")
public ResponseEntity<?> deviceQuota(@RequestParam("username") String username, @RequestHeader(value = "Authorization", required = false) String auth) {
return apiForwarder.get("/monitor/device/quota?username=" + username, auth);
}
@GetMapping("/api/device/list")
public ResponseEntity<?> deviceList(@RequestParam("username") String username, @RequestHeader(value = "Authorization", required = false) String auth) {
return apiForwarder.get("/monitor/device/list?username=" + username, auth);
}
}

View File

@@ -0,0 +1,25 @@
package com.tashow.erp.controller;
import com.tashow.erp.service.IGenmaiService;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/genmai")
public class GenmaiController {
private static final Logger logger = LoggerUtil.getLogger(GenmaiController.class);
@Autowired
private IGenmaiService genmaiService;
/**
* 打开跟卖精灵网页
*/
@PostMapping("/open")
public void openGenmaiWebsite() {
genmaiService.openGenmaiWebsite();
}
}

View File

@@ -0,0 +1,18 @@
package com.tashow.erp.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "redirect:/html/erp-dashboard.html";
}
@GetMapping("/erp")
public String erp() {
return "redirect:/html/erp-dashboard.html";
}
}

View File

@@ -0,0 +1,116 @@
package com.tashow.erp.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* 代理控制器用于解决CORS跨域问题
*/
@RestController
@RequestMapping("/api/proxy")
public class ProxyController {
@Autowired
private RestTemplate restTemplate;
/**
* 代理获取图片
* @param requestBody 包含图片URL的请求体
* @return 图片字节数组
*/
@PostMapping("/image")
public ResponseEntity<byte[]> proxyImage(@RequestBody Map<String, String> requestBody) {
String imageUrl = requestBody.get("imageUrl");
if (imageUrl == null || imageUrl.isEmpty()) {
return ResponseEntity.badRequest().build();
}
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/91.0.4472.124 Safari/537.36");
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
HttpEntity<String> entity = new HttpEntity<>(headers);
// 发送请求获取图片
ResponseEntity<byte[]> response = restTemplate.exchange(
imageUrl,
HttpMethod.GET,
entity,
byte[].class
);
// 设置响应头
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 通过URL参数代理获取图片为JavaFX WebView优化
* @param imageUrl 图片URL
* @return 图片字节数组
*/
@GetMapping("/image-url")
public ResponseEntity<byte[]> proxyImageByUrl(@RequestParam("url") String imageUrl) {
if (imageUrl == null || imageUrl.isEmpty()) {
System.err.println("图片代理请求失败: 图片URL为空");
return ResponseEntity.badRequest().build();
}
System.out.println("代理图片请求: " + imageUrl);
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/91.0.4472.124 Safari/537.36");
headers.set("Accept", "image/jpeg, image/png, image/webp, image/*");
headers.set("Referer", "https://item.rakuten.co.jp/");
HttpEntity<String> entity = new HttpEntity<>(headers);
// 发送请求获取图片
ResponseEntity<byte[]> response = restTemplate.exchange(
imageUrl,
HttpMethod.GET,
entity,
byte[].class
);
System.out.println("图片代理成功,响应大小: " + (response.getBody() != null ? response.getBody().length : 0) + " bytes");
// 设置响应头支持缓存以提升JavaFX WebView性能
HttpHeaders responseHeaders = new HttpHeaders();
// 尝试从原始响应中获取Content-Type
String contentType = response.getHeaders().getFirst("Content-Type");
if (contentType != null && contentType.startsWith("image/")) {
responseHeaders.setContentType(MediaType.parseMediaType(contentType));
} else {
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
}
// 设置缓存头以提升性能
responseHeaders.setCacheControl("max-age=3600");
responseHeaders.set("Access-Control-Allow-Origin", "*");
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
} catch (Exception e) {
System.err.println("图片代理失败: " + imageUrl + " - " + e.getMessage());
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}

View File

@@ -0,0 +1,163 @@
package com.tashow.erp.controller;
import com.tashow.erp.model.RakutenProduct;
import com.tashow.erp.model.SearchResult;
import com.tashow.erp.repository.RakutenProductRepository;
import com.tashow.erp.service.Alibaba1688Service;
import com.tashow.erp.service.IRakutenCacheService;
import com.tashow.erp.service.RakutenScrapingService;
import com.tashow.erp.service.impl.Alibaba1688ServiceImpl;
import com.tashow.erp.utils.DataReportUtil;
import com.tashow.erp.utils.ExcelParseUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.QiniuUtil;
import com.tashow.erp.fx.controller.JavaBridge;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.client.RestTemplate;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.stream.Collectors;
import java.util.Base64;
@RestController
@RequestMapping("/api/rakuten")
@Slf4j
public class RakutenController {
@Autowired
private RakutenScrapingService rakutenScrapingService;
@Autowired
private Alibaba1688Service alibaba1688Service;
@Autowired
private IRakutenCacheService rakutenCacheService;
@Autowired
private JavaBridge javaBridge;
@Autowired
private DataReportUtil dataReportUtil;
/**
* 获取乐天商品数据(支持单个店铺名或 Excel 文件上传)
*
* @param file 可选Excel 文件(首列为店铺名)
* @param shopName 可选,单个店铺名
* @param batchId 可选,批次号
* @return JsonData 响应
*/
@PostMapping(value = "/products")
public JsonData getProducts(@RequestParam(value = "file", required = false) MultipartFile file, @RequestParam(value = "shopName", required = false) String shopName, @RequestParam(value = "batchId", required = false) String batchId) {
try {
// 1. 获取店铺名集合(优先 shopName其次 Excel
List<String> shopNames = Optional.ofNullable(shopName).filter(s -> !s.trim().isEmpty()).map(s -> List.of(s.trim())).orElseGet(() -> file != null ? ExcelParseUtil.parseFirstColumn(file) : new ArrayList<>());
if (CollectionUtils.isEmpty(shopNames)) {
return JsonData.buildError("未从 Excel 中解析到店铺名,且 shopName 参数为空");
}
List<RakutenProduct> allProducts = new ArrayList<>();
List<String> skippedShops = new ArrayList<>();
// 2. 遍历店铺,优先缓存,缺失则爬取
for (String currentShopName : shopNames) {
if (rakutenCacheService.hasRecentData(currentShopName)) {
// 从缓存获取
List<RakutenProduct> cached = rakutenCacheService.getProductsByShopName(currentShopName).stream().filter(p -> currentShopName.equals(p.getOriginalShopName())).toList();
rakutenCacheService.updateSpecificProductsSessionId(cached, batchId);
allProducts.addAll(cached);
skippedShops.add(currentShopName);
log.info("使用缓存数据,店铺: {},数量: {}", currentShopName, cached.size());
} else {
// 爬取新数据
log.info("采集新数据: {}", currentShopName);
List<RakutenProduct> fresh = rakutenScrapingService.scrapeProductsWithSearch(currentShopName);
fresh.forEach(p -> p.setOriginalShopName(currentShopName));
allProducts.addAll(fresh);
log.info("采集完成: {}, 数量: {}", currentShopName, fresh.size());
}
}
// 3. 处理新采集的数据,存储并生成 sessionId
List<RakutenProduct> newProducts = allProducts.stream().filter(p -> !skippedShops.contains(p.getOriginalShopName())).toList();
if (!newProducts.isEmpty()) {
// 使用已有 sessionId 保存
rakutenCacheService.saveProductsWithSessionId(newProducts, batchId);
}
// 4. 上报缓存数据使用情况
int cachedCount = allProducts.size() - newProducts.size();
if (cachedCount > 0) {
dataReportUtil.reportDataCollection("RAKUTEN_CACHE", cachedCount, "0");
}
// 5. 如果是单店铺查询,只返回该店铺的商品
List<RakutenProduct> finalProducts = (shopName != null && !shopName.trim().isEmpty()) ? allProducts.stream().filter(p -> shopName.trim().equals(p.getOriginalShopName())).toList() : allProducts;
return JsonData.buildSuccess(Map.of("products", finalProducts, "total", finalProducts.size(), "sessionId", batchId, "skippedShops", skippedShops, "newProductsCount", newProducts.size()));
} catch (Exception e) {
log.error("获取乐天商品失败", e);
return JsonData.buildError("获取乐天商品失败: " + e.getMessage());
}
}
/**
* 1688识图搜索API - 自动保存1688搜索结果
*/
@PostMapping("/search1688")
public JsonData search1688(@RequestBody Map<String, Object> params) {
String imageUrl = (String) params.get("imageUrl");
String sessionId = (String) params.get("sessionId");
try {
SearchResult result = alibaba1688Service.get1688Detail(imageUrl);
rakutenScrapingService.update1688DataByImageUrl(result, sessionId, imageUrl);
return JsonData.buildSuccess(result);
} catch (Exception e) {
log.error("1688识图搜索失败", e);
return JsonData.buildError("搜索失败: " + e.getMessage());
}
}
@GetMapping("/products/latest")
public JsonData getLatestProducts() {
try {
List<Map<String, Object>> products = rakutenScrapingService.getLatestProductsForDisplay();
return JsonData.buildSuccess(Map.of("products", products, "total", products.size()));
} catch (Exception e) {
e.printStackTrace();
log.info("获取最新商品数据失败", e);
return JsonData.buildError("获取最新数据失败: " + e.getMessage());
}
}
@PostMapping("/export-and-save")
public JsonData exportAndSave(@RequestBody Map<String, Object> body) {
try {
@SuppressWarnings("unchecked") List<Map<String, Object>> products = (List<Map<String, Object>>) body.get("products");
if (CollectionUtils.isEmpty(products)) return JsonData.buildError("没有可导出的数据");
boolean skipImages = Optional.ofNullable((Boolean) body.get("skipImages")).orElse(false);
String fileName = Optional.ofNullable((String) body.get("fileName")).filter(name -> !name.trim().isEmpty()).orElse("乐天商品数据_" + java.time.LocalDate.now() + ".xlsx");
String[] headers = {"店铺名", "商品图片", "商品链接", "排名", "商品标题", "价格", "1688识图链接", "1688价格", "1688重量"};
byte[] excelData = com.tashow.erp.utils.ExcelExportUtil.createExcelWithImages("乐天商品数据", headers, products, skipImages ? -1 : 1, skipImages ? null : "imgUrl");
if (excelData == null || excelData.length == 0) return JsonData.buildError("生成Excel失败");
String savedPath = javaBridge.saveExcelFileToDesktop(excelData, fileName);
if (savedPath == null) return JsonData.buildError("保存文件失败");
log.info("导出Excel: {}, 记录数: {}", fileName, products.size());
return JsonData.buildSuccess(Map.of("filePath", savedPath, "fileName", fileName, "recordCount", products.size(), "hasImages", !skipImages));
} catch (Exception e) {
log.error("导出Excel失败", e);
return JsonData.buildError("导出Excel失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,603 @@
package com.tashow.erp.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tashow.erp.entity.UpdateStatusEntity;
import com.tashow.erp.repository.UpdateStatusRepository;
import com.tashow.erp.service.IAuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* Web版本更新控制器
*
* @author Claude
*/
@RestController
@RequestMapping("/api/update")
public class UpdateController {
@Value("${project.version:2.3.6}")
private String currentVersion;
@Value("${project.build.time:}")
private String buildTime;
private static final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private IAuthService authService;
@Autowired
private UpdateStatusRepository updateStatusRepository;
// 下载进度跟踪
private volatile int downloadProgress = 0;
private volatile long downloadedBytes = 0;
private volatile long totalBytes = 0;
private volatile String downloadStatus = "ready"; // ready, downloading, completed, failed, cancelled
private volatile String downloadSpeed = "0 KB/s";
private volatile long downloadStartTime = 0;
private volatile boolean downloadCancelled = false;
/**
* 获取当前版本号
*
* @return 当前版本号
*/
@GetMapping("/version")
public Map<String, Object> getVersion() {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("currentVersion", currentVersion);
result.put("buildTime", buildTime);
return result;
}
/**
* 检查版本更新
*
* @return 版本更新信息
*/
@GetMapping("/check")
public Map<String, Object> checkUpdate() {
Map<String, Object> result = new HashMap<>();
try {
String response = authService.checkVersion(currentVersion);
// 解析JSON响应
JsonNode responseNode = objectMapper.readTree(response);
result.put("success", true);
result.put("currentVersion", currentVersion);
// 检查响应格式并提取数据
JsonNode dataNode = null;
dataNode = responseNode.get("data");
// 直接使用服务器返回的needUpdate字段
boolean needUpdate = dataNode.has("needUpdate") &&
dataNode.get("needUpdate").asBoolean();
// 添加跳过版本信息,让前端自己判断
String skippedVersion = updateStatusRepository.findByKeyName("skippedUpdateVersion")
.map(entity -> entity.getValueData()).orElse(null);
result.put("skippedVersion", skippedVersion);
result.put("needUpdate", needUpdate);
// 获取最新版本号
if (dataNode.has("latestVersion")) {
result.put("latestVersion", dataNode.get("latestVersion").asText());
}
if (needUpdate) {
if (dataNode.has("downloadUrl")) {
String downloadUrl = dataNode.get("downloadUrl").asText();
result.put("downloadUrl", downloadUrl);
saveUpdateInfo("downloadUrl", downloadUrl);
}
if (dataNode.has("updateTime")) {
// 转换时间戳为可读格式
long updateTime = dataNode.get("updateTime").asLong();
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String releaseDate = sdf.format(new java.util.Date(updateTime));
result.put("releaseDate", releaseDate);
saveUpdateInfo("releaseDate", releaseDate);
}
if (dataNode.has("updateNotes")) {
String updateNotes = dataNode.get("updateNotes").asText();
result.put("updateNotes", updateNotes);
saveUpdateInfo("updateNotes", updateNotes);
}
if (dataNode.has("fileSize")) {
String fileSize = dataNode.get("fileSize").asText();
result.put("fileSize", fileSize);
saveUpdateInfo("fileSize", fileSize);
}
if (dataNode.has("latestVersion")) {
String latestVersion = dataNode.get("latestVersion").asText();
saveUpdateInfo("latestVersion", latestVersion);
}
} else {
// 如果不需要更新,清理之前保存的更新信息
clearUpdateInfo();
}
// 从SQLite读取之前保存的更新信息
result.putAll(getStoredUpdateInfo());
} catch (Exception e) {
result.put("success", false);
result.put("message", "版本检查失败:" + e.getMessage());
authService.reportError("UPDATE_CHECK_ERROR", "Web版本检查失败", e);
}
return result;
}
/**
* 保存更新信息到SQLite
*/
private void saveUpdateInfo(String key, String value) {
try {
UpdateStatusEntity entity = updateStatusRepository.findByKeyName(key)
.orElse(new UpdateStatusEntity());
entity.setKeyName(key);
entity.setValueData(value);
updateStatusRepository.save(entity);
} catch (Exception e) {
System.err.println("保存更新信息失败: " + key + " = " + value + ", 错误: " + e.getMessage());
}
}
/**
* 从SQLite获取存储的更新信息
*/
private Map<String, Object> getStoredUpdateInfo() {
Map<String, Object> info = new HashMap<>();
try {
String[] keys = {"downloadUrl", "releaseDate", "updateNotes", "fileSize", "latestVersion"};
for (String key : keys) {
updateStatusRepository.findByKeyName(key).ifPresent(entity ->
info.put(key, entity.getValueData())
);
}
} catch (Exception e) {
System.err.println("读取更新信息失败: " + e.getMessage());
}
return info;
}
/**
* 清理更新信息
*/
private void clearUpdateInfo() {
try {
String[] keys = {"downloadUrl", "releaseDate", "updateNotes", "fileSize", "latestVersion"};
for (String key : keys) {
updateStatusRepository.findByKeyName(key).ifPresent(entity ->
updateStatusRepository.delete(entity)
);
}
} catch (Exception e) {
System.err.println("清理更新信息失败: " + e.getMessage());
}
}
/**
* 获取下载进度
*
* @return 下载进度信息
*/
@GetMapping("/progress")
public Map<String, Object> getDownloadProgress() {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("progress", downloadProgress);
result.put("status", downloadStatus);
result.put("downloadedBytes", downloadedBytes);
result.put("totalBytes", totalBytes);
result.put("downloadSpeed", downloadSpeed);
result.put("downloadedMB", String.format("%.1f", downloadedBytes / 1024.0 / 1024.0));
result.put("totalMB", String.format("%.1f", totalBytes / 1024.0 / 1024.0));
return result;
}
/**
* 重置下载状态
*
* @return 重置结果
*/
@PostMapping("/reset")
public Map<String, Object> resetDownloadStatus() {
Map<String, Object> result = new HashMap<>();
downloadProgress = 0;
downloadedBytes = 0;
totalBytes = 0;
downloadStatus = "ready";
downloadSpeed = "0 KB/s";
downloadStartTime = 0;
downloadCancelled = false;
tempUpdateFilePath = null;
result.put("success", true);
result.put("message", "下载状态已重置");
return result;
}
/**
* 取消下载
*
* @return 取消结果
*/
@PostMapping("/cancel")
public Map<String, Object> cancelDownload() {
Map<String, Object> result = new HashMap<>();
if ("downloading".equals(downloadStatus)) {
downloadCancelled = true;
downloadStatus = "cancelled";
downloadSpeed = "已取消";
result.put("success", true);
result.put("message", "下载已取消");
} else if ("completed".equals(downloadStatus)) {
// 下载完成时点击取消,不删除文件,只是标记为稍后更新
result.put("success", true);
result.put("message", "已设置为稍后更新,文件保留");
System.out.println("用户选择稍后更新,文件路径: " + tempUpdateFilePath);
} else {
result.put("success", false);
result.put("message", "无效的操作状态");
}
return result;
}
/**
* 完全清除更新文件和状态
*
* @return 清除结果
*/
@PostMapping("/clear")
public Map<String, Object> clearUpdateFiles() {
Map<String, Object> result = new HashMap<>();
if (tempUpdateFilePath != null) {
try {
java.io.File tempFile = new java.io.File(tempUpdateFilePath);
if (tempFile.exists()) {
tempFile.delete();
System.out.println("已删除更新文件: " + tempUpdateFilePath);
}
} catch (Exception e) {
System.err.println("删除临时文件失败: " + e.getMessage());
}
}
// 重置状态
downloadProgress = 0;
downloadedBytes = 0;
totalBytes = 0;
downloadStatus = "ready";
downloadSpeed = "0 KB/s";
downloadCancelled = false;
tempUpdateFilePath = null;
result.put("success", true);
result.put("message", "更新文件和状态已清除");
return result;
}
/**
* 验证更新文件是否存在
*
* @return 验证结果
*/
@GetMapping("/verify-file")
public Map<String, Object> verifyUpdateFile() {
Map<String, Object> result = new HashMap<>();
try {
boolean fileExists = false;
String filePath = "";
if (tempUpdateFilePath != null) {
java.io.File updateFile = new java.io.File(tempUpdateFilePath);
fileExists = updateFile.exists();
filePath = tempUpdateFilePath;
if (fileExists) {
result.put("fileSize", updateFile.length());
result.put("lastModified", new java.util.Date(updateFile.lastModified()));
}
}
result.put("success", true);
result.put("fileExists", fileExists);
result.put("filePath", filePath);
result.put("downloadStatus", downloadStatus);
System.out.println("验证更新文件: " + filePath + ", 存在: " + fileExists);
} catch (Exception e) {
result.put("success", false);
result.put("message", "验证文件失败:" + e.getMessage());
result.put("fileExists", false);
}
return result;
}
/**
* 用户确认后执行安装
*
* @return 安装结果
*/
@PostMapping("/install")
public Map<String, Object> installUpdate() {
Map<String, Object> result = new HashMap<>();
if (tempUpdateFilePath == null || !new java.io.File(tempUpdateFilePath).exists()) {
result.put("success", false);
result.put("message", "更新文件不存在,请重新下载");
return result;
}
try {
result.put("success", true);
result.put("message", "开始安装更新...");
// 异步执行安装,避免阻塞响应
new Thread(() -> {
String updateScript = createUpdateScript(null, tempUpdateFilePath);
if (updateScript != null) {
executeUpdateAndExit(updateScript);
}
}).start();
} catch (Exception e) {
result.put("success", false);
result.put("message", "启动安装失败:" + e.getMessage());
}
return result;
}
/**
* 自动更新:下载、替换、重启
*
* @return 更新结果
*/
@PostMapping("/auto-update")
public Map<String, Object> autoUpdate(@RequestBody Map<String, String> request) {
Map<String, Object> result = new HashMap<>();
try {
String downloadUrl = request.get("downloadUrl");
result.put("success", true);
result.put("message", "开始自动更新...");
result.put("downloadUrl", downloadUrl);
String finalDownloadUrl = downloadUrl;
new Thread(() -> {
performUpdate(finalDownloadUrl);
}).start();
} catch (Exception e) {
result.put("success", false);
result.put("message", "启动更新失败:" + e.getMessage());
}
return result;
}
/**
* 执行更新过程
*/
private void performUpdate(String downloadUrl) {
String tempUpdateFile = downloadUpdate(downloadUrl);
if (tempUpdateFile == null) {
downloadStatus = "failed";
return;
}
// 下载完成后不自动重启,等待用户确认
downloadStatus = "completed";
System.out.println("下载完成,等待用户确认安装...");
// 将下载文件路径保存,供后续安装使用
this.tempUpdateFilePath = tempUpdateFile;
}
private String tempUpdateFilePath = null;
/**
* 下载更新文件
*/
private String downloadUpdate(String downloadUrl) {
try {
downloadStatus = "downloading";
downloadProgress = 0;
downloadedBytes = 0;
downloadStartTime = System.currentTimeMillis();
URL url = new URL(downloadUrl);
URLConnection connection = url.openConnection();
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
connection.setConnectTimeout(10000);
connection.setReadTimeout(30000);
// 获取文件大小
totalBytes = connection.getContentLength();
if (totalBytes <= 0) {
totalBytes = 70 * 1024 * 1024; // 默认70MB
}
String tempDir = System.getProperty("java.io.tmpdir");
String updateFileName = "erp_update_" + System.currentTimeMillis() + ".exe";
String tempUpdatePath = Paths.get(tempDir, updateFileName).toString();
System.out.println("开始下载更新文件,大小: " + (totalBytes / 1024 / 1024) + "MB");
try (InputStream inputStream = connection.getInputStream();
FileOutputStream outputStream = new FileOutputStream(tempUpdatePath)) {
byte[] buffer = new byte[8192];
int bytesRead;
long lastSpeedUpdate = System.currentTimeMillis();
long lastDownloadedBytes = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
if (downloadCancelled) {
System.out.println("下载已取消,停止下载进程");
downloadStatus = "cancelled";
downloadSpeed = "已取消";
outputStream.close();
try {
java.io.File partialFile = new java.io.File(tempUpdatePath);
if (partialFile.exists()) {
partialFile.delete();
}
} catch (Exception e) {
System.err.println("删除部分下载文件失败: " + e.getMessage());
}
return null;
}
outputStream.write(buffer, 0, bytesRead);
downloadedBytes += bytesRead;
// 计算下载进度
downloadProgress = (int) ((downloadedBytes * 100) / totalBytes);
// 每秒计算一次下载速度
long currentTime = System.currentTimeMillis();
if (currentTime - lastSpeedUpdate >= 1000) {
long speedBytes = downloadedBytes - lastDownloadedBytes;
double speedKB = speedBytes / 1024.0;
if (speedKB >= 1024) {
downloadSpeed = String.format("%.1f MB/s", speedKB / 1024.0);
} else {
downloadSpeed = String.format("%.1f KB/s", speedKB);
}
lastSpeedUpdate = currentTime;
lastDownloadedBytes = downloadedBytes;
System.out.println(String.format("下载进度: %d%% (%d/%d MB) 速度: %s",
downloadProgress,
downloadedBytes / 1024 / 1024,
totalBytes / 1024 / 1024,
downloadSpeed));
}
// 防止阻塞UI线程
if (downloadedBytes % (1024 * 1024) == 0) { // 每MB休息一下
Thread.sleep(10);
}
}
downloadStatus = "completed";
downloadProgress = 100;
downloadSpeed = "完成";
System.out.println("下载完成: " + (downloadedBytes / 1024 / 1024) + "MB");
}
return tempUpdatePath;
} catch (Exception e) {
downloadStatus = "failed";
downloadSpeed = "失败";
System.err.println("下载失败: " + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
* 创建更新脚本
*/
private String createUpdateScript(String currentExePath, String tempUpdateFile) {
try {
String tempDir = System.getProperty("java.io.tmpdir");
String scriptPath = Paths.get(tempDir, "update_erp.bat").toString();
File currentDir = new File(System.getProperty("user.dir"));
File targetExe = new File(currentDir, "erpClient.exe");
String targetPath = targetExe.getAbsolutePath();
StringBuilder script = new StringBuilder();
script.append("@echo off\r\n");
script.append("chcp 65001 > nul\r\n");
script.append("echo 正在等待程序完全退出...\r\n");
script.append("timeout /t 1 /nobreak > nul\r\n");
script.append("echo 开始更新程序文件...\r\n");
script.append("if exist \"").append(targetPath).append("\" (");
script.append(" move \"").append(targetPath).append("\" \"").append(targetPath).append(".backup\"");
script.append(" )\r\n");
script.append("move \"").append(tempUpdateFile).append("\" \"").append(targetPath).append("\"\r\n");
script.append("if exist \"").append(targetPath).append("\" (\r\n");
script.append(" echo 更新成功程序将在1秒后重新启动...\r\n");
script.append(" timeout /t 1 /nobreak > nul\r\n");
script.append(" start \"\" \"").append(targetPath).append("\"\r\n");
script.append(" if exist \"").append(targetPath).append(".backup\" del \"").append(targetPath).append(".backup\"\r\n");
script.append(") else (\r\n");
script.append(" echo 更新失败!正在恢复原版本...\r\n");
script.append(" if exist \"").append(targetPath).append(".backup\" (\r\n");
script.append(" move \"").append(targetPath).append(".backup\" \"").append(targetPath).append("\"\r\n");
script.append(" echo 已恢复原版本程序将在1秒后重新启动...\r\n");
script.append(" timeout /t 1 /nobreak > nul\r\n");
script.append(" start \"\" \"").append(targetPath).append("\"\r\n");
script.append(" )\r\n");
script.append(")\r\n");
script.append("echo 更新操作完成!\r\n");
script.append("timeout /t 1 /nobreak > nul\r\n");
script.append("(goto) 2>nul & del \"%~f0\" & exit\r\n");
Files.write(Paths.get(scriptPath), script.toString().getBytes("GBK"));
return scriptPath;
} catch (Exception e) {
return null;
}
}
/**
* 执行更新脚本并退出程序
*/
private void executeUpdateAndExit(String scriptPath) {
try {
System.out.println("下载完成!正在准备更新...");
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", scriptPath);
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
pb.redirectError(ProcessBuilder.Redirect.DISCARD);
pb.start();
System.out.println("更新程序已启动,当前程序即将退出...");
Thread.sleep(1000);
Runtime.getRuntime().halt(0);
} catch (Exception e) {
Runtime.getRuntime().halt(1);
}
}
/**
* 保存跳过的版本
*/
@PostMapping("/skip-version")
public Map<String, Object> saveSkippedVersion(@RequestBody Map<String, String> request) {
Map<String, Object> result = new HashMap<>();
try {
saveUpdateInfo("skippedUpdateVersion", request.get("version"));
result.put("success", true);
} catch (Exception e) {
result.put("success", false);
}
return result;
}
}

View File

@@ -0,0 +1,59 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 1688产品实体类
*/
@Entity
@Table(name = "alibaba_1688_products")
@Data
public class Alibaba1688ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product_id", unique = true, nullable = false)
private String productId;
@Column(name = "title", length = 1000)
private String title;
@Column(name = "price")
private String price;
@Column(name = "image_url", length = 1000)
private String imageUrl;
@Column(name = "product_url", length = 1000)
private String productUrl;
@Column(name = "supplier")
private String supplier;
@Column(name = "category")
private String category;
@Column(name = "min_order")
private String minOrder;
@Column(name = "trade_assurance")
private String tradeAssurance;
@Column(name = "session_id")
private String sessionId;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,64 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* Amazon产品缓存实体类
*/
@Entity
@Table(name = "amazon_products")
@Data
public class AmazonProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String asin;
@Column(name = "title", length = 1000)
private String title;
@Column(name = "price")
private String price;
@Column(name = "image_url", length = 1000)
private String imageUrl;
@Column(name = "product_url", length = 1000)
private String productUrl;
@Column(name = "brand")
private String brand;
@Column(name = "category")
private String category;
@Column(name = "rating")
private String rating;
@Column(name = "review_count")
private String reviewCount;
@Column(name = "availability")
private String availability;
@Column(name = "seller")
private String seller;
@Column(name = "session_id")
private String sessionId;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,38 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 认证令牌实体类
*/
@Entity
@Table(name = "auth_tokens")
@Data
public class AuthTokenEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "service_name", unique = true, nullable = false)
private String serviceName;
@Column(name = "token", nullable = false)
private String token;
@Column(name = "expire_time")
private LocalDateTime expireTime;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,38 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 斑马订单实体类
*/
@Entity
@Table(name = "banma_orders")
@Data
public class BanmaOrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "tracking_number", unique = true, nullable = false)
private String trackingNumber;
@Column(name = "order_data", columnDefinition = "TEXT")
private String orderData;
@Column(name = "session_id")
private String sessionId;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,38 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 缓存数据实体类
*/
@Entity
@Table(name = "cache_data")
@Data
public class CacheDataEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "cache_key", unique = true, nullable = false)
private String cacheKey;
@Column(name = "cache_value", columnDefinition = "TEXT")
private String cacheValue;
@Column(name = "expire_time")
private LocalDateTime expireTime;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,86 @@
package com.tashow.erp.entity;
import com.fasterxml.jackson.annotation.JsonRawValue;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 乐天产品缓存实体类
*/
@Entity
@Table(name = "rakuten_products")
@Data
public class RakutenProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "original_shop_name")
private String originalShopName;
@Column(name = "shop_name")
private String shopName;
@Column(name = "product_url", length = 1000)
private String productUrl;
@Column(name = "img_url", length = 1000)
private String imgUrl;
@Column(name = "product_title", length = 500)
private String productTitle;
@Column(name = "product_name", length = 500)
private String productName;
@Column(name = "price")
private String price;
@Column(name = "ranking")
private String ranking;
@Column(name = "price_1688")
private String price1688;
@Column(name = "detail_url_1688", length = 1000)
private String detailUrl1688;
@Column(name = "image_1688_url", length = 1000)
private String image1688Url;
@Column(name = "map_recognition_link", length = 1000)
private String mapRecognitionLink; // 1688识图链接
@Column(name = "freight")
private Double freight; // 运费
@Column(name = "median")
private Double median; // 中位价格
@Column(name = "weight")
private String weight; // 重量
@Column(name = "sku_price_json", columnDefinition = "JSON")
private String skuPriceJson; // SKU价格JSON字符串
@Column(name = "session_id")
private String sessionId; // 用于标识一次导入会话
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,81 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
/**
* 更新状态实体
*/
@Entity
@Table(name = "update_status")
public class UpdateStatusEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "key_name", unique = true)
private String keyName;
@Column(name = "value_data", columnDefinition = "TEXT")
private String valueData;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getKeyName() {
return keyName;
}
public void setKeyName(String keyName) {
this.keyName = keyName;
}
public String getValueData() {
return valueData;
}
public void setValueData(String valueData) {
this.valueData = valueData;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,65 @@
package com.tashow.erp.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 斑马订单缓存实体类
*/
@Entity
@Table(name = "zebra_orders")
@Data
public class ZebraOrderEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_id")
private String orderId;
@Column(name = "product_name", length = 500)
private String productName;
@Column(name = "quantity")
private Integer quantity;
@Column(name = "price_jpy")
private String priceJpy;
@Column(name = "price_cny")
private String priceCny;
@Column(name = "image_url", length = 1000)
private String imageUrl;
@Column(name = "customer_name")
private String customerName;
@Column(name = "order_status")
private String orderStatus;
@Column(name = "order_date")
private String orderDate;
@Column(name = "shipping_address", length = 1000)
private String shippingAddress;
@Column(name = "tracking_number")
private String trackingNumber;
@Column(name = "session_id")
private String sessionId;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,79 @@
package com.tashow.erp.fx.controller;
import lombok.extern.slf4j.Slf4j;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@Slf4j
public class JavaBridge {
/**
* 直接保存字节数组为Excel文件到桌面纯 Spring Boot 环境,无文件对话框)
*/
public String saveExcelFileToDesktop(byte[] data, String fileName) {
try {
if (data == null || data.length == 0) {
log.warn("文件数据为空,无法保存文件");
return null;
}
String userHome = System.getProperty("user.home");
File desktop = new File(userHome, "Desktop");
if (!desktop.exists()) {
// 回退到用户目录
desktop = new File(userHome);
}
File file = new File(desktop, fileName);
int counter = 1;
if (fileName != null && fileName.contains(".")) {
String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
String extension = fileName.substring(fileName.lastIndexOf('.'));
while (file.exists()) {
file = new File(desktop, baseName + "_" + counter + extension);
counter++;
}
} else {
while (file.exists()) {
file = new File(desktop, fileName + "_" + counter);
counter++;
}
}
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(data);
fos.flush();
}
String filePath = file.getAbsolutePath();
log.info("Excel文件已保存: {}", filePath);
return filePath;
} catch (IOException e) {
log.error("保存Excel文件失败: {}", e.getMessage(), e);
return null;
}
}
/**
* 复制文本到系统剪贴板
*/
public boolean copyToClipboard(String text) {
try {
if (text == null || text.trim().isEmpty()) {
return false;
}
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
StringSelection selection = new StringSelection(text);
clipboard.setContents(selection, null);
return true;
} catch (Exception e) {
log.error("复制到剪贴板失败: {}", e.getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,43 @@
// package com.tashow.erp.fx.controller;
//
// 已转为纯 Spring Boot不再包含 JavaFX 控制器与视图逻辑。如需恢复 FX请取消注释并恢复依赖。
//
// import javafx.application.Platform;
// import javafx.fxml.FXML;
// import javafx.fxml.Initializable;
// import javafx.scene.layout.BorderPane;
// import javafx.scene.web.WebEngine;
// import javafx.scene.web.WebView;
// import lombok.extern.slf4j.Slf4j;
// import net.rgielen.fxweaver.core.FxmlView;
// import org.springframework.stereotype.Component;
// import netscape.javascript.JSObject;
//
// import java.net.URL;
// import java.util.ResourceBundle;
//
// @Slf4j
// @Component
// @FxmlView("/static/fxml/Main.fxml")
// public class MainCtrl implements Initializable {
// @FXML
// public BorderPane rootPane;
// @FXML
// public WebView webView;
// private WebEngine webEngine;
// @Override
// public void initialize(URL location, ResourceBundle resources) {
// if (Platform.isFxApplicationThread()) {
// initWebView();
// } else {
// Platform.runLater(this::initWebView);
// }
// }
//
// private void initWebView() {
// webEngine = webView.getEngine();
// webEngine.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...");
// webEngine.setJavaScriptEnabled(true);
// webEngine.load("http://localhost:8081/html/erp-dashboard.html");
// }
// }

View File

@@ -0,0 +1,11 @@
// package com.tashow.erp.fx.view;
//
// 已转为纯 Spring Boot移除 FX 视图。
// import javafx.scene.Parent;
// import net.rgielen.fxweaver.core.FxmlView;
// import org.springframework.stereotype.Component;
//
// @Component
// @FxmlView("/static/fxml/Main.fxml")
// public class MainView {
// }

View File

@@ -0,0 +1,25 @@
package com.tashow.erp.model;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 1688爬取风控监控数据表实体类
*/
@Data
public class ClientAlibaba1688Monitor {
private Long id;
private String clientId;
private String ipAddress;
private String eventType;
private Long eventTime;
private Long duration;
private LocalDateTime createTime;
private String remark;
}

View File

@@ -0,0 +1,23 @@
package com.tashow.erp.model;
import lombok.Data;
/**
* 监控数据类
*/
@Data
public class MonitoringData {
private String ipAddress;
private MonitorEventType eventType;
private long eventTime;
private long duration;
/**
* 风控事件类型
*/
public enum MonitorEventType {
MOBILE_FIRST_ACCESS, // 移动端首次访问
MOBILE_BLOCKED, // 移动端被风控
DESKTOP_BLOCKED // 电脑端被风控
}
}

View File

@@ -0,0 +1,16 @@
package com.tashow.erp.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
/**
* 商品信息类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProductInfo {
private String price;
private String weight;
}

View File

@@ -0,0 +1,49 @@
package com.tashow.erp.model;
import lombok.Data;
import java.util.List;
/**
* 乐天商品模型类
*/
@Data
public class RakutenProduct {
private String productUrl;
private String shopName;
private String imgUrl;
private String productTitle;
private String productName;
private String price;
private String ranking;
// 1688相关字段
private String price1688;
private String weight1688;
private String detailUrl1688;
// 新增:用于导入导出与展示
private String originalShopName;
private String image1688Url;
private String mapRecognitionLink;
private Double freight;
private Double median;
private String weight;
@Override
public String toString() {
return "RakutenProduct{" +
"productName='" + productName + '\'' +
", productTitle='" + productTitle + '\'' +
", price='" + price + '\'' +
", imageUrl1='" + imgUrl+ '\'' +
", productUrl='" + productUrl + '\'' +
", shopName='" + shopName + '\'' +
", price1688='" + price1688 + '\'' +
", weight1688='" + weight1688 + '\'' +
", detailUrl1688='" + detailUrl1688 + '\'' +
'}';
}
}

View File

@@ -0,0 +1,29 @@
package com.tashow.erp.model;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 1688识图搜索结果
*/
@Data
public class SearchResult {
//运费
private Double freight;
//识图链接
private String mapRecognitionLink;
//sku价格
Map<Double,String> skuPrice;
//价格中间值
private Double median;
String weight;
}

View File

@@ -0,0 +1,58 @@
package com.tashow.erp.repository;
import com.tashow.erp.entity.Alibaba1688ProductEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* 1688产品Repository
*/
@Repository
public interface Alibaba1688ProductRepository extends JpaRepository<Alibaba1688ProductEntity, Long> {
/**
* 根据产品ID查找产品
*/
Optional<Alibaba1688ProductEntity> findByProductId(String productId);
/**
* 根据会话ID查找产品分页
*/
Page<Alibaba1688ProductEntity> findBySessionIdOrderByCreatedAtDesc(String sessionId, Pageable pageable);
/**
* 根据会话ID查找产品
*/
List<Alibaba1688ProductEntity> findBySessionIdOrderByCreatedAtDesc(String sessionId);
/**
* 根据会话ID删除产品
*/
@Modifying
@Transactional
void deleteBySessionId(String sessionId);
/**
* 清理过期数据7天前的数据
*/
@Modifying
@Transactional
@Query("DELETE FROM Alibaba1688ProductEntity a WHERE a.createdAt < :expireTime")
void deleteExpiredData(@Param("expireTime") LocalDateTime expireTime);
/**
* 获取最新的会话ID
*/
@Query("SELECT DISTINCT a.sessionId FROM Alibaba1688ProductEntity a ORDER BY a.createdAt DESC")
List<String> findRecentSessionIds(Pageable pageable);
}

View File

@@ -0,0 +1,73 @@
package com.tashow.erp.repository;
import com.tashow.erp.entity.AmazonProductEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* Amazon产品Repository
*/
@Repository
public interface AmazonProductRepository extends JpaRepository<AmazonProductEntity, Long> {
/**
* 根据ASIN查找产品取最新的一条
*/
@Query(value = "SELECT * FROM amazon_products WHERE asin = :asin ORDER BY created_at DESC LIMIT 1", nativeQuery = true)
Optional<AmazonProductEntity> findByAsin(@Param("asin") String asin);
/**
* 根据会话ID查找产品分页
*/
Page<AmazonProductEntity> findBySessionIdOrderByCreatedAtDesc(String sessionId, Pageable pageable);
/**
* 根据会话ID查找产品
*/
List<AmazonProductEntity> findBySessionIdOrderByCreatedAtDesc(String sessionId);
/**
* 根据会话ID删除产品
*/
@Modifying
@Transactional
void deleteBySessionId(String sessionId);
/**
* 清理过期数据7天前的数据
*/
@Modifying
@Transactional
@Query("DELETE FROM AmazonProductEntity a WHERE a.createdAt < :expireTime")
void deleteExpiredData(@Param("expireTime") LocalDateTime expireTime);
/**
* 获取最新的会话ID
*/
@Query("SELECT DISTINCT a.sessionId FROM AmazonProductEntity a ORDER BY a.createdAt DESC")
List<String> findRecentSessionIds(Pageable pageable);
/**
* 获取最新会话的产品数据(只返回最后一次采集的结果)
*/
@Query(value = "SELECT * FROM amazon_products WHERE session_id = (SELECT session_id FROM amazon_products ORDER BY created_at DESC LIMIT 1) ORDER BY created_at ASC, id ASC", nativeQuery = true)
List<AmazonProductEntity> findLatestProducts();
/**
* 删除指定ASIN在指定时间后的数据用于清理12小时内重复
*/
@Modifying
@Transactional
@Query("DELETE FROM AmazonProductEntity a WHERE a.asin = :asin AND a.createdAt >= :cutoffTime")
void deleteByAsinAndCreatedAtAfter(@Param("asin") String asin, @Param("cutoffTime") LocalDateTime cutoffTime);
}

View File

@@ -0,0 +1,44 @@
package com.tashow.erp.repository;
import com.tashow.erp.entity.AuthTokenEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
/**
* 认证令牌Repository
*/
@Repository
public interface AuthTokenRepository extends JpaRepository<AuthTokenEntity, Long> {
/**
* 根据服务名查找有效的认证令牌
*/
Optional<AuthTokenEntity> findByServiceNameAndExpireTimeAfter(String serviceName, LocalDateTime currentTime);
/**
* 根据服务名查找认证令牌(不考虑过期时间)
*/
Optional<AuthTokenEntity> findByServiceName(String serviceName);
/**
* 清理过期令牌
*/
@Modifying
@Transactional
@Query("DELETE FROM AuthTokenEntity a WHERE a.expireTime < :expireTime")
void deleteExpiredTokens(@Param("expireTime") LocalDateTime expireTime);
/**
* 根据服务名删除令牌
*/
@Modifying
@Transactional
void deleteByServiceName(String serviceName);
}

View File

@@ -0,0 +1,89 @@
package com.tashow.erp.repository;
import com.tashow.erp.entity.BanmaOrderEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* 斑马订单Repository
*/
@Repository
public interface BanmaOrderRepository extends JpaRepository<BanmaOrderEntity, Long> {
/**
* 根据追踪号查找订单
*/
Optional<BanmaOrderEntity> findByTrackingNumber(String trackingNumber);
/**
* 根据会话ID查找订单分页
*/
Page<BanmaOrderEntity> findBySessionIdOrderByCreatedAtDesc(String sessionId, Pageable pageable);
/**
* 根据会话ID查找订单
*/
List<BanmaOrderEntity> findBySessionIdOrderByCreatedAtDesc(String sessionId);
/**
* 根据会话ID删除订单
*/
@Modifying
@Transactional
void deleteBySessionId(String sessionId);
/**
* 清理过期数据7天前的数据
*/
@Modifying
@Transactional
@Query("DELETE FROM BanmaOrderEntity b WHERE b.createdAt < :expireTime")
void deleteExpiredData(@Param("expireTime") LocalDateTime expireTime);
/**
* 获取最新的会话ID
*/
@Query("SELECT DISTINCT b.sessionId FROM BanmaOrderEntity b ORDER BY b.createdAt DESC")
List<String> findRecentSessionIds(Pageable pageable);
/**
* 查找所有有效订单(按更新时间倒序)
*/
List<BanmaOrderEntity> findAllByOrderByUpdatedAtDesc();
/**
* 获取最新会话的订单数据(只返回最后一次采集的结果)
*/
@Query(value = "SELECT * FROM banma_orders WHERE session_id = (SELECT session_id FROM banma_orders ORDER BY created_at DESC LIMIT 1) ORDER BY updated_at ASC, id ASC", nativeQuery = true)
List<BanmaOrderEntity> findLatestOrders();
/**
* 删除指定追踪号在指定时间后的数据用于清理12小时内重复
*/
@Modifying
@Transactional
@Query("DELETE FROM BanmaOrderEntity b WHERE b.trackingNumber = :trackingNumber AND b.createdAt >= :cutoffTime")
void deleteByTrackingNumberAndCreatedAtAfter(@Param("trackingNumber") String trackingNumber, @Param("cutoffTime") LocalDateTime cutoffTime);
/**
* 检查追踪号是否有12小时内的缓存数据
*/
@Query("SELECT CASE WHEN COUNT(b) > 0 THEN true ELSE false END FROM BanmaOrderEntity b WHERE b.trackingNumber = :trackingNumber AND b.createdAt > :cutoffTime")
boolean existsByTrackingNumberAndCreatedAtAfter(@Param("trackingNumber") String trackingNumber, @Param("cutoffTime") LocalDateTime cutoffTime);
/**
* 根据追踪号获取最新的订单数据
*/
@Query("SELECT b FROM BanmaOrderEntity b WHERE b.trackingNumber = :trackingNumber ORDER BY b.createdAt DESC")
Optional<BanmaOrderEntity> findLatestByTrackingNumber(@Param("trackingNumber") String trackingNumber);
}

View File

@@ -0,0 +1,33 @@
package com.tashow.erp.repository;
import com.tashow.erp.entity.CacheDataEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
/**
* 缓存数据Repository
*/
@Repository
public interface CacheDataRepository extends JpaRepository<CacheDataEntity, Long> {
/**
* 根据缓存键查找缓存数据(不考虑过期时间)
*/
Optional<CacheDataEntity> findByCacheKey(String cacheKey);
/**
* 根据缓存键删除数据
*/
@Modifying
@Transactional
void deleteByCacheKey(String cacheKey);
}

View File

@@ -0,0 +1,99 @@
package com.tashow.erp.repository;
import com.tashow.erp.entity.RakutenProductEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface RakutenProductRepository extends JpaRepository<RakutenProductEntity, Long> {
/**
* 根据会话ID查找产品分页
*/
Page<RakutenProductEntity> findBySessionIdOrderByCreatedAtAscIdAsc(String sessionId, Pageable pageable);
/**
* 根据会话ID查找产品
*/
List<RakutenProductEntity> findBySessionIdOrderByCreatedAtAscIdAsc(String sessionId);
/**
* 根据会话ID删除产品
*/
@Modifying
@Transactional
void deleteBySessionId(String sessionId);
/**
* 清理过期数据7天前的数据
*/
@Modifying
@Transactional
@Query("DELETE FROM RakutenProductEntity r WHERE r.createdAt < :expireTime")
void deleteExpiredData(@Param("expireTime") LocalDateTime expireTime);
/**
* 根据原始店铺名查找产品按创建时间和ID升序排列与最新数据查询一致
*/
List<RakutenProductEntity> findByOriginalShopNameOrderByCreatedAtAscIdAsc(String originalShopName);
/**
* 检查指定店铺在指定时间后是否有数据
*/
boolean existsByOriginalShopNameAndCreatedAtAfter(String originalShopName, LocalDateTime sinceTime);
/**
* 获取最新的会话ID
*/
@Query("SELECT DISTINCT r.sessionId FROM RakutenProductEntity r ORDER BY r.createdAt DESC")
List<String> findRecentSessionIds(Pageable pageable);
/**
* 获取最新会话的产品数据(只返回最后一次采集的结果)
*/
@Query(value = "SELECT * FROM rakuten_products WHERE session_id = (SELECT session_id FROM rakuten_products ORDER BY created_at LIMIT 1) ORDER BY created_at ASC, id ASC", nativeQuery = true)
List<RakutenProductEntity> findLatestProducts();
/**
* 删除指定商品URL在指定时间后的数据用于清理12小时内重复
*/
@Modifying
@Transactional
@Query("DELETE FROM RakutenProductEntity r WHERE r.productUrl = :productUrl AND r.createdAt >= :cutoffTime")
void deleteByProductUrlAndCreatedAtAfter(@Param("productUrl") String productUrl, @Param("cutoffTime") LocalDateTime cutoffTime);
/**
* 删除指定店铺在指定时间后的数据用于清理12小时内重复
*/
@Modifying
@Transactional
@Query("DELETE FROM RakutenProductEntity r WHERE r.originalShopName = :shopName AND r.createdAt >= :cutoffTime")
void deleteByOriginalShopNameAndCreatedAtAfter(@Param("shopName") String shopName, @Param("cutoffTime") LocalDateTime cutoffTime);
/**
* 删除指定店铺在指定时间前的数据用于清理12小时之前的旧数据
*/
@Modifying
@Transactional
@Query("DELETE FROM RakutenProductEntity r WHERE r.originalShopName = :shopName AND r.createdAt < :cutoffTime")
void deleteByOriginalShopNameAndCreatedAtBefore(@Param("shopName") String shopName, @Param("cutoffTime") LocalDateTime cutoffTime);
/**
* 根据产品URL查找产品用于1688数据更新
*/
List<RakutenProductEntity> findByProductUrl(String productUrl);
/**
* 根据产品URL列表查找产品
*/
List<RakutenProductEntity> findByProductUrlIn(List<String> productUrls);
}

View File

@@ -0,0 +1,15 @@
package com.tashow.erp.repository;
import com.tashow.erp.entity.UpdateStatusEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UpdateStatusRepository extends JpaRepository<UpdateStatusEntity, Long> {
Optional<UpdateStatusEntity> findByKeyName(String keyName);
void deleteByKeyName(String keyName);
}

View File

@@ -0,0 +1,58 @@
package com.tashow.erp.repository;
import com.tashow.erp.entity.ZebraOrderEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
* 斑马订单Repository
*/
@Repository
public interface ZebraOrderRepository extends JpaRepository<ZebraOrderEntity, Long> {
/**
* 根据会话ID查找订单分页
*/
Page<ZebraOrderEntity> findBySessionIdOrderByCreatedAtDesc(String sessionId, Pageable pageable);
/**
* 根据会话ID查找订单
*/
List<ZebraOrderEntity> findBySessionIdOrderByCreatedAtDesc(String sessionId);
/**
* 根据会话ID删除订单
*/
@Modifying
@Transactional
void deleteBySessionId(String sessionId);
/**
* 清理过期数据7天前的数据
*/
@Modifying
@Transactional
@Query("DELETE FROM ZebraOrderEntity z WHERE z.createdAt < :expireTime")
void deleteExpiredData(@Param("expireTime") LocalDateTime expireTime);
/**
* 获取最新的会话ID
*/
@Query("SELECT DISTINCT z.sessionId FROM ZebraOrderEntity z ORDER BY z.createdAt DESC")
List<String> findRecentSessionIds(Pageable pageable);
/**
* 获取最新会话的所有订单
*/
@Query("SELECT z FROM ZebraOrderEntity z WHERE z.sessionId = (SELECT z2.sessionId FROM ZebraOrderEntity z2 ORDER BY z2.createdAt DESC LIMIT 1) ORDER BY z.createdAt DESC")
List<ZebraOrderEntity> findLatestOrders();
}

View File

@@ -0,0 +1,71 @@
package com.tashow.erp.security;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class JwksService {
@Value("${api.server.base-url}")
private String serverBaseUrl;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
private final Map<String, RSAPublicKey> cache = new ConcurrentHashMap<>();
public JwksService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public RSAPublicKey getKeyById(String kid) {
RSAPublicKey cached = cache.get(kid);
if (cached != null) return cached;
loadJwks();
return cache.get(kid);
}
public RSAPublicKey[] getAllPublicKeys() {
loadJwks();
return cache.values().toArray(new RSAPublicKey[0]);
}
private void loadJwks() {
try {
String url = serverBaseUrl + "/.well-known/jwks.json";
ResponseEntity<String> resp = restTemplate.getForEntity(url, String.class);
JsonNode root = objectMapper.readTree(resp.getBody());
JsonNode keys = root.get("keys");
if (keys == null || !keys.isArray()) return;
for (JsonNode key : keys) {
String id = key.path("kid").asText(null);
String n = key.path("n").asText(null);
String e = key.path("e").asText(null);
if (id == null || n == null || e == null) continue;
if (!cache.containsKey(id)) {
cache.put(id, buildRsaPublicKey(n, e));
}
}
} catch (Exception ignored) {}
}
private RSAPublicKey buildRsaPublicKey(String nB64u, String eB64u) throws Exception {
BigInteger n = new BigInteger(1, Base64.getUrlDecoder().decode(nB64u));
BigInteger e = new BigInteger(1, Base64.getUrlDecoder().decode(eB64u));
RSAPublicKeySpec spec = new RSAPublicKeySpec(n, e);
return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(spec);
}
}

View File

@@ -0,0 +1,80 @@
package com.tashow.erp.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tashow.erp.service.IAuthService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.IOException;
import java.util.Base64;
/**
* 本地拦截器
*/
@Component
public class LocalJwtAuthInterceptor implements HandlerInterceptor {
private final IAuthService authService;
private final ObjectMapper objectMapper = new ObjectMapper();
public LocalJwtAuthInterceptor(IAuthService authService) {
this.authService = authService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if (uri.startsWith("/libs/") || uri.startsWith("/static/") || uri.startsWith("/favicon")
|| uri.startsWith("/api/cache") || uri.startsWith("/api/update")) {
return true;
}
String auth = request.getHeader("Authorization");
if ((auth == null || !auth.startsWith("Bearer ")) && request.getCookies() != null) {
for (jakarta.servlet.http.Cookie c : request.getCookies()) {
if ("FX_TOKEN".equals(c.getName()) && c.getValue() != null && !c.getValue().isEmpty()) {
auth = "Bearer " + c.getValue();
break;
}
}
}
// 移除本地token查找逻辑改为仅依赖内存token或cookie
if (auth == null || !auth.startsWith("Bearer ")) {
return true; // 放行无token场景由后端处理
}
String token = auth.substring(7).trim();
String[] parts = token.split("\\.");
if (parts.length != 3) {
return true; // 放行,由后端处理
}
try {
// 仅解析payload来确保基本格式不进行设备强校验避免前端大量401
String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]));
objectMapper.readTree(payloadJson);
} catch (Exception e) {
return true; // 放行,由后端处理
}
return true;
}
private void writeJson(HttpServletResponse response, int code, String msg) throws IOException {
response.setStatus(code == 401 ? 401 : 403);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":" + code + ",\"msg\":\"" + msg + "\"}");
}
private String buildHttpOnlyCookie(String name, String value, int maxAgeSeconds) {
StringBuilder sb = new StringBuilder();
sb.append(name).append("=").append(value).append(";");
sb.append(" Path=/;");
sb.append(" HttpOnly;");
sb.append(" SameSite=Strict;");
if (maxAgeSeconds > 0) {
sb.append(" Max-Age=").append(maxAgeSeconds).append(";");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,31 @@
package com.tashow.erp.service;
import com.tashow.erp.model.SearchResult;
import java.util.List;
import java.util.Map;
/**
* 1688识图搜索服务接口
*/
public interface Alibaba1688Service {
/**
* 上传base64图片并获取imageId
* @param base64Image base64编码的图片数据
* @return 1688的imageId
*/
String uploadImageBase64(String base64Image);
/**
* 获取图片的1688详情数据
* @param imageUrl
* @return
*/
SearchResult get1688Detail(String imageUrl);
}

View File

@@ -0,0 +1,27 @@
package com.tashow.erp.service;
import java.util.List;
import java.util.Map;
/**
* 亚马逊数据采集服务接口
*
* @author ruoyi
*/
public interface IAmazonScrapingService {
/**
* 批量获取亚马逊产品信息
*
* @param asinList ASIN列表
* @param batchId 批次ID
* @return 产品信息列表
*/
Map<String, Object> batchGetProductInfo(List<String> asinList, String batchId);
}

View File

@@ -0,0 +1,50 @@
package com.tashow.erp.service;
import java.util.Map;
/**
* 认证服务接口
*/
public interface IAuthService {
/**
* 客户端登录认证
*/
Map<String, Object> login(String username, String password);
/**
* 获取客户端信息
*/
Map<String, Object> getClientInfo();
/**
* 上报错误
*/
void reportError(String errorType, String errorMessage, Exception e);
/**
* 检查版本更新
*/
String checkVersion(String currentVersion);
/**
* 获取客户端ID
*/
String getClientId();
/**
* 验证token
*/
Map<String, Object> verifyToken(String token);
/**
* 注册新账号
*/
Map<String, Object> register(String username, String password);
/**
* 检查用户名是否可用
*/
boolean checkUsername(String username);
}

View File

@@ -0,0 +1,35 @@
package com.tashow.erp.service;
import java.util.List;
import java.util.Map;
/**
* 斑马订单服务接口
*
* @author ruoyi
*/
public interface IBanmaOrderService {
/**
* 刷新认证Token
*/
void refreshToken();
/**
* 获取店铺列表
* @return 店铺列表数据
*/
Map<String, Object> getShops();
/**
* 分页获取订单数据支持batchId
* @param startDate 开始日期
* @param endDate 结束日期
* @param page 页码从1开始
* @param pageSize 每页大小
* @param batchId 批次ID
* @param shopIds 店铺ID列表
* @return 订单数据列表和总数
*/
Map<String, Object> getOrdersByPage(String startDate, String endDate, int page, int pageSize, String batchId, List<String> shopIds);
}

View File

@@ -0,0 +1,12 @@
package com.tashow.erp.service;
/**
* 缓存服务接口
*/
public interface ICacheService {
/**
* 保存认证令牌
*/
void saveAuthToken(String service, String token, long expireTimeMillis);
}

View File

@@ -0,0 +1,18 @@
package com.tashow.erp.service;
/**
* 跟卖精灵服务接口
*
* @author ruoyi
*/
public interface IGenmaiService {
/**
* 使用Playwright打开跟卖精灵网页
*
* @return 是否成功打开
*/
void openGenmaiWebsite();
}

View File

@@ -0,0 +1,50 @@
package com.tashow.erp.service;
import com.tashow.erp.model.RakutenProduct;
import java.util.List;
/**
* 乐天产品缓存服务接口
*/
public interface IRakutenCacheService {
/**
* 保存产品数据
*/
String saveProducts(List<RakutenProduct> products);
/**
* 使用指定sessionId保存产品数据
*/
void saveProductsWithSessionId(List<RakutenProduct> products, String sessionId);
/**
* 检查店铺是否有最近的数据12小时内
*/
boolean hasRecentData(String shopName);
/**
* 根据店铺名获取已有数据
*/
List<RakutenProduct> getProductsByShopName(String shopName);
/**
* 更新指定店铺的产品sessionId
*/
void updateProductsSessionId(String shopName, String newSessionId);
/**
* 更新指定产品列表的sessionId
*/
void updateSpecificProductsSessionId(List<RakutenProduct> products, String newSessionId);
/**
* 清理指定店铺12小时内的重复数据为新采集做准备
*/
void cleanRecentDuplicateData(String shopName);
}

View File

@@ -0,0 +1,40 @@
package com.tashow.erp.service;
import com.tashow.erp.model.RakutenProduct;
import com.tashow.erp.model.SearchResult;
import java.util.List;
import java.util.Map;
/**
* 乐天商品爬取服务接口
*/
public interface RakutenScrapingService {
/**
* 商品爬取包含1688识图搜索支持实时回调
*
* @param shopName 店铺名
* @return 包含1688搜索结果的商品列表
*/
List<RakutenProduct> scrapeProductsWithSearch(String shopName);
/**
* 根据图片URL更新1688搜索结果的所有字段
*/
void update1688DataByImageUrl(SearchResult searchResult, String sessionId, String imageUrl);
/**
* 获取最新产品数据并转换为前端格式
*/
List<Map<String, Object>> getLatestProductsForDisplay();
}

View File

@@ -0,0 +1,307 @@
package com.tashow.erp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tashow.erp.model.SearchResult;
import com.tashow.erp.service.Alibaba1688Service;
import com.tashow.erp.utils.Alibaba1688CookieUtil;
import com.tashow.erp.utils.ErrorReporter;
import com.tashow.erp.utils.QiniuUtil;
import com.tashow.erp.utils.PersistentDriverManager;
import org.openqa.selenium.chrome.ChromeDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 1688识图搜索服务
*/
@Service
public class Alibaba1688ServiceImpl implements Alibaba1688Service {
private static final Logger logger = LoggerFactory.getLogger(Alibaba1688ServiceImpl.class);
private final Random random = new Random();
ObjectMapper objectMapper = new ObjectMapper();
private final RestTemplate restTemplate = new RestTemplate();
private final RestTemplate noSslRestTemplate = createNoSslRestTemplate();
@Autowired
private ErrorReporter errorReporter;
@Autowired
private PersistentDriverManager driverManager;
private RestTemplate createNoSslRestTemplate() {
try {
TrustManager[] trustManagers = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}
};
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustManagers, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() {
@Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
super.prepareConnection(connection, httpMethod);
if (connection instanceof HttpsURLConnection) {
((HttpsURLConnection) connection).setSSLSocketFactory(sslContext.getSocketFactory());
((HttpsURLConnection) connection).setHostnameVerifier((hostname, session) -> true);
}
}
};
return new RestTemplate(requestFactory);
} catch (Exception e) {
logger.warn("创建忽略SSL证书的RestTemplate失败使用默认RestTemplate", e);
return new RestTemplate();
}
}
/**
* 通过1688 API获取商品详情链接价格
* @return
*/
@Override
public SearchResult get1688Detail(String uploadedUrl) {
String fileName = "temp_" + System.currentTimeMillis() + ".png";
List<String> detailUrls = new ArrayList<>();
SearchResult result = new SearchResult();
List<Double> prices = new ArrayList<>();
try {
String imageUrl = QiniuUtil.uploadFromUrl(uploadedUrl, fileName);
String token = Alibaba1688CookieUtil.getToken();
long timestamp = System.currentTimeMillis();
// Step 1: 获取推荐的 offerId 列表
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 = Alibaba1688CookieUtil.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=" + Alibaba1688CookieUtil.APP_KEY + "&t=" + timestamp + "&sign=" + sign + "&v=2.0&type=originaljson&isSec=0&timeout=10000" + "&api=mtop.relationrecommend.WirelessRecommend.recommend&ignoreLogin=true&prefix=h5api&dataType=json";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.set("Cookie", Alibaba1688CookieUtil.getCookieString());
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);
JsonNode root = objectMapper.readTree(response.getBody());
Iterator<JsonNode> offerIterator = root.path("data").path("offerList").path("offers").elements();
//运费
Set<Double> freight = new HashSet<>();
for (int i = 0; i < 10 && offerIterator.hasNext(); i++) {
JsonNode offer = offerIterator.next();
String offerId = offer.path("id").asText();
String freightProvFirstFee = offer.path("freightProvFirstFee").asText();
Optional.ofNullable(freightProvFirstFee)
.map(s -> s.split(";", 2)[0])
.map(s -> s.split(":", 2))
.filter(parts -> parts.length == 2 && !parts[1].isBlank())
.map(parts -> Double.parseDouble(parts[1]) / 100.0)
.ifPresent(freight::add);
prices.add(offer.path("normalPrice").asDouble());
detailUrls.add(offerId);
}
Collections.sort(prices);
List<Double> freightFee = new ArrayList<>(freight);
Collections.sort(freightFee);
//中间值
Double median=0.0;
if(!prices.isEmpty()){
median = prices.get(prices.size() / 2 - 1);
}
//sku价格
Map<Double, String> skuPrices = new TreeMap<>();
for (String offerId : detailUrls) {
skuPrices.clear();
skuPrices.putAll(getSkuPrice(offerId));
if (skuPrices.size() >= 3) {
break;
}
}
result.setSkuPrice(skuPrices);
result.setMedian( median);
result.setMapRecognitionLink( uploadImageBase64(imageUrl));
System.out.println("运费"+freightFee);
result.setFreight(freightFee.isEmpty() ? 0.0 :freightFee.get(freightFee.size()/2-1));
// String weight = getWeight(detailUrls);
// result.setWeight(weight);
return result;
} catch (Exception e) {
errorReporter.reportDataCollectError("Alibaba1688Service", e);
logger.error("通过1688 API获取商品详情链接失败: {}", e.getMessage(), e);
return result;
}
}
/**
* 获取sku价格
*
* @param offerId
* @return
*/
private Map<Double, String> getSkuPrice(String offerId) {
TreeMap<Double, String> skuPrice = new TreeMap<>();
HttpHeaders headers = new HttpHeaders();
TreeMap<Double, String> resultMap = new TreeMap<>();
try {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.set("Cookie", Alibaba1688CookieUtil.getCookieString());
String token = Alibaba1688CookieUtil.getToken();
long timestamp = System.currentTimeMillis();
Map<String, Object> payload = Map.of("offerId", offerId);
String requestJson = objectMapper.writeValueAsString(payload);
String requestSign = Alibaba1688CookieUtil.generateSign(token, String.valueOf(timestamp), requestJson);
String skuUrl = "https://h5api.m.1688.com/h5/mtop.1688.pc.plugin.od.sku.query/1.0/" + "?jsv=2.6.3&appKey=" + Alibaba1688CookieUtil.APP_KEY + "&t=" + timestamp + "&api=mtop.1688.pc.plugin.od.sku.query&sign=" + requestSign + "&v=1.0&type=originaljson&isSec=0&dataType=json";
MultiValueMap<String, String> skuForm = new LinkedMultiValueMap<>();
skuForm.add("data", requestJson);
HttpEntity<MultiValueMap<String, String>> skuRequestEntity = new HttpEntity<>(skuForm, headers);
ResponseEntity<String> skuResponse = restTemplate.exchange(skuUrl, HttpMethod.POST, skuRequestEntity, String.class);
JsonNode skuRoot = objectMapper.readTree(skuResponse.getBody());
JsonNode skuMap = skuRoot.path("data");
if (skuMap.isEmpty()) {
skuPrice.put(-1.0, "无sku");
return skuPrice;
}
if (skuMap.isObject()) {
for (Iterator<Map.Entry<String, JsonNode>> it = skuMap.fields(); it.hasNext(); ) {
Map.Entry<String, JsonNode> entry = it.next();
JsonNode skuInfo = entry.getValue();
String attributes = skuInfo.path("attributes").asText();
double price = extractPriceFromSku(skuInfo);
skuPrice.put(price, attributes);
}
}
List<Double> keys = new ArrayList<>(skuPrice.keySet());
Map.Entry<Double, String> midEntry = Map.entry(keys.get(keys.size() / 2), skuPrice.get(keys.get(keys.size() / 2)));
resultMap.put(skuPrice.firstKey(), skuPrice.firstEntry().getValue());
resultMap.put(midEntry.getKey(), midEntry.getValue());
resultMap.put(skuPrice.lastKey(), skuPrice.lastEntry().getValue());
} catch (JsonProcessingException e) {
logger.error("获取sku价格失败: {}", e.getMessage(), e);
return new HashMap<>();
}
return resultMap;
}
/**
* 从SKU信息中提取价格
* 处理1688 API中各种复杂的价格结构
*
* @param skuInfo SKU节点信息
* @return 提取到的价格如果无法提取则返回0.0
*/
private double extractPriceFromSku(JsonNode skuInfo) {
try {
JsonNode priceNode = skuInfo.path("price");
// 如果price是数值且大于0直接返回
if (priceNode.isNumber() && priceNode.asDouble() > 0) {
return priceNode.asDouble();
}
// 如果price是数组且有元素检查第一个元素
if (priceNode.isArray() && priceNode.size() > 0) {
JsonNode firstPrice = priceNode.get(0);
if (firstPrice.isNumber() && firstPrice.asDouble() > 0) {
return firstPrice.asDouble();
}
if (firstPrice.isObject() && firstPrice.has("price") && firstPrice.path("price").asDouble() > 0) {
return firstPrice.path("price").asDouble();
}
}
return 0.0;
} catch (Exception e) {
logger.error("提取SKU价格时发生异常: {}", e.getMessage(), e);
return 0.0;
}
}
/**
* 上传图片并获取图片ID
* @return
*/
public String uploadImageBase64(String imageUrl) {
String token = Alibaba1688CookieUtil.getToken();
long timestamp = System.currentTimeMillis();
byte[] imageBytes = noSslRestTemplate.getForObject(imageUrl, byte[].class);
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
String jsonData = "{\"appId\":32517,\"params\":\"{\\\"searchScene\\\":\\\"imageEx\\\",\\\"interfaceName\\\":\\\"imageBase64ToImageId\\\",\\\"serviceParam.extendParam[imageBase64]\\\":\\\"" + base64Image + "\\\",\\\"subChannel\\\":\\\"pc_image_search_image_id\\\"}\"}";
String sign = Alibaba1688CookieUtil.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=" + Alibaba1688CookieUtil.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());
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);
Alibaba1688CookieUtil.setCookie(response);
JSONObject json = JSON.parseObject(response.getBody());
return "https://s.1688.com/youyuan/index.htm?tab=imageSearch&imageId=" + json.getJSONObject("data").getString("imageId");
}
private String getWeight(List<String> ids) {
Pattern WEIGHT_PATTERN = Pattern.compile("\"(?:weight|unitWeight)\":\\s*([1-9]\\d*(?:\\.\\d+)?)");
List<String> weightList = new ArrayList<>();
ChromeDriver driver = driverManager.getCurrentDriver();
Set<String> weightSet = new HashSet<>();
try {
for (int i = 0; weightSet.size() <= 2; i++) {
driver.get("https://detail.1688.com/offer/" + ids.get(i) + ".html");
if(i==0) driver.navigate().refresh();
if (Objects.equals(driver.getTitle(), "验证码拦截")) {
driver = driverManager.switchToHeadful();
while (Objects.equals(driver.getTitle(), "验证码拦截")) {
Thread.sleep(1000);
}
}else if(Objects.equals(driver.getTitle(), "淘宝网 - 淘!我喜欢")){
}
System.out.println("标题"+driver.getTitle());
String source = driver.getPageSource();
Matcher weightMatcher = WEIGHT_PATTERN.matcher(source);
if (weightMatcher.find()) {
String weightValue = weightMatcher.group(1);
String width = weightValue.contains(".") ? (int) (Float.parseFloat(weightValue) * 1000) + "g" : weightValue + "g";
weightSet.add(width);
System.out.println("重量"+width);
}
Thread.sleep(2000+random.nextInt(3000));
}
} catch (InterruptedException e) {
e.printStackTrace();
errorReporter.reportDataCollectError("获取重量出错",e);
}
weightList.addAll(weightSet);
weightList.sort((a, b) -> Integer.compare(Integer.parseInt(a.replace("g", "")), Integer.parseInt(b.replace("g", ""))));
System.out.println("weightList: " +ids+"::::::"+ weightList);
return weightList.get(1);
}
}

View File

@@ -0,0 +1,163 @@
package com.tashow.erp.service.impl;
import com.tashow.erp.entity.AmazonProductEntity;
import com.tashow.erp.repository.AmazonProductRepository;
import com.tashow.erp.service.IAmazonScrapingService;
import com.tashow.erp.utils.DataReportUtil;
import com.tashow.erp.utils.RakutenProxyUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
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 java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 亚马逊数据采集服务实现类
*
* @author ruoyi
*/
@Service
public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PageProcessor {
private static final Logger logger = LoggerFactory.getLogger(AmazonScrapingServiceImpl.class);
@Autowired
private AmazonProductRepository amazonProductRepository;
@Autowired
private DataReportUtil dataReportUtil;
private final Random random = new Random();
private static volatile Spider activeSpider = null;
private static final Object spiderLock = new Object();
private final Map<String, Map<String, Object>> resultCache = new ConcurrentHashMap<>();
private final Site site = Site.me().setRetryTimes(3).setSleepTime(2000 + random.nextInt(2000))
.setTimeOut(15000).setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/128.0.0.0 Safari/537.36").addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9").addHeader("accept-language", "ja,en;q=0.9,zh-CN;q=0.8,zh;q=0.7").addHeader("cache-control", "max-age=0").addHeader("upgrade-insecure-requests", "1").addHeader("sec-ch-ua", "\"Chromium\";v=\"128\", \"Not=A?Brand\";v=\"24\"").addHeader("sec-ch-ua-mobile", "?0").addHeader("sec-ch-ua-platform", "\"Windows\"").addHeader("sec-fetch-site", "none").addHeader("sec-fetch-mode", "navigate").addHeader("sec-fetch-user", "?1").addHeader("sec-fetch-dest", "document").addCookie("i18n-prefs", "JPY").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");
/**
* 处理亚马逊页面数据提取
*/
@Override
public void process(Page page) {
Html html = page.getHtml();
Map<String, Object> resultMap = new HashMap<>();
String priceSymbol = html.xpath("//span[@class='a-price-symbol']/text()").toString();
String priceWhole = html.xpath("//span[@class='a-price-whole']/text()").toString();
String price = priceSymbol + priceWhole;
if (price.isEmpty()) {
price = html.xpath("//span[@class='a-price-range']/text()").toString();
}
String seller = html.xpath("//a[@id='sellerProfileTriggerId']/text()").toString();
if (seller == null || seller.isEmpty()) {
seller = html.xpath("//span[@class='a-size-small offer-display-feature-text-message']/text()").toString();
}
resultMap.put("seller", seller);
if (price != null || seller != null) {
resultMap.put("price", price);
} else {
throw new RuntimeException("Retry this page");
}
String asin = html.xpath("//input[@id='ASIN']/@value").toString();
if (asin == null || asin.isEmpty()) {
String[] parts = page.getUrl().toString().split("/dp/");
if (parts.length > 1) asin = parts[1].split("/")[0].split("\\?")[0];
}
String title = html.xpath("//span[@id='productTitle']/text()").toString();
if (title == null || title.isEmpty())
title = html.xpath("//h1[@class='a-size-large a-spacing-none']/text()").toString();
resultMap.put("asin", asin != null ? asin : "");
resultMap.put("title", (title == null || title.isEmpty()) ? "未获取" : title.trim());
resultCache.put(asin, resultMap);
page.putField("resultMap", resultMap);
}
/**
* 获取WebMagic站点配置
*/
@Override
public Site getSite() {
return site;
}
/**
* 批量获取产品信息
*/
@Override
public Map<String, Object> batchGetProductInfo(List<String> asinList, String batchId) {
String sessionId = (batchId != null) ? batchId : "SINGLE_" + UUID.randomUUID();
List<Map<String, Object>> products = new ArrayList<>();
for (String asin : asinList) {
if (asin == null || asin.trim().isEmpty()) continue;
String cleanAsin = asin.replaceAll("[^a-zA-Z0-9]", "");
Map<String, Object> result = new HashMap<>();
amazonProductRepository.findByAsin(cleanAsin).ifPresentOrElse(entity -> {
if (entity.getCreatedAt().isAfter(LocalDateTime.now().minusHours(1))) {
result.put("asin", entity.getAsin());
result.put("title", entity.getTitle());
result.put("price", entity.getPrice());
result.put("seller", entity.getSeller());
result.put("imageUrl", entity.getImageUrl());
result.put("productUrl", entity.getProductUrl());
result.put("brand", entity.getBrand());
result.put("category", entity.getCategory());
result.put("rating", entity.getRating());
result.put("reviewCount", entity.getReviewCount());
result.put("availability", entity.getAvailability());
products.add(result);
}
}, () -> {
// 数据库没有或过期 -> 爬取
String url = "https://www.amazon.co.jp/dp/" + cleanAsin;
RakutenProxyUtil proxyUtil = new RakutenProxyUtil();
synchronized (spiderLock) {
activeSpider = Spider.create(this)
.addUrl(url)
.setDownloader(proxyUtil.createProxyDownloader(proxyUtil.detectSystemProxy(url)))
.thread(1);
activeSpider.run();
activeSpider = null;
}
result.putAll(resultCache.getOrDefault(cleanAsin, Map.of("asin", cleanAsin, "price", "", "seller", "", "title", "")));
// 存库
AmazonProductEntity entity = new AmazonProductEntity();
entity.setAsin(cleanAsin);
entity.setTitle((String) result.get("title"));
entity.setPrice((String) result.get("price"));
entity.setSeller((String) result.get("seller"));
entity.setImageUrl((String) result.get("imageUrl"));
entity.setProductUrl((String) result.get("productUrl"));
entity.setBrand((String) result.get("brand"));
entity.setCategory((String) result.get("category"));
entity.setRating((String) result.get("rating"));
entity.setReviewCount((String) result.get("reviewCount"));
entity.setAvailability((String) result.get("availability"));
entity.setSessionId(sessionId);
entity.setCreatedAt(LocalDateTime.now());
try {
amazonProductRepository.save(entity);
dataReportUtil.reportDataCollection("AMAZON", 1, "0");
} catch (Exception e) {
logger.warn("保存商品数据失败: {}", cleanAsin);
}
products.add(result);
});
}
long failedCount = products.stream().filter(p -> p.get("price").toString().isEmpty()).count();
return Map.of(
"products", products,
"total", products.size(),
"success", true,
"failedCount", failedCount
);
}
}

View File

@@ -0,0 +1,246 @@
package com.tashow.erp.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tashow.erp.service.IAuthService;
import com.tashow.erp.utils.ApiForwarder;
import com.tashow.erp.utils.DeviceUtils;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import jakarta.annotation.PostConstruct;
/**
* 认证服务实现类 - 简化版
* 提供基础的登录、注册、token验证功能
*/
@Service
public class AuthServiceImpl implements IAuthService {
@Value("${api.server.base-url}")
private String serverApiUrl;
@Value("${project.version:2.1.0}")
private String appVersion;
private final ObjectMapper objectMapper = new ObjectMapper();
@org.springframework.beans.factory.annotation.Autowired
private ApiForwarder apiForwarder;
@org.springframework.beans.factory.annotation.Autowired
private com.tashow.erp.repository.CacheDataRepository cacheDataRepository;
@Getter
private String clientId = DeviceUtils.generateDeviceId();
private String accessToken;
private String refreshToken;
/**
* 应用启动时从SQLite恢复token
*/
@PostConstruct
private void initTokenFromCache() {
try {
cacheDataRepository.findByCacheKey("token").ifPresent(entity ->
accessToken = entity.getCacheValue()
);
} catch (Exception ignored) {}
}
/**
* 保存token到SQLite
*/
private void saveTokenToCache(String token) {
try {
com.tashow.erp.entity.CacheDataEntity entity = cacheDataRepository.findByCacheKey("token")
.orElse(new com.tashow.erp.entity.CacheDataEntity());
entity.setCacheKey("token");
entity.setCacheValue(token);
cacheDataRepository.save(entity);
} catch (Exception ignored) {}
}
/**
* 客户端登录认证
*/
@Override
public Map<String, Object> login(String username, String password) {
Map<String, Object> result = new HashMap<>();
Map<String, Object> loginData = new HashMap<>();
loginData.put("username", username);
loginData.put("password", password);
loginData.putAll(getClientInfo());
JsonNode response = sendPostRequest("/monitor/account/login", loginData);
if (response.has("code") && response.get("code").asInt() == 200) {
JsonNode data = response.has("data") ? response.get("data") : response;
accessToken = data.has("accessToken") ? data.get("accessToken").asText() :
data.has("token") ? data.get("token").asText() : null;
refreshToken = data.has("refreshToken") ? data.get("refreshToken").asText() : null;
if (accessToken != null) {
saveTokenToCache(accessToken);
}
result.put("success", true);
result.put("message", "登录成功");
result.put("token", accessToken);
result.put("permissions", data.has("permissions") ? data.get("permissions").asText() : null);
} else {
result.put("success", false);
result.put("message", response.has("msg") ? response.get("msg").asText() : "登录失败");
}
return result;
}
/**
* 获取客户端基本信息
*/
@Override
public Map<String, Object> getClientInfo() {
Map<String, Object> info = new HashMap<>();
info.put("clientId", clientId);
info.put("appVersion", appVersion);
info.put("osName", System.getProperty("os.name"));
return info;
}
/**
* 上报错误信息
*/
@Override
public void reportError(String errorType, String errorMessage, Exception e) {
try {
Map<String, Object> errorData = new HashMap<>();
errorData.put("clientId", clientId);
errorData.put("errorType", errorType);
errorData.put("errorMessage", errorMessage);
sendPostRequest("/monitor/error", errorData);
} catch (Exception ignored) {}
}
/**
* 检查版本更新
*/
@Override
public String checkVersion(String currentVersion) {
JsonNode response = sendGetRequest("/monitor/version?currentVersion=" + currentVersion);
return response.toString();
}
/**
* 发送POST请求
*/
private JsonNode sendPostRequest(String path, Map<String, Object> data){
if (data != null) {
data.putIfAbsent("_clientUser", System.getProperty("user.name"));
data.putIfAbsent("_clientOs", System.getProperty("os.name"));
}
org.springframework.http.ResponseEntity<?> resp = apiForwarder.post(path, data, buildAuthHeader());
return objectMapper.valueToTree(resp.getBody());
}
/**
* 发送GET请求
*/
private JsonNode sendGetRequest(String path){
org.springframework.http.ResponseEntity<?> resp = apiForwarder.get(path, buildAuthHeader());
return objectMapper.valueToTree(resp.getBody());
}
/**
* 构建认证头
*/
private String buildAuthHeader(){
return (accessToken == null || accessToken.isEmpty()) ? null : ("Bearer " + accessToken.trim());
}
/**
* 验证token
*/
@Override
public Map<String, Object> verifyToken(String token) {
Map<String, Object> result = new HashMap<>();
Map<String, Object> verifyData = new HashMap<>();
verifyData.put("token", token);
JsonNode response = sendPostRequest("/monitor/account/verify", verifyData);
if (response.has("code") && response.get("code").asInt() == 200) {
JsonNode dataNode = response.has("data") ? response.get("data") : response;
result.put("success", true);
result.put("username", dataNode.get("username").asText());
result.put("permissions", dataNode.get("permissions").asText());
} else {
result.put("success", false);
result.put("message", "token验证失败");
}
return result;
}
/**
* 注册新账号
*/
@Override
public Map<String, Object> register(String username, String password) {
Map<String, Object> result = new HashMap<>();
try {
Map<String, Object> registerData = new HashMap<>();
registerData.put("accountName", username);
registerData.put("username", username);
registerData.put("password", password);
registerData.putAll(getClientInfo());
JsonNode response = sendPostRequest("/monitor/account/register", registerData);
if (response.has("code") && response.get("code").asInt() == 200) {
JsonNode data = response.has("data") ? response.get("data") : response;
String newAccessToken = data.has("accessToken") ? data.get("accessToken").asText() : null;
String newRefreshToken = data.has("refreshToken") ? data.get("refreshToken").asText() : null;
if (newAccessToken != null) {
accessToken = newAccessToken;
refreshToken = newRefreshToken;
saveTokenToCache(newAccessToken);
}
result.put("success", true);
result.put("message", "注册成功");
} else {
String errorMessage = response.has("msg") ? response.get("msg").asText() :
(response.has("message") ? response.get("message").asText() : "注册失败");
result.put("success", false);
result.put("message", errorMessage);
}
} catch (Exception e) {
result.put("success", false);
result.put("message", "注册失败:" + e.getMessage());
}
return result;
}
/**
* 检查用户名是否可用
*/
@Override
public boolean checkUsername(String username) {
try {
JsonNode response = sendGetRequest("/monitor/account/check-username?username=" + username);
if (response.has("code") && response.get("code").asInt() == 200) {
JsonNode dataNode = response.has("data") ? response.get("data") : response;
return dataNode.asBoolean();
}
} catch (Exception ignored) {}
return false;
}
/**
* 退出登录清除本地token
*/
public void logout() {
try {
accessToken = null;
refreshToken = null;
cacheDataRepository.deleteByCacheKey("token");
} catch (Exception ignored) {}
}
}

View File

@@ -0,0 +1,292 @@
package com.tashow.erp.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tashow.erp.entity.BanmaOrderEntity;
import com.tashow.erp.repository.BanmaOrderRepository;
import com.tashow.erp.service.ICacheService;
import com.tashow.erp.service.IBanmaOrderService;
import com.tashow.erp.utils.DataReportUtil;
import com.tashow.erp.utils.LoggerUtil;
import com.tashow.erp.utils.SagawaExpressSdk;
import com.tashow.erp.utils.StringUtils;
import org.slf4j.Logger;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 斑马订单服务实现类 - 极简版
* 所有功能统一到核心方法,彻底消除代码分散
*
* @author ruoyi
*/
@Service
public class BanmaOrderServiceImpl implements IBanmaOrderService {
private static final Logger logger = LoggerUtil.getLogger(BanmaOrderServiceImpl.class);
private static final String SERVICE_NAME = "banma";
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?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&_t=%d";
private static final String API_URL_WITH_TIME = "https://banma365.cn/api/order/list?%srecipientName=&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 long TOKEN_EXPIRE_TIME = 24 * 60 * 60 * 1000;
private RestTemplate restTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
private final ICacheService cacheService;
private final BanmaOrderRepository banmaOrderRepository;
private final DataReportUtil dataReportUtil;
private String currentAuthToken;
// 当前批量采集的sessionId
private String currentBatchSessionId = null;
// 物流信息缓存,避免重复查询
private final Map<String, String> trackingInfoCache = new ConcurrentHashMap<>();
public BanmaOrderServiceImpl(BanmaOrderRepository banmaOrderRepository, ICacheService cacheService, DataReportUtil dataReportUtil) {
this.banmaOrderRepository = banmaOrderRepository;
this.cacheService = cacheService;
this.dataReportUtil = dataReportUtil;
RestTemplateBuilder builder = new RestTemplateBuilder();
builder.connectTimeout(Duration.ofSeconds(5));
builder.readTimeout(Duration.ofSeconds(10));
restTemplate = builder.build();
initializeAuthToken();
}
/**
* 初始化认证令牌
*/
private void initializeAuthToken() {
refreshToken();
/* currentAuthToken = cacheService.getAuthToken(SERVICE_NAME);
if (currentAuthToken == null) {
} else {
logger.info("从缓存加载斑马认证令牌成功");
}*/
}
/**
* 刷新斑马认证令牌
*/
@Override
public void refreshToken() {
Map<String, String> params = new HashMap<>();
params.put("username", LOGIN_USERNAME);
params.put("password", LOGIN_PASSWORD);
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
ResponseEntity<Map> response = restTemplate.postForEntity(LOGIN_URL, new HttpEntity<>(params, 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 -> {
currentAuthToken = "Bearer " + token;
cacheService.saveAuthToken(SERVICE_NAME, currentAuthToken, System.currentTimeMillis() + TOKEN_EXPIRE_TIME);
});
}
/**
* 获取店铺列表
*/
@Override
public Map<String, Object> getShops() {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", currentAuthToken);
HttpEntity<String> httpEntity = new HttpEntity<>(headers);
String url = "https://banma365.cn/api/shop/list?_t=" + System.currentTimeMillis();
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class);
return response.getBody() != null ? response.getBody() : new HashMap<>();
}
/**
* 分页获取订单数据
*/
@Override
public Map<String, Object> getOrdersByPage(String startDate, String endDate, int page, int pageSize, String batchId, List<String> shopIds) {
if (page == 1) {
currentBatchSessionId = batchId;
trackingInfoCache.clear();
}
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", currentAuthToken);
HttpEntity<String> httpEntity = new HttpEntity<>(headers);
String shopIdsParam = "";
if (shopIds != null && !shopIds.isEmpty()) {
List<String> validShopIds = shopIds.stream()
.filter(id -> id != null && !id.trim().isEmpty())
.collect(Collectors.toList());
if (!validShopIds.isEmpty()) {
shopIdsParam = "shopIds[]=" + String.join("&shopIds[]=", validShopIds) + "&";
}
}
String url = (StringUtils.isEmpty(startDate) || StringUtils.isEmpty(endDate))
? String.format(API_URL, shopIdsParam, page, pageSize, System.currentTimeMillis())
: String.format(API_URL_WITH_TIME, shopIdsParam, page, pageSize, startDate, endDate, System.currentTimeMillis());
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, Map.class);
if (response.getBody() == null || !Integer.valueOf(0).equals(response.getBody().get("code"))) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("success", false);
errorResult.put("message", "获取订单数据失败,请点击'刷新认证'按钮重试");
return errorResult;
}
Map<String, Object> dataMap = (Map<String, Object>) response.getBody().get("data");
int total = ((Number) dataMap.getOrDefault("total", 0)).intValue();
List orders = Optional.ofNullable(dataMap.get("list"))
.map(list -> (List<Map<String, Object>>) list)
.orElse(Collections.emptyList())
.stream()
.map(this::processOrderData)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (!orders.isEmpty()) dataReportUtil.reportDataCollection("BANMA", orders.size(), "0");
Map<String, Object> result = new HashMap<>();
result.put("orders", orders);
result.put("total", total);
result.put("page", page);
result.put("pageSize", pageSize);
result.put("totalPages", (int) Math.ceil((double) total / pageSize));
result.put("success", true);
return result;
}
/**
* 处理订单数据
*/
private Map processOrderData(Map<String, Object> order) {
String trackingNumber = (String) order.get("internationalTrackingNumber");
if (StringUtils.isNotEmpty(trackingNumber)) {
LocalDateTime cutoffTime = LocalDateTime.now().minusHours(1);
if (banmaOrderRepository.existsByTrackingNumberAndCreatedAtAfter(trackingNumber, cutoffTime)) {
return banmaOrderRepository.findLatestByTrackingNumber(trackingNumber)
.map(entity -> {
if (currentBatchSessionId != null && !currentBatchSessionId.equals(entity.getSessionId())) {
entity.setSessionId(currentBatchSessionId);
entity.setCreatedAt(LocalDateTime.now());
entity.setUpdatedAt(LocalDateTime.now());
banmaOrderRepository.save(entity);
}
try {
return objectMapper.readValue(entity.getOrderData(), Map.class);
} catch (Exception e) {
return new HashMap<>();
}
})
.orElse(null);
} else {
banmaOrderRepository.findByTrackingNumber(trackingNumber)
.ifPresent(banmaOrderRepository::delete);
}
}
// 构建新订单数据
Map<String, Object> result = new HashMap<>();
result.put("internationalTrackingNumber", trackingNumber);
result.put("internationalShippingFee", order.get("internationalShippingFee"));
result.put("trackInfo", trackingInfoCache.computeIfAbsent(trackingNumber, this::fetchTrackingInfo));
Optional.ofNullable(order.get("subOrders"))
.map(sub -> (List<Map<String, Object>>) sub)
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.ifPresent(subOrder -> extractSubOrderFields(result, subOrder));
BanmaOrderEntity entity = new BanmaOrderEntity();
String entityTrackingNumber = (String) result.get("internationalTrackingNumber");
if (StringUtils.isEmpty(entityTrackingNumber)) {
String shopOrderNumber = (String) result.get("shopOrderNumber");
String productTitle = (String) result.get("productTitle");
if (StringUtils.isNotEmpty(shopOrderNumber)) {
entityTrackingNumber = "ORDER_" + shopOrderNumber;
} else if (StringUtils.isNotEmpty(productTitle)) {
entityTrackingNumber = "PRODUCT_" + Math.abs(productTitle.hashCode());
} else {
entityTrackingNumber = "UNKNOWN_" + System.currentTimeMillis();
}
}
entity.setTrackingNumber(entityTrackingNumber);
try {
entity.setOrderData(objectMapper.writeValueAsString(result));
} catch (Exception e) {
entity.setOrderData("{}");
}
// 生成会话ID
String sessionId = currentBatchSessionId != null ? currentBatchSessionId :
Optional.ofNullable((String) result.get("orderedAt"))
.filter(orderedAt -> orderedAt.length() >= 10)
.map(orderedAt -> "SESSION_" + orderedAt.substring(0, 10))
.orElse("SESSION_" + java.time.LocalDate.now().toString());
entity.setSessionId(sessionId);
entity.setCreatedAt(LocalDateTime.now());
entity.setUpdatedAt(LocalDateTime.now());
try {
banmaOrderRepository.save(entity);
} catch (Exception e) {
logger.warn("保存订单数据失败,跳过: {}", entityTrackingNumber);
}
return result;
}
/**
* 提取子订单字段
*/
private void extractSubOrderFields(Map<String, Object> simplifiedOrder, Map<String, Object> subOrder) {
String[] basicFields = {"orderedAt", "timeSinceOrder", "createdAt", "poTrackingNumber"};
String[] productFields = {"productTitle", "shopOrderNumber", "priceJpy", "productQuantity", "shippingFeeJpy", "productNumber", "serviceFee", "productImage"};
String[] purchaseFields = {"poNumber", "shippingFeeCny", "poLogisticsCompany"};
Arrays.stream(basicFields).forEach(field -> simplifiedOrder.put(field, subOrder.get(field)));
Arrays.stream(productFields).forEach(field -> simplifiedOrder.put(field, subOrder.get(field)));
Arrays.stream(purchaseFields).forEach(field -> simplifiedOrder.put(field, subOrder.get(field)));
}
/**
* 从API获取物流信息
*/
private String fetchTrackingInfo(String trackingNumber) {
// 优先尝试佐川物流
Map<String, Object> trackInfoMap = (Map<String, Object>) new SagawaExpressSdk().getTrackingInfo(trackingNumber).get("trackInfo");
if (trackInfoMap != null) {
return trackInfoMap.get("dateTime") + " " + trackInfoMap.get("office") + " " + trackInfoMap.get("status");
}
// 斑马API
ResponseEntity<Map> response = restTemplate.getForEntity(String.format(TRACKING_URL, trackingNumber), Map.class);
return Optional.ofNullable(response.getBody())
.filter(body -> Integer.valueOf(0).equals(body.get("code")))
.map(body -> (List<Map<String, Object>>) body.get("data"))
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.map(track -> (String) track.get("track"))
.orElse("暂无物流信息");
}
}

View File

@@ -0,0 +1,34 @@
package com.tashow.erp.service.impl;
import com.tashow.erp.entity.AuthTokenEntity;
import com.tashow.erp.repository.AuthTokenRepository;
import com.tashow.erp.service.ICacheService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
public class CacheServiceImpl implements ICacheService {
private static final Logger logger = LoggerFactory.getLogger(CacheServiceImpl.class);
@Autowired
private AuthTokenRepository authTokenRepository;
@Override
public void saveAuthToken(String service, String token, long expireTimeMillis) {
try {
Optional<AuthTokenEntity> existing = authTokenRepository.findByServiceName(service);
AuthTokenEntity entity = existing.orElse(new AuthTokenEntity());
entity.setServiceName(service);
entity.setToken(token);
entity.setExpireTime(LocalDateTime.now().plusSeconds(expireTimeMillis / 1000));
authTokenRepository.save(entity);
} catch (Exception e) {
logger.error("保存认证令牌失败: {}", service, e);
}
}
}

View File

@@ -0,0 +1,95 @@
package com.tashow.erp.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qiniu.util.UrlUtils;
import io.github.bonigarcia.wdm.WebDriverManager;
import lombok.SneakyThrows;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import com.tashow.erp.service.IGenmaiService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
/**
* 跟卖精灵服务实现类
*
* @author ruoyi
*/
@Service
public class GenmaiServiceImpl implements IGenmaiService {
@Value("${api.server.base-url}")
private String serverApiUrl;
@Value("${api.server.paths.getGenmaijlToken}")
private String genmaijlToken;
@Value("${api.server.paths.updateGenmaijlToken}")
private String updateGenmaijlToken;
private final RestTemplate restTemplate = new RestTemplate();
ObjectMapper objectMapper = new ObjectMapper();
@Override
@SneakyThrows
public void openGenmaiWebsite() {
// 先关闭所有Chrome进程
Runtime.getRuntime().exec("taskkill /f /im chrome.exe");
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
String username = System.getProperty("user.name", "user");
String safeUsername;
char firstChar = username.charAt(0);
char flippedFirstChar = Character.isUpperCase(firstChar) ? Character.toLowerCase(firstChar) : Character.toUpperCase(firstChar);
safeUsername = flippedFirstChar + username.substring(1);
String chromeUserData = System.getProperty("user.home").replace(username, UrlUtils.urlEncode(safeUsername) )
+ "\\AppData\\Local\\Google\\Chrome\\User Data";
options.addArguments("user-data-dir=" + chromeUserData.toLowerCase());
options.addArguments("profile-directory=Default");
WebDriver driver = new ChromeDriver(options);
driver.get("https://www.genmaijl.com/#/profile");
JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript(String.format("localStorage.setItem('token','%s')", checkTokenExpired()));
driver.navigate().refresh();
}
/**
* 获取token验证token是否过期
*/
public String checkTokenExpired() {
ResponseEntity<String> exchange = restTemplate.exchange(serverApiUrl+genmaijlToken, HttpMethod.GET, null, String.class);
String token = exchange.getBody();
HttpHeaders headers = new HttpHeaders();
headers.set("Token", token);
HttpEntity<String> entity = new HttpEntity<>(null, headers);
try {
ResponseEntity<String> response = restTemplate.exchange("https://www.genmaijl.com/manage/user/balance", HttpMethod.GET, entity, String.class);
String body = response.getBody();
JsonNode root = objectMapper.readTree(body);
return root.get("code").asInt() == 0 ? token : login();
} catch (Exception e) {
return login();
}
}
/**
* 登录
*/
@SneakyThrows
public String login() {
Map<String, String> requestBody = new HashMap<>();
requestBody.put("nickname", "changzhu-4");
requestBody.put("password", "123456QWe@");
HttpEntity<Map<String, String>> entity = new HttpEntity<>(requestBody);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
ResponseEntity<String> response = restTemplate.exchange("https://www.genmaijl.com/manage/user/subLogin", HttpMethod.POST, entity, String.class);
String body = response.getBody();
JsonNode root = objectMapper.readTree(body);
requestBody.clear();
String token= root.get("result").asText();
HttpEntity<String> updateEntity = new HttpEntity<>(token,headers);
ResponseEntity<String> exchange = restTemplate.exchange(serverApiUrl + updateGenmaijlToken, HttpMethod.POST, updateEntity, String.class);
System.out.println(exchange.getBody());
return token;
}
}

View File

@@ -0,0 +1,153 @@
package com.tashow.erp.service.impl;
import com.alibaba.fastjson.JSON;
import com.tashow.erp.entity.RakutenProductEntity;
import com.tashow.erp.model.RakutenProduct;
import com.tashow.erp.repository.RakutenProductRepository;
import com.tashow.erp.service.IRakutenCacheService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* 乐天产品缓存服务实现类
* 负责乐天商品数据的缓存存储、检索和会话管理
*/
@Slf4j
@Service
public class RakutenCacheServiceImpl implements IRakutenCacheService {
@Autowired
private RakutenProductRepository repository;
/**
* 保存产品列表并生成新的会话ID
*/
@Override
@Transactional
public String saveProducts(List<RakutenProduct> products) {
String sessionId = UUID.randomUUID().toString();
List<RakutenProductEntity> entities = products.stream()
.map(product -> {
RakutenProductEntity entity = new RakutenProductEntity();
BeanUtils.copyProperties(product, entity);
entity.setSessionId(sessionId);
return entity;
})
.collect(Collectors.toList());
repository.saveAll(entities);
log.info("保存产品数据sessionId: {},数量: {}", sessionId, products.size());
return sessionId;
}
/**
* 使用指定会话ID保存产品列表
*/
@Override
@Transactional
public void saveProductsWithSessionId(List<RakutenProduct> products, String sessionId) {
List<RakutenProductEntity> entities = products.stream()
.map(product -> {
RakutenProductEntity entity = new RakutenProductEntity();
BeanUtils.copyProperties(product, entity);
entity.setSessionId(sessionId);
return entity;
})
.collect(Collectors.toList());
repository.saveAll(entities);
log.info("保存产品数据sessionId: {},数量: {}", sessionId, products.size());
}
/**
* 检查店铺是否有1小时内的缓存数据
*/
@Override
public boolean hasRecentData(String shopName) {
return repository.existsByOriginalShopNameAndCreatedAtAfter(shopName, LocalDateTime.now().minusHours(1));
}
/**
* 根据店铺名获取所有产品数据
*/
@Override
public List<RakutenProduct> getProductsByShopName(String shopName) {
return repository.findByOriginalShopNameOrderByCreatedAtAscIdAsc(shopName).stream()
.map(entity -> {
RakutenProduct product = new RakutenProduct();
BeanUtils.copyProperties(entity, product);
return product;
})
.collect(Collectors.toList());
}
/**
* 更新指定店铺的所有产品的会话ID
*/
@Override
@Transactional
public void updateProductsSessionId(String shopName, String newSessionId) {
List<RakutenProductEntity> entities = repository.findByOriginalShopNameOrderByCreatedAtAscIdAsc(shopName);
entities.forEach(entity -> {
if (!newSessionId.equals(entity.getSessionId())) {
entity.setSessionId(newSessionId);
entity.setUpdatedAt(LocalDateTime.now());
}
});
repository.saveAll(entities);
log.info("更新店铺产品sessionId: {} -> {}", shopName, newSessionId);
}
/**
* 更新指定产品列表的会话ID只更新这些具体的产品
*/
@Override
@Transactional
public void updateSpecificProductsSessionId(List<RakutenProduct> products, String newSessionId) {
if (products == null || products.isEmpty()) {
return;
}
// 根据产品的唯一标识如productUrl来查找并更新对应的数据库记录
List<String> productUrls = products.stream()
.map(RakutenProduct::getProductUrl)
.filter(url -> url != null && !url.trim().isEmpty())
.collect(Collectors.toList());
if (productUrls.isEmpty()) {
return;
}
List<RakutenProductEntity> entities = repository.findByProductUrlIn(productUrls);
entities.forEach(entity -> {
if (!newSessionId.equals(entity.getSessionId())) {
entity.setSessionId(newSessionId);
entity.setUpdatedAt(LocalDateTime.now());
}
});
repository.saveAll(entities);
log.info("更新特定产品sessionId数量: {} -> {}", entities.size(), newSessionId);
}
/**
* 清理指定店铺1小时之前的旧数据保留1小时内的缓存
*/
@Override
@Transactional
public void cleanRecentDuplicateData(String shopName) {
LocalDateTime cutoffTime = LocalDateTime.now().minusHours(1);
repository.deleteByOriginalShopNameAndCreatedAtBefore(shopName, cutoffTime);
log.info("清理店铺1小时之前的旧数据: {}", shopName);
}
}

View File

@@ -0,0 +1,158 @@
package com.tashow.erp.service.impl;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tashow.erp.entity.RakutenProductEntity;
import com.tashow.erp.model.RakutenProduct;
import com.tashow.erp.model.SearchResult;
import com.tashow.erp.repository.RakutenProductRepository;
import com.tashow.erp.service.RakutenScrapingService;
import com.tashow.erp.utils.RakutenProxyUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 乐天商品爬取服务实现类
* 负责从乐天网站采集商品数据包括商品搜索、1688链接更新和数据展示
*/
@Slf4j
@Service
public class RakutenScrapingServiceImpl implements RakutenScrapingService {
@Autowired
private RakutenProductRepository rakutenProductRepository;
@Autowired
ObjectMapper objectMapper;
/**
* 根据店铺名称搜索并采集乐天商品数据
*/
@Override
public List<RakutenProduct> scrapeProductsWithSearch(String shopName) {
String url = "https://ranking.rakuten.co.jp/search?stx=" + URLEncoder.encode(shopName, StandardCharsets.UTF_8);
List<RakutenProduct> products = new ArrayList<>();
Spider spider = Spider.create(new RakutenPageProcessor(products)).addUrl(url).setDownloader(new RakutenProxyUtil().createProxyDownloader(new RakutenProxyUtil().detectSystemProxy(url))).thread(1);
spider.run();
log.info("采集完成,店铺: {},数量: {}", shopName, products.size());
return products;
}
private class RakutenPageProcessor implements PageProcessor {
private final List<RakutenProduct> products;
RakutenPageProcessor(List<RakutenProduct> products) {
this.products = products;
}
@Override
public void process(Page page) {
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 = Stream.of(rankings, productUrls, imageUrls, titles, prices).mapToInt(List::size).min().orElse(0);
if (count == 0) return;
for (int i = 0; i < count; i++) {
RakutenProduct product = new RakutenProduct();
String productUrl = productUrls.get(i);
product.setProductUrl(productUrl);
// 提取商店名称
String[] parts = productUrl.split("/");
String shopName = parts.length > 3 ? parts[3] : "";
product.setShopName(shopName);
product.setOriginalShopName(shopName); // 设置原始店铺名称
product.setImgUrl(imageUrls.get(i));
String title = titles.get(i).trim();
product.setProductTitle(title);
product.setProductName(title);
product.setPrice(prices.get(i).replaceAll("[^0-9]", ""));
product.setRanking(rankings.get(i).trim());
// 设置默认的1688相关字段
product.setMapRecognitionLink("");
product.setImage1688Url("");
product.setDetailUrl1688("");
product.setFreight(null);
product.setMedian(null);
product.setWeight("");
products.add(product);
}
}
@Override
public Site getSite() {
Site site = Site.me().setRetryTimes(3).setCycleRetryTimes(3).setTimeOut(15000).setSleepTime(2000 + new Random().nextInt(3000));
site.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
site.addHeader("Accept", "text/html");
site.addHeader("Cache-Control", "no-cache");
return site;
}
}
/**
* 根据图片URL更新1688搜索结果的所有字段
*/
@Override
@Transactional
public void update1688DataByImageUrl(SearchResult searchResult, String sessionId, String imageUrl) {
List<RakutenProductEntity> matchingProducts = rakutenProductRepository.findBySessionIdOrderByCreatedAtAscIdAsc(sessionId).stream().filter(product -> imageUrl.equals(product.getImgUrl())).peek(product -> {
product.setMapRecognitionLink(searchResult.getMapRecognitionLink());
product.setFreight(searchResult.getFreight());
product.setSkuPriceJson(searchResult.getSkuPrice().toString());
product.setMedian(searchResult.getMedian());
product.setWeight(searchResult.getWeight());
product.setSkuPriceJson(JSON.toJSONString(searchResult.getSkuPrice()));
}).collect(Collectors.toList());
if (!matchingProducts.isEmpty()) {
rakutenProductRepository.saveAll(matchingProducts);
}
}
/**
* 获取最新的产品数据用于前端展示
*/
@Override
public List<Map<String, Object>> getLatestProductsForDisplay() {
return rakutenProductRepository.findLatestProducts().stream().map(entity -> {
Map<String, Object> result = new HashMap<>();
result.put("originalShopName", Optional.ofNullable(entity.getOriginalShopName()).orElse(""));
result.put("productUrl", Optional.ofNullable(entity.getProductUrl()).orElse(""));
result.put("imgUrl", Optional.ofNullable(entity.getImgUrl()).orElse(""));
result.put("productTitle", Optional.ofNullable(entity.getProductTitle()).orElse(""));
result.put("price", Optional.ofNullable(entity.getPrice()).orElse(""));
result.put("ranking", Optional.ofNullable(entity.getRanking()).orElse(""));
result.put("image1688Url", Optional.ofNullable(entity.getImage1688Url()).orElse(""));
result.put("detailUrl1688", Optional.ofNullable(entity.getDetailUrl1688()).orElse(""));
// 使用正确的实体字段
result.put("mapRecognitionLink", Optional.ofNullable(entity.getMapRecognitionLink()).orElse(""));
result.put("freight", entity.getFreight());
result.put("median", entity.getMedian());
result.put("weight", Optional.ofNullable(entity.getWeight()).orElse(""));
result.put("skuPrice",entity.getSkuPriceJson());
return result;
}).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,58 @@
package com.tashow.erp.test;
import com.tashow.erp.utils.StealthSelenium;
import org.openqa.selenium.chrome.ChromeDriver;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SeleniumWithProfile {
private static final Pattern WEIGHT_PATTERN = Pattern.compile("\"(?:unitWeight|weight)\":(\\d+(?:\\.\\d+)?)");
public static void main(String[] args) {
String uuid = "";
try {
Process process = Runtime.getRuntime().exec(
new String[]{"wmic", "csproduct", "get", "UUID"});
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !line.toLowerCase().contains("uuid")) {
uuid = line;
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("电脑序列号: " + uuid);
// ChromeDriver driver = StealthSelenium.createDriver();
// try {
//
// // 访问目标网站
// System.out.println("正在访问目标网站...");
// driver.get("https://detail.1688.com/offer/600366775654.html");
// // driver.navigate().refresh();
// String source = driver.getPageSource();
// String weight = "";
// Matcher weightMatcher = WEIGHT_PATTERN.matcher(source);
// if (weightMatcher.find()) {
// String weightValue = weightMatcher.group(1);
// weight = " Weight: " + (weightValue.contains(".") ? (int) (Float.parseFloat(weightValue) * 1000) + "g" : weightValue + "g");
// }
// System.out.println(driver.getTitle());
// } catch (Exception e) {
// System.err.println("运行出错: " + e.getMessage());
// e.printStackTrace();
// }finally {
// driver.quit();
//
// }
}
}

View File

@@ -0,0 +1,182 @@
package com.tashow.erp.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
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;
public static final String APP_KEY = "12574478";
private static final String M_H5_TK = "_m_h5_tk";
private static final String M_H5_TK_ENC = "_m_h5_tk_enc";
static RestTemplate restTemplate=new RestTemplateBuilder().build();
/**
* 获取1688 API的Token (从_m_h5_tk cookie中提取)
*
* @return Token字符串失败返回空字符串
*/
public static String getToken() {
String tokenCookie = getCookie(M_H5_TK);
if (tokenCookie != null && !tokenCookie.isEmpty()) {
return tokenCookie.split("_")[0];
}
return "";
}
/**
* 获取指定名称的Cookie值
*
* @param cookieName Cookie名称
* @return Cookie值未找到返回null
*/
public static String getCookie(String cookieName) {
if (System.currentTimeMillis() - lastUpdateTime > COOKIE_EXPIRE_TIME) {
refreshCookies();
}
return cookieCache.get(cookieName);
}
/**
* 获取完整的Cookie字符串用于HTTP请求头
* @return 完整的Cookie字符串
*/
public static String getCookieString() {
if (System.currentTimeMillis() - lastUpdateTime > COOKIE_EXPIRE_TIME) {
refreshCookies();
}
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
*/
public static synchronized void refreshCookies() {
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);
cookies = response.getHeaders().get("Set-Cookie");
if (cookies != null) {
for (String cookie : cookies) {
parseCookie(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
*
*/
public static void setCookie(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) {
cookieCache.put(parts[0], parts[1]);
}
}
}
lastUpdateTime = System.currentTimeMillis();
}
/**
* 清除所有Cookie缓存
*/
public static void clearCookies() {
cookieCache.clear();
lastUpdateTime = 0;
}
public static String generateSign(String token, String timestamp, String data) {
try {
String signStr = token + "&" + timestamp + "&" + APP_KEY + "&" + data;
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(signStr.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
return "";
}
}
}

View File

@@ -0,0 +1,58 @@
package com.tashow.erp.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@Component
public class ApiForwarder {
private final RestTemplate restTemplate;
@Value("${api.server.base-url}")
private String serverBaseUrl;
public ApiForwarder(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public ResponseEntity<?> post(String path, Map<String, Object> body, String auth) {
String url = serverBaseUrl + path;
HttpHeaders headers = buildHeaders(auth);
headers.set("X-Client-User", System.getProperty("user.name"));
headers.set("X-Client-OS", System.getProperty("os.name"));
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
return restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
}
public ResponseEntity<?> get(String pathWithQuery, String auth) {
String url = serverBaseUrl + pathWithQuery;
HttpHeaders headers = buildHeaders(auth);
headers.set("X-Client-User", System.getProperty("user.name"));
headers.set("X-Client-OS", System.getProperty("os.name"));
HttpEntity<Void> entity = new HttpEntity<>(headers);
return restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);
}
public ResponseEntity<?> sse(String pathWithQuery) {
String url = serverBaseUrl + pathWithQuery;
HttpHeaders headers = new HttpHeaders();
headers.setAccept(java.util.Collections.singletonList(MediaType.TEXT_EVENT_STREAM));
HttpEntity<Void> entity = new HttpEntity<>(headers);
return restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
}
private HttpHeaders buildHeaders(String auth) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
if (auth != null && !auth.isEmpty()) {
headers.set("Authorization", auth);
}
return headers;
}
}

View File

@@ -0,0 +1,63 @@
package com.tashow.erp.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 数据采集上报工具类
* 统一处理各服务的数据上报逻辑
*/
@Component
public class DataReportUtil {
private static final Logger logger = LoggerFactory.getLogger(DataReportUtil.class);
@Autowired
private ApiForwarder apiForwarder;
@Value("${api.server.base-url}")
private String serverApiUrl;
@Value("${api.server.paths.data}")
private String monitorApiPath;
/**
* 上报数据采集事件
*/
public void reportDataCollection(String dataType, int dataCount, String status) {
try {
Map<String, Object> reportData = new HashMap<>();
reportData.put("clientId", generateClientId(dataType));
reportData.put("dataType", dataType);
reportData.put("dataCount", dataCount);
reportData.put("status", status);
sendReportData(reportData);
logger.debug("数据上报成功: {} - {} 条", dataType, dataCount);
} catch (Exception e) {
logger.warn("数据上报失败: {}", e.getMessage());
}
}
private void sendReportData(Map<String, Object> reportData) {
apiForwarder.post(monitorApiPath, reportData, null);
}
/**
* 生成客户端ID
*/
private String generateClientId(String serviceType) {
try {
String hostname = java.net.InetAddress.getLocalHost().getHostName();
String username = System.getProperty("user.name", "user");
return hostname + "_" + username + "_" + serviceType.toLowerCase();
} catch (Exception e) {
return serviceType.toLowerCase() + "_client_" + System.currentTimeMillis();
}
}
}

View File

@@ -0,0 +1,33 @@
package com.tashow.erp.utils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.UUID;
/**
* 设备工具类
*/
public class DeviceUtils {
/**
* 生成设备ID
* 优先使用硬件UUID失败则使用随机UUID
*/
public static String generateDeviceId() {
try {
Process process = Runtime.getRuntime().exec(new String[]{"wmic", "csproduct", "get", "UUID"});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !line.toLowerCase().contains("uuid")) {
return line;
}
}
}
} catch (Exception e) {
// 静默处理异常
}
return UUID.randomUUID().toString();
}
}

View File

@@ -0,0 +1,110 @@
package com.tashow.erp.utils;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.io.StringWriter;
import java.io.PrintWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import com.tashow.erp.service.IAuthService;
/**
* 错误上报工具类 - 立即上报所有错误
*
* @author ERP
*/
@Component
public class ErrorReporter {
@Value("${server.monitor.url:http://localhost:8080}")
private String serverUrl;
@Autowired
private IAuthService authService;
private final RestTemplate restTemplate = new RestTemplate();
/**
* 上报启动失败错误
*/
public void reportStartupError(String component, Exception ex) {
reportError("STARTUP_ERROR", component + " 启动失败", ex);
}
/**
* 上报业务处理错误
*/
public void reportBusinessError(String operation, Exception ex) {
reportError("BUSINESS_ERROR", operation + " 处理失败", ex);
}
/**
* 上报数据采集错误
*/
public void reportDataCollectError(String platform, Exception ex) {
reportError("DATA_COLLECT_ERROR", platform + " 数据采集失败", ex);
}
/**
* 上报网络连接错误
*/
public void reportNetworkError(String url, Exception ex) {
reportError("NETWORK_ERROR", "网络请求失败: " + url, ex);
}
/**
* 上报系统异常
*/
public void reportSystemError(String message, Exception ex) {
reportError("SYSTEM_ERROR", message, ex);
}
/**
* 通用错误上报方法
*/
private void reportError(String errorType, String errorMessage, Exception ex) {
CompletableFuture.runAsync(() -> {
try {
Map<String, Object> errorData = new HashMap<>();
errorData.put("clientId", authService.getClientId());
errorData.put("username", System.getProperty("user.name", "user"));
errorData.put("errorType", errorType);
errorData.put("errorMessage", errorMessage);
errorData.put("stackTrace", getStackTrace(ex));
// 添加系统信息
errorData.put("osName", System.getProperty("os.name"));
errorData.put("osVersion", System.getProperty("os.version"));
errorData.put("appVersion", System.getProperty("project.version", "unknown"));
String url = serverUrl + "/monitor/client/api/error";
restTemplate.postForObject(url, errorData, Map.class);
} catch (Exception e) {
System.err.println("错误上报失败: " + e.getMessage());
}
});
}
/**
* 获取异常堆栈信息
*/
private String getStackTrace(Exception ex) {
if (ex == null) return "";
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
return sw.toString();
}
/**
* 设置全局异常处理器
*/
public void setupGlobalExceptionHandler() {
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
reportSystemError("未捕获异常: " + thread.getName(), (Exception) ex);
});
}
}

View File

@@ -0,0 +1,310 @@
package com.tashow.erp.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.concurrent.*;
/**
* Excel导出工具类支持图片嵌入
*/
@Slf4j
public class ExcelExportUtil {
/**
* 创建包含图片的Excel文件
* @param sheetName 工作表名称
* @param headers 表头
* @param data 数据列表
* @param imageColumnIndex 图片列索引
* @param imageUrlKey 图片URL的字段名
* @return Excel文件字节数组
*/
public static byte[] createExcelWithImages(String sheetName, String[] headers,
List<Map<String, Object>> data, int imageColumnIndex, String imageUrlKey) {
try {
XSSFWorkbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet(sheetName);
// 创建标题行
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
}
// 创建绘图容器
Drawing<?> drawing = sheet.createDrawingPatriarch();
// 批量下载图片
Map<String, byte[]> imageCache = new HashMap<>();
downloadImagesInBatches(data, imageCache, imageUrlKey);
// 填充数据
for (int rowIndex = 0; rowIndex < data.size(); rowIndex++) {
Map<String, Object> rowData = data.get(rowIndex);
Row dataRow = sheet.createRow(rowIndex + 1);
dataRow.setHeightInPoints(80);
// 填充各列数据
fillRowData(dataRow, rowData, headers, rowIndex);
// 处理图片列
if (imageColumnIndex >= 0) {
String imgUrl = getStringValue(rowData, imageUrlKey);
embedImage(workbook, drawing, imageCache, imgUrl, imageColumnIndex, rowIndex + 1, dataRow);
}
}
// 调整列宽
adjustColumnWidths(sheet, headers, imageColumnIndex);
// 输出文件
ByteArrayOutputStream out = new ByteArrayOutputStream();
workbook.write(out);
workbook.close();
return out.toByteArray();
} catch (Exception e) {
log.error("创建Excel文件失败: {}", e.getMessage(), e);
return new byte[0];
}
}
/**
* 填充行数据 - 需要子类实现具体逻辑
*/
private static void fillRowData(Row dataRow, Map<String, Object> rowData, String[] headers, int rowIndex) {
// 这里可以根据不同的数据类型进行扩展
int colIndex = 0;
for (String header : headers) {
if (!header.equals("商品图片")) { // 跳过图片列
String key = getKeyByHeader(header);
if (key != null) {
Object value = rowData.get(key);
dataRow.createCell(colIndex).setCellValue(formatValue(value, header));
}
}
colIndex++;
}
}
/**
* 根据表头获取对应的数据字段名
*/
private static String getKeyByHeader(String header) {
Map<String, String> headerKeyMap = new HashMap<>();
headerKeyMap.put("下单时间", "orderedAt");
headerKeyMap.put("商品名称", "productTitle");
headerKeyMap.put("乐天订单号", "shopOrderNumber");
headerKeyMap.put("下单距今时间", "timeSinceOrder");
headerKeyMap.put("乐天订单金额/日元", "priceJpy");
headerKeyMap.put("购买数量", "productQuantity");
headerKeyMap.put("税费/日元", "shippingFeeJpy");
headerKeyMap.put("服务商回款抽点rmb", "serviceFee");
headerKeyMap.put("商品番号", "productNumber");
headerKeyMap.put("1688采购订单号", "poNumber");
headerKeyMap.put("采购金额/rmb", "shippingFeeCny");
headerKeyMap.put("国际运费/rmb", "internationalShippingFee");
headerKeyMap.put("国内物流公司", "poLogisticsCompany");
headerKeyMap.put("国内物流单号", "poTrackingNumber");
headerKeyMap.put("日本物流单号", "internationalTrackingNumber");
headerKeyMap.put("地址状态", "trackInfo");
// 乐天商品相关
headerKeyMap.put("店铺名", "originalShopName");
headerKeyMap.put("商品链接", "productUrl");
headerKeyMap.put("排名", "ranking");
headerKeyMap.put("商品标题", "productTitle"); // 添加缺失的商品标题映射
headerKeyMap.put("价格", "price");
headerKeyMap.put("1688识图链接", "image1688Url");
headerKeyMap.put("1688价格", "median");
headerKeyMap.put("1688重量", "weight");
return headerKeyMap.get(header);
}
/**
* 格式化值
*/
private static String formatValue(Object value, String header) {
if (value == null) return "";
if (header.contains("日元")) {
try {
double amount = Double.parseDouble(value.toString());
return String.format("¥%.0f", amount);
} catch (NumberFormatException e) {
return value.toString();
}
} else if (header.contains("rmb")) {
try {
double amount = Double.parseDouble(value.toString());
return String.format("¥%.2f", amount);
} catch (NumberFormatException e) {
return value.toString();
}
} else if (value instanceof List) {
List<?> list = (List<?>) value;
return list.isEmpty() ? "" : list.get(0).toString();
}
return value.toString();
}
/**
* 嵌入图片
*/
private static void embedImage(XSSFWorkbook workbook, Drawing<?> drawing, Map<String, byte[]> imageCache,
String imgUrl, int imageColumnIndex, int rowIndex, Row dataRow) {
if (imgUrl != null && !imgUrl.isEmpty()) {
byte[] imageBytes = imageCache.get(imgUrl);
if (imageBytes != null && imageBytes.length > 0) {
try {
int pictureIdx = workbook.addPicture(imageBytes, Workbook.PICTURE_TYPE_JPEG);
ClientAnchor anchor = workbook.getCreationHelper().createClientAnchor();
anchor.setCol1(imageColumnIndex);
anchor.setRow1(rowIndex);
anchor.setCol2(imageColumnIndex + 1);
anchor.setRow2(rowIndex + 1);
Picture picture = drawing.createPicture(anchor, pictureIdx);
picture.resize(0.8);
} catch (Exception e) {
log.warn("嵌入图片失败: {}, 错误: {}", imgUrl, e.getMessage());
dataRow.createCell(imageColumnIndex).setCellValue("图片嵌入失败");
}
} else {
dataRow.createCell(imageColumnIndex).setCellValue("图片加载失败");
}
} else {
dataRow.createCell(imageColumnIndex).setCellValue("无图片");
}
}
/**
* 调整列宽
*/
private static void adjustColumnWidths(Sheet sheet, String[] headers, int imageColumnIndex) {
for (int i = 0; i < headers.length; i++) {
if (i != imageColumnIndex) {
sheet.autoSizeColumn(i);
} else {
sheet.setColumnWidth(i, 25 * 256);
}
}
}
/**
* 批量下载图片
*/
private static void downloadImagesInBatches(List<Map<String, Object>> data, Map<String, byte[]> imageCache, String imageUrlKey) {
final int BATCH_SIZE = 10; // 减少批次大小提升并发效率
final int MAX_RETRIES = 3; // 增加重试次数提高成功率
Set<String> uniqueUrls = new HashSet<>();
for (Map<String, Object> item : data) {
String imgUrl = getStringValue(item, imageUrlKey);
if (imgUrl != null && !imgUrl.isEmpty()) {
uniqueUrls.add(imgUrl);
}
}
List<String> urlList = new ArrayList<>(uniqueUrls);
log.info("开始批量下载图片,共 {} 个唯一图片", urlList.size());
for (int i = 0; i < urlList.size(); i += BATCH_SIZE) {
int endIndex = Math.min(i + BATCH_SIZE, urlList.size());
List<String> batch = urlList.subList(i, endIndex);
log.info("处理第 {}/{} 批图片,本批 {} 个",
(i / BATCH_SIZE) + 1, (urlList.size() + BATCH_SIZE - 1) / BATCH_SIZE, batch.size());
ExecutorService executor = Executors.newFixedThreadPool(Math.min(12, batch.size())); // 增加线程池大小
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (String url : batch) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
byte[] imageBytes = downloadImageWithRetry(url, MAX_RETRIES);
if (imageBytes != null && imageBytes.length > 0) {
synchronized (imageCache) {
imageCache.put(url, imageBytes);
}
}
}, executor);
futures.add(future);
}
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.get(30, TimeUnit.SECONDS); // 减少超时时间
} catch (Exception e) {
log.warn("批量下载图片超时或失败: {}", e.getMessage());
// 取消未完成的任务
futures.forEach(future -> future.cancel(true));
}
executor.shutdown();
if (i + BATCH_SIZE < urlList.size()) {
try {
Thread.sleep(500); // 减少批次间延迟
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
break;
}
}
}
log.info("图片下载完成,成功缓存 {} 个图片", imageCache.size());
}
/**
* 带重试的图片下载 - 优化版本
*/
private static byte[] downloadImageWithRetry(String imageUrl, int maxRetries) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
URL url = new URL(imageUrl);
// 设置连接超时和读取超时
java.net.URLConnection connection = url.openConnection();
connection.setConnectTimeout(5000); // 5秒连接超时
connection.setReadTimeout(10000); // 10秒读取超时
connection.setRequestProperty("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
try (InputStream inputStream = connection.getInputStream()) {
byte[] data = IOUtils.toByteArray(inputStream);
if (data.length > 0) {
return data;
}
}
} catch (Exception e) {
if (attempt == maxRetries) {
log.warn("下载图片失败 (已重试{}次): {}, 错误: {}", maxRetries, imageUrl, e.getMessage());
} else {
try {
Thread.sleep(500 * attempt); // 减少重试间隔
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
break;
}
}
}
}
return null;
}
/**
* 安全获取字符串值
*/
private static String getStringValue(Map<String, Object> map, String key) {
Object value = map.get(key);
return value != null ? value.toString() : "";
}
}

View File

@@ -0,0 +1,104 @@
package com.tashow.erp.utils;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* Excel 解析工具类
* 统一处理各种平台的 Excel 文件解析需求
*
* @author Claude Code
*/
@Slf4j
public class ExcelParseUtil {
/**
* 解析 Excel 文件第一列数据
* 通用方法适用于店铺名、ASIN、订单号等标识符解析
*
* @param file Excel 文件
* @return 解析出的字符串列表,跳过表头,过滤空值
*/
public static List<String> parseFirstColumn(MultipartFile file) {
List<String> result = new ArrayList<>();
if (file == null || file.isEmpty()) {
log.warn("Excel 文件为空");
return result;
}
try (InputStream in = file.getInputStream()) {
ExcelReader reader = ExcelUtil.getReader(in, 0);
List<List<Object>> rows = reader.read();
for (int i = 1; i < rows.size(); i++) { // 从第2行开始跳过表头
List<Object> row = rows.get(i);
if (row != null && !row.isEmpty()) {
Object cell = row.get(0); // 获取第一列
if (cell != null) {
String value = cell.toString().trim();
if (!value.isEmpty()) {
result.add(value);
}
}
}
}
log.info("成功解析 Excel 文件: {}, 共解析出 {} 条数据",
file.getOriginalFilename(), result.size());
} catch (Exception e) {
log.error("解析 Excel 文件失败: {}, 文件名: {}", e.getMessage(),
file.getOriginalFilename(), e);
}
return result;
}
/**
* 解析指定列的数据
*
* @param file Excel 文件
* @param columnIndex 列索引从0开始
* @return 解析出的字符串列表
*/
public static List<String> parseColumn(MultipartFile file, int columnIndex) {
List<String> result = new ArrayList<>();
if (file == null || file.isEmpty()) {
log.warn("Excel 文件为空");
return result;
}
try (InputStream in = file.getInputStream()) {
ExcelReader reader = ExcelUtil.getReader(in, 0);
List<List<Object>> rows = reader.read();
for (int i = 1; i < rows.size(); i++) { // 从第2行开始跳过表头
List<Object> row = rows.get(i);
if (row != null && row.size() > columnIndex) {
Object cell = row.get(columnIndex);
if (cell != null) {
String value = cell.toString().trim();
if (!value.isEmpty()) {
result.add(value);
}
}
}
}
log.info("成功解析 Excel 文件第{}列: {}, 共解析出 {} 条数据",
columnIndex + 1, file.getOriginalFilename(), result.size());
} catch (Exception e) {
log.error("解析 Excel 文件第{}列失败: {}, 文件名: {}",
columnIndex + 1, e.getMessage(), file.getOriginalFilename(), e);
}
return result;
}
}

View File

@@ -0,0 +1,96 @@
package com.tashow.erp.utils;
public class JsonData {
/**
* 状态码 0表示成功过1表示处理中-1 表示失败
*/
private Integer code;
/**
* 业务数据
*/
private Object data;
/**
* 信息表示
*/
private String msg;
public JsonData() {
}
public JsonData(Integer code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}
/**
* 成功,不用返回数据
*
* @return
*/
public static JsonData buildSuccess() {
return new JsonData(0, null, null);
}
/**
* 成功,返回数据
*
* @param data
* @return
*/
public static JsonData buildSuccess(Object data) {
return new JsonData(0, data, null);
}
/**
* 失败,固定状态码
*
* @param msg
* @return
*/
public static JsonData buildError(String msg) {
return new JsonData(-1, null, msg);
}
/**
* 失败,自定义错误码和信息
*
* @param code
* @param msg
* @return
*/
public static JsonData buildError(Integer code, String msg) {
return new JsonData(code, null, msg);
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

View File

@@ -0,0 +1,73 @@
package com.tashow.erp.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 日志工具类
*
* @author ruoyi
*/
public class LoggerUtil {
/**
* 获取Logger实例
*/
public static Logger getLogger(Class<?> clazz) {
return LoggerFactory.getLogger(clazz);
}
/**
* 记录API调用信息
*/
public static void logApiCall(Logger logger, String apiName, String url, Object params) {
if (logger.isDebugEnabled()) {
logger.debug("调用API: {} - URL: {} - 参数: {}", apiName, url, params);
}
}
/**
* 记录API响应信息
*/
public static void logApiResponse(Logger logger, String apiName, Object response) {
if (logger.isDebugEnabled()) {
logger.debug("API响应: {} - 结果: {}", apiName, response);
}
}
/**
* 记录错误信息
*/
public static void logError(Logger logger, String operation, Throwable throwable) {
logger.error("操作失败: {} - 错误: {}", operation, throwable.getMessage(), throwable);
}
/**
* 记录错误信息(不包含堆栈跟踪)
*/
public static void logErrorSimple(Logger logger, String operation, String errorMessage) {
logger.error("操作失败: {} - 错误: {}", operation, errorMessage);
}
/**
* 记录警告信息
*/
public static void logWarning(Logger logger, String operation, String message) {
logger.warn("操作警告: {} - 消息: {}", operation, message);
}
/**
* 记录信息
*/
public static void logInfo(Logger logger, String operation, String message) {
logger.info("操作信息: {} - 消息: {}", operation, message);
}
/**
* 记录性能信息
*/
public static void logPerformance(Logger logger, String operation, long startTime) {
long duration = System.currentTimeMillis() - startTime;
logger.info("性能统计: {} - 耗时: {}ms", operation, duration);
}
}

View File

@@ -0,0 +1,63 @@
package com.tashow.erp.utils;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.chrome.ChromeDriver;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class PersistentDriverManager {
private ChromeDriver headlessDriver;
private ChromeDriver headfulDriver;
private boolean usingHeadless = true;
@PostConstruct
public void init() {
headlessDriver = StealthSelenium.createDriver(true);
}
public ChromeDriver getCurrentDriver() {
return usingHeadless ? headlessDriver : headfulDriver;
}
public ChromeDriver switchToHeadful() {
if (usingHeadless) {
if (headfulDriver == null) {
headfulDriver = StealthSelenium.createDriver(false);
}
transferState(headlessDriver, headfulDriver);
usingHeadless = false;
}
return headfulDriver;
}
private void transferState(ChromeDriver from, ChromeDriver to) {
try {
String currentUrl = from.getCurrentUrl();
Set<Cookie> cookies = from.manage().getCookies();
to.get(currentUrl);
cookies.forEach(cookie -> {
try {
to.manage().addCookie(cookie);
} catch (Exception ignored) {}
});
to.navigate().refresh();
} catch (Exception ignored) {}
}
@PreDestroy
public void cleanup() {
if (headlessDriver != null) {
headlessDriver.quit();
}
if (headfulDriver != null) {
headfulDriver.quit();
}
}
}

View File

@@ -0,0 +1,44 @@
package com.tashow.erp.utils;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.util.Auth;
import org.springframework.web.client.RestTemplate;
import java.io.ByteArrayInputStream;
public class QiniuUtil {
// 七牛配置
private static final String ACCESS_KEY = "M1I8ItQjEYOYXyJYieloSSaIG8Ppi4lfCAyZ8BaF";
private static final String SECRET_KEY = "Xvi0SwtL9WVOl28h6DNRLKP9MnZZqsKBWrC8shAl";
private static final String BUCKET = "pxdj-prod";
private static final String DOMAIN = "https://qiniu.pxdj.tashowz.com/"; // 开启HTTPS的外链域名
public static String uploadFromUrl(String imageUrl, String key) throws Exception {
// 1. 下载远程图片
RestTemplate restTemplate = new RestTemplate();
byte[] imageBytes = restTemplate.getForObject(imageUrl, byte[].class);
// 2. 配置七牛上传管理器
Configuration cfg = new Configuration(Region.autoRegion());
UploadManager uploadManager = new UploadManager(cfg);
// 3. 生成上传凭证
Auth auth = Auth.create(ACCESS_KEY, SECRET_KEY);
String upToken = auth.uploadToken(BUCKET);
// 4. 上传图片
try {
Response response = uploadManager.put(new ByteArrayInputStream(imageBytes), key, upToken, null, null);
if (response.isOK()) {
// 返回HTTPS访问URL
return DOMAIN + key;
} else {
throw new RuntimeException("上传失败: " + response.bodyString());
}
} catch (QiniuException ex) {
throw new RuntimeException("上传异常: " + ex.response.toString(), ex);
}
}
}

View File

@@ -0,0 +1,123 @@
package com.tashow.erp.utils;
import us.codecraft.webmagic.downloader.HttpClientDownloader;
import us.codecraft.webmagic.proxy.SimpleProxyProvider;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Proxy;
/**
* 乐天爬取代理工具类
*/
public class RakutenProxyUtil {
/**
* 动态检测系统代理配置
* @param targetUrl 目标URL
* @return 代理信息如果没有代理返回null
*/
public Proxy detectSystemProxy(String targetUrl) {
try {
System.setProperty("java.net.useSystemProxies", "true");
Proxy windowsProxy = getWindowsProxy();
if (windowsProxy != null) {
return windowsProxy;
}
System.out.println("未找到可用的HTTP或SOCKS代理");
} catch (Exception e) {
System.out.println("检测系统代理失败:"+e );
}
return null;
}
/**
* 从Windows注册表获取代理设置
*/
private Proxy getWindowsProxy() {
try {
// 先检查ProxyEnable是否启用
String enableCommand = "reg query \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyEnable";
Process enableProcess = Runtime.getRuntime().exec(enableCommand);
try (BufferedReader enableReader = new BufferedReader(new InputStreamReader(enableProcess.getInputStream(), "GBK"))) {
String line;
while ((line = enableReader.readLine()) != null) {
if (line.contains("ProxyEnable") && line.contains("REG_DWORD")) {
String[] parts = line.trim().split("\\s+");
if (parts.length >= 3) {
String enableValue = parts[parts.length - 1];
if ("0x0".equals(enableValue) || "0".equals(enableValue)) {
System.out.println("系统代理未启用,不使用代理");
return null;
}
}
}
}
}
enableProcess.waitFor();
// ProxyEnable为1继续检查ProxyServer配置
String command = "reg query \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyServer";
Process process = Runtime.getRuntime().exec(command);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("ProxyServer") && line.contains("REG_SZ")) {
String[] parts = line.trim().split("\\s+");
if (parts.length >= 3) {
String proxyServer = parts[parts.length - 1];
if (proxyServer.contains(":")) {
String[] hostPort = proxyServer.split(":");
if (hostPort.length == 2) {
try {
String host = hostPort[0];
int port = Integer.parseInt(hostPort[1]);
InetSocketAddress address = new InetSocketAddress(host, port);
System.out.println("检测到系统代理: " + host + ":" + port);
return new Proxy(Proxy.Type.HTTP, address);
} catch (NumberFormatException e) {
System.out.println("Windows代理端口格式错误: "+hostPort[1]);
}
}
}
}
}
}
}
process.waitFor();
} catch (Exception e) {
System.out.println("读取Windows代理设置失败: " + e.getMessage());
}
return null;
}
/**
* 创建带代理的HttpClientDownloader - 禁用默认行为
* @param proxy 代理对象
* @return HttpClientDownloader实例
*/
public HttpClientDownloader createProxyDownloader(Proxy proxy) {
HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
if (proxy != null && proxy.address() instanceof InetSocketAddress) {
InetSocketAddress address = (InetSocketAddress) proxy.address();
String host = address.getHostName();
int port = address.getPort();
clearSystemProxy();
System.setProperty("http.proxyHost", host);
System.setProperty("http.proxyPort", String.valueOf(port));
System.setProperty("https.proxyHost", host);
System.setProperty("https.proxyPort", String.valueOf(port));
httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(
new us.codecraft.webmagic.proxy.Proxy(host, port)
));
System.out.println("已配置代理:"+ host+""+port);
}
return httpClientDownloader;
}
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");
}
}

View File

@@ -0,0 +1,23 @@
package com.tashow.erp.utils;
import lombok.extern.slf4j.Slf4j;
/**
* 资源预加载器(纯 Spring Boot 占位实现)
* 去除 JavaFX 依赖,保留方法签名以兼容调用方。
*/
@Slf4j
public class ResourcePreloader {
public static synchronized void init() {
// no-op in Spring Boot mode
}
public static java.util.concurrent.CompletableFuture<Void> preloadErpDashboard() {
return java.util.concurrent.CompletableFuture.completedFuture(null);
}
public static void executePreloading() {
// no-op in Spring Boot mode
}
}

View File

@@ -0,0 +1,99 @@
package com.tashow.erp.utils;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
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;
@Data
public class SagawaExpressSdk {
private static final Logger logger = LoggerFactory.getLogger(SagawaExpressSdk.class);
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
private int timeout = 5000; // 默认超时时间5秒
/**
* 关闭线程池
*/
// @PreDestroy
public void destroy() {
executorService.shutdownNow();
}
/**
* 查询佐川急便物流信息 - API入口
*/
public Map<String, Object> getTrackingInfo(String trackingNumber) {
try {
String url = "https://k2k.sagawa-exp.co.jp/p/web/okurijosearch.do?okurijoNo=" + trackingNumber.trim();
Map<String, Object> result = new HashMap<>();
Spider.create(new PageProcessor() {
@Override
public void process(Page page) {
String pageContent = page.getHtml().toString();
if (pageContent.contains("お荷物データが登録されておりません")) {
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);
System.out.println("11111111111"+trackInfo);
result.put("trackInfo", trackInfo);
} else {
}
}
}
@Override
public Site getSite() {
return Site.me()
.setTimeOut(timeout) // 使用设置的超时时间
.setRetryTimes(3)
.setSleepTime(500);
}
}).addUrl(url).thread(executorService, 5).start();
return result;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,143 @@
package com.tashow.erp.utils;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.Point;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* Selenium反检测工具类
* 使用方法ChromeDriver driver = StealthSelenium.createDriver();
*/
public class StealthSelenium {
private static final List<String> USER_AGENTS = Arrays.asList(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
);
private static final Random random = new Random();
/**
* 创建反检测ChromeDriver
* @return 配置好的ChromeDriver实例
*/
public static ChromeDriver createDriver() {
return createDriver(false);
}
/**
* 创建反检测ChromeDriver
* @param headless 是否无头模式
* @return 配置好的ChromeDriver实例
*/
public static ChromeDriver createDriver(boolean headless) {
WebDriverManager.chromedriver().setup();
ChromeOptions options = createStealthOptions(headless);
ChromeDriver driver = new ChromeDriver(options);
applyAntiDetection(driver);
setRandomWindowProperties(driver);
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
return driver;
}
private static ChromeOptions createStealthOptions(boolean headless) {
ChromeOptions options = new ChromeOptions();
options.addArguments(
"--disable-blink-features=AutomationControlled",
"--disable-extensions",
"--disable-gpu",
"--disable-dev-shm-usage",
"--no-sandbox",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-renderer-backgrounding",
"--disable-features=TranslateUI",
"--disable-ipc-flooding-protection",
"--exclude-switches=enable-automation",
"--disable-component-extensions-with-background-pages",
"--disable-web-security",
"--disable-client-side-phishing-detection",
"--lang=zh-CN"
);
if (headless) {
options.addArguments("--headless");
}
String userAgent = USER_AGENTS.get(random.nextInt(USER_AGENTS.size()));
options.addArguments("--user-agent=" + userAgent);
Map<String, Object> prefs = new HashMap<>();
prefs.put("profile.default_content_setting_values.notifications", 2);
prefs.put("profile.default_content_settings.popups", 0);
options.setExperimentalOption("prefs", prefs);
options.setExperimentalOption("excludeSwitches", Arrays.asList("enable-automation"));
options.setExperimentalOption("useAutomationExtension", false);
return options;
}
private static void applyAntiDetection(ChromeDriver driver) {
String stealthScript = """
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
delete Object.getPrototypeOf(navigator).webdriver;
window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {}, app: {} };
Object.defineProperty(navigator, 'permissions', {
get: () => ({ query: () => Promise.resolve({state: 'granted'}) })
});
Object.defineProperty(navigator, 'plugins', {
get: () => Array.from({length: Math.floor(Math.random() * 5) + 2}, (_, i) => ({
name: `Plugin ${i}`, description: `Plugin Description ${i}`,
filename: `plugin${i}.dll`, version: `${Math.floor(Math.random() * 10)}.0.0`
}))
});
Object.defineProperty(navigator, 'languages', {
get: () => ['zh-CN', 'zh', 'en-US', 'en']
});
const screenWidth = 1920 + Math.floor(Math.random() * 100);
const screenHeight = 1080 + Math.floor(Math.random() * 100);
Object.defineProperty(screen, 'width', { get: () => screenWidth });
Object.defineProperty(screen, 'height', { get: () => screenHeight });
Object.defineProperty(screen, 'availWidth', { get: () => screenWidth });
Object.defineProperty(screen, 'availHeight', { get: () => screenHeight - 40 });
if (window.document) {
['$cdc_asdjflasutopfhvcZLmcfl_', '$chrome_asyncScriptInfo'].forEach(prop => {
Object.defineProperty(window.document, prop, { get: () => undefined, set: () => {} });
});
}
""";
Map<String, Object> params = new HashMap<>();
params.put("source", stealthScript);
driver.executeCdpCommand("Page.addScriptToEvaluateOnNewDocument", params);
String userAgent = USER_AGENTS.get(random.nextInt(USER_AGENTS.size()));
Map<String, Object> networkParams = new HashMap<>();
networkParams.put("userAgent", userAgent);
networkParams.put("acceptLanguage", "zh-CN,zh;q=0.9,en;q=0.8");
networkParams.put("platform", "Win32");
driver.executeCdpCommand("Network.setUserAgentOverride", networkParams);
}
private static void setRandomWindowProperties(ChromeDriver driver) {
int width = 1200 + random.nextInt(400);
int height = 800 + random.nextInt(300);
driver.manage().window().setSize(new Dimension(width, height));
driver.manage().window().setPosition(new Point(random.nextInt(100), random.nextInt(100)));
}
}

View File

@@ -0,0 +1,93 @@
package com.tashow.erp.utils;
import com.tashow.erp.common.Convert;
/**
* 字符串格式化
*
* @author ruoyi
*/
public class StrFormatter
{
public static final String EMPTY_JSON = "{}";
public static final char C_BACKSLASH = '\\';
public static final char C_DELIM_START = '{';
public static final char C_DELIM_END = '}';
/**
* 格式化字符串<br>
* 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
* 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
* 例:<br>
* 通常使用format("this is {} for {}", "a", "b") -> this is a for b<br>
* 转义{} format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
* 转义\ format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
*
* @param strPattern 字符串模板
* @param argArray 参数列表
* @return 结果
*/
public static String format(final String strPattern, final Object... argArray)
{
if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray))
{
return strPattern;
}
final int strPatternLength = strPattern.length();
// 初始化定义好的长度以获得更好的性能
StringBuilder sbuf = new StringBuilder(strPatternLength + 50);
int handledPosition = 0;
int delimIndex;// 占位符所在位置
for (int argIndex = 0; argIndex < argArray.length; argIndex++)
{
delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition);
if (delimIndex == -1)
{
if (handledPosition == 0)
{
return strPattern;
}
else
{ // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果
sbuf.append(strPattern, handledPosition, strPatternLength);
return sbuf.toString();
}
}
else
{
if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH)
{
if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH)
{
// 转义符之前还有一个转义符,占位符依旧有效
sbuf.append(strPattern, handledPosition, delimIndex - 1);
sbuf.append(Convert.utf8Str(argArray[argIndex]));
handledPosition = delimIndex + 2;
}
else
{
// 占位符被转义
argIndex--;
sbuf.append(strPattern, handledPosition, delimIndex - 1);
sbuf.append(C_DELIM_START);
handledPosition = delimIndex + 1;
}
}
else
{
// 正常占位符
sbuf.append(strPattern, handledPosition, delimIndex);
sbuf.append(Convert.utf8Str(argArray[argIndex]));
handledPosition = delimIndex + 2;
}
}
}
// 加入最后一个占位符后所有的字符
sbuf.append(strPattern, handledPosition, strPattern.length());
return sbuf.toString();
}
}

View File

@@ -0,0 +1,718 @@
package com.tashow.erp.utils;
import com.tashow.erp.common.Constants;
import org.springframework.util.AntPathMatcher;
import java.util.*;
/**
* 字符串工具类
*
* @author ruoyi
*/
public class StringUtils extends org.apache.commons.lang3.StringUtils
{
/** 空字符串 */
private static final String NULLSTR = "";
/** 下划线 */
private static final char SEPARATOR = '_';
/** 星号 */
private static final char ASTERISK = '*';
/**
* 获取参数不为空值
*
* @param value defaultValue 要判断的value
* @return value 返回值
*/
public static <T> T nvl(T value, T defaultValue)
{
return value != null ? value : defaultValue;
}
/**
* * 判断一个Collection是否为空 包含ListSetQueue
*
* @param coll 要判断的Collection
* @return true为空 false非空
*/
public static boolean isEmpty(Collection<?> coll)
{
return isNull(coll) || coll.isEmpty();
}
/**
* * 判断一个Collection是否非空包含ListSetQueue
*
* @param coll 要判断的Collection
* @return true非空 false
*/
public static boolean isNotEmpty(Collection<?> coll)
{
return !isEmpty(coll);
}
/**
* * 判断一个对象数组是否为空
*
* @param objects 要判断的对象数组
** @return true为空 false非空
*/
public static boolean isEmpty(Object[] objects)
{
return isNull(objects) || (objects.length == 0);
}
/**
* * 判断一个对象数组是否非空
*
* @param objects 要判断的对象数组
* @return true非空 false
*/
public static boolean isNotEmpty(Object[] objects)
{
return !isEmpty(objects);
}
/**
* * 判断一个Map是否为空
*
* @param map 要判断的Map
* @return true为空 false非空
*/
public static boolean isEmpty(Map<?, ?> map)
{
return isNull(map) || map.isEmpty();
}
/**
* * 判断一个Map是否为空
*
* @param map 要判断的Map
* @return true非空 false
*/
public static boolean isNotEmpty(Map<?, ?> map)
{
return !isEmpty(map);
}
/**
* * 判断一个字符串是否为空串
*
* @param str String
* @return true为空 false非空
*/
public static boolean isEmpty(String str)
{
return isNull(str) || NULLSTR.equals(str.trim());
}
/**
* * 判断一个字符串是否为非空串
*
* @param str String
* @return true非空串 false空串
*/
public static boolean isNotEmpty(String str)
{
return !isEmpty(str);
}
/**
* * 判断一个对象是否为空
*
* @param object Object
* @return true为空 false非空
*/
public static boolean isNull(Object object)
{
return object == null;
}
/**
* * 判断一个对象是否非空
*
* @param object Object
* @return true非空 false
*/
public static boolean isNotNull(Object object)
{
return !isNull(object);
}
/**
* * 判断一个对象是否是数组类型Java基本型别的数组
*
* @param object 对象
* @return true是数组 false不是数组
*/
public static boolean isArray(Object object)
{
return isNotNull(object) && object.getClass().isArray();
}
/**
* 去空格
*/
public static String trim(String str)
{
return (str == null ? "" : str.trim());
}
/**
* 替换指定字符串的指定区间内字符为"*"
*
* @param str 字符串
* @param startInclude 开始位置(包含)
* @param endExclude 结束位置(不包含)
* @return 替换后的字符串
*/
public static String hide(CharSequence str, int startInclude, int endExclude)
{
if (isEmpty(str))
{
return NULLSTR;
}
final int strLength = str.length();
if (startInclude > strLength)
{
return NULLSTR;
}
if (endExclude > strLength)
{
endExclude = strLength;
}
if (startInclude > endExclude)
{
// 如果起始位置大于结束位置,不替换
return NULLSTR;
}
final char[] chars = new char[strLength];
for (int i = 0; i < strLength; i++)
{
if (i >= startInclude && i < endExclude)
{
chars[i] = ASTERISK;
}
else
{
chars[i] = str.charAt(i);
}
}
return new String(chars);
}
/**
* 截取字符串
*
* @param str 字符串
* @param start 开始
* @return 结果
*/
public static String substring(final String str, int start)
{
if (str == null)
{
return NULLSTR;
}
if (start < 0)
{
start = str.length() + start;
}
if (start < 0)
{
start = 0;
}
if (start > str.length())
{
return NULLSTR;
}
return str.substring(start);
}
/**
* 截取字符串
*
* @param str 字符串
* @param start 开始
* @param end 结束
* @return 结果
*/
public static String substring(final String str, int start, int end)
{
if (str == null)
{
return NULLSTR;
}
if (end < 0)
{
end = str.length() + end;
}
if (start < 0)
{
start = str.length() + start;
}
if (end > str.length())
{
end = str.length();
}
if (start > end)
{
return NULLSTR;
}
if (start < 0)
{
start = 0;
}
if (end < 0)
{
end = 0;
}
return str.substring(start, end);
}
/**
* 在字符串中查找第一个出现的 `open` 和最后一个出现的 `close` 之间的子字符串
*
* @param str 要截取的字符串
* @param open 起始字符串
* @param close 结束字符串
* @return 截取结果
*/
public static String substringBetweenLast(final String str, final String open, final String close)
{
if (isEmpty(str) || isEmpty(open) || isEmpty(close))
{
return NULLSTR;
}
final int start = str.indexOf(open);
if (start != INDEX_NOT_FOUND)
{
final int end = str.lastIndexOf(close);
if (end != INDEX_NOT_FOUND)
{
return str.substring(start + open.length(), end);
}
}
return NULLSTR;
}
/**
* 判断是否为空,并且不是空白字符
*
* @param str 要判断的value
* @return 结果
*/
public static boolean hasText(String str)
{
return (str != null && !str.isEmpty() && containsText(str));
}
private static boolean containsText(CharSequence str)
{
int strLen = str.length();
for (int i = 0; i < strLen; i++)
{
if (!Character.isWhitespace(str.charAt(i)))
{
return true;
}
}
return false;
}
/**
* 格式化文本, {} 表示占位符<br>
* 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
* 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
* 例:<br>
* 通常使用format("this is {} for {}", "a", "b") -> this is a for b<br>
* 转义{} format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
* 转义\ format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
*
* @param template 文本模板,被替换的部分用 {} 表示
* @param params 参数值
* @return 格式化后的文本
*/
public static String format(String template, Object... params)
{
if (isEmpty(params) || isEmpty(template))
{
return template;
}
return StrFormatter.format(template, params);
}
/**
* 是否为http(s)://开头
*
* @param link 链接
* @return 结果
*/
public static boolean ishttp(String link)
{
return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS);
}
/**
* 字符串转set
*
* @param str 字符串
* @param sep 分隔符
* @return set集合
*/
public static final Set<String> str2Set(String str, String sep)
{
return new HashSet<String>(str2List(str, sep, true, false));
}
/**
* 字符串转list
*
* @param str 字符串
* @param sep 分隔符
* @return list集合
*/
public static final List<String> str2List(String str, String sep)
{
return str2List(str, sep, true, false);
}
/**
* 字符串转list
*
* @param str 字符串
* @param sep 分隔符
* @param filterBlank 过滤纯空白
* @param trim 去掉首尾空白
* @return list集合
*/
public static final List<String> str2List(String str, String sep, boolean filterBlank, boolean trim)
{
List<String> list = new ArrayList<String>();
if (StringUtils.isEmpty(str))
{
return list;
}
// 过滤空白字符串
if (filterBlank && StringUtils.isBlank(str))
{
return list;
}
String[] split = str.split(sep);
for (String string : split)
{
if (filterBlank && StringUtils.isBlank(string))
{
continue;
}
if (trim)
{
string = string.trim();
}
list.add(string);
}
return list;
}
/**
* 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value
*
* @param collection 给定的集合
* @param array 给定的数组
* @return boolean 结果
*/
public static boolean containsAny(Collection<String> collection, String... array)
{
if (isEmpty(collection) || isEmpty(array))
{
return false;
}
else
{
for (String str : array)
{
if (collection.contains(str))
{
return true;
}
}
return false;
}
}
/**
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
*
* @param cs 指定字符串
* @param searchCharSequences 需要检查的字符串数组
* @return 是否包含任意一个字符串
*/
public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences)
{
if (isEmpty(cs) || isEmpty(searchCharSequences))
{
return false;
}
for (CharSequence testStr : searchCharSequences)
{
if (containsIgnoreCase(cs, testStr))
{
return true;
}
}
return false;
}
/**
* 驼峰转下划线命名
*/
public static String toUnderScoreCase(String str)
{
if (str == null)
{
return null;
}
StringBuilder sb = new StringBuilder();
// 前置字符是否大写
boolean preCharIsUpperCase = true;
// 当前字符是否大写
boolean curreCharIsUpperCase = true;
// 下一字符是否大写
boolean nexteCharIsUpperCase = true;
for (int i = 0; i < str.length(); i++)
{
char c = str.charAt(i);
if (i > 0)
{
preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1));
}
else
{
preCharIsUpperCase = false;
}
curreCharIsUpperCase = Character.isUpperCase(c);
if (i < (str.length() - 1))
{
nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1));
}
if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase)
{
sb.append(SEPARATOR);
}
else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase)
{
sb.append(SEPARATOR);
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
/**
* 是否包含字符串
*
* @param str 验证字符串
* @param strs 字符串组
* @return 包含返回true
*/
public static boolean inStringIgnoreCase(String str, String... strs)
{
if (str != null && strs != null)
{
for (String s : strs)
{
if (str.equalsIgnoreCase(trim(s)))
{
return true;
}
}
}
return false;
}
/**
* 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如HELLO_WORLD->HelloWorld
*
* @param name 转换前的下划线大写方式命名的字符串
* @return 转换后的驼峰式命名的字符串
*/
public static String convertToCamelCase(String name)
{
StringBuilder result = new StringBuilder();
// 快速检查
if (name == null || name.isEmpty())
{
// 没必要转换
return "";
}
else if (!name.contains("_"))
{
// 不含下划线,仅将首字母大写
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
// 用下划线将原始字符串分割
String[] camels = name.split("_");
for (String camel : camels)
{
// 跳过原始字符串中开头、结尾的下换线或双重下划线
if (camel.isEmpty())
{
continue;
}
// 首字母大写
result.append(camel.substring(0, 1).toUpperCase());
result.append(camel.substring(1).toLowerCase());
}
return result.toString();
}
/**
* 驼峰式命名法
* 例如user_name->userName
*/
public static String toCamelCase(String s)
{
if (s == null)
{
return null;
}
if (s.indexOf(SEPARATOR) == -1)
{
return s;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++)
{
char c = s.charAt(i);
if (c == SEPARATOR)
{
upperCase = true;
}
else if (upperCase)
{
sb.append(Character.toUpperCase(c));
upperCase = false;
}
else
{
sb.append(c);
}
}
return sb.toString();
}
/**
* 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
*
* @param str 指定字符串
* @param strs 需要检查的字符串数组
* @return 是否匹配
*/
public static boolean matches(String str, List<String> strs)
{
if (isEmpty(str) || isEmpty(strs))
{
return false;
}
for (String pattern : strs)
{
if (isMatch(pattern, str))
{
return true;
}
}
return false;
}
/**
* 判断url是否与规则配置:
* ? 表示单个字符;
* * 表示一层路径内的任意字符串,不可跨层级;
* ** 表示任意层路径;
*
* @param pattern 匹配规则
* @param url 需要匹配的url
* @return
*/
public static boolean isMatch(String pattern, String url)
{
AntPathMatcher matcher = new AntPathMatcher();
return matcher.match(pattern, url);
}
@SuppressWarnings("unchecked")
public static <T> T cast(Object obj)
{
return (T) obj;
}
/**
* 数字左边补齐0使之达到指定长度。注意如果数字转换为字符串后长度大于size则只保留 最后size个字符。
*
* @param num 数字对象
* @param size 字符串指定长度
* @return 返回数字的字符串格式,该字符串为指定长度。
*/
public static final String padl(final Number num, final int size)
{
return padl(num.toString(), size, '0');
}
/**
* 字符串左补齐。如果原始字符串s长度大于size则只保留最后size个字符。
*
* @param s 原始字符串
* @param size 字符串指定长度
* @param c 用于补齐的字符
* @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
*/
public static final String padl(final String s, final int size, final char c)
{
final StringBuilder sb = new StringBuilder(size);
if (s != null)
{
final int len = s.length();
if (s.length() <= size)
{
for (int i = size - len; i > 0; i--)
{
sb.append(c);
}
sb.append(s);
}
else
{
return s.substring(len - size, len);
}
}
else
{
for (int i = size; i > 0; i--)
{
sb.append(c);
}
}
return sb.toString();
}
}

View File

@@ -0,0 +1,71 @@
javafx:
title: ERP系统
appicons: /icon/icon.png
stage:
width: 1280
height: 800
# style: DECORATED # javafx.stage.StageStyle [DECORATED, UNDECORATED, TRANSPARENT, UTILITY, UNIFIED]
# resizable: false
spring:
datasource:
url: jdbc:sqlite:./data/erp-cache.db?journal_mode=WAL&synchronous=NORMAL&cache_size=10000&temp_store=memory&busy_timeout=30000
driver-class-name: org.sqlite.JDBC
username:
password:
hikari:
maximum-pool-size: 1
connection-timeout: 60000
idle-timeout: 300000
max-lifetime: 900000
auto-commit: false
jpa:
database-platform: org.hibernate.community.dialect.SQLiteDialect
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
format_sql: true
jdbc:
batch_size: 20
order_inserts: true
order_updates: true
connection:
provider_disables_autocommit: true
open-in-view: false
web:
resources:
static-locations: classpath:/static/
server:
port: 8081
address: 127.0.0.1
# 外部API服务配置
api:
server:
# 主服务器API配置
# base-url: "http://8.138.23.49:8080"
base-url: "http://192.168.1.89:8080"
paths:
monitor: "/monitor/client/api"
login: "/monitor/account/login"
heartbeat: "/monitor/client/api/heartbeat"
error: "/monitor/client/api/error"
data: "/monitor/client/api/data"
alibaba1688: "/monitor/client/api/alibaba1688"
version: "/system/version/check"
getGenmaijlToken: "/getToken"
updateGenmaijlToken: "/saveToken"
# 项目信息配置
project:
version: @project.version@
build:
time: @maven.build.timestamp@
logging:
level:
com.tashow.erp: INFO
org.hibernate.SQL: WARN
org.hibernate.type.descriptor.sql.BasicBinder: WARN

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 设置根日志级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
<!-- 设置特定包的日志级别 -->
<logger name="com.tashow.erp" level="INFO" additivity="false">
<appender-ref ref="CONSOLE" />
</logger>
</configuration>

View File

@@ -0,0 +1,3 @@
.root {
/*-fx-background-color: #f0f0f0;*/
}

View File

@@ -0,0 +1,709 @@
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
}
/* 主容器 */
.erp-container {
display: flex;
height: 100vh;
overflow: hidden;
}
/* 左侧导航栏 */
.sidebar {
width: 200px;
background-color: #ffffff;
border-right: 1px solid #e6e6e6;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.user-avatar {
padding: 20px;
text-align: center;
border-bottom: 1px solid #f0f0f0;
}
.menu-group-title {
padding: 15px 20px 8px;
font-size: 12px;
color: #999;
font-weight: 500;
}
.sidebar-menu {
border: none;
flex: 1;
}
.menu-item-custom {
height: 45px;
line-height: 45px;
margin: 0 10px;
border-radius: 6px;
}
.menu-item-custom:hover {
background-color: #f5f7fa !important;
}
.menu-item-custom.is-active {
background-color: #ecf5ff !important;
color: #409EFF !important;
}
.platform-item {
display: flex;
align-items: center;
}
.platform-logo {
width: 20px;
height: 20px;
margin-right: 8px;
border-radius: 4px;
}
.vip-section {
padding: 20px;
border-top: 1px solid #f0f0f0;
}
.vip-button {
width: 100%;
height: 60px;
background: linear-gradient(135deg, #FFD700, #FFA500);
border: none;
border-radius: 8px;
color: white;
font-weight: bold;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.vip-button:hover {
background: linear-gradient(135deg, #FFA500, #FF8C00);
}
.vip-subtitle {
font-size: 10px;
margin-top: 2px;
opacity: 0.9;
}
/* 主内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 顶部导航条 */
.top-navbar {
height: 60px;
background-color: #ffffff;
border-bottom: 1px solid #e6e6e6;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
}
.navbar-left .el-button-group {
margin-right: 20px;
}
.navbar-center {
flex: 1;
text-align: center;
}
.navbar-center .el-breadcrumb {
display: inline-block;
}
.navbar-right {
display: flex;
align-items: center;
gap: 10px;
}
/* 内容主体 */
.content-body {
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: #f5f5f5;
height: auto;
min-height: auto;
}
/* 功能卡片 */
.feature-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
/* 欢迎区域 */
.welcome-section {
padding: 20px 0;
}
.welcome-card {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
text-align: center;
max-width: 600px;
margin: 0 auto;
}
.welcome-card h2 {
color: #333;
font-size: 28px;
margin-bottom: 16px;
font-weight: 600;
}
.welcome-card p {
color: #666;
font-size: 16px;
margin-bottom: 24px;
line-height: 1.6;
}
.welcome-card ul {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-top: 20px;
}
.welcome-card li {
background: #f8f9fa;
padding: 12px 16px;
border-radius: 8px;
color: #555;
font-size: 14px;
border-left: 3px solid #409EFF;
}
.card-item {
background: white;
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.card-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.card-icon {
width: 50px;
height: 50px;
border-radius: 50%;
background: #f0f9ff;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
}
.card-icon i {
font-size: 24px;
color: #409EFF;
}
.card-content h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 5px;
color: #333;
}
.card-content p {
font-size: 12px;
color: #666;
line-height: 1.4;
}
/* 数据统计区域 */
.stats-section {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 0;
margin-bottom: 15px;
padding: 0;
background: white;
border-radius: 0;
box-shadow: none;
border-bottom: 1px solid #e6e6e6;
width: 100%;
}
.stats-item {
text-align: left;
padding: 8px 12px;
border-right: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-height: 60px;
}
.stats-item:last-child {
border-right: none;
}
.stats-label {
font-size: 11px;
color: #333;
margin-bottom: 4px;
line-height: 1.2;
font-weight: 500;
}
.stats-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
}
.stats-row:last-child {
margin-bottom: 0;
}
.stats-type {
font-size: 10px;
color: #666;
min-width: 30px;
}
.stats-value {
font-size: 12px;
font-weight: bold;
color: #333;
line-height: 1.1;
}
.stats-subtitle {
font-size: 10px;
color: #999;
margin-top: 2px;
line-height: 1.1;
}
.stats-unit {
font-size: 12px;
color: #999;
margin-top: 2px;
}
/* 筛选区域 */
.filter-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 12px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 表格区域 */
.table-section {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
width: 100%;
}
.table-section .el-table {
width: 100% !important;
}
.table-section .el-table__body-wrapper {
width: 100% !important;
height:calc(100%)
}
/* 内容主体区域优化 */
.content-body {
flex: 1;
padding: 15px;
overflow-y: auto;
background-color: #f5f5f5;
width: 100%;
height: auto;
min-height: auto;
}
/* 确保所有容器都占满宽度 */
.filter-section,
.table-section,
.stats-section {
width: 100% !important;
box-sizing: border-box;
}
/* Element UI表格强制全宽 */
.el-table,
.el-table__header-wrapper,
.el-table__body-wrapper,
.el-table__footer-wrapper {
width: 100% !important;
}
.el-table .el-table__header,
.el-table .el-table__body {
width: 100% !important;
table-layout: fixed;
}
.product-info {
display: flex;
align-items: center;
}
.product-image-placeholder {
width: 50px;
height: 50px;
border-radius: 4px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 18px;
}
.product-details {
flex: 1;
}
.product-title {
font-size: 14px;
color: #333;
margin-bottom: 5px;
line-height: 1.4;
}
.product-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.budget-info, .roi-info, .period-info, .create-time {
font-size: 12px;
line-height: 1.4;
}
.budget-info .current-budget {
font-weight: bold;
color: #333;
}
.budget-info .target-budget {
color: #666;
}
.roi-info .roi-current {
color: #333;
}
.roi-info .roi-actual {
color: #666;
}
.ad-spend {
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.spend-link a {
color: #409EFF;
text-decoration: none;
font-size: 12px;
}
.spend-link a:hover {
text-decoration: underline;
}
.status-info {
display: flex;
align-items: center;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-dot.running {
background-color: #67C23A;
}
.status-dot.paused {
background-color: #E6A23C;
}
.status-dot.stopped {
background-color: #909399;
}
/* 分页 */
.pagination-section {
padding: 20px;
text-align: right;
border-top: 1px solid #f0f0f0;
}
/* 悬浮账号管理窗口 */
.account-float-window {
position: fixed;
top: 80px;
right: 20px;
width: 280px;
background: white;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
z-index: 1000;
overflow: hidden;
}
.float-window-header {
height: 40px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
border-bottom: 1px solid #e6e6e6;
}
.window-title {
font-size: 14px;
font-weight: 500;
color: #333;
}
.window-controls {
display: flex;
gap: 5px;
}
.float-window-content {
padding: 15px;
}
.account-list {
margin-bottom: 15px;
}
.account-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.account-item:last-child {
border-bottom: none;
}
.account-status {
margin-right: 10px;
}
.account-status .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.account-status .status-dot.online {
background-color: #67C23A;
}
.account-status .status-dot.offline {
background-color: #F56C6C;
}
.account-name {
margin-left: 10px;
font-size: 14px;
color: #333;
}
.account-actions {
display: flex;
gap: 10px;
}
.account-actions .el-button {
flex: 1;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.feature-cards {
grid-template-columns: repeat(2, 1fr);
}
.stats-section {
flex-wrap: wrap;
gap: 15px;
}
}
@media (max-width: 768px) {
.sidebar {
width: 60px;
}
.sidebar .menu-item-custom span {
display: none;
}
.feature-cards {
grid-template-columns: 1fr;
}
.filter-section {
flex-direction: column;
gap: 15px;
}
.account-float-window {
width: 250px;
right: 10px;
}
}
/* 自定义滚动条 */
.sidebar::-webkit-scrollbar,
.content-body::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track,
.content-body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb,
.content-body::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover,
.content-body::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 动画效果 */
.card-item,
.account-float-window,
.menu-item-custom {
transition: all 0.3s ease;
}
/* Element UI 组件样式覆盖 */
.el-table {
font-size: 12px;
}
.el-table th {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
.el-table td {
padding: 12px 0;
}
.el-tag--mini {
height: 20px;
line-height: 18px;
font-size: 10px;
}
.el-breadcrumb__inner {
color: #666;
font-weight: normal;
}
.el-breadcrumb__inner:hover {
color: #409EFF;
}
/* 状态标签颜色 */
.el-tag--success {
background-color: #f0f9ff;
border-color: #b3d8ff;
color: #409EFF;
}
.el-tag--warning {
background-color: #fdf6ec;
border-color: #f5dab1;
color: #e6a23c;
}
.el-tag--danger {
background-color: #fef0f0;
border-color: #fbc4c4;
color: #f56c6c;
}
.el-tag--info {
background-color: #f4f4f5;
border-color: #d3d4d6;
color: #909399;
}
/* 价格/费用标签与卖家样式 */
.price-tag {
color: #F56C6C;
font-weight: 600;
}
.fee-tag {
color: #E6A23C;
font-weight: 600;
}
.primary-seller {
color: #303133;
font-weight: 600;
}

View File

@@ -0,0 +1,227 @@
/* 更新对话框样式 */
.update-dialog .el-dialog {
border-radius: 16px;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.15);
}
.update-dialog .el-dialog__header {
display: block;
padding: 12px 20px 0 20px;
text-align: right;
}
.update-dialog .el-dialog__headerbtn {
top: 12px;
right: 20px;
width: 32px;
height: 32px;
font-size: 18px;
border-radius: 50%;
background-color: transparent;
}
.update-dialog .el-dialog__headerbtn:hover {
background-color: #f5f5f5;
}
.update-dialog .el-dialog__close {
color: #909399;
font-weight: 400;
}
.update-dialog .el-dialog__close:hover {
color: #409EFF;
}
.update-dialog .el-dialog__body {
padding: 0;
}
.update-content {
padding: 32px 24px 24px;
}
.update-header {
display: flex;
align-items: flex-start;
margin-bottom: 24px;
}
.update-header.text-center {
text-align: center;
flex-direction: column;
align-items: center;
}
.app-icon {
width: 64px;
height: 64px;
border-radius: 12px;
margin-right: 16px;
flex-shrink: 0;
}
.update-header.text-center .app-icon {
margin-right: 0;
margin-bottom: 16px;
}
.update-header-content {
flex: 1;
min-width: 0;
}
.update-header h3 {
font-size: 20px;
font-weight: 600;
margin: 16px 0 8px 0;
color: #1f2937;
}
.update-header p {
font-size: 14px;
color: #6b7280;
margin: 0;
line-height: 1.5;
}
.update-details {
background-color: #f9fafb;
border-radius: 8px;
padding: 16px;
margin: 24px 0;
max-height: 200px;
overflow-y: auto;
}
.update-details h4 {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 0 0 8px 0;
}
.update-details p {
font-size: 13px;
color: #6b7280;
line-height: 1.6;
margin: 0 0 16px 0;
}
.update-details p:last-child {
margin-bottom: 0;
}
.auto-update-section {
margin: 16px 0;
}
.auto-update-section .el-checkbox {
color: #374151;
font-size: 14px;
}
.update-actions {
margin-top: 24px;
}
.update-buttons {
display: flex;
justify-content: space-between;
gap: 12px;
}
.update-buttons.text-center {
justify-content: center;
}
.update-buttons .el-button {
flex: 1;
height: 40px;
font-size: 14px;
border-radius: 8px;
}
.update-buttons.text-center .el-button {
flex: none;
min-width: 140px;
}
/* 下载进度区域 */
.download-progress {
margin: 24px 0;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
color: #6b7280;
}
.download-note {
background-color: #fef3cd;
border: 1px solid #fcd34d;
border-radius: 8px;
padding: 12px;
margin: 16px 0;
}
.download-note p {
font-size: 12px;
color: #92400e;
margin: 0;
line-height: 1.5;
}
/* 进度条自定义样式 */
.el-progress-bar__outer {
border-radius: 4px;
background-color: #e5e7eb;
}
.el-progress-bar__inner {
border-radius: 4px;
transition: width 0.3s ease;
}
/* 按钮样式调整 */
.update-buttons .el-button--primary {
background-color: #2563eb;
border-color: #2563eb;
font-weight: 500;
}
.update-buttons .el-button--primary:hover {
background-color: #1d4ed8;
border-color: #1d4ed8;
}
.update-buttons .el-button:not(.el-button--primary) {
background-color: #f3f4f6;
border-color: #d1d5db;
color: #374151;
font-weight: 500;
}
.update-buttons .el-button:not(.el-button--primary):hover {
background-color: #e5e7eb;
border-color: #9ca3af;
}
/* 响应式调整 */
@media (max-width: 480px) {
.update-content {
padding: 24px 16px;
}
.update-buttons {
flex-direction: column;
}
.update-buttons .el-button {
flex: none;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.web.WebView?>
<BorderPane fx:id="rootPane" prefHeight="600.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.tashow.erp.fx.controller.MainCtrl">
<center>
<WebView fx:id="webView" prefHeight="600.0" prefWidth="1000.0" BorderPane.alignment="CENTER" />
</center>
</BorderPane>

View File

@@ -0,0 +1,718 @@
<template>
<div>
<div class="main-container" :class="{ 'has-data': localProductData.length > 0, 'empty-data': localProductData.length === 0, 'loading-state': loading }">
<!-- 导入区域 -->
<div class="import-section">
<div class="import-controls">
<el-upload
class="excel-uploader"
action="#"
:http-request="handleExcelUpload"
:show-file-list="false"
accept=".xlsx,.xls"
:before-upload="beforeUpload"
:disabled="loading">
<el-button type="primary" :loading="loading" size="small">
<i class="el-icon-upload"></i> 导入ASIN列表
</el-button>
</el-upload>
<!-- 单个ASIN输入 -->
<div class="single-input">
<el-input
v-model="singleAsin"
placeholder="输入单个ASIN"
size="small"
style="width: 180px;"
:disabled="loading">
</el-input>
<el-button type="info" size="small" @click="searchSingleAsin" :disabled="!singleAsin || loading" style="margin-left: 8px;">
查询
</el-button>
</div>
<!-- 导出和停止按钮 -->
<div class="action-buttons">
<el-button type="danger" size="small" @click="stopFetch" :disabled="!loading">停止获取</el-button>
<el-button type="success" size="small" @click="exportToExcel" :disabled="!localProductData.length">导出Excel</el-button>
<el-button type="warning" size="small" @click="openGenmaiSpirit" :loading="genmaiLoading">跟卖精灵</el-button>
</div>
</div>
<div class="el-upload__tip">支持Excel批量导入(第一列为ASIN)或单个ASIN查询</div>
<!-- 进度条 -->
<div class="progress-section" v-if="loading">
<div class="progress-box">
<div class="progress-container">
<el-progress
:percentage="progressPercentage"
:status="progressPercentage >= 100 ? 'success' : null"
:stroke-width="6"
:show-text="false"
class="thin-progress">
</el-progress>
<div class="progress-text">{{progressPercentage}}%</div>
</div>
</div>
</div>
</div>
<!-- 数据表格容器 -->
<div class="table-container">
<!-- 无数据时的静态提示 -->
<div class="empty-loading-section" v-if="!loading && localProductData.length === 0">
<div class="empty-loading-container">
<i class="el-icon-document static-icon"></i>
<div class="empty-loading-text">暂无数据请导入ASIN列表</div>
</div>
</div>
<!-- 表格区域 -->
<div class="table-section custom-scrollbar" v-if="localProductData.length > 0 || loading">
<el-table
:data="paginatedData"
style="width: 100%"
border
height="100%"
:cell-style="cellStyle"
:header-cell-style="headerCellStyle"
v-loading="tableLoading"
lazy>
<el-table-column prop="asin" label="ASIN" width="130" show-overflow-tooltip>
<template slot-scope="scope">
<span :class="{ 'failed-data': (!scope.row.seller || scope.row.seller.trim() === '') || (!scope.row.price || scope.row.price.trim() === '') }">{{ scope.row.asin }}</span>
</template>
</el-table-column>
<el-table-column label="卖家/配送方" width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span :class="{ 'primary-seller': scope.row.seller, 'no-stock': !scope.row.seller }">{{ scope.row.seller || '无货' }}</span>
<span v-if="scope.row.shipper && scope.row.shipper !== scope.row.seller" class="shipper">配送方: {{ scope.row.shipper }}</span>
</template>
</el-table-column>
<el-table-column prop="price" label="当前售价" width="120">
<template slot-scope="scope">
<span :class="{ 'price-tag': scope.row.price, 'no-stock': !scope.row.price }">{{ scope.row.price || '无货' }}</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- 固定分页组件 -->
<div class="pagination-fixed" v-if="localProductData.length > 0">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[15, 30, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="localProductData.length">
</el-pagination>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.shipper { margin-left:8px; color:#606266; }
.import-section { margin-bottom: 10px; }
.import-controls { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; }
.single-input { display: flex; align-items: center; }
.action-buttons { display: flex; gap: 10px; margin-top: 8px; }
.progress-section {
margin-bottom: 10px;
}
.progress-box {
padding: 8px 0;
margin-bottom: 0;
}
.progress-container {
display: flex;
align-items: center;
position: relative;
padding-right: 40px;
}
.progress-container .el-progress {
flex: 1;
}
.thin-progress .el-progress-bar__outer {
background-color: #ebeef5;
border-radius: 10px;
}
.thin-progress .el-progress-bar__inner {
border-radius: 10px;
}
.progress-text {
position: absolute;
right: 0;
font-size: 13px;
color: #409EFF;
font-weight: 500;
}
.main-container {
background-color: #fff;
border-radius: 4px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
/* 表格容器布局 */
.table-container {
display: flex;
flex-direction: column;
height: calc(100vh - 220px); /* 根据需要调整高度 */
min-height: 400px;
}
.table-section {
flex: 1;
overflow: hidden;
/*padding-bottom: 80px;*/
/*margin-bottom: -80px!important;*/
display: flex;
flex-direction: column;
}
.table-section .el-table {
flex: 1;
height: 100%;
overflow-y: auto;
}
/* 固定分页样式 */
.pagination-fixed {
flex-shrink: 0;
padding: 10px 15px;
background-color: #f9f9f9;
border-radius: 4px;
display: flex;
justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: sticky;
bottom: 0;
z-index: 10;
margin-top: 0;
border-top: 1px solid #ebeef5;
height: 60px;
min-height: 60px;
}
/* 移除原来的分页样式 */
.pagination-section {
display: none;
}
/* 自定义滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 5px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 5px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 价格标签样式 */
.price-tag {
color: #e6a23c;
font-weight: bold;
}
/* 失败数据标记样式 */
.failed-data {
color: #f56c6c !important;
background-color: rgba(245, 108, 108, 0.1);
padding: 2px 4px;
border-radius: 3px;
font-weight: bold;
}
/* 无货数据样式 */
.no-stock {
color: #909399 !important;
font-style: italic;
background-color: rgba(144, 147, 153, 0.1);
padding: 2px 4px;
border-radius: 3px;
}
/* 表格滚动性能优化 */
.el-table {
/* 启用硬件加速 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
/* 优化滚动性能 */
-webkit-overflow-scrolling: touch;
/* 减少重绘 */
will-change: auto;
/* 强制使用复合层 */
backface-visibility: hidden;
}
.el-table__body-wrapper {
/* 禁用平滑滚动避免卡顿 */
scroll-behavior: auto;
/* 启用硬件加速 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
/* 减少重绘和重排 */
.el-table .cell {
text-overflow: ellipsis;
white-space: nowrap;
}
/* 统一表格行高 */
.el-table td {
padding: 4px 8px !important;
height: 25px !important;
line-height: 1.2 !important;
}
.el-table th {
padding: 6px 8px !important;
height: 25px !important;
line-height: 1.2 !important;
}
.el-table .el-table__row {
height: 25px !important;
}
/* Loading图标旋转动画 */
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 默认情况下所有loading图标都有动画 */
.el-icon-loading {
animation: rotate 2s linear infinite !important;
}
/* 表格loading遮罩动画 */
.el-loading-spinner .el-icon-loading {
animation: rotate 1.5s linear infinite !important;
}
/* 有数据时停止所有loading动画 */
.has-data .el-icon-loading,
.has-data .el-loading-spinner .el-icon-loading,
.has-data .el-button .el-icon-loading {
animation: none !important;
}
/* 导出按钮简单加载效果 */
.el-button.is-loading {
opacity: 0.8;
}
.el-button.is-loading .el-icon-loading {
animation: rotate 1s linear infinite !important;
}
/* 无数据时的加载状态样式 */
.empty-loading-section {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
background: white;
border-radius: 6px;
border: 1px solid #ebeef5;
}
.empty-loading-container {
text-align: center;
}
.empty-loading-container .static-icon {
font-size: 48px;
color: #c0c4cc;
margin-bottom: 16px;
display: block;
}
.empty-loading-text {
font-size: 14px;
color: #909399;
}
</style>
<script>
export default {
name: 'platform-amazon',
props: {
productData: { type: Array, default: function(){ return []; } }
},
data() {
return {
loading: false,
tableLoading: false,
progressPercentage: 0,
localProductData: [],
currentAsin: '',
importedAsinList: [],
singleAsin: '', // 单个ASIN输入
genmaiLoading: false,
// 分页相关数据
currentPage: 1,
pageSize: 15
}
},
mounted() {
// 页面加载时从localStorage恢复数据
this.loadDataFromStorage();
},
computed: {
// 分页后的数据
paginatedData() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.localProductData.slice(start, end);
}
},
methods: {
cellStyle({ row, column, rowIndex, columnIndex }) {
return {
padding: '6px 8px',
'will-change': 'auto'
};
},
headerCellStyle({ row, column, rowIndex, columnIndex }) {
return {
padding: '8px',
'background-color': '#f5f7fa',
'font-weight': '500'
};
},
// 文件上传前验证
beforeUpload(file) {
try {
return fileService.validateFile(file);
} catch (error) {
this.$message.error(error.message);
return false;
}
},
async handleExcelUpload(options) {
try {
await this.clearBrowserCache();
// 强制停止任何正在进行的任务
this.loading = false;
this.tableLoading = false;
// 完全清空所有数据和状态
this.localProductData = [];
this.importedAsinList = [];
this.currentPage = 1;
this.progressPercentage = 0;
this.currentAsin = '';
try {
const wasInterrupted = localStorage.getItem('amazon-data-interrupted');
if (wasInterrupted) {
localStorage.removeItem('amazon-data-interrupted');
// 只在检测到中断时才清理数据
this.clearStorageData();
}
} catch (storageError) {
console.warn('localStorage access error:', storageError);
}
this.$emit('update:productData', []);
this.$emit('product-data-updated', []);
await this.$nextTick();
this.$forceUpdate();
this.loading = true;
this.tableLoading = true;
this.$forceUpdate();
const file = options.file;
try {
const asinList = await fileService.parseExcelForASIN(file);
this.importedAsinList = asinList;
this.$message.success(`成功导入 ${asinList.length} 个ASIN`);
await this.batchGetProductInfo(asinList);
} catch (error) {
console.error('解析Excel文件失败:', error);
this.$message.error(error.message || '解析Excel文件失败');
this.loading = false;
this.tableLoading = false;
}
} catch (error) {
console.error('上传文件失败:', error);
this.$message.error(this.getErrorMessage('上传文件失败: ' + error.message));
this.loading = false;
this.tableLoading = false;
}
},
async batchGetProductInfo(asinList) {
try {
this.currentAsin = '正在处理...';
this.progressPercentage = 0;
this.localProductData = []; // 完全清空现有数据
// 强制更新UI确保表格立即显示为空
this.$emit('update:productData', []);
this.$emit('product-data-updated', []);
await this.$nextTick();
this.$forceUpdate();
const batchId = `BATCH_${Date.now()}`;
const batchSize = 2; // 每批处理2个ASIN
const totalBatches = Math.ceil(asinList.length / batchSize);
let processedCount = 0;
let failedCount = 0;
// 改回2个一批的处理方式避免亚马逊风控检测
for (let i = 0; i < totalBatches && this.loading; i++) {
const start = i * batchSize;
const end = Math.min(start + batchSize, asinList.length);
const batchAsins = asinList.slice(start, end);
this.currentAsin = `正在处理第${i + 1}/${totalBatches}批 (${batchAsins.join(', ')})`;
try {
// 2个ASIN一批处理加入随机延迟避免风控
const result = await amazonAPI.getProductsBatch(batchAsins, batchId);
if (result && result.products && result.products.length > 0) {
this.localProductData.push(...result.products);
// 实时更新表格数据
this.$emit('update:productData', [...this.localProductData]);
this.$emit('product-data-updated', [...this.localProductData]);
if (this.tableLoading) {
this.tableLoading = false;
}
}
// 统计失败的ASIN数量
const expectedCount = batchAsins.length;
const actualCount = result?.products?.length || 0;
failedCount += Math.max(0, expectedCount - actualCount);
} catch (error) {
failedCount += batchAsins.length;
console.error(`批次${i + 1}失败:`, error.message);
}
processedCount += batchAsins.length;
this.progressPercentage = Math.round((processedCount / asinList.length) * 100);
// 增加随机延迟,避免请求过于频繁触发风控
if (i < totalBatches - 1 && this.loading) {
const delay = 1000 + Math.random() * 1500; // 1000-2500ms随机延迟
await new Promise(resolve => setTimeout(resolve, delay));
}
}
// 保存数据到本地存储
if (this.localProductData.length > 0) {
this.saveDataToStorage();
}
// 处理完成
this.progressPercentage = 100;
this.currentAsin = '处理完成';
this.tableLoading = false;
if (failedCount > 0) {
this.$message.warning(`采集完成!共 ${asinList.length} 个ASIN成功 ${asinList.length - failedCount} 个,失败 ${failedCount}`);
} else {
this.$message.success(`采集完成!成功获取 ${asinList.length} 个产品信息`);
}
} catch (error) {
this.$message.error(error.message || '批量获取产品信息失败');
this.currentAsin = '处理失败';
} finally {
this.loading = false;
this.tableLoading = false;
this.taskId = null;
}
},
// 清理浏览器缓存方法
async clearBrowserCache() {
try {
this.clearStorageData();
sessionStorage.clear();
if (window.indexedDB) {
const databases = await window.indexedDB.databases();
for (const db of databases) {
if (db.name) {
const deleteReq = window.indexedDB.deleteDatabase(db.name);
await new Promise(resolve => {
deleteReq.onsuccess = resolve;
deleteReq.onerror = resolve;
});
}
}
}
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
}
console.log('浏览器缓存已清理');
} catch (error) {
console.error('清理浏览器缓存失败:', error);
}
},
// 数据持久化方法
saveDataToStorage() {
// 数据已经在后端自动保存,这里不需要操作
// 保留方法避免其他地方调用报错
},
async loadDataFromStorage() {
try {
const response = await amazonAPI.getLatestProducts();
const productsData = response.products || [];
if (productsData.length > 0) {
this.localProductData = productsData;
this.importedAsinList = []; // 重置导入列表
this.$emit('update:productData', this.localProductData);
this.$emit('product-data-updated', this.localProductData);
}
} catch (error) {
console.error('加载最新数据失败:', error);
// 加载失败时不显示错误,保持页面正常
}
},
clearStorageData() {
// 数据已经在后端管理,这里不需要操作
// 保留方法避免其他地方调用报错
},
async exportToExcel() {
try {
if (!this.localProductData || this.localProductData.length === 0) {
this.$message.warning('没有数据可供导出');
return;
}
this.loading = true;
this.$message.info('正在生成Excel文件请稍候...');
// 准备导出数据
const exportData = this.localProductData.map(product => {
let sellerShipper = product.seller || '无货';
if (product.shipper && product.shipper !== product.seller) {
sellerShipper += (sellerShipper && sellerShipper !== '无货' ? ' / ' : '') + product.shipper;
}
return {
asin: product.asin || '',
seller_shipper: sellerShipper || '无货',
price: product.price || '无货'
};
});
const headers = [
{ header: 'ASIN', key: 'asin', width: 15 },
{ header: '卖家/配送方', key: 'seller_shipper', width: 35 },
{ header: '当前售价', key: 'price', width: 15 }
];
// 创建工作簿
const workbook = await fileService.createExcelWorkbook('Amazon产品数据', headers, exportData);
// 生成文件名
const fileName = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xlsx`;
// 保存文件
const savedPath = await fileService.saveExcelToDesktop(workbook, fileName);
if (savedPath) {
this.$message.success(`Excel文件已保存到: ${savedPath}`);
} else {
this.$message.success('Excel文件导出成功');
}
} catch (error) {
console.error('导出Excel失败:', error);
this.$message.error(error.message || '导出Excel失败');
} finally {
this.loading = false;
}
},
stopFetch() {
console.log('停止获取产品数据');
this.loading = false;
this.currentAsin = '已停止';
// 标记数据已被中断,下次导入时需要完全清空
localStorage.setItem('amazon-data-interrupted', 'true');
this.$message.info('已停止获取产品数据');
},
// 分页相关方法
handleSizeChange(newSize) {
this.pageSize = newSize;
// 只有在改变页大小时才重置到第一页
this.currentPage = 1;
},
handleCurrentChange(newPage) {
this.currentPage = newPage;
},
// 单个ASIN查询
async searchSingleAsin() {
if (!this.singleAsin.trim()) return;
// 只清理当前组件数据不清理localStorage缓存
this.localProductData = [];
this.loading = true;
const asin = this.singleAsin.trim();
try {
const response = await amazonAPI.getProductsBatch([asin], `SINGLE_${Date.now()}`);
if (response.products?.length > 0) {
this.localProductData = response.products;
this.saveDataToStorage();
this.$message.success('查询成功');
this.singleAsin = '';
} else {
this.$message.warning('未找到商品信息');
}
} catch (error) {
this.$message.error(error.message || '查询失败');
}
this.loading = false;
},
async openGenmaiSpirit() {
this.genmaiLoading = true;
try {
await amazonAPI.openGenmaiSpirit();
} catch (error) {
this.$message.error(error.message || '打开跟卖精灵失败');
}
this.genmaiLoading = false;
},
},
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,699 @@
<template>
<div>
<div class="main-container">
<!-- 功能卡片区域 -->
<div class="feature-cards">
<div class="card-item"
v-for="card in featureCards"
:key="card.id"
:data-card-id="card.id"
@click="selectFeature(card.id)"
@mousedown="handleCardMouseDown(card.id)"
@mouseup="handleCardMouseUp(card.id)"
style="cursor: pointer; user-select: none;">
<div class="card-icon">
<i :class="card.icon"></i>
</div>
<div class="card-content">
<h3>{{ card.title }}</h3>
<p>{{ card.description }}</p>
</div>
</div>
</div>
<!-- 功能组件显示区域 -->
<div class="feature-content" v-if="selectedFeature">
<div v-if="selectedFeature === 1" id="ad-hosting-component"></div>
<div v-if="selectedFeature === 2" id="auto-review-component"></div>
<div v-if="selectedFeature === 3" id="flash-sale-auto-component"></div>
<div v-if="selectedFeature === 4" id="long-product-manage-component"></div>
</div>
<!-- 主组件内容区域 - 只在没有选择子功能时显示 -->
<div v-if="!selectedFeature">
<!-- 数据统计区域 -->
<div class="stats-section">
<div class="stats-item" v-for="stat in statsData" :key="stat.label">
<div class="stats-label">{{ stat.label }}</div>
<div class="stats-row" v-for="row in stat.rows" :key="row.type">
<span class="stats-type">{{ row.type }}</span>
<span class="stats-value">{{ row.value }}</span>
</div>
</div>
</div>
<!-- 筛选和状态切换 -->
<div class="filter-section">
<div class="filter-left">
<el-button-group>
<el-button
v-for="status in statusFilters"
:key="status.key"
:type="activeStatus === status.key ? 'primary' : ''"
size="small"
@click="setActiveStatus(status.key)">
{{ status.label }}
</el-button>
</el-button-group>
</div>
<div class="filter-right">
<el-input
placeholder="搜索"
v-model="searchText"
size="small"
style="width: 200px;">
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
</div>
</div>
</div>
</div>
</div>
</template>
<script scoped>
module.exports = {
name: 'platform-shopee',
props: {
// 可以接收从父组件传递的数据
onSearch: Function,
onImageError: Function
},
data() {
return {
// 选中的功能
selectedFeature: 1, // 默认选中广告投放托管
// 当前激活的状态筛选
activeStatus: '进行中',
// 搜索文本
searchText: '',
// 分页相关
currentPage: 1,
pageSize: 10,
total: 100,
// 功能卡片数据
featureCards: [
{
id: 1,
title: '广告投放托管',
description: '广告广告自动化托管',
icon: 'el-icon-s-promotion'
},
{
id: 2,
title: '自动回评',
description: '自动回复客户评价',
icon: 'el-icon-chat-dot-round'
},
{
id: 3,
title: '限时特卖自动化',
description: '限时特卖自动化设置上下架',
icon: 'el-icon-time'
},
{
id: 4,
title: '较长铺货商品',
description: '较长铺货商品管理',
icon: 'el-icon-goods'
}
],
// 统计数据
statsData: [
{
label: '广告营销周期',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
},
{
label: '新品/老品养成进度',
rows: [
{ type: '养定期', value: '7天' },
{ type: '老品', value: '7天' }
]
},
{
label: '预算/营销/营销时间',
rows: [
{ type: '每日', value: '119时30分' }
]
},
{
label: 'ROI变更周期',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
},
{
label: 'ROI自动优化',
rows: [
{ type: '新品', value: '3' },
{ type: '老品', value: '5' }
]
},
{
label: '新品/营销防控预算',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
}
],
// 状态筛选
statusFilters: [
{ key: '全部商品', label: '全部商品' },
{ key: '进行中', label: '进行中' },
{ key: '已排序', label: '已排序' },
{ key: '暂停中', label: '暂停中' },
{ key: '已结束', label: '已结束' },
{ key: '托管中', label: '托管中' },
{ key: '已归档', label: '已归档' }
],
// 表格数据
tableData: [
{
id: 1,
title: '商品标题商品标题商品标题商品标题商品标题商品标题',
tags: ['广告进行中', '新品/老品'],
budget: {
current: 'NT$ 100',
target: 'NT$ 180'
},
roi: {
target: '自由ROI',
current: '8'
},
adSpend: 'NT$ 27.86',
period: {
duration: '2',
updateTime: '07-09 17:00'
},
status: '进行中',
createTime: {
date: '2024-07-09',
time: '17:00',
creator: 'admin'
}
}
],
// 颜色数组
productColors: ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
}
},
methods: {
// 选择功能
selectFeature(featureId) {
console.log('selectFeature 被调用featureId:', featureId);
try {
// 清理所有现有组件
this.clearAllComponents();
this.selectedFeature = featureId;
console.log('selectedFeature 设置为:', this.selectedFeature);
this.loadFeatureComponent(featureId);
} catch (error) {
console.error('selectFeature 执行失败:', error);
this.$message.error('切换功能失败,请重试');
}
},
// 返回主界面
backToMain() {
this.clearAllComponents();
this.selectedFeature = null;
},
// 清理所有组件
clearAllComponents() {
const componentMap = {
1: 'ad-hosting',
2: 'auto-review',
3: 'flash-sale-auto',
4: 'long-product-manage'
};
Object.values(componentMap).forEach(name => {
const container = document.getElementById(`${name}-component`);
if (container) {
// 销毁Vue实例
if (container.__vue__) {
container.__vue__.$destroy();
container.__vue__ = null;
}
container.innerHTML = '';
}
});
},
// 加载功能组件
loadFeatureComponent(featureId) {
const componentMap = {
1: 'ad-hosting',
2: 'auto-review',
3: 'flash-sale-auto',
4: 'long-product-manage'
};
const componentName = componentMap[featureId];
if (componentName) {
this.$nextTick(() => {
const container = document.getElementById(`${componentName}-component`);
if (container) {
// 使用axios代替fetch在JavaFX WebView中更稳定
axios.get(`/html/components/shopee/${componentName}.html`)
.then(response => response.data)
.then(html => {
// 清空其他组件
Object.values(componentMap).forEach(name => {
const otherContainer = document.getElementById(`${name}-component`);
if (otherContainer && name !== componentName) {
// 销毁Vue实例
if (otherContainer.__vue__) {
otherContainer.__vue__.$destroy();
}
otherContainer.innerHTML = '';
}
});
// 解析HTML内容并提取模板和脚本
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const template = tempDiv.querySelector('template');
const script = tempDiv.querySelector('script');
const style = tempDiv.querySelector('style');
// 添加样式
if (style && style.textContent) {
const styleElement = document.createElement('style');
styleElement.textContent = style.textContent;
document.head.appendChild(styleElement);
}
// 处理脚本和组件注册
if (script && script.textContent) {
let scriptContent = script.textContent.trim();
if (scriptContent.includes('module.exports')) {
// 提取组件名称
const nameMatch = scriptContent.match(/name:\s*['"`]([^'"`]+)['"`]/);
const extractedComponentName = nameMatch ? nameMatch[1] : componentName;
// 替换 module.exports 为直接的组件对象
scriptContent = scriptContent.replace(/module\.exports\s*=\s*/, '');
try {
// 创建组件配置对象
const componentConfig = eval('(' + scriptContent + ')');
// 注册为 Vue 组件
if (componentConfig && extractedComponentName) {
// 从 template 标签提取模板内容
if (template && template.innerHTML) {
componentConfig.template = template.innerHTML;
}
// 检查组件是否已注册,避免重复注册
const componentKey = `${extractedComponentName}-${featureId}`;
if (!Vue.options.components[componentKey]) {
Vue.component(componentKey, componentConfig);
console.log(`子组件 ${componentKey} 已成功注册,模板长度:`, componentConfig.template ? componentConfig.template.length : 0);
}
// 销毁现有的Vue实例如果有
if (container.__vue__) {
container.__vue__.$destroy();
}
// 使用Vue实例渲染组件
container.innerHTML = `<${componentKey}></${componentKey}>`;
// 延迟创建Vue实例确保DOM更新完成
this.$nextTick(() => {
try {
// 创建新的Vue实例
const vueInstance = new Vue({
el: container,
data() {
return {};
},
mounted() {
console.log(`子组件Vue实例已挂载: ${componentKey}`);
},
errorCaptured(err, instance, info) {
console.error(`子组件Vue错误: ${err.message}`, err);
return false;
}
});
console.log(`Vue实例创建成功: ${componentKey}`);
} catch (error) {
console.error(`创建Vue实例失败: ${componentKey}`, error);
// 降级处理:直接插入模板内容
if (template) {
container.innerHTML = template.innerHTML;
}
}
});
}
} catch (error) {
console.error('解析子组件脚本失败:', error);
console.error('脚本内容:', scriptContent);
// 降级处理:直接插入模板内容
if (template) {
container.innerHTML = template.innerHTML;
}
}
} else {
// 普通脚本执行
const newScript = document.createElement('script');
newScript.textContent = scriptContent;
document.head.appendChild(newScript);
document.head.removeChild(newScript);
// 插入模板内容
if (template) {
container.innerHTML = template.innerHTML;
}
}
} else if (template) {
// 只有模板没有脚本的情况
container.innerHTML = template.innerHTML;
}
})
.catch(error => {
console.error('加载组件失败:', error);
this.$message.error('组件加载失败');
});
}
});
}
},
// 设置激活状态
setActiveStatus(status) {
this.activeStatus = status;
},
// 获取标签类型
getTagType(tag) {
if (tag.includes('进行中')) return 'success';
if (tag.includes('新品')) return 'warning';
if (tag.includes('老品')) return 'info';
return '';
},
// 获取状态样式类
getStatusClass(status) {
switch(status) {
case '进行中': return 'running';
case '暂停中': return 'paused';
case '已归档': return 'stopped';
default: return 'stopped';
}
},
// 获取商品图片背景颜色
getProductColor(id) {
return this.productColors[(id - 1) % this.productColors.length];
},
// 分页大小改变
handleSizeChange(val) {
this.pageSize = val;
},
// 当前页改变
handleCurrentChange(val) {
this.currentPage = val;
},
// JavaFX WebView兼容性检查
checkJavaFXCompatibility() {
try {
// 检测是否在JavaFX WebView环境中
const isJavaFX = navigator.userAgent.includes('Java') ||
window.javaConnector !== undefined ||
window.java !== undefined;
if (isJavaFX) {
console.log('检测到JavaFX WebView环境启用兼容模式');
// 为所有功能卡片添加原生点击事件处理
this.$nextTick(() => {
this.addNativeClickHandlers();
});
} else {
console.log('标准浏览器环境');
}
} catch (error) {
console.warn('环境检测失败:', error);
}
},
// 添加原生点击事件处理
addNativeClickHandlers() {
try {
this.featureCards.forEach(card => {
// 为每个功能卡片添加原生事件监听
const cardElement = document.querySelector(`[data-card-id="${card.id}"]`);
if (cardElement) {
cardElement.addEventListener('click', (event) => {
console.log('原生点击事件触发cardId:', card.id);
this.selectFeature(card.id);
});
}
});
console.log('原生点击事件处理器已添加');
} catch (error) {
console.error('添加原生点击事件失败:', error);
}
},
// 调试方法:鼠标按下
handleCardMouseDown(cardId) {
console.log('鼠标按下事件cardId:', cardId);
},
// 调试方法:鼠标松开
handleCardMouseUp(cardId) {
console.log('鼠标松开事件cardId:', cardId);
}
},
mounted() {
console.log('虾皮平台组件已加载');
// JavaFX WebView兼容性检查
this.checkJavaFXCompatibility();
// 初始加载广告投放托管组件
this.loadFeatureComponent(1);
},
beforeDestroy() {
// 组件销毁前清理所有子组件
this.clearAllComponents();
}
}
</script>
<style scoped>
.feature-content {
height: auto;
overflow: visible;
}
.main-container {
padding: 20px;
height: auto;
overflow: visible;
}
.feature-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.card-item {
background: white;
border-radius: 8px;
padding: 20px;
border: 1px solid #e4e7ed;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
}
.card-item:hover {
border-color: #409EFF;
box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 0.15);
transform: translateY(-2px);
}
.card-icon {
margin-right: 16px;
}
.card-icon i {
font-size: 32px;
color: #409EFF;
}
.card-content h3 {
margin: 0 0 8px 0;
color: #303133;
font-size: 16px;
}
.card-content p {
margin: 0;
color: #606266;
font-size: 14px;
}
.stats-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.stats-item {
background: white;
padding: 16px;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.stats-label {
font-weight: 600;
color: #303133;
margin-bottom: 8px;
font-size: 14px;
}
.stats-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 12px;
}
.stats-type {
color: #606266;
}
.stats-value {
color: #303133;
font-weight: 500;
}
.filter-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.table-section {
background: white;
border-radius: 6px;
overflow: hidden;
}
.product-info {
display: flex;
align-items: center;
}
.product-image-placeholder {
width: 40px;
height: 40px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 12px;
}
.product-details {
flex: 1;
}
.product-title {
font-size: 14px;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-tags {
display: flex;
gap: 4px;
}
.budget-info, .roi-info, .period-info, .create-time {
font-size: 12px;
}
.current-budget, .roi-current {
color: #303133;
margin-bottom: 2px;
}
.target-budget, .roi-actual {
color: #909399;
}
.ad-spend {
font-weight: 600;
color: #E6A23C;
margin-bottom: 4px;
}
.spend-link a {
color: #409EFF;
text-decoration: none;
font-size: 12px;
}
.status-info {
display: flex;
align-items: center;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-dot.running {
background-color: #67C23A;
}
.status-dot.paused {
background-color: #E6A23C;
}
.status-dot.stopped {
background-color: #F56C6C;
}
.pagination-section {
padding: 20px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,935 @@
<template>
<div>
<div class="main-container">
<!-- 日期选择区域 -->
<div class="date-filter-section">
<el-select v-model="selectedShops" multiple placeholder="选择店铺" style="width: 300px;">
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.shopName" :value="shop.id"></el-option>
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
:picker-options="pickerOptions">
</el-date-picker>
<el-button type="primary" size="small" @click="fetchData" :disabled="loading" :loading="loading">
<i class="el-icon-loading" v-if="loading"></i>
<i class="el-icon-search" v-if="!loading"></i>
{{ loading ? '获取中...' : '获取订单数据' }}
</el-button>
<el-button type="danger" size="small" @click="stopFetch" :disabled="!loading">停止获取</el-button>
<el-button type="success" size="small" @click="exportToExcel" :disabled="loading || !allOrderData.length || exportLoading" :loading="exportLoading">
<i class="el-icon-download" v-if="!exportLoading"></i>
<i class="el-icon-loading" v-if="exportLoading"></i>
{{ exportLoading ? '导出中...' : '导出Excel' }}
</el-button>
<el-button type="warning" size="small" @click="refreshToken" :loading="tokenRefreshing">
<i class="el-icon-refresh" v-if="!tokenRefreshing"></i>
刷新认证
</el-button>
</div>
<!-- 进度条区域 -->
<div class="progress-section">
<div class="progress-box">
<div class="progress-container">
<el-progress
:percentage="progressPercentage"
:status="progressPercentage >= 100 ? 'success' : (progressStatus === 'exception' ? 'exception' : null)"
:stroke-width="6"
:show-text="false"
class="thin-progress">
</el-progress>
<div class="progress-text">{{progressPercentage}}%</div>
</div>
</div>
</div>
<!-- 表格容器 -->
<div class="table-container">
<div class="table-section custom-scrollbar" v-show="paginatedData && paginatedData.length >= 0">
<el-table :data="paginatedData"
style="width: 100%"
border
stripe
lazy
height="100%"
:cell-style="cellStyle"
:header-cell-style="headerCellStyle"
v-loading="tableLoading"
element-loading-text="正在获取订单数据..."
element-loading-spinner="el-icon-loading"
>
<el-table-column prop="orderedAt" label="下单时间" width="120" show-overflow-tooltip></el-table-column>
<el-table-column label="商品图片" width="80">
<template slot-scope="scope">
<div class="image-container" v-if="scope.row.productImage">
<el-image
:src="scope.row.productImage"
@error="onImageError(scope.row)"
class="thumb"
:preview-src-list="[scope.row.productImage]"
fit="contain"
loading="lazy"
preview-teleported
:z-index="3000">
<div slot="placeholder" class="image-placeholder">
<i class="el-icon-loading"></i>
</div>
<div slot="error" class="image-placeholder">
<i class="el-icon-picture"></i>
</div>
</el-image>
</div>
<span v-else>无图片</span>
</template>
</el-table-column>
<el-table-column prop="productTitle" label="商品名称" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="shopOrderNumber" label="乐天订单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="timeSinceOrder" label="下单距今" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="priceJpy" label="订单金额/日元" width="120">
<template slot-scope="scope">
<span class="price-tag">{{ formatJpy(scope.row.priceJpy) }}</span>
</template>
</el-table-column>
<el-table-column prop="productQuantity" label="数量" width="60"></el-table-column>
<el-table-column prop="shippingFeeJpy" label="税费/日元" width="100">
<template slot-scope="scope">
<span class="fee-tag">{{ formatJpy(scope.row.shippingFeeJpy) }}</span>
</template>
</el-table-column>
<el-table-column label="回款抽点rmb" width="120" show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ scope.row.serviceFee || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="productNumber" label="商品番号" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="poNumber" label="1688订单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="shippingFeeCny" label="采购金额/rmb" width="120">
<template slot-scope="scope">
<span class="fee-tag">{{ formatCny(scope.row.shippingFeeCny) }}</span>
</template>
</el-table-column>
<el-table-column prop="internationalShippingFee" label="国际运费/rmb" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="poLogisticsCompany" label="国内物流" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="poTrackingNumber" label="国内单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="internationalTrackingNumber" label="日本单号" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="trackInfo" label="地址状态" width="120" show-overflow-tooltip>
<template slot-scope="scope">
<template v-if="scope.row.trackInfo">
<el-tooltip :content="scope.row.trackInfo" placement="top" effect="dark">
<el-tag size="mini">{{ scope.row.trackInfo }}</el-tag>
</el-tooltip>
</template>
<span v-else>暂无</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- 固定分页组件 -->
<div class="pagination-fixed" v-if="allOrderData.length > 0">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[15, 30, 50, 100, 200]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="allOrderData.length">
</el-pagination>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.thumb {
width: 40px;
height: 40px;
object-fit: contain; /* 保持图片宽高比 */
border-radius: 4px;
cursor: pointer;
transition: none;
backface-visibility: hidden;
transform: translateZ(0);
}
.thumb:hover {
transform: scale(1.05);
transition: transform 0.2s ease;
}
/* 图片容器样式 */
.image-container {
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 20px;
margin: 0 auto;
background-color: #f8f9fa;
border-radius: 2px;
transform: translateZ(0);
}
.image-container .el-image {
width: 16px;
height: 16px;
border-radius: 2px;
}
/* 图片占位符样式 */
.image-placeholder {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
color: #c0c4cc;
font-size: 10px;
border-radius: 2px;
}
.main-container {
background-color: #fff;
border-radius: 4px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.progress-section {
margin-bottom: 10px;
}
.progress-box {
padding: 8px 0;
margin-bottom: 0;
}
.progress-container {
display: flex;
align-items: center;
position: relative;
padding-right: 40px;
}
.progress-container .el-progress {
flex: 1;
}
.thin-progress .el-progress-bar__outer {
background-color: #ebeef5;
border-radius: 10px;
}
.thin-progress .el-progress-bar__inner {
border-radius: 10px;
}
.progress-text {
position: absolute;
right: 0;
font-size: 13px;
color: #409EFF;
font-weight: 500;
}
/* Loading图标旋转动画 */
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.el-icon-loading {
animation: rotate 2s linear infinite !important;
}
/* 表格loading遮罩动画 */
.el-loading-spinner .el-icon-loading {
animation: rotate 1.5s linear infinite !important;
}
/* 表格内loading图标 */
.el-table .el-icon-loading {
animation: rotate 1s linear infinite !important;
display: inline-block;
}
/* 导出按钮简单加载效果 */
.el-button.is-loading {
opacity: 0.8;
}
.el-button.is-loading .el-icon-loading {
animation: rotate 1s linear infinite !important;
}
/* 进度条加载状态动画 */
.progress-section {
animation: slideIn 0.3s ease-in-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 脉冲动画 */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
/* 加载状态的脉冲效果 */
.el-progress {
animation: pulse 2s ease-in-out infinite;
}
/* 进度条内部条纹动画 */
.el-progress-bar__inner {
background-image: linear-gradient(45deg,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%,
transparent) !important;
background-size: 20px 20px !important;
animation: progressStripes 1s linear infinite, pulse 2s ease-in-out infinite !important;
}
@keyframes progressStripes {
0% {
background-position: 0 0;
}
100% {
background-position: 20px 0;
}
}
/* 表格加载时的渐入效果 */
.el-table tbody tr {
animation: fadeInUp 0.3s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 按钮悬停效果增强 */
.el-button {
transition: all 0.3s ease;
transform: translateZ(0);
}
.el-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.el-button:active:not(:disabled) {
transform: translateY(0);
}
/* 表格行悬停效果增强 */
.el-table tbody tr {
transition: all 0.3s ease;
}
.el-table tbody tr:hover {
transform: translateZ(0);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 进度文本闪烁效果 */
.progress-text {
animation: textGlow 2s ease-in-out infinite;
}
@keyframes textGlow {
0%, 100% {
text-shadow: 0 0 5px rgba(64, 158, 255, 0.3);
}
50% {
text-shadow: 0 0 15px rgba(64, 158, 255, 0.8);
}
}
.progress-actions {
display: flex;
justify-content: flex-end;
}
.date-filter-section {
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 15px;
}
.table-section {
overflow-y: auto;
margin-bottom: 10px;
}
.table-actions {
margin-bottom: 15px;
display: flex;
justify-content: flex-end;
}
/* 表格容器布局 */
.table-container {
display: flex;
flex-direction: column;
height: calc(100vh - 230px);
min-height: 400px;
}
.table-section {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
margin-bottom: 0;
}
.table-section .el-table {
flex: 1;
height: 100%;
overflow-y: auto;
}
/* 固定分页样式 */
.pagination-fixed {
flex-shrink: 0;
padding: 10px 15px;
background-color: #f9f9f9;
border-radius: 4px;
display: flex;
justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: sticky;
bottom: 0;
z-index: 10;
border-top: 1px solid #ebeef5;
height: 60px;
min-height: 60px;
}
/* 自定义滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 5px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 5px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 图片预览优化 */
.el-image-viewer__wrapper {
z-index: 3000 !important;
}
.el-image-viewer__mask {
background-color: rgba(0, 0, 0, 0.8) !important;
}
/* 表格内滚动条样式 */
.el-table__body-wrapper::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.el-table__body-wrapper::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 5px;
}
.el-table__body-wrapper::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 5px;
}
.el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 价格和费用标签样式 */
.price-tag {
color: #e6a23c;
font-weight: bold;
}
.fee-tag {
color: #909399;
font-weight: 500;
}
/* 操作按钮样式优化 */
.el-table .el-button--mini {
padding: 5px 8px;
font-size: 12px;
}
/* 表格行悬停效果 */
.el-table tbody tr:hover {
background-color: #f5f7fa;
}
/* 表格滚动性能优化 */
.el-table {
/* 启用硬件加速 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
/* 优化滚动性能 */
-webkit-overflow-scrolling: touch;
/* 减少重绘 */
will-change: auto;
/* 强制使用复合层 */
backface-visibility: hidden;
}
.el-table__body-wrapper {
/* 禁用平滑滚动避免卡顿 */
scroll-behavior: auto;
/* 启用硬件加速 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
/* 减少重绘和重排 */
.el-table .cell {
text-overflow: ellipsis;
white-space: nowrap;
}
/* 优化表格行高度 */
.el-table td {
padding: 4px 8px !important;
height: 25px !important;
line-height: 1.2 !important;
}
.el-table th {
padding: 6px 8px !important;
height: 25px !important;
line-height: 1.2 !important;
background-color: #fafafa;
color: #606266;
font-weight: 600;
}
.el-table .el-table__row {
height: 25px !important;
}
/* 表格滚动性能优化 */
.el-table {
transform: translateZ(0);
-webkit-transform: translateZ(0);
backface-visibility: hidden;
will-change: auto;
}
.el-table__body-wrapper {
scroll-behavior: auto;
transform: translateZ(0);
-webkit-transform: translateZ(0);
-webkit-overflow-scrolling: touch;
transition: none;
}
</style>
<script>
export default {
name: 'platform-zebra',
props: {
orderData: { type: Array, default: function(){ return []; } },
formatJpy: { type: Function, default: function(v){ return '¥' + (Number(v) || 0); } },
formatCny: { type: Function, default: function(v){ return '¥' + (Number(v) || 0); } },
onImageError: { type: Function, default: function(){} }
},
watch: {
// 监听外部传入的orderData变化
orderData: {
handler(newData, oldData) {
if (newData && newData.length > 0) {
// 保存当前页码
const currentPageBackup = this.currentPage;
// 判断是否是全新数据(初次加载)还是增量更新
const isInitialLoad = !oldData || oldData.length === 0;
const isCompleteReset = oldData && oldData.length > 0 && newData.length < oldData.length;
this.allOrderData = [...newData];
this.totalItems = this.allOrderData.length;
// 只有在初次加载或完全重置时才跳转到第一页
if (isInitialLoad || isCompleteReset) {
this.currentPage = 1;
} else {
// 增量更新时保持当前页码,但需要检查页码是否超出范围
const maxPage = Math.ceil(newData.length / this.pageSize);
this.currentPage = Math.min(currentPageBackup, Math.max(1, maxPage));
}
}
},
immediate: true
}
},
data() {
return {
loading: false,
tableLoading: false,
progressPercentage: 0,
progressMessage: '',
progressStatus: '',
dateRange: [],
selectedShops: [],
shopList: [],
// 分页相关数据
currentPage: 1,
pageSize: 15,
totalItems: 0,
allOrderData: [], // 存储所有订单数据
// 爬取相关数据
fetchCurrentPage: 1,
fetchTotalPages: 0,
fetchTotalItems: 0,
isFetching: false,
exportLoading: false,
tokenRefreshing: false,
pickerOptions: {
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近三个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
}
}]
}
}
},
mounted() {
// 页面加载时获取店铺列表和从localStorage恢复数据
this.loadShops();
this.loadDataFromStorage();
},
computed: {
// 分页后的数据
paginatedData() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.allOrderData.slice(start, end);
}
},
methods: {
// 获取店铺列表
async loadShops() {
try {
const response = await zebraAPI.getShops();
if (response && response.data && response.data.list) {
this.shopList = response.data.list;
}
} catch (error) {
console.error('获取店铺列表失败:', error);
}
},
fetchData() {
Object.assign(this, {
loading: true,
tableLoading: true,
progressPercentage: 0,
progressStatus: '',
progressMessage: '',
allOrderData: [],
fetchCurrentPage: 1,
fetchTotalPages: 0,
fetchTotalItems: 0,
isFetching: true,
totalItems: 0,
currentPage: 1,
currentBatchId: `ZEBRA_${Date.now()}`
});
// 获取日期范围
const [startDate = '', endDate = ''] = this.dateRange || [];
this.fetchPageData(startDate, endDate);
},
fetchPageData(startDate, endDate) {
if (!this.isFetching) {
return;
}
// 使用zebraAPI获取当前页数据
zebraAPI.getOrders({
startDate,
endDate,
page: this.fetchCurrentPage,
pageSize: 10, // 每页10条数据
batchId: this.currentBatchId,
shopIds: this.selectedShops && this.selectedShops.length > 0 ? this.selectedShops.join(',') : ''
})
.then(data => {
// 获取成功,处理数据
const orders = data.orders || [];
this.allOrderData = [...this.allOrderData, ...orders];
// 有数据后立即停止表格loading
if (this.allOrderData.length > 0) {
this.tableLoading = false;
}
// 触发响应式更新(移除$forceUpdate依赖Vue响应式机制
this.$nextTick();
// 保存数据到localStorage
this.saveDataToStorage();
// 通知父组件数据更新
this.$emit('order-data-updated', this.allOrderData);
// 更新总数据信息
this.fetchTotalPages = data.totalPages || 0;
this.fetchTotalItems = data.total || 0;
// 使用当前实际获取的数据条数,而不是后端返回的总数
this.totalItems = this.allOrderData.length;
// 更新进度 - 改为显示已获取的数据条数占总数的百分比
const currentCount = this.allOrderData.length;
this.progressPercentage = Math.min(100, Math.round((currentCount / this.fetchTotalItems) * 100));
// 判断是否继续获取下一页
if (this.fetchCurrentPage < this.fetchTotalPages && this.isFetching) {
this.fetchCurrentPage++;
// 延迟一点时间再请求下一页,避免请求过于频繁
setTimeout(() => {
this.fetchPageData(startDate, endDate);
}, 300);
} else {
// 全部获取完成
this.finishFetching();
}
})
.catch(error => {
console.error('获取订单数据失败:', error);
this.$message.error(error.message || '获取订单数据失败');
this.finishFetching(false);
});
},
finishFetching(success = true) {
this.isFetching = false;
this.loading = false;
this.tableLoading = false;
if (success) {
this.progressStatus = '';
this.progressPercentage = 100;
this.totalItems = this.allOrderData.length;
// 通知父组件更新数据
this.$emit('update:orderData', this.allOrderData);
this.$emit('order-data-updated', this.allOrderData);
} else {
this.progressStatus = 'exception';
}
},
stopFetch() {
console.log('停止获取订单数据');
this.isFetching = false;
this.loading = false; // 重置loading状态使获取订单按钮重新可用
this.tableLoading = false; // 停止表格loading
this.$message.info('已停止获取订单数据');
},
// 分页相关方法
handleSizeChange(newSize) {
this.pageSize = newSize;
// 只有在改变页大小时才重置到第一页
this.currentPage = 1;
},
handleCurrentChange(newPage) {
this.currentPage = newPage;
},
// 性能优化:简化数据存储方法,移除空方法
saveDataToStorage() {}, // 空实现,后端自动保存
async loadDataFromStorage() {
const response = await zebraAPI.getLatestOrders().catch(error => {
console.error('加载最新数据失败:', error);
return { orders: [] };
});
const ordersData = response.orders || [];
if (ordersData.length > 0) {
Object.assign(this, {
allOrderData: ordersData,
dateRange: [],
totalItems: ordersData.length,
tableLoading: false
});
this.$emit('order-data-updated', this.allOrderData);
}
},
// 性能优化简化刷新token逻辑
async refreshToken() {
this.tokenRefreshing = true;
const result = await zebraAPI.refreshToken().catch(error => {
console.error('刷新认证失败:', error);
this.$message.error(error.message || '认证刷新失败');
return null;
});
if (result) this.$message.success('认证刷新成功');
this.tokenRefreshing = false;
},
// 导出Excel方法 - 多线程优化版本
async exportToExcel() {
try {
if (!this.allOrderData || this.allOrderData.length === 0) {
this.$message.warning('没有数据可供导出');
return;
}
// JavaFX环境检查可选
// if (!window.javaConnector) {
// this.$message.error('此功能仅在JavaFX环境中可用');
// return;
// }
// 防止重复点击
if (this.exportLoading) {
return;
}
this.exportLoading = true;
// 显示导出提示
this.$message({
message: '正在生成Excel文件请稍候...',
type: 'info',
duration: 3000
});
// 生成文件名和导出数据
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const fileName = `斑马订单数据_${timestamp}.xlsx`;
const exportData = {
orders: this.allOrderData,
title: '斑马订单数据导出',
fileName: fileName,
timestamp: new Date().toLocaleString('zh-CN'),
useMultiThread: true, // 启用多线程处理
chunkSize: 1000 // 每个线程处理1000条数据
};
const result = await zebraAPI.exportAndSaveOrders(exportData);
this.$message.closeAll();
this.$message.success(`Excel文件已保存到: ${result.filePath}`);
} catch (error) {
console.error('导出Excel失败:', error);
this.$message.closeAll();
this.$message.error(error.message || '导出Excel失败');
} finally {
this.exportLoading = false;
}
},
progressFormat(percentage) {
if (percentage === 100) return '完成';
if (this.allOrderData.length > 0) {
return `${this.allOrderData.length}/${this.fetchTotalItems}`;
}
return `${percentage}%`;
},
// 性能优化:简化批次数据加载
async loadBatchData(batchId) {
const response = await zebraAPI.getOrdersByBatch(batchId).catch(error => {
console.error('获取批次数据失败:', error);
return { orders: [] };
});
const orders = response.orders || [];
if (orders.length > 0) {
this.allOrderData = orders;
this.totalItems = orders.length;
this.$emit('order-data-updated', this.allOrderData);
}
},
// 表格单元格样式
cellStyle() {
return {
'font-size': '12px',
'padding': '8px 4px'
};
},
// 表格表头样式
headerCellStyle() {
return {
'background-color': '#f5f7fa',
'color': '#303133',
'font-weight': 'bold',
'font-size': '13px',
'padding': '8px 4px'
};
}
},
beforeDestroy() {
// 组件销毁前停止获取
this.isFetching = false;
}
}
</script>

View File

@@ -0,0 +1,431 @@
<template>
<div class="ad-hosting-container">
<!-- 数据统计区域 -->
<div class="stats-section">
<div class="stats-item" v-for="stat in statsData" :key="stat.label">
<div class="stats-label">{{ stat.label }}</div>
<div class="stats-row" v-for="row in stat.rows" :key="row.type">
<span class="stats-type">{{ row.type }}</span>
<span class="stats-value">{{ row.value }}</span>
</div>
</div>
</div>
<!-- 筛选和状态切换 -->
<div class="filter-section">
<div class="filter-left">
<el-button-group>
<el-button
v-for="status in statusFilters"
:key="status.key"
:type="activeStatus === status.key ? 'primary' : ''"
size="small"
@click="setActiveStatus(status.key)">
{{ status.label }}
</el-button>
</el-button-group>
</div>
<div class="filter-right">
<el-input
placeholder="搜索商品"
v-model="searchText"
size="small"
style="width: 200px;">
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
</div>
</div>
<!-- 数据表格 -->
<div class="table-section">
<el-table :data="tableData" style="width: 100%">
<el-table-column label="商品信息" width="300">
<template slot-scope="scope">
<div class="product-info">
<div class="product-image-placeholder" :style="{ backgroundColor: getProductColor(scope.row.id) }">
{{ scope.row.title.substr(0, 1) }}
</div>
<div class="product-details">
<div class="product-title">{{ scope.row.title }}</div>
<div class="product-tags">
<el-tag v-for="tag in scope.row.tags" :key="tag" size="mini" :type="getTagType(tag)">
{{ tag }}
</el-tag>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="budget" label="每日预算" width="120">
<template slot-scope="scope">
<div class="budget-info">
<div class="current-budget">{{ scope.row.budget.current }}</div>
<div class="target-budget">{{ scope.row.budget.target }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="roi" label="目标投入产出比" width="150">
<template slot-scope="scope">
<div class="roi-info">
<div class="roi-current">ROI目标{{ scope.row.roi.target }}</div>
<div class="roi-actual">当前ROI{{ scope.row.roi.current }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="adSpend" label="广告花费" width="120">
<template slot-scope="scope">
<div class="ad-spend">{{ scope.row.adSpend }}</div>
<div class="spend-link">
<a href="#" @click.prevent>历史记录</a>
</div>
</template>
</el-table-column>
<el-table-column prop="period" label="投放周期" width="120">
<template slot-scope="scope">
<div class="period-info">
<div>投放周期:{{ scope.row.period.duration }}</div>
<div>更新时间:{{ scope.row.period.updateTime }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="托管状态" width="120">
<template slot-scope="scope">
<div class="status-info">
<span class="status-dot" :class="getStatusClass(scope.row.status)"></span>
<span>{{ scope.row.status }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="150">
<template slot-scope="scope">
<div class="create-time">
<div>创建时间:{{ scope.row.createTime.date }}</div>
<div>创建时间:{{ scope.row.createTime.time }}</div>
<div>创建时间:{{ scope.row.createTime.creator }}</div>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-section">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
module.exports = {
name: 'ad-hosting',
data() {
return {
// 当前激活的状态筛选
activeStatus: '进行中',
// 搜索文本
searchText: '',
// 分页相关
currentPage: 1,
pageSize: 10,
total: 100,
// 统计数据
statsData: [
{
label: '广告营销周期',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
},
{
label: '新品/老品养成进度',
rows: [
{ type: '养定期', value: '7天' },
{ type: '老品', value: '7天' }
]
},
{
label: '预算/营销/营销时间',
rows: [
{ type: '每日', value: '119时30分' }
]
},
{
label: 'ROI变更周期',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
},
{
label: 'ROI自动优化',
rows: [
{ type: '新品', value: '3' },
{ type: '老品', value: '5' }
]
},
{
label: '新品/营销防控预算',
rows: [
{ type: '新品', value: '3天' },
{ type: '老品', value: '7天' }
]
}
],
// 状态筛选
statusFilters: [
{ key: '全部商品', label: '全部商品' },
{ key: '进行中', label: '进行中' },
{ key: '已排序', label: '已排序' },
{ key: '暂停中', label: '暂停中' },
{ key: '已结束', label: '已结束' },
{ key: '托管中', label: '托管中' },
{ key: '已归档', label: '已归档' }
],
// 表格数据
tableData: [
{
id: 1,
title: '广告托管商品1 - 自动化投放优化',
tags: ['广告进行中', '新品'],
budget: {
current: 'NT$ 150',
target: 'NT$ 200'
},
roi: {
target: '自动ROI',
current: '6.5'
},
adSpend: 'NT$ 45.50',
period: {
duration: '5',
updateTime: '07-16 10:30'
},
status: '进行中',
createTime: {
date: '2024-07-16',
time: '10:30',
creator: 'admin'
}
},
{
id: 2,
title: '广告托管商品2 - 智能出价管理',
tags: ['广告进行中', '老品'],
budget: {
current: 'NT$ 200',
target: 'NT$ 300'
},
roi: {
target: '目标ROI 8',
current: '7.2'
},
adSpend: 'NT$ 78.20',
period: {
duration: '10',
updateTime: '07-16 14:20'
},
status: '托管中',
createTime: {
date: '2024-07-10',
time: '14:20',
creator: 'admin'
}
}
],
// 颜色数组
productColors: ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
}
},
methods: {
// 设置激活状态
setActiveStatus(status) {
this.activeStatus = status;
},
// 获取标签类型
getTagType(tag) {
if (tag.includes('进行中')) return 'success';
if (tag.includes('新品')) return 'warning';
if (tag.includes('老品')) return 'info';
return '';
},
// 获取状态样式类
getStatusClass(status) {
switch(status) {
case '进行中': return 'running';
case '托管中': return 'hosting';
case '暂停中': return 'paused';
case '已归档': return 'stopped';
default: return 'stopped';
}
},
// 获取商品图片背景颜色
getProductColor(id) {
return this.productColors[(id - 1) % this.productColors.length];
},
// 分页大小改变
handleSizeChange(val) {
this.pageSize = val;
},
// 当前页改变
handleCurrentChange(val) {
this.currentPage = val;
}
},
mounted() {
console.log('广告投放托管组件已加载');
}
}
</script>
<style scoped>
.ad-hosting-container {
padding: 20px;
}
.header-section {
margin-bottom: 20px;
}
.header-section h2 {
color: #303133;
margin-bottom: 8px;
}
.header-section p {
color: #606266;
margin: 0;
}
.filter-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.table-section {
background: white;
border-radius: 6px;
overflow: hidden;
}
.product-info {
display: flex;
align-items: center;
}
.product-image-placeholder {
width: 40px;
height: 40px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 12px;
}
.product-details {
flex: 1;
}
.product-title {
font-size: 14px;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-tags {
display: flex;
gap: 4px;
}
.budget-info, .roi-info, .period-info, .create-time {
font-size: 12px;
}
.current-budget, .roi-current {
color: #303133;
margin-bottom: 2px;
}
.target-budget, .roi-actual {
color: #909399;
}
.ad-spend {
font-weight: 600;
color: #E6A23C;
margin-bottom: 4px;
}
.spend-link a {
color: #409EFF;
text-decoration: none;
font-size: 12px;
}
.status-info {
display: flex;
align-items: center;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-dot.running {
background-color: #67C23A;
}
.status-dot.hosting {
background-color: #409EFF;
}
.status-dot.paused {
background-color: #E6A23C;
}
.status-dot.stopped {
background-color: #F56C6C;
}
.pagination-section {
padding: 20px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,264 @@
<template>
<div class="auto-review-container">
<!-- 顶部筛选和统计栏 -->
<div class="filter-stats-bar">
<div class="left-section">
<span class="section-title">回评记录</span>
<span class="total-count">合计500条 <span class="sub-text">(可查看历史190天)</span></span>
</div>
<div class="right-section">
<div class="date-filters">
<span class="filter-label">回复日期</span>
<div class="date-picker-group">
<el-date-picker
size="small"
type="date"
placeholder="开始日期"
value-format="yyyy-MM-dd"
style="width: 130px;">
</el-date-picker>
<span class="separator"></span>
<el-date-picker
size="small"
type="date"
placeholder="结束日期"
value-format="yyyy-MM-dd"
style="width: 130px;">
</el-date-picker>
</div>
</div>
<div class="view-controls">
<el-button-group>
<el-button size="small" icon="el-icon-s-grid">重置</el-button>
<el-button size="small" icon="el-icon-s-order">回评模板</el-button>
</el-button-group>
</div>
</div>
</div>
<!-- 订单评价列表 -->
<div class="review-table-section">
<el-table :data="reviewData" style="width: 100%" border>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="星级" width="120">
<template slot-scope="scope">
<div class="rating-stars">
<i class="el-icon-star-on" v-for="n in scope.row.rating" :key="n" style="color: #FFAC2D;"></i>
</div>
</template>
</el-table-column>
<el-table-column label="订单编号" width="150" prop="orderNumber"></el-table-column>
<el-table-column label="买家评价" width="200" prop="buyerReview"></el-table-column>
<el-table-column label="商家回评" width="200">
<template slot-scope="scope">
<div>{{ scope.row.merchantReply || '法国专柜同步' }}</div>
</template>
</el-table-column>
<el-table-column label="操作时间" width="150" prop="operationTime"></el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-section">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
module.exports = {
name: 'auto-review',
data() {
return {
// 分页相关
currentPage: 1,
pageSize: 10,
total: 500,
// 评价数据
reviewData: [
{
id: 1,
rating: 5,
orderNumber: '250802NSFGYTD',
buyerReview: '支持专柜同步',
merchantReply: '法国专柜同步',
operationTime: '2023-07-01 14:00'
},
{
id: 2,
rating: 5,
orderNumber: '250802NSFGYTD',
buyerReview: '支持专柜同步',
merchantReply: '法国专柜同步',
operationTime: '2023-07-01 14:00'
},
{
id: 3,
rating: 5,
orderNumber: '250802NSFGYTD',
buyerReview: '支持专柜同步',
merchantReply: '法国专柜同步',
operationTime: '2023-07-01 14:00'
},
{
id: 4,
rating: 5,
orderNumber: '250802NSFGYTD',
buyerReview: '支持专柜同步',
merchantReply: '法国专柜同步',
operationTime: '2023-07-01 14:00'
},
{
id: 5,
rating: 5,
orderNumber: '250802NSFGYTD',
buyerReview: '支持专柜同步',
merchantReply: '法国专柜同步',
operationTime: '2023-07-01 14:00'
}
]
}
},
methods: {
// 分页大小改变
handleSizeChange(val) {
this.pageSize = val;
},
// 当前页改变
handleCurrentChange(val) {
this.currentPage = val;
},
// 加载数据
loadData() {
// 这里可以添加从后端获取数据的逻辑
console.log('加载数据...');
}
},
mounted() {
this.loadData();
}
}
</script>
<style scoped>
.auto-review-container {
padding: 20px;
background-color: #fff;
height: auto;
min-height: auto;
}
/* 顶部筛选和统计栏样式 */
.filter-stats-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
background-color: #fff;
border-radius: 4px;
border-bottom: 1px solid #EBEEF5;
}
.left-section {
display: flex;
align-items: center;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.total-count {
margin-left: 15px;
font-size: 14px;
color: #606266;
}
.total-count strong {
color: #409EFF;
font-weight: 600;
}
.sub-text {
color: #909399;
font-size: 12px;
margin-left: 4px;
}
.right-section {
display: flex;
align-items: center;
}
.date-filters {
display: flex;
align-items: center;
margin-right: 20px;
background-color: #f5f7fa;
padding: 5px 10px;
border-radius: 4px;
}
.filter-label {
margin-right: 10px;
color: #606266;
font-size: 13px;
}
.date-picker-group {
display: flex;
align-items: center;
}
.separator {
margin: 0 8px;
color: #909399;
}
.view-controls {
display: flex;
align-items: center;
}
.view-controls .el-button-group {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.review-table-section {
background: white;
border-radius: 4px;
overflow: hidden;
}
.rating-stars {
display: flex;
}
.rating-stars i {
margin-right: 2px;
}
.pagination-section {
padding: 15px;
text-align: right;
background-color: #fff;
border-top: 1px solid #ebeef5;
}
</style>

Some files were not shown because too many files have changed in this diff Show More