☰
早教机构小程序开发实战:从预约排课到课时核销的完整方案
2026/10/24 7:48:04 网站建设 项目流程

做早教机构的小程序,这个需求放在2024年来看,技术本身并不稀奇,真正容易被忽略的是业务链路。我接手这个项目的时候,客户是一家跨区域的早教连锁品牌,线下有十几家门店,每天排课和消课全靠前台在Excel里手工记账。家长约课靠微信群喊话,顾问排课靠脑子记,月底核销课时的时候经常对不上账。这种状态下,小程序要解决的根本不是"线上有个东西能用",而是把约课、排课、扣课时、签到这一整套流程从线下搬到线上,并且保证账目不出错。

这个系列笔记会完整记录我从0到100的开发过程,包括业务梳理、技术选型、数据库设计、接口开发、小程序端对接,以及上线后遇到的各种坑。第一篇先讲清楚整体思路和基建部分:为什么用SpringMVC,数据结构怎么设计,核心接口怎么做,以及小程序端怎么对接最省事。如果你手头也接了类似的机构预约类项目,或者想把一个传统线下业务做成小程序,这篇笔记应该能帮你少走不少弯路。

1. 早教机构需要什么小程序?先把业务逻辑捋清楚

1.1 从线下运营痛点反推需求清单

很多人接这类项目上来就打开设计工具画页面,然后写接口,这是最容易翻车的顺序。我在和客户聊完两个下午之后,发现早教机构的运营流程比想象中要复杂得多,核心痛点集中在四件事:

第一是约课混乱。家长在微信群喊"周六上午的创意美术还有位子吗",顾问得翻Excel表格、翻聊天记录,甚至靠脑子回忆。一旦两个顾问同时回消息,同一个名额可能被约出去两次,最后上课当天才发现有家长撞课。

第二是扣课时不透明。早教机构的收费模式是卖课时包,比如"96节课的亲子课包",每上一次课消一个课时。线下手工消课,记错、漏记、晚记的情况特别多,月底家长来查课时余额,前台永远在道歉。

第三是签到靠纸。很多机构上课前让家长签字,签了不代表孩子真的来了,也不代表扣了课时。纸质单据后面还要人工录入系统,一个月的单据积压下来,补录效率极低。

第四是家长没有自助渠道。家长想知道孩子还剩多少课时、下周有什么课,只能打电话问前台,前台忙起来电话根本接不完。

基于这四个痛点,我给客户列了一个需求优先级清单。第一阶段先做家长端约课和签到消课,这是核心闭环。第二阶段再做管理后台的排课和报表。第三阶段再考虑营销工具比如优惠券、转介绍裂变。千万不要一上来就什么都做,尤其是营销功能,会拖慢主链路的开发进度。客户最痛的是教材对不上账,先把课约清楚、把课时扣明白,信任感就建立起来了。

优先级功能模块解决痛点复杂度
P0课程列表与详情家长不知道有什么课低
P0在线预约/取消预约约课靠人工、易撞课中
P0签到与自动扣课时消课手工记账、对不上账高
P1课时包与余额查询家长查余课困难中
P1机构通知与课表提醒家长容易忘课中
P2分享裂变与优惠券获客难、老客带新客高

1.2 确定"课"为核心主数据

需求分析阶段最大的收获,是明确了整个系统的核心主数据是"课",不是"商品"。早教机构和电商不一样,家长买的不是一件实物,而是一个个课时段。

一次完整的业务链路是这样走的:机构在后台创建课程,然后排课,生成一个带具体时间段、带容量上限的课次。家长在小程序看到课次,点击预约,系统检查剩余名额和家长课时余额,两者都满足就预约成功并锁定名额。上课当天,家长到店扫码签到,系统确认到场,这才扣除一个课时。如果家长没签到,课时保留,但名额已经占掉,所以最好还要有请假规则。

这一条链路上有两个关键点:预约成功不扣课时,签到才扣课时;取消预约要释放名额,但距离上课太近的取消要有惩罚规则或者禁止取消。这些业务规则直接决定后面数据库表和接口怎么设计,所以一定要在动手前和客户反复确认。

