Files

金字塔后端

2026-09-29 已新增四档 80 格目录、profile_auto 首次锁档、免费首抽及登顶自动补齐授权。接入合同、配置草稿、反馈和验证范围见 V3-OFFERS.md。下文单档配置和支付流程仍用于兼容旧期。尚未发布或部署。

实现位于独立 workspace/server 仓库;不要把前端仓库内旧 server 副本作为发布源。本次统一入口改动仅在本地,尚未更新测试环境已创建的云函数。

配置

按天配置自动循环(2026-10-15)

在管理员发布的 period.schedule 中配置首期开启时间、开期间隔天数和每期持续天数。 服务器根据时间计算本期,不依赖定时器开期,也不需要每期复制发布配置。一天按 24 小时计算,目前支持北京时间 Asia/Shanghai。 intervalDays、durationDays 均为正整数,持续天数不能超过间隔天数;相等时两期首尾衔接。

首次切换及以后修改配置均使用现有 /pyramid/api 管理接口:

  • action: "admin"、operation: "publish",adminToken 使用服务端 ADMIN_TOKEN 对应凭证。
  • period 沿用已确认的正式奖励配置(包括 assignmentMode、audience、offer 或 levels),添加下方的 schedule 对象。
  • schedule.anchorAt 必须采用 YYYY-MM-DDTHH:mm:ss+08:00 格式,表示此配置版本的首期开启时间。
  • period.periodId/startAt/endAt 可以省略,服务端从排期推导并存储;显式填写时必须与推导值一致。configVersion 使用新且唯一的版本号。
  • effectiveFrom 省略或等于 anchorAt 转换后的毫秒时间戳;必须是未来时间,测试环境也不允许发布已开始的循环首期。environment 必须匹配部署环境。
  • enabled: true 启用;发布未来生效的新版本并设 enabled: false 可停用之后的自动开放,直至下一份启用版本生效。

例如从 2026-10-16 开始的排期字段(需合并进完整的正式 period 配置,以下不是完整发布请求):

{
  "schedule": {
    "type": "recurring",
    "timezone": "Asia/Shanghai",
    "anchorAt": "2026-10-16T21:00:00+08:00",
    "intervalDays": 7,
    "durationDays": 2
  },
  "configVersion": "pyramid_recurring_20261016_1",
  "environment": "production"
}

此例为每周五 21:00(含)至周日 21:00(不含);首期 startAt=1792155600000、endAt=1792328400000。 新期次 ID 为 pyramid:<本期开启毫秒时间戳>,例如 pyramid:1792155600000,不与旧日期格式混淆。 改为 intervalDays: 14, durationDays: 3 即每隔 14 天开启、每期持续 3 天;首期可为任意日期及时间,不再限制周五。

选配置时在同一活动、部署环境的已发布版本中,筛选 effectiveFrom <= 查询时刻 且 publishedAt <= 已知发布截止时刻,按 effectiveFrom 降序、再按 publishedAt 降序选择第一条。两个时间都相同的不同版本视为冲突,不能靠版本名称决定优先级。 普通玩法查询使用当前服务器时间;预告未来期时以未来开期时间选择已生效版本,但 publishedAt 截止于当前时间。最新选中版本停用时不回退旧版。 例如 A 于 10 月 16 日生效、B 于 10 月 23 日生效,即使提前发布 B,10 月 16 日仍使用 A,10 月 23 日起使用 B。玩家已参与的期次先读取其快照,不被新配置替换。

修改只从未来首期生效;已有玩家配置、订单和奖励快照不重写。发布时检查前后版本的周期边界,禁止新规则截断旧期,也禁止插入的规则与已排定未来版本发生截断。 首次切换必须在所有已发布单期结束之后,冲突返回 PERIOD_OVERLAP;切换后禁止再发布覆盖循环时间范围的单期配置。 已发布版本不可覆盖,旧期仍可通过玩家快照读取、结算、补发;迟到支付仍归原期,不转入新周。

兼容性:旧 schedule: "weekly" 继续按每周五 21:00 开放两天,并沿用 pyramid:YYYY-MM-DD;不带 schedule 的历史配置仍为一次性活动。没有自动修改数据库中的历史规则。

