Antom, leading provider of tailored payment solutionsAntom, leading provider of tailored payment solutions

快捷支付(Web/WAP)

注意:本文档已停止维护并下线归档,请访问 https://docs.antom.com/ 获取最新内容。

本文为您介绍支持从桌面浏览器或移动浏览器接收付款的集成解决方案。集成后,您可以接入数字钱包、银行卡和银行转账等多种支付方式。

用户体验

以下图片分别展示了钱包和银行转账的用户体验。在首次支付时,买家可以启用免密支付,这样后续支付无需再输入支付密码。后续支付的全流程都将在您的网站或应用上进行。

钱包

当使用钱包付款时,整个支付流程会保留在您的网站或应用程序内,买家可以在首次付款时启用免密支付,后续付款无需再输入支付密码。

第一次使用钱包支付

在首次支付时,买家完成支付授权流程以确保后续免密支付。

firsttime use wallet.png

银行转账

对于银行转账支付方式,以下图表展示了首次支付和后续支付的用户体验。

首次使用银行转账支付

在首次支付时,用户需要输入支付密码和验证码。

firstime use banktrans.png

支付流程

首次支付

当买家选择 Antom 提供的支付方式时,您需要采集支付请求 ID、订单金额、支付方式、订单详情、支付重定向链接和支付结果通知链接等必要信息。然后调用 支付会话创建(快捷支付)接口来下单,并调用 Antom SDK 中的相关方法完成支付流程。

流程图1中文.png

  1. 买家进入结账页面
  2. 创建支付会话请求
    在买家选择支付方式并提交订单后,您可以通过调用 支付会话创建(快捷支付)接口来获取支付会话。
  3. 调用客户端 SDK
    在客户端通过支付会话调用 SDK。SDK 将会基于支付方式的特点收集支付要素、展示代码信息、进行重定向、调用,并引导买家完成支付。
  4. 获取授权结果
  1. 获取支付结果
    通过以下两种方法获取支付结果:

集成步骤

通过以下步骤开始您的集成:

  1. 创建支付会话
  2. 调用 SDK
  3. 获取授权或支付结果

步骤 1:创建支付会话 服务端

当买家选择由 Antom 提供的支付方式时,您需要收集支付请求 ID、订单金额、支付方式、订单描述、支付重定向页面链接和支付结果通知链接等必要信息。随后,通过以下步骤调用 支付会话创建(快捷支付)接口来创建支付会话:

  1. 安装接口库
  2. 初始化请求实例
  3. 调用 支付会话创建(快捷支付)接口

注意Antom 提供多种语言的服务器端接口库。以下代码以 Java 为例,需要安装 Java 6 或更高版本。

安装接口库

您可以在 GitHub 上找到最新版本。

copy
<dependency>
    <groupId>com.alipay.global.sdk</groupId>
    <artifactId>global-open-sdk-java</artifactId>
    <version>2.0.44</version>
</dependency>

初始化请求实例

创建一个单例资源以向 Antom 发起请求。

copy
import com.alipay.global.api.AlipayClient;
import com.alipay.global.api.DefaultAlipayClient;
import com.alipay.global.api.model.constants.EndPointConstants;

public class Sample {
    public static final String        CLIENT_ID            = "";
    public static final String        ANTOM_PUBLIC_KEY     = "";
    public static final String        MERCHANT_PRIVATE_KEY = "";

    private final static AlipayClient CLIENT               = new DefaultAlipayClient(
            EndPointConstants.SG, MERCHANT_PRIVATE_KEY, ANTOM_PUBLIC_KEY, CLIENT_ID);

}

创建支付会话

首次支付

调用 支付会话创建(快捷支付)接口指定以下参数来创建支付会话:

参数

是否必填

说明

paymentRedirectUrl

支付完成后,页面将跳转至此商户传入的网址。有以下两种方式:

