PAYATHON 2026

Struts 1.3 中应使用哪个 API 验证信用卡并处理支付?

支付老李

结论

Struts 1.3 没有验证信用卡真实性或处理付款的标准 API。GenericValidator.isCreditCard() 只能检查卡号的格式,例如长度、字符和校验位。它无法确认卡片是否真实存在、是否过期、CVV 是否正确,也不能完成扣款。

实际支付需要接入收单机构或支付服务商的支付网关 API。建议优先使用服务商托管的支付页面、Hosted Fields 或前端 Tokenization,避免信用卡号和 CVV 经过 Struts 应用服务器。

如何划分验证和支付

一次信用卡支付通常分为三层处理:

  1. 页面验证
    检查必填项以及月份、年份的格式,减少用户输入错误。这类验证只能改善用户体验,不能作为安全保障。

  2. 服务端基础验证
    检查卡号格式、过期日期、金额和订单状态等。GenericValidator.isCreditCard() 属于这一层,只能识别明显的输入错误。

  3. 支付网关验证和授权
    支付服务商连接发卡行,检查卡片状态、CVV、余额或信用额度,并执行 3-D Secure 等认证。付款能否获得授权,最终由这一层决定。

即使 GenericValidator.isCreditCard() 返回 true,也不代表这张卡能够完成付款。

推荐的处理流程

较安全的支付流程可以这样设计:

  1. Struts 页面加载支付服务商提供的托管表单或 Hosted Fields。
  2. 浏览器把卡号、有效期和 CVV 直接提交给支付服务商。
  3. 支付服务商返回一次性的 payment token 或 payment method ID。
  4. 页面将该 token 和订单编号提交给 Struts Action。
  5. 服务端重新读取订单的应付金额,再调用支付网关 API。
  6. 根据 API 的返回结果,将订单标记为待处理、成功或失败。
  7. 通过支付网关的 webhook 再次确认最终支付状态。

最终付款金额不能由浏览器决定,也不能只根据支付成功页面或跳转参数更新订单状态。

Struts 1.3 中的代码结构

不同支付网关的接口各不相同。下面的代码只用于说明合理的分层方式,不对应任何特定服务商。

public interface PaymentGateway {
    PaymentResult authorize(
        String paymentToken,
        long amountInMinorUnits,
        String currency,
        String orderId,
        String idempotencyKey
    ) throws PaymentGatewayException;
}

PaymentResult 应保存网关返回的交易编号和具体状态,不要只用一个布尔值表示结果:

public final class PaymentResult {
    private final String transactionId;
    private final PaymentStatus status;
    private final String failureCode;

    public PaymentResult(
            String transactionId,
            PaymentStatus status,
            String failureCode) {
        this.transactionId = transactionId;
        this.status = status;
        this.failureCode = failureCode;
    }

    public String getTransactionId() {
        return transactionId;
    }

    public PaymentStatus getStatus() {
        return status;
    }

    public String getFailureCode() {
        return failureCode;
    }
}
public enum PaymentStatus {
    SUCCEEDED,
    PENDING,
    DECLINED
}

Struts Action 只接收支付 token,不接收原始卡号和 CVV:

public class SubmitPaymentAction extends Action {

    private PaymentGateway paymentGateway;
    private OrderService orderService;

