Magento2 Place Order 代码执行逻辑分析

1. 前台js执行逻辑

点击页面的 Place Order 按钮执行下面default.js中的 placeOrder 方法

vendor/magento/module-checkout/view/frontend/web/js/view/payment/default.js

placeOrder: function (data, event) {
var self = this;

if (event) {
event.preventDefault();
}


if (this.validate() &&
additionalValidators.validate() &&
this.isPlaceOrderActionAllowed() === true
) {
this.isPlaceOrderActionAllowed(false);

this.getPlaceOrderDeferredObject()
.done(
function () {
self.afterPlaceOrder();

if (self.redirectAfterPlaceOrder) {
redirectOnSuccessAction.execute();
}
}
).always(
function () {
self.isPlaceOrderActionAllowed(true);
}
);

return true;
}

return false;
},

其中重要方法是 getPlaceOrderDeferredObject

getPlaceOrderDeferredObject: function () {
return $.when(
placeOrderAction(this.getData(), this.messageContainer)
);
},

其中 placeOrderAction 调用 vendor/magento/module-checkout/view/frontend/web/js/action/place-order.js

/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

/**
 * @api
 */
define([
    'Magento_Checkout/js/model/quote',
    'Magento_Checkout/js/model/url-builder',
    'Magento_Customer/js/model/customer',
    'Magento_Checkout/js/model/place-order'
], function (quote, urlBuilder, customer, placeOrderService) {
    'use strict';

    return function (paymentData, messageContainer) {
        var serviceUrl, payload;

        payload = {
            cartId: quote.getQuoteId(),
            billingAddress: quote.billingAddress(),
            paymentMethod: paymentData
        };

        if (customer.isLoggedIn()) {
            serviceUrl = urlBuilder.createUrl('/carts/mine/payment-information', {});
        } else {
            serviceUrl = urlBuilder.createUrl('/guest-carts/:quoteId/payment-information', {
                quoteId: quote.getQuoteId()
            });
            payload.email = quote.guestEmail;
        }

        return placeOrderService(serviceUrl, payload, messageContainer);
    };
});

其中 placeOrderService 方法是’Magento_Checkout/js/model/place-order 产生的 ,该方法来自 vendor\magento\module-checkout\view\frontend\web\js\model\place-order.js

/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
/**
 * @api
 */
define(
    [
        'mage/storage',
        'Magento_Checkout/js/model/error-processor',
        'Magento_Checkout/js/model/full-screen-loader',
        'Magento_Customer/js/customer-data',
        'Magento_Checkout/js/model/payment/place-order-hooks',
        'underscore'
    ],
    function (storage, errorProcessor, fullScreenLoader, customerData, hooks, _) {
        'use strict';

        return function (serviceUrl, payload, messageContainer) {
            var headers = {}, redirectURL = '';

            fullScreenLoader.startLoader();
            _.each(hooks.requestModifiers, function (modifier) {
                modifier(headers, payload);
            });

            return storage.post(
                serviceUrl, JSON.stringify(payload), true, 'application/json', headers
            ).fail(
                function (response) {
                    errorProcessor.process(response, messageContainer);
                    redirectURL = response.getResponseHeader('errorRedirectAction');

                    if (redirectURL) {
                        setTimeout(function () {
                            errorProcessor.redirectTo(redirectURL);
                        }, 3000);
                    }
                }
            ).done(
                function (response) {
                    var clearData = {
                        'selectedShippingAddress': null,
                        'shippingAddressFromData': null,
                        'newCustomerShippingAddress': null,
                        'selectedShippingRate': null,
                        'selectedPaymentMethod': null,
                        'selectedBillingAddress': null,
                        'billingAddressFromData': null,
                        'newCustomerBillingAddress': null
                    };

                    if (response.responseType !== 'error') {
                        customerData.set('checkout-data', clearData);
                    }
                }
            ).always(
                function () {
                    fullScreenLoader.stopLoader();
                    _.each(hooks.afterRequestListeners, function (listener) {
                        listener();
                    });
                }
            );
        };
    }
);