iOS 系统网页浏览器

  • 如果可以检测到用户使用的浏览器:
  • 如果商户传入的网址是重定向网页网址,支付完成后用户将返回此重定向网页网址
  • 如果商户传入的网址是重定向 App 网址,支付完成后用户将返回此重定向 App 网址
  • 如果无法检测到用户使用的浏览器或用户使用的浏览器是 Safari:
  • 如果商户传入的链接是重定向网页网址,用户将返回默认浏览器并打开此网页网址
  • 如果默认浏览器与用户支付用的浏览器不同,用户需要切换浏览器,支付完成后将返回重定向网页网址。
  • 如果默认浏览器与用户支付用的浏览器相同但不在一个窗口时,支付结果页将打开新的窗口,用户可以停留在此窗口或切回原始窗口。如果用户切回原始窗口,支付完成后用户将返回此重定向网页网址。
  • 如果商户传入的网址是重定向 App 网址,支付完成后用户将返回应用并打开此 App 网址

iOS 系统 App

  • 如果商户传入的网址是重定向网页网址:
  • 如果该网页网址支持通用网址,用户将返回 App。
  • 如果该网页网址不支持通用网址,用户将返回默认浏览器并打开此网页网址。
  • 如果商户传入的网址是重定向 App 网址,支付完成后用户将返回应用并打开此 App 网址。

Android 系统网页浏览器

  • 如果商户传入的网址是重定向网页网址,支付完成后用户将返回此重定向网页网址。

Android 系统 App

  • 用户将返回 App。

authState

您用于发起授权而分配的专属ID,在后续免密支付中获取令牌。该参数仅在首次支付时传递,后续支付无需传递此参数。

paymentNotifyUrl

支付结果通知地址。这个地址必须是 HTTPS。

paymentSessionExpiryTime

支付会话超时。默认为 1 小时,可以设置在 1 小时内。该值的格式遵循 ISO 8601 标准。例如,2019-11-27T12:01:01+08:00。

userLoginId

买家支付方式的登录账户,可能是买家的邮箱地址或者手机号码。

order.buyer: referenceBuyerId/buyerPhoneNo/buyerEmail

传递买家信息以供风险决策。传递 referenceBuyerIdbuyerPhoneNobuyerEmail 其中一个参数即可。

以上参数并非完整参数,请参阅 支付会话创建(快捷支付)接口以获取完整参数列表以及特定支付方式的额外要求。

以下示例代码展示如何调用 支付会话创建(快捷支付)接口:

copy
public static void createPaymentSession() {
    AlipayPaymentSessionRequest alipayPaymentSessionRequest = new AlipayPaymentSessionRequest();
    alipayPaymentSessionRequest.setProductCode(ProductCodeType.AGREEMENT_PAYMENT);
    alipayPaymentSessionRequest.setProductScene(ProductSceneConstants.EASY_PAY);

    // replace with your paymentRequestId
    String paymentRequestId = UUID.randomUUID().toString();
    alipayPaymentSessionRequest.setPaymentRequestId(paymentRequestId);

    // set amount
    // you should convert amount unit(in practice, amount should be calculated on your serverside)
    Amount amount = Amount.builder().currency("HKD").value("98080").build();
    alipayPaymentSessionRequest.setPaymentAmount(amount);

    //set settlement currency
    SettlementStrategy settlementStrategy = new SettlementStrategy();
    settlementStrategy.setSettlementCurrency("USD");
    alipayPaymentSessionRequest.setSettlementStrategy(settlementStrategy);

    // set paymentMethod
    PaymentMethod paymentMethod = PaymentMethod.builder().paymentMethodType("ALIPAY_HK").build();
    alipayPaymentSessionRequest.setPaymentMethod(paymentMethod);

    // set agreementInfo
    // replace with your authState
    String authState = UUID.randomUUID().toString();
    // The login ID that the user used to register in the payment method client. The login ID can be the user's email address or phone number.
    // Specify this parameter to free users from manually entering their login IDs.
    String userLoginId = "852-91234567";
    AgreementInfo agreementInfo = AgreementInfo.builder().authState(authState).userLoginId(userLoginId).build();
    alipayPaymentSessionRequest.setAgreementInfo(agreementInfo);

    // set buyer info
    Buyer buyer = Buyer.builder().referenceBuyerId("yourBuyerId").build();

    // replace with your orderId
    String orderId = UUID.randomUUID().toString();
    // set order Info
    Order order = Order.builder().referenceOrderId(orderId).
            orderDescription("antom api testing order").orderAmount(amount).buyer(buyer).build();
    alipayPaymentSessionRequest.setOrder(order);

    // replace with your notify url
    alipayPaymentSessionRequest.setPaymentNotifyUrl("http://www.yourNotifyUrl.com");

    // replace with your redirect url
    alipayPaymentSessionRequest.setPaymentRedirectUrl("http://www.yourRedirectUrl.com");

    AlipayPaymentSessionResponse alipayPaymentSessionResponse;
    try {
        alipayPaymentSessionResponse = CLIENT.execute(alipayPaymentSessionRequest);
    } catch (AlipayApiException e) {
        String errorMsg = e.getMessage();
        // handle error condition
    }
}

