server/docs/goldMiner-backend-design.md
2026-09-24 17:59:52 +08:00

25 KiB
Raw Blame History

2026-09-20 实现更新:下文为原始七集合设计存档。当前已收敛为 activityConfigs 与 goldMinerPlayerPeriods,资格、发奖、补发均合并到玩家期记录;周期由周历和配置版本计算,后台按待处理状态分批扫描。以 当前公共配置和存储说明 为准,旧环境按 迁移说明 升级。

黄金矿工后端整体设计

版本:V1.1

日期:2026-09-18

状态:设计方案,尚未实现、部署或进行真实支付验证。

产品依据:黄金矿工 PRD

本次修订:黄金矿工订单扩展收拢到一个原生 JSON 对象字段;进度去重合并到玩家每期记录,不建设进度恢复队列;奖励由前端发放、后端保存,不调整通用金币协议。原 V1.0 中“后台离线加币、资源版本改造、跨集合钱包事务”不再属于本期方案。

1. 设计结论与范围

活动标识为 goldMiner,集合前缀为 gold_miner_。独立后端位于相邻的 ../server/laf-cloud 仓库;MatchMaster 内的 server 目录不作为本方案实施目标。

后端管理全服活动期、玩家每期快照、付费/顺延资格、活动进度、顺序领奖资格和待发奖励。前端按后端核定的奖励内容发放金币,再沿用现有资源接口保存余额。

本次仅修改设计文档。未来实现仍需客户端上报稳定通关标识、接入活动和奖励领取;不包含通用钱包版本迁移或将所有旧活动改为服务端发币。

核心边界:

  • 未付款、未打开活动页面也累计;主线和无尽成功结算每次最多增加 1。
  • 玩家每期记录单独保留完整配置、进度和领奖明细,不覆盖上一期。
  • 进度与去重信息在同一玩家期文档同步条件更新;不单独建通关事件表,不提供后台进度补偿恢复。
  • 订单绑定原期,履约路线固定为原期解锁、原期补发或紧接下一期顺延。
  • 到期后后台确定待发奖励,离线时不增加金币;玩家下次上线自动走前端发货和后端保存。
  • 活动领取授权与金币保存是不同步骤;沿用现有前端发货的故障和多设备限制,不能宣称已有服务端原子发币保证。

2. 现有代码事实

位置(相对各自仓库) 已核实事实与设计影响
server:laf-cloud/functions/userLevel.ts:50 count = levelAmount - oldlevelAmount,先通过主线等级差增加 addLevel
server:laf-cloud/functions/userLevel.ts:58 isWuXian == "true" 时再给 addLevel 加 1,不是给 levelAmount 加 1
MatchMaster:assets/Script/module/Tool/GameTool.ts:390 达到主线上限后主线等级不再增加,所以无尽通常产生等级差 0
MatchMaster:assets/Script/module/Tool/GameTool.ts:2446 无尽模板可重复抽到,模板 ID 不能用于通关去重
MatchMaster:assets/Script/Map.ts:4460 无尽序号在另一条保存链路更新,尚未与胜利上报原子关联
server:laf-cloud/functions/userCoin.ts:28 接收客户端金币总额并保存;本期继续沿用
server:laf-cloud/functions/wx/orderPaySig.ts:23 已有按活动商品校验资格和服务端定价的接入点
server:laf-cloud/functions/wx/payCallBack.ts:82 已有支付确认与活动权益处理接入点
server:laf-cloud/functions/login.ts:395 另有 iOS 登录补单,黄金矿工需与回调共用履约方法

2.1 “无尽额外加 1”的含义

现有逻辑可概括为:

addLevel = oldAddLevel + (newLevelAmount - oldLevelAmount)
if isWuXian:
    addLevel += 1

主线举例:100 → 101,差值 1,累计增加 1。

无尽举例:主线等级停在上限 L,本次上报仍为 L,差值 0;通过无尽分支增加 1,累计仍只增加 1。

因此这里的“额外”是相对于等级差计算而言,不表示无尽正常通关算两次。如果某次异常请求同时上报主线等级增长和无尽标记,当前实现确实可能将差值与 1 相加。黄金矿工不直接复制这个公式,而是对一笔已去重的有效胜利固定增加 1。

