MatchMaster/server/laf-cloud/rechargeStats.README.md

74 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 付费用户充值统计定时任务
云函数 `rechargeStats` 每次全量重算当前 `users.pay_user === true` 的玩家,将结果覆盖到 `users.rechargeStats`。默认每小时整点执行;不修改订单和玩家付费标记。
## 保存字段
| 字段 | 含义 |
| --- | --- |
| `amount15d` | 从本轮运行时刻倒推 15 × 24 小时的充值金额,元 |
| `amount30d` | 从本轮运行时刻倒推 30 × 24 小时的充值金额,元 |
| `amountTotal` | 截至本轮运行时刻、当前库中可统计的累计充值金额,元 |
| `currency` | 固定 `CNY` |
| `asOf` | 本轮统计截止时间,原始 Unix 毫秒时间戳 |
| `orderCount` | 纳入累计金额的去重订单数 |
| `fallbackTimeOrderCount` | 无支付确认时间,使用下单时间的订单数 |
| `missingTimeOrderCount` | 支付和下单时间均缺失,仅计入累计的订单数 |
| `invalidOrderCount` | 缺失订单号、金额或数量无效而跳过的记录数 |
| `duplicateOrderCount` | 跳过的重复订单记录数 |
| `missingOpenid` | 用户是否缺少可关联订单的 openid |
| `version` | 统计规则版本,当前为 1 |
## 统计口径
- 通过 `users.openid = order.openid` 关联,仅纳入数值型 `state: 1/2` 的记录。未确认支付的 `state: 0` 和临时集合 `iosOrder` 不参与。
- 排除 `paymentAppEnv: "test"` 或 `outTradeNo` 以 `wct_` 开头的测试订单;保留没有环境字段的历史订单。
- 每个 openid 内按 `outTradeNo` 去重,同号多条按 `_id` 升序取第一条金额有效、时间不在未来的记录。若同号金额不同,应人工核查重复数据。
- 金额为 `goodsPrice × itemCount`,先用整数分累加,再除以 100 保存为元。金额和数量兼容数字字符串,要求正安全整数;缺失数量不默认当作 1,异常记录计入 `invalidOrderCount`。累计金额超出安全整数范围则报错,不保存失真的数值。
- 每轮只获取一次当前时间,窗口为 `[asOf - N × 86400000, asOf]`,不是自然日,也不是从午夜倒推。
- 优先使用 `chargeTime`。当前支付写入代码使用 `new Date(Date.now() + 8小时)`,因此还原时减去 8 小时;此处针对现有存储约定,若以后修正支付时间存储方式,必须同步调整统计规则。
- `chargeTime` 缺失或无效时回退到原始 `order.time`,不再减 8 小时;两个时间都不可用时只计入累计,并记录异常数量。支持原始毫秒数、Date 和带时区的 ISO 字符串;不解析无时区日期字符串。
- 已知时间在未来的订单暂不纳入任何金额。没有匹配订单的付费用户也保存零值结果;这不代表迁移前从未充值,结合 `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` 的配置:每小时整点执行。
4. 检查已有触发器,避免重复创建。若使用已登录且已绑定正确应用的 Laf CLI,可执行:
```shell
laf trigger list
laf trigger create "付费用户充值统计(每小时)" rechargeStats "0 * * * *"
```
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
```
覆盖滚动时间边界、8 小时修正、支付时间优先、时间回退、无效金额、数量、去重、测试订单排除、超过 1000 条订单、多页用户、重跑、并发写保护、异常中断和关闭 HTTP 入口。使用内存 MongoDB 接口替身,不连接生产数据库。