アクティビティログ & ゲーミフィケーション 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.
- 型と metadata スキーマ:
api/src/lib/activity-types.ts(TypeScript のActivityType型 + 各種 Zod metadata スキーマ) - Type & metadata schema:
api/src/lib/activity-types.ts(the TSActivityTypeunion + per-type Zod metadata schemas). - 付与ロジック: PG 関数
public.award_user_activity(infra/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.tsのrecordActivity/recordActivityBestEffortが TypeScript で型付き呼出+実行時 Zod 検証 - BFF entry:
recordActivity/recordActivityBestEffortinlib/activity.ts— type-safe call with runtime Zod validation. - クライアントが参照:
GET /v1/meta/activity-typesが全種別と metadata JSON Schema を返す - Client view:
GET /v1/meta/activity-typesreturns every type with its metadata JSON Schema.
parking_complete → parking_end、save_parking → parking_saved、
vehicle_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_logs | user_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_targets | user_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_counts | user_activity_counts |
(user, type) 単位の累計カウンタ | Per-(user, type) running counter | UNIQUE(user_id, activity_type) |
UNIQUE(user_id, activity_type) |
activity_exp_rules | activity_exp_rules |
(activity_type → EXP 量) の対応表 | Map from activity_type to EXP amount |
is_active のみ使用 |
Only is_active=true rows apply |
user_exp | user_exp |
ユーザーの累計 EXP と現在レベル | Cumulative EXP and current level | UNIQUE(user_id) |
UNIQUE(user_id) |
level_definitions | level_definitions |
レベルカーブ(level, required_exp) |
Level curve (level, required_exp) |
(level) |
(level) |
badge_definitions | badge_definitions |
バッジマスタ: activity_type, threshold, conditions JSONB |
Badge master: activity_type, threshold, conditions JSONB |
(id) |
(id) |
user_badge_progress | user_badge_progress |
バッジ進捗カウンタ(threshold 到達まで) | Per-badge progress counter up to threshold | UNIQUE(user_id, badge_id) |
UNIQUE(user_id, badge_id) |
user_badges | user_badges |
獲得済みバッジ | Earned badges | UNIQUE(user_id, badge_id) |
UNIQUE(user_id, badge_id) |
2. 付与フロー(award_user_activity)2. 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.ts の activityTypes 定数。
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-schema で activityTypes
の 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:
- SSoT を更新:
api/src/lib/activity-types.tsのactivityTypesに行を追加(description / category / emittedBy / metadataSchema) - Update the SSoT: add an entry to
activityTypesinapi/src/lib/activity-types.ts(description / category / emittedBy / metadataSchema). - 本表(docs)に行を追加
- Add a row to this docs table.
- EXP 付与したいなら
activity_exp_rulesに INSERT(admin 画面/admin/gamificationから可能) - If EXP is awarded, insert an
activity_exp_rulesrow (via the admin UI at/admin/gamification). - バッジを作りたいなら
badge_definitionsに INSERT - For badges, add a
badge_definitionsrow. - API 側で
recordActivityBestEffort({ activityType: "..." })を呼ぶ - 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 |
|---|---|---|---|
admin | admin |
create, update, delete, reset_password, update_notif_prefs |
create, update, delete, reset_password, update_notif_prefs |
role | role |
create, update, delete, update_permissions |
create, update, delete, update_permissions |
parking_lot | parking_lot |
create, update, delete, import_preview, import_commit, 画像/タグ/オーナー操作 |
create, update, delete, import_preview, import_commit, image/tag/owner ops |
review | review |
create, update, delete(承認 / 却下は metadata.status 参照) |
create, update, delete (approve/reject via metadata.status) |
user_notification | user_notification |
create, update, delete, schedule, send |
create, update, delete, schedule, send |
owner | owner |
create, update, delete, adjust_credit, approve_application |
create, update, delete, adjust_credit, approve_application |
theme | theme |
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
- PATCH: 入力 body を
{ diff: body }として保存。どのフィールドを書き換えたか監査可能 - PATCH: store the input body as
{ diff: body }so reviewers can see exactly what was changed. - CREATE: 最小サマリ(ほぼ空でも可)
- CREATE: a minimal summary (empty is fine).
- DELETE:
{ soft: true/false }などの種類情報のみ - DELETE: just a flag like
{ soft: true/false }. - 業務動詞(approve/reject/send 等): 状態遷移を表す情報(例:
{ status: "approved" }、送信先件数、queue 件数) - Business verbs (approve/reject/send, etc.): include the resulting state (e.g.
{ status: "approved" }), fan-out counts, queue sizes.
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
- 参照:
GET /v1/admin/activity-logs?type=admin(監査画面用)または?type=user(ユーザー活動側) - Read:
GET /v1/admin/activity-logs?type=admin(audit UI) or?type=user(user-activity view). - 手動追加:
POST /v1/admin/activity-logs(後方互換。通常は BFF 自動配線を使う) - Manual write:
POST /v1/admin/activity-logs(legacy compat; prefer the BFF auto-wiring). - SSoT:
api/src/lib/admin-activity.ts - SSoT:
api/src/lib/admin-activity.ts.
7. バックフィル / 再計算7. Backfill / recompute
活動ログの欠損やバッジ定義の変更があったときのリカバリ用。
Recovery tools for when activity logs are missing or a badge definition is retuned.
SELECT public.backfill_all_badge_progress();— 全アクティブバッジの進捗再計算 — rebuild progress for every active badge.SELECT public.backfill_badge_progress(<badge_id>);— 個別バッジだけ再計算 — rebuild one badge.SELECT public.recalculate_all_user_levels();—level_definitionsを変えたあとのレベル再評価 — re-derive levels after editinglevel_definitions.