This commit is contained in:
2025-09-27 14:05:45 +08:00
parent 89f600fa11
commit fe3e24b945
23 changed files with 474 additions and 261 deletions

View File

@@ -3,15 +3,20 @@ import com.tashow.erp.entity.AmazonProductEntity;
import com.tashow.erp.repository.AmazonProductRepository;
import com.tashow.erp.service.IAmazonScrapingService;
import com.tashow.erp.utils.ExcelParseUtil;
import com.tashow.erp.utils.ExcelExportUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import com.tashow.erp.fx.controller.JavaBridge;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.Optional;
@RestController
@RequestMapping("/api/amazon")
public class AmazonController {
@@ -20,6 +25,8 @@ public class AmazonController {
private IAmazonScrapingService amazonScrapingService;
@Autowired
private AmazonProductRepository amazonProductRepository;
@Autowired
private JavaBridge javaBridge;
/**
* 批量获取亚马逊产品信息
*/
@@ -53,9 +60,6 @@ public class AmazonController {
*/
@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()) {
@@ -72,4 +76,5 @@ public class AmazonController {
return JsonData.buildError("解析失败: " + e.getMessage());
}
}
}

View File

@@ -21,8 +21,7 @@ public class BanmaOrderController {
IBanmaOrderService banmaOrderService;
@Autowired
BanmaOrderRepository banmaOrderRepository;
@Autowired
JavaBridge javaBridge;
@GetMapping("/orders")
public ResponseEntity<Map<String, Object>> getOrders(
@RequestParam(required = false, name = "accountId") Long accountId,
@@ -30,7 +29,7 @@ public class BanmaOrderController {
@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( "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(accountId, startDate, endDate, page, pageSize, batchId, shopIdList);
@@ -73,30 +72,4 @@ public class BanmaOrderController {
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

@@ -59,7 +59,7 @@ public class ProxyController {
}
/**
* 通过URL参数代理获取图片为JavaFX WebView优化
* 通过URL参数代理获取图片
* @param imageUrl 图片URL
* @return 图片字节数组
*/
@@ -104,7 +104,7 @@ public class ProxyController {
// 设置缓存头以提升性能
responseHeaders.setCacheControl("max-age=3600");
responseHeaders.set("Access-Control-Allow-Origin", "*");
// 删除手动CORS设置使用WebConfig中的全局CORS配置
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
} catch (Exception e) {

View File

@@ -126,75 +126,6 @@ public class RakutenController {
}
}
@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中位价",
"1688最低价",
"1688中间价",
"1688最高价"
};
List<Map<String, Object>> rows = new ArrayList<>();
for (Map<String, Object> p : products) {
LinkedHashMap<String, Object> row = new LinkedHashMap<>();
List<Double> priceList = parseSkuPriceList(p.get("skuPriceJson"), p.get("skuPrice"));
Double minPrice = priceList.isEmpty() ? null : priceList.get(0);
Double midPrice = priceList.isEmpty() ? null : priceList.get(priceList.size() / 2);
Double maxPrice = priceList.isEmpty() ? null : priceList.get(priceList.size() - 1);
row.put("店铺名", p.get("originalShopName"));
row.put("商品链接", p.get("productUrl"));
row.put("商品图片", p.get("imgUrl"));
row.put("排名", p.get("ranking"));
row.put("商品标题", p.get("productTitle"));
row.put("价格", p.get("price"));
row.put("1688识图链接", p.get("mapRecognitionLink"));
row.put("1688运费", p.get("freight"));
row.put("1688中位价", p.get("median"));
row.put("1688最低价", minPrice);
row.put("1688中间价", midPrice);
row.put("1688最高价", maxPrice);
rows.add(row);
}
byte[] excelData = com.tashow.erp.utils.ExcelExportUtil.createExcelWithImages(
"乐天商品数据",
headers,
rows,
skipImages ? -1 : 1,
skipImages ? null : "商品图片"
);
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());
}
}
// 解析 skuPriceJson 或 skuPrice 字段中的价格键,返回从小到大排序的价格列表
private static List<Double> parseSkuPriceList(Object skuPriceJson, Object skuPrice) {

View File

@@ -18,7 +18,7 @@ public class AmazonProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
@Column
private String asin;
@Column(name = "price")
@@ -34,7 +34,6 @@ public class AmazonProductEntity {
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -60,7 +60,7 @@ public interface AmazonProductRepository extends JpaRepository<AmazonProductEnti
/**
* 获取最新会话的产品数据(只返回最后一次采集的结果)
*/
@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)
@Query(value = "SELECT * FROM amazon_products WHERE session_id = (SELECT session_id FROM amazon_products GROUP BY session_id ORDER BY session_id DESC LIMIT 1) ORDER BY updated_at ", nativeQuery = true)
List<AmazonProductEntity> findLatestProducts();
/**
@@ -70,4 +70,12 @@ public interface AmazonProductRepository extends JpaRepository<AmazonProductEnti
@Transactional
@Query("DELETE FROM AmazonProductEntity a WHERE a.asin = :asin AND a.createdAt >= :cutoffTime")
void deleteByAsinAndCreatedAtAfter(@Param("asin") String asin, @Param("cutoffTime") LocalDateTime cutoffTime);
/**
* 删除指定时间之前的所有数据(用于数据库清理)
*/
@Modifying
@Transactional
@Query("DELETE FROM AmazonProductEntity a WHERE a.createdAt < :beforeTime")
void deleteAllDataBefore(@Param("beforeTime") LocalDateTime beforeTime);
}

View File

@@ -62,7 +62,7 @@ public interface RakutenProductRepository extends JpaRepository<RakutenProductEn
/**
* 获取最新会话的产品数据(只返回最后一次采集的结果)
*/
@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)
@Query(value = "SELECT * FROM rakuten_products WHERE session_id = (SELECT session_id FROM rakuten_products ORDER BY created_at DESC LIMIT 1) ORDER BY created_at ASC, id ASC", nativeQuery = true)
List<RakutenProductEntity> findLatestProducts();
/**
@@ -98,4 +98,12 @@ public interface RakutenProductRepository extends JpaRepository<RakutenProductEn
* 根据产品URL列表查找产品
*/
List<RakutenProductEntity> findByProductUrlIn(List<String> productUrls);
/**
* 删除指定时间之前的所有数据(用于数据库清理)
*/
@Modifying
@Transactional
@Query("DELETE FROM RakutenProductEntity r WHERE r.createdAt < :beforeTime")
void deleteAllDataBefore(@Param("beforeTime") LocalDateTime beforeTime);
}

