WordPress・CMS設計

アクアリアKLCC チケット予約システムのモック開発:WordPressでのデータ不整合解決記

WordPressでアクアリアKLCCのチケット予約システムをモック開発する場合、最初に難しくなるのは画面の作成ではない。チケットの種類をどう管理するか、タイムスロットごとの在庫をどこに持つか、外部APIとの通信が途中で失敗したときに何を予約成立とみなすか。この三つを曖昧にしたまま実装へ進むと、管理画面は早くできても、予約処理だけが最後まで安定しない。…

アクアリアKLCC チケット予約システムのモック開発:WordPressでのデータ不整合解決記

今回扱うのは、実在するアクアリアKLCCの本番システム改修記ではない。あくまで、観光施設のチケット予約を想定したモック環境で、WordPressを管理画面とコンテンツ基盤に使い、在庫や予約状態を専用テーブルで管理する構成を検討したケーススタディである。

チケットと料金

アクアリアKLCC

4.2/5 · 706件のレビュー料金確認日: 2026年8月27日直近の日程で予約可

I

節約する

公式チケット窓口とオンライン購入

  • 大人 80 MYR
窓口

80 MYR

公式サイト

II

入場チケット

入場チケット — 当日・翌日利用可能

5,000匹以上の海洋生物の観察、全長90メートルの水中トンネルの歩行、海の生物との触れ合い体験ができる入場チケットです。

4.2·706件のレビュー

料金は日付と混雑で変わります。当日の価格は予約ページでご確認ください。提携チケット販売GetYourGuide(アフィリエイトリンク)

「アクアリアKLCC チケット」のように、利用日や入場時間、年齢区分、居住者区分などが関係する予約では、単なる商品一覧のような設計はすぐに限界へ達する。表示するだけなら投稿とカスタムフィールドで足りる。しかし、同じ枠へ複数の利用者が同時に申し込み、外部の決済や予約APIも関係するとなると、必要なのはCMSの整理術ではなく、在庫を一度だけ減らすためのデータ設計である。

アクアリアKLCCのチケット構造と予約システムの要件定義

チケット予約システムのモックを組むとき、最初に手を入れるべきは画面ではなく、現実に近いデータモデルだ。アクアリアKLCCを題材にすると、施設情報と予約可能な枠を分けて考えやすい。

アクアリアKLCCは、クアラルンプール・コンベンション・センターにある海洋水族館として知られている。館内には水中トンネルを含む展示があり、観光客が訪れる施設として、入場券の販売経路も一つとは限らない。公式サイトでの販売、旅行予約プラットフォーム経由の販売、当日窓口での販売などを想定すると、同じ「チケット」という言葉の中に、いくつもの管理単位が含まれていることが分かる。

モックで最低限扱うべき単位は、次のようになる。

  • 利用日
  • 入場タイムスロット
  • チケットの年齢区分
  • 居住者区分や販売区分
  • 表示価格
  • 予約可能数
  • 予約済み数
  • 仮予約、確定、失敗、キャンセルなどの状態
  • 外部API側の予約番号や処理結果
  • 予約の有効期限

実在施設の料金や販売条件は、時期、販売チャネル、キャンペーン、為替などによって変動する。したがって、モックの要件定義で特定の価格を固定値として扱うより、料金を変更できるデータとして設計しておく方が現実的だ。

年齢区分も同じである。大人、子供、シニアといった区分を最初から想定しておくことはできるが、区分そのものをPHPの条件分岐に埋め込むと、後から販売条件が変わったときに修正範囲が広がる。adultchild のような内部値を使う場合でも、表示名や年齢条件は管理データとして分離しておきたい。

タイムスロットは、予約システムの設計を日付単位から枠単位へ変える。たとえば同じ利用日でも、午前の枠と午後の枠では受け入れ可能な人数が異なるかもしれない。日付だけをキーにして在庫を持つと、混雑する時間帯の制御ができない。モックだからといってタイムスロットを省略すると、肝心の競合処理を検証できなくなる。

予約システムの設計で先に決めるべきなのは、画面の見た目ではなく、何を一つの在庫枠として数えるかである。

ここで、チケットの投稿と予約枠の在庫を同じものとして扱わないことが重要になる。チケット情報は管理画面で編集するコンテンツだが、在庫は短い時間に何度も読み書きされる業務データだ。両者には、求められる性質が違う。

