并没有一个软件可以完全吻合我的要求,我也不希望下载很多软件。之前做了网站,但是没有提醒
在感觉之前网站功能太复杂了,这一次简化并添加了提醒。
前期规划
“做一个自我管理工具”只是方向,不是需求。为了避免项目最后长成一个什么都有、什么都不顺手的后台,第一步还是应该想清楚自己做什么。
| 目标 | 我想要做的 | 验收方式 |
|---|---|---|
| 容易记录 | 常用内容集中在一个入口,必填字段保持最少 | 一次记录不需要跨多个应用;无关字段可以不填 |
| 可回溯 | 原始数据可以按日期、类型和少量分类找回 | 能从年度视图定位到某一天,再回到原始记录 |
| 可行动 | 回看结果能够形成任务、日程或明确调整 | “发现问题”之后能落到一个下一步动作 |
| 按需提醒 | 只有存在明确动作或时间条件时才生成通知 | 已完成、空内容和重复任务不发送 |
| 可排查 | 每次投递都有状态和错误上下文 | 能区分规则未命中、任务未执行、渠道未受理和终端未收到 |
自我管理的基础闭环并不复杂:
flowchart LR
A[记录事实] --> B[按周期回看]
B --> C[形成具体任务或日程]
C --> D[必要时生成提醒]
D --> E[完成、取消或调整]
E --> B体重趋势异常,不应该只停在一张曲线上,而要转成一个可以执行的调整;账目回看发现某类支出过高,也要落到下个周期的具体行动。提醒只是把人叫回这个闭环,不能替代判断,更不能把每条数据都广播一遍。
领域建模
体重、账目、任务、日历和日记可以出现在同一个首页,但不适合共用一张含义模糊的 records 表。一个专业但仍然轻量的模型,可以从下面这些对象开始。
| 对象 | 最小字段 | 核心约束 | 主要用途 |
|---|---|---|---|
| Measurement | measured_at、type、value、unit | 同一测量类型保持单位一致 | 观察时间序列趋势 |
| LedgerEntry | occurred_at、direction、amount、currency、category | 金额使用定点数,收支方向明确 | 汇总资金流向 |
| Task | title、status、due_at?、completed_at? | 有状态生命周期,完成后停止催办 | 管理需要完成的动作 |
| CalendarEvent | title、start_at、end_at、timezone | 结束时间不早于开始时间 | 描述占用明确时间段的安排 |
| JournalEntry | entry_date、content | 允许缺席,不强制产生任务 | 保存叙述、想法和上下文 |
| ReminderRule | source_type、source_id?、trigger、channel、enabled | 引用业务对象,不修改业务事实 | 决定何时提醒什么 |
这几个边界可以用一句话记住:记录保存事实,任务管理承诺,日历占用时间,日记保存叙述,提醒负责触达。
例如,“今天 72.4 kg”是已经发生的测量记录;“周五前提交材料”是任务,即使有截止时间,也不代表它占用了整个日历时段;“周四 14:00 到 15:00 复诊”是日历事件;“复诊以后感觉轻松了一些”属于日记;“周四 13:30 提醒出门”才是从日历事件派生出来的提醒规则或通知任务。对象分开以后,统计和状态流转才不会互相污染。
下面展示的是第一版聚合后的日常入口。日期、体重、固定打卡、今日重点、临时事项和备注可以在同一页操作,这属于产品层面的统一体验。
但是我后面觉得固定大卡很麻烦,经常有很多突发情况,因为不能完成,看到自己打卡中断会很有挫败感..不如直接取消掉。最后的版本中取消了。

统一入口能够降低记录成本;它不能反过来证明底层数据也应该合并成一个模型。
下面则是按日期聚合的年度总览。它更适合作为读模型:从多个领域对象投影出适合浏览的数据,而不是直接承担所有原始数据的写入和校验。

