PAYATHON 2026

JEE6 电商项目如何集成银行转账与支付系统?

支付阿杰

结论

如果客户选择“下单后自行银行转账,商家确认到账再发货”,JEE6 应用通常不必直接调用银行转账 API。系统只需要生成订单和付款说明,保存用于识别银行流水的信息,并在款项到账后由管理员手动确认,或通过自动对账确认。

只有需要实时收款、自动核对到账、银行卡支付或退款等功能时,才需要接入银行开放接口或第三方支付服务。支付服务与 JEE6、GlassFish 并没有强绑定关系。应用只要能发送 HTTPS 请求、接收回调,并安全地保存订单状态,就可以完成集成。GlassFish V3.0 本身不会限制电子支付,但它和所依赖的 Java 版本已经比较旧。用于生产环境时,需要特别检查 TLS、证书、密码套件及安全更新能否满足支付服务当前的要求。

先区分两种支付模式

普通银行转账

这是最简单的实现方式:

  1. 用户在商城提交订单。
  2. 系统生成唯一订单号和转账附言。
  3. 页面展示收款账户、金额、币种和付款期限。
  4. 用户通过网银或银行柜台自行转账。
  5. 商家登录银行后台核对到账记录。
  6. 管理员在商城后台将订单标记为“已付款”。
  7. 仓库发货。

在这个流程中,商城不负责发起资金转移,也不会接触用户的银行凭据,因此通常不需要银行 API。它本质上是在线登记一种线下付款方式。

支付网关或支付服务

如果希望用户直接在结账页面完成银行卡、电子钱包或即时银行支付,通常需要接入第三方支付服务。一般流程如下:

  1. 商城在服务端创建支付请求。
  2. 支付服务返回托管收银台地址或支付会话。
  3. 用户跳转到支付服务页面完成付款。
  4. 支付服务将用户浏览器重定向回商城。
  5. 支付服务另外通过 webhook 通知商城支付结果。
  6. 商城验证通知并更新订单状态。

具体选择哪种服务,取决于经营国家、收款币种、公司资质和目标支付方式。PayPal、Stripe、Adyen 等可以作为调研对象,但它们支持的地区、接口和合规要求会发生变化。选型时应以服务商当前的官方文档和合同为准。部分国家或地区也有本地银行转账聚合服务。

推荐从人工确认转账开始

对于规模较小、订单量不高的商店,第一版可以先采用人工对账。需要保存的主要信息包括:

  • 订单号;
  • 应付金额和币种;
  • 唯一转账附言;
  • 收款账户标识;
  • 付款期限;
  • 用户提交的转账日期或凭证;
  • 实际到账金额;
  • 银行流水号;
  • 确认人员和确认时间;
  • 订单及付款状态。

转账附言可以使用订单号,但不要只依赖用户姓名。姓名可能重复,也可能被银行格式化,导致无法准确匹配。

订单状态最好明确区分:

CREATED
AWAITING_PAYMENT
PAYMENT_REVIEW
PAID
FULFILLING
SHIPPED
CANCELLED
REFUNDED

不要用一个简单的 paid 布尔值表示所有业务状态。布尔值无法描述等待付款、少付、多付、审核中和退款等情况。

JEE6 中的基本数据模型

下面是一个简化的 JPA 实体示例。实际项目还应加入商品明细、收货信息、版本控制和审计记录。

@Entity
@Table(name = "shop_order")
public class ShopOrder {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 40)
    private String orderNumber;

    @Column(nullable = false, precision = 19, scale = 2)
    private BigDecimal amount;

    @Column(nullable = false, length = 3)
    private String currency;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 30)
    private OrderStatus status;

    @Column(nullable = false, unique = true, length = 64)
    private String paymentReference;

    private Date paymentDeadline;
    private Date paidAt;

    @Version
    private long version;

    // getters and setters
}

金额必须使用 BigDecimal,不能使用 double 或 float:

BigDecimal total = unitPrice
        .multiply(BigDecimal.valueOf(quantity))
        .setScale(2, RoundingMode.HALF_UP);

币种也应单独保存,例如 CNY、EUR 或 USD,不能假定所有订单使用同一种币种。小数位数和舍入规则还要根据具体币种及支付服务的要求确定。

管理员确认收款时,应在事务中检查订单当前状态:

@Stateless
public class PaymentService {

    @PersistenceContext
    private EntityManager entityManager;

