瀏覽代碼

订单查询修改

yuhongqi 3 周之前
父節點
當前提交
89d497457b

+ 22 - 11
fs-admin/src/main/java/com/fs/task/LiveTask.java

@@ -8,9 +8,10 @@ import com.fs.erp.dto.ErpOrderQueryRequert;
 import com.fs.erp.dto.ErpOrderQueryResponse;
 import com.fs.erp.service.FsJstAftersalePushService;
 import com.fs.erp.service.impl.JSTErpOrderServiceImpl;
+import com.fs.erp.utils.JstApiExceptionUtils;
+import com.fs.erp.service.jst.JstLiveDeliverySyncService;
 import com.fs.express.FsStoreDeliversService;
 import com.fs.common.utils.StringUtils;
-import com.google.common.collect.Lists;
 import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
 import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
 import com.fs.live.domain.LiveOrderPayment;
@@ -26,13 +27,15 @@ import com.fs.live.domain.LiveOrder;
 import com.fs.live.service.*;
 import com.fs.store.domain.FsExpress;
 import com.fs.store.enums.OrderLogEnum;
-import com.fs.store.mapper.*;
 import com.fs.store.param.LiveAfterSalesAudit1Param;
-import com.fs.store.service.*;
+import com.fs.store.service.IFsExpressService;
+import com.fs.store.service.IFsUserService;
+import com.google.common.collect.Lists;
 import org.apache.commons.collections4.CollectionUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