年度总览是便于查询的投影视图,原始账目、测量、任务和日记仍应保留各自结构。
这就是“统一入口”和“统一数据”的区别:写模型维护各自的业务约束,读模型负责把结果组合成人容易理解的页面。即使第一版没有正式采用 CQRS,也可以保留这种思维,不让界面布局直接绑死数据库结构。
通知架构
提醒不应该直接散落在记账、日记和日历代码里。更稳妥的最小架构,是先根据业务数据和规则生成通知任务,再由渠道适配器负责投递。
flowchart LR
A[测量 / 账目 / 任务 / 日历] --> B[规则判断]
C[定时扫描器] --> B
B --> D[(NotificationJob)]
D --> E[投递 Worker]
E --> F{渠道适配器}
F --> G[WxPusher]
F --> H[Server 酱 SaaS]
F --> I[自建适配器或 wecomchan]
I --> J[企业微信 API]
E --> K[(DeliveryAttempt)]
E --> L[结构化日志与指标]这里要分清平台与实现:企业微信是上游平台和 API;wecomchan 是调用这套 API 的一种开源实现,也可以自己写一个很小的适配器。二者不能放在同一个概念层级。
核心记录先成功保存,通知任务再异步处理。这样换渠道、临时停用提醒或进行失败重试,都不会影响账目和日记本身。个人项目不一定要上完整消息队列;同一个数据库里的通知任务表加一个定时 Worker,已经能获得大部分收益。
NotificationJob 至少可以包含 user_id、rule_id、scheduled_slot_utc、scheduled_at、status、channel、payload_ref 和 lease_expires_at;每次渠道调用再写一条 DeliveryAttempt,记录尝试次数、耗时、服务商返回码和脱敏后的错误。通知正文与凭证不要直接复制进普通日志。
防止重复建单
计划任务可能重跑,服务也可能在发送途中重启。可以对下面的复合键建立唯一约束:
(user_id, rule_id, scheduled_slot_utc)其中 scheduled_slot_utc 是规范化后的计划时间槽。唯一约束可以防止同一轮扫描重复创建任务,但它无法解决所有重复投递:如果渠道已经受理,客户端却在收到响应前超时,系统并不知道消息究竟有没有发出去。此时再次调用,仍可能产生第二条消息。分布式系统里,“建单幂等”和“端到端 exactly-once”不是一回事。
Worker 领取任务时应使用原子状态更新,把 pending 改成 processing,同时写入租约到期时间。进程崩溃后,过期租约可以被其他 Worker 回收,避免任务永久卡住。真正发送前还要重新检查业务状态:若任务已经完成、规则已经停用或提醒已经失去意义,就把通知标记为 suppressed 或 cancelled。
推荐的最小状态机可以写成:
pending -> processing -> accepted
| | -> retry_wait -> processing
| | -> failed
| -> 租约超时后重新领取
-> suppressed / cancelled这里使用 accepted,不轻易写成 delivered。前者表示渠道已经受理,后者暗示终端已经收到,需要更强的回执证据。
重试必须区分“临时失败”“配置错误”和“结果未知”
| 失败类型 | 建议策略 |
|---|---|
| 明确的临时 5xx、限流 | 有上限的指数退避,并加入少量随机抖动 |
| access_token 过期 | 刷新 token 后立即重试一次 |
| 请求超时、连接中断 | 视为结果未知;能查询发送记录时先查询,否则按重复风险决定是否重试 |
60020 可信 IP 错误 | 标记配置失败并展示告警,不做无意义循环重试 |
| UserID、权限或可见范围错误 | 标记永久失败,修正配置后人工重放 |
| 内容为空、没有待处理事项 | 标记 suppressed,不视为失败 |
WxPusher 在取得 sendRecordId 后可以继续查询状态;企业微信提供的平台重复检查可以作为补充,但两者都不能让自己的系统凭空获得严格的 exactly-once 语义。对于低频个人提醒,允许极少量重复通常比漏掉关键提醒更现实,但这个取舍应该明确,而不是由重试代码暗中决定。
渠道选型
WxPusher、Server 酱和 Wecom 酱经常被放在同一篇教程里讨论,但它们并不是同一种交付形态。
| 方案 | 交付形态 | 关键凭证 | 主要约束 | 适合场景 |
|---|---|---|---|---|
| WxPusher | 第三方推送服务 | 标准模式的 appToken + UID,或极简模式的 SPT | 当前资料主推独立客户端;微信补充渠道有绑定和激活限制 | 希望快速接入,能接受其当前接收端 |
| Server 酱 | 托管推送服务 | 服务当期提供的 SendKey 等凭证 | 通道、额度和规则随服务当前版本变化 | 希望把渠道运维交给服务方 |
| 企业微信自建链路 | 企业微信平台 API;可自行实现,也可使用或参考 wecomchan | CorpID、AgentId、应用 Secret、UserID | 需要维护账号权限、可信 IP、token 和运行环境 | 希望控制调用链,并愿意承担配置成本 |
需要特别强调:Server 酱在线服务与 easychen/wecomchan 不是同一项服务。 后者是企业微信自建链路的一种开源实现,项目说明把它称为 Server 酱的开源替代。企业微信应用即使取名“Server酱”,也不会自动接入 Server 酱 SaaS;SaaS 的 SendKey、企业微信应用 Secret,以及自建接口额外设置的口令,不能混用。
本文没有对 Server 酱在线服务的当前后台、接口、额度和接收通道做端到端实测,因此只把它用于产品边界和选型比较,不把历史资料写成当前承诺。其最小配置链路可以抽象为:在当前后台取得并保护 SendKey,在服务端调用其当期官方接口,按后台支持情况选择接收通道,检查业务响应,最后在目标终端验收。具体 URL、参数、额度和通道必须以服务当前文档为准。
选择推送方案时,建议固定比较五项:目标接收端、当前可用性、凭证与数据托管边界、维护成本、失败后能否排查。价格和免费额度变化快,不适合作为长期不变的技术结论。对于个人工具,最便宜的方案也未必成本最低;每次失效都要重新研究,维护时间同样是成本。
WxPusher 现状
市面上关于 WxPusher 的很多内容,来自公众号仍是主要触达方式的时期。本文参考的知乎教程发布于 2020 年 10 月 25 日,流程是创建应用、扫码订阅、从公众号菜单取得 UID,再用 appToken 和 UID 调用发送接口。这个模型当时是完整的,今天仍能帮助理解“应用凭证”和“接收者标识”的关系;问题在于,接收端已经发生变化,旧流程却仍会出现在搜索结果里。
本次按旧流程创建应用后,后台能够完成应用配置。

