说实话,微信自带的系统日历提醒,看起来"够用",但真正用起来你会发现一个很实际的痛点:提醒方式完全是系统定的,粒度粗糙,数据不掌握在自己手里,更别说按自己的习惯去定制"提前多久提醒""重复规则""优先级"这些东西。我也不想为一个日程管理再去装一个陌生 App——信息分散、广告还多。所以去年年底,我抽了两周业余时间,自己动手做了一个个人日程计划弹窗提醒系统:后端用Python 写 API,前端用微信小程序做交互,支持日程的增删改查、到点弹窗提醒,以及通过微信订阅消息做离线触达。这篇文章我会把整个项目的选型逻辑、数据库设计、后端接口、小程序页面、弹窗触发机制和踩坑记录完整拆开讲一遍,适合那种"会一点 Python 基础、想完整走一遍前后端实战"的人参考。
1. 技术选型:Python 后端 + 小程序前端的组合逻辑
1.1 后端为什么选 Python,而不是 Java 或 Node
做这种轻量级工具类系统,第一原则是"降低开发和维护成本",而不是追求极致的并发性能。个人日程管理的并发量极低,同一时刻可能就你自己和少数几个用户在访问,所以后端框架完全没必要上 Spring Boot 这类重型框架。Python 里的 Flask 框架,一个文件就能跑起一个可用的 API 服务,写起来直观,调试也快。
这里要说一下我为什么不用 FastAPI 而用 Flask:FastAPI 的自动文档和类型检查确实很香,但如果是小程序端 + 简单 JSON 接口这种场景,Flask 的 route 写法反而更直白,依赖也更少。退一步说,后续真要换 FastAPI,主要逻辑也就是给函数加类型标注,迁移成本并不高。所以初期项目我坚持"能简单就简单"。
另一个选 Python 的理由是定时任务生态。日程提醒系统最核心的一个后端动作是"到点了去通知用户",这里需要定时扫描数据库里的日程表。Python 的 APScheduler 库可以很轻量地在 Flask 进程里跑定时任务,不用像 Java 那边专门引入 Quartz 或者搞消息队列。个人项目做到这个程度,架构上已经够了。
1.2 小程序前端的不可替代性优势
同样是"免安装 App",微信小程序和 H5 网页最本质的区别在于:小程序能直接使用微信的登录体系、订阅消息和分享能力。我可以不用自己搭一套账号注册登录流程,直接通过wx.login拿到 code,后端再调微信的code2Session接口换取 openid,用户身份问题就解决了。对个人开发者来说,这一步省掉的工作量非常可观。
另外,微信小程序有一个可以被"利用"的特性——它在用户主动进入时是可以弹窗的。所谓"弹窗提醒",很多人以为只能靠系统通知,其实在小程序内可以通过自定义组件做出提醒弹窗,效果相当于"打开小程序就能看到即将到期的日程"。同时配合订阅消息,能做到"用户没打开小程序也能收到微信里的服务通知",这两者结合才是完整的提醒链路。这一点我在第四章会详细讲。
1.3 弹窗提醒的三种实现路径对比
在动手写代码前,我先把"弹窗提醒"的技术方案拉出来对比了一轮。很多人一上来就纠结"到底该用哪种",其实就看你的使用场景和主体资质。
| 方案 | 实现难度 | 能不能离线触达 | 适合场景 |
|---|---|---|---|
| 小程序内自定义弹窗组件 | 低 | 否,必须打开过小程序 | 用户正在小程序里浏览时,提示即将到期的日程 |
| 微信订阅消息(一次性订阅) | 中 | 是,会出现在微信服务通知里 | 用户授权一次,触发一次通知,适合每次提醒都要重新授权的场景 |
| 服务号模板消息 / 长期订阅 | 高 | 是 | 需要服务号主体,且模板消息类目受限,个人主体很难拿下 |
对我这种个人项目来说,最终采用的是"小程序内自定义弹窗 + 订阅消息混合方案":用户打开小程序时,本地检查是否有 15 分钟内到期的日程并弹窗;同时用户每次新增日程时,引导授权订阅消息,到点后由后端调订阅消息接口做离线提醒。两套机制互不冲突,也各解决了不同的使用场景。
2. 系统架构与数据模型设计
2.1 整体架构拆解
整个系统从部署形态来看,是经典的"小程序客户端 + HTTP API + 数据库 + 定时任务"四层结构。
小程序端负责三件事:展示日程列表、录入和修改日程、接收/展示弹窗。它不直接连数据库,所有数据操作都走 HTTPS 请求到 Python 后端。
后端 Flask 应用一方面提供 REST API 给小程序端调用,另一方面用 APScheduler 启动一个独立的后台任务,每 30 秒扫描一次数据库里的日程表,把"即将到期且尚未通知"的日程查出来,调微信订阅消息接口发给用户。
数据库我用的是 SQLite。个人项目不需要单独装 MySQL,SQLite 单文件、零配置,Flask 配合sqlite3标准库就能跑起来。等数据量真的大到 SQLite 撑不住(我估计得好几年后),再切 MySQL 不迟。
2.2 数据库表设计
我设计了三张核心表:users、plans、remind_log。听起来好像很简单,但表结构的细节决定了很多后续开发是否顺畅。
users表的核心字段是openid,它是用户在微信生态里的唯一标识。后端通过wx.login返回的 code 去换,一个 openid 对应一个用户。还有个字段nickname,是从小程序端传上来的展示昵称,不强制唯一。
plans表是主体表,核心字段有:
id:主键自增user_id:关联 users 表title:日程标题content:日程详情,可空plan_time:日程具体执行时间(准确到分钟)remind_before:提前提醒的分钟数,比如 10、30、60status:0 待处理,1 已完成,2 已取消reminded:0 未提醒,1 已提醒created_at、updated_at:记录创建和更新时间
这里我特别加了reminded字段,配合定时任务使用。定时任务每轮只会处理status=0且reminded=0且plan_time - remind_before <= 当前时间的记录,处理完立刻把reminded置 1,避免重复推送。很多初学者会漏掉这个字段,结果就是每次进程重启或者定时任务重复执行,用户会收到好几条同样的提醒。
remind_log表用来记录每次提醒的推送结果,方便排查"用户为什么没收到通知"这类问题。字段包括plan_id、send_time、response_code、response_msg,把微信接口返回的原始信息存下来。
2.3 API 接口规划
我按 RESTful 风格规划了下面这些接口,不多,但足够覆盖整个业务流程:
| 方法 | 路径 | 功能 |
|---|---|---|
| POST | /api/login | 小程序 code 换 openid,返回自定义 token |
| GET | /api/plans | 获取当前用户的日程列表 |
| POST | /api/plans | 新增日程 |
| PUT | /api/plans/{id} | 更新日程 |
| DELETE | /api/plans/{id} | 删除日程 |
| POST | /api/plans/{id}/finish | 标记日程完成 |
| POST | /api/subscribe/send | 触发订阅消息推送(内部也可由定时任务调用) |
登录接口做了一层封装:拿到 openid 后,我用itsdangerous生成一个签名 token 返回给小程序端,后续请求都带上Authorization: Bearer <token>,后端解析 token 得到用户身份。不直接把 openid 透传给前端,这样就算别人抓包,也不会轻易拿到用户敏感标识。
3. Python 后端的具体实现
3.1 Flask 项目初始化和结构
项目我命名为reminder_server,目录结构如下:
reminder_server/ ├── app.py ├── models.py ├── auth.py ├── plans.py ├── scheduler.py ├── config.py └── requirements.txtapp.py是入口,负责创建 Flask 实例、注册蓝图和启动定时任务。我用了蓝图把登录、日程、订阅消息拆到不同模块,这样代码逻辑不会堆在一起。config.py里集中放 SQLite 数据库路径、微信小程序 AppID、AppSecret、token 密钥等配置。
启动方式很简单,python app.py就会在 5000 端口起服务,同时启动 APScheduler 的 scheduler。这里要注意一个点:Flask 自带的开发服务器不适合直接用。我用的是waitress,Windows 和 Linux 都能跑,性能比 Flask 自带服务器稳定得多。生产环境我用 Nginx 反向代理 + waitress 的组合,HTTP 服务这层基本不用操心了。
3.2 微信登录接口:code 换 token
小程序端调用wx.login拿到一个临时 code,然后传给后端。后端拿这个 code 去请求微信的jscode2session接口,换取 openid。这个 code 只能用一次,而且有效期很短,所以要尽快处理。
# auth.py @app_login.route('/login', methods=['POST']) def login(): data = request.get_json() code = data.get('code') if not code: return jsonify({'code': 1, 'msg': '缺少 code'}), 400 url = ( 'https://api.weixin.qq.com/sns/jscode2session' f'?appid={WX_APPID}&secret={WX_SECRET}' f'&js_code={code}&grant_type=authorization_code' ) resp = requests.get(url, timeout=5).json() if 'openid' not in resp: # 记录原始错误日志,方便排查 code 过期 / appid 不匹配 / secret 错误 app.logger.error('wx login failed: %s', resp) return jsonify({'code': 1, 'msg': '微信登录失败'}), 500 openid = resp['openid'] user = get_or_create_user(openid) token = generate_token(user['id']) return jsonify({'code': 0, 'data': {'token': token}})这段代码里我加了一个很关键的判断:如果响应里没有openid,必须把微信返回的原始错误信息记到日志里。因为实际开发中"登录失败"十有八九是 AppID 和 Secret 配错了,或者是 code 被重复使用,只看"微信登录失败"这五个字永远查不出原因。
3.3 日程 CRUD 接口实现
日程的新增和更新逻辑,核心就是解析 JSON、校验必填字段、写库。我吸取了一个教训:前端传过来的时间格式要统一。我规定小程序端一律传YYYY-MM-DD HH:mm:ss这种字符串,后端用datetime.strptime解析,如果格式不对就立刻返回参数错误,不等到落库后才发现问题。
新增日程的代码大概是这样:
@plans_bp.route('', methods=['POST']) def create_plan(): user_id = get_current_user_id() # 从 token 解析 data = request.get_json() title = (data.get('title') or '').strip() plan_time_str = data.get('plan_time', '') remind_before = int(data.get('remind_before', 10)) if not title: return jsonify({'code': 1, 'msg': '标题不能为空'}), 400 try: plan_time = datetime.strptime(plan_time_str, '%Y-%m-%d %H:%M:%S') except ValueError: return jsonify({'code': 1, 'msg': '时间格式错误'}), 400 # 提醒时间必须大于当前时间 remind_at = plan_time - timedelta(minutes=remind_before) if remind_at <= datetime.now(): return jsonify({'code': 1, 'msg': '提醒时间已过,请调整'}), 400 sql = """ INSERT INTO plans (user_id, title, content, plan_time, remind_before, status, reminded) VALUES (?, ?, ?, ?, ?, 0, 0) """ cur = db.execute(sql, (user_id, title, data.get('content'), plan_time_str, remind_before)) db.commit() return jsonify({'code': 0, 'data': {'id': cur.lastrowid}})这段代码里有个大家容易忽略的校验:新增日程时不光要校验plan_time合法性,还要校验"提醒时间"是否已经过掉。如果你设置的日程是 10 分钟后的,但提醒提前量是 30 分钟,那么这个日程创建的时候其实已经"错过提醒"了。我在初期就没做这个判断,结果出现了一条日程创建完下一秒就被定时任务推送提醒的诡异现象。加上这个校验后,体验正常了很多。
3.4 定时扫描与通知触达
定时任务用的是 APScheduler 的BackgroundScheduler,每 30 秒跑一次。为什么不每秒钟跑?因为日程提醒这种场景,30 秒的误差用户完全感知不到,但能显著降低数据库查询压力和微信接口调用频率。
# scheduler.py def scan_and_send(): now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S') sql = """ SELECT p.id, p.title, p.content, p.plan_time, p.remind_before, u.openid FROM plans p JOIN users u ON p.user_id = u.id WHERE p.status = 0 AND p.reminded = 0 AND datetime(p.plan_time) <= datetime(?, '+' || p.remind_before || ' minutes') """ # 注意这里用的是 SQLite 的 datetime 计算,把 plan_time 减去 remind_before 之后和当前时间比较等等,上面这个 SQL 我实际调试的时候发现写法有点绕。SQLite 的datetime函数支持 modifier,但拼接字符串可读性太差。后来我改成另一种方案:直接在 Python 里算出"提醒时间阈值",拼进 SQL。
更稳妥的写法是:
def scan_and_send(): now = datetime.now() # 查询所有需要提醒的记录,条件:未完成、未提醒、计划时间在「当前时间 + 提前分钟数」之内 rows = db.execute(""" SELECT p.id, p.title, p.content, p.plan_time, p.remind_before, u.openid FROM plans p JOIN users u ON p.user_id = u.id WHERE p.status = 0 AND p.reminded = 0 """).fetchall() for row in rows: plan_time = datetime.strptime(row['plan_time'], '%Y-%m-%d %H:%M:%S') remind_at = plan_time - timedelta(minutes=row['remind_before']) if remind_at <= now: send_subscribe_message(row) mark_reminded(row['id'])虽然每次扫描都查全表,但数据量小,完全没问题。这段逻辑也给我提了个醒:不要把 SQL 写得太"花哨",尽量把计算搬到业务代码里做,可读性和可调试性会好很多。
4. 微信小程序前端的实现
4.1 小程序页面结构
我用原生小程序语法来写,不依赖 uni-app 或 Taro。不是说那些框架不好,而是这个项目本身页面不多,用原生框架反而能减少一层编译和踩坑成本。页面一共四个:
pages/index/index:日程列表页pages/edit/edit:新增/编辑日程页pages/detail/detail:日程详情页pages/mine/mine:个人中心页(包含订阅消息授权入口)
app.json里注册这几个页面,窗口标题设为"我的日程"。
小程序端的难点其实不在页面数量,而在于登录态的管理。我通过封装一个request工具函数,在每次请求前检查本地是否有 token;没有就调wx.login获取 code,再调后端/api/login换 token,并把 token 存到wx.setStorageSync。后续所有请求自动带Authorization头。
// utils/request.js const BASE_URL = 'https://your-domain.com/api' function request(path, method, data) { return new Promise((resolve, reject) => { let token = wx.getStorageSync('token') if (!token) { wx.login({ success: res => { wx.request({ url: `${BASE_URL}/login`, method: 'POST', data: { code: res.code }, success: loginRes => { token = loginRes.data.data.token wx.setStorageSync('token', token) doRequest(path, method, data, token, resolve, reject) } }) } }) return } doRequest(path, method, data, token, resolve, reject) }) }这里有一个很容易踩的坑:wx.login拿到的 code只能使用一次。如果我在网络请求失败后重试时再次调wx.login,后端的code2session就会报invalid code。所以我在request里做了 token 不存在时才走登录逻辑,一旦换到 token 后续请求都复用,不重复执行 login。
4.2 日程列表与新增编辑
日程页的核心是列表展示。我用了小程序的scroll-view加onPullDownRefresh,下拉刷新时重新拉取/api/plans。列表项显示三个信息:标题、计划时间、当前状态。状态用颜色区分:未完成是黑色,已完成是灰色并加删除线。长按列表项弹出一个 ActionSheet,可以选择"编辑"、"删除"、"标记完成"。
新增和编辑共用一个pages/edit/edit页面。表单字段有标题、内容、时间、提前提醒分钟数、重复规则。提前提醒分钟数我用picker组件来选,可选项是 0、10、30、60、120,这个设计比直接输入数字体验好很多,也减少了前端校验的负担。
提交表单时会做一次前端校验:标题不能为空、时间不能早于当前时间。前端校验的意义在于提升响应速度,后端校验才是真正的安全兜底。所以两边我都写了。
4.3 弹窗交互的 UI 实现
这正是标题里最核心的"弹窗提醒"。我在小程序里实现了一种软弹窗:用户打开小程序,首页onShow时检查本地是否有"即将到期"的日程,如果有,就弹出一个自定义遮罩层组件。
自定义弹窗组件reminder-popup的结构大概是这样:
<!-- components/reminder-popup/reminder-popup.wxml --> <block wx:if="{{visible}}"> <view class="mask" bindtap="close"></view> <view class="popup"> <view class="popup-title">日程提醒</view> <view class="popup-content"> <view class="plan-title">{{plan.title}}</view> <view class="plan-time">{{plan.plan_time}}</view> <view class="plan-remain">距离开始还有 {{remainMinutes}} 分钟</view> </view> <view class="popup-actions"> <button size="mini" bindtap="close">知道了</button> <button size="mini" type="primary" bindtap="goDetail">查看详情</button> </view> </view> </block>判断"即将到期"的逻辑在首页的onShow里写:把从后端获取的日程列表过滤出status === 0且reminded为 false 且计划时间在当前时间到当前时间 + 15 分钟之间的记录,取最早的一条展示。这里有个细节:为什么不在定时器里轮询而要在 onShow 检查?因为小程序退到后台后,定时器会被系统冻结,轮询根本跑不起来。onShow 的机制能保证用户每次回到小程序时一定能触发检查,既节约资源又可靠。
4.4 弹窗联动订阅消息
弹窗只能解决"用户已经打开小程序"的场景,如果用户根本不打开小程序,再好看的弹窗也没用。所以我另外做了微信订阅消息的授权和发送。
小程序端在新增日程成功后,会调用wx.requestSubscribeMessage请求订阅消息授权。
wx.requestSubscribeMessage({ tmplIds: ['模板ID'], success(res) { if (res['模板ID'] === 'accept') { // 用户同意授权 } } })这里用户看到的提示是"允许发送提醒通知"之类的系统文案。微信的订阅消息规则比较特殊:用户点击一次授权,只允许你发送一条订阅消息。也就是说,我每发一条日程提醒前,都得先让用户授权一次。所以我的策略是:在新增日程的时候就引导用户授权一次,这样到点后端推送时才有额度。如果用户拒绝授权了,那就只能靠小程序内弹窗兜底。
后端发送订阅消息的核心接口是https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN,先要用 AppID 和 Secret 换取access_token:
def send_subscribe_message(plan): access_token = get_access_token() # 带缓存,避免频繁调用 url = f'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={access_token}' data = { 'touser': plan['openid'], 'template_id': WX_TEMPLATE_ID, 'page': f'pages/detail/detail?id={plan["id"]}', 'data': { 'thing1': {'value': plan['title'][:20]}, 'time2': {'value': plan['plan_time']}, 'thing3': {'value': '您有一条日程即将开始'} }, 'miniprogram_state': 'developer' # 开发版,发布后改为 formal } resp = requests.post(url, json=data).json() if resp.get('errcode') == 0: return True, None return False, resp注意thing类型的字段有长度限制,最多 20 个汉字,time类型必须是YYYY-MM-DD HH:mm:ss的格式,不然微信会直接拒绝。这些是我实际对接时被微信文档坑过才记住的。
5. 定时提醒与订阅消息的配合逻辑
5.1 后端定时扫描的策略细节
回到后端定时任务。每 30 秒一次的全表扫描,如果数据量大确实有性能隐患,但我这里个人项目完全可接受。真正需要注意的反而是重复提醒的防止。
我在plans表里加了reminded字段。定时任务处理一条记录时两步是关键:先调微信接口,再立刻把reminded置 1。如果微信接口调用失败,比如access_token过期或网络抖动,我不会直接标记为已提醒,而是等下一轮扫描时重试。但这里有个风险:如果微信一直返回错误,任务就会每 30 秒重试一次,可能把微信的接口限流打爆。所以我后来又加了一个重试次数控制:remind_log表里累计重试到 3 次就强制标记reminded = 1,然后记录失败原因,等人工排查。
这种"重试 + 熔断"的思路,在个人项目里也一定要有,不然后端日志里全是重复刷屏的错误信息。
5.2 小程序内弹窗的触发时机
很多人以为"弹窗提醒系统"就是小程序里写一个wx.showModal那么简单,其实弹窗的时机比弹窗本身重要得多。我总结了一套判断逻辑:
- 小程序启动后,
onShow触发,首先检查当前时间是否已有"过期未处理"的日程。如果有,说明用户错过提醒了,弹窗文案要强调"已过期",让用户知道要补处理。 - 然后检查"即将到来"的日程,比如 15 分钟内。如果有,弹窗提醒并给出"查看详情"入口。
- 每次关闭弹窗后,在本地存储里记一条
lastPopupTime,避免用户一直打开关闭页面导致弹窗反复弹出。这个去重逻辑很简单,但能显著提升使用体验。
function checkReminder(plans) { const now = new Date() const soon = plans.filter(p => { const planTime = new Date(p.plan_time.replace(/-/g, '/')) const diff = (planTime - now) / 60000 return p.status === 0 && diff >= 0 && diff <= 15 }) if (soon.length > 0) { const latest = soon.sort((a, b) => a.plan_time.localeCompare(b.plan_time))[0] const lastTime = wx.getStorageSync('lastPopupTime') const nowStr = Date.now() if (!lastTime || nowStr - lastTime > 60 * 1000) { this.setData({ popupVisible: true, currentPlan: latest }) wx.setStorageSync('lastPopupTime', nowStr) } } }这段代码里有个很细节的点:iOS 的new Date('2024-01-01 10:00:00')直接解析会返回 Invalid Date,所以我在解析前把-替换成/。这个坑对安卓和 iOS 的小程序兼容性影响很大,不处理的话 iOS 用户永远看不到弹窗。
5.3 本地提醒和订阅消息的分工
两套提醒机制分工总结一句话:订阅消息负责"把用户叫回来",小程序内弹窗负责"回来后看到完整信息"。
用户在小程序里新增日程并授权订阅消息之后,到点后端会推送一条订阅消息到微信的"服务通知"里。用户点这条通知,直接跳转到小程序的日程详情页。而如果用户当时正好打开着小程序,首页的onShow弹窗就会先出现,给用户一个更直观的提醒卡片。两条路径完全不冲突。
这样做还有一个额外的好处:用户不打开小程序也能收到提醒,提醒的"主动感"就出来了。这也是"弹窗提醒系统"相对普通日历应用的一个突出差异点。
6. 实际开发中的高频坑与优化方向
6.1 我踩过的五个高频坑位
第一个坑是wx.login 的 code 不能重复使用。我在调试时经常遇到 token 过期后自动重新登录的场景,如果代码写得不够严谨,同一秒内多个请求同时触发wx.login,一个 code 被后端换了两次,第二次必然报错。解决方案是给登录逻辑加一个 Promise 单例模式:第一次wx.login还没完成时,后面的请求直接await同一个 Promise,而不是重新发起登录。
第二个坑是小程序定时器在后台会被冻结。我最初想用setInterval在前端每隔一分钟扫描一次日程,后来发现只要用户切到后台,定时器基本就停了,回来后又突然乱跳。后来我彻底放弃了前端定时器方案,全部依赖onShow触发检查和后端订阅消息推送。
第三个坑是HBuilderX 和微信开发者工具的编译路径问题。如果只用原生开发,直接用微信开发者工具打开项目根目录就行,注意project.config.json里的miniprogramRoot要指向小程序代码的实际目录。我最初把 Flask 后端代码和小程序前端代码放在同一个仓库的根目录下,结果微信开发者工具把整个项目都当成了小程序来编译,报了一堆莫名其妙的模块错误。
第四个坑是订阅消息的thing字段长度限制。我一开始把日程标题原样传过去,结果某天标题超过 20 个字,微信直接返回errCode 47003(参数错误)。解决方案是在后端发送前做截断处理,但如果标题被截断了,用户看到的信息不完整,所以我后来调整了模板设计,主要内容用time字段承载,标题字段尽量精简。
第五个坑是基础库版本不兼容。像wx.requestSubscribeMessage这个 API,需要基础库版本在 2.8.2 以上才支持。用户手机微信版本太旧,调用会直接失败。我在小程序后台设置了最低基础库版本,同时在代码里做了wx.canIUse('requestSubscribeMessage')的兼容判断,不支持就提示用户升级微信版本。
6.2 项目的可扩展方向与性能优化
做完这个系统之后,我明显感觉到它已经具备了一个"可用工具"的雏形,但还是有些地方可以继续扩展。
一个是重复日程。目前每次日程都要单独建,如果要实现"每周一健身"这种周期性的日程,还需要加一个repeat_type字段,配合定时任务在每次标记完成后生成下一次日程。这个逻辑不复杂,但细节多,得单独开一轮开发。
另一个是多端数据同步。目前数据只在微信小程序端可以看到,如果我想在电脑上快速新增日程,就还得写一个 Web 端。好在我后端 API 已经是 RESTful 风格,Web 端直接复用现成接口就行,前端花一天就能搭出来。
还有一个值得做的方向是数据统计。建一张日志表记录用户每天完成了哪些日程,然后用 Python 的数据分析能力做一个简单的周报,比如"你这周完成了 12 个日程,最常被拖延的时间段是晚上 9 点以后"。这种个性化分析正是 Python 生态里最擅长的东西,也是日程管理 App 区别于普通日历的高级卖点。
6.3 部署环境里值得注意的两个细节
最后补充两个生产环境部署的细节。
第一,微信小程序正式环境必须用 HTTPS。开发时可以用开发者工具的"不校验合法域名"来绕过,上线前一定要在微信公众平台后台配置 HTTPS 域名,同时要保证后端服务器的证书是有效的。我用的是 Nginx 配置 SSL 证书,反向代理到本机的 8000 端口 waitress 服务。
第二,access_token必须加缓存。微信的access_token有效期是 7200 秒,但每天有获取次数限制。我在后端写了一个简单的文件缓存:先读本地 JSON 文件里的 token 和过期时间,如果没过期就直接用,过期了才请求新的。千万不要每次发订阅消息都去获取一次 token,那样很快就会触发微信的接口频率限制。
从最开始冒出"自己做一个日程提醒系统"的念头,到小程序内弹窗、后端定时任务、订阅消息推送全部跑通,整个过程让我最大的感受是:这类项目拼的不是单个技术点的深度,而是把登录态、数据表设计、定时任务、前端交互、第三方接口约束全部串起来的能力。如果你也想练手,我的建议是先不要急着写代码,把"用户会在什么时候打开小程序""提醒最晚提前多久发""订阅消息授权失败怎么办"这几个问题想清楚,再动工,整个开发过程会顺畅很多。