ゲーミフィケーション統合仕様(モバイル側責務) Gamification Integration (Mobile Responsibilities)
2026-04-22 のゲーミフィケーション脅威対応(T1〜T9)で、DB / BFF 側は防御の主要層を 実装済み。本書はモバイル(Flutter)が担う送信責務を規定する。 モバイルが正しい情報を送らないと、正規ユーザーの EXP 獲得がブロックされるため、 リリース前にここの実装と QA を通す必要がある。
/v1/parking-sessions 等)は 使用禁止 で、Flutter は
POST /v1/mobile/actions/sessions/start / POST /v1/mobile/actions/sessions/{id}/finalize を使うこと。
詳細は 画面 × API マップ。
The mobile client now talks only to the mobile BFF (Views & Actions)
(ADR-0004 SDUI L3). The bare resource paths sprinkled below (/v1/parking-sessions etc.) are not callable;
use POST /v1/mobile/actions/sessions/start and POST /v1/mobile/actions/sessions/{id}/finalize instead.
See the screen × API map.
The T1–T9 gamification hardening of 2026-04-22 placed the main defensive layer on the database and BFF. This document describes the client-side responsibilities that the Flutter app must fulfill so that legitimate users can earn EXP correctly.
- EXP 付与・倍率・キャップ判定は全てサーバー確定。クライアントは「事実」を正確に送るだけ。
- GPS / 写真ハッシュ / device_fingerprint のいずれかが欠けた要求は、対応する活動で EXP が付かない。
- 失敗しても主機能(駐車・レビュー投稿)はブロックしない。EXP だけ加算されない形にする。
- All EXP / multipliers / caps are confirmed server-side. The client simply reports facts.
- Requests missing GPS, photo hash, or device_fingerprint won't earn EXP for that activity.
- A failure here must never block the main flow (parking / reviews). EXP simply isn't granted.
1. GPS 位置情報(T1 対策) 1. GPS location (T1 defense)
駐車開始 / 終了時に端末の現在位置を取得し、API に start_lat/start_lng
または end_lat/end_lng として送る。サーバー側は
parking_lots.location との距離 300m 以内なら
geo_verified=true と判定し、parking_start /
parking_end の EXP を付与する。位置が取れないと EXP 0 になる。
推奨パッケージ Recommended package
geolocator(pub.dev/Flutter 公式推奨) — Android / iOS 両対応の権限リクエスト + 位置取得。
取得精度とタイムアウト Accuracy & timeout
| 項目 | 値 | 理由 |
|---|---|---|
LocationAccuracy | high(Android PRIORITY_HIGH_ACCURACY / iOS kCLLocationAccuracyBest) | サーバー検証閾値 300m に収めるため、Wi-Fi / セル測位では不足する場合あり |
| タイムアウト | 7 秒 | UX とのバランス。7 秒で取れなければ座標無しで送信し、EXP を諦める |
| キャッシュ許容 | 30 秒以内のキャッシュを許容 | 駐車場到着〜駐車開始で再取得コストを避ける |
権限フロー Permission flow
- 初回は駐車開始ボタン押下時に
Geolocator.requestPermission()。 - 拒否された場合は「位置情報を使えないと EXP が付きません。設定から許可できます」という控えめなトーストを表示。駐車自体は続行。
- iOS「許可しない」(
LocationPermission.deniedForever)なら OS 設定へのディープリンクを案内。 - VPN / モックプロバイダ疑惑(
Position.isMocked=true)は記録だけしてサーバーに送信。判定はサーバー側。
API 連携 API integration
// 駐車開始 (Idempotency-Key ヘッダ必須)
POST /v1/mobile/actions/sessions/start
Idempotency-Key: <uuid-v4>
{
"parking_lot_id": "...",
"vehicle_type": "sedan",
"start_lat": 35.681236, // ← 端末から取得
"start_lng": 139.767125, // ← 端末から取得
"planned_end_at": null // 任意
}
// 駐車終了 (Idempotency-Key ヘッダ必須)
POST /v1/mobile/actions/sessions/{id}/finalize
Idempotency-Key: <uuid-v4>
{
"ended_at": "2026-04-22T15:30:00+09:00", // 任意
"started_at": null, // 入庫時刻訂正(任意)
"user_entered_fee_minor": null, // 手入力料金(任意, minor unit)
"end_lat": 35.681240, // 任意
"end_lng": 139.767130
}
失敗時の UX Failure UX
- 座標未取得でも駐車開始 / 終了は成立させる。
finalizeのActionEnvelope.data.celebrationにenabled=falseが返れば EXP 未付与(旧 RPC・既 finalize 等)。enabled=true ならcelebration.reward内のgeo_verified/daily_cap_hit/reasonを見て理由ラベルを短く出す。celebration.enabled=trueかつreward.exp_granted > 0なら通常どおり Lottie / Haptic で祝う。
2. 写真コンテンツハッシュ(T5 対策) 2. Photo content hash (T5 defense)
駐車セッションの写真アップロード時、同一ユーザー内での写真使い回しを検知するため、
クライアント側で画像バイナリの SHA-256 (hex 64 文字) を計算して送る。
サーバー側で同一ユーザーの過去写真と重複したら duplicate_of 列に記録され、
写真つきレビュー EXP ボーナス等の対象外になる(投稿自体は可)。
推奨パッケージ Recommended package
crypto(Dart 標準) — SHA-256 / HMAC 用。
計算タイミング When to compute
- 画像ピック or 撮影完了直後、圧縮・リサイズを行う前の原本バイナリで計算する。
- 計算した hash を state に保持。R2 へのアップロード完了後、PUT API にセットで送る。
- 圧縮後のバイナリでハッシュを取ると同一写真でもハッシュがブレるので、必ず原本で計算すること。
API 連携 API integration
// 写真アップロードはレビュー作成 API に同梱する形に統一済み(2026-04-23 SDUI L3)。
// レビュー本文 + R2 へ PUT 済みの photo メタ情報を一括で送る。
POST /v1/mobile/actions/reviews/create
Idempotency-Key: <uuid-v4>
{
"lot_id": "...",
"rating": 5,
"body": "...",
"photos": [
{
"r2_key": "reviews/.../photo.jpg",
"content_type": "image/jpeg",
"size_bytes": 1048576,
"content_hash": "3f4d8b2a..." // ← SHA-256 hex 64 文字
}
]
}
// 駐車セッション写真も同パターン(review-compose と feedback フローで PUT 済み URL を返した上で
// content_hash を BFF へ渡す)。サーバー側で同一ユーザーの過去写真と一致したら
// duplicate_of に既存写真 id が記録され、写真ボーナス対象外になる(投稿自体は成功)。
Dart 実装例 Dart snippet
import 'package:crypto/crypto.dart';
import 'dart:convert';
Future<String> sha256OfFile(File file) async {
final bytes = await file.readAsBytes();
return sha256.convert(bytes).toString(); // hex 64 文字
}
is_duplicate=true のときの UX UX when is_duplicate=true
- 写真は保存されるが、バッジ対象の「写真つき駐車」条件は満たさない。
- ユーザーには静かに扱う(「この写真は過去に投稿した写真と同じため、特典の対象外です」などは表示しない)。運用側が監査 UI で検知する用途。
3. Device Fingerprint(T4 複垢紹介対策) 3. Device fingerprint (T4 multi-account referral defense)
紹介コード適用時、被紹介者のデバイス識別子を送る。招待元のデバイスと
一致する場合、or 他ユーザーで既に登録されている場合は
duplicate_device エラーで拒否される。正規ユーザーでも
家族で端末共有する場合は紹介できないが、この運用リスクは T4 の監査報告で
受容済み。
取得する識別子 Identifiers to collect
| プラットフォーム | 識別子 | 取得方法 | 永続性 |
|---|---|---|---|
| Android | SSAID(Settings.Secure.ANDROID_ID) | device_info_plus の AndroidDeviceInfo.id | ファクトリリセットまで不変 |
| iOS | IDFV(identifierForVendor) | device_info_plus の IosDeviceInfo.identifierForVendor | 同一ベンダーの全アプリ削除でリセット |
送信形式 Payload format
サーバー側は単一文字列として扱う。プラットフォームに応じて "android:<ssaid>"
/ "ios:<idfv>" のようにプレフィックスを付けると、監査 UI で
プラットフォーム別に可視化しやすい。
API 連携 API integration
// 紹介コードはサインアップ時に POST /v1/mobile/actions/auth/sign-up へ同梱、
// もしくはサインアップ後に Profile / Referrals フローから適用する設計(2026-04-30 時点)。
// device_fingerprint は紹介適用 1 リクエストにだけ同梱し、ローカルには保存しない。
POST /v1/mobile/actions/auth/sign-up
Idempotency-Key: <uuid-v4>
{
"email": "...",
"password": "...",
"referral_code": "A3B7CKDE",
"device_fingerprint": "android:f8e3...4c2d"
}
// ActionEnvelope 失敗例(duplicate_device は ActionEnvelope.states.error の code として返る)
{
"data": { "signup_pending": true },
"states": {
"error": [{ "code": "duplicate_device", "message_code": "referral.error.duplicate_device", ... }]
}
}
推奨パッケージ Recommended package
device_info_plus(公式)— Android / iOS の OS 情報とデバイス ID を取得。
取得失敗時 When it can't be obtained
- エラー時は
device_fingerprintを省略して送る(許可される)。ただし重複検知はスキップされるため、確定後 admin 監査で pending を可視化して対応。 - プライバシー配慮として、この識別子はローカルに保存せず、紹介適用の 1 リクエストにだけ同梱する。
4. レスポンス受信 — celebration オブジェクト 4. Response handling — the celebration object
POST /v1/mobile/actions/sessions/{id}/finalize の ActionEnvelope には
data.celebration が含まれる。Flutter は enabled=false なら静かに次画面へ遷移、
enabled=true なら badge → level_up → exp gauge の順に演出を再生する。
Schema 本体は api/src/schema/view/session-celebration-view.ts。
// ActionEnvelope (envelope 全体)
{
"data": {
"session_id": "...",
"celebration": {
"enabled": true, // false なら演出スキップ
"reward": {
"exp_granted": 30,
"exp_base": 20,
"multiplier": 1.5,
"total_exp": 1250,
"level_before": 8,
"level_after": 9,
"level_up": true,
"badges_earned": [ ... ],
"daily_cap_hit": false,
"geo_verified": true,
"reason": null // "geo_not_verified" / "daily_cap_reached" / "cooldown" / null
},
"referral_confirmation": {
"confirmed": true,
"usage_id": "...",
"referrer_reward": { ... },
"referee_reward": { ... }
}
}
},
"navigation": { "target": "parking_history", "strategy": "pop_to_root" },
"fallback_behavior": { ... },
"meta": { ... }
}
UI マッピング UI mapping
celebration.enabled=false→ 演出を出さず、navigation に従って遷移。reward.exp_granted > 0→ 獲得 EXP アニメーション。reward.multiplier > 1→ 「Parky+ 倍率 ×1.5」バッジ表示。reward.level_up=true→ Level Up 画面フル表示。reward.badges_earnedが空でなければ 1 つずつバッジ獲得モーダル。reward.daily_cap_hit=trueまたはreward.geo_verified=false→ 理由ラベルを短く表示。referral_confirmation.confirmed=true→ 「紹介ボーナス成立!」演出(被紹介者側のみ)。- celebration を閉じた後は
POST /v1/mobile/actions/sessions/{id}/feedbackを呼んで満足度を取り、ActionEnvelope のnavigation(またはdata.next_flow)に従って遷移する。
フロー図(駐車終了 → celebration → feedback) Flow (finalize → celebration → feedback)
(Idempotency-Key, end_lat, end_lng) BFF->>Core: finalizeParkingSession() Core->>DB: finalize_parking_session RPC
(award_user_activity_v2 内部で
geo_verified / daily_cap / multiplier 判定) DB-->>Core: { session, reward } Core-->>BFF: { sessionId, celebration } BFF-->>App: ActionEnvelope { data.celebration, navigation } alt celebration.enabled = true App->>U: badge → level_up → exp gauge U->>App: 閉じる end App->>BFF: POST /:id/feedback (good / bad / skip) BFF-->>App: ActionEnvelope { navigation: next_flow } App->>U: navigation 通り遷移(history / write-review 等)
5. サーバー側拒否の活動タイプ別対応 5. Reasons your request may earn 0 EXP
| activity_type | requires_geo | daily_cap | per_target_daily_cap | cooldown |
|---|---|---|---|---|
| parking_start / parking_end | ✅ | 10 | 2 | — |
| night_parking / ev_charge | ✅ | 5 | 2 | — |
| unique_parking / sponsor_checkin | ✅ | 3 / 10 | 1 | — |
| review_post | — | 5 | 1 | — |
| rating | — | 10 | 1 | — |
| share | — | 3 | — | 300 s |
| search / ai_search | — | 20 / 10 | — | 10–30 s |
| login / app_open | — | 1(1 日 1 回) | — | — |
| referral_applied / referral_confirmed | — | 1 / — | — | — |
上記は 2026-04-22 時点の activity_exp_rules seed。値は
BFF 側で変更される可能性があるため、モバイルで表示する場合は
GET /v1/mobile/views/profile/gamification
(recent_exp_events + earned_badges + pending_badges)
を参照する。activity 別の cap / exp_amount は非公開(クライアントは reason ラベルで結果を見る)。
6. 実装チェックリスト(リリース前) 6. Pre-release checklist
- ☐
geolocator導入、Android / iOS 両方で「使用中のみ許可」で取得可。 - ☐
Geolocator.requestPermission()のハンドリング(granted / denied / deniedForever)。 - ☐ 位置取得タイムアウト 7 秒、失敗時は座標なしで送信。
- ☐ 駐車開始 / 終了の両 API で lat/lng を送信。
- ☐
crypto導入、画像ピック→SHA-256 計算→PUT で送信。 - ☐ 圧縮前の原本で計算しているか確認。
- ☐
device_info_plus導入、紹介適用時にandroid:/ios:プレフィックス付きで送信。 - ☐
reward.reasonの 4 値(null / geo_not_verified / daily_cap_reached / cooldown)を UI で分岐。 - ☐
daily_cap_hit=true時に「本日の獲得上限に到達」と短く表示、ユーザーを落胆させない UX。 - ☐
duplicate_deviceエラー時のフレンドリーな文言と復旧導線(別の紹介コード?ゲストで続ける?)。 - ☐ 開発環境で T1–T9 のそれぞれを擬似的に再現(モック GPS / 同一画像連投 / 同一デバイスで 2 アカウント)してクライアント挙動を QA。
7. 関連仕様書・実装 7. Related specs & implementation
- 監査レポート:
.work/parky/2026-04-21_17-00_parky_gamification_audit.html(9 脅威の整理) - 実装レポート:
.work/parky/2026-04-22_18-00_gamification_t1_t9_implementation_report.html - BFF endpoints:
api/src/bff/mobile/actions/sessions/start.ts/api/src/bff/mobile/actions/sessions/finalize.ts/api/src/bff/mobile/actions/sessions/feedback.ts/api/src/bff/mobile/views/profile-gamification.ts - BFF core:
api/src/core/parking-sessions//api/src/core/gamification/load-gamification.ts/api/src/core/profile/get-profile-gamification.ts - Supabase RPC:
award_user_activity_v2/create_parking_session/finalize_parking_session/apply_user_referral_v2/confirm_pending_referrals_for_user/record_photo_upload/record_review_exp - Admin ダッシュボード:
dev-admin.parky.co.jp/gamification/anomalies(daily_cap ヒット / geo 未検証 / 大量 EXP 取得ユーザー / pending 紹介を可視化)