标记旧恢复接口废弃并明确 Plus 用户分支
- 在 3 个旧恢复路由上标记 [废弃] 注释,指向新的 ad-recovery 两步流程 - Plus 用户调用广告恢复接口时返回 subscriptionBenefits 权益摘要 - 包含 tier、unlimitedHearts、dailyHighRewardSessions 供客户端展示
This commit is contained in:
parent
8401d8c714
commit
de0055e794
@ -95,11 +95,11 @@
|
||||
| # | 任务 | 状态 | 验收标准 |
|
||||
|---|------|------|----------|
|
||||
| G4-1 | 对齐广告恢复奖励到统一奖励服务 | [x] | session complete 后通过统一奖励结算层发放,记录奖励流水 |
|
||||
| G4-2 | 确认恢复爱心规则 | [ ] | 免费用户广告恢复直接恢复至 5 颗,每日最多 3 次 |
|
||||
| G4-3 | 确认恢复高奖励挑战规则 | [ ] | 每次只恢复 1 组高奖励挑战,每日最多 3 次 |
|
||||
| G4-4 | 确认连续学习保护规则 | [ ] | 每 7 天最多广告恢复 1 次,当天补一次保护 |
|
||||
| G4-5 | 收敛旧恢复接口用途 | [ ] | 旧的直接恢复接口仅保留内部、测试或明确废弃,并在 API 文档中说明 |
|
||||
| G4-6 | 明确 Plus 分支 | [ ] | Plus 用户无需看广告;同入口返回订阅权益或已订阅原因,不消耗广告次数 |
|
||||
| G4-2 | 确认恢复爱心规则 | [x] | 免费用户广告恢复直接恢复至 5 颗,每日最多 3 次 |
|
||||
| G4-3 | 确认恢复高奖励挑战规则 | [x] | 每次只恢复 1 组高奖励挑战,每日最多 3 次 |
|
||||
| G4-4 | 确认连续学习保护规则 | [x] | 每 7 天最多广告恢复 1 次,当天补一次保护 |
|
||||
| G4-5 | 收敛旧恢复接口用途 | [x] | 旧的直接恢复接口仅保留内部、测试或明确废弃,并在 API 文档中说明 |
|
||||
| G4-6 | 明确 Plus 分支 | [x] | Plus 用户无需看广告;同入口返回订阅权益或已订阅原因,不消耗广告次数 |
|
||||
| G4-7 | 添加广告恢复回归测试 | [ ] | 覆盖 idempotency、过期、provider token 缺失、每日上限、Plus、重复 complete |
|
||||
|
||||
## Phase G5:本周排行榜与周期结算
|
||||
|
||||
@ -122,6 +122,8 @@ export async function appApiRoutes(app: FastifyInstance): Promise<void> {
|
||||
return { success: true, data, error: null };
|
||||
});
|
||||
|
||||
// [废弃] 请使用 POST /rewards/ad-recovery/session + /rewards/ad-recovery/complete 两步流程。
|
||||
// 该接口不做幂等、每日上限和 Plus 分支检查,仅供内部测试或过渡期使用。
|
||||
app.post('/rewards/hearts/restore', async (request) => {
|
||||
const parsed = rewardSourceSchema.safeParse(request.body);
|
||||
if (!parsed.success) return validationError(parsed.error.issues[0]?.message);
|
||||
@ -129,6 +131,8 @@ export async function appApiRoutes(app: FastifyInstance): Promise<void> {
|
||||
return { success: true, data, error: null };
|
||||
});
|
||||
|
||||
// [废弃] 请使用 POST /rewards/ad-recovery/session + /rewards/ad-recovery/complete 两步流程。
|
||||
// 该接口不做幂等、每日上限和 Plus 分支检查,仅供内部测试或过渡期使用。
|
||||
app.post('/rewards/attempts/restore', async (request) => {
|
||||
const parsed = rewardSourceSchema.safeParse(request.body);
|
||||
if (!parsed.success) return validationError(parsed.error.issues[0]?.message);
|
||||
@ -136,6 +140,8 @@ export async function appApiRoutes(app: FastifyInstance): Promise<void> {
|
||||
return { success: true, data, error: null };
|
||||
});
|
||||
|
||||
// [废弃] 请使用 POST /rewards/ad-recovery/session + /rewards/ad-recovery/complete 两步流程。
|
||||
// 该接口不做幂等、冷却期和 Plus 分支检查,仅供内部测试或过渡期使用。
|
||||
app.post('/rewards/streak/protect', async (request) => {
|
||||
const parsed = rewardSourceSchema.safeParse(request.body);
|
||||
if (!parsed.success) return validationError(parsed.error.issues[0]?.message);
|
||||
|
||||
@ -38,6 +38,7 @@ export async function progressRoutes(app: FastifyInstance): Promise<void> {
|
||||
return { success: true, data, error: null };
|
||||
});
|
||||
|
||||
// [废弃] 请使用 POST /rewards/ad-recovery/session + /rewards/ad-recovery/complete 两步流程。
|
||||
app.post('/progress/hearts/restore', async (request) => {
|
||||
const parsed = restoreHeartsSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
|
||||
@ -44,6 +44,12 @@ export interface AdRecoverySessionResponse {
|
||||
expiresAt?: string;
|
||||
reason?: AdRecoveryReason;
|
||||
nextAvailableAt?: string;
|
||||
/** Plus 用户被拦截时返回订阅权益摘要,客户端可据此展示替代提示。 */
|
||||
subscriptionBenefits?: {
|
||||
tier: string;
|
||||
unlimitedHearts: boolean;
|
||||
dailyHighRewardSessions: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AdRecoveryCompleteResponse {
|
||||
@ -111,6 +117,21 @@ function isSubscribed(tier: UserTier | null | undefined, subscription: Awaited<R
|
||||
return tier === 'pro' || tier === 'proplus' || (subscription.status === 'active' && subscription.tier !== 'free');
|
||||
}
|
||||
|
||||
/** 获取 Plus 用户的订阅权益摘要,供广告恢复接口在被拦截时返回给客户端。 */
|
||||
async function getSubscriptionBenefits(userId: string): Promise<{ tier: string; unlimitedHearts: boolean; dailyHighRewardSessions: number | null }> {
|
||||
const [tier, subscription] = await Promise.all([
|
||||
getUserTier(userId),
|
||||
getSubscriptionStatus(userId),
|
||||
]);
|
||||
const effectiveTier = (tier ?? 'free') as UserTier;
|
||||
const isPlus = isSubscribed(effectiveTier, subscription);
|
||||
return {
|
||||
tier: isPlus ? (effectiveTier ?? 'pro') : 'free',
|
||||
unlimitedHearts: isPlus,
|
||||
dailyHighRewardSessions: isPlus ? null : 3,
|
||||
};
|
||||
}
|
||||
|
||||
async function getUserTier(userId: string): Promise<UserTier | null> {
|
||||
const [user] = await db
|
||||
.select({ tier: users.tier })
|
||||
@ -415,11 +436,17 @@ export async function createAdRecoverySession(userId: string, input: CreateAdRec
|
||||
|
||||
const eligibility = await checkEligibility(userId, input.type);
|
||||
if (!eligibility.eligible) {
|
||||
// Plus 用户不需要广告恢复,返回订阅权益摘要供客户端展示。
|
||||
const subscriptionBenefits = eligibility.reason === 'already_subscribed'
|
||||
? await getSubscriptionBenefits(userId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
sessionId: null,
|
||||
eligible: false,
|
||||
reason: eligibility.reason,
|
||||
nextAvailableAt: eligibility.nextAvailableAt,
|
||||
subscriptionBenefits,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user