做阅读打卡类小程序,最怕的不是功能多,而是做出来之后没人愿意天天打开。“书洞”这个项目,核心就是想用一套轻量的前后端组合,把“在线阅读”和“打卡坚持”两件事串起来:用户在微信小程序里选书、读书、记进度、点打卡,后端用Flask把图书数据、阅读记录、打卡状态管起来。技术选型上后端用Python + Flask,前端用UniApp编译到微信小程序,这套组合最大的好处是开发链路短——Flask一个文件就能跑起接口,UniApp写一套代码还能兼顾H5和App,对个人开发者和学生项目特别友好。
这篇文章不是泛泛讲概念,而是把我从零搭完“书洞”的完整过程拆给你看:从Flask后端的数据表设计、打卡规则怎么定,到UniApp前端的请求封装、阅读器页面怎么处理富文本,再到小程序上线前必须处理的域名校验、分享卡片、防录屏这些容易被忽略的细节。你跟着走一遍,不仅能复现这个项目,还能摸清Flask + UniApp这类全栈小程序项目的通用套路。
1. 书洞项目的定位与全栈架构取舍
1.1 这个项目到底解决什么问题
市面上的阅读App动辄几万个书库、社交关系链、付费会员体系,但“书洞”的定位完全不同。它的核心使用场景是:一个人想坚持每天读书,但有惰性、容易断,需要一个小而美的工具帮他记录“今天读了哪本书、读了多久、连续打卡多少天”。所以产品功能收敛成三条主线:
- 图书浏览与在线阅读:用户能在小程序里看到书单、点开书目、阅读正文内容。
- 阅读打卡:读完一段时间后点击打卡,后端记录打卡时间和阅读时长,维护连续打卡天数。
- 个人阅读轨迹:展示累计天数、总时长、历史打卡日历,给用户正反馈。
听起来不复杂,但真做起来有个关键难点——打卡功能一旦涉及“连续天数”“每日一次”这种业务规则,就不能只在前端做,必须后端来判定。前端可以随便改时间、重复点按钮,后端必须把数据校验做严。这也是为什么必须用Flask这类后端框架,而不是把数据全塞进小程序的storage里。
1.2 为什么是Flask而不是Django或FastAPI
这个选择我纠结过一段时间。Django自带Admin后台和ORM,功能全但偏重;FastAPI性能好但生态里很多插件需要自己拼;而Flask刚好卡在中间——轻、灵活、文档多、上手快。对书洞这种只有十几个接口的小项目来说,Flask的蓝图(Blueprint)机制能把用户、图书、打卡三个模块分得清清楚楚,又不至于像Django那样把目录结构铺得很大。
用Flask还有一个现实考量:部署简单。你把app.py、requirements.txt、数据库文件往服务器上一放,用gunicorn或uwsgi就能跑起来,甚至直接python app.py开发模式也能顶一阵。而很多学生项目跑在云服务器上,配置不高,Flask这种单进程轻量框架压力更小。对比Django需要配WSGI、配静态文件、配Admin站点,Flask的部署心智负担确实低一个量级。
1.3 UniApp作为前端框架的现实收益
小程序原生开发其实也能做,但用UniApp有一个非常实际的收益:一套Vue风格代码,编译到微信小程序的同时,还能出H5和Android/iOS的包。热搜词里有人问“uniapp 开发 微信小程序 vs android / ios / 鸿蒙”,我的实测感受是:如果你的目标是快速验证产品,先用UniApp跑到微信小程序是成本最低的路径;如果以后想上架App,UniApp也能通过云打包生成安装包,不用重新写一套。
但要注意,UniApp不是神。它封装了微信小程序的API,但遇到需要深度调用原生能力的场景(比如扫码、蓝牙、录屏检测),你还是得写条件编译代码,甚至要写原生插件。对书洞这种纯表单、列表、阅读器场景,UniApp的优势远大于劣势。
1.4 整体目录结构规划
我按模块划分了前后端目录,先给你一个总览:
book_cave_backend/ ├── app.py # Flask入口,注册蓝图 ├── config.py # 配置文件(数据库地址、密钥) ├── models.py # SQLAlchemy数据模型 ├── utils/ │ ├── auth.py # JWT生成与校验 │ ├── response.py # 统一返回格式 │ └── validators.py # 参数校验工具 ├── blueprints/ │ ├── auth_bp.py # 登录注册接口 │ ├── book_bp.py # 图书列表、详情、内容 │ └── checkin_bp.py # 打卡相关接口 └── requirements.txt book_cave_miniapp/ ├── pages/ │ ├── index/ # 首页书架 │ ├── reader/ # 阅读器(富文本渲染) │ ├── checkin/ # 打卡页 │ └── mine/ # 个人中心 ├── api/ │ └── request.js # uni.request封装 ├── utils/ │ └── auth.js # token存取 └── manifest.json # 小程序配置这个结构不炫技,但胜在清晰。Flask就算只用单文件也能跑,但既然项目涉及三个业务模块,用蓝图拆开更利于维护。UniApp这边,页面按照小程序TabBar的四个入口划分,api目录单独放请求封装,避免每个页面重复写请求头逻辑。
2. Flask后端的数据模型与接口设计
2.1 数据表怎么定,才能支持打卡逻辑
书洞的数据库我用SQLite起步,原因很简单:本地开发零配置,一个文件搞定。等以后用户量大了再迁MySQL,SQLAlchemy的ORM层把迁移成本压得很低。数据模型我设计了四张表:用户表、图书表、阅读记录表、打卡记录表。其中最关键的是打卡记录表,它承载了所有打卡规则:
class CheckinRecord(db.Model): __tablename__ = 'checkin_record' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) book_id = db.Column(db.Integer, db.ForeignKey('book.id'), nullable=False) checkin_date = db.Column(db.Date, nullable=False) # 打卡日期,精确到天 reading_seconds = db.Column(db.Integer, nullable=False) # 本次阅读时长(秒) created_at = db.Column(db.DateTime, default=datetime.utcnow) __table_args__ = ( db.UniqueConstraint('user_id', 'checkin_date', name='uk_user_checkin_date'), )核心设计在两个地方:
checkin_date字段只存日期,不存时间。这是为了让“一天一次”的约束在数据库层面就能查重。如果存了datetime,判断“今天是否已打卡”就得做范围查询,而且容易出时区问题。UniqueConstraint唯一约束防止同一用户同一天重复打卡。就算接口被并发调用,数据库也能兜底挡住。这个约束在开发时看似多余,但小程序端用户快速双击打卡按钮时,没有这条约束就会出现两条同一天记录,后续算连续天数时就会出错。
图书表相对常规,包含书名、作者、封面图URL、分类、简介,正文内容我用LongText字段存HTML格式,配合前端mp-html组件渲染。用户表就是openid、昵称、头像、注册时间。还有一张阅读记录表,记录用户每次阅读的开始时间、结束时间、翻阅章节,用来给打卡页的“今日阅读时长”提供原始数据——注意,打卡时填的时长不应该让用户手输,而是根据阅读记录表自动汇总,这就堵住了“直接填个999分钟刷分”的漏洞。
2.2 接口清单与统一返回格式
书洞的接口不多,我按模块整理了一下:
| 模块 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 认证 | POST | /api/auth/login | 微信登录,用code换openid,签发JWT |
| 图书 | GET | /api/books | 图书列表,支持分类和关键词筛选 |
| 图书 | GET | /api/books/ | 图书详情 |
| 图书 | GET | /api/books/ /content | 图书正文(富文本) |
| 打卡 | POST | /api/checkin | 提交打卡,传book_id和阅读秒数 |
| 打卡 | GET | /api/checkin/history | 取用户全部打卡记录,用于日历展示 |
| 打卡 | GET | /api/checkin/stats | 累计打卡天数、连续天数、总时长 |
统一返回格式我封装在utils/response.py里:
def ok(data=None, message="success"): return jsonify({"code": 0, "message": message, "data": data}) def fail(message="error", code=400): return jsonify({"code": code, "message": message, "data": None}), code除了登录接口,其余接口都需要在Header里带Authorization: Bearer 。所有响应统一用code字段区分业务状态,0代表成功,非0代表具体错误码。这样前端只需判断一次code,不用为每个接口单独写错误处理。有些教程喜欢直接用HTTP状态码来区分业务错误,但实际开发中HTTP状态码会被CDN、运营商劫持干扰,不如业务code可靠。
2.3 登录鉴权:JWT还是session
微信小程序没有传统的Cookie机制,所以登录鉴权我直接用了JWT(JSON Web Token)。流程是:小程序端wx.login拿到code,传给后端;后端拿着code调微信的jscode2session接口,换取openid;用openid查用户表,新用户就自动注册;最后签发一个有效期为7天的JWT返回给前端。
用JWT的好处是后端无状态,不用存session,很适合Flask这种轻量框架。你只需要在utils/auth.py里写一个装饰器:
def login_required(f): @wraps(f) def decorated(*args, **kwargs): auth = request.headers.get('Authorization', '') token = auth.replace('Bearer ', '') if auth.startswith('Bearer ') else '' if not token: return fail("未登录", 401) try: payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) g.user_id = payload['user_id'] except jwt.ExpiredSignatureError: return fail("登录已过期", 401) except jwt.InvalidTokenError: return fail("无效的token", 401) return f(*args, **kwargs) return decorated要注意,JWT的secret key必须放在环境变量或者独立配置文件里,别硬编码在GitHub上。我见过太多人把SECRET_KEY写死在代码里传到公开仓库,然后token被人伪造。开发阶段可以把密钥放在config.py,但上线前一定改成从环境变量读取。
2.4 关键查询:连续打卡天数怎么算
连续打卡天数是打卡系统最有价值的数字,也是SQL最容易写错的地方。我一开始想到的办法是:把用户所有打卡日期取出来,排序后循环判断是否连续。但数据量大了以后,每次统计都全量查询会越来越慢。
更优雅的做法是在打卡表里维护一个streak字段,每次打卡时根据“昨天是否打卡”来更新:
def get_or_create_today_record(user_id): today = date.today() record = CheckinRecord.query.filter_by(user_id=user_id, checkin_date=today).first() if record: return record, False # 今天已打卡过 yesterday = today - timedelta(days=1) is_streak = CheckinRecord.query.filter_by(user_id=user_id, checkin_date=yesterday).first() is not None new_record = CheckinRecord(user_id=user_id, checkin_date=today, is_streak=is_streak) db.session.add(new_record) db.session.commit() return new_record, True连续天数的计算就变成了查最近连续is_streak为True的记录数。除了维护streak字段,还需要额外维护一个streak_count字段,表示“截至当前日期的连续天数”。这样在个人中心展示“已连续打卡X天”时,只需要查最近一条记录,不需要全量扫描。
3. 打卡业务的规则引擎与防作弊处理
3.1 一天一次,但不只是“一天一次”
打卡业务表面上是“点一下按钮,后端记一条”,但深入设计之后会发现几个隐藏规则:
- 时间维度:打卡日期必须是服务器本地日期,不能信任客户端传的日期。用户可以改手机时间,绕过“每天只能打一次”的限制。所以后端取
date.today(),忽略客户端传的打卡日期。 - 时长维度:打卡必须关联实际阅读行为,不能凭空打卡。我让前端在阅读器页面记录用户实际阅读页面的停留时间,计算方式用
beforeunload事件,累计停留秒数传给后端。后端设定一个最小时长——比如阅读超过60秒才能打卡,这是产品层面的防作弊策略。 - 地点和频率维度:同一天内多次打卡不同图书,是允许还是拒绝?我设计的是“每天只能打一次卡”,但不限制打的哪本书。因为如果每本书都能打一次,用户完全可以一天刷完十本书的打卡,连续天数会变得很水。反过来的业务价值是:打卡的动作代表“我今天完成了阅读”,而不是“我今天看了这本书”。
3.2 防重复提交的事务与幂等处理
前端防重复点击靠按钮disabled,但这远远不够。真正的防线在后端。我在打卡接口里做了两重防护:
第一重是事务内先查后插。SQLAlchemy的session默认是事务性的,我在同一个事务里先query今日是否已有记录,没有则add新记录并commit。这能挡住99.9%的重复请求。但极端并发下,两个请求同时查到“没有记录”,然后同时插入,还是会撞车——所以第二重防护就是数据库的唯一约束(UniqueConstraint)。有了唯一约束,第二个请求的commit会抛IntegrityError,我捕获后直接返回“今天已经打过卡了”。
实际开发中我发现有些教程只做第一重防护,然后在小程序端用loading状态防止用户重复点击。这对个人项目够用,但并发一旦上来就会出脏数据。如果你后面打算把系统扩大,建议数据库连接池和事务隔离级别也要调一下,确保这个唯一约束在高并发下依然生效。
3.3 签到日历与补卡策略
签到日历是打卡系统的门面,用户每天打开就是看这个日历。但日历展示有个坑:你不能把数据库里没有记录的日期显示成“未打卡”,因为用户可能昨天没打开小程序,但你直接展示灰色空格,会让用户感到挫败。
我的做法是:显示最近30天的日历,数据库有记录就标“打卡”,没记录的直接留空,但不标红不加提示。如果非要补卡功能,也建议限制一个月最多补3次,每次补卡需要消耗积分或个人积分,不能无限补。这属于产品策略,我的建议是初始版本先不做补卡,保持打卡数据的真实性——一旦允许补卡,“连续天数”这个指标就失去公信力了。
3.4 打卡统计的时区问题
时区是个看起来不起眼但实际必踩的坑。Flask服务器如果部署在海外,date.today()返回的是UTC日期,而用户在中国,现在是2月1日,UTC可能还是1月31日,打卡判断就会出错。解决方法是统一用Asia/Shanghai时区:
from datetime import datetime, timedelta import zoneinfo from zoneinfo import ZoneInfo CN_TZ = ZoneInfo("Asia/Shanghai") def get_today_cn(): return datetime.now(CN_TZ).date()所有打卡日期、查询操作都用这个函数,避免因为服务器时区不同导致用户“今天还没到”或者“昨天打过了”的诡异问题。Python 3.9以上自带zoneinfo,不需要额外装pytz,这点很方便。
4. UniApp前端的页面架构与请求层封装
4.1 四个核心页面的职责划分
UniApp部分我按四个Tab页组织:书架(首页)、阅读器、打卡、我的。书架页展示图书封面网格,下拉刷新加载最新书单;阅读器页是最复杂的,要处理富文本渲染、阅读进度存储、停留时间统计;打卡页展示今日状态和最近30天日历;我的页面展示统计数据和设置项。
页面间跳转逻辑是这样:书架点击图书进入阅读器;阅读器内点击“打卡”按钮跳转打卡页;打卡页显示当前选中图书和阅读时长,确认后调用打卡接口。这个过程的数据传递用Vuex或Pinia管理,因为阅读器到打卡页之间需要带book_id和阅读秒数,用URL参数传容易又长又乱。
4.2 request.js封装:处理token和错误码
微信小程序的uni.request用起来不难,但如果不做封装,每个页面都要写请求头、错误处理、loading逻辑,代码会非常冗余。我的request.js核心逻辑如下:
const BASE_URL = 'https://your-api-domain.com/api' function request(path, method = 'GET', data = {}, needAuth = true) { return new Promise((resolve, reject) => { uni.showLoading({ title: '加载中...' }) const token = uni.getStorageSync('token') const header = { 'Content-Type': 'application/json' } if (needAuth && token) header['Authorization'] = 'Bearer ' + token uni.request({ url: BASE_URL + path, method, data, header, success: (res) => { uni.hideLoading() if (res.data.code === 0) { resolve(res.data.data) } else if (res.data.code === 401) { uni.removeStorageSync('token') uni.navigateTo({ url: '/pages/login/login' }) reject(new Error('未登录')) } else { uni.showToast({ title: res.data.message, icon: 'none' }) reject(new Error(res.data.message)) } }, fail: (err) => { uni.hideLoading() uni.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }BASE_URL的配置我建议单独放在一个config.js里,不要散落在代码中。热搜词里问“uniapp封装h5如何指向2个域名”,这个问题的本质是:同一套代码在H5环境访问开发服务器地址,在微信小程序环境访问线上API域名。解决办法是在config.js里判断process.env.NODE_ENV或uni.getSystemInfoSync().uniPlatform,选择不同的BASE_URL。小程序端只能用HTTPS的合法域名,H5端可以用http://localhost:5000,这个环境判断一定要写对。
4.3 阅读器页面的富文本渲染:为什么用mp-html
小程序原生rich-text组件也能渲染HTML,但它的坑很明确:不支持自定义样式、图片不做域名校验时会被拦截、点击事件处理困难。书洞图书正文是HTML格式,我最后选了mp-html这个扩展组件,它有几点特别适合书洞这种长文阅读场景:
- 支持表格、代码块、图片懒加载。图书正文如果包含复杂排版,mp-html能渲染得更接近网页效果。
- 图片自动适配屏幕宽度。原生rich-text会把超宽图片撑破布局,mp-html会按容器宽度缩放。
- 支持点击图片预览、链接跳转。阅读器里用户点目录、点参考链接,mp-html能捕获事件,不必自己写解析。
安装方式:把mp-html的组件目录放到components/mp-html下,并在页面json里注册:
{ "usingComponents": { "mp-html": "/components/mp-html/mp-html" } }然后页面里直接<mp-html :content="bookContent" />就行。要注意的是,图书正文里的图片URL必须是HTTPS并且是微信后台配置过的合法下载域名,否则会被小程序拦截显示空白图。这个我在线上环境踩过坑,排查了很久才发现是图片资源域名没加入downloadFile合法域名列表。
4.4 阅读时长统计的实现与注意事项
阅读时长这个数据直接影响打卡门槛,所以统计必须合理。我在阅读器页面用了两种方式结合:页面onShow时记录开始时间,onHide时把时间差累加到总时长;同时监听onUnload做最后保存。期间用setInterval每30秒自动保存一次到后端阅读记录表,防止用户读了很久却没打卡就退出小程序。
还有一点细节:用户在阅读器里可能只是挂着不动,并未真正阅读。我额外监听页面的滚动事件,如果超过5分钟没有滚动,不做新增时长统计。这个“伪阅读”过滤逻辑写在阅读器的滚动监听里,如果用户一直在滚动,说明真在看,如果长时间静止,就判定为挂机。产品上这个逻辑需要前端和后端配合,后端也要校验单次阅读时长不能超过某个阈值(比如单日累计不超过8小时),防止出现极端刷数据的情况。
5. 上线前必踩的配置坑与发布流程
5.1 manifest.json配置的注意点
每次有人说“小程序真机预览一片空白”,我第一个建议就是查manifest.json。UniApp的manifest.json不光管App打包,还管小程序端的基础信息配置。书洞项目里我重点配置了三块:
mp-weixin下的appid必须填你自己的小程序appid,不能留测试号。否则真机预览时登录接口拿不到code,后续全部请求都会失败。mp-weixin下的setting里,urlCheck在开发阶段可以设为false跳过域名校验,但上传体验版之前必须改回true,并且在小程序后台配置request合法域名。h5配置里的devServer端口最好固定,别用随机端口,否则每次跑H5开发环境地址都会变,调试很烦。
manifest.json是UniApp的枢纽,改它之后通常需要重新编译才生效。很多小程序报错“request:fail url not in domain list”,就是manifest这边没配置合法域名,或者配置了但没重新上传编译。这个坑我建议写进你的检查清单里。
5.2 微信公众平台后台的域名校验
微信小程序的网络请求限制特别严格:request、downloadFile、uploadFile各有一套合法域名,而且必须是HTTPS、必须备案。书洞项目涉及接口请求和图片下载,我一开始只配了request合法域名,结果阅读器里图书封面加载不出来,排查后才发现图片属于downloadFile合法域名,需要单独配置。
这里的实操顺序很重要:先去微信公众平台把域名加白名单,再在代码里改BASE_URL,最后重新编译上传。顺序反了的话,你会在模拟器里一切正常,但真机上永远请求失败。云开发用户还有一套免域名方案,但书洞是自建Flask后端,只能老老实实走域名白名单流程。
5.3 从UniApp到微信小程序的上传步骤
UniApp的HBuilderX里,点击“发行 -> 小程序-微信”,会生成一个unpackage/dist/dev/mp-weixin目录。然后用微信开发者工具打开这个目录,就能看到编译后的原生小程序工程。这里有几个细节:
- 每次改完UniApp代码,都要重新“发行”生成新包,微信开发者工具那边点击“编译”刷新即可。
- 上传前先在微信开发者工具里“预览”,用手机扫码真机测试一遍登录、打卡、阅读三个核心流程,确认没有报错再点“上传”。
- 上传后去微信公众平台的“版本管理”里,把刚上传的版本设为体验版,再邀请几个朋友测试。体验版没经过审核,可以在成员列表里添加体验成员。
5.4 上架审核被拒的常见原因
小程序上架审核比App Store宽松得多,但书洞这类内容类小程序有几个高频被拒原因:
- 没有ICP备案的域名。这几乎是顶格红线,域名必须备案且主体和小程序主体一致,否则基本过不了。
- 涉及图书阅读,如果只是自研书库问题不大,但如果支持用户上传图书内容,就要增加内容过滤机制,否则审核认为你有版权风险。
- 强制登录。微信要求用户必须能“先浏览后登录”,不能一进小程序就弹登录框。书洞的首页书架完全可以匿名浏览,只有打卡功能才需要登录,这个体验要贯彻在代码里。
审核周期一般1-7天,第一次提交最好把“测试账号”“功能说明”写在审核备注里,能加速通过。另外微信小程序每年都要年审,费用300元/年,这个预算要提前算进去,别等到被暂停服务才去处理。
6. 提升体验的周边能力:分享卡片、防录屏与缓存策略
6.1 自定义分享卡片:让用户帮你拉新
书洞的核心增长场景是“我今天坚持阅读打卡了,秀一下”。微信小程序的分享卡片默认只显示页面截图,效果一般。我用onShareAppMessage自定义了分享文案和图片:
onShareAppMessage() { const stats = this.stats // 打卡统计 return { title: `我已在书洞连续打卡${stats.streakDays}天,累计读完${stats.totalBooks}本书`, path: '/pages/index/index?share=1', imageUrl: this.shareBannerUrl // 自定义分享图 } }分享图最好是750x750像素的正方形,不然在微信聊天里会被裁剪。实测下来,带具体数字的分享文案比“快来跟我一起读书”这种泛文案点击率高很多,因为数字能激起比较心理。
6.2 前端防录屏的基本思路
热搜词里提到“前端uniapp防止录屏的方法”,说实话,Web技术栈没有绝对防录屏的方案,只能在现有能力内增加难度。小程序端能做的有三层:
- 阅读器页面启用
security模式,把富文本内容通过canvas绘制而不是直接渲染DOM。但canvas的文本选择、字体渲染体验比较差,书洞没有采用。 - 禁止截屏/录屏的核心API主要针对Android原生App,小程序端没有这个权限。但可以通过检测App进入后台时的记录,提示用户“检测到切出阅读界面,阅读计时暂停”,至少让用户意识到刷时间的动作不会被计入。
- 用动态水印叠加:在阅读器内容区加半透明水印,水印内容带上用户昵称和手机号,一旦截图外传可以溯源。
对书洞这种非付费阅读类应用,我做的是第二和第三层,既不过度影响阅读体验,又能降低被恶意录屏传播的风险。真要完全防录屏,iOS上苹果官方也不允许App检测和阻断截屏行为,所以别把资源全押在防录屏上。
6.3 缓存策略:阅读进度和图书列表的本地存储
小程序启动速度影响留存率,书洞把两类数据做了本地缓存。第一是图书列表,用户首页打开一次后把列表和返回时间存到storage,设定缓存时间为1小时,超时才重新请求后端,这样用户在1小时内重复打开首页不用等网络。第二是阅读进度,每本书的最近阅读章节和滚动位置都存在storage里,用户下次进入直接定位到上次位置,不用从第一章翻起。
function getCache(key, maxAge) { const cache = uni.getStorageSync(key) if (!cache) return null if (Date.now() - cache.timestamp > maxAge) { uni.removeStorageSync(key) return null } return cache.data }缓存的时间戳逻辑我放在了工具函数里,所有读取缓存的地方统一走这个函数。注意阅读进度属于用户私密数据,本地缓存只存“书中位置”这种轻量信息,不要在本地存打卡记录和登录token以外的敏感数据。小店缓存方案的key最好加上userId后缀,避免多账号切换时数据串了。
6.4 后续可以扩展的方向
书洞当前版本做完之后,我盘了一下还有三个性价比高的扩展方向:
- 阅读数据周报:每周日晚生成“本周阅读时长、完成书目、打卡天数”的小程序订阅通知,促进用户回归。小程序订阅消息需要用户主动授权,可以放在打卡成功后的弹窗里引导。
- 书摘功能:阅读器里选中文字,保存为书摘,类似笔记功能。这个功能对阅读类产品来说属于粘性最高的功能,但它涉及文本选择交互,在mp-html里要自己实现,工作量中等。
- 好友排行榜:基于“连续打卡天数”做好友排名,需要获取用户微信好友关系——这一步只能通过开放数据域实现,而且有严格的规则限制,更像社交产品的功能,初期可以不做。
写在最后
从数据表建模到小程序发布,书洞这个项目让我踩得最深的一个坑是:打卡功能的“防作弊”设计必须在后端一开始就建好,而不是上线后打补丁。数据库唯一约束、服务器时区、阅读行为校验,这些在开发期多花一小时,就能避免上线后面对一堆脏数据无处下手。另一个体会是Flask和UniApp这对组合特别适合个人开发者做垂直小工具——后端轻、前端跨端,一个人一周就能跑通全流程。如果你正在做类似的学生项目或兴趣项目,强烈建议先把这套最小闭环做出来,再根据实际使用反馈迭代功能。