Files
MatchMaster/docs/pyramid-backend-design.md
T
2026-10-12 15:00:49 +08:00

26 KiB
Raw Blame History

金字塔活动后端设计

版本:V1.0

日期:2026-09-28

状态:设计对应实现已落地到本地,未上线;实现差异和验证范围见开发说明。

产品依据:金字塔活动 PRD

统一入口修订:为减少 Laf 云函数名额,所有活动请求改为 POST /pyramid/api,请求 JSON 必须包含下文对应的 action,其他业务参数及 response 保持原含义。delivery_save 使用 phase:prepare|save;管理员使用 action:admin,operation:具体操作,adminToken。只部署 pyramid/api 和不开放 HTTP 的 pyramid/jobs,原内部模块合并为 api 文件中的普通函数。支付下单仍走原有两个微信接口。旧拆分路由已移除,测试环境迁移顺序见后端 README。

1. 范围与接入原则

本方案覆盖活动状态、支付机会入账、不放回抽奖、终点领取、前端发货确认和跨期补发。

下单逻辑必须放在现有 /wx/orderPaySig 和 /wx/iosorderPaySig,不新增 /pyramid/order。活动状态合并到 /pyramid/api(action:list),不新增独立 /pyramid/query。

活动后端确定资格、中奖结果和奖励内容;客户端按授权发放道具并保存,再确认到账。本方案不将产品要求擅自改成后端离线直接增加余额。

本文的集合名、模块拆分、错误枚举和时间字段格式为实现建议;已确认的业务规则以 PRD 为准。没有新增跨期次数、自动退款、重复轮次或后台自动代抽。

实现补充:活动进度、请求回执和授权合并到单个 pyramid_players 玩家期次文档,以 revision CAS 完成原子写入;库存收据嵌入 users.pyramidDeliveries。新增 /pyramid/api(action:delivery_save) 承载前端计算后的库存保存与收据原子写入,再调用确认接口,避免保存成功但确认丢失导致重复发奖。具体参数以独立后端 laf-cloud/functions/pyramid/README.md 为准。支付主动渠道查单/关单适配尚未验证,当前持久化回调恢复与人工凭据关单不能视为渠道能力已经验收。

2. 现有代码核查与兼容要求

核查对象为独立后端工作区 C:/Users/NK/Desktop/workspace/server。MatchMaster/server 是另一份目录,实施时应先确认实际部署仓库,不能混用两份实现。

现有位置 已核查事实及接入要求
laf-cloud/functions/wx/orderPaySig.ts 接收 itemid、itemCount、itemPrice;已有活动分支转交专用支付模块,可增加金字塔分支
laf-cloud/functions/wx/iosorderPaySig.ts 同样接收上述字段;金字塔必须接入此入口,复用现有平台支付路由
上述两个下单入口 当前签名响应为 paySig、signature、signData、outTradeNo;金字塔分支应兼容其结构
laf-cloud/functions/wx/getPayInfo.ts 主要读取本地订单状态;“充值未成功”不能直接解释为支付平台终态失败
laf-cloud/functions/goldMiner/service.ts 存在发奖授权、确认和跨期待发列表的后端参考,不能据此认定客户端已完成接入
laf-cloud/functions/login.ts 有普通订单补发;金字塔需要专门的授权恢复入口,不能当普通商品再次发奖
客户端 assets/Script/module/Tool/GameTool.ts 存在客户端增加余额后上传保存的流程
后端 laf-cloud/functions/userCoin.ts 当前保存客户端完整余额;没有天然提供奖励授权与余额写入的原子去重

对既有支付通用分支保持兼容。活动查询返回 itemId,调用现有支付接口时映射为小写 itemid。两者表示同一支付商品,不是奖励格子编号。

3. 服务端不变量

  1. 每个玩家每期一份进度,从第一层开始,仅完成一轮。
  2. 同一玩家同一期最多一个未决订单;未决不是失败。
  3. 每笔成功订单最多记入一次机会,机会绑定订单期次和层数。
  4. 有可用机会或当前期次待确认发奖时,不能创建新的购买订单。
  5. 每次抽奖扣一次机会、获得一个格子;一个格子只获得一次。
  6. 当前层目标格子可有多个,抽中任意一个立即推进层数。
  7. 扣次数、中奖记录、发奖授权和升级必须构成一次原子状态变化。
  8. 第六层无购买和抽奖,只能领取剩余奖励。
  9. 已确认到账的奖励不能再次发放;未确认奖励不能因到期被删除。
  10. 同一奖励在正常领取、重试、活动结束补发之间只能归属于一个有效授权。
  11. 支付成功事实与机会是否有效分开保存;过期机会不代表支付失败。