这张图证明应用创建流程已经完成;它不能证明任何具体接收渠道已经可用。
扫码订阅后,用户列表也能看到对应 UID 和订阅关系。

这张图证明用户与应用已经关联;“能查到 UID”不等于“公众号仍会承担消息投递”。
真正容易误判的地方,是以下三个概念属于不同层:
| 层级 | WxPusher 中的例子 | 它回答的问题 |
|---|---|---|
| 身份与路由 | appToken、UID、SPT、主题 | 谁以什么身份给谁发 |
| 发送方式 | 标准推送、极简推送 | 调哪个 API、提交哪些字段 |
| 接收渠道 | 独立客户端、桌面端、微信 ClawBot 等 | 消息最后出现在哪里 |
标准推送使用 appToken 加 UID,或者通过主题管理接收者;极简推送使用 SPT。它们决定调用方式,却不能证明消息一定进入公众号。UID 可以通过公众号菜单取得,也不代表公众号就是当前消息终端。身份入口和接收渠道恰好都出现了“公众号”,是很多旧资料让人产生误解的根源。
本地保存的新版官方文档写得很直接:WxPusher “早期以微信公众号为主要触达通道,当前主推独立全平台客户端接收”,同时提供微信 ClawBot(iLink)等补充渠道。但同一份文档的“获取 UID”部分仍保留了关注公众号、点击“我的—我的 UID”的说明。一个位置描述当前接收端,另一个位置延续旧身份入口,信息没有完全收束到同一套口径。
本次实测结果是:后台订阅关系存在,但传统公众号接收路线没有收到预期消息。因此,新项目不应该再默认“关注公众号即可接收 WxPusher 提醒”,除非重新做完端到端验证。这里也不把结论扩大成“官方已经宣布所有用户的公众号通道全面停用”,因为手头资料里没有这样的公告。专业判断需要同时保留文档证据和实测边界。
如果仍选择 WxPusher,并希望消息落在微信内,当前文档给出的 ClawBot 路线需要先在 WxPusher App → 我的 → 推送渠道中完成绑定。用户每次在微信侧激活后,24 小时内通过该渠道最多接收 10 条推送;用尽后需要向 ClawBot 回复任意内容再次激活。这是 iLink 渠道的限制,不是整个平台每天只能发送 10 条。对于一天两三条的个人提醒,数量通常不是问题,真正需要考虑的是额外 App、绑定和再激活是否符合自己的使用成本。
企业微信自建链路
本项目最终采用了企业微信自建应用链路,并参考了 wecomchan 的配置与实现思路。准确地说,落地对象是企业微信平台;wecomchan 提供的是一种可复用的实现,不是 Server 酱在线服务。
flowchart LR
A[通知任务] --> B[自建适配器或 wecomchan]
B --> C[获取或复用 access_token]
C --> D[企业微信发送应用消息 API]
D --> E[企业成员]
E --> F[当前账号可用的微信插件或微工作台入口]配置时至少要对应好四个身份参数:
| 参数 | 作用 | 常见误填 |
|---|---|---|
| CorpID | 标识企业 | 与 AgentId 混用 |
| AgentId | 标识自建应用 | 使用了其他应用的 ID |
| Secret | 换取该应用 access_token 的密钥 | 放到前端、提交到仓库,或拿成其他应用的 Secret |
| UserID | 标识企业通讯录中的接收成员 | 填昵称、手机号、微信号、external_userid 或 WxPusher UID |
此外还要检查应用是否启用、接收成员是否位于可见范围内,以及当前企业或账号是否具备发送接口需要的权限。个人提醒优先只选择自己的明确 UserID,不建议用 @all 扩大误发范围。
可信 IP 指的是请求出口
企业可信 IP 是这条链路里最常见、也最容易被一句话带过的配置。要填的是实际调用企业微信 API 的服务器出站公网 IP,不是接收手机的 IP,也不一定是服务器域名解析出来的地址。经过 NAT、代理、容器、云函数或转发服务以后,真正出口可能已经变化。
企业微信官方错误码说明中,60020 与不安全的访问 IP 有关,并要求企业自建应用核对应用详情里的“企业可信 IP”。排查顺序应该是:确认究竟由谁向企业微信发请求,从运行环境确认公网出口,再检查后台配置和生效时间。如果运行环境出口会变化,需要先解决稳定出口;稳定出口本身也不保证平台接受,第三方服务商 IP 还可能受到额外限制。
token 要缓存,也要允许失效
服务端使用 CorpID 和应用 Secret 获取 access_token:
GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=CORP_ID&corpsecret=APP_SECRET随后调用发送应用消息接口:
POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN
Content-Type: application/jsoncorpsecret 和 access_token 都出现在 URL 查询参数中。HTTP 客户端、反向代理、访问日志、APM 和异常堆栈必须关闭完整查询串记录,或对这两个字段做定向脱敏;包含真实查询参数的请求 URL 不能写入错误日志。
{
"touser": "YOUR_USER_ID",
"msgtype": "text",
"agentid": 1000002,
"text": {
"content": "21:30:记账、检查明日日程,并处理未完成事项。"
}
}1000002 只是 AgentId 示例。token 应按接口返回的 expires_in 缓存,并按应用隔离;确认过期时刷新。低频个人提醒不需要为了缓存一个 token 部署重型中间件,数据库或带访问控制的本地缓存已经足够,但也不要在每条消息发送前无条件重新取 token。
第一条测试消息应该只发给自己,而且只发短纯文本。这样可以先验证身份、权限、可信 IP 和终端入口,把 Markdown 渲染、长文本截断等问题留到链路通过以后。企业微信官方文档明确说明微工作台不展示 Markdown;某些应用设置还会使微信端文本受更严格的长度限制。第一条测试消息写成一篇周报,只会让排错多一个变量。
HTTP 200 不等于业务成功
接口调用需要检查响应 JSON 中的 errcode、errmsg,以及 invaliduser、unlicenseduser 等接收者字段。部分接收人不存在或没有权限时,接口仍可能返回 errcode: 0;全部接收人失败时,则会得到相应业务错误。
| 典型结果 | 含义 | 处理方式 |
|---|---|---|
40014 | access_token 不合法 | 检查 token 类型;必要时刷新一次 |
42001 | access_token 已过期 | 刷新 token 后重试一次 |
60020 | 请求来源 IP 不符合要求 | 检查实际出站 IP 和可信 IP 配置,不要循环重试 |
81013 | 全部接收人无权限或不存在 | 检查 UserID、可见范围和基础接口权限 |
errcode: 0 且存在无效接收者字段 | 请求部分执行,但并非所有目标有效 | 记录失败目标,修正配置后再投递 |
API 受理、服务端执行、目标终端收到和用户已读,是四种不同证据。接口没有提供哪一层证据,系统就不应擅自把状态写成“已送达”。第一次上线仍应以目标手机实际收到短文本作为端到端验收。

