1
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 || '批量获取产品信息失败');
|
||||
|
||||
Reference in New Issue
Block a user