View File

@@ -58,11 +58,13 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
// 提取价格
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;
String price = null;
if (!isEmpty(priceSymbol) && !isEmpty(priceWhole)) {
price = priceSymbol + priceWhole;
}
if (isEmpty(price)) {
price = html.xpath("//span[@class='a-price-range']/text()").toString();
}
// 提取卖家
String seller = html.xpath("//a[@id='sellerProfileTriggerId']/text()").toString();
if (isEmpty(seller)) {
@@ -101,39 +103,44 @@ public class AmazonScrapingServiceImpl implements IAmazonScrapingService, PagePr
@Override
public List<AmazonProductEntity> batchGetProductInfo(List<String> asinList, String batchId) {
String sessionId = (batchId != null) ? batchId : "SINGLE_" + UUID.randomUUID();
List<AmazonProductEntity> products = new ArrayList<>();
LocalDateTime batchTime = LocalDateTime.now(); // 统一的批次时间
for (String asin : asinList) {
// 第一步清理1小时前的所有旧数据
amazonProductRepository.deleteAllDataBefore(LocalDateTime.now().minusHours(1));
// 第二步处理每个ASIN
Map<String, AmazonProductEntity> allProducts = new HashMap<>();
for (String asin : asinList.stream().distinct().toList()) {
if (asin == null || asin.trim().isEmpty()) continue;
String cleanAsin = asin.replaceAll("[^a-zA-Z0-9]", "");
AmazonProductEntity product = amazonProductRepository.findByAsin(cleanAsin).filter(entity -> entity.getCreatedAt().isAfter(LocalDateTime.now().minusHours(1)) && !isEmpty(entity.getPrice()) && !isEmpty(entity.getSeller())).orElseGet(() -> {
// 采集新数据
// 查找缓存,有缓存就用缓存,没缓存就爬取
Optional<AmazonProductEntity> cached = amazonProductRepository.findByAsin(cleanAsin);
if (cached.isPresent()) {
AmazonProductEntity entity = cached.get();
entity.setSessionId(sessionId);
entity.setUpdatedAt(LocalDateTime.now());
amazonProductRepository.save(entity);
allProducts.put(cleanAsin, entity);
} else {
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;
}
AmazonProductEntity entity = resultCache.getOrDefault(cleanAsin, new AmazonProductEntity());
entity.setAsin(cleanAsin);
entity.setSessionId(sessionId);
try {
amazonProductRepository.save(entity);
dataReportUtil.reportDataCollection("AMAZON", 1, "0");
} catch (Exception e) {
logger.warn("保存商品数据失败: {}", cleanAsin);
}
return entity;
});
products.add(product);
entity.setUpdatedAt(LocalDateTime.now());
amazonProductRepository.save(entity);
dataReportUtil.reportDataCollection("AMAZON", 1, "0");
allProducts.put(cleanAsin, entity);
}
}
return products;
return new ArrayList<>(allProducts.values());
}
private boolean isEmpty(String str) {

View File

@@ -18,9 +18,6 @@ import jakarta.annotation.PostConstruct;
@Service
public class AuthServiceImpl implements IAuthService {
@Value("${api.server.base-url}")
private String serverApiUrl;
@Value("${project.version:2.1.0}")
private String appVersion;

View File

@@ -10,6 +10,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -50,6 +51,21 @@ public class RakutenCacheServiceImpl implements IRakutenCacheService {
@Override
@Transactional
public void saveProductsWithSessionId(List<RakutenProduct> products, String sessionId) {
if (products == null || products.isEmpty()) {
return;
}
// 获取所有涉及的店铺名
Set<String> shopNames = products.stream()
.map(RakutenProduct::getOriginalShopName)
.filter(name -> name != null && !name.trim().isEmpty())
.collect(Collectors.toSet());
// 清理所有1小时前的旧数据不分店铺全部清掉
LocalDateTime cutoffTime = LocalDateTime.now().minusHours(1);
repository.deleteAllDataBefore(cutoffTime);
log.info("清理1小时前的所有旧数据");
List<RakutenProductEntity> entities = products.stream()
.map(product -> {
RakutenProductEntity entity = new RakutenProductEntity();
@@ -60,7 +76,7 @@ public class RakutenCacheServiceImpl implements IRakutenCacheService {
.collect(Collectors.toList());
repository.saveAll(entities);
log.info("保存产品数据sessionId: {},数量: {}", sessionId, products.size());
log.info("保存产品数据sessionId: {},数量: {},涉及店铺: {}", sessionId, products.size(), shopNames);
}
/**
@@ -68,7 +84,11 @@ public class RakutenCacheServiceImpl implements IRakutenCacheService {
*/
@Override
public boolean hasRecentData(String shopName) {
return repository.existsByOriginalShopNameAndCreatedAtAfter(shopName, LocalDateTime.now().minusHours(1));
boolean hasRecent = repository.existsByOriginalShopNameAndCreatedAtAfter(shopName, LocalDateTime.now().minusHours(1));
if (hasRecent) {
log.info("店铺 {} 存在1小时内缓存数据将使用缓存", shopName);
}
return hasRecent;
}
/**
@@ -112,6 +132,11 @@ public class RakutenCacheServiceImpl implements IRakutenCacheService {
return;
}
// 清理所有1小时前的旧数据不分店铺全部清掉
LocalDateTime cutoffTime = LocalDateTime.now().minusHours(1);
repository.deleteAllDataBefore(cutoffTime);
log.info("清理1小时前的所有旧缓存数据");
// 根据产品的唯一标识如productUrl来查找并更新对应的数据库记录
List<String> productUrls = products.stream()
.map(RakutenProduct::getProductUrl)

View File

@@ -527,11 +527,7 @@
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 || '批量获取产品信息失败');