はじめに
今回は、ECサイトの決済機能を作ったので、復習もかねて実装内容をまとめておきます。
やりたいことはざっくり言うとこの2つです。
- 決済フォームから送られてきた 名前・住所・カード情報 を受け取る
- それを データベースに保存 して、「誰が・何を・いくつ・いくらで買ったか?」がわかるようにする
そのために、まずはデータベースの設計(テーブル設計)から考えていきます。
DB設計をする
今回作るテーブルは次の3つです。
-
チェックアウト情報(Checkout)
名前・住所・メールアドレス・合計金額など、「注文そのもの」の情報 -
クレジットカード情報(Payment)
カード名義人・カード番号・有効期限などの情報
※実際のサービスではカード番号をDBにそのまま保存するのはNGですが、ここでは学習用サンプルとして保存しています。 -
注文ごとの商品明細(LineItem)
「どの商品を」「いくらで」「何個買ったか」を1行ずつ記録するテーブル
この3つを組み合わせることで、
- Checkout:注文の「ヘッダー」
- LineItem:注文の「中身(明細)」
- Payment:注文に紐づく「カード情報」
というイメージで管理していきます。
決済情報(名前・住所など)を保存するモデル
まずは、注文者の名前・住所・合計金額などを保存する Checkout モデルです。
class Checkout(models.Model):
class Meta:
db_table = "checkout"
# 新しい注文が上にくるように並べる
ordering = ["-created_at"]
last_name = models.CharField("姓", max_length=30)
first_name = models.CharField("名", max_length=30)
user_name = models.CharField("ユーザー名", max_length=30)
email = models.EmailField("メールアドレス")
zip_code = models.CharField("郵便番号", max_length=8)
prefecture = models.CharField("都道府県", max_length=30)
city = models.CharField("市区町村", max_length=30)
street_address = models.CharField("丁目・番地・号", max_length=30)
building_name = models.CharField("建物名・部屋番号", max_length=30, blank=True)
total_amount = models.IntegerField("合計金額", default=0)
total_quantity = models.IntegerField("合計数量", default=0)
created_at = models.DateTimeField("注文日時", auto_now_add=True, null=True)
- 1件の
Checkoutが「1回分の注文」を表します。 -
total_amountやtotal_quantityには、後でカートの中身から計算した合計金額・合計個数を入れます。
クレジットカード情報を保存するモデル
次に、カード情報を保存する Payment モデルです。
# クレジットカード情報モデル
class Payment(models.Model):
class Meta:
db_table = "payment"
checkout = models.ForeignKey(
Checkout, verbose_name="請求情報", on_delete=models.CASCADE
)
card_holder = models.CharField("カード名義人", max_length=50)
card_number = models.CharField("カード番号", max_length=19)
expiration_date = models.CharField("有効期限", max_length=10)
cvv = models.CharField("セキュリティコード", max_length=3)
-
checkoutはCheckoutへの外部キーで、「どの注文のカード情報か」を表します。 - 現実のサービスでは、カード番号/CVVなどは外部の決済サービス(Stripeなど)に任せて、自前のDBには保存はしません。
- 今回はあくまで学習用として「こういう形で紐づけるんだな」とイメージするための例として定義しています。
決済情報と「そのとき買った商品」を紐づけるモデル
次は、1回の注文の中で「どの商品を」「いくらで」「何個買ったか」を記録する LineItem モデルです。
# 注文商品の明細モデル
class LineItem(models.Model):
class Meta:
db_table = "line_item"
checkout = models.ForeignKey(
Checkout, verbose_name="注文情報", on_delete=models.CASCADE
)
name = models.CharField("商品名", max_length=100)
price = models.IntegerField("価格", default=0)
quantity = models.IntegerField("数量", default=1)
subtotal_amount = models.IntegerField("小計", default=0)
なぜ Product のフィールドをコピーしているのか?
ここでは
namepricequantitysubtotal_amount
など、Productモデルにもありそうな項目を、あえてもう一度フィールドとして作っています。
「じゃあ最初から Product や CartItem に外部キーを張ればいいんじゃないの?」
と思うかもしれませんが、そうしないのには理由があります。
CartItem をFKで結ばない理由
もし LineItem が CartItem や Product にそのまま外部キーで紐づいているだけだと、
- あとで商品名や価格を変更したとき
→ 過去の注文データまで「新しい名前・新しい価格」で見えてしまう
という問題が起きます。
でも、本当に知りたいのは
「注文当時の 商品名・価格・数量」
ですよね。
そのため、
- 決済が完了したタイミングで
- CartItem / Product から そのときの値をコピー
-
LineItemとして「スナップショット(その時点のコピー)」を保存する
という設計にしています。
簡単にいうと、
LineItem は「その注文時点の履歴」を残すためのテーブル
というイメージです。
フォームでバリデーションを設定する(FormView用)
次に、FormView から使うための**フォームの定義(バリデーション)**を作っていきます。
forms.py を作成し、決済情報+カード情報を受け取る OrderForm を定義します。
from django import forms
class OrderForm(forms.Form):
# 決済情報(名前・住所など)
last_name = forms.CharField(max_length=50)
first_name = forms.CharField(max_length=50)
user_name = forms.CharField(max_length=50)
email = forms.EmailField(max_length=50)
zip_code = forms.CharField(max_length=50)
prefecture = forms.CharField(max_length=50)
city = forms.CharField(max_length=50)
street_address = forms.CharField(max_length=50)
building_name = forms.CharField(max_length=50)
# クレジットカード情報
card_holder = forms.CharField(max_length=100)
card_number = forms.CharField(max_length=50, min_length=16)
expiration_date = forms.CharField(max_length=20)
cvv = forms.CharField(max_length=4, min_length=3)
ここでは、最低限のバリデーションだけを付けています。
-
CharField,EmailFieldなどの型 -
max_length/min_lengthで文字数のチェック- 例:カード番号 → 16桁以上にしたいので
min_length=16
- 例:カード番号 → 16桁以上にしたいので
実際のサービスでは、もっと厳しいチェックを入れますが、まずは「Formを通して入力値を受け取る」段階まで作ることを優先しています。
テンプレート側との対応
ここで定義したフィールド名は、テンプレートの <input> の name 属性と対応させます。
<input type="text" name="last_name" />
のように書いておくと、
- フォームから送信された
last_nameの値が -
OrderFormのlast_nameフィールドに入り - Djangoが自動でバリデーション(必須・文字数など)をしてくれる
という流れになります。
こんな感じで、
- モデル設計で「どんな情報を、どんなテーブルに分けるか」
- フォームで「どんな入力を受け取って、どうチェックするか」
まで整理したので、この後 FormView側で「Checkout / Payment / LineItem をまとめて作成する処理」 を書いていこうかと思います。