4. 数据设计

4.1 期次配置 activityConfigs

2026-09-29 实现调整:统一复用 activityConfig/store,外层为 activityId:pyramid, recordType:version, configVersion, status:published, enabled, effectiveFrom, publishedAt,下表业务参数存放在 config 内。同一活动版本不可覆盖;同一期更新版本也不改变起止时间或已有玩家快照。enabled:false 仅停止新参与,已参与玩家继续使用 pyramid_players.config。配置发布仍走 /pyramid/api 的管理员 publish 操作;公共 /activityConfig/list 已注册金字塔,列表查询不创建参与记录。

旧 pyramid_periods 不再用于运行时读写;后端提供 pyramid/migrate-config.cjs(默认只读预检)和迁移说明,将旧配置复制到公共集合,保留玩家、订单及发奖记录。此调整仅完成本地开发,环境升级需先迁移旧配置并更新已有云函数。

字段 含义
periodId 唯一期次编号
startAt / endAt UTC 毫秒时间戳,全服统一;区间为 [startAt, endAt)
configVersion 固定配置版本
levels 六层配置;第一至第五层含支付商品、价格及奖励格子,第六层含固定奖励
levels[].rewards[] rewardId, goods[{id,count}], up, weight;权重仅用于前五层

建议开期后冻结配置,并为订单、授权保存必要快照。校验六层结构、格子 ID 在本期内唯一、道具数量为正整数、金额为正整数分;前五层每层至少有一个可抽中的目标格子。

建议所有参与随机的格子使用正的有限权重,优先采用正整数权重,禁止 NaN、负数、全零奖池。此项为配置约束建议;如未来需要零权重格子,须单独定义其只能终点领取等含义,不静默引入本期。

4.2 玩家进度 pyramid_players

以 (uid, periodId) 为唯一键,建议将该玩家当期的有限状态放入同一记录,便于原子更新:

{
  uid, periodId, configVersion,
  currentLevel: 1,
  revision: 0,
  completed: false,
  settledAt: null,
  pendingOrderId: null,
  credits: [{ orderId, level, status, consumedByRequestId }],
  rewards: [{ rewardId, gain, deliveryStatus, grantId }],
  grants: [{ grantId, source, rewardIds, goods, status, createdAt, confirmedAt }],
  operations: [{ requestId, action, payloadHash, resultSnapshot, committedAt }]
}

记录结构是逻辑模型,实施时需核验文档大小、索引及数据库原子更新能力。拆分集合时必须补足事务或可靠恢复机制,不能把原子要求降为顺序写几个集合。

  • credits.status:available | consumed | expired。是否已记入通过 orderId 去重,不能只保存一个无法追溯的数字。
  • drawCount 是当前期次、当前层有效 available 机会数量的投影,正常为 0 或 1。
  • gain=false 时 deliveryStatus=not_eligible。
  • gain=true 时 deliveryStatus=unclaimed | pending | delivered。
  • 抽中格子直接变为 gain=true, pending,同时生成授权。
  • 到达第六层时,尚无资格的格子变为 gain=true, unclaimed,既有 pending/delivered 格子保持原状态。
  • completed 仅在第六层且本期全部奖励均为 delivered 时为 true。未通关玩家的已中奖奖励全部发完,不算完成本期。

4.3 通用订单 order 的活动扩展

复用原订单,不另造一条支付账本。建议扩展:

{
  activityType: "pyramid",
  uid,
  pyramid: { periodId, level, configVersion, startAt, endAt },
  itemid, itemCount: 1, goodsPrice,
  outTradeNo,
  paymentStatus,       // pending | succeeded | closed | failed
  firstVerifiedAt,
  fulfillmentStatus,  // pending | credited | expired | exception
  creditedAt,
  exceptionReason
}

保留现有通用字段,活动状态不能直接复用含义不同的旧 state 数字。credited 表示机会曾经入账,即使之后被使用或到期,也不改成“没有付过款”;具体机会状态从玩家记录读取。

