先说结论:这个项目是个标准的小程序业务系统,技术栈不复杂,但胜在业务链路完整,从微信登录、号源展示再到预约下单、订单管理,几乎把小程序开发里最常踩的坑都趟了一遍。如果你正准备做类似的预约类小程序,这篇东西应该能帮你省下不少排查时间。
1. 项目背景与整体规划
1.1 为什么做宠物医院挂号预约系统
宠物医疗这个赛道这几年增长很猛,但大部分中小型宠物医院的线上化程度其实很低。很多医院还在用微信群接龙、电话预约甚至到店排队的方式管理挂号,效率低不说,还容易出错。做这个小程序的初衷很直接:让宠物主人能像人去医院挂号一样,在小程序里看到医院有哪些科室、哪些医生、哪个时间段有空号,然后直接预约,不需要打电话问前台,到了店也不用干等。
从产品角度看,这个系统本质上是一个垂直领域的预约工具,核心解决三个问题:信息透明(让用户提前知道医生排班和号源情况)、流程标准化(把预约、取消、核销的规则固化成系统逻辑)、数据可回溯(商家能统计每个医生的接诊量、每个时段的预约率)。
1.2 技术选型:PHP + uni-app 的搭配逻辑
先说后端为什么选 PHP。这个项目的定位是中小型宠物医院的数字化工具,不是高并发大流量的平台级应用,用 PHP 开发速度快、部署成本低、生态成熟,ThinkPHP 或 Laravel 框架都够用。实际开发中我用的 ThinkPHP 8,因为它的路由、ORM 和中间件机制比较轻量,符合这类业务系统的需要。如果你的团队更熟悉 Laravel,那一套也行,核心业务逻辑差别不大。
前端选 uni-app 的理由更充分:一套代码编译成微信小程序、H5、App,对业务方来说意味着买一送一。哪怕初期只发布微信小程序,后续要扩展抖音小程序、百度小程序或者打包成 App,uni-app 的跨端能力能省掉一整条开发线。而且 uni-app 的语法基于 Vue,组件的生态也比较完善,像uni.request、uni.login、uni.navigateTo这些 API 封装得比较顺手,写起来不像原生小程序那么繁琐。
提示:如果你的项目只需要微信小程序一个端,用原生开发也没问题。但考虑到宠物医院这类甲方经常会在上线后提"能不能做个 H5 版本给用户在微信里打开",用 uni-app 前期多花的一点学习成本是值得的。
1.3 项目整体模块划分
整个系统分成用户端小程序和管理后台两个部分,我用一张功能清单来梳理:
| 模块 | 用户端小程序 | 管理后台 |
|---|---|---|
| 登录授权 | 微信授权登录、手机号绑定 | 账号密码登录 |
| 医院信息 | 首页展示、医院介绍、科室列表 | 医院信息编辑 |
| 医生管理 | 医生列表、医生详情 | 医生信息维护、排班设置 |
| 号源预约 | 选择科室/医生/日期/时间段、提交预约 | 号源管理、放号设置 |
| 订单管理 | 预约记录、取消预约、就诊状态 | 预约审核、核销、统计报表 |
| 个人中心 | 宠物档案、我的预约、消息通知 | 会员管理 |
| 消息通知 | 预约成功、就诊提醒(模板消息) | 消息模板配置 |
2. 系统架构与数据库设计
2.1 技术架构与部署方案
这个项目的架构可以用一张图说清楚:
前端 uni-app 编译后的微信小程序通过wx.request请求后端 API,后端采用 ThinkPHP 8 的 MVC 结构,Nginx 做 Web 服务器,MySQL 8.0 做数据存储,Redis 做缓存和号源防并发控制。小程序端通过 HTTPS 调用接口,域名需要在小程序后台配置白名单。
开发环境我建议用宝塔面板部署,省去手动配 Nginx、PHP、MySQL 的时间。PHP 版本选 8.0 以上,主要是性能更好,而且 TP8 本身也要求 PHP 8.0。Redis 用来存 token、短信验证码、号源锁,在生产环境是刚需。
2.2 核心数据表结构设计
数据库设计是这类系统的重头戏,直接决定后期开发的顺利程度。我这里把最关键的表结构列出来,具体的字段设计如下:
用户表(user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| openid | varchar(64) | 微信 openid,唯一索引 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(255) | 头像 URL |
| phone | varchar(20) | 手机号 |
| create_time | datetime | 创建时间 |
宠物档案表(pet)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| user_id | int | 所属用户 |
| name | varchar(50) | 宠物昵称 |
| type | tinyint | 宠物类型:1狗 2猫 3其他 |
| breed | varchar(50) | 品种 |
| gender | tinyint | 性别 |
| birthday | date | 出生日期 |
| avatar | varchar(255) | 宠物照片 |
科室表(department):id、名称、介绍、图标、排序、状态。
医生表(doctor):id、科室id、姓名、职称、头像、擅长领域、简介、状态。
排班表(schedule)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| doctor_id | int | 医生 |
| work_date | date | 日期 |
| time_slot | varchar(20) | 时间段,如09:00-09:30 |
| total_num | int | 总号源数 |
| remain_num | int | 剩余号数 |
| status | tinyint | 状态:1正常 0停诊 |
预约订单表(appointment)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| order_no | varchar(32) | 订单号 |
| user_id | int | 用户 |
| pet_id | int | 宠物档案 |
| doctor_id | int | 医生 |
| schedule_id | int | 排班 id |
| appoint_date | date | 预约日期 |
| time_slot | varchar(20) | 时间段 |
| status | tinyint | 状态映射 |
| cancel_reason | varchar(255) | 取消原因 |
| create_time | datetime | 创建时间 |
这里的status字段建议用状态机设计,我用的是:0待就诊、1已完成、2已取消、3已核销、4待支付(如果有付费需求)。后期如果要对接支付,可以在订单表加上pay_status和transaction_id字段。
2.3 核心设计思路:号源扣减的并发处理
预约系统中最重要的就是防止超卖——两个用户同时抢同一个时间段的最后一个号,不能都成功。我的方案是 Redis 原子扣减 + 数据库乐观锁兜底。
具体逻辑是:
// 1. 尝试扣减 Redis 中的号源 $redisKey = "schedule:{$scheduleId}:remain"; $remain = Redis::decr($redisKey); if ($remain < 0) { // 号已放完,回滚 Redis::incr($redisKey); return error('该时间段已约满'); } // 2. 扣减成功,生成订单 $result = Db::name('appointment')->insert($orderData); // 3. 数据库操作失败需要回滚 Redis if (!$result) { Redis::incr($redisKey); return error('系统繁忙,请重试'); }同时,在 appointment 表里对schedule_id加唯一约束(每个用户每个排班只能预约一次),防止用户重复提交。这个双保险机制在测试环境下可能看不出差别,一旦上线遇到真实流量就会明白它的价值。
3. 后端接口开发与核心逻辑实现
3.1 微信登录与 Token 鉴权
微信小程序的登录流程大家应该不陌生,但真正写好需要处理不少细节。核心流程是:
- 小程序端调用
uni.login获取 code - 后端拿 code 调用微信的
jscode2session接口,换回 openid 和 session_key - 自定义一个 token(我用的是
md5(openid . time() . random)),存 Redis 并设置过期时间(7 天) - 小程序后续请求在 header 里带 token,后端用中间件校验
关键代码如下:
public function login($code) { $appId = config('wechat.appid'); $appSecret = config('wechat.secret'); $url = "https://api.weixin.qq.com/sns/jscode2session?appid={$appId}&secret={$appSecret}&js_code={$code}&grant_type=authorization_code"; $result = file_get_contents($url); $data = json_decode($result, true); if (!isset($data['openid'])) { return json(['code' => 400, 'msg' => '登录失败']); } // 检查用户是否存在 $user = Db::name('user')->where('openid', $data['openid'])->find(); if (!$user) { $userId = Db::name('user')->insertGetId([ 'openid' => $data['openid'], 'create_time' => date('Y-m-d H:i:s') ]); } else { $userId = $user['id']; } // 生成 token $token = md5($data['openid'] . time() . mt_rand(1000, 9999)); Redis::setex('token:' . $token, 604800, $userId); return json(['code' => 200, 'token' => $token, 'user_id' => $userId]); }这里有个坑要注意:session_key涉及解密用户手机号的逻辑,如果你要获取用户手机号,需要保存 session_key,而它有效期很短。我的做法是登录后不主动解密手机号,等用户授权手机号时再临时用 code 换 session_key 解密,这样可以避免 session_key 过期的问题。
3.2 医生排班与号源列表接口
预约系统最核心的接口就是获取医生的排班信息和剩余号源。前端需要展示某一天哪些医生有号、哪些时间段还能约。接口设计上,我按照"日期维度"提供数据:
public function getScheduleList() { $doctorId = input('doctor_id'); $workDate = input('work_date'); // 查询某个医生某天的排班 $scheduleList = Db::name('schedule') ->where('doctor_id', $doctorId) ->where('work_date', $workDate) ->field('id, time_slot, total_num, remain_num, status') ->select() ->toArray(); return json(['code' => 200, 'data' => $scheduleList]); }前端拿到这段数据后,把时间段渲染成网格,已满的时间段置灰不可选,用户点击可用的时间段填入预约信息。这里有个产品层面的小设计:为了让号源看起来更真实,我把每个时间段的号源数默认设为 5,用户预约后remain_num就 -1。医生可以设置每天的号源总量,如果某个时间段停诊,后台把status置 0 即可。
3.3 预约下单与取消预约
预约下单接口是整个系统中逻辑最复杂的一块,涉及排班校验、宠物选择、订单生成三个环节。我建议把下单流程拆成两步:第一步选时间段,第二步填用户/宠物信息提交。这样前端页面结构更清晰,后端接口也更单一职责。
提交预约的核心逻辑:
public function createAppointment() { $userId = request()->userId; $scheduleId = input('schedule_id'); $petId = input('pet_id'); $doctorId = input('doctor_id'); $appointDate = input('appoint_date'); $timeSlot = input('time_slot'); // 验证排班状态 $schedule = Db::name('schedule')->where('id', $scheduleId)->find(); if (!$schedule || $schedule['status'] != 1) { return json(['code' => 400, 'msg' => '该时间段不可预约']); } if ($schedule['remain_num'] <= 0) { return json(['code' => 400, 'msg' => '该时间段已约满']); } // Redis 原子扣减 $redisKey = "schedule:{$scheduleId}:remain"; $remain = Redis::decr($redisKey); if ($remain < 0) { Redis::incr($redisKey); return json(['code' => 400, 'msg' => '手速太慢,号已被约走']); } // 生成订单 $orderNo = date('YmdHis') . mt_rand(1000, 9999); try { Db::name('appointment')->insert([ 'order_no' => $orderNo, 'user_id' => $userId, 'pet_id' => $petId, 'doctor_id' => $doctorId, 'schedule_id' => $scheduleId, 'appoint_date' => $appointDate, 'time_slot' => $timeSlot, 'status' => 0, 'create_time' => date('Y-m-d H:i:s') ]); Db::name('schedule')->where('id', $scheduleId)->dec('remain_num')->update(); } catch (\Exception $e) { Redis::incr($redisKey); return json(['code' => 500, 'msg' => '系统繁忙']); } return json(['code' => 200, 'msg' => '预约成功', 'order_no' => $orderNo]); }取消预约的逻辑是反向操作,把订单状态改为已取消,同时把排班的remain_num+1,并释放 Redis 中的号源。注意:取消操作必须做状态校验,只有待就诊状态的订单才能取消,已完成或已取消的订单要直接拦截。
3.4 消息通知:预约成功与就诊提醒
消息通知用的是微信小程序的订阅消息(模板消息已经下线了,别再踩这个坑)。用户预约成功后,需要引导用户订阅"预约成功通知"和"就诊提醒"。这里有个体验细节:订阅消息的弹窗不能由后端直接触发,必须由用户点击行为触发,所以要在预约提交后的成功页放一个"接收通知"按钮,点击后调用uni.requestSubscribeMessage让用户授权。
后端发送订阅消息的核心逻辑:
public function sendSubscribeMessage($openid, $page, $data) { $templateId = config('wechat.template_id'); $accessToken = $this->getAccessToken(); $url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={$accessToken}"; $body = [ 'touser' => $openid, 'template_id' => $templateId, 'page' => $page, 'data' => $data ]; $result = $this->httpPost($url, json_encode($body)); return $result; }提示:订阅消息有个关键限制——用户点击授权一次,后端只能发一条。所以预约成功通知和就诊提醒需要两个不同的模板 ID,用户分别授权。前端在提交成功的页面里,可以同时调用两次
uni.requestSubscribeMessage,一次传一个模板 ID。
4. 前端核心页面与交互实现
4.1 微信授权登录与全局状态管理
uniapp 里的登录和原生小程序略有一点区别。我用的方式是在 App.vue 的onLaunch里主动调用uni.login拿 code,然后请求后端登录接口拿 token 存到uni.setStorageSync。用一个全局的loginStatus变量管理登录态,可以在 Vuex 或 Pinia 中维护。
因为项目用的是 Vue 3 语法,所以状态管理我选了 Pinia。login module 的核心逻辑:
export const useUserStore = defineStore('user', { state: () => ({ token: '', userInfo: {}, petList: [] }), actions: { async login() { const { code } = await uni.login({ provider: 'weixin' }); const res = await request({ url: '/auth/login', method: 'POST', data: { code } }); this.token = res.data.token; this.userInfo = res.data.user_info; uni.setStorageSync('token', this.token); }, async checkLogin() { this.token = uni.getStorageSync('token') || ''; if (!this.token) { // 跳转到登录页 uni.navigateTo({ url: '/pages/login/login' }); } } } });这里有个细节:uni.login拿到的 code 是一次性的,5 分钟有效。如果用户在弱网环境下请求超时,第二次调用uni.login会重新生成 code,所以前端需要做重试机制。
4.2 首页与预约流程页面实现
首页在小程序中承担的功能是指引用户快速进入预约流程的导航页。考虑到宠物医院的信息结构,我把首页拆成几个模块:轮播图(宣传位)、科室入口(横向滚动 icon 列表)、推荐医生(卡片展示)、底部快捷操作(预约挂号按钮)。
预约流程从首页点击"预约挂号"开始,经过四个步骤:
第一步:选择科室
科室列表用最简单的列表页即可,但要注意加载性能。科室数量一般不超过 20 个,可以直接一次性加载,不需要做分页。每个科室带一个 icon,用v-for循环渲染。
<view class="department-list"> <view class="department-item" v-for="item in departmentList" :key="item.id" @click="selectDepartment(item)"> <image :src="item.icon" mode="aspectFill"></image> <text>{{ item.name }}</text> <text>{{ item.doctor_count }}位医生</text> </view> </view>第二步:选择医生
进入医生列表后,需要展示医生所属的科室、职称、擅长方向,用户点进医生详情页看简介,再返回列表点击"预约"按钮。这里有一个交互细节:医生列表的排序规则建议按"预约量"降序排列,让热门医生排在前面。这个排序逻辑后端接口直接处理好,前端只需要透传排序参数即可。
第三步:选择日期与时间段
日期选择器是整个预约流程中最核心的组件。我用了uni-calendar组件的二次封装。日历组件默认是整月展示的,但这会导致初始渲染 30 多天,体验不太好。我的做法是设置start-date为当天、end-date为未来 14 天,并且用插值的方式控制日历一次只渲染 7 天的数据,用户左右滑动切换周。
时间段的选择用网格布局展示:每个时间段的格子包含时间(如 09:00-09:30)和剩余号数。剩余为 0 的格子置灰,用户无法选中。
第四步:确认预约信息
这一步有一个重要的细节:宠物档案的选择。因为一个用户可能养多只宠物,要让用户在下单时选择给哪只宠物挂号,所以在确认页需要展示当前的宠物列表,默认选中第一只,用户可以切换。如果有支付需求,这步还需要展示订单金额并调起uni.requestPayment。
4.3 预约记录页与状态管理
预约记录页展示当前用户的所有预约订单,需要支持"待就诊、已完成、已取消"三个 tab 的切换。每个 tab 对应的数据通过后端接口带状态参数过滤。
我踩过的一个大坑是:tab 切换时直接通过v-if切换内容区域,导致每次切换都重新请求接口。后来优化成保持三个页面实例,用v-show切换显示,只有下拉刷新时才重新请求。这个改动让切换体验顺畅了不少,不会出现白屏或者 loading 闪烁的状态。
预约卡片的展示信息要完整:宠物头像+名字、医生姓名、医院名称、预约日期、时间段、状态标签。已取消的订单要显示取消原因,已完成的订单要让用户能评价和查看病例记录。状态标签的颜色建议做一个统一的映射表:
const statusMap = { 0: { text: '待就诊', color: '#FF9900' }, 1: { text: '已完成', color: '#07C160' }, 2: { text: '已取消', color: '#999999' }, 3: { text: '已核销', color: '#1989FA' } };4.4 宠物档案管理
宠物档案管理是一个"看起来简单但容易做废"的模块。需求很明确:用户可以增删改查自己的宠物信息,因为预约时需要选择宠物。
实现细节上,接口就是标准的 CRUD,但图片上传是一个绕不开的点。宠物头像和病例图片需要上传到服务器,uniapp 里的图片上传相对简单,使用uni.chooseImage选择图片,再用uni.uploadFile上传。后端接收文件后用 ThinkPHP 的文件上传逻辑存储,并返回 URL。这里建议把图片处理成缩略图后再返回,避免原图过大影响加载速度。ThinkPHP 的上传代码比较简单:
public function upload() { $file = request()->file('file'); $info = $file->validate(['size' => 2097152, 'ext' => 'jpg,jpeg,png,gif']) ->move(public_path('uploads')); if ($info) { $path = '/uploads/' . $info->getSaveName(); // 生成缩略图 $image = \think\Image::open($info->getPathname()); $image->thumb(200, 200, \think\Image::THUMB_CENTER)->save($info->getPathname()); return json(['code' => 200, 'url' => $path]); } return json(['code' => 400, 'msg' => '上传失败']); }5. 关键难点与常见问题排查
5.1 微信小程序登录态过期与静默刷新
小程序登录态过期是个高频问题。微信官方机制是 access_token 的有效期是 7200 秒,但那是接口调用凭证,不是用户登录态。用户登录态(openid)本身不会过期,但我们自定义的 token 有过期时间。
我遇到的情况是:用户在小程序里停留时间超过 token 有效期后,再点击接口就全部 401。解决办法是在 request 封装里做统一拦截,遇到 401 就自动调用静默登录重新拿 token,然后重放原请求。这个逻辑写好后基本无感:
// request.js const request = (options) => { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token'); uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': token }, success: async (res) => { if (res.data.code === 401) { // token 过期,静默登录 await userStore.refreshLogin(); // 重放请求 const retryRes = await request(options); resolve(retryRes); } else { resolve(res.data); } }, fail: (err) => { reject(err); } }); }); };5.2 跨域问题与发起请求时 Nginx 代理配置
开发环境下,小程序请求本地后端接口需要开启微信开发者工具的"不校验合法域名",否则会报url not in domain list。但正式上线必须走 HTTPS 域名。如果你用的是 H5 端调试,还会碰到跨域问题。
我的建议是,开发环境直接把接口地址配成局域网 IP + Nginx 转发,生产环境配正式域名。Nginx 配置参考:
server { listen 80; server_name yourdomain.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name yourdomain.com; ssl_certificate /etc/nginx/ssl/yourdomain.pem; ssl_certificate_key /etc/nginx/ssl/yourdomain.key; root /var/www/yourproject/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; break; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 上传文件大小限制,图片上传需要调大 client_max_body_size 20m; }5.3 并发预约导致号源超卖
前面提到过 Redis 原子扣减的方案,但实际生产环境中还会遇到一个问题:Redis 服务挂了怎么办?如果真的出现了 Redis 不可用的极端情况,系统会直接报错,这时需要数据库的乐观锁兜底。
数据库层面的兜底方案:在 schedule 表加一个version字段,更新号源时带上 version 条件:
UPDATE schedule SET remain_num = remain_num - 1, version = version + 1 WHERE id = 1 AND remain_num > 0 AND version = 5;如果影响行数为 0,说明号源被抢完或版本号不匹配,需要让用户重试。这个方案虽然不够优雅,但至少能保证系统在极端情况下的数据一致性。
5.4 uni-datetime-picker 在 iOS 滚动容器中的渲染兼容
如果你在项目中使用了uni-datetime-picker,并且把它放进了 scroll-view 里,那么在 iOS 的微信小程序上可能会出现渲染异常——日期选择器弹层被裁剪、滚动不顺畅。这是因为 iOS 上小程序 webview 的渲染机制对position: fixed元素在滚动容器内的表现不一样。
解决方案有三个方向:
- 把日期选择器的弹层通过
uni-popup组件渲染到页面根节点,而不是放在 scroll-view 内部。 - 如果只需要选择日期而非时间,建议直接用原生
<picker mode="date">组件,性能更好,bug 也更少。 - 如果非要保留
uni-datetime-picker,建议用v-show控制弹层的显示,数据变化用change事件监听。同时给滚动容器加上enhanced属性,能缓解一部分渲染问题。
5.5 微信小程序包体积优化
第一次打包上传时,我遇到过一个问题:项目的包体积接近 2.5MB,微信小程序主包限制是 2MB,直接提交不了。这个问题的本质在于 uni-app 把 Vue 运行时和组件库一起打包进去了。
优化思路有两条路径:
路径一是分包加载。把预约流程的页面、宠物档案页面拆到分包里,主包只保留首页和个人中心。pages.json中的长这样:
{ "pages": [ { "path": "pages/index/index" }, { "path": "pages/user/user" } ], "subPackages": [ { "root": "pagesAppoint", "pages": [ "appoint/department", "appoint/doctor", "appoint/date", "appoint/confirm" ] }, { "root": "pagesPet", "pages": [ "pet/list", "pet/edit" ] } ] }路径二是把不需要的组件库给剔除掉。如果你只用到了 uni-ui 的部分组件,在main.js里按需引入,别用全量uView之类的组件库。我观察到很多人为了省事把整个 UI 库引进来,包体积直接翻一倍。图片资源的压缩也很关键,把超过 100KB 的图全部走 CDN,本地只保留 loading 图等基础素材。
5.6 微信小程序上架与发布常见问题
这类系统上线时要过微信审核,有几个常见的坑:
第一个是用户隐私协议。现在微信要求小程序必须配置《用户隐私保护指引》,如果你收集了用户的手机号、头像、位置信息等,必须在后台把对应的事项勾选清楚,否则接口调用时会报隐私授权错误。
第二个是类目审核。挂号预约类的小程序需要选择"医疗"-"私立医疗机构"或者"商业服务"-"预约服务"类目,不同类目需要的资质文件不同。如果你做的是非盈利性质的宠物医院展示工具,建议先以"信息服务"类目提交,审核会容易一些。
第三个是订阅消息的审核。订阅消息模板申请时,关键词需要和实际使用场景一致。比如"预约成功通知"模板,关键词要包含"预约时间"、"宠物姓名"、"医生",但是模板库里的关键词有限,你也可以自定义关键词,但需要提供使用场景说明,审核可能需要 1-3 天。提前申请好模板,别等开发完再申请。
第四个是关于获取位置的需求。如果要做附近的宠物医院推荐,就需要在小程序端调用uni.getLocation,这涉及到用户隐私接口的申请,还需要在小程序管理后台配置位置接口的用途说明。如果只是展示医院地址方便用户导航,建议直接用wx.openLocation打开地图,不需要提前申请定位权限,省去一堆审核流程。
6. 项目扩展方向与总结
项目做完了,预约功能正常跑通,后期还有几个可以做的方向:
第一,对接微信支付。目前版本是免费预约,如果医院要做"线上挂号收费",就需要在小程序后台申请微信支付商户号,前端用uni.requestPayment调起支付,后端用回调接口处理支付结果。订单状态机从"待就诊"扩展成"待支付"->"已支付"->"待就诊"。
第二,管理后台的统计报表。目前我做的后台只是基础的预约管理和医生排班管理,但真的运营起来,医院方会需要数据报表。比如按日统计预约量、按医生统计接诊量、按时段统计预约饱和度。增加一个数据看板,对提升项目的整体价值感帮助很大。
第三,多端发布。uni-app 最香的地方就是可以一套代码发布多端。把微信小程序发布到抖音小程序、支付宝小程序没什么额外成本,前端代码基本不用改,只是底层 API 略微有些不同。如果需要 App 版本,用uni-app自带的打包功能云打包成 Android/iOS 原生 App,走一遍发布流程即可。
第四,智能提醒与回访机制。预约完成后,用定时任务在就诊前一天给用户发订阅消息提醒;就诊结束后,隔三天给用户推送回访问卷。这部分可以用 PHP 的队列功能来做,比如把需要推送的消息丢进 Redis 队列,再用一个消费者脚本轮询发送,能有效避免定时任务在大流量下堆积。
最后再分享一个实际开发中的细节。做这种多端小程序项目时,接口的返回格式一定要从一开始就统一,我用的格式是:
{ "code": 200, "msg": "success", "data": {} }前后端联调时,所有接口都遵守这个规范,前端request.js里统一拦截code != 200的情况做错误提示。一旦中途修改返回结构,前端所有页面都要跟着改,这种坑我踩过一次之后就变得特别敏感。项目规模越小,越要在一开始把基础规范定好,不然后面都是给自己挖坑。