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