pyramidActivities 与玩法共用选期算法,关闭期间会提前返回未来 7 天内的下一期。 activityConfig/list 保持 schemaVersion: 1 和现有 startsAt/endsAt 字段,info 保持 startAt/endAt,当前客户端无需更新。 客户端只在登录、回首页、回前台时重新请求排期,并根据已缓存时间自动开闭;连续停留首页超过缓存覆盖范围仍需重新进入首页。

部署更新已有 pyramid/api(包含列表提供函数),无需新增云函数或更改结算定时器。 随后发布一次完整循环配置才会启用;仅部署代码不会擅自转换旧配置或启用测试草稿。发布前需核对线上现有期次结束时间及正式奖励版本。

测试环境发布文件与 curl

完整请求见 recurring-test.publish.json,包含当前 v3 草稿的四档奖励、原始目录/策略字符串及对应哈希。 首期为北京时间 2026-10-16 21:00,每隔 7 天开启、持续 2 天,audience: [] 表示测试环境所有满足参与条件的玩家,沿用自动分档。 将文件的 adminToken 占位值替换为测试服务端 ADMIN_TOKEN;需已部署新版 pyramid/api,并确认测试服务端 PAYMENT_APP_ENV=test。 在后端仓库根目录的 PowerShell 中执行:

curl.exe --fail-with-body --request POST "https://sor779u2w8.sealoshzh.site/pyramid/api" --header "Content-Type: application/json" --data-binary "@laf-cloud/functions/pyramid/config/recurring-test.publish.json"

响应 code: 1 才表示发布成功。若首期时间已过,修改 schedule.anchorAt 为未来时间并使用新的 configVersion;effectiveFrom 已省略,服务端自动取首期开启时间。 遇到 PERIOD_OVERLAP 应选择不截断现有活动的开始时间。该请求文件仅生成并通过本地配置校验,未执行 curl 或修改测试数据库;真实令牌不要提交到 Git。

config/2026-09-28-test.json 是已补齐奖励的测试期配置:startAt:1790524800000、endAt:1790611200000,即北京时间 2026-09-28 00:00(含)至 2026-09-29 00:00(不含)。前五层沿用 pay_test、itemPrice:100 分/次。每个普通格子为一百金币,每个宝箱为一百金币加一个冻结道具;第六层固定奖励为一百金币。

格子位置及升级箭头按原效果图(从下到上为第一至六层,每层从左到右编号)。test-v2 使用指定权重:第一层 [49,5,4946,4946,5,49],第二层 [49,4949,4,4949,49],第三层 [97,9797,9,97],第四层 [1,1,1],第五层 [1,1]。前三层权重总和均为一万;第四层各三分之一,第五层各二分之一。较少奖励格子的具体数量尚未指定,奖励数量仍沿用一百金币。

published:false 是样例的草稿标记,提交发布后由公共存储生成 status:published,不会把该字段存进配置。配置仍通过管理员接口提交为对象 period,无需手工添加数据库 _id。configVersion 在金字塔活动内唯一,不得覆盖;新版本可调整同一期尚未参与玩家的参数,但不能改变该期起止时间,也不会覆盖已有玩家快照。该样例期次已经结束,重新联调应使用新的期次、版本号和有效时间。

/pyramid/api action:info 返回的 data 的每个奖励格子增加 weight 字段(第六层为 null),取自玩家绑定的配置快照。前端在本地排除 gain:true 的格子,以剩余权重总和计算展示概率,无需新增接口或额外请求。后端抽奖仍使用同一快照中的权重;已参与旧期次的玩家继续按旧权重计算,不能仅更换前端常量改变实际抽奖。第四层显示一位小数,其余最多两位;显示值因四舍五入可能不恰好合计百分之百。

层 格子数 宝箱位置 升级格子
一 6 第 4 格(chest-1) 全部
二 5 第 4 格(chest-2) 第 1、5 格
三 4 第 2 格(chest-3) 第 1、4 格
四 3 无 第 2 格
五 2 第 1 格(chest-4) 第 2 格
六 1 无,固定奖励 不再升级

全部领取合计二千一百金币、四个冻结道具。隔离预览和单元测试使用各自的样例,不会自动读取本配置。

