server/laf-cloud/starterPack.README.md
guanchao e2a5c3d2f6 Squash local into main while preserving battle pass compatibility
Integrate starter pack timing, reactivation, purchase analytics, payment routing and atomic user IDs. Resolve iOS conflicts by preserving both battle pass delivery and starter pack snapshots. Add coexistence regression checks and record remaining compatibility risks. Validation: 118/125 tests passed; 7 existing failures remain, plus a separately reproduced pre-existing V2 task login failure.
2026-09-10 16:31:14 +08:00

10 KiB
Raw Blame History

幸运礼包:48 小时期限与重新激活

当前方案:后端负责活动有效期,奖励数量与实际发放由客户端负责。商品 ID 仍为 starter_pack。支付、查单、转单和领取接口恢复 a1d3bfe 之前的行为,不对新手礼包新增订单版本、奖励快照、价格固定或下单资格校验。

期限规则

  • limitedTimeEvent 的首次 save 在内部关卡 levelAmount >= 15 时保存服务器时间 + 48 小时,read 不主动开启未触发的礼包。
  • 旧礼包按访问时迁移:只延长未购买且在迁移时仍有效的旧记录,在旧截止时间上加 24 小时,即原触发时间 + 48 小时。
  • 已购买记录不重新开启;已过期记录只有通过下述重新激活动作才会重开。原 read/save 不重开过期礼包。重复或并发请求不会重复延长,也不会覆盖购买状态。
  • 用户字段 starter_pack 是截止时间戳(毫秒),starter_packState=1 表示已购买;starterPackVersion=2 仅标记期限已按 48 小时处理。缺失/1 表示旧期限,不再代表订单奖励。
  • 保留 login 和 wucaiMigration 对用户期限标记的保存,避免用户迁移丢失标记而重复加时。
  • limitedTimeEvent 返回 serverTime 供客户端校正倒计时。48 小时计算和迁移函数均内置在 limitedTimeEvent.ts,不依赖额外的 starterPackConfig 云函数;仍使用项目已有的 Utils 校验 token。

重新激活(2026-09-10)

新增 limitedTimeEvent 动作(event 仍为 starter_pack):

  • shown:客户端在实际展示礼包时传入当前截止时间 expiry(毫秒)。只有该期限与当前有效、未购买的礼包一致时,才记录服务器时间 starterPackLastShownAt;旧周期或已过期的展示通知不能推迟当前周期。
  • reactivate:礼包未购买、已触发且到期,且服务器当前时间距最近展示严格超过 48 小时,才重新保存当前时间 + 48 小时。保持原等级门槛。返回数据增加 reactivated 和 starterPackLastShownAt。

reason 的可选值与判断:low_coin(数据库 coinAmount < 500)、shop(客户端已进入商城)、level_purchase(客户端点击局内购买入口)、four_failures(客户端上报 failureCount >= 4 的整数且 level 与数据库 levelAmount 一致)。前端负责行为上报与连续失败计数,计数按账号和关卡保存在设备上,通关清零,单次挑战内重复失败不重复累计。后台没有独立的关卡失败结算,因此此处不是服务器权威验证失败次数。

老记录缺少展示时间时保守使用其原截止时间,需要从到期后再经过 48 小时。客户端附带 lastShownAt 用于补充尚未同步的本地展示时间,只能延后重开,且最多按服务器当前时间处理。数据库更新同时比对原截止时间、购买状态、期限版本、展示时间;金币触发额外比对金币,避免并发覆盖购买或在金币已变化时误触发。

首次重新激活和每次后续重新激活都沿用同一规则,不设置次数上限。提醒仍沿用每日首次进首页;不增加每次 login 弹窗,也暂不增加商城宣传标签。已经支付的旧订单继续原查单与领取流程。

本次后端只需发布完整 limitedTimeEvent.ts,无新增模块依赖;与新客户端配套发布。没有部署、修改线上数据或改动支付回调分流。

奖励与支付约定

前端发起 300 分、数量 1 的购买;所有未发放新手礼包订单在新版客户端统一发放 8000 金币和锤子/冻结/魔法棒各 8 个,不再区分新旧订单。不自动给已经发放的旧订单补差额。

普通支付查询的 pay_state=2 表示已支付待领取,pay_state=1 且 code=1 表示已领取。原 getOrderReward 返回 data="ok",客户端自行携带订单号去重。旧 iOS 客服查单成功会直接完成订单,客户端收到成功即发奖;登录补单继续沿用原行为。

到期禁购由客户端检查服务器截止时间并禁用入口实现;恢复原后端下单接口后,不再保证到期后无法直接创建订单。到期前已有有效订单可以继续支付。服务端不直接增加金币和道具,前端发奖仍有跨设备及中断恢复限制;本次没有引入新的服务端发奖事务。

发布范围

  • 活动期限修复只需发布最新 limitedTimeEvent.ts,并保留用户迁移时的期限标记。不再需要新建或发布 starterPackConfig;原独立模块文件已移除。
  • 如果已经部署 a1d3bfe:还需重新发布恢复后的 wx/orderPaySig、wx/iosorderPaySig、wx/KeFuInfo、wx/checkIos、wx/getOrderReward、wx/getPayInfo、wx/iosgetPayInfo 和 login,配套使用本次客户端。
  • 已存入历史订单的版本/快照字段无需批量清理,新版客户端不再使用这些字段。