たとえば「外国人向け」「居住者向け」といった販売区分を、それぞれ別のカスタム投稿タイプにする必要はない。投稿タイプを分けると、管理画面の一覧は一見分かりやすくなる。しかし、料金改定や項目追加のたびに複数の投稿タイプを横断して処理することになり、予約API側の分岐も増える。

一つのチケット投稿に、販売区分や年齢区分をメタ情報として持たせる方が、コンテンツ管理としては扱いやすい。一方、在庫数や予約済み数については、後述する専用テーブルに切り出す。この分離が、WordPressを使いながら予約処理を破綻させないための基本線になる。

要件定義では、次のような問いを先に決めておくとよい。

1. 在庫はチケットの種類ごとに持つのか、それともタイムスロット全体で共有するのか。

2. 大人二枚と子供一枚を、一つの予約として扱うのか、明細を分けるのか。

3. 仮予約の有効期限が切れたとき、在庫をどの処理で戻すのか。

4. 外部APIの応答がタイムアウトした場合、予約を失敗とみなすのか、確認待ちにするのか。

5. 決済成功後に内部更新が失敗した場合、再実行できる識別子を持つのか。

6. 管理画面からの手動キャンセルと、利用者によるキャンセルをどう区別するのか。

この段階で「将来的に繁忙期の在庫を変えたい」「年齢区分を増やしたい」「販売経路ごとに配分を持たせたい」といった変更可能性も見ておく。最初からすべてを実装する必要はないが、変更される情報をコードの条件分岐に埋め込まないことは、モックでも守っておきたい。

WordPressカスタム投稿タイプによるチケット管理

データモデルが決まったら、WordPress側の管理単位を作る。ここで扱いやすいのは、チケット情報を一種類のカスタム投稿タイプにまとめる方法だ。

たとえば aquaria_ticket というカスタム投稿タイプを用意し、チケットの名称、説明、販売状態、表示用の画像などを投稿として管理する。タイムスロットや年齢区分、販売区分などの可変情報は、必要に応じてメタ情報として保存する。

メタキーの例としては、次のようなものが考えられる。

  • slot_date:利用日
  • slot_time:表示用の入場時間
  • age_tier:年齢区分
  • customer_type:居住者区分や販売区分
  • price:表示用価格
  • external_product_code:外部サービスとの対応コード
  • is_bookable:予約受付の可否

ここで注意したいのは、表示用のチケットデータと、予約処理で競合する在庫データを一つのメタ情報だけで完結させようとしないことだ。

wp_postmeta は柔軟だが、キーと値を行単位で保存する構造である。複数のメタ条件を組み合わせた検索や、在庫更新のような頻繁な書き込みには向いていない。投稿一覧や詳細ページの表示には便利でも、「この日付、この時間、この区分の在庫を一件だけ安全に減らす」という処理を任せる場所ではない。

そのため、在庫については wp_aquaria_inventory のような専用テーブルを作り、投稿IDまたは独自のスロットIDと関連付ける構成が扱いやすい。テーブルには少なくとも、次のような情報を持たせる。

項目役割
slot_id利用日と時間枠を識別するID
ticket_idWordPressのチケット投稿との関連
age_tier大人、子供などの区分
capacityその枠で受け入れられる上限
reserved予約処理で確保された数量
status販売中、停止中などの状態
updated_at最終更新時刻

タイムスロットとチケット区分の組み合わせが一意になるなら、slot_idage_tier などに一意制約を設ける。どの行が同じ在庫を表すのかが曖昧なままだと、アプリケーション側で重複登録を防ぐ処理が必要になり、結局そこでも競合が起きる。

reserved を持つ理由は、予約のたびに予約明細を集計して現在の在庫を算出する処理を避けるためだ。ただし、予約明細を持たなくてよいという意味ではない。予約履歴は別テーブルに保存し、在庫テーブルの集計値と照合できるようにする。集計値だけに依存すると、障害復旧や監査のときに原因を追えなくなる。

この構成では、WordPressは管理画面とコンテンツ管理を担当し、予約処理は専用サービスが担当する。アプリケーションの境界を最初から分けておくと、管理画面での編集処理と、利用者が同時に送信する予約処理が同じフックに押し込まれずに済む。