每层格子结构:{rewardId, weight, up, goods:[{id,count}], icon?}。前五层必须至少一个 up:true,权重为正整数、不必加总为一;实际抽取只对剩余格子归一化。第六层允许空奖励,固定奖励不需要权重。每层最多三十格,每格最多二十种道具;当前支持金币 1001、冻结 2001、锤子 2002、魔法棒 2003、无限体力 2005(秒)。icon 可选 chest-1 至 chest-4,其他情况客户端自动选道具图。

配置完整后,使用专用 PYRAMID_ADMIN_TOKEN 调用 /pyramid/api,传 action:admin:先 operation:setup_indexes,再 operation:publish, period:{...}。不同期次不可重叠。请求顶层可传 enabled(布尔值,默认 true)和 effectiveFrom(默认 period.startAt,必须位于活动有效期内)。生产配置只能在未来生效,且禁止 pay_test;测试环境保留发布进行中测试期的能力,已结束期不可发布。公共 store 的这项例外仅向金字塔测试配置开放,其他活动的发布限制保持不变。

期次现在统一保存在 activityConfigs,使用已有 activityConfig/store.publish 包装:

{
  _id: /* store 生成的 activityId + configVersion 哈希 */,
  activityId: "pyramid", recordType: "version", configVersion: "test-v2",
  status: "published", enabled: true,
  effectiveFrom: /* 配置生效时间 */, publishedAt: /* 服务端发布时间 */,
  config: { periodId, configVersion, environment, startAt, endAt, levels }
}

版本按 effectiveFrom / publishedAt 倒序选择,再判断 enabled 和期次有效时间,不回退到旧的开启版本。已有玩家先读取 pyramid_players.config 快照,关闭新参与后仍能继续支付、抽奖、领奖及补发;未参与玩家无法通过详情或签名下单绕过开关。/activityConfig/list 仅返回日程,查询不创建参与记录、不冻结档位、不结算。/pyramid/api action:info 返回完整个人状态,符合资格的首次详情查询会创建本期进度并冻结档位。unlockLevel:50 表示通过第 50 关后解锁。完成或到期后移除入口,历史详情和发奖接口保留。

运行环境沿用 PAYMENT_APP_ENV=test|production、WX_MIDAS_PAY_SIGN_KEY。测试支付回调经过生产转发时,生产还需已有 TEST_WX_PAY_NOTIFY_URL 指向测试回调。管理员密钥不进入客户端或配置 JSON。仅创建两个金字塔云函数:pyramid/api 开放 POST,pyramid/jobs 不开放 HTTP、仅挂定时触发器。规则、库存、支付和管理员逻辑都是 api 文件内普通函数,不再单独占用云函数名额。

接口

统一 POST /pyramid/api,JSON 请求体用 action 指定操作,用户操作需要 uid,token。成功 {code:1,data,msg},失败 {code:0,data:null,errorCode,msg}。时间均 Unix 毫秒;价格为分。

路径 额外参数 用途
/activityConfig/list uid 使用 onlyId,token 仅返回入口日程;不返回 pyramid、pyramidError、pyramidErrorMessage
/pyramid/api,action:info uid 使用 users._id,token;可选 periodId data 返回完整个人状态,无可查询活动时为 null;失败返回标准 errorCode 和中文 msg
/wx/orderPaySig、/wx/iosorderPaySig activityType:pyramid,periodId,level,requestId,outTradeNo,itemid,itemCount:1 前端生成订单号,服务端校验、保存订单并按配置价格签名
/wx/iosgetPayInfo uid,token,outTradeNo iOS 客服支付回到小游戏后查询微信商户订单并恢复活动机会
/pyramid/api,action:query orderId(兼容 outTradeNo) 已验签支付状态、机会状态及轮询间隔
/pyramid/api,action:draw periodId,level,requestId 扣机会、决定中奖、升级、生成授权,单文档 CAS 原子提交
/pyramid/api,action:claim periodId,level:6,requestId 登顶后生成全部剩余奖励授权
/pyramid/api,action:delivery_list 可选 limit,cursor 跨期未完成授权,游标由服务端返回
/pyramid/api,action:delivery_save 见下文 前端库存保存与收据原子写入
/pyramid/api,action:delivery_confirm grantId 验证保存收据后确认到账,幂等

