JEE6 电商项目如何集成银行转账与支付系统?
结论
如果客户选择“下单后自行银行转账,商家确认到账再发货”,JEE6 应用通常不必直接调用银行转账 API。系统只需要生成订单和付款说明,保存用于识别银行流水的信息,并在款项到账后由管理员手动确认,或通过自动对账确认。
只有需要实时收款、自动核对到账、银行卡支付或退款等功能时,才需要接入银行开放接口或第三方支付服务。支付服务与 JEE6、GlassFish 并没有强绑定关系。应用只要能发送 HTTPS 请求、接收回调,并安全地保存订单状态,就可以完成集成。GlassFish V3.0 本身不会限制电子支付,但它和所依赖的 Java 版本已经比较旧。用于生产环境时,需要特别检查 TLS、证书、密码套件及安全更新能否满足支付服务当前的要求。
先区分两种支付模式
普通银行转账
这是最简单的实现方式:
- 用户在商城提交订单。
- 系统生成唯一订单号和转账附言。
- 页面展示收款账户、金额、币种和付款期限。
- 用户通过网银或银行柜台自行转账。
- 商家登录银行后台核对到账记录。
- 管理员在商城后台将订单标记为“已付款”。
- 仓库发货。
在这个流程中,商城不负责发起资金转移,也不会接触用户的银行凭据,因此通常不需要银行 API。它本质上是在线登记一种线下付款方式。
支付网关或支付服务
如果希望用户直接在结账页面完成银行卡、电子钱包或即时银行支付,通常需要接入第三方支付服务。一般流程如下:
- 商城在服务端创建支付请求。
- 支付服务返回托管收银台地址或支付会话。
- 用户跳转到支付服务页面完成付款。
- 支付服务将用户浏览器重定向回商城。
- 支付服务另外通过 webhook 通知商城支付结果。
- 商城验证通知并更新订单状态。
具体选择哪种服务,取决于经营国家、收款币种、公司资质和目标支付方式。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、隐私保护、税务或消费者权益方面的责任。具体要求取决于经营地区、支付方式和合同关系,应咨询支付服务商及当地专业人士。
合理的开发顺序
第一阶段可以只提供普通银行转账:
- 建立订单和付款数据模型。
- 生成不可预测且唯一的订单号。
- 展示收款账户、金额、币种和转账附言。
- 提供由管理员人工确认到账的后台功能。
- 实现订单状态流转和审计日志。
- 建立每日对账流程。
- 确认付款后再触发发货。
订单号不应简单地使用数据库自增 ID,例如 1234。这种编号会泄露订单量,也容易被枚举。可以改用随机标识,或使用包含随机部分的业务编号。
第二阶段再评估自动化:
- 确认销售地区和目标支付方式。
- 比较服务费、结算周期、退款规则和争议处理方式。
- 申请支付服务的测试账户。
- 优先集成托管收银台。
- 实现 webhook 验签和幂等处理。
- 测试成功、失败、取消、超时、重复通知和退款等情况。
- 在测试环境完成对账后,再启用生产账户。
如果需求只是“客户自行汇款,到账后发货”,一开始没有必要引入复杂的支付网关。先完善订单状态、人工对账和审计记录,就能形成一套可用的传统电商付款流程。等订单量增加,或用户需要即时支付时,再接入第三方支付服务更合适。
备注:内容仅供参考。
