Files
eryao/docs/protocols/common/user-points-chat-data-protocol.md
T
qzl ff40ff9dd8 feat: 新人初始礼包购买追踪功能
- 数据库:添加 has_purchased_starter_pack 字段到 register_bonus_claims
- 后端:创建静态配置管理套餐信息,支持按国家/地区区分
- 后端:新增 GET /api/v1/points/packages API 返回可用套餐
- 后端:创建 utils/paths.py 统一路径管理
- 前端:动态获取套餐信息,移除硬编码
- 前端:添加 ProductCode 枚举约束,前后端类型安全
- 配置:Profile 默认国家改为 US(ISO 3166-1 alpha-2)
- 文档:更新协议文档说明新 API 和字段
2026-04-16 16:11:09 +08:00

11 KiB

User Points & Chat Data Protocol

This protocol defines the canonical data contract for user profile, points account, points ledger, chat session, and chat messages.

Protocol verification status:

  • Last audited migration: backend/alembic/versions/20260413_0004_register_bonus_claims_snapshot.py
  • Last audited models: backend/src/models/profile.py, backend/src/models/user_points.py, backend/src/models/points_ledger.py, backend/src/models/points_audit_ledger.py, backend/src/models/register_bonus_claims.py, backend/src/models/agent_chat_session.py, backend/src/models/agent_chat_message.py
  • Current status: aligned with register bonus moved to application service

Scope

  • profiles
  • user_points
  • points_ledger
  • points_audit_ledger
  • register_bonus_claims
  • sessions
  • messages

Compatibility strategy

  • Current strategy: additive evolution.
  • Breaking changes (drop/rename/type change on core fields) require explicit migration + rollback notes.
  • points_ledger.metadata.schema_version is mandatory and current value is 1.

Runtime charging policy (chat)

  • Charge unit: 20 points per successful run.
  • Charge timing: deduct after worker run succeeds (RUN_FINISHED path).
  • Failure behavior: failed/canceled runs do not deduct points.
  • Precheck: before accepting a run, backend must verify available = balance - frozen_balance >= 20.
  • Session follow-up cap: one session allows at most 2 user runs total (initial divination + 1 follow-up).
  • Billing idempotency key for per-run consume: chat.run.success:{sha1(session_id:run_id)}.
  • Failed/canceled runs do not deduct user points. If real provider cost is observed, audit record is written with billed_to='platform'.

Table contract

profiles

  • PK: id (auth.users.id, on delete cascade)
  • Core fields: username, avatar_url, bio, settings, referred_by, created_at, updated_at, deleted_at
  • Constraints:
    • username not empty
  • Indexes:
    • ix_profiles_username
    • ix_profiles_settings_gin
  • Notes:
    • referred_by is FK to profiles.id (on delete set null) for invite/referral tracking
    • settings stores ProfileSettingsV1 JSON including preferences, privacy, notification, divination_tutorial

user_points

  • PK: user_id (auth.users.id, on delete cascade)
  • Core fields: balance, frozen_balance, lifetime_earned, lifetime_spent, version, created_at, updated_at
  • Constraints:
    • all numeric totals must be non-negative
    • frozen_balance <= balance

points_ledger

  • PK: id
  • FK:
    • user_id -> auth.users.id (on delete cascade)
    • biz_id -> sessions.id (on delete restrict, nullable)
    • operator_id -> auth.users.id (on delete set null)
  • Core fields: direction, amount, balance_after, change_type, biz_type, biz_id, event_id, operator_id, metadata, created_at, updated_at
  • Constraints:
    • amount > 0
    • direction in (1, -1)
    • balance_after >= 0
    • change_type in ('register', 'consume', 'grant', 'adjust')
    • biz_type is null or biz_type='chat'
    • biz binding:
      • register => biz_type is null and biz_id is null
      • consume/grant/adjust => biz_type='chat' and biz_id not null
    • direction and change_type coupling:
      • register/grant => direction = 1
      • consume => direction = -1
      • adjust => direction in (1, -1)
    • idempotency: unique (user_id, event_id)

points_audit_ledger

  • PK: id
  • No FK to auth.users for user_id_snapshot to avoid cascade delete and preserve audit retention
  • Core fields: event_id, user_id_snapshot, user_email_snapshot, change_type, biz_type, biz_id, direction, amount, balance_after, billed_to, run_id, request_id, input_tokens, output_tokens, cost, metadata, created_at, updated_at
  • Constraints:
    • amount >= 0
    • direction in (1, 0, -1)
    • balance_after >= 0
    • change_type in ('register', 'consume', 'grant', 'adjust')
    • biz_type is null or biz_type='chat'
    • billed_to in ('user', 'platform')
    • metadata must be object
    • idempotency: unique (event_id)

register_bonus_claims

  • PK: id
  • Core fields: email_hash, user_email_snapshot, first_user_id_snapshot, balance_snapshot, grant_event_id, has_purchased_starter_pack, created_at, updated_at
  • Constraints:
    • email_hash unique
    • grant_event_id unique
  • Notes:
    • email_hash must be HMAC-SHA256 over normalized email (trim + lower)
    • key source: backend config points_policy.register_bonus_hmac_key
    • balance_snapshot stores the latest pre-delete account balance for same-email re-registration recovery
    • has_purchased_starter_pack tracks whether user has purchased the starter pack ($0.99/60 credits)

points_ledger.metadata (schema_version=1)

Canonical shape:

{
  "schema_version": 1,
  "operator_type": "user|system|admin",
  "run_id": "string",
  "request_id": "string|null",
  "charge": {
    "message_id": "uuid",
    "message_seq": 1,
    "model_code": "string",
    "input_tokens": 0,
    "output_tokens": 0,
    "cost": "0.000000"
  },
  "ext": {}
}