不新增 /pyramid/order 或 /pyramid/query。每次用户点击由前端生成新 requestId 和 outTradeNo,不恢复上次未付款订单;同一网络请求重试必须同时复用原 requestId 和 outTradeNo。订单号最多 32 个英文字母、数字、下划线或连字符,测试环境以 wct_ 开头,生产环境以 wcx_ 开头。后端不生成或替换订单号,仍校验玩家、期次、层数、商品、金额和重复订单归属;同一请求换号、不同请求复用订单号或跨玩家占用订单号都会拒绝。旧 pendingOrder 不再阻止购买或抽奖,也不作为客户端支付查询的依据。前后端需要配套更新,旧客户端缺少 outTradeNo 的签名请求会被拒绝;已有订单的查单和回调继续按原订单号处理。

客户端通过 Utils.buyProp 拉起支付,通过 Utils.POST 的可选 JSON 模式调用活动接口。Android 和支持虚拟支付的 iOS 走米大师;iOS 返回错误码 16(或没有米大师 API)时复用服务端已校验的同一订单号进入客服支付,不接受客户端改价或另造订单。支付窗口打开时按钮防连点;普通取消只提示“取消充值”,不查询支付结果;米大师 success 或客服入口成功才记录 confirmingOrder,未确认时禁止再次购买。客服支付会在微信 onShow 后通过 /wx/iosgetPayInfo 续查,记录也支持重启后恢复。普通商城和通行证默认继续使用表单请求。

每笔已验证支付按订单号只增加一次机会,有次数时禁止新购买。到期未用的机会(含免费次数)由服务端自动抽奖。若异常情况下多笔旧订单的成功通知延迟到达,同层机会分别计入次数;升级时本层多余机会仍不转层,并记录 expiryReason:level_changed。旧层订单在升级后才确认成功时记录为 fulfillmentStatus:exception 供核查,不能用于新层。未付款订单不自动标记为支付失败或关闭。

发奖顺序:先传 {action:delivery_save,phase:prepare,grantId,uid,token} 取得库存与服务器时间;若 saved:false,前端按授权计算新库存,再传 {action:delivery_save,phase:save,grantId,base,inventory,calculatedAt,uid,token}。服务端核验差额,以原库存值和原始收据字段 CAS,一次写入涉及库存字段及 users.pyramidDeliveries 收据。该字段保存 JSON.stringify({ [grantId]: { periodId, savedAt, inventory } }) 的完整 JSON 字符串;读取时解析,同时兼容已有对象格式,并在下次成功写入时转成字符串;成功后客户端更新本地库存并调用 confirm。保存响应丢失时 prepare 返回现有收据及当前库存,不重复加奖。必须保留收据;不能仅清理收据后保留可重试授权。

活动进度、机会、请求结果、授权嵌入 pyramid_players(uid+periodId 唯一);期配置为 activityConfigs 中 activityId:pyramid 的版本;订单沿用 order。旧 pyramid_periods 仅作为迁移来源,运行时不再读写。不需要多文档事务,跨文档投影失败由幂等恢复修复。收据与旧期进度的归档需联合设计,本次不自动删除历史。

支付恢复与运行边界

两签名入口沿用项目的微信虚拟支付。回调校验原始 payload HMAC、商品、数量、金额、OpenId 和部署环境,再记录首次确认时间并建立机会。迟到成功保留 succeeded,按原期次、原层数自动消耗机会,并准备结算清单;订单履约状态为 credited,机会为 consumed。credited 表示订单权益已兑现,不代表结算奖励已领取。不退款、不转层、不转期。不变量异常记录 fulfillmentStatus:exception,供管理员核对。

米大师查单读取持久化的可信回调并修复机会入账;仓库没有已验证适用于虚拟支付渠道的主动查单/关单适配,因此 SDK 已返回 success 但缺失服务端回调的订单仍保持确认中。iOS 客服渠道使用已有微信商户 API 的签名查询,严格核对商户、AppId、OpenId、订单号、金额、币种和交易号后才确认同一金字塔订单。两种渠道都不因超时创建第二单,普通取消订单不进入确认流程。上线验收仍需 Android/iOS 微信真机支付、客服消息支付页及回调/商户查询验证。