管理画面には、チケットの編集画面だけでなく、タイムスロット別の在庫サマリーも用意したい。WP_List_Table を使って、利用日、時間枠、販売区分、上限、予約済み数、残数、状態を一覧にすると、運用担当者が異常を見つけやすくなる。

ただし、管理画面に表示する残数と、予約確定時に判定する残数は別物だ。管理画面は数秒前の情報を表示しても運用上許容される場合があるが、予約処理はその表示値を信頼してはいけない。確定処理では、必ずデータベース上の最新行を条件付きで更新する。

API連携におけるデータ不整合の発生原因

外部APIを含む予約処理では、内部データベースと外部サービスが同じタイミングで成功するとは限らない。ここを、通常のフォーム送信と同じ感覚で組むとデータ不整合が起きる。

典型的な流れは、利用者がタイムスロットと枚数を選択し、内部で仮予約を作成し、外部の予約または決済APIへリクエストを送り、成功したら予約を確定するというものだ。

問題は、その途中で次のようなことが起きる点にある。

  • 内部DBへの書き込みは成功したが、外部APIがタイムアウトする。
  • 外部APIでは処理が成功したが、内部サーバーが応答を受け取れない。
  • 外部APIからエラーが返ったが、内部では仮予約だけが残る。
  • 利用者がブラウザの戻る操作や再送信を行い、同じ予約が複数回送信される。
  • 決済完了の通知が遅れて到着し、画面上の状態とWebhookの状態が食い違う。

ここで大切なのは、データベースのトランザクションと外部APIの処理を同じトランザクションだと考えないことである。MySQLのトランザクションは、同じデータベース内の処理をまとめて確定または取り消しする仕組みだ。外部APIに送ったリクエストまで、データベースの ROLLBACK で取り消せるわけではない。

WordPressの $wpdb に、汎用的なトランザクションメソッドや自動ロールバック機構が標準で用意されているわけでもない。$wpdb はSQLを実行するためのインターフェースであり、必要な場合はSQLとして START TRANSACTIONCOMMITROLLBACK を明示的に発行する。もちろん、利用するテーブルがトランザクションを扱えるストレージエンジンであることも前提になる。

内部DBでトランザクションを使う範囲

たとえば、在庫の確保と仮予約レコードの作成を同じ単位で扱うなら、処理の概念は次のようになる。

1. START TRANSACTION を発行する。

2. 条件付きUPDATEで在庫を確保する。

3. UPDATEされた行数を確認する。

4. 成功した場合だけ仮予約レコードを作成する。

5. すべて成功したら COMMIT を発行する。

6. 途中で失敗したら ROLLBACK を発行する。

このとき、条件付きUPDATEの結果が一行も更新されなければ、在庫不足または対象枠の不在と判断する。PHP側で先に在庫を読み取り、その値を見てから更新するのではない。読み取りと書き込みの間に、別のリクエストが同じ在庫を確保する可能性があるからだ。

SQLの実行結果や $wpdb->last_error を確認せず、処理が進んだことだけを見て成功扱いにするのも危険である。SQLエラー、制約違反、接続断などを、予約失敗として記録できるようにしておく。

外部APIは状態遷移として管理する

外部APIへの通信をデータベーストランザクションの中に長時間含める設計は、慎重に扱う必要がある。ネットワークの応答を待つ間、行ロックを保持し続けると、同じ枠への別リクエストが待たされる。応答が遅いほどロックの保持時間も伸び、予約処理全体の詰まりにつながる。

現実的には、内部DBと外部APIを一つの原子的な処理にするのではなく、予約状態を明確な状態遷移として管理する。

  • pending:内部で仮予約を作成し、外部処理を待っている状態
  • processing:外部APIへの送信を開始した状態
  • confirmed:外部側の成功を確認し、内部でも確定した状態
  • failed:外部側の失敗を確認した状態
  • unknown:タイムアウトなどで外部側の結果を確定できない状態
  • cancelled:利用者または管理者が取り消した状態
  • expired:仮予約の有効期限が切れた状態

特に必要なのが unknown のような確認待ち状態である。タイムアウトは、外部APIが失敗したことを意味しない。リクエストを受け取った後で外部側の処理が成功し、応答だけが内部に届かなかった可能性もある。

