云上代金券与资源到期的每日快照与幂等兑换

举报
yd_254065487 发表于 2026/10/31 05:21:20 2026/10/31
【摘要】 代金券的余额和有效期是两个独立维度,资源到期又是第三件事。本文给出一套每日只读快照加幂等兑换的盯守方案,含可直接执行的 SQL、shell 与告警分档。

账上有券,账单还是超了

这个月对账时遇到一组很典型的组合故障:账户里还有两张代金券,余额合计足够覆盖当月 ECS 与 RDS 的开销,财务看了一眼就放过了。结果第一张券 10 月 12 日 23:59:59 到期,第二张只覆盖对象存储;而真正花钱的包年包月实例 10 月 8 日到期后自动转成了按需。月底账单超预算四成,券一分没少,钱一分没省。

拆开看,代金券至少有三个互相独立的维度:余额(还能抵多少)、有效期(什么时候作废)、适用范围(能不能抵这笔)。只盯余额,等于只看了三分之一。资源侧同理,到期时间和自动续费开关跟券完全解耦——券再多,实例转按需照样产生费用。

为什么是每日快照,不是实时轮询

  • 费用中心查询接口有 QPS 限制和日调用配额。cron 一分钟跑一次,一天 1440 次调用,换回来的信息量接近零。
  • 余额只在兑换、核销、过期三个时刻变化,有效期根本不变。轮询拿不到新信息。
  • 快照能回答“和昨天比差了什么”,实时轮询只有瞬时值。过期那一刻没采样到,信息就永久丢了。
  • 只读快照可以放心交给所有值班同学执行;一旦脚本里混进写操作,权限和审计就得整套收紧。

快照流水线:原始 JSON 落盘 + 按天主键表

第一步只做一件事:把接口原始返回落盘,不做解析。解析逻辑改了还能重放,原始返回丢了只能等明天。

hcloud BSS <OperationName> --cli-region=cn-north-4 --cli-output=json > /var/lib/cost-guard/raw/coupons-$(date -u +%F).json

操作名用 API Explorer 里费用中心查询代金券列表对应的那个,不要直接照抄示例。第二步入库,主键是(snap_date, coupon_id),同一天重跑不会产生脏数据:

CREATE TABLE IF NOT EXISTS coupon_snapshot(snap_date TEXT NOT NULL, coupon_id TEXT NOT NULL, amount REAL, balance REAL, expire_at TEXT, scope TEXT, status TEXT, PRIMARY KEY(snap_date, coupon_id));

写入用 INSERT OR REPLACE,snap_date 取 UTC 日期,重跑当天任务就是覆盖,天然幂等。到期时间统一转成 UTC 再入库(形如 2026-11-12 23:59:59),否则下面的 julianday 差值会算错。

解析字段集中在一处,返回体字段跟文档对不上只改这里:FIELDS = {'id':'coupon_id','balance':'balance','expire':'expire_time','amount':'face_value','scope':'product_scope','status':'status'}。取不到就整条丢弃并打一条 skip coupon: field missing 日志,不要用默认值兜底——兜底出来的余额 0 会让你误判成券是空的。

有了按天表,“临期且有余额”就是一句 SQL:

SELECT coupon_id, balance, expire_at FROM coupon_snapshot WHERE snap_date = date('now') AND balance > 0 AND julianday(expire_at) - julianday('now') BETWEEN 0 AND 30 ORDER BY expire_at;

资源侧再建一张 resource_snapshot(snap_date, resource_id, product, expire_at, auto_renew),两条查询结果合并成当天的风险清单,再进告警。

兑换动作:一次性开关、幂等记录、失败不重试

代金券的兑换接口大概率不是幂等的:超时返回时你无法判断是“没成功”还是“成功了但响应丢了”。所以这个动作绝不能挂在定时任务里自动重试,必须拆成独立脚本,加三道闸。

  • 一次性开关:脚本开头判断 [ -f /etc/cost-guard/redeem.enabled ] || exit 0。文件由人手动创建,跑完手动删除。没这个文件,脚本什么都不做。
  • 幂等记录:先写记录再调接口。CREATE TABLE redeem_log(coupon_id TEXT PRIMARY KEY, decided_at TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('pending','done','failed')), err TEXT);
  • 失败不自动重试:接口失败就把 state 写成 failed 并 exit 1,交给告警和人。超时这类状态不确定的情况一律按 failed 处理,人工到控制台核对后再决定。

抢占式写入用一条语句拿结果,避免“查了再插”的竞态:

sqlite3 $DB "INSERT OR IGNORE INTO redeem_log(coupon_id,decided_at,state) VALUES('$CID',datetime('now'),'pending'); SELECT changes();"

返回 1 才继续调兑换接口,返回 0 说明这张券此前已处理过,直接退出。成功后再执行 UPDATE redeem_log SET state='done' WHERE coupon_id='$CID';

监控脚本里不要顺手触发任何写操作

这条不是洁癖。快照脚本每天被 cron 跑一次,值班同学排查问题还会手动跑几次,一旦里面混进兑换或续费,等于把写操作的执行次数交给“谁手滑跑了一遍”决定。更糟的是告警链路:写操作失败触发告警,告警动作又去重跑脚本,一个下午就能把兑换接口打進风控。

做法很简单:读和写分成两个脚本、两套权限、两条告警通道。只读脚本给所有人执行权限,只写 snapshot 表;写脚本只有运维负责人能执行,且必须有人为创建的一次性开关文件。审计也好交代——查 redeem_log 就能还原每一次写操作是谁、什么时候、为什么触发的。

告警节奏

按剩余天数分档:30 天进周报,14 天进日常群,7 天和 3 天各提醒一次,1 天内升到电话。去重键用 coupon_id + 档位,同一张券同一档只发一次;否则每天一跑就是每天一条,三天之后没人再看。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。