SALON BOARD クローン ERD (Mermaid)
SALON BOARD クローン ERD (Mermaid)
Section titled “SALON BOARD クローン ERD (Mermaid)”境界コンテキスト別に分割。テナント境界は全テーブル store_id 必須。FK制約は同一テナント内のみ(将来のRLS化を見据える)。graphile-worker の graphile_worker.* スキーマは別管理(自動マイグレーション)。
規約(詳細は親SRS §7.2 参照):
idは原則 UUIDv7(Postgresuuid型、16バイト、アプリ層で生成)- 例外的に
bigserialを許容するテーブル:- ログ系(
operator_action_log,reservation_event,message_delivery_log) - 中間テーブル(
customer_tag_link,role_permission) - 純内部マスタ(
business_hours,day_override,shift,cash_movement) - 条件:外部露出なし+件数漏れ無害+大量生成、の3条件を満たす場合のみ
- ログ系(
menu_staff_eligibility/menu_equipment_requirementは複合主キー(独立IDなし、親SRS §7.14)- 表示用ID(
customer_no、coupon_code、salon_slug等)は主キーと別列、店舗内unique - 金額は全て整数(税抜・税額・税込を分離、
bigint型) - タイムスタンプは
timestamptz - ソフトデリートは基本使わない(必要な箇所のみ
deleted_at) - 集計用カラム(
visit_count_cache等)は掲載しない(ビューまたはマテビューで算出) - 全テーブル
store_id必須、FK制約は同一テナント内のみ(RLS + 複合FK)、graphile-workerのgraphile_worker.*は別管理
1. テナント・認証コンテキスト
Section titled “1. テナント・認証コンテキスト”注:
OPERATORは単一店舗に縛られない(親SRS用語集 §2、§4.1)。1オペレーターは複数店舗に所属しうるため、店舗スコープはOPERATOR_STORE_LINK(M:N、role 紐付け)で表現する。role/role_permission/permission/operator_permission_overrideの詳細スキーマは SRS-TEN-001 §6.1 を参照(本図では関係概略のみ)。
erDiagram STORE ||--o{ OPERATOR_STORE_LINK : "scopes" OPERATOR ||--o{ OPERATOR_STORE_LINK : "scoped_to" STORE ||--o{ STORE_SETTING : "has" STORE ||--o{ ROLE : "defines" ROLE ||--o{ OPERATOR_STORE_LINK : "assigned_via" OPERATOR ||--o{ OPERATOR_SESSION : "has" OPERATOR ||--o{ PASSKEY_CREDENTIAL : "registers" OPERATOR ||--o{ TOTP_SECRET : "owns"
STORE { string id PK string code UK "H000551123相当" string name string timezone "Asia/Tokyo" string plan "free/paid" timestamptz created_at } STORE_SETTING { string store_id PK "FK" int reservation_slot_min "5/10/15/30" int hpb_slot_min "10/15/30" int net_accept_weeks "1..12" string net_cutoff_policy "json" string cancel_policy "json" boolean accept_no_nominate string customer_question_json } OPERATOR { string id PK string email UK string name boolean totp_enabled timestamptz last_login_at timestamptz created_at } OPERATOR_STORE_LINK { string operator_id PK "FK" string store_id PK "FK" string role_id FK "owner/manager/staff/receptionist" timestamptz created_at } ROLE { string id PK string store_id FK string key "owner/manager/staff/receptionist" string name boolean is_preset timestamptz created_at } OPERATOR_SESSION { string id PK string operator_id FK string token_hash UK timestamptz expires_at string ip string user_agent } PASSKEY_CREDENTIAL { string id PK string operator_id FK string credential_id UK bytea public_key bigint sign_count string transports timestamptz created_at } TOTP_SECRET { string operator_id PK "FK" string secret_encrypted timestamptz activated_at }2. マスタ(リソース)コンテキスト
Section titled “2. マスタ(リソース)コンテキスト”注:メニュー対応関係は親SRS §7.14 に従い
MENU_STAFF_ELIGIBILITY(スタッフ対応、ホワイトリスト)とMENU_EQUIPMENT_REQUIREMENT(設備要件)に分離する。MENU.restrict_staff_requiredは仮置き名で命名は SRS-MST-002 で確定。STAFFの確定スキーマは SRS-MST-001 が所有(本図では業務上の関係を示すスケッチに留める)。
erDiagram STORE ||--o{ STAFF : "employs" STORE ||--o{ EQUIPMENT : "owns" STORE ||--o{ MENU : "offers" STORE ||--o{ BUSINESS_HOURS : "opens" STORE ||--o{ DAY_OVERRIDE : "overrides" STORE ||--o{ SHIFT : "schedules" STAFF ||--o{ SHIFT : "works" MENU ||--o{ MENU_STAFF_ELIGIBILITY : "whitelists" STAFF ||--o{ MENU_STAFF_ELIGIBILITY : "performs" MENU ||--o{ MENU_EQUIPMENT_REQUIREMENT : "requires" EQUIPMENT ||--o{ MENU_EQUIPMENT_REQUIREMENT : "satisfies"
STAFF { string id PK string store_id FK string name boolean is_bookable int display_order timestamptz terminated_at "離職" timestamptz created_at timestamptz updated_at } EQUIPMENT { string id PK string store_id FK string name boolean bookable int capacity "同時予約可能数" int display_order } MENU { string id PK string store_id FK string category "フェイシャル/ボディ/店販..." string name int duration_min int price_excl_tax int tax_rate_bp "1000=10%" boolean restrict_staff_required "仮置き、§7.14 / SRS-MST-002 で確定" boolean active } MENU_STAFF_ELIGIBILITY { string store_id PK "FK" string menu_id PK "FK" string staff_id PK "FK" } MENU_EQUIPMENT_REQUIREMENT { string store_id PK "FK" string menu_id PK "FK" string equipment_id PK "FK" } BUSINESS_HOURS { string id PK string store_id FK int weekday "0=Mon..6=Sun, 7=祝" time open_at time close_at int capacity boolean closed } DAY_OVERRIDE { string id PK string store_id FK date target_date time open_at time close_at int capacity boolean closed string memo } SHIFT { string id PK string store_id FK string staff_id FK date work_date time start_at time end_at string breaks_json }3. 顧客(CRM)コンテキスト
Section titled “3. 顧客(CRM)コンテキスト”erDiagram STORE ||--o{ CUSTOMER : "owns" CUSTOMER ||--o{ CUSTOMER_NOTE : "has" CUSTOMER ||--o{ CUSTOMER_TAG_LINK : "tagged" STORE ||--o{ CUSTOMER_TAG : "defines" CUSTOMER_TAG ||--o{ CUSTOMER_TAG_LINK : "used_by"
CUSTOMER { string id PK string store_id FK string name_kanji string name_kana date birthday string gender "F/M/NA" string tel string email string occupation boolean post_mail_opt_in boolean watch_flag "要注意" string preferred_staff_id boolean direct_member timestamptz created_at timestamptz deleted_at } CUSTOMER_NOTE { string id PK string customer_id FK string type "memo/sketch/karte" string body string sketch_url string author_operator_id timestamptz created_at } CUSTOMER_TAG { string id PK string store_id FK string name string color } CUSTOMER_TAG_LINK { string customer_id FK string tag_id FK }4. 予約・会計コンテキスト(コア)
Section titled “4. 予約・会計コンテキスト(コア)”予約の状態遷移は RESERVATION_EVENT に時系列で記録する(イベントソース)。後続の分析・リピート率・キャンセル発生タイミング集計で必須。
erDiagram STORE ||--o{ RESERVATION : "accepts" CUSTOMER ||--o{ RESERVATION : "books" STAFF ||--o{ RESERVATION : "assigned_to" EQUIPMENT ||--o{ RESERVATION : "uses" RESERVATION ||--o{ RESERVATION_MENU : "includes" RESERVATION ||--o{ RESERVATION_EVENT : "transitions" RESERVATION ||--o| VISIT : "checks_out_as" VISIT ||--o{ VISIT_LINE : "composes" MENU ||--o{ RESERVATION_MENU : "snapshotted"
RESERVATION { string id PK string store_id FK string customer_id FK string staff_id FK string equipment_id FK string code UK "人間可読" string channel_code "phone_own/phone_hpb/hpb/direct/walkin" int status "1..11" boolean confirmed date visit_date timestamptz start_at timestamptz end_at int total_excl_tax int total_tax int cancel_fee string cancel_reason timestamptz cancelled_at string note string external_ref "HPB等" timestamptz created_at timestamptz updated_at } RESERVATION_MENU { string id PK string reservation_id FK string menu_id FK string name_snapshot int duration_min int price_excl_tax int tax_rate_bp } RESERVATION_EVENT { string id PK string reservation_id FK int from_status int to_status string actor_type "system/operator/customer" string actor_id string reason timestamptz occurred_at } VISIT { string id PK string reservation_id FK "UK" timestamptz checkin_at timestamptz checkout_at int gross_excl_tax int discount int tax int net string payment_type "cash/card/smartpay/other" timestamptz settled_at } VISIT_LINE { string id PK string visit_id FK string type "service/goods/option/discount" string source_menu_id string name_snapshot int unit_price_excl_tax int quantity int amount_excl_tax int tax_rate_bp }5. レジ締め・キャッシュフロー
Section titled “5. レジ締め・キャッシュフロー”erDiagram STORE ||--o{ REGISTER_CLOSING : "closes" STORE ||--o{ CASH_MOVEMENT : "records" REGISTER_CLOSING ||--o{ CASH_MOVEMENT : "groups"
REGISTER_CLOSING { string id PK string store_id FK date closing_date UK int expected_cash int counted_cash int diff string operator_id string memo timestamptz closed_at } CASH_MOVEMENT { string id PK string store_id FK string closing_id FK string type "in/out" int amount string category "opening/sales_cash/refund/withdraw/expense" string memo timestamptz occurred_at }6. メッセージ配信コンテキスト
Section titled “6. メッセージ配信コンテキスト”erDiagram STORE ||--o{ MESSAGE : "sends" STORE ||--o{ AUTO_MESSAGE_RULE : "defines" STORE ||--o{ COUPON : "issues" AUTO_MESSAGE_RULE ||--o{ MESSAGE : "spawns" COUPON ||--o{ MESSAGE : "attached_to" MESSAGE ||--o{ MESSAGE_RECIPIENT : "delivers_to" CUSTOMER ||--o{ MESSAGE_RECIPIENT : "receives"
MESSAGE { string id PK string store_id FK string kind "manual/auto" string title string body string audience_filter_json string coupon_id FK string auto_rule_id FK timestamptz scheduled_at timestamptz sent_at int delivered_count int failed_count } MESSAGE_RECIPIENT { string id PK string message_id FK string customer_id FK string status "queued/sent/bounced/failed" timestamptz delivered_at string error } AUTO_MESSAGE_RULE { string id PK string store_id FK string trigger_event "reservation_confirmed/visit_completed/no_visit_days..." int offset_hours "負数=来店前" string title string body_template string coupon_id boolean enabled } COUPON { string id PK string store_id FK string name string scope "hpb/direct/both" int discount_amount int discount_bp "1000=10%" string condition_json date valid_from date valid_to int max_uses }7. スマート支払い・精算コンテキスト
Section titled “7. スマート支払い・精算コンテキスト”erDiagram STORE ||--o{ SMART_PAY_TXN : "receives" RESERVATION ||--o{ SMART_PAY_TXN : "settled_by" STORE ||--o{ POINT_LEDGER : "accrues" STORE ||--o{ SETTLEMENT : "receives_payout"
SMART_PAY_TXN { string id PK string store_id FK string reservation_id FK string provider "stripe/gmo/komoju" string provider_txn_id UK int auth_amount int captured_amount int refund_amount int cancel_fee_amount string status "authorized/captured/refunded/cancelled/failed" timestamptz authorized_at timestamptz captured_at } POINT_LEDGER { string id PK string store_id FK string year_month UK "YYYYMM" int earned int redeemed int settle_amount string status "pending/confirmed/paid" timestamptz settled_at } SETTLEMENT { string id PK string store_id FK string year_month string kind "smartpay/point/paygfee" int gross int fee int net string status timestamptz paid_at }8. 掲載コンテンツ(媒体連携)コンテキスト
Section titled “8. 掲載コンテンツ(媒体連携)コンテキスト”erDiagram STORE ||--o{ SALON_LISTING : "publishes" SALON_LISTING ||--o{ STAFF_LISTING : "features" SALON_LISTING ||--o{ MENU_LISTING : "features" SALON_LISTING ||--o{ PHOTO_GALLERY : "features" STAFF ||--o| STAFF_LISTING : "represented_by" MENU ||--o| MENU_LISTING : "represented_by" STORE ||--o{ SALON_KODAWARI : "has" KODAWARI_TAG ||--o{ SALON_KODAWARI : "applied_to" STORE ||--o{ SALON_FEATURE_ENTRY : "entries" FEATURE ||--o{ SALON_FEATURE_ENTRY : "hosts" STORE ||--o{ BLOG_POST : "writes" STAFF ||--o{ BLOG_POST : "authors" STORE ||--o{ REVIEW : "receives" RESERVATION ||--o| REVIEW : "reviewed_as"
SALON_LISTING { string id PK string store_id FK string plan_code string plan_name date period_from date period_to jsonb draft jsonb published timestamptz published_at } STAFF_LISTING { string id PK string store_id FK string staff_id FK string listing_id FK string catch_copy string profile string specialty jsonb photos } MENU_LISTING { string id PK string store_id FK string menu_id FK string listing_id FK string public_name string price_display string description } PHOTO_GALLERY { string id PK string store_id FK string listing_id FK string category "hair/nail/interior/before_after" string url string caption int display_order } KODAWARI_TAG { string id PK string name string category } SALON_KODAWARI { string store_id FK string tag_id FK } FEATURE { string id PK string name date period_from date period_to } SALON_FEATURE_ENTRY { string id PK string store_id FK string feature_id FK string status "draft/submitted/approved" } BLOG_POST { string id PK string store_id FK string author_staff_id string title string body_rich string cover_image string category string status "draft/scheduled/published/archived" timestamptz scheduled_at timestamptz published_at int view_count int like_count } REVIEW { string id PK string store_id FK string customer_id FK string reservation_id FK int rating "1..5" string body timestamptz posted_at string reply timestamptz replied_at string replied_by_staff_id string moderation_status "visible/hidden/reported" }補足: マルチテナント運用の規約
Section titled “補足: マルチテナント運用の規約”- 全テーブルに
store_id。ただし純粋なマスタ(KODAWARI_TAG,FEATURE)は例外。 - FK制約はテナント内のみ を PostgreSQL のチェック制約で強制。
→ 複合FKで「違うテナントのリソースを参照できない」をDBレベルで保証。ALTER TABLE reservationADD CONSTRAINT fk_customer_same_tenantFOREIGN KEY (store_id, customer_id)REFERENCES customer (store_id, id);
- RLSポリシー(Postgres移行/本番化のタイミングで有効化):
ALTER TABLE reservation ENABLE ROW LEVEL SECURITY;CREATE POLICY tenant_isolation ON reservationUSING (store_id = current_setting('app.current_store_id'));
- アプリ層ではリクエスト開始時に
SET LOCAL app.current_store_idを必ず呼ぶ。 - graphile-worker のジョブペイロードにも
storeIdを必ず含める。ワーカー側でもテナントコンテキストを復元してからクエリを実行。
補足: 予約ステータスと状態遷移
Section titled “補足: 予約ステータスと状態遷移”stateDiagram-v2 [*] --> 仮予約確定待ち : ネット予約受信 [*] --> 受付待ち : 電話/直接登録 仮予約確定待ち --> 受付待ち : サロンが確定 仮予約確定待ち --> お断り : サロンが拒否 仮予約確定待ち --> 自動キャンセル : 期限切れ 仮予約確定待ち --> お客様キャンセル : 客都合 受付待ち --> 施術中 : チェックイン 受付待ち --> お客様キャンセル : 客都合 受付待ち --> サロンキャンセル : サロン都合 受付待ち --> 無断キャンセル : 来店せず 施術中 --> 来店処理待ち : 施術完了 来店処理待ち --> 会計済み : レジ会計 会計済み --> [*] お断り --> [*] お客様キャンセル --> [*] サロンキャンセル --> [*] 無断キャンセル --> [*] 自動キャンセル --> [*]補足: graphile-worker が使うジョブの例
Section titled “補足: graphile-worker が使うジョブの例”予約ドメインから派生する非同期タスクの棚卸し。全ペイロードに storeId を含める。
| タスク名 | 起点 | ペイロード | 遅延 |
|---|---|---|---|
send-reservation-reminder | 予約確定時にenqueue | {storeId, reservationId} | visitDate - 2日 |
auto-cancel-expired-reservation | 予約作成時にenqueue | {storeId, reservationId} | 受付期限到達時 |
charge-cancel-fee-smartpay | ステータス変更時 | {storeId, reservationId} | 即時 |
send-message-batch | 配信予約時 | {storeId, messageId} | scheduled_at |
auto-message-trigger-scan | cron: 5分毎 | なし | - |
aggregate-daily-sales | cron: 日次 3:00 | なし | - |
settle-monthly-points | cron: 月次 1日 4:00 | なし | - |
export-reservations-csv | ユーザー操作 | {storeId, operatorId, filter, password} | 即時 |
resize-gallery-photo | アップロード時 | {storeId, photoId} | 即時 |
cleanup-old-sessions | cron: 日次 2:00 | なし | - |