78 lines
6.9 KiB
Markdown
78 lines
6.9 KiB
Markdown
# 付费用户充值统计定时任务
|
||
|
||
云函数 `rechargeStats` 每次全量重算当前 `users.pay_user === true` 的玩家,将结果覆盖到 `users.rechargeStats`。每日凌晨 03:00 执行,统计截止时间固定回退 3 小时;不修改订单和玩家付费标记。
|
||
|
||
## 保存字段
|
||
|
||
| 字段 | 含义 |
|
||
| --- | --- |
|
||
| `amount15d` | 从统计截止时间倒推 15 × 24 小时的充值金额,整数分 |
|
||
| `amount30d` | 从统计截止时间倒推 30 × 24 小时的充值金额,整数分 |
|
||
| `amountTotal` | 统计截止时间之前、当前库中可统计的累计充值金额,整数分 |
|
||
| `currency` | 固定 `CNY` |
|
||
| `unit` | 固定 `fen`,金额单位为分 |
|
||
| `asOf` | 本轮运行时间减 3 小时,原始 Unix 毫秒时间戳 |
|
||
| `orderCount` | 纳入累计金额的去重订单数 |
|
||
| `fallbackTimeOrderCount` | 无支付确认时间,使用下单时间的订单数 |
|
||
| `missingTimeOrderCount` | 支付和下单时间均缺失,仅计入累计的订单数 |
|
||
| `invalidOrderCount` | 缺失订单号、金额或数量无效而跳过的记录数 |
|
||
| `duplicateOrderCount` | 跳过的重复订单记录数 |
|
||
| `missingOpenid` | 用户是否缺少可关联订单的 openid |
|
||
| `version` | 统计规则版本,当前为 2 |
|
||
|
||
版本 1 的金额单位为元。版本 2 首次运行会重新计算并整体覆盖旧统计,即使旧版 `asOf` 晚于新版截止时间,也允许完成升级。读取方应以 `version: 2 / unit: "fen"` 识别分单位;运行未覆盖到的用户仍可能保留旧版数据,不能仅按字段名判断单位。
|
||
|
||
## 统计口径
|
||
|
||
- 通过 `users.openid = order.openid` 关联,仅纳入数值型 `state: 1/2` 的记录。未确认支付的 `state: 0` 和临时集合 `iosOrder` 不参与。
|
||
- 排除 `paymentAppEnv: "test"` 或 `outTradeNo` 以 `wct_` 开头的测试订单;保留没有环境字段的历史订单。
|
||
- 每个 openid 内按 `outTradeNo` 去重,同号多条按 `_id` 升序取第一条金额有效、未达到统计截止时间的记录。若同号金额不同,应人工核查重复数据。
|
||
- 金额为 `goodsPrice × itemCount`,用整数分累加并直接保存,不除以 100。金额和数量兼容数字字符串,要求正安全整数;缺失数量不默认当作 1,异常记录计入 `invalidOrderCount`。累计金额超出安全整数范围则报错,不保存失真的数值。
|
||
- 每轮只获取一次当前时间,`asOf = Date.now() - 3小时`,窗口为 `[asOf - N × 86400000, asOf)`,含起点、不含截止点。累计金额也使用同一截止点。每天 03:00 准时运行时,截止点即当天 00:00,凌晨 00:00~03:00 的订单留到下一天统计。手动运行或触发延迟时也固定回退 3 小时,不自动截断到午夜。
|
||
- 例如北京时间 2026-09-15 03:00 执行:15 天窗口为 `[2026-08-31 00:00, 2026-09-15 00:00)`;30 天窗口为 `[2026-08-16 00:00, 2026-09-15 00:00)`;累计统计到 2026-09-15 00:00 之前。
|
||
- 优先使用 `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 数据库控制台建立以下非唯一索引(若已有等价索引则复用):
|
||
|
||
```javascript
|
||
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` 字段。
|
||
|
||
1. 在目标 Laf 应用发布 `functions/rechargeStats.ts` 和同名 YAML,保持 `methods: []`,不开放公共 HTTP 入口。
|
||
2. 在云函数控制台手动执行一次 `rechargeStats`(无参数),检查返回的 `processedUsers`、`updatedUsers` 和部分玩家的结果,确认运行耗时。
|
||
3. 在触发器面板绑定函数 `rechargeStats`,使用 `recharge-stats.trigger.json` 的配置:每日 03:00 执行。表达式 `0 3 * * *` 按触发器时区解释;目标是北京时间 03:00,应确认调度时区为 `Asia/Shanghai`。如果部署使用 UTC 调度,则使用 `0 19 * * *`(UTC 19:00 为次日北京时间 03:00)。3 小时统计回退和订单存储的 8 小时修正是两件事,不改变 Unix 时间戳所属时区。
|
||
4. 检查已有触发器,避免重复创建。若使用已登录且已绑定正确应用的 Laf CLI,可执行:
|
||
|
||
```shell
|
||
laf trigger list
|
||
laf trigger create "付费用户充值统计(每日凌晨3点)" rechargeStats "0 3 * * *"
|
||
```
|
||
|
||
5. 首次定时触发后确认日志出现 `rechargeStats completed`,抽查 `users.rechargeStats.asOf` 已更新。大规模历史数据上线前应确认全量耗时在云函数执行时限内。
|
||
|
||
参考:[Laf 定时任务文档](https://doc.laf.run/zh/cloud-function/cron.html)、[官方 CLI 触发器命令](https://github.com/labring/laf/blob/main/cli/src/command/trigger/index.ts)、[创建触发器字段](https://github.com/labring/laf/blob/main/server/src/trigger/dto/create-trigger.dto.ts)。
|
||
|
||
## 本地测试
|
||
|
||
Node.js 24.11 或兼容 `registerHooks` / `stripTypeScriptTypes` 的版本:
|
||
|
||
```shell
|
||
node --test laf-cloud/tests/recharge-stats.test.mjs
|
||
```
|
||
|
||
覆盖分单位、3 小时截止回退、午夜半开区间边界、凌晨订单次日计入、旧版元统计升级、8 小时存储修正、支付时间优先、时间回退、无效金额、数量、去重、测试订单排除、超过 1000 条订单、多页用户、重跑、并发写保护、异常中断和关闭 HTTP 入口。使用内存 MongoDB 接口替身,不连接生产数据库。
|