12 changed files with 355 additions and 53 deletions
@ -0,0 +1,31 @@ |
|||||
|
package com.project.logistics.config; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
@Component |
||||
|
@ConfigurationProperties(prefix = "logistics.sample-scanner") |
||||
|
public class LogisticsSampleScannerProperties { |
||||
|
private boolean enabled = true; |
||||
|
|
||||
|
private String goLiveTime = "2026-04-01"; |
||||
|
/** |
||||
|
* 默认每 10 分钟唤醒一次检查(10分钟唤醒一次开销极低,且能满足20分钟及以上的间隔) |
||||
|
*/ |
||||
|
private String cron = "0 0/10 * * * ?"; |
||||
|
|
||||
|
private List<LogisticsScannerProperties.ScanWindow> windows = new ArrayList<>(); |
||||
|
|
||||
|
@Data |
||||
|
public static class ScanWindow { |
||||
|
private String name; // 描述:如 "上午班次"
|
||||
|
private String startTime; // "11:00"
|
||||
|
private String endTime; // "12:00"
|
||||
|
private int intervalMinutes; // 最小执行间隔,如 20
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
package com.project.logistics.domain.enums; |
||||
|
|
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Getter; |
||||
|
|
||||
|
@Getter |
||||
|
@AllArgsConstructor |
||||
|
public enum SampleOrderInfoFieldEnum { |
||||
|
orderNo("DocNo" , "出货单号") , |
||||
|
actualSendTime("DescFlexField_PubDescSeg19" , "实际发货时间"), |
||||
|
salesman("DescFlexField_PrivateDescSeg7" , "CRM业务员") , |
||||
|
transportMethod("DescFlexField_PrivateDescSeg11" , "运输方式及货场名称") , |
||||
|
recipientContact("DescFlexField_PubDescSeg15" , "收货人") , |
||||
|
recipientMobile("DescFlexField_PubDescSeg16" , "收货人联系电话") , |
||||
|
recipientAddress("DescFlexField_PubDescSeg14" , "收货地址") , |
||||
|
payer("DescFlexField_PubDescSeg20" , "运费承担") , |
||||
|
waybillNo("DescFlexField_PrivateDescSeg20" , "货运单号") , |
||||
|
expressType("DescFlexField_PubDescSeg25" , "寄付方式") , |
||||
|
fee("DescFlexField_PrivateDescSeg21" , "运费") , |
||||
|
quantity("DescFlexField_PrivateDescSeg13" , "件数") , |
||||
|
|
||||
|
; |
||||
|
|
||||
|
private final String fieldName; |
||||
|
|
||||
|
private final String desc; |
||||
|
} |
||||
@ -0,0 +1,160 @@ |
|||||
|
package com.project.logistics.domain.scheduler; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import cn.hutool.json.JSONObject; |
||||
|
import com.project.logistics.config.LogisticsScannerProperties; |
||||
|
import com.project.logistics.config.LogisticsSampleScannerProperties; // 注入新配置类
|
||||
|
import com.project.logistics.domain.entity.ApiRetryTaskEntity; |
||||
|
import com.project.logistics.domain.entity.LogisticsOrderEntity; |
||||
|
import com.project.logistics.domain.enums.*; |
||||
|
import com.project.logistics.domain.service.base.ApiRetryTaskService; |
||||
|
import com.project.logistics.domain.service.base.ErpService; |
||||
|
import com.project.logistics.domain.service.base.LogisticsOrderService; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.data.redis.core.StringRedisTemplate; |
||||
|
import org.springframework.scheduling.annotation.Scheduled; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.springframework.transaction.annotation.Transactional; |
||||
|
|
||||
|
import java.time.LocalTime; |
||||
|
import java.time.format.DateTimeFormatter; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Component |
||||
|
public class SampleOrderScannerJob { |
||||
|
|
||||
|
@Autowired |
||||
|
private LogisticsSampleScannerProperties sampleProperties; // 使用样品单专有配置
|
||||
|
|
||||
|
@Autowired |
||||
|
private StringRedisTemplate redisTemplate; |
||||
|
|
||||
|
// 独立的 Redis 控制 key
|
||||
|
private static final String REDIS_LAST_SUCCESS_KEY = "auto_logistics:sample_scanner:last_success_ts"; |
||||
|
|
||||
|
@Autowired |
||||
|
private LogisticsOrderService logisticsOrderService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ApiRetryTaskService apiRetryTaskService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ErpService erpService; |
||||
|
|
||||
|
/** |
||||
|
* 读取样品单专有的 Cron 配置进行唤醒 |
||||
|
*/ |
||||
|
@Scheduled(cron = "${logistics.sample-scanner.cron:0 0/10 * * * ?}") |
||||
|
public void execute() { |
||||
|
if (!sampleProperties.isEnabled()) return; |
||||
|
|
||||
|
LocalTime now = LocalTime.now(); |
||||
|
// 使用样品单专有的时间窗口配置
|
||||
|
LogisticsScannerProperties.ScanWindow currentWindow = findMatchedWindow(now); |
||||
|
|
||||
|
if (currentWindow == null) { |
||||
|
log.debug("当前时刻 {} 不在样品单业务窗口内", now); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (checkInterval(currentWindow)) { |
||||
|
log.info(">>> 命中样品单业务窗口 [{}], 开始扫描 U9 待下单数据...", currentWindow.getName()); |
||||
|
|
||||
|
boolean success = doScanWork(); |
||||
|
|
||||
|
if (success) { |
||||
|
redisTemplate.opsForValue().set(REDIS_LAST_SUCCESS_KEY, |
||||
|
String.valueOf(System.currentTimeMillis()), 24, TimeUnit.HOURS); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 核心扫库逻辑 |
||||
|
*/ |
||||
|
public boolean doScanWork() { |
||||
|
try { |
||||
|
// 1. 调用 ErpService 查询符合“样品单”条件的数据
|
||||
|
List<JSONObject> sampleOrders = erpService.listPendingSampleOrders(); |
||||
|
|
||||
|
if (CollUtil.isEmpty(sampleOrders)) { |
||||
|
log.info("U9 样品单库中暂无待处理数据"); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
log.info("发现 {} 个样品单需要同步下单...", sampleOrders.size()); |
||||
|
int successCount = 0; |
||||
|
|
||||
|
for (JSONObject u9Data : sampleOrders) { |
||||
|
String orderNo = u9Data.getStr("orderNo"); |
||||
|
try { |
||||
|
if (processSampleImport(orderNo, u9Data)) { |
||||
|
successCount++; |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.error("样品单 [{}] 导入失败: {}", orderNo, e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
log.info(">>> 样品单同步结束。总共获取 {} 条,成功启动自动化流程 {} 条", sampleOrders.size(), successCount); |
||||
|
return true; |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("样品单扫库任务异常", e); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Transactional(rollbackFor = Exception.class) |
||||
|
public boolean processSampleImport(String orderNo, JSONObject u9Data) throws Exception { |
||||
|
if (logisticsOrderService.existsByOrderNo(orderNo)) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// 持久化订单:跳过上传资源步骤,直接设置为 RESOURCE_CREATED
|
||||
|
LogisticsOrderEntity order = new LogisticsOrderEntity(); |
||||
|
order.setOrderNo(orderNo); |
||||
|
order.setOrderType(OrderTypeEnum.SAMPLE.getCode()); |
||||
|
order.setOrderStatus(OrderStatusEnum.RESOURCE_CREATED.getCode()); |
||||
|
order.setOrderInfo(u9Data.toString()); |
||||
|
order.setOriginalPdfPath(null); |
||||
|
logisticsOrderService.save(order); |
||||
|
|
||||
|
// 创建异步下单任务
|
||||
|
ApiRetryTaskEntity task = new ApiRetryTaskEntity(); |
||||
|
task.setOrderNo(orderNo); |
||||
|
task.setActionCode(RetryActionEnum.SF_CREATE_ORDER.getCode()); |
||||
|
task.setTaskStatus(TaskStatusEnum.PENDING.getCode()); |
||||
|
task.setRetryCount(0); |
||||
|
task.setMaxRetries(5); |
||||
|
task.setNextExecuteTime(new Date()); |
||||
|
apiRetryTaskService.save(task); |
||||
|
|
||||
|
log.info(">>> 样品单 {} 导入成功,已开启下单链条", orderNo); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// --- 私有工具方法 ---
|
||||
|
|
||||
|
private boolean checkInterval(LogisticsScannerProperties.ScanWindow window) { |
||||
|
String lastTs = redisTemplate.opsForValue().get(REDIS_LAST_SUCCESS_KEY); |
||||
|
if (lastTs == null) return true; |
||||
|
long diffMinutes = (System.currentTimeMillis() - Long.parseLong(lastTs)) / (1000 * 60); |
||||
|
return diffMinutes >= window.getIntervalMinutes(); |
||||
|
} |
||||
|
|
||||
|
private LogisticsScannerProperties.ScanWindow findMatchedWindow(LocalTime now) { |
||||
|
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm"); |
||||
|
// 关键:遍历的是样品单专有的 windows 集合
|
||||
|
for (LogisticsScannerProperties.ScanWindow window : sampleProperties.getWindows()) { |
||||
|
LocalTime start = LocalTime.parse(window.getStartTime(), dtf); |
||||
|
LocalTime end = LocalTime.parse(window.getEndTime(), dtf); |
||||
|
if (!now.isBefore(start) && !now.isAfter(end)) return window; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue