MENU

• 2026 年 10 月 26 日 • 已有 4 只咪围观过 • -学海无涯-

并没有一个软件可以完全吻合我的要求,我也不希望下载很多软件。之前做了网站,但是没有提醒
在感觉之前网站功能太复杂了,这一次简化并添加了提醒。

前期规划

“做一个自我管理工具”只是方向,不是需求。为了避免项目最后长成一个什么都有、什么都不顺手的后台,第一步还是应该想清楚自己做什么。

目标我想要做的验收方式
容易记录常用内容集中在一个入口,必填字段保持最少一次记录不需要跨多个应用;无关字段可以不填
可回溯原始数据可以按日期、类型和少量分类找回能从年度视图定位到某一天,再回到原始记录
可行动回看结果能够形成任务、日程或明确调整“发现问题”之后能落到一个下一步动作
按需提醒只有存在明确动作或时间条件时才生成通知已完成、空内容和重复任务不发送
可排查每次投递都有状态和错误上下文能区分规则未命中、任务未执行、渠道未受理和终端未收到

自我管理的基础闭环并不复杂:

flowchart LR
    A[记录事实] --> B[按周期回看]
    B --> C[形成具体任务或日程]
    C --> D[必要时生成提醒]
    D --> E[完成、取消或调整]
    E --> B

体重趋势异常,不应该只停在一张曲线上,而要转成一个可以执行的调整;账目回看发现某类支出过高,也要落到下个周期的具体行动。提醒只是把人叫回这个闭环,不能替代判断,更不能把每条数据都广播一遍。

领域建模

体重、账目、任务、日历和日记可以出现在同一个首页,但不适合共用一张含义模糊的 records 表。一个专业但仍然轻量的模型,可以从下面这些对象开始。

对象最小字段核心约束主要用途
Measurementmeasured_at、type、value、unit同一测量类型保持单位一致观察时间序列趋势
LedgerEntryoccurred_at、direction、amount、currency、category金额使用定点数,收支方向明确汇总资金流向
Tasktitle、status、due_at?、completed_at?有状态生命周期,完成后停止催办管理需要完成的动作
CalendarEventtitle、start_at、end_at、timezone结束时间不早于开始时间描述占用明确时间段的安排
JournalEntryentry_date、content允许缺席,不强制产生任务保存叙述、想法和上下文
ReminderRulesource_type、source_id?、trigger、channel、enabled引用业务对象,不修改业务事实决定何时提醒什么

这几个边界可以用一句话记住:记录保存事实,任务管理承诺,日历占用时间,日记保存叙述,提醒负责触达。

例如,“今天 72.4 kg”是已经发生的测量记录;“周五前提交材料”是任务,即使有截止时间,也不代表它占用了整个日历时段;“周四 14:00 到 15:00 复诊”是日历事件;“复诊以后感觉轻松了一些”属于日记;“周四 13:30 提醒出门”才是从日历事件派生出来的提醒规则或通知任务。对象分开以后,统计和状态流转才不会互相污染。

下面展示的是第一版聚合后的日常入口。日期、体重、固定打卡、今日重点、临时事项和备注可以在同一页操作,这属于产品层面的统一体验。
但是我后面觉得固定大卡很麻烦,经常有很多突发情况,因为不能完成,看到自己打卡中断会很有挫败感..不如直接取消掉。最后的版本中取消了。

2026-10-26T03:14:18.jpg

统一入口能够降低记录成本;它不能反过来证明底层数据也应该合并成一个模型。

下面则是按日期聚合的年度总览。它更适合作为读模型:从多个领域对象投影出适合浏览的数据,而不是直接承担所有原始数据的写入和校验。

2026-10-26T03:14:35.jpg

年度总览是便于查询的投影视图,原始账目、测量、任务和日记仍应保留各自结构。

这就是“统一入口”和“统一数据”的区别:写模型维护各自的业务约束,读模型负责把结果组合成人容易理解的页面。即使第一版没有正式采用 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;可自行实现,也可使用或参考 wecomchanCorpID、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 调用发送接口。这个模型当时是完整的,今天仍能帮助理解“应用凭证”和“接收者标识”的关系;问题在于,接收端已经发生变化,旧流程却仍会出现在搜索结果里。

本次按旧流程创建应用后,后台能够完成应用配置。

2026-10-26T03:14:52.png

这张图证明应用创建流程已经完成;它不能证明任何具体接收渠道已经可用。

扫码订阅后,用户列表也能看到对应 UID 和订阅关系。

2026-10-26T03:15:09.png

这张图证明用户与应用已经关联;“能查到 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/json

corpsecret 和 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;全部接收人失败时,则会得到相应业务错误。

典型结果含义处理方式
40014access_token 不合法检查 token 类型;必要时刷新一次
42001access_token 已过期刷新 token 后重试一次
60020请求来源 IP 不符合要求检查实际出站 IP 和可信 IP 配置,不要循环重试
81013全部接收人无权限或不存在检查 UserID、可见范围和基础接口权限
errcode: 0 且存在无效接收者字段请求部分执行,但并非所有目标有效记录失败目标,修正配置后再投递

API 受理、服务端执行、目标终端收到和用户已读,是四种不同证据。接口没有提供哪一层证据,系统就不应擅自把状态写成“已送达”。第一次上线仍应以目标手机实际收到短文本作为端到端验收。

2026-10-26T03:15:27.png

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

调度和消息设计

2026-10-26T03:15:51.png

这张图只展示计划任务表单中拟配置的周期;它不能证明任务已经保存、启用或成功执行。

上线前还要确认运行用户、命令绝对路径、工作目录、环境变量和时区。计划时间应保存明确时区,国内固定作息可以统一使用 Asia/Shanghai;服务重启后的补偿窗口也要有边界,例如只补最近 30 分钟的高优先级提醒,避免恢复后把几天前的消息集中发出。

同一时段的低优先级事项可以合并,完成的任务和空内容不发送。图7中的早间消息列出当天事项,晚间消息只保留尚未完成的内容,并在确实缺少体重记录时提示补录。这比把整页数据原样推到微信更有效。

2026-10-26T03:16:22.jpg

*终端实际收到的提醒。

消息生成可以遵循四条规则:

  1. 同一时间窗口尽量合并成一条。
  2. 发送前重新检查状态,已完成事项不再出现。
  3. 没有可执行内容时不发送。
  4. 通知只携带完成动作所需的信息,不发送完整日记或详细账目。

最后一条既是信息设计,也是隐私控制。通知会经过第三方服务并显示在终端锁屏上,正文不应该默认承载全部原始数据。

安全与可观测性

这类系统存储的不是普通演示数据。账目、日记和日程通常比推送服务本身更敏感,至少要守住以下边界:

  • appToken、SPT、SendKey、企业微信 Secret 和 access_token 只保存在服务端环境变量、受限配置文件或密钥服务中,不进入前端、仓库、截图和普通日志。
  • 日志和追踪系统对 URL 查询串默认从严处理,明确脱敏 corpsecret、access_token 以及其他渠道密钥。
  • 凭证按渠道和应用隔离并支持单独轮换;TLS 证书校验保持开启,不照抄旧示例中关闭校验的代码。
  • 企业微信应用可见范围先只选自己,推送目标使用明确 UserID;可信 IP 只能限制请求来源,不能替代鉴权和密钥保护。
  • 备份要加密,并定期验证能够恢复。“生成了备份文件”和“数据真的能恢复”是两件事。
  • 日志记录 message_id、规则 ID、任务状态、渠道、耗时、返回码和脱敏后的错误,不记录完整凭证、私人正文和完整 UserID。

可观测性的目标不是做一套监控大屏,而是快速回答:规则有没有命中,任务有没有生成,Worker 有没有执行,渠道有没有受理,终端有没有完成过人工验收。个人项目至少可以在管理页展示成功数、失败数、重试数、最近一次成功时间和连续失败次数。只有当提醒本身具有明确的高重要性时,才值得额外维护一条独立告警渠道。

后话

这套系统没有加入 AI,也没有试图自动解释体重、消费和情绪。原因很明确:当前核心问题是可靠记录、可回溯和必要提醒,AI 分析既不是必需能力,还会增加数据暴露、成本和不可预测输出。与其先做“人生总结”,更值得优先解决导出、备份恢复、幂等、失败日志和凭证轮换。

同样,本文没有把低频个人提醒设计成企业级消息平台。数据库任务表可以代替复杂消息队列,短纯文本可以避开多端格式兼容,一条明确 UserID 的消息也比向全部成员广播更容易排错。专业工程不等于组件越多,而是每个组件为什么存在,都能说清楚。

最终的验收标准可以保持简单:记录能正确保存,历史能找回;回看结果能转化为下一步行动;相同提醒不会重复建单,重复投递风险受到控制;渠道失败能够定位;敏感数据不会随着日志和通知四处扩散;该提醒的时候,目标终端确实能收到一条有用的消息。做到这些,这套个人工具就已经从“能运行的页面”变成了“可以长期维护的系统”。

返回文章列表 打赏
本页链接的二维码
打赏二维码