以下代码是请求报文的示例:

copy
{
    "agreementInfo": {
        "authState": "2b7a4a24-b909-4633-8ffe-8ed3648e99b4",
        "userLoginId": "852-91234567"
    },
    "order": {
        "buyer": {
            "referenceBuyerId": "yourBuyerId"
        },
        "orderAmount": {
            "currency": "HKD",
            "value": "98080"
        },
        "orderDescription": "antom api testing order",
        "referenceOrderId": "1a57b8cd-9a25-4d44-94f7-3467a4ebe532"
    },
    "paymentAmount": {
        "currency": "HKD",
        "value": "98080"
    },
    "paymentMethod": {
        "paymentMethodType": "ALIPAY_HK"
    },
    "paymentNotifyUrl": "http://www.yourNotifyUrl.com",
    "paymentRedirectUrl": "http://www.yourRedirectUrl.com",
    "paymentRequestId": "bc93d19e-e1f6-4b68-b6b1-3d6ddc2a792a",
    "productCode": "AGREEMENT_PAYMENT",
    "productScene": "EASY_PAY",
    "settlementStrategy": {
        "settlementCurrency": "USD"
    }
}

以下代码是包含了以下参数的响应示例:

  • paymentSessionData: 需要转发给您客户端的支付会话数据。
  • paymentSessionExpiryTime: 支付会话的过期时间。
copy
{
    "paymentSessionData": "ZqeGpu7pbMb/I3dNWTTEL3o4w5mXh20j13VnmsE1p3cjK3CVpnMXY7BfQlIvwNqQWtXHEMUo0R5pQwnSyNtxTA==&&SG&&188&&eyJh***",
    "paymentSessionExpiryTime": "2024-09-27T15:57:30+08:00",
    "paymentSessionId": "ZqeGpu7pbMb/I3dNWTTEL3o4w5mXh20j13VnmsE1p3fI21eGbgq240lFVquZsLrM",
    "result": {
        "resultCode": "SUCCESS",
        "resultMessage": "success.",
        "resultStatus": "S"
    }
}

常见问题

问:请求参数的值可以使用中文字符吗?

答:为了避免某些支付方式的不兼容,不要在请求的字段中使用中文字符。

问:如何设置接收支付通知的链接?

答:在 支付会话创建(快捷支付)接口中指定参数 paymentNotifyUrl,以接收支付结果的异步通知(支付通知),或者在 Antom Dashboard 中配置接收链接。如果请求和 Antom Dashboard 中都指定了链接,请求中指定的值优先。

步骤 2:调用SDK 客户端

安装

在开始集成之前,请确保您已完成以下环境准备:

  • 处理兼容性问题:为 Internet Explorer 和其他旧版浏览器提供相应的 Polyfills。我们建议您在构建项目时使用 babel-preset-env 来解决浏览器兼容性问题。
  • 使用以下推荐的浏览器版本:
    • 对于移动浏览器:
      • iOS 11及更高版本。
      • Android 5.0及更高版本。
    • 对于电脑浏览器,使用以下推荐版本:

image.png Edge

最近两个版本

image.png Firefox

最近两个版本

image.png Chrome

最近两个版本

image.png Safari

最近两个版本

image.png Opera

最近两个版本

image.png Electron

最近两个版本

请查阅 Web/WAP 端集成 SDK 资源包文档来集成 SDK 资源包。

实例化 SDK

通过 AMSEasyPay 创建 SDK 实例化并设置基础配置。配置对象包括以下参数:

参数

是否必填

说明

environment

用于指定环境信息。有效值有:

  • sandbox沙箱环境
  • prod生产环境

locale

用于指定语言信息。根据支付方式所在地区选择合适的值。此参数的有效值如下所示,如果使用了以下列表中未列出的值,将使用本地语言代替。

  • en_US:英语
  • in_ID印尼语
  • ms_MY马来语
  • tl_PH菲律宾语
  • ko_KR韩语
  • zh_CN简体中文
  • zh_HK繁体中文

