Contents
1. 要件定義と開発ゴール設定
EC サイトは 機能要件 と 非機能要件 を明確に分けて管理することで、MVP のスコープがブレにくくなります。このセクションでは、2025 年 12 月の記事「ECサイトに必要な機能と KPI」をベースに、実際の開発プロジェクトで使える要件整理シートを作成します。
1‑1. MVP と拡張要件の分離
まずは 最小限のリリース(MVP) を決め、その後に追加したい機能を「インターフェイスだけ」実装しておくと、将来的な拡張が容易です。
| カテゴリ | MVP で必須の機能 | 後期に追加予定 |
|---|---|---|
| 商品管理 | 商品登録・カテゴリ分け・在庫管理 | バリエーション(サイズ/色) |
| ユーザー | メール認証、Google / Apple ソーシャルログイン | プロファイル編集、ポイント制度 |
| カート・注文 | カート保存、注文確定、メール通知 | 複数配送先、予約販売 |
| 決済 | Stripe と Pay.jp のカード決済 | Apple Pay、後払いサービス |
1‑2. KPI の設定例
- 月間売上:目標 ¥5,000,000(初期は 10 万円単位で測定)
- コンバージョン率:商品詳細 → カート → 購入の完了率を 2 % 以上にする
- 平均注文額 (AOV):¥3,500 を目標に、クロスセル/アップセル施策で向上
KPI が数値化されていると、開発途中でも「成功指標」に対して進捗を可視化しやすくなります。
2. 開発環境構築とプロジェクトセットアップ
ローカルと本番の差異をできるだけ小さく保つために、Python 仮想環境 + Docker Compose の組み合わせが最適です。以下では具体的なコマンド例と設定ファイルを示します。
2‑1. Python 環境(pyenv + venv)
|
1 2 3 4 5 6 7 8 |
# pyenv に 3.12 系をインストールし、プロジェクト専用の仮想環境を作成 pyenv install 3.12.4 pyenv virtualenv 3.12.4 ecsite-env pyenv activate ecsite-env # パッケージ管理ツールは pip と poetry の併用が便利 pip install --upgrade pip setuptools wheel poetry |
ポイント
-pyenvはシステム全体に影響しないので、チームメンバー全員で同一バージョンを共有できます。
-poetry lockによる deterministic な依存関係は CI でも再現性が高くなります。
2‑2. Docker Compose 設定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# docker-compose.yml version: "3.9" services: web: build: . command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - "8000:8000" env_file: .env db: image: postgres:16-alpine environment: POSTGRES_USER: django POSTGRES_PASSWORD: secret POSTGRES_DB: ecsite volumes: - pgdata:/var/lib/postgresql/data redis: image: redis:7-alpine volumes: pgdata: |
Docker Compose の利点
- データベース・キャッシュがコンテナ化され、ローカルでも本番に近いネットワーク構成が再現できる。
docker compose up -dで 全スタックが一括起動 するため、環境セットアップの工数が大幅削減。
2‑3. Django 5.0 のインストールと ASGI 設定
|
1 2 3 |
poetry add "django>=5,<6" uvicorn[standard] psycopg2-binary django-admin startproject ecsite . |
ecsite/asgi.py はそのままで OK。settings.py に以下を追記して ASGI を有効化します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# settings.py の抜粋 INSTALLED_APPS = [ # 省略... "django.contrib.sites", "allauth", "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.google", "allauth.socialaccount.providers.apple", ] ASGI_APPLICATION = "ecsite.asgi.application" |
注意:Django 5.0 の async ORM は未実装ですが、ビュー層で
async defを書くことで外部 API 呼び出しの待ち時間を削減できます。
2‑4. アプリ分割と URL 設計
機能ごとに product, cart, order, payment の 4 つのアプリへ分離すると、責務が明確になりテストや再利用が楽になります。
|
1 2 3 4 5 |
python manage.py startapp product python manage.py startapp cart python manage.py startapp order python manage.py startapp payment |
ecsite/urls.py は次のようにシンプルにまとめます。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
from django.contrib import admin from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), path("", include("product.urls")), path("cart/", include("cart.urls")), path("order/", include("order.urls")), path("payment/", include("payment.urls")), path("accounts/", include("allauth.urls")), ] |
3. データモデル設計と管理画面カスタマイズ
データベースの構造は 検索性能 と 拡張性 を両立させることが重要です。ここでは主要エンティティ(Product, Category, CartItem, Order, PaymentLog)を例に、インデックスやユニーク制約の付け方を解説します。
3‑1. ER 図に基づくモデル定義
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# product/models.py class Category(models.Model): name = models.CharField(max_length=100, unique=True) def __str__(self) -> str: return self.name class Product(models.Model): category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="products") title = models.CharField(max_length=200) description = models.TextField() price = models.DecimalField(max_digits=10, decimal_places=0) # JPY は整数で OK stock = models.PositiveIntegerField() slug = models.SlugField(unique=True) class Meta: indexes = [ models.Index(fields=["slug"]), models.Index(fields=["price"]), ] def __str__(self) -> str: return self.title |
- JPY の価格は小数点以下が不要 なので
decimal_places=0にしておくと、金額計算時に余計な丸め処理を回避できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# order/models.py class Order(models.Model): STATUS_CHOICES = [ ("pending", "未決済"), ("paid", "支払済み"), ("canceled", "キャンセル"), ] user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending") total_price = models.DecimalField(max_digits=12, decimal_places=0) # JPY は整数 class Meta: indexes = [models.Index(fields=["created_at", "status"])] |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# payment/models.py class PaymentLog(models.Model): PROVIDER_CHOICES = [ ("stripe", "Stripe"), ("payjp", "Pay.jp"), ] order = models.OneToOneField(Order, on_delete=models.CASCADE) provider = models.CharField(max_length=10, choices=PROVIDER_CHOICES) charge_id = models.CharField(max_length=100) # Stripe/Pay.jp のトランザクション ID amount = models.DecimalField(max_digits=12, decimal_places=0) # JPY は整数 succeeded_at = models.DateTimeField(null=True, blank=True) def save(self, *args, **kwargs): # セキュリティ上、外部トークンはハッシュ化して保存 import hashlib self.charge_id = hashlib.sha256(self.charge_id.encode()).hexdigest() super().save(*args, **kwargs) |
マイグレーション
python manage.py makemigrations && python manage.py migrateで自動生成。インデックスはMeta.indexesに列挙するだけで、DB 側の最適化が即座に反映されます。
3‑2. 管理画面の拡張(Import‑Export)
大量商品や受注情報を CSV で一括操作したいケースは多いため、django-import-export と ModelAdmin の組み合わせが便利です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# product/admin.py from django.contrib import admin from import_export.admin import ImportExportModelAdmin from .models import Product, Category @admin.register(Product) class ProductAdmin(ImportExportModelAdmin): list_display = ("title", "category", "price", "stock") search_fields = ("title", "slug") list_filter = ("category",) @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display = ("name",) |
注文管理画面に CSV エクスポート アクションを追加すれば、レポート作成がボタン一つで完了します。
4. 認証・商品表示・カートフロー実装
4‑1. django-allauth とソーシャルログイン設定
allauth を導入すると、メール認証だけでなく Google / Apple の OAuth が 数分 で利用可能になります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# settings.py(抜粋) AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "allauth.account.auth_backends.AuthenticationBackend", ] SITE_ID = 1 ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_VERIFICATION = "mandatory" SOCIALACCOUNT_PROVIDERS = { "google": {"SCOPE": ["profile", "email"]}, "apple": {"APP_ID": "<YOUR_APP_ID>", "TEAM_ID": "<YOUR_TEAM_ID>"}, } |
テンプレート例(templates/account/login.html):
|
1 2 3 |
<a href="{% provider_login_url 'google' %}" class="btn btn-google">Google でログイン</a> <a href="{% provider_login_url 'apple' %}" class="btn btn-apple">Apple でログイン</a> |
4‑2. HTMX による部分更新
HTMX は AJAX の記述量を激減 させ、サーバー側テンプレートだけでリッチな UI が実装できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!-- product/templates/product/list.html --> <form id="filter-form" hx-get="{% url 'product:list' %}" hx-target="#product-list" hx-push-url="true"> <select name="category" onchange="this.form.submit()"> {% for cat in categories %} <option value="{{ cat.id }}">{{ cat.name }}</option> {% endfor %} </select> </form> <div id="product-list"> {% include "product/_list_partial.html" with products=page_obj.object_list %} </div> |
ビューは async にでき、データ取得は sync_to_async でラップします。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# product/views.py from asgiref.sync import sync_to_async from django.shortcuts import render async def product_list(request): category_id = request.GET.get("category") qs = Product.objects.all() if category_id: qs = qs.filter(category_id=category_id) page_obj = await sync_to_async(paginate)(qs, request) # paginate は Django の Paginator return render(request, "product/list.html", {"page_obj": page_obj}) |
4‑3. カートと注文フロー(DB 永続化モデル)
設計方針
- 未ログインでもカートを保持 → Cart に UUID のセッションキーを付与。
- トランザクションで在庫減算 → transaction.atomic() で競合を防止。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# cart/models.py import uuid from django.conf import settings class Cart(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE, related_name="carts", ) session_key = models.UUIDField(default=uuid.uuid4, editable=False) class CartItem(models.Model): cart = models.ForeignKey(Cart, related_name="items", on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.PROTECT) quantity = models.PositiveIntegerField(default=1) |
カート追加ビュー(async):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# cart/views.py from django.db import transaction from asgiref.sync import sync_to_async @sync_to_async def _add_item(cart, product_id): item, created = CartItem.objects.get_or_create( cart=cart, product_id=product_id, defaults={"quantity": 1}, ) if not created: item.quantity = F("quantity") + 1 item.save() return item async def add_to_cart(request, product_id): cart = await get_or_create_cart(request) # 別途実装 async with transaction.atomic(): await _add_item(cart, product_id) return JsonResponse({"status": "ok"}) |
5. 決済統合とテスト戦略
5‑1. Stripe / Pay.jp の最新 API と Webhook 設定(2026 年版)
5‑1‑a. 金額計算の注意点(JPY は 円単位)
Stripe の amount パラメータは 最小通貨単位 で指定します。JPY の場合は 1 円 = 1 が最小単位なので、100 倍にしないことが重要です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import os, stripe stripe.api_key = os.getenv("STRIPE_SECRET_KEY") def create_payment_intent(order): # JPY は整数で OK。order.total_price は Decimal のまま掛け算不要。 intent = stripe.PaymentIntent.create( amount=int(order.total_price), # 例: ¥12,340 → 12340 currency="jpy", metadata={"order_id": str(order.id)}, automatic_payment_methods={"enabled": True}, ) return intent.client_secret |
5‑1‑b. Webhook ハンドラ(非同期対応)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# payment/views.py from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse, JsonResponse import asyncio @csrf_exempt async def stripe_webhook(request): payload = await request.body sig_header = request.META.get("HTTP_STRIPE_SIGNATURE") try: event = stripe.Webhook.construct_event( payload, sig_header, os.getenv("STRIPE_WEBHOOK_SECRET") ) except (ValueError, stripe.error.SignatureVerificationError): return HttpResponse(status=400) if event["type"] == "payment_intent.succeeded": intent = event["data"]["object"] order_id = intent["metadata"]["order_id"] await sync_to_async(update_order_status)(order_id, "paid") return HttpResponse(status=200) |
5‑1‑c. Pay.jp の実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 |
import payjp payjp.api_key = os.getenv("PAYJP_API_KEY") def create_payjp_charge(order): charge = payjp.Charge.create( amount=int(order.total_price), # JPY は整数で OK currency="jpy", description=f"Order {order.id}", metadata={"order_id": str(order.id)}, ) return charge["id"] |
5‑2. pytest‑django と factory_boy によるテスト自動化
5‑2‑a. テストデータファクトリ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# tests/factories.py import factory from django.contrib.auth import get_user_model from product.models import Category, Product class UserFactory(factory.django.DjangoModelFactory): class Meta: model = get_user_model() email = factory.Sequence(lambda n: f"user{n}@example.com") password = factory.PostGenerationMethodCall("set_password", "password123") class CategoryFactory(factory.django.DjangoModelFactory): class Meta: model = Category name = factory.Sequence(lambda n: f"Category {n}") class ProductFactory(factory.django.DjangoModelFactory): class Meta: model = Product title = factory.Faker("word") price = factory.Faker("pydecimal", left_digits=5, right_digits=0, positive=True) stock = 100 category = factory.SubFactory(CategoryFactory) |
5‑2‑b. 非同期ビューのテスト例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# tests/test_cart.py import pytest from django.urls import reverse from asgiref.sync import sync_to_async @pytest.mark.django_db async def test_add_to_cart(client): user = await sync_to_async(UserFactory)() client.force_login(user) product = await sync_to_async(ProductFactory)() resp = await client.post(reverse("cart:add"), {"product_id": product.id}) assert resp.status_code == 200 exists = await CartItem.objects.filter(product=product, cart__user=user).aexists() assert exists |
5‑3. CI(GitHub Actions)で自動テスト実行
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# .github/workflows/ci.yml name: Django CI on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_USER: django POSTGRES_PASSWORD: secret POSTGRES_DB: ecsite_test ports: ["5432:5432"] options: >- --health-cmd "pg_isready -U $POSTGRES_USER" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.12" - name: Install Poetry & deps run: | pip install --upgrade pip pip install poetry poetry install - name: Run tests env: DATABASE_URL: postgresql://django:secret@localhost:5432/ecsite_test run: poetry run pytest -v |
6. CI/CD パイプライン・本番デプロイ、セキュリティ&パフォーマンス最適化
6‑1. Docker デプロイの全体像(Heroku / Render / Vercel)
| プラットフォーム | デプロイ手順概要 | 注意点 |
|---|---|---|
| Heroku | heroku stack:set container → GitHub Actions の heroku container:push web でデプロイ |
環境変数は Dashboard > Settings > Config Vars に設定 |
| Render | サービス作成時に「Docker」タイプを選択し、リポジトリとブランチを指定 | 自動ビルドが走るので Dockerfile が最新であることを確認 |
| Vercel | Docker 対応は ベータ版 です。vercel --prod でデプロイ可能だが、本番利用前に必ずテスト環境で検証することを推奨 |
Vercel の公式ドキュメントに「Docker はベータ対応」と明記されているため、リスクを理解した上で使用 |
重要:Vercel の Docker デプロイはまだベータ段階です。機能制限や不具合が起きた際のロールバック手順を事前に用意しておくと安心です。
6‑1‑a. 共通 .env.example
|
1 2 3 4 5 6 7 8 9 10 |
DEBUG=False SECRET_KEY=CHANGE_ME DATABASE_URL=postgres://django:secret@db:5432/ecsite REDIS_URL=redis://redis:6379/1 STRIPE_SECRET_KEY=sk_live_... PAYJP_API_KEY=pk_test_... ALLOWED_HOSTS=.herokuapp.com,.render.com,.vercel.app |
6‑2. セキュリティベストプラクティス(OWASP Top 10 対応)
| 項目 | 推奨設定例 |
|---|---|
| CSRF | Django のデフォルト CSRF トークンは必ず有効にし、{% csrf_token %} を全フォームで使用 |
| CSP (Content Security Policy) | django-csp パッケージを導入し、外部スクリプトは Stripe のみ許可 |
| HTTPS 強制 | SECURE_SSL_REDIRECT = True、SESSION_COOKIE_SECURE = True、CSRF_COOKIE_SECURE = True |
| データ暗号化 | カード情報は決済プロバイダに委託し、ローカル DB にはトークンハッシュだけ保存 |
| 脆弱性スキャン | pip-audit と GitHub Dependabot を有効化し、PR 時に自動で警告を出す |
|
1 2 3 4 5 6 7 8 |
# settings.py の抜粋 INSTALLED_APPS += ["csp"] CSP_DEFAULT_SRC = ("'self'",) CSP_SCRIPT_SRC = ("'self'", "https://js.stripe.com") SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True |
6‑3. パフォーマンス最適化(キャッシュ・非同期ビュー・インデックス)
- Redis キャッシュ
django-redisを利用してページキャッシュとセッションストアを統一。
|
1 2 3 4 5 6 7 8 9 10 |
# settings.py CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": os.getenv("REDIS_URL"), "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, } } SESSION_ENGINE = "django.contrib.sessions.backends.cache" |
- 商品一覧のキャッシュ(5 分間)
|
1 2 3 4 5 6 7 8 |
# product/views.py from django.views.decorators.cache import cache_page @cache_page(60 * 5) async def product_list(request): # 前述と同様に async 実装 ... |
- DB インデックスはモデル
Meta.indexesに列挙済みです。検索系クエリの実行計画をEXPLAIN ANALYZEで定期的に確認し、必要なら複合インデックスを追加してください。
6‑4. モニタリング・ロギング(付録)
| ツール | 用途 |
|---|---|
| Prometheus + Grafana | コンテナ CPU/メモリ・DB クエリ遅延の可視化 |
| Sentry | Django の例外をリアルタイムで捕捉し、スタックトレースを共有 |
| Datadog (Optional) | 1 つのダッシュボードでインフラとアプリケーション両方を監視 |
設定例(sentry_sdk.init):
|
1 2 3 4 5 6 7 8 9 10 |
import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration sentry_sdk.init( dsn=os.getenv("SENTRY_DSN"), integrations=[DjangoIntegration()], traces_sample_rate=0.2, send_default_pii=True, ) |
7. まとめと次のステップ
- 要件定義で MVP と拡張計画を分離し、KPI を数値化したことで開発指標が明確に。
- Docker + pyenv によるローカル環境は、本番と同一構成を保証し CI でも再利用可能。
- モデル設計では JPY の整数扱い、インデックス付与、ハッシュ化したトランザクション ID でセキュリティと検索性能を両立。
- 認証・UIは allauth と HTMX により実装コストを削減しつつ、UX を向上させた。
- 決済統合では Stripe の
amount計算ミス(100 倍)を排除し、Webhook で非同期結果を安全にハンドリング。 - テスト・CI/CDは pytest‑django と GitHub Actions で自動化し、Docker デプロイは Heroku/Render の本番対応と Vercel ベータ版の注意点を明示。
- セキュリティ・パフォーマンスは CSP、HTTPS、Redis キャッシュ、async ビューで現代的な Web アプリ要件を満たす。
次にやるべきこと
1. 本稿の手順通りにローカル環境と GitHub リポジトリを作成し、CI が緑になるまで走らせる。
2. ステージング環境(Render の無料枠や Heroku の Review Apps)で Stripe テストキー を使い決済フローを検証する。
3. KPI ダッシュボード(Grafana + Prometheus)を構築し、リリース後の指標をリアルタイムでモニタリング。
以上が 2026 年版 Django EC サイト開発フルスタックガイドです。ぜひ本稿をプロジェクトのロードマップに組み込み、実際のサービス構築へと踏み出してください!