この場合に、内部だけを即座に失敗扱いにして在庫を戻すと、外部側では成立済みなのに内部では在庫が復活するという、別の不整合が生まれる。外部APIが照会機能を提供しているなら、予約番号や冪等キーを使って後から状態を確認する。照会できない場合は、運用上の確認フローを用意するしかない。

ROLLBACK で取り消せるのは、同じデータベース内でまだ確定していない処理である。外部APIの結果まで巻き戻せるわけではない。

冪等性を予約番号に持たせる

利用者の再送信や通信障害を考えると、リクエストごとに冪等キーを持たせる必要がある。たとえば、利用者の画面から送られた予約要求に一意なキーを付け、同じキーを受け取った場合は既存の処理結果を返す。

冪等キーは、単にPHPの変数として持つだけでは意味がない。予約テーブルに保存し、データベース側で一意制約を設定する。そうすれば、同じキーのリクエストが同時に到着しても、重複レコードの作成を防げる。

外部APIへ送るリクエストにも、可能なら同じ業務上の予約番号や冪等キーを渡す。内部と外部で異なる識別子を使うと、再試行時に新規予約として扱われる可能性がある。

予約枠の競合を防ぐ排他制御

予約処理で最も避けたいのは、利用可能数を画面で確認したあと、PHPの条件分岐だけで在庫を減らすことである。

「現在の予約済み数を取得する」「上限を超えないことを確認する」「予約済み数に枚数を加える」という三つの処理を別々に実行すると、その間に別のリクエストが割り込める。これは同じPHPプロセス内の問題ではなく、複数のリクエストが同時にデータベースへ到達することによる競合だ。

在庫10に対して残り3と表示されているとき、利用者Aと利用者Bが同時に3枚ずつ申し込む。両方のリクエストが「残り3」を読み取れば、両方とも条件を満たしたと判断してしまう。読み取り時点では正しくても、書き込みの結果は正しくない。

条件付きUPDATEで在庫を確保する

この問題には、データベース側で条件と更新を一つにまとめる方法が有効だ。たとえば、在庫テーブルの該当行に対して、予約済み数へ3を加算し、その結果が容量を超えない場合だけ更新する。

SQLの考え方は、reserved = reserved + 3 としつつ、reserved + 3 <= capacity をWHERE条件に含めるというものだ。対象の slot_idage_tier を指定し、更新された行数が1なら在庫確保に成功、0なら在庫不足または対象なしと判定する。

実装上は、次の点を確認する必要がある。

  • 枚数が正の整数であることをサーバー側で検証する。
  • slot_id や区分値を利用者の入力のままSQLへ連結しない。
  • テーブル名やカラム名を固定し、値は $wpdb->prepare() などで適切に扱う。
  • UPDATEの戻り値を成功判定に使う。
  • 在庫確保と仮予約作成を同じDBトランザクションで処理する。
  • 予約取消や期限切れ処理では、二重に在庫を戻さない。

SELECT ... FOR UPDATE による行ロックも選択肢になる。先に対象行をロックして現在値を読み、アプリケーション側で判定してから更新する方法だ。ただし、ロックを取った後の処理を長くしないことが重要である。外部APIへのネットワーク通信までロック中に実行すると、障害時にロックが残りやすくなる。

在庫確保に必要な判定が一つのUPDATEで表現できるなら、条件付きUPDATEの方が処理の境界を小さくできる。反対に、複数の在庫行を組み合わせる必要がある場合や、複雑な割当ルールがある場合は、トランザクション内で行ロックを使う方が読みやすいこともある。

ロックだけに頼らない

排他制御を実装したからといって、すべての問題が終わるわけではない。ロックの取得順序が処理ごとに違うと、複数の行を扱う処理でデッドロックが起きる可能性がある。

大人用と子供用の在庫を同時に確保する場合、ある処理は大人から、別の処理は子供からロックすると、互いに相手のロック解除を待つ状態になる。複数行を扱うときは、キーの順序などを決め、常に同じ順番で処理する。

また、予約処理が失敗したときに、在庫を戻す処理を必ず一回だけ実行できるようにする。予約状態が pending のときだけ expired に変更し、その状態変更に成功した場合だけ在庫を戻す、といった条件が必要だ。状態更新と在庫返却を分ける場合も、二重返却を防ぐ識別子を残しておく。

