MatchMaster/server/laf-cloud/starterPack.README.md
2026-09-30 21:49:05 +08:00

16 KiB
Raw Blame History

当前规则:付费因子分档(2026-09-30)

本节取代下方历史记录中的 48 小时期限、每日提醒、永久已购和统一奖励规则。实现位于本仓库的 server/laf-cloud/functions,继续扩展现有新手礼包支付、查询、领取和客户端发奖流程。

档位与报价

商品 ID 价格 金币 冻结/锤子/魔法棒各 无限体力 倒计时
starter_pack 3 元 3000 1 30 分钟 12 小时
starter_pack_2 6 元 3000 1 30 分钟 6 小时
starter_pack_3 12 元 4500 3 45 分钟 6 小时
starter_pack_4 18 元 5000 5 60 分钟 6 小时
starter_pack_5 30 元 8000 8 180 分钟 6 小时

没有 starter_pack_1。pay_user=false 使用 3 元;pay_user=true 使用付费公式,缺失或非布尔身份不开新轮。上线前由业务校准 pay_user。

取 userRechargeStats.spending.daily_reference_fen 为 d,ticket_factors.lucky.anchor_fen 为 A,计算 S=max(d,0.7A),在 600/1200/1800/3000 分中取不超过 S 的最高档,最低 600 分。公式不受历史成功价格、拒绝次数或降档机制影响。因子沿用 rechargeStats 最近 30 天订单的昨日画像;只接受当前规则版本、有效且身份一致的画像,截止时间须为今日北京时间零点或滞后一批。缺失、无效、过旧或读取异常时新轮固定 6 元。

paymentProfile 版本为 payment_profile_v2_20260930。3 元 SKU 仍使用 0.25 权重;新常规 SKU 在 lucky 因子、activityType=lucky 且已验证奖励版本 starter_pack_v3_20260930 时使用 1,其他上下文或未知版本仍为 0.25。

轮次与触发

  • 等级至少 15。金币小于 500、普通体力为 0 且无限体力无效、三个道具各不超过 3、商城/局内购买意向、真实购买行为、当前关卡连续失败至少 4 次,任一满足即可。失败次数仍由客户端按账号和关卡计数,后端校验关卡和整数次数。
  • 新轮只在满足触发条件且可安全展示的界面开始计时。普通读取不启动。购买意向恢复入口,不强制弹窗;资源提醒每轮最多自动展示一次,玩法、支付及领奖过程延后处理。
  • 本轮 starterPackOffer 固定商品、价格、起止时间和轮号;期间身份或画像改变不会重选。奖励由客户端按商品 ID 固定配置。
  • 下轮最早为原截止时间所属北京时间日期加 2 天的零点:9 月 23 日 02:00 到期,9 月 25 日 00:00 才能再触发。提前购买不改变这个截止时间。购买状态按轮次重置。
  • 旧有效礼包保留原截止时间并按 starter_pack 使用客户端固定的 3 元奖励;旧已购用户无可信截止时间时可在下一次合格触发进入新轮。

limitedTimeEvent 保留 read/save/reactivate/shown 动作;save/reactivate 需 reason。返回新增 starterPackOffer 和 starterPackAutoShownRound。shown 传本轮 expiry 和 automatic;支持客户端表单的字符串编码。starterPackLastShownAt 保留作为记录,不再决定冷却。

支付及兼容

现有下单接口仅对上述五个 SKU 校验当前有效轮次、未购买状态与商品匹配,并固定服务端报价和数量 1。订单保存不含奖励内容的 starterPackOffer、starterPackBuyRound、starterPackExpiresAt、activityType、rewardVersion,旧 iOS 转单继续携带这些字段。

新订单到期后不可创建;到期前的有效订单可继续原支付流程。回调只关闭所属轮次,不关闭后开的礼包。查单携带商品和轮次快照;getOrderReward 仍返回 data="ok",礼包快照位于响应顶层。前端只按订单商品 ID 读取本地固定奖励,不读取后端奖励字段。登录和账号迁移保留轮次及报价。

保留原跨设备并发下单、客户端发奖及旧 iOS 响应中断的边界,未新增订单占位锁或服务端发奖事务。礼包待支付订单不再按原通用 3 小时期限清理,以免删除仍可能支付的旧单;关闭但未成功的礼包订单也会保留,不因此发奖。退款不在本次范围内。

rookie_gift 支付成功埋点扩展至五个 SKU,沿用订单级去重;轮次和剩余秒数取原订单快照。

发布与验证

本次只做本地代码修改和模拟验证,没有部署或真实支付。发布时先更新 paymentProfile,再更新依赖它的 limitedTimeEvent,随后更新现有 wx/orderPaySig、wx/iosorderPaySig、wx/KeFuInfo、wx/checkIos、wx/payCallBack、wx/getPayInfo、wx/iosgetPayInfo、wx/getOrderReward、login、wucaiMigration,最后发布配套客户端。无需新建云函数。新画像生成前,付费新轮会按 6 元兜底。

在项目根目录运行:

$env:TEST_WX_PAY_NOTIFY_URL = 'https://sor779u2w8.sealoshzh.site/wx/payCallBack'
node --test server/laf-cloud/tests/starter-pack.test.mjs server/laf-cloud/tests/payment-profile.test.mjs server/laf-cloud/tests/rookie-gift.test.mjs server/laf-cloud/tests/payment-routing.test.mjs server/laf-cloud/tests/login-ios-reborn.test.mjs
node --test tools/test-starter-pack.cjs

本次前端 51 项、后端专项 74 项通过;月卡附加回归经 TypeScript ESM 测试加载器运行,7 项通过。数据库、微信支付和数数均由测试模拟,真机显示及渠道支付需配套发布后验收。

扩展回归:login-wucai-state 为 33/34,通过修改前 HEAD 复现同一 CloudRise 失败;recharge-stats 为 24/39,HEAD 同样有 15 项失败,集中于旧版本断言和模拟库缺少 deleteMany。未改无关逻辑或测试。19 个修改的 TypeScript 文件与 HEAD 相比没有新增语法诊断;原 Utils.ts 的 getGacha 名称中不可见字符仍造成 21 条诊断,所以不宣称项目全量编译通过。


历史记录(以下旧规则已由上文取代)

幸运礼包: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 通过。支付、数数、数据库均使用模拟,未做线上联调。