PAYATHON 2026

Broadleaf REST API checkout/payment 如何创建 OrderPaymentWrapper

支付小周

结论

仅凭 customerId、addressId 和 orderId,无法正确创建 OrderPaymentWrapper。

在 Broadleaf REST checkout 流程中,通常由 Spring/Jackson 将请求体反序列化为 OrderPaymentWrapper。服务端随后需要:

  1. 根据 orderId 查询订单;
  2. 验证订单是否属于当前登录客户,不能直接信任 customerId;
  3. 根据 addressId 查询账单地址,并校验其归属;
  4. 设置支付类型、金额、账单地址和支付网关要求的字段;
  5. 将 wrapper 转换为 OrderPayment,再交给 Broadleaf 的支付服务保存或处理。

具体字段和方法名取决于 Broadleaf 版本及支付网关扩展,不能假定框架提供了一个接收这三个 ID 的通用构造函数。

推荐的请求方式

REST 客户端应通过请求体提交支付信息,例如:

POST /api/orders/123/payments
Content-Type: application/json
Authorization: Bearer <token>

请求体可以写成:

{
  "type": "CREDIT_CARD",
  "amount": "99.00",
  "billingAddressId": 456,
  "paymentToken": "gateway-generated-token"
}

这些只是常见字段名,实际应以项目中 OrderPaymentWrapper 的属性、Controller 参数和 Jackson 映射为准。

不要把原始银行卡号、CVV 等敏感数据直接传给 Broadleaf。通常由支付网关的前端 SDK 生成一次性 paymentToken,服务端只接收 token。

Endpoint 的处理思路

如果 addPaymentToOrder 已通过 @RequestBody 接收 OrderPaymentWrapper,一般不用在方法内重新 new 一个 wrapper:

@PostMapping("/orders/{orderId}/payments")
public OrderPaymentWrapper addPaymentToOrder(
        @PathVariable Long orderId,
        @RequestBody OrderPaymentWrapper paymentWrapper,
        HttpServletRequest request,
        HttpServletResponse response) {

    // 1. 根据 orderId 查询订单
    // 2. 验证订单属于当前登录客户
    // 3. 校验 billingAddressId、amount、type 等字段
    // 4. 将 wrapper 转换为 OrderPayment
    // 5. 保存支付记录或调用支付网关

    return paymentWrapper;
}

实际项目中,应优先复用当前 Broadleaf 版本已有的 endpoint、wrapper converter 和 payment service,避免在 Controller 里重复实现整套转换逻辑。

必须在服务端完成的校验

1. 校验订单归属

不能根据请求中的 customerId 判断订单归属。服务端应从登录上下文获取当前客户,再检查订单:

Customer customer = customerState.getCustomer();
Order order = orderService.findOrderById(orderId);

if (order == null || customer == null ||
        !customer.getId().equals(order.getCustomer().getId())) {
    throw new IllegalArgumentException("Order does not belong to the current customer");
}

这里的异常类型应换成项目统一使用的 REST 业务异常,例如能够返回 404 或 403 的异常。

2. 校验地址

addressId 只用于查询地址,不能直接赋给支付实体。查到地址后,还要确认当前客户有权使用它:

Address billingAddress = addressService.readAddressById(addressId);

if (billingAddress == null) {
    throw new IllegalArgumentException("Billing address was not found");
}

不同 Broadleaf 版本的地址查询方法和客户地址关联方式可能不同。归属校验需要结合项目中的 CustomerAddress、AddressService 或自定义地址服务完成。

3. 设置支付信息

如果确实要在服务层手工创建 wrapper,可以使用下面的结构:

OrderPaymentWrapper wrapper = new OrderPaymentWrapper();

// 以下 setter 名称取决于当前 Broadleaf 版本
wrapper.setOrderId(order.getId());
wrapper.setAmount(amount);
wrapper.setType(paymentType);
wrapper.setPaymentToken(paymentToken);

// 某些版本接收地址 ID,某些版本接收嵌套的 AddressWrapper
wrapper.setBillingAddressId(billingAddress.getId());

这段代码只说明创建流程,不代表其中所有 setter 都存在于你的 Broadleaf 版本中。如果 OrderPaymentWrapper 使用嵌套地址对象,应先通过项目提供的 wrapper 或 converter,将 Address 转换成 AddressWrapper:

AddressWrapper addressWrapper = addressConverter.wrap(billingAddress);
wrapper.setBillingAddress(addressWrapper);

转换器名称也取决于具体版本或项目实现,应以现有源码和 Spring Bean 配置为准。

customerId 应该放在哪里

通常不需要把 customerId 放入 OrderPaymentWrapper。客户、订单和支付之间的关系应由服务端实体关系建立:

Current Customer
    └── Order(orderId)
            └── OrderPayment
                    └── Billing Address

各个 ID 和 wrapper 的职责分别是:

  • customerId 用于鉴权或查询当前客户;
  • orderId 用于定位订单;
  • addressId 用于定位账单地址;
  • wrapper 用于承载支付类型、金额、token、账单地址等支付数据。

这样可以防止攻击者篡改 customerId,将支付信息添加到其他客户的订单中。

还需要注意

  • 服务端应根据订单余额核对支付金额,不能完全相信客户端提交的值。
  • 同一订单可能允许添加多个 OrderPayment,因此要检查累计支付金额是否超过应付金额。
  • 创建 OrderPayment 和真正扣款通常分属两个阶段,添加支付记录不代表支付成功。
  • 支付网关可能要求 nonce、token、交易类型、网关名称等额外字段。这些字段一般放在 wrapper 的扩展字段或网关专用请求对象中。
  • 如果当前版本提供了 OrderPaymentWrapper 的 unwrap、converter 或 endpoint adapter,应优先使用,避免手工构造 OrderPayment。
  • 字段定义应以项目实际依赖版本中的 OrderPaymentWrapper 类和现有 checkout endpoint 为准,因为不同 Broadleaf 版本的 REST wrapper API 并不完全一致。

备注:内容仅供参考。