analytics

用于配置和分析数据。有效值有:

  • enabled可选布尔值。默认为true, 这意味着允许 SDK 上传并分析可操作数据以便提供更好的服务。如果不允许数据上传和分析,将其设置为false

onLog

在 SDK 运行期间,用于生成关于日志和接口异常错误信息的回调函数。

onEventCallback

当一个支付时间发生在 SDK 运行时,返回指定事件代码的回调函数。例如支付结果或格式提交错误。

以下示例代码展示了如何获取浏览器语言:

copy
let language = navigator.language || navigator.userLanguage;
language = language.replace("-", "_"); //将 "-" 替换成 "_"

以下示例代码展示了如何通过 npm 实例化 SDK:

copy
import { AMSEasyPay } from '@alipay/ams-checkout' //包管理

const checkoutApp = new AMSEasyPay({
  environment: "sandbox",
  locale: "en_US",
  analytics: {
    enabled: true
  },
  onLog: ({code, message})=>{},
  onClose:()=>{
    //关闭半屏弹窗
  },
  onEventCallback: ({code, message})=>{},
});

以下示例代码展示了如何通过 CDN 实例化 SDK:

copy

const checkoutApp = new window.AMSEasyPay({
  environment: "sandbox",
  locale: "en_US",
  analytics: {
    enabled: true
  },
  onLog: ({code, message})=>{},
  onClose:()=>{
    //关闭半屏弹窗 Close the half-screen popup
  },
  onEventCallback: ({code, message})=>{},
});

调用 SDK

使用实例对象中的createComponent 去创建一个支付组件:

参数

是否必填

说明

sessionData

使用 sessionData 参数创建配置对象:将 支付会话创建(快捷支付)接口响应中的 paymentSessionData 完整数据作为此参数的值。

appearance

自定义外观主题配置,其中包含以下子参数:

  • showLoading可选布尔值。 默认值为 true,显示默认动画。如果不使用默认动画,请将参数设置为 false

isAppWebview

表示是否为 App 的网页视图。有效值为:

  • true:表示商户在 App 网页视图中集成网页版 SDK。
  • false:表示商户通过 H5 网页集成网页版 SDK。

以下示例代码展示了如何渲染组件:

copy
async function create(sessionData) {
  await checkoutApp.createComponent({ 
    sessionData: sessionData, 
    appearance:{
      showLoading: true, // 默认为true,展示初始化加载动画
    },
    isAppWebview: false // 默认为false,表示商户通过 H5 网页集成网页版 SDK
  });
}

调用实例对象中的 unmount 函数进行 SDK 组件的资源回收,请在以下两种场景中调用:

copy
//回收组件资源
checkoutApp.unmount();

常见问题

问:当我收到 SDK_CREATEPAYMENT_PARAMETER_ERROR 时能做什么?

答:当您收到这个事件代码时,请检查 sessionData 是否传入及是否正确。

问:当我收到SDK_PAYMENT_ERROR 或者发生了视图渲染错误时能做什么?

答:检查接口初始化时网络请求是否存在任何异常,比如网络连接超时。确保创建支付会话请求的环境与用于 SDK 实例的环境一致。检查 支付会话创建(快捷支付)接口中的参数是否正确传递。如果接口异常持续存在,请随时联系我们以获得进一步的故障排查帮助。

步骤 3:获取授权或支付结果 服务端

对于首次支付,您可以获取授权和支付结果,而对于后续支付,仅提供支付结果。

您可以通过以下方法之一获取授权或支付结果:

  • 接收异步通知
  • 查询结果

接收异步通知

获取授权结果

授权成功后, Antom 会通过 授权结果通知 接口给您发送异步通知。当您收到通知,您需要按照返回收到确认信息返回响应。同时,您必须在系统中更新买家的授权状态,并在您的授权管理页面上显示从通知中获取的买家无敏感信息账户。

以下代码是通知请求的示例:

copy
{
    "accessToken": "28288803001319861727421828000Cv96OFlYoi17100****",
    "accessTokenExpiryTime": 2145916817000,
    "authState": "36a38e87-0453-495e-ad17-b46553b918da",
    "authorizationNotifyType": "TOKEN_CREATED",
    "userLoginId": "852-91****67",
    "result": {
        "resultCode": "SUCCESS",
        "resultMessage": "success.",
        "resultStatus": "S"
    }
}