当前无尽分支没有请求去重,重复上报也可能再次增加旧统计。本设计只解决黄金矿工活动进度的去重,不顺带改造旧排行榜统计。

3. 周期、资格和并发规则

  • 北京时间每周四 00:00 开始,到紧接周日的周一 00:00 截止,采用 [startsAt, endsAt)。
  • periodId = goldMiner:<北京时间周四日期>;下一期固定为开始日期 +7 天。
  • 例如 goldMiner:2026-09-17 对应 UTC 2026-09-16T16:00:00Z 至 2026-09-20T16:00:00Z。
  • 已通过主线第 41 关获得资格,第 41 关本身不计数;随后主线或无尽有效胜利才累计。
  • 玩家身份来自有效鉴权,当前接入现有 users 付费业务域,不信任客户端切换账号集合。
  • 购买资格由服务端时钟、冻结配置和权益记录决定,不能仅通过前端按钮控制。
  • uid + periodId 玩家记录唯一;同一期内去重记录和进度必须一次条件写成功或一起失败。
  • 领取须按序;已授权但尚未完成前端保存的任务仍在处理中,不能直接跳到下一档。
  • 支付重试不得重新选择履约路线;顺延只能用一次,不能延至第三期。

4. 模块划分

实现代码集中在 laf-cloud/functions/goldMiner/,实际接口和部署顺序见 模块说明。

模块 职责
goldMiner/config 配置校验、版本、活动日历和冻结快照
goldMiner/service 玩家期状态、进度条件更新、顺序领奖授权与确认
goldMiner/payment 下单校验、订单 JSON、支付履约和顺延
goldMiner/index 接口 状态、领取、发货确认、补发清单和展示确认
goldMiner/jobs 到期冻结、待发清单、顺延激活、订单履约重试
现有胜利/资源接口 成功上报接入活动;保存前端已发放后的资源余额

不新增通关事件恢复模块或通用钱包模块。公共逻辑与 HTTP 入口分离,定时处理和在线请求调用同一业务方法。

5. 数据库结构

金额为整数分,奖励数量为正整数,时间为 UTC 毫秒。以下为字段方案,不是可直接上线的首期数值配置。

5.1 goldMinerConfigs:版本配置

_id / configVersion
status: draft | published | retired
effectiveFromPeriodId
unlockPassedLevel: 41
timezone: Asia/Shanghai
schedule: 周四开始、周一截止
productId, priceFen, currency: CNY
tasks: [{ taskId, sequence, targetWins, items: [{ type, count, itemId? }] }]
createdAt, publishedAt

任务数量可配置;序号连续,目标严格递增,任务 ID 唯一;首期只允许已实现的 coin 奖励,保留 items 数组扩展。价格、任务数量和数值待定,不完整配置不得销售。

发布后不原地修改配置;变更新建版本,下期生效。

5.2 goldMinerPeriods:全服每期快照

_id = periodId
startsAt, endsAt, nextPeriodId
configVersion, configSnapshot, configHash
phase: scheduled | active | closed
purchaseEnabled
closeCursor, closedAt
createdAt, updatedAt

冗余完整配置,所有当期玩家使用同一份内容。活动是否开放最终按时间判定,不依赖定时任务恰好更新 phase。停购不删除已付款权益。

5.3 goldMinerPlayerPeriods:玩家每期数据和去重

字段 含义
_id 由业务域、uid、periodId 生成稳定键
uid, accountScope, periodId 归属
startsAt, endsAt, nextPeriodId 冗余周期
configVersion, configSnapshot, configHash 完整任务、奖励、价格、门槛快照
qualifiedAt, joinedAt, qualifiedPassedLevel 资格与首次记录
progressWins 当期累计,最多等于最终目标
progressClosed, progressAtClose, progressFrozenAt 截止冻结状态
progressDedup.entries[] 已实际计数的结算键、请求 ID 和内容摘要,见 5.6
entitlementStatus none / unlocked
entitlementId, entitlementSource 普通购买或延迟支付顺延
sourceOrderNo, sourcePeriodId, unlockedAt 权益来源
carryTargetPeriodId, carryDisposition 原期顺延结果
claimedThrough 连续完成前端发货并确认保存的最后任务,初始 0
tasks[] 任务领取和发货状态
settlementState open / pending_delivery / partial / client_saved / no_reward / manual_review
settlementIds[] 补发清单引用
revision, createdAt, updatedAt 单文档条件更新与审计