firstVerifiedAt 为服务端首次可信核验成功的持久化时间,不使用客户端时间,也不直接等同于平台付款时间。首次在活动结束后核验成功的,履约为 expired,不产生可用机会。

4.4 发奖授权

{
  grantId, periodId,
  source: "draw" | "final_claim" | "expiry_settlement",
  rewardIds,
  goods: [{ id, count }],
  status: "pending" | "delivered",
  createdAt, confirmedAt
}

授权内容生成后固定。可以合并同种道具数量,但必须保留来源格子列表。不同授权中不得出现同一玩家同一期的同一个格子。补发既有 pending 格子时返回原授权,不另建 expiry_settlement 授权覆盖它。

5. API 公共约定

  • 新活动接口建议使用 POST JSON,身份参数为 uid, token;token 与玩家归属由后端校验。
  • 沿用返回信封:成功 { code: 1, data, msg };失败 { code: 0, data: null, msg, errorCode }。支付适配如需其他已有包装,应保持客户端兼容。
  • 新状态响应统一提供服务端时间;金额使用整数分,时间使用 UTC 毫秒。
  • 下单、抽奖、终点领取携带 requestId。同一业务动作网络重试沿用同一个值,不得每次重试重新生成。
  • 幂等范围为 (uid, periodId, action, requestId);同键不同参数返回 IDEMPOTENCY_CONFLICT。
  • 已成功动作的重试先查原结果,再判断当前时间及层数;不能因已升级或到期而丢失原中奖结果。
  • 前端不上传最终中奖格子、随机结果或可信奖励数量;商品价格由后端配置确定。

6. API 契约

6.1 /pyramid/api(action:list):完整活动状态

请求:{ action:"list", uid, token, periodId? }。不传期次返回当前期,或尚未开始的最近一期;没有可展示期次时返回 data=null,不虚构一期。历史期次查询仅用于读取状态,不能恢复已过期机会。

响应主体:

{
  periodId, startAt, endAt, serverTime,
  status: "not_started" | "active" | "ended",
  currentLevel, drawCount, completed,
  canPay, canDraw, canClaimFinal,
  revision,
  pendingOrder: null | { orderId, paymentStatus },
  pendingGrants: Grant[],
  levels: [{
    level,
    itemId,       // 第六层为 null
    itemPrice,    // 第六层为 null
    rewards: [{
      rewardId, goods: [{ id, count }], up,
      gain, deliveryStatus
    }]
  }]
}

操作能力依据同一状态快照计算,前端仍可能拿到过时响应,所以操作接口必须再次校验。权重由后端配置保存,当前契约不要求在列表中返回。

canPay = 活动有效 AND 层数<=5 AND drawCount=0
         AND 无未决订单 AND 无本期待确认发奖 AND 未完成
canDraw = 活动有效 AND 层数<=5 AND drawCount>0
          AND 无未决订单 AND 无本期待确认发奖
canClaimFinal = 活动有效 AND 层数=6 AND 存在unclaimed奖励
                AND 无未决订单 AND 无本期待确认发奖

6.2 /wx/orderPaySig、/wx/iosorderPaySig:下单接入

两个入口识别后端配置登记的金字塔商品后,进入同一活动下单服务;不得依赖可被伪造的客户端活动标记绕过验证。

金字塔分支请求字段:

{
  uid, token, activityType: "pyramid",
  itemid, itemCount: 1, itemPrice,
  periodId, level, requestId
}

itemid 对应列表中的 itemId;itemPrice 即使为兼容旧调用而传入,也不得成为后端签名金额的依据。商品须匹配服务端当前层配置,金额从该配置读取;数量固定为 1。

新增订单前校验活动时间、层数、次数、发货状态及玩家身份。存在未决订单时恢复原订单,不创建第二笔。不同平台重复调用也不能产生两笔有效订单;无法复用支付参数时返回原订单状态并引导查单,不重新开单。

响应保留原 paySig, signature, signData, outTradeNo 等支付字段;可以追加 periodId, level。后续活动接口中的 orderId 统一使用 outTradeNo,不引入另一个客户端订单编号。

幂等重试若原订单已支付成功,返回该订单及恢复提示,不再要求付款。只有可信确认原单失败或关闭后,才允许新的购买动作创建另一订单。