其中 storage.post 提交数据,response是返回的数据。可以通过google F12 为js设置断点进行调试。

2. 后台php执行逻辑

登录用户执行:/carts/mine/payment-information

未登录用户执行:/guest-carts/:quoteId/payment-information

上面url定义在vendor/magento/module-checkout/etc/webapi.xml

2.1 登录用户

<route url="/V1/carts/mine/payment-information" method="POST">
    <service class="Magento\Checkout\Api\PaymentInformationManagementInterface" method="savePaymentInformationAndPlaceOrder"/>
    <resources>
        <resource ref="self" />
    </resources>
    <data>
        <parameter name="cartId" force="true">%cart_id%</parameter>
    </data>
</route>

(1) 核心属性

属性说明
url="/V1/carts/mine/payment-information"API 端点路径,mine 表示当前用户的购物车
method="POST"仅接受 POST 请求

(2) 关键节点

节点作用
<service class="...PaymentInformationManagementInterface" method="savePaymentInformationAndPlaceOrder"/>绑定到 PaymentInformationManagementInterface 的 savePaymentInformationAndPlaceOrder 方法
<resources><resource ref="self" /></resources>权限控制:仅允许操作当前用户的购物车(需登录认证)
<data><parameter name="cartId" force="true">%cart_id%</parameter></data>
  1. 后端处理
    • 框架自动将 %cart_id% 替换为当前用户的购物车 ID。
    • 调用 PaymentInformationManagementInterface::savePaymentInformationAndPlaceOrder($cartId, $paymentMethod, $billingAddress)

上面接口是由“Magento\Checkout\Model\PaymentInformationManagement”来实现的。配置在

vendor/magento/module-checkout/etc/di.xml

即:vendor/magento/module-checkout/Model/PaymentInformationManagement.php的 savePaymentInformationAndPlaceOrder 方法

public function savePaymentInformationAndPlaceOrder(
        $cartId,
        PaymentInterface $paymentMethod,
        ?QuoteAddressInterface $billingAddress = null
    ) {

        $this->paymentRateLimiter->limit();
        try {
            //Have to do this hack because of plugins for savePaymentInformation()
            $this->saveRateLimiterDisabled = true;
            $this->savePaymentInformation($cartId, $paymentMethod, $billingAddress);
        } finally {
            $this->saveRateLimiterDisabled = false;
        }
        try {
            $orderId = $this->cartManagement->placeOrder($cartId);
        } catch (LocalizedException $e) {
            $this->logger->critical(
                'Placing an Order failed (reason: '.  $e->getMessage() .')',
                [
                    'quote_id' => $cartId,
                    'exception' => (string)$e,
                    'is_guest_checkout' => false
                ]
            );
            throw new CouldNotSaveException(
                __($e->getMessage()),
                $e
            );
        } catch (\Exception $e) {
            $this->logger->critical($e);
            throw new CouldNotSaveException(
                __('A server error stopped your order from being placed. Please try to place your order again.'),
                $e
            );
        }
        return $orderId;
    }

其中 速率限制检查

if (!$this->saveRateLimiterDisabled) {
    try {
        $this->saveRateLimiter->limit();
    } catch (PaymentProcessingRateLimitExceededException $ex) {
        //Limit reached
        return false;
    }
}

含义:防止恶意用户频繁调用此API。如果启用速率限制且超过阈值,返回 false 阻止操作。

2.2 未登录用户执行

2.2.1 登录用户执行url地址

<route url="/V1/guest-carts/:cartId/payment-information" method="POST">
        <service class="Magento\Checkout\Api\GuestPaymentInformationManagementInterface" method="savePaymentInformationAndPlaceOrder"/>
        <resources>
            <resource ref="anonymous" />
        </resources>
    </route>

2.2.2 GuestPaymentInformationManagementInterface 该接口由下面类来实现:vendor\magento\module-checkout\etc\di.xml

   <preference for="Magento\Checkout\Api\GuestPaymentInformationManagementInterface" type="Magento\Checkout\Model\GuestPaymentInformationManagement" />