每档任务包含:

taskId, sequence, targetWins, itemsSnapshot
claimStatus: unclaimed | issuing | claimed
grantId, authorizedAt, clientSavedAt
claimChannel: manual | expiry_auto | delayed_payment
grantedItems

issuing 表示已固定领取凭证、等待前端发货保存,不能被解释为金币已到账。claimed 表示已按本协议收到客户端保存成功后的确认,不是服务端独立验证了本地发货。

未付款玩家也建记录。首次有效通关可以直接建记录,不依赖先打开页面;读取和合法下单也可幂等建立。每期独立保留,不覆盖上一期,不默认配置历史快照自动删除。

5.4 goldMinerEntitlements:购买和顺延资格

_id = 由来源订单确定的稳定键
uid, accountScope, sourceOrderNo, sourcePeriodId
targetPeriodId
source: purchase | delayed_payment_carry
state: reserved | active | consumed | manual_review
carryCount: 0 | 1
createdAt, activatesAt, activatedAt, consumedAt

保留独立权益记录,便于在周一至周三预留尚未开期的资格;无需提前冻结下一期配置。来源订单唯一,同用户同目标期不能重复分配有效权益。

5.5 order:只增加一个黄金矿工对象字段

保留现有通用订单字段,如 outTradeNo, openid, itemid, goodsPrice, itemCount, paymentAppEnv, state。黄金矿工新增信息统一放入一个 goldMiner 字段:

goldMiner: {
  schemaVersion: 1,
  uid, accountScope,
  periodId, startsAt, endsAt,
  configVersion, configHash, productSnapshot,
  createRequestId,
  confirmedAt,
  fulfillmentStatus: pending | processing | fulfilled | retryable | manual_review,
  fulfillmentRoute: current_period | settle_original | carry_next | null,
  targetPeriodId, entitlementId, fulfilledAt,
  revision, lastErrorCode, nextRetryAt
}

推荐存为数据库原生嵌套对象,不存 JSON.stringify(...) 后的字符串:

  • 同样只有一个顶层扩展字段,但可查询和索引 goldMiner.periodId 等子字段。
  • 可对 goldMiner.fulfillmentRoute 做条件更新,不必反序列化并重写整个对象。
  • 重复字段尽量复用现有订单:成交金额使用服务端填写的 goodsPrice,数量使用 itemCount=1,通道支付状态沿用现有 state;对象中的状态只描述黄金矿工权益履约。

如果部署的数据访问层只能保存 JSON 字符串,也可将同一结构序列化,但会失去方便的子字段查询/索引,更新必须比较旧完整值,批量查找待履约订单也更困难。本方案优先采用原生对象。

路线一旦选定不得改变。通道已支付不等于业务权益已交付;当期全部任务领完也不是订单权益履约成功的前提。

5.6 去重并入玩家期记录,不建独立进度集合

采用:

progressWins: 3
progressDedup: {
  entries: [
    { eventId, businessKey, payloadHash, countedAt }
  ]
}
revision
  • 每次胜利有稳定 eventId,同一次胜利重试必须使用原 ID。
  • businessKey 为 main:<主线通关号> 或 endless:<唯一成功结算序号>,防止同一次结算换请求 ID 重复计数。无尽模板 ID 不适用。
  • 请求携带首次上报时绑定的活动期,重试不更换;后端独立核对开放期,不允许客户端任意指定可累计期。通关上报链路自动取得期信息,不要求玩家打开活动页面。
  • progressWins + 1、去重条目写入和 revision 增加在同一玩家期文档条件更新中提交。
  • 最多保存与最终目标数相同数量的成功计数条目;封顶后不继续追加,避免无界增长。发布配置时限制文档大小可承受的任务目标。
  • 活动结束后同一期的已成功请求仍可查询原结果,但不能增加进度;携带旧期的未成功请求不计到下一期。
  • 不增加 accepted/applied 事件队列、补偿扫描或历史重放能力。更新失败就不记入;活动内可以重试,截止后不补记失败进度。
  • 同期删除去重信息前必须保证不再开放写入;首期随玩家期快照一起保留即可。