管理员使用 action:admin,adminToken,可用 operation:query_order,orderId 查看证据。只有在支付渠道已不可逆关单后,才能通过 operation:record_closed_order,orderId,providerStatus:CLOSED,evidenceReference 记录渠道凭据;这不是调用微信关单。客户端不能调用该操作。保留对旧版本 pendingOrder 字段的清理兼容,正常取消支付不依赖管理员操作。

按 trigger.json 注册每分钟结束结算与订单恢复任务。结束先自动消耗可用机会,再为未发放奖励准备清单;查询清单不入账,玩家点击领取后才入账。订单恢复扫描支付成功且履约状态为 pending 或 expired 的订单,每次最多 100 笔;恢复成功退出扫描范围。定时任务返回 errors,运营应监控异常订单和长期未决订单。

旧库存接口仍有绝对值覆盖写入,本次没有重构全游戏库存协议。客户端在首页弹窗队列/活动遮罩内串行发奖;仍需真机确认其他自动保存不会携带旧库存覆盖到账值。

验证

node --test laf-cloud/functions/pyramid/tests/pyramid.test.mjs 使用本地内存 Mongo 适配器执行真实规则/服务/支付模块,不访问外部服务。既有支付路由回归需先设置测试用 TEST_WX_PAY_NOTIFY_URL=https://sor779u2w8.sealoshzh.site/wx/payCallBack;测试的网络请求本身已被桩替换,不会访问该地址。没有执行实际支付、部署或线上数据变更。

从旧拆分版本迁移

配置集合升级另见 配置迁移说明。本次新增对已有 activityConfig/store 的依赖,并更新已有 activityConfig/list 注册金字塔,不新增云函数。尚未运行远端迁移或部署。

原版本定义十三个金字塔云函数;现在只需保留两个,释放十一个名额。不要再按旧目录创建 model、service 等内部函数。

  1. 更新已有 pyramid/api 的代码并开启 POST(原 api 的 HTTP 方法为空);如果已经创建它,直接编辑,无需额外新建名额。
  2. 更新现有 wx/orderPaySig、wx/iosorderPaySig、wx/payCallBack,使其导入 @/pyramid/api;wx/getPayInfo、wx/getOrderReward、wx/iosgetPayInfo 继续导入 api。iOS 客服渠道还需同步更新 wx/KeFuInfo 与 wx/checkIos。
  3. 更新 pyramid/jobs,沿用原触发器目标和每分钟排期。它只调用 api 导出的 runJobs,HTTP 请求不能通过 action 触发该方法。
  4. 更新客户端为统一入口。验证统一列表、签名下单、回调、抽奖、发奖及定时结算后,再在 Laf 控制台移除已创建的旧函数:pyramid/model、pyramid/service、pyramid/payment、pyramid/admin、pyramid/list、pyramid/draw、pyramid/claim、pyramid/order/query、pyramid/delivery/list、pyramid/delivery/save、pyramid/delivery/confirm。本地删除文件不会自动释放远端名额;本次未执行远端删除。

不要删除 pyramid/api、pyramid/jobs 或任何活动集合/订单。云函数合并本身不改变既有 requestId/grantId;此次配置集合升级需要迁移旧配置,玩家进度和订单无需迁移。若已有旧客户端在测试,需要同步更新调用地址;已移除的旧路由不再提供兼容。

请求示例:

{"action":"delivery_confirm","uid":"玩家ID","token":"登录令牌","grantId":"授权ID"}

响应继续为 {code,data,msg};错误额外带 errorCode。缺失或未知 action 返回 INVALID_ACTION。玩家鉴权与管理员鉴权分开;不允许按客户端传入的任意函数名动态执行。

2026-09-30:历史待支付订单不再阻塞新一期查询