    @Override
    public ActionForward execute(
            ActionMapping mapping,
            ActionForm form,
            HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        PaymentForm paymentForm = (PaymentForm) form;
        String orderId = paymentForm.getOrderId();
        String paymentToken = paymentForm.getPaymentToken();

        Order order = orderService.findPayableOrder(orderId);

        if (order == null || paymentToken == null
                || paymentToken.trim().length() == 0) {
            return mapping.findForward("invalid");
        }

        /*
         * 金额必须从服务端订单中读取。
         * 例如 1099 表示 10.99 个货币单位,避免使用浮点数处理金额。
         */
        long amount = order.getAmountInMinorUnits();

        try {
            PaymentResult result = paymentGateway.authorize(
                paymentToken,
                amount,
                order.getCurrency(),
                order.getId(),
                order.getPaymentAttemptId()
            );

            if (result.getStatus() == PaymentStatus.SUCCEEDED) {
                orderService.recordAuthorizedPayment(
                    order.getId(),
                    result.getTransactionId()
                );
                return mapping.findForward("success");
            }

            if (result.getStatus() == PaymentStatus.PENDING) {
                orderService.markPaymentPending(
                    order.getId(),
                    result.getTransactionId()
                );
                return mapping.findForward("pending");
            }

            orderService.recordPaymentFailure(
                order.getId(),
                result.getFailureCode()
            );
            return mapping.findForward("declined");

        } catch (PaymentGatewayException ex) {
            /*
             * 网络超时不一定表示付款失败。
             * 应保留本次支付尝试,并通过交易查询接口或 webhook 确认状态。
             */
            orderService.markPaymentUnknown(order.getId());
            return mapping.findForward("pending");
        }
    }
}

具体的 PaymentGateway 实现应调用所选支付服务商的官方 SDK 或 HTTPS API,并按照服务商文档处理认证、签名、超时、重试和 webhook。

服务端检查有效期

如果页面仍分别提交月份和年份,可以先做基础检查:

YearMonth expiration;

try {
    int month = Integer.parseInt(paymentForm.getExpirationMonth());
    int year = Integer.parseInt(paymentForm.getExpirationYear());

    expiration = YearMonth.of(year, month);
} catch (DateTimeException | NumberFormatException ex) {
    errors.add("expirationDate",
        new ActionMessage("error.payment.expiration.invalid"));
    return errors;
}

YearMonth currentMonth = YearMonth.now(ZoneOffset.UTC);

if (expiration.isBefore(currentMonth)) {
    errors.add("expirationDate",
        new ActionMessage("error.payment.expiration.expired"));
}

YearMonth 适用于 Java 8 及以上版本。如果旧项目使用更早的 Java 版本,需要通过 Calendar 或其他日期库实现相同的月份比较。无论采用哪种方式,本地检查都不能代替支付网关的授权结果。

选择支付 API 时需要考虑什么

信用卡支付没有适用于所有场景的统一 Java API。选择支付服务商时,需要考虑业务所在的国家或地区、结算币种、商户账户和合规要求,并确认服务商支持:

  • 托管支付页面或 Hosted Fields;
  • 前端 Tokenization,避免服务器接触卡号和 CVV;
  • 3-D Secure 等持卡人认证;
  • webhook 签名验证;
  • 幂等键,防止重复提交导致重复扣款;
  • 授权、扣款、撤销和退款;
  • 交易状态查询;
  • 官方支持的 Java SDK,或文档明确的 HTTPS API;
  • 当前适用的 PCI DSS 合规方案。

安全注意事项

不要将信用卡号、CVV 或完整的支付请求写入数据库、Session、访问日志或异常日志。授权完成后尤其不能保存 CVV。

还需要注意以下事项:

  • 所有页面和接口都必须使用 HTTPS;
  • 支付金额、币种和订单状态由服务端确定;
  • 提交支付时使用一次性 token 和幂等键;
  • 验证 webhook 的签名和事件标识;
  • 支付请求超时后主动查询交易状态,不要直接再次扣款;
  • 页面只显示卡号末四位等必要信息;
  • 支付密钥存放在安全配置或密钥管理系统中,不要写入 JSP 或源码;
  • 不要通过异常信息向用户暴露支付网关的内部细节。

GenericValidator.isCreditCard() 可以继续用于检查输入格式,但卡片验证和付款必须交给支付网关。新的实现应让 Struts 应用只处理支付 token、订单和交易状态,尽量避免接触原始信用卡数据。

备注:内容仅供参考。