PAYATHON 2026

如何从原始付款记录获取最新 payment status

支付老李

结论

程序卡住,是因为循环查询得到的是 rs2,读取数据时用的却还是 rs。结果就是 isales_id 和 ref_isales_id 一直没有更新,while 条件始终为真,最终形成死循环。

循环也不该用当前记录的 ref_isales_id != 0 判断是否继续。沿付款链向后查询时,应该在找不到 ref_isales_id = 当前 isales_id 的下一条记录时结束。

如果使用 MySQL 8.0 及以上版本,可以用递归 CTE 一次查出整条付款链。旧版本可以继续使用 Java 循环,但要确保读取 rs2,在没有下一条记录时退出,并处理可能出现的循环引用。

为什么现有循环不会结束

问题出在这里:

if (rs2.next()) {
    isales_id = rs.getString("isales_id");
    // 其余字段也全部从 rs 读取
    ref_isales_id = rs.getString("ref_isales_id");
}

rs2.next() 移动的是第二个结果集,后续读取的却是第一个结果集 rs。第一次查询完成后,rs 中的数据已经固定,循环变量自然不会更新。

即使把这里改成 rs2,原来的循环条件仍然不对:

while (!"0".equals(ref_isales_id))

ref_isales_id 表示当前记录指向的上一条记录,并不代表下一条记录。只要当前记录不是原始记录,这个值通常就不是 "0"。要判断是否还有下一条记录,只能查询:

WHERE ref_isales_id = 当前记录的 isales_id

方案一:使用 MySQL 8.0+ 递归 CTE

假设付款记录是一条单链,也就是每条付款记录最多只有一条后续记录,可以通过一次查询取得付款链中最后一条非全额付款记录:

WITH RECURSIVE payment_chain AS (
    SELECT
        isales_id,
        rce,
        total_payment,
        pay_status,
        pay_amount,
        remaining_bal,
        it_id,
        client_id,
        ic_id,
        ref_isales_id,
        0 AS depth
    FROM installation_sales
    WHERE isales_id = ?

    UNION ALL

    SELECT
        child.isales_id,
        child.rce,
        child.total_payment,
        child.pay_status,
        child.pay_amount,
        child.remaining_bal,
        child.it_id,
        child.client_id,
        child.ic_id,
        child.ref_isales_id,
        parent.depth + 1
    FROM installation_sales AS child
    JOIN payment_chain AS parent
      ON child.ref_isales_id = parent.isales_id
)
SELECT
    isales_id,
    rce,
    total_payment,
    pay_status,
    pay_amount,
    remaining_bal,
    it_id,
    client_id,
    ic_id,
    ref_isales_id
FROM payment_chain
WHERE pay_status <> 'full payment'
ORDER BY depth DESC
LIMIT 1;

对应的 Java 代码如下:

String sql =
    "WITH RECURSIVE payment_chain AS (" +
    "    SELECT isales_id, rce, total_payment, pay_status, pay_amount, " +
    "           remaining_bal, it_id, client_id, ic_id, ref_isales_id, " +
    "           0 AS depth " +
    "    FROM installation_sales " +
    "    WHERE isales_id = ? " +
    "    UNION ALL " +
    "    SELECT child.isales_id, child.rce, child.total_payment, " +
    "           child.pay_status, child.pay_amount, child.remaining_bal, " +
    "           child.it_id, child.client_id, child.ic_id, " +
    "           child.ref_isales_id, parent.depth + 1 " +
    "    FROM installation_sales child " +
    "    JOIN payment_chain parent " +
    "      ON child.ref_isales_id = parent.isales_id " +
    ") " +
    "SELECT isales_id, rce, total_payment, pay_status, pay_amount, " +
    "       remaining_bal, it_id, client_id, ic_id, ref_isales_id " +
    "FROM payment_chain " +
    "WHERE pay_status <> 'full payment' " +
    "ORDER BY depth DESC " +
    "LIMIT 1";

try (Connection conn = MySQLConnect.ConnectDB();
     PreparedStatement ps = conn.prepareStatement(sql)) {

    ps.setString(1, clickedTable);

    try (ResultSet rs = ps.executeQuery()) {
        if (rs.next()) {
            isales_id = rs.getString("isales_id");
            rce = rs.getString("rce");
            total_payment = rs.getDouble("total_payment");
            pay_status = rs.getString("pay_status");
            pay_amount1 = rs.getDouble("pay_amount");
            remaining_bal = rs.getDouble("remaining_bal");
            it_id = rs.getInt("it_id");
            client_id = rs.getString("client_id");
            ic_id = rs.getString("ic_id");
            ref_isales_id = rs.getString("ref_isales_id");
        }
    }
} catch (SQLException e) {
    e.printStackTrace();
}

这里把 pay_status <> 'full payment' 放在递归结束后的最终查询中,是为了先遍历完整的付款链,再选择最后一条符合条件的记录。如果在递归部分提前排除某种状态,该记录之后的节点也可能无法访问。

方案二:修正 Java 循环