list 初始化自动分档活动时,不再因其他期次的 pending 订单返回 PAYMENT_PENDING 或已结束的旧活动。玩家已存在的有效期参与仍复用原快照;新一期照常检查活动、人群及画像条件。历史订单不会被删除、改成支付失败或移到新一期。延迟支付仍按原订单绑定的期次和层数处理,不计入新一期;2026-10-17 起结束后到账改为自动抽奖并追加原期结算。

部署只需更新已有 pyramid/api 云函数,无需新增云函数或清理订单。客户端下一次成功查询会恢复入口;本地修复不代表已部署。

pyramidDeliveries 是防止重复到账的发奖凭据,不是待支付订单。并发更新必须比较完整字段,冲突后读取最新数据重试,以免覆盖其他授权的凭据。非法 JSON 返回 INVALID_DELIVERY_RECEIPTS,不按空凭据处理。接口返回的 receipt 仍为对象,客户端无需改格式。

2026-10-09 接口迁移:同步发布 pyramid/api、activityConfig/list 及配套客户端。/pyramid/api action:list 不再支持;旧客户端需一起更新。没有新增云函数或集合。详见 统一查询合同。

2026-10-09 人群规则:profile_auto 的 audience: [] 表示全服开放;非空数组仅允许其中的 users._id 新参与。是否新注册不影响白名单判断。入口日程和首次锁档/签名下单共用此规则;已有本期参与快照继续保留。enabled:true、通过第 50 关、有效时间、至少两小时剩余时间和画像条件仍需满足。

2026-10-10 详情拆分:统一列表顶层及金字塔条目均移除 pyramid、pyramidError、pyramidErrorMessage,只保留日程。详情改由 /pyramid/api action:info 返回;data 包含 periodId、startAt/endAt、serverTime、currentLevel、completed、drawCount、levels、pendingGrants 和操作资格。可选 periodId 可恢复本人历史状态,不创建历史参与记录。旧 action:list 仍不支持。抽奖、领奖、支付确认后的客户端刷新只请求 info。前后端须同步发布,无需新增云函数。

金字塔错误响应统一保留 errorCode,msg 返回中文提示;详情查询也使用标准错误结构,不在日程中嵌套错误字段。抽奖、领奖、订单查询、奖励发放、管理发布及支付创建均适用。明确的中文业务详情保留,未知异常返回中文重试提示。微信支付回调保持 ErrCode/ErrMsg 协议,金字塔分支的错误说明同样使用中文。

到期清单与点击领取入账

定时任务 pyramid/jobs 为已结束期次自动消耗可用次数(含免费次数),按原快照的权重及未获得奖励池抽奖。第 5 层抽中升级奖励时,全部尚未获得的奖励取得领取资格。随后生成结算清单,不增加库存;其他未取得资格的格子不补发。已由客户端入账的奖励仅补确认。

进入/返回首页、切回前台或活动结束时请求 /pyramid/api action:settlement_read。该分支只返回未领取清单与 serverTime,不入账、不标记已读、不返回库存。准备尚未完成时 status 为 settling,完成后为 ready;沿用与黄金矿工一致的有限重试,持续停留不轮询。

点击领取后请求 action:settlement_claim,参数 uid、token、periodId、noticeId。后端核对所属账号、期次和清单,只处理该批次的奖励,通过 users.pyramidDeliveries 原子凭据幂等入账,完成后写该批 depositedAt 和 readAt,返回 notice、inventory、serverTime。全部已准备批次入账后才标记期次 settlementCompletedAt。无限体力从实际入账时间开始累计。重复请求或丢失响应重试返回对应批次清单和当前绝对库存,不再次增加奖励;兼容旧逻辑已经入账的清单。

前端领取成功后同步绝对库存、关闭补发弹窗、播放战令奖励动画。领取失败保留弹窗供重试;关闭弹窗不入账、不播放奖励动画、不消耗服务端清单,下次首页/前台检查仍可领取。动画只展示,不修改库存。

部署须同步更新 pyramid/api、客户端及金字塔分包,旧客户端的查询即入账协议不再使用。现有每分钟 jobs 触发器不变,无需新增云函数或集合。

2026-10-13 payment 埋点补齐

