简介:一款基于微信小程序的车位预约系统完整源码,面向正在学习微信小程序开发或准备课程设计、毕业设计的开发者。项目采用微信开发者工具与Java、MySQL组合,实现了用户端登录后查看停车场、浏览资讯、停车记录与个人中心,并支持管理员对用户、停车场、停车记录、资讯信息进行管理。压缩包共981个文件,涵盖png、svg、gif等界面素材,js、css等前端交互和样式,以及java、sql等后端与数据库脚本,整体约9.87MB,目录结构清晰,便于按模块对照学习。目前已有530人浏览学习,源码经作者亲测可正常运行,可直接导入开发工具部署,适合作为项目实例参考或二次开发基础。
1. 拿到车位预约小程序源码包之后,先回答三个问题
上个月有个朋友从网上下了一份车位预约小程序源码,解压完第一句话是“怎么导入到微信开发者工具就报 app.json 找不到”。这其实是这类打包源码最常见的开场白。所谓微信小程序开发项目实例,尤其是带后端和数据库脚本的完整源码,它的价值不在“能跑”这件事,而在两件事:一是它把一个小程序从页面到接口到数据库的完整链路摆在你面前,照着拆能省掉大量试错;二是它给你一个可改的业务底子,换皮之后就能接真实需求。这篇笔记我按自己的习惯,把解压、选型、跑通、改业务、排错这条路径完整写一遍,内容偏向可以做出来的操作,不只是讲道理。适合刚入门的开发者,也适合拿到代码后想快速上手的私单开发。
2. 车位预约小程序源码的结构与选型:先从解压目录看懂这个项目
2.1 解压后的目录结构,其实是源码的第一张地图
拿到一个 rar 压缩包,别急着双击导入。你先解压到纯英文路径下,比如D:\work\parking-miniapp,然后打开目录看第一层。这套车位预约类项目,不管具体哪个版本,目录结构大概率逃不出下面几种形态。
第一类是微信原生小程序工程,最明显的标志是根目录有project.config.json,页面目录叫miniprogram/或者直接叫pages/。里面按页面拆成多个文件夹,每个页面文件夹放着同名的.js、.json、.wxml、.wxss四个文件。这种工程最省心,微信开发者工具选目录直接认。
第二类是云开发工程,看有没有cloudfunctions/目录。如果有,说明后端逻辑放在云函数里,数据库用的是腾讯云开发自带数据库,不需要自己装 MySQL。这类项目的运行成本最低,但不适合做重业务,因为云函数的冷启动和数据库聚合能力有限。
第三类是 uni-app 或 Taro 跨端工程,看有没有src/、pages.json、manifest.json。这种源码是 Vue 或 React 语法,需要先在命令行跑npm install和npm run dev:mp-weixin编译成小程序,再导入开发者工具。如果你拿到的源码属于这一类,就不要傻乎乎地在开发者工具里直接打开src目录了,先在 HBuilderX 或命令行里编一遍。
判断清楚了形态,你才能决定后面每一步操作方式。原生工程用“导入项目”,uni-app 工程用“编译后导入”,云开发工程导入后还要记得开通云环境。一个很常见的翻车,就是拿到 uni-app 的源码包,当成原生小程序去导入,结果提示“未找到 app.json”。
2.2 技术选型:原生小程序加 Spring Boot 后端,为什么是源码包最常见的答案
车位预约这种带状态流转、订单、超时释放的业务,源码作者很少只写一个小程序端。小程序端只是展示和操作页面,真正的车位状态、订单记录、定时任务必须落在服务端。市面上的车位预约源码,后端常见的有三种:Java Spring Boot、Node.js Express、微信云开发。
我最常见到的是原生小程序配 Spring Boot。原因很直接:这类源码很多脱胎于课设或者实际项目沉淀,Java 后端在事务、定时任务、权限上成熟,开发者写起来省事。如果你在后端目录里看到pom.xml,那就是 Maven 管理的 Java 工程;看到package.json,则是 Node 工程。不同后端对应不同启动方式,别拿一种命令套所有源码。
同是“小程序自动化”,现在也有用一个 uniapp 写多端的方案。但注意,uniapp 写出来的微信小程序,底层仍然是微信的wx.request、wx.login、wx.requestPayment,只是语法换成了 Vue。它在安卓、iOS、鸿蒙上的表现差异并不在小程序端,而在打包原生应用时才能感知。拿到的源码如果是 uniapp 形态,你要先确认它是编译到微信小程序用,还是要再打包成 App;两者在生命周期和平台差异处理上完全不同。
选型的核心标准是你要把代码部署到哪里。如果接的是小区物业的私单,物业方只有一台 Windows 主机,要求简单可维护,那 Spring Boot 打包成 jar 扔上去最稳妥;如果只是自用或个人学习,云开发零成本,不需要服务器,也不用配域名和 HTTPS。这个判断直接决定你要不要花钱买服务器和域名,所以放到动手之前想清楚。
2.3 从页面到后端的第一条请求链路:接口封装的位置与改造口径
不管你是什么形态的工程,小程序里发请求的代码都会集中在一个文件里。原生小程序一般叫utils/request.js或service/api.js,uni-app 则喜欢放在src/utils/request.js。改动后端地址、统一加 token、统一处理错误码,都先找这个文件。
先看一个很典型的封装长什么样:
// utils/request.js const baseUrl = 'http://192.168.1.100:8080/api' const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { wx.request({ url: baseUrl + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') }, success(res) { // 业务约定:code 为 0 表示成功 if (res.data.code === 0) { resolve(res.data) } else { wx.showToast({ title: res.data.msg, icon: 'none' }) reject(res.data) } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) } module.exports = { request }这段逻辑的核心有三处。第一,baseUrl写死在代码里,后面要区分开发环境和正式环境,就是改这个变量;第二,Authorization从本地缓存里取 token,每次请求自动带,后端用它识别登录用户;第三,res.data.code === 0是业务成功约定,和后端返回结构强相关。
你在改造前要做的第一件事,就是打开后端接口文档或者看后端 Controller,确认返回结构到底是{code, msg, data}还是{success: true, data},然后再调封装。我看到很多人拿到源码后直接拿原封装的判断逻辑去请求新接口,结果 code 对不上,请求永远走失败分支,还四处怀疑服务器问题,这种问题属于黑匣子不拆就乱猜。请求链路拆清楚之后,后续所有页面上的接口调用就都通顺了。
3. 把车位预约源码跑起来:最小启动命令与三个必改配置
3.1 导入微信开发者工具的三个配置:AppID、基础库与调试开关
当你确认了工程类型,下一步就是把它导入微信开发者工具。打开工具,点“导入项目”,选择解压后的根目录。这里要分三种情况:原生小程序选根目录就行;云开发工程同样选根目录,之后在开发者工具里点“云开发”开通环境;uni-app 编译出来的文件在dist/dev/mp-weixin目录下,选这个目录,不要再选回源码根目录。
导入后三个配置必须检查。
第一个是 AppID。源码包里自带的 AppID 是原作者的,不改会出现两种现象:一是真机预览时提示 AppID 不合法;二是你自己的后台配置不了 request 合法域名,因为域名绑定和 AppID 一一对应。点工具栏“详情 - 基本信息 - AppID”,改成你自己的,个人开发和测试可以直接点“测试号”,不用注册企业号也能编译预览。
第二个是基础库版本。车位预约这种中等复杂度项目,用到wx.getAccountInfoSync、wx.requestPayment之类的 API 都用 2.x 基础库够用。但如果源码用了新语法或者新的组件属性,基础库太低会报“组件编译错误”。我会先保持默认,编译报错了再把“调试基础库”切高一级试试。这里没有统一最优版本,以源码实际语法为准。
第三个是本地调试开关。在“详情 - 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。这个是开发期必需项,原因后面避坑章节详细说。注意,这个选项勾上之后,所有请求都能打到你本地起的 HTTP 后端上;不勾的话,开发者工具会拦截所有非 HTTPS 请求,测试阶段直接寸步难行。
这三个配置调完,先编译一次,看到模拟器里出现页面再往后走。如果报错,按章节 5.1 的排查方法对照处理。
3.2 初始化数据库并启动后端:建库脚本与启动命令
后端启动前必须先确认数据库存在,否则接口一调用就 500。打开后端目录,找application.yml或application.properties,看数据库连接串里配置的库名、账号、密码。常见配置长这样:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/parking_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456这里的库名是parking_db。用命令行或者 Navicat 建库后,再执行源码包里的sql目录或db目录下那个.sql脚本,把表和初始数据导进去。注意执行前确认脚本里 CREATE DATABASE 的语句,如果已有就会报错,手动把这几行删掉再跑。
数据库就绪后,启动方式看后端工程类型。Maven 工程在根目录执行:
mvn spring-boot:run第一次执行会下载依赖,耗时三五分钟属正常。Node 后端则执行:
npm install npm run dev启动成功的标志不是控制台不报错,而是你能在浏览器直接访问后端接口,比如http://localhost:8080/api/parking/list,返回 JSON 才算真正活着。很多新手看到 Spring 的启动 banner 出来就以为成功了,结果端口冲突、数据库没连上这类问题,全都要等第一个请求才暴露。
3.3 小程序端切换后端地址:baseURL、局域网 IP 与真机调试
后端跑起来之后,小程序端还差最后一步:把请求地址指向你的这台开发机。第一章讲的baseUrl这里要动刀了。开发阶段,推荐按微信环境自动判断:
// utils/env.js const accountInfo = wx.getAccountInfoSync() const envVersion = accountInfo.miniProgram.envVersion // envVersion 取值:develop 开发版 / trial 体验版 / release 正式版 let baseUrl = 'https://api.example.com' if (envVersion === 'develop') { baseUrl = 'http://192.168.1.100:8080/api' } module.exports = { baseUrl }这段代码的思路是:正式环境永远走 HTTPS 域名,开发环境走局域网 IP。wx.getAccountInfoSync()是微信官方提供的环境判断接口,不用你每次调代码去改上线地址。真机预览时,手机和电脑连同一个 Wi-Fi,然后打开“真机调试”,小程序就会请求你电脑的局域网 IP。
这里有个很关键的认知:真机上访问localhost不是指你电脑,而是指手机自己。你要把baseUrl改成开发机的局域网 IP,否则真机必定request:fail。同时确认电脑防火墙放行了 8080 端口,不然手机请求会被拒。Windows 上改“防火墙 - 允许应用通过”,把 Java 或 Node 的监听端口放出来。
4. 预约核心链路:车位状态机、锁位逻辑与超时释放
4.1 数据库表设计与订单状态机的五个边界
跑通只是开始,真正决定这套源码能不能用,要看预约业务的核心链路。车位预约最核心的难点不是页面多复杂,而是同一时刻两个人同时预约同一个车位时,系统怎么保证不超卖。这个问题的答案,藏在数据库表设计和接口实现里。
先说表结构。一套最简单但完整的车位预约,至少需要两张表:车位表和订单表。我在自己的项目中常用这样的建表方案:
CREATE TABLE parking_space ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL COMMENT '车位编号', space_status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1锁定 2维护', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_status (space_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车位表'; CREATE TABLE appointment_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT '订单号', user_id BIGINT NOT NULL COMMENT '用户ID', space_id BIGINT NOT NULL COMMENT '车位ID', appoint_time DATETIME NOT NULL COMMENT '预约时段', order_status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已锁定 2已完成 3已取消 4超时释放', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_space_time (space_id, appoint_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约订单表';space_status表示车位当前状态,order_status表示订单状态。两者不是一回事:车位可以空闲,但订单可能是完成或取消状态;车位锁定,订单一定是待支付或已锁定。状态流转关系是:用户在页面选车位,点击预约,后端先锁车位(车位从 0 变 1),同时生成状态为 0 的订单;用户支付成功,订单变 1;入场核销后订单变 2;用户主动取消,车位释放回 0,订单变 3;超时未支付,车位释放,订单变 4。
把订单状态定义到 5 个,是为了让超时释放和用户取消不打架。如果只有一个“进行中”概念,定时任务扫到就释放,用户此时刚好取消,就会把已释放的车位再置成空闲,出现重复操作。状态字段分得细,才能保证每次更新订单状态时,都明确知道自己从哪个状态来、该往哪个状态去。
4.2 锁位代码:事务和条件更新,先把“并发翻车”堵死
有了表结构,接下来是锁位接口。这是全项目最容易翻车的地方,因为并发问题只在两个人同时请求时才出现,平时调试根本看不出来。先说一个反面写法,很多人第一次写预约接口是这样:
// 错误示范:先查再改,并发下必出问题 ParkingSpace space = spaceMapper.selectById(spaceId); if (space.getSpaceStatus() == 0) { space.setSpaceStatus(1); spaceMapper.updateById(space); // 创建订单 }代码逻辑看着没问题,但两个请求同时读到 status = 0,同时通过判断,同时执行 update,最后两个订单都建出来。问题不在判断条件,而在“查”和“改”之间留了时间窗口。正确做法是把状态判断全部挪进一条 UPDATE 语句,利用数据库行锁保证原子性:
@Transactional(rollbackFor = Exception.class) public boolean reserveSpace(Long spaceId, Long userId, LocalDateTime appointTime) { // 条件更新:只有 status = 0 的车位才会被更新 int updated = parkingSpaceMapper.lockSpace( spaceId, AppConstants.SPACE_STATUS_LOCKED, AppConstants.SPACE_STATUS_FREE ); if (updated == 0) { return false; // 车位已被抢,预约失败 } AppointmentOrder order = new AppointmentOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setSpaceId(spaceId); order.setAppointTime(appointTime); order.setOrderStatus(AppConstants.ORDER_STATUS_PENDING_PAY); orderMapper.insert(order); return true; }对应的 SQL 是这样:
UPDATE parking_space SET space_status = #{lockedStatus} WHERE id = #{spaceId} AND space_status = #{freeStatus}这段 SQL 的作用是关键:WHERE条件里带space_status = 0,意味着只有当前确实是空闲状态的行才会被更新。数据库执行 UPDATE 时会锁住这一行,第二个事务的 UPDATE 会阻塞到第一个事务提交,然后发现状态已经是 1,更新 0 行。updated == 0就说明没抢到车位,直接返回失败。这比“先查后改”可靠得多,也是我最推荐的条件更新方案。
事务上的@Transactional保证了车位更新和订单插入要么一起成功、要么一起回滚。如果订单插入失败,车位状态更新也会回滚回空闲,不会出现“车位锁了订单没了”的脏数据。rollbackFor = Exception.class这个参数建议保留,因为 Spring 默认只在遇到运行时异常时回滚,不配这个参数,检查型异常会导致事务不回滚,留下一堆中间状态。
4.3 超时释放与定时任务:状态机之外的补偿机制
车位锁上之后,如果用户不支付,车位就永远锁下去了,必须有一个定时任务把超时订单释放掉。源码里如果没有这段逻辑,你要自己补上。常见做法是用 Spring 的@Scheduled,在启动类上加@EnableScheduling,然后写一个轮询任务:
@Scheduled(fixedRate = 60000) public void releaseTimeoutOrders() { LocalDateTime timeoutPoint = LocalDateTime.now().minusMinutes(15); List<AppointmentOrder> timeoutOrders = orderMapper.selectTimeoutOrders( AppConstants.ORDER_STATUS_PENDING_PAY, timeoutPoint ); for (AppointmentOrder order : timeoutOrders) { // 订单置为超时释放,车位回到空闲 orderMapper.updateStatus( order.getId(), AppConstants.ORDER_STATUS_TIMEOUT_RELEASE, AppConstants.ORDER_STATUS_PENDING_PAY ); parkingSpaceMapper.releaseSpace(order.getSpaceId()); } }fixedRate = 60000表示每 60 秒跑一次。这里的参数不是越小越好,扫描频率越高对数据库压力越大;也不是越大越好,频率太低会让用户等很久才能抢回那个车位。我自己一般用 60 秒,用户可接受的等待范围是合理的。
还需要一个细节:释放之前再判断一次订单状态。selectTimeoutOrders的 SQL 里必须带上order_status = 0和create_time < timeoutPoint两个条件,避免把用户已经支付过的订单释放掉。只按时间筛选会误伤刚支付成功的订单,这种问题属于典型的“定时任务补偿逻辑没写严”。
定时任务属于兜底方案。任何状态机在正常流程之外都必须有补偿机制,因为用户可能完成支付但网络回调丢失,可能点取消时接口报错,这些意外情况最后都靠定时任务或者补偿脚本兜住。判断一个源码成熟不成熟,不要只看页面和接口,直接看它有没有补偿任务。
5. 从源码到上线的避坑记录:五类高频问题与排查方法
5.1 报错“未找到 app.json”:工程目录选错是头号翻车点
现象:项目导入微信开发者工具,直接报错“未找到 app.json”或“app.json 文件内容错误”。
原因:大多数情况是导入时选了错误的目录。拿到源码包解压后,最外层通常有一层文件夹,里面才是真正的工程根目录。如果你选中了最外层,或者直接选中了src目录,工具就找不到project.config.json,更找不到app.json。另一种可能是工程里有多个app.json,比如编译产物和源码混在一起,开发工具识别了错误的一层。
解决:先手动找到那个同时包含app.json、project.config.json、pages/三个东西的目录。在“导入项目”时选择它,不要选中里面的pages或package子目录。如果你拿的是 uni-app 源码,找到编译输出目录dist/dev/mp-weixin再导入。用这个规则,九成“app.json 找不到”都能解决。
5.2 真机请求全部失败,开发者工具里却一切正常
现象:开发者工具里页面数据正常,后端日志也能看到请求,但点“真机调试”,手机上请求全部报request:fail,页面一片空白。
原因:开发者工具默认勾选了“不校验合法域名”,所以本地http://192.168.x.x:8080随便请求都放行;真机上,小程序宿主环境会校验 request 合法域名,HTTP 明文地址默认被拦。如果手机上连的 Wi-Fi 跟电脑不在同一网段,或者电脑防火墙挡了 8080 端口,同样会请求失败。
解决:真机预览时点右上角菜单,开启“开发调试”。在开发者工具的“详情 - 本地设置”里保持勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。同时确认手机和电脑在同一局域网,并且baseUrl用的是电脑的局域网 IP,不是localhost。部署上线之前,把后端挂到 HTTPS 域名上,去微信公众平台把域名配进“request 合法域名”,再取消调试模式。这一条是源码从本机走向线上的必经之路,绕不过去。
5.3 两个用户同时预约,车位被重复锁定
现象:两个人同时在页面上预约同一个车位,两个订单都创建成功,但车位上只显示一个状态;或者只有一个订单成功,但车位状态变成空闲了,用户下次还能预约这个已占用的车位。
原因:后端接口用了“先查状态再更新”的写法,两条并发请求都查到车位空闲,接着都去更新。这是高并发场景下的竞态条件,本地单线程测试永远不会触发,一上线人多就翻车。还有种可能是没有事务,锁车位和插入订单不是一起提交,中间步骤失败留下一半数据。
解决:按第 4.2 节改成条件更新,UPDATE ... WHERE id = ? AND space_status = 0,以受影响行数判断是否预约成功。同时给订单表加UNIQUE KEY uk_user_time (user_id, appoint_time),防止同一用户在同一时间段重复下单。另外,在锁位方法上补上@Transactional,保证车位更新和订单插入同生共死。排查时可以并发测试验证,具体命令在第 6 章写。
5.4 地图组件白屏,车位坐标偏到马路对面
现象:小程序里展示停车场位置的地图一直转圈,拿到源码后在开发者工具上地图能显示,但真机上一片白;或者地图显示出来了,车位分布在完全偏离实际位置的地方。
原因:微信小程序的地图组件map使用的是腾讯坐标体系 GCJ-02,源码数据库里如果存的是高德/谷歌地图的坐标,属于火星坐标,直接放上去就会偏。地图白屏一般是缺少腾讯位置服务的 Key,或者 Key 配置的request 合法域名没生效;开发工具可能因调试模式放行,真机上校验严格直接拦截。
解决:先到腾讯位置服务控制台申请一个微信小程序专用的 Key,将 Key 填入map组件的subkey属性或者按源码要求配置。坐标维度确认当前用的坐标系,如果是其他坐标系,先用坐标转换接口把它转成 GCJ-02 再落库。注意,转换要在服务端做,不要在小程序端每次加载时转,会拖慢首屏渲染。
5.5 支付遇到“虚拟支付”限制:先分清订单类型再谈支付
现象:源码里接入了wx.requestPayment,但开发者工具里调用支付时提示“当前小程序未开通微信支付”,或者代码编译后审核被驳回,原因是支付类目与小程序服务类目不一致。
原因:微信支付商户号不是小程序自带的能力,需要提前在微信支付商户平台申请,并且在小程序后台关联商户号。另外,微信小程序对苹果端的“虚拟支付”有边界限制,虚拟商品、数字内容、会员等类型在 iOS 端不可用;车位预约属于线下实体服务,本身不在虚拟支付管控范围内,但提交审核时如果订单描述写得含糊,比如只写“订单支付”“在线购买”,容易被误判。
解决:提前办理微信支付商户号,并确认小程序主体与商户号主体一致。代码里调用支付时,订单描述尽量具体,写清“车位预约费-停车场名称-车位编号-预约时段”,让审核人员一眼看出是线下服务。审核被驳回时,根据驳回的类目提示去补充对应资质;如果是 iOS 虚拟支付限制,则将虚拟商品入口在 iOS 端隐藏或改用线下扫码支付。支付这块最忌临时抱佛脚,商户号申请周期长,最好在动手改代码前就提交申请。
6. 改造方向:给车位预约订单加支付回调,并用并发脚本验证状态机
钱包到车位预约,关键技术点在支付回调。如果你的源码已经包含wx.requestPayment,那要检查后端有没有notify_url。微信支付是异步的,用户点支付成功,微信服务器也会发一个回调给后端,后端验签通过后才应该把订单状态从“待支付”改成“已锁定”。如果只在小程序端回调里改状态,用户关掉支付页面或网络断开,数据和微信侧就对不上。
改造流程是:小程序端拿到后端下单接口返回的支付参数,调用wx.requestPayment拉起收银台;支付成功后微信服务器异步通知你配置的notify_url;后端在通知接口里校验签名、金额、订单号,确认无误后更新订单状态。支付参数字段是固定的,对照检查源码有没有按要求传参:
| 参数 | 说明 |
|---|---|
| timeStamp | 支付签名时间戳,单位秒 |
| nonceStr | 随机字符串,不长于 32 位 |
| package | 统一下单生成的prepay_id=xxx |
| signType | 签名类型,一般用 RSA2 |
| paySign | 使用商户私钥生成的签名 |
改完后别只测正常路径。用一个 AB 并发脚本直接打锁位接口,看数据库会不会重复:
ab -n 100 -c 20 -p /tmp/reserve.json -T application/json \ http://127.0.0.1:8080/api/appointment/reserve-n 100表示总请求 100 次,-c 20表示同时 20 个并发,-p指定请求体 JSON 文件。跑完去查parking_space里该车位的状态和appointment_order订单数,锁位成功的数量应该和数据库记录一致,不能出现两个成功订单对应一个车位的情况。这一步通过,说明状态机的并发保护是真实的,不是纸上谈兵。
我现在的习惯是,拿到一份源码先花十分钟把状态转移画出来,再动手改接口。很多所谓的源码 bug,最后都是状态没定义清楚、补偿机制缺失导致的。先理清边界再写代码,比反复调试省时间得多,希望帮到你。
本文还有配套的精品资源,点击获取