アクティビティログ & ゲーミフィケーション Activity logs & gamification

ユーザーの行動ログ(user_activity_logs)と経験値/レベル/バッジ (user_exp / level_definitions / user_badges)の 配線、および各 API がどのアクティビティを記録するかの一次情報。

Source-of-truth documentation for user activity logs (user_activity_logs) and the gamification pipeline (user_exp / level_definitions / user_badges), including which API calls emit which activity.

SSoT 2 層構成: Two-layer SSoT:
  • 型と metadata スキーマ: api/src/lib/activity-types.ts(TypeScript の ActivityType 型 + 各種 Zod metadata スキーマ)
  • Type & metadata schema: api/src/lib/activity-types.ts (the TS ActivityType union + per-type Zod metadata schemas).
  • 付与ロジック: PG 関数 public.award_user_activityinfra/supabase/migrations/035_award_user_activity.sql
  • Awarding logic: PG function public.award_user_activity (infra/supabase/migrations/035_award_user_activity.sql).
  • BFF から呼ぶ: lib/activity.tsrecordActivity / recordActivityBestEffort が TypeScript で型付き呼出+実行時 Zod 検証
  • BFF entry: recordActivity / recordActivityBestEffort in lib/activity.ts — type-safe call with runtime Zod validation.
  • クライアントが参照: GET /v1/meta/activity-types が全種別と metadata JSON Schema を返す
  • Client view: GET /v1/meta/activity-types returns every type with its metadata JSON Schema.
2026-04-20 リネーム: Rename on 2026-04-20: 命名を BFF 側の短い形に統一した。 parking_complete → parking_endsave_parking → parking_savedvehicle_register → vehicle_added。 既存データは 036_rename_activity_types.sql で一括変換済み。 Canonical names were aligned with the BFF short form: parking_complete → parking_end, save_parking → parking_saved, vehicle_register → vehicle_added. Existing data was migrated via 036_rename_activity_types.sql.

1. テーブル構成1. Tables

