diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java index 38786b88..890e54ad 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BanmaOrderController.java @@ -209,6 +209,7 @@ public class BanmaOrderController extends BaseController { */ @SuppressWarnings("unchecked") private String getTrackingInfo(String trackingNumber) { + try { R> sagawaResult = sagawaExpressController.getTrackingInfo(trackingNumber); if (sagawaResult != null && sagawaResult.getCode() == 200) { Map sagawaData = sagawaResult.getData(); @@ -223,23 +224,31 @@ public class BanmaOrderController extends BaseController { } } } - try { - String url = String.format(TRACKING_URL, trackingNumber); - ResponseEntity response = restTemplate.getForEntity(url, Map.class); - Map responseBody = response.getBody(); - if (responseBody != null && Integer.valueOf(0).equals(responseBody.get("code"))) { - return Optional.ofNullable(responseBody.get("data")) - .map(data -> (List>) data) - .filter(list -> !list.isEmpty()) - .map(list -> list.get(0)) - .map(track -> (String) track.get("track")) - .orElse(null); + + // 如果从佐川获取失败,尝试从斑马API获取 + try { + String url = String.format(TRACKING_URL, trackingNumber); + ResponseEntity response = restTemplate.getForEntity(url, Map.class); + Map responseBody = response.getBody(); + if (responseBody != null && Integer.valueOf(0).equals(responseBody.get("code"))) { + return Optional.ofNullable(responseBody.get("data")) + .map(data -> (List>) data) + .filter(list -> !list.isEmpty()) + .map(list -> list.get(0)) + .map(track -> (String) track.get("track")) + .orElse(null); + } + } catch (Exception e) { + logger.error("从斑马API获取物流信息失败: {}", e.getMessage()); + // 继续处理,不中断流程 } } catch (Exception e) { logger.error("获取物流信息失败: {}", e.getMessage()); + // 继续处理,不中断流程 } - return null; + // 如果所有尝试都失败,返回默认值 + return "暂无物流信息"; } /** @@ -298,7 +307,8 @@ public class BanmaOrderController extends BaseController { // 计算分页信息 int totalPages = (int) Math.ceil((double) totalCount / DEFAULT_PAGE_SIZE); - boolean hasMore = orders.size() == DEFAULT_PAGE_SIZE; + // 修改hasMore判断逻辑,根据当前页数和总页数判断 + boolean hasMore = totalCount > 0 && 1 < totalPages; // 构建结果 Map resultMap = new HashMap<>(); @@ -340,12 +350,26 @@ public class BanmaOrderController extends BaseController { @ApiParam("开始日期(yyyy-MM-dd)") @RequestParam(required = false) String startDate, @ApiParam("结束日期(yyyy-MM-dd)") @RequestParam(required = false) String endDate) { - DeferredResult>> deferredResult = new DeferredResult<>(120000L); + DeferredResult>> deferredResult = new DeferredResult<>(999999999L); Thread thread = new Thread(() -> { try { if (deferredResult.isSetOrExpired()) return; + // 获取总页数信息 + HttpEntity entity = createHttpEntity(); + String url = buildApiUrl(1, DEFAULT_PAGE_SIZE, startDate, endDate); + ResponseEntity countResponse = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class); + Map countResponseBody = countResponse.getBody(); + int totalPages = 1; + + if (countResponseBody != null && countResponseBody.containsKey("data")) { + Map dataMap = (Map) countResponseBody.get("data"); + int totalCount = ((Number) dataMap.getOrDefault("total", 0)).intValue(); + totalPages = (int) Math.ceil((double) totalCount / DEFAULT_PAGE_SIZE); + } + + // 获取当前页数据 R>> pageResult = fetchOrdersFromApi(page, DEFAULT_PAGE_SIZE, startDate, endDate); if (pageResult.getCode() != 200) { @@ -355,10 +379,14 @@ public class BanmaOrderController extends BaseController { List> pageData = pageResult.getData(); + // 修改hasMore判断逻辑,根据当前页数和总页数判断 + boolean hasMore = page < totalPages; + Map resultMap = new HashMap<>(); resultMap.put("orders", pageData); - resultMap.put("hasMore", pageData != null && pageData.size() == DEFAULT_PAGE_SIZE); + resultMap.put("hasMore", hasMore); resultMap.put("nextPage", page + 1); + resultMap.put("totalPages", totalPages); setDeferredResult(deferredResult, R.ok(resultMap)); } catch (Exception e) { @@ -369,7 +397,6 @@ public class BanmaOrderController extends BaseController { thread.setDaemon(true); thread.start(); - return deferredResult; } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/SagawaExpressController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/SagawaExpressController.java new file mode 100644 index 00000000..66cfba79 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/SagawaExpressController.java @@ -0,0 +1,160 @@ +package com.ruoyi.web.controller.tool; + +import java.util.HashMap; +import java.util.Map; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +import us.codecraft.webmagic.Page; +import us.codecraft.webmagic.Site; +import us.codecraft.webmagic.Spider; +import us.codecraft.webmagic.processor.PageProcessor; + +/** + * 佐川急便物流查询控制器 + * + * @author ruoyi + */ +@Api("佐川急便物流查询接口") +@RestController +@RequestMapping("/tool/sagawa") +@Anonymous +public class SagawaExpressController extends BaseController implements PageProcessor { + private static final Logger logger = LoggerFactory.getLogger(SagawaExpressController.class); + + // 站点配置 + private final Site site = Site.me() + .setRetryTimes(3) + .setSleepTime(1000) + .setTimeOut(99999999) + .setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + + // 查询结果 + private Map resultMap = new HashMap<>(); + @Override + public void process(Page page) { + try { + String pageContent = page.getHtml().toString(); + + // 检查是否有包裹数据未登记的信息 + boolean hasNoData = pageContent.contains("お荷物データが登録されておりません"); + + if (hasNoData) { + resultMap.put("status", "notFound"); + resultMap.put("message", "没有找到对应的包裹信息"); + return; + } + + String trackingTableRegex = "]*class=\"table_basic table_okurijo_detail2\"[^>]*>\\s*\\s*(?:.*?]*>荷物状況.*?.*?.*?.*?)+\\s*\\s*"; + Pattern pattern = Pattern.compile(trackingTableRegex, Pattern.DOTALL); + Matcher matcher = pattern.matcher(pageContent); + + if (matcher.find()) { + String trackingTable = matcher.group(0); + + // 提取表格中的最后一行 + String rowRegex = "\\s*\\s*([^<]*?)\\s*\\s*\\s*([^<]*?)\\s*\\s*\\s*([^<]*?)\\s*\\s*"; + 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 trackInfo = new HashMap<>(); + trackInfo.put("status", status); + trackInfo.put("dateTime", dateTime); + trackInfo.put("office", office); + + resultMap.put("status", "success"); + resultMap.put("trackInfo", trackInfo); + } else { + resultMap.put("status", "noRecords"); + resultMap.put("message", "没有物流记录"); + } + } else { + resultMap.put("status", "noTable"); + resultMap.put("message", "未找到物流跟踪表格"); + } + } catch (Exception e) { + logger.error("解析页面失败", e); + resultMap.put("status", "error"); + resultMap.put("message", "解析页面失败: " + e.getMessage()); + } + } + + @Override + public Site getSite() { + return site; + } + + /** + * 构建佐川急便查询URL + */ + private String buildSagawaUrl(String trackingNumber) { + return "https://k2k.sagawa-exp.co.jp/p/web/okurijosearch.do?okurijoNo=" + trackingNumber.trim(); + } + + /** + * 查询佐川急便物流信息 + */ + @ApiOperation("查询佐川急便物流信息") + @GetMapping("/tracking/{trackingNumber}") + public R> getTrackingInfo(@PathVariable("trackingNumber") String trackingNumber) { + try { + if (trackingNumber == null || trackingNumber.trim().isEmpty()) { + return R.fail("运单号不能为空"); + } + + resultMap = new HashMap<>(); + String url = buildSagawaUrl(trackingNumber); + + try { + Spider spider = Spider.create(this) + .addUrl(url) + .thread(1); + spider.run(); + } catch (Exception e) { + logger.error("爬取物流信息失败,运单号:" + trackingNumber, e); + Map defaultTrackInfo = new HashMap<>(); + defaultTrackInfo.put("status", "处理中"); + defaultTrackInfo.put("dateTime", ""); + defaultTrackInfo.put("office", ""); + + resultMap.put("status", "success"); + resultMap.put("trackInfo", defaultTrackInfo); + } + + return R.ok(resultMap); + } catch (Exception e) { + logger.error("查询物流信息失败", e); + Map errorResult = new HashMap<>(); + errorResult.put("status", "error"); + errorResult.put("message", "查询物流信息失败: " + e.getMessage()); + return R.ok(errorResult); + } + } +} \ No newline at end of file diff --git a/ruoyi-ui/src/utils/request.js b/ruoyi-ui/src/utils/request.js index 7150ecb1..757871f9 100644 --- a/ruoyi-ui/src/utils/request.js +++ b/ruoyi-ui/src/utils/request.js @@ -17,11 +17,13 @@ const service = axios.create({ // axios中请求配置有baseURL选项,表示请求URL公共部分 baseURL: process.env.VUE_APP_BASE_API, // 超时 - timeout: 10000 + timeout: 99999999 }) // request拦截器 service.interceptors.request.use(config => { + config.timeout = 99999999; + // 是否需要设置 token const isToken = (config.headers || {}).isToken === false // 是否需要防止数据重复提交 diff --git a/ruoyi-ui/src/views/banma/orders/index.vue b/ruoyi-ui/src/views/banma/orders/index.vue index 00c426b5..611a21ef 100644 --- a/ruoyi-ui/src/views/banma/orders/index.vue +++ b/ruoyi-ui/src/views/banma/orders/index.vue @@ -6,7 +6,7 @@ 斑马订单数据 - +
@@ -62,7 +62,7 @@
- + - +
-
- + - +
已加载全部数据 @@ -232,7 +230,7 @@ export default { console.error('加载缓存数据失败:', error); } }, - + /** 保存数据到本地缓存 */ saveToCache() { try { @@ -253,13 +251,13 @@ export default { this.$message.warning('保存数据到本地缓存失败'); } }, - + /** 清空缓存数据 */ clearCache() { localStorage.removeItem('banma_orders_data'); this.$message.success('缓存数据已清除'); }, - + /** 获取订单列表 */ loadData() { this.loading = true; @@ -273,11 +271,11 @@ export default { total: 100, percentage: 0 }; - + this.clearCache(); this.loadFirstPage(); }, - + /** 加载第一页数据 */ loadFirstPage() { // 准备请求参数,添加时间范围 @@ -286,7 +284,7 @@ export default { params.startDate = this.dateRange[0]; params.endDate = this.dateRange[1]; } - + request({ url: '/tool/banma/orders/all', method: 'get', @@ -300,10 +298,10 @@ export default { this.currentPage = 1; this.totalRecords = response.data.total || 0; this.totalPages = response.data.totalPages || 1; - + this.updateProgress(); this.saveToCache(); - + if (this.hasMore) { this.autoLoadNextPage(); } else { @@ -323,11 +321,11 @@ export default { this.$message.error("获取订单失败: " + (error.message || error)); }); }, - + /** 自动加载下一页 */ autoLoadNextPage() { if (!this.hasMore || !this.loading) return; - + setTimeout(() => { // 准备请求参数,添加时间范围 const params = { page: this.nextPage }; @@ -335,7 +333,7 @@ export default { params.startDate = this.dateRange[0]; params.endDate = this.dateRange[1]; } - + request({ url: '/tool/banma/orders/next', method: 'get', @@ -343,15 +341,23 @@ export default { timeout: 99999999 }).then(response => { if (response.code === 200) { - this.orderData = [...this.orderData, ...response.data.orders]; + // 添加当前页数据 + this.orderData = [...this.orderData, ...(response.data.orders || [])]; this.hasMore = response.data.hasMore; this.nextPage = response.data.nextPage; this.currentPage++; - + this.totalPages = response.data.totalPages || this.totalPages; + this.updateProgress(); this.saveToCache(); - - if (this.hasMore) { + + console.log(`已加载第${this.currentPage}页,共${this.totalPages}页,hasMore=${this.hasMore}`); + + // 如果hasMore为false,但当前页小于总页数,强制继续加载 + if (!this.hasMore && this.currentPage < this.totalPages) { + this.hasMore = true; + this.autoLoadNextPage(); + } else if (this.hasMore) { this.autoLoadNextPage(); } else { this.loading = false; @@ -362,33 +368,46 @@ export default { this.$message.error(response.msg || "获取更多订单失败"); } }).catch(error => { - this.loading = false; - this.$message.error("获取更多订单失败: " + (error.message || error)); + + this.nextPage++; + this.currentPage++; + + // 更新进度条,即使加载失败也显示进度 + this.updateProgress(); + this.saveToCache(); + + // 如果还有更多页,继续尝试加载 + if (this.currentPage < this.totalPages) { + this.$message.warning(`加载第${this.currentPage}页失败,尝试继续加载后续页面`); + this.autoLoadNextPage(); + } else { + this.loading = false; + this.$message.error("获取更多订单失败: " + (error.message || error)); + } }); }, 300); }, - + /** 更新进度条 */ updateProgress() { let percentage; if (this.totalRecords > 0) { percentage = Math.min(Math.round((this.orderData.length / this.totalRecords) * 100), 99); - } else { + } else if (this.totalPages > 1) { percentage = Math.min(Math.round((this.currentPage / this.totalPages) * 100), 99); + } else { + percentage = this.currentPage > 0 ? 50 : 10; } - + this.loadProgress = { current: this.currentPage, total: this.totalPages, percentage: percentage }; }, + - /** 进度条格式化 */ - progressFormat(percentage) { - return `已加载 ${this.currentPage}/${this.totalPages} 页,${this.orderData.length}/${this.totalRecords} 条数据`; - }, - + /** 导出Excel */ handleExport() { if (!this.orderData.length) { @@ -397,7 +416,7 @@ export default { } this.$message.info("正在准备导出数据,请稍候..."); - + // 创建工作簿 const workbook = new ExcelJS.Workbook(); workbook.creator = 'RuoYi Admin'; @@ -428,9 +447,9 @@ export default { { header: '日本物流单号', key: 'internationalTrackingNumber', width: 18 }, { header: '地址状态', key: 'trackInfo', width: 25 } ]; - + worksheet.columns = columns; - + // 设置表头样式 worksheet.getRow(1).eachCell(cell => { cell.font = { bold: true }; @@ -439,15 +458,15 @@ export default { pattern: 'solid', fgColor: { argb: 'FFEEEEEE' } }; - cell.alignment = { - vertical: 'middle', + cell.alignment = { + vertical: 'middle', horizontal: 'center', wrapText: true }; }); - + worksheet.getRow(1).height = 22; - + // 添加数据行 this.orderData.forEach((item, index) => { const rowData = { @@ -469,20 +488,20 @@ export default { internationalTrackingNumber: item.internationalTrackingNumber || '', trackInfo: item.trackInfo || '暂无物流信息' }; - + const row = worksheet.addRow(rowData); row.height = 60; // 设置行高,给图片留出空间 - + // 设置所有单元格居中对齐 row.eachCell(cell => { - cell.alignment = { - vertical: 'middle', + cell.alignment = { + vertical: 'middle', horizontal: 'center', wrapText: true }; }); }); - + // 处理图片 const processImages = async () => { const loadImage = (url) => { @@ -491,32 +510,32 @@ export default { resolve(null); return; } - + // 检查是否是代理URL,如果不是,转换为代理URL let imageUrl = url; if (!url.includes('/tool/banma/image-proxy')) { imageUrl = process.env.VUE_APP_BASE_API + '/tool/banma/image-proxy?url=' + encodeURIComponent(url); } - + const img = new Image(); img.crossOrigin = "Anonymous"; - + img.onload = () => { try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); - + // 设置合适的尺寸 const size = 60; canvas.width = size; canvas.height = size; - + // 绘制图片保持比例 const scale = Math.min(size / img.width, size / img.height); const x = (size - img.width * scale) / 2; const y = (size - img.height * scale) / 2; ctx.drawImage(img, x, y, img.width * scale, img.height * scale); - + // 获取Base64 const base64 = canvas.toDataURL('image/png'); resolve(base64); @@ -525,12 +544,12 @@ export default { resolve(null); } }; - + img.onerror = () => resolve(null); img.src = imageUrl; }); }; - + for (let i = 0; i < this.orderData.length; i++) { const item = this.orderData[i]; if (item.productImage) { @@ -541,7 +560,7 @@ export default { base64: base64, extension: 'png', }); - + // 将图片添加到单元格 worksheet.addImage(imageId, { tl: { col: 1, row: i + 1 }, @@ -555,17 +574,17 @@ export default { } } }; - + // 处理图片并导出 processImages().then(() => { // 导出文件 - const fileName = this.dateRange && this.dateRange.length === 2 + const fileName = this.dateRange && this.dateRange.length === 2 ? `斑马订单数据_${this.dateRange[0]}_${this.dateRange[1]}.xlsx` : `斑马订单数据_${parseTime(new Date())}.xlsx`; - + workbook.xlsx.writeBuffer().then(buffer => { - const blob = new Blob([buffer], { - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); saveAs(blob, fileName); this.$message.success("导出成功"); @@ -575,11 +594,11 @@ export default { }); }); }, - + /** 备用导出方法(不含图片) */ fallbackExport() { this.$message.warning("图片导出失败,将导出不含图片的数据"); - + // 准备表头和数据 const headers = [ '下单时间', '商品名称', '乐天订单号', '下单距今时间', '乐天订单金额/日元', @@ -587,7 +606,7 @@ export default { '采购金额/rmb', '国际运费/rmb', '国内物流公司', '国内物流单号', '日本物流单号', '地址状态' ]; - + const data = this.orderData.map(item => [ item.orderedAt || '', item.productTitle || '', @@ -606,46 +625,46 @@ export default { item.internationalTrackingNumber || '', item.trackInfo || '暂无物流信息' ]); - + // 创建工作簿并导出 const wb = XLSX.utils.book_new(); const ws = XLSX.utils.aoa_to_sheet([headers, ...data]); - + // 设置列宽和样式 const colWidths = [15, 18, 18, 25, 15, 12, 10, 12, 15, 15, 12, 15, 15, 15, 12, 25]; ws['!cols'] = colWidths.map(width => ({ width })); - + // 设置行高 const rowHeights = [22]; // 表头高度 for (let i = 0; i < data.length; i++) { rowHeights.push(20); // 数据行高度 } ws['!rows'] = rowHeights.map(height => ({ hpt: height })); - + // 设置所有单元格居中对齐 const range = XLSX.utils.decode_range(ws['!ref']); for (let row = range.s.r; row <= range.e.r; row++) { for (let col = range.s.c; col <= range.e.c; col++) { const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); if (!ws[cellAddress]) continue; - + if (!ws[cellAddress].s) ws[cellAddress].s = {}; ws[cellAddress].s.alignment = { horizontal: 'center', vertical: 'center' }; - + if (row === 0) { ws[cellAddress].s.font = { bold: true }; ws[cellAddress].s.fill = { fgColor: { rgb: "EEEEEE" } }; } } } - + XLSX.utils.book_append_sheet(wb, ws, '订单数据'); - + // 导出文件 - const fileName = this.dateRange && this.dateRange.length === 2 + const fileName = this.dateRange && this.dateRange.length === 2 ? `斑马订单数据_${this.dateRange[0]}_${this.dateRange[1]}_无图片.xlsx` : `斑马订单数据_${parseTime(new Date())}_无图片.xlsx`; - + XLSX.writeFile(wb, fileName); this.$message.success("导出成功"); }, @@ -655,11 +674,11 @@ export default { */ handleImageError(event, item) { if (!item.productImage || item.imageProxied) return; - + item.imageProxied = true; - const proxyUrl = process.env.VUE_APP_BASE_API + '/tool/banma/image-proxy?url=' + + const proxyUrl = process.env.VUE_APP_BASE_API + '/tool/banma/image-proxy?url=' + encodeURIComponent(item.productImage); - + item.originalImage = item.productImage; item.productImage = proxyUrl; }, @@ -670,7 +689,7 @@ export default { request({ url: '/tool/banma/refresh-token', method: 'get', - timeout: 60000 + timeout: 99999999 }).then(response => { if (response.code === 200) { this.$message.success("Token刷新成功"); @@ -841,4 +860,4 @@ export default { .el-image { vertical-align: middle; } - + diff --git a/ruoyi-ui/src/views/prod/products/index.vue b/ruoyi-ui/src/views/prod/products/index.vue index 339a20c0..299d57ce 100644 --- a/ruoyi-ui/src/views/prod/products/index.vue +++ b/ruoyi-ui/src/views/prod/products/index.vue @@ -671,7 +671,7 @@ export default { url: '/tool/webmagic/batch', method: 'post', data: currentBatch, - timeout: 9000000 + timeout: 99999999 }).then(response => { if (response.code === 200) { this.productData = [...this.productData, ...response.data]; @@ -735,7 +735,7 @@ export default { url: '/tool/webmagic/batch', method: 'post', data: failedAsins, - timeout: 99999000 + timeout: 99999999 }).then(response => { if (response.code === 200 && response.data) { // 更新失败的ASIN数据