6.3 /pyramid/api(action:query):支付状态与履约查询

请求:{ action:"query", uid, token, orderId }。

{
  orderId, periodId, level,
  paymentStatus: "pending" | "succeeded" | "closed" | "failed",
  fulfillmentStatus: "pending" | "credited" | "expired" | "exception",
  creditStatus: null | "available" | "consumed" | "expired",
  drawCount, serverTime,
  retryAfterMs
}

先验证订单归属,再通过本地持久化状态与必要的渠道主动查单恢复结果。drawCount 仅表示该订单绑定期次和层数在当前时间的有效剩余次数,不能直接覆盖另一期页面;前端必须同时核对期次与层数。

回调和主动查单共用履约服务。网络错误、超时、未收到回调、支付弹窗取消不直接产生 failed。只有渠道适配能证明为不可继续支付的终态,才能返回 closed/failed 并解除未决锁。

前台轮询应退避并遵守 retryAfterMs,退出页面可停止前台请求,但不删除服务端订单锁;重进、重登和后台查单可以继续恢复。具体间隔及后台查单频率作为运行参数确定。

6.4 /pyramid/api(action:draw):抽奖

请求:{ action:"draw", uid, token, periodId, level, requestId }。

响应:{ requestId, rewardId, goods, up, previousLevel, currentLevel, drawCount, grant, revision, serverTime }。

处理顺序:读取已成功的幂等结果;若没有,再检查活动有效、请求层数与当前层一致、有本层机会、无未决订单及待确认发奖。按剩余权重在服务端开奖,一次原子提交完成扣机会、记录中奖、创建授权、升级和保存响应快照。

第五层目标中奖推进至第六层时,在同次状态变化中标记全部剩余格子的领取资格。第五层目标自己的奖励仍通过本次抽奖授权发货,终点领取不能再次包含它。

开奖使用适合付费随机业务的服务端随机源;可用整数权重累计区间抽样,避免浮点累计遗漏尾部。并发竞争失败的计算结果不能返回客户端,必须恢复获胜提交或重新校验状态。

6.5 /pyramid/api(action:claim):第六层领取

请求:{ action:"claim", uid, token, periodId, level:6, requestId }。

响应:{ grant, currentLevel: 6, completed, revision, serverTime }。

校验当前第六层、活动有效、无未决订单及未确认发奖;选择所有 gain=true && deliveryStatus=unclaimed 的格子,一次形成稳定授权并改为 pending。不能用 gain=false 筛选终点奖励,因为到达第六层时资格已建立。

同一请求重试返回原授权。不同请求重复点击也不得生成第二份奖励;已有终点授权时返回已有状态或授权。确认前 completed=false,全部授权到账后才完成。

若第六层未配置固定奖励且所有格子都已抽中,不创建空授权;最后一笔中奖授权确认后直接满足本期完成条件,canClaimFinal=false。

活动到期后不创建新的主动领取,转由结束结算及待发列表恢复;原已成功领取请求仍返回原结果。

6.6 /pyramid/api(action:delivery_list):当前及历史期次待发列表

请求:{ action:"delivery_list", uid, token, cursor?, limit? },不要求传入当前期次。

响应:{ grants: Grant[], nextCursor, serverTime }。

返回该玩家所有期次 pending 授权。列表读取可以触发幂等的到期结算补偿,以覆盖定时任务延迟;不能因为前端未打开旧活动就漏掉奖励。

使用稳定游标顺序,例如 (createdAt, grantId),避免确认前页后集合变短造成偏移分页漏发。处理完一轮后允许从第一页重新拉取,所有授权按 grantId 去重。

登录、活动重连、在线活动到期后查询。补发处理与普通抽奖发货使用同一客户端授权处理队列,不另开一条无去重的加余额流程。

6.7 /pyramid/api(action:delivery_confirm):发货确认

请求:{ action:"delivery_confirm", uid, token, grantId }。无需客户端重传奖励数量。

响应:{ grantId, periodId, status: "delivered", completed, serverTime }。

校验授权归属,幂等地将授权及对应格子标记为 delivered,必要时更新完成状态。重复确认成功,不能再次发放。即使活动已结束也接受合法确认。

该接口记录客户端的到账确认,不凭空证明通用库存接口确实保存成功。严格的到账凭证与一致性方案必须按第 9 节实现和验证。