该类的 savePaymentInformationAndPlaceOrder 方法如下:

public function savePaymentInformationAndPlaceOrder(
        $cartId,
        PaymentInterface $paymentMethod,
        ?QuoteAddressInterface $billingAddress = null
    ) {

        $this->paymentRateLimiter->limit();
        try {
            //Have to do this hack because of plugins for savePaymentInformation()
            $this->saveRateLimiterDisabled = true;
            $this->savePaymentInformation($cartId, $paymentMethod, $billingAddress);
        } finally {
            $this->saveRateLimiterDisabled = false;
        }
        try {
            $orderId = $this->cartManagement->placeOrder($cartId);
        } catch (LocalizedException $e) {
            $this->logger->critical(
                'Placing an Order failed (reason: '.  $e->getMessage() .')',
                [
                    'quote_id' => $cartId,
                    'exception' => (string)$e,
                    'is_guest_checkout' => false
                ]
            );
            throw new CouldNotSaveException(
                __($e->getMessage()),
                $e
            );
        } catch (\Exception $e) {
            $this->logger->critical($e);
            throw new CouldNotSaveException(
                __('A server error stopped your order from being placed. Please try to place your order again.'),
                $e
            );
        }
        return $orderId;
    }

上面有savePaymentInformation 方法如下:

public function savePaymentInformation(
        $cartId,
        $email,
        \Magento\Quote\Api\Data\PaymentInterface $paymentMethod,
        ?\Magento\Quote\Api\Data\AddressInterface $billingAddress = null
    ) {
        if (!$this->saveRateLimitDisabled) {
            try {
                $this->savingRateLimiter->limit();
            } catch (PaymentProcessingRateLimitExceededException $ex) {
                //Limit reached
                return false;
            }
        }

        $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id');
        /** @var Quote $quote */
        $quote = $this->cartRepository->getActive($quoteIdMask->getQuoteId());
        $shippingAddress = $quote->getShippingAddress();
        if ($this->addressComparator->isEqual($shippingAddress, $billingAddress)) {
            $shippingAddress->setSameAsBilling(1);
        }
        if ($billingAddress) {
            $billingAddress->setEmail($email);
            $quote->removeAddress($quote->getBillingAddress()->getId());
            $quote->setBillingAddress($billingAddress);
            $quote->setDataChanges(true);
        } else {
            $quote->getBillingAddress()->setEmail($email);
        }
        $this->limitShippingCarrier($quote);

        if (!(float)$quote->getItemsQty()) {
            throw new CouldNotSaveException(__('Some of the products are disabled.'));
        }

        $this->paymentMethodManagement->set($cartId, $paymentMethod);
        return true;
    }

其中生成订单方法

 $orderId = $this->cartManagement->placeOrder($cartId);

$this->cartManagement 这个对象来源接口 Magento\Quote\Api\GuestCartManagementInterface

这个接口是由 \vendor\magento\module-quote\Model\GuestCart\GuestCartManagement.php 类来实现其对象的.

该类下有placeOrder 方法如下:

public function placeOrder($cartId, ?PaymentInterface $paymentMethod = null)
    {
        /** @var $quoteIdMask QuoteIdMask */
        $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id');
        $this->cartRepository->get($quoteIdMask->getQuoteId())
            ->setCheckoutMethod(CartManagementInterface::METHOD_GUEST);
        return $this->quoteManagement->placeOrder($quoteIdMask->getQuoteId(), $paymentMethod);
    }

其中 $this->quoteManagement->placeOrder($quoteIdMask->getQuoteId(), $paymentMethod);

$quoteManagement 对象是接口Magento\Quote\Api\CartManagementInterface 通过Magento\Quote\Model\QuoteManagement 类来实现的对象

vendor\magento\module-quote\etc\di.xml

 <preference for="Magento\Quote\Api\CartManagementInterface" type="Magento\Quote\Model\QuoteManagement" />

发表评论