This commit is contained in:
ZiJIe 2025-07-15 10:20:26 +08:00
parent 90ecbd90cd
commit 1388d39376
5 changed files with 308 additions and 100 deletions

View File

@ -209,6 +209,7 @@ public class BanmaOrderController extends BaseController {
*/
@SuppressWarnings("unchecked")
private String getTrackingInfo(String trackingNumber) {
try {
R<Map<String, Object>> sagawaResult = sagawaExpressController.getTrackingInfo(trackingNumber);
if (sagawaResult != null && sagawaResult.getCode() == 200) {
Map<String, Object> sagawaData = sagawaResult.getData();
@ -223,23 +224,31 @@ public class BanmaOrderController extends BaseController {
}
}
}
try {
String url = String.format(TRACKING_URL, trackingNumber);
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
Map<String, Object> responseBody = response.getBody();
if (responseBody != null && Integer.valueOf(0).equals(responseBody.get("code"))) {
return Optional.ofNullable(responseBody.get("data"))
.map(data -> (List<Map<String, Object>>) data)
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.map(track -> (String) track.get("track"))
.orElse(null);
// 如果从佐川获取失败尝试从斑马API获取
try {
String url = String.format(TRACKING_URL, trackingNumber);
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
Map<String, Object> responseBody = response.getBody();
if (responseBody != null && Integer.valueOf(0).equals(responseBody.get("code"))) {
return Optional.ofNullable(responseBody.get("data"))
.map(data -> (List<Map<String, Object>>) data)
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.map(track -> (String) track.get("track"))
.orElse(null);
}
} catch (Exception e) {
logger.error("从斑马API获取物流信息失败: {}", e.getMessage());
// 继续处理不中断流程
}
} catch (Exception e) {
logger.error("获取物流信息失败: {}", e.getMessage());
// 继续处理不中断流程
}
return 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<String, Object> 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<R<Map<String, Object>>> deferredResult = new DeferredResult<>(120000L);
DeferredResult<R<Map<String, Object>>> deferredResult = new DeferredResult<>(999999999L);
Thread thread = new Thread(() -> {
try {
if (deferredResult.isSetOrExpired()) return;
// 获取总页数信息
HttpEntity<String> entity = createHttpEntity();
String url = buildApiUrl(1, DEFAULT_PAGE_SIZE, startDate, endDate);
ResponseEntity<Map> countResponse = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);
Map<String, Object> countResponseBody = countResponse.getBody();
int totalPages = 1;
if (countResponseBody != null && countResponseBody.containsKey("data")) {
Map<String, Object> dataMap = (Map<String, Object>) countResponseBody.get("data");
int totalCount = ((Number) dataMap.getOrDefault("total", 0)).intValue();
totalPages = (int) Math.ceil((double) totalCount / DEFAULT_PAGE_SIZE);
}
// 获取当前页数据
R<List<Map<String, Object>>> pageResult = fetchOrdersFromApi(page, DEFAULT_PAGE_SIZE, startDate, endDate);
if (pageResult.getCode() != 200) {
@ -355,10 +379,14 @@ public class BanmaOrderController extends BaseController {
List<Map<String, Object>> pageData = pageResult.getData();
// 修改hasMore判断逻辑根据当前页数和总页数判断
boolean hasMore = page < totalPages;
Map<String, Object> 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;
}

View File

@ -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<String, Object> 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 = "<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);
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<Map<String, Object>> 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<String, String> 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<String, Object> errorResult = new HashMap<>();
errorResult.put("status", "error");
errorResult.put("message", "查询物流信息失败: " + e.getMessage());
return R.ok(errorResult);
}
}
}

View File

@ -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
// 是否需要防止数据重复提交

View File

@ -6,7 +6,7 @@
<span class="card-title">斑马订单数据</span>
</div>
</template>
<!-- 时间范围选择器和操作按钮区域 -->
<div class="filter-container">
<el-form :inline="true" class="filter-form">
@ -62,7 +62,7 @@
</el-form-item>
</el-form>
</div>
<!-- 数据统计信息 -->
<el-alert
v-if="orderData.length > 0"
@ -77,30 +77,28 @@
</div>
</template>
</el-alert>
<!-- 加载进度条 -->
<el-row v-if="loading">
<el-col :span="24">
<div class="progress-container">
<el-progress
:percentage="loadProgress.percentage"
:format="progressFormat"
status="primary"
<el-progress
:percentage="loadProgress.percentage"
:stroke-width="12"
class="load-progress">
</el-progress>
</div>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="orderData" border style="width: 100%">
<el-table-column label="下单时间" align="center" prop="orderedAt" min-width="140" header-align="center" />
<el-table-column label="商品图片" align="center" prop="productImage" min-width="90" header-align="center">
<template #default="scope">
<el-image
v-if="scope.row.productImage"
:src="scope.row.productImage"
<el-image
v-if="scope.row.productImage"
:src="scope.row.productImage"
style="width: 60px; height: 60px; object-fit: contain;"
:preview-src-list="[scope.row.productImage]"
@error="handleImageError($event, scope.row)">
@ -147,7 +145,7 @@
</template>
</el-table-column>
</el-table>
<!-- 无更多数据提示 -->
<div v-if="orderData.length > 0 && !hasMore && !loading" class="no-more-data">
已加载全部数据
@ -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}`);
// hasMorefalse
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;
}
// URLURL
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;
}
</style>
</style>

View File

@ -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