因此单独集合没有必要。这里需要的是“避免重复加进度”,不是“保存完整通关日志以支持恢复”。唯一事件标识只能约束重试,不代表后端已经权威验证战斗结果。

5.7 goldMinerRewardGrants:领奖授权及前端保存回执

_id = accountScope + uid + periodId + taskId
uid, accountScope, periodId, taskId, sequence
itemsSnapshot, channel, sourceOrderNo?, settlementId?
status: authorized | client_saved
authorizedAt, clientSavedAt

该集合记录奖励凭证,不再表示后端已加金币。任务中的 grantId 是授权事实来源;先用玩家期记录的条件更新固定授权,再按确定性 ID 补齐回执。回执补写失败可按任务信息重建,不能重新授权另一份奖励。

不要求金币余额与本集合跨集合事务提交,也不新增资源版本字段。

5.8 goldMinerSettlements:到期待发清单

保存 settlementId, uid, periodId, taskIds, itemsSummary, deliveryStatus, createdAt, clientSavedAt, acknowledgedAt。

deliveryStatus 为 pending / partial / client_saved。后台生成清单不等于到账;前端按 taskIds 顺序自动发货,完成后标记 client_saved。展示确认仅影响通知,不替代发货确认。

同一任务在手动领取、到期补发和延迟支付处理中共用同一个 grantId,不复制第二份奖励。

5.9 主要索引

集合 索引
periods _id 唯一;endsAt + phase
player_periods accountScope + uid + periodId 唯一;periodId + settlementState + _id;uid + startsAt
entitlements sourceOrderNo 唯一;有效分配的 accountScope + uid + targetPeriodId 唯一;targetPeriodId + state
reward_grants accountScope + uid + periodId + taskId 唯一
settlements settlementId 唯一;uid + deliveryStatus + createdAt
order outTradeNo 唯一;goldMiner.uid + goldMiner.periodId + goldMiner.createRequestId 唯一;goldMiner.fulfillmentStatus + goldMiner.nextRetryAt

订单扩展索引仅覆盖有 goldMiner 对象的记录,避免其他商品缺失字段造成冲突。去重条目不建跨文档数组唯一索引,使用同文档条件更新检查。

6. 进度处理

  1. 校验用户、成功结果、玩法、稳定请求/业务结算键和绑定的活动期;登录同步、迁移或整值存档不能直接视为新胜利。
  2. 读取绑定期玩家记录,若该事件已成功计数,返回原结果;同 ID 不同内容拒绝。
  3. 新事件只接受活动开放时间内的请求;校验已通过 41 关且本局不是第 41 关,无尽和主线每笔最多增加 1。
  4. 未建记录则用唯一键幂等初始化,进度从 0 起,不追溯已有等级差。
  5. 以 uid、periodId、revision、未冻结状态及事件不存在为条件,一次更新进度和去重条目。并发失败后重新读取、检查时间与去重,再决定重试。
  6. 达到最后一档目标后返回封顶状态,不再写去重条目。
  7. 普通关卡存档与活动计数分别返回结果;活动期外或活动更新失败不阻断普通关卡保存。

原子写实现需验证数据库条件更新返回值和并发语义,不使用“读后无条件覆盖整个文档”。

到期任务以同一 revision/未冻结条件固定 progressAtClose 并关闭进度写入。更新与冻结冲突时重新读取;只有活动期间通过校验并成功条件写入的计数保留。没有异步待应用事件,也没有关闭后排队恢复进度的过程。仅收到 HTTP 请求、但写入失败,不视为已获得进度。

7. 支付、履约与顺延

7.1 下单

严格鉴权 → 按服务端时间读取冻结期 → 校验门槛/停购开关 → 先激活该期预留顺延资格 → 已解锁则拒绝 → 幂等创建订单和 goldMiner 对象 → 返回服务端定价的支付参数。

同一期优先复用有效待支付单,createRequestId 去重。不允许在周一至周三预购下一期。已预留顺延权益的玩家开期后不展示购买按钮,接口同样拒绝购买。

7.2 统一支付履约方法