这里我特别提醒一点:早教行业存在"占位不扣费"模式,也存在"预约即扣费"模式。两种模式的接口逻辑完全不同。我们这次采用前者,因为家长对"约了没去还要扣课时"非常敏感,但这也带来了一个副作用:会有家长恶意占位不来上课,导致名额被浪费。所以在后面的表结构设计里,我加了一个课程状态流转字段,支持顾问后台手动取消和补位。

2. 技术选型:为什么还用SpringMVC?

2.1 这个项目为什么不用SpringBoot再炫技一把

坦率地说,现在新起项目用SpringBoot是主流,SpringMVC确实有点"老"。但在接手具体项目的时候,技术选型从来不是新不新能完全解释的。客户原有的管理后台是一个跑了三年的老工程,基于SpringMVC加MyBatis,部署在客户指定的私有服务器上,整个运维体系围绕Tomcat和手动打war包运转。如果强行引入SpringBoot,意味着要并行维护两套技术栈,要么花时间把老工程迁过去,而业务方不愿意为这个"内部升级"承担风险。

另外还有一个现实原因:SpringMVC对于这类以接口服务为主的中小型项目完全够用。注解开发、参数绑定、拦截器、JSON序列化这些能力一样不缺。技术债务要看场景,如果一个项目的瓶颈不在框架能力,而在业务规则复杂度上,那换框架解决不了业务问题,反而增加迁移风险。所以我最终的方案是:沿用SpringMVC作为Web层,保持原有工程结构统一,后面如果真有性能和开发效率的硬需求,再考虑渐进式引入其他组件。

2.2 SpringMVC项目的工程结构组织

工程结构上,我没有用传统的三层分包硬套,而是在controller、service、dao的基础上增加了一层facade。原因很简单:小程序端的接口粒度比PC管理端更细,前后端需要对齐的是一个个原子操作,而不是service层的业务方法。facade层专门负责接收前端参数、组装服务调用、返回视图模型,controller只做最薄的参数校验和路由转发。

整个工程按功能域分包,而不是按技术分层分包。比如appointment包下面有AppointmentController、AppointmentService、AppointmentMapper、AppointmentQuery,这样找代码的时候按业务域进去直接能找到完整链路。刚开始团队有人不习惯,觉得包名重复度高,但跑过两个迭代之后,新同事上手找代码的效率明显提升。

2.3 关键依赖与配置文件要点

既然是SpringMVC老工程,就没有SpringBoot那种自动装配的便利,所有配置都要手动挂载。我梳理了一份最精简的依赖清单:Spring核心、SpringMVC、MyBatis、MyBatis-Spring、Jackson、Druid连接池、Servlet API。这个组合可以覆盖全部需求,再多加框架就是增加管理负担。

MVC配置里有几个容易漏的细节。第一个是注解驱动必须开启,否则controller里的注解全部不生效:

<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper" ref="customObjectMapper"/> </bean> </mvc:message-converters> </mvc:annotation-driven>

第二个是static资源放行,小程序的接口都在 /api 前缀下,所以拦截器只拦 /api/ 就行,静态资源和后台上传图片的路径要提前在SpringMVC配置里排除,不然后台上传的图片在小程序端永远加载不出来。第三个是统一编码过滤器,这个老生常谈但如果忘了配,POST请求中文乱码排查起来特别耽误时间。

2.4 基于拦截器的登录态设计

小程序的登录态和后端Session天然不匹配,所以我没有启用Spring Session,而是自己写了一套基于Token的拦截器方案。

小程序调用wx.login拿到code,后端用code去微信服务端换openid,然后生成一个32位的随机token,把这个token和openid、用户信息一起放进Redis,同时把token返回给小程序。小程序后续每次请求都在header里带X-Token,后端拦截器从Redis查token,查到就放行并设置用户上下文,查不到就返回401。

这个方案比单纯把openid当凭证安全得多,也比JWT好在token过期和服务端踢人非常方便。拦截器里有个细节值得注意:放行白名单不要只写登录接口,还要把课程列表这类不需要登录就能看的接口单独列出来,不然家长还没登录就看不到课程,预约转化率会打折扣。

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("X-Token"); if (StringUtils.hasText(token)) { UserContext current = tokenService.getUserByToken(token); if (current != null) { UserContextHolder.set(current); return true; } } response.setStatus(401); return false; } }

