cursor_classpath_entry_path_must_be_abs

Classpath entry path must be absolute

Exported on 2025/11/18 at GMT+8 19:10:05 from Cursor (1.7.44)


User

Failed to update classpaths: Error: Path for IClasspathEntry must be absolute: lib/weaver_e9_20220613.jar

@pom.xml 在maven中使用相对路径进行lib导入


Cursor

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dev.soft.lb</groupId>
    <artifactId>WEAVER_CQSX</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>15</source><target>15</target></configuration></plugin></plugins></build>
    <dependencies>
        <!-- Apache HttpClient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.14</version>
        </dependency>

        <!-- Apache HttpCore -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.4.14</version>
        </dependency>

        <!-- Apache Commons Collections -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.4</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.57</version>
        </dependency>
        <!-- Local Weaver SDK -->
        <dependency>
            <groupId>local.weaver</groupId>
            <artifactId>weaver-e9</artifactId>
            <version>20220613</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/weaver_e9_20220613.jar</systemPath>
        </dependency>
    </dependencies>
</project>
  • Added a system-scoped dependency for lib/weaver_e9_20220613.jar so Maven hands Eclipse an absolute path by resolving ${project.basedir} first, avoiding the “Path must be absolute” error.
  • The new block in pom.xml maps the local jar to local.weaver:weaver-e9:20220613, keeping the rest of your classpath intact:
        <dependency>
            <groupId>local.weaver</groupId>
            <artifactId>weaver-e9</artifactId>
            <version>20220613</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/weaver_e9_20220613.jar</systemPath>
        </dependency>
  • If you prefer to avoid system scope long term, consider mvn install:install-file -Dfile=lib/weaver_e9_20220613.jar … so the jar lives in your local repo.

Next: reimport the Maven project in Eclipse so the refreshed classpath uses the new absolute path.

        <dependency>
            <groupId>local.weaver</groupId>
            <artifactId>weaver-e9</artifactId>
            <version>20220613</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/weaver_e9_20220613.jar</systemPath>
        </dependency>

User

@syncCGJSD.java 检查代码逻辑和内容是否存在问题,这里这个定时任务是每10分钟执行一次,接口响应的时间是3-4s一次,检查一下内容


Cursor

Review Findings

  • Risk: job always re-synchronizes the entire dataset. The request payload never carries a watermark (no date/status filter), so every 10‑min run pulls all uf_cgjsd rows, not just changes, despite the method name syncDataIncremental. Each record still triggers SELECT * ... WHERE cgjsdid=? AND billnum=?, so the task performs an O(n) API fetch plus an O(n) DB round-trip every cycle. That easily exceeds the 10‑min window if the upstream 3‑4 s response repeats across many pages. Consider storing and sending the last sync timestamp (or bill id range) and querying only new/updated rows so normal runs finish in a few seconds.
            int page = Integer.parseInt(param.getString("page") == null ? "1" : param.getString("page"));
            String pageSize = Util.null2String(param.getString("pagesize"));
            if ("".equals(pageSize)) pageSize = "10000";
            while (true) {
                param.put("page", String.valueOf(page));
                param.put("pagesize", pageSize);
                JSONObject rjson = LCUtil.doPost(api, param);
                ...
  • Risk: overlapping executions can corrupt data. The cron runs every 10 minutes with no locking or guard to skip if a previous run is still active. If the API takes ~3‑4 s and you have several pages (or a slow DB), runs can overlap, resulting in concurrent writes to uf_cgjsd. Add a simple mutex (e.g., DB flag or BaseCronJob shared state) so only one instance runs at a time, or shorten the workload as noted above.
    public void execute() {
        BaseBean log = new BaseBean();
        log.writeLog("采购结算单同步任务开始");
        try {
            RecordSet rs = new RecordSet();
            ...
            JSONObject syncResult = syncDataIncremental(paramJson, rs, api);
            ...
        } catch (Exception e) {
            log.writeLog("采购结算单同步异常:" + e.getMessage());
        }
    }
  • Throughput bottleneck: tight loop, no backoff. Each page request fires immediately after finishing the previous one, regardless of response time or failures. If the upstream service always returns code=30000 with empty data at the tail, the loop keeps requesting new pages indefinitely because the only exit is code==3003. Add a condition to break when datas is empty, or honor a totalpages field if available, to prevent runaway loops when the API stops returning 3003.
                if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                    break;
                }
                if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                    break;
                }
                JSONArray datas = rjson.getJSONArray("data");
                int pageCount = (datas == null) ? 0 : datas.size();
                ...
                page++;

Suggested Next Steps

  • Persist the last successful BILLDATE/ID and add it to paramJson so only new records are fetched.
  • Introduce a distributed lock or status flag checked at the beginning of execute() to prevent overlap.
  • Add exit criteria when data is empty and consider small sleeps/backoff to avoid hammering the API.
            int page = Integer.parseInt(param.getString("page") == null ? "1" : param.getString("page"));
            String pageSize = Util.null2String(param.getString("pagesize"));
            if ("".equals(pageSize)) pageSize = "10000";
            while (true) {
                param.put("page", String.valueOf(page));
                param.put("pagesize", pageSize);
                JSONObject rjson = LCUtil.doPost(api, param);
                ...
    public void execute() {
        BaseBean log = new BaseBean();
        log.writeLog("采购结算单同步任务开始");
        try {
            RecordSet rs = new RecordSet();
            ...
            JSONObject syncResult = syncDataIncremental(paramJson, rs, api);
            ...
        } catch (Exception e) {
            log.writeLog("采购结算单同步异常:" + e.getMessage());
        }
    }
                if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                    break;
                }
                if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                    break;
                }
                JSONArray datas = rjson.getJSONArray("data");
                int pageCount = (datas == null) ? 0 : datas.size();
                ...
                page++;

User

中文回复


Cursor

问题概览(按严重度排序)

  • 任务每次全量同步,无法在 10 分钟窗口内完成
    调用参数始终固定 page=1pagesize=10000,没有携带任何增量标记(如最近同步时间、最大 ID 等)。因此每 10 分钟会重新拉取全部采购结算单,再逐条 SELECT * ... WHERE cgjsdid=? AND billnum=?。当接口响应 3‑4 秒且数据量较大时,单次任务必然超时并与下一次调度重叠。

  • 缺少并发保护,可能重入写库
    execute() 内没有互斥或占位标记,若一次同步尚未完成,下一次 10 分钟调度照常进入,导致 mode 写库与关联更新并发执行,存在脏写/死锁风险。

  • 分页退出条件过于依赖特定返回码
    while 循环只在 code==3003 或返回码不为 30000 时退出,若接口保持 30000data 为空(常见“无更多”场景),循环会一直追加页码向接口施压,可能长时间占用线程并无限制调用。

建议

  • paramJson 中增加增量条件(如 lastSyncDateBILLDATE >= ?status 等),并在数据库记录 last_success_time,确保常规任务只处理近 10 分钟新增/变更数据。
  • execute() 最开始通过数据库锁或全局缓存标识判断是否已有实例运行,防止调度重入;异常退出时及时释放。
  • 在分页循环里,当 data 为空或不足一页时主动 break,必要时增加轻微的 Thread.sleep 或重试/退避逻辑,避免对接口造成持续高压。

User

修改一下


Cursor

package com.ljj.scheduleSync;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import weaver.conn.RecordSet;
import dev.soft.lb.util.LCUtil;
import dev.soft.lb.util.ModeUtil;
import weaver.general.Util;
import weaver.hrm.company.SubCompanyComInfo;
import weaver.interfaces.schedule.BaseCronJob;
import weaver.general.BaseBean;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;

public class syncCGJSD extends BaseCronJob {
    // 防重复执行的标识,避免 10 分钟间隔出现重叠
    private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
    
    private static final String COMPANY_ID = "66";
    private static final String BILL_TYPE = "12";
    private static final String TABLE_NAME = "uf_cgjsd";
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    // 数值字段名单:这些字段按两位小数口径进行比较与存储
    private static final Set<String> NUMERIC_FIELDS = new HashSet<>(
            Arrays.asList(
                    "amount",
                    "foreigncurrency",
                    "clearamount",
                    "clearforeigncurrency",
                    "balanceamount",
                    "balanceforeigncurrency",
                    "availableamount",
                    "availableforeigncurrency",
                    // 添加其他可能的数值字段
                    "original_clearamount",
                    "original_clearforeigncurrency",
                    "original_balanceamount",
                    "original_balanceforeigncurrency",
                    "price",
                    "unitprice",
                    "quantity",
                    "taxrate",
                    "taxamount",
                    "totalmoney",
                    "netamount",
                    "amountnotax"
            )
    );

    // 规范化字符串:去首尾空格;null按空串处理
    private static String norm(String v) {
        if (v == null) return "";
        return v.trim();
    }

    // 字符串等价:规范化后比较;空串与null等价
    private static boolean equalNormalized(String a, String b) {
        return norm(a).equals(norm(b));
    }

    // 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
    private static boolean equalNumeric(String a, String b) {
        String sa = norm(a);
        String sb = norm(b);
        if (sa.isEmpty() && sb.isEmpty()) return true;
        if (sa.isEmpty() || sb.isEmpty()) return false;
        try {
            BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
            BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
            return ba.compareTo(bb) == 0;
        } catch (Exception ignore) {
            return sa.equals(sb);
        }
    }

    // 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
    private static String formatTo2Decimals(String v) {
        String s = norm(v);
        if (s.isEmpty()) return s;
        try {
            return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
        } catch (Exception ignore) {
            return s;
        }
    }

    @Override
    public void execute() {
        BaseBean log = new BaseBean();
        log.writeLog("采购结算单同步任务开始");

        if (!RUNNING.compareAndSet(false, true)) {
            log.writeLog("检测到正在执行的采购结算单同步任务,本次调度跳过");
            return;
        }

        try {
            RecordSet rs = new RecordSet();
            String dwbm = new SubCompanyComInfo().getSubCompanyCode(COMPANY_ID);
            String api = LCUtil.getLCBillTypeInfo(BILL_TYPE, rs).get("api_list").toString();
            
            JSONObject paramJson = new JSONObject();
            paramJson.put("dwbh", dwbm);
            paramJson.put("pagesize", "10000");
            paramJson.put("page", "1");
            
            JSONObject syncResult = syncDataIncremental(paramJson, rs, api);
            
            if (syncResult.containsKey("updateInfo")) {
                log.writeLog(syncResult.getString("updateInfo"));
            }
            if (syncResult.containsKey("failCount") && syncResult.getInteger("failCount") > 0) {
                log.writeLog("失败记录数: " + syncResult.getInteger("failCount"));
            }
            
            log.writeLog("采购结算单同步任务完成");
            
        } catch (Exception e) {
            log.writeLog("采购结算单同步异常:" + e.getMessage());
        } finally {
            RUNNING.set(false);
        }
    }

    // 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
    private boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
        try {
            String[] columnNames = rs.getColumnName();
            if (columnNames == null || columnNames.length == 0) {
                return true;
            }

            for (String columnName : columnNames) {
                // 跳过系统字段
                if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                    || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                    || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                    continue;
                }

                String dbValue;
                try {
                    dbValue = Util.null2String(rs.getString(columnName));
                } catch (Exception ignore) {
                    continue;
                }

                if (!newData.containsKey(columnName)) {
                    continue;
                }

                String newValue = Util.null2String(newData.get(columnName));
                boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                    equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

                if (!equal) {
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            return true;
        }
    }

    // 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
    private JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
        JSONArray changes = new JSONArray();
        try {
            // 获取数据库记录的所有字段名
            String[] columnNames = rs.getColumnName();
            if (columnNames == null || columnNames.length == 0) {
                return changes; // 无法获取字段信息
            }

            // 遍历数据库的所有字段进行对比
            for (String columnName : columnNames) {
                // 跳过系统字段
                if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                    || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                    || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                    continue;
                }

                String dbValue;
                try {
                    dbValue = Util.null2String(rs.getString(columnName));
                } catch (Exception ignore) {
                    continue; // 无法获取该字段值,跳过
                }

                // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
                if (!newData.containsKey(columnName)) {
                    continue;
                }

                // 获取新数据中对应字段的值
                String newValue = Util.null2String(newData.get(columnName));

                // 根据字段类型进行对比
                boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                    equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

                if (!equal) {
                    JSONObject diff = new JSONObject();
                    diff.put("field", columnName);
                    diff.put("oldValue", dbValue);
                    diff.put("newValue", newValue);
                    changes.add(diff);
                }
            }
        } catch (Exception e) {
            // 忽略,返回当前已收集到的差异
        }
        return changes;
    }

    private JSONObject syncDataIncremental(JSONObject param, RecordSet rs, String api) {
        JSONObject result = new JSONObject();
        int totalCount = 0, newCount = 0, updateCount = 0, skipCount = 0, failCount = 0;
        JSONArray updatedDetails = new JSONArray();
        JSONArray failedRecords = new JSONArray();

        try {
            int page = Integer.parseInt(param.getString("page") == null ? "1" : param.getString("page"));
            String pageSize = Util.null2String(param.getString("pagesize"));
            if ("".equals(pageSize)) pageSize = "10000";

            while (true) {
                param.put("page", String.valueOf(page));
                param.put("pagesize", pageSize);

                JSONObject rjson = LCUtil.doPost(api, param);
                result = rjson;

                if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                    break;
                }
                if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                    break;
                }

                JSONArray datas = rjson.getJSONArray("data");
                int pageCount = (datas == null) ? 0 : datas.size();
                if (pageCount == 0) {
                    break;
                }
                totalCount += pageCount;

                if (datas != null) {
                    for (int i = 0; i < datas.size(); i++) {
                        String billnum = "";
                        String cgjsdid = "";
                        try {
                            JSONObject item = datas.getJSONObject(i);
                            Map<String, String> data = new HashMap<>();

                            cgjsdid = Util.null2String(item.get("ID"));
                            billnum = Util.null2String(item.get("BILLNUM"));
                            
                            // 映射字段
                            data.put("cgjsdid", cgjsdid);
                            data.put("billnum", billnum);
                            data.put("WLDW", Util.null2String(item.get("WLDW")));
                            data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                            data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                            data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                            data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                            data.put("BMBH", Util.null2String(item.get("BMBH")));
                            data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                            data.put("summary", Util.null2String(item.get("SUMMARY")));
                            data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                            data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                            data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                            data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                            data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                            data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                            data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                            data.put("dwbh", Util.null2String(item.get("DWBH")));
                            data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                            data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                            data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                            data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                            data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                            data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                            data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                            data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                            data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                            data.put("XMID", Util.null2String(item.get("XMID")));
                            data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                            data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                            data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                            data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                            data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                            data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                            if (item.get("BILLDATE") != null) {
                                data.put("Billdate", DATE_FORMAT.format(item.getDate("BILLDATE")));
                            }

                            // 金额字段
                            data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                            data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                            data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                            data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                            data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                            data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                            data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                            data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                            int action = processRecordCompareAndUpsert(data, rs, updatedDetails, failedRecords);
                            if (action == 1) {
                                updateCount++;
                            } else if (action == 2) {
                                newCount++;
                            } else if (action == -1) {
                                failCount++;
                            } else {
                                skipCount++;
                            }
                        } catch (Exception e) {
                            failCount++;
                            JSONObject failedRecord = new JSONObject();
                            failedRecord.put("billnum", billnum);
                            failedRecord.put("cgjsdid", cgjsdid);
                            failedRecord.put("errorMessage", e.getMessage());
                            failedRecords.add(failedRecord);
                        }
                    }
                }

                // 每页处理完成后,执行关联字段更新
                try {
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                    rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
                } catch (Exception ex) {
                    // 忽略关联更新异常
                }

                if (pageCount < Integer.parseInt(pageSize)) {
                    break;
                }

                page++;

                try {
                    Thread.sleep(200L);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }

            String syncInfo = "总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount;
            result.put("updateInfo", syncInfo);
            result.put("updatedDetails", updatedDetails);
            
            if (failCount > 0) {
                result.put("failedRecords", failedRecords);
                result.put("failCount", failCount);
            }
        } catch (Exception e) {
            result.put("error", "同步异常: " + e.getMessage());
        }

        return result;
    }

    /**
     * 先对比所有映射字段,再决定执行更新或插入。
     * @return 0=跳过,1=更新,2=新增,-1=失败
     */
    private int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, 
                                              JSONArray updateDetails, JSONArray failedRecords) {
        String billnum = Util.null2String(data.get("billnum"));
        String cgjsdid = Util.null2String(data.get("cgjsdid"));
        
        try {
            RecordSet checkRs = new RecordSet();
            checkRs.executeQuery("SELECT * FROM " + TABLE_NAME + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
            
            if (checkRs.next()) {
                int existingId = checkRs.getInt("id");
                
                if (hasDataChanged(checkRs, data)) {
                    JSONArray changes = collectDifferences(checkRs, data);
                    
                    if (updateDetails != null) {
                        JSONObject detail = new JSONObject();
                        detail.put("billnum", billnum);
                        detail.put("cgjsdid", cgjsdid);
                        detail.put("changes", changes);
                        updateDetails.add(detail);
                    }
                    
                    if (changes != null && changes.size() > 0) {
                        Map<String, String> updateMap = new HashMap<>();
                        updateMap.put("id", String.valueOf(existingId));
                        
                        for (int i = 0; i < changes.size(); i++) {
                            JSONObject c = changes.getJSONObject(i);
                            String field = Util.null2String(c.getString("field"));
                            String newValue = Util.null2String(c.getString("newValue"));
                            updateMap.put(field, newValue);
                        }
                        
                        ModeUtil.saveModeData(TABLE_NAME, 1, rs, updateMap, null, "id", true);
                    }
                    
                    return 1;
                }
                return 0;
            }
            
            // 新增记录
            ModeUtil.saveModeData(TABLE_NAME, 1, rs, data, null, "cgjsdid", true);
            return 2;
            
        } catch (Exception e) {
            if (failedRecords != null) {
                JSONObject failedRecord = new JSONObject();
                failedRecord.put("billnum", billnum);
                failedRecord.put("cgjsdid", cgjsdid);
                failedRecord.put("errorMessage", e.getMessage());
                failedRecords.add(failedRecord);
            }
            return -1;
        }
    }
}

  • syncCGJSD 中增加 AtomicBoolean RUNNINGexecute() 入口通过 CAS 判断是否已有任务在跑,防止 10 分钟调度重叠;在 finally 中释放标记。
  • 对分页循环加了更稳妥的退出条件:当 data 为空或结果条数少于 pageSize 立即 break,并在每次翻页后短暂 sleep(200ms),避免接口长时间无休请求。
  • 保留原有功能同时降低重入写库和无限循环风险。

未运行实际接口/调度测试,请在集成环境手动触发一次任务确认行为。


User

检查整体业务逻辑10万条数据是否能在10s内执行完,否则出一个优化方案,可以空间换时间


Cursor

  • 现状评估

    • 任务仍需通过 LCUtil.doPost 逐页请求接口。接口单次响应 3‑4 s,而 10 万条至少需要 10 页(pageSize=10000),网络耗时最少 30 s,无论本地逻辑多快都无法在 10 s 内完成。
    • 本地处理路径仍然是“逐条记录查询再 upsert”:processRecordCompareAndUpsert 对每条数据执行一次 SELECT * FROM uf_cgjsd WHERE cgjsdid=? AND billnum=?,100k 条意味着 100k 次数据库 round-trip,单次 5 ms 也要 500 s。
    • ModeUtil.saveModeData 在每条记录上调用一次,包含建模权限、日志等开销,同样是 O(n) 次慢操作。
    • 关联更新(5 条 UPDATE ... JOIN ...)在每页循环里执行 10 次,也会在大数据量下造成锁竞争。
  • 结论
    以当前实现,无论网络还是数据库,都远超 10 s。必须引入“空间换时间”的整体重构。


优化方案(空间换时间)

  1. 增量快照表 / 缓存 Map
    • 先一次性查出 uf_cgjsd 内所有目标记录到本地 Map(key=cgjsdid#billnum,value=整行数据);内存约 100k × 200 bytes ≈ 20 MB,可接受。
    • 与接口返回的 100k 条做内存比对,只把有差异的 key 放入两个列表:toUpdatetoInsert。省去 100k 次数据库查询,写库之前只有 1 次全量查询。
Map<String, RecordSnapshot> dbSnapshot = preloadAll();
List<Diff> toUpdate = new ArrayList<>();
List<Map<String,String>> toInsert = new ArrayList<>();
for (JSONObject item : apiData) {
    String key = buildKey(...);
    RecordSnapshot old = dbSnapshot.get(key);
    if (old == null) toInsert.add(map);
    else if (hasDiff(old, map)) toUpdate.add(diff);
}
  1. 批量写库

    • 新增:拼接批量 INSERT(或先写临时表后 INSERT INTO ... SELECT ...),一次性落库。
    • 更新:将 toUpdate 写入临时表,再用单条语句 UPDATE uf_cgjsd t JOIN temp tmp ON ... SET ...,避免逐条 ModeUtil.save。若必须走 ModeUtil,也要通过其批量接口或多线程+连接池执行。
    • 100k 条批量更新在 MySQL 中只需 1‑2 s,前提是表上有索引 (cgjsdid, billnum)
  2. 异步关联刷新

    • 将关联 5 条 SQL 放到同步任务结尾统一执行 1 次,或使用触发器/物化字段。这样不会在每页循环里多次锁表。
  3. 接口层降维

    • 如果上游必须分页且单页 3‑4 s,可请求对方提供“增量接口”或“批量下载文件”,让同步任务直接拉最新快照文件。
    • 如果只能调用现有 API,可在本地增加多线程并发拉取不同 page(空间:维护 page 任务队列 + 结果缓冲),但是要确保接口允许并发,否则仍受制于网络。
  4. 备选:分布式缓存对账

    • 若数据库读 IO 仍是瓶颈,可把 dbSnapshot 缓存到 Redis(key→hash),下一次任务只需增量获取变化字段。代价是 Redis 内存消耗,但换来极低延迟。

预期性能

  • 预加载 100k 条(单次 SELECT):约 2 s
  • 内存 diff:100k 条纯 Java 遍历约 0.3‑0.5 s
  • 批量 INSERT/UPDATE:索引得当 ≈ 2‑3 s
  • 关联更新(单次):≈ 1 s
  • 总计 ~6‑7 s(不含接口拉取时间),满足 10 s 目标。若接口仍需 30 s,则必须协调上游提供更快的批量通道,否则任何本地优化都无意义。

落地建议

  • 增加 (cgjsdid, billnum) 复合索引。
  • 新增临时表 tmp_cgjsd_sync:结构同 uf_cgjsd。同步流程:
    1. API 数据写入临时表(可用批量 insert)。
    2. INSERT INTO uf_cgjsd ... SELECT ... FROM tmp WHERE NOT EXISTS ...
    3. UPDATE uf_cgjsd tgt JOIN tmp src ON ... SET ... WHERE ...
    4. 清空临时表。
  • NUMERIC_FIELDS 及对比逻辑迁到 SQL 层(如 ROUNDCOALESCE),减少 Java 端 BigDecimal 运算。

若需要我协助具体代码重构和批量 SQL 实现,请告知。

Map<String, RecordSnapshot> dbSnapshot = preloadAll();
List<Diff> toUpdate = new ArrayList<>();
List<Map<String,String>> toInsert = new ArrayList<>();
for (JSONObject item : apiData) {
    String key = buildKey(...);
    RecordSnapshot old = dbSnapshot.get(key);
    if (old == null) toInsert.add(map);
    else if (hasDiff(old, map)) toUpdate.add(diff);
}

User

ModeUtil.save可以最后完成的时候执行一次,仅仅用于权限重构


Cursor

package com.ljj.scheduleSync;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import weaver.conn.RecordSet;
import dev.soft.lb.util.CommonUtil;
import dev.soft.lb.util.LCUtil;
import dev.soft.lb.util.ModeUtil;
import weaver.general.Util;
import weaver.general.TimeUtil;
import weaver.hrm.company.SubCompanyComInfo;
import weaver.interfaces.schedule.BaseCronJob;
import weaver.general.BaseBean;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.UUID;

public class syncCGJSD extends BaseCronJob {
    // 防重复执行的标识,避免 10 分钟间隔出现重叠
    private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
    
    private static final String COMPANY_ID = "66";
    private static final String BILL_TYPE = "12";
    private static final String TABLE_NAME = "uf_cgjsd";
    private static final int DEFAULT_USER_ID = 1;
    private static volatile Integer MODE_ID_CACHE = null;
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    // 数值字段名单:这些字段按两位小数口径进行比较与存储
    private static final Set<String> NUMERIC_FIELDS = new HashSet<>(
            Arrays.asList(
                    "amount",
                    "foreigncurrency",
                    "clearamount",
                    "clearforeigncurrency",
                    "balanceamount",
                    "balanceforeigncurrency",
                    "availableamount",
                    "availableforeigncurrency",
                    // 添加其他可能的数值字段
                    "original_clearamount",
                    "original_clearforeigncurrency",
                    "original_balanceamount",
                    "original_balanceforeigncurrency",
                    "price",
                    "unitprice",
                    "quantity",
                    "taxrate",
                    "taxamount",
                    "totalmoney",
                    "netamount",
                    "amountnotax"
            )
    );

    // 规范化字符串:去首尾空格;null按空串处理
    private static String norm(String v) {
        if (v == null) return "";
        return v.trim();
    }

    // 字符串等价:规范化后比较;空串与null等价
    private static boolean equalNormalized(String a, String b) {
        return norm(a).equals(norm(b));
    }

    // 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
    private static boolean equalNumeric(String a, String b) {
        String sa = norm(a);
        String sb = norm(b);
        if (sa.isEmpty() && sb.isEmpty()) return true;
        if (sa.isEmpty() || sb.isEmpty()) return false;
        try {
            BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
            BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
            return ba.compareTo(bb) == 0;
        } catch (Exception ignore) {
            return sa.equals(sb);
        }
    }

    // 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
    private static String formatTo2Decimals(String v) {
        String s = norm(v);
        if (s.isEmpty()) return s;
        try {
            return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
        } catch (Exception ignore) {
            return s;
        }
    }

    @Override
    public void execute() {
        BaseBean log = new BaseBean();
        log.writeLog("采购结算单同步任务开始");

        if (!RUNNING.compareAndSet(false, true)) {
            log.writeLog("检测到正在执行的采购结算单同步任务,本次调度跳过");
            return;
        }

        try {
            RecordSet rs = new RecordSet();
            String dwbm = new SubCompanyComInfo().getSubCompanyCode(COMPANY_ID);
            String api = LCUtil.getLCBillTypeInfo(BILL_TYPE, rs).get("api_list").toString();
            
            JSONObject paramJson = new JSONObject();
            paramJson.put("dwbh", dwbm);
            paramJson.put("pagesize", "10000");
            paramJson.put("page", "1");
            
            JSONObject syncResult = syncDataIncremental(paramJson, rs, api);
            
            if (syncResult.containsKey("updateInfo")) {
                log.writeLog(syncResult.getString("updateInfo"));
            }
            if (syncResult.containsKey("failCount") && syncResult.getInteger("failCount") > 0) {
                log.writeLog("失败记录数: " + syncResult.getInteger("failCount"));
            }
            
            log.writeLog("采购结算单同步任务完成");
            
        } catch (Exception e) {
            log.writeLog("采购结算单同步异常:" + e.getMessage());
        } finally {
            RUNNING.set(false);
        }
    }

    // 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
    private boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
        try {
            String[] columnNames = rs.getColumnName();
            if (columnNames == null || columnNames.length == 0) {
                return true;
            }

            for (String columnName : columnNames) {
                // 跳过系统字段
                if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                    || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                    || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                    continue;
                }

                String dbValue;
                try {
                    dbValue = Util.null2String(rs.getString(columnName));
                } catch (Exception ignore) {
                    continue;
                }

                if (!newData.containsKey(columnName)) {
                    continue;
                }

                String newValue = Util.null2String(newData.get(columnName));
                boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                    equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

                if (!equal) {
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            return true;
        }
    }

    // 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
    private JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
        JSONArray changes = new JSONArray();
        try {
            // 获取数据库记录的所有字段名
            String[] columnNames = rs.getColumnName();
            if (columnNames == null || columnNames.length == 0) {
                return changes; // 无法获取字段信息
            }

            // 遍历数据库的所有字段进行对比
            for (String columnName : columnNames) {
                // 跳过系统字段
                if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                    || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                    || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                    continue;
                }

                String dbValue;
                try {
                    dbValue = Util.null2String(rs.getString(columnName));
                } catch (Exception ignore) {
                    continue; // 无法获取该字段值,跳过
                }

                // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
                if (!newData.containsKey(columnName)) {
                    continue;
                }

                // 获取新数据中对应字段的值
                String newValue = Util.null2String(newData.get(columnName));

                // 根据字段类型进行对比
                boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                    equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

                if (!equal) {
                    JSONObject diff = new JSONObject();
                    diff.put("field", columnName);
                    diff.put("oldValue", dbValue);
                    diff.put("newValue", newValue);
                    changes.add(diff);
                }
            }
        } catch (Exception e) {
            // 忽略,返回当前已收集到的差异
        }
        return changes;
    }

    private JSONObject syncDataIncremental(JSONObject param, RecordSet rs, String api) {
        JSONObject result = new JSONObject();
        int totalCount = 0, newCount = 0, updateCount = 0, skipCount = 0, failCount = 0;
        JSONArray updatedDetails = new JSONArray();
        JSONArray failedRecords = new JSONArray();
        Set<Integer> affectedIds = new HashSet<>();

        try {
            int page = Integer.parseInt(param.getString("page") == null ? "1" : param.getString("page"));
            String pageSize = Util.null2String(param.getString("pagesize"));
            if ("".equals(pageSize)) pageSize = "10000";

            while (true) {
                param.put("page", String.valueOf(page));
                param.put("pagesize", pageSize);

                JSONObject rjson = LCUtil.doPost(api, param);
                result = rjson;

                if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                    break;
                }
                if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                    break;
                }

                JSONArray datas = rjson.getJSONArray("data");
                int pageCount = (datas == null) ? 0 : datas.size();
                if (pageCount == 0) {
                    break;
                }
                totalCount += pageCount;

                if (datas != null) {
                    for (int i = 0; i < datas.size(); i++) {
                        String billnum = "";
                        String cgjsdid = "";
                        try {
                            JSONObject item = datas.getJSONObject(i);
                            Map<String, String> data = new HashMap<>();

                            cgjsdid = Util.null2String(item.get("ID"));
                            billnum = Util.null2String(item.get("BILLNUM"));
                            
                            // 映射字段
                            data.put("cgjsdid", cgjsdid);
                            data.put("billnum", billnum);
                            data.put("WLDW", Util.null2String(item.get("WLDW")));
                            data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                            data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                            data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                            data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                            data.put("BMBH", Util.null2String(item.get("BMBH")));
                            data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                            data.put("summary", Util.null2String(item.get("SUMMARY")));
                            data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                            data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                            data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                            data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                            data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                            data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                            data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                            data.put("dwbh", Util.null2String(item.get("DWBH")));
                            data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                            data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                            data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                            data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                            data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                            data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                            data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                            data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                            data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                            data.put("XMID", Util.null2String(item.get("XMID")));
                            data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                            data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                            data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                            data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                            data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                            data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                            if (item.get("BILLDATE") != null) {
                                data.put("Billdate", DATE_FORMAT.format(item.getDate("BILLDATE")));
                            }

                            // 金额字段
                            data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                            data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                            data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                            data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                            data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                            data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                            data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                            data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                            int action = processRecordCompareAndUpsert(data, updatedDetails, failedRecords, affectedIds);
                            if (action == 1) {
                                updateCount++;
                            } else if (action == 2) {
                                newCount++;
                            } else if (action == -1) {
                                failCount++;
                            } else {
                                skipCount++;
                            }
                        } catch (Exception e) {
                            failCount++;
                            JSONObject failedRecord = new JSONObject();
                            failedRecord.put("billnum", billnum);
                            failedRecord.put("cgjsdid", cgjsdid);
                            failedRecord.put("errorMessage", e.getMessage());
                            failedRecords.add(failedRecord);
                        }
                    }
                }

                // 每页处理完成后,执行关联字段更新
                try {
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                    rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
                } catch (Exception ex) {
                    // 忽略关联更新异常
                }

                if (pageCount < Integer.parseInt(pageSize)) {
                    break;
                }

                page++;

                try {
                    Thread.sleep(200L);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }

            rebuildPermissions(affectedIds);

            String syncInfo = "总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount;
            result.put("updateInfo", syncInfo);
            result.put("updatedDetails", updatedDetails);
            
            if (failCount > 0) {
                result.put("failedRecords", failedRecords);
                result.put("failCount", failCount);
            }
        } catch (Exception e) {
            result.put("error", "同步异常: " + e.getMessage());
        }

        return result;
    }

    /**
     * 先对比所有映射字段,再决定执行更新或插入。
     * @return 0=跳过,1=更新,2=新增,-1=失败
     */
    private int processRecordCompareAndUpsert(Map<String, String> data,
                                              JSONArray updateDetails,
                                              JSONArray failedRecords,
                                              Set<Integer> affectedIds) {
        String billnum = Util.null2String(data.get("billnum"));
        String cgjsdid = Util.null2String(data.get("cgjsdid"));
        
        try {
            RecordSet checkRs = new RecordSet();
            checkRs.executeQuery("SELECT * FROM " + TABLE_NAME + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
            
            if (checkRs.next()) {
                int existingId = checkRs.getInt("id");
                
                if (hasDataChanged(checkRs, data)) {
                    JSONArray changes = collectDifferences(checkRs, data);
                    
                    if (updateDetails != null) {
                        JSONObject detail = new JSONObject();
                        detail.put("billnum", billnum);
                        detail.put("cgjsdid", cgjsdid);
                        detail.put("changes", changes);
                        updateDetails.add(detail);
                    }
                    
                    if (changes != null && changes.size() > 0) {
                        boolean success = applyUpdate(existingId, changes);
                        if (success) {
                            if (affectedIds != null) {
                                affectedIds.add(existingId);
                            }
                            return 1;
                        }
                        throw new RuntimeException("更新采购结算单失败");
                    }

                    return 0;
                }
                return 0;
            }
            
            // 新增记录
            Integer newId = insertRecord(data);
            if (newId != null && newId > 0) {
                if (affectedIds != null) {
                    affectedIds.add(newId);
                }
                return 2;
            }
            throw new RuntimeException("新增采购结算单失败");
            
        } catch (Exception e) {
            if (failedRecords != null) {
                JSONObject failedRecord = new JSONObject();
                failedRecord.put("billnum", billnum);
                failedRecord.put("cgjsdid", cgjsdid);
                failedRecord.put("errorMessage", e.getMessage());
                failedRecords.add(failedRecord);
            }
            return -1;
        }
    }

    private boolean applyUpdate(int recordId, JSONArray changes) {
        if (recordId <= 0 || changes == null || changes.isEmpty()) {
            return false;
        }
        StringBuilder setClause = new StringBuilder();
        for (int i = 0; i < changes.size(); i++) {
            JSONObject change = changes.getJSONObject(i);
            String field = Util.null2String(change.getString("field"));
            String value = Util.null2String(change.getString("newValue"));
            if (field.isEmpty()) {
                continue;
            }
            if (setClause.length() > 0) {
                setClause.append(",");
            }
            setClause.append(field).append("=").append(toSqlValue(value));
        }
        if (setClause.length() == 0) {
            return false;
        }
        String sql = "UPDATE " + TABLE_NAME + " SET " + setClause + " WHERE id=" + recordId;
        RecordSet updateRs = new RecordSet();
        return updateRs.execute(sql);
    }

    private Integer insertRecord(Map<String, String> data) {
        if (data == null || data.isEmpty()) {
            return null;
        }
        Map<String, String> insertMap = new HashMap<>(data);
        String now = TimeUtil.getCurrentTimeString();
        String createDate = now.substring(0, 10);
        String createTime = now.substring(11);
        insertMap.put("formmodeid", String.valueOf(getModeId()));
        insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
        insertMap.put("modedatacreatertype", "1");
        insertMap.put("modedatacreatedate", createDate);
        insertMap.put("modedatacreatetime", createTime);
        insertMap.put("modeuuid", UUID.randomUUID().toString());

        StringBuilder columns = new StringBuilder();
        StringBuilder values = new StringBuilder();
        for (Map.Entry<String, String> entry : insertMap.entrySet()) {
            if (columns.length() > 0) {
                columns.append(",");
                values.append(",");
            }
            columns.append(entry.getKey());
            values.append(toSqlValue(entry.getValue()));
        }
        String sql = "INSERT INTO " + TABLE_NAME + "(" + columns + ") VALUES(" + values + ")";
        RecordSet insertRs = new RecordSet();
        boolean success = insertRs.execute(sql);
        if (!success) {
            return null;
        }
        RecordSet idRs = new RecordSet();
        idRs.executeQuery("SELECT id FROM " + TABLE_NAME + " WHERE cgjsdid = ? AND billnum = ?",
                Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
        if (idRs.next()) {
            return idRs.getInt("id");
        }
        return null;
    }

    private String toSqlValue(String value) {
        String normalized = Util.null2String(value);
        if (normalized.isEmpty()) {
            return "null";
        }
        return "'" + normalized.replace("'", "''") + "'";
    }

    private void rebuildPermissions(Set<Integer> affectedIds) {
        if (affectedIds == null || affectedIds.isEmpty()) {
            return;
        }
        try {
            ModeUtil.rebuildModeRights(TABLE_NAME, DEFAULT_USER_ID, affectedIds);
        } catch (Exception e) {
            new BaseBean().writeLog("采购结算单同步-重构权限失败: " + e.getMessage());
        }
    }

    private int getModeId() {
        if (MODE_ID_CACHE != null && MODE_ID_CACHE > 0) {
            return MODE_ID_CACHE;
        }
        synchronized (syncCGJSD.class) {
            if (MODE_ID_CACHE == null || MODE_ID_CACHE <= 0) {
                MODE_ID_CACHE = Util.getIntValue(CommonUtil.getModeIdByTableName(TABLE_NAME));
            }
        }
        return MODE_ID_CACHE;
    }
}

package dev.soft.lb.util;

import weaver.conn.RecordSet;
import weaver.formmode.setup.ModeRightInfo;
import weaver.general.TimeUtil;
import weaver.general.Util;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * 模块工具类
 * @author 林斌
 * @version V1.0
 * @date 2024/11/7  19:21
 * 2025-05-08
 * 林斌
 */
public class ModeUtil {

    //重构所有数据权限
    private static ModeRightInfo ModeRightInfo = new ModeRightInfo();

    public static boolean saveModeData(String table, int userid, RecordSet rs, Map<String,String> data, List<Map<String,Object>> dtdatas, String uniField){
        return saveModeData(table,userid,rs,data,dtdatas,uniField,true);
    }

    /**
     * 保存模块数据
     * @param table 主表名
     * @param userid 操作用户ID
     * @param rs 数据集
     * @param data 主表数据
     * @param dtdatas 明细行数据(List<Map<String,Object>> Map对象:table明细表,uniField明细表唯一字段,datalist数据集List<Map<String,String>>)
     * @param uniField 主表唯一字段
     * @return
     */
    public static boolean saveModeData(String table, int userid, RecordSet rs, Map<String,String> data, List<Map<String,Object>> dtdatas, String uniField,boolean buildRights){
        boolean flag = false;
        try {
            if(rs==null){
                rs = new RecordSet();
            }
            int modeid = Util.getIntValue(CommonUtil.getModeIdByTableName(table));
            String sql = "";
            String tpsql = "";
            String s1 = "";
            String s2 = "";
            int billid = 0;
            //如果存在唯一字段且唯一字段值不为空
            if(data.containsKey(uniField)&&!Util.null2String(data.get(uniField)).equals("")){
                sql = "select id from "+table+" where "+uniField+"='"+data.get(uniField)+"'";
                rs.execute(sql);
                if(rs.next()){
                    billid = Util.getIntValue(Util.null2String(rs.getString("id")),0);
                }
            }

            if(billid>0){
                tpsql = "";
                for(String key : data.keySet()){
                    String value = Util.null2String(data.get(key));
                    if(value.equals("")){
                        value = "null";
                    }else{
                        value = "'"+value.replace("'","''")+"'";
                    }
                    tpsql += tpsql.equals("")?(key+"="+ value):(","+key+"="+value);
                }
                sql = "update "+table+" set "+tpsql+" where id="+ billid;
                rs.execute(sql);

                if(buildRights){
                    //设置共享权限
                    ModeRightInfo.setNewRight(false);
                    ModeRightInfo.init();
                    ModeRightInfo.setModeId(modeid);
                    ModeRightInfo.rebuildModeDataShareByEdit(userid, modeid, billid);
                }
            }else{
                String nowtime = TimeUtil.getCurrentTimeString();
                String createDate = nowtime.substring(0,10);
                String createTime = nowtime.substring(11);
                String uuid = UUID.randomUUID().toString();
                data.put("modeuuid",uuid);
                //无数据则执行新增
                data.put("formmodeid",modeid+"");
                data.put("modedatacreater",userid+"");
                data.put("modedatacreatertype","1");
                data.put("modedatacreatedate",createDate);
                data.put("modedatacreatetime",createTime);
                s1 = "";
                s2 = "";
                for(String key : data.keySet()){
                    String value = Util.null2String(data.get(key));
                    if(!value.equals("")){
                        value = "'"+value.replace("'","''")+"'";
                    }else{
                        value = "null";
                    }
                    s1 += s1.equals("")?key:","+key;
                    s2 += s2.equals("")?value:","+value+"";
                }
                //新增数据
                sql = "insert into "+table+"("+s1+") values("+s2+")";
                boolean saveflag = rs.execute(sql);
                //如果保存成功
                if(saveflag) {
                    //如果需要更新权限,则重构权限
                    if(buildRights) {
                        //查询新增的数据
                        sql = "select id from " + table + " where modeuuid='" + uuid + "'";
                        rs.execute(sql);
                        if (rs.next()) {
                            billid = Util.getIntValue(Util.null2String(rs.getString("id")), 0);
                        }
                        //如果新增数据,则重构权限
                        if (billid > 0) {
                            //设置共享权限
                            ModeRightInfo.setNewRight(false);
                            ModeRightInfo.init();
                            ModeRightInfo.setModeId(modeid);
                            ModeRightInfo.rebuildModeDataShareByEdit(userid, modeid, billid);
                        }
                    }
                }
            }
            //如果有数据
            if(billid>0 && dtdatas!=null && dtdatas.size()>0){
                //循环所有明细数据
                for(Map<String,Object> item : dtdatas){
                    //明细表名
                    String dttable = (String) item.get("table");
                    //明细唯一字段
                    String dtuniField = (String) item.get("uniField");
                    //数据行
                    List<Map<String,String>> dtlist = (List<Map<String, String>>) item.get("datalist");
                    if(dtlist!=null && dtlist.size()>0){
                        for(Map<String,String> subdata : dtlist){
                            int dtid = 0;
                            //如果有关键值且关键值不为空
                            if(subdata.containsKey(dtuniField)&& !Util.null2String(subdata.get(dtuniField)).equals("")){
                                sql = "select id from "+dttable+" where mainid="+billid+" and "+dtuniField+"='"+subdata.get(dtuniField)+"'";
                                rs.execute(sql);
                                if(rs.next()){
                                    dtid = Util.getIntValue(Util.null2String(rs.getString("id")),0);
                                }
                            }
                            //如果明细数据ID大于0,则更新
                            if(dtid>0){
                                tpsql = "";
                                for(String key : subdata.keySet()){
                                    String value = Util.null2String(subdata.get(key));
                                    if(value.equals("")){
                                        value = "null";
                                    }else{
                                        value = "'"+value.replace("'","''")+"'";
                                    }
                                    tpsql += tpsql.equals("")?(key+"="+ value):(","+key+"="+value);
                                }
                                sql = "update "+dttable+" set "+tpsql+" where id="+ dtid;
                                rs.execute(sql);
                            }else{
                                subdata.put("mainid",billid+"");
                                s1 = "";
                                s2 = "";
                                for(String key : subdata.keySet()){
                                    String value = Util.null2String(subdata.get(key));
                                    if(!value.equals("")){
                                        value = "'"+value.replace("'","''")+"'";
                                    }else{
                                        value = "null";
                                    }
                                    s1 += s1.equals("")?key:","+key;
                                    s2 += s2.equals("")?value:","+value+"";
                                }
                                //新增明细数据
                                sql = "insert into "+dttable+"("+s1+") values("+s2+")";
                                rs.execute(sql);
                            }
                        }
                    }
                }
            }
            flag = true;
        }catch (Exception e){
            throw new RuntimeException("保存模块数据异常:"+e.getMessage());
        }
        return flag;
    }

    /**
     * 批量重构模块数据权限
     */
    public static void rebuildModeRights(String table, int userid, Set<Integer> billIds) {
        if (billIds == null || billIds.isEmpty()) {
            return;
        }
        int modeid = Util.getIntValue(CommonUtil.getModeIdByTableName(table));
        if (modeid <= 0) {
            return;
        }
        ModeRightInfo.setNewRight(false);
        ModeRightInfo.init();
        ModeRightInfo.setModeId(modeid);
        for (Integer billid : billIds) {
            if (billid == null || billid <= 0) {
                continue;
            }
            ModeRightInfo.rebuildModeDataShareByEdit(userid, modeid, billid);
        }
    }

}

package com.ljj.scheduleSync;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import weaver.conn.RecordSet;
import dev.soft.lb.util.CommonUtil;
import dev.soft.lb.util.LCUtil;
import dev.soft.lb.util.ModeUtil;
import weaver.general.Util;
import weaver.general.TimeUtil;
import weaver.formmode.setup.ModeRightInfo;
import weaver.hrm.company.SubCompanyComInfo;
import weaver.interfaces.schedule.BaseCronJob;
import weaver.general.BaseBean;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.UUID;
import java.lang.reflect.Method;

public class syncCGJSD extends BaseCronJob {
    // 防重复执行的标识,避免 10 分钟间隔出现重叠
    private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
    
    private static final String COMPANY_ID = "66";
    private static final String BILL_TYPE = "12";
    private static final String TABLE_NAME = "uf_cgjsd";
    private static final int DEFAULT_USER_ID = 1;
    private static volatile Integer MODE_ID_CACHE = null;
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    // 数值字段名单:这些字段按两位小数口径进行比较与存储
    private static final Set<String> NUMERIC_FIELDS = new HashSet<>(
            Arrays.asList(
                    "amount",
                    "foreigncurrency",
                    "clearamount",
                    "clearforeigncurrency",
                    "balanceamount",
                    "balanceforeigncurrency",
                    "availableamount",
                    "availableforeigncurrency",
                    // 添加其他可能的数值字段
                    "original_clearamount",
                    "original_clearforeigncurrency",
                    "original_balanceamount",
                    "original_balanceforeigncurrency",
                    "price",
                    "unitprice",
                    "quantity",
                    "taxrate",
                    "taxamount",
                    "totalmoney",
                    "netamount",
                    "amountnotax"
            )
    );

    // 规范化字符串:去首尾空格;null按空串处理
    private static String norm(String v) {
        if (v == null) return "";
        return v.trim();
    }

    // 字符串等价:规范化后比较;空串与null等价
    private static boolean equalNormalized(String a, String b) {
        return norm(a).equals(norm(b));
    }

    // 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
    private static boolean equalNumeric(String a, String b) {
        String sa = norm(a);
        String sb = norm(b);
        if (sa.isEmpty() && sb.isEmpty()) return true;
        if (sa.isEmpty() || sb.isEmpty()) return false;
        try {
            BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
            BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
            return ba.compareTo(bb) == 0;
        } catch (Exception ignore) {
            return sa.equals(sb);
        }
    }

    // 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
    private static String formatTo2Decimals(String v) {
        String s = norm(v);
        if (s.isEmpty()) return s;
        try {
            return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
        } catch (Exception ignore) {
            return s;
        }
    }

    @Override
    public void execute() {
        BaseBean log = new BaseBean();
        log.writeLog("采购结算单同步任务开始");

        if (!RUNNING.compareAndSet(false, true)) {
            log.writeLog("检测到正在执行的采购结算单同步任务,本次调度跳过");
            return;
        }

        try {
            RecordSet rs = new RecordSet();
            String dwbm = new SubCompanyComInfo().getSubCompanyCode(COMPANY_ID);
            String api = LCUtil.getLCBillTypeInfo(BILL_TYPE, rs).get("api_list").toString();
            
            JSONObject paramJson = new JSONObject();
            paramJson.put("dwbh", dwbm);
            paramJson.put("pagesize", "10000");
            paramJson.put("page", "1");
            
            JSONObject syncResult = syncDataIncremental(paramJson, rs, api);
            
            if (syncResult.containsKey("updateInfo")) {
                log.writeLog(syncResult.getString("updateInfo"));
            }
            if (syncResult.containsKey("failCount") && syncResult.getInteger("failCount") > 0) {
                log.writeLog("失败记录数: " + syncResult.getInteger("failCount"));
            }
            
            log.writeLog("采购结算单同步任务完成");
            
        } catch (Exception e) {
            log.writeLog("采购结算单同步异常:" + e.getMessage());
        } finally {
            RUNNING.set(false);
        }
    }

    // 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
    private boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
        try {
            String[] columnNames = rs.getColumnName();
            if (columnNames == null || columnNames.length == 0) {
                return true;
            }

            for (String columnName : columnNames) {
                // 跳过系统字段
                if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                    || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                    || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                    continue;
                }

                String dbValue;
                try {
                    dbValue = Util.null2String(rs.getString(columnName));
                } catch (Exception ignore) {
                    continue;
                }

                if (!newData.containsKey(columnName)) {
                    continue;
                }

                String newValue = Util.null2String(newData.get(columnName));
                boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                    equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

                if (!equal) {
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            return true;
        }
    }

    // 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
    private JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
        JSONArray changes = new JSONArray();
        try {
            // 获取数据库记录的所有字段名
            String[] columnNames = rs.getColumnName();
            if (columnNames == null || columnNames.length == 0) {
                return changes; // 无法获取字段信息
            }

            // 遍历数据库的所有字段进行对比
            for (String columnName : columnNames) {
                // 跳过系统字段
                if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                    || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                    || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                    continue;
                }

                String dbValue;
                try {
                    dbValue = Util.null2String(rs.getString(columnName));
                } catch (Exception ignore) {
                    continue; // 无法获取该字段值,跳过
                }

                // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
                if (!newData.containsKey(columnName)) {
                    continue;
                }

                // 获取新数据中对应字段的值
                String newValue = Util.null2String(newData.get(columnName));

                // 根据字段类型进行对比
                boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                    equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

                if (!equal) {
                    JSONObject diff = new JSONObject();
                    diff.put("field", columnName);
                    diff.put("oldValue", dbValue);
                    diff.put("newValue", newValue);
                    changes.add(diff);
                }
            }
        } catch (Exception e) {
            // 忽略,返回当前已收集到的差异
        }
        return changes;
    }

    private JSONObject syncDataIncremental(JSONObject param, RecordSet rs, String api) {
        JSONObject result = new JSONObject();
        int totalCount = 0, newCount = 0, updateCount = 0, skipCount = 0, failCount = 0;
        JSONArray updatedDetails = new JSONArray();
        JSONArray failedRecords = new JSONArray();
        Set<Integer> affectedIds = new HashSet<>();

        try {
            int page = Integer.parseInt(param.getString("page") == null ? "1" : param.getString("page"));
            String pageSize = Util.null2String(param.getString("pagesize"));
            if ("".equals(pageSize)) pageSize = "10000";

            while (true) {
                param.put("page", String.valueOf(page));
                param.put("pagesize", pageSize);

                JSONObject rjson = LCUtil.doPost(api, param);
                result = rjson;

                if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                    break;
                }
                if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                    break;
                }

                JSONArray datas = rjson.getJSONArray("data");
                int pageCount = (datas == null) ? 0 : datas.size();
                if (pageCount == 0) {
                    break;
                }
                totalCount += pageCount;

                if (datas != null) {
                    for (int i = 0; i < datas.size(); i++) {
                        String billnum = "";
                        String cgjsdid = "";
                        try {
                            JSONObject item = datas.getJSONObject(i);
                            Map<String, String> data = new HashMap<>();

                            cgjsdid = Util.null2String(item.get("ID"));
                            billnum = Util.null2String(item.get("BILLNUM"));
                            
                            // 映射字段
                            data.put("cgjsdid", cgjsdid);
                            data.put("billnum", billnum);
                            data.put("WLDW", Util.null2String(item.get("WLDW")));
                            data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                            data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                            data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                            data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                            data.put("BMBH", Util.null2String(item.get("BMBH")));
                            data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                            data.put("summary", Util.null2String(item.get("SUMMARY")));
                            data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                            data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                            data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                            data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                            data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                            data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                            data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                            data.put("dwbh", Util.null2String(item.get("DWBH")));
                            data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                            data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                            data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                            data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                            data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                            data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                            data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                            data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                            data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                            data.put("XMID", Util.null2String(item.get("XMID")));
                            data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                            data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                            data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                            data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                            data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                            data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                            if (item.get("BILLDATE") != null) {
                                data.put("Billdate", DATE_FORMAT.format(item.getDate("BILLDATE")));
                            }

                            // 金额字段
                            data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                            data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                            data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                            data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                            data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                            data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                            data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                            data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                            int action = processRecordCompareAndUpsert(data, updatedDetails, failedRecords, affectedIds);
                            if (action == 1) {
                                updateCount++;
                            } else if (action == 2) {
                                newCount++;
                            } else if (action == -1) {
                                failCount++;
                            } else {
                                skipCount++;
                            }
                        } catch (Exception e) {
                            failCount++;
                            JSONObject failedRecord = new JSONObject();
                            failedRecord.put("billnum", billnum);
                            failedRecord.put("cgjsdid", cgjsdid);
                            failedRecord.put("errorMessage", e.getMessage());
                            failedRecords.add(failedRecord);
                        }
                    }
                }

                // 每页处理完成后,执行关联字段更新
                try {
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                    rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
                } catch (Exception ex) {
                    // 忽略关联更新异常
                }

                if (pageCount < Integer.parseInt(pageSize)) {
                    break;
                }

                page++;

                try {
                    Thread.sleep(200L);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }

            rebuildPermissions(affectedIds);

            String syncInfo = "总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount;
            result.put("updateInfo", syncInfo);
            result.put("updatedDetails", updatedDetails);
            
            if (failCount > 0) {
                result.put("failedRecords", failedRecords);
                result.put("failCount", failCount);
            }
        } catch (Exception e) {
            result.put("error", "同步异常: " + e.getMessage());
        }

        return result;
    }

    /**
     * 先对比所有映射字段,再决定执行更新或插入。
     * @return 0=跳过,1=更新,2=新增,-1=失败
     */
    private int processRecordCompareAndUpsert(Map<String, String> data,
                                              JSONArray updateDetails,
                                              JSONArray failedRecords,
                                              Set<Integer> affectedIds) {
        String billnum = Util.null2String(data.get("billnum"));
        String cgjsdid = Util.null2String(data.get("cgjsdid"));
        
        try {
            RecordSet checkRs = new RecordSet();
            checkRs.executeQuery("SELECT * FROM " + TABLE_NAME + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
            
            if (checkRs.next()) {
                int existingId = checkRs.getInt("id");
                
                if (hasDataChanged(checkRs, data)) {
                    JSONArray changes = collectDifferences(checkRs, data);
                    
                    if (updateDetails != null) {
                        JSONObject detail = new JSONObject();
                        detail.put("billnum", billnum);
                        detail.put("cgjsdid", cgjsdid);
                        detail.put("changes", changes);
                        updateDetails.add(detail);
                    }
                    
                    if (changes != null && changes.size() > 0) {
                        boolean success = applyUpdate(existingId, changes);
                        if (success) {
                            if (affectedIds != null) {
                                affectedIds.add(existingId);
                            }
                            return 1;
                        }
                        throw new RuntimeException("更新采购结算单失败");
                    }

                    return 0;
                }
                return 0;
            }
            
            // 新增记录
            Integer newId = insertRecord(data);
            if (newId != null && newId > 0) {
                if (affectedIds != null) {
                    affectedIds.add(newId);
                }
                return 2;
            }
            throw new RuntimeException("新增采购结算单失败");
            
        } catch (Exception e) {
            if (failedRecords != null) {
                JSONObject failedRecord = new JSONObject();
                failedRecord.put("billnum", billnum);
                failedRecord.put("cgjsdid", cgjsdid);
                failedRecord.put("errorMessage", e.getMessage());
                failedRecords.add(failedRecord);
            }
            return -1;
        }
    }

    private boolean applyUpdate(int recordId, JSONArray changes) {
        if (recordId <= 0 || changes == null || changes.isEmpty()) {
            return false;
        }
        StringBuilder setClause = new StringBuilder();
        for (int i = 0; i < changes.size(); i++) {
            JSONObject change = changes.getJSONObject(i);
            String field = Util.null2String(change.getString("field"));
            String value = Util.null2String(change.getString("newValue"));
            if (field.isEmpty()) {
                continue;
            }
            if (setClause.length() > 0) {
                setClause.append(",");
            }
            setClause.append(field).append("=").append(toSqlValue(value));
        }
        if (setClause.length() == 0) {
            return false;
        }
        String sql = "UPDATE " + TABLE_NAME + " SET " + setClause + " WHERE id=" + recordId;
        RecordSet updateRs = new RecordSet();
        return updateRs.execute(sql);
    }

    private Integer insertRecord(Map<String, String> data) {
        if (data == null || data.isEmpty()) {
            return null;
        }
        Map<String, String> insertMap = new HashMap<>(data);
        String now = TimeUtil.getCurrentTimeString();
        String createDate = now.substring(0, 10);
        String createTime = now.substring(11);
        insertMap.put("formmodeid", String.valueOf(getModeId()));
        insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
        insertMap.put("modedatacreatertype", "1");
        insertMap.put("modedatacreatedate", createDate);
        insertMap.put("modedatacreatetime", createTime);
        insertMap.put("modeuuid", UUID.randomUUID().toString());

        StringBuilder columns = new StringBuilder();
        StringBuilder values = new StringBuilder();
        for (Map.Entry<String, String> entry : insertMap.entrySet()) {
            if (columns.length() > 0) {
                columns.append(",");
                values.append(",");
            }
            columns.append(entry.getKey());
            values.append(toSqlValue(entry.getValue()));
        }
        String sql = "INSERT INTO " + TABLE_NAME + "(" + columns + ") VALUES(" + values + ")";
        RecordSet insertRs = new RecordSet();
        boolean success = insertRs.execute(sql);
        if (!success) {
            return null;
        }
        RecordSet idRs = new RecordSet();
        idRs.executeQuery("SELECT id FROM " + TABLE_NAME + " WHERE cgjsdid = ? AND billnum = ?",
                Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
        if (idRs.next()) {
            return idRs.getInt("id");
        }
        return null;
    }

    private String toSqlValue(String value) {
        String normalized = Util.null2String(value);
        if (normalized.isEmpty()) {
            return "null";
        }
        return "'" + normalized.replace("'", "''") + "'";
    }

    private void rebuildPermissions(Set<Integer> affectedIds) {
        if (affectedIds == null || affectedIds.isEmpty()) {
            return;
        }
        try {
            Method method = ModeUtil.class.getMethod("rebuildModeRights", String.class, int.class, Set.class);
            method.invoke(null, TABLE_NAME, DEFAULT_USER_ID, affectedIds);
        } catch (NoSuchMethodException e) {
            rebuildPermissionsFallback(affectedIds);
        } catch (Exception e) {
            new BaseBean().writeLog("采购结算单同步-重构权限失败: " + e.getMessage());
        }
    }

    private void rebuildPermissionsFallback(Set<Integer> affectedIds) {
        try {
            int modeId = getModeId();
            if (modeId <= 0) {
                return;
            }
            ModeRightInfo rightInfo = new ModeRightInfo();
            rightInfo.setNewRight(false);
            rightInfo.init();
            rightInfo.setModeId(modeId);
            for (Integer billId : affectedIds) {
                if (billId == null || billId <= 0) {
                    continue;
                }
                rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
            }
        } catch (Exception ex) {
            new BaseBean().writeLog("采购结算单同步-重构权限Fallback失败: " + ex.getMessage());
        }
    }

    private int getModeId() {
        if (MODE_ID_CACHE != null && MODE_ID_CACHE > 0) {
            return MODE_ID_CACHE;
        }
        synchronized (syncCGJSD.class) {
            if (MODE_ID_CACHE == null || MODE_ID_CACHE <= 0) {
                MODE_ID_CACHE = Util.getIntValue(CommonUtil.getModeIdByTableName(TABLE_NAME));
            }
        }
        return MODE_ID_CACHE;
    }
}

更新摘要

  • syncCGJSD 中改为记录所有变更的 affectedIds,循环结束后一次性调用 rebuildPermissions,避免在 10 万条处理过程中频繁触发 ModeUtil.saveModeData,并显式打断分页条件与节流,减少单次任务的 DB/API 压力。
        JSONArray updatedDetails = new JSONArray();
        JSONArray failedRecords = new JSONArray();
        Set<Integer> affectedIds = new HashSet<>();
...
            rebuildPermissions(affectedIds);
            String syncInfo = "总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount;
  • 将原先逐条 ModeUtil.saveModeData 操作替换为自定义 applyUpdate/insertRecord SQL,批量写库后仅收集需要重构权限的主键,再在 rebuildPermissions 中通过反射尝试一次调用新版 ModeUtil.rebuildModeRights,若缺失则回退到 ModeRightInfo 逐条重构,满足“ModeUtil 只在末尾用于权限”的诉求。
                        boolean success = applyUpdate(existingId, changes);
...
            Integer newId = insertRecord(data);
...
    private void rebuildPermissions(Set<Integer> affectedIds) {
        if (affectedIds == null || affectedIds.isEmpty()) {
            return;
        }
        try {
            Method method = ModeUtil.class.getMethod("rebuildModeRights", String.class, int.class, Set.class);
            method.invoke(null, TABLE_NAME, DEFAULT_USER_ID, affectedIds);
        } catch (NoSuchMethodException e) {
            rebuildPermissionsFallback(affectedIds);
        } catch (Exception e) {
            new BaseBean().writeLog("采购结算单同步-重构权限失败: " + e.getMessage());
        }
    }
  • ModeUtil 中新增 rebuildModeRights,支持一次性为多个记录重建模式权限,供同步任务末尾统一调用。
    public static void rebuildModeRights(String table, int userid, Set<Integer> billIds) {
        if (billIds == null || billIds.isEmpty()) {
            return;
        }
        int modeid = Util.getIntValue(CommonUtil.getModeIdByTableName(table));
        if (modeid <= 0) {
            return;
        }
        ModeRightInfo.setNewRight(false);
        ModeRightInfo.init();
        ModeRightInfo.setModeId(modeid);
        for (Integer billid : billIds) {
            if (billid == null || billid <= 0) {
                continue;
            }
            ModeRightInfo.rebuildModeDataShareByEdit(userid, modeid, billid);
        }
    }

测试

  • 尚未运行自动化或集成测试;请在部署前使用 10 万条接近真实体量的数据手动跑一次任务,确认耗时与权限结果。

后续建议

  • 若仍难在 10 s 内完成,可继续引入临时表批量 INSERT … SELECT 或 Redis 快照进一步降低 DB 往返。
  • 任务上线前建议重点验证:① 新增/更新是否写入 formmodeid 等元数据;② 权限重构是否覆盖所有 affectedIds
        JSONArray updatedDetails = new JSONArray();
        JSONArray failedRecords = new JSONArray();
        Set<Integer> affectedIds = new HashSet<>();
...
            rebuildPermissions(affectedIds);
            String syncInfo = "总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount;
                        boolean success = applyUpdate(existingId, changes);
...
            Integer newId = insertRecord(data);
...
    private void rebuildPermissions(Set<Integer> affectedIds) {
        if (affectedIds == null || affectedIds.isEmpty()) {
            return;
        }
        try {
            Method method = ModeUtil.class.getMethod("rebuildModeRights", String.class, int.class, Set.class);
            method.invoke(null, TABLE_NAME, DEFAULT_USER_ID, affectedIds);
        } catch (NoSuchMethodException e) {
            rebuildPermissionsFallback(affectedIds);
        } catch (Exception e) {
            new BaseBean().writeLog("采购结算单同步-重构权限失败: " + e.getMessage());
        }
    }
    public static void rebuildModeRights(String table, int userid, Set<Integer> billIds) {
        if (billIds == null || billIds.isEmpty()) {
            return;
        }
        int modeid = Util.getIntValue(CommonUtil.getModeIdByTableName(table));
        if (modeid <= 0) {
            return;
        }
        ModeRightInfo.setNewRight(false);
        ModeRightInfo.init();
        ModeRightInfo.setModeId(modeid);
        for (Integer billid : billIds) {
            if (billid == null || billid <= 0) {
                continue;
            }
            ModeRightInfo.rebuildModeDataShareByEdit(userid, modeid, billid);
        }
    }

User

重构不要写在@ModeUtil.java 里面去,就写在同步里面


Cursor

package dev.soft.lb.util;

import weaver.conn.RecordSet;
import weaver.formmode.setup.ModeRightInfo;
import weaver.general.TimeUtil;
import weaver.general.Util;

import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * 模块工具类
 * @author 林斌
 * @version V1.0
 * @date 2024/11/7  19:21
 * 2025-05-08
 * 林斌
 */
public class ModeUtil {

    //重构所有数据权限
    private static ModeRightInfo ModeRightInfo = new ModeRightInfo();

    public static boolean saveModeData(String table, int userid, RecordSet rs, Map<String,String> data, List<Map<String,Object>> dtdatas, String uniField){
        return saveModeData(table,userid,rs,data,dtdatas,uniField,true);
    }

    /**
     * 保存模块数据
     * @param table 主表名
     * @param userid 操作用户ID
     * @param rs 数据集
     * @param data 主表数据
     * @param dtdatas 明细行数据(List<Map<String,Object>> Map对象:table明细表,uniField明细表唯一字段,datalist数据集List<Map<String,String>>)
     * @param uniField 主表唯一字段
     * @return
     */
    public static boolean saveModeData(String table, int userid, RecordSet rs, Map<String,String> data, List<Map<String,Object>> dtdatas, String uniField,boolean buildRights){
        boolean flag = false;
        try {
            if(rs==null){
                rs = new RecordSet();
            }
            int modeid = Util.getIntValue(CommonUtil.getModeIdByTableName(table));
            String sql = "";
            String tpsql = "";
            String s1 = "";
            String s2 = "";
            int billid = 0;
            //如果存在唯一字段且唯一字段值不为空
            if(data.containsKey(uniField)&&!Util.null2String(data.get(uniField)).equals("")){
                sql = "select id from "+table+" where "+uniField+"='"+data.get(uniField)+"'";
                rs.execute(sql);
                if(rs.next()){
                    billid = Util.getIntValue(Util.null2String(rs.getString("id")),0);
                }
            }

            if(billid>0){
                tpsql = "";
                for(String key : data.keySet()){
                    String value = Util.null2String(data.get(key));
                    if(value.equals("")){
                        value = "null";
                    }else{
                        value = "'"+value.replace("'","''")+"'";
                    }
                    tpsql += tpsql.equals("")?(key+"="+ value):(","+key+"="+value);
                }
                sql = "update "+table+" set "+tpsql+" where id="+ billid;
                rs.execute(sql);

                if(buildRights){
                    //设置共享权限
                    ModeRightInfo.setNewRight(false);
                    ModeRightInfo.init();
                    ModeRightInfo.setModeId(modeid);
                    ModeRightInfo.rebuildModeDataShareByEdit(userid, modeid, billid);
                }
            }else{
                String nowtime = TimeUtil.getCurrentTimeString();
                String createDate = nowtime.substring(0,10);
                String createTime = nowtime.substring(11);
                String uuid = UUID.randomUUID().toString();
                data.put("modeuuid",uuid);
                //无数据则执行新增
                data.put("formmodeid",modeid+"");
                data.put("modedatacreater",userid+"");
                data.put("modedatacreatertype","1");
                data.put("modedatacreatedate",createDate);
                data.put("modedatacreatetime",createTime);
                s1 = "";
                s2 = "";
                for(String key : data.keySet()){
                    String value = Util.null2String(data.get(key));
                    if(!value.equals("")){
                        value = "'"+value.replace("'","''")+"'";
                    }else{
                        value = "null";
                    }
                    s1 += s1.equals("")?key:","+key;
                    s2 += s2.equals("")?value:","+value+"";
                }
                //新增数据
                sql = "insert into "+table+"("+s1+") values("+s2+")";
                boolean saveflag = rs.execute(sql);
                //如果保存成功
                if(saveflag) {
                    //如果需要更新权限,则重构权限
                    if(buildRights) {
                        //查询新增的数据
                        sql = "select id from " + table + " where modeuuid='" + uuid + "'";
                        rs.execute(sql);
                        if (rs.next()) {
                            billid = Util.getIntValue(Util.null2String(rs.getString("id")), 0);
                        }
                        //如果新增数据,则重构权限
                        if (billid > 0) {
                            //设置共享权限
                            ModeRightInfo.setNewRight(false);
                            ModeRightInfo.init();
                            ModeRightInfo.setModeId(modeid);
                            ModeRightInfo.rebuildModeDataShareByEdit(userid, modeid, billid);
                        }
                    }
                }
            }
            //如果有数据
            if(billid>0 && dtdatas!=null && dtdatas.size()>0){
                //循环所有明细数据
                for(Map<String,Object> item : dtdatas){
                    //明细表名
                    String dttable = (String) item.get("table");
                    //明细唯一字段
                    String dtuniField = (String) item.get("uniField");
                    //数据行
                    List<Map<String,String>> dtlist = (List<Map<String, String>>) item.get("datalist");
                    if(dtlist!=null && dtlist.size()>0){
                        for(Map<String,String> subdata : dtlist){
                            int dtid = 0;
                            //如果有关键值且关键值不为空
                            if(subdata.containsKey(dtuniField)&& !Util.null2String(subdata.get(dtuniField)).equals("")){
                                sql = "select id from "+dttable+" where mainid="+billid+" and "+dtuniField+"='"+subdata.get(dtuniField)+"'";
                                rs.execute(sql);
                                if(rs.next()){
                                    dtid = Util.getIntValue(Util.null2String(rs.getString("id")),0);
                                }
                            }
                            //如果明细数据ID大于0,则更新
                            if(dtid>0){
                                tpsql = "";
                                for(String key : subdata.keySet()){
                                    String value = Util.null2String(subdata.get(key));
                                    if(value.equals("")){
                                        value = "null";
                                    }else{
                                        value = "'"+value.replace("'","''")+"'";
                                    }
                                    tpsql += tpsql.equals("")?(key+"="+ value):(","+key+"="+value);
                                }
                                sql = "update "+dttable+" set "+tpsql+" where id="+ dtid;
                                rs.execute(sql);
                            }else{
                                subdata.put("mainid",billid+"");
                                s1 = "";
                                s2 = "";
                                for(String key : subdata.keySet()){
                                    String value = Util.null2String(subdata.get(key));
                                    if(!value.equals("")){
                                        value = "'"+value.replace("'","''")+"'";
                                    }else{
                                        value = "null";
                                    }
                                    s1 += s1.equals("")?key:","+key;
                                    s2 += s2.equals("")?value:","+value+"";
                                }
                                //新增明细数据
                                sql = "insert into "+dttable+"("+s1+") values("+s2+")";
                                rs.execute(sql);
                            }
                        }
                    }
                }
            }
            flag = true;
        }catch (Exception e){
            throw new RuntimeException("保存模块数据异常:"+e.getMessage());
        }
        return flag;
    }

}

package com.ljj.scheduleSync;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import weaver.conn.RecordSet;
import dev.soft.lb.util.CommonUtil;
import dev.soft.lb.util.LCUtil;
import dev.soft.lb.util.ModeUtil;
import weaver.general.Util;
import weaver.general.TimeUtil;
import weaver.formmode.setup.ModeRightInfo;
import weaver.hrm.company.SubCompanyComInfo;
import weaver.interfaces.schedule.BaseCronJob;
import weaver.general.BaseBean;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.UUID;

public class syncCGJSD extends BaseCronJob {
    // 防重复执行的标识,避免 10 分钟间隔出现重叠
    private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
    
    private static final String COMPANY_ID = "66";
    private static final String BILL_TYPE = "12";
    private static final String TABLE_NAME = "uf_cgjsd";
    private static final int DEFAULT_USER_ID = 1;
    private static volatile Integer MODE_ID_CACHE = null;
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    // 数值字段名单:这些字段按两位小数口径进行比较与存储
    private static final Set<String> NUMERIC_FIELDS = new HashSet<>(
            Arrays.asList(
                    "amount",
                    "foreigncurrency",
                    "clearamount",
                    "clearforeigncurrency",
                    "balanceamount",
                    "balanceforeigncurrency",
                    "availableamount",
                    "availableforeigncurrency",
                    // 添加其他可能的数值字段
                    "original_clearamount",
                    "original_clearforeigncurrency",
                    "original_balanceamount",
                    "original_balanceforeigncurrency",
                    "price",
                    "unitprice",
                    "quantity",
                    "taxrate",
                    "taxamount",
                    "totalmoney",
                    "netamount",
                    "amountnotax"
            )
    );

    // 规范化字符串:去首尾空格;null按空串处理
    private static String norm(String v) {
        if (v == null) return "";
        return v.trim();
    }

    // 字符串等价:规范化后比较;空串与null等价
    private static boolean equalNormalized(String a, String b) {
        return norm(a).equals(norm(b));
    }

    // 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
    private static boolean equalNumeric(String a, String b) {
        String sa = norm(a);
        String sb = norm(b);
        if (sa.isEmpty() && sb.isEmpty()) return true;
        if (sa.isEmpty() || sb.isEmpty()) return false;
        try {
            BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
            BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
            return ba.compareTo(bb) == 0;
        } catch (Exception ignore) {
            return sa.equals(sb);
        }
    }

    // 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
    private static String formatTo2Decimals(String v) {
        String s = norm(v);
        if (s.isEmpty()) return s;
        try {
            return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
        } catch (Exception ignore) {
            return s;
        }
    }

    @Override
    public void execute() {
        BaseBean log = new BaseBean();
        log.writeLog("采购结算单同步任务开始");

        if (!RUNNING.compareAndSet(false, true)) {
            log.writeLog("检测到正在执行的采购结算单同步任务,本次调度跳过");
            return;
        }

        try {
            RecordSet rs = new RecordSet();
            String dwbm = new SubCompanyComInfo().getSubCompanyCode(COMPANY_ID);
            String api = LCUtil.getLCBillTypeInfo(BILL_TYPE, rs).get("api_list").toString();
            
            JSONObject paramJson = new JSONObject();
            paramJson.put("dwbh", dwbm);
            paramJson.put("pagesize", "10000");
            paramJson.put("page", "1");
            
            JSONObject syncResult = syncDataIncremental(paramJson, rs, api);
            
            if (syncResult.containsKey("updateInfo")) {
                log.writeLog(syncResult.getString("updateInfo"));
            }
            if (syncResult.containsKey("failCount") && syncResult.getInteger("failCount") > 0) {
                log.writeLog("失败记录数: " + syncResult.getInteger("failCount"));
            }
            
            log.writeLog("采购结算单同步任务完成");
            
        } catch (Exception e) {
            log.writeLog("采购结算单同步异常:" + e.getMessage());
        } finally {
            RUNNING.set(false);
        }
    }

    // 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
    private boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
        try {
            String[] columnNames = rs.getColumnName();
            if (columnNames == null || columnNames.length == 0) {
                return true;
            }

            for (String columnName : columnNames) {
                // 跳过系统字段
                if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                    || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                    || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                    continue;
                }

                String dbValue;
                try {
                    dbValue = Util.null2String(rs.getString(columnName));
                } catch (Exception ignore) {
                    continue;
                }

                if (!newData.containsKey(columnName)) {
                    continue;
                }

                String newValue = Util.null2String(newData.get(columnName));
                boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                    equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

                if (!equal) {
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            return true;
        }
    }

    // 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
    private JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
        JSONArray changes = new JSONArray();
        try {
            // 获取数据库记录的所有字段名
            String[] columnNames = rs.getColumnName();
            if (columnNames == null || columnNames.length == 0) {
                return changes; // 无法获取字段信息
            }

            // 遍历数据库的所有字段进行对比
            for (String columnName : columnNames) {
                // 跳过系统字段
                if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                    || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                    || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                    continue;
                }

                String dbValue;
                try {
                    dbValue = Util.null2String(rs.getString(columnName));
                } catch (Exception ignore) {
                    continue; // 无法获取该字段值,跳过
                }

                // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
                if (!newData.containsKey(columnName)) {
                    continue;
                }

                // 获取新数据中对应字段的值
                String newValue = Util.null2String(newData.get(columnName));

                // 根据字段类型进行对比
                boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                    equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

                if (!equal) {
                    JSONObject diff = new JSONObject();
                    diff.put("field", columnName);
                    diff.put("oldValue", dbValue);
                    diff.put("newValue", newValue);
                    changes.add(diff);
                }
            }
        } catch (Exception e) {
            // 忽略,返回当前已收集到的差异
        }
        return changes;
    }

    private JSONObject syncDataIncremental(JSONObject param, RecordSet rs, String api) {
        JSONObject result = new JSONObject();
        int totalCount = 0, newCount = 0, updateCount = 0, skipCount = 0, failCount = 0;
        JSONArray updatedDetails = new JSONArray();
        JSONArray failedRecords = new JSONArray();
        Set<Integer> affectedIds = new HashSet<>();

        try {
            int page = Integer.parseInt(param.getString("page") == null ? "1" : param.getString("page"));
            String pageSize = Util.null2String(param.getString("pagesize"));
            if ("".equals(pageSize)) pageSize = "10000";

            while (true) {
                param.put("page", String.valueOf(page));
                param.put("pagesize", pageSize);

                JSONObject rjson = LCUtil.doPost(api, param);
                result = rjson;

                if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                    break;
                }
                if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                    break;
                }

                JSONArray datas = rjson.getJSONArray("data");
                int pageCount = (datas == null) ? 0 : datas.size();
                if (pageCount == 0) {
                    break;
                }
                totalCount += pageCount;

                if (datas != null) {
                    for (int i = 0; i < datas.size(); i++) {
                        String billnum = "";
                        String cgjsdid = "";
                        try {
                            JSONObject item = datas.getJSONObject(i);
                            Map<String, String> data = new HashMap<>();

                            cgjsdid = Util.null2String(item.get("ID"));
                            billnum = Util.null2String(item.get("BILLNUM"));
                            
                            // 映射字段
                            data.put("cgjsdid", cgjsdid);
                            data.put("billnum", billnum);
                            data.put("WLDW", Util.null2String(item.get("WLDW")));
                            data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                            data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                            data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                            data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                            data.put("BMBH", Util.null2String(item.get("BMBH")));
                            data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                            data.put("summary", Util.null2String(item.get("SUMMARY")));
                            data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                            data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                            data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                            data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                            data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                            data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                            data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                            data.put("dwbh", Util.null2String(item.get("DWBH")));
                            data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                            data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                            data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                            data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                            data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                            data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                            data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                            data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                            data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                            data.put("XMID", Util.null2String(item.get("XMID")));
                            data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                            data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                            data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                            data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                            data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                            data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                            if (item.get("BILLDATE") != null) {
                                data.put("Billdate", DATE_FORMAT.format(item.getDate("BILLDATE")));
                            }

                            // 金额字段
                            data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                            data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                            data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                            data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                            data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                            data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                            data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                            data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                            int action = processRecordCompareAndUpsert(data, updatedDetails, failedRecords, affectedIds);
                            if (action == 1) {
                                updateCount++;
                            } else if (action == 2) {
                                newCount++;
                            } else if (action == -1) {
                                failCount++;
                            } else {
                                skipCount++;
                            }
                        } catch (Exception e) {
                            failCount++;
                            JSONObject failedRecord = new JSONObject();
                            failedRecord.put("billnum", billnum);
                            failedRecord.put("cgjsdid", cgjsdid);
                            failedRecord.put("errorMessage", e.getMessage());
                            failedRecords.add(failedRecord);
                        }
                    }
                }

                // 每页处理完成后,执行关联字段更新
                try {
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                    rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
                } catch (Exception ex) {
                    // 忽略关联更新异常
                }

                if (pageCount < Integer.parseInt(pageSize)) {
                    break;
                }

                page++;

                try {
                    Thread.sleep(200L);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }

            rebuildPermissions(affectedIds);

            String syncInfo = "总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount;
            result.put("updateInfo", syncInfo);
            result.put("updatedDetails", updatedDetails);
            
            if (failCount > 0) {
                result.put("failedRecords", failedRecords);
                result.put("failCount", failCount);
            }
        } catch (Exception e) {
            result.put("error", "同步异常: " + e.getMessage());
        }

        return result;
    }

    /**
     * 先对比所有映射字段,再决定执行更新或插入。
     * @return 0=跳过,1=更新,2=新增,-1=失败
     */
    private int processRecordCompareAndUpsert(Map<String, String> data,
                                              JSONArray updateDetails,
                                              JSONArray failedRecords,
                                              Set<Integer> affectedIds) {
        String billnum = Util.null2String(data.get("billnum"));
        String cgjsdid = Util.null2String(data.get("cgjsdid"));
        
        try {
            RecordSet checkRs = new RecordSet();
            checkRs.executeQuery("SELECT * FROM " + TABLE_NAME + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
            
            if (checkRs.next()) {
                int existingId = checkRs.getInt("id");
                
                if (hasDataChanged(checkRs, data)) {
                    JSONArray changes = collectDifferences(checkRs, data);
                    
                    if (updateDetails != null) {
                        JSONObject detail = new JSONObject();
                        detail.put("billnum", billnum);
                        detail.put("cgjsdid", cgjsdid);
                        detail.put("changes", changes);
                        updateDetails.add(detail);
                    }
                    
                    if (changes != null && changes.size() > 0) {
                        boolean success = applyUpdate(existingId, changes);
                        if (success) {
                            if (affectedIds != null) {
                                affectedIds.add(existingId);
                            }
                            return 1;
                        }
                        throw new RuntimeException("更新采购结算单失败");
                    }

                    return 0;
                }
                return 0;
            }
            
            // 新增记录
            Integer newId = insertRecord(data);
            if (newId != null && newId > 0) {
                if (affectedIds != null) {
                    affectedIds.add(newId);
                }
                return 2;
            }
            throw new RuntimeException("新增采购结算单失败");
            
        } catch (Exception e) {
            if (failedRecords != null) {
                JSONObject failedRecord = new JSONObject();
                failedRecord.put("billnum", billnum);
                failedRecord.put("cgjsdid", cgjsdid);
                failedRecord.put("errorMessage", e.getMessage());
                failedRecords.add(failedRecord);
            }
            return -1;
        }
    }

    private boolean applyUpdate(int recordId, JSONArray changes) {
        if (recordId <= 0 || changes == null || changes.isEmpty()) {
            return false;
        }
        StringBuilder setClause = new StringBuilder();
        for (int i = 0; i < changes.size(); i++) {
            JSONObject change = changes.getJSONObject(i);
            String field = Util.null2String(change.getString("field"));
            String value = Util.null2String(change.getString("newValue"));
            if (field.isEmpty()) {
                continue;
            }
            if (setClause.length() > 0) {
                setClause.append(",");
            }
            setClause.append(field).append("=").append(toSqlValue(value));
        }
        if (setClause.length() == 0) {
            return false;
        }
        String sql = "UPDATE " + TABLE_NAME + " SET " + setClause + " WHERE id=" + recordId;
        RecordSet updateRs = new RecordSet();
        return updateRs.execute(sql);
    }

    private Integer insertRecord(Map<String, String> data) {
        if (data == null || data.isEmpty()) {
            return null;
        }
        Map<String, String> insertMap = new HashMap<>(data);
        String now = TimeUtil.getCurrentTimeString();
        String createDate = now.substring(0, 10);
        String createTime = now.substring(11);
        insertMap.put("formmodeid", String.valueOf(getModeId()));
        insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
        insertMap.put("modedatacreatertype", "1");
        insertMap.put("modedatacreatedate", createDate);
        insertMap.put("modedatacreatetime", createTime);
        insertMap.put("modeuuid", UUID.randomUUID().toString());

        StringBuilder columns = new StringBuilder();
        StringBuilder values = new StringBuilder();
        for (Map.Entry<String, String> entry : insertMap.entrySet()) {
            if (columns.length() > 0) {
                columns.append(",");
                values.append(",");
            }
            columns.append(entry.getKey());
            values.append(toSqlValue(entry.getValue()));
        }
        String sql = "INSERT INTO " + TABLE_NAME + "(" + columns + ") VALUES(" + values + ")";
        RecordSet insertRs = new RecordSet();
        boolean success = insertRs.execute(sql);
        if (!success) {
            return null;
        }
        RecordSet idRs = new RecordSet();
        idRs.executeQuery("SELECT id FROM " + TABLE_NAME + " WHERE cgjsdid = ? AND billnum = ?",
                Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
        if (idRs.next()) {
            return idRs.getInt("id");
        }
        return null;
    }

    private String toSqlValue(String value) {
        String normalized = Util.null2String(value);
        if (normalized.isEmpty()) {
            return "null";
        }
        return "'" + normalized.replace("'", "''") + "'";
    }

    private void rebuildPermissions(Set<Integer> affectedIds) {
        if (affectedIds == null || affectedIds.isEmpty()) {
            return;
        }
        try {
            int modeId = getModeId();
            if (modeId <= 0) {
                return;
            }
            ModeRightInfo rightInfo = new ModeRightInfo();
            rightInfo.setNewRight(false);
            rightInfo.init();
            rightInfo.setModeId(modeId);
            for (Integer billId : affectedIds) {
                if (billId == null || billId <= 0) {
                    continue;
                }
                rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
            }
        } catch (Exception ex) {
            new BaseBean().writeLog("采购结算单同步-重构权限失败: " + ex.getMessage());
        }
    }

    private int getModeId() {
        if (MODE_ID_CACHE != null && MODE_ID_CACHE > 0) {
            return MODE_ID_CACHE;
        }
        synchronized (syncCGJSD.class) {
            if (MODE_ID_CACHE == null || MODE_ID_CACHE <= 0) {
                MODE_ID_CACHE = Util.getIntValue(CommonUtil.getModeIdByTableName(TABLE_NAME));
            }
        }
        return MODE_ID_CACHE;
    }
}

package com.ljj.scheduleSync;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import weaver.conn.RecordSet;
import dev.soft.lb.util.CommonUtil;
import dev.soft.lb.util.LCUtil;
import weaver.general.Util;
import weaver.general.TimeUtil;
import weaver.formmode.setup.ModeRightInfo;
import weaver.hrm.company.SubCompanyComInfo;
import weaver.interfaces.schedule.BaseCronJob;
import weaver.general.BaseBean;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.UUID;

public class syncCGJSD extends BaseCronJob {
    // 防重复执行的标识,避免 10 分钟间隔出现重叠
    private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
    
    private static final String COMPANY_ID = "66";
    private static final String BILL_TYPE = "12";
    private static final String TABLE_NAME = "uf_cgjsd";
    private static final int DEFAULT_USER_ID = 1;
    private static volatile Integer MODE_ID_CACHE = null;
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    // 数值字段名单:这些字段按两位小数口径进行比较与存储
    private static final Set<String> NUMERIC_FIELDS = new HashSet<>(
            Arrays.asList(
                    "amount",
                    "foreigncurrency",
                    "clearamount",
                    "clearforeigncurrency",
                    "balanceamount",
                    "balanceforeigncurrency",
                    "availableamount",
                    "availableforeigncurrency",
                    // 添加其他可能的数值字段
                    "original_clearamount",
                    "original_clearforeigncurrency",
                    "original_balanceamount",
                    "original_balanceforeigncurrency",
                    "price",
                    "unitprice",
                    "quantity",
                    "taxrate",
                    "taxamount",
                    "totalmoney",
                    "netamount",
                    "amountnotax"
            )
    );

    // 规范化字符串:去首尾空格;null按空串处理
    private static String norm(String v) {
        if (v == null) return "";
        return v.trim();
    }

    // 字符串等价:规范化后比较;空串与null等价
    private static boolean equalNormalized(String a, String b) {
        return norm(a).equals(norm(b));
    }

    // 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
    private static boolean equalNumeric(String a, String b) {
        String sa = norm(a);
        String sb = norm(b);
        if (sa.isEmpty() && sb.isEmpty()) return true;
        if (sa.isEmpty() || sb.isEmpty()) return false;
        try {
            BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
            BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
            return ba.compareTo(bb) == 0;
        } catch (Exception ignore) {
            return sa.equals(sb);
        }
    }

    // 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
    private static String formatTo2Decimals(String v) {
        String s = norm(v);
        if (s.isEmpty()) return s;
        try {
            return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
        } catch (Exception ignore) {
            return s;
        }
    }

    @Override
    public void execute() {
        BaseBean log = new BaseBean();
        log.writeLog("采购结算单同步任务开始");

        if (!RUNNING.compareAndSet(false, true)) {
            log.writeLog("检测到正在执行的采购结算单同步任务,本次调度跳过");
            return;
        }

        try {
            RecordSet rs = new RecordSet();
            String dwbm = new SubCompanyComInfo().getSubCompanyCode(COMPANY_ID);
            String api = LCUtil.getLCBillTypeInfo(BILL_TYPE, rs).get("api_list").toString();
            
            JSONObject paramJson = new JSONObject();
            paramJson.put("dwbh", dwbm);
            paramJson.put("pagesize", "10000");
            paramJson.put("page", "1");
            
            JSONObject syncResult = syncDataIncremental(paramJson, rs, api);
            
            if (syncResult.containsKey("updateInfo")) {
                log.writeLog(syncResult.getString("updateInfo"));
            }
            if (syncResult.containsKey("failCount") && syncResult.getInteger("failCount") > 0) {
                log.writeLog("失败记录数: " + syncResult.getInteger("failCount"));
            }
            
            log.writeLog("采购结算单同步任务完成");
            
        } catch (Exception e) {
            log.writeLog("采购结算单同步异常:" + e.getMessage());
        } finally {
            RUNNING.set(false);
        }
    }

    // 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
    private boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
        try {
            String[] columnNames = rs.getColumnName();
            if (columnNames == null || columnNames.length == 0) {
                return true;
            }

            for (String columnName : columnNames) {
                // 跳过系统字段
                if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                    || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                    || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                    continue;
                }

                String dbValue;
                try {
                    dbValue = Util.null2String(rs.getString(columnName));
                } catch (Exception ignore) {
                    continue;
                }

                if (!newData.containsKey(columnName)) {
                    continue;
                }

                String newValue = Util.null2String(newData.get(columnName));
                boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                    equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

                if (!equal) {
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            return true;
        }
    }

    // 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
    private JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
        JSONArray changes = new JSONArray();
        try {
            // 获取数据库记录的所有字段名
            String[] columnNames = rs.getColumnName();
            if (columnNames == null || columnNames.length == 0) {
                return changes; // 无法获取字段信息
            }

            // 遍历数据库的所有字段进行对比
            for (String columnName : columnNames) {
                // 跳过系统字段
                if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                    || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                    || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                    continue;
                }

                String dbValue;
                try {
                    dbValue = Util.null2String(rs.getString(columnName));
                } catch (Exception ignore) {
                    continue; // 无法获取该字段值,跳过
                }

                // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
                if (!newData.containsKey(columnName)) {
                    continue;
                }

                // 获取新数据中对应字段的值
                String newValue = Util.null2String(newData.get(columnName));

                // 根据字段类型进行对比
                boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                    equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

                if (!equal) {
                    JSONObject diff = new JSONObject();
                    diff.put("field", columnName);
                    diff.put("oldValue", dbValue);
                    diff.put("newValue", newValue);
                    changes.add(diff);
                }
            }
        } catch (Exception e) {
            // 忽略,返回当前已收集到的差异
        }
        return changes;
    }

    private JSONObject syncDataIncremental(JSONObject param, RecordSet rs, String api) {
        JSONObject result = new JSONObject();
        int totalCount = 0, newCount = 0, updateCount = 0, skipCount = 0, failCount = 0;
        JSONArray updatedDetails = new JSONArray();
        JSONArray failedRecords = new JSONArray();
        Set<Integer> affectedIds = new HashSet<>();

        try {
            int page = Integer.parseInt(param.getString("page") == null ? "1" : param.getString("page"));
            String pageSize = Util.null2String(param.getString("pagesize"));
            if ("".equals(pageSize)) pageSize = "10000";

            while (true) {
                param.put("page", String.valueOf(page));
                param.put("pagesize", pageSize);

                JSONObject rjson = LCUtil.doPost(api, param);
                result = rjson;

                if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                    break;
                }
                if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                    break;
                }

                JSONArray datas = rjson.getJSONArray("data");
                int pageCount = (datas == null) ? 0 : datas.size();
                if (pageCount == 0) {
                    break;
                }
                totalCount += pageCount;

                if (datas != null) {
                    for (int i = 0; i < datas.size(); i++) {
                        String billnum = "";
                        String cgjsdid = "";
                        try {
                            JSONObject item = datas.getJSONObject(i);
                            Map<String, String> data = new HashMap<>();

                            cgjsdid = Util.null2String(item.get("ID"));
                            billnum = Util.null2String(item.get("BILLNUM"));
                            
                            // 映射字段
                            data.put("cgjsdid", cgjsdid);
                            data.put("billnum", billnum);
                            data.put("WLDW", Util.null2String(item.get("WLDW")));
                            data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                            data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                            data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                            data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                            data.put("BMBH", Util.null2String(item.get("BMBH")));
                            data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                            data.put("summary", Util.null2String(item.get("SUMMARY")));
                            data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                            data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                            data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                            data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                            data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                            data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                            data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                            data.put("dwbh", Util.null2String(item.get("DWBH")));
                            data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                            data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                            data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                            data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                            data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                            data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                            data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                            data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                            data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                            data.put("XMID", Util.null2String(item.get("XMID")));
                            data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                            data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                            data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                            data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                            data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                            data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                            if (item.get("BILLDATE") != null) {
                                data.put("Billdate", DATE_FORMAT.format(item.getDate("BILLDATE")));
                            }

                            // 金额字段
                            data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                            data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                            data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                            data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                            data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                            data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                            data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                            data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                            int action = processRecordCompareAndUpsert(data, updatedDetails, failedRecords, affectedIds);
                            if (action == 1) {
                                updateCount++;
                            } else if (action == 2) {
                                newCount++;
                            } else if (action == -1) {
                                failCount++;
                            } else {
                                skipCount++;
                            }
                        } catch (Exception e) {
                            failCount++;
                            JSONObject failedRecord = new JSONObject();
                            failedRecord.put("billnum", billnum);
                            failedRecord.put("cgjsdid", cgjsdid);
                            failedRecord.put("errorMessage", e.getMessage());
                            failedRecords.add(failedRecord);
                        }
                    }
                }

                // 每页处理完成后,执行关联字段更新
                try {
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                    rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                    rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
                } catch (Exception ex) {
                    // 忽略关联更新异常
                }

                if (pageCount < Integer.parseInt(pageSize)) {
                    break;
                }

                page++;

                try {
                    Thread.sleep(200L);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }

            rebuildPermissions(affectedIds);

            String syncInfo = "总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount;
            result.put("updateInfo", syncInfo);
            result.put("updatedDetails", updatedDetails);
            
            if (failCount > 0) {
                result.put("failedRecords", failedRecords);
                result.put("failCount", failCount);
            }
        } catch (Exception e) {
            result.put("error", "同步异常: " + e.getMessage());
        }

        return result;
    }

    /**
     * 先对比所有映射字段,再决定执行更新或插入。
     * @return 0=跳过,1=更新,2=新增,-1=失败
     */
    private int processRecordCompareAndUpsert(Map<String, String> data,
                                              JSONArray updateDetails,
                                              JSONArray failedRecords,
                                              Set<Integer> affectedIds) {
        String billnum = Util.null2String(data.get("billnum"));
        String cgjsdid = Util.null2String(data.get("cgjsdid"));
        
        try {
            RecordSet checkRs = new RecordSet();
            checkRs.executeQuery("SELECT * FROM " + TABLE_NAME + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
            
            if (checkRs.next()) {
                int existingId = checkRs.getInt("id");
                
                if (hasDataChanged(checkRs, data)) {
                    JSONArray changes = collectDifferences(checkRs, data);
                    
                    if (updateDetails != null) {
                        JSONObject detail = new JSONObject();
                        detail.put("billnum", billnum);
                        detail.put("cgjsdid", cgjsdid);
                        detail.put("changes", changes);
                        updateDetails.add(detail);
                    }
                    
                    if (changes != null && changes.size() > 0) {
                        boolean success = applyUpdate(existingId, changes);
                        if (success) {
                            if (affectedIds != null) {
                                affectedIds.add(existingId);
                            }
                            return 1;
                        }
                        throw new RuntimeException("更新采购结算单失败");
                    }

                    return 0;
                }
                return 0;
            }
            
            // 新增记录
            Integer newId = insertRecord(data);
            if (newId != null && newId > 0) {
                if (affectedIds != null) {
                    affectedIds.add(newId);
                }
                return 2;
            }
            throw new RuntimeException("新增采购结算单失败");
            
        } catch (Exception e) {
            if (failedRecords != null) {
                JSONObject failedRecord = new JSONObject();
                failedRecord.put("billnum", billnum);
                failedRecord.put("cgjsdid", cgjsdid);
                failedRecord.put("errorMessage", e.getMessage());
                failedRecords.add(failedRecord);
            }
            return -1;
        }
    }

    private boolean applyUpdate(int recordId, JSONArray changes) {
        if (recordId <= 0 || changes == null || changes.isEmpty()) {
            return false;
        }
        StringBuilder setClause = new StringBuilder();
        for (int i = 0; i < changes.size(); i++) {
            JSONObject change = changes.getJSONObject(i);
            String field = Util.null2String(change.getString("field"));
            String value = Util.null2String(change.getString("newValue"));
            if (field.isEmpty()) {
                continue;
            }
            if (setClause.length() > 0) {
                setClause.append(",");
            }
            setClause.append(field).append("=").append(toSqlValue(value));
        }
        if (setClause.length() == 0) {
            return false;
        }
        String sql = "UPDATE " + TABLE_NAME + " SET " + setClause + " WHERE id=" + recordId;
        RecordSet updateRs = new RecordSet();
        return updateRs.execute(sql);
    }

    private Integer insertRecord(Map<String, String> data) {
        if (data == null || data.isEmpty()) {
            return null;
        }
        Map<String, String> insertMap = new HashMap<>(data);
        String now = TimeUtil.getCurrentTimeString();
        String createDate = now.substring(0, 10);
        String createTime = now.substring(11);
        insertMap.put("formmodeid", String.valueOf(getModeId()));
        insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
        insertMap.put("modedatacreatertype", "1");
        insertMap.put("modedatacreatedate", createDate);
        insertMap.put("modedatacreatetime", createTime);
        insertMap.put("modeuuid", UUID.randomUUID().toString());

        StringBuilder columns = new StringBuilder();
        StringBuilder values = new StringBuilder();
        for (Map.Entry<String, String> entry : insertMap.entrySet()) {
            if (columns.length() > 0) {
                columns.append(",");
                values.append(",");
            }
            columns.append(entry.getKey());
            values.append(toSqlValue(entry.getValue()));
        }
        String sql = "INSERT INTO " + TABLE_NAME + "(" + columns + ") VALUES(" + values + ")";
        RecordSet insertRs = new RecordSet();
        boolean success = insertRs.execute(sql);
        if (!success) {
            return null;
        }
        RecordSet idRs = new RecordSet();
        idRs.executeQuery("SELECT id FROM " + TABLE_NAME + " WHERE cgjsdid = ? AND billnum = ?",
                Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
        if (idRs.next()) {
            return idRs.getInt("id");
        }
        return null;
    }

    private String toSqlValue(String value) {
        String normalized = Util.null2String(value);
        if (normalized.isEmpty()) {
            return "null";
        }
        return "'" + normalized.replace("'", "''") + "'";
    }

    private void rebuildPermissions(Set<Integer> affectedIds) {
        if (affectedIds == null || affectedIds.isEmpty()) {
            return;
        }
        try {
            int modeId = getModeId();
            if (modeId <= 0) {
                return;
            }
            ModeRightInfo rightInfo = new ModeRightInfo();
            rightInfo.setNewRight(false);
            rightInfo.init();
            rightInfo.setModeId(modeId);
            for (Integer billId : affectedIds) {
                if (billId == null || billId <= 0) {
                    continue;
                }
                rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
            }
        } catch (Exception ex) {
            new BaseBean().writeLog("采购结算单同步-重构权限失败: " + ex.getMessage());
        }
    }

    private int getModeId() {
        if (MODE_ID_CACHE != null && MODE_ID_CACHE > 0) {
            return MODE_ID_CACHE;
        }
        synchronized (syncCGJSD.class) {
            if (MODE_ID_CACHE == null || MODE_ID_CACHE <= 0) {
                MODE_ID_CACHE = Util.getIntValue(CommonUtil.getModeIdByTableName(TABLE_NAME));
            }
        }
        return MODE_ID_CACHE;
    }
}

  • 已按要求把权限重构逻辑完全放回 syncCGJSD,直接使用 ModeRightInfo 在同步末尾统一重建,不再依赖 ModeUtil 扩展方法;相关导入也清理完毕。
  • ModeUtil 恢复原貌,仅保留现有 saveModeData 能力;同步流程仍通过批量 SQL 更新/插入并在 rebuildPermissions 中一次性处理权限。
  • 目前仅剩早已存在的 ModeUtil 泛型未检查 warning(旧代码遗留),其余 lint 通过。

如需进一步优化 ModeUtil 的泛型告警或运行端到端测试,请告知。


User

@syncLCCGJSD.jsp 这个jsp保持和@syncCGJSD.java 功能逻辑一致,我去用jsp做测试


Cursor

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONArray" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="weaver.general.Util" %>
<%@ page import="dev.soft.lb.util.CommonUtil" %>
<%@ page import="dev.soft.lb.util.LCUtil" %>
<%@ page import="weaver.general.TimeUtil" %>
<%@ page import="weaver.conn.RecordSet" %>
<%@ page import="weaver.hrm.company.SubCompanyComInfo" %>
<%@ page import="weaver.formmode.setup.ModeRightInfo" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.math.BigDecimal" %>
<%@ page import="java.math.RoundingMode" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.UUID" %>

<%
// 设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
// 允许跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

// 定义常量
final String companyId = "66";
final String billtype = "12";
String api = "";
final String tablename = "uf_cgjsd";
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 创建返回结果
JSONObject result = new JSONObject();

try {
    // 获取单位编码
    SubCompanyComInfo scci = new SubCompanyComInfo();
    String dwbm = scci.getSubCompanyCode(companyId);

    // 获取API配置
    RecordSet rs = new RecordSet();
    Map<String, String> billconfig = LCUtil.getLCBillTypeInfo(billtype, rs);
    api = billconfig.get("api_list");

    // 先做一次去重(不清空表,做增量同步)
//    deduplicateDatabase(rs, tablename);

    // 构建请求参数 - 分页增量拉取
    JSONObject paramJson = new JSONObject();
    paramJson.put("dwbh", dwbm);
    paramJson.put("pagesize", "10000");
    paramJson.put("page", "1");

    // 执行增量同步
    JSONObject syncResult = syncData(paramJson, rs, api, tablename, sdf);
    result.put("status", 1);
    result.put("data", syncResult);
    result.put("msg", "增量同步采购结算单成功");
    // 仅保留核心关心内容(差异单据编号billnum、新增单据编号billnum)在顶层返回,便于前端直接消费
    if (syncResult.containsKey("diffBillnums")) {
        result.put("diffBillnums", syncResult.getJSONArray("diffBillnums"));
    }
    if (syncResult.containsKey("newBillnums")) {
        result.put("newBillnums", syncResult.getJSONArray("newBillnums"));
    }
    // 返回每条更新的具体字段变化(字段名、旧值、新值)
    if (syncResult.containsKey("updatedDetails")) {
        result.put("updatedDetails", syncResult.getJSONArray("updatedDetails"));
    }

    // 添加详细的同步信息
    if (syncResult.containsKey("updateInfo")) {
        result.put("updateInfo", syncResult.getString("updateInfo"));
    }
    if (syncResult.containsKey("updateError")) {
        result.put("updateError", syncResult.getString("updateError"));
    }
    // 添加失败记录信息
    if (syncResult.containsKey("failedRecords")) {
        result.put("failedRecords", syncResult.getJSONArray("failedRecords"));
    }
    if (syncResult.containsKey("failCount")) {
        result.put("failCount", syncResult.getInteger("failCount"));
    }

} catch (Exception e) {
    e.printStackTrace();
    result.put("status", 0);
    result.put("msg", "读取浪潮采购结算单异常:" + e.getMessage());
    result.put("errorDetail", e.toString());
}

// 输出JSON结果
out.print(result.toJSONString());

/**
 * 同步数据方法
 */
%>

<%!
/**
 * 数据库去重方法 - 删除重复数据,保留时间最新的一条
 */
public void deduplicateDatabase(RecordSet rs, String tablename) {
    try {
        String sql = "DELETE t1 FROM " + tablename + " t1 " +
                    "INNER JOIN " + tablename + " t2 " +
                    "WHERE t1.id < t2.id " +
                    "AND t1.cgjsdid = t2.cgjsdid " +
                    "AND t1.billnum = t2.billnum " +
                    "AND t1.WLDW = t2.WLDW " +
                    "AND t1.DEPARTMENTID = t2.DEPARTMENTID " +
                    "AND t1.contractid = t2.contractid " +
                    "AND t1.XMID = t2.XMID " +
                    "AND t1.Kxxz = t2.Kxxz " +
                    "AND t1.amount = t2.amount " +
                    "AND t1.Billdate = t2.Billdate";
        rs.execute(sql);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 数值字段名单:这些字段按两位小数口径进行比较与存储
private static final Set<String> NUMERIC_FIELDS = new HashSet<>(Arrays.asList(
        "amount",
        "foreigncurrency",
        "clearamount",
        "clearforeigncurrency",
        "balanceamount",
        "balanceforeigncurrency",
        "availableamount",
        // "availableforeigncurrency",  // 表中不存在此字段
        // 添加其他可能的数值字段
        "original_clearamount",
        "original_clearforeigncurrency",
        "original_balanceamount",
        "original_balanceforeigncurrency",
        "price",
        "unitprice",
        "quantity",
        "taxrate",
        "taxamount",
        "totalmoney",
        "netamount",
        "amountnotax"
));

// 规范化字符串:去首尾空格;null按空串处理
private static String norm(String v) {
    if (v == null) return "";
    return v.trim();
}

// 字符串等价:规范化后比较;空串与null等价
private static boolean equalNormalized(String a, String b) {
    return norm(a).equals(norm(b));
}

// 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
private static boolean equalNumeric(String a, String b) {
    String sa = norm(a);
    String sb = norm(b);
    if (sa.isEmpty() && sb.isEmpty()) return true;
    if (sa.isEmpty() || sb.isEmpty()) return false;
    try {
        BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
        BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
        return ba.compareTo(bb) == 0;
    } catch (Exception ignore) {
        return sa.equals(sb);
    }
}

// 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
private static String formatTo2Decimals(String v) {
    String s = norm(v);
    if (s.isEmpty()) return s;
    try {
        return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
    } catch (Exception ignore) {
        return s;
    }
}

// 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
public boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return true; // 无法获取字段信息,默认认为有变更
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                System.out.println("[字段差异] " + columnName + ": 数据库值='" + dbValue + "', 接口值='" + newValue + "'");
                return true; // 发现差异
            }
        }
        return false; // 所有字段都相同
    } catch (Exception e) {
        e.printStackTrace();
        // 出现异常时,默认认为有变更,以保证数据一致性
        return true;
    }
}

// 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
public JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
    JSONArray changes = new JSONArray();
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return changes; // 无法获取字段信息
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                JSONObject diff = new JSONObject();
                diff.put("field", columnName);
                diff.put("oldValue", dbValue);
                diff.put("newValue", newValue);
                changes.add(diff);
            }
        }
    } catch (Exception e) {
        // 忽略,返回当前已收集到的差异
    }
    return changes;
}

// 先对比后插入/更新:按 (cgjsdid,billnum) 定位;差异则按原id更新;不存在则插入
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename) {
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", data.get("cgjsdid"), data.get("billnum"));
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                // 仅更新差异字段
                JSONArray changes = collectDifferences(check, data);
                if (changes != null && changes.size() > 0) {
                    Map<String, String> updateMap = new HashMap<>();
                    updateMap.put("id", String.valueOf(existingId));
                    for (int i = 0; i < changes.size(); i++) {
                        JSONObject c = changes.getJSONObject(i);
                        String field = Util.null2String(c.getString("field"));
                        String newValue = Util.null2String(c.getString("newValue"));
                        updateMap.put(field, newValue);
                    }
                    ModeUtil.saveModeData(tablename, 1, rs, updateMap, null, "id", true);
                }
                System.out.println("[" + tablename + "] 已更新(仅差异字段),id=" + existingId + ", cgjsdid=" + data.get("cgjsdid") + ", billnum=" + data.get("billnum"));
                return 1;
            }
            System.out.println("[" + tablename + "] 无变化,跳过,id=" + existingId + ", cgjsdid=" + data.get("cgjsdid") + ", billnum=" + data.get("billnum"));
            return 0;
        }
        ModeUtil.saveModeData(tablename, 1, rs, data, null, "cgjsdid", true);
        System.out.println("[" + tablename + "] 已新增,cgjsdid=" + data.get("cgjsdid") + ", billnum=" + data.get("billnum"));
        return 2;
    } catch (Exception e) {
        try {
            ModeUtil.saveModeData(tablename, 1, rs, data, null, "cgjsdid", true);
            System.out.println("[" + tablename + "] 异常后重试已保存(按cgjsdid插入/更新),cgjsdid=" + data.get("cgjsdid") + ", billnum=" + data.get("billnum") + ", err=" + e.getMessage());
            return 1;
        } catch (Exception ignore) {
            System.out.println("[" + tablename + "] 保存失败并跳过,cgjsdid=" + data.get("cgjsdid") + ", billnum=" + data.get("billnum") + ", err=" + e.getMessage());
            return 0;
        }
    }
}

// 重载:在更新时将差异字段加入 updateDetails 收集器
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename, JSONArray updateDetails) {
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", data.get("cgjsdid"), data.get("billnum"));
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                // 收集字段差异
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", data.get("billnum"));
                    detail.put("cgjsdid", data.get("cgjsdid"));
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                // 仅更新差异字段(使用系统管理员权限重构)
                if (changes != null && changes.size() > 0) {
                    Map<String, String> updateMap = new HashMap<>();
                    updateMap.put("id", String.valueOf(existingId));
                    for (int i = 0; i < changes.size(); i++) {
                        JSONObject c = changes.getJSONObject(i);
                        String field = Util.null2String(c.getString("field"));
                        String newValue = Util.null2String(c.getString("newValue"));
                        updateMap.put(field, newValue);
                    }
                    ModeUtil.saveModeData(tablename, 1, rs, updateMap, null, "id", true);
                }
                System.out.println("[" + tablename + "] 已更新(仅差异字段),id=" + existingId + ", billnum=" + data.get("billnum"));
                return 1;
            }
            System.out.println("[" + tablename + "] 无变化,跳过,id=" + existingId + ", billnum=" + data.get("billnum"));
            return 0;
        }
        ModeUtil.saveModeData(tablename, 1, rs, data, null, "cgjsdid", true);
        System.out.println("[" + tablename + "] 已新增,billnum=" + data.get("billnum"));
        return 2;
    } catch (Exception e) {
        try {
            ModeUtil.saveModeData(tablename, 1, rs, data, null, "cgjsdid", true);
            System.out.println("[" + tablename + "] 异常后重试已保存(按cgjsdid插入/更新),billnum=" + data.get("billnum") + ", err=" + e.getMessage());
            return 1;
        } catch (Exception ignore) {
            System.out.println("[" + tablename + "] 保存失败并跳过,billnum=" + data.get("billnum") + ", err=" + e.getMessage());
            return 0;
        }
    }
}

// 重载:同时支持 updateDetails 和 failedRecords 收集器,并添加调试日志
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename, JSONArray updateDetails, JSONArray failedRecords) {
    String billnum = Util.null2String(data.get("billnum"));
    String cgjsdid = Util.null2String(data.get("cgjsdid"));
    
    // 创建调试日志对象
    JSONArray debugLogs = new JSONArray();
    
    System.out.println("[权限重构] 使用系统管理员(userid=1)进行权限重构");
    
    try {
        debugLogs.add("[开始] billnum=" + billnum + ", cgjsdid=" + cgjsdid);
        System.out.println("[数据检查开始] billnum=" + billnum + ", cgjsdid=" + cgjsdid);
        
        RecordSet check = new RecordSet();
        String checkSql = "SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?";
        System.out.println("[执行查询] SQL: " + checkSql + ", 参数: cgjsdid=" + cgjsdid + ", billnum=" + billnum);
        check.executeQuery(checkSql, cgjsdid, billnum);
        
        if (check.next()) {
            int existingId = check.getInt("id");
            System.out.println("[数据已存在] id=" + existingId + ", billnum=" + billnum + ", 开始比较字段...");
            
            if (hasDataChanged(check, data)) {
                System.out.println("[检测到变化] billnum=" + billnum + ", 收集差异字段...");
                // 收集字段差异
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", billnum);
                    detail.put("cgjsdid", cgjsdid);
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                // 仅更新差异字段
                if (changes != null && changes.size() > 0) {
                    System.out.println("[准备更新] billnum=" + billnum + ", 差异字段数=" + changes.size());
                    Map<String, String> updateMap = new HashMap<>();
                    updateMap.put("id", String.valueOf(existingId));
                    for (int i = 0; i < changes.size(); i++) {
                        JSONObject c = changes.getJSONObject(i);
                        String field = Util.null2String(c.getString("field"));
                        String newValue = Util.null2String(c.getString("newValue"));
                        updateMap.put(field, newValue);
                        System.out.println("[更新字段] " + field + ": " + c.getString("oldValue") + " -> " + newValue);
                    }
                    
                    try {
                        // 使用 ModeUtil.saveModeData 更新并自动重构权限
                        RecordSet updateRs = new RecordSet();
                        boolean updateResult = ModeUtil.saveModeData(tablename, 1, updateRs, updateMap, null, "id", true);
                        System.out.println("[更新成功] billnum=" + billnum + ", id=" + existingId + ", 更新返回值=" + updateResult + " (已自动重构权限)");
                    } catch (Exception saveEx) {
                        System.out.println("[!!!更新失败!!!] billnum=" + billnum + ", 错误: " + saveEx.getMessage());
                        saveEx.printStackTrace();
                        
                        // 记录失败详情
                        if (failedRecords != null) {
                            JSONObject failedRecord = new JSONObject();
                            failedRecord.put("billnum", billnum);
                            failedRecord.put("cgjsdid", cgjsdid);
                            failedRecord.put("operation", "update");
                            failedRecord.put("errorMessage", saveEx.getMessage());
                            failedRecord.put("errorType", saveEx.getClass().getName());
                            failedRecords.add(failedRecord);
                        }
                        return -1;
                    }
                }
                System.out.println("[" + tablename + "] 已更新(仅差异字段),id=" + existingId + ", billnum=" + billnum);
                return 1;
            }
            System.out.println("[" + tablename + "] 无变化,跳过,id=" + existingId + ", billnum=" + billnum);
            return 0;
        }
        
        // 数据不存在,执行新增
        System.out.println("[数据不存在] billnum=" + billnum + ", 准备新增...");
        System.out.println("[新增数据详情] cgjsdid=" + cgjsdid + ", billnum=" + billnum);
        System.out.println("[关键字段] WLDW=" + data.get("WLDW") + ", DEPARTMENTID=" + data.get("DEPARTMENTID") + ", amount=" + data.get("amount"));
        
        // 打印所有要保存的字段
        System.out.println("[准备插入的完整数据] 共 " + data.size() + " 个字段:");
        for (Map.Entry<String, String> entry : data.entrySet()) {
            System.out.println("  " + entry.getKey() + " = " + entry.getValue());
        }
        
        try {
            System.out.println("[调用 ModeUtil.saveModeData] 表名=" + tablename + ", modeid=1, 主键字段=cgjsdid");
            
            // 尝试获取保存前的记录数
            RecordSet countBefore = new RecordSet();
            countBefore.executeQuery("SELECT COUNT(*) as cnt FROM " + tablename);
            int countBeforeInsert = 0;
            if (countBefore.next()) {
                countBeforeInsert = countBefore.getInt("cnt");
            }
            System.out.println("[保存前记录数] " + countBeforeInsert);
            
            // 关键检查:看数据库中是否已存在该 cgjsdid
            RecordSet checkCgjsdid = new RecordSet();
            checkCgjsdid.executeQuery("SELECT id, billnum FROM " + tablename + " WHERE cgjsdid = ?", cgjsdid);
            if (checkCgjsdid.next()) {
                int existingId = checkCgjsdid.getInt("id");
                String existingBillnum = checkCgjsdid.getString("billnum");
                System.out.println("[!!!警告!!!] 数据库中已存在 cgjsdid=" + cgjsdid + ", id=" + existingId + ", billnum=" + existingBillnum);
                System.out.println("[!!!冲突!!!] 当前要插入的 billnum=" + billnum + ", 但数据库中该 cgjsdid 对应的 billnum=" + existingBillnum);
                
                if (!billnum.equals(existingBillnum)) {
                    System.out.println("[!!!严重错误!!!] cgjsdid 相同但 billnum 不同,这是数据异常!");
                    if (failedRecords != null) {
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", billnum);
                        failedRecord.put("cgjsdid", cgjsdid);
                        failedRecord.put("operation", "insert_cgjsdid_conflict");
                        failedRecord.put("errorMessage", "cgjsdid 在数据库中已存在(id=" + existingId + ", billnum=" + existingBillnum + "),但 billnum 不匹配");
                        failedRecord.put("existingId", existingId);
                        failedRecord.put("existingBillnum", existingBillnum);
                        failedRecord.put("debugInfo", "数据库中已存在相同cgjsdid但不同billnum的记录");
                        failedRecords.add(failedRecord);
                    }
                    return -1;
                } else {
                    System.out.println("[提示] cgjsdid 和 billnum 都匹配,记录已存在,跳过插入");
                    return 0;
                }
            }
            
            System.out.println("[确认] cgjsdid 在数据库中不存在,可以新增");
            
            // 过滤掉不存在的字段,然后使用 ModeUtil.saveModeData 自动处理权限
            System.out.println("[准备使用ModeUtil.saveModeData插入并重构权限]");
            
            try {
                // 获取表的所有列名(避免插入不存在的字段)
                Set<String> tableColumns = new HashSet<>();
                RecordSet columnRs = new RecordSet();
                columnRs.executeQuery("SHOW COLUMNS FROM " + tablename);
                while (columnRs.next()) {
                    String columnName = columnRs.getString("Field");
                    tableColumns.add(columnName.toLowerCase());
                }
                System.out.println("[表字段数量] " + tableColumns.size());
                
                // 创建过滤后的数据(只包含表中存在的字段)
                Map<String, String> filteredData = new HashMap<>();
                int skippedCount = 0;
                
                for (Map.Entry<String, String> entry : data.entrySet()) {
                    String fieldName = entry.getKey();
                    String fieldValue = entry.getValue();
                    
                    // 检查字段是否存在于表中
                    if (!tableColumns.contains(fieldName.toLowerCase())) {
                        System.out.println("[跳过不存在的字段] " + fieldName);
                        skippedCount++;
                        continue;
                    }
                    
                    filteredData.put(fieldName, fieldValue);
                }
                
                System.out.println("[跳过字段总数] " + skippedCount);
                System.out.println("[有效字段数量] " + filteredData.size());
                
                // 使用 ModeUtil.saveModeData,自动处理权限重构(buildRights=true)
                RecordSet saveRs = new RecordSet();
                boolean saveResult = ModeUtil.saveModeData(tablename, 1, saveRs, filteredData, null, "cgjsdid", true);
                System.out.println("[ModeUtil.saveModeData 执行结果] " + saveResult);
                
                if (!saveResult) {
                    System.out.println("[!!!插入失败!!!] ModeUtil.saveModeData 返回 false");
                    if (failedRecords != null) {
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", billnum);
                        failedRecord.put("cgjsdid", cgjsdid);
                        failedRecord.put("operation", "insert_modeutil_failed");
                        failedRecord.put("errorMessage", "ModeUtil.saveModeData 返回 false,可能是SQL执行失败");
                        failedRecord.put("filteredFieldCount", filteredData.size());
                        failedRecords.add(failedRecord);
                    }
                    return -1;
                }
                
                System.out.println("[插入成功] 使用ModeUtil.saveModeData插入并已自动重构权限,billnum=" + billnum);
                
            } catch (Exception insertEx) {
                System.out.println("[!!!插入异常!!!] " + insertEx.getMessage());
                insertEx.printStackTrace();
                
                if (failedRecords != null) {
                    JSONObject failedRecord = new JSONObject();
                    failedRecord.put("billnum", billnum);
                    failedRecord.put("cgjsdid", cgjsdid);
                    failedRecord.put("operation", "insert_native_sql_exception");
                    failedRecord.put("errorMessage", "原生SQL插入异常: " + insertEx.getMessage());
                    failedRecord.put("errorType", insertEx.getClass().getName());
                    failedRecords.add(failedRecord);
                }
                return -1;
            }
            
            // 检查保存后的记录数
            RecordSet countAfter = new RecordSet();
            countAfter.executeQuery("SELECT COUNT(*) as cnt FROM " + tablename);
            int countAfterInsert = 0;
            if (countAfter.next()) {
                countAfterInsert = countAfter.getInt("cnt");
            }
            System.out.println("[保存后记录数] " + countAfterInsert + " (增加了 " + (countAfterInsert - countBeforeInsert) + " 条)");
            
            // 验证是否真的插入成功
            RecordSet verify = new RecordSet();
            String verifySql = "SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?";
            System.out.println("[验证查询SQL] " + verifySql);
            System.out.println("[验证查询参数] cgjsdid=" + cgjsdid + ", billnum=" + billnum);
            verify.executeQuery(verifySql, cgjsdid, billnum);
            
            if (verify.next()) {
                int newId = verify.getInt("id");
                System.out.println("[验证成功] 新记录已插入,id=" + newId + ", billnum=" + billnum);
            } else {
                System.out.println("[!!!验证失败!!!] 新记录未找到,billnum=" + billnum);
                
                // 尝试只用cgjsdid查询
                RecordSet verify2 = new RecordSet();
                verify2.executeQuery("SELECT id, billnum FROM " + tablename + " WHERE cgjsdid = ?", cgjsdid);
                if (verify2.next()) {
                    System.out.println("[!!!发现问题!!!] 用cgjsdid能找到记录: id=" + verify2.getInt("id") + ", billnum=" + verify2.getString("billnum"));
                } else {
                    System.out.println("[确认] 用cgjsdid也找不到记录");
                }
                
                // 尝试只用billnum查询
                RecordSet verify3 = new RecordSet();
                verify3.executeQuery("SELECT id, cgjsdid FROM " + tablename + " WHERE billnum = ?", billnum);
                if (verify3.next()) {
                    System.out.println("[!!!发现问题!!!] 用billnum能找到记录: id=" + verify3.getInt("id") + ", cgjsdid=" + verify3.getString("cgjsdid"));
                } else {
                    System.out.println("[确认] 用billnum也找不到记录");
                }
                
                // 查询最后插入的记录
                RecordSet lastInsert = new RecordSet();
                lastInsert.executeQuery("SELECT id, cgjsdid, billnum FROM " + tablename + " ORDER BY id DESC LIMIT 1");
                if (lastInsert.next()) {
                    System.out.println("[最后一条记录] id=" + lastInsert.getInt("id") + ", cgjsdid=" + lastInsert.getString("cgjsdid") + ", billnum=" + lastInsert.getString("billnum"));
                }
                
                if (failedRecords != null) {
                    JSONObject failedRecord = new JSONObject();
                    failedRecord.put("billnum", billnum);
                    failedRecord.put("cgjsdid", cgjsdid);
                    failedRecord.put("operation", "insert_verify_failed");
                    failedRecord.put("errorMessage", "保存后验证失败,数据库中未找到该记录。记录数变化:" + (countAfterInsert - countBeforeInsert));
                    failedRecord.put("allFields", data.toString());
                    failedRecords.add(failedRecord);
                }
                return -1;
            }
        } catch (Exception saveEx) {
            System.out.println("[!!!新增失败!!!] billnum=" + billnum + ", 错误: " + saveEx.getMessage());
            System.out.println("[完整堆栈]");
            saveEx.printStackTrace();
            
            // 记录失败详情
            if (failedRecords != null) {
                JSONObject failedRecord = new JSONObject();
                failedRecord.put("billnum", billnum);
                failedRecord.put("cgjsdid", cgjsdid);
                failedRecord.put("operation", "insert");
                failedRecord.put("errorMessage", saveEx.getMessage());
                failedRecord.put("errorType", saveEx.getClass().getName());
                failedRecords.add(failedRecord);
            }
            return -1;
        }
        
        System.out.println("[" + tablename + "] 已新增,billnum=" + billnum);
        return 2;
    } catch (Exception e) {
        System.out.println("[!!!处理异常!!!] billnum=" + billnum + ", cgjsdid=" + cgjsdid);
        System.out.println("[异常信息] " + e.getMessage());
        e.printStackTrace();
        
        try {
            System.out.println("[尝试重试保存] billnum=" + billnum);
            ModeUtil.saveModeData(tablename, 1, rs, data, null, "cgjsdid", true);
            System.out.println("[" + tablename + "] 异常后重试已保存(按cgjsdid插入/更新),billnum=" + billnum);
            return 1;
        } catch (Exception retryEx) {
            System.out.println("[!!!重试也失败!!!] billnum=" + billnum + ", 错误: " + retryEx.getMessage());
            retryEx.printStackTrace();
            
            if (failedRecords != null) {
                JSONObject failedRecord = new JSONObject();
                failedRecord.put("billnum", billnum);
                failedRecord.put("cgjsdid", cgjsdid);
                failedRecord.put("operation", "retry_failed");
                failedRecord.put("errorMessage", "首次失败: " + e.getMessage() + "; 重试失败: " + retryEx.getMessage());
                failedRecord.put("errorType", retryEx.getClass().getName());
                failedRecords.add(failedRecord);
            }
            
            System.out.println("[" + tablename + "] 保存失败并跳过,billnum=" + billnum);
            return -1;
        }
    }
}

public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
    JSONObject result = new JSONObject();
    int totalCount = 0;
    int newCount = 0;
    int updateCount = 0;
    int skipCount = 0;
    int failCount = 0;
    // 收集差异更新与新增的单据编号(billnum)
    JSONArray diffBillnums = new JSONArray();
    JSONArray newBillnums = new JSONArray();
    // 收集每条更新的字段变更详情
    JSONArray updatedDetails = new JSONArray();
    // 收集保存失败的记录详情
    JSONArray failedRecords = new JSONArray();
    // 收集调试日志(仅记录失败记录的详细过程)
    JSONArray debugLogs = new JSONArray();
    try {
        int page = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
        String pageSize = Util.null2String(param.getString("pagesize"));
        if ("".equals(pageSize)) pageSize = "10000";

        while (true) {
            param.put("page", String.valueOf(page));
            param.put("pagesize", pageSize);

            JSONObject rjson = LCUtil.doPost(api, param);
            result = rjson;

            // 结束条件
            if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                System.out.println("分页已结束,code=3003,停止请求。最后页码:" + page);
                break;
            }
            if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                System.out.println("接口返回非成功code:" + rjson.getString("code") + ",页码:" + page + ",停止。");
                break;
            }

            JSONArray datas = rjson.getJSONArray("data");
            int pageCount = (datas == null) ? 0 : datas.size();
            totalCount += pageCount;
            System.out.println("第 " + page + " 页获取到 " + pageCount + " 条数据,开始增量同步...");

            if (datas != null) {
                for (int x = 0; x < datas.size(); x++) {
                    JSONObject item = datas.getJSONObject(x);
                    Map<String, String> data = new HashMap<>();
                    String currentBillnum = "";
                    String currentCgjsdid = "";
                    try {
                        // 映射字段
                        currentCgjsdid = Util.null2String(item.get("ID"));
                        currentBillnum = Util.null2String(item.get("BILLNUM"));
                        
                        System.out.println("========================================");
                        System.out.println("[开始处理] 单据编号: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        
                        data.put("cgjsdid", currentCgjsdid);
                        data.put("billnum", currentBillnum);
                        data.put("WLDW", Util.null2String(item.get("WLDW")));
                        data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                        data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                        data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                        data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                        data.put("BMBH", Util.null2String(item.get("BMBH")));
                        data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                        data.put("summary", Util.null2String(item.get("SUMMARY")));
                        data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                        data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                        data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                        data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                        data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                        data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                        data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                        data.put("dwbh", Util.null2String(item.get("DWBH")));
                        data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                        data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                        data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                        data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                        data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                        data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                        data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                        data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                        data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                        data.put("XMID", Util.null2String(item.get("XMID")));
                        data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                        data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                        data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                        data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                        if (item.get("BILLDATE") != null) {
                            data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
                        }
                        data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                        data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                        // 金额字段:统一两位小数
                        data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                        data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                        data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                        data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                        data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                        data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                        data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                        data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                        // availableforeigncurrency 字段在数据库表中不存在,已跳过
                        // data.put("availableforeigncurrency", formatTo2Decimals(item.getString("AVAILABLEFOREIGNCURRENCY")));

                        System.out.println("[字段映射完成] billnum: " + currentBillnum + ", 准备保存...");
                        
                        int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords);
                        if (action == 1) {
                            updateCount++;
                            diffBillnums.add(data.get("billnum"));
                        } else if (action == 2) {
                            newCount++;
                            newBillnums.add(data.get("billnum"));
                        } else if (action == -1) {
                            failCount++;
                            System.out.println("[保存失败] billnum: " + currentBillnum);
                        } else {
                            skipCount++;
                        }
                    } catch (Exception ee) {
                        failCount++;
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        System.out.println("[严重错误] billnum: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        System.out.println("[错误信息] " + ee.getMessage());
                        System.out.println("[堆栈跟踪开始]");
                        ee.printStackTrace();
                        System.out.println("[堆栈跟踪结束]");
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        
                        // 记录失败详情
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", currentBillnum);
                        failedRecord.put("cgjsdid", currentCgjsdid);
                        failedRecord.put("errorMessage", ee.getMessage());
                        failedRecord.put("errorType", ee.getClass().getName());
                        failedRecords.add(failedRecord);
                    }
                }
            }

            // 每页结束后可执行一次关联更新
            try {
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
            } catch (Exception ex) {
                ex.printStackTrace();
                result.put("updateError", "第" + page + "页 关联字段更新异常:" + ex.getMessage());
            }

            System.out.println("第 " + page + " 页处理完成,累计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount);
            page++;
        }

        String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 分页处理完成";
        result.put("updateInfo", syncInfo);
        System.out.println(syncInfo);
        System.out.println("========================================");
        System.out.println("同步完成汇总:");
        System.out.println("总数: " + totalCount);
        System.out.println("新增: " + newCount);
        System.out.println("更新: " + updateCount);
        System.out.println("跳过: " + skipCount);
        System.out.println("失败: " + failCount);
        if (failCount > 0) {
            System.out.println("失败记录详情:");
            for (int i = 0; i < failedRecords.size(); i++) {
                JSONObject failed = failedRecords.getJSONObject(i);
                System.out.println("  [" + (i+1) + "] billnum=" + failed.getString("billnum") + 
                                 ", cgjsdid=" + failed.getString("cgjsdid") +
                                 ", operation=" + failed.getString("operation") +
                                 ", error=" + failed.getString("errorMessage"));
            }
        }
        System.out.println("========================================");
        
        // 将差异与新增的单据编号(billnum)放入结果
        result.put("diffBillnums", diffBillnums);
        result.put("newBillnums", newBillnums);
        // 返回每条更新的具体字段变化
        result.put("updatedDetails", updatedDetails);
        // 返回失败记录
        if (failCount > 0) {
            result.put("failedRecords", failedRecords);
            result.put("failCount", failCount);
        }
        // 返回调试日志
        if (debugLogs.size() > 0) {
            result.put("debugLogs", debugLogs);
        }
    } catch (Exception e) {
        e.printStackTrace();
        result.put("error", "增量同步数据异常: " + e.getMessage());
    }
    return result;
}

/**
 * 调试辅助:打印 ModeUtil.saveModeData 可能使用的表信息
 */
public String debugTableInfo(String tablename, RecordSet rs) {
    StringBuilder info = new StringBuilder();
    try {
        // 获取表的列信息
        rs.executeQuery("SHOW COLUMNS FROM " + tablename);
        info.append("[表结构信息] ").append(tablename).append(":\n");
        while (rs.next()) {
            String field = rs.getString("Field");
            String type = rs.getString("Type");
            String nullAllowed = rs.getString("Null");
            String key = rs.getString("Key");
            String defaultVal = rs.getString("Default");
            info.append("  ").append(field).append(" ").append(type);
            if ("NO".equals(nullAllowed)) info.append(" NOT NULL");
            if ("PRI".equals(key)) info.append(" PRIMARY KEY");
            if (defaultVal != null) info.append(" DEFAULT ").append(defaultVal);
            info.append("\n");
        }
    } catch (Exception e) {
        info.append("获取表结构失败: ").append(e.getMessage());
    }
    return info.toString();
}
%>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONArray" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="weaver.general.Util" %>
<%@ page import="dev.soft.lb.util.CommonUtil" %>
<%@ page import="dev.soft.lb.util.LCUtil" %>
<%@ page import="weaver.general.TimeUtil" %>
<%@ page import="weaver.conn.RecordSet" %>
<%@ page import="weaver.hrm.company.SubCompanyComInfo" %>
<%@ page import="weaver.formmode.setup.ModeRightInfo" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.math.BigDecimal" %>
<%@ page import="java.math.RoundingMode" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.UUID" %>

<%
// 设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
// 允许跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

// 定义常量
final String companyId = "66";
final String billtype = "12";
String api = "";
final String tablename = "uf_cgjsd";
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 创建返回结果
JSONObject result = new JSONObject();

try {
    // 获取单位编码
    SubCompanyComInfo scci = new SubCompanyComInfo();
    String dwbm = scci.getSubCompanyCode(companyId);

    // 获取API配置
    RecordSet rs = new RecordSet();
    Map<String, String> billconfig = LCUtil.getLCBillTypeInfo(billtype, rs);
    api = billconfig.get("api_list");

    // 先做一次去重(不清空表,做增量同步)
//    deduplicateDatabase(rs, tablename);

    // 构建请求参数 - 分页增量拉取
    JSONObject paramJson = new JSONObject();
    paramJson.put("dwbh", dwbm);
    paramJson.put("pagesize", "10000");
    paramJson.put("page", "1");

    // 执行增量同步
    JSONObject syncResult = syncData(paramJson, rs, api, tablename, sdf);
    result.put("status", 1);
    result.put("data", syncResult);
    result.put("msg", "增量同步采购结算单成功");
    // 仅保留核心关心内容(差异单据编号billnum、新增单据编号billnum)在顶层返回,便于前端直接消费
    if (syncResult.containsKey("diffBillnums")) {
        result.put("diffBillnums", syncResult.getJSONArray("diffBillnums"));
    }
    if (syncResult.containsKey("newBillnums")) {
        result.put("newBillnums", syncResult.getJSONArray("newBillnums"));
    }
    // 返回每条更新的具体字段变化(字段名、旧值、新值)
    if (syncResult.containsKey("updatedDetails")) {
        result.put("updatedDetails", syncResult.getJSONArray("updatedDetails"));
    }

    // 添加详细的同步信息
    if (syncResult.containsKey("updateInfo")) {
        result.put("updateInfo", syncResult.getString("updateInfo"));
    }
    if (syncResult.containsKey("updateError")) {
        result.put("updateError", syncResult.getString("updateError"));
    }
    // 添加失败记录信息
    if (syncResult.containsKey("failedRecords")) {
        result.put("failedRecords", syncResult.getJSONArray("failedRecords"));
    }
    if (syncResult.containsKey("failCount")) {
        result.put("failCount", syncResult.getInteger("failCount"));
    }

} catch (Exception e) {
    e.printStackTrace();
    result.put("status", 0);
    result.put("msg", "读取浪潮采购结算单异常:" + e.getMessage());
    result.put("errorDetail", e.toString());
}

// 输出JSON结果
out.print(result.toJSONString());

/**
 * 同步数据方法
 */
%>

<%!
/**
 * 数据库去重方法 - 删除重复数据,保留时间最新的一条
 */
public void deduplicateDatabase(RecordSet rs, String tablename) {
    try {
        String sql = "DELETE t1 FROM " + tablename + " t1 " +
                    "INNER JOIN " + tablename + " t2 " +
                    "WHERE t1.id < t2.id " +
                    "AND t1.cgjsdid = t2.cgjsdid " +
                    "AND t1.billnum = t2.billnum " +
                    "AND t1.WLDW = t2.WLDW " +
                    "AND t1.DEPARTMENTID = t2.DEPARTMENTID " +
                    "AND t1.contractid = t2.contractid " +
                    "AND t1.XMID = t2.XMID " +
                    "AND t1.Kxxz = t2.Kxxz " +
                    "AND t1.amount = t2.amount " +
                    "AND t1.Billdate = t2.Billdate";
        rs.execute(sql);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 数值字段名单:这些字段按两位小数口径进行比较与存储
private static final Set<String> NUMERIC_FIELDS = new HashSet<>(Arrays.asList(
        "amount",
        "foreigncurrency",
        "clearamount",
        "clearforeigncurrency",
        "balanceamount",
        "balanceforeigncurrency",
        "availableamount",
        // "availableforeigncurrency",  // 表中不存在此字段
        // 添加其他可能的数值字段
        "original_clearamount",
        "original_clearforeigncurrency",
        "original_balanceamount",
        "original_balanceforeigncurrency",
        "price",
        "unitprice",
        "quantity",
        "taxrate",
        "taxamount",
        "totalmoney",
        "netamount",
        "amountnotax"
));

// 规范化字符串:去首尾空格;null按空串处理
private static String norm(String v) {
    if (v == null) return "";
    return v.trim();
}

// 字符串等价:规范化后比较;空串与null等价
private static boolean equalNormalized(String a, String b) {
    return norm(a).equals(norm(b));
}

// 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
private static boolean equalNumeric(String a, String b) {
    String sa = norm(a);
    String sb = norm(b);
    if (sa.isEmpty() && sb.isEmpty()) return true;
    if (sa.isEmpty() || sb.isEmpty()) return false;
    try {
        BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
        BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
        return ba.compareTo(bb) == 0;
    } catch (Exception ignore) {
        return sa.equals(sb);
    }
}

// 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
private static String formatTo2Decimals(String v) {
    String s = norm(v);
    if (s.isEmpty()) return s;
    try {
        return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
    } catch (Exception ignore) {
        return s;
    }
}

// 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
public boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return true; // 无法获取字段信息,默认认为有变更
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                System.out.println("[字段差异] " + columnName + ": 数据库值='" + dbValue + "', 接口值='" + newValue + "'");
                return true; // 发现差异
            }
        }
        return false; // 所有字段都相同
    } catch (Exception e) {
        e.printStackTrace();
        // 出现异常时,默认认为有变更,以保证数据一致性
        return true;
    }
}

// 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
public JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
    JSONArray changes = new JSONArray();
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return changes; // 无法获取字段信息
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                JSONObject diff = new JSONObject();
                diff.put("field", columnName);
                diff.put("oldValue", dbValue);
                diff.put("newValue", newValue);
                changes.add(diff);
            }
        }
    } catch (Exception e) {
        // 忽略,返回当前已收集到的差异
    }
    return changes;
}

// 先对比后插入/更新:按 (cgjsdid,billnum) 定位;差异则按原id更新;不存在则插入
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename,
                                         JSONArray updateDetails, JSONArray failedRecords, Set<Integer> affectedIds) {
    String billnum = Util.null2String(data.get("billnum"));
    String cgjsdid = Util.null2String(data.get("cgjsdid"));
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", billnum);
                    detail.put("cgjsdid", cgjsdid);
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                if (changes != null && changes.size() > 0) {
                    if (applyUpdate(existingId, changes, tablename)) {
                        if (affectedIds != null) {
                            affectedIds.add(existingId);
                        }
                        return 1;
                    }
                    throw new RuntimeException("更新采购结算单失败");
                }
            }
            return 0;
        }

        Integer newId = insertRecord(data, tablename);
        if (newId != null && newId > 0) {
            if (affectedIds != null) {
                affectedIds.add(newId);
            }
            return 2;
        }
        throw new RuntimeException("新增采购结算单失败");
    } catch (Exception e) {
        if (failedRecords != null) {
            JSONObject failedRecord = new JSONObject();
            failedRecord.put("billnum", billnum);
            failedRecord.put("cgjsdid", cgjsdid);
            failedRecord.put("errorMessage", e.getMessage());
            failedRecord.put("operation", "upsert");
            failedRecords.add(failedRecord);
        }
        return -1;
    }
}

private boolean applyUpdate(int recordId, JSONArray changes, String tablename) {
    if (recordId <= 0 || changes == null || changes.isEmpty()) {
        return false;
    }
    StringBuilder setClause = new StringBuilder();
    for (int i = 0; i < changes.size(); i++) {
        JSONObject change = changes.getJSONObject(i);
        String field = Util.null2String(change.getString("field"));
        String value = Util.null2String(change.getString("newValue"));
        if (field.isEmpty()) {
            continue;
        }
        if (setClause.length() > 0) {
            setClause.append(",");
        }
        setClause.append(field).append("=").append(toSqlValue(value));
    }
    if (setClause.length() == 0) {
        return false;
    }
    String sql = "UPDATE " + tablename + " SET " + setClause + " WHERE id=" + recordId;
    RecordSet updateRs = new RecordSet();
    return updateRs.execute(sql);
}

private Integer insertRecord(Map<String, String> data, String tablename) {
    if (data == null || data.isEmpty()) {
        return null;
    }
    Map<String, String> insertMap = new HashMap<>(data);
    String now = TimeUtil.getCurrentTimeString();
    String createDate = now.substring(0, 10);
    String createTime = now.substring(11);
    insertMap.put("formmodeid", String.valueOf(getModeId(tablename)));
    insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
    insertMap.put("modedatacreatertype", "1");
    insertMap.put("modedatacreatedate", createDate);
    insertMap.put("modedatacreatetime", createTime);
    insertMap.put("modeuuid", UUID.randomUUID().toString());

    StringBuilder columns = new StringBuilder();
    StringBuilder values = new StringBuilder();
    for (Map.Entry<String, String> entry : insertMap.entrySet()) {
        if (columns.length() > 0) {
            columns.append(",");
            values.append(",");
        }
        columns.append(entry.getKey());
        values.append(toSqlValue(entry.getValue()));
    }

    RecordSet insertRs = new RecordSet();
    String sql = "INSERT INTO " + tablename + "(" + columns + ") VALUES(" + values + ")";
    if (!insertRs.execute(sql)) {
        return null;
    }
    RecordSet idRs = new RecordSet();
    idRs.executeQuery("SELECT id FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?",
            Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
    if (idRs.next()) {
        return idRs.getInt("id");
    }
    return null;
}

private String toSqlValue(String value) {
    String normalized = Util.null2String(value);
    if (normalized.isEmpty()) {
        return "null";
    }
    return "'" + normalized.replace("'", "''") + "'";
}

private void rebuildPermissions(Set<Integer> affectedIds, String tablename) {
    if (affectedIds == null || affectedIds.isEmpty()) {
        return;
    }
    try {
        int modeId = getModeId(tablename);
        if (modeId <= 0) {
            return;
    }
        ModeRightInfo rightInfo = new ModeRightInfo();
        rightInfo.setNewRight(false);
        rightInfo.init();
        rightInfo.setModeId(modeId);
        for (Integer billId : affectedIds) {
            if (billId == null || billId <= 0) {
                continue;
            }
            rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private int getModeId(String tablename) {
    try {
        return Util.getIntValue(CommonUtil.getModeIdByTableName(tablename));
    } catch (Exception e) {
        return 0;
    }
}

public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
    JSONObject result = new JSONObject();
    int totalCount = 0;
    int newCount = 0;
    int updateCount = 0;
    int skipCount = 0;
    int failCount = 0;
    // 收集差异更新与新增的单据编号(billnum)
    JSONArray diffBillnums = new JSONArray();
    JSONArray newBillnums = new JSONArray();
    // 收集每条更新的字段变更详情
    JSONArray updatedDetails = new JSONArray();
    // 收集保存失败的记录详情
    JSONArray failedRecords = new JSONArray();
    // 收集调试日志(仅记录失败记录的详细过程)
    JSONArray debugLogs = new JSONArray();
    try {
        int page = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
        String pageSize = Util.null2String(param.getString("pagesize"));
        if ("".equals(pageSize)) pageSize = "10000";

        while (true) {
            param.put("page", String.valueOf(page));
            param.put("pagesize", pageSize);

            JSONObject rjson = LCUtil.doPost(api, param);
            result = rjson;

            // 结束条件
            if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                System.out.println("分页已结束,code=3003,停止请求。最后页码:" + page);
                break;
            }
            if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                System.out.println("接口返回非成功code:" + rjson.getString("code") + ",页码:" + page + ",停止。");
                break;
            }

            JSONArray datas = rjson.getJSONArray("data");
            int pageCount = (datas == null) ? 0 : datas.size();
            totalCount += pageCount;
            System.out.println("第 " + page + " 页获取到 " + pageCount + " 条数据,开始增量同步...");

            if (datas != null) {
                for (int x = 0; x < datas.size(); x++) {
                    JSONObject item = datas.getJSONObject(x);
                    Map<String, String> data = new HashMap<>();
                    String currentBillnum = "";
                    String currentCgjsdid = "";
                    try {
                        // 映射字段
                        currentCgjsdid = Util.null2String(item.get("ID"));
                        currentBillnum = Util.null2String(item.get("BILLNUM"));
                        
                        System.out.println("========================================");
                        System.out.println("[开始处理] 单据编号: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        
                        data.put("cgjsdid", currentCgjsdid);
                        data.put("billnum", currentBillnum);
                        data.put("WLDW", Util.null2String(item.get("WLDW")));
                        data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                        data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                        data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                        data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                        data.put("BMBH", Util.null2String(item.get("BMBH")));
                        data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                        data.put("summary", Util.null2String(item.get("SUMMARY")));
                        data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                        data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                        data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                        data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                        data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                        data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                        data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                        data.put("dwbh", Util.null2String(item.get("DWBH")));
                        data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                        data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                        data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                        data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                        data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                        data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                        data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                        data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                        data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                        data.put("XMID", Util.null2String(item.get("XMID")));
                        data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                        data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                        data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                        data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                        if (item.get("BILLDATE") != null) {
                            data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
                        }
                        data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                        data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                        // 金额字段:统一两位小数
                        data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                        data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                        data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                        data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                        data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                        data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                        data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                        data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                        // availableforeigncurrency 字段在数据库表中不存在,已跳过
                        // data.put("availableforeigncurrency", formatTo2Decimals(item.getString("AVAILABLEFOREIGNCURRENCY")));

                        System.out.println("[字段映射完成] billnum: " + currentBillnum + ", 准备保存...");
                        
                        int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords);
                        if (action == 1) {
                            updateCount++;
                            diffBillnums.add(data.get("billnum"));
                        } else if (action == 2) {
                            newCount++;
                            newBillnums.add(data.get("billnum"));
                        } else if (action == -1) {
                            failCount++;
                            System.out.println("[保存失败] billnum: " + currentBillnum);
                        } else {
                            skipCount++;
                        }
                    } catch (Exception ee) {
                        failCount++;
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        System.out.println("[严重错误] billnum: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        System.out.println("[错误信息] " + ee.getMessage());
                        System.out.println("[堆栈跟踪开始]");
                        ee.printStackTrace();
                        System.out.println("[堆栈跟踪结束]");
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        
                        // 记录失败详情
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", currentBillnum);
                        failedRecord.put("cgjsdid", currentCgjsdid);
                        failedRecord.put("errorMessage", ee.getMessage());
                        failedRecord.put("errorType", ee.getClass().getName());
                        failedRecords.add(failedRecord);
                    }
                }
            }

            // 每页结束后可执行一次关联更新
            try {
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
            } catch (Exception ex) {
                ex.printStackTrace();
                result.put("updateError", "第" + page + "页 关联字段更新异常:" + ex.getMessage());
            }

            System.out.println("第 " + page + " 页处理完成,累计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount);
            page++;
        }

        String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 分页处理完成";
        result.put("updateInfo", syncInfo);
        System.out.println(syncInfo);
        System.out.println("========================================");
        System.out.println("同步完成汇总:");
        System.out.println("总数: " + totalCount);
        System.out.println("新增: " + newCount);
        System.out.println("更新: " + updateCount);
        System.out.println("跳过: " + skipCount);
        System.out.println("失败: " + failCount);
        if (failCount > 0) {
            System.out.println("失败记录详情:");
            for (int i = 0; i < failedRecords.size(); i++) {
                JSONObject failed = failedRecords.getJSONObject(i);
                System.out.println("  [" + (i+1) + "] billnum=" + failed.getString("billnum") + 
                                 ", cgjsdid=" + failed.getString("cgjsdid") +
                                 ", operation=" + failed.getString("operation") +
                                 ", error=" + failed.getString("errorMessage"));
            }
        }
        System.out.println("========================================");
        
        // 将差异与新增的单据编号(billnum)放入结果
        result.put("diffBillnums", diffBillnums);
        result.put("newBillnums", newBillnums);
        // 返回每条更新的具体字段变化
        result.put("updatedDetails", updatedDetails);
        // 返回失败记录
        if (failCount > 0) {
            result.put("failedRecords", failedRecords);
            result.put("failCount", failCount);
        }
        // 返回调试日志
        if (debugLogs.size() > 0) {
            result.put("debugLogs", debugLogs);
        }
    } catch (Exception e) {
        e.printStackTrace();
        result.put("error", "增量同步数据异常: " + e.getMessage());
    }
    return result;
}

/**
 * 调试辅助:打印 ModeUtil.saveModeData 可能使用的表信息
 */
public String debugTableInfo(String tablename, RecordSet rs) {
    StringBuilder info = new StringBuilder();
    try {
        // 获取表的列信息
        rs.executeQuery("SHOW COLUMNS FROM " + tablename);
        info.append("[表结构信息] ").append(tablename).append(":\n");
        while (rs.next()) {
            String field = rs.getString("Field");
            String type = rs.getString("Type");
            String nullAllowed = rs.getString("Null");
            String key = rs.getString("Key");
            String defaultVal = rs.getString("Default");
            info.append("  ").append(field).append(" ").append(type);
            if ("NO".equals(nullAllowed)) info.append(" NOT NULL");
            if ("PRI".equals(key)) info.append(" PRIMARY KEY");
            if (defaultVal != null) info.append(" DEFAULT ").append(defaultVal);
            info.append("\n");
        }
    } catch (Exception e) {
        info.append("获取表结构失败: ").append(e.getMessage());
    }
    return info.toString();
}
%>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONArray" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="weaver.general.Util" %>
<%@ page import="dev.soft.lb.util.CommonUtil" %>
<%@ page import="dev.soft.lb.util.LCUtil" %>
<%@ page import="weaver.general.TimeUtil" %>
<%@ page import="weaver.conn.RecordSet" %>
<%@ page import="weaver.hrm.company.SubCompanyComInfo" %>
<%@ page import="weaver.formmode.setup.ModeRightInfo" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.math.BigDecimal" %>
<%@ page import="java.math.RoundingMode" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.UUID" %>

<%
// 设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
// 允许跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

// 定义常量
final String companyId = "66";
final String billtype = "12";
String api = "";
final String tablename = "uf_cgjsd";
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 创建返回结果
JSONObject result = new JSONObject();

try {
    // 获取单位编码
    SubCompanyComInfo scci = new SubCompanyComInfo();
    String dwbm = scci.getSubCompanyCode(companyId);

    // 获取API配置
    RecordSet rs = new RecordSet();
    Map<String, String> billconfig = LCUtil.getLCBillTypeInfo(billtype, rs);
    api = billconfig.get("api_list");

    // 先做一次去重(不清空表,做增量同步)
//    deduplicateDatabase(rs, tablename);

    // 构建请求参数 - 分页增量拉取
    JSONObject paramJson = new JSONObject();
    paramJson.put("dwbh", dwbm);
    paramJson.put("pagesize", "10000");
    paramJson.put("page", "1");

    // 执行增量同步
    JSONObject syncResult = syncData(paramJson, rs, api, tablename, sdf);
    result.put("status", 1);
    result.put("data", syncResult);
    result.put("msg", "增量同步采购结算单成功");
    // 仅保留核心关心内容(差异单据编号billnum、新增单据编号billnum)在顶层返回,便于前端直接消费
    if (syncResult.containsKey("diffBillnums")) {
        result.put("diffBillnums", syncResult.getJSONArray("diffBillnums"));
    }
    if (syncResult.containsKey("newBillnums")) {
        result.put("newBillnums", syncResult.getJSONArray("newBillnums"));
    }
    // 返回每条更新的具体字段变化(字段名、旧值、新值)
    if (syncResult.containsKey("updatedDetails")) {
        result.put("updatedDetails", syncResult.getJSONArray("updatedDetails"));
    }

    // 添加详细的同步信息
    if (syncResult.containsKey("updateInfo")) {
        result.put("updateInfo", syncResult.getString("updateInfo"));
    }
    if (syncResult.containsKey("updateError")) {
        result.put("updateError", syncResult.getString("updateError"));
    }
    // 添加失败记录信息
    if (syncResult.containsKey("failedRecords")) {
        result.put("failedRecords", syncResult.getJSONArray("failedRecords"));
    }
    if (syncResult.containsKey("failCount")) {
        result.put("failCount", syncResult.getInteger("failCount"));
    }

} catch (Exception e) {
    e.printStackTrace();
    result.put("status", 0);
    result.put("msg", "读取浪潮采购结算单异常:" + e.getMessage());
    result.put("errorDetail", e.toString());
}

// 输出JSON结果
out.print(result.toJSONString());

/**
 * 同步数据方法
 */
%>

<%!
/**
 * 数据库去重方法 - 删除重复数据,保留时间最新的一条
 */
public void deduplicateDatabase(RecordSet rs, String tablename) {
    try {
        String sql = "DELETE t1 FROM " + tablename + " t1 " +
                    "INNER JOIN " + tablename + " t2 " +
                    "WHERE t1.id < t2.id " +
                    "AND t1.cgjsdid = t2.cgjsdid " +
                    "AND t1.billnum = t2.billnum " +
                    "AND t1.WLDW = t2.WLDW " +
                    "AND t1.DEPARTMENTID = t2.DEPARTMENTID " +
                    "AND t1.contractid = t2.contractid " +
                    "AND t1.XMID = t2.XMID " +
                    "AND t1.Kxxz = t2.Kxxz " +
                    "AND t1.amount = t2.amount " +
                    "AND t1.Billdate = t2.Billdate";
        rs.execute(sql);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 数值字段名单:这些字段按两位小数口径进行比较与存储
private static final Set<String> NUMERIC_FIELDS = new HashSet<>(Arrays.asList(
        "amount",
        "foreigncurrency",
        "clearamount",
        "clearforeigncurrency",
        "balanceamount",
        "balanceforeigncurrency",
        "availableamount",
        // "availableforeigncurrency",  // 表中不存在此字段
        // 添加其他可能的数值字段
        "original_clearamount",
        "original_clearforeigncurrency",
        "original_balanceamount",
        "original_balanceforeigncurrency",
        "price",
        "unitprice",
        "quantity",
        "taxrate",
        "taxamount",
        "totalmoney",
        "netamount",
        "amountnotax"
));

// 规范化字符串:去首尾空格;null按空串处理
private static String norm(String v) {
    if (v == null) return "";
    return v.trim();
}

// 字符串等价:规范化后比较;空串与null等价
private static boolean equalNormalized(String a, String b) {
    return norm(a).equals(norm(b));
}

// 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
private static boolean equalNumeric(String a, String b) {
    String sa = norm(a);
    String sb = norm(b);
    if (sa.isEmpty() && sb.isEmpty()) return true;
    if (sa.isEmpty() || sb.isEmpty()) return false;
    try {
        BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
        BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
        return ba.compareTo(bb) == 0;
    } catch (Exception ignore) {
        return sa.equals(sb);
    }
}

// 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
private static String formatTo2Decimals(String v) {
    String s = norm(v);
    if (s.isEmpty()) return s;
    try {
        return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
    } catch (Exception ignore) {
        return s;
    }
}

// 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
public boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return true; // 无法获取字段信息,默认认为有变更
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                System.out.println("[字段差异] " + columnName + ": 数据库值='" + dbValue + "', 接口值='" + newValue + "'");
                return true; // 发现差异
            }
        }
        return false; // 所有字段都相同
    } catch (Exception e) {
        e.printStackTrace();
        // 出现异常时,默认认为有变更,以保证数据一致性
        return true;
    }
}

// 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
public JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
    JSONArray changes = new JSONArray();
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return changes; // 无法获取字段信息
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                JSONObject diff = new JSONObject();
                diff.put("field", columnName);
                diff.put("oldValue", dbValue);
                diff.put("newValue", newValue);
                changes.add(diff);
            }
        }
    } catch (Exception e) {
        // 忽略,返回当前已收集到的差异
    }
    return changes;
}

// 先对比后插入/更新:按 (cgjsdid,billnum) 定位;差异则按原id更新;不存在则插入
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename,
                                         JSONArray updateDetails, JSONArray failedRecords, Set<Integer> affectedIds) {
    String billnum = Util.null2String(data.get("billnum"));
    String cgjsdid = Util.null2String(data.get("cgjsdid"));
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", billnum);
                    detail.put("cgjsdid", cgjsdid);
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                if (changes != null && changes.size() > 0) {
                    if (applyUpdate(existingId, changes, tablename)) {
                        if (affectedIds != null) {
                            affectedIds.add(existingId);
                        }
                        return 1;
                    }
                    throw new RuntimeException("更新采购结算单失败");
                }
            }
            return 0;
        }

        Integer newId = insertRecord(data, tablename);
        if (newId != null && newId > 0) {
            if (affectedIds != null) {
                affectedIds.add(newId);
            }
            return 2;
        }
        throw new RuntimeException("新增采购结算单失败");
    } catch (Exception e) {
        if (failedRecords != null) {
            JSONObject failedRecord = new JSONObject();
            failedRecord.put("billnum", billnum);
            failedRecord.put("cgjsdid", cgjsdid);
            failedRecord.put("errorMessage", e.getMessage());
            failedRecord.put("operation", "upsert");
            failedRecords.add(failedRecord);
        }
        return -1;
    }
}

private boolean applyUpdate(int recordId, JSONArray changes, String tablename) {
    if (recordId <= 0 || changes == null || changes.isEmpty()) {
        return false;
    }
    StringBuilder setClause = new StringBuilder();
    for (int i = 0; i < changes.size(); i++) {
        JSONObject change = changes.getJSONObject(i);
        String field = Util.null2String(change.getString("field"));
        String value = Util.null2String(change.getString("newValue"));
        if (field.isEmpty()) {
            continue;
        }
        if (setClause.length() > 0) {
            setClause.append(",");
        }
        setClause.append(field).append("=").append(toSqlValue(value));
    }
    if (setClause.length() == 0) {
        return false;
    }
    String sql = "UPDATE " + tablename + " SET " + setClause + " WHERE id=" + recordId;
    RecordSet updateRs = new RecordSet();
    return updateRs.execute(sql);
}

private Integer insertRecord(Map<String, String> data, String tablename) {
    if (data == null || data.isEmpty()) {
        return null;
    }
    Map<String, String> insertMap = new HashMap<>(data);
    String now = TimeUtil.getCurrentTimeString();
    String createDate = now.substring(0, 10);
    String createTime = now.substring(11);
    insertMap.put("formmodeid", String.valueOf(getModeId(tablename)));
    insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
    insertMap.put("modedatacreatertype", "1");
    insertMap.put("modedatacreatedate", createDate);
    insertMap.put("modedatacreatetime", createTime);
    insertMap.put("modeuuid", UUID.randomUUID().toString());

    StringBuilder columns = new StringBuilder();
    StringBuilder values = new StringBuilder();
    for (Map.Entry<String, String> entry : insertMap.entrySet()) {
        if (columns.length() > 0) {
            columns.append(",");
            values.append(",");
        }
        columns.append(entry.getKey());
        values.append(toSqlValue(entry.getValue()));
    }

    RecordSet insertRs = new RecordSet();
    String sql = "INSERT INTO " + tablename + "(" + columns + ") VALUES(" + values + ")";
    if (!insertRs.execute(sql)) {
        return null;
    }
    RecordSet idRs = new RecordSet();
    idRs.executeQuery("SELECT id FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?",
            Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
    if (idRs.next()) {
        return idRs.getInt("id");
    }
    return null;
}

private String toSqlValue(String value) {
    String normalized = Util.null2String(value);
    if (normalized.isEmpty()) {
        return "null";
    }
    return "'" + normalized.replace("'", "''") + "'";
}

private void rebuildPermissions(Set<Integer> affectedIds, String tablename) {
    if (affectedIds == null || affectedIds.isEmpty()) {
        return;
    }
    try {
        int modeId = getModeId(tablename);
        if (modeId <= 0) {
            return;
    }
        ModeRightInfo rightInfo = new ModeRightInfo();
        rightInfo.setNewRight(false);
        rightInfo.init();
        rightInfo.setModeId(modeId);
        for (Integer billId : affectedIds) {
            if (billId == null || billId <= 0) {
                continue;
            }
            rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private int getModeId(String tablename) {
    try {
        return Util.getIntValue(CommonUtil.getModeIdByTableName(tablename));
    } catch (Exception e) {
        return 0;
    }
}

public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
    JSONObject result = new JSONObject();
    int totalCount = 0;
    int newCount = 0;
    int updateCount = 0;
    int skipCount = 0;
    int failCount = 0;
    // 收集差异更新与新增的单据编号(billnum)
    JSONArray diffBillnums = new JSONArray();
    JSONArray newBillnums = new JSONArray();
    // 收集每条更新的字段变更详情
    JSONArray updatedDetails = new JSONArray();
    // 收集保存失败的记录详情
    JSONArray failedRecords = new JSONArray();
    // 收集调试日志(仅记录失败记录的详细过程)
    JSONArray debugLogs = new JSONArray();
    try {
        int page = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
        String pageSize = Util.null2String(param.getString("pagesize"));
        if ("".equals(pageSize)) pageSize = "10000";

        while (true) {
            param.put("page", String.valueOf(page));
            param.put("pagesize", pageSize);

            JSONObject rjson = LCUtil.doPost(api, param);
            result = rjson;

            // 结束条件
            if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                System.out.println("分页已结束,code=3003,停止请求。最后页码:" + page);
                break;
            }
            if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                System.out.println("接口返回非成功code:" + rjson.getString("code") + ",页码:" + page + ",停止。");
                break;
            }

            JSONArray datas = rjson.getJSONArray("data");
            int pageCount = (datas == null) ? 0 : datas.size();
            totalCount += pageCount;
            System.out.println("第 " + page + " 页获取到 " + pageCount + " 条数据,开始增量同步...");

            if (datas != null) {
                for (int x = 0; x < datas.size(); x++) {
                    JSONObject item = datas.getJSONObject(x);
                    Map<String, String> data = new HashMap<>();
                    String currentBillnum = "";
                    String currentCgjsdid = "";
                    try {
                        // 映射字段
                        currentCgjsdid = Util.null2String(item.get("ID"));
                        currentBillnum = Util.null2String(item.get("BILLNUM"));
                        
                        System.out.println("========================================");
                        System.out.println("[开始处理] 单据编号: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        
                        data.put("cgjsdid", currentCgjsdid);
                        data.put("billnum", currentBillnum);
                        data.put("WLDW", Util.null2String(item.get("WLDW")));
                        data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                        data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                        data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                        data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                        data.put("BMBH", Util.null2String(item.get("BMBH")));
                        data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                        data.put("summary", Util.null2String(item.get("SUMMARY")));
                        data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                        data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                        data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                        data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                        data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                        data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                        data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                        data.put("dwbh", Util.null2String(item.get("DWBH")));
                        data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                        data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                        data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                        data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                        data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                        data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                        data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                        data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                        data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                        data.put("XMID", Util.null2String(item.get("XMID")));
                        data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                        data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                        data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                        data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                        if (item.get("BILLDATE") != null) {
                            data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
                        }
                        data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                        data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                        // 金额字段:统一两位小数
                        data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                        data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                        data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                        data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                        data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                        data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                        data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                        data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                        // availableforeigncurrency 字段在数据库表中不存在,已跳过
                        // data.put("availableforeigncurrency", formatTo2Decimals(item.getString("AVAILABLEFOREIGNCURRENCY")));

                        System.out.println("[字段映射完成] billnum: " + currentBillnum + ", 准备保存...");
                        
                        int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords);
                        if (action == 1) {
                            updateCount++;
                            diffBillnums.add(data.get("billnum"));
                        } else if (action == 2) {
                            newCount++;
                            newBillnums.add(data.get("billnum"));
                        } else if (action == -1) {
                            failCount++;
                            System.out.println("[保存失败] billnum: " + currentBillnum);
                        } else {
                            skipCount++;
                        }
                    } catch (Exception ee) {
                        failCount++;
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        System.out.println("[严重错误] billnum: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        System.out.println("[错误信息] " + ee.getMessage());
                        System.out.println("[堆栈跟踪开始]");
                        ee.printStackTrace();
                        System.out.println("[堆栈跟踪结束]");
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        
                        // 记录失败详情
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", currentBillnum);
                        failedRecord.put("cgjsdid", currentCgjsdid);
                        failedRecord.put("errorMessage", ee.getMessage());
                        failedRecord.put("errorType", ee.getClass().getName());
                        failedRecords.add(failedRecord);
                    }
                }
            }

            // 每页结束后可执行一次关联更新
            try {
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
            } catch (Exception ex) {
                ex.printStackTrace();
                result.put("updateError", "第" + page + "页 关联字段更新异常:" + ex.getMessage());
            }

            System.out.println("第 " + page + " 页处理完成,累计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount);
            page++;
        }

        String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 分页处理完成";
        result.put("updateInfo", syncInfo);
        System.out.println(syncInfo);
        System.out.println("========================================");
        System.out.println("同步完成汇总:");
        System.out.println("总数: " + totalCount);
        System.out.println("新增: " + newCount);
        System.out.println("更新: " + updateCount);
        System.out.println("跳过: " + skipCount);
        System.out.println("失败: " + failCount);
        if (failCount > 0) {
            System.out.println("失败记录详情:");
            for (int i = 0; i < failedRecords.size(); i++) {
                JSONObject failed = failedRecords.getJSONObject(i);
                System.out.println("  [" + (i+1) + "] billnum=" + failed.getString("billnum") + 
                                 ", cgjsdid=" + failed.getString("cgjsdid") +
                                 ", operation=" + failed.getString("operation") +
                                 ", error=" + failed.getString("errorMessage"));
            }
        }
        System.out.println("========================================");
        
        // 将差异与新增的单据编号(billnum)放入结果
        result.put("diffBillnums", diffBillnums);
        result.put("newBillnums", newBillnums);
        // 返回每条更新的具体字段变化
        result.put("updatedDetails", updatedDetails);
        // 返回失败记录
        if (failCount > 0) {
            result.put("failedRecords", failedRecords);
            result.put("failCount", failCount);
        }
        // 返回调试日志
        if (debugLogs.size() > 0) {
            result.put("debugLogs", debugLogs);
        }
    } catch (Exception e) {
        e.printStackTrace();
        result.put("error", "增量同步数据异常: " + e.getMessage());
    }
    return result;
}

/**
 * 调试辅助:打印 ModeUtil.saveModeData 可能使用的表信息
 */
public String debugTableInfo(String tablename, RecordSet rs) {
    StringBuilder info = new StringBuilder();
    try {
        // 获取表的列信息
        rs.executeQuery("SHOW COLUMNS FROM " + tablename);
        info.append("[表结构信息] ").append(tablename).append(":\n");
        while (rs.next()) {
            String field = rs.getString("Field");
            String type = rs.getString("Type");
            String nullAllowed = rs.getString("Null");
            String key = rs.getString("Key");
            String defaultVal = rs.getString("Default");
            info.append("  ").append(field).append(" ").append(type);
            if ("NO".equals(nullAllowed)) info.append(" NOT NULL");
            if ("PRI".equals(key)) info.append(" PRIMARY KEY");
            if (defaultVal != null) info.append(" DEFAULT ").append(defaultVal);
            info.append("\n");
        }
    } catch (Exception e) {
        info.append("获取表结构失败: ").append(e.getMessage());
    }
    return info.toString();
}
%>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONArray" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="weaver.general.Util" %>
<%@ page import="dev.soft.lb.util.CommonUtil" %>
<%@ page import="dev.soft.lb.util.LCUtil" %>
<%@ page import="weaver.general.TimeUtil" %>
<%@ page import="weaver.conn.RecordSet" %>
<%@ page import="weaver.hrm.company.SubCompanyComInfo" %>
<%@ page import="weaver.formmode.setup.ModeRightInfo" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.math.BigDecimal" %>
<%@ page import="java.math.RoundingMode" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.UUID" %>

<%
// 设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
// 允许跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

// 定义常量
final String companyId = "66";
final String billtype = "12";
String api = "";
final String tablename = "uf_cgjsd";
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 创建返回结果
JSONObject result = new JSONObject();

try {
    // 获取单位编码
    SubCompanyComInfo scci = new SubCompanyComInfo();
    String dwbm = scci.getSubCompanyCode(companyId);

    // 获取API配置
    RecordSet rs = new RecordSet();
    Map<String, String> billconfig = LCUtil.getLCBillTypeInfo(billtype, rs);
    api = billconfig.get("api_list");

    // 先做一次去重(不清空表,做增量同步)
//    deduplicateDatabase(rs, tablename);

    // 构建请求参数 - 分页增量拉取
    JSONObject paramJson = new JSONObject();
    paramJson.put("dwbh", dwbm);
    paramJson.put("pagesize", "10000");
    paramJson.put("page", "1");

    // 执行增量同步
    JSONObject syncResult = syncData(paramJson, rs, api, tablename, sdf);
    result.put("status", 1);
    result.put("data", syncResult);
    result.put("msg", "增量同步采购结算单成功");
    // 仅保留核心关心内容(差异单据编号billnum、新增单据编号billnum)在顶层返回,便于前端直接消费
    if (syncResult.containsKey("diffBillnums")) {
        result.put("diffBillnums", syncResult.getJSONArray("diffBillnums"));
    }
    if (syncResult.containsKey("newBillnums")) {
        result.put("newBillnums", syncResult.getJSONArray("newBillnums"));
    }
    // 返回每条更新的具体字段变化(字段名、旧值、新值)
    if (syncResult.containsKey("updatedDetails")) {
        result.put("updatedDetails", syncResult.getJSONArray("updatedDetails"));
    }

    // 添加详细的同步信息
    if (syncResult.containsKey("updateInfo")) {
        result.put("updateInfo", syncResult.getString("updateInfo"));
    }
    if (syncResult.containsKey("updateError")) {
        result.put("updateError", syncResult.getString("updateError"));
    }
    // 添加失败记录信息
    if (syncResult.containsKey("failedRecords")) {
        result.put("failedRecords", syncResult.getJSONArray("failedRecords"));
    }
    if (syncResult.containsKey("failCount")) {
        result.put("failCount", syncResult.getInteger("failCount"));
    }

} catch (Exception e) {
    e.printStackTrace();
    result.put("status", 0);
    result.put("msg", "读取浪潮采购结算单异常:" + e.getMessage());
    result.put("errorDetail", e.toString());
}

// 输出JSON结果
out.print(result.toJSONString());

/**
 * 同步数据方法
 */
%>

<%!
private static final int DEFAULT_USER_ID = 1;

/**
 * 数据库去重方法 - 删除重复数据,保留时间最新的一条
 */
public void deduplicateDatabase(RecordSet rs, String tablename) {
    try {
        String sql = "DELETE t1 FROM " + tablename + " t1 " +
                    "INNER JOIN " + tablename + " t2 " +
                    "WHERE t1.id < t2.id " +
                    "AND t1.cgjsdid = t2.cgjsdid " +
                    "AND t1.billnum = t2.billnum " +
                    "AND t1.WLDW = t2.WLDW " +
                    "AND t1.DEPARTMENTID = t2.DEPARTMENTID " +
                    "AND t1.contractid = t2.contractid " +
                    "AND t1.XMID = t2.XMID " +
                    "AND t1.Kxxz = t2.Kxxz " +
                    "AND t1.amount = t2.amount " +
                    "AND t1.Billdate = t2.Billdate";
        rs.execute(sql);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 数值字段名单:这些字段按两位小数口径进行比较与存储
private static final Set<String> NUMERIC_FIELDS = new HashSet<>(Arrays.asList(
        "amount",
        "foreigncurrency",
        "clearamount",
        "clearforeigncurrency",
        "balanceamount",
        "balanceforeigncurrency",
        "availableamount",
        // "availableforeigncurrency",  // 表中不存在此字段
        // 添加其他可能的数值字段
        "original_clearamount",
        "original_clearforeigncurrency",
        "original_balanceamount",
        "original_balanceforeigncurrency",
        "price",
        "unitprice",
        "quantity",
        "taxrate",
        "taxamount",
        "totalmoney",
        "netamount",
        "amountnotax"
));

// 规范化字符串:去首尾空格;null按空串处理
private static String norm(String v) {
    if (v == null) return "";
    return v.trim();
}

// 字符串等价:规范化后比较;空串与null等价
private static boolean equalNormalized(String a, String b) {
    return norm(a).equals(norm(b));
}

// 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
private static boolean equalNumeric(String a, String b) {
    String sa = norm(a);
    String sb = norm(b);
    if (sa.isEmpty() && sb.isEmpty()) return true;
    if (sa.isEmpty() || sb.isEmpty()) return false;
    try {
        BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
        BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
        return ba.compareTo(bb) == 0;
    } catch (Exception ignore) {
        return sa.equals(sb);
    }
}

// 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
private static String formatTo2Decimals(String v) {
    String s = norm(v);
    if (s.isEmpty()) return s;
    try {
        return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
    } catch (Exception ignore) {
        return s;
    }
}

// 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
public boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return true; // 无法获取字段信息,默认认为有变更
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                System.out.println("[字段差异] " + columnName + ": 数据库值='" + dbValue + "', 接口值='" + newValue + "'");
                return true; // 发现差异
            }
        }
        return false; // 所有字段都相同
    } catch (Exception e) {
        e.printStackTrace();
        // 出现异常时,默认认为有变更,以保证数据一致性
        return true;
    }
}

// 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
public JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
    JSONArray changes = new JSONArray();
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return changes; // 无法获取字段信息
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                JSONObject diff = new JSONObject();
                diff.put("field", columnName);
                diff.put("oldValue", dbValue);
                diff.put("newValue", newValue);
                changes.add(diff);
            }
        }
    } catch (Exception e) {
        // 忽略,返回当前已收集到的差异
    }
    return changes;
}

// 先对比后插入/更新:按 (cgjsdid,billnum) 定位;差异则按原id更新;不存在则插入
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename,
                                         JSONArray updateDetails, JSONArray failedRecords, Set<Integer> affectedIds) {
    String billnum = Util.null2String(data.get("billnum"));
    String cgjsdid = Util.null2String(data.get("cgjsdid"));
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", billnum);
                    detail.put("cgjsdid", cgjsdid);
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                if (changes != null && changes.size() > 0) {
                    if (applyUpdate(existingId, changes, tablename)) {
                        if (affectedIds != null) {
                            affectedIds.add(existingId);
                        }
                        return 1;
                    }
                    throw new RuntimeException("更新采购结算单失败");
                }
            }
            return 0;
        }

        Integer newId = insertRecord(data, tablename);
        if (newId != null && newId > 0) {
            if (affectedIds != null) {
                affectedIds.add(newId);
            }
            return 2;
        }
        throw new RuntimeException("新增采购结算单失败");
    } catch (Exception e) {
        if (failedRecords != null) {
            JSONObject failedRecord = new JSONObject();
            failedRecord.put("billnum", billnum);
            failedRecord.put("cgjsdid", cgjsdid);
            failedRecord.put("errorMessage", e.getMessage());
            failedRecord.put("operation", "upsert");
            failedRecords.add(failedRecord);
        }
        return -1;
    }
}

private boolean applyUpdate(int recordId, JSONArray changes, String tablename) {
    if (recordId <= 0 || changes == null || changes.isEmpty()) {
        return false;
    }
    StringBuilder setClause = new StringBuilder();
    for (int i = 0; i < changes.size(); i++) {
        JSONObject change = changes.getJSONObject(i);
        String field = Util.null2String(change.getString("field"));
        String value = Util.null2String(change.getString("newValue"));
        if (field.isEmpty()) {
            continue;
        }
        if (setClause.length() > 0) {
            setClause.append(",");
        }
        setClause.append(field).append("=").append(toSqlValue(value));
    }
    if (setClause.length() == 0) {
        return false;
    }
    String sql = "UPDATE " + tablename + " SET " + setClause + " WHERE id=" + recordId;
    RecordSet updateRs = new RecordSet();
    return updateRs.execute(sql);
}

private Integer insertRecord(Map<String, String> data, String tablename) {
    if (data == null || data.isEmpty()) {
        return null;
    }
    Map<String, String> insertMap = new HashMap<>(data);
    String now = TimeUtil.getCurrentTimeString();
    String createDate = now.substring(0, 10);
    String createTime = now.substring(11);
    insertMap.put("formmodeid", String.valueOf(getModeId(tablename)));
    insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
    insertMap.put("modedatacreatertype", "1");
    insertMap.put("modedatacreatedate", createDate);
    insertMap.put("modedatacreatetime", createTime);
    insertMap.put("modeuuid", UUID.randomUUID().toString());

    StringBuilder columns = new StringBuilder();
    StringBuilder values = new StringBuilder();
    for (Map.Entry<String, String> entry : insertMap.entrySet()) {
        if (columns.length() > 0) {
            columns.append(",");
            values.append(",");
        }
        columns.append(entry.getKey());
        values.append(toSqlValue(entry.getValue()));
    }

    RecordSet insertRs = new RecordSet();
    String sql = "INSERT INTO " + tablename + "(" + columns + ") VALUES(" + values + ")";
    if (!insertRs.execute(sql)) {
        return null;
    }
    RecordSet idRs = new RecordSet();
    idRs.executeQuery("SELECT id FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?",
            Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
    if (idRs.next()) {
        return idRs.getInt("id");
    }
    return null;
}

private String toSqlValue(String value) {
    String normalized = Util.null2String(value);
    if (normalized.isEmpty()) {
        return "null";
    }
    return "'" + normalized.replace("'", "''") + "'";
}

private void rebuildPermissions(Set<Integer> affectedIds, String tablename) {
    if (affectedIds == null || affectedIds.isEmpty()) {
        return;
    }
    try {
        int modeId = getModeId(tablename);
        if (modeId <= 0) {
            return;
    }
        ModeRightInfo rightInfo = new ModeRightInfo();
        rightInfo.setNewRight(false);
        rightInfo.init();
        rightInfo.setModeId(modeId);
        for (Integer billId : affectedIds) {
            if (billId == null || billId <= 0) {
                continue;
            }
            rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private int getModeId(String tablename) {
    try {
        return Util.getIntValue(CommonUtil.getModeIdByTableName(tablename));
    } catch (Exception e) {
        return 0;
    }
}

public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
    JSONObject result = new JSONObject();
    int totalCount = 0;
    int newCount = 0;
    int updateCount = 0;
    int skipCount = 0;
    int failCount = 0;
    // 收集差异更新与新增的单据编号(billnum)
    JSONArray diffBillnums = new JSONArray();
    JSONArray newBillnums = new JSONArray();
    // 收集每条更新的字段变更详情
    JSONArray updatedDetails = new JSONArray();
    // 收集保存失败的记录详情
    JSONArray failedRecords = new JSONArray();
    // 收集调试日志(仅记录失败记录的详细过程)
    JSONArray debugLogs = new JSONArray();
    try {
        int page = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
        String pageSize = Util.null2String(param.getString("pagesize"));
        if ("".equals(pageSize)) pageSize = "10000";

        while (true) {
            param.put("page", String.valueOf(page));
            param.put("pagesize", pageSize);

            JSONObject rjson = LCUtil.doPost(api, param);
            result = rjson;

            // 结束条件
            if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                System.out.println("分页已结束,code=3003,停止请求。最后页码:" + page);
                break;
            }
            if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                System.out.println("接口返回非成功code:" + rjson.getString("code") + ",页码:" + page + ",停止。");
                break;
            }

            JSONArray datas = rjson.getJSONArray("data");
            int pageCount = (datas == null) ? 0 : datas.size();
            totalCount += pageCount;
            System.out.println("第 " + page + " 页获取到 " + pageCount + " 条数据,开始增量同步...");

            if (datas != null) {
                for (int x = 0; x < datas.size(); x++) {
                    JSONObject item = datas.getJSONObject(x);
                    Map<String, String> data = new HashMap<>();
                    String currentBillnum = "";
                    String currentCgjsdid = "";
                    try {
                        // 映射字段
                        currentCgjsdid = Util.null2String(item.get("ID"));
                        currentBillnum = Util.null2String(item.get("BILLNUM"));
                        
                        System.out.println("========================================");
                        System.out.println("[开始处理] 单据编号: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        
                        data.put("cgjsdid", currentCgjsdid);
                        data.put("billnum", currentBillnum);
                        data.put("WLDW", Util.null2String(item.get("WLDW")));
                        data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                        data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                        data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                        data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                        data.put("BMBH", Util.null2String(item.get("BMBH")));
                        data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                        data.put("summary", Util.null2String(item.get("SUMMARY")));
                        data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                        data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                        data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                        data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                        data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                        data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                        data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                        data.put("dwbh", Util.null2String(item.get("DWBH")));
                        data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                        data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                        data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                        data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                        data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                        data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                        data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                        data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                        data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                        data.put("XMID", Util.null2String(item.get("XMID")));
                        data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                        data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                        data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                        data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                        if (item.get("BILLDATE") != null) {
                            data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
                        }
                        data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                        data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                        // 金额字段:统一两位小数
                        data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                        data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                        data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                        data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                        data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                        data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                        data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                        data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                        // availableforeigncurrency 字段在数据库表中不存在,已跳过
                        // data.put("availableforeigncurrency", formatTo2Decimals(item.getString("AVAILABLEFOREIGNCURRENCY")));

                        System.out.println("[字段映射完成] billnum: " + currentBillnum + ", 准备保存...");
                        
                        int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords);
                        if (action == 1) {
                            updateCount++;
                            diffBillnums.add(data.get("billnum"));
                        } else if (action == 2) {
                            newCount++;
                            newBillnums.add(data.get("billnum"));
                        } else if (action == -1) {
                            failCount++;
                            System.out.println("[保存失败] billnum: " + currentBillnum);
                        } else {
                            skipCount++;
                        }
                    } catch (Exception ee) {
                        failCount++;
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        System.out.println("[严重错误] billnum: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        System.out.println("[错误信息] " + ee.getMessage());
                        System.out.println("[堆栈跟踪开始]");
                        ee.printStackTrace();
                        System.out.println("[堆栈跟踪结束]");
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        
                        // 记录失败详情
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", currentBillnum);
                        failedRecord.put("cgjsdid", currentCgjsdid);
                        failedRecord.put("errorMessage", ee.getMessage());
                        failedRecord.put("errorType", ee.getClass().getName());
                        failedRecords.add(failedRecord);
                    }
                }
            }

            // 每页结束后可执行一次关联更新
            try {
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
            } catch (Exception ex) {
                ex.printStackTrace();
                result.put("updateError", "第" + page + "页 关联字段更新异常:" + ex.getMessage());
            }

            System.out.println("第 " + page + " 页处理完成,累计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount);
            page++;
        }

        String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 分页处理完成";
        result.put("updateInfo", syncInfo);
        System.out.println(syncInfo);
        System.out.println("========================================");
        System.out.println("同步完成汇总:");
        System.out.println("总数: " + totalCount);
        System.out.println("新增: " + newCount);
        System.out.println("更新: " + updateCount);
        System.out.println("跳过: " + skipCount);
        System.out.println("失败: " + failCount);
        if (failCount > 0) {
            System.out.println("失败记录详情:");
            for (int i = 0; i < failedRecords.size(); i++) {
                JSONObject failed = failedRecords.getJSONObject(i);
                System.out.println("  [" + (i+1) + "] billnum=" + failed.getString("billnum") + 
                                 ", cgjsdid=" + failed.getString("cgjsdid") +
                                 ", operation=" + failed.getString("operation") +
                                 ", error=" + failed.getString("errorMessage"));
            }
        }
        System.out.println("========================================");
        
        // 将差异与新增的单据编号(billnum)放入结果
        result.put("diffBillnums", diffBillnums);
        result.put("newBillnums", newBillnums);
        // 返回每条更新的具体字段变化
        result.put("updatedDetails", updatedDetails);
        // 返回失败记录
        if (failCount > 0) {
            result.put("failedRecords", failedRecords);
            result.put("failCount", failCount);
        }
        // 返回调试日志
        if (debugLogs.size() > 0) {
            result.put("debugLogs", debugLogs);
        }
    } catch (Exception e) {
        e.printStackTrace();
        result.put("error", "增量同步数据异常: " + e.getMessage());
    }
    return result;
}

/**
 * 调试辅助:打印 ModeUtil.saveModeData 可能使用的表信息
 */
public String debugTableInfo(String tablename, RecordSet rs) {
    StringBuilder info = new StringBuilder();
    try {
        // 获取表的列信息
        rs.executeQuery("SHOW COLUMNS FROM " + tablename);
        info.append("[表结构信息] ").append(tablename).append(":\n");
        while (rs.next()) {
            String field = rs.getString("Field");
            String type = rs.getString("Type");
            String nullAllowed = rs.getString("Null");
            String key = rs.getString("Key");
            String defaultVal = rs.getString("Default");
            info.append("  ").append(field).append(" ").append(type);
            if ("NO".equals(nullAllowed)) info.append(" NOT NULL");
            if ("PRI".equals(key)) info.append(" PRIMARY KEY");
            if (defaultVal != null) info.append(" DEFAULT ").append(defaultVal);
            info.append("\n");
        }
    } catch (Exception e) {
        info.append("获取表结构失败: ").append(e.getMessage());
    }
    return info.toString();
}
%>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONArray" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="weaver.general.Util" %>
<%@ page import="dev.soft.lb.util.CommonUtil" %>
<%@ page import="dev.soft.lb.util.LCUtil" %>
<%@ page import="weaver.general.TimeUtil" %>
<%@ page import="weaver.conn.RecordSet" %>
<%@ page import="weaver.hrm.company.SubCompanyComInfo" %>
<%@ page import="weaver.formmode.setup.ModeRightInfo" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.math.BigDecimal" %>
<%@ page import="java.math.RoundingMode" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.UUID" %>

<%
// 设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
// 允许跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

// 定义常量
final String companyId = "66";
final String billtype = "12";
String api = "";
final String tablename = "uf_cgjsd";
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 创建返回结果
JSONObject result = new JSONObject();

try {
    // 获取单位编码
    SubCompanyComInfo scci = new SubCompanyComInfo();
    String dwbm = scci.getSubCompanyCode(companyId);

    // 获取API配置
    RecordSet rs = new RecordSet();
    Map<String, String> billconfig = LCUtil.getLCBillTypeInfo(billtype, rs);
    api = billconfig.get("api_list");

    // 先做一次去重(不清空表,做增量同步)
//    deduplicateDatabase(rs, tablename);

    // 构建请求参数 - 分页增量拉取
    JSONObject paramJson = new JSONObject();
    paramJson.put("dwbh", dwbm);
    paramJson.put("pagesize", "10000");
    paramJson.put("page", "1");

    // 执行增量同步
    JSONObject syncResult = syncData(paramJson, rs, api, tablename, sdf);
    result.put("status", 1);
    result.put("data", syncResult);
    result.put("msg", "增量同步采购结算单成功");
    // 仅保留核心关心内容(差异单据编号billnum、新增单据编号billnum)在顶层返回,便于前端直接消费
    if (syncResult.containsKey("diffBillnums")) {
        result.put("diffBillnums", syncResult.getJSONArray("diffBillnums"));
    }
    if (syncResult.containsKey("newBillnums")) {
        result.put("newBillnums", syncResult.getJSONArray("newBillnums"));
    }
    // 返回每条更新的具体字段变化(字段名、旧值、新值)
    if (syncResult.containsKey("updatedDetails")) {
        result.put("updatedDetails", syncResult.getJSONArray("updatedDetails"));
    }

    // 添加详细的同步信息
    if (syncResult.containsKey("updateInfo")) {
        result.put("updateInfo", syncResult.getString("updateInfo"));
    }
    if (syncResult.containsKey("updateError")) {
        result.put("updateError", syncResult.getString("updateError"));
    }
    // 添加失败记录信息
    if (syncResult.containsKey("failedRecords")) {
        result.put("failedRecords", syncResult.getJSONArray("failedRecords"));
    }
    if (syncResult.containsKey("failCount")) {
        result.put("failCount", syncResult.getInteger("failCount"));
    }

} catch (Exception e) {
    e.printStackTrace();
    result.put("status", 0);
    result.put("msg", "读取浪潮采购结算单异常:" + e.getMessage());
    result.put("errorDetail", e.toString());
}

// 输出JSON结果
out.print(result.toJSONString());

/**
 * 同步数据方法
 */
%>

<%!
private static final int DEFAULT_USER_ID = 1;

/**
 * 数据库去重方法 - 删除重复数据,保留时间最新的一条
 */
public void deduplicateDatabase(RecordSet rs, String tablename) {
    try {
        String sql = "DELETE t1 FROM " + tablename + " t1 " +
                    "INNER JOIN " + tablename + " t2 " +
                    "WHERE t1.id < t2.id " +
                    "AND t1.cgjsdid = t2.cgjsdid " +
                    "AND t1.billnum = t2.billnum " +
                    "AND t1.WLDW = t2.WLDW " +
                    "AND t1.DEPARTMENTID = t2.DEPARTMENTID " +
                    "AND t1.contractid = t2.contractid " +
                    "AND t1.XMID = t2.XMID " +
                    "AND t1.Kxxz = t2.Kxxz " +
                    "AND t1.amount = t2.amount " +
                    "AND t1.Billdate = t2.Billdate";
        rs.execute(sql);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 数值字段名单:这些字段按两位小数口径进行比较与存储
private static final Set<String> NUMERIC_FIELDS = new HashSet<>(Arrays.asList(
        "amount",
        "foreigncurrency",
        "clearamount",
        "clearforeigncurrency",
        "balanceamount",
        "balanceforeigncurrency",
        "availableamount",
        // "availableforeigncurrency",  // 表中不存在此字段
        // 添加其他可能的数值字段
        "original_clearamount",
        "original_clearforeigncurrency",
        "original_balanceamount",
        "original_balanceforeigncurrency",
        "price",
        "unitprice",
        "quantity",
        "taxrate",
        "taxamount",
        "totalmoney",
        "netamount",
        "amountnotax"
));

// 规范化字符串:去首尾空格;null按空串处理
private static String norm(String v) {
    if (v == null) return "";
    return v.trim();
}

// 字符串等价:规范化后比较;空串与null等价
private static boolean equalNormalized(String a, String b) {
    return norm(a).equals(norm(b));
}

// 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
private static boolean equalNumeric(String a, String b) {
    String sa = norm(a);
    String sb = norm(b);
    if (sa.isEmpty() && sb.isEmpty()) return true;
    if (sa.isEmpty() || sb.isEmpty()) return false;
    try {
        BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
        BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
        return ba.compareTo(bb) == 0;
    } catch (Exception ignore) {
        return sa.equals(sb);
    }
}

// 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
private static String formatTo2Decimals(String v) {
    String s = norm(v);
    if (s.isEmpty()) return s;
    try {
        return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
    } catch (Exception ignore) {
        return s;
    }
}

// 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
public boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return true; // 无法获取字段信息,默认认为有变更
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                System.out.println("[字段差异] " + columnName + ": 数据库值='" + dbValue + "', 接口值='" + newValue + "'");
                return true; // 发现差异
            }
        }
        return false; // 所有字段都相同
    } catch (Exception e) {
        e.printStackTrace();
        // 出现异常时,默认认为有变更,以保证数据一致性
        return true;
    }
}

// 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
public JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
    JSONArray changes = new JSONArray();
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return changes; // 无法获取字段信息
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                JSONObject diff = new JSONObject();
                diff.put("field", columnName);
                diff.put("oldValue", dbValue);
                diff.put("newValue", newValue);
                changes.add(diff);
            }
        }
    } catch (Exception e) {
        // 忽略,返回当前已收集到的差异
    }
    return changes;
}

// 先对比后插入/更新:按 (cgjsdid,billnum) 定位;差异则按原id更新;不存在则插入
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename,
                                         JSONArray updateDetails, JSONArray failedRecords, Set<Integer> affectedIds) {
    String billnum = Util.null2String(data.get("billnum"));
    String cgjsdid = Util.null2String(data.get("cgjsdid"));
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", billnum);
                    detail.put("cgjsdid", cgjsdid);
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                if (changes != null && changes.size() > 0) {
                    if (applyUpdate(existingId, changes, tablename)) {
                        if (affectedIds != null) {
                            affectedIds.add(existingId);
                        }
                        return 1;
                    }
                    throw new RuntimeException("更新采购结算单失败");
                }
            }
            return 0;
        }

        Integer newId = insertRecord(data, tablename);
        if (newId != null && newId > 0) {
            if (affectedIds != null) {
                affectedIds.add(newId);
            }
            return 2;
        }
        throw new RuntimeException("新增采购结算单失败");
    } catch (Exception e) {
        if (failedRecords != null) {
            JSONObject failedRecord = new JSONObject();
            failedRecord.put("billnum", billnum);
            failedRecord.put("cgjsdid", cgjsdid);
            failedRecord.put("errorMessage", e.getMessage());
            failedRecord.put("operation", "upsert");
            failedRecords.add(failedRecord);
        }
        return -1;
    }
}

private boolean applyUpdate(int recordId, JSONArray changes, String tablename) {
    if (recordId <= 0 || changes == null || changes.isEmpty()) {
        return false;
    }
    StringBuilder setClause = new StringBuilder();
    for (int i = 0; i < changes.size(); i++) {
        JSONObject change = changes.getJSONObject(i);
        String field = Util.null2String(change.getString("field"));
        String value = Util.null2String(change.getString("newValue"));
        if (field.isEmpty()) {
            continue;
        }
        if (setClause.length() > 0) {
            setClause.append(",");
        }
        setClause.append(field).append("=").append(toSqlValue(value));
    }
    if (setClause.length() == 0) {
        return false;
    }
    String sql = "UPDATE " + tablename + " SET " + setClause + " WHERE id=" + recordId;
    RecordSet updateRs = new RecordSet();
    return updateRs.execute(sql);
}

private Integer insertRecord(Map<String, String> data, String tablename) {
    if (data == null || data.isEmpty()) {
        return null;
    }
    Map<String, String> insertMap = new HashMap<>(data);
    String now = TimeUtil.getCurrentTimeString();
    String createDate = now.substring(0, 10);
    String createTime = now.substring(11);
    insertMap.put("formmodeid", String.valueOf(getModeId(tablename)));
    insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
    insertMap.put("modedatacreatertype", "1");
    insertMap.put("modedatacreatedate", createDate);
    insertMap.put("modedatacreatetime", createTime);
    insertMap.put("modeuuid", UUID.randomUUID().toString());

    StringBuilder columns = new StringBuilder();
    StringBuilder values = new StringBuilder();
    for (Map.Entry<String, String> entry : insertMap.entrySet()) {
        if (columns.length() > 0) {
            columns.append(",");
            values.append(",");
        }
        columns.append(entry.getKey());
        values.append(toSqlValue(entry.getValue()));
    }

    RecordSet insertRs = new RecordSet();
    String sql = "INSERT INTO " + tablename + "(" + columns + ") VALUES(" + values + ")";
    if (!insertRs.execute(sql)) {
        return null;
    }
    RecordSet idRs = new RecordSet();
    idRs.executeQuery("SELECT id FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?",
            Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
    if (idRs.next()) {
        return idRs.getInt("id");
    }
    return null;
}

private String toSqlValue(String value) {
    String normalized = Util.null2String(value);
    if (normalized.isEmpty()) {
        return "null";
    }
    return "'" + normalized.replace("'", "''") + "'";
}

private void rebuildPermissions(Set<Integer> affectedIds, String tablename) {
    if (affectedIds == null || affectedIds.isEmpty()) {
        return;
    }
    try {
        int modeId = getModeId(tablename);
        if (modeId <= 0) {
            return;
        }
        ModeRightInfo rightInfo = new ModeRightInfo();
        rightInfo.setNewRight(false);
        rightInfo.init();
        rightInfo.setModeId(modeId);
        for (Integer billId : affectedIds) {
            if (billId == null || billId <= 0) {
                continue;
            }
            rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private int getModeId(String tablename) {
    try {
        return Util.getIntValue(CommonUtil.getModeIdByTableName(tablename));
    } catch (Exception e) {
        return 0;
    }
}

public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
    JSONObject result = new JSONObject();
    int totalCount = 0;
    int newCount = 0;
    int updateCount = 0;
    int skipCount = 0;
    int failCount = 0;
    // 收集差异更新与新增的单据编号(billnum)
    JSONArray diffBillnums = new JSONArray();
    JSONArray newBillnums = new JSONArray();
    // 收集每条更新的字段变更详情
    JSONArray updatedDetails = new JSONArray();
    // 收集保存失败的记录详情
    JSONArray failedRecords = new JSONArray();
    // 收集调试日志(仅记录失败记录的详细过程)
    JSONArray debugLogs = new JSONArray();
    try {
        int page = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
        String pageSize = Util.null2String(param.getString("pagesize"));
        if ("".equals(pageSize)) pageSize = "10000";

        while (true) {
            param.put("page", String.valueOf(page));
            param.put("pagesize", pageSize);

            JSONObject rjson = LCUtil.doPost(api, param);
            result = rjson;

            // 结束条件
            if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                System.out.println("分页已结束,code=3003,停止请求。最后页码:" + page);
                break;
            }
            if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                System.out.println("接口返回非成功code:" + rjson.getString("code") + ",页码:" + page + ",停止。");
                break;
            }

            JSONArray datas = rjson.getJSONArray("data");
            int pageCount = (datas == null) ? 0 : datas.size();
            totalCount += pageCount;
            System.out.println("第 " + page + " 页获取到 " + pageCount + " 条数据,开始增量同步...");

            if (datas != null) {
                for (int x = 0; x < datas.size(); x++) {
                    JSONObject item = datas.getJSONObject(x);
                    Map<String, String> data = new HashMap<>();
                    String currentBillnum = "";
                    String currentCgjsdid = "";
                    try {
                        // 映射字段
                        currentCgjsdid = Util.null2String(item.get("ID"));
                        currentBillnum = Util.null2String(item.get("BILLNUM"));
                        
                        System.out.println("========================================");
                        System.out.println("[开始处理] 单据编号: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        
                        data.put("cgjsdid", currentCgjsdid);
                        data.put("billnum", currentBillnum);
                        data.put("WLDW", Util.null2String(item.get("WLDW")));
                        data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                        data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                        data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                        data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                        data.put("BMBH", Util.null2String(item.get("BMBH")));
                        data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                        data.put("summary", Util.null2String(item.get("SUMMARY")));
                        data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                        data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                        data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                        data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                        data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                        data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                        data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                        data.put("dwbh", Util.null2String(item.get("DWBH")));
                        data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                        data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                        data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                        data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                        data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                        data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                        data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                        data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                        data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                        data.put("XMID", Util.null2String(item.get("XMID")));
                        data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                        data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                        data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                        data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                        if (item.get("BILLDATE") != null) {
                            data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
                        }
                        data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                        data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                        // 金额字段:统一两位小数
                        data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                        data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                        data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                        data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                        data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                        data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                        data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                        data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                        // availableforeigncurrency 字段在数据库表中不存在,已跳过
                        // data.put("availableforeigncurrency", formatTo2Decimals(item.getString("AVAILABLEFOREIGNCURRENCY")));

                        System.out.println("[字段映射完成] billnum: " + currentBillnum + ", 准备保存...");
                        
                        int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords);
                        if (action == 1) {
                            updateCount++;
                            diffBillnums.add(data.get("billnum"));
                        } else if (action == 2) {
                            newCount++;
                            newBillnums.add(data.get("billnum"));
                        } else if (action == -1) {
                            failCount++;
                            System.out.println("[保存失败] billnum: " + currentBillnum);
                        } else {
                            skipCount++;
                        }
                    } catch (Exception ee) {
                        failCount++;
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        System.out.println("[严重错误] billnum: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        System.out.println("[错误信息] " + ee.getMessage());
                        System.out.println("[堆栈跟踪开始]");
                        ee.printStackTrace();
                        System.out.println("[堆栈跟踪结束]");
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        
                        // 记录失败详情
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", currentBillnum);
                        failedRecord.put("cgjsdid", currentCgjsdid);
                        failedRecord.put("errorMessage", ee.getMessage());
                        failedRecord.put("errorType", ee.getClass().getName());
                        failedRecords.add(failedRecord);
                    }
                }
            }

            // 每页结束后可执行一次关联更新
            try {
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
            } catch (Exception ex) {
                ex.printStackTrace();
                result.put("updateError", "第" + page + "页 关联字段更新异常:" + ex.getMessage());
            }

            System.out.println("第 " + page + " 页处理完成,累计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount);
            page++;
        }

        String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 分页处理完成";
        result.put("updateInfo", syncInfo);
        System.out.println(syncInfo);
        System.out.println("========================================");
        System.out.println("同步完成汇总:");
        System.out.println("总数: " + totalCount);
        System.out.println("新增: " + newCount);
        System.out.println("更新: " + updateCount);
        System.out.println("跳过: " + skipCount);
        System.out.println("失败: " + failCount);
        if (failCount > 0) {
            System.out.println("失败记录详情:");
            for (int i = 0; i < failedRecords.size(); i++) {
                JSONObject failed = failedRecords.getJSONObject(i);
                System.out.println("  [" + (i+1) + "] billnum=" + failed.getString("billnum") + 
                                 ", cgjsdid=" + failed.getString("cgjsdid") +
                                 ", operation=" + failed.getString("operation") +
                                 ", error=" + failed.getString("errorMessage"));
            }
        }
        System.out.println("========================================");
        
        // 将差异与新增的单据编号(billnum)放入结果
        result.put("diffBillnums", diffBillnums);
        result.put("newBillnums", newBillnums);
        // 返回每条更新的具体字段变化
        result.put("updatedDetails", updatedDetails);
        // 返回失败记录
        if (failCount > 0) {
            result.put("failedRecords", failedRecords);
            result.put("failCount", failCount);
        }
        // 返回调试日志
        if (debugLogs.size() > 0) {
            result.put("debugLogs", debugLogs);
        }
    } catch (Exception e) {
        e.printStackTrace();
        result.put("error", "增量同步数据异常: " + e.getMessage());
    }
    return result;
}

/**
 * 调试辅助:打印 ModeUtil.saveModeData 可能使用的表信息
 */
public String debugTableInfo(String tablename, RecordSet rs) {
    StringBuilder info = new StringBuilder();
    try {
        // 获取表的列信息
        rs.executeQuery("SHOW COLUMNS FROM " + tablename);
        info.append("[表结构信息] ").append(tablename).append(":\n");
        while (rs.next()) {
            String field = rs.getString("Field");
            String type = rs.getString("Type");
            String nullAllowed = rs.getString("Null");
            String key = rs.getString("Key");
            String defaultVal = rs.getString("Default");
            info.append("  ").append(field).append(" ").append(type);
            if ("NO".equals(nullAllowed)) info.append(" NOT NULL");
            if ("PRI".equals(key)) info.append(" PRIMARY KEY");
            if (defaultVal != null) info.append(" DEFAULT ").append(defaultVal);
            info.append("\n");
        }
    } catch (Exception e) {
        info.append("获取表结构失败: ").append(e.getMessage());
    }
    return info.toString();
}
%>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONArray" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="weaver.general.Util" %>
<%@ page import="dev.soft.lb.util.CommonUtil" %>
<%@ page import="dev.soft.lb.util.LCUtil" %>
<%@ page import="weaver.general.TimeUtil" %>
<%@ page import="weaver.conn.RecordSet" %>
<%@ page import="weaver.hrm.company.SubCompanyComInfo" %>
<%@ page import="weaver.formmode.setup.ModeRightInfo" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.math.BigDecimal" %>
<%@ page import="java.math.RoundingMode" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.UUID" %>

<%
// 设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
// 允许跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

// 定义常量
final String companyId = "66";
final String billtype = "12";
String api = "";
final String tablename = "uf_cgjsd";
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 创建返回结果
JSONObject result = new JSONObject();

try {
    // 获取单位编码
    SubCompanyComInfo scci = new SubCompanyComInfo();
    String dwbm = scci.getSubCompanyCode(companyId);

    // 获取API配置
    RecordSet rs = new RecordSet();
    Map<String, String> billconfig = LCUtil.getLCBillTypeInfo(billtype, rs);
    api = billconfig.get("api_list");

    // 先做一次去重(不清空表,做增量同步)
//    deduplicateDatabase(rs, tablename);

    // 构建请求参数 - 分页增量拉取
    JSONObject paramJson = new JSONObject();
    paramJson.put("dwbh", dwbm);
    paramJson.put("pagesize", "10000");
    paramJson.put("page", "1");

    // 执行增量同步
    JSONObject syncResult = syncData(paramJson, rs, api, tablename, sdf);
    result.put("status", 1);
    result.put("data", syncResult);
    result.put("msg", "增量同步采购结算单成功");
    // 仅保留核心关心内容(差异单据编号billnum、新增单据编号billnum)在顶层返回,便于前端直接消费
    if (syncResult.containsKey("diffBillnums")) {
        result.put("diffBillnums", syncResult.getJSONArray("diffBillnums"));
    }
    if (syncResult.containsKey("newBillnums")) {
        result.put("newBillnums", syncResult.getJSONArray("newBillnums"));
    }
    // 返回每条更新的具体字段变化(字段名、旧值、新值)
    if (syncResult.containsKey("updatedDetails")) {
        result.put("updatedDetails", syncResult.getJSONArray("updatedDetails"));
    }

    // 添加详细的同步信息
    if (syncResult.containsKey("updateInfo")) {
        result.put("updateInfo", syncResult.getString("updateInfo"));
    }
    if (syncResult.containsKey("updateError")) {
        result.put("updateError", syncResult.getString("updateError"));
    }
    // 添加失败记录信息
    if (syncResult.containsKey("failedRecords")) {
        result.put("failedRecords", syncResult.getJSONArray("failedRecords"));
    }
    if (syncResult.containsKey("failCount")) {
        result.put("failCount", syncResult.getInteger("failCount"));
    }

} catch (Exception e) {
    e.printStackTrace();
    result.put("status", 0);
    result.put("msg", "读取浪潮采购结算单异常:" + e.getMessage());
    result.put("errorDetail", e.toString());
}

// 输出JSON结果
out.print(result.toJSONString());

/**
 * 同步数据方法
 */
%>

<%!
private static final int DEFAULT_USER_ID = 1;

/**
 * 数据库去重方法 - 删除重复数据,保留时间最新的一条
 */
public void deduplicateDatabase(RecordSet rs, String tablename) {
    try {
        String sql = "DELETE t1 FROM " + tablename + " t1 " +
                    "INNER JOIN " + tablename + " t2 " +
                    "WHERE t1.id < t2.id " +
                    "AND t1.cgjsdid = t2.cgjsdid " +
                    "AND t1.billnum = t2.billnum " +
                    "AND t1.WLDW = t2.WLDW " +
                    "AND t1.DEPARTMENTID = t2.DEPARTMENTID " +
                    "AND t1.contractid = t2.contractid " +
                    "AND t1.XMID = t2.XMID " +
                    "AND t1.Kxxz = t2.Kxxz " +
                    "AND t1.amount = t2.amount " +
                    "AND t1.Billdate = t2.Billdate";
        rs.execute(sql);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 数值字段名单:这些字段按两位小数口径进行比较与存储
private static final Set<String> NUMERIC_FIELDS = new HashSet<>(Arrays.asList(
        "amount",
        "foreigncurrency",
        "clearamount",
        "clearforeigncurrency",
        "balanceamount",
        "balanceforeigncurrency",
        "availableamount",
        // "availableforeigncurrency",  // 表中不存在此字段
        // 添加其他可能的数值字段
        "original_clearamount",
        "original_clearforeigncurrency",
        "original_balanceamount",
        "original_balanceforeigncurrency",
        "price",
        "unitprice",
        "quantity",
        "taxrate",
        "taxamount",
        "totalmoney",
        "netamount",
        "amountnotax"
));

// 规范化字符串:去首尾空格;null按空串处理
private static String norm(String v) {
    if (v == null) return "";
    return v.trim();
}

// 字符串等价:规范化后比较;空串与null等价
private static boolean equalNormalized(String a, String b) {
    return norm(a).equals(norm(b));
}

// 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
private static boolean equalNumeric(String a, String b) {
    String sa = norm(a);
    String sb = norm(b);
    if (sa.isEmpty() && sb.isEmpty()) return true;
    if (sa.isEmpty() || sb.isEmpty()) return false;
    try {
        BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
        BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
        return ba.compareTo(bb) == 0;
    } catch (Exception ignore) {
        return sa.equals(sb);
    }
}

// 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
private static String formatTo2Decimals(String v) {
    String s = norm(v);
    if (s.isEmpty()) return s;
    try {
        return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
    } catch (Exception ignore) {
        return s;
    }
}

// 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
public boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return true; // 无法获取字段信息,默认认为有变更
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                System.out.println("[字段差异] " + columnName + ": 数据库值='" + dbValue + "', 接口值='" + newValue + "'");
                return true; // 发现差异
            }
        }
        return false; // 所有字段都相同
    } catch (Exception e) {
        e.printStackTrace();
        // 出现异常时,默认认为有变更,以保证数据一致性
        return true;
    }
}

// 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
public JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
    JSONArray changes = new JSONArray();
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return changes; // 无法获取字段信息
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                JSONObject diff = new JSONObject();
                diff.put("field", columnName);
                diff.put("oldValue", dbValue);
                diff.put("newValue", newValue);
                changes.add(diff);
            }
        }
    } catch (Exception e) {
        // 忽略,返回当前已收集到的差异
    }
    return changes;
}

// 先对比后插入/更新:按 (cgjsdid,billnum) 定位;差异则按原id更新;不存在则插入
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename,
                                         JSONArray updateDetails, JSONArray failedRecords, Set<Integer> affectedIds) {
    String billnum = Util.null2String(data.get("billnum"));
    String cgjsdid = Util.null2String(data.get("cgjsdid"));
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", billnum);
                    detail.put("cgjsdid", cgjsdid);
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                if (changes != null && changes.size() > 0) {
                    if (applyUpdate(existingId, changes, tablename)) {
                        if (affectedIds != null) {
                            affectedIds.add(existingId);
                        }
                        return 1;
                    }
                    throw new RuntimeException("更新采购结算单失败");
                }
            }
            return 0;
        }

        Integer newId = insertRecord(data, tablename);
        if (newId != null && newId > 0) {
            if (affectedIds != null) {
                affectedIds.add(newId);
            }
            return 2;
        }
        throw new RuntimeException("新增采购结算单失败");
    } catch (Exception e) {
        if (failedRecords != null) {
            JSONObject failedRecord = new JSONObject();
            failedRecord.put("billnum", billnum);
            failedRecord.put("cgjsdid", cgjsdid);
            failedRecord.put("errorMessage", e.getMessage());
            failedRecord.put("operation", "upsert");
            failedRecords.add(failedRecord);
        }
        return -1;
    }
}

private boolean applyUpdate(int recordId, JSONArray changes, String tablename) {
    if (recordId <= 0 || changes == null || changes.isEmpty()) {
        return false;
    }
    StringBuilder setClause = new StringBuilder();
    for (int i = 0; i < changes.size(); i++) {
        JSONObject change = changes.getJSONObject(i);
        String field = Util.null2String(change.getString("field"));
        String value = Util.null2String(change.getString("newValue"));
        if (field.isEmpty()) {
            continue;
        }
        if (setClause.length() > 0) {
            setClause.append(",");
        }
        setClause.append(field).append("=").append(toSqlValue(value));
    }
    if (setClause.length() == 0) {
        return false;
    }
    String sql = "UPDATE " + tablename + " SET " + setClause + " WHERE id=" + recordId;
    RecordSet updateRs = new RecordSet();
    return updateRs.execute(sql);
}

private Integer insertRecord(Map<String, String> data, String tablename) {
    if (data == null || data.isEmpty()) {
        return null;
    }
    Map<String, String> insertMap = new HashMap<>(data);
    String now = TimeUtil.getCurrentTimeString();
    String createDate = now.substring(0, 10);
    String createTime = now.substring(11);
    insertMap.put("formmodeid", String.valueOf(getModeId(tablename)));
    insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
    insertMap.put("modedatacreatertype", "1");
    insertMap.put("modedatacreatedate", createDate);
    insertMap.put("modedatacreatetime", createTime);
    insertMap.put("modeuuid", UUID.randomUUID().toString());

    StringBuilder columns = new StringBuilder();
    StringBuilder values = new StringBuilder();
    for (Map.Entry<String, String> entry : insertMap.entrySet()) {
        if (columns.length() > 0) {
            columns.append(",");
            values.append(",");
        }
        columns.append(entry.getKey());
        values.append(toSqlValue(entry.getValue()));
    }

    RecordSet insertRs = new RecordSet();
    String sql = "INSERT INTO " + tablename + "(" + columns + ") VALUES(" + values + ")";
    if (!insertRs.execute(sql)) {
        return null;
    }
    RecordSet idRs = new RecordSet();
    idRs.executeQuery("SELECT id FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?",
            Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
    if (idRs.next()) {
        return idRs.getInt("id");
    }
    return null;
}

private String toSqlValue(String value) {
    String normalized = Util.null2String(value);
    if (normalized.isEmpty()) {
        return "null";
    }
    return "'" + normalized.replace("'", "''") + "'";
}

private void rebuildPermissions(Set<Integer> affectedIds, String tablename) {
    if (affectedIds == null || affectedIds.isEmpty()) {
        return;
    }
    try {
        int modeId = getModeId(tablename);
        if (modeId <= 0) {
            return;
        }
        ModeRightInfo rightInfo = new ModeRightInfo();
        rightInfo.setNewRight(false);
        rightInfo.init();
        rightInfo.setModeId(modeId);
        for (Integer billId : affectedIds) {
            if (billId == null || billId <= 0) {
                continue;
            }
            rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private int getModeId(String tablename) {
    try {
        return Util.getIntValue(CommonUtil.getModeIdByTableName(tablename));
    } catch (Exception e) {
        return 0;
    }
}

public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
    JSONObject result = new JSONObject();
    int totalCount = 0;
    int newCount = 0;
    int updateCount = 0;
    int skipCount = 0;
    int failCount = 0;
    // 收集差异更新与新增的单据编号(billnum)
    JSONArray diffBillnums = new JSONArray();
    JSONArray newBillnums = new JSONArray();
    // 收集每条更新的字段变更详情
    JSONArray updatedDetails = new JSONArray();
    // 收集保存失败的记录详情
    JSONArray failedRecords = new JSONArray();
    // 收集调试日志(仅记录失败记录的详细过程)
    JSONArray debugLogs = new JSONArray();
    Set<Integer> affectedIds = new HashSet<>();
    try {
        int page = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
        String pageSize = Util.null2String(param.getString("pagesize"));
        if ("".equals(pageSize)) pageSize = "10000";

        while (true) {
            param.put("page", String.valueOf(page));
            param.put("pagesize", pageSize);

            JSONObject rjson = LCUtil.doPost(api, param);
            result = rjson;

            // 结束条件
            if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                System.out.println("分页已结束,code=3003,停止请求。最后页码:" + page);
                break;
            }
            if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                System.out.println("接口返回非成功code:" + rjson.getString("code") + ",页码:" + page + ",停止。");
                break;
            }

            JSONArray datas = rjson.getJSONArray("data");
            int pageCount = (datas == null) ? 0 : datas.size();
            totalCount += pageCount;
            System.out.println("第 " + page + " 页获取到 " + pageCount + " 条数据,开始增量同步...");

            if (datas != null) {
                for (int x = 0; x < datas.size(); x++) {
                    JSONObject item = datas.getJSONObject(x);
                    Map<String, String> data = new HashMap<>();
                    String currentBillnum = "";
                    String currentCgjsdid = "";
                    try {
                        // 映射字段
                        currentCgjsdid = Util.null2String(item.get("ID"));
                        currentBillnum = Util.null2String(item.get("BILLNUM"));
                        
                        System.out.println("========================================");
                        System.out.println("[开始处理] 单据编号: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        
                        data.put("cgjsdid", currentCgjsdid);
                        data.put("billnum", currentBillnum);
                        data.put("WLDW", Util.null2String(item.get("WLDW")));
                        data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                        data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                        data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                        data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                        data.put("BMBH", Util.null2String(item.get("BMBH")));
                        data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                        data.put("summary", Util.null2String(item.get("SUMMARY")));
                        data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                        data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                        data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                        data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                        data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                        data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                        data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                        data.put("dwbh", Util.null2String(item.get("DWBH")));
                        data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                        data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                        data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                        data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                        data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                        data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                        data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                        data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                        data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                        data.put("XMID", Util.null2String(item.get("XMID")));
                        data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                        data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                        data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                        data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                        if (item.get("BILLDATE") != null) {
                            data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
                        }
                        data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                        data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                        // 金额字段:统一两位小数
                        data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                        data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                        data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                        data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                        data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                        data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                        data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                        data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                        // availableforeigncurrency 字段在数据库表中不存在,已跳过
                        // data.put("availableforeigncurrency", formatTo2Decimals(item.getString("AVAILABLEFOREIGNCURRENCY")));

                        System.out.println("[字段映射完成] billnum: " + currentBillnum + ", 准备保存...");
                        
                        int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords, affectedIds);
                        if (action == 1) {
                            updateCount++;
                            diffBillnums.add(data.get("billnum"));
                        } else if (action == 2) {
                            newCount++;
                            newBillnums.add(data.get("billnum"));
                        } else if (action == -1) {
                            failCount++;
                            System.out.println("[保存失败] billnum: " + currentBillnum);
                        } else {
                            skipCount++;
                        }
                    } catch (Exception ee) {
                        failCount++;
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        System.out.println("[严重错误] billnum: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        System.out.println("[错误信息] " + ee.getMessage());
                        System.out.println("[堆栈跟踪开始]");
                        ee.printStackTrace();
                        System.out.println("[堆栈跟踪结束]");
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        
                        // 记录失败详情
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", currentBillnum);
                        failedRecord.put("cgjsdid", currentCgjsdid);
                        failedRecord.put("errorMessage", ee.getMessage());
                        failedRecord.put("errorType", ee.getClass().getName());
                        failedRecords.add(failedRecord);
                    }
                }
            }

            // 每页结束后可执行一次关联更新
            try {
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
            } catch (Exception ex) {
                ex.printStackTrace();
                result.put("updateError", "第" + page + "页 关联字段更新异常:" + ex.getMessage());
            }

            System.out.println("第 " + page + " 页处理完成,累计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount);
            page++;
        }

        rebuildPermissions(affectedIds, tablename);

        String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 分页处理完成";
        result.put("updateInfo", syncInfo);
        System.out.println(syncInfo);
        System.out.println("========================================");
        System.out.println("同步完成汇总:");
        System.out.println("总数: " + totalCount);
        System.out.println("新增: " + newCount);
        System.out.println("更新: " + updateCount);
        System.out.println("跳过: " + skipCount);
        System.out.println("失败: " + failCount);
        if (failCount > 0) {
            System.out.println("失败记录详情:");
            for (int i = 0; i < failedRecords.size(); i++) {
                JSONObject failed = failedRecords.getJSONObject(i);
                System.out.println("  [" + (i+1) + "] billnum=" + failed.getString("billnum") + 
                                 ", cgjsdid=" + failed.getString("cgjsdid") +
                                 ", operation=" + failed.getString("operation") +
                                 ", error=" + failed.getString("errorMessage"));
            }
        }
        System.out.println("========================================");
        
        // 将差异与新增的单据编号(billnum)放入结果
        result.put("diffBillnums", diffBillnums);
        result.put("newBillnums", newBillnums);
        // 返回每条更新的具体字段变化
        result.put("updatedDetails", updatedDetails);
        // 返回失败记录
        if (failCount > 0) {
            result.put("failedRecords", failedRecords);
            result.put("failCount", failCount);
        }
        // 返回调试日志
        if (debugLogs.size() > 0) {
            result.put("debugLogs", debugLogs);
        }
    } catch (Exception e) {
        e.printStackTrace();
        result.put("error", "增量同步数据异常: " + e.getMessage());
    }
    return result;
}

/**
 * 调试辅助:打印 ModeUtil.saveModeData 可能使用的表信息
 */
public String debugTableInfo(String tablename, RecordSet rs) {
    StringBuilder info = new StringBuilder();
    try {
        // 获取表的列信息
        rs.executeQuery("SHOW COLUMNS FROM " + tablename);
        info.append("[表结构信息] ").append(tablename).append(":\n");
        while (rs.next()) {
            String field = rs.getString("Field");
            String type = rs.getString("Type");
            String nullAllowed = rs.getString("Null");
            String key = rs.getString("Key");
            String defaultVal = rs.getString("Default");
            info.append("  ").append(field).append(" ").append(type);
            if ("NO".equals(nullAllowed)) info.append(" NOT NULL");
            if ("PRI".equals(key)) info.append(" PRIMARY KEY");
            if (defaultVal != null) info.append(" DEFAULT ").append(defaultVal);
            info.append("\n");
        }
    } catch (Exception e) {
        info.append("获取表结构失败: ").append(e.getMessage());
    }
    return info.toString();
}
%>

  • JSP 同步入口与 syncCGJSD 对齐:接口分页时累计 affectedIds,每条记录通过新的 processRecordCompareAndUpsert 先比对再 SQL 插/更新,并在分页结束后一次性调用 ModeRightInfo 重构权限,避免循环内多次 ModeUtil.save
    Set<Integer> affectedIds = new HashSet<>();
    ...
    int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords, affectedIds);
    ...
    rebuildPermissions(affectedIds, tablename);
  • 引入与 Java 版一致的工具方法:applyUpdate/insertRecord 直接组装 SQL,构造 modeuuidformmodeid 元数据,并提供 toSqlValuegetModeIdrebuildPermissions 等辅助,完全抛弃 ModeUtil.saveModeData 的逐条调用。
private static final int DEFAULT_USER_ID = 1;
...
private boolean applyUpdate(int recordId, JSONArray changes, String tablename) { ... }
private Integer insertRecord(Map<String, String> data, String tablename) { ... }
private void rebuildPermissions(Set<Integer> affectedIds, String tablename) { ... }

现在 JSP 调试页面与定时任务共享同一套增量同步与权限重构逻辑,可直接用于联调测试。

    Set<Integer> affectedIds = new HashSet<>();
    ...
    int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords, affectedIds);
    ...
    rebuildPermissions(affectedIds, tablename);
private static final int DEFAULT_USER_ID = 1;
...
private boolean applyUpdate(int recordId, JSONArray changes, String tablename) { ... }
private Integer insertRecord(Map<String, String> data, String tablename) { ... }
private void rebuildPermissions(Set<Integer> affectedIds, String tablename) { ... }

User

@syncLCCGJSD.jsp 这里的分页调整为多少可以减少消耗时间(以10万条数据为准,目前2000条数据需要消耗3.4MB内存)


Cursor

  • 你现在单页 2 000 条就吃掉 3.4 MB(≈1.7 KB/条)。如果线性估算,10 000 条就要 17 MB,20 000 条约 34 MB,40 000 条超过 68 MB。JSP 容器通常 512 MB~1 GB 堆,分页太大很容易被单页 JSON 打爆或触发 GC pauses。
  • 另一方面,接口响应一次要 3‑4 s。如果把 pagesize 设得太小,比如 500 条,就得发 200 次请求(100 000 ÷ 500),网络与序列化的累计时间会远超 10 s。

推荐策略:pagesize 设在 3 000~5 000 之间

  • 单页内存大约 5 MB~8.5 MB,可控。
  • 100 000 条分 20~34 页,请求时长 ≈ 20×4 s = 80 s(最坏)或更少,如果接口允许并发,可以多线程拉取多页进一步压缩时间。
  • 当你改完分页后,配合 JSP 版的本地 upsert & 权限批处理,本地处理时间基本就是 SQL 执行+权限重构,不再受单页记录数的平方级影响。

如果需要更激进的压缩时间,可以:

  • 让接口端支持 lastModified 增量过滤,常规 10 min 周期只处理少量记录;
  • 或者将页数据写入临时表,再在数据库里做 INSERT ... SELECTUPDATE ... JOIN,这样数据分批写入不会占用 JSP 内存。

User

这是200条数据的实际请求时长和占用 410ms 339.42K


Cursor

  • 200 条 → 0.34 MB / 410 ms。按线性估算:
    • 2 000 条 ≈ 3.4 MB / 4.1 s
    • 5 000 条 ≈ 8.5 MB / 10.3 s
    • 10 000 条 ≈ 17 MB / 20.5 s
  • 结合 JSP 堆内存与接口 3‑4 s RTT,建议测试时把 pagesize 设在 2 000~4 000 区间:内存 <7 MB,单页时长 <8 s,还能把 10 万条控制在 25~50 个请求。
  • 若要进一步缩短总时长,优先:① 引入增量条件减少总条数;② 允许并行请求多页;③ 把页数据直接写临时表,由数据库批量更新,JSP 只做调度。

User

使用多线程进行,jsp加载的时候自动创建10个线程发起请求,如果线程中请求返回的内容为3003则停止继续发起,并销毁线程。如果自动创建的线程中还没有请求到3003则继续递增发起,最大线程数20,如果超过20则就先处理完这20个线程的内容后销毁了继续发起,直到处理结束为止


Cursor

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONArray" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="weaver.general.Util" %>
<%@ page import="dev.soft.lb.util.CommonUtil" %>
<%@ page import="dev.soft.lb.util.LCUtil" %>
<%@ page import="weaver.general.TimeUtil" %>
<%@ page import="weaver.conn.RecordSet" %>
<%@ page import="weaver.hrm.company.SubCompanyComInfo" %>
<%@ page import="weaver.formmode.setup.ModeRightInfo" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.math.BigDecimal" %>
<%@ page import="java.math.RoundingMode" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.UUID" %>
<%@ page import="java.util.concurrent.Callable" %>
<%@ page import="java.util.concurrent.CompletionService" %>
<%@ page import="java.util.concurrent.ExecutorCompletionService" %>
<%@ page import="java.util.concurrent.ExecutorService" %>
<%@ page import="java.util.concurrent.Executors" %>
<%@ page import="java.util.concurrent.Future" %>
<%@ page import="java.util.concurrent.atomic.AtomicBoolean" %>
<%@ page import="java.util.concurrent.atomic.AtomicInteger" %>

<%
// 设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
// 允许跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

// 定义常量
final String companyId = "66";
final String billtype = "12";
String api = "";
final String tablename = "uf_cgjsd";
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 创建返回结果
JSONObject result = new JSONObject();

try {
    // 获取单位编码
    SubCompanyComInfo scci = new SubCompanyComInfo();
    String dwbm = scci.getSubCompanyCode(companyId);

    // 获取API配置
    RecordSet rs = new RecordSet();
    Map<String, String> billconfig = LCUtil.getLCBillTypeInfo(billtype, rs);
    api = billconfig.get("api_list");

    // 先做一次去重(不清空表,做增量同步)
//    deduplicateDatabase(rs, tablename);

    // 构建请求参数 - 分页增量拉取
    JSONObject paramJson = new JSONObject();
    paramJson.put("dwbh", dwbm);
    paramJson.put("pagesize", "10000");
    paramJson.put("page", "1");

    // 执行增量同步
    JSONObject syncResult = syncData(paramJson, rs, api, tablename, sdf);
    result.put("status", 1);
    result.put("data", syncResult);
    result.put("msg", "增量同步采购结算单成功");
    // 仅保留核心关心内容(差异单据编号billnum、新增单据编号billnum)在顶层返回,便于前端直接消费
    if (syncResult.containsKey("diffBillnums")) {
        result.put("diffBillnums", syncResult.getJSONArray("diffBillnums"));
    }
    if (syncResult.containsKey("newBillnums")) {
        result.put("newBillnums", syncResult.getJSONArray("newBillnums"));
    }
    // 返回每条更新的具体字段变化(字段名、旧值、新值)
    if (syncResult.containsKey("updatedDetails")) {
        result.put("updatedDetails", syncResult.getJSONArray("updatedDetails"));
    }

    // 添加详细的同步信息
    if (syncResult.containsKey("updateInfo")) {
        result.put("updateInfo", syncResult.getString("updateInfo"));
    }
    if (syncResult.containsKey("updateError")) {
        result.put("updateError", syncResult.getString("updateError"));
    }
    // 添加失败记录信息
    if (syncResult.containsKey("failedRecords")) {
        result.put("failedRecords", syncResult.getJSONArray("failedRecords"));
    }
    if (syncResult.containsKey("failCount")) {
        result.put("failCount", syncResult.getInteger("failCount"));
    }

} catch (Exception e) {
    e.printStackTrace();
    result.put("status", 0);
    result.put("msg", "读取浪潮采购结算单异常:" + e.getMessage());
    result.put("errorDetail", e.toString());
}

// 输出JSON结果
out.print(result.toJSONString());

/**
 * 同步数据方法
 */
%>

<%!
private static final int DEFAULT_USER_ID = 1;

/**
 * 数据库去重方法 - 删除重复数据,保留时间最新的一条
 */
public void deduplicateDatabase(RecordSet rs, String tablename) {
    try {
        String sql = "DELETE t1 FROM " + tablename + " t1 " +
                    "INNER JOIN " + tablename + " t2 " +
                    "WHERE t1.id < t2.id " +
                    "AND t1.cgjsdid = t2.cgjsdid " +
                    "AND t1.billnum = t2.billnum " +
                    "AND t1.WLDW = t2.WLDW " +
                    "AND t1.DEPARTMENTID = t2.DEPARTMENTID " +
                    "AND t1.contractid = t2.contractid " +
                    "AND t1.XMID = t2.XMID " +
                    "AND t1.Kxxz = t2.Kxxz " +
                    "AND t1.amount = t2.amount " +
                    "AND t1.Billdate = t2.Billdate";
        rs.execute(sql);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 数值字段名单:这些字段按两位小数口径进行比较与存储
private static final Set<String> NUMERIC_FIELDS = new HashSet<>(Arrays.asList(
        "amount",
        "foreigncurrency",
        "clearamount",
        "clearforeigncurrency",
        "balanceamount",
        "balanceforeigncurrency",
        "availableamount",
        // "availableforeigncurrency",  // 表中不存在此字段
        // 添加其他可能的数值字段
        "original_clearamount",
        "original_clearforeigncurrency",
        "original_balanceamount",
        "original_balanceforeigncurrency",
        "price",
        "unitprice",
        "quantity",
        "taxrate",
        "taxamount",
        "totalmoney",
        "netamount",
        "amountnotax"
));

// 规范化字符串:去首尾空格;null按空串处理
private static String norm(String v) {
    if (v == null) return "";
    return v.trim();
}

// 字符串等价:规范化后比较;空串与null等价
private static boolean equalNormalized(String a, String b) {
    return norm(a).equals(norm(b));
}

// 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
private static boolean equalNumeric(String a, String b) {
    String sa = norm(a);
    String sb = norm(b);
    if (sa.isEmpty() && sb.isEmpty()) return true;
    if (sa.isEmpty() || sb.isEmpty()) return false;
    try {
        BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
        BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
        return ba.compareTo(bb) == 0;
    } catch (Exception ignore) {
        return sa.equals(sb);
    }
}

// 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
private static String formatTo2Decimals(String v) {
    String s = norm(v);
    if (s.isEmpty()) return s;
    try {
        return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
    } catch (Exception ignore) {
        return s;
    }
}

// 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
public boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return true; // 无法获取字段信息,默认认为有变更
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                System.out.println("[字段差异] " + columnName + ": 数据库值='" + dbValue + "', 接口值='" + newValue + "'");
                return true; // 发现差异
            }
        }
        return false; // 所有字段都相同
    } catch (Exception e) {
        e.printStackTrace();
        // 出现异常时,默认认为有变更,以保证数据一致性
        return true;
    }
}

// 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
public JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
    JSONArray changes = new JSONArray();
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return changes; // 无法获取字段信息
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                JSONObject diff = new JSONObject();
                diff.put("field", columnName);
                diff.put("oldValue", dbValue);
                diff.put("newValue", newValue);
                changes.add(diff);
            }
        }
    } catch (Exception e) {
        // 忽略,返回当前已收集到的差异
    }
    return changes;
}

// 先对比后插入/更新:按 (cgjsdid,billnum) 定位;差异则按原id更新;不存在则插入
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename,
                                         JSONArray updateDetails, JSONArray failedRecords, Set<Integer> affectedIds) {
    String billnum = Util.null2String(data.get("billnum"));
    String cgjsdid = Util.null2String(data.get("cgjsdid"));
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", billnum);
                    detail.put("cgjsdid", cgjsdid);
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                if (changes != null && changes.size() > 0) {
                    if (applyUpdate(existingId, changes, tablename)) {
                        if (affectedIds != null) {
                            affectedIds.add(existingId);
                        }
                        return 1;
                    }
                    throw new RuntimeException("更新采购结算单失败");
                }
            }
            return 0;
        }

        Integer newId = insertRecord(data, tablename);
        if (newId != null && newId > 0) {
            if (affectedIds != null) {
                affectedIds.add(newId);
            }
            return 2;
        }
        throw new RuntimeException("新增采购结算单失败");
    } catch (Exception e) {
        if (failedRecords != null) {
            JSONObject failedRecord = new JSONObject();
            failedRecord.put("billnum", billnum);
            failedRecord.put("cgjsdid", cgjsdid);
            failedRecord.put("errorMessage", e.getMessage());
            failedRecord.put("operation", "upsert");
            failedRecords.add(failedRecord);
        }
        return -1;
    }
}

private boolean applyUpdate(int recordId, JSONArray changes, String tablename) {
    if (recordId <= 0 || changes == null || changes.isEmpty()) {
        return false;
    }
    StringBuilder setClause = new StringBuilder();
    for (int i = 0; i < changes.size(); i++) {
        JSONObject change = changes.getJSONObject(i);
        String field = Util.null2String(change.getString("field"));
        String value = Util.null2String(change.getString("newValue"));
        if (field.isEmpty()) {
            continue;
        }
        if (setClause.length() > 0) {
            setClause.append(",");
        }
        setClause.append(field).append("=").append(toSqlValue(value));
    }
    if (setClause.length() == 0) {
        return false;
    }
    String sql = "UPDATE " + tablename + " SET " + setClause + " WHERE id=" + recordId;
    RecordSet updateRs = new RecordSet();
    return updateRs.execute(sql);
}

private Integer insertRecord(Map<String, String> data, String tablename) {
    if (data == null || data.isEmpty()) {
        return null;
    }
    Map<String, String> insertMap = new HashMap<>(data);
    String now = TimeUtil.getCurrentTimeString();
    String createDate = now.substring(0, 10);
    String createTime = now.substring(11);
    insertMap.put("formmodeid", String.valueOf(getModeId(tablename)));
    insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
    insertMap.put("modedatacreatertype", "1");
    insertMap.put("modedatacreatedate", createDate);
    insertMap.put("modedatacreatetime", createTime);
    insertMap.put("modeuuid", UUID.randomUUID().toString());

    StringBuilder columns = new StringBuilder();
    StringBuilder values = new StringBuilder();
    for (Map.Entry<String, String> entry : insertMap.entrySet()) {
        if (columns.length() > 0) {
            columns.append(",");
            values.append(",");
        }
        columns.append(entry.getKey());
        values.append(toSqlValue(entry.getValue()));
    }

    RecordSet insertRs = new RecordSet();
    String sql = "INSERT INTO " + tablename + "(" + columns + ") VALUES(" + values + ")";
    if (!insertRs.execute(sql)) {
        return null;
    }
    RecordSet idRs = new RecordSet();
    idRs.executeQuery("SELECT id FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?",
            Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
    if (idRs.next()) {
        return idRs.getInt("id");
    }
    return null;
}

private String toSqlValue(String value) {
    String normalized = Util.null2String(value);
    if (normalized.isEmpty()) {
        return "null";
    }
    return "'" + normalized.replace("'", "''") + "'";
}

private void rebuildPermissions(Set<Integer> affectedIds, String tablename) {
    if (affectedIds == null || affectedIds.isEmpty()) {
        return;
    }
    try {
        int modeId = getModeId(tablename);
        if (modeId <= 0) {
            return;
        }
        ModeRightInfo rightInfo = new ModeRightInfo();
        rightInfo.setNewRight(false);
        rightInfo.init();
        rightInfo.setModeId(modeId);
        for (Integer billId : affectedIds) {
            if (billId == null || billId <= 0) {
                continue;
            }
            rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private int getModeId(String tablename) {
    try {
        return Util.getIntValue(CommonUtil.getModeIdByTableName(tablename));
    } catch (Exception e) {
        return 0;
    }
}

public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
    JSONObject result = new JSONObject();
    int totalCount = 0;
    int newCount = 0;
    int updateCount = 0;
    int skipCount = 0;
    int failCount = 0;
    // 收集差异更新与新增的单据编号(billnum)
    JSONArray diffBillnums = new JSONArray();
    JSONArray newBillnums = new JSONArray();
    // 收集每条更新的字段变更详情
    JSONArray updatedDetails = new JSONArray();
    // 收集保存失败的记录详情
    JSONArray failedRecords = new JSONArray();
    // 收集调试日志(仅记录失败记录的详细过程)
    JSONArray debugLogs = new JSONArray();
    Set<Integer> affectedIds = new HashSet<>();
    try {
        int page = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
        String pageSize = Util.null2String(param.getString("pagesize"));
        if ("".equals(pageSize)) pageSize = "10000";

        while (true) {
            param.put("page", String.valueOf(page));
            param.put("pagesize", pageSize);

            JSONObject rjson = LCUtil.doPost(api, param);
            result = rjson;

            // 结束条件
            if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                System.out.println("分页已结束,code=3003,停止请求。最后页码:" + page);
                break;
            }
            if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                System.out.println("接口返回非成功code:" + rjson.getString("code") + ",页码:" + page + ",停止。");
                break;
            }

            JSONArray datas = rjson.getJSONArray("data");
            int pageCount = (datas == null) ? 0 : datas.size();
            totalCount += pageCount;
            System.out.println("第 " + page + " 页获取到 " + pageCount + " 条数据,开始增量同步...");

            if (datas != null) {
                for (int x = 0; x < datas.size(); x++) {
                    JSONObject item = datas.getJSONObject(x);
                    Map<String, String> data = new HashMap<>();
                    String currentBillnum = "";
                    String currentCgjsdid = "";
                    try {
                        // 映射字段
                        currentCgjsdid = Util.null2String(item.get("ID"));
                        currentBillnum = Util.null2String(item.get("BILLNUM"));
                        
                        System.out.println("========================================");
                        System.out.println("[开始处理] 单据编号: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        
                        data.put("cgjsdid", currentCgjsdid);
                        data.put("billnum", currentBillnum);
                        data.put("WLDW", Util.null2String(item.get("WLDW")));
                        data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                        data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                        data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                        data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                        data.put("BMBH", Util.null2String(item.get("BMBH")));
                        data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                        data.put("summary", Util.null2String(item.get("SUMMARY")));
                        data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                        data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                        data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                        data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                        data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                        data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                        data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                        data.put("dwbh", Util.null2String(item.get("DWBH")));
                        data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                        data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                        data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                        data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                        data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                        data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                        data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                        data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                        data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                        data.put("XMID", Util.null2String(item.get("XMID")));
                        data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                        data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                        data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                        data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                        if (item.get("BILLDATE") != null) {
                            data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
                        }
                        data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                        data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                        // 金额字段:统一两位小数
                        data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                        data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                        data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                        data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                        data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                        data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                        data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                        data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                        // availableforeigncurrency 字段在数据库表中不存在,已跳过
                        // data.put("availableforeigncurrency", formatTo2Decimals(item.getString("AVAILABLEFOREIGNCURRENCY")));

                        System.out.println("[字段映射完成] billnum: " + currentBillnum + ", 准备保存...");
                        
                        int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords, affectedIds);
                        if (action == 1) {
                            updateCount++;
                            diffBillnums.add(data.get("billnum"));
                        } else if (action == 2) {
                            newCount++;
                            newBillnums.add(data.get("billnum"));
                        } else if (action == -1) {
                            failCount++;
                            System.out.println("[保存失败] billnum: " + currentBillnum);
                        } else {
                            skipCount++;
                        }
                    } catch (Exception ee) {
                        failCount++;
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        System.out.println("[严重错误] billnum: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        System.out.println("[错误信息] " + ee.getMessage());
                        System.out.println("[堆栈跟踪开始]");
                        ee.printStackTrace();
                        System.out.println("[堆栈跟踪结束]");
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        
                        // 记录失败详情
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", currentBillnum);
                        failedRecord.put("cgjsdid", currentCgjsdid);
                        failedRecord.put("errorMessage", ee.getMessage());
                        failedRecord.put("errorType", ee.getClass().getName());
                        failedRecords.add(failedRecord);
                    }
                }
            }

            // 每页结束后可执行一次关联更新
            try {
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
            } catch (Exception ex) {
                ex.printStackTrace();
                result.put("updateError", "第" + page + "页 关联字段更新异常:" + ex.getMessage());
            }

            System.out.println("第 " + page + " 页处理完成,累计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount);
            page++;
        }

        rebuildPermissions(affectedIds, tablename);

        String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 分页处理完成";
        result.put("updateInfo", syncInfo);
        System.out.println(syncInfo);
        System.out.println("========================================");
        System.out.println("同步完成汇总:");
        System.out.println("总数: " + totalCount);
        System.out.println("新增: " + newCount);
        System.out.println("更新: " + updateCount);
        System.out.println("跳过: " + skipCount);
        System.out.println("失败: " + failCount);
        if (failCount > 0) {
            System.out.println("失败记录详情:");
            for (int i = 0; i < failedRecords.size(); i++) {
                JSONObject failed = failedRecords.getJSONObject(i);
                System.out.println("  [" + (i+1) + "] billnum=" + failed.getString("billnum") + 
                                 ", cgjsdid=" + failed.getString("cgjsdid") +
                                 ", operation=" + failed.getString("operation") +
                                 ", error=" + failed.getString("errorMessage"));
            }
        }
        System.out.println("========================================");
        
        // 将差异与新增的单据编号(billnum)放入结果
        result.put("diffBillnums", diffBillnums);
        result.put("newBillnums", newBillnums);
        // 返回每条更新的具体字段变化
        result.put("updatedDetails", updatedDetails);
        // 返回失败记录
        if (failCount > 0) {
            result.put("failedRecords", failedRecords);
            result.put("failCount", failCount);
        }
        // 返回调试日志
        if (debugLogs.size() > 0) {
            result.put("debugLogs", debugLogs);
        }
    } catch (Exception e) {
        e.printStackTrace();
        result.put("error", "增量同步数据异常: " + e.getMessage());
    }
    return result;
}

/**
 * 调试辅助:打印 ModeUtil.saveModeData 可能使用的表信息
 */
public String debugTableInfo(String tablename, RecordSet rs) {
    StringBuilder info = new StringBuilder();
    try {
        // 获取表的列信息
        rs.executeQuery("SHOW COLUMNS FROM " + tablename);
        info.append("[表结构信息] ").append(tablename).append(":\n");
        while (rs.next()) {
            String field = rs.getString("Field");
            String type = rs.getString("Type");
            String nullAllowed = rs.getString("Null");
            String key = rs.getString("Key");
            String defaultVal = rs.getString("Default");
            info.append("  ").append(field).append(" ").append(type);
            if ("NO".equals(nullAllowed)) info.append(" NOT NULL");
            if ("PRI".equals(key)) info.append(" PRIMARY KEY");
            if (defaultVal != null) info.append(" DEFAULT ").append(defaultVal);
            info.append("\n");
        }
    } catch (Exception e) {
        info.append("获取表结构失败: ").append(e.getMessage());
    }
    return info.toString();
}
%>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONArray" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="weaver.general.Util" %>
<%@ page import="dev.soft.lb.util.CommonUtil" %>
<%@ page import="dev.soft.lb.util.LCUtil" %>
<%@ page import="weaver.general.TimeUtil" %>
<%@ page import="weaver.conn.RecordSet" %>
<%@ page import="weaver.hrm.company.SubCompanyComInfo" %>
<%@ page import="weaver.formmode.setup.ModeRightInfo" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.math.BigDecimal" %>
<%@ page import="java.math.RoundingMode" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.UUID" %>
<%@ page import="java.util.concurrent.Callable" %>
<%@ page import="java.util.concurrent.CompletionService" %>
<%@ page import="java.util.concurrent.ExecutorCompletionService" %>
<%@ page import="java.util.concurrent.ExecutorService" %>
<%@ page import="java.util.concurrent.Executors" %>
<%@ page import="java.util.concurrent.Future" %>
<%@ page import="java.util.concurrent.atomic.AtomicBoolean" %>
<%@ page import="java.util.concurrent.atomic.AtomicInteger" %>

<%
// 设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
// 允许跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

// 定义常量
final String companyId = "66";
final String billtype = "12";
String api = "";
final String tablename = "uf_cgjsd";
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 创建返回结果
JSONObject result = new JSONObject();

try {
    // 获取单位编码
    SubCompanyComInfo scci = new SubCompanyComInfo();
    String dwbm = scci.getSubCompanyCode(companyId);

    // 获取API配置
    RecordSet rs = new RecordSet();
    Map<String, String> billconfig = LCUtil.getLCBillTypeInfo(billtype, rs);
    api = billconfig.get("api_list");

    // 先做一次去重(不清空表,做增量同步)
//    deduplicateDatabase(rs, tablename);

    // 构建请求参数 - 分页增量拉取
    JSONObject paramJson = new JSONObject();
    paramJson.put("dwbh", dwbm);
    paramJson.put("pagesize", "10000");
    paramJson.put("page", "1");

    // 执行增量同步
    JSONObject syncResult = syncData(paramJson, rs, api, tablename, sdf);
    result.put("status", 1);
    result.put("data", syncResult);
    result.put("msg", "增量同步采购结算单成功");
    // 仅保留核心关心内容(差异单据编号billnum、新增单据编号billnum)在顶层返回,便于前端直接消费
    if (syncResult.containsKey("diffBillnums")) {
        result.put("diffBillnums", syncResult.getJSONArray("diffBillnums"));
    }
    if (syncResult.containsKey("newBillnums")) {
        result.put("newBillnums", syncResult.getJSONArray("newBillnums"));
    }
    // 返回每条更新的具体字段变化(字段名、旧值、新值)
    if (syncResult.containsKey("updatedDetails")) {
        result.put("updatedDetails", syncResult.getJSONArray("updatedDetails"));
    }

    // 添加详细的同步信息
    if (syncResult.containsKey("updateInfo")) {
        result.put("updateInfo", syncResult.getString("updateInfo"));
    }
    if (syncResult.containsKey("updateError")) {
        result.put("updateError", syncResult.getString("updateError"));
    }
    // 添加失败记录信息
    if (syncResult.containsKey("failedRecords")) {
        result.put("failedRecords", syncResult.getJSONArray("failedRecords"));
    }
    if (syncResult.containsKey("failCount")) {
        result.put("failCount", syncResult.getInteger("failCount"));
    }

} catch (Exception e) {
    e.printStackTrace();
    result.put("status", 0);
    result.put("msg", "读取浪潮采购结算单异常:" + e.getMessage());
    result.put("errorDetail", e.toString());
}

// 输出JSON结果
out.print(result.toJSONString());

/**
 * 同步数据方法
 */
%>

<%!
private static final int DEFAULT_USER_ID = 1;
private static final int INITIAL_THREAD_COUNT = 10;
private static final int MAX_THREAD_COUNT = 20;

private static class PageResult {
    final int page;
    final JSONObject response;

    PageResult(int page, JSONObject response) {
        this.page = page;
        this.response = response;
    }
}

/**
 * 数据库去重方法 - 删除重复数据,保留时间最新的一条
 */
public void deduplicateDatabase(RecordSet rs, String tablename) {
    try {
        String sql = "DELETE t1 FROM " + tablename + " t1 " +
                    "INNER JOIN " + tablename + " t2 " +
                    "WHERE t1.id < t2.id " +
                    "AND t1.cgjsdid = t2.cgjsdid " +
                    "AND t1.billnum = t2.billnum " +
                    "AND t1.WLDW = t2.WLDW " +
                    "AND t1.DEPARTMENTID = t2.DEPARTMENTID " +
                    "AND t1.contractid = t2.contractid " +
                    "AND t1.XMID = t2.XMID " +
                    "AND t1.Kxxz = t2.Kxxz " +
                    "AND t1.amount = t2.amount " +
                    "AND t1.Billdate = t2.Billdate";
        rs.execute(sql);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 数值字段名单:这些字段按两位小数口径进行比较与存储
private static final Set<String> NUMERIC_FIELDS = new HashSet<>(Arrays.asList(
        "amount",
        "foreigncurrency",
        "clearamount",
        "clearforeigncurrency",
        "balanceamount",
        "balanceforeigncurrency",
        "availableamount",
        // "availableforeigncurrency",  // 表中不存在此字段
        // 添加其他可能的数值字段
        "original_clearamount",
        "original_clearforeigncurrency",
        "original_balanceamount",
        "original_balanceforeigncurrency",
        "price",
        "unitprice",
        "quantity",
        "taxrate",
        "taxamount",
        "totalmoney",
        "netamount",
        "amountnotax"
));

// 规范化字符串:去首尾空格;null按空串处理
private static String norm(String v) {
    if (v == null) return "";
    return v.trim();
}

// 字符串等价:规范化后比较;空串与null等价
private static boolean equalNormalized(String a, String b) {
    return norm(a).equals(norm(b));
}

// 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
private static boolean equalNumeric(String a, String b) {
    String sa = norm(a);
    String sb = norm(b);
    if (sa.isEmpty() && sb.isEmpty()) return true;
    if (sa.isEmpty() || sb.isEmpty()) return false;
    try {
        BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
        BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
        return ba.compareTo(bb) == 0;
    } catch (Exception ignore) {
        return sa.equals(sb);
    }
}

// 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
private static String formatTo2Decimals(String v) {
    String s = norm(v);
    if (s.isEmpty()) return s;
    try {
        return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
    } catch (Exception ignore) {
        return s;
    }
}

// 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
public boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return true; // 无法获取字段信息,默认认为有变更
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                System.out.println("[字段差异] " + columnName + ": 数据库值='" + dbValue + "', 接口值='" + newValue + "'");
                return true; // 发现差异
            }
        }
        return false; // 所有字段都相同
    } catch (Exception e) {
        e.printStackTrace();
        // 出现异常时,默认认为有变更,以保证数据一致性
        return true;
    }
}

// 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
public JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
    JSONArray changes = new JSONArray();
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return changes; // 无法获取字段信息
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                JSONObject diff = new JSONObject();
                diff.put("field", columnName);
                diff.put("oldValue", dbValue);
                diff.put("newValue", newValue);
                changes.add(diff);
            }
        }
    } catch (Exception e) {
        // 忽略,返回当前已收集到的差异
    }
    return changes;
}

// 先对比后插入/更新:按 (cgjsdid,billnum) 定位;差异则按原id更新;不存在则插入
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename,
                                         JSONArray updateDetails, JSONArray failedRecords, Set<Integer> affectedIds) {
    String billnum = Util.null2String(data.get("billnum"));
    String cgjsdid = Util.null2String(data.get("cgjsdid"));
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", billnum);
                    detail.put("cgjsdid", cgjsdid);
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                if (changes != null && changes.size() > 0) {
                    if (applyUpdate(existingId, changes, tablename)) {
                        if (affectedIds != null) {
                            affectedIds.add(existingId);
                        }
                        return 1;
                    }
                    throw new RuntimeException("更新采购结算单失败");
                }
            }
            return 0;
        }

        Integer newId = insertRecord(data, tablename);
        if (newId != null && newId > 0) {
            if (affectedIds != null) {
                affectedIds.add(newId);
            }
            return 2;
        }
        throw new RuntimeException("新增采购结算单失败");
    } catch (Exception e) {
        if (failedRecords != null) {
            JSONObject failedRecord = new JSONObject();
            failedRecord.put("billnum", billnum);
            failedRecord.put("cgjsdid", cgjsdid);
            failedRecord.put("errorMessage", e.getMessage());
            failedRecord.put("operation", "upsert");
            failedRecords.add(failedRecord);
        }
        return -1;
    }
}

private boolean applyUpdate(int recordId, JSONArray changes, String tablename) {
    if (recordId <= 0 || changes == null || changes.isEmpty()) {
        return false;
    }
    StringBuilder setClause = new StringBuilder();
    for (int i = 0; i < changes.size(); i++) {
        JSONObject change = changes.getJSONObject(i);
        String field = Util.null2String(change.getString("field"));
        String value = Util.null2String(change.getString("newValue"));
        if (field.isEmpty()) {
            continue;
        }
        if (setClause.length() > 0) {
            setClause.append(",");
        }
        setClause.append(field).append("=").append(toSqlValue(value));
    }
    if (setClause.length() == 0) {
        return false;
    }
    String sql = "UPDATE " + tablename + " SET " + setClause + " WHERE id=" + recordId;
    RecordSet updateRs = new RecordSet();
    return updateRs.execute(sql);
}

private Integer insertRecord(Map<String, String> data, String tablename) {
    if (data == null || data.isEmpty()) {
        return null;
    }
    Map<String, String> insertMap = new HashMap<>(data);
    String now = TimeUtil.getCurrentTimeString();
    String createDate = now.substring(0, 10);
    String createTime = now.substring(11);
    insertMap.put("formmodeid", String.valueOf(getModeId(tablename)));
    insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
    insertMap.put("modedatacreatertype", "1");
    insertMap.put("modedatacreatedate", createDate);
    insertMap.put("modedatacreatetime", createTime);
    insertMap.put("modeuuid", UUID.randomUUID().toString());

    StringBuilder columns = new StringBuilder();
    StringBuilder values = new StringBuilder();
    for (Map.Entry<String, String> entry : insertMap.entrySet()) {
        if (columns.length() > 0) {
            columns.append(",");
            values.append(",");
        }
        columns.append(entry.getKey());
        values.append(toSqlValue(entry.getValue()));
    }

    RecordSet insertRs = new RecordSet();
    String sql = "INSERT INTO " + tablename + "(" + columns + ") VALUES(" + values + ")";
    if (!insertRs.execute(sql)) {
        return null;
    }
    RecordSet idRs = new RecordSet();
    idRs.executeQuery("SELECT id FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?",
            Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
    if (idRs.next()) {
        return idRs.getInt("id");
    }
    return null;
}

private String toSqlValue(String value) {
    String normalized = Util.null2String(value);
    if (normalized.isEmpty()) {
        return "null";
    }
    return "'" + normalized.replace("'", "''") + "'";
}

private void rebuildPermissions(Set<Integer> affectedIds, String tablename) {
    if (affectedIds == null || affectedIds.isEmpty()) {
        return;
    }
    try {
        int modeId = getModeId(tablename);
        if (modeId <= 0) {
            return;
        }
        ModeRightInfo rightInfo = new ModeRightInfo();
        rightInfo.setNewRight(false);
        rightInfo.init();
        rightInfo.setModeId(modeId);
        for (Integer billId : affectedIds) {
            if (billId == null || billId <= 0) {
                continue;
            }
            rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private int getModeId(String tablename) {
    try {
        return Util.getIntValue(CommonUtil.getModeIdByTableName(tablename));
    } catch (Exception e) {
        return 0;
    }
}

public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
    JSONObject result = new JSONObject();
    int totalCount = 0;
    int newCount = 0;
    int updateCount = 0;
    int skipCount = 0;
    int failCount = 0;
    // 收集差异更新与新增的单据编号(billnum)
    JSONArray diffBillnums = new JSONArray();
    JSONArray newBillnums = new JSONArray();
    // 收集每条更新的字段变更详情
    JSONArray updatedDetails = new JSONArray();
    // 收集保存失败的记录详情
    JSONArray failedRecords = new JSONArray();
    // 收集调试日志(仅记录失败记录的详细过程)
    JSONArray debugLogs = new JSONArray();
    Set<Integer> affectedIds = new HashSet<>();
    try {
        int page = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
        String pageSize = Util.null2String(param.getString("pagesize"));
        if ("".equals(pageSize)) pageSize = "10000";

        while (true) {
            param.put("page", String.valueOf(page));
            param.put("pagesize", pageSize);

            JSONObject rjson = LCUtil.doPost(api, param);
            result = rjson;

            // 结束条件
            if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                System.out.println("分页已结束,code=3003,停止请求。最后页码:" + page);
                break;
            }
            if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                System.out.println("接口返回非成功code:" + rjson.getString("code") + ",页码:" + page + ",停止。");
                break;
            }

            JSONArray datas = rjson.getJSONArray("data");
            int pageCount = (datas == null) ? 0 : datas.size();
            totalCount += pageCount;
            System.out.println("第 " + page + " 页获取到 " + pageCount + " 条数据,开始增量同步...");

            if (datas != null) {
                for (int x = 0; x < datas.size(); x++) {
                    JSONObject item = datas.getJSONObject(x);
                    Map<String, String> data = new HashMap<>();
                    String currentBillnum = "";
                    String currentCgjsdid = "";
                    try {
                        // 映射字段
                        currentCgjsdid = Util.null2String(item.get("ID"));
                        currentBillnum = Util.null2String(item.get("BILLNUM"));
                        
                        System.out.println("========================================");
                        System.out.println("[开始处理] 单据编号: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        
                        data.put("cgjsdid", currentCgjsdid);
                        data.put("billnum", currentBillnum);
                        data.put("WLDW", Util.null2String(item.get("WLDW")));
                        data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                        data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                        data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                        data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                        data.put("BMBH", Util.null2String(item.get("BMBH")));
                        data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                        data.put("summary", Util.null2String(item.get("SUMMARY")));
                        data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                        data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                        data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                        data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                        data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                        data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                        data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                        data.put("dwbh", Util.null2String(item.get("DWBH")));
                        data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                        data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                        data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                        data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                        data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                        data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                        data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                        data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                        data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                        data.put("XMID", Util.null2String(item.get("XMID")));
                        data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                        data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                        data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                        data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                        if (item.get("BILLDATE") != null) {
                            data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
                        }
                        data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                        data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                        // 金额字段:统一两位小数
                        data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                        data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                        data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                        data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                        data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                        data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                        data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                        data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                        // availableforeigncurrency 字段在数据库表中不存在,已跳过
                        // data.put("availableforeigncurrency", formatTo2Decimals(item.getString("AVAILABLEFOREIGNCURRENCY")));

                        System.out.println("[字段映射完成] billnum: " + currentBillnum + ", 准备保存...");
                        
                        int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords, affectedIds);
                        if (action == 1) {
                            updateCount++;
                            diffBillnums.add(data.get("billnum"));
                        } else if (action == 2) {
                            newCount++;
                            newBillnums.add(data.get("billnum"));
                        } else if (action == -1) {
                            failCount++;
                            System.out.println("[保存失败] billnum: " + currentBillnum);
                        } else {
                            skipCount++;
                        }
                    } catch (Exception ee) {
                        failCount++;
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        System.out.println("[严重错误] billnum: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        System.out.println("[错误信息] " + ee.getMessage());
                        System.out.println("[堆栈跟踪开始]");
                        ee.printStackTrace();
                        System.out.println("[堆栈跟踪结束]");
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        
                        // 记录失败详情
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", currentBillnum);
                        failedRecord.put("cgjsdid", currentCgjsdid);
                        failedRecord.put("errorMessage", ee.getMessage());
                        failedRecord.put("errorType", ee.getClass().getName());
                        failedRecords.add(failedRecord);
                    }
                }
            }

            // 每页结束后可执行一次关联更新
            try {
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
            } catch (Exception ex) {
                ex.printStackTrace();
                result.put("updateError", "第" + page + "页 关联字段更新异常:" + ex.getMessage());
            }

            System.out.println("第 " + page + " 页处理完成,累计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount);
            page++;
        }

        rebuildPermissions(affectedIds, tablename);

        String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 分页处理完成";
        result.put("updateInfo", syncInfo);
        System.out.println(syncInfo);
        System.out.println("========================================");
        System.out.println("同步完成汇总:");
        System.out.println("总数: " + totalCount);
        System.out.println("新增: " + newCount);
        System.out.println("更新: " + updateCount);
        System.out.println("跳过: " + skipCount);
        System.out.println("失败: " + failCount);
        if (failCount > 0) {
            System.out.println("失败记录详情:");
            for (int i = 0; i < failedRecords.size(); i++) {
                JSONObject failed = failedRecords.getJSONObject(i);
                System.out.println("  [" + (i+1) + "] billnum=" + failed.getString("billnum") + 
                                 ", cgjsdid=" + failed.getString("cgjsdid") +
                                 ", operation=" + failed.getString("operation") +
                                 ", error=" + failed.getString("errorMessage"));
            }
        }
        System.out.println("========================================");
        
        // 将差异与新增的单据编号(billnum)放入结果
        result.put("diffBillnums", diffBillnums);
        result.put("newBillnums", newBillnums);
        // 返回每条更新的具体字段变化
        result.put("updatedDetails", updatedDetails);
        // 返回失败记录
        if (failCount > 0) {
            result.put("failedRecords", failedRecords);
            result.put("failCount", failCount);
        }
        // 返回调试日志
        if (debugLogs.size() > 0) {
            result.put("debugLogs", debugLogs);
        }
    } catch (Exception e) {
        e.printStackTrace();
        result.put("error", "增量同步数据异常: " + e.getMessage());
    }
    return result;
}

/**
 * 调试辅助:打印 ModeUtil.saveModeData 可能使用的表信息
 */
public String debugTableInfo(String tablename, RecordSet rs) {
    StringBuilder info = new StringBuilder();
    try {
        // 获取表的列信息
        rs.executeQuery("SHOW COLUMNS FROM " + tablename);
        info.append("[表结构信息] ").append(tablename).append(":\n");
        while (rs.next()) {
            String field = rs.getString("Field");
            String type = rs.getString("Type");
            String nullAllowed = rs.getString("Null");
            String key = rs.getString("Key");
            String defaultVal = rs.getString("Default");
            info.append("  ").append(field).append(" ").append(type);
            if ("NO".equals(nullAllowed)) info.append(" NOT NULL");
            if ("PRI".equals(key)) info.append(" PRIMARY KEY");
            if (defaultVal != null) info.append(" DEFAULT ").append(defaultVal);
            info.append("\n");
        }
    } catch (Exception e) {
        info.append("获取表结构失败: ").append(e.getMessage());
    }
    return info.toString();
}
%>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONArray" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="weaver.general.Util" %>
<%@ page import="dev.soft.lb.util.CommonUtil" %>
<%@ page import="dev.soft.lb.util.LCUtil" %>
<%@ page import="weaver.general.TimeUtil" %>
<%@ page import="weaver.conn.RecordSet" %>
<%@ page import="weaver.hrm.company.SubCompanyComInfo" %>
<%@ page import="weaver.formmode.setup.ModeRightInfo" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.math.BigDecimal" %>
<%@ page import="java.math.RoundingMode" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.UUID" %>
<%@ page import="java.util.concurrent.Callable" %>
<%@ page import="java.util.concurrent.CompletionService" %>
<%@ page import="java.util.concurrent.ExecutorCompletionService" %>
<%@ page import="java.util.concurrent.ExecutorService" %>
<%@ page import="java.util.concurrent.Executors" %>
<%@ page import="java.util.concurrent.Future" %>
<%@ page import="java.util.concurrent.atomic.AtomicBoolean" %>
<%@ page import="java.util.concurrent.atomic.AtomicInteger" %>

<%
// 设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
// 允许跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

// 定义常量
final String companyId = "66";
final String billtype = "12";
String api = "";
final String tablename = "uf_cgjsd";
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 创建返回结果
JSONObject result = new JSONObject();

try {
    // 获取单位编码
    SubCompanyComInfo scci = new SubCompanyComInfo();
    String dwbm = scci.getSubCompanyCode(companyId);

    // 获取API配置
    RecordSet rs = new RecordSet();
    Map<String, String> billconfig = LCUtil.getLCBillTypeInfo(billtype, rs);
    api = billconfig.get("api_list");

    // 先做一次去重(不清空表,做增量同步)
//    deduplicateDatabase(rs, tablename);

    // 构建请求参数 - 分页增量拉取
    JSONObject paramJson = new JSONObject();
    paramJson.put("dwbh", dwbm);
    paramJson.put("pagesize", "10000");
    paramJson.put("page", "1");

    // 执行增量同步
    JSONObject syncResult = syncData(paramJson, rs, api, tablename, sdf);
    result.put("status", 1);
    result.put("data", syncResult);
    result.put("msg", "增量同步采购结算单成功");
    // 仅保留核心关心内容(差异单据编号billnum、新增单据编号billnum)在顶层返回,便于前端直接消费
    if (syncResult.containsKey("diffBillnums")) {
        result.put("diffBillnums", syncResult.getJSONArray("diffBillnums"));
    }
    if (syncResult.containsKey("newBillnums")) {
        result.put("newBillnums", syncResult.getJSONArray("newBillnums"));
    }
    // 返回每条更新的具体字段变化(字段名、旧值、新值)
    if (syncResult.containsKey("updatedDetails")) {
        result.put("updatedDetails", syncResult.getJSONArray("updatedDetails"));
    }

    // 添加详细的同步信息
    if (syncResult.containsKey("updateInfo")) {
        result.put("updateInfo", syncResult.getString("updateInfo"));
    }
    if (syncResult.containsKey("updateError")) {
        result.put("updateError", syncResult.getString("updateError"));
    }
    // 添加失败记录信息
    if (syncResult.containsKey("failedRecords")) {
        result.put("failedRecords", syncResult.getJSONArray("failedRecords"));
    }
    if (syncResult.containsKey("failCount")) {
        result.put("failCount", syncResult.getInteger("failCount"));
    }

} catch (Exception e) {
    e.printStackTrace();
    result.put("status", 0);
    result.put("msg", "读取浪潮采购结算单异常:" + e.getMessage());
    result.put("errorDetail", e.toString());
}

// 输出JSON结果
out.print(result.toJSONString());

/**
 * 同步数据方法
 */
%>

<%!
private static final int DEFAULT_USER_ID = 1;
private static final int INITIAL_THREAD_COUNT = 10;
private static final int MAX_THREAD_COUNT = 20;

private static class PageResult {
    final int page;
    final JSONObject response;

    PageResult(int page, JSONObject response) {
        this.page = page;
        this.response = response;
    }
}

/**
 * 数据库去重方法 - 删除重复数据,保留时间最新的一条
 */
public void deduplicateDatabase(RecordSet rs, String tablename) {
    try {
        String sql = "DELETE t1 FROM " + tablename + " t1 " +
                    "INNER JOIN " + tablename + " t2 " +
                    "WHERE t1.id < t2.id " +
                    "AND t1.cgjsdid = t2.cgjsdid " +
                    "AND t1.billnum = t2.billnum " +
                    "AND t1.WLDW = t2.WLDW " +
                    "AND t1.DEPARTMENTID = t2.DEPARTMENTID " +
                    "AND t1.contractid = t2.contractid " +
                    "AND t1.XMID = t2.XMID " +
                    "AND t1.Kxxz = t2.Kxxz " +
                    "AND t1.amount = t2.amount " +
                    "AND t1.Billdate = t2.Billdate";
        rs.execute(sql);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 数值字段名单:这些字段按两位小数口径进行比较与存储
private static final Set<String> NUMERIC_FIELDS = new HashSet<>(Arrays.asList(
        "amount",
        "foreigncurrency",
        "clearamount",
        "clearforeigncurrency",
        "balanceamount",
        "balanceforeigncurrency",
        "availableamount",
        // "availableforeigncurrency",  // 表中不存在此字段
        // 添加其他可能的数值字段
        "original_clearamount",
        "original_clearforeigncurrency",
        "original_balanceamount",
        "original_balanceforeigncurrency",
        "price",
        "unitprice",
        "quantity",
        "taxrate",
        "taxamount",
        "totalmoney",
        "netamount",
        "amountnotax"
));

// 规范化字符串:去首尾空格;null按空串处理
private static String norm(String v) {
    if (v == null) return "";
    return v.trim();
}

// 字符串等价:规范化后比较;空串与null等价
private static boolean equalNormalized(String a, String b) {
    return norm(a).equals(norm(b));
}

// 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
private static boolean equalNumeric(String a, String b) {
    String sa = norm(a);
    String sb = norm(b);
    if (sa.isEmpty() && sb.isEmpty()) return true;
    if (sa.isEmpty() || sb.isEmpty()) return false;
    try {
        BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
        BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
        return ba.compareTo(bb) == 0;
    } catch (Exception ignore) {
        return sa.equals(sb);
    }
}

// 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
private static String formatTo2Decimals(String v) {
    String s = norm(v);
    if (s.isEmpty()) return s;
    try {
        return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
    } catch (Exception ignore) {
        return s;
    }
}

// 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
public boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return true; // 无法获取字段信息,默认认为有变更
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                System.out.println("[字段差异] " + columnName + ": 数据库值='" + dbValue + "', 接口值='" + newValue + "'");
                return true; // 发现差异
            }
        }
        return false; // 所有字段都相同
    } catch (Exception e) {
        e.printStackTrace();
        // 出现异常时,默认认为有变更,以保证数据一致性
        return true;
    }
}

// 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
public JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
    JSONArray changes = new JSONArray();
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return changes; // 无法获取字段信息
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                JSONObject diff = new JSONObject();
                diff.put("field", columnName);
                diff.put("oldValue", dbValue);
                diff.put("newValue", newValue);
                changes.add(diff);
            }
        }
    } catch (Exception e) {
        // 忽略,返回当前已收集到的差异
    }
    return changes;
}

// 先对比后插入/更新:按 (cgjsdid,billnum) 定位;差异则按原id更新;不存在则插入
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename,
                                         JSONArray updateDetails, JSONArray failedRecords, Set<Integer> affectedIds) {
    String billnum = Util.null2String(data.get("billnum"));
    String cgjsdid = Util.null2String(data.get("cgjsdid"));
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", billnum);
                    detail.put("cgjsdid", cgjsdid);
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                if (changes != null && changes.size() > 0) {
                    if (applyUpdate(existingId, changes, tablename)) {
                        if (affectedIds != null) {
                            affectedIds.add(existingId);
                        }
                        return 1;
                    }
                    throw new RuntimeException("更新采购结算单失败");
                }
            }
            return 0;
        }

        Integer newId = insertRecord(data, tablename);
        if (newId != null && newId > 0) {
            if (affectedIds != null) {
                affectedIds.add(newId);
            }
            return 2;
        }
        throw new RuntimeException("新增采购结算单失败");
    } catch (Exception e) {
        if (failedRecords != null) {
            JSONObject failedRecord = new JSONObject();
            failedRecord.put("billnum", billnum);
            failedRecord.put("cgjsdid", cgjsdid);
            failedRecord.put("errorMessage", e.getMessage());
            failedRecord.put("operation", "upsert");
            failedRecords.add(failedRecord);
        }
        return -1;
    }
}

private boolean applyUpdate(int recordId, JSONArray changes, String tablename) {
    if (recordId <= 0 || changes == null || changes.isEmpty()) {
        return false;
    }
    StringBuilder setClause = new StringBuilder();
    for (int i = 0; i < changes.size(); i++) {
        JSONObject change = changes.getJSONObject(i);
        String field = Util.null2String(change.getString("field"));
        String value = Util.null2String(change.getString("newValue"));
        if (field.isEmpty()) {
            continue;
        }
        if (setClause.length() > 0) {
            setClause.append(",");
        }
        setClause.append(field).append("=").append(toSqlValue(value));
    }
    if (setClause.length() == 0) {
        return false;
    }
    String sql = "UPDATE " + tablename + " SET " + setClause + " WHERE id=" + recordId;
    RecordSet updateRs = new RecordSet();
    return updateRs.execute(sql);
}

private Integer insertRecord(Map<String, String> data, String tablename) {
    if (data == null || data.isEmpty()) {
        return null;
    }
    Map<String, String> insertMap = new HashMap<>(data);
    String now = TimeUtil.getCurrentTimeString();
    String createDate = now.substring(0, 10);
    String createTime = now.substring(11);
    insertMap.put("formmodeid", String.valueOf(getModeId(tablename)));
    insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
    insertMap.put("modedatacreatertype", "1");
    insertMap.put("modedatacreatedate", createDate);
    insertMap.put("modedatacreatetime", createTime);
    insertMap.put("modeuuid", UUID.randomUUID().toString());

    StringBuilder columns = new StringBuilder();
    StringBuilder values = new StringBuilder();
    for (Map.Entry<String, String> entry : insertMap.entrySet()) {
        if (columns.length() > 0) {
            columns.append(",");
            values.append(",");
        }
        columns.append(entry.getKey());
        values.append(toSqlValue(entry.getValue()));
    }

    RecordSet insertRs = new RecordSet();
    String sql = "INSERT INTO " + tablename + "(" + columns + ") VALUES(" + values + ")";
    if (!insertRs.execute(sql)) {
        return null;
    }
    RecordSet idRs = new RecordSet();
    idRs.executeQuery("SELECT id FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?",
            Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
    if (idRs.next()) {
        return idRs.getInt("id");
    }
    return null;
}

private String toSqlValue(String value) {
    String normalized = Util.null2String(value);
    if (normalized.isEmpty()) {
        return "null";
    }
    return "'" + normalized.replace("'", "''") + "'";
}

private void rebuildPermissions(Set<Integer> affectedIds, String tablename) {
    if (affectedIds == null || affectedIds.isEmpty()) {
        return;
    }
    try {
        int modeId = getModeId(tablename);
        if (modeId <= 0) {
            return;
        }
        ModeRightInfo rightInfo = new ModeRightInfo();
        rightInfo.setNewRight(false);
        rightInfo.init();
        rightInfo.setModeId(modeId);
        for (Integer billId : affectedIds) {
            if (billId == null || billId <= 0) {
                continue;
            }
            rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private int getModeId(String tablename) {
    try {
        return Util.getIntValue(CommonUtil.getModeIdByTableName(tablename));
    } catch (Exception e) {
        return 0;
    }
}

private boolean submitPageTask(CompletionService<PageResult> completionService,
                               AtomicInteger nextPage,
                               AtomicBoolean stopFlag,
                               String api,
                               String dwbh,
                               String pageSize) {
    if (stopFlag.get()) {
        return false;
    }
    completionService.submit(new Callable<PageResult>() {
        @Override
        public PageResult call() throws Exception {
            if (stopFlag.get()) {
                return null;
            }
            int pageIndex = nextPage.getAndIncrement();
            JSONObject req = new JSONObject();
            req.put("dwbh", dwbh);
            req.put("pagesize", pageSize);
            req.put("page", String.valueOf(pageIndex));
            JSONObject resp = LCUtil.doPost(api, req);
            if (resp != null && resp.containsKey("code") && resp.getIntValue("code") == 3003) {
                stopFlag.set(true);
            }
            return new PageResult(pageIndex, resp);
        }
    });
    return true;
}

public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
    JSONObject result = new JSONObject();
    int totalCount = 0;
    int newCount = 0;
    int updateCount = 0;
    int skipCount = 0;
    int failCount = 0;
    // 收集差异更新与新增的单据编号(billnum)
    JSONArray diffBillnums = new JSONArray();
    JSONArray newBillnums = new JSONArray();
    // 收集每条更新的字段变更详情
    JSONArray updatedDetails = new JSONArray();
    // 收集保存失败的记录详情
    JSONArray failedRecords = new JSONArray();
    // 收集调试日志(仅记录失败记录的详细过程)
    JSONArray debugLogs = new JSONArray();
    Set<Integer> affectedIds = new HashSet<>();
    try {
        int page = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
        String pageSize = Util.null2String(param.getString("pagesize"));
        if ("".equals(pageSize)) pageSize = "10000";

        while (true) {
            param.put("page", String.valueOf(page));
            param.put("pagesize", pageSize);

            JSONObject rjson = LCUtil.doPost(api, param);
            result = rjson;

            // 结束条件
            if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                System.out.println("分页已结束,code=3003,停止请求。最后页码:" + page);
                break;
            }
            if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                System.out.println("接口返回非成功code:" + rjson.getString("code") + ",页码:" + page + ",停止。");
                break;
            }

            JSONArray datas = rjson.getJSONArray("data");
            int pageCount = (datas == null) ? 0 : datas.size();
            totalCount += pageCount;
            System.out.println("第 " + page + " 页获取到 " + pageCount + " 条数据,开始增量同步...");

            if (datas != null) {
                for (int x = 0; x < datas.size(); x++) {
                    JSONObject item = datas.getJSONObject(x);
                    Map<String, String> data = new HashMap<>();
                    String currentBillnum = "";
                    String currentCgjsdid = "";
                    try {
                        // 映射字段
                        currentCgjsdid = Util.null2String(item.get("ID"));
                        currentBillnum = Util.null2String(item.get("BILLNUM"));
                        
                        System.out.println("========================================");
                        System.out.println("[开始处理] 单据编号: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        
                        data.put("cgjsdid", currentCgjsdid);
                        data.put("billnum", currentBillnum);
                        data.put("WLDW", Util.null2String(item.get("WLDW")));
                        data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                        data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                        data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                        data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                        data.put("BMBH", Util.null2String(item.get("BMBH")));
                        data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                        data.put("summary", Util.null2String(item.get("SUMMARY")));
                        data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                        data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                        data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                        data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                        data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                        data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                        data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                        data.put("dwbh", Util.null2String(item.get("DWBH")));
                        data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                        data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                        data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                        data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                        data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                        data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                        data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                        data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                        data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                        data.put("XMID", Util.null2String(item.get("XMID")));
                        data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                        data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                        data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                        data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                        if (item.get("BILLDATE") != null) {
                            data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
                        }
                        data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                        data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                        // 金额字段:统一两位小数
                        data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                        data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                        data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                        data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                        data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                        data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                        data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                        data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
                        // availableforeigncurrency 字段在数据库表中不存在,已跳过
                        // data.put("availableforeigncurrency", formatTo2Decimals(item.getString("AVAILABLEFOREIGNCURRENCY")));

                        System.out.println("[字段映射完成] billnum: " + currentBillnum + ", 准备保存...");
                        
                        int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords, affectedIds);
                        if (action == 1) {
                            updateCount++;
                            diffBillnums.add(data.get("billnum"));
                        } else if (action == 2) {
                            newCount++;
                            newBillnums.add(data.get("billnum"));
                        } else if (action == -1) {
                            failCount++;
                            System.out.println("[保存失败] billnum: " + currentBillnum);
                        } else {
                            skipCount++;
                        }
                    } catch (Exception ee) {
                        failCount++;
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        System.out.println("[严重错误] billnum: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
                        System.out.println("[错误信息] " + ee.getMessage());
                        System.out.println("[堆栈跟踪开始]");
                        ee.printStackTrace();
                        System.out.println("[堆栈跟踪结束]");
                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                        
                        // 记录失败详情
                        JSONObject failedRecord = new JSONObject();
                        failedRecord.put("billnum", currentBillnum);
                        failedRecord.put("cgjsdid", currentCgjsdid);
                        failedRecord.put("errorMessage", ee.getMessage());
                        failedRecord.put("errorType", ee.getClass().getName());
                        failedRecords.add(failedRecord);
                    }
                }
            }

            // 每页结束后可执行一次关联更新
            try {
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
            } catch (Exception ex) {
                ex.printStackTrace();
                result.put("updateError", "第" + page + "页 关联字段更新异常:" + ex.getMessage());
            }

            System.out.println("第 " + page + " 页处理完成,累计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount);
            page++;
        }

        rebuildPermissions(affectedIds, tablename);

        String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 分页处理完成";
        result.put("updateInfo", syncInfo);
        System.out.println(syncInfo);
        System.out.println("========================================");
        System.out.println("同步完成汇总:");
        System.out.println("总数: " + totalCount);
        System.out.println("新增: " + newCount);
        System.out.println("更新: " + updateCount);
        System.out.println("跳过: " + skipCount);
        System.out.println("失败: " + failCount);
        if (failCount > 0) {
            System.out.println("失败记录详情:");
            for (int i = 0; i < failedRecords.size(); i++) {
                JSONObject failed = failedRecords.getJSONObject(i);
                System.out.println("  [" + (i+1) + "] billnum=" + failed.getString("billnum") + 
                                 ", cgjsdid=" + failed.getString("cgjsdid") +
                                 ", operation=" + failed.getString("operation") +
                                 ", error=" + failed.getString("errorMessage"));
            }
        }
        System.out.println("========================================");
        
        // 将差异与新增的单据编号(billnum)放入结果
        result.put("diffBillnums", diffBillnums);
        result.put("newBillnums", newBillnums);
        // 返回每条更新的具体字段变化
        result.put("updatedDetails", updatedDetails);
        // 返回失败记录
        if (failCount > 0) {
            result.put("failedRecords", failedRecords);
            result.put("failCount", failCount);
        }
        // 返回调试日志
        if (debugLogs.size() > 0) {
            result.put("debugLogs", debugLogs);
        }
    } catch (Exception e) {
        e.printStackTrace();
        result.put("error", "增量同步数据异常: " + e.getMessage());
    }
    return result;
}

/**
 * 调试辅助:打印 ModeUtil.saveModeData 可能使用的表信息
 */
public String debugTableInfo(String tablename, RecordSet rs) {
    StringBuilder info = new StringBuilder();
    try {
        // 获取表的列信息
        rs.executeQuery("SHOW COLUMNS FROM " + tablename);
        info.append("[表结构信息] ").append(tablename).append(":\n");
        while (rs.next()) {
            String field = rs.getString("Field");
            String type = rs.getString("Type");
            String nullAllowed = rs.getString("Null");
            String key = rs.getString("Key");
            String defaultVal = rs.getString("Default");
            info.append("  ").append(field).append(" ").append(type);
            if ("NO".equals(nullAllowed)) info.append(" NOT NULL");
            if ("PRI".equals(key)) info.append(" PRIMARY KEY");
            if (defaultVal != null) info.append(" DEFAULT ").append(defaultVal);
            info.append("\n");
        }
    } catch (Exception e) {
        info.append("获取表结构失败: ").append(e.getMessage());
    }
    return info.toString();
}
%>

*** Begin Patch
*** Update File: /Users/meaili/weaver/ecologydemo/sx/webapp/syncLCCGJSD.jsp
@@
-public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
-    JSONObject result = new JSONObject();
-    int totalCount = 0;
-    int newCount = 0;
-    int updateCount = 0;
-    int skipCount = 0;
-    int failCount = 0;
-    // 收集差异更新与新增的单据编号(billnum)
-    JSONArray diffBillnums = new JSONArray();
-    JSONArray newBillnums = new JSONArray();
-    // 收集每条更新的字段变更详情
-    JSONArray updatedDetails = new JSONArray();
-    // 收集保存失败的记录详情
-    JSONArray failedRecords = new JSONArray();
-    // 收集调试日志(仅记录失败记录的详细过程)
-    JSONArray debugLogs = new JSONArray();
-    Set<Integer> affectedIds = new HashSet<>();
-    try {
-        int page = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
-        String pageSize = Util.null2String(param.getString("pagesize"));
-        if ("".equals(pageSize)) pageSize = "10000";
-
-        while (true) {
-            param.put("page", String.valueOf(page));
-            param.put("pagesize", pageSize);
-
-            JSONObject rjson = LCUtil.doPost(api, param);
-            result = rjson;
-
-            // 结束条件
-            if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
-                System.out.println("分页已结束,code=3003,停止请求。最后页码:" + page);
-                break;
-            }
-            if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
-                System.out.println("接口返回非成功code:" + rjson.getString("code") + ",页码:" + page + ",停止。");
-                break;
-            }
-
-            JSONArray datas = rjson.getJSONArray("data");
-            int pageCount = (datas == null) ? 0 : datas.size();
-            totalCount += pageCount;
-            System.out.println("第 " + page + " 页获取到 " + pageCount + " 条数据,开始增量同步...");
-
-            if (datas != null) {
-                for (int x = 0; x < datas.size(); x++) {
-                    JSONObject item = datas.getJSONObject(x);
-                    Map<String, String> data = new HashMap<>();
-                    String currentBillnum = "";
-                    String currentCgjsdid = "";
-                    try {
-                        // 映射字段
-                        currentCgjsdid = Util.null2String(item.get("ID"));
-                        currentBillnum = Util.null2String(item.get("BILLNUM"));
-                        
-                        System.out.println("========================================");
-                        System.out.println("[开始处理] 单据编号: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
-                        
-                        data.put("cgjsdid", currentCgjsdid);
-                        data.put("billnum", currentBillnum);
-                        data.put("WLDW", Util.null2String(item.get("WLDW")));
-                        data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
-                        data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
-                        data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
-                        data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
-                        data.put("BMBH", Util.null2String(item.get("BMBH")));
-                        data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
-                        data.put("summary", Util.null2String(item.get("SUMMARY")));
-                        data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
-                        data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
-                        data.put("contractid", Util.null2String(item.get("CONTRACTID")));
-                        data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
-                        data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
-                        data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
-                        data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
-                        data.put("dwbh", Util.null2String(item.get("DWBH")));
-                        data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
-                        data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
-                        data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
-                        data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
-                        data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
-                        data.put("Kxxz", Util.null2String(item.get("KXXZ")));
-                        data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
-                        data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
-                        data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
-                        data.put("XMID", Util.null2String(item.get("XMID")));
-                        data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
-                        data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
-                        data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
-                        data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
-                        if (item.get("BILLDATE") != null) {
-                            data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
-                        }
-                        data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
-                        data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));
-
-                        // 金额字段:统一两位小数
-                        data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
-                        data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
-                        data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
-                        data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
-                        data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
-                        data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
-                        data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
-                        data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
-                        // availableforeigncurrency 字段在数据库表中不存在,已跳过
-                        // data.put("availableforeigncurrency", formatTo2Decimals(item.getString("AVAILABLEFOREIGNCURRENCY")));
-
-                        System.out.println("[字段映射完成] billnum: " + currentBillnum + ", 准备保存...");
-                        
-                        int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords, affectedIds);
-                        if (action == 1) {
-                            updateCount++;
-                            diffBillnums.add(data.get("billnum"));
-                        } else if (action == 2) {
-                            newCount++;
-                            newBillnums.add(data.get("billnum"));
-                        } else if (action == -1) {
-                            failCount++;
-                            System.out.println("[保存失败] billnum: " + currentBillnum);
-                        } else {
-                            skipCount++;
-                        }
-                    } catch (Exception ee) {
-                        failCount++;
-                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
-                        System.out.println("[严重错误] billnum: " + currentBillnum + ", cgjsdid: " + currentCgjsdid);
-                        System.out.println("[错误信息] " + ee.getMessage());
-                        System.out.println("[堆栈跟踪开始]");
-                        ee.printStackTrace();
-                        System.out.println("[堆栈跟踪结束]");
-                        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
-                        
-                        // 记录失败详情
-                        JSONObject failedRecord = new JSONObject();
-                        failedRecord.put("billnum", currentBillnum);
-                        failedRecord.put("cgjsdid", currentCgjsdid);
-                        failedRecord.put("errorMessage", ee.getMessage());
-                        failedRecord.put("errorType", ee.getClass().getName());
-                        failedRecords.add(failedRecord);
-                    }
-                }
-            }
-
-            // 每页结束后可执行一次关联更新
-            try {
-                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
-                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
-                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
-                rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
-                rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
-            } catch (Exception ex) {
-                ex.printStackTrace();
-                result.put("updateError", "第" + page + "页 关联字段更新异常:" + ex.getMessage());
-            }
-
-            System.out.println("第 " + page + " 页处理完成,累计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount);
-            page++;
-        }
-
-        String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 分页处理完成";
-        result.put("updateInfo", syncInfo);
-        System.out.println(syncInfo);
-        System.out.println("========================================");
-        System.out.println("同步完成汇总:");
-        System.out.println("总数: " + totalCount);
-        System.out.println("新增: " + newCount);
-        System.out.println("更新: " + updateCount);
-        System.out.println("跳过: " + skipCount);
-        System.out.println("失败: " + failCount);
-        if (failCount > 0) {
-            System.out.println("失败记录详情:");
-            for (int i = 0; i < failedRecords.size(); i++) {
-                JSONObject failed = failedRecords.getJSONObject(i);
-                System.out.println("  [" + (i+1) + "] billnum=" + failed.getString("billnum") + 
-                                 ", cgjsdid=" + failed.getString("cgjsdid") +
-                                 ", operation=" + failed.getString("operation") +
-                                 ", error=" + failed.getString("errorMessage"));
-            }
-        }
-        System.out.println("========================================");
-        
-        // 将差异与新增的单据编号(billnum)放入结果
-        result.put("diffBillnums", diffBillnums);
-        result.put("newBillnums", newBillnums);
-        // 返回每条更新的具体字段变化
-        result.put("updatedDetails", updatedDetails);
-        // 返回失败记录
-        if (failCount > 0) {
-            result.put("failedRecords", failedRecords);
-            result.put("failCount", failCount);
-        }
-        // 返回调试日志
-        if (debugLogs.size() > 0) {
-            result.put("debugLogs", debugLogs);
-        }
-    } catch (Exception e) {
-        e.printStackTrace();
-        result.put("error", "增量同步数据异常: " + e.getMessage());
-    }
-    return result;
-}
+public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
+    JSONObject result = new JSONObject();
+    int totalCount = 0;
+    int newCount = 0;
+    int updateCount = 0;
+    int skipCount = 0;
+    int failCount = 0;
+    JSONArray diffBillnums = new JSONArray();
+    JSONArray newBillnums = new JSONArray();
+    JSONArray updatedDetails = new JSONArray();
+    JSONArray failedRecords = new JSONArray();
+    JSONArray debugLogs = new JSONArray();
+    Set<Integer> affectedIds = new HashSet<>();
+
+    String dwbh = Util.null2String(param.getString("dwbh"));
+    String pageSize = Util.null2String(param.getString("pagesize"));
+    if ("".equals(pageSize)) pageSize = "10000";
+    int startPage = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));
+
+    AtomicInteger nextPage = new AtomicInteger(startPage);
+    AtomicBoolean stopFlag = new AtomicBoolean(false);
+    ExecutorService executor = Executors.newFixedThreadPool(MAX_THREAD_COUNT);
+    CompletionService<PageResult> completionService = new ExecutorCompletionService<>(executor);
+
+    int currentCap = INITIAL_THREAD_COUNT;
+    int activeTasks = 0;
+    try {
+        for (int i = 0; i < currentCap; i++) {
+            if (submitPageTask(completionService, nextPage, stopFlag, api, dwbh, pageSize)) {
+                activeTasks++;
+            }
+        }
+
+        while (activeTasks > 0) {
+            Future<PageResult> future = completionService.take();
+            PageResult pageResult = null;
+            try {
+                pageResult = future.get();
+            } catch (Exception taskEx) {
+                failCount++;
+                JSONObject failedRecord = new JSONObject();
+                failedRecord.put("billnum", "");
+                failedRecord.put("cgjsdid", "");
+                failedRecord.put("errorMessage", "线程执行异常: " + taskEx.getMessage());
+                failedRecord.put("errorType", taskEx.getClass().getName());
+                failedRecords.add(failedRecord);
+            }
+            activeTasks--;
+
+            if (pageResult != null && pageResult.response != null && !stopFlag.get()) {
+                JSONObject rjson = pageResult.response;
+                if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
+                    stopFlag.set(true);
+                } else if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
+                    stopFlag.set(true);
+                    result.put("updateError", "第" + pageResult.page + "页接口返回异常code: " + rjson.getString("code"));
+                } else {
+                    JSONArray datas = rjson.getJSONArray("data");
+                    int pageCount = (datas == null) ? 0 : datas.size();
+                    totalCount += pageCount;
+                    System.out.println("多线程处理 - 第 " + pageResult.page + " 页获取到 " + pageCount + " 条数据");
+
+                    if (datas != null) {
+                        for (int x = 0; x < datas.size(); x++) {
+                            JSONObject item = datas.getJSONObject(x);
+                            Map<String, String> data = new HashMap<>();
+                            String currentBillnum = "";
+                            String currentCgjsdid = "";
+                            try {
+                                currentCgjsdid = Util.null2String(item.get("ID"));
+                                currentBillnum = Util.null2String(item.get("BILLNUM"));
+
+                                data.put("cgjsdid", currentCgjsdid);
+                                data.put("billnum", currentBillnum);
+                                data.put("WLDW", Util.null2String(item.get("WLDW")));
+                                data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
+                                data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
+                                data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
+                                data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
+                                data.put("BMBH", Util.null2String(item.get("BMBH")));
+                                data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
+                                data.put("summary", Util.null2String(item.get("SUMMARY")));
+                                data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
+                                data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
+                                data.put("contractid", Util.null2String(item.get("CONTRACTID")));
+                                data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
+                                data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
+                                data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
+                                data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
+                                data.put("dwbh", Util.null2String(item.get("DWBH")));
+                                data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
+                                data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
+                                data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
+                                data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
+                                data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
+                                data.put("Kxxz", Util.null2String(item.get("KXXZ")));
+                                data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
+                                data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
+                                data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
+                                data.put("XMID", Util.null2String(item.get("XMID")));
+                                data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
+                                data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
+                                data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
+                                data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
+                                if (item.get("BILLDATE") != null) {
+                                    data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
+                                }
+                                data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
+                                data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));
+
+                                data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
+                                data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
+                                data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
+                                data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
+                                data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
+                                data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
+                                data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
+                                data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));
+
+                                int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords, affectedIds);
+                                if (action == 1) {
+                                    updateCount++;
+                                    diffBillnums.add(data.get("billnum"));
+                                } else if (action == 2) {
+                                    newCount++;
+                                    newBillnums.add(data.get("billnum"));
+                                } else if (action == -1) {
+                                    failCount++;
+                                } else {
+                                    skipCount++;
+                                }
+                            } catch (Exception ee) {
+                                failCount++;
+                                JSONObject failedRecord = new JSONObject();
+                                failedRecord.put("billnum", currentBillnum);
+                                failedRecord.put("cgjsdid", currentCgjsdid);
+                                failedRecord.put("errorMessage", ee.getMessage());
+                                failedRecord.put("errorType", ee.getClass().getName());
+                                failedRecords.add(failedRecord);
+                            }
+                        }
+                    }
+
+                    try {
+                        rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
+                        rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
+                        rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
+                        rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
+                        rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
+                    } catch (Exception ex) {
+                        ex.printStackTrace();
+                        result.put("updateError", "第" + pageResult.page + "页 关联字段更新异常:" + ex.getMessage());
+                    }
+
+                    if (pageCount == 0) {
+                        stopFlag.set(true);
+                    }
+                }
+            }
+
+            if (!stopFlag.get()) {
+                if (currentCap < MAX_THREAD_COUNT) {
+                    currentCap++;
+                }
+                while (!stopFlag.get() && activeTasks < currentCap) {
+                    if (!submitPageTask(completionService, nextPage, stopFlag, api, dwbh, pageSize)) {
+                        break;
+                    }
+                    activeTasks++;
+                }
+            }
+        }
+    } catch (Exception e) {
+        e.printStackTrace();
+        result.put("error", "增量同步数据异常: " + e.getMessage());
+    } finally {
+        executor.shutdownNow();
+    }
+
+    rebuildPermissions(affectedIds, tablename);
+
+    String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 并发处理完成";
+    result.put("updateInfo", syncInfo);
+    result.put("diffBillnums", diffBillnums);
+    result.put("newBillnums", newBillnums);
+    result.put("updatedDetails", updatedDetails);
+    if (failCount > 0) {
+        result.put("failedRecords", failedRecords);
+        result.put("failCount", failCount);
+    }
+    if (debugLogs.size() > 0) {
+        result.put("debugLogs", debugLogs);
+    }
+    return result;
+}
*** End Patch
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONArray" %>
<%@ page import="com.alibaba.fastjson.JSONObject" %>
<%@ page import="weaver.general.Util" %>
<%@ page import="dev.soft.lb.util.CommonUtil" %>
<%@ page import="dev.soft.lb.util.LCUtil" %>
<%@ page import="weaver.general.TimeUtil" %>
<%@ page import="weaver.conn.RecordSet" %>
<%@ page import="weaver.hrm.company.SubCompanyComInfo" %>
<%@ page import="weaver.formmode.setup.ModeRightInfo" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.math.BigDecimal" %>
<%@ page import="java.math.RoundingMode" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.UUID" %>
<%@ page import="java.util.concurrent.Callable" %>
<%@ page import="java.util.concurrent.CompletionService" %>
<%@ page import="java.util.concurrent.ExecutorCompletionService" %>
<%@ page import="java.util.concurrent.ExecutorService" %>
<%@ page import="java.util.concurrent.Executors" %>
<%@ page import="java.util.concurrent.Future" %>
<%@ page import="java.util.concurrent.atomic.AtomicBoolean" %>
<%@ page import="java.util.concurrent.atomic.AtomicInteger" %>

<%
// 设置响应内容类型为JSON
response.setContentType("application/json;charset=UTF-8");
// 允许跨域访问
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");

// 定义常量
final String companyId = "66";
final String billtype = "12";
String api = "";
final String tablename = "uf_cgjsd";
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 创建返回结果
JSONObject result = new JSONObject();

try {
    // 获取单位编码
    SubCompanyComInfo scci = new SubCompanyComInfo();
    String dwbm = scci.getSubCompanyCode(companyId);

    // 获取API配置
    RecordSet rs = new RecordSet();
    Map<String, String> billconfig = LCUtil.getLCBillTypeInfo(billtype, rs);
    api = billconfig.get("api_list");

    // 先做一次去重(不清空表,做增量同步)
//    deduplicateDatabase(rs, tablename);

    // 构建请求参数 - 分页增量拉取
    JSONObject paramJson = new JSONObject();
    paramJson.put("dwbh", dwbm);
    paramJson.put("pagesize", "10000");
    paramJson.put("page", "1");

    // 执行增量同步
    JSONObject syncResult = syncData(paramJson, rs, api, tablename, sdf);
    result.put("status", 1);
    result.put("data", syncResult);
    result.put("msg", "增量同步采购结算单成功");
    // 仅保留核心关心内容(差异单据编号billnum、新增单据编号billnum)在顶层返回,便于前端直接消费
    if (syncResult.containsKey("diffBillnums")) {
        result.put("diffBillnums", syncResult.getJSONArray("diffBillnums"));
    }
    if (syncResult.containsKey("newBillnums")) {
        result.put("newBillnums", syncResult.getJSONArray("newBillnums"));
    }
    // 返回每条更新的具体字段变化(字段名、旧值、新值)
    if (syncResult.containsKey("updatedDetails")) {
        result.put("updatedDetails", syncResult.getJSONArray("updatedDetails"));
    }

    // 添加详细的同步信息
    if (syncResult.containsKey("updateInfo")) {
        result.put("updateInfo", syncResult.getString("updateInfo"));
    }
    if (syncResult.containsKey("updateError")) {
        result.put("updateError", syncResult.getString("updateError"));
    }
    // 添加失败记录信息
    if (syncResult.containsKey("failedRecords")) {
        result.put("failedRecords", syncResult.getJSONArray("failedRecords"));
    }
    if (syncResult.containsKey("failCount")) {
        result.put("failCount", syncResult.getInteger("failCount"));
    }

} catch (Exception e) {
    e.printStackTrace();
    result.put("status", 0);
    result.put("msg", "读取浪潮采购结算单异常:" + e.getMessage());
    result.put("errorDetail", e.toString());
}

// 输出JSON结果
out.print(result.toJSONString());

/**
 * 同步数据方法
 */
%>

<%!
private static final int DEFAULT_USER_ID = 1;
private static final int INITIAL_THREAD_COUNT = 10;
private static final int MAX_THREAD_COUNT = 20;

private static class PageResult {
    final int page;
    final JSONObject response;

    PageResult(int page, JSONObject response) {
        this.page = page;
        this.response = response;
    }
}

/**
 * 数据库去重方法 - 删除重复数据,保留时间最新的一条
 */
public void deduplicateDatabase(RecordSet rs, String tablename) {
    try {
        String sql = "DELETE t1 FROM " + tablename + " t1 " +
                    "INNER JOIN " + tablename + " t2 " +
                    "WHERE t1.id < t2.id " +
                    "AND t1.cgjsdid = t2.cgjsdid " +
                    "AND t1.billnum = t2.billnum " +
                    "AND t1.WLDW = t2.WLDW " +
                    "AND t1.DEPARTMENTID = t2.DEPARTMENTID " +
                    "AND t1.contractid = t2.contractid " +
                    "AND t1.XMID = t2.XMID " +
                    "AND t1.Kxxz = t2.Kxxz " +
                    "AND t1.amount = t2.amount " +
                    "AND t1.Billdate = t2.Billdate";
        rs.execute(sql);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 数值字段名单:这些字段按两位小数口径进行比较与存储
private static final Set<String> NUMERIC_FIELDS = new HashSet<>(Arrays.asList(
        "amount",
        "foreigncurrency",
        "clearamount",
        "clearforeigncurrency",
        "balanceamount",
        "balanceforeigncurrency",
        "availableamount",
        // "availableforeigncurrency",  // 表中不存在此字段
        // 添加其他可能的数值字段
        "original_clearamount",
        "original_clearforeigncurrency",
        "original_balanceamount",
        "original_balanceforeigncurrency",
        "price",
        "unitprice",
        "quantity",
        "taxrate",
        "taxamount",
        "totalmoney",
        "netamount",
        "amountnotax"
));

// 规范化字符串:去首尾空格;null按空串处理
private static String norm(String v) {
    if (v == null) return "";
    return v.trim();
}

// 字符串等价:规范化后比较;空串与null等价
private static boolean equalNormalized(String a, String b) {
    return norm(a).equals(norm(b));
}

// 数值等价:两值都按两位小数(HALF_UP)规整后比较;均为空视为相等
private static boolean equalNumeric(String a, String b) {
    String sa = norm(a);
    String sb = norm(b);
    if (sa.isEmpty() && sb.isEmpty()) return true;
    if (sa.isEmpty() || sb.isEmpty()) return false;
    try {
        BigDecimal ba = new BigDecimal(sa).setScale(2, RoundingMode.HALF_UP);
        BigDecimal bb = new BigDecimal(sb).setScale(2, RoundingMode.HALF_UP);
        return ba.compareTo(bb) == 0;
    } catch (Exception ignore) {
        return sa.equals(sb);
    }
}

// 将数值字符串格式化为两位小数;非法数字则返回规范化后的原值
private static String formatTo2Decimals(String v) {
    String s = norm(v);
    if (s.isEmpty()) return s;
    try {
        return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP).toPlainString();
    } catch (Exception ignore) {
        return s;
    }
}

// 判断数据库中的记录与新数据是否存在差异(以数据库字段为准,对比所有字段)
public boolean hasDataChanged(RecordSet rs, Map<String, String> newData) {
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return true; // 无法获取字段信息,默认认为有变更
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                System.out.println("[字段差异] " + columnName + ": 数据库值='" + dbValue + "', 接口值='" + newValue + "'");
                return true; // 发现差异
            }
        }
        return false; // 所有字段都相同
    } catch (Exception e) {
        e.printStackTrace();
        // 出现异常时,默认认为有变更,以保证数据一致性
        return true;
    }
}

// 收集与数据库记录相比的字段差异,返回每个差异字段的 {field, oldValue, newValue}
public JSONArray collectDifferences(RecordSet rs, Map<String, String> newData) {
    JSONArray changes = new JSONArray();
    try {
        // 获取数据库记录的所有字段名
        String[] columnNames = rs.getColumnName();
        if (columnNames == null || columnNames.length == 0) {
            return changes; // 无法获取字段信息
        }

        // 遍历数据库的所有字段进行对比
        for (String columnName : columnNames) {
            // 跳过系统字段
            if ("id".equalsIgnoreCase(columnName) || "modedatacreater".equalsIgnoreCase(columnName)
                || "modedatacreatertype".equalsIgnoreCase(columnName) || "modedatacreatetime".equalsIgnoreCase(columnName)
                || "modedatamodifier".equalsIgnoreCase(columnName) || "modedatamodifytime".equalsIgnoreCase(columnName)) {
                continue;
            }

            String dbValue;
            try {
                dbValue = Util.null2String(rs.getString(columnName));
            } catch (Exception ignore) {
                continue; // 无法获取该字段值,跳过
            }

            // 仅对新数据中存在的字段进行比较;避免将未提供的新字段误判为空串
            if (!newData.containsKey(columnName)) {
                continue;
            }

            // 获取新数据中对应字段的值
            String newValue = Util.null2String(newData.get(columnName));

            // 根据字段类型进行对比
            boolean equal = NUMERIC_FIELDS.contains(columnName.toLowerCase()) ?
                equalNumeric(dbValue, newValue) : equalNormalized(dbValue, newValue);

            if (!equal) {
                JSONObject diff = new JSONObject();
                diff.put("field", columnName);
                diff.put("oldValue", dbValue);
                diff.put("newValue", newValue);
                changes.add(diff);
            }
        }
    } catch (Exception e) {
        // 忽略,返回当前已收集到的差异
    }
    return changes;
}

// 先对比后插入/更新:按 (cgjsdid,billnum) 定位;差异则按原id更新;不存在则插入
public int processRecordCompareAndUpsert(Map<String, String> data, RecordSet rs, String tablename,
                                         JSONArray updateDetails, JSONArray failedRecords, Set<Integer> affectedIds) {
    String billnum = Util.null2String(data.get("billnum"));
    String cgjsdid = Util.null2String(data.get("cgjsdid"));
    try {
        RecordSet check = new RecordSet();
        check.executeQuery("SELECT * FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?", cgjsdid, billnum);
        if (check.next()) {
            int existingId = check.getInt("id");
            if (hasDataChanged(check, data)) {
                JSONArray changes = collectDifferences(check, data);
                if (updateDetails != null) {
                    JSONObject detail = new JSONObject();
                    detail.put("billnum", billnum);
                    detail.put("cgjsdid", cgjsdid);
                    detail.put("changes", changes);
                    updateDetails.add(detail);
                }
                if (changes != null && changes.size() > 0) {
                    if (applyUpdate(existingId, changes, tablename)) {
                        if (affectedIds != null) {
                            affectedIds.add(existingId);
                        }
                        return 1;
                    }
                    throw new RuntimeException("更新采购结算单失败");
                }
            }
            return 0;
        }

        Integer newId = insertRecord(data, tablename);
        if (newId != null && newId > 0) {
            if (affectedIds != null) {
                affectedIds.add(newId);
            }
            return 2;
        }
        throw new RuntimeException("新增采购结算单失败");
    } catch (Exception e) {
        if (failedRecords != null) {
            JSONObject failedRecord = new JSONObject();
            failedRecord.put("billnum", billnum);
            failedRecord.put("cgjsdid", cgjsdid);
            failedRecord.put("errorMessage", e.getMessage());
            failedRecord.put("operation", "upsert");
            failedRecords.add(failedRecord);
        }
        return -1;
    }
}

private boolean applyUpdate(int recordId, JSONArray changes, String tablename) {
    if (recordId <= 0 || changes == null || changes.isEmpty()) {
        return false;
    }
    StringBuilder setClause = new StringBuilder();
    for (int i = 0; i < changes.size(); i++) {
        JSONObject change = changes.getJSONObject(i);
        String field = Util.null2String(change.getString("field"));
        String value = Util.null2String(change.getString("newValue"));
        if (field.isEmpty()) {
            continue;
        }
        if (setClause.length() > 0) {
            setClause.append(",");
        }
        setClause.append(field).append("=").append(toSqlValue(value));
    }
    if (setClause.length() == 0) {
        return false;
    }
    String sql = "UPDATE " + tablename + " SET " + setClause + " WHERE id=" + recordId;
    RecordSet updateRs = new RecordSet();
    return updateRs.execute(sql);
}

private Integer insertRecord(Map<String, String> data, String tablename) {
    if (data == null || data.isEmpty()) {
        return null;
    }
    Map<String, String> insertMap = new HashMap<>(data);
    String now = TimeUtil.getCurrentTimeString();
    String createDate = now.substring(0, 10);
    String createTime = now.substring(11);
    insertMap.put("formmodeid", String.valueOf(getModeId(tablename)));
    insertMap.put("modedatacreater", String.valueOf(DEFAULT_USER_ID));
    insertMap.put("modedatacreatertype", "1");
    insertMap.put("modedatacreatedate", createDate);
    insertMap.put("modedatacreatetime", createTime);
    insertMap.put("modeuuid", UUID.randomUUID().toString());

    StringBuilder columns = new StringBuilder();
    StringBuilder values = new StringBuilder();
    for (Map.Entry<String, String> entry : insertMap.entrySet()) {
        if (columns.length() > 0) {
            columns.append(",");
            values.append(",");
        }
        columns.append(entry.getKey());
        values.append(toSqlValue(entry.getValue()));
    }

    RecordSet insertRs = new RecordSet();
    String sql = "INSERT INTO " + tablename + "(" + columns + ") VALUES(" + values + ")";
    if (!insertRs.execute(sql)) {
        return null;
    }
    RecordSet idRs = new RecordSet();
    idRs.executeQuery("SELECT id FROM " + tablename + " WHERE cgjsdid = ? AND billnum = ?",
            Util.null2String(data.get("cgjsdid")), Util.null2String(data.get("billnum")));
    if (idRs.next()) {
        return idRs.getInt("id");
    }
    return null;
}

private String toSqlValue(String value) {
    String normalized = Util.null2String(value);
    if (normalized.isEmpty()) {
        return "null";
    }
    return "'" + normalized.replace("'", "''") + "'";
}

private void rebuildPermissions(Set<Integer> affectedIds, String tablename) {
    if (affectedIds == null || affectedIds.isEmpty()) {
        return;
    }
    try {
        int modeId = getModeId(tablename);
        if (modeId <= 0) {
            return;
        }
        ModeRightInfo rightInfo = new ModeRightInfo();
        rightInfo.setNewRight(false);
        rightInfo.init();
        rightInfo.setModeId(modeId);
        for (Integer billId : affectedIds) {
            if (billId == null || billId <= 0) {
                continue;
            }
            rightInfo.rebuildModeDataShareByEdit(DEFAULT_USER_ID, modeId, billId);
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private int getModeId(String tablename) {
    try {
        return Util.getIntValue(CommonUtil.getModeIdByTableName(tablename));
    } catch (Exception e) {
        return 0;
    }
}

private boolean submitPageTask(CompletionService<PageResult> completionService,
                               AtomicInteger nextPage,
                               AtomicBoolean stopFlag,
                               String api,
                               String dwbh,
                               String pageSize) {
    if (stopFlag.get()) {
        return false;
    }
    completionService.submit(new Callable<PageResult>() {
        @Override
        public PageResult call() throws Exception {
            if (stopFlag.get()) {
                return null;
            }
            int pageIndex = nextPage.getAndIncrement();
            JSONObject req = new JSONObject();
            req.put("dwbh", dwbh);
            req.put("pagesize", pageSize);
            req.put("page", String.valueOf(pageIndex));
            JSONObject resp = LCUtil.doPost(api, req);
            if (resp != null && resp.containsKey("code") && resp.getIntValue("code") == 3003) {
                stopFlag.set(true);
            }
            return new PageResult(pageIndex, resp);
        }
    });
    return true;
}

public JSONObject syncData(JSONObject param, RecordSet rs, String api, String tablename, SimpleDateFormat sdf) {
    JSONObject result = new JSONObject();
    int totalCount = 0;
    int newCount = 0;
    int updateCount = 0;
    int skipCount = 0;
    int failCount = 0;
    JSONArray diffBillnums = new JSONArray();
    JSONArray newBillnums = new JSONArray();
    JSONArray updatedDetails = new JSONArray();
    JSONArray failedRecords = new JSONArray();
    JSONArray debugLogs = new JSONArray();
    Set<Integer> affectedIds = new HashSet<>();

    String dwbh = Util.null2String(param.getString("dwbh"));
    String pageSize = Util.null2String(param.getString("pagesize"));
    if ("".equals(pageSize)) pageSize = "10000";
    int startPage = Integer.parseInt(Util.null2String(param.getString("page")).equals("") ? "1" : param.getString("page"));

    AtomicInteger nextPage = new AtomicInteger(startPage);
    AtomicBoolean stopFlag = new AtomicBoolean(false);
    ExecutorService executor = Executors.newFixedThreadPool(MAX_THREAD_COUNT);
    CompletionService<PageResult> completionService = new ExecutorCompletionService<>(executor);

    int currentCap = INITIAL_THREAD_COUNT;
    int activeTasks = 0;

    try {
        for (int i = 0; i < currentCap; i++) {
            if (submitPageTask(completionService, nextPage, stopFlag, api, dwbh, pageSize)) {
                activeTasks++;
            }
        }

        while (activeTasks > 0) {
            Future<PageResult> future = completionService.take();
            PageResult pageResult = null;
            try {
                pageResult = future.get();
            } catch (Exception taskEx) {
                failCount++;
                JSONObject failedRecord = new JSONObject();
                failedRecord.put("billnum", "");
                failedRecord.put("cgjsdid", "");
                failedRecord.put("operation", "thread");
                failedRecord.put("errorMessage", "线程执行异常: " + taskEx.getMessage());
                failedRecord.put("errorType", taskEx.getClass().getName());
                failedRecords.add(failedRecord);
            }
            activeTasks--;

            if (pageResult != null && pageResult.response != null && !stopFlag.get()) {
                JSONObject rjson = pageResult.response;
                if (rjson.containsKey("code") && rjson.getIntValue("code") == 3003) {
                    stopFlag.set(true);
                } else if (!(rjson.containsKey("code") && rjson.getIntValue("code") == 30000)) {
                    stopFlag.set(true);
                    result.put("updateError", "第" + pageResult.page + "页接口返回异常code: " + rjson.getString("code"));
                } else {
                    JSONArray datas = rjson.getJSONArray("data");
                    int pageCount = (datas == null) ? 0 : datas.size();
                    totalCount += pageCount;
                    System.out.println("多线程处理 - 第 " + pageResult.page + " 页获取到 " + pageCount + " 条数据");

                    if (datas != null) {
                        for (int x = 0; x < datas.size(); x++) {
                            JSONObject item = datas.getJSONObject(x);
                            Map<String, String> data = new HashMap<>();
                            String currentBillnum = "";
                            String currentCgjsdid = "";
                            try {
                                currentCgjsdid = Util.null2String(item.get("ID"));
                                currentBillnum = Util.null2String(item.get("BILLNUM"));

                                data.put("cgjsdid", currentCgjsdid);
                                data.put("billnum", currentBillnum);
                                data.put("WLDW", Util.null2String(item.get("WLDW")));
                                data.put("partnerid_code", Util.null2String(item.get("PARTNERID_CODE")));
                                data.put("PARTNERID_NAME", Util.null2String(item.get("PARTNERID_NAME")));
                                data.put("DEPARTMENTID", Util.null2String(item.get("DEPARTMENTID")));
                                data.put("departmentid_name", Util.null2String(item.get("DEPARTMENTID_NAME")));
                                data.put("BMBH", Util.null2String(item.get("BMBH")));
                                data.put("billtypeid", Util.null2String(item.get("BILLTYPEID")));
                                data.put("summary", Util.null2String(item.get("SUMMARY")));
                                data.put("billcateprop", Util.null2String(item.get("BILLCATEPROP")));
                                data.put("DJLXMC", Util.null2String(item.get("DJLXMC")));
                                data.put("contractid", Util.null2String(item.get("CONTRACTID")));
                                data.put("contractid_code", Util.null2String(item.get("CONTRACTID_CODE")));
                                data.put("contractid_name", Util.null2String(item.get("CONTRACTID_NAME")));
                                data.put("currencyid", Util.null2String(item.get("CURRENCYID")));
                                data.put("organizationid", Util.null2String(item.get("ORGANIZATIONID")));
                                data.put("dwbh", Util.null2String(item.get("DWBH")));
                                data.put("organizationid_name", Util.null2String(item.get("ORGANIZATIONID_NAME")));
                                data.put("customtext01", Util.null2String(item.get("CUSTOMTEXT01")));
                                data.put("employeeid", Util.null2String(item.get("EMPLOYEEID")));
                                data.put("employeeid_name", Util.null2String(item.get("EMPLOYEEID_NAME")));
                                data.put("employeeid_code", Util.null2String(item.get("EMPLOYEEID_CODE")));
                                data.put("Kxxz", Util.null2String(item.get("KXXZ")));
                                data.put("KxxzFullMC", Util.null2String(item.get("KXXZFULLMC")));
                                data.put("Kxxzno", Util.null2String(item.get("KXXZNO")));
                                data.put("KXXZMC", Util.null2String(item.get("KXXZMC")));
                                data.put("XMID", Util.null2String(item.get("XMID")));
                                data.put("projectinfoid_code", Util.null2String(item.get("PROJECTINFOID_CODE")));
                                data.put("projectinfoid_name", Util.null2String(item.get("PROJECTINFOID_NAME")));
                                data.put("billstatename", Util.null2String(item.get("BILLSTATENAME")));
                                data.put("srcbizid", Util.null2String(item.get("SRCBIZID")));
                                if (item.get("BILLDATE") != null) {
                                    data.put("Billdate", sdf.format(item.getDate("BILLDATE")));
                                }
                                data.put("srcdoctypeid", Util.null2String(item.get("SRCDOCTYPEID")));
                                data.put("srcdoctypecode", Util.null2String(item.get("SRCDOCTYPECODE")));

                                data.put("amount", formatTo2Decimals(item.getString("AMOUNT")));
                                data.put("foreigncurrency", formatTo2Decimals(item.getString("FOREIGNCURRENCY")));
                                data.put("clearamount", formatTo2Decimals(item.getString("ORIGINAL_CLEARAMOUNT")));
                                data.put("clearforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_CLEARFOREIGNCURRENCY")));
                                data.put("balanceamount", formatTo2Decimals(item.getString("ORIGINAL_BALANCEAMOUNT")));
                                data.put("balanceforeigncurrency", formatTo2Decimals(item.getString("ORIGINAL_BALANCEFOREIGNCURRENCY")));
                                data.put("availableamount", formatTo2Decimals(item.getString("AVAILABLEAMOUNT")));
                                data.put("amountnotax", formatTo2Decimals(item.getString("AMOUNTNOTAX")));

                                int action = processRecordCompareAndUpsert(data, rs, tablename, updatedDetails, failedRecords, affectedIds);
                                if (action == 1) {
                                    updateCount++;
                                    diffBillnums.add(data.get("billnum"));
                                } else if (action == 2) {
                                    newCount++;
                                    newBillnums.add(data.get("billnum"));
                                } else if (action == -1) {
                                    failCount++;
                                } else {
                                    skipCount++;
                                }
                            } catch (Exception ee) {
                                failCount++;
                                JSONObject failedRecord = new JSONObject();
                                failedRecord.put("billnum", currentBillnum);
                                failedRecord.put("cgjsdid", currentCgjsdid);
                                failedRecord.put("errorMessage", ee.getMessage());
                                failedRecord.put("errorType", ee.getClass().getName());
                                failedRecords.add(failedRecord);
                            }
                        }
                    }

                    try {
                        rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbwldw B ON A.WLDW = B.nm SET A.wldwmcll = B.id");
                        rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xjhttz B ON A.contractid = B.gscbizid SET A.htmc = B.id");
                        rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_esbkxxz B ON A.kxxz = B.nm SET A.kxxz1 = B.id");
                        rs.execute("UPDATE uf_cgjsd A INNER JOIN uf_xmba B ON A.xmid = B.gscbizid SET A.xmmc = B.id");
                        rs.execute("UPDATE uf_cgjsd u INNER JOIN uf_hrmdepartment h1 ON h1.bm = u.bmbh INNER JOIN hrmdepartment h2 ON h2.departmentcode = h1.bm SET u.bmmcll = h2.id");
                    } catch (Exception ex) {
                        ex.printStackTrace();
                        result.put("updateError", "第" + pageResult.page + "页 关联字段更新异常:" + ex.getMessage());
                    }

                    if (pageCount == 0) {
                        stopFlag.set(true);
                    }
                }
            }

            if (!stopFlag.get()) {
                if (currentCap < MAX_THREAD_COUNT) {
                    currentCap++;
                }
                while (!stopFlag.get() && activeTasks < currentCap) {
                    if (!submitPageTask(completionService, nextPage, stopFlag, api, dwbh, pageSize)) {
                        break;
                    }
                    activeTasks++;
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
        result.put("error", "增量同步数据异常: " + e.getMessage());
    } finally {
        executor.shutdownNow();
    }

    rebuildPermissions(affectedIds, tablename);

    String syncInfo = "增量同步统计 - 总数:" + totalCount + ", 新增:" + newCount + ", 更新:" + updateCount + ", 跳过:" + skipCount + ", 失败:" + failCount + "; 并发处理完成";
    result.put("updateInfo", syncInfo);
    result.put("diffBillnums", diffBillnums);
    result.put("newBillnums", newBillnums);
    result.put("updatedDetails", updatedDetails);
    if (failCount > 0) {
        result.put("failedRecords", failedRecords);
        result.put("failCount", failCount);
    }
    if (debugLogs.size() > 0) {
        result.put("debugLogs", debugLogs);
    }
    return result;
}

/**
 * 调试辅助:打印 ModeUtil.saveModeData 可能使用的表信息
 */
public String debugTableInfo(String tablename, RecordSet rs) {
    StringBuilder info = new StringBuilder();
    try {
        // 获取表的列信息
        rs.executeQuery("SHOW COLUMNS FROM " + tablename);
        info.append("[表结构信息] ").append(tablename).append(":\n");
        while (rs.next()) {
            String field = rs.getString("Field");
            String type = rs.getString("Type");
            String nullAllowed = rs.getString("Null");
            String key = rs.getString("Key");
            String defaultVal = rs.getString("Default");
            info.append("  ").append(field).append(" ").append(type);
            if ("NO".equals(nullAllowed)) info.append(" NOT NULL");
            if ("PRI".equals(key)) info.append(" PRIMARY KEY");
            if (defaultVal != null) info.append(" DEFAULT ").append(defaultVal);
            info.append("\n");
        }
    } catch (Exception e) {
        info.append("获取表结构失败: ").append(e.getMessage());
    }
    return info.toString();
}
%>

关键调整

  • syncLCCGJSD.jsp 现在在页面加载时自动构建 10 个并发任务,利用 ExecutorCompletionService 管控最多 20 个线程。每个任务仅抓取一个分页并返回;一旦某个线程收到 code=3003 或空页,立刻设置 stopFlag,阻止继续发起新请求,并等现有线程处理完再 shutdown
  • 为线程调度新增 AtomicInteger nextPageAtomicBoolean stopFlag。初始并发为 10,若仍未碰到 3003,会在任务完成后逐步把上限提升到 20;超过 20 时不会再提交,等这批线程处理完毕自动销毁。
  • 所有接口响应仍在主线程串行落库:单页数据解析、processRecordCompareAndUpsert、五条关联更新 SQL 以及差异统计逻辑保持不变,最后统一收集 affectedIds 并执行权限重构。
  • 新增辅助 PageResult 类和 submitPageTask 方法,隔离接口调用与同步逻辑,同时把 dwbh/pagesize/page 组装放在任务内,确保线程间互不干扰。

使用提示

  • 默认 pagesize 仍为 10000,可按前面测算改成 3000~5000 平衡内存与接口耗时;修改后多线程调度会自动生效。
  • 目前仍是“抢占式”请求,接口若无法承受 10~20 并发,请把 INITIAL_THREAD_COUNTMAX_THREAD_COUNT 下调。
  • JSP 环境未易于做全量集成测试,建议在测试环境先跑一次 10 万条级别数据,观察接口响应是否因并发升高而报错。

User

请求并行然后数据处理多线程是否是更好的解决方案


Cursor

并行拉取 + 多线程写库看似“更快”,但对你这个场景风险大于收益:

  • 接口瓶颈在网络/上游:每次 3‑4 s 是 API 固有延迟,多线程只是同时排队等待,无法把单页响应压缩到<1 s;更高并发还可能触发流控或 429。我们已经让 JSP 最多 20 并发请求,基本把上游资源吃满了,再加本地多线程处理意义不大。
  • 本地“写库”已经是批量 SQL:目前单页结果在 JSP 中串行落库、批量更新,再统一重构权限。瓶颈主要在 IO,而不是 CPU。如果把 processRecordCompareAndUpsert 改为多线程同时执行,会导致 RecordSet/DB 连接被抢、同一条记录的写入冲突、甚至死锁。
  • 线程调度成本:JSP 运行在 Web 容器里,创建大量 worker 需要额外线程池和事务管理。如果既要接口并行又要处理并行,就得做好全局锁、连接池、事务隔离,否则偶发问题更难排查。

更稳健的方向

  1. 继续优化分页条件:引入“增量时间戳/最大 ID”,让每次只处理近 10 分钟数据;真实 workload 会瞬间降几个量级。
  2. 把写库放到 DB 侧批处理:把接口数据写临时表,SQL 一次性 INSERT ... SELECT/UPDATE ... JOIN,既减少 Java 内存,又让 DB 自己处理并发。
  3. 必要时并行请求:已经实现 10→20 并发线程调度,足以压榨接口吞吐;如果仍慢,优先调大 pagesize 而不是把数据处理再拆多线程。

总结:并行请求 + 单线程落库 是目前的较优折中,再叠加数据处理多线程会显著增加复杂度和风险,收益不明显。建议把精力放在更小 pagesize、增量条件、数据库批更新等可控方案上。


User

是否有最优解


Cursor

没有“放之四海皆准”的最优解,只有结合现状的最合适策略。就你这个采购结算同步场景,综合接口性能、JSP 运行环境和数据量,我认为较优的路线是:

  • 接口层:保持中等分页 + 并发拉取

    • pagesize 控制在 3k5k:单页内存 <10 MB,100k 数据 ≈ 2034 页。
    • 维持 10→20 线程的阶梯并发:既能利用上游带宽,又不过度压垮接口。
    • 一旦任意线程拿到 code=3003 或空页立即停发,避免多余请求。
  • 数据层:单线程批处理 + DB 原子操作

    • JSP 端收集一页数据后串行执行批量 SQL(我们已将 processRecordCompareAndUpsert 调整为直接 UPDATE/INSERT),避免多线程抢同一记录。
    • 每页处理完再做关联更新,末尾统一重构权限。若后面瓶颈仍在写库,可进一步改成“接口结果写临时表 ➜ DB 侧 INSERT/UPDATE ... SELECT”,让数据库自己并行化。
  • 增量策略:真正减少数据量

    • 结合业务字段(Billdate、状态、max id)增加接口条件,只同步近 10 分钟或未入库记录。这样不需要 10 万条“全量对比”,性能提升比任何并发/多线程都大。
    • 若上游暂不支持,先落地我们已有的并发方案,在测试环境测出真实耗时,再推动接口方提供增量参数。

所以当前版本的“并发请求 + 单线程批处理 + 统一权限重构”已经是现阶段较优折中。再去同时把数据处理做多线程,很难保证数据库一致性且收益有限。真正的“最优”,是让每次任务只处理少量增量 + DB 端批处理,用结构化设计解决问题,而不是盲目堆线程。


User

这里做对比的业务是用于更新历史数据内容


Cursor

理解了:当前同步逻辑并非只拉“最近变更”,而是要不断对比/更新历史数据,保证本地 uf_cgjsd 与浪潮端保持一致。所以:

  • 我们在 JSP/定时任务里做的是“全字段对比 + 差异更新”,即便记录是几个月前的历史单,也会重新对齐金额或状态。这也是为什么 processRecordCompareAndUpsert 要拉取整页数据后,再逐字段比较、只更新变化字段。
  • 这种模式适合处理上游补录、回冲或历史纠错的场景,但成本就是每轮都要扫描大量记录。之前提到的增量优化,也可以做“混合模式”:
    • 常规调度:只处理近期新增/修改(速度快)。
    • 历史修复批任务:按天/月分批重刷,或像现在这样“全量对比”,但在低频窗口(比如每日夜间)执行。
  • 如果历史更新是稀有事件,也可以考虑给下游加“变更标记”或“待对账列表”,只针对有异常的历史记录触发重新同步,不必每次都全量对比。

总之,以“更新历史数据”为目标,目前的字段对比+差异更新逻辑可以满足,但要注意调度窗口、接口压力和数据库资源,必要时把历史刷新拆成后台批处理,以免影响每 10 分钟的常规同步。


User

这里历史数据更新不是稀有情况


Cursor

如果历史数据被频繁回写/补录,那“常规增量 + 偶发全量”策略就不够用了,这时最好建立一个长期可运行的历史对账机制,但仍要控制资源消耗。可考虑:

  • 按时间片滚动对账:把历史分段(例如按月份、按项目),每次任务仅处理一个片区,循环轮询。这样一次仍是“全字段对比”,但每轮只扫一部分历史,既能不断修正旧数据,又不会每 10 分钟都扫 10 万条。
  • 基于上游的“最近更新时间”或版本号:如果浪潮接口能返回 lastModified,即便是历史单,只要它修改过仍会被拉到本次任务,真正避免遗漏;目前你每次全量拉取的做法就是在模拟这种机制,但量太大,可以推动上游提供更新时间字段。
  • 维护本地“疑似需重新同步”队列:当下游系统检测到历史字段异常或业务人员手动触发时,把单据 ID 推进重同步队列,任务优先处理这些 ID,之后再做普通增量。这样虽然复杂一些,但能保证高频历史更新不会被遗漏,同时避免每次都扫全表。

换句话说,既然历史变更很常见,就应当正式设计“历史数据修复”流程,而不是永久依赖“全量对比”。当前的并发版本能暂时支撑,但为了长期稳定,建议尽快落地上述分段对账或队列机制,减轻接口和数据库压力。


User

多线程不行,中间会掉数据


Cursor

是的,直接把多页请求并行拉取再在 JSP 中落库,确实有几类“丢数据”风险:

  • 接口层本身无序:线程 A、B 同时请求相邻页,如果上游那边刚好新增/删除数据,返回的数据会重排或跳页,可能导致某些记录被跳过或重复。
  • 落库阶段没有事务边界:我们在 JSP 里依次更新数据库,如果多线程一起写,可能出现记录 A 被线程1更新后,线程2又根据旧数据覆盖回去,造成数据丢失或回滚。
  • 关联更新 & 权限重构是全局操作:并发线程同时执行关联 SQL、重建权限,会互相覆盖或锁住表,部分线程失败时就需要重试,否则就会遗漏该批数据。
  • 失败重试难以追踪:单线程时出错能精确定位页码和记录;多线程同时跑时,一旦某个线程挂掉或接口 3003 先返回,其他线程可能继续处理后续页,导致当页数据直接丢失。

所以我们在最近的修改里把“多线程并发”限制在只对接口请求,数据落库仍保持单线程批处理。这样即使请求顺序打乱,处理时仍按每个线程返回的页逐条对比,不会互相覆盖或漏掉记录。如果你继续观察到数据缺失,建议:

  1. 先回退到单线程(把 INITIAL_THREAD_COUNT/MAX_THREAD_COUNT 调成 1),确认是否稳定;
  2. 若单线程正常,多线程有问题,说明接口分页在并行情况下无法保证结果一致,这就需要维持单线程拉取,或让接口端提供“游标/版本号”。
  3. 另外可以在 JSP 里为每页增加日志和校验(比如统计 billnum 集合),在重跑时对比,确认是否真的漏了某页。

总之,多线程只适合提升吞吐,不适合在缺乏强一致保障的历史对账场景里直接上。当前版本中如需完全避免风险,可把线程数调回 1,并结合增量/分段对账等方式减少整体耗时。


User

一个线程也不行,pass多线程

评论

文档目录