若遇到 function starterPackConfig not found,表示云端旧版 limitedTimeEvent 引用了未发布的模块。用本次完整文件替换并发布后可消除该依赖。limitedTimeEvent 的错误日志是活动接口日志,不是 wx/payCallBack 发货回调日志;它本身不能说明 state=0 订单的支付回调是否已到达。

本次没有发布云函数、修改线上用户数据或调用真实支付。

验证

node --test laf-cloud/tests/starter-pack.test.mjs laf-cloud/tests/monthly-card-renewal.test.mjs

期限测试覆盖首次 48 小时、重复/并发请求、旧期限迁移、已购买/已过期、关卡和身份检查,以及购买状态与迁移竞争。月卡测试作为原支付流程的回归检查。原先针对订单版本和奖励快照的测试已移除,客户端另有原支付协议的发奖回归测试。

2026-09-10 回归:新手礼包 15 项、月卡 7 项通过。额外支付分流 9 项在提供 TEST_WX_PAY_NOTIFY_URL=https://sor779u2w8.sealoshzh.site/wx/payCallBack 后全部通过(网络、数据库与签名均由测试模拟);不提供该现有环境变量时,分流测试中地址断言会失败。本次没有修改支付分流实现或测试。

购买成功埋点 rookie_gift(2026-09-10)

仅在后端确认商品 starter_pack 支付成功后发送,前端发奖和弹窗不直接发送此事件。奖励、支付环境分流、下单资格与发奖协议沿用当前行为。

属性 类型与口径
buy_round 整数;订单所属的 48 小时活动周期。首次开启为 1,每次成功重新激活加 1;每日提醒、手动打开、read/save 和 24→48 小时迁移不递增。
time_left 非负整数秒;max(0, floor((订单快照截止时间-支付成功时间)/1000))。到期后的有效订单完成支付记为 0。
order_id 订单号,辅助核对与去重。

用户字段 starterPackRound 在活动更新时与期限一起写入,并加入并发更新条件。login、wucaiMigration 保留轮次。旧用户缺失轮次时,已有周期视为第 1 轮;部署前累计重开次数无法还原,下一次重开从第 2 轮继续。

原生 Android/iOS 下单和旧 iOS 客服下单保存 starterPackBuyRound、starterPackExpiresAt(仅埋点快照,不是奖励快照);checkIos 转单原样保留。订单跨周期支付时使用原订单快照,不读取新周期替换它。

时间来源:wx/payCallBack 使用后端首次确认成功时的服务器毫秒时间(当前原生回调没有接入明确的支付完成时间字段,因此微信回调延迟会影响结果);旧 wx/iosgetPayInfo 优先使用微信查单响应 success_time,无效则回退本次服务器确认时间。不是点击购买时间,也不是前端领取奖励时间。首次上报前在订单条件写入 rookieGiftPaidAt、rookieGiftBuyRound、rookieGiftTimeLeft,重试读取固定值。

部署前创建的待支付订单没有快照:创建时间落在用户当前活动周期内时使用当前期限和轮次,否则保守使用第 1 轮、剩余 0 秒。这类历史订单无法恢复准确的旧周期倒计时。

上报沿用现有 payment 事件的数数项目选择(isDebug 字符串 true 使用测试项目),accountId 为订单 openid,存在 distinctId 时一并发送。使用 trackFirst,以订单号作为 firstCheckId;重复回调、并发查单由数数按同一事件和订单号去重。数数首次事件默认约延迟 1 小时入库,验收不要只看实时事件列表:官方 Node.js 首次事件说明。部署的 thinkingdata-node 必须支持 trackFirst。

上报错误记录 rookie_gift 上报失败,不阻断支付确认或发奖。原生回调重试、带有已保存埋点快照的原生已支付查单、旧 iOS 成功查单会再次尝试发送。SDK 沿用现有 BatchConsumer;没有增加定时补报任务或持久消息队列,长期服务故障后如无后续回调/查单,仍需要人工补报。埋点快照写入失败时也只记录错误,需要后续成功回调重试。

发布顺序与范围:

  1. 先发布完整 limitedTimeEvent(新增轮次与共享埋点方法,仍只依赖既有 Utils)。
  2. 发布 wx/orderPaySig、wx/iosorderPaySig、wx/KeFuInfo、wx/checkIos、wx/payCallBack、wx/getPayInfo、wx/iosgetPayInfo。
  3. 发布 login 和 wucaiMigration,保留账号迁移后的轮次。

不用新建云函数,也无需修改前端或 UI。生产、测试环境分别更新各自的函数,继续保留现有支付回调分流配置。本地没有部署或触发真实数数事件。

新增模拟测试 rookie-gift.test.mjs 覆盖下单快照、跨周期支付、秒数取整/到期归零、重复与并发去重键、SDK/数据库异常、旧 iOS 转单及支付成功时间;starter-pack.test.mjs 增加轮次与多次实际曝光的区分。

本次验证:rookie-gift、starter-pack、payment-routing、monthly-card-renewal、login-wucai-state、wucai-migration 共 72 项,71 项通过。唯一失败为已有 pristine classification requires explicit unpaid state and no activity(wucai-migration.test.mjs:71),已使用修改前 HEAD 文件复现同一失败;本次不调整该迁移判定。10 个修改的 TypeScript 文件转译无语法诊断,git diff --check 通过。支付、数数、数据库均使用模拟,未做线上联调。