如何验证通知的签名并对此作出响应,请参阅签名与验签

问:支付结果通知何时发送?

答:这取决于支付是否完成:

  • 如果支付成功完成,Antom通常会在 3 到 5 秒内发送异步通知。对于像 OTC 这样的支付方式,通知可能会稍有延迟。
  • 如果支付未完成,Antom需要先关闭订单,然后发送异步通知。不同支付方式关闭订单所需的时间会有所不同,通常默认为 14 分钟。

问:异步通知会被重新发送吗?

答:是的,对于以下情况,异步通知将在 24 小时内自动重新发送:

  • 如果由于网络原因没有收到异步通知;
  • 如果您收到 Antom 的异步通知,但您没有按照处理通知的示例代码格式进行响应。

通知可以重发最多 8 次,或者直到收到正确的响应以终止传递。发送间隔如下:0 分钟,2 分钟,10 分钟,10 分钟,1 小时,2 小时,6 小时,15 小时。

问:在响应异步通知时,我需要添加数字签名吗?

答:如果您收到 Antom 的异步通知,您需要按照处理通知的示例代码格式返回响应,但不需要对响应进行签名。

问:在支付结果通知中,我需要使用哪些关键参数?

答:请注意以下关键参数:

  • result:表示订单的支付结果。
  • paymentRequestId:用于咨询、取消和对账的支付请求 ID。
  • paymentId:由 Antom 生成的支付订单 ID,用于退款和对账。
  • paymentAmount:表示支付金额。

问:在授权结果通知中,我需要使用哪些关键参数?

答:请注意以下关键参数:

  • result:表示订单的支付结果。
  • authState:由商户生成的资产绑定请求 ID。
  • accessToken:由 Antom 生成的代扣 ID,用于后续支付。
  • userLoginId:用户的无敏感信息账户 ID。

支付结果查询

您可以调用 支付结果查询 接口来发起对订单结果的查询。

参数

是否必填

说明

paymentRequestId

由您生成的支付请求 ID。

以上参数并非完整参数,请参考 支付结果查询 接口获取完整参数集以及特定支付方式的额外要求。

以下示例代码展示了如何调用 支付结果查询 接口:

copy
public static void inquiryPayment() {
    AlipayPayQueryRequest alipayPayQueryRequest = new AlipayPayQueryRequest();

    // replace with your paymentRequestId
    alipayPayQueryRequest.setPaymentRequestId("yourPaymentRequestId");

    AlipayPayQueryResponse alipayPayQueryResponse = null;
    try {
        alipayPayQueryResponse = CLIENT.execute(alipayPayQueryRequest);
    } catch (AlipayApiException e) {
        String errorMsg = e.getMessage();
        // handle error condition
    }
}

以下是请求报文的示例:

copy
{
  "paymentRequestId": "bc93d19e-e1f6-4b68-b6b1-3d6ddc2a792a"
}

以下是响应报文的示例:

copy
{
    "cardInfo": {},
    "paymentResultCode": "SUCCESS",
    "paymentRequestId": "bc93d19e-e1f6-4b68-b6b1-3d6ddc2a792a",
    "paymentResultInfo": {},
    "paymentAmount": {
        "currency": "HKD",
        "value": "98080"
    },
    "result": {
        "resultStatus": "S",
        "resultCode": "SUCCESS",
        "resultMessage": "success."
    },
    "actualPaymentAmount": {
        "currency": "HKD",
        "value": "98080"
    },
    "paymentId": "202409271940108001001881E0211235544",
    "paymentResultMessage": "success",
    "pspCustomerInfo": {
        "pspName": "ALIPAY_HK"
    },
    "paymentTime": "2024-09-27T00:23:46-07:00",
    "customsDeclarationAmount": {},
    "paymentStatus": "SUCCESS"
}

常见问题

问:我应该多久调用一次支付结果查询接口?

答:以 2 秒为间隔持续调用 支付结果查询 接口,直到获取最终的支付结果或收到异步支付结果通知。

问:我在通知中需要使用哪些关键参数?

