server/laf-cloud/functions/cloudRise/README.md
2026-09-24 17:59:52 +08:00

237 lines
28 KiB
Markdown
Raw Permalink 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.

# 百人赛 cloudRise 接入说明
2026-09-23 百人赛单独上线,以 [百人赛生产发布清单](../../百人赛生产发布清单.md) 为准:相对线上 main 新增 8 个函数/模块,更新 login;黄金矿工和新版每日/周任务暂不发布。当前活动列表只返回百人赛,首次上线需要准备 activityConfigs、cloudRiseSamples、cloudRiseRuns 三个集合。样本仅使用每人每期一条的 samples 数组结构,不提供旧样本迁移。
当前配置已统一存入 `activityConfigs`(activityId=cloudRise),使用不可变版本。发布示例见 [完整配置请求](cloudRise-config.publish.example.json),公共规则与黄金矿工示例见 [公共配置说明](../activityConfig/README.md),已有环境升级先执行 [数据迁移](../activityConfig/MIGRATION.md)。
对应客户端 PRD:`MatchMaster/docs/cloudRise-PRD.md`(V1.1)。客户端与本仓库需配套发布。
## 登录开启个人活动期
百人赛改为按玩家首页请求开期。登录时读取 users.cloudRisePeriod;若当前期未到期,且对应挑战尚未失败、过期或完成三阶段,则保持期号和截止时间。没有可用期时创建新期,即使上一期原截止时间尚未到达。首次登录且未达到解锁关卡的玩家也建立个人期,原解锁门槛仍用于报名。
- 配置取 activityConfigs 中 activityId=cloudRise、status=published 且已生效、已发布的最新版本,按 effectiveFrom/publishedAt 选取。使用 durationHours、unlockLevel、pools 作为模板;旧 config.startsAt/endsAt 不再限制个人活动开放。没有模板时不开期,无效模板记录登录错误;两者都不阻断基础登录。
- 版本记录外层 `enabled` 控制登录是否创建新个人期。先选最新已发布且已生效版本,再判断 `enabled`;`false` 时不开新期,也不回退旧版本。缺省按 `true` 兼容。已有未结束个人期仍可报名、推进、结算,并继续由 activityConfig/list 返回。新开期仅在后续登录触发。
- users.cloudRisePeriod 保存 periodId、configVersion、startsAt、endsAt、durationHours、unlockLevel、pools。periodId 每人每期唯一,startsAt 为首页请求开期时间,endsAt = startsAt + durationHours 小时。重复及并发登录通过原子条件更新避免重复开期。
- 登录只开期,不匹配、不占用参与记录。点击「开始」才准备并确认匹配,run.expiresAt 沿用个人期 endsAt,不重新开始倒计时。后续阶段仍手动开始。
- 开新期不覆盖 cloudRiseState,不丢弃旧奖励回执、待保存奖励和统计事件;旧轮次继续按原流程结算、归档后再参与新期。上线前已有未结束挑战沿用原期号与截止时间。
- activityConfig/list 携带 uid/token 后返回当前玩家仍开放的个人期;挑战结束或到期后不再返回这一期。列表、status 和首页刷新均不创建下一期;下一次成功登录才创建。
- 登录响应不透传 cloudRiseState/cloudRisePeriod,对外活动信息仍从列表和 cloudRise/index 读取。
已有百人赛环境升级个人期时:先发布 activityConfig/store 与新增内部模块 cloudRise/periods(methods: []),再同步发布 login、cloudRise/index、activityConfig/list 及配套客户端,无需额外新集合或批量改玩家数据。首次上线须按发布清单准备三个集合。本地改动不代表已经部署。
## 目录与发布
百人赛专属文件统一位于 `laf-cloud/functions/cloudRise/`:
```text
cloudRise/
index.ts / .yaml 玩家接口
admin.ts / .yaml 运营配置接口
rules.ts / .yaml 内部活动规则模块
stats.ts / .yaml 生成的内部统计数据模块
periods.ts / .yaml 个人活动期查询与显式开期模块
analytics.ts / .yaml 内部埋点模块
data/cloudRiseLevelStats.json
tools/generate-cloud-rise-stats.mjs
tests/cloud-rise.test.mjs
cloudRise-config.example.json
README.md
```
另外必须发布目录外的 `activityConfig/store.ts/.yaml`、`activityConfig/list.ts/.yaml`。
采用现有 wx 目录的 Laf 命名约定,六个云函数名称分别为 `cloudRise/index`、`cloudRise/admin`、`cloudRise/rules`、`cloudRise/stats`、`cloudRise/periods`、`cloudRise/analytics`,与 YAML 的 name 一致。内部通过 `@/cloudRise/...` 引用模块;客户端玩家接口需同步使用 `cloudRise/index`。发布时先发布内部依赖,再发布玩家与管理接口。
`rules` 仅供内部导入常量与规则函数,没有默认请求处理函数,YAML 使用 `methods: []`。它与统计模块都需发布以支持内部导入,但不作为 HTTP 接口开放;对外玩家接口为 `cloudRise/index`,运营接口为校验管理密钥的 `cloudRise/admin`。
`stats` 只导出 `LEVEL_STATS`,没有默认请求处理函数,YAML 使用 `methods: []` 关闭 HTTP 调用;仍需发布该模块供 `@/cloudRise/stats` 内部导入。生成器不会再生成对外处理函数,`--check` 校验导出与数据内容并允许格式化。若线上已发布旧版本,需要同步更新其 HTTP 方法配置。
共用的 `login`、`userCoin`、`jungleTreasure` 保留在原目录,继续使用原有登录、金币保存与丛林宝藏结算逻辑。数据库集合和存档格式不因本次目录整理而改变。此次仅修改本地文件,没有部署或删除线上旧接口。
## 云函数与数据
### 匹配关卡来源
客户端在 prepare_match/start/start_stage 中直接携带本地 GM_INFO.level,字段名为 levelAmount(非负安全整数,支持表单数字字符串)。服务端使用 levelAmount + 1 作为该阶段 start_level 和对手抽样中心;不修改 users.levelAmount,也不要求先调用 userLevel 上传进度。玩家进度不受对手样本起点 1960 的限制。
省略字段的旧客户端或旧离线队列仍回退到数据库进度;显式非法值返回“当前关卡数无效”。同一 matchId 重试及 confirm_match 使用已保存草稿,不会重新抽样;取消后使用新 matchId 可按最新本地进度重新匹配。活动资格、通关和奖励规则保持原状。
发布顺序:先更新 cloudRise/index 玩家接口,再发布配套客户端,避免旧服务端忽略新字段。
### 对手数据与精简归档
- 正式对手只存于 users.cloudRiseState.run.stages,各阶段各保存自己的 99 个对手。第三阶段时整轮合计 297 条,但单个阶段始终只有 99 条;仍保留历史阶段名单用于结算展示和恢复。
- pendingMatch.run 保留轮次元数据,stages 只含本次待确认阶段,不复制此前阶段。确认时将新阶段追加到最新正式 run,保留匹配期间到账的奖励记录;取消仅删除草稿。旧格式草稿在下次成功请求时自动精简,不重新匹配。
- 新写入 cloudRiseRuns 的终局归档不再保存各阶段 opponents,其余进度、奖池、存活人数、奖励回执和结果记录保留。users 中的正式存档不受归档精简影响,cloudRiseSamples 保持原状。
- 更新 cloudRise/index 与 cloudRise/admin 后,可通过管理员接口 /cloudRise/admin 执行历史归档精简:传 action=compact_archives、adminToken(ADMIN_TOKEN)、limit(默认 100,最多 500),后续请求将返回的 nextCursor 作为 afterId,直至 nextCursor=null。失败可原参数重试,也可从头重跑;仅移除终局归档的 stages[].opponents,不删除整条归档、不改玩家金币或正式存档。此操作会永久移除旧归档内的对手副本,如需历史名单审计应先备份。
- 本次仅提供代码与管理入口,不自动执行数据库清理;新归档无需迁移,旧客户端接口响应结构保持兼容。
### 可取消匹配
匹配采用两阶段提交,先发布更新后的 `cloudRise/index`,再发布客户端。原 `start/start_stage` 保留供旧客户端使用;新客户端不再通过这两个操作进入匹配。
- `prepare_match`:传 `matchId`、`stage`、`levelAmount`(前端已通过的主线关卡数);第一阶段传 `periodId`,后续传 `runId`。仅在 `users.cloudRiseState.pendingMatch` 保存待匹配名单,响应 `data.matching = { id, stage }`。不占用 `playedPeriods`,不改动当前 run,不产生失败样本。
- `cancel_match`:传 `matchId`。删除对应草稿,并记录取消标识,防止在途准备请求通过并发重试恢复已取消匹配。可以重复调用;旧标识的取消不会删除新的匹配。报名窗口结束后仍可清理草稿。
- `confirm_match`:匹配人数达到 100/100 后传 `matchId`。检查草稿、资格和活动截止时间后正式创建/推进 run。第一阶段沿用首页请求开期时的截止时间;后续阶段保留已获奖励、进度和原截止时间。重复确认不会清空游戏进度。
玩家在达到 100/100 前取消,可在报名窗口及个人挑战有效期内重新匹配。客户端持久化取消队列处理断网和重启;未确认草稿即使未及时清理,也不会消耗参加机会。匹配阶段取消不等于已进入关卡后的放弃。
| 文件/集合 | 用途 |
| --- | --- |
| `index.ts` | 玩家报名、手动开始下一阶段、开局、胜负上报与状态恢复 |
| `rules.ts` | 三阶段状态、截断离散正态权重、样本抽取、通关模拟 |
| `stats.ts` | 由 JSON 生成的云函数可导入数据模块 |
| `admin.ts` | 活动期配置与索引初始化 |
| `data/cloudRiseLevelStats.json` | CSV 解析后的原始统计,保留 1~2000 关;模拟起点上限 1960 |
| `activityConfigs` | 用 activityId=cloudRise、recordType=version 保存每期配置版本,config 内保存开期模板,个人活动期冻结时长、门槛和奖池 |
| `users.cloudRisePeriod` | 首页请求创建的个人期与配置快照,独立于挑战存档 |
| `users.cloudRiseState` | JSON 字符串;一次资格、当前轮次、阶段快照、尝试编号、真实结算的权威记录 |
| `cloudRiseSamples` | 每个玩家每期一条记录,samples 数组保存最多三个已结算阶段;相同成绩的不同参赛记录不去重 |
| `cloudRiseRuns` | 整轮完成、失败或超时后的归档 |
阶段目标固定为 5/7/9,一轮时长由 durationHours 配置(默认 24 小时)。各阶段奖池通过 `activityConfigs.config.pools` 配置,例如 `[10000, 15000, 20000]`,按 stage1/2/3 排列,必须是三个正安全整数。旧配置缺少该字段时沿用默认 10000/15000/20000;管理接口发布时会写入默认值。
首页请求开期时冻结全部三个奖池,报名时复制到 `run.pools`,各阶段的 `pool` 从该快照取得。新配置只影响之后新建的个人期;已有个人期继续使用原快照。旧轮次没有 `pools` 时,使用已存在的阶段 `pool` 和其余阶段的旧默认值,不套用新配置。奖励仍由客户端按阶段奖池和存活人数计算,服务端负责保存。
样本文档 `_id = runId`,runId 由玩家 uid(users.onlyId 的字符串)和 periodId 确定。每完成一个真实阶段就立即写入,未完成任何阶段时不创建样本文档;无需等整轮结束。新结构示例:
```json
{
"_id": "<runId>",
"schemaVersion": 2,
"uid": "1001",
"runId": "<runId>",
"periodId": "2026-09-15",
"samples": [
{ "stage": 1, "start_level": 100, "success_num": 5, "outcome": "won", "reason": "win", "endedAt": 1789459200000 },
{ "stage": 2, "start_level": 105, "success_num": 2, "outcome": "lost", "reason": "lose", "endedAt": 1789462800000 }
]
}
```
根字段使用 `$setOnInsert` 初始化,随后通过 `samples.stage != 当前阶段` 的原子条件追加。每个阶段最多一条,数组最多三条;并发重试和旧快照补写不会重复追加,也不会覆盖后续阶段。不同期使用不同文档,不累计成玩家的无限历史数组。
匹配只读取 schemaVersion=2 的 samples 数组,按阶段筛选后统计 start_level + success_num 的次数;每个阶段只写一次,同一成绩来自不同玩家或不同期时仍分别计权。不再读取旧平铺样本。
超时未完成阶段只保存在轮次中,不加入抽样集合。进行中阶段超时时按 success_num + 1 结算当前轮对手,更新 round 和 survivors(不含玩家本人),供前端播放晋级到下一朵云及淘汰掉落动画;玩家通关数不增加,不生成关卡尝试或奖励。阶段已获胜但等待下一阶段时超时,保留已获胜阶段及奖励快照。成功阶段的样本在后续阶段失败或超时后仍保留。
## 初次配置
1. 按本次发布清单发布新增云函数及对应 YAML,更新 `login`。相对 main,`userCoin` 和 `jungleTreasure` 没有变化,无需重复发布。
2. 设置服务端环境变量 `ADMIN_TOKEN`,仅用于运营配置,不下发客户端。
3. 对 `cloudRise/admin` 发 POST:`{ "action": "setup_indexes", "adminToken": "<运营密钥>" }`。
4. 参考 `cloudRise-config.example.json` 设置 `periodId/startsAt/endsAt/durationHours/unlockLevel/pools`。时间均为 Unix 毫秒。
5. POST `cloudRise/admin`:`{ "action": "save", "adminToken": "<运营密钥>", "configVersion": "cloudRiseTestV1", "enabled": true, "config": { ... } }`。startsAt 必须在未来;版本标识同一活动内唯一,重复同内容可安全重试,不能覆盖已有版本。
活动参数 config 内不再使用 status/enabled;公共记录外层 status=published 用于区分发布版本,外层 enabled 为布尔值,用于控制首页请求新开期(不放在 config 内)。save 接受顶层 enabled,缺省 true,read/save 响应返回 enabled。首页开期请求按最新已生效模板创建个人期,publishedAt 保留用于发布记录和版本排序。管理接口仍按原格式接受 startsAt/endsAt,effectiveFrom 等于 startsAt;endsAt 不再决定玩家个人活动期的结束时间。
save 不再覆盖当前期,必须指定新的 configVersion 发布未来配置;同一期起止时间固定,已开启期禁止修改。read 带 periodId 返回该期最近发布版本,不传则返回当前开放期;返回中增加 configVersion。原 migrate_config 操作已停用,按公共迁移脚本迁入 activityConfigs。setup_indexes 建立带 activityId 的公共版本索引,避免两个活动同名版本互相冲突。
示例中的配置为联调数值,不会自动发布。发布新模板仍需未来生效时间及新的配置版本;已经生效的模板会持续用于首页请求开期,直到新版本生效。持有未结束个人活动期的玩家登录不会重开。若直接维护数据库,可将当前最新已生效 version 记录的外层 enabled 改为 false 暂停新开期,改回 true 后在满足冷却条件的下一次首页开期请求时恢复;管理 save 仍遵守版本不可覆盖的规则,新发布版本应明确指定 enabled。
## 测试环境短时活动
服务端环境变量 PAYMENT_APP_ENV=test 时,durationHours 允许正数小数小时,例如 0.1 为 6 分钟(360000 毫秒)。正式环境或未设置该变量时仍要求至少 1 个整数小时。小数时长须能表示为至少 1 个整数毫秒;0、负数、非数字和溢出值仍被拒绝。请求中的 isDebug 不控制这项放宽。
配置校验、首页请求开期和玩家活动接口使用同一规则。先发布更新后的 cloudRise/periods,再发布 cloudRise/index;管理接口调用 index 的校验,无需更改请求格式。确认测试服务器已设置上述环境变量。将模板改为 0.1 后,只有新建个人期使用 6 分钟;已有未结束个人期保留原截止时间,不会被追溯缩短。
## 挑战时长与旧配置迁移
`durationHours` 正式环境为正整数小时,测试环境可使用上述小数时长,缺省为 `24`。首页请求开期时保存个人起止时间;匹配后写入 run.durationHours,并将 run.expiresAt 设置为个人期 endsAt。阶段切换和模板更新不会延长倒计时;旧轮次保留原 expiresAt。
旧的 migrate_config HTTP 操作已停用。请按 [公共集合迁移说明](../activityConfig/MIGRATION.md) 先将旧配置转入 activityConfigs;脚本保留原 durationHours,缺省补 24,不修改玩家个人截止时间或历史轮次。
前端在未参加、挑战中、等待下一阶段显示同一张总览说明图;同一期全部完成、失败或超时隐藏首页入口,下次登录重新开期;待播放结算和奖励补存独立处理。独自获胜仍显示“你和其他 0 名玩家分享了获胜奖励”。
## 玩家接口
所有请求通过现有 `Utils.POST` 携带 `uid` 和登录 `token`,地址为 `cloudRise/index`。这里的 uid 为 `users.onlyId`(正整数或其十进制字符串),客户端取登录后的 `GM_INFO.userId`;服务端用户查询、CAS 更新和候选池排除均使用 onlyId,样本、归档的 uid 统一保存为其字符串。Mongo `_id` 不作为百人赛玩家标识,其他接口仍沿用原有账号参数。
| action | 额外字段 | 行为 |
| --- | --- | --- |
| `status` | 无 | 恢复状态、检查超时、重试样本归档 |
| `start` | `periodId` | 本期首次报名并匹配 stage1;重复请求恢复同一名单 |
| `start_stage` | `runId`, `stage` | 仅在前一阶段成功后的 waiting 状态开始后续阶段 |
| `begin` | `runId`, `stage`, `attemptId` | 登记本关首次有效操作;前一关未结算不能开始另一尝试 |
| `finish` | 同上,加 `outcome` | `win`、`lose` 或 `interrupted`;重复编号不重复推进;成功只记录阶段结果 |
| `save_reward` | `runId`, `stage`, `reward`, `coinAmount` | 保存客户端计算的奖励、加奖后的余额和阶段 rewardSaved 标记;重复上报不再次写余额 |
未找到当前开放配置且玩家没有个人轮次记录时,返回 `{ "code": 0, "data": null, "msg": "活动未开启" }`,不返回目标、奖池或其他活动数据。已有个人轮次仍可恢复状态、继续个人期限内的挑战及重试结算,不受报名关闭影响;无当前时段配置时 period 为 null,targets/pools 返回个人轮次使用的目标和奖池。
成功返回 `code: 1`,`data` 含 `serverNow/available/period/targets/pools/run/coinAmount/sampleSyncPending`;失败返回 `code: 0` 与提示文案。
未报名时 data.pools 使用当前活动配置;展示个人轮次时使用 run.pools,已结算旧轮次让位给新一期时切换至新配置。period.pools 始终对应该 period 的配置。
公开阶段信息包含进度、存活数、奖励以及对手的昵称、头像、存活状态,不返回对手未来成绩和起点。登录接口也移除了原始 `cloudRiseState`,金币恢复沿用原有登录流程。
`start_stage` 必须由活动页面按钮触发。成功结算、关闭弹窗、返回首页、继续主线和状态查询都不会隐式开始下一阶段。
阶段等待期间的主线通关不计入下一阶段;点击按钮前客户端先同步主线关卡,服务端用当前 `levelAmount + 1` 确定起点。
## 前端发奖与服务端保存
当前版本遵循客户端发奖方案,后续再统一迁移到服务端发奖。阶段成功时服务端仅保存进度、存活人数和成功状态,不计算奖励、不增加金币。
客户端 `CloudRiseRuntime` 根据 `Math.ceil(stage.pool / stage.survivors)` 算出奖励,上报奖励金额及 `当前客户端金币 + 奖励`。服务端验证账号、阶段成功状态和整数格式,通过同一次用户文档更新保存 `coinAmount`、`stage.reward` 和 `stage.rewardSaved = true`,不进行余额加法或重算奖励金额。同一阶段已保存时直接返回现有状态,不覆盖之后的金币余额。
前端收到保存成功后把奖励加到本地金币并保存本地余额;请求期间有普通金币变化时,再走原有 userCoin 保存最新余额。保存失败时显示待到账并重试;同一会话响应丢失时保留待确认记录,重试成功只加一次。重新登录沿用已有的金币恢复流程,服务端 rewardSaved 标记阻止再次领取。个人期限已过但之前成功阶段的奖励尚未保存时,仍允许补存,并在补存完成前阻止替换旧轮次。
每次活动修改对 `users.cloudRiseState` 的完整旧值进行 CAS,首次写入用 `$exists: false`;冲突最多重试 5 次。终局归档等待所有成功阶段的奖励已保存,确保归档包含上报金额和保存标记。
`userCoin` 和 `jungleTreasure` 已恢复原有行为;不再使用 cloudRiseCoinTotal 或累计奖励补差,也不为百人赛改写通用金币存档与丛林结算规则。跨接口旧余额覆盖问题随未来统一服务端发奖方案处理,本版不实现跨活动余额保护。
样本或归档写入失败时,用户活动记录作为补写来源,响应标记 sampleSyncPending;后续活动请求继续重试,没有独立定时扫描任务。
## 匹配与兜底
- 资料候选池:`users.taskTime` 大于 0 且早于当前时间减三个日历月,排除玩家本人,随机取最多 500 条。月底日期向目标月月末收敛。
- 有候选资料但不足 99 条时可重复使用;头像昵称抽取与成绩抽取独立。头像保留 `useravatarIcon` 字符串,兼容 `icon_9` 和远程头像。
- 候选池为空时返回匹配失败,首次报名不消耗资格;已生成名单永不通过重试重抽。
- 起点权重为进入人数乘离散正态权重,先在正整数上确定截断分布标准差,再对 1~1960 范围的乘积归一化。
- 有真实样本时按样本次数抽取;没有样本时逐关 Bernoulli 模拟,首次失败结束。
- 缺失通关率取最近较低的有效关卡通关率;当前 1921 及以上使用 1920 关的 0.818。原始 JSON 的缺失值保持 null。
## 本地验证
```sh
node laf-cloud/functions/cloudRise/tools/generate-cloud-rise-stats.mjs --check
node --test laf-cloud/functions/cloudRise/tests/cloud-rise.test.mjs
```
测试使用隔离的内存数据库替身,执行真实活动处理函数、抽样规则、币额更新与 CAS 冲突;其中一项直接加载相邻 `MatchMaster` 仓库的实际客户端服务进行联调,需要该仓库的 TypeScript 依赖。
覆盖每玩家每期样本合并、并发追加、补写失败恢复、数组样本按阶段统计、整轮三阶段、手动推进、每期一次、超时、重复结算、零分与重复样本、资料池空、样本写入失败、客户端计算上报、并发重复保存、过期补存和前后端联调。
本地预览和测试均不连接生产账号,不会自动开放或部署活动。
### 新旧活动期切换
当前开放配置与玩家个人轮次分别判断:旧轮次仍在挑战中,或有成功阶段奖励待保存时,继续返回旧 run,禁止同时报名新一期。旧轮次已完成、失败或超时,且奖励均已保存时,如果当前开放 periodId 不同,接口返回新一期 period、run: null 和对应 available,不再把旧结算当作当前活动。旧 run 仍保存在 users.cloudRiseState 供重试,并归档到 cloudRiseRuns;成功报名新一期后才替换存档,playedPeriods 保留。新一期首次 start 请求也可以先结算旧轮次超时,再创建新轮次。每一期必须使用不同 periodId。
换期响应的 previousRun 是已结算旧轮次的恢复记录,仅用于确认奖励保存重试(例如保存成功但客户端丢失响应),不用于当前页面和自动结算弹窗。客户端需先完成该恢复,再报名新一期;不能仅因 run 为 null 就丢弃待确认的奖励。
奖励保存参数兼容客户端 `application/x-www-form-urlencoded`:`reward`、`coinAmount` 可传十进制整数字符串或 JSON 数值。服务端先严格转换,再检查安全整数、正奖励及余额不小于奖励,并按数值类型持久化;空值、布尔值、数组、小数和越界整数不会被宽松转换。联调测试执行实际 `Utils.POST` 编码,覆盖表单上报、重复保存和丢失响应恢复。
`unlockLevel` 表示必须已通过的主线关卡数。例如设为 110,`users.levelAmount >= 110` 才可首次报名;客户端使用对应的 `GM_INFO.level`,未达到时首页点击提示“月光宝盒活动通过第110关后解锁”,不打开活动弹窗。已报名轮次的继续挑战和结算不受后续门槛调整影响。
### 百人赛数数埋点
埋点由后端生成并发送至数数,不在客户端重复上报,也不向客户端暴露机器人的预测终点人数。先发布内部模块 cloudRise/analytics(methods: []),再发布 cloudRise/index;使用服务器已有 thinkingdata-node 依赖。
| 事件 | 触发时机 | 属性 |
| --- | --- | --- |
| cloud_rise_start | confirm_match 确认匹配;兼容 start/start_stage | stage: int 1–3;end_count: int,99 个机器人中 success_num >= target 的数量,不含玩家 |
| cloud_rise_step | 每个真实关卡结果成功保存 | stage: int;step: int 1–5/7/9;result: success/failure |
| cloud_rise_fail | 阶段首次失败 | stage: int;step: int,失败所在步骤 |
| cloud_rise_succeed | 阶段胜利且 save_reward 首次保存奖励回执 | stage: int;coin_amount: int,实际保存的阶段奖励,不是账户总余额 |
- 匹配草稿与取消匹配不发 start;begin 不发 step;重复请求、状态轮询不生成新事件。放弃、重试、关卡倒计时失败及断线中断均按已保存的失败结果发送 step(failure)+fail。个人活动期限到期仅发送 fail,不虚构一次关卡完成;已获胜阶段等待下阶段时到期,不补发该获胜阶段的 fail。
- 环境沿用现有客户端协议:isDebug=true 或字符串 true 实际代表正式版,AppID=95993f9ab6f1402a87abe5147827e5e0;false 代表测试版,AppID=40e3d5c5f2af49a4a074205564ce5dbb。请求省略该字段时回退到用户保存的 isDebug。待发送事件固定原 AppID,重试不切换项目。
- 事件随 gameplay 状态原子保存到 users.cloudRiseState.analyticsQueue;上报成功后通过 CAS 清除。上报失败或超时不影响活动结果,后续成功调用活动接口时重试。队列只保存小型事件属性,不复制对手列表;没有 openid/distinctId 时保留待报,不伪造用户身份。
- 发送采用 trackFirst,firstCheckId 由轮次、阶段、匹配批次、事件及尝试编号确定,防止并发和回包丢失造成重复计数。没有定时补报任务,玩家不再调用接口时待报事件会继续保留。
- SDK 显式 flush,等待最多 1 秒;数数后台首次事件去重处理可能延迟入库,详见 [数数首次事件说明](https://docs.thinkingdata.cn/ta-manual/v4.0/en/installation/installation_menu/server_sdk/nodejs_sdk_installation/nodejs_sdk_advanced/nodejs_sdk_advanced.html)。本地测试使用发送替身,不向真实项目发送测试事件。
### HomeScene 显式开期与 12 小时冷却
- 登录和 `userLevel/save` 不再创建活动期;活动列表、状态查询也不创建活动。
- 进入 HomeScene(或首页回到前台)时刷新 `activityConfig/list`,用返回的 `openingRules` 检查:`GM_INFO.level >= unlockLevel`、无已开启个人期、距上期结束严格超过 12 小时,且配置 `enabled` 开启。没有历史期时不需要等 12 小时。
- 条件满足后 POST `cloudRise/index`:`{uid, token, action:"open_period", levelAmount}`。levelAmount 可省略,后端不读取也不记录;关卡门槛和 12 小时间隔仅由首页判断。后端保留账号鉴权和有效启用模板的读取,只检查是否已有未结束个人期,并用 CAS 防止并发重复开期。重复请求返回同一期,不重置倒计时。
- 成功响应 `{code:1,data:{serverNow,period}}`,period 包含 configVersion、periodId、startsAt、endsAt、durationHours、unlockLevel 和 pools;倒计时从本次请求成功创建个人期时开始。客户端接收成功回执后更新缓存、显示入口,玩家仍需点击开始才匹配。
- 失败/完成第三阶段以最终阶段 endedAt 为上期结束时间;自然超时取原 endsAt/expiresAt,不采用延迟状态同步时写入的 endedAt。已结束旧数据缺少阶段结束时间时保守使用原截止时间。
- 首页普通轮询只读,不会在停留期间自行续开;开期失败、断网或离开首页不会乐观显示入口,下次进入首页可以重试。活动规则按服务器校准时间判断,账号切换后的迟到响应会丢弃。
- 更新已有测试环境:先移除 `login` 和 `userLevel` 的旧自动开期调用,再发布 `cloudRise/periods`,然后 `cloudRise/index`、`activityConfig/list`,最后使用新版客户端。保留旧个人活动期及奖励存档,无须迁移集合。
开期成功后的 status/start/prepare_match/confirm_match 不再使用 users.levelAmount 重复检查解锁条件,避免数据库关卡滞后导致 available=false。报名次数、阶段状态、奖励保存等原有规则保持不变。