试用转正、自动续费总是扣款后才想起来?用 MPChat Bot API + Python 写一个订阅到期提醒机器人(附 Mock 测试)
海外软件试用转正、会员涨价、年度续费,多数人是在扣款短信到了以后才想起来。这篇用 MPChat Bot API 和 Python 标准库写一个订阅到期提醒机器人:私聊里发 /add 记下日期,到期前 3 天、前 1 天各推送一次,并按事件类型附上处理建议(取消试用、关闭自动续费、重新评估涨价)。文末附本地 Mock 测试,模拟 9 天的日期推进,验证提醒时点、去重和 429 重试。
你会用到的点:getUpdates 长轮询、sendMessage 推送、429 指数退避、状态持久化防重复提醒、注入 today_fn 做时间相关测试。
一、为什么把提醒放进聊天机器人
日历也能提醒,用起来有两处不顺:添加时要切出当前对话,提醒弹出来也只有一行标题,接下来该做什么还得自己想。放进每天都会打开的聊天工具里,一条 /add 就能记下,到点时提醒和处理建议在同一条消息里,扫一眼就能做决定。
MPChat Bot API 有一条限制直接决定了交互设计:机器人不能主动发起会话。用户先给机器人发过消息,机器人才能往这个聊天里推送。这种被动触发的机制,对提醒类机器人来说正好把骚扰挡在外面。
二、接口规则速览
以下来自 MPChat 帮助中心 Bot 开发文档:
| 项 | 规则 |
| 请求地址 | https://call.mp.net/bot/bot{TOKEN}/{methodName},token 在路径里 |
| 拉消息 | getUpdates 长轮询,timeout 最大 50,limit 最大 100 |
| 发消息 | sendMessage,必填 chat_id、text |
| 频率 | 每个 token 默认每秒 30 次,超出返回 429 |
| 重试 | 429、500 用有上限的指数退避;400、401、403 不重试 |
| ID | chat_id 可能超出 32 位,按字符串或 BIGINT 存 |
三、数据模型
每个聊天对应一组记录。每条记录有四个业务字段,外加一个“哪些时点已经提醒过”:
{ "offset": 15, "chats": { "778899001122": [ {"name": "修图App", "date": "2026-11-03", "kind": "试用", "amount": "9.99", "sent": [3]} ] }}
kind 只有三种:试用、续费、涨价。三种事件的容错成本不同,要做的决定也不同,提醒文案分开写。sent 记下已经发过的提前天数,同一个时点只提醒一次,机器人重启后也不会重发。
四、核心逻辑:每天检查一次,只在固定时点提醒
REMIND_DAYS = (3, 1)
def due_reminders(state, today): out = [] for chat_id, items in state["chats"].items(): for it in items: left = (date.fromisoformat(it["date"]) - today).days
if left in REMIND_DAYS and left not in it["sent"]: it["sent"].append(left) out.append((chat_id, it, left)) return out
选前 3 天和前 1 天,是因为很多服务取消自动续费后仍能用到本期结束。前 3 天是决策窗口,前 1 天用来最后确认。按类型给出的处理建议:
ACTION = { "试用": "不打算付费的话,今天就去服务方账户里取消试用。", "续费": "还在用就留着;不常用的,去账户设置里关掉自动续费,一般能用到本期结束。", "涨价": "先看看上个月用了几次,再决定继续、降档还是取消。",}
主循环把“增量消息处理”和“每日时点检查”解耦,每日检查按日期去重:
def run(bot, today_fn=date.today, max_loops=None): state = load_state() last_check = None while ...: today = today_fn() for upd in bot.call("getUpdates", offset=state["offset"], timeout=30, limit=50, allowed_updates=["message"]) or []: state["offset"] = upd["update_id"] + 1 if "message" in upd: handle(bot, state, upd["message"], today) save_state(state) if last_check != today: send_reminders(bot, state, today) last_check = today
save_state(state)
today_fn 做成可注入参数,测试时可以直接模拟日期推进。
五、请求封装与 token 安全
def call(self, method, **params): ...
if data.get("ok"): return data.get("result") code = data.get("error_code") if code in RETRYABLE and attempt < self.max_retries: self.sleep(delay) delay = min(delay * 2, self.max_delay) continue raise MPBotError(code, data.get("description", ""))
帮助中心对 token 有几条要求:不进前端代码,日志脱敏,.env 加入 .gitignore,不贴进工单或 AI 对话,泄露后立刻在 BotFather 吊销并更换。本文代码里 token 只拼进 URL,日志只打印方法名和错误码。
六、本地 Mock 测试:不用干等,把 9 天压缩成几秒
test_with_mock.py 起一个本地 HTTP 服务模拟接口。用户先添加三条记录,另发一条格式错误的命令,然后把日期从 10/31 推进到 11/08,每天跑一轮。第一次 sendMessage 故意返回 429,用来验证退避重试。实际输出(节选):
[retry] sendMessage 返回 429,1s 后重试
[10-31] 已记下:修图App,2026-11-03 免费试用到期,之后自动转为付费。
[10-31] 格式:/add 名称 YYYY-MM-DD 试用|续费|涨价 [金额]
[10-31] 1. 修图App|试用|2026-11-03(还有 3 天) 9.99 美元
2. 云盘|续费|2026-11-05(还有 5 天) 2.99 美元
3. 音乐会员|涨价|2026-11-07(还有 7 天) 12.99 美元
[10-31] ⏰ 修图App:3 天后(2026-11-03)免费试用到期,之后自动转为付费,金额 9.99 美元。
不打算付费的话,今天就去服务方账户里取消试用。
[11-02] ⏰ 修图App:1 天后(2026-11-03)……
[11-02] ⏰ 云盘:3 天后(2026-11-05)自动续费扣款,金额 2.99 美元。
[11-04] ⏰ 云盘:1 天后……
[11-04] ⏰ 音乐会员:3 天后(2026-11-07)新价格生效……
[11-06] ⏰ 音乐会员:1 天后……
全部断言通过
断言覆盖四件事:3 条记录共触发 6 次提醒;提醒日期正好落在前 3 天和前 1 天;格式错误会返回提示;过期两天以上的记录自动清理。示例里的软件名和金额为构造数据。
七、几个常见问题
机器人重启后会不会重复提醒? 不会。每条记录的 sent 字段持久化在状态文件里,重启后读回来,已发过的时点会被跳过。
chat_id 为什么存成字符串? 文档说明 chat_id 可能超出 32 位整数范围,用字符串或 BIGINT 存最省心,在 JSON 里直接当 key 用也方便。
为什么不用系统 cron 定时发?getUpdates 长轮询本身就在常驻循环里,顺手按日期做一次检查,少维护一个进程,状态也只在一处读写。
八、可以继续扩展的方向
-
按钮代替命令:提醒消息下挂“已取消”“继续用”的
inline_keyboard按钮,收到callback_query后在 300 秒内调用answerCallbackQuery,再把记录归档。 -
自动顺延:续费类记录到期后按月顺延到下一次日期。
-
团队群共用:把机器人放进研发团队群,盯住公用 SaaS 账号的续费节点,比如 CI/CD 服务、设计协作工具,谁都能看到哪个账号快到期了。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)