答:请注意这些关键参数:

  • result:表示此 支付结果查询 接口调用的结果,需要根据 paymentStatus 来判断订单状态:
    • SUCCESSFAIL表示最终结果;
    • PROCESSING表示处理中。
  • paymentAmount:表示支付的金额。

示例代码

使用客户端 SDK 的完整示例代码:

copy
// 步骤一:实例化SDK
const checkoutApp = new window.AMSCashierPayment({
  environment: "sandbox",
  locale: "en_US",
  onLog: ({code, message}) => {},
  onEventCallback: ({code, message}) => {},
});

// 处理卡支付按钮事件
document
  .querySelector("#your form id")
  .addEventListener("submit", handleSubmit);

async function handleSubmit() {
  // 步骤二:服务端调用支付会话创建接口,获取paymentSessionData
  async function getPaymentSessionData() {
    const url = "Fill in the server address";
    const config = {
      // 填写请求配置
    };
    const response = await fetch(url, config);
    // 获取响应中的paymentSessionData参数值
    const { paymentSessionData } = await response.json();
    return paymentSessionData;
  }
  const paymentSessionData = await getPaymentSessionData();

  // 步骤三:创建渲染卡组件
  await checkoutApp.createComponent({ 
    sessionData: paymentSessionData, 
    appearance:{
      showLoading: true, // 默认为true,表示展示初始化加载动画
    }, 
  });
  // 步骤三:创建渲染卡组件
  await checkoutApp.createComponent({ 
    sessionData: paymentSessionData, 
    appearance:{
      showLoading: true, // 默认为true,展示初始化加载动画
    }, 
      isAppWebview: false // 默认为false,表示商户通过 H5 网页集成网页版 SDK
});
}

自定义加载动画的示例代码

银行卡支付场景支持配置自定义加载动画:

  1. 当渲染组件时,将 showLoading 配置为 false
  2. 当调用 createComponent 方法时,在当前页面渲染自定义加载动画。
  3. 监听 onEventCallback 事件。
    1. 当您收到 SDK_START_OF_LOADING 事件代码时,显示您的自定义加载动画。
    2. 当您收到 SDK_END_OF_LOADING 事件代码时,关闭您的自定义加载动画。
copy
async function create(sessionData) {
  await checkoutApp.createComponent({ 
    sessionData: sessionData, 
    appearance:{
      showLoading: true, // 默认为true,展示初始化加载动画
    },
    isAppWebview: false // 默认为false,表示商户通过 H5 网页集成网页版 SDK
  });
}

事件码

您可能会看到两种类型的事件码:

  • 状态码:在组件运行生命周期内,通过 onEventCallback 回调函数返回。
  • 错误码:在组件初始化阶段,通过 onEventCallback 或 onError 回调函数返回。

类型

代码

说明

后续操作

状态码

SDK_START_OF_LOADING

配置 showLoading 为 false 时,使用自定义动画,您可在这个时机渲染并展示自定义加载动画。

无需进一步操作。

SDK_END_OF_LOADING

配置 showLoading 为 false 时,使用自定义动画,您可在这个时机结束自定义加载动画。

无需进一步操作。

SDK_CALL_URL_ERROR

此事件码代表以下事件信息中的一种情况:

Web/WAP 场景下通常不会出现跳转异常,如果出现异常建议您校验重定向链接。在 App 场景下,如果频繁出现跳转异常,请联系 Antom 技术支持,排查跳转或唤端问题。

无需进一步操作。

SDK_PAYMENT_CANCEL

表示用户取消支付,即用户未提交订单退出支付页面。您可使用有效期内的 paymentSessionData 重新调用 SDK。如已过期,需要重新发起 支付会话创建(快捷支付)请求。

无需进一步操作。

错误码

SDK_INTERNAL_ERROR

SDK 内部错误。

请联系 Antom 技术支持。

SDK_CREATEPAYMENT_PARAMETER_ERROR

createComponent

函数传入参数异常。

请检查参数是否正确并重新初始化组件。

SDK_INIT_PARAMETER_ERROR

AMSEasyPay

函数传入参数异常。

请检查参数是否正确并重新实例化 SDK。

SDK_CREATECOMPONENT_ERROR

组件初始化异常。

请联系 Antom 技术支持。

SDK_SUBMIT_NETWORK_ERROR

因网络原因,接口调用失败。在 submit 函数提交中可能会出现。

请尝试再次提交。