所有可购买该商品的 Android、iOS 回调、查单、登录补单入口调用同一方法。核对来源、签名、环境、用户、商品、数量与金额,不信任客户端“支付成功”。

已有 goldMiner.fulfillmentRoute -> 幂等继续原路线
尚无路线:
  原期开放 -> current_period:解锁原期
  原期结束 -> 读取已冻结进度
    有达标未完成发货的奖励 -> settle_original:固定原期待发清单
    无可发奖励 -> carry_next:预留紧接下一期权益

以当前确认并交付权益的业务阶段处理延迟,不以实际付款时间分流。正常在期内解锁但最终零达标,不顺延。原期已选择 settle_original,即便客户端后来已领完,重复回调也不能改成 carry_next。

先条件写固定订单路线,再按来源订单唯一键幂等建立权益/待发清单,最后标记 fulfilled。任一步中断继续原路线,不用跨集合钱包事务;订单确认与业务履约状态不能混为一谈。

7.3 下一期开期

顺延资格预绑定 source.nextPeriodId,carryCount=1。开期任务激活,查询/通关/下单入口也调用幂等激活校验,避免任务延迟产生购买窗口。

首次创建下一期玩家记录时进度初始化为 0,绑定下一期配置;若已有未付款进度,仅补权益,不清零或覆盖快照。原期进度不转移。下一期结束按普通规则处理,不再顺延。

三天间隔内对上期未履约单查单。极端晚确认导致目标期已购买/结束时保留订单并转人工核对,不静默丢弃、不自动转第三期;不建设可囤积的通用资格券。

8. 前端发货、后端保存

8.1 正常领取流程

  1. 前端请求 claim,携带 periodId、taskId 和稳定 requestId。
  2. 后端校验付费、达标、活动时间和前置任务均已 claimed。将目标任务由 unclaimed 条件更新为 issuing,固定 grantId 和奖励快照;重复请求返回同一凭证。
  3. 前端读取本账号本地发货记录,按 grantId 去重;未发放时按服务端返回内容增加本地金币,保存本地已应用记录。
  4. 前端调用现有资源保存接口,上报发放后的余额。失败时保留本地待保存状态,不重新加币。
  5. 资源保存成功后调用 confirm_delivery(grantId)。后端幂等标记任务 claimed、推进 claimedThrough、更新回执,再允许下一档。
  6. 若第 5 步响应丢失,只重试确认;已 claimed 的重复请求返回原状态,不再次发货。

资源余额与本地应用记录应在客户端持久化上尽量作为一个完整状态保存,降低崩溃窗口;具体接入现有存档机制。本次不改前端实现。

issuing 的授权在活动结束后仍可完成保存/确认。超时不能自动把 issuing 回退成 unclaimed 后生成新 grantId,否则可能重复发货。

8.2 到期与延迟支付补发

到期固定最终进度,并为已付费且达标未完成领取的任务生成有序待发清单。已有 issuing 的任务复用原凭证,不新发第二份。

玩家在线时可立即拉取处理;离线时等待下次登录。前端按顺序自动执行同一发货—保存—确认流程;部分完成保留剩余清单。后台不直接修改 coinAmount。

旧期补发清单独立于新期活动,不覆盖新期进度或购买状态。关闭补发弹窗不等于发货成功;展示确认与发货确认分开。

8.3 暂不改金币协议的实际边界

继续使用客户端计算后的金币总额保存,不新增 resourceVersion、walletOpId 或全局金币 delta 协议,不改造旧活动/迁移的余额写入方式。

本地 grantId 去重、服务端固定授权和保存后确认可处理常见同设备重试。但在清除本地数据、跨设备同时操作、前端发货后保存/确认中断等情况下,既有模型仍有重复发放或漏保存风险;旧总余额也可能覆盖另一个设备的更新。当前方案不能承诺跨设备、跨步骤的严格一次到账。

这属于本期明确保留的架构限制,不以“领取状态已写入”冒充“金币已原子到账”。异常奖励保留 grantId、任务状态和保存时间,供核对;不通过重复生成领奖凭证自动修复不确定到账。

9. 接口建议

沿用 code/data/msg 外壳,增加稳定 errorCode;客户端接口均需鉴权。

