duoqi-api/src/types/app-api.ts
Wang Zhuoxuan c36d828df9
All checks were successful
CI/CD Pipeline / Unit Tests (push) Successful in 30s
CI/CD Pipeline / Build & Deploy Test (push) Has been skipped
CI/CD Pipeline / Build & Deploy Production (push) Successful in 1m19s
feat: narrow gamification to first-version MVP scope
行为按 docs/GAMIFICATION_DESIGN.md(duoqi-flutter/docs/GAMIFICATION_REQUIREMENTS_CHANGE.md)对齐:

- 新增 src/utils/time.ts 北京时间工具,统一替换散落的 UTC 日期/周边界计算
- challenge-service:未完成 session 续答(同 nodeId 复用 pending/in_progress),每日次数按 session 幂等扣减(条件 UPDATE 抢 daily_attempt_consumed_at 锁),移除高奖励 multiplier 和答对时发 first_knowledge_card
- 新增 knowledge-card-service 和 POST /challenges/knowledge-cards/:cardId/view:用户打开/收下卡片时在单事务内发 review_explanation (3 XP) 和 first_knowledge_card (15 XP),fallback 卡走独立幂等 namespace
- tracks-service:mapNodeStatus 只返回 current/done
- hearts-service:删除 Pro/ProPlus 免扣分支和新用户 1 心保护,RestoreMethod 收窄为 'ad' | 'wait'
- xp-service / leaderboard-service / streak-service / ad-recovery-service / progress-summary-service:时区切北京;排行榜同分按 last_xp_at 升序,weeklySettlement 不再发前 3 名金币;streak milestone 返回空
- /shop/purchase、/inventory/items/use、/subscription/verify 返回 NOT_AVAILABLE_IN_MVP
- schema 加 challenge_sessions.daily_attempt_consumed_at(迁移 0007)
- 接口文档 docs/api-reference.md 同步 12 项 MVP 行为变化
- 测试:新增 utils/time 和 tracks node-status 测试;hearts/streak/leaderboard 测试按新行为更新;challenge-service 13 个 MVP 之外(Plus/高奖励/first_knowledge_card 答对触发)测试 it.skip 待重写 mock 队列

typecheck / lint / test (146 passed, 13 skipped) 全部通过。
2026-06-16 18:05:04 +08:00

