package com.ruoyi.web.controller.jarvis;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.jarvis.domain.JDOrder;
import com.ruoyi.jarvis.service.ITencentDocService;
import com.ruoyi.jarvis.service.IJDOrderService;
import com.ruoyi.jarvis.util.TencentDocDataParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.MediaType;

import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 腾讯文档控制器
 *
 * @author system
 */
@RestController
@RequestMapping("/jarvis/tendoc")
public class TencentDocController extends BaseController {

    private static final Logger log = LoggerFactory.getLogger(TencentDocController.class);

    @Autowired
    private ITencentDocService tencentDocService;

    @Autowired
    private IJDOrderService jdOrderService;

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private com.ruoyi.jarvis.service.ITencentDocTokenService tencentDocTokenService;

    @Autowired
    private com.ruoyi.jarvis.config.TencentDocConfig tencentDocConfig;

    @Autowired
    private com.ruoyi.jarvis.mapper.TencentDocOperationLogMapper operationLogMapper;

    @Autowired
    private com.ruoyi.jarvis.service.ITencentDocBatchPushService batchPushService;

    @Autowired
    private com.ruoyi.jarvis.service.ITencentDocDelayedPushService delayedPushService;

    @Autowired
    private com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient wxSendGoofishNotifyClient;

    /** 单次请求最大行数（腾讯文档 API：行数≤1000） */
    private static final int API_MAX_ROWS_PER_REQUEST = 200;
    /** 用 rowTotal 时接口实际单次只能读 200 行 */
    private static final int READ_ROWS_WHEN_USE_ROW_TOTAL = 200;
    /** 用 rowTotal 时 startRow = rowTotal - 200 必须大于此值（保证至少为 3，避免表头） */
    private static final int MIN_START_ROW_WHEN_USE_ROW_TOTAL = 3;
    /** 数据区 range 列尾（30 列 = A 到 AD，API 列数≤200） */
    private static final String DATA_RANGE_COL_END = "AD";
    /** 回溯行数：下次起始行 = 本次扫描终点 + 1 - 回溯，用于覆盖物流变更或延后补填 */
    private static final int BACKTRACK_ROWS = 80;
    /** 整批都跳过（0 写入）时使用的回溯行数，减小回溯以尽快扫到后续行（如 308） */
    private static final int BACKTRACK_ROWS_WHEN_ALL_SKIPPED = 20;
    /** 最小前进行数：本次至少“扫过”这么多行，下次起始行至少 = 本次起始 + 最小前进（避免空行多时原地打转） */
    private static final int MIN_ADVANCE_ROWS = 50;

    /** Redis key前缀，用于分布式锁 */
    private static final String TENCENT_DOC_LOCK_KEY = "tendoc:write:lock:";

    /**
     * 测试回调接口是否可访问（用于调试）
     */
    @Anonymous
    @GetMapping("/test-callback")
    public AjaxResult testCallback() {
        return AjaxResult.success("回调接口测试成功，路由配置正确");
    }

    /**
     * 获取授权URL（用于手动授权，获取token后通过setToken接口保存）
     */
    @GetMapping("/authUrl")
    public AjaxResult getAuthUrl() {
        try {
            String authUrl = tencentDocService.getAuthUrl();
            return AjaxResult.success("获取授权URL成功", authUrl);
        } catch (Exception e) {
            log.error("获取授权URL失败", e);
            return AjaxResult.error("获取授权URL失败: " + e.getMessage());
        }
    }

    /**
     * 手动设置访问令牌（用于首次授权后保存token）
     */
    @PostMapping("/setToken")
    public AjaxResult setToken(@RequestBody Map<String, Object> params) {
        try {
            String accessToken = (String) params.get("accessToken");
            String refreshToken = (String) params.get("refreshToken");
            Integer expiresIn = params.get("expiresIn") != null ?
                Integer.valueOf(params.get("expiresIn").toString()) : 7200;

            if (accessToken == null || accessToken.trim().isEmpty()) {
                return AjaxResult.error("accessToken不能为空");
            }

            // 通过反射调用setToken方法
            if (tencentDocTokenService instanceof com.ruoyi.jarvis.service.impl.TencentDocTokenServiceImpl) {
                ((com.ruoyi.jarvis.service.impl.TencentDocTokenServiceImpl) tencentDocTokenService)
                    .setToken(accessToken, refreshToken, expiresIn);
                return AjaxResult.success("访问令牌已保存");
            } else {
                return AjaxResult.error("Token服务类型不支持");
            }
        } catch (Exception e) {
            log.error("设置访问令牌失败", e);
            return AjaxResult.error("设置访问令牌失败: " + e.getMessage());
        }
    }

    /**
     * 测试获取用户信息接口
     */
    @GetMapping("/testUserInfo")
    public AjaxResult testUserInfo() {
        try {
            String token = tencentDocTokenService.getValidAccessToken();
            JSONObject userInfo = tencentDocService.getUserInfo(token);
            log.info("获取用户信息成功: {}", userInfo);
            return AjaxResult.success("获取用户信息成功", userInfo);
        } catch (Exception e) {
            log.error("测试获取用户信息失败", e);
            return AjaxResult.error("测试失败: " + e.getMessage());
        }
    }

    /**
     * 获取当前token状态
     */
    @GetMapping("/tokenStatus")
    public AjaxResult getTokenStatus() {
        try {
            String token;
            try {
                token = tencentDocTokenService.getValidAccessToken();
                return AjaxResult.success("访问令牌有效", new JSONObject().fluentPut("hasToken", true)
                    .fluentPut("token", token.substring(0, Math.min(20, token.length())) + "..."));
            } catch (Exception e) {
                return AjaxResult.success("访问令牌无效", new JSONObject().fluentPut("hasToken", false)
                    .fluentPut("message", e.getMessage()));
            }
        } catch (Exception e) {
            log.error("获取token状态失败", e);
            return AjaxResult.error("获取token状态失败: " + e.getMessage());
        }
    }

    /**
     * OAuth回调 - 通过授权码获取访问令牌
     * 根据腾讯文档官方文档：https://docs.qq.com/open/document/app/oauth2/authorize.html
     * 用户授权成功后，腾讯文档会重定向到此回调地址，并携带code和state参数
     * 返回HTML页面，自动关闭窗口并通知父窗口
     */
    @Anonymous
    @GetMapping(value = "/oauth/callback", produces = MediaType.TEXT_HTML_VALUE)
    public String oauthCallback(@RequestParam(value = "code", required = false) String code,
                                @RequestParam(value = "state", required = false) String state,
                                @RequestParam(value = "error", required = false) String error,
                                @RequestParam(value = "error_description", required = false) String errorDescription) {
        try {
            // 处理授权错误
            if (error != null) {
                log.error("腾讯文档授权失败 - error: {}, error_description: {}", error, errorDescription);
                String errorMsg = errorDescription != null ? errorDescription : error;
                return generateCallbackHtml(false, "授权失败: " + errorMsg, null);
            }

            // 验证授权码
            if (code == null || code.trim().isEmpty()) {
                log.error("授权码为空");
                return generateCallbackHtml(false, "授权码不能为空", null);
            }

            log.info("收到腾讯文档授权回调 - code: {}, state: {}", code, state);

            // 使用授权码换取access_token
            JSONObject tokenInfo = tencentDocService.getAccessTokenByCode(code);

            // 验证返回的token信息
            if (tokenInfo == null || !tokenInfo.containsKey("access_token")) {
                log.error("获取访问令牌失败 - 响应数据: {}", tokenInfo);
                return generateCallbackHtml(false, "获取访问令牌失败，响应数据格式不正确", null);
            }

            String accessToken = tokenInfo.getString("access_token");
            String refreshToken = tokenInfo.getString("refresh_token");
            Integer expiresIn = tokenInfo.getIntValue("expires_in");

            log.info("成功获取访问令牌 - access_token: {}", accessToken);

            // 自动保存token到后端
            try {
                if (tencentDocTokenService instanceof com.ruoyi.jarvis.service.impl.TencentDocTokenServiceImpl) {
                    ((com.ruoyi.jarvis.service.impl.TencentDocTokenServiceImpl) tencentDocTokenService)
                        .setToken(accessToken, refreshToken, expiresIn);
                    log.info("访问令牌已自动保存到后端缓存");
                }
            } catch (Exception e) {
                log.error("保存访问令牌失败", e);
                return generateCallbackHtml(false, "保存访问令牌失败: " + e.getMessage(), null);
            }

            return generateCallbackHtml(true, "授权成功，访问令牌已自动保存", null);
        } catch (Exception e) {
            log.error("OAuth回调处理失败", e);
            return generateCallbackHtml(false, "授权失败: " + e.getMessage(), null);
        }
    }

    /**
     * 生成回调HTML页面
     */
    private String generateCallbackHtml(boolean success, String message, Object data) {
        StringBuilder html = new StringBuilder();
        html.append("<!DOCTYPE html>");
        html.append("<html lang='zh-CN'>");
        html.append("<head>");
        html.append("<meta charset='UTF-8'>");
        html.append("<meta name='viewport' content='width=device-width, initial-scale=1.0'>");
        html.append("<title>腾讯文档授权").append(success ? "成功" : "失败").append("</title>");
        html.append("<style>");
        html.append("body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; ");
        html.append("display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; ");
        html.append("background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }");
        html.append(".container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); text-align: center; max-width: 400px; }");
        html.append(".icon { font-size: 64px; margin-bottom: 20px; }");
        html.append(".success { color: #52c41a; }");
        html.append(".error { color: #ff4d4f; }");
        html.append(".message { font-size: 16px; color: #333; margin-bottom: 20px; line-height: 1.6; }");
        html.append("</style>");
        html.append("</head>");
        html.append("<body>");
        html.append("<div class='container'>");
        if (success) {
            html.append("<div class='icon success'>✓</div>");
            html.append("<h2 style='color: #52c41a; margin-bottom: 10px;'>授权成功</h2>");
        } else {
            html.append("<div class='icon error'>✗</div>");
            html.append("<h2 style='color: #ff4d4f; margin-bottom: 10px;'>授权失败</h2>");
        }
        html.append("<div class='message'>").append(message).append("</div>");
        html.append("<p style='color: #999; font-size: 14px;'>窗口将在3秒后自动关闭...</p>");
        html.append("</div>");
        html.append("<script>");
        html.append("// 通知父窗口授权结果");
        html.append("if (window.opener) {");
        html.append("  window.opener.postMessage({");
        html.append("    type: 'tendoc_oauth_callback',");
        html.append("    success: ").append(success).append(",");
        html.append("    message: '").append(message.replace("'", "\\'").replace("\n", "\\n").replace("\r", "\\r")).append("'");
        html.append("  }, '*');");
        html.append("}");
        html.append("// 3秒后自动关闭窗口");
        html.append("setTimeout(function() {");
        html.append("  if (window.opener) {");
        html.append("    window.close();");
        html.append("  } else {");
        html.append("    window.location.href = '/';");
        html.append("  }");
        html.append("}, 3000);");
        html.append("</script>");
        html.append("</body>");
        html.append("</html>");
        return html.toString();
    }

    /**
     * 刷新访问令牌
     */
    @PostMapping("/refreshToken")
    public AjaxResult refreshToken(@RequestBody Map<String, String> params) {
        try {
            String refreshToken = params.get("refreshToken");
            if (refreshToken == null || refreshToken.isEmpty()) {
                return AjaxResult.error("refreshToken不能为空");
            }

            JSONObject tokenInfo = tencentDocService.refreshAccessToken(refreshToken);
            return AjaxResult.success("刷新令牌成功", tokenInfo);
        } catch (Exception e) {
            log.error("刷新访问令牌失败", e);
            return AjaxResult.error("刷新令牌失败: " + e.getMessage());
        }
    }

    /**
     * 将订单物流信息上传到腾讯文档表格
     */
    @PostMapping("/uploadLogistics")
    public AjaxResult uploadLogistics(@RequestBody Map<String, Object> params) {
        try {
            String accessToken = (String) params.get("accessToken");
            String fileId = (String) params.get("fileId");
            String sheetId = (String) params.get("sheetId");

            if (accessToken == null || fileId == null || sheetId == null) {
                return AjaxResult.error("accessToken、fileId和sheetId不能为空");
            }

            // 获取订单ID列表
            @SuppressWarnings("unchecked")
            List<Long> orderIds = (List<Long>) params.get("orderIds");

            if (orderIds == null || orderIds.isEmpty()) {
                return AjaxResult.error("订单ID列表不能为空");
            }

            // 查询订单信息
            List<JDOrder> orders = new java.util.ArrayList<>();
            for (Long orderId : orderIds) {
                JDOrder order = jdOrderService.selectJDOrderById(orderId);
                if (order != null) {
                    orders.add(order);
                }
            }

            if (orders.isEmpty()) {
                return AjaxResult.error("未找到有效的订单信息");
            }

            JSONObject result = tencentDocService.uploadLogisticsToSheet(accessToken, fileId, sheetId, orders);
            return AjaxResult.success("上传物流信息成功", result);
        } catch (Exception e) {
            log.error("上传物流信息失败", e);
            return AjaxResult.error("上传物流信息失败: " + e.getMessage());
        }
    }


    /**
     * 将单个订单的物流信息追加到表格
     */
    @PostMapping("/appendLogistics")
    public AjaxResult appendLogistics(@RequestBody Map<String, Object> params) {
        try {
            String accessToken = (String) params.get("accessToken");
            String fileId = (String) params.get("fileId");
            String sheetId = (String) params.get("sheetId");
            Long orderId = params.get("orderId") != null ? Long.valueOf(params.get("orderId").toString()) : null;
            // 起始行号，默认为3
            Integer startRow = params.get("startRow") != null ? Integer.valueOf(params.get("startRow").toString()) : 3;

            if (accessToken == null || fileId == null || sheetId == null || orderId == null) {
                return AjaxResult.error("accessToken、fileId、sheetId和orderId不能为空");
            }

            // 查询订单信息
            JDOrder order = jdOrderService.selectJDOrderById(orderId);
            if (order == null) {
                return AjaxResult.error("未找到订单信息");
            }

            JSONObject result = tencentDocService.appendLogisticsToSheet(accessToken, fileId, sheetId, startRow, order);
            return AjaxResult.success("追加物流信息成功", result);
        } catch (Exception e) {
            log.error("追加物流信息失败", e);
            return AjaxResult.error("追加物流信息失败: " + e.getMessage());
        }
    }

    /**
     * 读取表格数据
     */
    @GetMapping("/readSheet")
    public AjaxResult readSheet(@RequestParam("accessToken") String accessToken,
                                @RequestParam("fileId") String fileId,
                                @RequestParam("sheetId") String sheetId,
                                @RequestParam(value = "range", defaultValue = "A1:Z100") String range) {
        try {
            JSONObject result = tencentDocService.readSheetData(accessToken, fileId, sheetId, range);
            return AjaxResult.success("读取表格数据成功", result);
        } catch (Exception e) {
            log.error("读取表格数据失败", e);
            return AjaxResult.error("读取表格数据失败: " + e.getMessage());
        }
    }

    /**
     * 获取文件信息
     */
    @GetMapping("/fileInfo")
    public AjaxResult getFileInfo(@RequestParam("accessToken") String accessToken,
                                  @RequestParam("fileId") String fileId) {
        try {
            JSONObject result = tencentDocService.getFileInfo(accessToken, fileId);
            return AjaxResult.success("获取文件信息成功", result);
        } catch (Exception e) {
            log.error("获取文件信息失败", e);
            return AjaxResult.error("获取文件信息失败: " + e.getMessage());
        }
    }

    /**
     * 获取工作表列表
     */
    @GetMapping("/sheetList")
    public AjaxResult getSheetList(@RequestParam("accessToken") String accessToken,
                                   @RequestParam("fileId") String fileId) {
        try {
            JSONObject result = tencentDocService.getSheetList(accessToken, fileId);
            return AjaxResult.success("获取工作表列表成功", result);
        } catch (Exception e) {
            log.error("获取工作表列表失败", e);
            return AjaxResult.error("获取工作表列表失败: " + e.getMessage());
        }
    }

    /**
     * 自动发货 - 将指定订单的物流信息上传到腾讯文档并标记为已发货
     */
    @PostMapping("/autoShip")
    public AjaxResult autoShip(@RequestBody Map<String, Object> params) {
        try {
            String accessToken = (String) params.get("accessToken");
            String fileId = (String) params.get("fileId");
            String sheetId = (String) params.get("sheetId");
            Long orderId = params.get("orderId") != null ? Long.valueOf(params.get("orderId").toString()) : null;
            // 起始行号，默认为3
            Integer startRow = params.get("startRow") != null ? Integer.valueOf(params.get("startRow").toString()) : 3;

            if (accessToken == null || fileId == null || sheetId == null || orderId == null) {
                return AjaxResult.error("accessToken、fileId、sheetId和orderId不能为空");
            }

            // 查询订单信息
            JDOrder order = jdOrderService.selectJDOrderById(orderId);
            if (order == null) {
                return AjaxResult.error("未找到订单信息");
            }

            // 检查是否有物流链接
            if (order.getLogisticsLink() == null || order.getLogisticsLink().trim().isEmpty()) {
                return AjaxResult.error("订单缺少物流链接，无法自动发货");
            }

            // 上传物流信息到腾讯文档
            JSONObject uploadResult = tencentDocService.appendLogisticsToSheet(accessToken, fileId, sheetId, startRow, order);

            // 更新订单状态为已发货（可选）
            // order.setStatus("已发货");
            // jdOrderService.updateJDOrder(order);

            JSONObject result = new JSONObject();
            result.put("uploadResult", uploadResult);
            result.put("orderId", orderId);
            result.put("message", "物流信息已上传到腾讯文档，自动发货成功");

            return AjaxResult.success("自动发货成功", result);
        } catch (Exception e) {
            log.error("自动发货失败", e);
            return AjaxResult.error("自动发货失败: " + e.getMessage());
        }
    }