    public void confirmBankTransfer(
            Long orderId,
            BigDecimal receivedAmount,
            String bankTransactionId) {

        ShopOrder order = entityManager.find(
                ShopOrder.class,
                orderId,
                LockModeType.OPTIMISTIC
        );

        if (order == null) {
            throw new IllegalArgumentException("Order not found");
        }

        if (order.getStatus() == OrderStatus.PAID) {
            return;
        }

        if (order.getStatus() != OrderStatus.AWAITING_PAYMENT
                && order.getStatus() != OrderStatus.PAYMENT_REVIEW) {
            throw new IllegalStateException("Order cannot be paid in current state");
        }

        if (receivedAmount.compareTo(order.getAmount()) != 0) {
            throw new IllegalStateException("Received amount does not match order amount");
        }

        order.setStatus(OrderStatus.PAID);
        order.setPaidAt(new Date());

        // bankTransactionId 应保存到独立的 Payment 实体中,
        // 此处省略该实体以保持示例简洁。
    }
}

实际系统应建立独立的 Payment 表。一个订单可能有多次付款尝试,也可能出现拆分付款、重复转账或退款,因此不能把所有付款信息都放进订单表。

第三方支付软件通常如何工作

第三方支付通常提供以下一种或多种集成方式。

托管收银台

用户会被重定向到支付服务商维护的页面,付款结束后再返回商城。

这种方式通常更适合缺少支付系统开发经验的团队。银行卡号等敏感信息不会直接进入商城服务器,合规范围相对较小。不过,具体需要承担哪些合规责任,仍取决于接入方式、所在地区和服务商协议。

嵌入式支付组件

支付表单显示在商城页面中,但敏感数据通常由服务商提供的 JavaScript 组件直接提交给服务商。商城得到的是 token 或支付方式标识,而不是原始银行卡数据。

这种方式能让支付流程更加连贯,但前端安全、内容安全策略和合规配置也会更复杂。

服务端 API

商城后端通过 HTTPS 创建支付、查询状态或发起退款。例如:

POST /payments
Authorization: Bearer <secret>
Content-Type: application/json
Idempotency-Key: 2e581f30-53e0-4ec1-a64f-a87f1fab63cc

{
  "merchantOrderId": "ORD-20260901-000123",
  "amount": 129900,
  "currency": "CNY",
  "returnUrl": "https://shop.example.com/payment/return",
  "notificationUrl": "https://shop.example.com/api/payment/webhook"
}

这只是一个通用结构,并非某个支付服务的真实接口。金额是使用最小货币单位还是十进制字符串,以及字段名称如何定义,都必须遵循所选服务的官方文档。

Webhook 通知

支付状态发生变化后,支付服务会主动调用商城提供的 HTTPS 地址。浏览器返回地址只能用于展示结果,不能作为确认付款的唯一依据。用户可能直接关闭页面,攻击者也可能伪造请求。

可靠的判断依据通常包括:

  • 通过签名验证的 webhook;
  • 服务端调用支付 API 查询到的最终状态;
  • 银行或支付机构提供的正式对账记录。

JEE6 中接收支付回调

JEE6 可以使用 JAX-RS 接收 webhook:

@Path("/payment/webhook")
public class PaymentWebhookResource {

    @EJB
    private PaymentWebhookService webhookService;

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response receive(
            String rawBody,
            @HeaderParam("X-Payment-Signature") String signature) {

        if (!webhookService.isValidSignature(rawBody, signature)) {
            return Response.status(Response.Status.UNAUTHORIZED).build();
        }

        webhookService.process(rawBody);
        return Response.ok().build();
    }
}

签名算法、请求头名称、时间戳校验方式和密钥格式因支付服务而异,不能自行推测。必须严格按照服务商文档验证原始请求体。如果先解析 JSON,再重新序列化,空格、字段顺序或转义方式的变化都可能导致验签失败。

处理 webhook 时还必须保证幂等性,因为同一个事件可能被重复发送:

@Stateless
public class PaymentWebhookService {

    @PersistenceContext
    private EntityManager entityManager;

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void processVerifiedEvent(PaymentEvent event) {
        ProcessedEvent existing =
                entityManager.find(ProcessedEvent.class, event.getEventId());

        if (existing != null) {
            return;
        }

        ShopOrder order = findOrder(event.getMerchantOrderId());

        if (!order.getCurrency().equals(event.getCurrency())
                || order.getAmount().compareTo(event.getAmount()) != 0) {
            throw new IllegalStateException("Payment details do not match order");
        }

        if (event.isSuccessful() && order.getStatus() != OrderStatus.PAID) {
            order.setStatus(OrderStatus.PAID);
            order.setPaidAt(new Date());
        }

        entityManager.persist(new ProcessedEvent(event.getEventId()));
    }

    private ShopOrder findOrder(String orderNumber) {
        return entityManager.createQuery(
                        "select o from ShopOrder o where o.orderNumber = :number",
                        ShopOrder.class)
                .setParameter("number", orderNumber)
                .getSingleResult();
    }
}