动作/接口 输入与输出
goldMiner info 返回当前期、serverTime、资格、已购/顺延、进度、任务和待发清单摘要
现有下单入口 输入商品和 createRequestId;服务端生成原期、价格和订单 goldMiner 对象
现有成功上报扩展 eventId、模式、结算序号、绑定期;返回是否计数、原因和当前进度
goldMiner claim periodId、taskId、requestId;返回 grantId、奖励快照和领取状态,不返回“后端已加币”
现有资源保存接口 前端发货后的资源总额,沿用当前协议
goldMiner confirm_delivery grantId;保存成功后的幂等领取确认
goldMiner settlements 分页返回当前账号历史期的待发/部分完成/已确认清单
goldMiner ack_settlement 仅确认展示,不替代发货或金币保存

查询响应按 V1.3 精简:活动使用 status(unavailable / locked / purchasable / purchase_disabled / unlocked),不返回 availability、qualified、purchasable、entitlement、nextStartsAt、nextPeriodId、currency 和 claimedThrough。

任务按领取顺序返回 taskId、targetWins、itemsSnapshot、claimStatus、progress、completed。查询中的 claimStatus 为 locked / claimable / issuing / claimed,100% 但未付款或前置未领时仍为 locked。不返回 sequence、claimed、issuing、claimable、blockReason 等冗余字段,也不直接展开数据库任务记录。领取与确认接口的 grantId、claimedThrough 协议,以及数据库状态不变。

错误建议:UNAUTHORIZED、NOT_QUALIFIED、PERIOD_ENDED、CONFIG_UNAVAILABLE、ALREADY_UNLOCKED、NOT_PAID、TARGET_NOT_REACHED、PREVIOUS_NOT_CLAIMED、EVENT_PAYLOAD_CONFLICT、DELIVERY_PENDING、RETRYABLE。合法重复操作返回既有结果。

10. 后台任务与实施顺序

后台任务:冻结活动配置、激活顺延、冻结到期进度、生成待发清单、支付对账及权益履约重试。没有金币自动入账任务、通关事件恢复任务或钱包版本迁移任务。

仓库未查到已版本化的 cron/schedule 绑定;需要另行配置实际调度、鉴权和失败补扫。任务按稳定游标分页、幂等执行;在线查询可补齐期状态和待发清单,但不改变截止时间。

建议实施顺序:

  1. 建集合、唯一索引、配置草稿,验证玩家期文档条件更新语义。
  2. 接入主线/无尽稳定结算键,将进度去重合并到玩家期记录。
  3. 接入订单 goldMiner 对象和统一支付履约,完成下一期预留与自动激活。
  4. 实现顺序授权、前端发货后保存/确认、到期待发清单。
  5. 兼容客户端联调后,配置首期数值和商品,演练期末与延迟支付再启用。

本次仍仅提交文档变更,不实施这些步骤。上线停止销售时继续处理已付款权益和待发清单,不删除历史记录。

11. 验证计划

本次未运行功能测试;以下为未来实现的验收清单。

类别 验收内容
时间/门槛 通过 41 关进度 0;下一次成功 +1;周四开放、周一截止;期外新请求不计数
主线/无尽 主线差值正常;无尽等级不变仍 +1;同模板的新局分别计数;旧统计不作为活动去重依据
去重 同期重复请求/换 ID 同业务键;不同事件并发;计数和去重一起成功或失败;封顶后不再追加
无恢复范围 活动写入失败不阻断普通存档;期内可重试;截止后不回补失败进度;不出现跨期旧局重记
订单对象 嵌套字段查单/索引;服务端定价;幂等下单;回调路线固定;不影响其他商品
顺延 有奖励留原期;零奖励只顺延一次;下一期开期自动解锁且无购买入口;激活保留已有未付款进度
顺序领取 issuing 不允许跳到下一档;重复领取同凭证;前端保存成功后确认;到期授权仍可完成
补发 离线只产生待发清单,不增加余额;下次登录顺序发货;手领和补发复用同一凭证
前端发货限制 单设备重试去重;余额保存失败不再加币;确认失败只重试确认;多设备/清档异常记录供核对
历史 完整玩家期快照;奖励清单和来源订单可追溯;新期不覆盖旧期