7. 支付原子性与异常恢复

7.1 下单锁

不能采用“查不到订单就插入”的非原子检查。建议在玩家期次记录上通过条件更新占用 pendingOrderId,预先生成订单号,再持久化订单及签名所需快照。

如果占位成功后进程中断,恢复流程必须根据占位订单号幂等地完成原订单创建或核验原订单,而非超时后换号重开。对外返回支付参数之前,订单和恢复信息必须持久化完成。

7.2 支付机会入账

核验订单身份、商品、金额、渠道与业务归属后,使用统一 fulfillPyramidOrder 服务:

  1. 持久化可信成功支付事实及首次核验时间;重复通知不得覆盖该首次时间。
  2. 已有该订单机会记录时返回原结果,不再次增加次数。
  3. 首次核验时活动已经结束:记录履约 expired,无可用机会。
  4. 首次核验有效且订单层数仍匹配:原子写入机会记录并解除该订单的未决锁。
  5. 结束前核验但入账恢复已跨越截止时,不能恢复可用机会;记录到期结果供对账。
  6. 当前层与订单层不符、额外成功订单或其他不变量破坏:进入 exception,不挪到高层或下一期,也不自动退款;保存证据并提供人工查单处理。这是异常防线,不是允许累积购买的新玩法。

玩家记录内的订单去重与机会写入必须原子完成。订单履约字段可以作为可恢复投影;若与玩家记录分开写入失败,下一次查单依据玩家记录修复订单投影,而不是再加一次机会。

7.3 渠道能力边界

现有通用查询不是可靠的支付终态判定器。实施时必须确认两个签名入口实际对应渠道的主动查单、关闭及签名更新能力;不能直接把其他活动的商户支付分支能力套用到本渠道。

在缺乏可信关闭能力时,未决继续保持,用户可以退出活动。提供订单号、归属期次、最后查询时间、渠道状态和错误信息供查单处理;不得通过“等待若干秒就失败”恢复购买。

8. 活动结束结算

后台扫描已结束期次的已参与玩家;待发列表读取也可以触发同一幂等结算服务。仅靠用户打开活动页面不能保证补发覆盖。

单个玩家结算需要与抽奖、终点领取、确认操作使用同一版本控制或事务边界:

  • 将剩余有效机会改为 expired,结束后不自动抽奖。
  • 保留全部已有 pending 授权。
  • 玩家在第六层时,将 unclaimed 奖励生成结束补发授权。
  • 玩家未到第六层时,不为 not_eligible 格子生成授权。
  • 已发放格子保持 delivered,设置结算标记,重跑不得新增重复奖励。

到达第六层和获得全部剩余资格必须与第五层目标中奖原子提交。因此,即使最后一次中奖响应丢失,结束结算也能确定应补发全部剩余奖励。

新一期创建独立玩家进度;旧期记录及授权保持可查。旧期未决支付不会转换为新期机会。未解决的旧期发货通过登录恢复队列处理,不把旧订单绑定到新期。

9. 前端发货的一致性要求

推荐客户端处理顺序:领取/恢复授权 → 检查 grantId 是否已处理 → 将奖励变更和处理记录一致地持久化 → 成功同步到账数据 → 调用确认接口。

如果客户端已经处理该授权,则重试保存/确认,不再次增加奖励。保存失败、确认超时、程序崩溃都不得改用新授权。一个授权包含多个道具时,需要整体持久化或记录可恢复的逐项进度,不能重试时重复发放先前已成功的部分。

现有余额整值上传与授权确认是独立操作:

  • 仅在后端记录 grantId 已确认,不能覆盖“余额已保存但确认丢失”的窗口。
  • 仅在本地记录已处理授权,不能天然覆盖缓存丢失、重新安装或服务端保存响应丢失。
  • 先确认后发货会导致中断时永久漏发,禁止采用。

实施依赖:需要选择并验证一种能将库存保存与已处理授权记录关联、持久化并恢复的方案,例如扩展保存协议,使授权去重和相关库存写入在同一原子操作中完成,前端仍负责按授权触发发货。不能未经验证就声明严格只到账一次;若保持现有通用保存协议不变,必须明确其保证边界,不能把限制隐藏在确认接口后面。

本次只给出该一致性要求,不把未经确认的通用库存重构当作已完成能力,也不扩展本活动到全游戏库存重构。

