From eced02209ce5fdf07d6841da5c18eb629c0e2604 Mon Sep 17 00:00:00 2001 From: huanghaipeng Date: Mon, 7 Sep 2026 20:14:03 +0800 Subject: [PATCH] fix: return pre-claim display state for automatic sign-in --- laf-cloud/functions/signInActivityInfo.ts | 15 ++++++------- laf-cloud/signInActivity.API.md | 24 +++++++++++--------- laf-cloud/signInActivity.TESTING.md | 2 +- laf-cloud/tests/sign-in-activity.test.mjs | 27 ++++++++++++++++++----- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/laf-cloud/functions/signInActivityInfo.ts b/laf-cloud/functions/signInActivityInfo.ts index 8e745d1..f4abd43 100644 --- a/laf-cloud/functions/signInActivityInfo.ts +++ b/laf-cloud/functions/signInActivityInfo.ts @@ -53,8 +53,12 @@ export default async function (ctx: FunctionContext) { } let claims = await getSignInClaims(uid, state.activityId); - const canAutoClaim = getSignInActivityStatus(state, claims.length, now) === "active" - && !claims.some(claim => String(claim.dateKey) === getChinaDateKey(now)); + // 展示字段保留自动领取前的快照,领取凭据单独返回。 + const claimedDays = new Set(claims.map(claim => Number(claim.rewardDay))); + const todayClaimed = claims.some(claim => String(claim.dateKey) === getChinaDateKey(now)); + const claimedCount = claims.length; + const status = getSignInActivityStatus(state, claimedCount, now); + const canAutoClaim = status === "active" && !todayClaimed; if (canAutoClaim) { // 等待领取落库后再返回;不在云函数响应结束后启动无人等待的后台任务。 const result = await signInClaim(ctx); @@ -62,16 +66,11 @@ export default async function (ctx: FunctionContext) { claims = await getSignInClaims(uid, state.activityId); // 并发请求可能已领取成功,以数据库记录为准;其他失败不伪装成成功。 if (result.code !== 1 - && !claims.some(claim => String(claim.dateKey) === getChinaDateKey(now)) - && getSignInActivityStatus(state, claims.length, now) === "active") { + && !claims.some(claim => String(claim.dateKey) === getChinaDateKey(now))) { return result; } } const todayClaim = claims.find(claim => String(claim.dateKey) === getChinaDateKey(now)); - const claimedDays = new Set(claims.map(claim => Number(claim.rewardDay))); - const todayClaimed = claims.some(claim => String(claim.dateKey) === getChinaDateKey(now)); - const claimedCount = claims.length; - const status = getSignInActivityStatus(state, claimedCount, now); return { code: 1, diff --git a/laf-cloud/signInActivity.API.md b/laf-cloud/signInActivity.API.md index 20e8575..c18545f 100644 --- a/laf-cloud/signInActivity.API.md +++ b/laf-cloud/signInActivity.API.md @@ -18,9 +18,9 @@ 1. 玩家进入主界面后调用 `signInActivityInfo`。 2. 如果返回 `data: false`,表示玩家尚未达到目标关卡,不展示活动。 -3. 符合领取条件时,服务端异步执行并等待领取落库,返回领取后的活动状态。 +3. 符合领取条件时,服务端保存领取前的展示快照,再异步执行并等待领取落库。 4. 如果 `data.todayClaim` 非空,前端检查其 `claimId` 是否已处理,未处理才发放 `todayClaim.rewards` 并保存处理标记。 -5. 按照 `status`、`canClaim`、`todayClaimed` 和 `rewards` 渲染活动。自动领取成功后 `canClaim: false`,不再需要调用 `signInClaim`。 +5. 按照领取前的 `status`、`canClaim`、`todayClaimed` 和 `rewards` 渲染活动。本次请求触发自动领取时 `canClaim: true`,当档奖励的 `claimed: false`,便于播放领取展示;前端完成发奖后更新本地状态,不再需要调用 `signInClaim`。当天再次请求会返回已领取状态。 当天重复请求会返回同一个 `todayClaim`,用于首次响应丢失后的重试;它不表示本次新增领取。必须按 `claimId` 去重,不能每次收到就发奖。此接口具有写入行为,不应被预加载或轮询当作纯查询使用。`todayClaim` 只返回当天记录,不包含跨日未处理奖励的补领机制。 @@ -62,7 +62,9 @@ interface SignInActivityInfoRequest { 用户没有活动状态时,本次请求会创建活动;已有活动状态时不会重置开始和结束时间。 -活动进行中且当天未领取时,本次请求会自动领取下一档奖励。当天已领、活动完成或过期时不新增领取记录。响应中的活动字段均为自动领取后的状态;数据库写入失败时返回失败或抛出服务端错误,不返回虚假的领取成功。 +活动进行中且当天未领取时,本次请求会自动领取下一档奖励。当天已领、活动完成或过期时不新增领取记录。`status`、`claimedCount`、`todayClaimed`、`canClaim`、`nextRewardDay` 和 `rewards[].claimed` 均为本次自动领取前的快照;`todayClaim` 为落库后的当天领取凭据。数据库写入失败时返回失败或抛出服务端错误,不返回虚假的领取成功。 + +例如已有 Day 1、2 记录,本次自动领取 Day 3:返回 `claimedCount: 2`、`todayClaimed: false`、`canClaim: true`、`nextRewardDay: 3`,Day 1、2 的 `claimed` 为 `true`,Day 3~7 为 `false`,同时 `todayClaim.rewardDay: 3`。当天再次请求则返回 `claimedCount: 3`、`todayClaimed: true`、`canClaim: false` 和 Day 3 的 `claimed: true`。第 7 次领取的首次响应同样保留 `status: active`,再次请求才显示 `completed`。并发请求可能均读到领取前快照,前端发奖始终按 `claimId` 去重,不能只依赖 `canClaim`。 ```json { @@ -75,22 +77,22 @@ interface SignInActivityInfoRequest { "triggerReached": true, "startAt": 1788278400000, "endAt": 1789488000000, - "claimedCount": 1, - "todayClaimed": true, + "claimedCount": 0, + "todayClaimed": false, "todayClaim": { "claimId": "71b4...9a2f", "rewardDay": 1, "rewards": [{ "type": "infinite_health", "count": 900 }] }, - "canClaim": false, - "nextRewardDay": null, + "canClaim": true, + "nextRewardDay": 1, "rewards": [ { "day": 1, "items": [ { "type": "infinite_health", "count": 900 } ], - "claimed": true + "claimed": false } ] }, @@ -109,10 +111,10 @@ interface SignInActivityInfoRequest { | `triggerReached` | boolean | 服务端根据 `users.levelAmount` 确认的关卡是否达标 | | `startAt` | number | 活动第 1 天 UTC+8 零点时间戳 | | `endAt` | number | 活动结束时间戳,不包含该时刻 | -| `claimedCount` | number | 已领取奖励天数,范围 0~7 | -| `todayClaimed` | boolean | 当前 UTC+8 自然日是否已经领取 | +| `claimedCount` | number | 本次自动领取前的已领取奖励天数,范围 0~7 | +| `todayClaimed` | boolean | 本次自动领取前,当前 UTC+8 自然日是否已经领取 | | `todayClaim` | object \| null | 当天领取凭据:`claimId: string`、`rewardDay: number`、`rewards: RewardItem[]`;当天无记录为 `null`,重复请求返回相同凭据 | -| `canClaim` | boolean | 当前是否可以领取下一档奖励 | +| `canClaim` | boolean | 本次自动领取前是否可领,用于展示领取过程,不代表响应后还能再次领取 | | `nextRewardDay` | number \| null | 下一档奖励序号;当前不可领取时为 `null` | | `rewards` | RewardDay[] | 完整七天奖励及每档领取状态 | diff --git a/laf-cloud/signInActivity.TESTING.md b/laf-cloud/signInActivity.TESTING.md index 1498bee..6c11e5d 100644 --- a/laf-cloud/signInActivity.TESTING.md +++ b/laf-cloud/signInActivity.TESTING.md @@ -71,7 +71,7 @@ action=prepare&uid=&scenario=<场景>&adminToken= ## 推荐测试顺序 -`signInActivityInfo` 现在会自动领取。以下每次准备 `day_N_ready` 后,直接请求活动信息,检查 `todayClaim.rewardDay`、奖励内容和 `canClaim: false`。当天再次请求应返回相同 `claimId`,前端不得重复发奖;完成和过期场景不得新增记录。检查数据库状态可使用 `signInTestAdmin inspect`,避免活动信息请求触发领取。 +`signInActivityInfo` 会自动领取,但展示字段返回领取前快照。以下每次准备 `day_N_ready` 后,直接请求活动信息,检查 `todayClaim.rewardDay`、奖励内容、`canClaim: true`、`claimedCount: N-1` 及当天奖励的 `claimed: false`,同时数据库应已有 N 条记录。当天再次请求应返回相同 `claimId`、`canClaim: false` 和当天奖励的 `claimed: true`,前端不得重复发奖。Day 7 首次响应为 `active`,再次请求为 `completed`;完成和过期场景不得新增记录。检查数据库状态可使用 `signInTestAdmin inspect`,避免活动信息请求触发领取。 1. `prepare locked`,验证未达标不展示。 2. `prepare eligible`,验证首次请求创建活动且重复请求不重置时间。 diff --git a/laf-cloud/tests/sign-in-activity.test.mjs b/laf-cloud/tests/sign-in-activity.test.mjs index 134d4c2..b4f3a03 100644 --- a/laf-cloud/tests/sign-in-activity.test.mjs +++ b/laf-cloud/tests/sign-in-activity.test.mjs @@ -178,9 +178,12 @@ test("activity info activates an eligible player and returns rewards", async () assert.equal(result.data.triggerLevel, 10); assert.equal(result.data.triggerReached, true); assert.equal(result.data.rewards.length, 7); - assert.equal(result.data.todayClaimed, true); - assert.equal(result.data.canClaim, false); - assert.equal(result.data.claimedCount, 1); + assert.equal(result.data.todayClaimed, false); + assert.equal(result.data.canClaim, true); + assert.equal(result.data.claimedCount, 0); + assert.equal(result.data.nextRewardDay, 1); + assert.equal(result.data.rewards[0].claimed, false); + assert.equal(state.claims.length, 1); assert.equal(result.data.todayClaim.rewardDay, 1); assert.deepEqual(result.data.todayClaim.rewards, config.rewards[0].items); assert.equal(typeof state.lastUserUpdate.signInActivity, "string"); @@ -205,6 +208,9 @@ test("automatic claims return the same receipt on retry and advance on a later d const retry = await withNow(now + 1000, request); assert.deepEqual(retry.data.todayClaim, first.data.todayClaim); assert.equal(retry.data.claimedCount, 1); + assert.equal(retry.data.canClaim, false); + assert.equal(retry.data.todayClaimed, true); + assert.equal(retry.data.rewards[0].claimed, true); assert.equal(retry.data.startAt, first.data.startAt); const later = await withNow(now + 2 * 86400000, request); assert.equal(later.data.todayClaim.rewardDay, 2); @@ -226,7 +232,7 @@ test("concurrent automatic and manual claims share the same daily unique record" assert.equal(state.claims.length, 1); for (const result of results.slice(0, 2)) { assert.equal(result.code, 1); - assert.equal(result.data.canClaim, false); + assert.equal(result.data.canClaim, true); assert.equal(result.data.todayClaim.claimId, state.claims[0].claimId); } }); @@ -401,8 +407,17 @@ test("test admin prepares every reward day for the same account", async () => { const claim = await activityInfo({ body: { uid: "u1", token: "secret" } }); assert.equal(claim.code, 1); assert.equal(claim.data.todayClaim.rewardDay, day); - assert.equal(claim.data.canClaim, false); - assert.equal(claim.data.status, day === 7 ? "completed" : "active"); + assert.equal(claim.data.canClaim, true); + assert.equal(claim.data.status, "active"); + assert.equal(claim.data.claimedCount, day - 1); + assert.equal(claim.data.nextRewardDay, day); + assert.deepEqual(claim.data.rewards.map(reward => reward.claimed), + config.rewards.map(reward => reward.day < day)); + assert.equal(state.claims.length, day); + const repeated = await activityInfo({ body: { uid: "u1", token: "secret" } }); + assert.equal(repeated.data.canClaim, false); + assert.equal(repeated.data.rewards[day - 1].claimed, true); + assert.equal(repeated.data.status, day === 7 ? "completed" : "active"); } })); });