如果数据库不支持递归 CTE,可以逐条查询后续记录。循环应在没有查到下一条记录时退出:

String rootSql =
    "SELECT isales_id, rce, total_payment, pay_status, pay_amount, " +
    "       remaining_bal, it_id, client_id, ic_id, ref_isales_id " +
    "FROM installation_sales " +
    "WHERE isales_id = ?";

String nextSql =
    "SELECT isales_id, rce, total_payment, pay_status, pay_amount, " +
    "       remaining_bal, it_id, client_id, ic_id, ref_isales_id " +
    "FROM installation_sales " +
    "WHERE ref_isales_id = ? " +
    "ORDER BY isales_id DESC " +
    "LIMIT 1";

try (Connection conn = MySQLConnect.ConnectDB();
     PreparedStatement rootStatement = conn.prepareStatement(rootSql);
     PreparedStatement nextStatement = conn.prepareStatement(nextSql)) {

    rootStatement.setString(1, clickedTable);

    try (ResultSet rootResult = rootStatement.executeQuery()) {
        if (!rootResult.next()) {
            return;
        }

        isales_id = rootResult.getString("isales_id");

        if (!"full payment".equals(rootResult.getString("pay_status"))) {
            copyPaymentData(rootResult);
        }
    }

    Set<String> visitedIds = new HashSet<>();
    visitedIds.add(isales_id);

    while (true) {
        nextStatement.setString(1, isales_id);

        try (ResultSet nextResult = nextStatement.executeQuery()) {
            if (!nextResult.next()) {
                break;
            }

            String nextId = nextResult.getString("isales_id");

            // 防止错误数据形成 A -> B -> A 一类循环引用
            if (!visitedIds.add(nextId)) {
                throw new SQLException(
                    "检测到付款记录循环引用,isales_id=" + nextId
                );
            }

            isales_id = nextId;

            if (!"full payment".equals(nextResult.getString("pay_status"))) {
                copyPaymentData(nextResult);
            }
        }
    }
} catch (SQLException e) {
    e.printStackTrace();
}

公共字段的读取逻辑可以抽取成一个方法:

private void copyPaymentData(ResultSet rs) throws SQLException {
    isales_id = rs.getString("isales_id");
    rce = rs.getString("rce");
    total_payment = rs.getDouble("total_payment");
    pay_status = rs.getString("pay_status");
    pay_amount1 = rs.getDouble("pay_amount");
    remaining_bal = rs.getDouble("remaining_bal");
    it_id = rs.getInt("it_id");
    client_id = rs.getString("client_id");
    ic_id = rs.getString("ic_id");
    ref_isales_id = rs.getString("ref_isales_id");
}

下面这段代码负责在没有后续付款记录时结束循环:

if (!nextResult.next()) {
    break;
}

查到记录后,数据必须从本次查询得到的 nextResult 中读取,不能继续使用第一次查询的 ResultSet。

还需要注意的地方

不要吞掉异常

原代码中的空 catch 会把 SQL 错误和循环读取错误全部隐藏:

catch (Exception e) {
}

至少要记录异常:

catch (SQLException e) {
    e.printStackTrace();
}

正式项目可以改用统一的日志记录,并在界面中显示用户能够理解的错误信息。

使用 PreparedStatement

不要直接拼接 clickedTable:

"... WHERE isales_id='" + clickedTable + "'"

应使用参数占位符 ?。这既能避免 SQL 注入,也能减少字符串类型、引号和特殊字符引发的错误。如果 isales_id 是整数,应使用:

ps.setInt(1, Integer.parseInt(clickedTable));

ORDER BY isales_id DESC 不一定代表“最新”

只有当 isales_id 单调递增,而且数值大小确实对应付款顺序时,这种排序才可靠。更稳妥的方式是保存明确的时间列,例如 created_at 或 payment_at:

ORDER BY payment_at DESC, isales_id DESC
LIMIT 1

明确是否允许多个后续记录

如果一个 isales_id 可以被多条记录通过 ref_isales_id 引用,数据结构就是一棵树,而不是单链。此时,LIMIT 1 只会选择排序最靠前的一条分支。业务规则需要明确:究竟是选择付款时间最新的一条,还是遍历所有分支。

如果业务规定每条记录只能有一个直接后继,可以考虑给 ref_isales_id 添加唯一约束。否则,应使用递归 CTE 遍历所有分支,再根据明确的时间字段选择最新记录。

不要在 Swing 事件线程中执行长查询

manageBalancesTableMouseClicked 通常运行在 Swing Event Dispatch Thread 上。数据库查询和多次循环查询会阻塞界面。即使代码没有死循环,用户也会觉得程序“卡住”。可以把查询放进 SwingWorker 等后台任务,执行完成后再更新控件。

数据库还应为关联列建立索引:

CREATE INDEX idx_installation_sales_ref
    ON installation_sales (ref_isales_id);

如果经常同时按引用和付款状态查询,也可以结合实际执行计划考虑联合索引。具体采用哪种索引,应根据表的数据量和 EXPLAIN 结果决定。

备注:内容仅供参考。