PAYATHON 2026

如何在 jBilling scheduled plugin 中创建 payment?

支付小周

结论

创建不关联 invoice 的 payment 时,不要使用:

applyPayment(payment, null, executorUserId)

applyPayment 用于创建并应用付款,通常会进入 invoice 分配逻辑。如果只需保存一笔未分配付款,应调用当前 jBilling 版本中用于“创建 payment”的 API,例如内部接口的 create(...),或 Web Service API 的 createPayment(...)。

LazyInitializationException 与认证失败无关。userbl.webServicesAuthenticate(user, pass) 只负责身份验证,不会为 scheduled plugin 打开 Hibernate Session,因此不能解决该异常。

推荐实现

如果当前版本的 IPaymentSessionBean 提供 create 方法,可以在 scheduled plugin 中直接调用:

IPaymentSessionBean paymentSession =
        Context.getBean(Context.Name.PAYMENT_SESSION);

PaymentDTOEx payment = new PaymentDTOEx(paymentWS);

Integer paymentId = paymentSession.create(payment, executorUserId);

注意区分这里的两个用户标识:

  • payment 中的 userId:付款所属的客户。
  • executorUserId:执行本次操作的 jBilling 用户,主要用于权限检查和审计。

不同 jBilling 分支中的 create 方法签名可能略有不同,请以当前版本的 IPaymentSessionBean 接口为准。如果内部接口没有对应方法,可以改用公开 API:

JbillingAPI api = JbillingAPIFactory.getAPI(
        url,
        username,
        password
);

Integer paymentId = api.createPayment(paymentWS);

scheduled plugin 已经运行在 jBilling 服务端时,通常应优先调用内部 session bean,无需再次通过 Web Service 登录。

为什么会出现 LazyInitializationException

scheduled plugin 通常由调度线程执行,不会经过 Web 请求中的 Open Session in View 流程。以下情况都可能导致代码在 Hibernate Session 关闭后访问代理对象:

  • PaymentDTOEx 中保存了 Hibernate entity 或尚未初始化的关联对象。
  • payment、用户、币种或 payment instrument 是在之前的事务中加载的。
  • scheduled plugin 缓存了某个 entity,并在后续任务中继续使用。
  • 代码在事务外访问了 entity 的关联属性。
  • 实际取得的业务对象没有经过 Spring 或 EJB 的事务代理。

不要通过手动打开 Hibernate Session 来掩盖这类问题。任务之间只传递 ID 或普通 DTO,并在事务内重新加载所需数据,会更稳妥。

事务边界的处理

如果创建付款前还要读取用户、币种或支付方式,应将完整操作放进由 Spring 或 EJB 管理的事务服务:

@Service
public class ScheduledPaymentService {

    private final IPaymentSessionBean paymentSession;

    public ScheduledPaymentService(IPaymentSessionBean paymentSession) {
        this.paymentSession = paymentSession;
    }

    @Transactional
    public Integer createUnallocatedPayment(
            PaymentWS paymentWS,
            Integer executorUserId) {

        PaymentDTOEx payment = new PaymentDTOEx(paymentWS);
        return paymentSession.create(payment, executorUserId);
    }
}

scheduled plugin 应通过容器取得该服务:

ScheduledPaymentService service =
        Context.getBean(ScheduledPaymentService.class);

Integer paymentId =
        service.createUnallocatedPayment(paymentWS, executorUserId);

不要在 plugin 中通过 new ScheduledPaymentService(...) 创建服务对象,否则 @Transactional 不会生效。具体的 Bean 注册方式和 Context.getBean 重载形式取决于所使用的 jBilling 版本。

DTO 的构造要求

传给 payment service 的对象应是完整、独立的 DTO,不能携带从旧 Hibernate Session 中取得的实体代理。至少需要确认以下字段已经设置:

PaymentWS payment = new PaymentWS();

payment.setUserId(customerId);
payment.setAmount(amount);
payment.setCurrencyId(currencyId);
payment.setPaymentDate(new Date());
payment.setIsRefund(0);

还需要根据当前 jBilling 版本设置所需的 payment method 或 payment instrument 信息。字段要求会随版本和支付插件配置变化,请参考当前代码中的 PaymentWS、PaymentDTOEx 和 IPaymentSessionBean.create(...) 实现。

尤其不要将以下对象长期保存在 scheduled plugin 的实例字段中:

UserDTO user;
PaymentDTO payment;
InvoiceDTO invoice;

应保存对应的 ID,并在每次执行任务时,在事务内重新查询对象。

注意事项

  • invoiceId == null 不代表“只创建未分配付款”。它仍可能使 applyPayment 进入不适合该场景的逻辑。
  • webServicesAuthenticate(...) 不负责创建 Hibernate Session,也不会建立事务。
  • 不要把手动调用 Hibernate.initialize(...) 当作长期解决方案,因为调用时仍然必须存在有效的 Session。
  • payment 能否保持未分配状态,还可能受自动分配、账龄处理或付款处理插件配置影响。
  • 如果改用 create(...) 后仍然出现同样的异常,应结合异常堆栈确认是哪个属性触发了代理初始化。问题通常出现在 DTO 转换过程,或事务外读取实体关联的位置,而不是 invoiceId 本身。

备注:内容仅供参考。