Skip to content

支付回执与权益事务 ​

java-db 的线程绑定连接让收款事实、权益批次和原操作结果可以在同一短事务中提交。结合数据库唯一约束和行锁,可以同时处理重复回执、并发通知和返回丢失。

区分三个事实 ​

发起付款、核明实际收款、实际权益交付分别建模。提交付款请求后发生超时,保留原请求号并查询原请求;页面成功提示不能作为收款依据。回执先完成验签、商户作用域、币种、金额与订单匹配,再进入下列本地事务。

示例约定订单由服务端锁定金额,现金表对 (provider, merchant_scope, movement_no) 建立唯一约束,权益表对 order_id 建立唯一约束。这里只演示一单一批次;多现金行应使用订单行作为授予来源。

sql
create table demo_order (
  id bigint primary key,
  expected_fen bigint not null check(expected_fen > 0),
  beneficiary_id bigint not null,
  quantity bigint not null check(quantity > 0),
  state varchar(24) not null
);
create table demo_receipt (
  id bigint primary key,
  order_id bigint not null unique references demo_order(id),
  provider varchar(32) not null,
  merchant_scope varchar(64) not null,
  movement_no varchar(128) not null,
  amount_fen bigint not null check(amount_fen > 0),
  unique(provider, merchant_scope, movement_no)
);
create table demo_card_batch (
  id bigint primary key,
  order_id bigint not null unique references demo_order(id),
  beneficiary_id bigint not null,
  quantity bigint not null check(quantity > 0)
);
java
import java.sql.Connection;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;

public final class VerifiedReceiptService {
  public void accept(long orderId, String provider, String merchantScope,
      String movementNo, long verifiedAmountFen) {
    boolean committed = Db.tx(Connection.TRANSACTION_READ_COMMITTED, () -> {
      Row order = Db.findFirst("select * from demo_order where id=? for update", orderId);
      if (order == null || order.getLong("expected_fen") != verifiedAmountFen) {
        throw new IllegalArgumentException("Verified receipt does not match the order");
      }
      Row prior = Db.findFirst("""
          select order_id, amount_fen from demo_receipt
          where provider=? and merchant_scope=? and movement_no=?
          """, provider, merchantScope, movementNo);
      if (prior != null) {
        if (prior.getLong("order_id") != orderId
            || prior.getLong("amount_fen") != verifiedAmountFen) {
          throw new IllegalStateException("Receipt reconciliation is required");
        }
        return true;
      }
      Db.update("""
          insert into demo_receipt(id, order_id, provider, merchant_scope, movement_no, amount_fen)
          values(?,?,?,?,?,?)
          """, SnowflakeIdUtils.id(), orderId, provider, merchantScope, movementNo, verifiedAmountFen);
      Db.update("""
          insert into demo_card_batch(id, order_id, beneficiary_id, quantity) values(?,?,?,?)
          """, SnowflakeIdUtils.id(), orderId, order.getLong("beneficiary_id"), order.getLong("quantity"));
      Db.update("update demo_order set state='GRANTED' where id=?", orderId);
      return true;
    });
    if (!committed) {
      throw new IllegalStateException("Receipt transaction was rolled back");
    }
  }
}

Db.tx 与其中的 Db 操作共享当前数据源的线程连接;事务统一使用同一隔离级别,避免内层调用尝试改变已经开始的事务。异常使相关事实一起回滚,调用方应保留原通知标识,重新查询后再以原标识重试。不要在数据库事务内部等待外部付款网络请求。

该示例的一单一收款约束适合演示正常订单。真实通道如果允许一单出现第二笔独立收款,应另存异常现金事实并对账,不能仅因已发卡而丢弃第二笔实际现金。

退款与本金覆盖 ​

退款申请、局部保留、独立批准、指定执行、外部终局各有状态。执行发出后的取消请求只能记为待确认,收到原执行不可能成功的终局证据后再释放。部分成功只撤销成功范围,其余部分需分别有终局证据。

现金支出以实际移动号去重;本金覆盖以原现金行的范围并集去重。两笔独立支出覆盖相同本金时,两笔现金都保留,但本金只覆盖一次。因此净留款可能为负,差额单独核验,不自动成为客户债务。金额已核但覆盖未核时,显示分项待核,不自行猜测优先退已用还是未用。

对数量型服务,累计交付金额采用整数分计算。联系实际揭示、卡量扣减、原现金行交付范围及原操作结果放入同一事务;解密或授权校验失败则整个事务回滚。退款不删除原交付历史。

验证建议 ​

  • 并发发送同一回执,只有一笔收款事实和一次授予。
  • 授予写入故障时收款事务回滚,原回执可重试。
  • 退款与最后一次联系同时发生,只允许一个动作占用最后单位。
  • 退款处理中重复取消,保留量保持不变。
  • 独立拒付后迟到退款如实入账,本金范围只覆盖一次。
  • 卡使用期结束后仍可查询原现金、退款和未结服务责任。

按实际服务区间交付的业务,可继续阅读商业库存与按量履约事务,将现金行累计金额与独立服务区间一起维护。