简介:面向高校计算机相关专业毕业设计场景的课程答疑微信小程序完整项目,基于微信小程序前端与SSM(Spring+SpringMVC+MyBatis)后端架构,配合MySQL数据库实现管理员、教师、学生三类角色的核心功能,覆盖课程视频发布、作业布置与提交、提问答疑、系统管理等典型业务模块,界面清晰、操作流程完整,具备一定的工程实用性与可扩展性。资源包共883个文件、约55.51MB,包含java后端源码、vue管理端页面、js与wxml/wxss小程序端代码、png/svg界面素材、2份sql数据库脚本及毕业论文文档,并附带mp4演示视频,目录按功能模块组织便于查阅。已有245人学习浏览,适合需要完成课程答疑类小程序课题、参考SSM与小程序前后端整合方案的本专科毕业生使用,可用于系统设计、数据库建模与答辩演示等环节,能够帮助读者完整把握从需求分析、数据库设计到前后端联调部署的全过程。
1. 课程答疑微信小程序:毕业设计选型的第一步,为什么是SSM+MySql这套组合
真正动手做毕业设计的人都知道,最折磨人的不是编码,而是"选哪个题目、用什么框架、能不能跑通、答辩老师问什么"。课程答疑微信小程序恰好卡在这个需求上:业务闭环完整——学生提问、老师回答、课程分类、收藏点赞,每一条都能在论文里写成独立章节;技术栈又是课堂里反复讲过的微信小程序+SSM+MySql。更关键的是,这类题目通常附带源码、数据库脚本、毕业论文和视频演示,交付物齐全,意味着你拿到手之后不是从零开始,而是从"读懂->改造->跑通->讲清楚"这条捷径进入。这套组合不是最时髦的,但一定是最稳的。
2. SSM后端从0搭建:五张核心表与答疑闭环的接口设计
2.1 为什么是SSM而不是Spring Boot:选型逻辑与能力边界
SSM指的是Spring + SpringMVC + MyBatis,三层各管一件事:Spring管对象创建和事务,SpringMVC管接口路由,MyBatis管SQL映射。课程答疑这种业务,本质就是一组表的新增、查询、关联、统计,SSM这套"配置驱动"的老架子完全撑得住,而且答辩时老师最常问的IOC、AOP、动态代理、#{}和${}的区别,全都能在SSM里找到对应的代码去答。
我说句实在话,Spring Boot确实省事,自动配置、内嵌Tomcat、起步依赖一拉就起来。但很多学校的毕业设计大纲和课程设计还是以SSM为基准,论文模板里的系统架构图、分层结构、核心代码说明都是按SSM写的。你拿Spring Boot去套,反而要在论文里解释"我为什么不用SSM"。这不是技术对错问题,是性价比问题。SSM的边界也很清楚:它不适合超高并发、不适合复杂的分布式事务,但一个课程答疑小程序,日活几百就是天花板,SSM+MySql绰绰有余。
2.2 数据库设计:用户、课程、答疑帖、回答、收藏五张表
我一般建议把答疑系统的表控制在五张以内,表一多,外键关系、级联删除、联合查询全来了,论文篇幅上去了,但答辩时也更容易被问倒。核心五张表:用户表、课程表、答疑帖表、回答表、收藏表。建库建表脚本可以直接跑,注意字符集用utf8mb4,后面讲emoji踩坑时你会明白为什么。
CREATE DATABASE course_qa_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE COMMENT '微信openid,一个用户一条', nickname VARCHAR(50) NOT NULL, avatar_url VARCHAR(255) DEFAULT '', role TINYINT NOT NULL DEFAULT 0 COMMENT '0学生 1老师', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL COMMENT '关联t_user,该课程的答疑老师', description VARCHAR(255) DEFAULT '' ) ENGINE=InnoDB; CREATE TABLE t_question ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT '提问学生', course_id INT NOT NULL COMMENT '所属课程', title VARCHAR(100) NOT NULL, content TEXT NOT NULL, image_urls VARCHAR(1000) DEFAULT '' COMMENT '逗号分隔的图片地址,最多3张', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待答疑 1已回答 2已关闭', favorite_count INT NOT NULL DEFAULT 0 COMMENT '收藏数,冗余统计', answer_count INT NOT NULL DEFAULT 0 COMMENT '回答数,冗余统计', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_course_id (course_id), KEY idx_user_id (user_id) ) ENGINE=InnoDB; CREATE TABLE t_answer ( id INT PRIMARY KEY AUTO_INCREMENT, question_id INT NOT NULL, user_id INT NOT NULL COMMENT '回答者,可以是老师也可以是学生', content TEXT NOT NULL, is_accepted TINYINT NOT NULL DEFAULT 0 COMMENT '是否被采纳为最佳答案', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_question_id (question_id) ) ENGINE=InnoDB; CREATE TABLE t_favorite ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, question_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_question (user_id, question_id) ) ENGINE=InnoDB;五张表的逻辑关系是这样的:答疑帖挂在课程下面,回答挂在帖子下面,收藏是用户和帖子的多对多关系,用唯一索引保证一个用户只能收藏同一帖子一次。favorite_count和answer_count是冗余字段,每次新增回答或收藏时在事务里同步更新,这样列表页就不用COUNT子查询,SQL性能好一截。create_time全部由数据库生成,避免各服务器时间不一致。
这里有一个容易被忽略的参数:status字段。如果学生提问后一直没人答,status一直是0,列表页默认只展示有待答疑状态的帖子,已关闭的进归档。这个小设计在论文里可以写成"答疑状态机",答辩老师对状态机这个概念是买账的。
2.3 接口清单与登录鉴权:先定好路径再写代码
后端接口不要想到哪写到哪,先把接口清单敲定,前端和后端各写各的才不打架。课程答疑小程序最少需要八个接口:登录、课程列表、发布提问、提问列表、提问详情、发布回答、收藏切换、我的提问。
| 接口 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 登录 | POST | /api/user/login | 用wx.login的code换openid,返回token |
| 课程列表 | GET | /api/course/list | 首页筛选条件用 |
| 发布提问 | POST | /api/question/add | 需要登录 |
| 提问列表 | GET | /api/question/list | 支持courseId、status、分页 |
| 提问详情 | GET | /api/question/detail?id= | 返回帖子+全部回答 |
| 发布回答 | POST | /api/answer/add | 需要登录,更新answer_count |
| 收藏切换 | POST | /api/favorite/toggle | 已收藏则取消,未收藏则加入 |
| 我的提问 | GET | /api/question/my | 当前登录用户的提问列表 |
登录鉴权是SSM的一个考点,不建议用Session,小程序端不会有Cookie机制,自定义token才是常见做法。用拦截器统一处理,SpringMVC里配置一个HandlerInterceptor,放行登录接口,其他接口全部走token校验。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); if (token == null || token.isEmpty()) { return reject(response, 401, "未登录或token已过期"); } Integer userId = TokenCache.get(token); if (userId == null) { return reject(response, 401, "未登录或token已过期"); } request.setAttribute("userId", userId); return true; } private boolean reject(HttpServletResponse response, int code, String msg) throws IOException { response.setStatus(code); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":" + code + ",\"msg\":\"" + msg + "\"}"); return false; } }TokenCache建议用ConcurrentHashMap加过期时间简单实现,或者直接引入一个轻量级Redis。我做过两次课程设计,用本地缓存就够了,但你论文里如果能写一句"生产环境可替换为Redis",老师会觉得你考虑到了扩展性。登录接口里wx.login换openid这一步,需要往微信服务器发起请求,SSM里可以用RestTemplate或HttpClient,把这部分单独封装成一个WxApiService,别写在Controller里。
3. 微信小程序前端:从页面骨架到提问、回答、我的三个核心页面
3.1 小程序端项目结构:pages目录与tabBar配置
微信小程序的项目结构不需要复杂的分包,课程答疑这种体量,四个页面加一个公共请求封装就够了:首页答疑列表、详情页、发布提问页、我的页面。用tabBar把首页、提问、我的三个入口固化下来,用户操作路径最短。
{ "pages": [ "pages/index/index", "pages/detail/detail", "pages/publish/publish", "pages/my/my" ], "window": { "navigationBarTitleText": "课程答疑", "navigationBarBackgroundColor": "#2c3e50", "navigationBarTextStyle": "white" }, "tabBar": { "color": "#666666", "selectedColor": "#2c3e50", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/publish/publish", "text": "提问" }, { "pagePath": "pages/my/my", "text": "我的" } ] } }这个app.json里有个参数需要注意:tabBar的list每一项如果指定了iconPath,必须有对应的本地图片文件,路径不能是网络地址。很多初学者在这里翻车,tabBar配置了图标但图片没放在项目里,编译直接报错。建议初期先不配图标,把文字tab跑通,图表后续再补。每个页面对应一个同名目录,页面内包含index.js、index.wxml、index.wxss、index.json四个文件,这是小程序的标准组织方式。
3.2 登录态管理:从wx.login到自定义token的完整链路
小程序端的登录不能直接拿openid当密码用,正确链路是:wx.login拿到临时code,把code发给后端,后端用code去微信服务器换openid,然后签发一个token返回给前端。前端把token存进wx.setStorageSync,后续每个请求放进header。token过期时后端返回401,前端收到后清理本地token并引导重新登录。
// pages/index/index.js login() { wx.login({ success: (res) => { if (!res.code) { wx.showToast({ title: '获取code失败', icon: 'none' }); return; } wx.getUserProfile({ desc: '用于完善用户资料', success: (profile) => { request('/api/user/login', 'POST', { code: res.code, nickname: profile.userInfo.nickName, avatar: profile.userInfo.avatarUrl }).then((data) => { wx.setStorageSync('token', data.data.token); wx.setStorageSync('userId', data.data.user.id); this.loadQuestions(); }); }, fail: () => { // 用户拒绝授权时用默认昵称创建账号 request('/api/user/login', 'POST', { code: res.code, nickname: '微信用户', avatar: '' }).then((data) => { wx.setStorageSync('token', data.data.token); this.loadQuestions(); }); } }); } }); }这里有个关键细节:wx.getUserProfile必须由用户点击行为触发,不能在onLoad里自动调用,否则会静默失败。拒绝授权是常态,你不应该把整个登录流程卡死在授权弹窗上,拒绝后用一个默认昵称把账号建出来,后续在"我的"页面再引导补全资料,这是微信生态里更稳妥的做法。token的失效时间建议后端设为7天,学生答辩演示那天不需要频繁重新登录。
3.3 答疑交互实现:发布提问、回答列表、点赞收藏
发布提问页的核心是校验+提交。课程下拉选择器用picker组件加载t_course的数据,标题和内容分别做长度校验,标题20字以内、内容500字以内,图片最多三张。这里我踩过坑:因为图片上传是异步的,用户点了发布按钮但图片还没传完,就会出现帖子发出去了图片还在转圈。解决方式是前端先等所有uploadFile完成,再统一提交帖子内容。
// pages/publish/publish.js submitQuestion() { if (!this.data.courseId) { wx.showToast({ title: '请选择课程', icon: 'none' }); return; } const title = this.data.title.trim(); const content = this.data.content.trim(); if (title.length === 0 || content.length === 0) { wx.showToast({ title: '标题和内容不能为空', icon: 'none' }); return; } wx.showLoading({ title: '发布中' }); this.uploadImages().then((urls) => { // uploadImages内部用Promise.all等待所有图片上传完成 return request('/api/question/add', 'POST', { courseId: this.data.courseId, title: title, content: content, imageUrls: urls.join(',') }); }).then(() => { wx.hideLoading(); wx.showToast({ title: '发布成功' }); wx.switchTab({ url: '/pages/index/index' }); }).catch(() => { wx.hideLoading(); wx.showToast({ title: '发布失败', icon: 'none' }); }); }回答列表放在详情页,用scroll-view实现"回答区滚动、底部固定输入框"的布局。每个回答要显示用户昵称、头像、回答时间和"采纳为最佳答案"的标记。点赞收藏我建议合并成一个接口,前端维护一个isFavorite状态,点击后调toggle接口,接口内部先查t_favorite有没有这条记录,有就删并favorite_count减一,没有就插并加一。这个接口不是幂等的,所以前端要加防重复点击,等上一次请求返回再更新按钮状态,否则用户手快点了两次,收藏状态就反了。
4. 前后端联调与本地调试:请求封装、图片上传和真机预览
4.1 请求封装:统一baseURL、token注入和错误码处理
小程序端的wx.request只是个基础API,直接在每个页面里写会出现大量重复代码,而且错误处理分散。我习惯先封装一个request函数,集中处理三件事:拼接baseURL、从storage里取token塞进header、统一处理HTTP状态码和业务状态码。
// utils/request.js const BASE_URL = 'http://192.168.1.100:8080'; // 改成你电脑的局域网IP function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method, data: data, timeout: 10000, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode === 401) { wx.removeStorageSync('token'); wx.showToast({ title: '登录已过期,请重新登录', icon: 'none' }); reject(res); return; } if (res.data.code === 200) { 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, BASE_URL };BASE_URL这里值得多说一句:开发阶段直接用局域网IP加端口,比如192.168.1.100:8080,手机和电脑连同一个WiFi就能访问。性能调优方面,timeout设成10秒比较合理,答疑列表接口正常响应在300毫秒以内,如果超过3秒基本是后端SQL慢查询或网络不通,不要傻等。所有业务接口约定统一返回格式{code, msg, data},code为200表示成功,前端只认这一个值,后端不要把"成功"散装成不同的字符串。
4.2 图片上传:从wx.chooseMedia到后端接收保存
提问带图是答疑场景的刚需,学生在问代码报错时直接截屏贴图,比纯文字描述高效得多。小程序端选图用wx.chooseMedia替代老旧的wx.chooseImage,返回的tempFilePath就是可以传给后端的位置。上传接口用wx.uploadFile,注意它的header和request的header是独立的,token要重新塞一次。
uploadImages() { const tasks = this.data.tempFiles.map((file) => { return new Promise((resolve, reject) => { wx.uploadFile({ url: BASE_URL + '/api/upload/image', filePath: file.tempFilePath, name: 'file', header: { 'token': wx.getStorageSync('token') || '' }, success: (res) => { const data = JSON.parse(res.data); if (data.code === 200) { resolve(data.data.url); } else { reject(new Error(data.msg)); } }, fail: reject }); }); }); return Promise.all(tasks); }Promise.all的用法是关键:三张图片并发上传,全部成功才走then,一旦有一张失败就整体拒绝,这样就不会出现"帖子提交成功但图片缺一半"的问题。后端接收图片有一个SSM下的特定配置坑,SpringMVC的xml里必须显式声明CommonsMultipartResolver,否则MultipartFile参数永远是null,这个问题在Spring Boot里几乎不会遇到,但在SSM里你绕不开。
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="5242880"/> <property name="maxInMemorySize" value="1048576"/> <property name="defaultEncoding" value="UTF-8"/> </bean>maxUploadSize设为5MB,对应一张手机照片的合理上限。图片保存路径别放在src目录里,打包战争时会被清掉,我在项目根目录下建一个upload目录,然后配置一个静态资源映射,让http://ip:8080/upload/xxx.jpg直接可访问,小程序端的image组件src指向这个完整URL。
4.3 真机调试:同一局域网下让手机访问到本机后端
开发工具里一切正常,一扫码到真机就"无法连接服务器",这是联调阶段最高频的翻车现场。原因有两个层面:第一,开发工具的模拟器里localhost指向你电脑,但手机上的localhost指向手机自己,必须写局域网IP;第二,微信对小程序请求的域名有白名单校验,http加IP天然不合法,所以开发阶段必须勾选工具的"不校验合法域名"选项。
正确做法是:电脑和手机连同一个WiFi,查一下电脑的局域网IP,在Windows下用ipconfig,Mac下用ifconfig,然后把request.js里的BASE_URL改成这个IP。开发者工具右上角"详情-本地设置"里勾选"不校验合法域名"。手机端不要用"预览"生成二维码,用"真机调试",它会自动拉起一个调试白名单,大多数情况下能绕过域名校验。这一步你第一次做会觉得很玄学,其实原理就一句话:微信只拦正式版的域名,开发版加上调试模式放行。
还有一个很容易忽略的点:Windows防火墙默认拦截外部访问8080端口。后端明明启动了,手机却连不上,通过浏览器在手机上访问http://IP:8080也打不开,那就是防火墙的事。在Windows防火墙里放行Java进程或8080端口,或者直接用管理员命令行执行netsh advfirewall firewall add rule name="java8080" dir=in action=allow protocol=TCP localport=8080,这是最快的一招。
5. 部署与踩坑:数据库连不上、域名不合法、答辩翻车的五连排
5.1 MySQL连不上的三张面孔:sock文件、密码权限、连接串缺失
现象:后端一启动就报错,日志里出现"Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)",或者"Access denied for user 'root'@'localhost'",还有一种报"Unknown database 'course_qa_db'"。这三个错误长得不一样,原因也各不同,但都集中在你第一次配数据库环境的那半小时里。
原因:第一个是MySQL服务根本没启动,或者JDBC URL里的地址写成了localhost,而Linux下MySQL默认走unix socket,你的Java进程找不到socket文件;第二个是root账号密码不对,或者密码对了但host不匹配;第三个是数据库没建,或者建库脚本根本没执行成功。
解决:先确认MySQL在跑,Ubuntu下用systemctl status mysql,Windows下看服务列表。JDBC URL里的localhost改成127.0.0.1,强制走TCP。权限问题不要纠结,直接新建一个专用账号比折腾root更省事:
CREATE USER 'qa_user'@'%' IDENTIFIED BY 'Qa123456!'; GRANT ALL PRIVILEGES ON course_qa_db.* TO 'qa_user'@'%'; FLUSH PRIVILEGES;5.2 连接池空闲超时断线的早死问题
现象:项目早上启动时一切正常,学生登录、提问、回答都流畅,中午休息一小时回来,再点列表页就报"Communications link failure. The last packet successfully received from the server was 8,000 milliseconds ago."
原因:MySQL服务端的wait_timeout默认是8小时,但实际上你的电脑可能休眠或断网,连接池里的连接被服务端断开,而MyBatis池里的连接对象还认为自己是活的,拿去用才发现底下已经断气。这种问题在演示当天特别致命,因为它只在"闲置一段时间"后出现,开发时根本注意不到。
解决:两处配置并行做。JDBC连接串加autoReconnect=true,同时把连接池的空闲检测打开,C3P0或DBCP都有testWhileIdle类的参数。
jdbc.url=jdbc:mysql://127.0.0.1:3306/course_qa_db?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true jdbc.maxIdleTime=1800 jdbc.validationQuery=SELECT 1 jdbc.testConnectionOnCheckout=true5.3 小程序合法域名拦截:开发版和正式版是两套规则
现象:开发者工具里接口全部返回正常,手机扫码预览后,所有请求都失败,调试器报"不在以下request合法域名列表中"。
原因:微信小程序有个安全策略,真机上运行的小程序只能请求在mp后台配置过的HTTPS域名,默认的http局域网IP坚决不放行。开发者工具是开发态,能勾选关闭校验,但真机是另一个执行环境。
解决:开发阶段用"真机调试"模式扫调试码;如果项目要走完整上线流程,就需要备案域名、配HTTPS证书,在微信公众平台的小程序后台把域名加进request合法域名列表,而且域名不能带端口。最容易被忽略的是:同一个域名如果同时用于图片上传,还要在"uploadFile合法域名"里单独配置,这两个白名单是分开的。
5.4 emoji和表情包入库变问号的编码血泪
现象:学生在提问内容里输入😀这个表情,后端不报错,但数据库里存的是两个问号,或者直接抛"Incorrect string value: '\xF0\x9F\x98\x80' for column"。
原因:MySQL的utf8字符集一个字符最多3字节,而emoji是4字节,只有utf8mb4才支持。建库时如果没指定utf8mb4,默认就是utf8,哪怕你在JDBC连接串里写了characterEncoding=utf8也没用。
解决:建库脚本把字符集写成utf8mb4,已经建错的库要转换。
ALTER DATABASE course_qa_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE t_question CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE t_answer CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;同时JDBC连接串必须同步写characterEncoding=utf8mb4,MySQL驱动版本不要太老,5.1.47以上的驱动才认这个参数。检查这三个地方——库、表、连接串——全部齐了才彻底根治。
5.5 答辩现场端到端演示的兜底预案
现象:答辩当天站在讲台前,双击项目启动,IDEA编译报错、MySQL没启动、端口被占用,或者手机连着教室的WiFi访问不到电脑上的后端,演示必然翻车。
原因:毕业设计演示的环境与本机开发环境高度耦合,电脑休眠恢复后DHCP把局域网IP换了,MySQL开机不自启,IDEA的Maven依赖第一次加载失败,这些都是概率事件,叠加起来概率就很高。
解决:演示前写一个启动脚本,按顺序执行MySQL服务启动、Tomcat启动、打开浏览器。把整段演示过程提前录成视频存在手机相册里,后端挂了直接放视频,这是最靠谱的后悔药。另外把JDBC配置里的localhost全部改成127.0.0.1,少一次DNS解析就少一次故障可能。答辩前至少做两次完整的"冷启动演练"——关掉所有东西,从零开始启动一遍,你会发现很多平时没暴露的问题。
6. 答辩加分项:给答疑小程序加三个超出课程设计的优化
6.1 发布提问的防重复提交
答辩展示时可以主动说"我对发布接口做了防重复提交处理"。实现是前端在点击发布后把按钮置灰,再配合后端拦截同一个用户5秒内不能连续创建两篇内容相同的帖子。这个小功能实现成本低,但体现的是并发安全意识。
6.2 答疑列表的本地缓存加速
首页的答疑列表接口在真机上每次加载要1秒左右,把列表数据缓存到wx.setStorageSync,设置5分钟有效期,用户二次进入直接渲染缓存,后台再静默刷新。这一步能让演示时的"返回首页"操作明显变快,老师体验到的流畅感是真实的。
6.3 用一张图表收尾答辩
引入小程序端的ec-canvas组件,在"我的"页面做一个近7天答疑趋势的柱状图。后端加一个按日期分组的统计接口,SELECT DATE(create_time), COUNT(*) FROM t_question GROUP BY DATE(create_time)。这张图放在答辩PPT的最后一页,比文字更能说明你的系统有数据积累。
我带毕设这几年最深的体会是:代码量从来不是答辩好坏的标准,真正拉开差距的是"闭环是否流畅"和"异常是否有人处理过"。所有能自动化收尾的细节提前跑一遍,把最容易被追问的边界问题在代码里留好注释。希望帮到你。
本文还有配套的精品资源,点击获取