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