7.3 KiB
付费用户充值统计定时任务
云函数 rechargeStats 每次全量重算当前 users.pay_user === true 的玩家,将结果覆盖到 users.rechargeStats。每日凌晨 03:00 执行,统计截止时间固定为北京时间前一天 23:59:59.999;不修改订单和玩家付费标记。
保存字段
| 字段 | 含义 |
|---|---|
amount15d |
从统计截止时间倒推 15 × 24 小时的充值金额,整数分 |
amount30d |
从统计截止时间倒推 30 × 24 小时的充值金额,整数分 |
amountTotal |
截至统计截止时间(含)、当前库中可统计的累计充值金额,整数分 |
currency |
固定 CNY |
unit |
固定 fen,金额单位为分 |
asOf |
本轮执行日期的北京时间前一天 23:59:59.999,原始 Unix 毫秒时间戳 |
orderCount |
纳入累计金额的去重订单数 |
fallbackTimeOrderCount |
无支付确认时间,使用下单时间的订单数 |
missingTimeOrderCount |
支付和下单时间均缺失,仅计入累计的订单数 |
invalidOrderCount |
缺失订单号、金额或数量无效而跳过的记录数 |
duplicateOrderCount |
跳过的重复订单记录数 |
missingOpenid |
用户是否缺少可关联订单的 openid |
version |
统计规则版本,当前为 3 |
版本 1 的金额单位为元,版本 2 使用分单位但按运行时间减 3 小时作为截止点。版本 3 首次运行会重新计算并整体覆盖版本 1/2 的统计,即使旧版 asOf 晚于新版截止时间,也允许完成升级。读取方应以 unit: "fen" 识别分单位,以 version: 3 识别固定前一天末尾的截止口径;运行未覆盖到的用户仍可能保留旧版数据,不能仅按字段名判断单位。
统计口径
- 通过
users.openid = order.openid关联,仅纳入数值型state: 1/2的记录。未确认支付的state: 0和临时集合iosOrder不参与。 - 排除
paymentAppEnv: "test"或outTradeNo以wct_开头的测试订单;保留没有环境字段的历史订单。 - 每个 openid 内按
outTradeNo去重,同号多条按_id升序取第一条金额有效、不晚于统计截止时间的记录。若同号金额不同,应人工核查重复数据。 - 金额为
goodsPrice × itemCount,用整数分累加并直接保存,不除以 100。金额和数量兼容数字字符串,要求正安全整数;缺失数量不默认当作 1,异常记录计入invalidOrderCount。累计金额超出安全整数范围则报错,不保存失真的数值。 - 每轮只获取一次当前时间,按北京时间计算当天 00:00,再减 1 毫秒得到
asOf。15/30 天窗口为(asOf - N × 86400000, asOf],不含起点、包含截止点,恰好覆盖过去 N 个完整自然日。累计金额也包含同一截止点。当天订单留到下一天统计;同一北京时间日期内,无论凌晨准时执行、延迟执行还是白天手动执行,截止点均一致,不依赖服务器本地时区。 - 例如北京时间 2026-09-15 任意时间执行:
asOf = 2026-09-14 23:59:59.999;15 天包含2026-08-31 00:00:00.000至2026-09-14 23:59:59.999;30 天包含2026-08-16 00:00:00.000至2026-09-14 23:59:59.999;累计统计至同一截止点(含)。 - 优先使用
chargeTime。当前支付写入代码使用new Date(Date.now() + 8小时),因此还原时减去 8 小时;此处针对现有存储约定,若以后修正支付时间存储方式,必须同步调整统计规则。 chargeTime缺失或无效时回退到原始order.time,不再减 8 小时;两个时间都不可用时只计入累计,并记录异常数量。支持原始毫秒数、Date 和带时区的 ISO 字符串;不解析无时区日期字符串。- 已知时间晚于
asOf的订单暂不纳入任何金额。没有匹配订单的付费用户也保存零值结果;这不代表迁移前从未充值,结合orderCount核查历史完整性。 - 当前项目没有退款同步逻辑,因此这是当前已确认订单的金额统计,不扣除退款,不还原渠道优惠后的实付。订单状态异常仍可能造成偏差。
执行与一致性
用户按 _id 游标每批 100 条读取,订单使用 MongoDB 游标遍历,避免默认查询上限截断。每批只更新 rechargeStats,写入时重新检查 pay_user 和 openid,且较早统计日期不会覆盖较新 asOf 的快照。同一天重跑会覆盖该天统计,不重复累加;跨到下一个北京时间日期时,旧订单会按新截止时间退出滚动窗口。
整轮不是数据库事务:运行期间新支付或用户变更可能下一轮才体现。失败时抛出错误,已完成批次保留,剩余用户下次重算;每个用户的 asOf 可判断新旧结果,不能把混合轮次当作同一时刻的全库快照。
建议上线前在 Laf 数据库控制台建立以下非唯一索引(若已有等价索引则复用):
db.collection('users').createIndex({ pay_user: 1, _id: 1 });
db.collection('order').createIndex({ openid: 1, outTradeNo: 1, _id: 1 });
发布和启用
代码与触发器配置是独立资源,提交本地文件不会自动启动线上定时任务。recharge-stats.trigger.json 使用 Laf 创建触发器 API 的 desc / target / cron 字段。
- 在目标 Laf 应用发布
functions/rechargeStats.ts和同名 YAML,保持methods: [],不开放公共 HTTP 入口。 - 在云函数控制台手动执行一次
rechargeStats(无参数),检查返回的processedUsers、updatedUsers和部分玩家的结果,确认运行耗时。 - 在触发器面板绑定函数
rechargeStats,使用recharge-stats.trigger.json的配置:每日 03:00 执行。表达式0 3 * * *按触发器时区解释;目标是北京时间 03:00,应确认调度时区为Asia/Shanghai。如果部署使用 UTC 调度,则使用0 19 * * *(UTC 19:00 为次日北京时间 03:00)。统计日期固定按北京时间计算,与订单存储的 8 小时修正分别处理。 - 检查已有触发器,避免重复创建。若使用已登录且已绑定正确应用的 Laf CLI,可执行:
laf trigger list
laf trigger create "付费用户充值统计(每日凌晨3点)" rechargeStats "0 3 * * *"
- 首次定时触发后确认日志出现
rechargeStats completed,抽查users.rechargeStats.asOf已更新。大规模历史数据上线前应确认全量耗时在云函数执行时限内。
参考:Laf 定时任务文档、官方 CLI 触发器命令、创建触发器字段。
本地测试
Node.js 24.11 或兼容 registerHooks / stripTypeScriptTypes 的版本:
node --test laf-cloud/tests/recharge-stats.test.mjs
覆盖分单位、固定北京时间前一天末尾截止、15/30 天毫秒边界、准时/延迟/手动执行、跨月/跨年/闰日、当天订单次日计入、版本 1/2 统计升级、8 小时存储修正、支付时间优先、时间回退、无效金额、数量、去重、测试订单排除、超过 1000 条订单、多页用户、重跑、跨统计日期写保护、异常中断和关闭 HTTP 入口。使用内存 MongoDB 接口替身,不连接生产数据库。