MatchMaster/server/laf-cloud/starterPack.README.md

150 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 当前规则:付费因子分档(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` 固定商品、价格、奖励、起止时间和轮号;期间身份或画像改变不会重选。
- 下轮最早为原截止时间所属北京时间日期加 2 天的零点:9 月 23 日 02:00 到期,9 月 25 日 00:00 才能再触发。提前购买不改变这个截止时间。购买状态按轮次重置。
- 旧有效礼包保留原截止时间和原奖励(3000 金币、三个道具各 3、无限体力 1800 秒);旧已购用户无可信截止时间时可在下一次合格触发进入新轮。
`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"`,礼包快照位于响应顶层。前端从该订单的快照发奖,缺快照旧单走原奖励,不使用当前轮报价替换旧单。登录和账号迁移保留轮次及报价。
保留原跨设备并发下单、客户端发奖及旧 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 元兜底。
在项目根目录运行:
```powershell
$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` 订单的支付回调是否已到达。
本次没有发布云函数、修改线上用户数据或调用真实支付。
## 验证
```sh
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 首次事件说明](https://docs.thinkingdata.cn/ta-manual/v4.0/en/installation/installation_menu/server_sdk/nodejs_sdk_installation/nodejs_sdk_advanced/nodejs_sdk_advanced.html)。部署的 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 通过。支付、数数、数据库均使用模拟,未做线上联调。