3. 数据库设计:8张核心表撑起整个预约业务

3.1 核心表全景概览

数据库设计是整个项目最值得花时间的部分。我第一版设计只有5张表,后来评审的时候发现取消预约、签到历史、请假记录无处存放,硬生生扩展到了8张。这8张表构成了整个业务闭环的数据底座:

表名功能核心字段说明
member家长/会员账户openid、手机号、姓名一个家长可以绑定多个孩子
child孩子信息昵称、生日、性别课时按孩子维度消耗
course课程信息课程名、适用年龄、单课时价相当于课程字典
course_schedule排课实例课程ID、门店ID、上课时间、容量、已约人数每次上课的具体安排
appointment预约记录会员ID、孩子ID、课次ID、状态预约/取消/已上课/已请假
course_package课时包模板名称、总课时、有效期相当于收费套餐
member_package会员持有课时包会员ID、剩余课时、到期时间关键的业务账本
consume_record消课流水预约ID、课时包ID、变动数量每一笔课时进出都有据可查

这里最想强调的一点是:预约记录和消课流水必须分开。预约记录管"约没约",消课流水管"扣没扣"。如果合在一张表里,签到、请假、取消这些状态变更会把表弄成一团乱麻。分开之后,appointment记录约课状态,consume_record只记录签到扣课时这一笔账,两边用appointment_id关联,职责清晰,排查问题的时候也只要顺着一条链路查。

3.2 表结构细节设计与建表SQL

以最核心的appointment预约表为例,我列出关键字段的设计思路:

CREATE TABLE `appointment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `appointment_no` varchar(32) NOT NULL COMMENT '预约单号', `member_id` bigint(20) NOT NULL COMMENT '预约家长ID', `child_id` bigint(20) NOT NULL COMMENT '上课孩子ID', `schedule_id` bigint(20) NOT NULL COMMENT '课次ID', `package_id` bigint(20) DEFAULT NULL COMMENT '锁定课时包ID', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0已预约 1已签到 2已取消 3已请假', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `cancel_time` datetime DEFAULT NULL, `cancel_reason` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_member_schedule` (`member_id`,`schedule_id`), KEY `idx_schedule_status` (`schedule_id`,`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这个表设计有三个关键细节。第一个是appointment_no预约单号,所有和前端交互的地方都用这个业务号,不把自增id暴露给外部,避免被遍历。第二个是唯一索引uk_member_schedule,同一个家长对同一个课次只能有一条预约记录,这个唯一索引是防止重复下单的兜底。第三个是status字段配合schedule_id的复合索引,因为查询"某课次下有哪些状态的人"是所有后台列表页都要用的高频查询。

课时包和消课流水这两张表同样有讲究。member_package里有total_count和remaining_count两个字段,每次课时包发放初始余额写total_count,每次消课更新remaining_count。注意这里不能用total_count减去一条聚合查询来算余额,因为历史表数据可能被清理或调整,余额必须作为冗余字段实时维护。consume_record里变更类型用一个正负号表示,正的1代表发放课时包,负的1代表消课扣减,这样月底对账的时候只要sum一下流水,就能和member_package里的remaining_count对上。

3.3 排课容量与名额锁定的设计

课程容量控制是这个系统最容易出现并发问题的地方。一个课次最多10个孩子,但可能有30个家长同时抢。我设计了两个字段:courseschedule 表里的 total_capacity 和 booked_count。每次预约成功,booked_count 加1,同时预约记录变成已预约状态;取消预约或者请假,booked_count 减1。

这里必须配合事务和行锁来保证不超卖。我在service层做了一个update语句,让数据库帮我们把关名额是否满员:

UPDATE course_schedule SET booked_count = booked_count + 1 WHERE id = #{scheduleId} AND booked_count < total_capacity

这个语句返回受影响的行数,如果返回1说明名额抢到了,返回0说明已经满员。因为有行锁的存在,并发请求到达数据库时会被串行化,天然避免了超卖。这是我整个项目里最满意的一个设计,没有用分布式锁,也没有用Redis计数器,单纯依靠SQL的原子性就把问题解决了。

4. 后端核心接口设计:从课程展示到签到消课的完整链路

4.1 接口清单与角色权限

小程序端的接口不能设计得太重,每个接口只做一件事。我最终梳理出的核心接口一共19个,第一优先级是下面这十个:

接口路径方法功能登录要求
/api/course/listGET课程列表(按门店筛选)否
/api/course/detailGET课程详情与最近排课否
/api/schedule/listGET某课程的可约课次列表是
/api/appointment/createPOST提交预约是
/api/appointment/cancelPOST取消预约是
/api/appointment/myGET我的预约列表是
/api/sign/checkinPOST扫码签到并扣课时是
/api/package/myGET我的课时包与余额是
/api/consume/listGET课时消费流水是
/api/member/updatePOST更新家长及孩子信息是

接口设计上我遵循一个原则:写接口返回前端的视图模型,而不是直接返回数据库实体。比如课程详情的返回值,需要包括课程名、封面图、年龄段文案、剩余名额、下一节课时间。这些信息散落在course、course_schedule和门店表里,我在facade层组装好之后再吐给小程序。直接返回实体类虽然省事,但前端拿到一堆用不上的字段,后续改接口字段两边都要同步,太狼狈了。

4.2 预约接口的幂等与事务控制

预约是整个系统里最容易出问题的接口,因为一个动作同时涉及三张表的变更:检查名额、写预约记录、加booked_count。任何一个环节失败,都可能导致数据不一致。我把这一整个操作封装在一个事务方法里:

@Transactional(rollbackFor = Exception.class) public AppointmentResult createAppointment(AppointmentRequest request) { CourseSchedule schedule = scheduleMapper.lockById(request.getScheduleId()); if (schedule.getBookedCount() >= schedule.getTotalCapacity()) { throw new BusinessException("该课次已约满"); } MemberPackage memberPkg = packageMapper.lockByMemberAndChild(request.getMemberId(), request.getChildId()); if (memberPkg.getRemainingCount() <= 0) { throw new BusinessException("课时余额不足"); } if (appointmentMapper.selectUnique(request.getMemberId(), request.getScheduleId()) != null) { throw new BusinessException("您已预约过该课次"); } Appointment appointment = new Appointment(); appointment.setMemberId(request.getMemberId()); appointment.setScheduleId(request.getScheduleId()); appointment.setStatus(0); appointmentMapper.insert(appointment); scheduleMapper.increaseBookedCount(request.getScheduleId()); return new AppointmentResult(appointment.getAppointmentNo()); }

lockById和lockByMemberAndChild都是在查询语句后面加了FOR UPDATE,这样同一个课次和同一个家长在多线程并发操作时,后面的请求会阻塞等待前面的锁释放。事务的隔离级别用的是MySQL默认的RR级别,在这个场景下不会出现幻读问题。

这里有一个容易踩坑的细节:事务内尽量不要捕获异常后吞掉。如果我在createAppointment里catch住BusinessException然后返回一个错误对象,Spring的事务代理会失效,前面的写操作不会回滚,就会出现预约记录插进去了、booked_count也加了,但用户收到的是"预约失败"的尴尬情况。正确做法是让异常抛出去,统一由全局异常处理器处理。

4.3 签到消课的正确打开方式

签到是另一个核心接口,它和预约不同,预约不扣课时,签到了才扣。正常流程是:家长到店,前台在小程序管理端点击签到,或者家长自己扫码,后端把appointment状态从"已预约"改成"已签到",同时扣减会员课时包的剩余课时,写入consume_record消课流水。

实现的要点在于这三步同样要放在一个事务里。很多人在这一步图省事,直接在controller里先更新预约状态,再调service扣课时,结果更新预约成功了,扣课时失败了,孩子来上了一节课但账上没扣。这个问题的根源就是把跨表操作拆到了方法外面,事务边界没兜住。

@Transactional(rollbackFor = Exception.class) public void checkin(String appointmentNo) { Appointment appointment = appointmentMapper.selectByNo(appointmentNo); if (appointment == null) { throw new BusinessException("预约单不存在"); } if (appointment.getStatus() != 0) { throw new BusinessException("当前预约状态不可签到"); } appointmentMapper.updateStatus(appointmentNo, 1); MemberPackage memberPkg = packageMapper.lockByMemberAndChild(appointment.getMemberId(), appointment.getChildId()); memberPkg.setRemainingCount(memberPkg.getRemainingCount() - 1); packageMapper.updateRemaining(memberPkg); ConsumeRecord record = new ConsumeRecord(); record.setAppointmentId(appointment.getId()); record.setPackageId(memberPkg.getId()); record.setChangeAmount(-1); consumeRecordMapper.insert(record); }

签到接口还有两个业务细节需要和客户明确:第一,如果家长预约了但没来,课时要不要扣。我们最后的规则是不扣,但名额释放或者记录爽约,这个由后台顾问手动操作。第二,签到的操作人和时间必须记录。appointment表里我额外加了sign_user和sign_time字段,方便后续出现投诉时回溯到底是谁签的、为什么签。

5. 小程序端对接:三天跑通核心流程

5.1 登录与授权:最容易被低估的环节

小程序端的登录流程我单独拿出来说,因为这里坑最多。很多新手以为小程序登录就是调用wx.login然后拿openid就行,实际上微信在2021年之后收紧了用户信息授权,不能再直接通过login接口拿到用户的手机号和个人信息。想要手机号必须用button组件的open-type=\getPhoneNumber\让用户主动授权。

我采用了两阶段方案。第一阶段用户进来,只需要通过wx.login拿到code,后端换openid建一个匿名账户,这时候用户可以浏览课程。第二阶段用户想预约了,就必须完善孩子信息和手机号,这一步会弹窗授权,拿到信息后和匿名账户合并。这个方案的好处是第一次打开小程序的流失率不会那么高,毕竟看课不需要那么多隐私权限。

小程序的请求封装也是一个细节活。除了基础的request下载拦截,我还做了两件小事:统一在header里附加X-Token,以及统一处理401状态码跳转登录页。登录页和课程详情页的跳转去重非常容易忘记处理,用户在弹窗授权时反复点击,会触发多个wx.login并发请求,最终导致token错乱。

5.2 首页课程展示与课次卡片聚合

课程列表页的接口如果直接按数据库的schedule记录逐条返回,前端就疯了,因为一个课程可能有未来两周的二十多个课次。我设计了两个粒度的接口:课程维度只返回"下一场可约课次"作为展示信息,课次维度返回完整的可约时间列表。前端首页只需要调一次列表接口,就能渲染出课程卡片加"最近可约时间"。

小程序端的渲染没有太多技术含量,唯一要注意的是时间格式处理。后端的datetime返回到前端变成了UTC格式的字符串,如果不做格式化,界面直接显示一大串带T和Z的内容,家长根本看不懂。我在后端Jackson配置里统一设置了日期格式,同时指定了时区。这个坑非常典型,我见过太多项目在联调阶段花两天调时区,最终原因就是后端默认时区不是Asia/Shanghai。

<bean id="customObjectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"> <property name="simpleDateFormat" value="yyyy-MM-dd HH:mm"/> <property name="timeZone" value="Asia/Shanghai"/> </bean>

5.3 预约交互里的状态保护

预约按钮的点击是一个高频操作,用户可能因为网络慢或者手抖连续点击好几次。我除了在后端加唯一索引兜底之外,前端也做了三层保护:按钮点击后立刻置灰并显示loading,用setTimeout限制5秒内不可重复点击,以及预约成功后将课程卡片状态置为已约。这三层配合下来,我在测试环境用移动网络模拟低网速,连续点击十几下也只产生了一条预约记录。

这里强烈建议在小程序端做预约结果页而不是toast提示。预约成功之后,家长需要看到预约单号、上课时间、门店地址、剩余课时,这些信息放在一个成功页里,比toast弹一下有价值得多。而且成功页可以顺便放一个"加入日历"的按钮,调用wx.addPhoneCalendar直接帮家长把上课时间加到手机日历,这个功能虽然只有一行代码,但家长对这种贴心体验的感知非常强。

6. 上线前的高频问题与排查实录

6.1 问题速查表

开发过程中积累了不少问题,我把最高频的几个整理成了排查速查表,后续维护的同学照着这个表能解决掉80%的线上问题:

问题现象根本原因解决方案
课程列表接口返回特别慢排课查询没有按schedule_time建索引加复合索引(store_id, course_id, schedule_time)
用户重复提交生成两条预约前端未做防抖+后端无唯一约束前端按钮置灰,后端加唯一索引
签到成功但课时没扣扣课时方法在事务外被调用把状态更新和扣课时放进同一事务方法
小程序请求报跨域未配置CORS放行特定域名在SpringMVC里配置跨域过滤器
课时余额偶尔变负数并发签到同一课时包扣减时加FOR UPDATE行锁
后台修改排课时间前端不生效小程序端缓存了旧数据在课次数据变更时主动清理缓存
家长取消预约后名额未恢复取消和释放名额不在同一事务恢复booked_count与更新状态一起提交
图片上传成功但无法访问Tomcat静态资源路径未映射配置servlet的静态资源映射到上传目录

6.2 一次并发抢课的复盘

上线第二天,客户做了一场朋友圈推广,同时有接近200个家长涌入小程序,抢一个只有15个名额的体验课。课程列表接口的QPS瞬间到了40多,数据库连接数飙到上百。我在监控面板上看到预约接口的TPS并不高,但数据库的活跃连接数一直在涨,排查下来发现是两条慢SQL拖垮了连接池:一条是查询预约记录时的索引失效,另一条是查课时包余额时没走主键索引。

预约记录那条SQL我写了一个member_id和status的复合索引,但排查执行计划发现MySQL仍然走了全表扫描。原因是查询条件里用了OR,比如status=0 OR status=1,导致索引条件下推失败。改掉这个SQL逻辑之后,数据库活跃连接数立刻降下来了。这个案例给我的教训是:上线前一定要拿预估的最大并发量做压测,而不是在测试环境随便点点就以为没问题。服务能跑起来和扛得住流量是两回事。

6.3 从"能跑"到"好用"的优化方向

第一个版本上线稳定运行两周之后,我开始梳理第二阶段的优化清单,核心方向有三个。第一个是Redis缓存课次列表。目前课程列表接口每次都实时查MySQL,虽然加了索引之后快了不少,但这类读多写少的接口天然适合加缓存。缓存更新策略用的是更新排课时主动删除缓存,下次请求回源并重建。第二个是后台管理端的排课冲突检测。目前排课只做到了同一教室同一时间段不能重复排,但还没有做到授课老师在两个教室同时段撞课。第三个是报表模块。客户月底需要一个课时核销汇总表,机构运营也要看每个课程的上座率,这部分在第二阶段会开放给客户的管理后台。

优化方向里最需要注意的是,不要为了技术炫技引入多余的组件。我当时也考虑过引入消息队列做预约流量削峰,但和客户确认后,预约高峰其实只有每次活动推广的几分钟,数据库加上连接池控制完全扛得住。消息队列带来的运维成本对客户来说是一种负担,不划算的架构调整宁可砍掉。

7. 系列第一篇的收尾与新坑预告

第一篇笔记写到这里,项目已经从零到一跑通了核心链路:家长看课、预约、签到、消课对账。我做这个项目最大的体会是,技术难点其实不在SpringMVC或者小程序本身,而在于你对业务链条的理解有多深。把约课和消课的账理清楚,让每一项数据的变动都有据可查,系统的基本盘就稳了。

最后分享一个踩过两次坑才记住的经验:开发阶段的测试数据一定要模拟真实业务,不要只是各建几条订单就算完。我在测试环境里准备了一个"月底对账"脚本,模拟某个月有100个会员、600次上课、40次请假、20次退费的情况,每次改完数据库表结构就全量跑一遍这套数据,确认所有余额和流水对得上再上线。这个习惯帮我挡掉了很多线上的低级错误。

系列第二篇我会重点写后台管理端的排课和报表开发,以及微信支付对接和退款流程的踩坑记录。如果你也正在做类似的预约类小程序,欢迎在评论区聊聊你踩过的坑,也许下一篇文章就会把你的问题拆开来一起复盘。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询