キャッシュ戦略と在庫表示の分離

WordPressには、オブジェクトキャッシュやトランジェントなど、表示を高速化するための仕組みがある。チケット一覧や施設情報のような、頻繁には変わらないデータには適している。

一方、予約可能数をそのままキャッシュする場合は注意が必要だ。キャッシュに残った値が古いままでも、一覧画面の表示なら大きな問題にならないことがある。しかし、予約確定時の在庫判定にその値を使うと、すでに埋まった枠を予約可能として表示し続けることになる。

設計上は、在庫の「表示」と「確保」を分ける。

  • 詳細ページのチケット説明や画像はキャッシュしてよい。
  • 利用日やタイムスロットの候補も、短時間のキャッシュが許容される場合がある。
  • 「残りわずか」のような補助表示は、多少古くても画面上の目安として扱える。
  • 予約確保の可否は、キャッシュではなくデータベースの条件付きUPDATEで判断する。
  • 予約確定後の状態も、重要な画面ではデータベースから取得する。

「残りわずか」と「予約できる」は同じ意味ではない。前者は利用者への案内であり、後者は業務処理の判定だ。ここを同じキャッシュ値で済ませようとすると、表示を速くする代わりに整合性を失う。

キャッシュを使う場合も、キャッシュ削除のタイミングを設計しておきたい。在庫確保、キャンセル、期限切れによる返却、管理画面からの容量変更など、残数が変わるイベントを洗い出し、表示用キャッシュを更新または削除する。

ただし、キャッシュを削除したからといって予約処理が安全になるわけではない。削除処理自体が失敗することもあるし、複数台構成では各サーバーのキャッシュが同時に消えるとは限らない。キャッシュはあくまで表示の補助であり、正しさの根拠にはしない。

仮予約の期限と外部APIの再試行

決済や外部予約APIを含む場合、仮予約をいつまで保持するかも要件になる。仮予約に期限がなければ、利用者が決済画面を閉じただけで在庫が戻らず、時間とともに販売可能数が減っていく。

仮予約には有効期限を保存し、期限を過ぎたレコードを回収する。WP-Cronを使う方法は、WordPressの運用環境では実装しやすい。ただし、WP-Cronは厳密な時刻に必ず実行される仕組みではないため、期限を過ぎた瞬間に在庫が戻ることを前提にしてはいけない。

予約確保の処理では、期限切れの仮予約を見つけたら状態を変更し、在庫返却を一度だけ行う。予約の状態と在庫の変更を、できるだけ同じトランザクションで扱う。大量の期限切れを一度に処理するとロック時間が長くなるため、一定件数ずつ処理し、失敗時には次回へ回せる設計にする。

外部APIの再試行も、無条件に行ってはいけない。タイムアウトや一時的なサーバーエラーは再試行できる場合があるが、入力エラーや在庫不足を繰り返しても結果は変わらない。エラーの種類を記録し、再試行可能かを判定する。

再試行を行う場合は、次の情報をログに残しておく。

  • 内部予約番号
  • 外部APIへ送った冪等キー
  • リクエスト開始時刻
  • 応答コード
  • タイムアウトや接続失敗の有無
  • 再試行回数
  • 最終的な予約状態
  • 外部側の照会結果

特に「応答がなかった」と「外部側で失敗した」は分けて記録する。前者は結果不明であり、後者は失敗が確認できた状態だからだ。

監査ログと管理画面の役割

予約システムでは、最終状態だけでなく、そこへ至った経緯を追えることが重要になる。予約が失敗したとき、利用者の入力ミスなのか、在庫不足なのか、外部APIのタイムアウトなのか、内部DBのエラーなのかで、対応は変わる。

監査ログ用に wp_aquaria_audit_log のような専用テーブルを設け、予約状態の変更や在庫操作を記録する。利用者IDがある場合はそのID、匿名予約ならセッションや予約番号など、追跡に必要な情報を保存する。IPアドレスなどの情報を扱う場合は、保存期間やアクセス権限も決めておく。