@@ -52,10 +55,6 @@ import static com.fs.live.utils.redis.RedisBatchHandler.CONSUME_INTERVAL;
 public class LiveTask {
     Logger logger = LoggerFactory.getLogger(LiveTask.class);
 
-    /** 聚水潭发货查询每批 o_id 数量 */
-    private static final int DELIVERY_QUERY_BATCH_SIZE = 30;
-    private static final String JST_SHOP_ID = "18675245";
-
     @Autowired
     private ILiveOrderService liveOrderService;
     @Autowired
@@ -92,6 +91,7 @@ public class LiveTask {
     private static final long APP_USER_SYNC_TASK_LOCK_SECONDS = 600L;
     /** 两个 APP 用户同步任务错峰启动间隔(秒) */
     private static final long APP_USER_SYNC_TASK_STAGGER_SECONDS = 15L;
+    private static final int DELIVERY_QUERY_BATCH_SIZE = 30;
 
     // 订单银行回调数据丢失补偿
     public void recoveryBankOrder() {
@@ -178,6 +178,9 @@ public class LiveTask {
         }
     }
 
+    @Value("${jst.shop_code}")
+    private String JST_SHOP_ID;
+
     private void jstErpOrderDelivery(List<Long> oIds) throws InterruptedException {
         if (CollectionUtils.isEmpty(oIds)) {
             return;
@@ -194,7 +197,7 @@ public class LiveTask {
             do {
                 Thread.sleep(300);
                 ErpOrderQueryRequert request = new ErpOrderQueryRequert();
-                request.setPage_size(100);
+                request.setPage_size(DELIVERY_QUERY_BATCH_SIZE);
                 request.setPage_index(pageIndex);
                 request.setShop_id(JST_SHOP_ID);
                 request.setO_ids(batch);
@@ -322,10 +325,18 @@ public class LiveTask {
     public void updateOrderItem() throws ParseException {
         List<Long> ids = liveOrderService.selectOrderIdByNoErp();
         for (Long id : ids) {
-            try{
+            try {
                 liveOrderService.createOmsOrder(id);
-            }catch (Exception e){
-                logger.error("推送管易失败 {}",id,e);
+            } catch (Exception e) {
+                logger.error("推送管易失败 {}", id, e);
+                if (JstApiExceptionUtils.isRateLimitExceeded(e)) {
+                    logger.warn("聚水潭调用推送频次超限,终止本批次推单");
+                    break;
+                }
+                if (JstApiExceptionUtils.isErrorToken(e)) {
+                    logger.warn("聚水潭调用推送无效token,终止本批次推单");
+                    break;
+                }
             }
         }
     }

+ 27 - 11
fs-admin/src/main/java/com/fs/task/StoreTask.java

@@ -7,6 +7,8 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.erp.service.impl.JSTErpOrderServiceImpl;
+import com.fs.erp.utils.JstApiExceptionUtils;
+import com.fs.erp.service.jst.JstStoreDeliverySyncService;
 import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
 import com.fs.huifuPay.dto.*;
 import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
@@ -50,6 +52,7 @@ import org.apache.http.util.Asserts;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
@@ -72,7 +75,6 @@ public class StoreTask {
 
     /** 聚水潭发货查询每批 o_id 数量 */
     private static final int DELIVERY_QUERY_BATCH_SIZE = 30;
-    private static final String JST_SHOP_ID = "18675245";
 
     @Autowired
     private RedisTemplate redisTemplate;
@@ -254,30 +256,35 @@ public class StoreTask {
     }
 
     @Autowired
-    private JSTErpOrderServiceImpl jstErpOrderService;
+    private JstStoreDeliverySyncService jstStoreDeliverySyncService;
+
+    @Value("${jst.shop_code}")
+    private String JST_SHOP_ID;
 
+    @Autowired
+    private JSTErpOrderServiceImpl jstErpOrderService;
 
     private void jstErpOrderDelivery(List<Long> o_ids) throws InterruptedException {
         if (org.springframework.util.CollectionUtils.isEmpty(o_ids)) return;
 
         List<List<Long>> batches = Lists.partition(o_ids, DELIVERY_QUERY_BATCH_SIZE);
+        List<String> extendIds = new ArrayList<>();
         for (List<Long> batch : batches) {
-            List<String> extendIds = new ArrayList<>();
-            for (Long oId : batch) {
-                extendIds.add(oId.toString());
-            }
             int pageIndex = 1;
             boolean has_next = false;
             do {
                 Thread.sleep(300);
                 ErpOrderQueryRequert request = new ErpOrderQueryRequert();
-                request.setPage_size(100);
+                request.setPage_size(DELIVERY_QUERY_BATCH_SIZE);
                 request.setPage_index(pageIndex);
                 request.setShop_id(JST_SHOP_ID);
-                request.setO_ids(batch);
+                request.setO_ids(batch); // 设置当前批次
 
                 ErpOrderQueryResponse response = jstErpOrderService.getOrder(request);
                 has_next = response.hasNextSafe();
+                for (Long l : batch) {
+                    extendIds.add(l.toString());
+                }
                 orderService.updateUpdateTimeByExtendIds(extendIds);
 
                 if (response.getOrders() != null) {
@@ -313,6 +320,7 @@ public class StoreTask {
             } while (has_next);
         }
     }
+
     /**
      * 发货任务
      */
@@ -606,10 +614,18 @@ public class StoreTask {
     public void updateOrderItem() throws ParseException {
         List<Long> ids = itemMapper.selectOrderIdByNoErp();
         for (Long id : ids) {
-            try{
+            try {
                 fsStoreOrderService.createOmsOrder(id);
-            }catch (Exception e){
-                logger.error("推送管易失败 {}",id,e);
+            } catch (Exception e) {
+                logger.error("推送管易失败 {}", id, e);
+                if (JstApiExceptionUtils.isRateLimitExceeded(e)) {
+                    logger.warn("聚水潭调用推送频次超限,终止本批次推单");
+                    break;
+                }
+                if (JstApiExceptionUtils.isErrorToken(e)) {
+                    logger.warn("聚水潭调用推送无效token,终止本批次推单");
+                    break;
+                }
             }
         }
     }

+ 10 - 4
fs-common/src/main/java/com/fs/common/utils/PhoneUtils.java

@@ -210,8 +210,8 @@ public class PhoneUtils {
             return phone;
         }
         if (isMaskedPhone(phone)) {
-            log.warn("ERP上传遇到脱敏手机号: {}", phone);
-            return phone;
+            log.warn("ERP上传遇到脱敏手机号,需业务侧先解析: {}", phone);
+            return null;
         }
         String plain = tryDecryptToPlain(phone);
         return StrUtil.isNotBlank(plain) ? plain : phone;
@@ -222,10 +222,16 @@ public class PhoneUtils {
      */
     public static String resolveErpOrderReceiverPhone(String receiverMobile, String receiverPhone) {
         if (StrUtil.isNotBlank(receiverMobile)) {
-            return resolvePhoneForErp(receiverMobile);
+            String resolved = resolvePhoneForErp(receiverMobile);
+            if (StrUtil.isNotBlank(resolved)) {
+                return resolved;
+            }
         }
         if (StrUtil.isNotBlank(receiverPhone)) {
-            return resolvePhoneForErp(receiverPhone);
+            String resolved = resolvePhoneForErp(receiverPhone);
+            if (StrUtil.isNotBlank(resolved)) {
+                return resolved;
+            }
         }
         return "";
     }

+ 68 - 0
fs-live-socket/src/main/resources/logback-spring.xml

@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds">
+
+    <!-- 日志目录,可通过 logging.file.path 覆盖 -->
+    <springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="/home/fs-live-socket/logs"/>
+    <springProperty scope="context" name="MAX_FILE_SIZE" source="logging.logback.rollingpolicy.max-file-size" defaultValue="200MB"/>
+    <springProperty scope="context" name="MAX_HISTORY" source="logging.logback.rollingpolicy.max-history" defaultValue="15"/>
+    <springProperty scope="context" name="TOTAL_SIZE_CAP" source="logging.logback.rollingpolicy.total-size-cap" defaultValue="10GB"/>
+
+    <property name="APP_NAME" value="fs-live-socket"/>
+    <property name="LOG_CHARSET" value="UTF-8"/>
+    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
+
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>${LOG_PATTERN}</pattern>
+            <charset>${LOG_CHARSET}</charset>
+            <immediateFlush>true</immediateFlush>
+        </encoder>
+    </appender>
+
+    <!-- 按日期+大小切割,归档 gzip 压缩,超过 15 天自动删除 -->
+    <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_PATH}/${APP_NAME}.log</file>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>${LOG_PATTERN}</pattern>
+            <charset>${LOG_CHARSET}</charset>
+            <immediateFlush>true</immediateFlush>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_PATH}/archive/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
+            <cleanHistoryOnStart>false</cleanHistoryOnStart>
+        </rollingPolicy>
+    </appender>
+
+    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_PATH}/${APP_NAME}-error.log</file>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>${LOG_PATTERN}</pattern>
+            <charset>${LOG_CHARSET}</charset>
+            <immediateFlush>true</immediateFlush>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_PATH}/archive/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
+            <cleanHistoryOnStart>false</cleanHistoryOnStart>
+        </rollingPolicy>
+    </appender>
+
+    <root level="INFO">
+        <appender-ref ref="CONSOLE"/>
+        <appender-ref ref="FILE_ALL"/>
+        <appender-ref ref="FILE_ERROR"/>
+    </root>
+
+    <logger name="org.springframework" level="WARN"/>
+
+</configuration>

+ 66 - 0
fs-service-system/src/main/java/com/fs/erp/service/jst/JstDeliveryDiagnoseResult.java

@@ -0,0 +1,66 @@
+package com.fs.erp.service.jst;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 聚水潭发货回传诊断结果(只读排查,默认不写库)
+ */
+@Data
+@Accessors(chain = true)
+public class JstDeliveryDiagnoseResult {
+
+    private String inputOrderNo;
+    private String orderType;
+    private String orderCode;
+    private String extendOrderId;
+    private Integer localStatus;
+    private String localStatusDesc;
+    private Date createTime;
+    private String deliverySn;
+    private String deliveryName;
+
+    /** 是否满足 deliveryOp 待同步队列条件 */
+    private Boolean inPendingQueue;
+    private List<String> localBlockers = new ArrayList<>();
+
+    /** 配置中的当前店铺(StoreTask.deliveryOp 默认只用此 shop) */
+    private String configuredShopId;
+    private List<String> queriedShopIds = new ArrayList<>();
+
+    private List<JstShopDiagnoseItem> shopResults = new ArrayList<>();
+
+    /** 综合结论 */
+    private String conclusion;
+    private String suggestion;
+
+    /** trySync=true 时附带实际回传结果 */
+    private JstDeliverySyncReportItem syncAttempt;
+
+    @Data
+    @Accessors(chain = true)
+    public static class JstShopDiagnoseItem {
+        private String shopId;
+        private Boolean jstOrderFound;
+        private String jstOrderCode;
+        private Integer jstDeliveryState;
+        private List<JstDeliveryLineItem> deliveryLines = new ArrayList<>();
+        private String shopConclusion;
+    }
+
+    @Data
+    @Accessors(chain = true)
+    public static class JstDeliveryLineItem {
+        private Boolean delivery;
+        private String mailNo;
+        private String expressCode;
+        private String expressName;
+        private Boolean expressExists;
+        private String blockReason;
+        private Boolean wouldSync;
+    }
+}

+ 432 - 0
fs-service-system/src/main/java/com/fs/erp/service/jst/JstDeliveryDiagnoseService.java

@@ -0,0 +1,432 @@
+package com.fs.erp.service.jst;
+
+import com.fs.common.utils.StringUtils;
+import com.fs.erp.domain.ErpDeliverys;
+import com.fs.erp.domain.ErpOrderQuery;
+import com.fs.erp.dto.ErpOrderQueryRequert;
+import com.fs.erp.dto.ErpOrderQueryResponse;
+import com.fs.erp.service.impl.JSTErpOrderServiceImpl;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.service.ILiveOrderService;
+import com.fs.store.domain.FsExpress;
+import com.fs.store.domain.FsStoreOrder;
+import com.fs.store.enums.OrderInfoEnum;
+import com.fs.store.service.IFsExpressService;
+import com.fs.store.service.IFsStoreOrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 诊断订单为何未聚水潭发货回传(参考 StoreTask.deliveryOp / LiveTask.deliveryOp 查询与回传条件)
+ */
+@Service
+public class JstDeliveryDiagnoseService {
+
+    private static final Date ORDER_CUTOFF_DATE;
+
+    static {
+        try {
+            ORDER_CUTOFF_DATE = new SimpleDateFormat("yyyyMMdd").parse("20260626");
+        } catch (ParseException e) {
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    @Value("${jst.shop_code}")
+    private String currentShopId;
+
+    @Autowired
+    private IFsStoreOrderService storeOrderService;
+    @Autowired
+    private ILiveOrderService liveOrderService;
+    @Autowired
+    private JSTErpOrderServiceImpl jstErpOrderService;
+    @Autowired
+    private IFsExpressService expressService;
+    @Autowired
+    private JstStoreDeliverySyncService jstStoreDeliverySyncService;
+    @Autowired
+    private JstLiveDeliverySyncService jstLiveDeliverySyncService;
+
+    public JstDeliveryDiagnoseResult diagnose(String orderNo, String shopCode, boolean trySync)
+            throws InterruptedException {
+        JstDeliveryDiagnoseResult result = new JstDeliveryDiagnoseResult()
+                .setInputOrderNo(orderNo)
+                .setConfiguredShopId(currentShopId);
+
+        ResolvedOrder resolved = resolveOrder(orderNo);
+        if (resolved.storeOrder != null) {
+            fillStoreLocal(result, resolved.storeOrder);
+        } else if (resolved.liveOrder != null) {
+            fillLiveLocal(result, resolved.liveOrder);
+        } else {
+            result.setOrderType("NOT_FOUND")
+                    .setConclusion("本地未找到商城/直播订单")
+                    .setSuggestion("请确认 orderNo 为 order_code、extend_order_id 或订单主键");
+            return result;
+        }
+
+        List<String> shopIds = resolveShopIds(shopCode, result.getCreateTime());
+        result.setQueriedShopIds(shopIds);
+
+        Long oId = parseExtendOId(result.getExtendOrderId());
+        if (oId == null) {
+            result.setConclusion("本地订单缺少 extend_order_id,deliveryOp 不会拉取聚水潭发货")
+                    .setSuggestion("先确认订单是否已成功推送到聚水潭并写入 extend_order_id");
+            return result;
+        }
+
+        for (String shopId : shopIds) {
+            result.getShopResults().add(queryJstForShop(result, shopId, oId));
+        }
+
+        buildConclusion(result);
+
+        if (trySync) {
+            result.setSyncAttempt(attemptSync(resolved, result.getQueriedShopIds()));
+        }
+        return result;
+    }
+
+    private JstDeliveryDiagnoseResult.JstShopDiagnoseItem queryJstForShop(JstDeliveryDiagnoseResult result,
+                                                                            String shopId, Long oId) {
+        JstDeliveryDiagnoseResult.JstShopDiagnoseItem shopItem = new JstDeliveryDiagnoseResult.JstShopDiagnoseItem()
+                .setShopId(shopId)
+                .setJstOrderFound(false);
+
+        ErpOrderQueryRequert request = new ErpOrderQueryRequert();
+        request.setPage_size(10);
+        request.setPage_index(1);
+        request.setShop_id(shopId);
+        request.setO_ids(Collections.singletonList(oId));
+
+        ErpOrderQueryResponse response = "LIVE".equals(result.getOrderType())
+                ? jstErpOrderService.getOrderLive(request)
+                : jstErpOrderService.getOrder(request);
+
+        if (response == null || CollectionUtils.isEmpty(response.getOrders())) {
+            shopItem.setShopConclusion("该店铺未查到聚水潭订单(o_id=" + oId + ")");
+            return shopItem;
+        }
+
+        ErpOrderQuery orderQuery = response.getOrders().get(0);
+        shopItem.setJstOrderFound(true)
+                .setJstOrderCode(orderQuery.getCode())
+                .setJstDeliveryState(orderQuery.getDelivery_state());
+
+        if (CollectionUtils.isEmpty(orderQuery.getDeliverys())) {
+            shopItem.setShopConclusion("聚水潭有订单但未返回物流明细(可能尚未发货)");
+            return shopItem;
+        }
+
+        boolean anyWouldSync = false;
+        List<String> blocks = new ArrayList<>();
+        for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
+            JstDeliveryDiagnoseResult.JstDeliveryLineItem line = analyzeDeliveryLine(result, delivery);
+            shopItem.getDeliveryLines().add(line);
+            if (Boolean.TRUE.equals(line.getWouldSync())) {
+                anyWouldSync = true;
+            } else if (StringUtils.isNotEmpty(line.getBlockReason())) {
+                blocks.add(line.getBlockReason());
+            }
+        }
+
+        if (anyWouldSync) {
+            shopItem.setShopConclusion("聚水潭物流满足 deliveryOp 回传条件");
+        } else if (!blocks.isEmpty()) {
+            shopItem.setShopConclusion(String.join(";", blocks));
+        } else {
+            shopItem.setShopConclusion("聚水潭有物流但未满足回传条件");
+        }
+        return shopItem;
+    }
+
+    private JstDeliveryDiagnoseResult.JstDeliveryLineItem analyzeDeliveryLine(JstDeliveryDiagnoseResult result,
+                                                                                ErpDeliverys delivery) {
+        JstDeliveryDiagnoseResult.JstDeliveryLineItem line = new JstDeliveryDiagnoseResult.JstDeliveryLineItem()
+                .setDelivery(delivery.getDelivery())
+                .setMailNo(delivery.getMail_no())
+                .setExpressCode(delivery.getExpress_code())
+                .setExpressName(delivery.getExpress_name());
+
+        if (!Boolean.TRUE.equals(delivery.getDelivery()) || StringUtils.isEmpty(delivery.getMail_no())) {
+            line.setWouldSync(false)
+                    .setBlockReason("delivery=false 或 mail_no 为空");
+            return line;
+        }
+
+        FsExpress express = expressService.selectFsExpressByOmsCode(delivery.getExpress_code());
+        line.setExpressExists(express != null);
+        if (express == null) {
+            line.setWouldSync(false)
+                    .setBlockReason("快递公司 oms_code=" + delivery.getExpress_code() + " 在 fs_express 不存在");
+            return line;
+        }
+
+        if ("STORE".equals(result.getOrderType())) {
+            if (!Objects.equals(result.getLocalStatus(), OrderInfoEnum.STATUS_1.getValue())
+                    && !Objects.equals(result.getLocalStatus(), OrderInfoEnum.STATUS_2.getValue())) {
+                line.setWouldSync(false)
+                        .setBlockReason("本地商城订单 status=" + result.getLocalStatus() + ",非待发货/待收货");
+                return line;
+            }
+            if (Objects.equals(result.getLocalStatus(), OrderInfoEnum.STATUS_2.getValue())) {
+                line.setWouldSync(true)
+                        .setBlockReason("本地已是待收货,deliveryOp 视为已同步");
+                return line;
+            }
+        } else if ("LIVE".equals(result.getOrderType())) {
+            if (!Objects.equals(result.getLocalStatus(), 2) && !Objects.equals(result.getLocalStatus(), 3)) {
+                line.setWouldSync(false)
+                        .setBlockReason("本地直播订单 status=" + result.getLocalStatus() + ",非待发货(2)/待收货(3)");
+                return line;
+            }
+            if (Objects.equals(result.getLocalStatus(), 3)) {
+                line.setWouldSync(true)
+                        .setBlockReason("本地已是待收货,deliveryOp 视为已同步");
+                return line;
+            }
+        }
+
+        if (Boolean.FALSE.equals(result.getInPendingQueue())) {
+            line.setWouldSync(false)
+                    .setBlockReason("不在 deliveryOp 待同步队列,定时任务不会扫描此单");
+            return line;
+        }
+
+        line.setWouldSync(true);
+        return line;
+    }
+
+    private void buildConclusion(JstDeliveryDiagnoseResult result) {
+        if (!Boolean.TRUE.equals(result.getInPendingQueue()) && !result.getLocalBlockers().isEmpty()) {
+            result.setConclusion("本地状态不满足 deliveryOp 扫描条件:" + String.join(";", result.getLocalBlockers()));
+        }
+
+        JstDeliveryDiagnoseResult.JstShopDiagnoseItem matched = null;
+        for (JstDeliveryDiagnoseResult.JstShopDiagnoseItem shop : result.getShopResults()) {
+            if (Boolean.TRUE.equals(shop.getJstOrderFound())) {
+                for (JstDeliveryDiagnoseResult.JstDeliveryLineItem line : shop.getDeliveryLines()) {
+                    if (Boolean.TRUE.equals(line.getWouldSync())) {
+                        matched = shop;
+                        break;
+                    }
+                }
+            }
+            if (matched != null) {
+                break;
+            }
+        }
+
+        if (matched != null) {
+            if (Boolean.FALSE.equals(result.getInPendingQueue())) {
+                result.setSuggestion("聚水潭在 shopId=" + matched.getShopId()
+                        + " 已有可回传物流,但本地不在待同步队列,请修正 status/extend_order_id 后手动回传或等任务扫描");
+            } else if (!Objects.equals(matched.getShopId(), currentShopId)) {
+                result.setConclusion("聚水潭在店铺 " + matched.getShopId() + " 可回传,但 StoreTask.deliveryOp 默认只查 "
+                        + currentShopId);
+                result.setSuggestion("对 2026-06-26 之前订单需查历史店铺 " + JstStoreDeliverySyncService.LEGACY_JST_SHOP_ID
+                        + ",或指定 shopCode 调用本接口 trySync=true");
+            } else {
+                result.setConclusion("聚水潭物流满足回传条件,若仍未回传可能是定时任务未跑到或回传异常,可 trySync=true 重试");
+                result.setSuggestion("GET .../diagnoseJstDelivery?orderNo=" + result.getInputOrderNo() + "&trySync=true");
+            }
+            return;
+        }
+
+        if (StringUtils.isEmpty(result.getConclusion())) {
+            boolean anyFound = result.getShopResults().stream().anyMatch(s -> Boolean.TRUE.equals(s.getJstOrderFound()));
+            if (!anyFound) {
+                result.setConclusion("所有查询店铺均未查到聚水潭订单 o_id=" + result.getExtendOrderId());
+                result.setSuggestion("确认订单推单店铺与 shopCode 是否一致;2026-06-26 前订单可试 shopCode="
+                        + JstStoreDeliverySyncService.LEGACY_JST_SHOP_ID);
+            } else {
+                result.setConclusion("聚水潭有订单但物流未满足 deliveryOp 回传条件");
+                result.setSuggestion("查看 shopResults.deliveryLines.blockReason;常见:未发货、mail_no 为空、快递公司未配置");
+            }
+        }
+    }
+
+    private JstDeliverySyncReportItem attemptSync(ResolvedOrder resolved, List<String> shopIds)
+            throws InterruptedException {
+        if (CollectionUtils.isEmpty(shopIds)) {
+            shopIds = Collections.singletonList(currentShopId);
+        }
+        for (String shopId : shopIds) {
+            JstDeliverySyncReportItem report;
+            if (resolved.storeOrder != null) {
+                List<JstDeliverySyncReportItem> reports = jstStoreDeliverySyncService.syncOrdersWithShopReport(
+                        Collections.singletonList(resolved.storeOrder), shopId, null);
+                report = reports.isEmpty() ? null : reports.get(0);
+            } else if (resolved.liveOrder != null) {
+                List<JstDeliverySyncReportItem> reports = jstLiveDeliverySyncService.syncOrdersWithShopReport(
+                        Collections.singletonList(resolved.liveOrder), shopId, null);
+                report = reports.isEmpty() ? null : reports.get(0);
+            } else {
+                return null;
+            }
+            if (report != null && JstDeliverySyncReportItem.RESULT_SUCCESS.equals(report.getResult())) {
+                return report;
+            }
+            if (report != null && shopIds.indexOf(shopId) == shopIds.size() - 1) {
+                return report;
+            }
+        }
+        return null;
+    }
+
+    private void fillStoreLocal(JstDeliveryDiagnoseResult result, FsStoreOrder order) {
+        result.setOrderType("STORE")
+                .setOrderCode(order.getOrderCode())
+                .setExtendOrderId(order.getExtendOrderId())
+                .setLocalStatus(order.getStatus())
+                .setLocalStatusDesc(describeStoreStatus(order.getStatus()))
+                .setCreateTime(order.getCreateTime())
+                .setDeliverySn(order.getDeliverySn())
+                .setDeliveryName(order.getDeliveryName());
+
+        List<String> blockers = new ArrayList<>();
+        if (StringUtils.isEmpty(order.getExtendOrderId())) {
+            blockers.add("extend_order_id 为空");
+        }
+        if (!Objects.equals(order.getStatus(), OrderInfoEnum.STATUS_1.getValue())) {
+            blockers.add("status=" + order.getStatus() + "(deliveryOp 仅扫描 status=1 待发货)");
+        }
+        result.setLocalBlockers(blockers);
+        result.setInPendingQueue(blockers.isEmpty());
+    }
+
+    private void fillLiveLocal(JstDeliveryDiagnoseResult result, LiveOrder order) {
+        result.setOrderType("LIVE")
+                .setOrderCode(order.getOrderCode())
+                .setExtendOrderId(order.getExtendOrderId())
+                .setLocalStatus(order.getStatus())
+                .setLocalStatusDesc(describeLiveStatus(order.getStatus()))
+                .setCreateTime(order.getCreateTime())
+                .setDeliverySn(order.getDeliverySn())
+                .setDeliveryName(order.getDeliveryName());
+
+        List<String> blockers = new ArrayList<>();
+        if (StringUtils.isEmpty(order.getExtendOrderId())) {
+            blockers.add("extend_order_id 为空");
+        }
+        if (!Objects.equals(order.getStatus(), 2)) {
+            blockers.add("status=" + order.getStatus() + "(deliveryOp 仅扫描 status=2 待发货)");
+        }
+        result.setLocalBlockers(blockers);
+        result.setInPendingQueue(blockers.isEmpty());
+    }
+
+    private List<String> resolveShopIds(String shopCode, Date createTime) {
+        if (StringUtils.isNotEmpty(shopCode)) {
+            return Collections.singletonList(shopCode.trim());
+        }
+        LinkedHashSet<String> shopIds = new LinkedHashSet<>();
+        shopIds.add(currentShopId);
+        if (needsDualShopQuery(createTime)) {
+            shopIds.add(JstStoreDeliverySyncService.LEGACY_JST_SHOP_ID);
+        }
+        return new ArrayList<>(shopIds);
+    }
+
+    private boolean needsDualShopQuery(Date createTime) {
+        if (createTime == null) {
+            return true;
+        }
+        return createTime.before(ORDER_CUTOFF_DATE);
+    }
+
+    private Long parseExtendOId(String extendOrderId) {
+        if (StringUtils.isEmpty(extendOrderId)) {
+            return null;
+        }
+        try {
+            return Long.parseLong(extendOrderId.trim());
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    private String describeStoreStatus(Integer status) {
+        if (status == null) {
+            return "未知";
+        }
+        if (Objects.equals(status, OrderInfoEnum.STATUS_1.getValue())) {
+            return status + "-" + OrderInfoEnum.STATUS_1.getDesc();
+        }
+        if (Objects.equals(status, OrderInfoEnum.STATUS_2.getValue())) {
+            return status + "-" + OrderInfoEnum.STATUS_2.getDesc();
+        }
+        if (Objects.equals(status, OrderInfoEnum.STATUS_0.getValue())) {
+            return status + "-" + OrderInfoEnum.STATUS_0.getDesc();
+        }
+        if (Objects.equals(status, OrderInfoEnum.STATUS_3.getValue())) {
+            return status + "-" + OrderInfoEnum.STATUS_3.getDesc();
+        }
+        return String.valueOf(status);
+    }
+
+    private String describeLiveStatus(Integer status) {
+        if (status == null) {
+            return "未知";
+        }
+        switch (status) {
+            case 1: return "1-待支付";
+            case 2: return "2-待发货";
+            case 3: return "3-待收货";
+            case 4: return "4-已完成";
+            default: return String.valueOf(status);
+        }
+    }
+
+    private ResolvedOrder resolveOrder(String orderNo) {
+        ResolvedOrder match = new ResolvedOrder();
+        if (StringUtils.isEmpty(orderNo)) {
+            return match;
+        }
+        String trimmed = orderNo.trim();
+
+        FsStoreOrder storeOrder = storeOrderService.selectFsStoreOrderByOrderCode(trimmed);
+        if (storeOrder == null) {
+            storeOrder = storeOrderService.selectFsStoreOrderByExtendOrderId(trimmed);
+        }
+        if (storeOrder == null && StringUtils.isNumeric(trimmed)) {
+            try {
+                storeOrder = storeOrderService.selectFsStoreOrderById(Long.parseLong(trimmed));
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        if (storeOrder != null) {
+            match.storeOrder = storeOrder;
+            return match;
+        }
+
+        LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderCode(trimmed);
+        if (liveOrder == null) {
+            liveOrder = liveOrderService.selectLiveOrderByExtendId(trimmed);
+        }
+        if (liveOrder == null) {
+            liveOrder = liveOrderService.selectLiveOrderByOrderId(trimmed);
+        }
+        if (liveOrder != null) {
+            match.liveOrder = liveOrder;
+        }
+        return match;
+    }
+
+    private static class ResolvedOrder {
+        private FsStoreOrder storeOrder;
+        private LiveOrder liveOrder;
+    }
+}

+ 32 - 0
fs-service-system/src/main/java/com/fs/erp/service/jst/JstDeliverySyncReportItem.java

@@ -0,0 +1,32 @@
+package com.fs.erp.service.jst;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * 聚水潭发货回传单条结果
+ */
+@Data
+@Accessors(chain = true)
+public class JstDeliverySyncReportItem {
+
+    public static final String RESULT_SUCCESS = "SUCCESS";
+    public static final String RESULT_FAILED = "FAILED";
+    public static final String RESULT_NOT_FOUND = "NOT_FOUND";
+    public static final String RESULT_SKIPPED = "SKIPPED";
+
+    /** Excel 中的平台订单号 */
+    private String platformOrderNo;
+    /** 本地订单号 */
+    private String orderCode;
+    /** 聚水潭 o_id */
+    private String extendOrderId;
+    /** STORE / LIVE */
+    private String orderType;
+    /** SUCCESS / FAILED / NOT_FOUND / SKIPPED */
+    private String result;
+    private String reason;
+    private String mailNo;
+    private String expressCode;
+    private String expressName;
+}

+ 423 - 0
fs-service-system/src/main/java/com/fs/erp/service/jst/JstExcelDeliveryBatchService.java

@@ -0,0 +1,423 @@
+package com.fs.erp.service.jst;
+
+import com.fs.common.utils.StringUtils;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.service.ILiveOrderService;
+import com.fs.store.domain.FsStoreOrder;
+import com.fs.store.service.IFsStoreOrderService;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.DataFormatter;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 从 Excel 读取平台订单号,按商城/直播分别走 deliveryOp 同步逻辑回传发货状态
+ */
+@Service
+public class JstExcelDeliveryBatchService {
+
+    private static final Logger log = LoggerFactory.getLogger(JstExcelDeliveryBatchService.class);
+
+    public static final String PLATFORM_ORDER_HEADER = "平台订单号";
+    public static final String DEFAULT_RESULT_RELATIVE_PATH = "myfile/发货结果.txt";
+    private static final int DEFAULT_PLATFORM_ORDER_COLUMN = 3;
+
+    @Autowired
+    private IFsStoreOrderService storeOrderService;
+    @Autowired
+    private ILiveOrderService liveOrderService;
+    @Autowired
+    private JstStoreDeliverySyncService jstStoreDeliverySyncService;
+    @Autowired
+    private JstLiveDeliverySyncService jstLiveDeliverySyncService;
+
+    public Map<String, Object> syncFromExcelPath(String filePath, String shopCode) throws Exception {
+        return syncFromExcelPath(filePath, shopCode, null);
+    }
+
+    public Map<String, Object> syncFromExcelPath(String filePath, String shopCode, String resultFilePath)
+            throws Exception {
+        Path path = Paths.get(filePath);
+        if (!Files.exists(path)) {
+            throw new IllegalArgumentException("Excel 文件不存在: " + filePath);
+        }
+        try (InputStream in = Files.newInputStream(path)) {
+            return syncFromExcel(in, shopCode, filePath, resultFilePath);
+        }
+    }
+
+    public Map<String, Object> syncFromExcel(InputStream inputStream, String shopCode, String sourceName)
+            throws Exception {
+        return syncFromExcel(inputStream, shopCode, sourceName, null);
+    }
+
+    public Map<String, Object> syncFromExcel(InputStream inputStream, String shopCode, String sourceName,
+                                           String resultFilePath) throws Exception {
+        List<String> platformOrderNos = readPlatformOrderNos(inputStream);
+        return syncPlatformOrderNos(platformOrderNos, shopCode, sourceName, resultFilePath);
+    }
+
+    public Map<String, Object> syncPlatformOrderNos(List<String> platformOrderNos, String shopCode, String sourceName)
+            throws Exception {
+        return syncPlatformOrderNos(platformOrderNos, shopCode, sourceName, null);
+    }
+
+    public Map<String, Object> syncPlatformOrderNos(List<String> platformOrderNos, String shopCode, String sourceName,
+                                                    String resultFilePath) throws Exception {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("source", sourceName);
+        result.put("shopCode", shopCode);
+        result.put("totalRead", platformOrderNos.size());
+
+        Map<String, String> platformOrderNoMap = new HashMap<>();
+        List<FsStoreOrder> storeOrders = new ArrayList<>();
+        List<LiveOrder> liveOrders = new ArrayList<>();
+        List<JstDeliverySyncReportItem> allReports = new ArrayList<>();
+
+        for (String platformOrderNo : platformOrderNos) {
+            OrderMatch match = resolveOrder(platformOrderNo);
+            if (match.storeOrder != null) {
+                storeOrders.add(match.storeOrder);
+                bindPlatformOrderNo(platformOrderNoMap, platformOrderNo, match.storeOrder.getOrderCode(),
+                        match.storeOrder.getExtendOrderId());
+                continue;
+            }
+            if (match.liveOrder != null) {
+                liveOrders.add(match.liveOrder);
+                bindPlatformOrderNo(platformOrderNoMap, platformOrderNo, match.liveOrder.getOrderCode(),
+                        match.liveOrder.getExtendOrderId());
+                continue;
+            }
+            allReports.add(new JstDeliverySyncReportItem()
+                    .setPlatformOrderNo(platformOrderNo)
+                    .setResult(JstDeliverySyncReportItem.RESULT_NOT_FOUND)
+                    .setReason(match.reason));
+        }
+
+        for (FsStoreOrder order : new ArrayList<>(storeOrders)) {
+            if (StringUtils.isEmpty(order.getExtendOrderId())) {
+                allReports.add(new JstDeliverySyncReportItem()
+                        .setPlatformOrderNo(resolvePlatformKey(platformOrderNoMap, order))
+                        .setOrderCode(order.getOrderCode())
+                        .setOrderType("STORE")
+                        .setResult(JstDeliverySyncReportItem.RESULT_SKIPPED)
+                        .setReason("extend_order_id 为空,无法查聚水潭 o_id"));
+                storeOrders.remove(order);
+            }
+        }
+        for (LiveOrder order : new ArrayList<>(liveOrders)) {
+            if (StringUtils.isEmpty(order.getExtendOrderId())) {
+                allReports.add(new JstDeliverySyncReportItem()
+                        .setPlatformOrderNo(resolvePlatformKey(platformOrderNoMap, order))
+                        .setOrderCode(order.getOrderCode())
+                        .setOrderType("LIVE")
+                        .setResult(JstDeliverySyncReportItem.RESULT_SKIPPED)
+                        .setReason("extend_order_id 为空,无法查聚水潭 o_id"));
+                liveOrders.remove(order);
+            }
+        }
+
+        if (!CollectionUtils.isEmpty(storeOrders)) {
+            log.info("Excel 批量发货回传-商城: {} 单, shopCode={}", storeOrders.size(), shopCode);
+            allReports.addAll(jstStoreDeliverySyncService.syncOrdersWithShopReport(storeOrders, shopCode,
+                    platformOrderNoMap));
+        }
+        if (!CollectionUtils.isEmpty(liveOrders)) {
+            log.info("Excel 批量发货回传-直播: {} 单, shopCode={}", liveOrders.size(), shopCode);
+            allReports.addAll(jstLiveDeliverySyncService.syncOrdersWithShopReport(liveOrders, shopCode,
+                    platformOrderNoMap));
+        }
+
+        List<JstDeliverySyncReportItem> successList = filterReports(allReports, JstDeliverySyncReportItem.RESULT_SUCCESS);
+        List<JstDeliverySyncReportItem> failedList = filterReports(allReports, JstDeliverySyncReportItem.RESULT_FAILED);
+        List<JstDeliverySyncReportItem> notFoundList = filterReports(allReports,
+                JstDeliverySyncReportItem.RESULT_NOT_FOUND);
+        List<JstDeliverySyncReportItem> skippedList = filterReports(allReports, JstDeliverySyncReportItem.RESULT_SKIPPED);
+
+        String resolvedResultPath = resolveResultFilePath(resultFilePath);
+        writeResultFile(resolvedResultPath, sourceName, shopCode, successList, failedList, notFoundList, skippedList);
+
+        result.put("resultFilePath", resolvedResultPath);
+        result.put("successCount", successList.size());
+        result.put("failedCount", failedList.size());
+        result.put("notFoundCount", notFoundList.size());
+        result.put("skippedCount", skippedList.size());
+        result.put("successList", successList);
+        result.put("failedList", failedList);
+        result.put("notFoundList", notFoundList);
+        result.put("skippedList", skippedList);
+        result.put("message", String.format("成功 %d,失败 %d,未找到 %d,跳过 %d;结果已写入 %s",
+                successList.size(), failedList.size(), notFoundList.size(), skippedList.size(), resolvedResultPath));
+        return result;
+    }
+
+    public String resolveResultFilePath(String resultFilePath) {
+        if (StringUtils.isNotEmpty(resultFilePath)) {
+            return resultFilePath.trim();
+        }
+        Path workspacePath = Paths.get(System.getProperty("user.dir")).resolve(DEFAULT_RESULT_RELATIVE_PATH).normalize();
+        Path parentPath = Paths.get(System.getProperty("user.dir")).getParent();
+        if (parentPath != null) {
+            Path projectRootPath = parentPath.resolve(DEFAULT_RESULT_RELATIVE_PATH).normalize();
+            Path projectRootDir = projectRootPath.getParent();
+            if (projectRootDir != null) {
+                try {
+                    Files.createDirectories(projectRootDir);
+                } catch (Exception e) {
+                    log.warn("创建结果目录失败: {}", projectRootDir, e);
+                }
+            }
+            return projectRootPath.toString();
+        }
+        try {
+            Path parent = workspacePath.getParent();
+            if (parent != null) {
+                Files.createDirectories(parent);
+            }
+        } catch (Exception e) {
+            log.warn("创建结果目录失败: {}", workspacePath.getParent(), e);
+        }
+        return workspacePath.toString();
+    }
+
+    private void writeResultFile(String resultFilePath, String sourceName, String shopCode,
+                                 List<JstDeliverySyncReportItem> successList,
+                                 List<JstDeliverySyncReportItem> failedList,
+                                 List<JstDeliverySyncReportItem> notFoundList,
+                                 List<JstDeliverySyncReportItem> skippedList) throws Exception {
+        StringBuilder sb = new StringBuilder();
+        sb.append("聚水潭发货回传结果").append(System.lineSeparator());
+        sb.append("生成时间: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))
+                .append(System.lineSeparator());
+        sb.append("数据来源: ").append(sourceName).append(System.lineSeparator());
+        sb.append("聚水潭店铺: ").append(shopCode).append(System.lineSeparator());
+        sb.append("汇总: 成功 ").append(successList.size())
+                .append(",失败 ").append(failedList.size())
+                .append(",未找到 ").append(notFoundList.size())
+                .append(",跳过 ").append(skippedList.size())
+                .append(System.lineSeparator());
+        sb.append(System.lineSeparator());
+
+        appendReportSection(sb, "回传成功", successList, true);
+        appendReportSection(sb, "回传失败", failedList, false);
+        appendReportSection(sb, "未找到订单信息", notFoundList, false);
+        appendReportSection(sb, "跳过处理", skippedList, false);
+
+        Path path = Paths.get(resultFilePath);
+        Path parent = path.getParent();
+        if (parent != null) {
+            Files.createDirectories(parent);
+        }
+        Files.write(path, sb.toString().getBytes(StandardCharsets.UTF_8));
+        log.info("发货回传结果已写入: {}", resultFilePath);
+    }
+
+    private void appendReportSection(StringBuilder sb, String title, List<JstDeliverySyncReportItem> items,
+                                     boolean showExpress) {
+        sb.append("========== ").append(title).append(" (").append(items.size()).append(") ==========")
+                .append(System.lineSeparator());
+        if (CollectionUtils.isEmpty(items)) {
+            sb.append("无").append(System.lineSeparator());
+            sb.append(System.lineSeparator());
+            return;
+        }
+        int index = 1;
+        for (JstDeliverySyncReportItem item : items) {
+            sb.append(index++).append(". ");
+            sb.append("平台订单号=").append(nullToEmpty(item.getPlatformOrderNo()));
+            if (StringUtils.isNotEmpty(item.getOrderCode())) {
+                sb.append(" | 本地订单号=").append(item.getOrderCode());
+            }
+            if (StringUtils.isNotEmpty(item.getOrderType())) {
+                sb.append(" | 类型=").append(item.getOrderType());
+            }
+            if (StringUtils.isNotEmpty(item.getExtendOrderId())) {
+                sb.append(" | o_id=").append(item.getExtendOrderId());
+            }
+            if (showExpress && StringUtils.isNotEmpty(item.getMailNo())) {
+                sb.append(" | 运单号=").append(item.getMailNo());
+            }
+            if (showExpress && StringUtils.isNotEmpty(item.getExpressName())) {
+                sb.append(" | 快递=").append(item.getExpressName());
+            } else if (showExpress && StringUtils.isNotEmpty(item.getExpressCode())) {
+                sb.append(" | 快递编码=").append(item.getExpressCode());
+            }
+            if (StringUtils.isNotEmpty(item.getReason())) {
+                sb.append(" | 说明=").append(item.getReason());
+            }
+            sb.append(System.lineSeparator());
+        }
+        sb.append(System.lineSeparator());
+    }
+
+    private List<JstDeliverySyncReportItem> filterReports(List<JstDeliverySyncReportItem> allReports, String result) {
+        List<JstDeliverySyncReportItem> list = new ArrayList<>();
+        for (JstDeliverySyncReportItem item : allReports) {
+            if (result.equals(item.getResult())) {
+                list.add(item);
+            }
+        }
+        return list;
+    }
+
+    public List<String> readPlatformOrderNos(InputStream inputStream) throws Exception {
+        Set<String> orderNos = new LinkedHashSet<>();
+        DataFormatter formatter = new DataFormatter();
+        try (Workbook workbook = WorkbookFactory.create(inputStream)) {
+            Sheet sheet = workbook.getSheetAt(0);
+            if (sheet == null) {
+                return new ArrayList<>();
+            }
+            int platformOrderColumn = resolvePlatformOrderColumn(sheet, formatter);
+            int startRow = hasHeaderRow(sheet, formatter) ? 1 : 0;
+            for (int r = startRow; r <= sheet.getLastRowNum(); r++) {
+                Row row = sheet.getRow(r);
+                if (row == null) {
+                    continue;
+                }
+                String value = readCell(row.getCell(platformOrderColumn - 1), formatter);
+                if (StringUtils.isNotEmpty(value) && !PLATFORM_ORDER_HEADER.equals(value)) {
+                    orderNos.add(value.trim());
+                }
+            }
+        }
+        return new ArrayList<>(orderNos);
+    }
+
+    private int resolvePlatformOrderColumn(Sheet sheet, DataFormatter formatter) {
+        Row header = sheet.getRow(0);
+        if (header == null) {
+            return DEFAULT_PLATFORM_ORDER_COLUMN;
+        }
+        for (int c = 0; c < header.getLastCellNum(); c++) {
+            String text = readCell(header.getCell(c), formatter);
+            if (PLATFORM_ORDER_HEADER.equals(text)) {
+                return c + 1;
+            }
+        }
+        return DEFAULT_PLATFORM_ORDER_COLUMN;
+    }
+
+    private boolean hasHeaderRow(Sheet sheet, DataFormatter formatter) {
+        Row header = sheet.getRow(0);
+        if (header == null) {
+            return false;
+        }
+        for (int c = 0; c < header.getLastCellNum(); c++) {
+            String text = readCell(header.getCell(c), formatter);
+            if (PLATFORM_ORDER_HEADER.equals(text)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String readCell(Cell cell, DataFormatter formatter) {
+        if (cell == null) {
+            return null;
+        }
+        String value = formatter.formatCellValue(cell);
+        return value == null ? null : value.trim();
+    }
+
+    private OrderMatch resolveOrder(String platformOrderNo) {
+        OrderMatch match = new OrderMatch();
+        if (StringUtils.isEmpty(platformOrderNo)) {
+            match.reason = "平台订单号为空";
+            return match;
+        }
+
+        FsStoreOrder storeOrder = storeOrderService.selectFsStoreOrderByOrderCode(platformOrderNo);
+        if (storeOrder == null) {
+            storeOrder = storeOrderService.selectFsStoreOrderByExtendOrderId(platformOrderNo);
+        }
+        if (storeOrder == null && StringUtils.isNumeric(platformOrderNo)) {
+            try {
+                storeOrder = storeOrderService.selectFsStoreOrderById(Long.parseLong(platformOrderNo));
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        if (storeOrder != null) {
+            match.storeOrder = storeOrder;
+            return match;
+        }
+
+        LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderCode(platformOrderNo);
+        if (liveOrder == null) {
+            liveOrder = liveOrderService.selectLiveOrderByExtendId(platformOrderNo);
+        }
+        if (liveOrder == null) {
+            liveOrder = liveOrderService.selectLiveOrderByOrderId(platformOrderNo);
+        }
+        if (liveOrder != null) {
+            match.liveOrder = liveOrder;
+            return match;
+        }
+
+        match.reason = "本地未找到商城/直播订单";
+        return match;
+    }
+
+    private void bindPlatformOrderNo(Map<String, String> platformOrderNoMap, String platformOrderNo,
+                                     String orderCode, String extendOrderId) {
+        if (StringUtils.isNotEmpty(orderCode)) {
+            platformOrderNoMap.put(orderCode, platformOrderNo);
+        }
+        if (StringUtils.isNotEmpty(extendOrderId)) {
+            platformOrderNoMap.put(extendOrderId, platformOrderNo);
+        }
+        platformOrderNoMap.put(platformOrderNo, platformOrderNo);
+    }
+
+    private String resolvePlatformKey(Map<String, String> platformOrderNoMap, FsStoreOrder order) {
+        if (platformOrderNoMap.containsKey(order.getOrderCode())) {
+            return platformOrderNoMap.get(order.getOrderCode());
+        }
+        if (order.getExtendOrderId() != null && platformOrderNoMap.containsKey(order.getExtendOrderId())) {
+            return platformOrderNoMap.get(order.getExtendOrderId());
+        }
+        return order.getOrderCode();
+    }
+
+    private String resolvePlatformKey(Map<String, String> platformOrderNoMap, LiveOrder order) {
+        if (platformOrderNoMap.containsKey(order.getOrderCode())) {
+            return platformOrderNoMap.get(order.getOrderCode());
+        }
+        if (order.getExtendOrderId() != null && platformOrderNoMap.containsKey(order.getExtendOrderId())) {
+            return platformOrderNoMap.get(order.getExtendOrderId());
+        }
+        return order.getOrderCode();
+    }
+
+    private String nullToEmpty(String value) {
+        return value == null ? "" : value;
+    }
+
+    private static class OrderMatch {
+        private FsStoreOrder storeOrder;
+        private LiveOrder liveOrder;
+        private String reason;
+    }
+}

+ 327 - 0
fs-service-system/src/main/java/com/fs/erp/service/jst/JstLiveDeliverySyncService.java

@@ -0,0 +1,327 @@
+package com.fs.erp.service.jst;
+
+import com.fs.erp.domain.ErpDeliverys;
+import com.fs.erp.domain.ErpOrderQuery;
+import com.fs.erp.dto.ErpOrderQueryRequert;
+import com.fs.erp.dto.ErpOrderQueryResponse;
+import com.fs.erp.service.impl.JSTErpOrderServiceImpl;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.service.ILiveOrderLogsService;
+import com.fs.live.service.ILiveOrderService;
+import com.fs.store.domain.FsExpress;
+import com.fs.store.enums.OrderLogEnum;
+import com.fs.store.service.IFsExpressService;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * 直播订单聚水潭发货同步(LiveTask.deliveryOp)
+ */
+@Service
+public class JstLiveDeliverySyncService {
+
+    private static final Logger logger = LoggerFactory.getLogger(JstLiveDeliverySyncService.class);
+
+    private static final int DELIVERY_QUERY_BATCH_SIZE = 30;
+
+    public static final String LEGACY_JST_SHOP_ID = "20486972";
+
+    private static final Date ORDER_CUTOFF_DATE;
+
+    static {
+        try {
+            ORDER_CUTOFF_DATE = new SimpleDateFormat("yyyyMMdd").parse("20260626");
+        } catch (ParseException e) {
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    @Value("${jst.shop_code}")
+    private String currentShopId;
+
+    @Autowired
+    private JSTErpOrderServiceImpl jstErpOrderService;
+
+    @Autowired
+    private ILiveOrderService liveOrderService;
+
+    @Autowired
+    private IFsExpressService expressService;
+
+    @Autowired
+    private ILiveOrderLogsService orderLogsService;
+
+    public void syncPendingDeliveries(List<LiveOrder> pendingOrders) throws InterruptedException {
+        if (CollectionUtils.isEmpty(pendingOrders)) {
+            return;
+        }
+
+        List<Long> legacyOIds = new ArrayList<>();
+        List<Long> currentOIds = new ArrayList<>();
+        List<String> legacyExtendIds = new ArrayList<>();
+        List<String> currentExtendIds = new ArrayList<>();
+
+        for (LiveOrder order : pendingOrders) {
+            if (StringUtils.isBlank(order.getExtendOrderId())) {
+                continue;
+            }
+            Long oId = Long.parseLong(order.getExtendOrderId());
+            if (needsDualShopQuery(order.getCreateTime())) {
+                legacyOIds.add(oId);
+                legacyExtendIds.add(order.getExtendOrderId());
+            } else {
+                currentOIds.add(oId);
+                currentExtendIds.add(order.getExtendOrderId());
+            }
+        }
+
+        syncByShopStrategy(legacyOIds, legacyExtendIds, true);
+        syncByShopStrategy(currentOIds, currentExtendIds, false);
+    }
+
+    public void syncOrdersWithShop(List<LiveOrder> orders, String shopId) throws InterruptedException {
+        syncOrdersWithShopReport(orders, shopId, null);
+    }
+
+    public List<JstDeliverySyncReportItem> syncOrdersWithShopReport(List<LiveOrder> orders, String shopId,
+                                                                    Map<String, String> platformOrderNoMap)
+            throws InterruptedException {
+        List<JstDeliverySyncReportItem> reports = new ArrayList<>();
+        if (CollectionUtils.isEmpty(orders) || StringUtils.isBlank(shopId)) {
+            return reports;
+        }
+        Map<Long, LiveOrder> orderByOId = new HashMap<>();
+        List<Long> oIds = new ArrayList<>();
+        List<String> extendIds = new ArrayList<>();
+        for (LiveOrder order : orders) {
+            if (StringUtils.isBlank(order.getExtendOrderId())) {
+                continue;
+            }
+            Long oId = Long.parseLong(order.getExtendOrderId());
+            orderByOId.put(oId, order);
+            oIds.add(oId);
+            extendIds.add(order.getExtendOrderId());
+        }
+        reports.addAll(syncByFixedShopReport(oIds, extendIds, shopId, orderByOId, platformOrderNoMap));
+        return reports;
+    }
+
+    private void syncByShopStrategy(List<Long> oIds, List<String> extendIds, boolean dualShop)
+            throws InterruptedException {
+        if (CollectionUtils.isEmpty(oIds)) {
+            return;
+        }
+        List<String> shopIds = resolveShopIds(dualShop);
+        List<List<Long>> batches = Lists.partition(oIds, DELIVERY_QUERY_BATCH_SIZE);
+        List<List<String>> extendBatches = Lists.partition(extendIds, DELIVERY_QUERY_BATCH_SIZE);
+        for (int i = 0; i < batches.size(); i++) {
+            for (String shopId : shopIds) {
+                queryAndProcessBatch(batches.get(i), extendBatches.get(i), shopId);
+            }
+        }
+    }
+
+    private List<JstDeliverySyncReportItem> syncByFixedShopReport(List<Long> oIds, List<String> extendIds,
+                                                                  String shopId, Map<Long, LiveOrder> orderByOId,
+                                                                  Map<String, String> platformOrderNoMap)
+            throws InterruptedException {
+        List<JstDeliverySyncReportItem> reports = new ArrayList<>();
+        if (CollectionUtils.isEmpty(oIds)) {
+            return reports;
+        }
+        List<List<Long>> batches = Lists.partition(oIds, DELIVERY_QUERY_BATCH_SIZE);
+        List<List<String>> extendBatches = Lists.partition(extendIds, DELIVERY_QUERY_BATCH_SIZE);
+        for (int i = 0; i < batches.size(); i++) {
+            reports.addAll(queryAndProcessBatchReport(batches.get(i), extendBatches.get(i), shopId,
+                    orderByOId, platformOrderNoMap));
+        }
+        return reports;
+    }
+
+    private void queryAndProcessBatch(List<Long> batch, List<String> extendIds, String shopId)
+            throws InterruptedException {
+        Map<Long, LiveOrder> orderByOId = new HashMap<>();
+        for (int i = 0; i < batch.size(); i++) {
+            LiveOrder order = liveOrderService.selectLiveOrderByExtendId(extendIds.get(i));
+            if (order != null) {
+                orderByOId.put(batch.get(i), order);
+            }
+        }
+        queryAndProcessBatchReport(batch, extendIds, shopId, orderByOId, null);
+    }
+
+    private List<JstDeliverySyncReportItem> queryAndProcessBatchReport(List<Long> batch, List<String> extendIds,
+                                                                       String shopId, Map<Long, LiveOrder> orderByOId,
+                                                                       Map<String, String> platformOrderNoMap)
+            throws InterruptedException {
+        List<JstDeliverySyncReportItem> reports = new ArrayList<>();
+        Set<String> returnedOrderCodes = new HashSet<>();
+        int pageIndex = 1;
+        boolean hasNext;
+        do {
+            Thread.sleep(300);
+            ErpOrderQueryRequert request = new ErpOrderQueryRequert();
+            request.setPage_size(100);
+            request.setPage_index(pageIndex);
+            request.setShop_id(shopId);
+            request.setO_ids(batch);
+
+            ErpOrderQueryResponse response = jstErpOrderService.getOrderLive(request);
+            hasNext = response.hasNextSafe();
+            if (!CollectionUtils.isEmpty(extendIds)) {
+                liveOrderService.updateUpdateTimeByExtendIds(extendIds);
+            }
+
+            if (response.getOrders() != null) {
+                for (ErpOrderQuery orderQuery : response.getOrders()) {
+                    returnedOrderCodes.add(orderQuery.getCode());
+                    LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderCode(orderQuery.getCode());
+                    reports.add(processOrderDeliveryReport(orderQuery, liveOrder, platformOrderNoMap));
+                }
+            }
+            pageIndex++;
+        } while (hasNext);
+
+        for (Long oId : batch) {
+            LiveOrder order = orderByOId.get(oId);
+            if (order == null) {
+                continue;
+            }
+            if (!returnedOrderCodes.contains(order.getOrderCode())) {
+                reports.add(buildLiveReport(order, platformOrderNoMap, JstDeliverySyncReportItem.RESULT_FAILED,
+                        "聚水潭 shopId=" + shopId + " 未查到该订单"));
+            }
+        }
+        return reports;
+    }
+
+    private JstDeliverySyncReportItem processOrderDeliveryReport(ErpOrderQuery orderQuery, LiveOrder liveOrder,
+                                                                   Map<String, String> platformOrderNoMap) {
+        JstDeliverySyncReportItem report = buildLiveReport(liveOrder, platformOrderNoMap,
+                JstDeliverySyncReportItem.RESULT_FAILED, null);
+        if (liveOrder == null) {
+            report.setOrderCode(orderQuery.getCode());
+            report.setResult(JstDeliverySyncReportItem.RESULT_FAILED);
+            report.setReason("聚水潭返回订单但本地直播订单不存在");
+            return report;
+        }
+        if (!Objects.equals(liveOrder.getStatus(), 2) && !Objects.equals(liveOrder.getStatus(), 3)) {
+            report.setResult(JstDeliverySyncReportItem.RESULT_FAILED);
+            report.setReason("本地订单 status=" + liveOrder.getStatus() + ",非待发货(2)/待收货(3)");
+            return report;
+        }
+        if (orderQuery.getDeliverys() == null || orderQuery.getDeliverys().isEmpty()) {
+            report.setResult(JstDeliverySyncReportItem.RESULT_FAILED);
+            report.setReason("聚水潭未返回物流信息(可能未发货)");
+            return report;
+        }
+
+        boolean synced = false;
+        String failReason = "聚水潭有物流但未满足回传条件";
+        for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
+            if (!delivery.getDelivery() || StringUtils.isEmpty(delivery.getMail_no())) {
+                failReason = "物流明细 delivery=false 或 mail_no 为空";
+                continue;
+            }
+            FsExpress express = expressService.selectFsExpressByOmsCode(delivery.getExpress_code());
+            if (express == null) {
+                failReason = "快递公司 oms_code=" + delivery.getExpress_code() + " 在 fs_express 不存在";
+                continue;
+            }
+            if (Objects.equals(liveOrder.getStatus(), 3)) {
+                synced = true;
+                report.setMailNo(delivery.getMail_no());
+                report.setExpressCode(delivery.getExpress_code());
+                report.setExpressName(delivery.getExpress_name());
+                report.setReason("订单已是待收货状态");
+                break;
+            }
+            orderLogsService.create(liveOrder.getOrderId(), OrderLogEnum.DELIVERY_GOODS.getValue(),
+                    OrderLogEnum.DELIVERY_GOODS.getDesc());
+            try {
+                liveOrderService.deliveryOrder(orderQuery.getCode(), delivery.getMail_no(),
+                        delivery.getExpress_code(), delivery.getExpress_name());
+                synced = true;
+                report.setMailNo(delivery.getMail_no());
+                report.setExpressCode(delivery.getExpress_code());
+                report.setExpressName(delivery.getExpress_name());
+                break;
+            } catch (Exception e) {
+                logger.error("同步直播订单发货状态失败 orderCode={} mailNo={}",
+                        orderQuery.getCode(), delivery.getMail_no(), e);
+                failReason = "回传异常: " + e.getMessage();
+            }
+        }
+        report.setResult(synced ? JstDeliverySyncReportItem.RESULT_SUCCESS : JstDeliverySyncReportItem.RESULT_FAILED);
+        if (!synced) {
+            report.setReason(failReason);
+        }
+        return report;
+    }
+
+    private JstDeliverySyncReportItem buildLiveReport(LiveOrder order, Map<String, String> platformOrderNoMap,
+                                                      String result, String reason) {
+        JstDeliverySyncReportItem report = new JstDeliverySyncReportItem();
+        if (order != null) {
+            report.setPlatformOrderNo(resolvePlatformOrderNo(order.getOrderCode(), order.getExtendOrderId(),
+                    platformOrderNoMap));
+            report.setOrderCode(order.getOrderCode());
+            report.setExtendOrderId(order.getExtendOrderId());
+        }
+        report.setOrderType("LIVE");
+        report.setResult(result);
+        report.setReason(reason);
+        return report;
+    }
+
+    private String resolvePlatformOrderNo(String orderCode, String extendOrderId,
+                                          Map<String, String> platformOrderNoMap) {
+        if (platformOrderNoMap == null) {
+            return orderCode;
+        }
+        if (platformOrderNoMap.containsKey(orderCode)) {
+            return platformOrderNoMap.get(orderCode);
+        }
+        if (extendOrderId != null && platformOrderNoMap.containsKey(extendOrderId)) {
+            return platformOrderNoMap.get(extendOrderId);
+        }
+        return orderCode;
+    }
+
+    private List<String> resolveShopIds(boolean dualShop) {
+        if (!dualShop) {
+            List<String> shopIds = new ArrayList<>(1);
+            shopIds.add(currentShopId);
+            return shopIds;
+        }
+        LinkedHashSet<String> shopIds = new LinkedHashSet<>();
+        shopIds.add(LEGACY_JST_SHOP_ID);
+        shopIds.add(currentShopId);
+        return new ArrayList<>(shopIds);
+    }
+
+    private boolean needsDualShopQuery(Date createTime) {
+        if (createTime == null) {
+            return true;
+        }
+        return createTime.before(ORDER_CUTOFF_DATE);
+    }
+}

+ 354 - 0
fs-service-system/src/main/java/com/fs/erp/service/jst/JstStoreDeliverySyncService.java

@@ -0,0 +1,354 @@
+package com.fs.erp.service.jst;
+
+import com.fs.erp.domain.ErpDeliverys;
+import com.fs.erp.domain.ErpOrderQuery;
+import com.fs.erp.dto.ErpOrderQueryRequert;
+import com.fs.erp.dto.ErpOrderQueryResponse;
+import com.fs.erp.service.impl.JSTErpOrderServiceImpl;
+import com.fs.store.domain.FsExpress;
+import com.fs.store.domain.FsStoreOrder;
+import com.fs.store.enums.OrderInfoEnum;
+import com.fs.store.enums.OrderLogEnum;
+import com.fs.store.service.IFsExpressService;
+import com.fs.store.service.IFsStoreOrderService;
+import com.fs.store.service.IFsStoreOrderStatusService;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * 商城订单聚水潭发货同步(StoreTask.deliveryOp)
+ */
+@Service
+public class JstStoreDeliverySyncService {
+
+    private static final Logger logger = LoggerFactory.getLogger(JstStoreDeliverySyncService.class);
+
+    private static final int DELIVERY_QUERY_BATCH_SIZE = 30;
+
+    /** 2026-06-26 之前订单使用的历史店铺 */
+    public static final String LEGACY_JST_SHOP_ID = "20486972";
+
+    private static final Date ORDER_CUTOFF_DATE;
+
+    static {
+        try {
+            ORDER_CUTOFF_DATE = new SimpleDateFormat("yyyyMMdd").parse("20260626");
+        } catch (ParseException e) {
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    @Value("${jst.shop_code}")
+    private String currentShopId;
+
+    @Autowired
+    private JSTErpOrderServiceImpl jstErpOrderService;
+
+    @Autowired
+    private IFsStoreOrderService orderService;
+
+    @Autowired
+    private IFsStoreOrderStatusService orderStatusService;
+
+    @Autowired
+    private IFsExpressService expressService;
+
+    public void syncPendingDeliveries(List<FsStoreOrder> pendingOrders) throws InterruptedException {
+        if (CollectionUtils.isEmpty(pendingOrders)) {
+            return;
+        }
+
+        List<Long> legacyOIds = new ArrayList<>();
+        List<Long> currentOIds = new ArrayList<>();
+        List<String> legacyExtendIds = new ArrayList<>();
+        List<String> currentExtendIds = new ArrayList<>();
+
+        for (FsStoreOrder order : pendingOrders) {
+            if (StringUtils.isBlank(order.getExtendOrderId())) {
+                continue;
+            }
+            Long oId = Long.parseLong(order.getExtendOrderId());
+            if (needsDualShopQuery(order.getCreateTime())) {
+                legacyOIds.add(oId);
+                legacyExtendIds.add(order.getExtendOrderId());
+            } else {
+                currentOIds.add(oId);
+                currentExtendIds.add(order.getExtendOrderId());
+            }
+        }
+
+        syncByShopStrategy(legacyOIds, legacyExtendIds, true);
+        syncByShopStrategy(currentOIds, currentExtendIds, false);
+    }
+
+    /**
+     * 指定店铺同步商城待发货订单(Excel 批量回传等场景)
+     */
+    public void syncOrdersWithShop(List<FsStoreOrder> orders, String shopId) throws InterruptedException {
+        syncOrdersWithShopReport(orders, shopId, null);
+    }
+
+    /**
+     * 指定店铺同步并返回逐单回传结果
+     */
+    public List<JstDeliverySyncReportItem> syncOrdersWithShopReport(List<FsStoreOrder> orders, String shopId,
+                                                                    Map<String, String> platformOrderNoMap)
+            throws InterruptedException {
+        List<JstDeliverySyncReportItem> reports = new ArrayList<>();
+        if (CollectionUtils.isEmpty(orders) || StringUtils.isBlank(shopId)) {
+            return reports;
+        }
+        Map<Long, FsStoreOrder> orderByOId = new HashMap<>();
+        List<Long> oIds = new ArrayList<>();
+        List<String> extendIds = new ArrayList<>();
+        for (FsStoreOrder order : orders) {
+            if (StringUtils.isBlank(order.getExtendOrderId())) {
+                continue;
+            }
+            Long oId = Long.parseLong(order.getExtendOrderId());
+            orderByOId.put(oId, order);
+            oIds.add(oId);
+            extendIds.add(order.getExtendOrderId());
+        }
+        reports.addAll(syncByFixedShopReport(oIds, extendIds, shopId, orderByOId, platformOrderNoMap));
+        return reports;
+    }
+
+    private void syncByShopStrategy(List<Long> oIds, List<String> extendIds, boolean dualShop)
+            throws InterruptedException {
+        if (CollectionUtils.isEmpty(oIds)) {
+            return;
+        }
+        List<String> shopIds = resolveShopIds(dualShop);
+        List<List<Long>> batches = Lists.partition(oIds, DELIVERY_QUERY_BATCH_SIZE);
+        List<List<String>> extendBatches = Lists.partition(extendIds, DELIVERY_QUERY_BATCH_SIZE);
+        for (int i = 0; i < batches.size(); i++) {
+            List<Long> batch = batches.get(i);
+            List<String> extendBatch = extendBatches.get(i);
+            for (String shopId : shopIds) {
+                queryAndProcessBatch(batch, extendBatch, shopId);
+            }
+        }
+    }
+
+    private List<JstDeliverySyncReportItem> syncByFixedShopReport(List<Long> oIds, List<String> extendIds,
+                                                                  String shopId,
+                                                                  Map<Long, FsStoreOrder> orderByOId,
+                                                                  Map<String, String> platformOrderNoMap)
+            throws InterruptedException {
+        List<JstDeliverySyncReportItem> reports = new ArrayList<>();
+        if (CollectionUtils.isEmpty(oIds)) {
+            return reports;
+        }
+        List<List<Long>> batches = Lists.partition(oIds, DELIVERY_QUERY_BATCH_SIZE);
+        List<List<String>> extendBatches = Lists.partition(extendIds, DELIVERY_QUERY_BATCH_SIZE);
+        for (int i = 0; i < batches.size(); i++) {
+            reports.addAll(queryAndProcessBatchReport(batches.get(i), extendBatches.get(i), shopId,
+                    orderByOId, platformOrderNoMap));
+        }
+        return reports;
+    }
+
+    private void syncByFixedShop(List<Long> oIds, List<String> extendIds, String shopId)
+            throws InterruptedException {
+        if (CollectionUtils.isEmpty(oIds)) {
+            return;
+        }
+        Map<Long, FsStoreOrder> orderByOId = new HashMap<>();
+        for (int i = 0; i < oIds.size(); i++) {
+            FsStoreOrder order = orderService.selectFsStoreOrderByExtendOrderId(extendIds.get(i));
+            if (order != null) {
+                orderByOId.put(oIds.get(i), order);
+            }
+        }
+        syncByFixedShopReport(oIds, extendIds, shopId, orderByOId, null);
+    }
+
+    private List<JstDeliverySyncReportItem> queryAndProcessBatchReport(List<Long> batch, List<String> extendIds,
+                                                                       String shopId,
+                                                                       Map<Long, FsStoreOrder> orderByOId,
+                                                                       Map<String, String> platformOrderNoMap)
+            throws InterruptedException {
+        List<JstDeliverySyncReportItem> reports = new ArrayList<>();
+        Set<String> returnedOrderCodes = new HashSet<>();
+        int pageIndex = 1;
+        boolean hasNext;
+        do {
+            Thread.sleep(300);
+            ErpOrderQueryRequert request = new ErpOrderQueryRequert();
+            request.setPage_size(100);
+            request.setPage_index(pageIndex);
+            request.setShop_id(shopId);
+            request.setO_ids(batch);
+
+            ErpOrderQueryResponse response = jstErpOrderService.getOrder(request);
+            hasNext = response.hasNextSafe();
+            if (!CollectionUtils.isEmpty(extendIds)) {
+                orderService.updateUpdateTimeByExtendIds(extendIds);
+            }
+
+            if (response.getOrders() != null) {
+                for (ErpOrderQuery orderQuery : response.getOrders()) {
+                    returnedOrderCodes.add(orderQuery.getCode());
+                    FsStoreOrder storeOrder = orderService.selectFsStoreOrderByOrderCode(orderQuery.getCode());
+                    reports.add(processOrderDeliveryReport(orderQuery, storeOrder, platformOrderNoMap));
+                }
+            }
+            pageIndex++;
+        } while (hasNext);
+
+        for (Long oId : batch) {
+            FsStoreOrder order = orderByOId.get(oId);
+            if (order == null) {
+                continue;
+            }
+            if (!returnedOrderCodes.contains(order.getOrderCode())) {
+                reports.add(buildStoreReport(order, platformOrderNoMap, JstDeliverySyncReportItem.RESULT_FAILED,
+                        "聚水潭 shopId=" + shopId + " 未查到该订单"));
+            }
+        }
+        return reports;
+    }
+
+    private JstDeliverySyncReportItem processOrderDeliveryReport(ErpOrderQuery orderQuery, FsStoreOrder storeOrder,
+                                                                   Map<String, String> platformOrderNoMap) {
+        JstDeliverySyncReportItem report = buildStoreReport(storeOrder, platformOrderNoMap,
+                JstDeliverySyncReportItem.RESULT_FAILED, null);
+        if (storeOrder == null) {
+            report.setOrderCode(orderQuery.getCode());
+            report.setResult(JstDeliverySyncReportItem.RESULT_FAILED);
+            report.setReason("聚水潭返回订单但本地商城订单不存在");
+            return report;
+        }
+        if (!Objects.equals(storeOrder.getStatus(), OrderInfoEnum.STATUS_1.getValue())
+                && !Objects.equals(storeOrder.getStatus(), OrderInfoEnum.STATUS_2.getValue())) {
+            report.setResult(JstDeliverySyncReportItem.RESULT_FAILED);
+            report.setReason("本地订单 status=" + storeOrder.getStatus() + ",非待发货/待收货");
+            return report;
+        }
+        if (orderQuery.getDeliverys() == null || orderQuery.getDeliverys().isEmpty()) {
+            report.setResult(JstDeliverySyncReportItem.RESULT_FAILED);
+            report.setReason("聚水潭未返回物流信息(可能未发货)");
+            return report;
+        }
+
+        boolean synced = false;
+        String failReason = "聚水潭有物流但未满足回传条件";
+        for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
+            if (!delivery.getDelivery() || StringUtils.isEmpty(delivery.getMail_no())) {
+                failReason = "物流明细 delivery=false 或 mail_no 为空";
+                continue;
+            }
+            FsExpress express = expressService.selectFsExpressByOmsCode(delivery.getExpress_code());
+            if (express == null) {
+                failReason = "快递公司 oms_code=" + delivery.getExpress_code() + " 在 fs_express 不存在";
+                continue;
+            }
+            if (Objects.equals(storeOrder.getStatus(), OrderInfoEnum.STATUS_2.getValue())) {
+                synced = true;
+                report.setMailNo(delivery.getMail_no());
+                report.setExpressCode(delivery.getExpress_code());
+                report.setExpressName(delivery.getExpress_name());
+                report.setReason("订单已是待收货状态");
+                break;
+            }
+            orderStatusService.create(Long.valueOf(orderQuery.getCode()), OrderLogEnum.DELIVERY_GOODS.getValue(),
+                    OrderLogEnum.DELIVERY_GOODS.getDesc());
+            try {
+                orderService.deliveryOrder(orderQuery.getCode(), delivery.getMail_no(),
+                        delivery.getExpress_code(), delivery.getExpress_name());
+                synced = true;
+                report.setMailNo(delivery.getMail_no());
+                report.setExpressCode(delivery.getExpress_code());
+                report.setExpressName(delivery.getExpress_name());
+                break;
+            } catch (Exception e) {
+                logger.error("同步订单状态失败 orderCode={} mailNo={}", orderQuery.getCode(), delivery.getMail_no(), e);
+                failReason = "回传异常: " + e.getMessage();
+            }
+        }
+        report.setResult(synced ? JstDeliverySyncReportItem.RESULT_SUCCESS : JstDeliverySyncReportItem.RESULT_FAILED);
+        if (!synced) {
+            report.setReason(failReason);
+        }
+        return report;
+    }
+
+    private JstDeliverySyncReportItem buildStoreReport(FsStoreOrder order, Map<String, String> platformOrderNoMap,
+                                                       String result, String reason) {
+        JstDeliverySyncReportItem report = new JstDeliverySyncReportItem();
+        if (order != null) {
+            report.setPlatformOrderNo(resolvePlatformOrderNo(order.getOrderCode(), order.getExtendOrderId(),
+                    platformOrderNoMap));
+            report.setOrderCode(order.getOrderCode());
+            report.setExtendOrderId(order.getExtendOrderId());
+        }
+        report.setOrderType("STORE");
+        report.setResult(result);
+        report.setReason(reason);
+        return report;
+    }
+
+    private String resolvePlatformOrderNo(String orderCode, String extendOrderId,
+                                          Map<String, String> platformOrderNoMap) {
+        if (platformOrderNoMap == null) {
+            return orderCode;
+        }
+        if (platformOrderNoMap.containsKey(orderCode)) {
+            return platformOrderNoMap.get(orderCode);
+        }
+        if (extendOrderId != null && platformOrderNoMap.containsKey(extendOrderId)) {
+            return platformOrderNoMap.get(extendOrderId);
+        }
+        return orderCode;
+    }
+
+    private void queryAndProcessBatch(List<Long> batch, List<String> extendIds, String shopId)
+            throws InterruptedException {
+        Map<Long, FsStoreOrder> orderByOId = new HashMap<>();
+        for (int i = 0; i < batch.size(); i++) {
+            FsStoreOrder order = orderService.selectFsStoreOrderByExtendOrderId(extendIds.get(i));
+            if (order != null) {
+                orderByOId.put(batch.get(i), order);
+            }
+        }
+        queryAndProcessBatchReport(batch, extendIds, shopId, orderByOId, null);
+    }
+
+    private List<String> resolveShopIds(boolean dualShop) {
+        if (!dualShop) {
+            List<String> shopIds = new ArrayList<>(1);
+            shopIds.add(currentShopId);
+            return shopIds;
+        }
+        LinkedHashSet<String> shopIds = new LinkedHashSet<>();
+        shopIds.add(LEGACY_JST_SHOP_ID);
+        shopIds.add(currentShopId);
+        return new ArrayList<>(shopIds);
+    }
+
+    private boolean needsDualShopQuery(Date createTime) {
+        if (createTime == null) {
+            return true;
+        }
+        return createTime.before(ORDER_CUTOFF_DATE);
+    }
+}

+ 36 - 0
fs-service-system/src/main/java/com/fs/erp/utils/JstApiExceptionUtils.java

@@ -0,0 +1,36 @@
+package com.fs.erp.utils;
+
+/**
+ * 聚水潭 API 异常识别工具
+ */
+public final class JstApiExceptionUtils {
+
+    private static final String RATE_LIMIT_KEYWORD = "调用频次超过限制";
+    private static final String ERROR_TOKEN = "无效的access_token";
+
+    private JstApiExceptionUtils() {
+    }
+
+    /**
+     * 判断是否为聚水潭调用频次超限异常
+     */
+    public static boolean isRateLimitExceeded(Throwable throwable) {
+        for (Throwable current = throwable; current != null; current = current.getCause()) {
+            String message = current.getMessage();
+            if (message != null && message.contains(RATE_LIMIT_KEYWORD)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean isErrorToken(Throwable throwable) {
+        for (Throwable current = throwable; current != null; current = current.getCause()) {
+            String message = current.getMessage();
+            if (message != null && message.contains(ERROR_TOKEN)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

+ 53 - 23
fs-service-system/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -1670,11 +1670,9 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         }
         erpOrder.setDetails(details);
         erpOrder.setReceiver_name(order.getUserName());
-        if (order.getUserPhone().length() > 11) {
-            erpOrder.setReceiver_phone(order.getUserPhone());
-        } else {
-            erpOrder.setReceiver_mobile(order.getUserPhone());
-        }
+        String erpReceiverPhone = resolvePhoneForErpPush(order);
+        erpOrder.setReceiver_mobile(erpReceiverPhone);
+        erpOrder.setReceiver_phone(null);
         String[] address = order.getUserAddress().split(" ");
         erpOrder.setReceiver_province(address[0]);
         erpOrder.setReceiver_city(address[1]);
@@ -1842,15 +1840,15 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             fsWxExpressTaskMapper.insert(fsWxExpressTask);
 
             // 微信消息通知
-            TemplateBean templateBean = TemplateBean.builder()
-                    .orderId(order.getOrderId().toString())
-                    .orderCode(order.getOrderCode())
-                    .deliveryId(deliveryId)
-                    .deliveryName(express.getName())
-                    .userId(Long.valueOf(order.getUserId()))
-                    .templateType(TemplateListenEnum.TYPE_2.getValue())
-                    .build();
-            publisher.publishEvent(new TemplateEvent(this, templateBean));
+//            TemplateBean templateBean = TemplateBean.builder()
+//                    .orderId(order.getOrderId().toString())
+//                    .orderCode(order.getOrderCode())
+//                    .deliveryId(deliveryId)
+//                    .deliveryName(express.getName())
+//                    .userId(Long.valueOf(order.getUserId()))
+//                    .templateType(TemplateListenEnum.TYPE_2.getValue())
+//                    .build();
+//            publisher.publishEvent(new TemplateEvent(this, templateBean));
 
             //订阅物流回调
             String lastFourNumber = "";
@@ -2065,15 +2063,15 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                             liveOrderMapper.updateLiveOrder(map);
                             liveOrderLogsService.create(order.getOrderId(), OrderLogEnum.DELIVERY_GOODS.getValue(),
                                     OrderLogEnum.DELIVERY_GOODS.getDesc());
-                            TemplateBean templateBean = TemplateBean.builder()
-                                    .orderId(order.getOrderId().toString())
-                                    .orderCode(order.getOrderCode().toString())
-                                    .deliveryId(order.getDeliverySn())
-                                    .deliveryName(order.getDeliveryName())
-                                    .userId(Long.valueOf(order.getUserId()))
-                                    .templateType(TemplateListenEnum.TYPE_2.getValue())
-                                    .build();
-                            publisher.publishEvent(new TemplateEvent(this, templateBean));
+//                            TemplateBean templateBean = TemplateBean.builder()
+//                                    .orderId(order.getOrderId().toString())
+//                                    .orderCode(order.getOrderCode().toString())
+//                                    .deliveryId(order.getDeliverySn())
+//                                    .deliveryName(order.getDeliveryName())
+//                                    .userId(Long.valueOf(order.getUserId()))
+//                                    .templateType(TemplateListenEnum.TYPE_2.getValue())
+//                                    .build();
+//                            publisher.publishEvent(new TemplateEvent(this, templateBean));
                             return R.ok();
                         }
                     }
@@ -2220,6 +2218,9 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         BigDecimal storePostage = BigDecimal.ZERO;
         // 优化:直接字符串拼接,避免创建List和Stream中间对象
         String ids = String.valueOf(fsStoreProduct.getTempId());
+        if (4190L == cityId) {
+            cityId = 419001L;
+        }
         String cityIds = cityId + ",0";
 
         // 构建缓存 key:基于 tempId 和 cityIds
@@ -2875,6 +2876,35 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         });
     }
 
+    /**
+     * 推聚水潭前将订单手机号解析为11位明文:脱敏号先走入库同款还原逻辑,密文走解密,仍失败则用户表兜底
+     */
+    private String resolvePhoneForErpPush(LiveOrder order) {
+        String phone = order.getUserPhone();
+        if (PhoneUtils.isMaskedPhone(phone)) {
+            resolveMaskedPhoneBeforeStore(order);
+            phone = order.getUserPhone();
+        }
+        String erpPhone = PhoneUtils.resolvePhoneForErp(phone);
+        if (PhoneUtils.isPlainMobile11(erpPhone)) {
+            return erpPhone;
+        }
+        try {
+            FsUser fsUser = userService.selectFsUserById(Long.parseLong(order.getUserId()));
+            if (fsUser != null && StringUtils.isNotEmpty(fsUser.getPhone())) {
+                erpPhone = PhoneUtils.resolvePhoneForErp(fsUser.getPhone());
+            }
+        } catch (Exception e) {
+            log.error("推单解析手机号-用户表兜底失败, orderId:{}, userId:{}", order.getOrderId(), order.getUserId(), e);
+        }
+        if (!PhoneUtils.isPlainMobile11(erpPhone)) {
+            log.error("推聚水潭前无法解析为11位明文手机号, orderId:{}, orderCode:{}, userPhone:{}",
+                    order.getOrderId(), order.getOrderCode(), order.getUserPhone());
+            return "";
+        }
+        return erpPhone;
+    }
+
     /**
      * 对面传入脱敏手机号时,先解析为明文或用户表中的密文,再交由 normalizePhoneForStorage 统一加密入库
      */

+ 9 - 9
fs-service-system/src/main/java/com/fs/store/service/impl/FsStoreOrderServiceImpl.java

@@ -2724,15 +2724,15 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService
                             fsStoreOrderMapper.updateFsStoreOrder(map);
                             orderStatusService.create(order.getId(), OrderLogEnum.DELIVERY_GOODS.getValue(),
                                     OrderLogEnum.DELIVERY_GOODS.getDesc());
-                            TemplateBean templateBean = TemplateBean.builder()
-                                    .orderId(order.getId().toString())
-                                    .orderCode(order.getOrderCode().toString())
-                                    .deliveryId(order.getDeliveryId())
-                                    .deliveryName(order.getDeliveryName())
-                                    .userId(order.getUserId())
-                                    .templateType(TemplateListenEnum.TYPE_2.getValue())
-                                    .build();
-                            publisher.publishEvent(new TemplateEvent(this, templateBean));
+//                            TemplateBean templateBean = TemplateBean.builder()
+//                                    .orderId(order.getId().toString())
+//                                    .orderCode(order.getOrderCode().toString())
+//                                    .deliveryId(order.getDeliveryId())
+//                                    .deliveryName(order.getDeliveryName())
+//                                    .userId(order.getUserId())
+//                                    .templateType(TemplateListenEnum.TYPE_2.getValue())
+//                                    .build();
+//                            publisher.publishEvent(new TemplateEvent(this, templateBean));
                             return R.ok();
                         }
                     }

+ 86 - 2
fs-user-app/src/main/java/com/fs/app/controller/LiveCouponController.java

@@ -3,24 +3,37 @@ package com.fs.app.controller;
 import com.fs.app.annotation.Login;
 import com.fs.app.facade.LiveFacadeService;
 import com.fs.common.core.domain.R;
+import com.fs.erp.service.jst.JstDeliveryDiagnoseService;
+import com.fs.erp.service.jst.JstExcelDeliveryBatchService;
 import com.fs.live.param.CouponPO;
-import com.fs.live.param.RedPO;
 import com.fs.live.service.ILiveCouponService;
-import com.fs.live.service.ILiveRedConfService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
 
 @RestController
 @RequestMapping("/app/live/coupon")
 public class LiveCouponController extends AppBaseController {
 
+    private static final String DEFAULT_EXCEL_RELATIVE_PATH = "myfile/悦享生活-聚水潭发货,商城待发货.xlsx";
+
     @Autowired
     private ILiveCouponService liveCouponService;
     @Autowired
     private LiveFacadeService liveFacadeService;
+    @Autowired
+    private JstExcelDeliveryBatchService jstExcelDeliveryBatchService;
+    @Autowired
+    private JstDeliveryDiagnoseService jstDeliveryDiagnoseService;
 
     /**
      * 领取优惠券
@@ -52,4 +65,75 @@ public class LiveCouponController extends AppBaseController {
         return liveCouponService.curCoupon(coupon);
     }
 
+//    /**
+//     * 读取 Excel「平台订单号」,用指定聚水潭店铺查询并发货回传。
+//     * 商城走 StoreTask.deliveryOp 同款逻辑,直播走 LiveTask.deliveryOp 同款逻辑。
+//     *
+//     * 用法1(默认读项目 myfile 下 Excel):
+//     * GET /app/live/coupon/syncJstDeliveryFromExcel?shopCode=20486972
+//     *
+//     * 用法2(指定文件路径):
+//     * GET /app/live/coupon/syncJstDeliveryFromExcel?filePath=C:/mycode/ffhx-new/myfile/悦享生活-聚水潭发货,商城待发货.xlsx
+//     *
+//     * 用法3(上传 Excel):
+//     * POST /app/live/coupon/syncJstDeliveryFromExcel  form-data: file=xxx.xlsx, shopCode=20486972
+//     */
+//    @GetMapping("/syncJstDeliveryFromExcel")
+//    public R syncJstDeliveryFromExcel(
+//            @RequestParam(value = "filePath", required = false) String filePath,
+//            @RequestParam(value = "resultFilePath", required = false) String resultFilePath,
+//            @RequestParam(value = "shopCode", defaultValue = "20486972") String shopCode) throws Exception {
+//        String resolvedPath = resolveExcelPath(filePath);
+//        Map<String, Object> result = jstExcelDeliveryBatchService.syncFromExcelPath(resolvedPath, shopCode,
+//                resultFilePath);
+//        return R.ok(result);
+//    }
+//
+//    @PostMapping("/syncJstDeliveryFromExcel")
+//    public R syncJstDeliveryFromExcelUpload(
+//            @RequestParam(value = "file", required = false) MultipartFile file,
+//            @RequestParam(value = "filePath", required = false) String filePath,
+//            @RequestParam(value = "resultFilePath", required = false) String resultFilePath,
+//            @RequestParam(value = "shopCode", defaultValue = "20486972") String shopCode) throws Exception {
+//        Map<String, Object> result;
+//        if (file != null && !file.isEmpty()) {
+//            result = jstExcelDeliveryBatchService.syncFromExcel(file.getInputStream(), shopCode,
+//                    file.getOriginalFilename(), resultFilePath);
+//        } else {
+//            result = jstExcelDeliveryBatchService.syncFromExcelPath(resolveExcelPath(filePath), shopCode,
+//                    resultFilePath);
+//        }
+//        return R.ok(result);
+//    }
+//
+    /**
+     * 诊断指定订单为何未聚水潭发货回传(默认只查原因不写库,逻辑对齐 StoreTask.deliveryOp / LiveTask.deliveryOp)。
+     *
+     * GET /app/live/coupon/diagnoseJstDelivery?orderNo=2069709195675734016
+     * GET /app/live/coupon/diagnoseJstDelivery?orderNo=xxx&shopCode=20486972
+     * GET /app/live/coupon/diagnoseJstDelivery?orderNo=xxx&trySync=true
+     */
+//    @GetMapping("/diagnoseJstDelivery")
+//    public R diagnoseJstDelivery(
+//            @RequestParam("orderNo") String orderNo,
+//            @RequestParam(value = "shopCode", required = false) String shopCode,
+//            @RequestParam(value = "trySync", defaultValue = "false") boolean trySync) throws Exception {
+//        return R.ok().put("data", jstDeliveryDiagnoseService.diagnose(orderNo, shopCode, trySync));
+//    }
+
+//    private String resolveExcelPath(String filePath) {
+//        if (filePath != null && !filePath.trim().isEmpty()) {
+//            return filePath.trim();
+//        }
+//        Path workspacePath = Paths.get(System.getProperty("user.dir")).resolve(DEFAULT_EXCEL_RELATIVE_PATH).normalize();
+//        Path parentPath = Paths.get(System.getProperty("user.dir")).getParent();
+//        if (parentPath != null) {
+//            Path projectRootPath = parentPath.resolve(DEFAULT_EXCEL_RELATIVE_PATH).normalize();
+//            if (projectRootPath.toFile().exists()) {
+//                return projectRootPath.toString();
+//            }
+//        }
+//        return workspacePath.toString();
+//    }
+
 }