金字塔在统一 fulfill 流程确认 paymentStatus=succeeded 且 firstVerifiedAt 有效后上报 payment,覆盖原生支付、iOS 客服支付、查询恢复和 jobs 发货恢复。按服务器订单填写金额(分)、商品、数量和已验证支付时间;沿用现有用户/AB 属性与 Android、ios、ios_old 渠道口径。测试环境订单进入测试数数项目。未支付订单不报;已付款但次数过期的订单仍报。

使用项目已有 trackFirst,firstCheckId=outTradeNo,重复请求由数数按订单去重;不新增云函数或集合。SDK 异常不阻断发货,后续回调/查单可再次尝试;没有新增独立的埋点补偿定时任务。首次事件可能延迟入库,不能以查单调用次数统计支付笔数。没有 distinctId 时记录警告并等待后续确认重试。

部署更新 pyramid/api 与 login。login 的旧商户补单跳过金字塔,避免通用普通 payment 上报与专用去重事件混用;金字塔仍由专用支付回调、客户端支付恢复及 jobs 处理。无需更新前端。此处为 2026-10-13 的埋点调整记录;历史过期订单恢复规则见下节。

2026-10-17:到期自动抽奖与追加结算

活动结算与支付恢复共用内部抽奖核心,正常玩家请求仍校验活动开放时间和待发奖状态。自动抽奖以期次和机会订单号生成稳定请求 ID;机会消耗、抽奖结果和奖励授权在同一个玩家文档 CAS 中保存。任务或回调重试只复用结果,不再次抽奖。免费次数使用原有 orderId:free,不创建支付订单。

结算批次保存于玩家记录的 settlementNotices 数组,每批包含原有公开清单字段及内部 grantIds;接口不返回 grantIds。第一批沿用原 noticeId,后续批次根据奖励授权生成独立 ID。已准备清单不合并新奖励,旧批次的领取重试不会领取新批次。保留 settlementNotice 作为最新批次的兼容投影;旧单清单在处理时原位转换,保留 noticeId、已入账凭据及已读状态。只读查询不会持久化迁移或新增奖励。

新增奖励使 settlementPreparedAt 重新待处理,但不会取消旧批次的领取资格。支付恢复在完成自动抽奖和清单准备后才更新订单为 credited;中途失败由下一次回调、查单或 jobs 恢复。正常实时回调也立即走同一条路径,不必等到下一次定时任务。

历史补偿范围为 paymentStatus:succeeded 且 fulfillmentStatus:expired 的支付订单。核对机会是否已消耗及其原层数;因到期作废、仍对应当前层的未用机会可以恢复,已消耗的机会只修复订单状态。层数不符、升级作废、参与记录缺失等异常转 exception,保留核查线索,不跨层补抽。不会批量恢复旧规则下已过期的免费次数;旧期已标为 credited 的订单不在定时订单补偿扫描范围内。

本次只需更新已有后端 pyramid/api,pyramid/jobs 继续调用其 runJobs;保持现有触发器、集合和前端接口。当前前端按 noticeId 去重、逐批展示,继续在进入/返回首页、切回前台等时机查询;持续停留首页不会实时推送迟到支付奖励。部署后首次任务会开始恢复历史候选订单,因此上线前应确认该补偿范围。本地开发与测试不代表已经部署或补偿线上数据。

本地验证(2026-10-17):

  • 金字塔回归 138/138、活动日程回归 13/13,通过;覆盖自动免费/付费抽奖、登顶补齐、历史订单恢复、批次隔离、旧数据兼容、并发和中断恢复。
  • payment-routing.test.mjs 与 check-ios-routing.test.mjs 启动失败:分别缺少 @/passCheckSettlement 别名解析、测试桩缺少 matchesCustomerLinkTime 导出。使用修改前 HEAD 复现相同失败。
  • 全仓 tsc --noEmit 被既有 drawHead.ts 第 35 行语法错误阻断,修改前 HEAD 同样失败;另用本机客户端 TypeScript 编译器比较完整诊断,修改前后均为 386 项,其中金字塔均为 22 项,没有新增诊断。
  • 附带客户端 client/settlement/entry 检查 52/59;7 项失败涉及既有取消提示断言、素材内容/引用、倒计时断言及缺少 unschedule 的测试桩。这些检查使用本地前端和模拟接口,不加载后端源码;本次未修改前端。