テーブル Table 役割 Role キー Key
user_activity_logsuser_activity_logs アクション履歴の生ログ(1 行 = 1 イベント) Raw event log (one row per user action) (id) / idx: user_id, activity_type, created_at (id) / idx: user_id, activity_type, created_at
user_activity_log_targetsuser_activity_log_targets ログ行が指す対象エンティティ(1:N) Entities the log row refers to (1:N) (activity_log_id, entity_type, entity_id) (activity_log_id, entity_type, entity_id)
user_activity_countsuser_activity_counts (user, type) 単位の累計カウンタ Per-(user, type) running counter UNIQUE(user_id, activity_type) UNIQUE(user_id, activity_type)
activity_exp_rulesactivity_exp_rules (activity_type → EXP 量) の対応表 Map from activity_type to EXP amount is_active のみ使用 Only is_active=true rows apply
user_expuser_exp ユーザーの累計 EXP と現在レベル Cumulative EXP and current level UNIQUE(user_id) UNIQUE(user_id)
level_definitionslevel_definitions レベルカーブ(level, required_exp Level curve (level, required_exp) (level) (level)
badge_definitionsbadge_definitions バッジマスタ: activity_type, threshold, conditions JSONB Badge master: activity_type, threshold, conditions JSONB (id) (id)
user_badge_progressuser_badge_progress バッジ進捗カウンタ(threshold 到達まで) Per-badge progress counter up to threshold UNIQUE(user_id, badge_id) UNIQUE(user_id, badge_id)
user_badgesuser_badges 獲得済みバッジ Earned badges UNIQUE(user_id, badge_id) UNIQUE(user_id, badge_id)

2. 付与フロー(award_user_activity2. Awarding flow (award_user_activity)

1 トランザクションで以下の 6 段を実行する。戻り値 JSON にレベルアップ/バッジ獲得情報を含めるので、 BFF はこれをそのまま UI に渡してユーザーへ即時フィードバックできる。

The RPC performs the 6 stages below in a single transaction and returns a JSON describing level-ups and newly earned badges, which the BFF passes straight to the UI for real-time feedback.

1. INSERT user_activity_logs (user_id, activity_type, metadata, created_at)
2. INSERT user_activity_log_targets × targets[]
3. UPSERT user_activity_counts  (+1, last_at = now)
4. EXP (if activity_exp_rules has an active row for this type):
     UPSERT user_exp.total_exp += exp_amount
     → recompute level from level_definitions
     → level_up = (level_after > level_before)
5. BADGES (for each active badge_definitions where activity_type matches):
     if evaluate_badge_conditions(metadata, conditions):
       UPSERT user_badge_progress.count += 1
       if count >= threshold:
         INSERT user_badges ON CONFLICT DO NOTHING
         → newly earned row counted in badges_earned[]
6. RETURN { activity_log_id, exp_granted, total_exp,
            level_before, level_after, level_up, badges_earned[] }

戻り値スキーマReturn schema

{
  "activity_log_id": "018f...",
  "exp_granted":     10,
  "total_exp":       1230,
  "level_before":    4,
  "level_after":     5,
  "level_up":        true,
  "badges_earned": [
    { "id":"...", "name":"駐車マスター I", "icon":"🅿️",
      "category":"feature_use", "description":"初めての finalize" }
  ]
}

3. activity_type カタログ3. activity_type catalog

BFF 側で投げる活動種別はすべてここに列挙する。 activity_exp_rules / badge_definitions で扱う activity_type と必ず一致させる。命名は英語 snake_case。

Every activity_type emitted by the BFF is listed here, kept in sync with activity_exp_rules / badge_definitions. Names are English snake_case.

activity_type 発火元(API / RPC) Emitted by metadata の代表フィールド Key metadata fields targets targets
parking_start POST /v1/parking-sessions → RPC create_parking_session POST /v1/parking-sessions → RPC create_parking_session session_id, parking_lot_id session_id, parking_lot_id parking_lot, parking_session parking_lot, parking_session
parking_end POST /v1/parking-sessions/{id}/finalize → RPC finalize_parking_session POST /v1/parking-sessions/{id}/finalize → RPC finalize_parking_session session_id, parking_lot_id, total_amount, duration_minutes session_id, parking_lot_id, total_amount, duration_minutes parking_lot, parking_session parking_lot, parking_session
parking_cancel POST /v1/parking-sessions/{id}/cancel → RPC cancel_parking_session POST /v1/parking-sessions/{id}/cancel → RPC cancel_parking_session session_id, parking_lot_id session_id, parking_lot_id parking_session parking_session
parking_saved POST /v1/me/saved-parking-lots POST /v1/me/saved-parking-lots parking_lot_id parking_lot_id parking_lot parking_lot
vehicle_added POST /v1/me/vehicles POST /v1/me/vehicles vehicle_id, vehicle_type vehicle_id, vehicle_type user_vehicle user_vehicle
sponsor_checkin POST /v1/sponsors/{id}/checkin POST /v1/sponsors/{id}/checkin sponsor_id, checkin_id sponsor_id, checkin_id area_sponsor area_sponsor
error_reported POST /v1/error-reports POST /v1/error-reports report_id, report_type, parking_lot_id report_id, report_type, parking_lot_id parking_lot(任意) parking_lot (optional)
review_post POST /v1/me/reviews POST /v1/me/reviews review_id, parking_lot_id, rating review_id, parking_lot_id, rating parking_lot, parking_review parking_lot, parking_review
rating POST /v1/me/parking-lots/{id}/rating(初回 INSERT のみ emit) POST /v1/me/parking-lots/{id}/rating (emit only on first INSERT) parking_lot_id, value(good|bad) parking_lot_id, value(good|bad) parking_lot parking_lot
ai_search POST /v1/search/ai POST /v1/search/ai session_id, message session_id, message
save_search_condition POST /v1/me/search-presets POST /v1/me/search-presets condition_id condition_id user_search_preset user_search_preset
login middleware requireUser(KV で 1 日 1 回 throttle) middleware requireUser (1×/day KV-throttled)
night_parking RPC finalize_parking_session の派生(JST 0-5 時) RPC finalize_parking_session derived (JST 0-5 h) session_id session_id parking_session parking_session
unique_parking RPC finalize_parking_session の派生(初回訪問時) RPC finalize_parking_session derived (first visit) parking_lot_id parking_lot_id parking_lot parking_lot
account_withdrawn RPC withdraw_account RPC withdraw_account

※ 上表は人間可読のビュー。正本は api/src/lib/activity-types.tsactivityTypes 定数。 emitted=false の種別(search, share, fare_simulation, app_open, consecutive_day, ev_charge)は seed / バッジ定義には存在するが BFF 側 emit は未配線(対応する UI / エンドポイントが未実装)。

This table is a human-readable view. The source of truth is the activityTypes const in api/src/lib/activity-types.ts. Types marked emitted=false (search, share, fare_simulation, app_open, consecutive_day, ev_charge) exist in seeds and badge definitions but are not yet wired on the BFF (the corresponding UI / endpoints are not implemented yet).

3-1. クライアント向け API: GET /v1/meta/activity-types3-1. Client-facing API: GET /v1/meta/activity-types

管理ポータル / モバイルクライアントが各 activity_type の metadata JSON Schema を動的に取得するための パブリックエンドポイント。admin UI でのフォーム自動生成や、集計画面のラベル引きに使う。

Public endpoint for admin portal / mobile clients to fetch each activity type's metadata JSON Schema at runtime. Useful for auto-generating admin forms and resolving labels in analytics views.

GET /v1/meta/activity-types

{
  "items": [
    {
      "type":        "parking_end",
      "description": "駐車を完了(finalize)",
      "category":    "session",
      "emitted_by":  "POST /v1/parking-sessions/{id}/finalize → RPC finalize_parking_session",
      "emitted":     true,
      "metadata_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type":    "object",
        "properties": {
          "session_id":       { "type": "string", "format": "uuid" },
          "parking_lot_id":   { "type": "string", "format": "uuid" },
          "total_amount":     { "type": "integer", "minimum": 0 },
          "duration_minutes": { "type": "integer", "minimum": 0 },
          "area":             { "type": "string" }
        },
        "required": ["session_id", "parking_lot_id", "total_amount", "duration_minutes"]
      }
    },
    ...
  ]
}

metadata_schema は JSON Schema 7 形式(zod-to-json-schemaactivityTypes の Zod スキーマから自動生成)。Cache-Control: public, max-age=300, s-maxage=3600 で 5 分キャッシュ。

metadata_schema is JSON Schema 7 (auto-generated from the Zod schemas in activityTypes via zod-to-json-schema). Cached for 5 min (Cache-Control: public, max-age=300, s-maxage=3600).

新しい activity_type を増やす時: When adding a new activity_type:
  1. SSoT を更新: api/src/lib/activity-types.tsactivityTypes に行を追加(description / category / emittedBy / metadataSchema)
  2. Update the SSoT: add an entry to activityTypes in api/src/lib/activity-types.ts (description / category / emittedBy / metadataSchema).
  3. 本表(docs)に行を追加
  4. Add a row to this docs table.
  5. EXP 付与したいなら activity_exp_rules に INSERT(admin 画面 /admin/gamification から可能)
  6. If EXP is awarded, insert an activity_exp_rules row (via the admin UI at /admin/gamification).
  7. バッジを作りたいなら badge_definitions に INSERT
  8. For badges, add a badge_definitions row.
  9. API 側で recordActivityBestEffort({ activityType: "..." }) を呼ぶ
  10. Call recordActivityBestEffort({ activityType: "..." }) in the route handler.

4. BFF からの呼び方4. Calling from the BFF

import { recordActivityBestEffort } from "../lib/activity";

// 主処理が成功してから呼ぶ(RPC エラーで UI を落とさない fire-and-forget)
await recordActivityBestEffort({
  sql,
  userId,
  activityType: "parking_saved",
  metadata: { parking_lot_id: lotId },
  targets: [{ entity_type: "parking_lot", entity_id: lotId }],
  logger: c.var.log,
});

// レベルアップ / バッジ獲得を UI に返したい場合は戻り値版
import { recordActivity } from "../lib/activity";
const reward = await recordActivity({ sql, userId, activityType: "parking_end", metadata, targets });
if (reward.level_up) {
  // クライアントに level_after / badges_earned を返す
}

駐車セッション系の返却値Parking session response payload

finalize_parking_session は 035 以降、応答の reward フィールドに { exp_granted, total_exp, level_before, level_after, level_up, badges_earned[] } を同梱する。idempotent=true(2 回目以降)の場合は exp_granted=0 で空のペイロードになる(重複付与を防ぐため)。

From migration 035 onwards, finalize_parking_session includes a reward field in its response carrying { exp_granted, total_exp, level_before, level_after, level_up, badges_earned[] }. Idempotent (repeat) calls return an empty reward (exp_granted=0) to avoid duplicate grants.

5. バッジ条件 DSL5. Badge condition DSL

badge_definitions.conditions は以下の JSONB 配列。各要素で log_metadata の値を比較し、**全条件を AND** で評価する。

badge_definitions.conditions is a JSONB array of predicates evaluated against the activity's log_metadata. All predicates are AND-ed.

[
  { "field": "duration_minutes", "operator": "gte", "value": "60" },
  { "field": "parking_lot_id",   "operator": "eq",  "value": "018f..." }
]

サポート演算子: eq, neq, contains, gt, gte, lt, lte, in(カンマ区切り)。ネストは "a.b.c" のドット記法で走査。

Supported operators: eq, neq, contains, gt, gte, lt, lte, in (comma-separated). Nested metadata is accessed with dotted paths ("a.b.c").

6. 管理者アクティビティログ6. Admin activity log

管理者操作の監査ログは別テーブル admin_activity_logs。ユーザー側ゲーミフィケーションとは 独立した仕組みだが、BFF 側の配線はユーザーアクティビティと対称的に設計している。

Admin audit trails live in the separate admin_activity_logs table. Independent of user gamification, but wired symmetrically to the user-activity helper on the BFF side.

6-1. BFF 側の共通ヘルパー6-1. BFF helper

/v1/admin/* の state-changing ハンドラ(POST/PATCH/PUT/DELETE)は、 主処理が成功した直後に recordAdminActivityBestEffort を呼んで監査ログを書き込む。 失敗は fire-and-forget で warn ログに残し、主処理はブロックしない。

Every /v1/admin/* state-changing handler (POST/PATCH/PUT/DELETE) calls recordAdminActivityBestEffort right after the main mutation succeeds. Failures are fire-and-forget (warn log only) so audit never blocks the main flow.

import { recordAdminActivityBestEffort } from "../../lib/admin-activity";

// ルートハンドラ内、主処理成功の直後
await recordAdminActivityBestEffort({
  sql,
  logger: c.var.log,
  adminId: c.get("adminId")!,
  action: "parking_lot.update",     // <resource>.<verb> 形式(SSoT: 下記)
  targetType: "parking_lot",         // DB テーブル名と揃える
  targetId: lot.id,
  targetLabel: lot.name,             // 監査 UI での可読性
  metadata: { diff: body },          // PATCH なら入力 body を diff として保存
});

配置は必ず try ブロック内・主処理の成功経路のみ(ロールバック時に偽陽性ログを出さない)。 admin_email_snapshot はヘルパー内のサブクエリで自動取得されるので呼出側で渡す必要はない。

Place only inside the try block on the success path (so a rollback doesn't leave a false-positive log). admin_email_snapshot is resolved by a sub-query inside the helper, so callers do not need to pass it.

6-2. action 命名規約6-2. action naming

<resource>.<verb> の英小文字スネークケース。`verb` は CRUD の create / update / delete を基本とし、業務固有操作は専用 verb を立てる。

<resource>.<verb> lowercase snake_case. Core verbs: create / update / delete; use dedicated verbs for business-specific operations.

リソース Resource 主な action Main actions
adminadmin create, update, delete, reset_password, update_notif_prefs create, update, delete, reset_password, update_notif_prefs
rolerole create, update, delete, update_permissions create, update, delete, update_permissions
parking_lotparking_lot create, update, delete, import_preview, import_commit, 画像/タグ/オーナー操作 create, update, delete, import_preview, import_commit, image/tag/owner ops
reviewreview create, update, delete(承認 / 却下は metadata.status 参照) create, update, delete (approve/reject via metadata.status)
user_notificationuser_notification create, update, delete, schedule, send create, update, delete, schedule, send
ownerowner create, update, delete, adjust_credit, approve_application create, update, delete, adjust_credit, approve_application
themetheme create, update, delete, gift create, update, delete, gift
badge / exp_rule / level badge / exp_rule / level create, update, delete, backfill, bulk_update, recalc create, update, delete, backfill, bulk_update, recalc
その他: tag / operator / code / article / ad / boost / sponsor / asset / parking_session / user_vehicle / app_user / revenue Others: tag / operator / code / article / ad / boost / sponsor / asset / parking_session / user_vehicle / app_user / revenue 標準 CRUD(create/update/delete)+ 個別業務動詞 Standard CRUD (create/update/delete) + op-specific verbs

6-3. metadata の使い分け6-3. metadata guidance

禁止事項: Never: パスワード、JWT、API キー、vault シークレットの raw 値を metadata に入れない。 ai_provider.register_key では vault_secret_id のみを残し、実値は絶対に載せない。 Do not put passwords, JWTs, API keys, or raw vault secrets in metadata. For ai_provider.register_key, keep only vault_secret_id — never the raw key.

6-4. API6-4. Related APIs

7. バックフィル / 再計算7. Backfill / recompute

活動ログの欠損やバッジ定義の変更があったときのリカバリ用。

Recovery tools for when activity logs are missing or a badge definition is retuned.