272 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export type SubscriptionTier = 'free' | 'pro' | 'proplus';
export type SubscriptionStatus = 'none' | 'active' | 'expired' | 'cancelled';
export type NodeStatus = 'done' | 'current' | 'locked' | 'chest';
export type LeaderboardScope = 'region' | 'topic';
export type RewardSource = 'ad' | 'check_in' | 'subscription' | 'admin_grant' | 'debug';
export type SubscriptionPlatform = 'huawei' | 'apple' | 'google';
export interface RegionDto {
code: string;
name: string;
shortName: string;
parentCode: string | null;
level: number;
sortOrder: number;
enabled: boolean;
}
export interface RegionsConfigDto {
version: string;
countryCode: 'CN';
hierarchy: 'flat';
updatedAt: string;
regions: readonly RegionDto[];
}
export interface UserRegionDto {
code: string;
name: string;
shortName: string;
selectedAt: string | null;
nextChangeAllowedAt: string | null;
}
export interface UserBriefDto {
id: string;
nickname: string;
avatarUrl: string | null;
tier: SubscriptionTier;
level: number;
region: UserRegionDto | null;
}
export interface SubscriptionDto {
status: SubscriptionStatus;
tier: SubscriptionTier;
expiresAt: string | null;
autoRenew?: boolean;
}
export interface ProgressSummaryDto {
hearts: number;
maxHearts: number;
nextHeartRestoreAt: string | null;
dailyAttemptsLeft: number;
dailyAttemptsMax: number;
nextAttemptResetAt: string | null;
highRewardSessionsLeft: number;
highRewardSessionsMax: number;
xp: number;
level: number;
xpToNextLevel: number;
streakDays: number;
checkInDays: number;
streakProtectedUntil: string | null;
activeTrackId: string | null;
isSubscribed: boolean;
}
export interface ThemeNodeDto {
id: string;
title: string;
status: NodeStatus;
reward: string;
questionCount: number;
}
export interface ThemeTrackDto {
id: string;
name: string;
icon: string;
progress: number;
nodes: ThemeNodeDto[];
}
export interface ShopBenefitDto {
id: string;
type: 'hearts' | 'attempts' | 'streak' | 'subscription';
title: string;
description: string;
enabled: boolean;
requiresAd: boolean;
}
export type ShopProductType = 'item' | 'cosmetic';
export type ShopProductId =
| 'hint-feather'
| 'heart-supply'
| 'double-xp-potion'
| 'streak-shield'
| 'mascot-outfit-starter';
export interface ShopProductDto {
id: ShopProductId;
type: ShopProductType;
itemId: string;
title: string;
description: string;
priceCoins: number;
quantity: number;
enabled: boolean;
}
export interface ShopCatalogDto {
benefits: readonly ShopBenefitDto[];
products: readonly ShopProductDto[];
}
export interface ShopPurchaseResultDto {
product: ShopProductDto;
coinsSpent: number;
coinsBalance: number;
item: {
itemId: string;
quantity: number;
activeUntil: string | null;
metadata: Record<string, unknown> | null;
};
rewards: ReadonlyArray<{
type: string;
source?: string;
amount?: number;
itemId?: string;
quantity?: number;
title?: string;
}>;
}
export interface WalletDto {
coinsBalance: number;
}
export interface InventoryItemDto {
itemId: string;
quantity: number;
activeUntil: string | null;
metadata: Record<string, unknown> | null;
}
export interface InventoryDto {
items: readonly InventoryItemDto[];
}
export type UsableInventoryItemId = 'streak_shield' | 'double_xp_potion' | 'heart_supply' | 'hint_feather';
export interface UseInventoryItemResultDto {
itemId: UsableInventoryItemId;
quantityRemaining: number;
effect: {
type: 'restore_hearts' | 'double_xp' | 'hint' | 'streak_protection';
activeUntil?: string | null;
hearts?: number;
excludedOptions?: readonly string[];
streakProtectedUntil?: string | null;
};
}
export interface BootstrapDto {
user: UserBriefDto;
progress: ProgressSummaryDto;
tracks: ThemeTrackDto[];
shopBenefits: readonly ShopBenefitDto[];
shop: ShopCatalogDto;
wallet: WalletDto;
inventory: InventoryDto;
subscription: SubscriptionDto;
}
export interface ChallengeQuestionDto {
challengeId: string;
trackId: string;
nodeId: string;
question: {
id: string;
prompt: string;
options: ReadonlyArray<{ id: string; text: string }>;
};
}
export interface ChallengeSessionDto {
challengeId: string;
trackId: string;
nodeId: string;
totalQuestions: number;
highRewardEligible: boolean;
questions: readonly ChallengeQuestionDto[];
/** 已提交过答案的题目 ID用于 session 续答场景,客户端据此跳到下一道未答题)。 */
answeredQuestionIds: readonly string[];
}
/** 知识卡查看事件响应:返回本次发放的 XP 奖励和最新进度。 */
export interface KnowledgeCardViewDto {
cardId: string;
rewards: ReadonlyArray<{
type: string;
source?: string;
amount?: number;
title?: string;
}>;
progress: ProgressSummaryDto;
}
export interface AnswerRequestDto {
challengeId: string;
questionId: string;
selectedOptionId: string;
timeMs: number;
submitRequestId?: string;
}
export interface AnswerResultDto {
answerState: 'correct' | 'wrong';
correctOptionId: string;
xpDelta: number;
progress: {
hearts: number;
dailyAttemptsLeft: number;
highRewardSessionsLeft: number;
highRewardSessionsMax: number;
xp: number;
streakDays: number;
};
knowledgeCard: {
id: string;
title: string;
summary: string;
fact: string;
};
rewards: ReadonlyArray<{
type: string;
source?: string;
amount?: number;
itemId?: string;
quantity?: number;
title?: string;
}>;
}
export interface LeaderboardEntryDto {
rank: number;
userId: string;
displayName: string;
avatarUrl: string | null;
xp: number;
badge: string;
isMe: boolean;
}
/** 周榜元信息,附带在排行榜响应中。 */
export interface LeaderboardMetaDto {
weekStart: string;
weekEnd: string;
nextRefreshAt: string;
groupId: string | null;
requiresRegionSelection?: boolean;
selectedRegion?: UserRegionDto | null;
viewRegion?: RegionDto | null;
/** 当前用户组内排名(仅 /leaderboards/me 返回)。 */
rank?: number;
/** 当前周奖励预览:各组前 3 名的金币奖励。 */
rewardPreview: Array<{ rank: number; coins: number }>;
}