最近手头做了一个家教中介平台的小程序,从技术选型到上线前后花了一个多月,用的就是UniApp。简单说,这个项目解决的是家长找家教和大学生老师接单这个双向匹配问题:家长在小程序里发布需求、浏览老师简历、下单付费,老师端完成接单、约课、查看课酬结算。整套系统从零开始搭,涉及微信登录、支付、订阅消息、订单状态机这些常规但绕不开的模块。如果你正准备用UniApp开发微信小程序,或者想了解家教中介这类平台型小程序从设计到上线的完整链路,这篇内容可以当一份实操参考。
1. 项目设计:先把家教中介的底子打对
1.1 业务角色和核心流程
家教中介本质是双边平台,同时服务家长/学生端和老师端。家长端的核心诉求是快速找到靠谱的老师,查看老师的教龄、学校/专业、试讲评价,按次或按课时付费。老师端的核心诉求是展示自己的授课能力、接单、管理上课时间、结算课酬。中间的平台方(也就是家教中介)需要控制订单流程和资金流向。
我在设计时把整个业务拆成了五条主线:
- 找老师和发需求:家长可以浏览老师列表,也可以发布一条带科目、年级、上课时长的需求单。
- 接单:老师看到需求大厅里的单子或收到订阅消息推送,确认接单。
- 支付:家长下单后先支付课时费,平台作为第三方托管。
- 授课与确认:线下或线上授课后,家长确认完成,课时费解冻并结算给老师。
- 评价与售后:家长对老师评价,产生纠纷时可申请退款。
这五条主线最后都落到“订单”这个核心实体上,所以开发前把订单状态机和数据关系理清楚,比急着写UI重要得多。我见过不少项目前期图省事,订单状态随手定义几个字符串,后面接单、退款、结算全在业务代码里散着写,改一个需求就要翻遍所有接口,代价非常大。
1.2 为什么选UniApp而非原生微信小程序
其实纯做微信小程序,原生开发也完全可行,这次选UniApp是有几层考虑。
第一,客户明确提到后续可能要做App和H5,不想每端都养一套代码。UniApp用Vue语法,一套代码编译到微信小程序、H5、App,团队里前端继续写Vue,不用额外学小程序那套自定义组件语法和WXML模板。第二,UniApp对微信小程序的兼容性已经相当成熟,uni.login、uni.requestPayment这些API在小程序端会自动映射成微信的wx.login、wx.requestPayment,不用自己判断平台差异。第三,HBuilderX的开箱即用体验不错,新建项目选中“默认模板”,里面已经配好了pages.json和manifest.json,直接在微信开发者工具里Ctrl+R刷新就能看到效果。
当然UniApp也有代价:如果用到比较偏门的小程序原生能力,比如某些硬件SDK、高级画布能力,框架这层有时会成为瓶颈。但家教中介这种CRUD加支付类业务,UniApp完全够用,省下来的开发时间可以直接投入业务逻辑打磨。
1.3 数据模型与订单状态设计
我用了一个比较朴素但很稳定的数据库设计,核心就几张表:用户表、老师信息表、订单表、评价表、退款表。字段列一下,方便你照着建表。
用户表(user):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| openid | varchar | 微信openid |
| unionid | varchar | 微信unionid |
| role | tinyint | 1家长/2老师/3管理员 |
| nickname | varchar | 用户昵称 |
| avatar_url | varchar | 头像地址 |
| phone | char | 手机号 |
| status | tinyint | 1正常/0禁用 |
| create_time | datetime | 注册时间 |
老师信息表(teacher_info):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| user_id | int | 关联user.id |
| subject | varchar | 授课科目 |
| school | varchar | 就读/毕业院校 |
| intro | text | 个人简介 |
| price | decimal(10,2) | 单课时价格 |
| total_lessons | int | 累计授课次数 |
| audit_status | tinyint | 0待审核/1通过/2驳回 |
订单表(order):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| order_no | varchar | 订单号 |
| parent_id | int | 家长用户ID |
| teacher_id | int | 老师用户ID |
| subject | varchar | 科目 |
| lesson_count | int | 课时数 |
| amount | decimal(10,2) | 实付金额 |
| status | tinyint | 状态 |
| pay_time | datetime | 支付时间 |
| create_time | datetime | 下单时间 |
订单状态我定义成了7个数字,后面所有接口逻辑都围着这组状态转:
- 0 待支付
- 1 已支付待接单
- 2 老师已接单
- 3 授课中(第一次课开始后)
- 4 已完成(家长确认)
- 5 已取消
- 6 退款中
- 7 已退款
这里最关键的一个决定是“资金托管”的思路:家长付款后钱先进平台的商户号,而不是直接打给老师,等家长确认完成某次课后,平台再把对应课酬结算给老师。这样做的好处是降低交易纠纷,代价是后端要单独维护一个“可结算余额”字段,逻辑比直接打款复杂一些。跟客户解释这个方案时我用了一个类比:就像在电商平台买东西,钱先由平台保管,签收后商家才能拿到钱。家长一听就理解了,业务上也更稳。
2. 核心功能实现:登录、下单、支付一条线
2.1 微信登录与用户角色绑定
家教中介和普通内容类小程序不一样,用户注册完必须明确自己的身份是家长还是老师,因为两边看到的首页和功能完全不同。所以我做登录时没有一上来就让用户选角色,而是先静默登录,拿到openid生成token,再把角色选择放在注册资料页。
登录流程核心代码大致是这样:
// pages/login/login.vue uni.login({ provider: 'weixin', success: (loginRes) => { const code = loginRes.code uni.request({ url: `${baseUrl}/api/auth/login`, method: 'POST', data: { code }, success: (res) => { if (res.data.code === 0) { uni.setStorageSync('token', res.data.data.token) uni.setStorageSync('userInfo', res.data.data.userInfo) // 根据userInfo.role跳转不同页面 if (res.data.data.userInfo.role === 0) { uni.navigateTo({ url: '/pages/register/register' }) } else { uni.switchTab({ url: '/pages/index/index' }) } } } }) } })后端拿着code去微信的jscode2session接口换openid和session_key,这一步注意两点:code只能用一次,前端重复触发登录,后端要做好幂等,不能因为网络重试就报错;openid是每个小程序加每个微信用户唯一的,同一个用户在“家教小程序”和另一个小程序里的openid不一样,所以如果后续要做App、公众号数据打通,最好同时存储unionid。我这次项目里就存了unionid,后面客户要开通公众号会员卡时直接用上了。
再说新版头像昵称获取。2022年后微信调整了规则,wx.getUserProfile拿不到真实头像昵称,现在推荐的做法是让用户手动填:头像用button的open-type="chooseAvatar",昵称用input的type="nickname"。微信会自动带出用户微信昵称,用户点了就能回填,体验很顺。如果还按旧资料去写wx.getUserProfile,真机会直接返回灰色头像和一串“微信用户”占位昵称。这个坑我调了整整一个下午,印象非常深。
2.2 需求发布与订单状态流转
家长端发布需求是一个大表单:科目、年级、老师性别偏好、预算、上课方式(线上/上门)、期望上课时间段,提交后生成一条“找老师需求单”。老师端首页通过“推荐老师”和“需求大厅”两个入口做匹配,需求大厅按发布时间倒序展示,老师可以筛选科目和年级。
订单状态流转我用了状态机来管理。因为家教订单和电商订单不一样,它不是“下单-发货-收货”的简单线性流程,而是支持多次课、部分退款、老师拒绝接单等分支。我在后端写了一个OrderStateMachine,所有订单操作(支付、接单、取消、确认完成)都走同一个处理器,按“当前状态+操作事件”决定下一步状态和是否触发消息通知。
状态流转核心关系:
| 操作/当前状态 | 待支付 | 已支付待接单 | 授课中 | 已完成 |
|---|---|---|---|---|
| 支付 | 已支付待接单 | - | - | - |
| 接单 | - | 授课中 | - | - |
| 确认完成 | - | - | 已完成 | - |
| 取消订单 | 已取消 | 已取消 | 不允许 | 不允许 |
这个状态机避免了大量散落在业务代码里的if else,后来查线上问题效率高了不少。订阅消息这里要特别提一下:小程序为了防骚扰,消息推送要用订阅消息模板,每次发送前都要用户主动订阅一次,一次性模板只能发一条。我的实现是:家长下单成功后弹窗,让家长授权“订单进度通知”和“老师接单通知”,同时老师端在接单前也授权“新订单通知”。模板ID在微信公众平台申请并审核通过后,以常量配置在代码里,不要写死在请求URL里。
2.3 微信支付的接入与退款处理
支付是家教中介这类业务最绕不开的环节。小程序内支付的流程是:前端拿订单号调后端创建支付单接口;后端调微信支付统一下单接口,传入openid、订单号、金额,得到prepay_id和paySign参数;前端用uni.requestPayment拉起收银台,用户输入密码完成支付;最后微信支付异步通知后端支付结果,后端更新订单状态。
前端代码核心就这几行:
uni.requestPayment({ provider: 'wxpay', timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: 'RSA', paySign: payParams.paySign, success: () => { uni.showToast({ title: '支付成功' }) }, fail: (err) => { // 用户取消或支付失败 console.error('支付失败', err) } })注意这里的package字段在uni-app里是保留字,传给requestPayment时直接写package,但如果你在某些组件或模板里用了同名变量,可能触发编译报错。我给项目组定的规范是一律叫payPackage,避免踩坑。
退款部分建议前端只做“申请退款”,不直接调退款接口。因为退款涉及资金安全,必须由后端校验订单状态、计算可退金额,再调用微信支付的退款接口。如果家长只上了一次课要退剩余课时,后端要做一次按比例退的计算,并把退款记录写到退款表,方便记账和对账。支付回调还要做幂等处理,因为微信支付的通知有可能重复推送,后端必须判断订单是否已处理过,避免重复更新。
3. 从HBuilderX到微信开发者工具:开发调试与上线
3.1 HBuilderX里创建项目和appid配置
用HBuilderX新建项目,选择“默认模板(uni-app)”,框架选Vue 3,然后打开项目根目录的manifest.json,在“微信小程序配置”这一栏填上微信小程序的AppID。
有个坑非常多的人问:明明在HBuilderX里改了appid,为什么运行到微信开发者工具里,模拟器上报错还带着旧的小程序标识?原因在于HBuilderX运行微信小程序时,会生成一个unpackage/dist/dev/mp-weixin目录,里面有一套project.config.json,如果这个文件里保留了旧的appid,微信开发者工具会用这个旧值覆盖HBuilderX同步过来的配置。
解决办法很简单:每次修改appid后先清理unpackage/dist/dev/mp-weixin目录,再重新运行;或者直接在微信开发者工具里点“详情-基本信息”,把AppID改成新的。如果是用cli方式创建的项目,还要检查src/manifest.json和根目录project.config.json,两个文件都要改。
提示:HBuilderX运行到微信开发者工具时,如果开发者工具的端口被占用或版本不一致,会频繁报“编译失败”或“上传失败”。建议把HBuilderX和微信开发者工具都升级到正式版,并在微信开发者工具“设置-安全设置”里开启服务端口。
3.2 微信小程序的特殊配置与提审
小程序上线前有几步必须提前做,不然后端接口会全部请求失败。
第一,配置request合法域名。在微信公众平台的开发管理-开发设置-服务器域名里,把https接口域名加进去。开发阶段可以勾选“不校验合法域名”,但上传正式版后这个勾选无效,必须走真实域名配置。第二,开通支付。小程序后台要关联微信支付商户号,然后把商户号mch_id、APIv3密钥写入后端配置文件。这一步如果没做,前端调uni.requestPayment会一直报“商户号未关联”。第三,小程序上传代码后,在微信公众平台提交审核。家教中介这种平台类小程序,审核时容易因为涉及“在线交易+教育服务”被要求补充类目资质,我这次在服务类目里选了“教育服务-培训机构”,并把平台的营业执照和ICP备案传上去,审核才顺利通过。
如果只做微信小程序端,到这里基本就结束了。但UniApp的好处在于,如果客户后面说“我要个安卓App”,你可以在HBuilderX里选择“发行-原生App云打包”,填好Android包名和证书,就能生成apk。代码里如果有平台差异化逻辑,用条件编译:
// #ifdef MP-WEIXIN console.log('这一段只在微信小程序里执行') // #endif // #ifdef APP-PLUS console.log('这一段只在App里执行') // #endif3.3 WebView、地图等原生能力的兼容差异
我做家教中介时,有一部分老师需要上传试讲视频或展示教学场地,我用了web-view嵌套一个视频详情页。在小程序里打开web-view页面会有过渡白屏,原因是web-view加载H5页面需要时间,尤其首次冷启动时明显。优化办法是先加载一个带loading动画的原生页面,等web-view的加载完成事件触发后再切换显示。如果加载速度依然太慢,可以考虑直接用小程序原生video组件替代页面跳转。
另外有些页面要用地图标注老师的上门授课范围。UniApp编译到微信小程序时,map组件是原生的,H5端只能用腾讯地图JS,这种差异要用条件编译分别处理,否则在H5端直接白屏。当时为了让两端都能用,我把地图封装成了一个公共组件,内部用条件编译区分,调用方不需要感知差异。
4. 开发中踩过的坑与排查记录
4.1 登录态失效:获取登录后的微信用户失败
开发中后台会报错“获取微信用户失败”,小程序端提示信息里有时会带类似wx1cb4398e1413dce7这样的小程序标识,当时我排查了很久。这种问题有几种典型成因:manifest.json里配的还是测试appid,后端实际用的却是另一个小程序的appid,导致前端拿到的code在后端解析时和前端对不上;后端调jscode2session接口返回session_key为空,通常是小程序没有做服务器域名配置或appsecret配错;前端在非用户主动操作时调了uni.login,微信对这种静默请求有频率限制。
排查方法是:先在前端打印uni.login拿到的code,后端拿到code后手动用REST工具调用一次jscode2session,返回的openid如果和数据库对不上,基本就是appid和appsecret的问题,不是代码逻辑的问题。定位到这一步,问题通常就解决了一半。
4.2 真机调试报net::ERR_CONNECTION_RESET
这个问题是客户在验收时遇到的:电脑模拟器一切正常,真机扫码一打开就报请求失败,错误信息是net::ERR_CONNECTION_RESET。排查了一遍,最可能的原因是开发环境的后端地址是http的局域网IP,真机和小程序要求必须是https且配置合法域名。
我在开发阶段用了一个小技巧:本地起一个支持https的反向代理,然后把这个https域名临时加到微信公众平台的工作台域名里,再把手机和电脑连同一个网段调试。线上环境因为域名本来就合法,所以不存在这个问题。如果只是本机调试,也可以临时在微信开发者工具里勾选“不校验合法域名”,但注意这只对开发者工具有效,真机预览时还是要靠配置解决。
4.3 下拉刷新与滚动冲突、导航栏高度适配
小程序页面开启下拉刷新后,如果页面里同时有scroll-view或较长list,很容易出现“手指往下滑时,先触发了页面级下拉刷新,而不是列表滚动”的问题。解决办法是在scroll-view上监听@scrolltoupper,当scrollTop为0时再调用uni.startPullDownRefresh,同时在页面onPullDownRefresh里做数据刷新;或者在scroll-view外层用一个普通view包着,避免两套滚动叠加。
顶部导航栏的高度在自定义导航栏时也要单独算。用uni.getSystemInfoSync()可以拿到statusBarHeight,安卓和iOS不一样,刘海屏也不一样,计算整个导航栏高度时用statusBarHeight + 44px,44px是微信小程序默认导航栏高度。如果直接用固定px写死,部分机型上按钮会被刘海遮住。我封装了一个工具函数:
const systemInfo = uni.getSystemInfoSync() export const navBarHeight = systemInfo.statusBarHeight + 444.4 常见问题速查表
| 问题 | 原因 | 解决参考 |
|---|---|---|
| 运行后小程序标识还是旧的 | unpackage目录里项目配置文件残留旧appid | 清理mp-weixin目录再运行,或在微信开发者工具详情中修改AppID |
| 真机请求报net::ERR_CONNECTION_RESET | 请求域名不是https或不在合法域名列表 | 配置合法域名,或临时勾选不校验域名 |
| 页面白屏 | 页面路由不存在、组件未注册或基础库版本过低 | 检查pages.json路由,升级微信开发者工具基础库 |
| 按钮在刘海屏上错位 | 自定义导航栏用了固定px | 用statusBarHeight + 44计算 |
| 下单支付失败 | 商户号未关联或参数格式错误 | 检查商户号绑定和RSA签名参数 |
| 用户头像昵称为灰色占位 | 仍在使用旧版wx.getUserProfile | 改用chooseAvatar和type="nickname"输入框 |
结尾
我做完这个项目最大的体会是:家教中介这种平台型小程序,技术本身的难点并不在于某个单点功能,而在于把用户角色、订单状态、资金流和消息通知串成一条完整且一致的链路。微信登录、支付、订阅消息这些能力单独看都不难,但一旦订单状态设计得模棱两可,后面接单、退款、结算每个环节都会连环出问题。所以动手之前,建议先把状态机画清楚,用纸笔画就行,然后再写业务代码。
最后再分享一个小技巧:尽量把业务逻辑往“服务端驱动”靠,前端只负责渲染和用户交互。比如订单状态是否可取消、退款金额怎么算,这种逻辑放在后端统一处理,前端拿到状态后按条件渲染按钮就行。这样哪怕以后你多端发布,App、H5复用同一套后端逻辑,不至于每一端都要重写一遍业务规则。希望这篇内容对准备用UniApp做微信小程序家教中介项目的朋友有点帮助。