ログには、少なくとも次の区別を持たせたい。

  • 予約要求を受け付けた
  • 在庫確保に成功した
  • 在庫不足で拒否した
  • 外部APIへ送信した
  • 外部APIの成功を確認した
  • 外部APIの結果が不明になった
  • 仮予約が期限切れになった
  • キャンセルした
  • 在庫を返却した
  • 管理者が手動で状態を変更した

管理画面は、予約ロジックそのものを実行する場所ではなく、状態を確認し、必要な運用操作を行う場所として設計する。新規予約を投稿保存のフックから作成するのではなく、利用者向けの公開エンドポイントから予約サービスを呼び出す。管理画面では予約の閲覧、照会、キャンセル、再処理の指示などに役割を絞る。

save_post はコンテンツ更新のための仕組みであり、予約確保のトランザクション境界ではない。投稿保存の途中で外部APIを呼び出したり、予約在庫を減らしたりすると、編集画面の保存、インポート、プログラムによる更新など、意図しない経路から業務処理が動く可能性がある。

モック開発で確認すべき設計上の勘所

モック開発の価値は、本番相当の性能数値を出すことではない。どの条件で在庫を確保し、どの状態なら再試行し、どこから先を運用で確認するのかを、実装前に明らかにすることにある。

負荷試験を行う場合も、試験環境、データ量、同時接続数、ネットワーク条件、キャッシュの有無、データベースの構成を記録しなければ、数値だけを本番性能の根拠にはできない。モック環境で確認できるのは、その環境と条件における挙動であって、本番で同じ結果が出る保証ではない。

確認したいのは、たとえば次のような性質だ。

1. 同じ在庫枠へ同時に複数の予約要求を送っても、条件付きUPDATEの判定が破綻しない。

2. 在庫確保に失敗した要求が、予約成立として保存されない。

3. 仮予約の作成途中でSQLエラーが起きた場合、在庫だけが減った状態にならない。

4. 外部APIのタイムアウト時に、予約を即時失敗ではなく確認待ちへ移せる。

5. 同じ冪等キーで再送信しても、予約レコードが重複しない。

6. 期限切れ処理を複数回実行しても、在庫が二重に戻らない。

7. 外部側で成功した後に内部応答が失われても、照会によって状態を確定できる。

8. 管理者によるキャンセルが、確定済み予約と仮予約で適切に扱われる。

このような試験は、レスポンスの平均値だけを見るよりも、失敗時の状態遷移を確認する方が有益だ。予約システムの事故は、通常時の画面表示ではなく、通信が切れた瞬間や、同じリクエストが再送された瞬間に起こる。

モックの実装では、予約処理を Aquaria_Reservation_Service のようなサービスクラスへまとめる構成が扱いやすい。サービス側で入力検証、在庫確保、仮予約作成、外部API連携、状態更新、監査ログ記録を管理し、REST APIや管理画面はその呼び出し口にする。

ただし、サービスクラスにまとめたから自動的に安全になるわけではない。トランザクションの開始と終了、SQLの成否確認、例外時の ROLLBACK、外部APIの結果不明状態、再試行の条件まで明示する必要がある。抽象化は、処理の責任範囲を見えやすくするために使うもので、難しい部分を隠すために使うものではない。

スケーラブルな予約システム設計の考え方

今回のモック開発から見えてくる設計上の原則は、次のように整理できる。

  • チケットの表示情報はカスタム投稿タイプで管理し、競合する在庫は専用テーブルへ分離する。
  • 在庫の判定はPHPの読み取り結果に依存せず、データベース側の条件付き更新で行う。
  • 内部DBのまとまった処理には、START TRANSACTIONCOMMITROLLBACK を明示的に使う。
  • $wpdb に自動ロールバックや汎用のトランザクションメソッドがある前提で設計しない。
  • 外部APIはDBトランザクションの一部ではなく、冪等キーと状態遷移で連携する。
  • タイムアウトは失敗と同義ではないため、結果不明の状態と照会処理を用意する。
  • 在庫の正しさをキャッシュに任せず、キャッシュは表示の高速化に限定する。
  • 仮予約の期限切れと在庫返却を、二重実行できない処理として設計する。
  • 監査ログを後付けせず、予約状態と在庫操作の履歴を最初から保存する。
  • 負荷試験の結果は、条件と測定方法を示せる場合に限って判断材料として扱う。

