この記事は WooCommerce Advent Calendar 2016 の9日目です。
カートシステムを導入するにあたり、システム屋として気になるのは高負荷時の挙動です。
そろそろ福袋の季節ですが、福袋は特に気をつかわなければいけない特徴があります。
まず、ECサイトでの福袋の売り方は2種類あります。
- 元旦または2日に福袋が家に届くように予約販売する
- 元旦またはその前後に販売開始する
多くの場合は2番目ですが、この場合、販売開始時刻にアクセスが集中することになります。
また、福袋はたいていの場合「数量限定」です。ここでも売り方が2種類あります。
- 注文を全部受け付け、後日抽選する
- 早い者勝ち(決済の早い順、またはカートに入れたのが早い順)
やはり多くの場合は2番目です。つまりは「時間指定の早押し競争」であり、言い換えると「この時間きっかりにみんなでF5アタックしろよ!」とサイト主自身が言っているわけです。負荷が非常に集中しやすい構造になっています。純粋なF5アタックとは違い、そのリクエストのほとんどにカート・在庫・受注データ更新処理が入ります。普段のリードオンリーなPV数やUU数からは負荷を推測できず、サーバ増強計画は立てにくいです。
アパレルECの雄、zozotownも福袋でたびたびサーバダウンさせています。落ちないに越したことはないのですが「落ちた場合に復旧できるか」はシステム屋として備えたいところです。サーバを再起動すればよいというものではなく、再起動したときに
- 在庫がずれていないか(在庫以上に注文を取ったりしないか)
- 決済だけして注文の記録が消えていないか(お金だけ取っていないか)
- 受注メールだけ送って注文の記録が消えていないか(商品を送るという連絡をしっぱなしにしたりしないか)
などの問題を、対応は後日だとしても、クリアできる目途がないとサービスを再開できません。
特に決済処理は問題になりやすいです。クレジット決済などはたいてい応答が遅く数秒かかり、同時リクエスト許可本数もわずか数本だったりして、webサーバの接続上限までwaitが埋まりやすくなります。
そのうえ外部連携なのでDBトランザクション処理の中に収めることができず、タイムアウトでロールバックすると「決済だけして注文データは巻き戻った(上記2のパターン)」ということになりかねません。
これらをクリアするには在庫管理と決済処理のフローを確実に把握しておく必要があります。
前置きが長くなりましたが、そんなわけでWooCommerceの決済処理について、コードリーディングを行っていきます。
環境設定
コードを読む前にまずは動かしてみましょう。次のものをローカル開発環境に用意しました。
- Fedora 25
- MariaDB(Fedoraのdnfでインストール)
- Nginx + PHP7.0.13(php-fpm)
- WordPress 4.6.1 日本語版
- WP Multibyte Patch 2.7
- WooCommerce 2.6.7
- WooCommerce PayPal Express Checkout Gateway 1.1.2
決済手段は代引きではなくPayPal Express Checkoutを使うことにしました。小規模なECサイトでは代引きどころか「商品とともに振込用紙を送る」ということすらありますが、前述の心配をクリアしたいという意味で外部決済を利用します。実際に調達しやすい決済手段という点からPayPal Express Checkoutを選びました。
購入フロー
PayPal Express Checkoutを使った場合、購入フローは次のようになります。
- 商品ページを表示
- 商品をカートに入れる
- カートを表示する
- PayPal Express Checkoutのボタンを押す
- ポップアップが表示されてPayPalのログイン画面へ
- ポップアップ上でPayPalの決済をする
- WordPressに戻り購入確認画面が表示される
- 購入ボタンを押す
- 購入完了画面が表示される
PayPalの決済操作は6で行いますが、実際に課金が行われるのは8と9の間です。7まではPayPalの決済履歴に何も残らないことが確認できます。また、7までは「やっぱりやめた」とブラウザを閉じてしまうということもあるでしょう。
詳しく見るべきは8と9の間です。ブラウザのデベロッパーツールでNetworkのログを見ていきます。
決済時のHTTP通信内容
購入ボタンを押してから購入完了画面が表示されるまで、次のようなURLをたどりました。
- ajax通信
http://wordpress-woo.localdev/checkout/?woo-paypal-return=true&token=EC-14J34011DL967272S&PayerID=FTGLRYSFWK24C&wc-ajax=checkout
- 画面遷移(すぐリダイレクト)
http://wordpress-woo.localdev/checkout/order-received/12?key=wc_order_5846caef9acba
- リダイレクト先
http://wordpress-woo.localdev/checkout/order-received/12/?key=wc_order_5846caef9acba
重要なのはajax通信です。数秒待たされたので、ここで決済しているのでしょう。
通常のPOSTではなくajaxにすることで、ボタン2連打やリロードに強くなります。2重投稿対策はサーバサイドで担保するにしても、クライアントサイドでそもそも2重リクエストを発生させないことはサーバ負荷軽減につながります。
リダイレクトを1つ挟んでいるのは単に「末尾にスラッシュがないから」という理由であるように見えます。ajaxでなければリダイレクトする意味はあるのですが、ここでは意味がありません。
決済時のSQLログ
コードリーディングの手がかりとするため、SQLのログも見てみましょう。トランザクションを張るタイミングが具体的にわかります。
MariaDBの次の設定でSELECTを含めたログを取得できます。
[mysqld] general_log=1 general_log_file="/var/log/mariadb/sql.log"
購入ボタンを押す前にログファイルを削除してMariaDBを再起動すると該当箇所を把握しやすくなります。
ちょっと長くなりますが、SELECT以外のログをここに張ってしまいます。重要そうなところには色をつけました。
/usr/libexec/mysqld, Version: 10.1.19-MariaDB (MariaDB Server). started with: Tcp port: 0 Unix socket: /var/run/mariadb/mysql.sock Time Id Command Argument 161206 23:27:58 2 Connect wp_woo@localhost as anonymous on 2 Query SET NAMES utf8mb4 2 Query SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' 2 Query SET SESSION sql_mode='NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' 2 Init DB wp_woo 2 Query START TRANSACTION 2 Query SHOW FULL COLUMNS FROM `wp_posts` 2 Query INSERT INTO `wp_posts` (`post_author`, `post_date`, `post_date_gmt`, `post_content`, `post_content_filtered`, `post_title`, `post_excerpt`, `post_status`, `post_type`, `comment_status`, `ping_status`, `post_password`, `post_name`, `to_ping`, `pinged`, `post_modified`, `post_modified_gmt`, `post_parent`, `menu_order`, `post_mime_type`, `guid`) VALUES (1, '2016-12-06 23:27:59', '2016-12-06 14:27:59', '', '', 'Order – 12月 6, 2016 @ 11:27 PM', '', 'wc-pending', 'shop_order', 'open', 'closed', 'order_5846caef967c0', 'order-dec-06-2016-0227-pm', '', '', '2016-12-06 23:27:59', '2016-12-06 14:27:59', 0, 0, '', '') 2 Query UPDATE `wp_posts` SET `guid` = 'http://wordpress-woo.localdev/?post_type=shop_order&p=12' WHERE `ID` = 12 2 Query SHOW FULL COLUMNS FROM `wp_options` 2 Query DELETE FROM `wp_options` WHERE `option_name` = '_transient_is_multi_author' 2 Query DELETE FROM `wp_options` WHERE `option_name` = '_transient_twentysixteen_categories' 2 Query SHOW FULL COLUMNS FROM `wp_postmeta` 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_order_key', 'wc_order_5846caef9acba') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_order_currency', 'JPY') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_prices_include_tax', 'no') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_customer_ip_address', '127.0.0.1') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_customer_user_agent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.100 Safari/537.36') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_customer_user', '0') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_created_via', 'checkout') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_cart_hash', 'a88b4cf0d2c0e2731d03618844e05492') 2 Query UPDATE `wp_postmeta` SET `meta_value` = '0' WHERE `post_id` = 12 AND `meta_key` = '_customer_user' 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_order_version', '2.6.7') 2 Query SHOW FULL COLUMNS FROM `wp_woocommerce_order_items` 2 Query INSERT INTO `wp_woocommerce_order_items` (`order_item_name`, `order_item_type`, `order_id`) VALUES ('nice thing', 'line_item', 12) 2 Query SHOW FULL COLUMNS FROM `wp_woocommerce_order_itemmeta` 2 Query INSERT INTO `wp_woocommerce_order_itemmeta` (`order_item_id`, `meta_key`, `meta_value`) VALUES ('4', '_qty', '1') 2 Query INSERT INTO `wp_woocommerce_order_itemmeta` (`order_item_id`, `meta_key`, `meta_value`) VALUES ('4', '_tax_class', '') 2 Query INSERT INTO `wp_woocommerce_order_itemmeta` (`order_item_id`, `meta_key`, `meta_value`) VALUES ('4', '_product_id', '8') 2 Query INSERT INTO `wp_woocommerce_order_itemmeta` (`order_item_id`, `meta_key`, `meta_value`) VALUES ('4', '_variation_id', '0') 2 Query INSERT INTO `wp_woocommerce_order_itemmeta` (`order_item_id`, `meta_key`, `meta_value`) VALUES ('4', '_line_subtotal', '1200') 2 Query INSERT INTO `wp_woocommerce_order_itemmeta` (`order_item_id`, `meta_key`, `meta_value`) VALUES ('4', '_line_total', '1200') 2 Query INSERT INTO `wp_woocommerce_order_itemmeta` (`order_item_id`, `meta_key`, `meta_value`) VALUES ('4', '_line_subtotal_tax', '0') 2 Query INSERT INTO `wp_woocommerce_order_itemmeta` (`order_item_id`, `meta_key`, `meta_value`) VALUES ('4', '_line_tax', '0') 2 Query INSERT INTO `wp_woocommerce_order_itemmeta` (`order_item_id`, `meta_key`, `meta_value`) VALUES ('4', '_line_tax_data', 'a:2:{s:5:\"total\";a:0:{}s:8:\"subtotal\";a:0:{}}') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_payment_method', 'ppec_paypal') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_payment_method_title', 'PayPal Express Checkout') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_order_shipping', '') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_cart_discount', '0') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_cart_discount_tax', '0') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_order_tax', '0') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_order_shipping_tax', '0') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_order_total', '1200') 2 Query COMMIT 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_first_name', '彰成') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_last_name', '竹腰') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_company', '') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_address_1', '') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_address_2', '') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_city', '') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_state', '') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_postcode', '') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_country', 'JP') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_phone', '') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_billing_email', 'example@example.com') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_woo_pp_txnData', 'a:2:{s:15:\"refundable_txns\";a:1:{i:0;a:4:{s:5:\"txnID\";s:17:\"9KW54460KT230605A\";s:6:\"amount\";d:1200;s:15:\"refunded_amount\";i:0;s:6:\"status\";s:9:\"Completed\";}}s:8:\"txn_type\";s:4:\"sale\";}') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_paypal_status', 'completed') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_transaction_id', '9KW54460KT230605A') 2 Query UPDATE `wp_posts` SET `post_author` = 1, `post_date` = '2016-12-06 23:28:06', `post_date_gmt` = '2016-12-06 14:28:06', `post_content` = '', `post_content_filtered` = '', `post_title` = 'Order – 12月 6, 2016 @ 11:28 PM', `post_excerpt` = '', `post_status` = 'wc-processing', `post_type` = 'shop_order', `comment_status` = 'open', `ping_status` = 'closed', `post_password` = 'order_5846caef967c0', `post_name` = 'order-dec-06-2016-0227-pm', `to_ping` = '', `pinged` = '', `post_modified` = '2016-12-06 23:28:06', `post_modified_gmt` = '2016-12-06 14:28:06', `post_parent` = 0, `menu_order` = 0, `post_mime_type` = '', `guid` = 'http://wordpress-woo.localdev/?post_type=shop_order&p=12' WHERE `ID` = 12 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_download_permissions_granted', '1') 2 Query SHOW FULL COLUMNS FROM `wp_comments` 2 Query INSERT INTO `wp_comments` (`comment_post_ID`, `comment_author`, `comment_author_email`, `comment_author_url`, `comment_author_IP`, `comment_date`, `comment_date_gmt`, `comment_content`, `comment_karma`, `comment_approved`, `comment_agent`, `comment_type`, `comment_parent`, `user_id`) VALUES (12, 'WooCommerce', 'woocommerce@wordpress-woo.localdev', '', '', '2016-12-06 23:28:06', '2016-12-06 14:28:06', 'Order status changed from Pending Payment to Processing.', 0, '1', 'WooCommerce', 'order_note', 0, 0) 2 Query UPDATE `wp_posts` SET `comment_count` = 1 WHERE `ID` = 12 2 Query UPDATE `wp_postmeta` SET `meta_value` = '4' WHERE `post_id` = 8 AND `meta_key` = 'total_sales' 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_recorded_sales', 'yes') 2 Query DELETE FROM wp_options WHERE option_name LIKE '\\_transient\\_%1481033589' ORDER BY option_id LIMIT 1000 2 Query UPDATE `wp_options` SET `option_value` = '1481034486' WHERE `option_name` = '_transient_orders-transient-version' 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_paid_date', '2016-12-06 23:28:06') 2 Query INSERT INTO `wp_postmeta` (`post_id`, `meta_key`, `meta_value`) VALUES (12, '_order_stock_reduced', '1') 2 Query SHOW FULL COLUMNS FROM `wp_woocommerce_sessions` 2 Query REPLACE INTO `wp_woocommerce_sessions` (`session_key`, `session_value`, `session_expiry`) VALUES ('79b7010ebecb22b49e1434dafba796b4', 'a:4:{s:21:\"removed_cart_contents\";s:6:\"a:0:{}\";s:14:\"shipping_total\";N;s:10:\"wc_notices\";N;s:21:\"chosen_payment_method\";s:11:\"ppec_paypal\";}', 1481206304) 2 Quit 3 Connect wp_woo@localhost as anonymous on 3 Query SET NAMES utf8mb4 3 Query SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' 3 Query SET SESSION sql_mode='NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' 3 Init DB wp_woo 3 Quit 4 Connect wp_woo@localhost as anonymous on 4 Query SET NAMES utf8mb4 4 Query SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' 4 Query SET SESSION sql_mode='NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' 4 Init DB wp_woo 4 Query INSERT INTO `wp_options` (`option_name`, `option_value`, `autoload`) VALUES ('_transient_is_multi_author', '0', 'yes') ON DUPLICATE KEY UPDATE `option_name` = VALUES(`option_name`), `option_value` = VALUES(`option_value`), `autoload` = VALUES(`autoload`) 4 Quit 5 Connect wp_woo@localhost as anonymous on 5 Query SET NAMES utf8mb4 5 Query SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' 5 Query SET SESSION sql_mode='NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' 5 Init DB wp_woo 5 Quit
接続番号2ですべてのデータ保存をしています。おそらくこれがajax通信の部分でしょう。
wp_postsテーブルのid=12のデータを注文番号として取り扱っているようです。
DBトランザクションを見ると、START TRANSACTION(=BEGIN)は最初から始まり、処理途中でCOMMITが行われています。決済リクエスト前にタイムアウト対策として一度DBトランザクションを打ち切っているのでしょう。
しかし、その後の処理にはDBトランザクションがありません。決済リクエスト完了後はDBトランザクションを再開して良いはずです。
DBトランザクションを使わずに「このフラグが立っていない場合は途中のデータである」という手法でデータの一貫性を確認することはできます。今回の場合、_order_stock_reduced
がなければ途中であるとみなすことができますが……実際に壊れたデータを見てこの判断をしていくのは面倒です。すぐの復旧は無理でしょう。
負荷の高い状態で再起動をかけると、この処理途中にハマることは十分あり得ます。再起動処理はロードバランサから切り離した状態でかけるほうが安全そうです。
また、KUSANAGIの標準設定であるhhmvはどうも不安定な様子で、このblogですら一年のあいだに数回seg faultをしています。php7に切り替えておいたほうが安全かもしれません。
外側から攻めるのはここまでにして、実際のコードを見てみましょう。
該当コード
コードをindex.phpから追っていくと大変なので、端折ります。
URLのうち「woo-paypal-return」が処理のキーになっていそうです。これをgrepしましょう。
$ find . -name '*.php' | xargs grep woo-paypal-return ./wp-content/plugins/woocommerce-gateway-paypal-express-checkout/includes/abstracts/abstract-wc-gateway-ppec.php:if ( empty( $_GET['woo-paypal-return'] ) ) { ./wp-content/plugins/woocommerce-gateway-paypal-express-checkout/includes/class-wc-gateway-ppec-checkout-handler.php: if ( empty( $_GET['woo-paypal-return'] ) || empty( $_GET['token'] ) || empty( $_GET['PayerID'] ) ) { ./wp-content/plugins/woocommerce-gateway-paypal-express-checkout/includes/class-wc-gateway-ppec-checkout-handler.php: return add_query_arg( 'woo-paypal-return', 'true', WC()->cart->get_checkout_url() );
WooCommerce本体ではなくGatewayプラグインのほうが出てきました。
このうち該当は2行目のほうです。
/**
* Checks data is correctly set when returning from PayPal Express Checkout
*/
public function maybe_return_from_paypal() {
if ( empty( $_GET['woo-paypal-return'] ) || empty( $_GET['token'] ) || empty( $_GET['PayerID'] ) ) {
return;
}
maybe(=たぶん)とかすごい投げやりな名前が出てきました。関数名でファイル内検索をすると
add_action( 'wp', array( $this, 'maybe_return_from_paypal' ) );
と出てきたので、ああ確かにこれはmaybeです。ページ表示ごとに毎回この関数は実行され、GETリクエストのパラメータがそろっていたら処理に入るという仕組みです。こうやってトリガを”引っ掛けて”いるんですね。
関数の中身に入ります。
// Get order
$order = wc_get_order( $session->order_id );
// Store address given by PayPal
$order->set_address( $this->get_mapped_shipping_address( $checkout_details ), 'shipping' );
// Complete the payment now.
$this->do_payment( $order, $session->token, $session->payerID );
// Clear Cart
WC()->cart->empty_cart();
// Redirect
wp_redirect( $order->get_checkout_order_received_url() );
exit;
do_payment()
で決済していそうです。
後続のWC()->cart->empty_cart()
でカートの中身を消しています。WCはWooCommerceの略でしょう。決済モジュールからカートを操作している、ということになりそうです。
do_payment()
の中身を――書いていくときりがないので省略しますが、この中で決済していました。決済後、
update_post_meta( $order->id, '_woo_pp_txnData', $txnData );
などでwp_postmeta
テーブルの'_woo_pp_txnData'
を更新している箇所など、SQLログと一致するところが確認できます。処理中、
$order->payment_complete( $payment->transaction_id );
を呼び出しており、これはWooCommerce本体の呼び出しにつながっています。上記カート処理と合わせて考えると、処理の主体(ドライバ)は決済プラグインで、WooCommerce本体は呼び出される側(下位モジュール)という形式をとっているのでしょう。
ということは、DBトランザクションなど処理の堅牢性の担保責任は決済プラグイン側にあるということになります。前述の決済処理後のDBトランザクションがない課題はGatewayプラグイン側で解決することになるでしょう(※メール送信処理がどこでどう行われているかを確認し、それと矛盾しないようにしなければいけません)。
得られた内容
ここまでの確認で、次のことが分かりました。
あくまで「WooCommerce PayPal Express Checkout Gateway 1.1.2の場合は」という但し書きがつきますが、
- 決済プラグインを使うと、決済トランザクションの制御はプラグインに丸投げする
- そのため決済処理の堅牢性はプラグインまかせ。決済手段を増やすときはそのたびに確認する必要がある
- 現バージョンは再起動に弱く、php-fpmやmod_phpの再起動時はデータに不整合を起こす可能性がある
本体をきちんと読めば別の結論が出るかもしれませんが、今回はつまみ食いで読み進めたのでこんなところになります。
この状態を避けるために負荷の低いうちにロードバランサでsorry画面を返すなどするとかえって悪化することもありますので、高負荷が見込まれるような商品を販売する場合、現在のところは「どのトラブル対応が一番ましか」を考慮して対応することになりそうです。
予算と時間が許すなら、できれば決済モジュールに手を入れて堅牢性を高めたいところですね。予防に勝る治療なしです。ASPとは異なる、オープンソースの強みでもあります。
以上、WooCommerce採用判断の一材料としていただければ幸いです。
明日はhideokamotoさんの「WooCommerceとMauticを連携させるプラグインをリリースした話」です。
最近のコメント