ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+uniapp预约小程序实战:从疫苗预约到通用预约系统

Python+uniapp预约小程序实战:从疫苗预约到通用预约系统 2023年之后再看这个项目名字多少带点时间印记。但把“Python_uniapp-新冠疫苗预约小程序”拆开看它本质上是一个特别典型的预约业务系统Python 提供接口uniapp 搭小程序前端用户登录、选择时间场次、锁定名额、生成预约码到线下核销整个闭环走一遍。我复盘这个项目不只是因为它把小程序开发到上线的链路走得很完整更重要的是这套骨架可以直接复用到体检预约、场馆预约、活动报名这些场景里。适合刚学完 Python 基础、想拿一个完整项目练手的朋友也适合已经写过小程序、但没正经联调过后端接口的前端同学。全文没有多高深的东西但很多细节是文档里不会写的我把能交代的坑都交代一遍。1. 项目拆解与总体方案设计1.1 这个预约小程序到底在解决什么问题预约类业务看着简单实际要处理的边界比想象中多。用户打开小程序要能看到哪些服务点可用、哪些时间场次还有余量选完时间后提交预约后台要生成一个唯一预约码到现场由工作人员扫码或输入编号核销。整个流程里有几个点特别容易翻车一是多人同时抢最后一个名额时会不会超卖二是用户约了又不来名额怎么释放三是用户在微信里授权登录、拿手机号这步权限和流程处理不好审核都过不了。在这个项目里“疫苗”只是业务名称代码层面其实就是一个 AppointMent 模型加一组状态流转。我习惯先把业务对象抽象成“服务点 场次 预约单”这样后面换任何预约场景都不用大改。1.2 后端接口为什么用 Python 而不是 Java、Node选型阶段有过争论。Java 生态里 Spring Boot 很成熟但团队里几个人对 Python 更熟而且这类预约系统的并发量根本没到需要 Java 那套重型框架的地步。Python 在这一层有两个优势开发效率极高写接口、连数据库、做定时任务都很快生态里有现成的 Flask、FastAPI、SQLAlchemy十几分钟就能把工程骨架拉起来。Flask 和 FastAPI 之间我选了 Flask。原因是预约项目要写的接口不多Flask 的灵活度足够网上资料也多遇到问题随便一搜就有答案。FastAPI 的自动接口文档和异步性能确实更好但如果团队没人用过反而会增加沟通成本。这里没有绝对的对错关键是团队能快速上手。1.3 前端为什么用 uniapp 而不是原生微信小程序如果只做微信小程序原生 WXML 完全够用。但这个项目一开始就打算以后可能上支付宝小程序和抖音小程序用 uniapp 写一套 Vue 语法编译到不同平台性价比就体现出来了。另外团队里有人熟 Vue 不熟小程序原生语法uniapp 的学习成本低很多。uniapp 打包到微信小程序时需要借助微信开发者工具这步很多新手会卡住。后面我会专门讲打包以及 source size 超过 2MB 的解决办法。用 uniapp 的另一个好处是组件库选择多像 uview-plus、uni-ui 都能直接拉进来用比自己写样式省时间。1.4 整体架构与核心数据流架构不难三个角色微信小程序端、Python 后端接口、MySQL 数据库。小程序通过 uni.request 调后端接口后端返回 JSON前端渲染页面。用户首次打开小程序先静默登录拿到 openid后端生成 token 返回之后所有请求都带 token 标识身份。预约的核心数据流是用户进入首页 → 查服务点和日期 → 前端展示某天的场次和余量 → 用户选定场次提交 → 后端事务内扣减场次余量并生成预约单 → 前端跳转预约成功页展示预约码 → 到现场工作人员输入编号核销 → 预约单状态改为“已核销”。这里所有对余量的修改都必须在后端完成绝不能依赖前端传过来的剩余数字去计算这是整个项目最核心的一条设计原则。2. 数据库设计与预约状态机2.1 核心表结构长什么样数据库设计直接决定后面写接口顺不顺手。这个项目用五张核心表用户表、服务点表、场次表、预约单表再加上一张配置表。为了好移植我尽量用通用字段命名。表名关键字段说明userid, openid, phone, nickname, avatar, created_atopenid 唯一手机号可为空stationid, name, address, work_start, work_end, status对应预约点状态控制是否可约scheduleid, station_id, service_date, start_time, end_time, total, booked, status场次表booked 是已预约数appointmentid, user_id, schedule_id, code, status, created_at, verify_at预约单code 生成 6 位数字app_configid, config_key, config_value放号规则、爽约次数限制等场次表里的 total 和 booked 是防超卖的第一道防线。每次有人预约成功booked 加一等 booked 等于 total 时这个场次就不该再被预约。这个判断不能再依赖前端灰掉按钮因为前端可以做假数据绕过后端必须硬校验。appointment 表里的 status 是预约状态机的地基设计好状态业务逻辑才会清晰。2.2 预约状态机怎么流转预约单不能只存一个“已预约”状态否则取消、爽约、过期全都没有办法区分。我用五个状态PENDING已提交待确认、CONFIRMED已确认待核销、USED已核销、CANCELLED已取消、EXPIRED已过期。PENDING 一般不做强制确认因为这种预约场景不像拼团要审核所以提交后直接置为 CONFIRMED 可以省一个步骤。但如果对接了人工审核流程就必须保留 PENDING。CONFIRMED 状态在核销后变 USED用户主动取消时变 CANCELLED超过预约时间还没核销则由定时任务把状态置为 EXPIRED。这个状态机其实很老套但好处是每个状态职责单一后面查数据、做统计都能轻松按状态分组。还有一个细节用户在 CONFIRMED 状态下取消预约场次余量必须回补否则名额就白白浪费了。回补操作要和状态变更放在同一个事务里先更新预约单状态再把 schedule.booked 减一。2.3 接口清单与参数约定接口设计我遵循一个原则资源路径用名词动作靠 HTTP 方法表达操作文档统一写清楚参数和返回结构。这个项目里接口不多列出来基本就是登录、获取手机号、服务点列表、场次列表、创建预约、我的预约、取消预约、核销预约。方法路径作用POST/api/auth/login用 code 换 tokenPOST/api/auth/phone用手机号授权 code 换手机号GET/api/station/list获取预约点列表GET/api/schedule/list?station_iddate查某天某点的场次余量POST/api/appointment/create创建预约单GET/api/appointment/mine查当前用户预约记录POST/api/appointment/cancel取消预约并回补余量POST/api/appointment/verify现场核销输入或扫码所有接口的返回结构我都统一成{ code: 0, msg: ok, data: {...} }前端封装好 request 之后只需要判断 code 就能处理绝大多数情况。千万别一会儿返回data一会儿返回result联调的时候会疯掉。3. Python 后端核心功能实现3.1 工程骨架与运行准备我用的依赖是 Flask Flask-SQLAlchemy Flask-CORS PyMySQL APScheduler。安装命令很简单但有个坑MySQL 的驱动必须装 PyMySQL直接用mysqldb在 Python3 环境会报编码问题。数据库连接串要显式加上charsetutf8mb4不然前端拿到中文容易乱码。pip install flask flask-sqlalchemy flask-cors pymysql apscheduler工程目录不用搞太复杂model、api、service、task 四个目录就够了。项目刚起步时如果设计过度每写一个接口都要在十几个文件里来回跳反而拖慢进度。我的习惯是先让接口跑通再按模块抽离公共逻辑。from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:password127.0.0.1/appointment?charsetutf8mb4 db SQLAlchemy(app) CORS(app)这段代码里CORS(app)很多人会漏。本地方便做联调但上线后如果接口和小程序都在腾讯云体系内也可以把跨域关掉改用微信小程序后台配置的 request 合法域名。CORS 开不开影响不大开发阶段开着能省很多莫名其妙的“请求失败”问题。3.2 微信登录与手机号授权微信小程序的登录不是传统用户名密码核心是wx.login()拿一个临时 code后端拿 code 去微信接口换 openid 和 session_key。这个 code 只能用一次有效期五分钟所以拿到后必须立刻处理。def wx_login(code): url https://api.weixin.qq.com/sns/jscode2session params { appid: app.config[WX_APPID], secret: app.config[WX_SECRET], js_code: code, grant_type: authorization_code } resp requests.get(url, paramsparams).json() openid resp.get(openid) # 查表或创建用户 user User.query.filter_by(openidopenid).first() if not user: user User(openidopenid) db.session.add(user) db.session.commit() return generate_token(user.id)手机号获取要注意2023 年后微信收紧了这个能力小程序必须认证还得在“小程序管理后台-设置-服务内容声明”里申请手机号快速验证组件。前端用button open-typegetPhoneNumber拿到一个动态令牌把令牌传给后端后端调微信接口换手机号。这个过程里手机号属于敏感信息不能打进日志也不能随便存到第三方数据库能加密最好。3.3 预约名额扣减并发不超卖的关键这是全项目最需要动脑的地方。最自然的写法是先查一下余量再决定能不能预约很多人会写成这样schedule Schedule.query.get(schedule_id) if schedule.booked schedule.total: schedule.booked 1 db.session.commit()单用户测试没问题但两个用户同时操作都查到 booked 小于 total都走到booked 1就可能超卖。解决思路是对同一行数据加锁或者用一条 SQL 完成“判断 更新”。schedule Schedule.query.filter_by(idschedule_id, statusopen).with_for_update().first() if schedule and schedule.booked schedule.total: appointment Appointment( user_iduser_id, schedule_idschedule_id, codegenerate_code(), statusCONFIRMED ) db.session.add(appointment) schedule.booked 1 db.session.commit()我实际更推荐用一条 SQL 做原子条件更新result db.session.execute( text(UPDATE schedule SET booked booked 1 WHERE id :id AND booked total), {id: schedule_id} ) if result.rowcount 1: # 创建预约单 passrowcount 1表示确实把余量加成功了说明名额抢到了如果等于 0说明场次已满直接告诉用户“手慢了”。这种写法不依赖数据库事务隔离级别行锁时间短性能也够用。预约码建议用随机 6 位数字重复概率不高但可以在 appointment 表里加唯一索引兜底。3.4 定时任务与超时释放预约系统离不开定时任务。我用 APScheduler 做三个任务每天凌晨创建未来 14 天的场次、每隔 5 分钟清理超时未确认的预约单、每天标记过期预约。scheduler BackgroundScheduler() scheduler.add_job(generate_schedule, cron, hour0, minute5) scheduler.add_job(expire_appointments, interval, minutes5) scheduler.start()定时任务最大的坑不是逻辑而是重复执行。如果项目跑在多进程或多实例下每个进程都会启动一个 scheduler放号的定时任务就会被执行多次。最简单的方案是加一个全局配置表锁任务执行前先查询今天的场次是否已生成已生成就直接跳过。这种幂等设计看起来不起眼但真上线后能省掉很多运维事故。4. uniapp 前端实现与微信打包4.1 创建支持 TypeScript 的 uniapp 项目uniapp 创建项目有两种常见方式。习惯用 HBuilderX 的话新建项目时可以直接勾选“使用 TypeScript”会自动生成 main.ts 和 tsconfig.json如果习惯命令行可以用 Vite 创建的 uniapp 项目模板。npx degit dcloudio/uni-preset-vue#vite my-projectTypeScript 在这类项目里的价值不是写出多复杂的类型体操而是约束接口数据结构。我会把后端的返回结果定义成 interface比如AppointmentResult、ScheduleResult这样前端联调时能少很多低级错误。不过也要注意uniapp 的 TS 环境偶尔会有类型报错常见的坑是uni.getSystemInfoSync()返回类型里的字段在 TS 下没有完整定义遇到这种情况可以直接断言成any没必要为了类型折腾半天。4.2 登录、手机号与动态设置标题前端登录逻辑比较简单先静默登录拿到 code再传给后端接口换 tokentoken 存到uni.setStorageSync。之后每次请求都在 header 里带上 token。uni.login({ provider: weixin, success: async (loginRes) { const res await request.post(/api/auth/login, { code: loginRes.code }) uni.setStorageSync(token, res.data.token) } })手机号授权用的是button open-typegetPhoneNumber getphonenumbergetPhone。回调里拿到的e.detail.code就是动态令牌把它交给后端换手机号。这里要注意如果e.detail.errMsg包含fail说明用户拒绝了授权不能继续处理要给出友好提示。动态设置标题用一行代码uni.setNavigationBarTitle({ title: 选择接种点 })这个能力在页面配置了自定义导航栏时无效需要配合页面的navigationStyle: custom自行渲染导航栏否则标题会重叠到状态栏上。4.3 列表分页加载与下拉刷新小程序列表页最容易犯的错误是把所有数据一次性渲染出来数据一多页面卡成 PPT。我做了一个通用分页逻辑page 从 1 开始每页 10 条滚动到底部时自动加载下一页下拉时重新从第一页拉。onReachBottom() { if (this.hasMore !this.loading) { this.page 1 this.loadSchedule() } }hasMore的判断依据是后端返回的数据条数是否等于 pageSize如果小于 pageSize说明没有更多数据把hasMore置成 false。加载时要加 loading 状态防止用户在请求过程中反复触底导致重复请求同一页。这是我实际开发里遇到过最多次的体验问题。下拉刷新则通过页面的onPullDownRefresh钩子实现。请求完成后必须调用uni.stopPullDownRefresh()否则 loading 动画一直转用户会觉得页面卡死了。4.4 打包超过 2MB 的解决方法微信小程序主包大小限制是 2MB但如果开了分包每个分包独立限制 2MB。项目里只要有图片资源、图表组件、第三方库稍微多一点很容易在打包时报source size 2612kb exceed max limit 2mb。我的处理顺序是先查哪里有体积大头HBuilderX 的“运行-运行到小程序-构建后输出”信息里会列出各文件体积。最常见的大头是图片解决方法是把图片从代码里挪到 CDN 或对象存储不要在本地包资源里放超过 50KB 的图片。如果用到了 uview-plus 这类组件库少用按需引入不要整个 import。最后一步是分包把业务页面拆到pages-station、pages-appointment这类分包目录主包只保留 tabBar 页面和公共组件。分包过后首次加载更快审核也更容易过。5. 接口联调与常见问题排查5.1 用流量转发工具查看小程序真实请求前端调试时后端接口报错不能只靠前端控制台那点信息判断最好能看到真实请求和响应。Charles 和 Fiddler 这类流量查看工具就是干这个的。手机和电脑连同一个局域网在工具里开启 SSL 转发并添加要看的域名设置好手机端转发就能看到小程序发出的完整请求。不同版本的工具配置有点差别但核心就三步一是手机端把网络请求转发到电脑 IP 和对应端口二是给手机装调试证书三是在工具里启用 HTTPS 解密。这套流程只建议在你自己开发调试的环境里用不要拿流量工具去分析别人的线上应用既没意义也可能踩合规红线。调试完记得把手机的转发配置关掉不然手机会一直连不上外网。5.2 uniapp 不打印日志信息的几个原因这个问题被问过很多次明明代码里写了console.log真机上却什么都看不到。常见原因有几个一是小程序真机调试时vConsole 没有打开要在开发者工具里点开“调试器”再看二是在 release 包或体验版里微信做了日志过滤建议用console.info或debug级别三是有些人把try catch包住了所有请求错误被吞掉控制台什么都没留下。排查时先打开项目配置里的开发调试模式再在 HBuilderX 里用“真机运行”而不是“发行”来测日志基本就能看到了。5.3 顶部导航栏高度适配用自定义导航栏时最怕顶部内容被刘海屏遮挡。微信小程序的导航栏由状态栏和导航栏标题区组成状态栏高度可以通过uni.getSystemInfoSync().statusBarHeight获取右侧胶囊按钮的位置可以用uni.getMenuButtonBoundingClientRect()获取。const systemInfo uni.getSystemInfoSync() const statusBarHeight systemInfo.statusBarHeight || 20 const menuButton uni.getMenuButtonBoundingClientRect()自定义导航栏内容的 top 高度一般可以直接用胶囊按钮的 top 值对齐这样在 iPhone 和安卓上的观感都比较和谐。如果不用自定义导航栏就别动这个逻辑系统默认的导航栏在大多数机型上表现都不差。5.4 常见问题速查表现象可能原因处理办法打包超过 2MB本地图片/组件库过大图片走 CDN、按需引入组件、子包拆分真机请求失败域名未配置、未开调试、证书问题后台配 request 合法域名开发时勾选“不校验域名”获取手机号失败小程序未认证、组件次数超限认证小程序查看次数额度改用实时验证组件列表加载不出更多页面高度不足或 hasMore 判断错误检查滚动容器改为判断返回条数数据库中文乱码连接串没带 utf8mb4Python 连接串显式加 charsetutf8mb4预约时间差 8 小时MySQL 时区设置连接串加 server_timezoneAsia/Shanghai这张表我基本是照着一个多月踩坑记录总结出来的每一条都真实发生过。做类似项目时遇到问题可以先来这查一遍比重新搜资料快不少。6. 复盘、复用与下一步还能做什么6.1 从 2048 小程序到预约项目我迁移了什么之前交付过一个 2048 小游戏的微信小程序源码包它不能像网页端那样直接扔到浏览器看效果必须用微信开发者工具编译预览。这个预约项目继承了同样的交付方式但多了一层后端联调。从纯前端项目跳到全栈项目最大的变化不是代码量而是思维小游戏里所有状态都在本地预约项目里必须考虑服务端数据的一致性比如用户换手机号登录、在不同设备上查看预约单状态都能同步回来。这个迁移过程对我来说比重新学一门框架更有价值。6.2 改成其他预约场景要改什么这套代码的可复用性很高。把“服务点”改成“体检中心”“疫苗接种点”“会议室”把“场次”改成日期和时段核心的表结构、接口逻辑都不用动。如果要上线还要接上微信订阅消息在用户预约成功或前一天提醒。后端加一个消息推送任务前端在用户确认预约时调订阅消息授权接口这两步都是比较标准的做法。另外可以加一个排队序号展示功能预约码改成按时间递增的排队号现场叫号体验更好。6.3 最后想说的三个小建议做完这个项目再回头看最想提醒自己的是三件事。第一接口约定一定要在写代码前定好字段名、返回结构、错误码统一整理成文档后面前后端联调至少能省一倍的沟通成本。第二并发问题不是靠压测压出来的是靠设计防出来的余量扣减这种核心操作必须在数据库层面保证原子性。第三打包体积从第一天就要在意等到功能写完了再优化各种页面依赖已经缠在一起拆包要花的力气远大于一开始就规划。如果你也在做预约类小程序按这套骨架去改遇到问题对着第 5 节的速查表排查大概率能少走很多弯路。
返回列表