金字塔后端
2026-09-29 已新增四档 80 格目录、profile_auto 首次锁档、免费首抽及登顶自动补齐授权。接入合同、配置草稿、反馈和验证范围见 V3-OFFERS.md。下文单档配置和支付流程仍用于兼容旧期。尚未发布或部署。
实现位于独立 workspace/server 仓库;不要把前端仓库内旧 server 副本作为发布源。本次统一入口改动仅在本地,尚未更新测试环境已创建的云函数。
配置
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 续查,记录也支持重启后恢复。普通商城和通行证默认继续使用表单请求。
每笔已验证支付按订单号只增加一次机会。若多笔旧订单的成功通知延迟到达,同层机会分别计入 number 次数;有次数时禁止新购买。升级时本层多余机会作废;旧层订单在升级后才确认成功时也记录为作废,不能用于新层。未付款订单不自动标记为支付失败或关闭。
发奖顺序:先传 {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,机会为 expired;不退款、不转层、不转期。不变量异常记录 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 注册每分钟结束结算与已成功待履约恢复任务。结束仅给已有资格的奖励建立授权和未读清单,客户端查询清单时由服务器完成入账与确认。定时任务返回 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 等内部函数。
- 更新已有
pyramid/api的代码并开启 POST(原 api 的 HTTP 方法为空);如果已经创建它,直接编辑,无需额外新建名额。 - 更新现有
wx/orderPaySig、wx/iosorderPaySig、wx/payCallBack,使其导入@/pyramid/api;wx/getPayInfo、wx/getOrderReward、wx/iosgetPayInfo继续导入 api。iOS 客服渠道还需同步更新wx/KeFuInfo与wx/checkIos。 - 更新
pyramid/jobs,沿用原触发器目标和每分钟排期。它只调用 api 导出的runJobs,HTTP 请求不能通过 action 触发该方法。 - 更新客户端为统一入口。验证统一列表、签名下单、回调、抽奖、发奖及定时结算后,再在 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 或已结束的旧活动。玩家已存在的有效期参与仍复用原快照;新一期照常检查活动、人群及画像条件。历史订单不会被删除、改成支付失败或移到新一期。延迟支付仍按原订单绑定的期次和层数处理,过期机会作废,不计入新一期。
部署只需更新已有 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 只为已结束期次生成 settlementNotice,记录 noticeId、periodId、offerTier、goods、createdAt、readAt、depositedAt,不增加库存。未获得资格的格子不补发,剩余次数作废。已由客户端入账的奖励仅补确认。
进入/返回首页、切回前台或活动结束时请求 /pyramid/api action:settlement_read。该分支只返回未领取清单与 serverTime,不入账、不标记已读、不返回库存。准备尚未完成时 status 为 settling,完成后为 ready;沿用与黄金矿工一致的有限重试,持续停留不轮询。
点击领取后请求 action:settlement_claim,参数 uid、token、periodId、noticeId。后端核对所属账号、期次和清单,然后通过 users.pyramidDeliveries 原子凭据幂等入账,完成后写 depositedAt、settlementCompletedAt 和 readAt,返回 notice、inventory、serverTime。无限体力从实际入账时间开始累计。重复请求或丢失响应重试返回相同清单和当前绝对库存,不再次增加奖励;兼容旧逻辑已经入账的清单。
前端领取成功后同步绝对库存、关闭补发弹窗、播放战令奖励动画。领取失败保留弹窗供重试;关闭弹窗不入账、不播放奖励动画、不消耗服务端清单,下次首页/前台检查仍可领取。动画只展示,不修改库存。
部署须同步更新 pyramid/api、客户端及金字塔分包,旧客户端的查询即入账协议不再使用。现有每分钟 jobs 触发器不变,无需新增云函数或集合。