JSON constraints:

  • Common:
    • must be object
    • schema_version = 1
    • operator_type in (user, system, admin)
    • run_id non-empty
    • if present, ext must be object
  • Per change_type:
    • register: no charge, and no chat binding (biz_type/biz_id both null)
    • consume: requires charge object with required fields
    • grant: no extra metadata shape requirement
    • adjust: requires ext.ticket_id non-empty

Signup initialization contract

  • DB trigger (auth.users after insert):
    • Function: public.initialize_profile_and_invite_code_on_signup()
    • Side effects: profile init + invite code init
  • Application service (in POST /auth/email-session):
    • grant_register_bonus_if_eligible() restores balance_snapshot first when present; otherwise grants register bonus via register_bonus_claims
    • Bonus amount from config.points_policy.register_bonus_points

sessions

  • PK: id
  • FK: user_id -> auth.users.id
  • Core fields: session_type, job_id, title, status, last_activity_at, message_count, total_tokens, total_cost, state_snapshot, created_at, updated_at, deleted_at
  • Constraints:
    • session_type in ('chat', 'automation')
    • status in ('pending', 'running', 'completed', 'failed')
    • message_count/total_tokens/total_cost non-negative

messages

  • PK: id
  • FK: session_id -> sessions.id (on delete cascade)
  • Core fields: seq, role, content, model_code, tool_name, input_tokens, output_tokens, cost, latency_ms, visibility_mask, metadata, created_at, updated_at, deleted_at
  • Constraints:
    • unique (session_id, seq)
    • seq > 0
    • role in ('user', 'assistant', 'system', 'tool')
    • token/cost non-negative
    • latency_ms null or non-negative

Security and ownership

  • Backend service must derive owner identity from verified auth context.
  • Client must not be trusted for user_id/operator_id ownership semantics.
  • metadata and settings must not include secrets.

Application initialization contract

Application initialization is split into two separate concerns: deterministic seed data and dynamic notification template sync. These are intentionally decoupled to avoid non-deterministic side effects during bootstrap.

Seed data (deterministic)

Managed by python -m core.runtime.cli init-data (called by dev-migrate.sh bootstrap):

  • initialize_llm_catalog() — seeds llm_factory and llms tables with known model definitions
  • initialize_system_agents() — seeds system_agents table

These are idempotent and version-locked to code; they do not depend on runtime state or user data.

Notification templates (declarative sync)

Managed by python -m core.runtime.cli sync-notifications [flags]:

  • Reads notification definitions from YAML files under backend/src/core/config/static/notification/notifications/
  • Syncs to notifications table; links existing users via user_notifications when --reconcile-targets is set
  • Supports flags:
    • --dry-run — validate without writing
    • --prune — remove static notifications no longer present in YAML files
    • --reconcile-targets — insert user_notifications entries for all existing users matching each notification's target predicate
    • --source-key <key> — sync only the notification with the matching source_key

Run after migrations on fresh environments or after adding new notification YAML definitions. Not included in bootstrap to keep bootstrap fast and free of unintended side effects.

Packages API

GET /api/v1/points/packages

Returns available purchase packages for the current user's region, including starter pack eligibility.

Request:

  • Auth: Required (JWT)
  • Headers: Authorization: Bearer <token>

Response:

{
  "region": "US",
  "currency": "USD",
  "packages": [
    {
      "productCode": "new_user_pack_099_60",
      "type": "starter",
      "priceUsd": "0.99",
      "credits": 60,
      "badge": null,
      "isStarter": true,
      "starterEligible": true,
      "sortOrder": 0
    },
    {
      "productCode": "basic_pack_499_100",
      "type": "regular",
      "priceUsd": "4.99",
      "credits": 100,
      "badge": null,
      "isStarter": false,
      "starterEligible": false,
      "sortOrder": 10
    }
  ]
}

Fields:

  • region: ISO 3166-1 alpha-2 country code (e.g., "US", "CN")
  • currency: ISO 4217 currency code (e.g., "USD")
  • packages: List of available packages
    • productCode: Unique product identifier
    • type: "starter" (new user pack) or "regular"
    • priceUsd: Price in USD (decimal string)
    • credits: Number of credits
    • badge: Optional badge text (e.g., "Popular")
    • isStarter: Whether this is a starter pack
    • starterEligible: Whether user is eligible to purchase starter pack
    • sortOrder: Display order (ascending)

Business Logic:

  1. Determine user's region from profile.settings.preferences.country (default: "US")
  2. Load package configuration from backend/src/core/config/static/packages/{country}.yaml (fallback to default.yaml)
  3. Check starter pack eligibility:
    • If register_bonus_claims.has_purchased_starter_pack = true, exclude starter pack from response
    • Otherwise, include starter pack with starterEligible: true

Configuration Files:

  • Path: backend/src/core/config/static/packages/
  • Format: YAML
  • Example: us.yaml
region: US
currency: USD
packages:
  - product_code: new_user_pack_099_60
    type: starter
    price_usd: "0.99"
    credits: 60
    badge: null
    sort_order: 0
    enabled: true
  - product_code: basic_pack_499_100
    type: regular
    price_usd: "4.99"
    credits: 100
    badge: null
    sort_order: 10
    enabled: true

Country/Region Codes:

  • Uses ISO 3166-1 alpha-2 standard
  • Default: US (United States)
  • Examples: CN (China), TW (Taiwan), HK (Hong Kong), JP (Japan)