除了事件 ID,还可以为服务商支付 ID 建立唯一约束,防止同一笔付款被关联到多个订单。

集成复杂度主要在哪里

从 Java 代码本身来看,发送 HTTPS 请求和接收 JSON 并不难。需要投入更多精力的是业务完整性和安全边界:

  • 创建订单时锁定商品价格和应付金额;
  • 防止重复提交导致重复付款;
  • 校验订单号、金额、币种和商户账户;
  • 验证 webhook 签名和时间戳;
  • 正确处理重复通知与乱序通知;
  • 区分支付成功、待处理、失败、取消和退款;
  • 记录原始事件和状态变更,以便审计;
  • 设置超时、重试和故障告警;
  • 处理少付、多付、重复付款和退款;
  • 定期使用银行账单或支付报表进行对账。

支付接口返回“成功”,并不表示商品一定可以立即发出。某些付款方式会先进入待处理状态,之后才会成功或失败。只有达到服务商定义的可交付最终状态,系统才能触发发货。

GlassFish V3.0 的注意事项

GlassFish V3.0 对电子支付没有特殊限制。支付服务通常就是外部 HTTPS API,因此应用服务器只需提供常规的 Java EE 能力。

需要重点检查的是运行环境是否过旧:

  • Java 运行时是否支持服务商要求的 TLS 版本;
  • CA 根证书是否仍然有效且完整;
  • HTTPS 客户端是否支持所需的密码套件;
  • JSON 库能否满足服务商 SDK 的要求;
  • 服务商官方 SDK 是否仍支持当前 Java 版本;
  • 应用服务器和 JDK 是否仍能获得安全更新。

如果官方 SDK 要求使用更高版本的 Java,也不一定必须先升级整个应用。只要服务商支持,可以直接调用 REST API。不过,从安全维护的角度看,不应长期使用已经停止维护的 JDK 或应用服务器来运行互联网支付系统。能否原地升级,还需要结合现有应用的兼容性单独评估。

GlassFish 的连接池和线程管理同样需要注意。不要为每个请求创建无限量线程,也不要在 webhook 请求中执行耗时的发货流程。回调处理应尽快完成验证和持久化,然后通过 JMS 等异步机制触发库存、邮件和发货任务。

安全与合规要求

无论采用哪种方案,至少都应遵守以下原则:

  • 整个结账流程强制使用 HTTPS;
  • API secret、签名密钥和数据库密码不得写入源码;
  • 生产环境和测试环境使用不同的密钥及账户;
  • 不在日志中记录银行卡号、安全码、访问令牌或完整个人信息;
  • 管理员确认收款和退款时必须经过身份认证与权限控制;
  • 为关键操作保存不可随意修改的审计记录;
  • 订单状态更新必须在数据库事务中完成;
  • webhook 地址必须验证签名,不能只依赖 IP 白名单;
  • 定期核对商城记录、支付服务报表和银行账单。

除非团队具备相应的安全及合规能力,否则不要自行采集或保存银行卡号、CVV 等敏感数据。使用托管收银台通常更稳妥,但这并不意味着商户会自动免除所有 PCI DSS、隐私保护、税务或消费者权益方面的责任。具体要求取决于经营地区、支付方式和合同关系,应咨询支付服务商及当地专业人士。

合理的开发顺序

第一阶段可以只提供普通银行转账:

  1. 建立订单和付款数据模型。
  2. 生成不可预测且唯一的订单号。
  3. 展示收款账户、金额、币种和转账附言。
  4. 提供由管理员人工确认到账的后台功能。
  5. 实现订单状态流转和审计日志。
  6. 建立每日对账流程。
  7. 确认付款后再触发发货。

订单号不应简单地使用数据库自增 ID,例如 1234。这种编号会泄露订单量,也容易被枚举。可以改用随机标识,或使用包含随机部分的业务编号。

第二阶段再评估自动化:

  1. 确认销售地区和目标支付方式。
  2. 比较服务费、结算周期、退款规则和争议处理方式。
  3. 申请支付服务的测试账户。
  4. 优先集成托管收银台。
  5. 实现 webhook 验签和幂等处理。
  6. 测试成功、失败、取消、超时、重复通知和退款等情况。
  7. 在测试环境完成对账后,再启用生产账户。

如果需求只是“客户自行汇款,到账后发货”,一开始没有必要引入复杂的支付网关。先完善订单状态、人工对账和审计记录,就能形成一套可用的传统电商付款流程。等订单量增加,或用户需要即时支付时,再接入第三方支付服务更合适。

备注:内容仅供参考。