截图证明本次企业微信链路完成了端到端接收。应用名称为“Server酱”,这只是命名,不能据此推断使用了 Server 酱在线服务。公开版本已遮盖昵称、头像和无关会话信息。
调度和消息设计

这张图只展示计划任务表单中拟配置的周期;它不能证明任务已经保存、启用或成功执行。
上线前还要确认运行用户、命令绝对路径、工作目录、环境变量和时区。计划时间应保存明确时区,国内固定作息可以统一使用 Asia/Shanghai;服务重启后的补偿窗口也要有边界,例如只补最近 30 分钟的高优先级提醒,避免恢复后把几天前的消息集中发出。
同一时段的低优先级事项可以合并,完成的任务和空内容不发送。图7中的早间消息列出当天事项,晚间消息只保留尚未完成的内容,并在确实缺少体重记录时提示补录。这比把整页数据原样推到微信更有效。

*终端实际收到的提醒。
消息生成可以遵循四条规则:
- 同一时间窗口尽量合并成一条。
- 发送前重新检查状态,已完成事项不再出现。
- 没有可执行内容时不发送。
- 通知只携带完成动作所需的信息,不发送完整日记或详细账目。
最后一条既是信息设计,也是隐私控制。通知会经过第三方服务并显示在终端锁屏上,正文不应该默认承载全部原始数据。
安全与可观测性
这类系统存储的不是普通演示数据。账目、日记和日程通常比推送服务本身更敏感,至少要守住以下边界:
- appToken、SPT、SendKey、企业微信 Secret 和 access_token 只保存在服务端环境变量、受限配置文件或密钥服务中,不进入前端、仓库、截图和普通日志。
- 日志和追踪系统对 URL 查询串默认从严处理,明确脱敏
corpsecret、access_token以及其他渠道密钥。 - 凭证按渠道和应用隔离并支持单独轮换;TLS 证书校验保持开启,不照抄旧示例中关闭校验的代码。
- 企业微信应用可见范围先只选自己,推送目标使用明确 UserID;可信 IP 只能限制请求来源,不能替代鉴权和密钥保护。
- 备份要加密,并定期验证能够恢复。“生成了备份文件”和“数据真的能恢复”是两件事。
- 日志记录 message_id、规则 ID、任务状态、渠道、耗时、返回码和脱敏后的错误,不记录完整凭证、私人正文和完整 UserID。
可观测性的目标不是做一套监控大屏,而是快速回答:规则有没有命中,任务有没有生成,Worker 有没有执行,渠道有没有受理,终端有没有完成过人工验收。个人项目至少可以在管理页展示成功数、失败数、重试数、最近一次成功时间和连续失败次数。只有当提醒本身具有明确的高重要性时,才值得额外维护一条独立告警渠道。
后话
这套系统没有加入 AI,也没有试图自动解释体重、消费和情绪。原因很明确:当前核心问题是可靠记录、可回溯和必要提醒,AI 分析既不是必需能力,还会增加数据暴露、成本和不可预测输出。与其先做“人生总结”,更值得优先解决导出、备份恢复、幂等、失败日志和凭证轮换。
同样,本文没有把低频个人提醒设计成企业级消息平台。数据库任务表可以代替复杂消息队列,短纯文本可以避开多端格式兼容,一条明确 UserID 的消息也比向全部成员广播更容易排错。专业工程不等于组件越多,而是每个组件为什么存在,都能说清楚。
最终的验收标准可以保持简单:记录能正确保存,历史能找回;回看结果能转化为下一步行动;相同提醒不会重复建单,重复投递风险受到控制;渠道失败能够定位;敏感数据不会随着日志和通知四处扩散;该提醒的时候,目标终端确实能收到一条有用的消息。做到这些,这套个人工具就已经从“能运行的页面”变成了“可以长期维护的系统”。