10. 错误与客户端行为

建议错误码/状态 含义及处理
AUTH_INVALID 身份或 token 无效,重新认证
PERIOD_NOT_FOUND 指定期次不存在,刷新活动
ACTIVITY_NOT_STARTED / ACTIVITY_ENDED 禁止新业务动作;已有授权仍可恢复
LEVEL_MISMATCH 请求层数过时,刷新状态
PAYMENT_PENDING 恢复原订单查询,不新建订单
DELIVERY_PENDING 先处理待发授权
NO_DRAW_CHANCE / DRAW_CHANCE_EXISTS 次数不满足抽奖/购买前置条件
FINAL_NOT_REACHED 未到第六层,不能领取全部
ALREADY_COMPLETED 本期已完成,不能重开
IDEMPOTENCY_CONFLICT 同一请求号被用于不同操作参数
CONFIG_INVALID 配置不可用,阻止继续付费并告警
PAYMENT_RECONCILE_REQUIRED 支付或履约异常,需要查单,不能默认为成功机会或失败订单

网络故障不是业务失败。幂等重复成功优先返回成功原结果,不使用错误码迫使客户端重新抽奖或重新付款。

11. 实施拆分与运行要求

建议活动内部模块为配置、状态服务、支付适配、授权发货、结束结算。两个现有签名入口只分派到同一金字塔支付服务,回调与主动查单共用同一履约逻辑。

数据库需至少支持玩家期次唯一约束、订单号唯一约束、授权唯一标识,以及玩家状态条件更新/事务。若按集合拆分授权,还需要保证格子到授权的唯一映射与中奖原子性。

审计记录保留:期次与配置版本、订单及层数、首次核验时间、机会入账/使用/作废、开奖候选权重快照或可重建配置、中奖格子、授权、确认及异常。不能输出支付密钥、token 等敏感内容。

监控至少覆盖长期未决订单、支付成功但履约未决、过期机会、长期未确认授权、结束结算失败、重复请求冲突及数据库并发冲突。具体运行周期、告警阈值、历史留存期在部署时确定,不在本次文档中虚构。

12. 验证计划

验证项 必须成立的结果
两个签名入口购买同一期同层 共用规则及锁,不产生两个未决订单;既有其他商品仍兼容
篡改价格、商品、数量、层数 不按客户端金额签名,不获得错误层机会
下单占位后崩溃 恢复原订单,不通过换订单号重复购买
回调与主动查单并发 同一订单只有一条机会记录
支付轮询超时/弹窗取消 保持未决,仍可退出游戏活动页面
升级后的旧订单重复通知 不给新层加次数
多目标格子及不放回 任一目标都能升级;已获得格子不再参与;概率按剩余权重计算
同一抽奖请求重试、不同请求并发 一次机会只消耗一次,返回的中奖结果与持久化结果一致
中奖到升级中途进程中断 不出现扣次数但无授权、升级但无终点资格的部分状态
终点领取与结束结算竞争 每个剩余格子只属于一个授权
到账保存响应丢失、确认丢失、多道具部分发放 恢复不漏发、不重复发放;按第 9 节验证库存层实际能力
截止时刻前后边界 now=endAt 禁止新操作;旧成功操作仍能重放结果
到期支付成功 支付事实为 succeeded,机会为 expired,不挪到新期
第五层中奖响应丢失后到期 根据已提交第六层资格补发全部剩余奖励
未通关到期 只保留已抽中未到账奖励,不补发未获得资格格子
结束任务重跑、分页期间逐笔确认 不生成重复授权,稳定游标不漏掉剩余待发记录
新一期开始、旧入口关闭 新进度独立,登录仍能恢复旧期待发授权

13. 待落实的实现事项

  1. 运营提供首期时间、五层商品与价格、格子、权重及终点奖励配置。
  2. 确认实际部署仓库,验证数据库唯一约束、条件更新及事务能力。
  3. 验证实际支付渠道的可信终态和原订单恢复能力。
  4. 落实授权记录与库存保存的一致性方案,并接入客户端登录恢复队列。
  5. 配置查单、结束结算、告警与数据留存运行参数。

以上为实施依赖,不改变已经确认的活动规则。本次交付仅为文档,未新增路由、数据库集合、支付逻辑或客户端行为。