    /**
     * 直接填充单个订单的物流链接
     *
     * @param params 包含 thirdPartyOrderNo（第三方单号）和 logisticsLink（物流链接）
     * @return 填充结果
     */
    @PostMapping("/fillSingleLogistics")
    public AjaxResult fillSingleLogistics(@RequestBody Map<String, Object> params) {
        String thirdPartyOrderNo = null;
        String fileId = null;
        String sheetId = null;
        String lockKey = null;

        try {
            // 1. 获取参数
            thirdPartyOrderNo = (String) params.get("thirdPartyOrderNo");
            String logisticsLink = (String) params.get("logisticsLink");
            Boolean forceRePush = params.get("forceRePush") != null ? (Boolean) params.get("forceRePush") : false;

            if (thirdPartyOrderNo == null || thirdPartyOrderNo.isEmpty()) {
                return AjaxResult.error("第三方单号不能为空");
            }
            if (logisticsLink == null || logisticsLink.isEmpty()) {
                return AjaxResult.error("物流链接不能为空");
            }

            // 2. 检查订单是否已推送（防止重复推送）
            JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(thirdPartyOrderNo);
            if (order == null) {
                logOperation(null, null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "FAILED", "订单不存在");
                return AjaxResult.error("订单不存在：" + thirdPartyOrderNo);
            }

            // 如果已推送且不是强制重推，则拒绝
            if (order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1 && !forceRePush) {
                String pushTimeStr = order.getTencentDocPushTime() != null ?
                    new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(order.getTencentDocPushTime()) : "未知";
                logOperation(null, null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "SKIPPED", String.format("订单已推送，推送时间: %s", pushTimeStr));
                return AjaxResult.error(String.format("该订单已推送到腾讯文档（推送时间：%s），请勿重复操作！如需重新推送，请使用强制推送功能。", pushTimeStr));
            }

            // 3. 获取访问令牌
            String accessToken;
            try {
                accessToken = tencentDocTokenService.getValidAccessToken();
            } catch (Exception e) {
                logOperation(null, null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "FAILED", "访问令牌无效: " + e.getMessage());
                return AjaxResult.error("访问令牌无效，请先完成授权");
            }

            // 3. 从配置中读取文档信息
            final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:";
            fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId");
            sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId");
            Integer headerRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow");
            Integer configStartRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "startRow");

            if (fileId == null || fileId.isEmpty()) {
                fileId = tencentDocConfig.getFileId();
            }
            if (sheetId == null || sheetId.isEmpty()) {
                sheetId = tencentDocConfig.getSheetId();
            }
            if (headerRow == null) {
                headerRow = tencentDocConfig.getHeaderRow();
            }
            if (configStartRow == null) {
                configStartRow = tencentDocConfig.getStartRow();
            }

            if (fileId == null || fileId.isEmpty() || sheetId == null || sheetId.isEmpty()) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "FAILED", "文档配置不完整");
                return AjaxResult.error("文档配置不完整，请先完成配置");
            }

            // 4. 获取分布式锁（针对该订单单号+文档+工作表）
            lockKey = TENCENT_DOC_LOCK_KEY + fileId + ":" + sheetId + ":" + thirdPartyOrderNo;

            // 检查锁是否已存在
            String existingLock = redisCache.getCacheObject(lockKey);
            boolean lockAcquired = (existingLock == null);

            if (!lockAcquired) {
                log.warn("获取锁失败，该订单正在被其他请求处理：{}", thirdPartyOrderNo);
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "FAILED", "获取分布式锁失败，该订单正在处理中");
                return AjaxResult.error("该订单正在处理中，请稍后再试");
            }

            // 设置锁
            redisCache.setCacheObject(lockKey, "locked", 30, TimeUnit.SECONDS);
            log.info("✓ 获取分布式锁成功 - 单号: {}, lockKey: {}", thirdPartyOrderNo, lockKey);

            // 5. 读取表头（从配置的 headerRow 读取，与 startRow 独立）
            String headerRange = String.format("A%d:Z%d", headerRow, headerRow);

            JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange);
            if (headerData == null || !headerData.containsKey("gridData")) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "FAILED", "无法读取表头数据");
                return AjaxResult.error("无法读取表头数据");
            }

            // 6. 解析表头，找到"单号"和"物流单号"列
            JSONObject gridData = headerData.getJSONObject("gridData");
            JSONArray rows = gridData.getJSONArray("rows");
            if (rows == null || rows.isEmpty()) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "FAILED", "表头数据为空");
                return AjaxResult.error("表头数据为空");
            }

            JSONObject headerRowData = rows.getJSONObject(0);
            JSONArray headerCells = headerRowData.getJSONArray("values");

            int orderNoColumn = -1;
            int logisticsColumn = -1;

            for (int i = 0; i < headerCells.size(); i++) {
                JSONObject cell = headerCells.getJSONObject(i);
                if (cell.containsKey("cellValue")) {
                    String cellText = cell.getJSONObject("cellValue").getString("text");
                    if (cellText != null) {
                        // 「物流单号」也含「单号」，须排除，否则会误把物流列当成单号列
                        if (cellText.contains("单号") && !cellText.contains("物流")) {
                            orderNoColumn = i;
                        } else if (cellText.contains("物流")) {
                            logisticsColumn = i;
                        }
                    }
                }
            }

            if (orderNoColumn == -1 || logisticsColumn == -1) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "FAILED", "未找到'单号'或'物流'列");
                return AjaxResult.error("未找到「单号/客户单号/第三方单号」或「物流」列，请检查表头配置");
            }

            log.info("表头解析完成 - 单号列: {}, 物流列: {}", orderNoColumn, logisticsColumn);

            // 7. 读取数据区域，查找指定单号
            String dataRange = String.format("A%d:Z%d", configStartRow, configStartRow + 999);
            JSONObject data = tencentDocService.readSheetData(accessToken, fileId, sheetId, dataRange);

            if (data == null || !data.containsKey("gridData")) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "FAILED", "无法读取数据区域");
                return AjaxResult.error("无法读取数据区域");
            }

            JSONObject dataGridData = data.getJSONObject("gridData");
            JSONArray dataRows = dataGridData.getJSONArray("rows");

            if (dataRows == null || dataRows.isEmpty()) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "FAILED", "数据区域为空");
                return AjaxResult.error("数据区域为空");
            }

            // 8. 查找匹配的单号
            int targetRow = -1;
            for (int i = 0; i < dataRows.size(); i++) {
                JSONObject row = dataRows.getJSONObject(i);
                JSONArray cells = row.getJSONArray("values");

                if (cells != null && cells.size() > orderNoColumn) {
                    JSONObject orderNoCell = cells.getJSONObject(orderNoColumn);
                    if (orderNoCell.containsKey("cellValue")) {
                        String cellText = orderNoCell.getJSONObject("cellValue").getString("text");
                        if (thirdPartyOrderNo.equals(cellText)) {
                            targetRow = configStartRow + i;
                            break;
                        }
                    }
                }
            }

            if (targetRow == -1) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
                    "FAILED", "未找到单号");
                return AjaxResult.error("未找到单号: " + thirdPartyOrderNo);
            }

            log.info("找到单号 {} 在第 {} 行", thirdPartyOrderNo, targetRow);

            // 9. 再次读取该行数据，验证单号和物流链接列是否为空（防止并发覆盖）
            String verifyRange = String.format("A%d:Z%d", targetRow, targetRow);
            JSONObject verifyData = tencentDocService.readSheetData(accessToken, fileId, sheetId, verifyRange);

            if (verifyData == null || !verifyData.containsKey("gridData")) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
                    "FAILED", "验证读取失败");
                return AjaxResult.error("验证读取失败");
            }

            JSONObject verifyGridData = verifyData.getJSONObject("gridData");
            JSONArray verifyRows = verifyGridData.getJSONArray("rows");

            if (verifyRows == null || verifyRows.isEmpty()) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
                    "FAILED", "验证行数据为空");
                return AjaxResult.error("验证行数据为空");
            }

            JSONObject verifyRowData = verifyRows.getJSONObject(0);
            JSONArray verifyCells = verifyRowData.getJSONArray("values");

            // 验证单号是否仍然匹配
            if (verifyCells == null || verifyCells.size() <= orderNoColumn) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
                    "SKIPPED", "验证时单号列为空，行已变化");
                return AjaxResult.error("验证失败：行数据已变化，单号列为空");
            }

            JSONObject verifyOrderNoCell = verifyCells.getJSONObject(orderNoColumn);
            String verifyOrderNo = null;
            if (verifyOrderNoCell.containsKey("cellValue")) {
                verifyOrderNo = verifyOrderNoCell.getJSONObject("cellValue").getString("text");
            }

            if (!thirdPartyOrderNo.equals(verifyOrderNo)) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
                    "SKIPPED", String.format("验证失败：单号不匹配，期望 %s，实际 %s", thirdPartyOrderNo, verifyOrderNo));
                return AjaxResult.error("验证失败：单号不匹配，行数据已变化");
            }

            // 验证物流链接列是否为空
            if (verifyCells.size() > logisticsColumn) {
                JSONObject logisticsCell = verifyCells.getJSONObject(logisticsColumn);
                if (logisticsCell.containsKey("cellValue")) {
                    String existingLogistics = logisticsCell.getJSONObject("cellValue").getString("text");
                    if (existingLogistics != null && !existingLogistics.trim().isEmpty()) {
                        logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
                            "SKIPPED", String.format("物流链接列已有值：%s", existingLogistics));
                        return AjaxResult.error(String.format("该订单物流链接已存在：%s", existingLogistics));
                    }
                }
            }

            log.info("✓ 验证通过 - 单号匹配且物流列为空，可以写入");

            // 10. 获取用户信息（获取 openId）
            JSONObject userInfo = com.ruoyi.jarvis.util.TencentDocApiUtil.getUserInfo(accessToken);
            JSONObject userData = userInfo.getJSONObject("data");
            if (userData == null) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
                    "FAILED", "无法获取用户数据");
                return AjaxResult.error("无法获取用户数据");
            }
            String openId = userData.getString("openID");
            if (openId == null || openId.isEmpty()) {
                logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
                    "FAILED", "无法获取Open-Id");
                return AjaxResult.error("无法获取Open-Id");
            }

            // 11. 写入物流链接
            com.ruoyi.jarvis.util.TencentDocApiUtil.writeSheetData(
                accessToken,
                tencentDocConfig.getAppId(),
                openId,
                fileId,
                sheetId,
                String.format("%s%d", getColumnLetter(logisticsColumn), targetRow),
                logisticsLink,
                tencentDocConfig.getApiBaseUrl()
            );

            // 12. 记录成功日志
            logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
                "SUCCESS", null);

            log.info("✓ 物流链接写入成功 - 单号: {}, 行: {}, 链接: {}", thirdPartyOrderNo, targetRow, logisticsLink);

            // 13. 更新订单的推送状态（标记为已推送）
            try {
                order.setTencentDocPushed(1);
                order.setTencentDocPushTime(new java.util.Date());
                jdOrderService.updateJDOrder(order);
                log.info("✓ 订单推送状态已更新 - 单号: {}", thirdPartyOrderNo);
            } catch (Exception e) {
                log.error("更新订单推送状态失败，但写入腾讯文档已成功", e);
                // 不影响主流程，继续返回成功
            }

            // 14. 发送微信推送通知（手动执行）
            try {
                Map<String, Object> successLog = new java.util.HashMap<>();
                successLog.put("orderNo", thirdPartyOrderNo);
                successLog.put("row", targetRow);
                successLog.put("logisticsLink", logisticsLink);

                List<Map<String, Object>> successLogs = new java.util.ArrayList<>();
                successLogs.add(successLog);

                // 手动执行时，batchId为null
                sendWeChatNotification(successLogs, 1, 0, 0, null, fileId, sheetId, true, null, null);
            } catch (Exception e) {
                log.error("发送微信推送失败", e);
                // 不影响主流程，继续返回成功
            }

            JSONObject result = new JSONObject();
            result.put("thirdPartyOrderNo", thirdPartyOrderNo);
            result.put("logisticsLink", logisticsLink);
            result.put("row", targetRow);
            result.put("column", logisticsColumn);
            result.put("pushed", true);
            result.put("pushTime", order.getTencentDocPushTime());

            return AjaxResult.success("物流链接填充成功", result);

        } catch (Exception e) {
            log.error("填充物流链接失败", e);
            logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, null,
                "FAILED", "异常: " + e.getMessage());
            return AjaxResult.error("填充物流链接失败: " + e.getMessage());
        } finally {
            // 释放分布式锁
            if (lockKey != null) {
                try {
                    redisCache.deleteObject(lockKey);
                    log.info("✓ 释放分布式锁 - lockKey: {}", lockKey);
                } catch (Exception e) {
                    log.error("释放分布式锁失败", e);
                }
            }
        }
    }

    /**
     * 记录操作日志到数据库
     */
    private void logOperation(String batchId, String fileId, String sheetId, String operationType,
                              String orderNo, Integer targetRow, String logisticsLink,
                              String status, String errorMessage) {
        try {
            com.ruoyi.jarvis.domain.TencentDocOperationLog log = new com.ruoyi.jarvis.domain.TencentDocOperationLog();
            log.setBatchId(batchId);
            log.setFileId(fileId);
            log.setSheetId(sheetId);
            log.setOperationType(operationType);
            log.setOrderNo(orderNo);
            log.setTargetRow(targetRow);
            log.setLogisticsLink(logisticsLink);
            log.setOperationStatus(status);
            log.setErrorMessage(errorMessage);
            // 异步/定时任务线程无登录上下文，使用 fallback 避免获取用户异常
            String operator;
            try {
                operator = getUsername();
            } catch (Exception ignored) {
                operator = "system";
            }
            log.setOperator(operator);
            operationLogMapper.insertLog(log);
        } catch (Exception e) {
            TencentDocController.log.error("记录操作日志失败", e);
        }
    }

    /**
     * 查询操作日志列表
     */
    @GetMapping("/operationLogs")
    public AjaxResult getOperationLogs(com.ruoyi.jarvis.domain.TencentDocOperationLog query) {
        try {
            List<com.ruoyi.jarvis.domain.TencentDocOperationLog> list = operationLogMapper.selectLogList(query);
            return AjaxResult.success(list);
        } catch (Exception e) {
            log.error("查询操作日志失败", e);
            return AjaxResult.error("查询操作日志失败: " + e.getMessage());
        }
    }

    /**
     * 查询最近的操作日志
     */
    @GetMapping("/recentLogs")
    public AjaxResult getRecentLogs(@RequestParam(required = false) String fileId,
                                     @RequestParam(defaultValue = "50") int limit) {
        try {
            List<com.ruoyi.jarvis.domain.TencentDocOperationLog> list = operationLogMapper.selectRecentLogs(fileId, limit);
            return AjaxResult.success(list);
        } catch (Exception e) {
            log.error("查询最近操作日志失败", e);
            return AjaxResult.error("查询最近操作日志失败: " + e.getMessage());
        }
    }

    /**
     * 从文本中提取手机号码
     * 支持11位手机号码，可能包含空格、横线等分隔符
     *
     * @param text 文本信息（可以是备注、状态等字段）
     * @return 提取到的手机号码，如果没有则返回null
     */
    private String extractPhoneFromRemark(String text) {
        if (text == null || text.trim().isEmpty()) {
            log.debug("文本为空，无法提取手机号码");
            return null;
        }

        log.debug("原始文本: [{}]", text);

        // 移除所有空格、横线、括号等分隔符
        String cleanedText = text.replaceAll("[\\s\\-\\(\\)\\[\\]（）\\【\\】]", "");
        log.debug("清理后文本: [{}]", cleanedText);

        // 匹配11位手机号码（1开头的11位数字）
        java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("1[3-9]\\d{9}");
        java.util.regex.Matcher matcher = pattern.matcher(cleanedText);

        if (matcher.find()) {
            String phone = matcher.group();
            log.info("成功提取手机号码: [{}] <- 原文本: [{}]", phone, text);
            return phone;
        }

        log.debug("未找到匹配的手机号码，文本: [{}]", text);
        return null;
    }

    /**
     * 批量同步中出现报错时，将摘要推送到企业微信（wxSend 闲鱼应用通道）
     */
    private void pushTencentDocRowErrorsToWeCom(String batchId, String fileId, String sheetId,
                                                int filledCount, int skippedCount, int errorCount,
                                                List<Map<String, Object>> errorLogs) {
        if (errorCount <= 0 && (errorLogs == null || errorLogs.isEmpty())) {
            return;
        }
        StringBuilder sb = new StringBuilder();
        sb.append("【腾讯文档推送】同步存在报错\n");
        if (batchId != null && !batchId.isEmpty()) {
            sb.append("批次: ").append(batchId).append("\n");
        }
        if (fileId != null && !fileId.isEmpty()) {
            sb.append("fileId: ").append(fileId).append("\n");
        }
        if (sheetId != null && !sheetId.isEmpty()) {
            sb.append("sheetId: ").append(sheetId).append("\n");
        }
        sb.append(String.format("成功队列: %d, 跳过: %d, 错误: %d\n", filledCount, skippedCount, errorCount));
        if (errorLogs != null && !errorLogs.isEmpty()) {
            int max = Math.min(15, errorLogs.size());
            for (int i = 0; i < max; i++) {
                Map<String, Object> el = errorLogs.get(i);
                Object on = el != null ? el.get("orderNo") : null;
                Object row = el != null ? el.get("row") : null;
                Object em = el != null ? el.get("errorMessage") : null;
                Object et = el != null ? el.get("errorType") : null;
                sb.append(String.format("%d. 单号:%s 行:%s", i + 1,
                    on != null ? on : "-", row != null ? row : "-"));
                if (et != null && String.valueOf(et).length() > 0) {
                    sb.append(" ").append(et);
                }
                sb.append("\n");
                if (em != null) {
                    String msg = String.valueOf(em);
                    if (msg.length() > 120) {
                        msg = msg.substring(0, 119) + "…";
                    }
                    sb.append("   ").append(msg).append("\n");
                }
            }
            if (errorLogs.size() > max) {
                sb.append("… 共 ").append(errorLogs.size()).append(" 条错误\n");
            }
        }
        wxSendGoofishNotifyClient.pushGoofishAgentText(null, "", sb.toString());
    }

    /**
     * 合并同一行的腾讯文档填充任务（物流更新与仅补京东单号合并为一次 batchUpdate）
     */
    private void mergeTencentDocRowFillUpdate(JSONObject target, JSONObject extra) {
        if (extra.containsKey("logisticsLink")) {
            target.put("logisticsLink", extra.getString("logisticsLink"));
            target.remove("isJdOrderIdOnlyUpdate");
        }
        if (Boolean.TRUE.equals(extra.getBoolean("isLinkUpdated"))) {
            target.put("isLinkUpdated", true);
            if (extra.containsKey("oldLogisticsLink")) {
                target.put("oldLogisticsLink", extra.getString("oldLogisticsLink"));
            }
            if (extra.containsKey("newLogisticsLink")) {
                target.put("newLogisticsLink", extra.getString("newLogisticsLink"));
            }
        }
        if (extra.containsKey("phone")) {
            target.put("phone", extra.getString("phone"));
        }
        if (extra.containsKey("phoneColumn")) {
            target.put("phoneColumn", extra.getInteger("phoneColumn"));
        }
        if (extra.containsKey("jdOrderId")) {
            target.put("jdOrderId", extra.getString("jdOrderId"));
        }
        if (extra.containsKey("jdOrderIdColumn")) {
            target.put("jdOrderIdColumn", extra.getInteger("jdOrderIdColumn"));
        }
        if (Boolean.TRUE.equals(extra.getBoolean("isJdOrderIdOnlyUpdate")) && !target.containsKey("logisticsLink")) {
            target.put("isJdOrderIdOnlyUpdate", true);
        }
    }

    /**
     * 批量同步物流链接 - 读取表格数据，根据单号查询订单系统中的物流链接，并填充到表格
     * 优化：记录上次处理的最大行数，每次从最大行数-100开始读取，避免重复处理历史数据
     * 自动获取和管理访问令牌，点击同步时自动刷新token
     */
    @Anonymous
    @PostMapping("/fillLogisticsByOrderNo")
    public AjaxResult fillLogisticsByOrderNo(@RequestBody Map<String, Object> params) {
        String batchId = null;
        try {
            // 直接尝试刷新token（如果失败，说明需要首次授权）
            String accessToken;
            try {
                accessToken = tencentDocTokenService.refreshAccessToken();
                log.info("成功刷新访问令牌");
            } catch (Exception e) {
                log.error("刷新访问令牌失败", e);
                // 如果刷新失败，尝试获取缓存的token
                try {
                    accessToken = tencentDocTokenService.getValidAccessToken();
                } catch (Exception e2) {
                    return AjaxResult.error("访问令牌无效，请先完成授权。获取授权URL: GET /jarvis/tendoc/authUrl");
                }
            }

            // 从参数获取批次ID（如果是批量调用会传入）
            batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null;

            // 从参数或配置中获取文档信息
            String fileId = (String) params.get("fileId");
            String sheetId = (String) params.get("sheetId");

            // 如果前端没有传fileId/sheetId，则从配置中读取
            final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:";
            if (fileId == null || fileId.isEmpty()) {
                fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId");
                if (fileId == null || fileId.isEmpty()) {
                    fileId = tencentDocConfig.getFileId();
                }
            }
            if (sheetId == null || sheetId.isEmpty()) {
                sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId");
                if (sheetId == null || sheetId.isEmpty()) {
                    sheetId = tencentDocConfig.getSheetId();
                }
            }

            // 从配置中读取表头行（仅用于读表头，不参与范围计算）
            Integer headerRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow");
            if (headerRow == null) {
                headerRow = tencentDocConfig.getHeaderRow();
            }

            // 是否跳过已推送的订单（默认true，防止重复推送）
            Boolean skipPushedOrders = params.get("skipPushedOrders") != null ?
                Boolean.valueOf(params.get("skipPushedOrders").toString()) : true;

            if (accessToken == null || fileId == null || sheetId == null) {
                return AjaxResult.error("文档配置不完整，请先配置 fileId 和 sheetId");
            }

            // 每次从接口获取当前行数，无任何缓存。取「最后 200 行」：startRow = rowCount - 199，endRow = rowCount
            int rowCount = tencentDocService.getSheetRowTotal(accessToken, fileId, sheetId);
            if (rowCount <= 0) {
                log.warn("接口未返回有效行数，无法执行填充");
                return AjaxResult.error("无法获取表格行数，请检查文档与授权");
            }
            int startRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL, rowCount - (READ_ROWS_WHEN_USE_ROW_TOTAL - 1));
            int endRow = rowCount;

            // 如果batchId为空，创建批量推送记录（用于日志）
            if (batchId == null || batchId.trim().isEmpty()) {
                batchId = batchPushService.createBatchPushRecord(
                    fileId,
                    sheetId,
                    "MANUAL",
                    "MANUAL_TRIGGER",
                    startRow,
                    endRow
                );
                log.info("未提供batchId，自动创建批量推送记录: {}", batchId);
            }

            log.info("同步物流 - fileId: {}, sheetId: {}, 接口行数: {}, 本次范围: 第 {} ~ {} 行（共 {} 行）",
                fileId, sheetId, rowCount, startRow, endRow, endRow - startRow + 1);

            // 读取表格数据（先读取表头行用于识别列位置）
            // 根据官方文档，使用 A1 表示法（Excel格式）
            // 参考：https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_range.html
            // 与数据区列宽一致，避免「京东下单订单号」在 Z 列之后时无法识别表头
            String headerRange = String.format("A%d:%s%d", headerRow, DATA_RANGE_COL_END, headerRow);
            log.info("读取表头 - 行号: {}, range: {}", headerRow, headerRange);

            JSONObject headerData = null;
            try {
                headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange);
                log.info("表头数据响应: {}", headerData != null ? headerData.toJSONString() : "null");
            } catch (Exception e) {
                log.error("读取表头失败", e);
                return AjaxResult.error("读取表头失败: " + e.getMessage());
            }

            if (headerData == null) {
                return AjaxResult.error("读取表头返回null，请检查Access Token是否有效或文档权限");
            }

            JSONArray headerValues = headerData.getJSONArray("values");

            if (headerValues == null || headerValues.isEmpty()) {
                log.error("表头数据中values数组为空，完整响应: {}", headerData.toJSONString());
                return AjaxResult.error("无法读取表头，请检查headerRow参数。API响应: " + headerData.toJSONString());
            }

            // 自动识别列位置（从表头中识别）
            Integer orderNoColumn = null;       // 第三方「单号」列（排除京东下单订单号、物流单号）
            Integer jdPlaceOrderNoColumn = null; // 「京东下单订单号」列，对应本地 orderId
            Integer logisticsLinkColumn = null; // "物流单号"列
            Integer remarkColumn = null;        // "备注"列
            Integer arrangedColumn = null;      // "是否安排"列
            Integer markColumn = null;          // "标记"列
            Integer phoneColumn = null;         // "下单电话"列

            JSONArray headerRowData = headerValues.getJSONArray(0);
            if (headerRowData == null || headerRowData.isEmpty()) {
                return AjaxResult.error("无法识别表头，表头数据为空");
            }

            // 列名须与表格完全一致（仅忽略首尾空白、不间断空格等）：备注、是否安排、物流单号、下单电话、标记、京东下单订单号；另需「单号」「客户单号」或「第三方单号」之一
            log.info("开始识别表头列（完全匹配列名），共 {} 列", headerRowData.size());
            for (int i = 0; i < headerRowData.size(); i++) {
                String cellValue = headerRowData.getString(i);
                if (cellValue == null) {
                    continue;
                }
                String norm = TencentDocDataParser.normalizeTencentDocHeader(cellValue);
                log.debug("列 {} 原始: [{}] 规范化: [{}]", i, cellValue.trim(), norm);

                if (jdPlaceOrderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "京东下单订单号")) {
                    jdPlaceOrderNoColumn = i;
                    log.info("✓ 列名完全匹配「京东下单订单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "单号")) {
                    orderNoColumn = i;
                    log.info("✓ 列名完全匹配「单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "客户单号")) {
                    orderNoColumn = i;
                    log.info("\u2713 列名完全匹配「客户单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
                    orderNoColumn = i;
                    log.info("✓ 列名完全匹配「第三方单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流单号")) {
                    logisticsLinkColumn = i;
                    log.info("✓ 列名完全匹配「物流单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流链接")) {
                    logisticsLinkColumn = i;
                    log.warn("✓ 列名匹配「物流链接」（建议改为「物流单号」）：第 {} 列（索引{}）", i + 1, i);
                }
                if (remarkColumn == null && TencentDocDataParser.headerEquals(cellValue, "备注")) {
                    remarkColumn = i;
                    log.info("✓ 列名完全匹配「备注」：第 {} 列（索引{}）", i + 1, i);
                }
                if (arrangedColumn == null && TencentDocDataParser.headerEquals(cellValue, "是否安排")) {
                    arrangedColumn = i;
                    log.info("✓ 列名完全匹配「是否安排」：第 {} 列（索引{}）", i + 1, i);
                }
                if (markColumn == null && TencentDocDataParser.headerEquals(cellValue, "标记")) {
                    markColumn = i;
                    log.info("✓ 列名完全匹配「标记」：第 {} 列（索引{}）", i + 1, i);
                }
                if (phoneColumn == null && TencentDocDataParser.headerEquals(cellValue, "下单电话")) {
                    phoneColumn = i;
                    log.info("✓ 列名完全匹配「下单电话」：第 {} 列（索引{}）", i + 1, i);
                }
            }
            log.info("表头列识别完成");

            // 检查必需的列是否都已识别
            if (orderNoColumn == null) {
                return AjaxResult.error("无法找到列名完全为「单号」「客户单号」或「第三方单号」的列（请与表格列名一致，勿加空格或后缀）");
            }
            if (logisticsLinkColumn == null) {
                return AjaxResult.error("无法找到列名完全为「物流单号」的列（兼容「物流链接」）；请与表格列名一致");
            }

            // 提示可选列的识别情况
            if (remarkColumn == null) {
                log.warn("未找到'备注'列，将跳过该字段的更新");
            }
            if (arrangedColumn == null) {
                log.warn("未找到'是否安排'列，将跳过该字段的更新");
            }
            if (markColumn == null) {
                log.warn("未找到'标记'列，将跳过该字段的更新");
            }
            if (phoneColumn == null) {
                log.warn("未找到'下单电话'列，将跳过该字段的更新");
            }
            if (jdPlaceOrderNoColumn == null) {
                log.warn("未找到'京东下单订单号'列，将跳过该字段的同步（请在表头增加该列以写入京东 orderId）");
            }

            log.info("列位置识别完成 - 单号: {}, 京东下单订单号: {}, 物流单号: {}, 备注: {}, 是否安排: {}, 标记: {}, 下单电话: {}",
                orderNoColumn, jdPlaceOrderNoColumn, logisticsLinkColumn, remarkColumn, arrangedColumn, markColumn, phoneColumn);

            // 读取数据行：接口实际只能读 200 行，严格限制单次行数，失败时逐步缩小范围重试
            // 腾讯文档 get_range 的 range 为「结束行不包含」：要读到 endRow 含最后一行，须传 endRow+1
            int effectiveEndRow = Math.min(endRow, startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1);
            JSONObject sheetData = null;
            int[] retryDecrements = new int[] { 0, 20, 50, 100 };
            for (int decrement : retryDecrements) {
                int tryEndRow = Math.max(startRow, effectiveEndRow - decrement);
                int tryRowCount = tryEndRow - startRow + 1;
                if (tryRowCount > READ_ROWS_WHEN_USE_ROW_TOTAL) {
                    tryEndRow = startRow + READ_ROWS_WHEN_USE_ROW_TOTAL - 1;
                    tryRowCount = READ_ROWS_WHEN_USE_ROW_TOTAL;
                }
                if (tryEndRow < startRow) {
                    continue;
                }
                int rangeEndInclusive = tryEndRow + 1;  // API 结束行不包含，+1 才能读到 tryEndRow 行
                String range = String.format("A%d:%s%d", startRow, DATA_RANGE_COL_END, rangeEndInclusive);
                log.info("开始读取数据行 - 行号: {} ~ {} (共 {} 行), range: {} (尝试 decrement={})", startRow, tryEndRow, tryRowCount, range, decrement);
                try {
                    sheetData = tencentDocService.readSheetData(accessToken, fileId, sheetId, range);
                    if (sheetData != null) {
                        effectiveEndRow = tryEndRow;
                        log.info("数据行读取成功 (使用 endRow={}, 行数={})", effectiveEndRow, tryRowCount);
                        break;
                    }
                } catch (Exception e) {
                    log.warn("读取数据行失败 (endRow={}, 行数={}, decrement={})，将重试更小范围: {}", tryEndRow, tryRowCount, decrement, e.getMessage());
                    if (decrement == retryDecrements[retryDecrements.length - 1]) {
                        log.error("读取数据行多次重试后仍失败", e);
                        return AjaxResult.error("读取数据行失败: " + e.getMessage());
                    }
                }
            }

            if (sheetData == null) {
                return AjaxResult.error("读取数据行返回null");
            }

            JSONArray values = null;
            try {
                values = sheetData.getJSONArray("values");
            } catch (Exception e) {
                log.warn("解析 values 异常，尝试将 endRow 缩小后重新读取: {}", e.getMessage());
                for (int decrement : new int[] { 1, 10 }) {
                    int tryEndRow = Math.max(startRow, effectiveEndRow - decrement);
                    if (tryEndRow >= startRow) {
                        String retryRange = String.format("A%d:%s%d", startRow, DATA_RANGE_COL_END, tryEndRow + 1);  // API 结束行不包含
                        try {
                            sheetData = tencentDocService.readSheetData(accessToken, fileId, sheetId, retryRange);
                            if (sheetData != null) {
                                values = sheetData.getJSONArray("values");
                                effectiveEndRow = tryEndRow;
                                break;
                            }
                        } catch (Exception ex) {
                            log.warn("重新读取 (endRow={}) 仍失败: {}", tryEndRow, ex.getMessage());
                        }
                    }
                }
            }
            if (values == null && sheetData != null) {
                try {
                    values = sheetData.getJSONArray("values");
                } catch (Exception e) {
                    log.error("解析 values 失败", e);
                    return AjaxResult.error("解析数据行失败: " + e.getMessage());
                }
            }
            endRow = effectiveEndRow; // 后续统一使用实际读取成功的 endRow
            String range = String.format("A%d:%s%d", startRow, DATA_RANGE_COL_END, endRow);
            log.info("解析后的数据行数: {}", values != null ? values.size() : "null");

            if (values == null || values.isEmpty()) {
                log.warn("指定范围内没有数据，可能已处理完毕。range={}, sheetData keys={}",
                    range, sheetData != null ? sheetData.keySet() : null);

                // 打印前10个键值对用于调试
                if (sheetData != null && !sheetData.isEmpty()) {
                    log.warn("sheetData内容预览: {}", sheetData.toJSONString().substring(0, Math.min(500, sheetData.toJSONString().length())));
                }

                // 无数据时计算下次起始行（不再写入 Redis，下次从接口获取 rowCount）
                int emptyCurrentMax = effectiveEndRow;
                int emptyBacktrack = BACKTRACK_ROWS_WHEN_ALL_SKIPPED;
                int emptyNextStart = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL,
                    Math.max(startRow + MIN_ADVANCE_ROWS, emptyCurrentMax + 1 - emptyBacktrack));
                log.info("本批无数据，下次起始行: {}（不再保存进度，下次从接口获取 rowCount）", emptyNextStart);

                JSONObject result = new JSONObject();
                result.put("startRow", startRow);
                result.put("endRow", effectiveEndRow);
                result.put("lastMaxRow", null);  // 不再使用 Redis 存储，改为从接口获取 rowCount
                result.put("nextStartRow", emptyNextStart);
                result.put("rowTotal", rowCount > 0 ? rowCount : null);
                result.put("currentMaxRow", emptyCurrentMax);
                result.put("filledCount", 0);
                result.put("skippedCount", 0);
                result.put("errorCount", 0);
                result.put("message", "指定范围内没有数据");
                result.put("range", range);
                result.put("hasSheetData", sheetData != null);
                result.put("sheetDataKeys", sheetData != null ? sheetData.keySet() : null);
                return AjaxResult.success("没有需要处理的数据", result);
            }

            log.info("成功读取 {} 行数据，开始处理...", values.size());

            // 处理数据行
            int filledCount = 0;
            int skippedCount = 0;
            int errorCount = 0;
            int successUpdates = 0; // 实际成功写入的数量
            int maxSuccessRow = 0; // 记录实际成功写入的最大行号

            // 收集同步成功的详细日志（用于微信推送）
            List<Map<String, Object>> successLogs = new java.util.ArrayList<>();
            
            // 收集物流链接更新的记录（用于推送消息说明）
            List<Map<String, Object>> logisticsLinkUpdatedLogs = new java.util.ArrayList<>();
            
            // 收集错误详情日志（用于微信推送）
            List<Map<String, Object>> errorLogs = new java.util.ArrayList<>();

            JSONArray updates = new JSONArray(); // 存储需要更新的行和值

            for (int i = 0; i < values.size(); i++) {
                JSONArray row;
                try {
                    Object rowObj = values.get(i);
                    if (rowObj == null || !(rowObj instanceof JSONArray)) {
                        continue; // 非数组（空数据行等）跳过
                    }
                    row = (JSONArray) rowObj;
                } catch (Exception e) {
                    log.warn("解析第 {} 行数据异常，跳过（index={}）: {}", startRow + i, i, e.getMessage());
                    errorCount++;
                    continue;
                }
                // 仅要求能读到第三方单号、物流列；京东列常在更右侧且稀疏行可能未返回尾部空列，不能要求 row 长度覆盖京东列
                int minDataColIdx = Math.max(orderNoColumn, logisticsLinkColumn);
                if (row == null || row.size() <= minDataColIdx) {
                    continue; // 跳过空行或列数不足的行
                }

                // 计算实际的行号（Excel行号，从1开始）
                int excelRow = startRow + i;

                // 获取单号（空数据行可能导致 getString 异常，单行 try 保证整批继续执行）
                String orderNo;
                try {
                    orderNo = row.getString(orderNoColumn);
                } catch (Exception e) {
                    log.warn("读取单号列异常，跳过行 {}: {}", excelRow, e.getMessage());
                    errorCount++;
                    continue;
                }
                if (orderNo == null || orderNo.trim().isEmpty()) {
                    skippedCount++;
                    continue; // 跳过空单号的行
                }

                orderNo = orderNo.trim();

                // 检查物流链接列是否已有值（可能是别人手动填写的）
                String existingLogisticsLink = row.getString(logisticsLinkColumn);
                String trimmedExistingLink = (existingLogisticsLink != null) ? existingLogisticsLink.trim() : "";
                
                if (!trimmedExistingLink.isEmpty()) {
                    // 文档中已有物流链接，需要比对数据库中的物流链接
                    try {
                        JDOrder existingOrder = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo);
                        if (existingOrder != null) {
                            String dbLogisticsLink = existingOrder.getLogisticsLink();
                            String trimmedDbLink = (dbLogisticsLink != null) ? dbLogisticsLink.trim() : "";
                            String dbJdOrderId = existingOrder.getOrderId() != null ? existingOrder.getOrderId().trim() : "";
                            String docJdOrderId = "";
                            if (jdPlaceOrderNoColumn != null && row.size() > jdPlaceOrderNoColumn) {
                                try {
                                    String cj = row.getString(jdPlaceOrderNoColumn);
                                    docJdOrderId = cj != null ? cj.trim() : "";
                                } catch (Exception ex) {
                                    log.debug("读取京东下单订单号列异常 行 {}: {}", excelRow, ex.getMessage());
                                }
                            }

                            // 比对物流链接，如果不一致则需要更新
                            if (!trimmedDbLink.isEmpty() && !trimmedExistingLink.equals(trimmedDbLink)) {
                                // 物流链接不一致，需要更新文档中的链接
                                log.info("========== 检测到物流链接不一致 - 单号: {}, 行号: {}, 文档链接: {}, 数据库链接: {}, 开始更新 ==========",
                                    orderNo, excelRow, trimmedExistingLink, trimmedDbLink);
                                
                                // 添加到更新列表
                                JSONObject update = new JSONObject();
                                update.put("row", excelRow);
                                update.put("column", logisticsLinkColumn);
                                update.put("orderNo", orderNo);
                                update.put("logisticsLink", trimmedDbLink);
                                update.put("isLinkUpdated", true); // 标记为物流链接更新
                                update.put("oldLogisticsLink", trimmedExistingLink); // 旧链接
                                update.put("newLogisticsLink", trimmedDbLink); // 新链接
                                // 物流变更：与京东单号一并同步（单号随物流变，覆盖文档原京东单号）
                                if (jdPlaceOrderNoColumn != null && !dbJdOrderId.isEmpty()) {
                                    update.put("jdOrderId", dbJdOrderId);
                                    update.put("jdOrderIdColumn", jdPlaceOrderNoColumn);
                                }
                                updates.add(update);
                                
                                filledCount++; // 统计为填充数量
                                
                                // 记录物流链接更新日志
                                Map<String, Object> updatedLog = new java.util.HashMap<>();
                                updatedLog.put("orderNo", orderNo);
                                updatedLog.put("row", excelRow);
                                updatedLog.put("oldLogisticsLink", trimmedExistingLink);
                                updatedLog.put("newLogisticsLink", trimmedDbLink);
                                logisticsLinkUpdatedLogs.add(updatedLog);
                                
                                log.info("========== 物流链接不一致，已加入更新队列 - 单号: {}, 行号: {}, 旧链接: {}, 新链接: {} ==========",
                                    orderNo, excelRow, trimmedExistingLink, trimmedDbLink);
                            } else if (docJdOrderId.isEmpty() && !dbJdOrderId.isEmpty() && jdPlaceOrderNoColumn != null) {
                                // 文档已有物流、京东单号列为空：仅补写京东单号
                                JSONObject jdUpdate = new JSONObject();
                                jdUpdate.put("row", excelRow);
                                jdUpdate.put("orderNo", orderNo);
                                jdUpdate.put("isJdOrderIdOnlyUpdate", true);
                                jdUpdate.put("jdOrderId", dbJdOrderId);
                                jdUpdate.put("jdOrderIdColumn", jdPlaceOrderNoColumn);
                                updates.add(jdUpdate);
                                filledCount++;
                                log.info("========== 文档有物流无京东单号，补写京东下单订单号 - 第三方单号: {}, 行: {} ==========", orderNo, excelRow);
                            } else {
                                // 物流链接一致或数据库中没有链接，只需同步订单状态
                                if (existingOrder.getTencentDocPushed() == null || existingOrder.getTencentDocPushed() == 0) {
                                    // 订单未标记为已推送，但文档中已有值，同步状态
                                    existingOrder.setTencentDocPushed(1);
                                    existingOrder.setTencentDocPushTime(new java.util.Date());
                                    int syncResult = jdOrderService.updateJDOrder(existingOrder);
                                    if (syncResult > 0) {
                                        log.info("✓ 同步订单状态成功 - 单号: {}, 行号: {}, 原因: 文档中已有物流链接（可能手动填写）",
                                            orderNo, excelRow);
                                    } else {
                                        log.warn("⚠️ 同步订单状态返回0 - 单号: {}, 行号: {}, 可能未更新",
                                            orderNo, excelRow);
                                    }

                                    // 记录同步日志
                                    logOperation(batchId, fileId, sheetId, "BATCH_SYNC", orderNo, excelRow, trimmedExistingLink,
                                        "SKIPPED", "文档中已有物流链接，已同步订单状态");
                                }
                                skippedCount++; // 物流链接一致，跳过写入
                            }
                        } else {
                            // 未找到订单，跳过
                            skippedCount++;
                        }
                    } catch (Exception e) {
                        log.error("❌ 处理已有物流链接时发生异常 - 单号: {}, 行号: {}", orderNo, excelRow, e);
                        skippedCount++;
                    }
                    
                    if (!trimmedExistingLink.isEmpty()) {
                        // 如果已经处理过（无论是更新还是跳过），都继续下一个
                        continue;
                    }
                }

                try {
                    // 根据第三方单号查询订单（与文档「单号/客户单号/第三方单号」列单元格一致）
                    JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo);

                    if (order == null) {
                        // 订单不存在，跳过但不统计为错误
                        skippedCount++;
                        log.info("跳过订单不存在 - 单号: {}, 行号: {}", orderNo, excelRow);
                        continue;
                    }

                    // 检查订单是否已推送（防止重复推送）
                    if (skipPushedOrders && order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1) {
                        skippedCount++;
                        log.info("跳过已推送订单 - 单号: {}, 推送时间: {}, 行号: {}",
                            orderNo,
                            order.getTencentDocPushTime() != null ?
                                new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(order.getTencentDocPushTime()) : "未知",
                            excelRow);
                        continue;
                    }

                    if (order.getLogisticsLink() != null && !order.getLogisticsLink().trim().isEmpty()) {
                        String logisticsLink = order.getLogisticsLink().trim();

                        // 从status字段中提取手机号码
                        String phone = null;
                        if (phoneColumn != null) {
                            String statusValue = order.getStatus();
                            log.debug("准备从status字段提取手机号 - 单号: {}, status内容: [{}]", orderNo, statusValue);
                            phone = extractPhoneFromRemark(statusValue);
                            if (phone != null) {
                                log.info("✓ 从status字段提取手机号码 - 单号: {}, status: [{}], 手机号: {}", orderNo, statusValue, phone);
                            } else {
                                log.debug("未能从status字段提取手机号码 - 单号: {}, status: [{}]", orderNo, statusValue);
                            }
                        } else {
                            log.debug("phoneColumn为null，跳过手机号提取 - 单号: {}", orderNo);
                        }

                        // 构建更新请求
                        JSONObject update = new JSONObject();
                        update.put("row", excelRow);
                        update.put("column", logisticsLinkColumn);
                        update.put("orderNo", orderNo);
                        update.put("logisticsLink", logisticsLink);

                        // 如果找到手机号码，也添加到更新中
                        if (phone != null && phoneColumn != null) {
                            update.put("phone", phone);
                            update.put("phoneColumn", phoneColumn);
                        }

                        // 文档无物流：与物流一并写入京东单号（无物流无单号 → 一起写入）
                        if (jdPlaceOrderNoColumn != null && order.getOrderId() != null && !order.getOrderId().trim().isEmpty()) {
                            update.put("jdOrderId", order.getOrderId().trim());
                            update.put("jdOrderIdColumn", jdPlaceOrderNoColumn);
                        }

                        // 注意：不再保存order对象，写入成功后会重新查询以确保数据最新
                        updates.add(update);

                        filledCount++;
                        log.info("找到订单物流链接 - 单号: {}, 物流链接: {}, 手机号: {}, 行号: {}, 已推送: {}",
                            orderNo, logisticsLink, phone != null ? phone : "无", excelRow,
                            order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1 ? "是" : "否");
                    } else {
                        errorCount++;
                        log.warn("订单物流链接为空 - 单号: {}, 行号: {}", orderNo, excelRow);
                        
                        // 记录错误详情
                        Map<String, Object> errorLog = new java.util.HashMap<>();
                        errorLog.put("orderNo", orderNo);
                        errorLog.put("row", excelRow);
                        errorLog.put("errorType", "物流链接为空");
                        errorLog.put("errorMessage", "订单物流链接为空");
                        errorLogs.add(errorLog);
                    }
                } catch (Exception e) {
                    errorCount++;
                    log.error("处理订单失败 - 单号: {}, 行号: {}", orderNo, excelRow, e);
                    
                    // 记录错误详情
                    Map<String, Object> errorLog = new java.util.HashMap<>();
                    errorLog.put("orderNo", orderNo);
                    errorLog.put("row", excelRow);
                    errorLog.put("errorType", "处理订单异常");
                    errorLog.put("errorMessage", e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName());
                    errorLogs.add(errorLog);
                }
            }

            // 批量更新表格
            if (!updates.isEmpty()) {
                // 获取今天的日期，格式：yyMMdd（如：251105）
                String today = new java.text.SimpleDateFormat("yyMMdd").format(new java.util.Date());

                // 将更新按行分组，批量写入（同一行可能同时有物流与京东单号任务，需合并）
                Map<Integer, JSONObject> rowUpdates = new java.util.LinkedHashMap<>();
                for (int i = 0; i < updates.size(); i++) {
                    JSONObject update = updates.getJSONObject(i);
                    int row = update.getIntValue("row");
                    JSONObject existing = rowUpdates.get(row);
                    if (existing == null) {
                        rowUpdates.put(row, update);
                    } else {
                        mergeTencentDocRowFillUpdate(existing, update);
                    }
                }

                // 批量写入（每行单独写入，同时更新多个字段）
                // 在写入前，重新读取该行验证单号匹配和物流链接列为空
                // 重置成功计数（在循环外已定义）
                int currentBatchSuccessUpdates = 0;
                int currentBatchMaxSuccessRow = 0;
                for (Map.Entry<Integer, JSONObject> entry : rowUpdates.entrySet()) {
                    try {
                        int row = entry.getKey();
                        JSONObject update = entry.getValue();
                        String expectedOrderNo = update.getString("orderNo");

                        // 文档已有物流、仅补写京东下单订单号（有物流无京东单号）
                        if (Boolean.TRUE.equals(update.getBoolean("isJdOrderIdOnlyUpdate"))) {
                            Integer jdCol = update.getInteger("jdOrderIdColumn");
                            String jdId = update.getString("jdOrderId");
                            if (jdCol == null || jdId == null || jdId.trim().isEmpty()) {
                                log.warn("仅补京东单号任务参数不全 - 行 {}", row);
                                errorCount++;
                                continue;
                            }
                            jdId = jdId.trim();
                            String jdVerifyRange = String.format("A%d:%s%d", row, DATA_RANGE_COL_END, row);
                            JSONObject jdVerifyData = tencentDocService.readSheetData(accessToken, fileId, sheetId, jdVerifyRange);
                            if (jdVerifyData == null || !jdVerifyData.containsKey("values")) {
                                errorCount++;
                                continue;
                            }
                            JSONArray jdVerifyRows = jdVerifyData.getJSONArray("values");
                            if (jdVerifyRows == null || jdVerifyRows.isEmpty()) {
                                errorCount++;
                                continue;
                            }
                            JSONArray jdVerifyRow = jdVerifyRows.getJSONArray(0);
                            int jdBaseNeedIdx = Math.max(orderNoColumn, logisticsLinkColumn);
                            if (jdVerifyRow == null || jdVerifyRow.size() <= jdBaseNeedIdx) {
                                errorCount++;
                                continue;
                            }
                            String jdCurOrderNo = jdVerifyRow.getString(orderNoColumn);
                            if (jdCurOrderNo == null || !jdCurOrderNo.trim().equals(expectedOrderNo)) {
                                errorCount++;
                                continue;
                            }
                            String jdDocLogistics = jdVerifyRow.getString(logisticsLinkColumn);
                            if (jdDocLogistics == null || jdDocLogistics.trim().isEmpty()) {
                                log.info("跳过仅补京东单号 - 行 {} 物流列为空", row);
                                skippedCount++;
                                continue;
                            }
                            String docJd = "";
                            if (jdVerifyRow.size() > jdCol) {
                                try {
                                    String sj = jdVerifyRow.getString(jdCol);
                                    docJd = sj != null ? sj.trim() : "";
                                } catch (Exception ignore) {
                                    // ignore
                                }
                            }
                            if (!docJd.isEmpty()) {
                                skippedCount++;
                                continue;
                            }
                            JSONArray jdRequests = new JSONArray();
                            jdRequests.add(buildUpdateCellRequest(sheetId, row - 1, jdCol, jdId, false));
                            JSONObject jdBatch = new JSONObject();
                            jdBatch.put("requests", jdRequests);
                            tencentDocService.batchUpdate(accessToken, fileId, jdBatch);
                            currentBatchSuccessUpdates++;
                            if (row > currentBatchMaxSuccessRow) {
                                currentBatchMaxSuccessRow = row;
                            }
                            log.info("✓ 已补写京东下单订单号 - 第三方单号: {}, 行: {}, 值: {}", expectedOrderNo, row, jdId);
                            logOperation(batchId, fileId, sheetId, "BATCH_SYNC", expectedOrderNo, row, jdId,
                                "SUCCESS", "补写京东下单订单号");
                            Map<String, Object> jdSuccessLog = new java.util.HashMap<>();
                            jdSuccessLog.put("orderNo", expectedOrderNo);
                            jdSuccessLog.put("row", row);
                            jdSuccessLog.put("jdOrderId", jdId);
                            jdSuccessLog.put("updateType", "JD_ORDER_ID");
                            successLogs.add(jdSuccessLog);
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            }
                            continue;
                        }

                        String logisticsLink = update.getString("logisticsLink");
                        if (logisticsLink == null || logisticsLink.trim().isEmpty()) {
                            log.warn("跳过写入 - 行 {} 缺少物流链接", row);
                            skippedCount++;
                            continue;
                        }

                        // 重新读取该行（列宽与批量读取一致，含 Z 列之后的京东单号列）
                        String verifyRange = String.format("A%d:%s%d", row, DATA_RANGE_COL_END, row);
                        JSONObject verifyData = tencentDocService.readSheetData(accessToken, fileId, sheetId, verifyRange);

                        if (verifyData == null || !verifyData.containsKey("values")) {
                            log.warn("验证失败 - 无法读取行 {}", row);
                            errorCount++;
                            
                            // 记录错误详情
                            Map<String, Object> errorLog = new java.util.HashMap<>();
                            errorLog.put("orderNo", expectedOrderNo);
                            errorLog.put("row", row);
                            errorLog.put("errorType", "验证失败");
                            errorLog.put("errorMessage", "无法读取行数据");
                            errorLogs.add(errorLog);
                            
                            continue;
                        }

                        JSONArray verifyRows = verifyData.getJSONArray("values");
                        if (verifyRows == null || verifyRows.isEmpty()) {
                            log.warn("验证失败 - 行 {} 数据为空", row);
                            errorCount++;
                            
                            // 记录错误详情
                            Map<String, Object> errorLog = new java.util.HashMap<>();
                            errorLog.put("orderNo", expectedOrderNo);
                            errorLog.put("row", row);
                            errorLog.put("errorType", "验证失败");
                            errorLog.put("errorMessage", "行数据为空");
                            errorLogs.add(errorLog);
                            
                            continue;
                        }

                        JSONArray verifyRow = verifyRows.getJSONArray(0);
                        int verifyMinColIdx = Math.max(orderNoColumn, logisticsLinkColumn);
                        if (verifyRow == null || verifyRow.size() <= verifyMinColIdx) {
                            log.warn("验证失败 - 行 {} 列数不足（需覆盖单号列与物流列）", row);
                            errorCount++;
                            
                            // 记录错误详情
                            Map<String, Object> errorLog = new java.util.HashMap<>();
                            errorLog.put("orderNo", expectedOrderNo);
                            errorLog.put("row", row);
                            errorLog.put("errorType", "验证失败");
                            errorLog.put("errorMessage", "行列数不足");
                            errorLogs.add(errorLog);
                            
                            continue;
                        }

                        // 验证1：单号是否匹配
                        String currentOrderNo = verifyRow.getString(orderNoColumn);
                        if (currentOrderNo == null || !currentOrderNo.trim().equals(expectedOrderNo)) {
                            log.warn("验证失败 - 行 {} 单号不匹配，预期: {}, 实际: {}", row, expectedOrderNo, currentOrderNo);
                            errorCount++;
                            
                            // 记录错误详情
                            Map<String, Object> errorLog = new java.util.HashMap<>();
                            errorLog.put("orderNo", expectedOrderNo);
                            errorLog.put("row", row);
                            errorLog.put("errorType", "验证失败");
                            errorLog.put("errorMessage", String.format("单号不匹配，预期: %s, 实际: %s", expectedOrderNo, currentOrderNo));
                            errorLogs.add(errorLog);
                            
                            continue;
                        }

                        // 验证2：物流链接列是否为空（如果是物流链接更新，则允许覆盖）
                        Boolean isLinkUpdated = update.getBoolean("isLinkUpdated");
                        String currentLogisticsLink = verifyRow.getString(logisticsLinkColumn);
                        if (!Boolean.TRUE.equals(isLinkUpdated) && currentLogisticsLink != null && !currentLogisticsLink.trim().isEmpty()) {
                            log.info("跳过写入 - 行 {} 单号 {} 的物流链接列已有值: {}", row, expectedOrderNo, currentLogisticsLink);
                            skippedCount++;
                            continue;
                        }
                        
                        // 如果是物流链接更新，记录日志
                        if (Boolean.TRUE.equals(isLinkUpdated)) {
                            String oldLink = update.getString("oldLogisticsLink");
                            String newLink = update.getString("newLogisticsLink");
                            log.info("========== 开始更新物流链接 - 行: {}, 单号: {}, 旧链接: {}, 新链接: {} ==========",
                                row, expectedOrderNo, oldLink, newLink);
                        }

                        // 验证通过，执行写入
                        // 使用 batchUpdate 一次性更新多个字段
                        JSONArray requests = new JSONArray();

                        // 如果是物流链接更新，先清除单元格内容，然后再写入新内容
                        if (Boolean.TRUE.equals(isLinkUpdated)) {
                            // 先清除单元格内容（写入空文本）
                            requests.add(buildUpdateCellRequest(sheetId, row - 1, logisticsLinkColumn, "", false));
                            log.info("✓ 先清除单元格内容 - 行: {}, 列: {}", row, logisticsLinkColumn);
                        }

                        // 1. 更新物流单号（超链接类型）
                        requests.add(buildUpdateCellRequest(sheetId, row - 1, logisticsLinkColumn, logisticsLink, true));

                        // 2. 更新"下单电话"列（如果存在且提取到了手机号码）
                        String phone = update.getString("phone");
                        Integer phoneCol = update.getInteger("phoneColumn");
                        if (phone != null && phoneCol != null) {
                            requests.add(buildUpdateCellRequest(sheetId, row - 1, phoneCol, phone, false));
                            log.info("✓ 准备写入手机号码 - 单号: {}, 手机号: {}, 行: {}, 列: {}", expectedOrderNo, phone, row, phoneCol);
                        }

                        // 3. 更新"备注"列（如果存在，固定填写"有货"）
                        if (remarkColumn != null) {
                            requests.add(buildUpdateCellRequest(sheetId, row - 1, remarkColumn, "有货", false));
                            log.info("✓ 准备写入备注 - 单号: {}, 备注: 有货, 行: {}, 列: {}", expectedOrderNo, row, remarkColumn);
                        }

                        // 4. 更新"是否安排"列（如果存在，固定填写"2"）
                        if (arrangedColumn != null) {
                            requests.add(buildUpdateCellRequest(sheetId, row - 1, arrangedColumn, "2", false));
                            log.info("✓ 准备写入是否安排 - 单号: {}, 是否安排: 2, 行: {}, 列: {}", expectedOrderNo, row, arrangedColumn);
                        }

                        // 5. 更新"标记"列（如果存在，且不是物流链接更新时才写入，物流链接更新时保留原日期）
                        if (markColumn != null && !Boolean.TRUE.equals(isLinkUpdated)) {
                            requests.add(buildUpdateCellRequest(sheetId, row - 1, markColumn, today, false));
                            log.info("✓ 准备写入标记日期 - 单号: {}, 日期: {}, 行: {}, 列: {}", expectedOrderNo, today, row, markColumn);
                        } else if (markColumn != null && Boolean.TRUE.equals(isLinkUpdated)) {
                            log.info("✓ 物流链接更新，保留原标记日期 - 单号: {}, 行: {}", expectedOrderNo, row);
                        }

                        // 物流变更：与京东单号一并覆盖写入；首次填物流：仅文档京东列为空时写入
                        boolean wroteJdOrderIdCell = false;
                        String jdIdUpdForLog = null;
                        Integer jdColUpd = update.getInteger("jdOrderIdColumn");
                        String jdIdUpd = update.getString("jdOrderId");
                        if (jdColUpd != null && jdIdUpd != null && !jdIdUpd.trim().isEmpty()) {
                            jdIdUpd = jdIdUpd.trim();
                            String docJdVal = "";
                            if (verifyRow.size() > jdColUpd) {
                                try {
                                    String sj = verifyRow.getString(jdColUpd);
                                    docJdVal = sj != null ? sj.trim() : "";
                                } catch (Exception ignore) {
                                    // ignore
                                }
                            }
                            boolean shouldWriteJd = Boolean.TRUE.equals(isLinkUpdated) || docJdVal.isEmpty();
                            if (shouldWriteJd) {
                                requests.add(buildUpdateCellRequest(sheetId, row - 1, jdColUpd, jdIdUpd, false));
                                wroteJdOrderIdCell = true;
                                jdIdUpdForLog = jdIdUpd;
                                if (Boolean.TRUE.equals(isLinkUpdated)) {
                                    log.info("✓ 同步京东下单订单号（随物流变更） - 第三方单号: {}, 行: {}, 值: {}", expectedOrderNo, row, jdIdUpd);
                                } else {
                                    log.info("✓ 写入京东下单订单号（与物流一并） - 第三方单号: {}, 行: {}, 值: {}", expectedOrderNo, row, jdIdUpd);
                                }
                            } else {
                                log.info("✓ 文档已有京东下单订单号，跳过写入 - 第三方单号: {}, 行: {}, 现有: [{}]", expectedOrderNo, row, docJdVal);
                            }
                        }

                        // 构建完整的 batchUpdate 请求体
                        JSONObject batchUpdateBody = new JSONObject();
                        batchUpdateBody.put("requests", requests);

                        // 调用 batchUpdate API
                        tencentDocService.batchUpdate(accessToken, fileId, batchUpdateBody);
                        currentBatchSuccessUpdates++;
                        // 更新实际成功写入的最大行号
                        if (row > currentBatchMaxSuccessRow) {
                            currentBatchMaxSuccessRow = row;
                        }

                        String logMsg = String.format("✓ 写入成功 - 行: %d, 单号: %s, 物流链接: %s", row, expectedOrderNo, logisticsLink);
                        if (phone != null) {
                            logMsg += String.format(", 手机号: %s", phone);
                        }
                        log.info(logMsg);

                        // 更新订单的推送状态（重新查询订单，避免使用旧对象）
                        JDOrder orderToUpdate = null;
                        try {
                            orderToUpdate = jdOrderService.selectJDOrderByThirdPartyOrderNo(expectedOrderNo);
                            if (orderToUpdate != null) {
                                orderToUpdate.setTencentDocPushed(1);
                                orderToUpdate.setTencentDocPushTime(new java.util.Date());
                                int updateResult = jdOrderService.updateJDOrder(orderToUpdate);
                                if (updateResult > 0) {
                                    log.info("✓ 订单推送状态已更新 - 单号: {}, updateResult: {}", expectedOrderNo, updateResult);
                                } else {
                                    log.warn("⚠️ 订单推送状态更新返回0 - 单号: {}, 可能未更新", expectedOrderNo);
                                }
                            } else {
                                log.warn("⚠️ 重新查询订单失败 - 单号: {}", expectedOrderNo);
                            }
                        } catch (Exception e) {
                            log.error("❌ 更新订单推送状态失败（但写入腾讯文档已成功） - 单号: {}", expectedOrderNo, e);
                            // 不影响主流程，继续执行
                        }

                        // 记录操作日志
                        logOperation(batchId, fileId, sheetId, "BATCH_SYNC", expectedOrderNo, row, logisticsLink,
                            "SUCCESS", null);

                        // 收集成功日志信息（用于微信推送）
                        Map<String, Object> successLog = new java.util.HashMap<>();
                        successLog.put("orderNo", expectedOrderNo);
                        successLog.put("row", row);
                        successLog.put("logisticsLink", logisticsLink);
                        if (phone != null) {
                            successLog.put("phone", phone);
                        }
                        // 添加型号信息
                        if (orderToUpdate != null && orderToUpdate.getModelNumber() != null) {
                            successLog.put("modelNumber", orderToUpdate.getModelNumber());
                        }
                        if (wroteJdOrderIdCell && jdIdUpdForLog != null) {
                            successLog.put("jdOrderId", jdIdUpdForLog);
                        }
                        
                        // 检查是否为物流链接更新（复用之前的变量）
                        if (Boolean.TRUE.equals(isLinkUpdated)) {
                            String oldLink = update.getString("oldLogisticsLink");
                            String newLink = update.getString("newLogisticsLink");
                            successLog.put("isLinkUpdated", true);
                            successLog.put("oldLogisticsLink", oldLink);
                            successLog.put("newLogisticsLink", newLink);
                            log.info("✓ 记录物流链接更新到成功日志 - 单号: {}, 旧链接: {}, 新链接: {}", 
                                expectedOrderNo, oldLink, newLink);
                        }
                        
                        successLogs.add(successLog);
                    } catch (Exception e) {
                        log.error("写入数据失败 - 行: {}", entry.getKey(), e);
                        errorCount++;

                        // 记录失败日志
                        String orderNo = entry.getValue().getString("orderNo");
                        String logisticsLink = entry.getValue().getString("logisticsLink");
                        logOperation(batchId, fileId, sheetId, "BATCH_SYNC", orderNo, entry.getKey(), logisticsLink,
                            "FAILED", "写入异常: " + e.getMessage());
                        
                        // 记录错误详情
                        Map<String, Object> errorLog = new java.util.HashMap<>();
                        errorLog.put("orderNo", orderNo);
                        errorLog.put("row", entry.getKey());
                        errorLog.put("errorType", "写入失败");
                        errorLog.put("errorMessage", e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName());
                        errorLogs.add(errorLog);
                    }

                    // 添加延迟，避免API调用频率过高
                    try {
                        Thread.sleep(100); // 100ms延迟
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }

                // 更新全局成功计数
                successUpdates = currentBatchSuccessUpdates;
                maxSuccessRow = currentBatchMaxSuccessRow;

                log.info("批量填充物流链接完成 - 成功: {}, 跳过: {}, 错误: {}, 最大成功行号: {}",
                    successUpdates, skippedCount, errorCount, maxSuccessRow > 0 ? maxSuccessRow : "无");
            }

            // 根据本次扫描/写入结果计算下次起始行（不再写入 Redis，下次从接口获取 rowCount 决定范围）
            // currentMaxRow = 本批实际涉及的最大行（有写入用最大成功行，否则用本批扫描终点）
            int currentMaxRow = (maxSuccessRow > 0) ? maxSuccessRow : effectiveEndRow;
            // 整批都跳过时用较小回溯，尽快扫到后续行；有写入时用完整回溯覆盖物流变更
            int backtrack = (successUpdates > 0) ? BACKTRACK_ROWS : BACKTRACK_ROWS_WHEN_ALL_SKIPPED;
            int nextStartRow = Math.max(MIN_START_ROW_WHEN_USE_ROW_TOTAL,
                Math.max(startRow + MIN_ADVANCE_ROWS, currentMaxRow + 1 - backtrack));

            String nextSyncHint = String.format(
                "下次将从第 %d 行开始（本批最大行: %d，回溯 %d 行；已写入 %d 条，跳过 %d 条；下次从接口获取 rowCount）",
                nextStartRow, currentMaxRow, backtrack, successUpdates, skippedCount);
            log.info("本批范围: {}~{}，实际最大行: {}，下次起始: {}（不再保存 Redis 进度）", startRow, effectiveEndRow, currentMaxRow, nextStartRow);

            JSONObject result = new JSONObject();
            result.put("startRow", startRow);
            result.put("endRow", effectiveEndRow);
            result.put("currentMaxRow", currentMaxRow);
            result.put("lastMaxRow", null);  // 不再使用 Redis 存储，改为从接口获取 rowCount
            result.put("nextStartRow", nextStartRow);
            result.put("rowTotal", rowCount > 0 ? rowCount : null);
            result.put("filledCount", filledCount);
            result.put("skippedCount", skippedCount);
            result.put("errorCount", errorCount);
            result.put("orderNoColumn", orderNoColumn);
            result.put("jdPlaceOrderNoColumn", jdPlaceOrderNoColumn);
            result.put("logisticsLinkColumn", logisticsLinkColumn);
            result.put("skipPushedOrders", skipPushedOrders); // 是否跳过已推送订单

            // 构建详细的提示信息
            String message = String.format(
                "✓ 同步完成：成功填充 %d 条，跳过 %d 条，错误 %d 条\n" +
                "  本次范围：第 %d-%d 行\n" +
                "  当前进度：第 %d 行，下次从第 %d 行开始（回溯 %d 行）\n" +
                "  防重推送：%s\n" +
                "  %s",
                filledCount, skippedCount, errorCount,
                startRow, effectiveEndRow,
                currentMaxRow, nextStartRow, backtrack,
                skipPushedOrders ? "已启用（跳过已推送订单）" : "已禁用",
                nextSyncHint
            );
            result.put("message", message);

            // 更新批量推送记录状态（batchId 应该总是存在，因为如果没有会创建）
            if (batchId != null && !batchId.trim().isEmpty()) {
                try {
                    String status = "SUCCESS";
                    // 使用实际成功写入数量 successUpdates 而不是 filledCount
                    if (errorCount > 0 && successUpdates == 0) {
                        status = "FAILED";
                    } else if (errorCount > 0) {
                        status = "PARTIAL";
                    }
                    batchPushService.updateBatchPushRecord(batchId, status, successUpdates, skippedCount, errorCount,
                        message, null);
                    log.info("✓ 批量推送记录已更新 - batchId: {}, status: {}, 实际成功: {}, 跳过: {}, 错误: {}",
                        batchId, status, successUpdates, skippedCount, errorCount);
                } catch (Exception e) {
                    log.error("更新批量推送记录失败", e);
                    // 不影响主流程，继续执行
                }
            } else {
                log.warn("⚠️ batchId 为空，无法更新批量推送记录");
            }

            // 如果有成功记录或错误记录，发送微信推送
            if (!successLogs.isEmpty() || !errorLogs.isEmpty()) {
                try {
                    // 传递batchId、物流链接更新日志和错误日志给推送方法，用于日志关联和说明
                    sendWeChatNotificationWithBatchId(successLogs, filledCount, skippedCount, errorCount, batchId, fileId, sheetId, false, logisticsLinkUpdatedLogs, errorLogs);
                } catch (Exception e) {
                    log.error("发送微信推送失败", e);
                    // 不影响主流程，继续返回成功
                }
            }

            return AjaxResult.success("填充物流链接完成", result);
        } catch (Exception e) {
            log.error("填充物流链接失败", e);
            String errMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
            if (batchId != null && !batchId.trim().isEmpty()) {
                try {
                    batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, errMsg);
                } catch (Exception ex) {
                    log.error("异常后更新批量推送记录失败 batchId={}", batchId, ex);
                }
                try {
                    wxSendGoofishNotifyClient.pushGoofishAgentText(null, "",
                        "【腾讯文档推送】批量同步异常\n批次: " + batchId + "\n" + errMsg);
                } catch (Exception ex) {
                    log.warn("腾讯文档推送异常企微通知失败: {}", ex.toString());
                }
            }
            return AjaxResult.error("填充物流链接失败: " + errMsg);
        }
    }

    /**
     * 构建单个单元格的更新请求（用于 batchUpdate）- 普通文本
     *
     * @param sheetId 工作表ID
     * @param rowIndex 行索引（从0开始）
     * @param columnIndex 列索引（从0开始）
     * @param value 要写入的值
     * @return updateRangeRequest 对象
     */
    private JSONObject buildUpdateCellRequest(String sheetId, int rowIndex, int columnIndex, String value) {
        return buildUpdateCellRequest(sheetId, rowIndex, columnIndex, value, false);
    }

    /**
     * 构建单个单元格的更新请求（用于 batchUpdate）- 支持超链接
     *
     * @param sheetId 工作表ID
     * @param rowIndex 行索引（从0开始）
     * @param columnIndex 列索引（从0开始）
     * @param value 要写入的值（文本或链接URL）
     * @param isLink 是否为超链接类型
     * @return updateRangeRequest 对象
     */
    private JSONObject buildUpdateCellRequest(String sheetId, int rowIndex, int columnIndex, String value, boolean isLink) {
        // 构建 updateRangeRequest
        JSONObject updateRangeRequest = new JSONObject();
        updateRangeRequest.put("sheetId", sheetId);

        // 构建 gridData
        JSONObject gridData = new JSONObject();
        gridData.put("startRow", rowIndex);
        gridData.put("startColumn", columnIndex);

        // 构建 rows 数组
        JSONArray rows = new JSONArray();
        JSONObject rowData = new JSONObject();
        JSONArray cellValues = new JSONArray();

        // 构建单元格数据
        JSONObject cellData = new JSONObject();
        JSONObject cellValue = new JSONObject();

        if (isLink) {
            // 写入超链接类型（url 和 text 都设置为相同的值）
            JSONObject link = new JSONObject();
            link.put("url", value);
            link.put("text", value);
            cellValue.put("link", link);
        } else {
            // 写入普通文本
            cellValue.put("text", value);
        }

        cellData.put("cellValue", cellValue);
        cellValues.add(cellData);

        rowData.put("values", cellValues);
        rows.add(rowData);
        gridData.put("rows", rows);

        updateRangeRequest.put("gridData", gridData);

        // 包装为 request 对象
        JSONObject request = new JSONObject();
        request.put("updateRangeRequest", updateRangeRequest);

        return request;
    }

    /**
     * 将列索引转换为Excel列字母（0->A, 1->B, ..., 25->Z, 26->AA, ...）
     */
    private String getColumnLetter(int columnIndex) {
        StringBuilder sb = new StringBuilder();
        columnIndex++; // 转换为1-based（A=1, B=2, ...）
        while (columnIndex > 0) {
            columnIndex--;
            sb.insert(0, (char)('A' + (columnIndex % 26)));
            columnIndex /= 26;
        }
        return sb.toString();
    }

    /**
     * 获取批量推送记录列表（带操作日志）
     */
    @GetMapping("/batchPushRecords")
    public AjaxResult getBatchPushRecords(@RequestParam(required = false) String fileId,
                                           @RequestParam(required = false) String sheetId,
                                           @RequestParam(defaultValue = "10") Integer limit) {
        try {
            // 如果没有指定文件ID，使用配置的默认值
            if (fileId == null || fileId.trim().isEmpty()) {
                fileId = tencentDocConfig.getFileId();
            }
            if (sheetId == null || sheetId.trim().isEmpty()) {
                sheetId = tencentDocConfig.getSheetId();
            }

            List<com.ruoyi.jarvis.domain.TencentDocBatchPushRecord> records =
                batchPushService.getBatchPushRecordListWithLogs(fileId, sheetId, limit);

            return AjaxResult.success(records);
        } catch (Exception e) {
            log.error("查询批量推送记录失败", e);
            return AjaxResult.error("查询批量推送记录失败: " + e.getMessage());
        }
    }

    /**
     * 获取批量推送记录详情（包含操作日志）
     */
    @GetMapping("/batchPushRecord/{batchId}")
    public AjaxResult getBatchPushRecordDetail(@PathVariable String batchId) {
        try {
            com.ruoyi.jarvis.domain.TencentDocBatchPushRecord record =
                batchPushService.getBatchPushRecord(batchId);

            if (record == null) {
                return AjaxResult.error("未找到批次记录");
            }

            return AjaxResult.success(record);
        } catch (Exception e) {
            log.error("查询批量推送记录详情失败", e);
            return AjaxResult.error("查询批量推送记录详情失败: " + e.getMessage());
        }
    }

    /**
     * 获取推送状态和倒计时信息
     */
    @GetMapping("/pushStatus")
    public AjaxResult getPushStatus() {
        try {
            Map<String, Object> status = batchPushService.getPushStatusAndCountdown();

            // 添加最后一次成功的推送记录信息
            String fileId = tencentDocConfig.getFileId();
            String sheetId = tencentDocConfig.getSheetId();

            if (fileId != null && !fileId.trim().isEmpty()) {
                batchPushService.reconcileStaleRunningRecords(fileId);
            }

            if (fileId != null && sheetId != null) {
                com.ruoyi.jarvis.domain.TencentDocBatchPushRecord lastSuccess =
                    batchPushService.getLastSuccessRecord(fileId, sheetId);
                status.put("lastSuccessRecord", lastSuccess);
            }

            return AjaxResult.success(status);
        } catch (Exception e) {
            log.error("查询推送状态失败", e);
            return AjaxResult.error("查询推送状态失败: " + e.getMessage());
        }
    }

    /**
     * 手动触发立即推送
     */
    @PostMapping("/triggerPushNow")
    public AjaxResult triggerPushNow() {
        try {
            delayedPushService.executePushNow();
            return AjaxResult.success("推送已触发");
        } catch (Exception e) {
            log.error("触发推送失败", e);
            return AjaxResult.error("触发推送失败: " + e.getMessage());
        }
    }

    /**
     * 取消待推送任务
     */
    @PostMapping("/cancelPendingPush")
    public AjaxResult cancelPendingPush() {
        try {
            delayedPushService.cancelPendingPush();
            return AjaxResult.success("待推送任务已取消");
        } catch (Exception e) {
            log.error("取消待推送任务失败", e);
            return AjaxResult.error("取消待推送任务失败: " + e.getMessage());
        }
    }

    /**
     * 反向同步第三方单号
     * 从腾讯文档的物流单号列读取链接，通过链接匹配本地订单，将腾讯文档的单号列值写入到订单的第三方单号字段
     *
     * @param params 包含 fileId, sheetId, startRow（起始行，默认850）
     * @return 同步结果
     */
    @PostMapping("/reverseSyncThirdPartyOrderNo")
    public AjaxResult reverseSyncThirdPartyOrderNo(@RequestBody Map<String, Object> params) {
        String batchId = java.util.UUID.randomUUID().toString().replace("-", "");

        try {
            // 获取访问令牌
            String accessToken;
            try {
                accessToken = tencentDocTokenService.refreshAccessToken();
                log.info("成功刷新访问令牌");
            } catch (Exception e) {
                log.error("刷新访问令牌失败", e);
                try {
                    accessToken = tencentDocTokenService.getValidAccessToken();
                } catch (Exception e2) {
                    return AjaxResult.error("访问令牌无效，请先完成授权。获取授权URL: GET /jarvis/tendoc/authUrl");
                }
            }

            // 从参数或配置中获取文档信息
            String fileId = (String) params.get("fileId");
            String sheetId = (String) params.get("sheetId");

            final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:";
            if (fileId == null || fileId.isEmpty()) {
                fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId");
                if (fileId == null || fileId.isEmpty()) {
                    fileId = tencentDocConfig.getFileId();
                }
            }
            if (sheetId == null || sheetId.isEmpty()) {
                sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId");
                if (sheetId == null || sheetId.isEmpty()) {
                    sheetId = tencentDocConfig.getSheetId();
                }
            }

            // 从配置中读取表头行
            Integer headerRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow");
            if (headerRow == null) {
                headerRow = tencentDocConfig.getHeaderRow();
            }

            // 起始行，默认850
            Integer startRow = params.get("startRow") != null ?
                Integer.valueOf(params.get("startRow").toString()) : 850;

            // 结束行，默认到2500行
            Integer endRow = params.get("endRow") != null ?
                Integer.valueOf(params.get("endRow").toString()) : 2500;

            // 记录接收到的参数
            log.info("接收到参数 - startRow: {}, endRow: {}, params: {}", startRow, endRow, params);

            // 如果 endRow 小于 startRow + 1000，可能是前端传错了，强制设置为 2500
            if (endRow < startRow + 1000) {
                log.warn("检测到 endRow ({}) 可能不正确，强制设置为 2500", endRow);
                endRow = 2500;
            }

            if (accessToken == null || fileId == null || sheetId == null) {
                return AjaxResult.error("文档配置不完整，请先配置 fileId 和 sheetId");
            }

            log.info("反向同步第三方单号开始 - fileId: {}, sheetId: {}, 起始行: {}, 结束行: {}",
                fileId, sheetId, startRow, endRow);

            // 读取表头，识别列位置
            String headerRange = String.format("A%d:Z%d", headerRow, headerRow);
            JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange);

            if (headerData == null || !headerData.containsKey("values")) {
                return AjaxResult.error("读取表头失败");
            }

            JSONArray headerValues = headerData.getJSONArray("values");
            if (headerValues == null || headerValues.isEmpty()) {
                return AjaxResult.error("表头数据为空");
            }

            JSONArray headerRowData = headerValues.getJSONArray(0);
            if (headerRowData == null || headerRowData.isEmpty()) {
                return AjaxResult.error("无法识别表头");
            }

            // 识别列位置
            Integer orderNoColumn = null;        // "单号"列
            Integer logisticsLinkColumn = null;  // "物流单号"列

            for (int i = 0; i < headerRowData.size(); i++) {
                String cellValue = headerRowData.getString(i);
                if (cellValue == null) {
                    continue;
                }
                if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "单号")) {
                    orderNoColumn = i;
                    log.info("✓ 列名完全匹配「单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "客户单号")) {
                    orderNoColumn = i;
                    log.info("\u2713 列名完全匹配「客户单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
                    orderNoColumn = i;
                    log.info("✓ 列名完全匹配「第三方单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流单号")) {
                    logisticsLinkColumn = i;
                    log.info("✓ 列名完全匹配「物流单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流链接")) {
                    logisticsLinkColumn = i;
                    log.info("✓ 列名匹配「物流链接」：第 {} 列（索引{}）", i + 1, i);
                }
            }

            if (orderNoColumn == null || logisticsLinkColumn == null) {
                return AjaxResult.error("无法识别表头列，请确保存在列名完全为「单号」「客户单号」或「第三方单号」，以及「物流单号」（或「物流链接」）");
            }

            // 统计结果
            int successCount = 0;
            int skippedCount = 0;
            int errorCount = 0;

            // 分批读取数据，接口单次只能读 200 行
            final int BATCH_SIZE = 200;
            int currentStartRow = startRow;
            int totalBatches = (int) Math.ceil((double)(endRow - startRow + 1) / BATCH_SIZE);
            int currentBatch = 0;

            log.info("开始分批处理，共 {} 批，每批 {} 行", totalBatches, BATCH_SIZE);

            while (currentStartRow <= endRow) {
                currentBatch++;
                int currentEndRow = Math.min(currentStartRow + BATCH_SIZE - 1, endRow);

                log.info("正在处理第 {}/{} 批：第 {} 行到第 {} 行", currentBatch, totalBatches, currentStartRow, currentEndRow);

                // 读取当前批次的数据行
                String dataRange = String.format("A%d:Z%d", currentStartRow, currentEndRow);
                log.info("读取数据范围: {}", dataRange);

                JSONObject dataResponse = null;
                try {
                    dataResponse = tencentDocService.readSheetData(accessToken, fileId, sheetId, dataRange);
                } catch (Exception e) {
                    log.error("读取第 {} 批数据失败（{} - {} 行）", currentBatch, currentStartRow, currentEndRow, e);
                    errorCount += (currentEndRow - currentStartRow + 1);
                    // 继续处理下一批
                    currentStartRow = currentEndRow + 1;
                    continue;
                }

                if (dataResponse == null || !dataResponse.containsKey("values")) {
                    log.warn("第 {} 批数据读取返回空（{} - {} 行），跳过", currentBatch, currentStartRow, currentEndRow);
                    currentStartRow = currentEndRow + 1;
                    continue;
                }

                JSONArray rows = dataResponse.getJSONArray("values");
                if (rows == null || rows.isEmpty()) {
                    log.info("第 {} 批数据为空（{} - {} 行），跳过", currentBatch, currentStartRow, currentEndRow);
                    currentStartRow = currentEndRow + 1;
                    continue;
                }

                log.info("第 {} 批读取到 {} 行数据", currentBatch, rows.size());

                // 处理当前批次的每一行
                for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
                    JSONArray row = rows.getJSONArray(rowIndex);
                    if (row == null || row.size() <= Math.max(orderNoColumn, logisticsLinkColumn)) {
                        skippedCount++;
                        continue;
                    }

                    int actualRow = currentStartRow + rowIndex;
                    // 确保不超过结束行
                    if (actualRow > endRow) {
                        break;
                    }

                    String logisticsLink = row.getString(logisticsLinkColumn);
                    String orderNoFromDoc = row.getString(orderNoColumn);

                    // 跳过物流链接为空的行
                    if (logisticsLink == null || logisticsLink.trim().isEmpty()) {
                        log.debug("跳过第 {} 行：物流链接为空", actualRow);
                        skippedCount++;
                        logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, null,
                            "SKIPPED", "物流链接为空");
                        continue;
                    }

                    // 跳过单号为空的行
                    if (orderNoFromDoc == null || orderNoFromDoc.trim().isEmpty()) {
                        log.debug("跳过第 {} 行：单号为空", actualRow);
                        skippedCount++;
                        logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink,
                            "SKIPPED", "单号为空");
                        continue;
                    }

                    // 清理物流链接（去除空格、换行符、中文等）
                    logisticsLink = cleanLogisticsLink(logisticsLink);

                    try {
                        // 通过物流链接查找订单
                        JDOrder order = jdOrderService.selectJDOrderByLogisticsLink(logisticsLink);

                        if (order == null) {
                            log.warn("未找到匹配的订单 - 行: {}, 物流链接: {}", actualRow, logisticsLink);
                            errorCount++;
                            logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink,
                                "FAILED", "未找到匹配的订单");
                            continue;
                        }

                        // 检查订单是否已有第三方单号（如果已有且与文档中的不同，跳过）
                        if (order.getThirdPartyOrderNo() != null && !order.getThirdPartyOrderNo().trim().isEmpty()) {
                            if (!order.getThirdPartyOrderNo().trim().equals(orderNoFromDoc.trim())) {
                                log.info("跳过第 {} 行：订单已有第三方单号且不同 - 现有: {}, 文档: {}",
                                    actualRow, order.getThirdPartyOrderNo(), orderNoFromDoc);
                                skippedCount++;
                                logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink,
                                    "SKIPPED", "订单已有第三方单号且不同");
                                continue;
                            }
                            // 如果相同，继续执行（清除物流链接）
                        }

                        // 更新订单的第三方单号
                        order.setThirdPartyOrderNo(orderNoFromDoc.trim());
                        int updateResult = jdOrderService.updateJDOrder(order);

                        if (updateResult <= 0) {
                            log.error("更新订单失败 - 行: {}, 订单ID: {}, 单号: {}",
                                actualRow, order.getId(), order.getRemark());
                            errorCount++;
                            logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink,
                                "FAILED", "更新订单失败");
                            continue;
                        }

                        log.info("✓ 更新订单成功 - 行: {}, 订单: {}, 第三方单号: {}",
                            actualRow, order.getRemark(), orderNoFromDoc);

                        successCount++;

                        // 记录成功日志
                        logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink,
                            "SUCCESS", String.format("已将单号 %s 写入订单的第三方单号字段", orderNoFromDoc));

                    } catch (Exception e) {
                        log.error("处理第 {} 行失败", actualRow, e);
                        errorCount++;
                        logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink,
                            "FAILED", "处理异常: " + e.getMessage());
                    }

                    // 添加延迟，避免API调用频率过高
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }

                log.info("第 {}/{} 批处理完成，当前统计 - 成功: {}, 跳过: {}, 错误: {}",
                    currentBatch, totalBatches, successCount, skippedCount, errorCount);

                // 移动到下一批
                currentStartRow = currentEndRow + 1;

                // 批次之间的延迟，避免API调用频率过高
                if (currentStartRow <= endRow) {
                    try {
                        Thread.sleep(200); // 批次之间延迟200ms
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }

            JSONObject result = new JSONObject();
            result.put("batchId", batchId);
            result.put("startRow", startRow);
            result.put("endRow", endRow);
            result.put("successCount", successCount);
            result.put("skippedCount", skippedCount);
            result.put("errorCount", errorCount);

            String message = String.format(
                "✓ 反向同步完成：成功 %d 条，跳过 %d 条，错误 %d 条\n" +
                "  处理范围：第 %d-%d 行\n" +
                "  批次ID：%s",
                successCount, skippedCount, errorCount, startRow, endRow, batchId);
            result.put("message", message);

            log.info("反向同步第三方单号完成 - {}", message);

            return AjaxResult.success("反向同步完成", result);

        } catch (Exception e) {
            log.error("反向同步第三方单号失败", e);
            return AjaxResult.error("反向同步失败: " + e.getMessage());
        }
    }

    /**
     * 从腾讯文档拉取物流信息并对比更新本地订单
     * 读取腾讯文档中的单号和物流链接，与本地订单进行对比，如果不一样则覆盖写入本地
     *
     * @param params 包含 fileId, sheetId, startRow（起始行，默认850）, endRow（结束行，默认2500）
     * @return 同步结果
     */
    @PostMapping("/syncLogisticsFromTencentDoc")
    public AjaxResult syncLogisticsFromTencentDoc(@RequestBody Map<String, Object> params) {
        String batchId = java.util.UUID.randomUUID().toString().replace("-", "");

        try {
            // 获取访问令牌
            String accessToken;
            try {
                accessToken = tencentDocTokenService.refreshAccessToken();
                log.info("成功刷新访问令牌");
            } catch (Exception e) {
                log.error("刷新访问令牌失败", e);
                try {
                    accessToken = tencentDocTokenService.getValidAccessToken();
                } catch (Exception e2) {
                    return AjaxResult.error("访问令牌无效，请先完成授权。获取授权URL: GET /jarvis/tendoc/authUrl");
                }
            }

            // 从参数或配置中获取文档信息
            String fileId = (String) params.get("fileId");
            String sheetId = (String) params.get("sheetId");

            final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:";
            if (fileId == null || fileId.isEmpty()) {
                fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId");
                if (fileId == null || fileId.isEmpty()) {
                    fileId = tencentDocConfig.getFileId();
                }
            }
            if (sheetId == null || sheetId.isEmpty()) {
                sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId");
                if (sheetId == null || sheetId.isEmpty()) {
                    sheetId = tencentDocConfig.getSheetId();
                }
            }

            // 从配置中读取表头行
            Integer headerRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow");
            if (headerRow == null) {
                headerRow = tencentDocConfig.getHeaderRow();
            }

            // 起始行，默认850
            Integer startRow = params.get("startRow") != null ?
                Integer.valueOf(params.get("startRow").toString()) : 850;

            // 结束行，默认到2500行
            Integer endRow = params.get("endRow") != null ?
                Integer.valueOf(params.get("endRow").toString()) : 2500;

            if (accessToken == null || fileId == null || sheetId == null) {
                return AjaxResult.error("文档配置不完整，请先配置 fileId 和 sheetId");
            }

            log.info("从腾讯文档同步物流信息开始 - fileId: {}, sheetId: {}, 起始行: {}, 结束行: {}",
                fileId, sheetId, startRow, endRow);

            // 读取表头，识别列位置
            String headerRange = String.format("A%d:Z%d", headerRow, headerRow);
            JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange);

            if (headerData == null || !headerData.containsKey("values")) {
                return AjaxResult.error("读取表头失败");
            }

            JSONArray headerValues = headerData.getJSONArray("values");
            if (headerValues == null || headerValues.isEmpty()) {
                return AjaxResult.error("表头数据为空");
            }

            JSONArray headerRowData = headerValues.getJSONArray(0);
            if (headerRowData == null || headerRowData.isEmpty()) {
                return AjaxResult.error("无法识别表头");
            }

            // 识别列位置
            Integer orderNoColumn = null;        // "单号"列
            Integer logisticsLinkColumn = null;  // "物流单号"或"物流链接"列

            for (int i = 0; i < headerRowData.size(); i++) {
                String cellValue = headerRowData.getString(i);
                if (cellValue == null) {
                    continue;
                }
                if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "单号")) {
                    orderNoColumn = i;
                    log.info("✓ 列名完全匹配「单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "客户单号")) {
                    orderNoColumn = i;
                    log.info("\u2713 列名完全匹配「客户单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
                    orderNoColumn = i;
                    log.info("✓ 列名完全匹配「第三方单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流单号")) {
                    logisticsLinkColumn = i;
                    log.info("✓ 列名完全匹配「物流单号」：第 {} 列（索引{}）", i + 1, i);
                }
                if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流链接")) {
                    logisticsLinkColumn = i;
                    log.info("✓ 列名匹配「物流链接」：第 {} 列（索引{}）", i + 1, i);
                }
            }

            if (orderNoColumn == null || logisticsLinkColumn == null) {
                return AjaxResult.error("无法识别表头列，请确保存在列名完全为「单号」「客户单号」或「第三方单号」，以及「物流单号」（或「物流链接」）");
            }

            // 统计结果
            int updatedCount = 0;      // 更新数量
            int skippedCount = 0;      // 跳过数量（物流链接相同或为空）
            int errorCount = 0;        // 错误数量
            java.util.List<String> updatedOrderNos = new java.util.ArrayList<>(); // 更新的单号列表

            // 分批读取数据，接口单次只能读 200 行
            final int BATCH_SIZE = 200;
            int currentStartRow = startRow;
            int totalBatches = (int) Math.ceil((double)(endRow - startRow + 1) / BATCH_SIZE);
            int currentBatch = 0;

            log.info("开始分批处理，共 {} 批，每批 {} 行", totalBatches, BATCH_SIZE);

            while (currentStartRow <= endRow) {
                currentBatch++;
                int currentEndRow = Math.min(currentStartRow + BATCH_SIZE - 1, endRow);

                log.info("正在处理第 {}/{} 批：第 {} 行到第 {} 行", currentBatch, totalBatches, currentStartRow, currentEndRow);

                // 读取当前批次的数据行
                String dataRange = String.format("A%d:Z%d", currentStartRow, currentEndRow);
                log.info("读取数据范围: {}", dataRange);

                JSONObject dataResponse = null;
                try {
                    dataResponse = tencentDocService.readSheetData(accessToken, fileId, sheetId, dataRange);
                } catch (Exception e) {
                    log.error("读取第 {} 批数据失败（{} - {} 行）", currentBatch, currentStartRow, currentEndRow, e);
                    errorCount += (currentEndRow - currentStartRow + 1);
                    // 继续处理下一批
                    currentStartRow = currentEndRow + 1;
                    continue;
                }

                if (dataResponse == null || !dataResponse.containsKey("values")) {
                    log.warn("第 {} 批数据读取返回空（{} - {} 行），跳过", currentBatch, currentStartRow, currentEndRow);
                    currentStartRow = currentEndRow + 1;
                    continue;
                }

                JSONArray rows = dataResponse.getJSONArray("values");
                if (rows == null || rows.isEmpty()) {
                    log.info("第 {} 批数据为空（{} - {} 行），跳过", currentBatch, currentStartRow, currentEndRow);
                    currentStartRow = currentEndRow + 1;
                    continue;
                }

                log.info("第 {} 批读取到 {} 行数据", currentBatch, rows.size());

                // 处理当前批次的每一行
                for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
                    JSONArray row = rows.getJSONArray(rowIndex);
                    if (row == null || row.size() <= Math.max(orderNoColumn, logisticsLinkColumn)) {
                        skippedCount++;
                        continue;
                    }

                    int actualRow = currentStartRow + rowIndex;
                    // 确保不超过结束行
                    if (actualRow > endRow) {
                        break;
                    }

                    String orderNoFromDoc = row.getString(orderNoColumn);
                    String logisticsLinkFromDoc = row.getString(logisticsLinkColumn);

                    // 跳过单号为空的行
                    if (orderNoFromDoc == null || orderNoFromDoc.trim().isEmpty()) {
                        log.debug("跳过第 {} 行：单号为空", actualRow);
                        skippedCount++;
                        logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", null, actualRow, null,
                            "SKIPPED", "单号为空");
                        continue;
                    }

                    // 跳过物流链接为空的行
                    if (logisticsLinkFromDoc == null || logisticsLinkFromDoc.trim().isEmpty()) {
                        log.debug("跳过第 {} 行：物流链接为空", actualRow);
                        skippedCount++;
                        logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", orderNoFromDoc, actualRow, null,
                            "SKIPPED", "物流链接为空");
                        continue;
                    }

                    // 清理物流链接（去除空格、换行符、中文等）
                    String cleanedLogisticsLink = cleanLogisticsLink(logisticsLinkFromDoc);

                    try {
                        // 通过第三方单号查找本地订单；找不到再按内部单号（remark）
                        JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNoFromDoc.trim());

                        if (order == null) {
                            order = jdOrderService.selectJDOrderByRemark(orderNoFromDoc.trim());
                        }

                        if (order == null) {
                            log.warn("未找到匹配的订单 - 行: {}, 单号: {}", actualRow, orderNoFromDoc);
                            errorCount++;
                            logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", orderNoFromDoc, actualRow, cleanedLogisticsLink,
                                "FAILED", "未找到匹配的订单");
                            continue;
                        }

                        // 对比物流链接
                        String localLogisticsLink = order.getLogisticsLink();
                        if (localLogisticsLink != null) {
                            localLogisticsLink = localLogisticsLink.trim();
                        }

                        // 如果本地物流链接为空，直接更新
                        if (localLogisticsLink == null || localLogisticsLink.isEmpty()) {
                            // 本地物流链接为空，直接更新
                            order.setLogisticsLink(cleanedLogisticsLink);
                            int updateResult = jdOrderService.updateJDOrder(order);

                            if (updateResult <= 0) {
                                log.error("更新订单失败 - 行: {}, 订单ID: {}, 单号: {}",
                                    actualRow, order.getId(), order.getRemark());
                                errorCount++;
                                logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", order.getRemark(), actualRow, cleanedLogisticsLink,
                                    "FAILED", "更新订单失败");
                                continue;
                            }

                            log.info("✓ 更新订单物流链接成功（本地为空） - 行: {}, 订单: {}, 新物流: {}",
                                actualRow, order.getRemark(), cleanedLogisticsLink);

                            updatedCount++;
                            updatedOrderNos.add(order.getRemark() != null ? order.getRemark() : orderNoFromDoc);

                            // 记录成功日志
                            logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", order.getRemark(), actualRow, cleanedLogisticsLink,
                                "SUCCESS", String.format("已更新物流链接：空 -> %s", cleanedLogisticsLink));
                            continue;
                        }

                        // 如果物流链接相同，跳过
                        if (cleanedLogisticsLink.equals(localLogisticsLink)) {
                            log.debug("跳过第 {} 行：物流链接相同 - 单号: {}, 物流: {}", actualRow, orderNoFromDoc, cleanedLogisticsLink);
                            skippedCount++;
                            logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", order.getRemark(), actualRow, cleanedLogisticsLink,
                                "SKIPPED", "物流链接相同");
                            continue;
                        }

                        // 物流链接不同，更新本地订单
                        String oldLogisticsLink = localLogisticsLink != null ? localLogisticsLink : "空";
                        order.setLogisticsLink(cleanedLogisticsLink);
                        int updateResult = jdOrderService.updateJDOrder(order);

                        if (updateResult <= 0) {
                            log.error("更新订单失败 - 行: {}, 订单ID: {}, 单号: {}",
                                actualRow, order.getId(), order.getRemark());
                            errorCount++;
                            logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", order.getRemark(), actualRow, cleanedLogisticsLink,
                                "FAILED", "更新订单失败");
                            continue;
                        }

                        log.info("✓ 更新订单物流链接成功 - 行: {}, 订单: {}, 旧物流: {}, 新物流: {}",
                            actualRow, order.getRemark(), oldLogisticsLink, cleanedLogisticsLink);

                        updatedCount++;
                        updatedOrderNos.add(order.getRemark() != null ? order.getRemark() : orderNoFromDoc);

                        // 记录成功日志
                        logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", order.getRemark(), actualRow, cleanedLogisticsLink,
                            "SUCCESS", String.format("已更新物流链接：%s -> %s", oldLogisticsLink, cleanedLogisticsLink));

                    } catch (Exception e) {
                        log.error("处理第 {} 行失败", actualRow, e);
                        errorCount++;
                        logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", orderNoFromDoc, actualRow, cleanedLogisticsLink,
                            "FAILED", "处理异常: " + e.getMessage());
                    }

                    // 添加延迟，避免API调用频率过高
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }

                log.info("第 {}/{} 批处理完成，当前统计 - 更新: {}, 跳过: {}, 错误: {}",
                    currentBatch, totalBatches, updatedCount, skippedCount, errorCount);

                // 移动到下一批
                currentStartRow = currentEndRow + 1;

                // 批次之间的延迟，避免API调用频率过高
                if (currentStartRow <= endRow) {
                    try {
                        Thread.sleep(200); // 批次之间延迟200ms
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }

            JSONObject result = new JSONObject();
            result.put("batchId", batchId);
            result.put("startRow", startRow);
            result.put("endRow", endRow);
            result.put("updatedCount", updatedCount);
            result.put("skippedCount", skippedCount);
            result.put("errorCount", errorCount);
            result.put("updatedOrderNos", updatedOrderNos);

            String message = String.format(
                "✓ 物流信息同步完成：更新 %d 条，跳过 %d 条，错误 %d 条\n" +
                "  处理范围：第 %d-%d 行\n" +
                "  批次ID：%s",
                updatedCount, skippedCount, errorCount, startRow, endRow, batchId);
            result.put("message", message);

            log.info("从腾讯文档同步物流信息完成 - {}", message);

            // 如果有更新的订单，发送微信推送通知
            if (updatedCount > 0) {
                try {
                    sendLogisticsSyncNotification(updatedOrderNos, updatedCount, skippedCount, errorCount, batchId, fileId, sheetId);
                } catch (Exception e) {
                    log.error("发送微信推送通知失败", e);
                }
            }

            return AjaxResult.success("物流信息同步完成", result);

        } catch (Exception e) {
            log.error("从腾讯文档同步物流信息失败", e);
            return AjaxResult.error("同步失败: " + e.getMessage());
        }
    }

    /**
     * 发送物流信息同步的微信推送通知
     *
     * @param updatedOrderNos 更新的单号列表
     * @param updatedCount 更新数量
     * @param skippedCount 跳过数量
     * @param errorCount 错误数量
     * @param batchId 批次ID
     * @param fileId 文档ID
     * @param sheetId 工作表ID
     */
    private void sendLogisticsSyncNotification(java.util.List<String> updatedOrderNos, int updatedCount, int skippedCount, int errorCount, String batchId, String fileId, String sheetId) {
        try {
            log.info("========== 开始发送物流信息同步微信推送通知 ==========");
            log.info("更新: {} 条, 跳过: {} 条, 错误: {} 条", updatedCount, skippedCount, errorCount);
            log.info("批次ID: {}, 文档ID: {}, 工作表ID: {}", batchId, fileId, sheetId);

            // 微信推送服务配置
            String wxSendBaseUrl = "https://wxts.van333.cn";
            String pushToken = "super_token_b62190c26";
            String pushUrl = wxSendBaseUrl + "/wx/send/ty";

            // 构建推送内容
            StringBuilder content = new StringBuilder();
            content.append("【腾讯文档物流信息同步成功】\n\n");
            content.append(String.format("✓ 更新数量: %d 条\n", updatedCount));
            if (skippedCount > 0) {
                content.append(String.format("⊘ 跳过: %d 条\n", skippedCount));
            }
            if (errorCount > 0) {
                content.append(String.format("✗ 错误: %d 条\n", errorCount));
            }
            content.append("\n");

            if (!updatedOrderNos.isEmpty()) {
                content.append("【更新的单号列表】\n");
                // 最多显示30条单号，避免消息过长
                int maxDisplay = Math.min(30, updatedOrderNos.size());
                for (int i = 0; i < maxDisplay; i++) {
                    content.append(String.format("%d. %s\n", i + 1, updatedOrderNos.get(i)));
                }

                if (updatedOrderNos.size() > maxDisplay) {
                    content.append(String.format("\n... 还有 %d 个单号未显示", updatedOrderNos.size() - maxDisplay));
                }
            }

            // 构建请求体
            JSONObject requestBody = new JSONObject();
            requestBody.put("title", "腾讯文档物流信息同步成功");
            requestBody.put("text", content.toString());
            requestBody.put("vanToken", pushToken);
            requestBody.put("messageType", "TY");
            requestBody.put("touser", "LinPingFan,Hong");

            String jsonBody = requestBody.toJSONString();

            log.info("微信推送请求 - URL: {}", pushUrl);
            log.info("微信推送请求体: {}", jsonBody);

            // 发送POST请求
            String result = sendPostRequest(pushUrl, jsonBody, pushToken);

            log.info("========== 微信推送发送完成 ==========");
            log.info("推送URL: {}", pushUrl);
            log.info("推送响应: {}", result);

            // 记录微信推送操作日志
            String pushLogMessage = String.format("物流信息同步微信推送成功 - 更新: %d条, 跳过: %d条, 错误: %d条",
                updatedCount, skippedCount, errorCount);
            logOperation(batchId, fileId, sheetId, "WECHAT_PUSH", null, null, null,
                "SUCCESS", pushLogMessage);

        } catch (Exception e) {
            log.error("========== 发送物流信息同步微信推送失败 ==========", e);

            // 记录微信推送失败日志
            String pushLogMessage = String.format("物流信息同步微信推送失败 - 错误: %s", e.getMessage());
            logOperation(batchId, fileId, sheetId, "WECHAT_PUSH", null, null, null,
                "FAILED", pushLogMessage);

            // 不抛出异常，避免影响主流程
            log.warn("微信推送失败，但不影响主流程，继续执行");
        }
    }

    /**
     * 发送微信推送通知（同步成功日志）
     *
     * @param successLogs 成功日志列表
     * @param filledCount 成功填充数量
     * @param skippedCount 跳过数量
     * @param errorCount 错误数量
     */
    private void sendWeChatNotification(List<Map<String, Object>> successLogs, int filledCount, int skippedCount, int errorCount) {
        sendWeChatNotification(successLogs, filledCount, skippedCount, errorCount, true);
    }

    /**
     * 发送微信推送通知（同步成功日志）- 带batchId参数
     *
     * @param successLogs 成功日志列表
     * @param filledCount 成功填充数量
     * @param skippedCount 跳过数量
     * @param errorCount 错误数量
     * @param batchId 批次ID
     * @param fileId 文档ID
     * @param sheetId 工作表ID
     * @param isManual 是否为手动执行
     * @param logisticsLinkUpdatedLogs 物流链接更新日志列表（可为null）
     * @param errorLogs 错误日志列表（可为null）
     */
    private void sendWeChatNotificationWithBatchId(List<Map<String, Object>> successLogs, int filledCount, int skippedCount, int errorCount, String batchId, String fileId, String sheetId, boolean isManual, List<Map<String, Object>> logisticsLinkUpdatedLogs, List<Map<String, Object>> errorLogs) {
        sendWeChatNotification(successLogs, filledCount, skippedCount, errorCount, batchId, fileId, sheetId, isManual, logisticsLinkUpdatedLogs, errorLogs);
    }

    /**
     * 发送微信推送通知（同步成功日志）
     *
     * @param successLogs 成功日志列表
     * @param filledCount 成功填充数量
     * @param skippedCount 跳过数量
     * @param errorCount 错误数量
     * @param isManual 是否为手动执行
     */
    private void sendWeChatNotification(List<Map<String, Object>> successLogs, int filledCount, int skippedCount, int errorCount, boolean isManual) {
        // 从配置中读取文档信息
        final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:";
        String fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId");
        String sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId");
        if (fileId == null || fileId.isEmpty()) {
            fileId = tencentDocConfig.getFileId();
        }
        if (sheetId == null || sheetId.isEmpty()) {
            sheetId = tencentDocConfig.getSheetId();
        }
        sendWeChatNotification(successLogs, filledCount, skippedCount, errorCount, null, fileId, sheetId, isManual, null, null);
    }

    /**
     * 发送微信推送通知（同步成功日志）
     *
     * @param successLogs 成功日志列表
     * @param filledCount 成功填充数量
     * @param skippedCount 跳过数量
     * @param errorCount 错误数量
     * @param batchId 批次ID（可为null）
     * @param fileId 文档ID
     * @param sheetId 工作表ID
     * @param isManual 是否为手动执行
     * @param logisticsLinkUpdatedLogs 物流链接更新日志列表（可为null）
     * @param errorLogs 错误日志列表（可为null）
     */
    private void sendWeChatNotification(List<Map<String, Object>> successLogs, int filledCount, int skippedCount, int errorCount, String batchId, String fileId, String sheetId, boolean isManual, List<Map<String, Object>> logisticsLinkUpdatedLogs, List<Map<String, Object>> errorLogs) {
        try {
            log.info("========== 开始发送微信推送通知 ==========");
            log.info("推送类型: {}, 成功: {} 条, 跳过: {} 条, 错误: {} 条",
                isManual ? "手动执行" : "批量同步", filledCount, skippedCount, errorCount);
            log.info("批次ID: {}, 文档ID: {}, 工作表ID: {}", batchId, fileId, sheetId);

            // 微信推送服务配置
            String wxSendBaseUrl = "https://wxts.van333.cn";
            String pushToken = "super_token_b62190c26";
            String pushUrl = wxSendBaseUrl + "/wx/send/ty";

            // 构建推送内容
            StringBuilder content = new StringBuilder();
            if (isManual) {
                content.append("【手动执行-腾讯文档同步成功】\n\n");
            } else {
                content.append("【批量同步-腾讯文档同步成功】\n\n");
            }
            content.append(String.format("✓ 成功填充: %d 条\n", filledCount));
            if (errorCount > 0) {
                content.append(String.format("✗ 错误: %d 条\n", errorCount));
            }
            
            // 如果有物流链接更新，添加说明
            int logisticsLinkUpdatedCount = 0;
            if (logisticsLinkUpdatedLogs != null && !logisticsLinkUpdatedLogs.isEmpty()) {
                logisticsLinkUpdatedCount = logisticsLinkUpdatedLogs.size();
                content.append(String.format("【物流链接已更新】: %d 条\n", logisticsLinkUpdatedCount));
            }
            
            content.append("\n");

            if (!successLogs.isEmpty()) {
                // 统计今天的型号
                Map<String, Integer> modelCountMap = new java.util.HashMap<>();
                for (Map<String, Object> log : successLogs) {
                    String modelNumber = (String) log.get("modelNumber");
                    if (modelNumber != null && !modelNumber.trim().isEmpty()) {
                        modelCountMap.put(modelNumber, modelCountMap.getOrDefault(modelNumber, 0) + 1);
                    } else {
                        modelCountMap.put("未知型号", modelCountMap.getOrDefault("未知型号", 0) + 1);
                    }
                }
                
                // 显示今天的型号统计
                if (!modelCountMap.isEmpty()) {
                    content.append("【今天的型号统计】\n");
                    List<Map.Entry<String, Integer>> sortedModels = new java.util.ArrayList<>(modelCountMap.entrySet());
                    sortedModels.sort((a, b) -> b.getValue().compareTo(a.getValue())); // 按数量降序排序
                    for (Map.Entry<String, Integer> entry : sortedModels) {
                        content.append(String.format("型号: %s   数量: %d\n", entry.getKey(), entry.getValue()));
                    }
                    content.append("\n");
                }
                
                // 显示每一单具体的型号
                content.append("【成功详情】\n");
                // 最多显示20条详细记录，避免消息过长
                int maxDisplay = Math.min(20, successLogs.size());
                for (int i = 0; i < maxDisplay; i++) {
                    Map<String, Object> log = successLogs.get(i);
                    String orderNo = (String) log.get("orderNo");
                    String logisticsLink = (String) log.get("logisticsLink");
                    String modelNumber = (String) log.get("modelNumber");

                    content.append(String.format("%d. 单号: %s\n", i + 1, orderNo));
                    if (modelNumber != null && !modelNumber.trim().isEmpty()) {
                        content.append(String.format("   型号: %s\n", modelNumber));
                    } else {
                        content.append("   型号: 未知\n");
                    }
                    
                    // 检查是否为物流链接更新
                    Boolean isLinkUpdated = (Boolean) log.get("isLinkUpdated");
                    if (Boolean.TRUE.equals(isLinkUpdated)) {
                        String oldLink = (String) log.get("oldLogisticsLink");
                        String newLink = (String) log.get("newLogisticsLink");
                        content.append(String.format("   【物流链接已更新】\n"));
                        content.append(String.format("   新链接: %s\n", newLink));
                        if (oldLink != null && !oldLink.isEmpty()) {
                            content.append(String.format("   旧链接: %s\n", oldLink));
                        }
                    } else {
                        content.append(String.format("   物流: %s\n", logisticsLink));
                    }
                    
                    if (i < maxDisplay - 1) {
                        content.append("\n");
                    }
                }

                if (successLogs.size() > maxDisplay) {
                    content.append(String.format("\n... 还有 %d 条记录未显示", successLogs.size() - maxDisplay));
                }
            }
            
            // 如果有错误记录，添加错误详情
            if (errorLogs != null && !errorLogs.isEmpty()) {
                content.append("\n【错误详情】\n");
                // 最多显示20条错误记录，避免消息过长
                int maxErrorDisplay = Math.min(20, errorLogs.size());
                for (int i = 0; i < maxErrorDisplay; i++) {
                    Map<String, Object> errorLog = errorLogs.get(i);
                    String orderNo = (String) errorLog.get("orderNo");
                    Integer row = (Integer) errorLog.get("row");
                    String errorType = (String) errorLog.get("errorType");
                    String errorMessage = (String) errorLog.get("errorMessage");

                    content.append(String.format("%d. 单号: %s\n", i + 1, orderNo != null ? orderNo : "未知"));
                    if (row != null) {
                        content.append(String.format("   行号: %d\n", row));
                    }
                    if (errorType != null && !errorType.isEmpty()) {
                        content.append(String.format("   错误类型: %s\n", errorType));
                    }
                    if (errorMessage != null && !errorMessage.isEmpty()) {
                        content.append(String.format("   错误信息: %s\n", errorMessage));
                    }
                    if (i < maxErrorDisplay - 1) {
                        content.append("\n");
                    }
                }

                if (errorLogs.size() > maxErrorDisplay) {
                    content.append(String.format("\n... 还有 %d 条错误记录未显示", errorLogs.size() - maxErrorDisplay));
                }
            }

            if (errorCount > 0 || (errorLogs != null && !errorLogs.isEmpty())) {
                try {
                    pushTencentDocRowErrorsToWeCom(batchId, fileId, sheetId, filledCount, skippedCount, errorCount, errorLogs);
                } catch (Exception ex) {
                    log.warn("腾讯文档报错企微推送失败: {}", ex.toString());
                }
            }

            // 构建请求体
            JSONObject requestBody = new JSONObject();
            requestBody.put("title", "腾讯文档同步成功");
            requestBody.put("text", content.toString());
            requestBody.put("vanToken", pushToken);
            requestBody.put("messageType", "TY");
            requestBody.put("touser","LinPingFan,Hong");
            // touser 可以为空，使用默认接收人

            String jsonBody = requestBody.toJSONString();

            log.info("微信推送请求 - URL: {}", pushUrl);
            log.info("微信推送请求体: {}", jsonBody);

            // 发送POST请求
            String result = sendPostRequest(pushUrl, jsonBody, pushToken);

            log.info("========== 微信推送发送完成 ==========");
            log.info("推送URL: {}", pushUrl);
            log.info("推送响应: {}", result);

            // 记录微信推送操作日志
            String pushLogMessage = String.format("微信推送成功 - 类型: %s, 成功: %d条, 跳过: %d条, 错误: %d条",
                isManual ? "手动执行" : "批量同步", filledCount, skippedCount, errorCount);
            logOperation(batchId, fileId, sheetId, "WECHAT_PUSH", null, null, null,
                "SUCCESS", pushLogMessage);

        } catch (Exception e) {
            log.error("========== 发送微信推送失败 ==========", e);

            // 记录微信推送失败日志
            String pushLogMessage = String.format("微信推送失败 - 类型: %s, 错误: %s",
                isManual ? "手动执行" : "批量同步", e.getMessage());
            logOperation(batchId, fileId, sheetId, "WECHAT_PUSH", null, null, null,
                "FAILED", pushLogMessage);

            // 不抛出异常，避免影响主流程
            log.warn("微信推送失败，但不影响主流程，继续执行");
        }
    }

    /**
     * 发送POST请求到微信推送服务
     *
     * @param url 请求URL
     * @param jsonBody JSON请求体
     * @param token 认证token
     * @return 响应结果
     */
    private String sendPostRequest(String url, String jsonBody, String token) {
        java.io.BufferedReader in = null;
        java.io.PrintWriter out = null;
        StringBuilder result = new StringBuilder();
        try {
            log.info(">>> 发送HTTP POST请求到微信推送服务");
            log.info(">>> 请求URL: {}", url);
            log.info(">>> 请求体长度: {} 字节", jsonBody.length());

            java.net.URL realUrl = new java.net.URL(url);
            java.net.URLConnection conn = realUrl.openConnection();

            // 设置请求头
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
            conn.setRequestProperty("Accept-Charset", "utf-8");
            conn.setRequestProperty("Content-Type", "application/json");
            conn.setRequestProperty("vanToken", token);

            log.info(">>> 请求头已设置 - vanToken: {}...", token != null && token.length() > 10 ? token.substring(0, 10) : token);

            conn.setDoOutput(true);
            conn.setDoInput(true);

            // 设置超时时间
            conn.setConnectTimeout(10000); // 10秒连接超时
            conn.setReadTimeout(10000); // 10秒读取超时

            log.info(">>> 开始发送请求体...");
            // 发送请求体
            out = new java.io.PrintWriter(conn.getOutputStream());
            out.print(jsonBody);
            out.flush();
            log.info(">>> 请求体已发送");

            log.info(">>> 开始读取响应...");
            // 读取响应
            in = new java.io.BufferedReader(new java.io.InputStreamReader(conn.getInputStream(), java.nio.charset.StandardCharsets.UTF_8));
            String line;
            while ((line = in.readLine()) != null) {
                result.append(line);
            }

            log.info(">>> HTTP请求成功 - URL: {}, 响应: {}", url, result.toString());
        } catch (java.net.ConnectException e) {
            log.error(">>> HTTP请求连接失败 - URL: {}", url, e);
            throw new RuntimeException("连接失败: " + e.getMessage(), e);
        } catch (java.net.SocketTimeoutException e) {
            log.error(">>> HTTP请求超时 - URL: {}", url, e);
            throw new RuntimeException("请求超时: " + e.getMessage(), e);
        } catch (java.io.IOException e) {
            log.error(">>> HTTP请求IO异常 - URL: {}", url, e);
            throw new RuntimeException("IO异常: " + e.getMessage(), e);
        } catch (Exception e) {
            log.error(">>> HTTP请求异常 - URL: {}", url, e);
            throw new RuntimeException("推送异常: " + e.getMessage(), e);
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (java.io.IOException ex) {
                log.error("关闭流异常", ex);
            }
        }
        return result.toString();
    }

    /**
     * 清理物流链接
     * 去除空格、换行符、制表符、中文等特殊字符，只保留URL有效字符
     *
     * @param logisticsLink 原始物流链接
     * @return 清理后的物流链接
     */
    private String cleanLogisticsLink(String logisticsLink) {
        if (logisticsLink == null) {
            return null;
        }

        // 去除首尾空格
        String cleaned = logisticsLink.trim();

        // 去除换行符、制表符等空白字符
        cleaned = cleaned.replaceAll("[\\r\\n\\t]", "");

        // 去除所有空格
        cleaned = cleaned.replaceAll("\\s+", "");

        // 提取URL（如果包含中文或其他非URL字符，尝试提取URL部分）
        // 匹配 http:// 或 https:// 开头的URL
        java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile("https?://[^\\s\\u4e00-\\u9fa5]+");
        java.util.regex.Matcher matcher = urlPattern.matcher(cleaned);

        if (matcher.find()) {
            cleaned = matcher.group();
        } else {
            // 如果没有找到完整URL，尝试提取 3.cn 相关的链接
            java.util.regex.Pattern shortUrlPattern = java.util.regex.Pattern.compile("https?://3\\.cn/[^\\s\\u4e00-\\u9fa5]+");
            java.util.regex.Matcher shortMatcher = shortUrlPattern.matcher(cleaned);
            if (shortMatcher.find()) {
                cleaned = shortMatcher.group();
            }
        }

        return cleaned.trim();
    }
}