この中でも、WordPressで予約システムを組むときに最も大事なのは、投稿保存と予約確定を同じ仕組みで処理しないことだろう。WordPressは管理画面やコンテンツ配信に強い。一方で、在庫の競合を短時間に処理し、外部サービスとの不確実な通信を状態として管理する部分には、専用のデータモデルとサービス層が必要になる。

「WordPressだけで全部やる」という表現は、実装範囲を簡単に見せる。しかし、実際にはWordPressのテーブル、独自テーブル、外部API、決済結果、運用ログという複数の境界をまたぐ。すべてを投稿とメタ情報だけで済ませようとすると、管理画面の都合が予約処理へ侵入し、競合制御や障害復旧の責任が曖昧になる。

逆に、予約部分を専用サービスとして切り出し、WordPressには管理と表示を担当させると、構成は少し複雑になるが、責任の所在は明確になる。利用者向けの予約受付はAPI経由、管理者向けの操作は管理画面経由、在庫と状態の正しさは専用テーブルとトランザクションで担保する。この分担が、WordPressを使い続けながら業務ロジックを守る現実的な落としどころだ。

整合性が必要な場所はデータベースに寄せ、運用で吸収できる場所だけをWordPress側へ残す。

アクアリアKLCCのような観光施設を題材にすると、チケットは単なる商品ではなくなる。利用日、時間枠、年齢区分、販売経路、外部予約、決済、キャンセル、当日の入場確認が一つの流れに連なっているからだ。

モック開発で本当に確認すべきなのは、見栄えのよい予約画面が表示されることではない。通信が失敗したとき、同じリクエストが繰り返されたとき、在庫が最後の一枚になったとき、管理者が手動でキャンセルしたときに、データがどの状態へ遷移するかである。

完璧な予約システムを最初から作るのは難しい。それでも、在庫を一度だけ減らすこと、外部APIの結果を誤って断定しないこと、失敗後に追跡できること。この三つを最初のモックで確認できれば、本番実装へ進む前に大きな事故の種を見つけられる。

WordPressを選んだなら、WordPressらしさだけに処理を合わせる必要はない。管理画面はWordPressのままでも、在庫処理にはリレーショナルデータベースの原則を使い、外部API連携には冪等性と状態遷移を使う。その境界を割り切って設計することが、個人開発者にとって最も堅実な投資になる。

関連記事: リスボン海洋水族館 チケット予約の負荷対策:Docker環境でのRedisキャッシュ検証アルティス・マイクロピアのチケット連携:API開発と埋め込みコードの設計比較.

現地の基本情報

通貨: MYR

通行区分: 左側通行

緊急通報番号: 999

主なデータ

建設: 2005

公式サイト: aquariaklcc.com

最安 56.36 MYRオンラインで予約

よくある質問

WordPressでチケット予約システムを作る場合、在庫を投稿メタで管理してもよいですか?
投稿メタはチケットの表示や管理には便利ですが、頻繁な書き込みや在庫更新には向いていません。在庫は専用テーブルに分離し、投稿IDやスロットIDと関連付ける構成が扱いやすいです。
予約システムの在庫競合はどのように防げますか?
在庫を読み取ってからPHPで判定するのではなく、予約済み数の加算と上限確認を条件付きUPDATEで一度に行います。更新された行数が1なら確保成功、0なら在庫不足または対象枠なしと判定します。
外部予約APIがタイムアウトした場合、予約を失敗にしてよいですか?
タイムアウトは外部API側の処理失敗を意味しないため、即座に失敗扱いにはしません。結果不明の状態を設け、予約番号や冪等キーを使った照会、または運用上の確認フローで状態を確定します。
予約の再送信による二重登録を防ぐにはどうすればよいですか?
リクエストごとに冪等キーを付け、そのキーを予約テーブルへ保存してデータベース側で一意制約を設定します。同じキーの再送信には、既存の処理結果を返します。
仮予約の期限切れで在庫を二重に戻さない方法はありますか?
仮予約がpending状態のときだけexpiredへ変更し、その状態変更に成功した場合に限って在庫を返却します。期限切れ処理を複数回実行しても、同じ予約の在庫が二重に戻らない条件を設けます。

Photo: Phalinn Ooi from Kuala Lumpur, Malaysia / CC BY 2.0 — Wikimedia Commons

参考情報