ER図
データモデル設計方針
本システムのデータモデルは 10 エンティティ で構成されます。
マスタ系(静的データ)・トランザクション系(業務データ)・締め系(日次運用データ)の 3 層に分類し、
フロントエンドの Jotai AppStore と
バックエンドの Cloudflare D1 データベースの両方を意識した設計です。
EMPLOYEE · TABLE
ORDER_ITEM · BILL · BILL_ITEM
全体ER図
全 10 エンティティのリレーションシップ。Mermaid.js によるレンダリング。
erDiagram
CATEGORY {
int id PK
string name
string slug UK
}
PRODUCT {
int product_id PK
int category_id FK
string product_name
int price
string description
boolean is_active
string image_url
string created_at
string updated_at
}
EMPLOYEE {
int employee_id PK
string name
string role
string email
string password_hash
string photo_url
string nearest_station
string created_at
}
TABLE {
int table_id PK
string label
int seats
}
VISITOR {
string visitor_id PK
int table_id FK
string visit_date
int visit_sequence
int party
string status
int staff_called
int bill_requested
string created_at
}
ORDER {
int order_id PK
string visitor_id FK
string order_time
string note
int total_amount
string status
string created_at
}
ORDER_ITEM {
int order_item_id PK
int order_id FK
int product_id FK
int quantity
int subtotal
string status
}
BILL {
int bill_id PK
string visitor_id FK
int employee_id FK
int subtotal_amount
int discount_amount
int tax_amount
int total_amount
string status
string payment_method
string void_reason
string voided_at
int voided_by FK
string created_at
}
BILL_ITEM {
int bill_item_id PK
int bill_id FK
string product_name
int unit_price
float tax_rate
int quantity
int subtotal
}
DAILY_CLOSE {
string date PK
string status
string closed_at
string memo
}
CATEGORY ||--o{ PRODUCT : "has"
PRODUCT ||--o{ ORDER_ITEM : "ordered as"
TABLE ||--o{ VISITOR : "seats"
VISITOR ||--o{ ORDER : "places"
VISITOR ||--o| BILL : "settled as"
ORDER ||--|{ ORDER_ITEM : "contains"
BILL ||--|{ BILL_ITEM : "contains"
EMPLOYEE ||--o{ BILL : "handles"
EMPLOYEE ||--o{ BILL : "voids"
ドリップ・エスプレッソ・ティー・フード・デザートなど商品分類を管理。スラッグは URL フレンドリーな一意識別子。
| 属性名 | 型 | 制約 | 説明 |
|---|---|---|---|
| id | integer | PK | 自動採番 ID |
| name | string | NOT NULL | カテゴリ表示名(例: コーヒー) |
| slug | string | UK NOT NULL | URL 用識別子(例: coffee)。重複不可 |
メニュー商品を管理。カテゴリに所属し、POS 画面での注文・売上ランキングの基礎データとなる。価格変更は BILL_ITEM のスナップショットには影響しない。
| 属性名 | 型 | 制約 | 説明 |
|---|---|---|---|
| product_id | integer | PK | 自動採番 ID |
| category_id | integer | FK NOT NULL 論理参照 | CATEGORY.id への論理参照(DB の FK 制約なし)。0 は「未分類」を表す(カテゴリ削除時にリセットされる) |
| product_name | string | NOT NULL | 商品名(例: カフェラテ) |
| price | integer | NOT NULL ≥ 0 | 税抜き価格(円) |
| description | string | nullable | 商品説明文 |
| is_active | integer | NOT NULL | 有効フラグ(0 / 1)。0 の商品は注文不可 |
| image_url | string | nullable | 商品画像 URL(R2 オブジェクトキー) |
| created_at | string | NOT NULL | 登録日時(ISO 8601) |
| updated_at | string | NOT NULL | 更新日時(ISO 8601) |
店舗スタッフを管理。ロール(店長 / バリスタ / ホール)によって操作権限が異なる。会計時の担当者として BILL に紐づく。
| 属性名 | 型 | 制約 | 説明 |
|---|---|---|---|
| employee_id | integer | PK | 自動採番 ID |
| name | string | NOT NULL | スタッフ氏名 |
| role | string | NOT NULL | 役割: 店長 / バリスタ / ホール |
| string | UK nullable | ログイン用メールアドレス(一意) | |
| password_hash | string | nullable | パスワードハッシュ(PBKDF2-SHA256)。API レスポンスには一切含めない |
| photo_url | string | nullable | プロフィール写真 URL(R2) |
| nearest_station | string | nullable | 最寄り駅(勤怠参考情報) |
| created_at | string | NOT NULL | 登録日(ISO 8601) |
フロアのテーブル・カウンター席を定義。VISITOR 作成時に紐づき、フロアプランで視覚化される。
| 属性名 | 型 | 制約 | 説明 |
|---|---|---|---|
| table_id | integer | PK | 自動採番 ID |
| label | string | NOT NULL | 表示名(例: T1、C1) |
| seats | integer | NOT NULL ≥ 1 | 最大着席人数 |
お客様の 1 来店を表すエンティティ。着席時に生成され、会計完了後は status = checked_out で論理削除される(行は残る)。
visitor_id は
v_YYYYMMDD_NNN 形式の文字列。
ORDER と BILL の結合軸となる中核エンティティ。
| 属性名 | 型 | 制約 | 説明 |
|---|---|---|---|
| visitor_id | string | PK | v_YYYYMMDD_NNN 形式(例: v_20260526_003) |
| table_id | integer | FK NOT NULL | TABLE.table_id への外部キー |
| visit_date | string | NOT NULL | 来店日(YYYY-MM-DD) |
| visit_sequence | integer | NOT NULL ≥ 1 | 当日の来店通し番号(NNN) |
| party | integer | NOT NULL ≥ 1 | 人数 |
| status | string | NOT NULL | seated(着席中) / checked_out(退席・会計済み、論理削除) |
| staff_called | integer | NOT NULL | スタッフ呼び出しフラグ(0 / 1)。POS のポーリングで全デバイスに伝播 |
| bill_requested | integer | NOT NULL | 会計依頼フラグ(0 / 1)。精算完了時に自動クリア |
| created_at | string | NOT NULL | 着席日時(ISO 8601) |
POS から送信される 1 回の注文。同一 VISITOR が複数回注文可能(追加注文)。ステータスが served になると調理完了。
| 属性名 | 型 | 制約 | 説明 |
|---|---|---|---|
| order_id | integer | PK | 自動採番 ID |
| visitor_id | string | FK NOT NULL | VISITOR.visitor_id への外部キー |
| order_time | string | NOT NULL | 注文時刻(HH:MM) |
| note | string | nullable | 注文時の備考(例: アイスで) |
| total_amount | integer | NOT NULL | ORDER_ITEM の subtotal 合計(円) |
| status | string | NOT NULL | pending(受付) / preparing(調理中) / served(提供完了)。この順にのみ遷移可(逆行・スキップ不可) |
| created_at | string | NOT NULL | 注文受付日時(ISO 8601) |
ORDER に含まれる 1 商品ライン。PRODUCT への参照を持ち、キッチン画面でのステータス管理の単位となる。
| 属性名 | 型 | 制約 | 説明 |
|---|---|---|---|
| order_item_id | integer | PK | 自動採番 ID |
| order_id | integer | FK NOT NULL | ORDER.order_id への外部キー |
| product_id | integer | FK NOT NULL | PRODUCT.product_id への外部キー |
| quantity | integer | NOT NULL ≥ 1 | 注文数量 |
| subtotal | integer | NOT NULL | price × quantity(円) |
| status | string | NOT NULL | pending(未提供) / served(提供完了) |
1 来店の清算を表す。VISITOR と 1 対 0..1 の関係(清算前は存在しない)。
会計確定後も VISITOR・ORDER は論理削除(status 更新)で残るが、商品の価格・名称変更に影響されないよう売上明細は BILL / BILL_ITEM にスナップショットとして永続化される。
消費税は (subtotal - discount) × 0.10 で計算。
| 属性名 | 型 | 制約 | 説明 |
|---|---|---|---|
| bill_id | integer | PK | 自動採番 ID(uint32) |
| visitor_id | string | FK 論理参照 | 会計時点の visitor_id を記録。会計履歴の不変性を優先し、DB 外部キー制約は張らない論理参照とする(schema 上も REFERENCES 未指定) |
| employee_id | integer | FK NOT NULL | 会計担当 EMPLOYEE.employee_id |
| subtotal_amount | integer | NOT NULL | 値引き前小計(円) |
| discount_amount | integer | NOT NULL | 値引き額(円・0 以上) |
| tax_amount | integer | NOT NULL | 消費税額(円・端数は四捨五入) |
| total_amount | integer | NOT NULL | 税込み請求合計(円) |
| status | string | NOT NULL | settled(確定) / voided(取消) |
| payment_method | string | NOT NULL | cash / credit |
| void_reason | string | nullable | 取消理由(status = voided 時のみ設定) |
| voided_at | string | nullable | 取消日時(ISO 8601) |
| voided_by | integer | FK nullable | 取消実行者 EMPLOYEE.employee_id(status = voided 時のみ設定) |
| created_at | string | NOT NULL | 会計確定日時(ISO 8601) |
BILL に含まれる商品明細。product_name・unit_price はスナップショットとして保持するため、 後から PRODUCT の価格を変更しても過去の会計履歴には影響しない。 ORDER_ITEM を集約(同一商品は合算)して生成される。
| 属性名 | 型 | 制約 | 説明 |
|---|---|---|---|
| bill_item_id | integer | PK | 自動採番 ID |
| bill_id | integer | FK NOT NULL | BILL.bill_id への外部キー |
| product_name | string | NOT NULL | スナップショット: 会計時点の商品名 |
| unit_price | integer | NOT NULL | スナップショット: 会計時点の単価(円) |
| tax_rate | float | NOT NULL | 適用税率(例: 0.10) |
| quantity | integer | NOT NULL ≥ 1 | 合算数量 |
| subtotal | integer | NOT NULL | unit_price × quantity(円) |
1 営業日の締め状態を管理する運用テーブル。
日付を主キーとし、締め処理の実行で status = closed に更新される。
売上金額自体は保持せず(BILL から都度集計)、締め完了フラグと特記メモのみを記録する。
締め実行者(API レスポンスの closed_by)は JWT から取得して返却するのみで、このテーブルには永続化しない。
| 属性名 | 型 | 制約 | 説明 |
|---|---|---|---|
| date | string | PK | 対象営業日(YYYY-MM-DD) |
| status | string | NOT NULL | open(未締め) / closed(締め済み) |
| closed_at | string | nullable | 締め実行日時(ISO 8601) |
| memo | string | nullable | 特記事項(機材トラブル・廃棄ロス・引継ぎ事項など) |
| エンティティ A | カーディナリティ | エンティティ B | 説明 |
|---|---|---|---|
| CATEGORY | 1 ─── 0..* |
PRODUCT | 1 カテゴリに 0 件以上の商品が属する |
| PRODUCT | 1 ─── 0..* |
ORDER_ITEM | 1 商品が複数の注文明細で参照される(ライブ参照) |
| TABLE | 1 ─── 0..* |
VISITOR | 1 テーブルに同時に 0..1 来店、通算では複数来店 |
| VISITOR | 1 ─── 0..* |
ORDER | 1 来店で 0 件以上の注文(追加注文あり) |
| VISITOR | 1 ─── 0..1 |
BILL | 1 来店に最大 1 件の会計(清算後に VISITOR は論理削除 = checked_out) |
| ORDER | 1 ─── 1..* |
ORDER_ITEM | 1 注文に 1 件以上の明細(空注文は禁止) |
| BILL | 1 ─── 1..* |
BILL_ITEM | 1 会計に 1 件以上の明細(空会計は禁止) |
| EMPLOYEE | 1 ─── 0..* |
BILL | 1 スタッフが複数の会計を担当する(employee_id) |
| EMPLOYEE | 1 ─── 0..* |
BILL | 1 スタッフが複数の会計を取消す(voided_by・取消時のみ) |
BILL_ITEM はなぜ product_name・unit_price をスナップショット保持するのか
商品の価格や名称は将来変更される可能性がある。BILL_ITEM が PRODUCT への外部キーのみを保持する設計では、 価格変更後に過去の会計伝票を参照すると誤った金額が表示される。 そのため会計確定時点の product_name・unit_price・tax_rate を BILL_ITEM に複製(スナップショット) することで、 会計履歴の不変性を保証する。これは EC サイトの注文履歴と同様のパターン。
BILL_ITEM に category_id がない理由と日次締めでの対応
BILL_ITEM はスナップショット設計のため、あえて category_id を持たない(カテゴリ変更も不変性を崩さないため)。
日次締めのカテゴリ別集計では
BILL_ITEM.product_name →
PRODUCT.product_name
の名前逆引きで現在のカテゴリを推定する。
本番では BILL_ITEM に category_id / category_name をスナップショットとして追加し、名前逆引きに依存しない集計に切り替える設計を推奨する。
DAILY_CLOSE を独立テーブルにした理由
売上金額・件数・カテゴリ別内訳は BILL / BILL_ITEM から都度集計 するため、
DAILY_CLOSE に金額そのものは保持しない(二重管理を避け、取消反映漏れを防ぐ)。
DAILY_CLOSE が持つのは「その日を締めたか(status)」「締め時刻」「特記メモ」のみ。
日付を主キーにすることで ON CONFLICT(date) DO UPDATE による冪等な締め処理が可能。
売上集計の事前計算(マテビュー化)は将来データ量が増えた段階での最適化余地として残す。
VISITOR は会計後に論理削除(status = checked_out)
会計確定時、VISITOR を物理削除せず
UPDATE visitors SET status = 'checked_out'
で論理削除し、関連 ORDER は served に更新する。
フロアプランは status = 'seated' のみを「使用中」として描画するため、
論理削除だけで自動的に「空席」に戻る。
行を残すことで BILL.visitor_id から来店記録(テーブル・人数・来店時刻)を遡って参照でき、会計履歴と来店情報の突合が可能。
当日の来店通し番号(visit_sequence)も checked_out を含めて採番するため番号の連続性が保たれる。
visitor_id の採番規則(v_YYYYMMDD_NNN)
v_20260526_003 のように日付 + 当日通し番号とする。
これにより human-readable なうえ、日付フィルタリングを文字列前方一致で高速化 できる。
UUID を使わない理由は、POS スタッフが口頭確認する際「本日 3 番のお客様」と伝えやすいため。
来店数が 999 件を超える場合は 4 桁(0001〜9999)への拡張を推奨。
payment_method を BILL に直接保持する単純化
一般的には「支払い方法マスタ」テーブルを用意するが、本システムでは現金・クレジットの 2 種に限定されるため、
BILL.payment_method に文字列 enum(cash / credit)として直接保持する。
電子マネー・QR コード決済等を追加する際は PAYMENT_METHOD テーブルへの正規化を推奨。
金額はすべて整数(円)で保持する
price /
subtotal /
subtotal_amount /
discount_amount /
tax_amount /
total_amount
など金額系カラムはすべて integer 型(円単位・小数なし) で保持する。
日本円は最小単位が 1 円のため小数が不要で、浮動小数を使うと税計算で丸め誤差が蓄積する。
唯一 BILL_ITEM.tax_rate のみ float(0.10)だが、これは「率」であり金額ではない。
消費税は Math.round((subtotal - discount) × 0.10) で計算し、結果を整数に丸めて保存する。
合計金額・税額・担当者はサーバー側で確定する(クライアント値を信頼しない)
会計確定時、subtotal_amount は
対象 VISITOR の未会計 ORDER を DB から再集計して算出し、
クライアントが送信した合計値は一切採用しない。
税額・合計も同じくサーバー側で再計算する。
会計担当の employee_id もリクエストボディではなく
JWT ペイロードから取得し、他スタッフへのなりすまし計上を防ぐ。
これにより、改ざんされたリクエストが来ても DB 上の金額・担当者の整合性が保たれる(信頼境界をサーバーに置く設計)。