简介:这是一份面向计算机专业学生毕业设计与期末大作业的教学辅助微信小程序项目,基于SSM框架实现教学场景应用。资源包内含完整源码、论文、PPT、数据库文档及开发说明,源码均经本地编译调试可运行,适合需要参考完整项目结构、学习微信小程序与SSM整合开发的学习者。压缩包共1306个文件,以png图片、java后端代码、js逻辑、vue组件以及wxss/wxml小程序页面文件为主,另有sql数据库脚本与doc/pptx文档,整体59.61MB,目录结构清晰,便于按模块查阅。通过阅读源码可掌握项目搭建、教学功能模块实现及业务逻辑处理;论文、PPT与数据库文档则覆盖需求分析、系统设计、数据表结构和展示思路,帮助读者将理论付诸实践。已有50人学习下载,可作为课程设计、项目实战练手和求职作品的重要参考。
1. 教学辅助微信小程序+SSM:这类毕设包里到底有什么,为什么值得自己跑一遍
把一套“教学辅助微信小程序+SSM”项目包从解压到真正在微信开发者工具里跑通,大多数人会花掉两三个晚上。这个组合本身不复杂:前端是微信小程序,后端是SSM的Java Web接口,数据落在MySQL。真正卡人的不是算法,而是小程序请求、后端端口、数据库字符集、Tomcat版本这一整条链路里处处有坑。标题里带了源码、论文、PPT、数据库文档、说明文档,意味着它试图给你一条从需求到实现再到文档的完整路径。这套东西适合三类人:正在做毕业设计的学生、想用Java技术栈接一个真实小程序的从业者,以及需要快速搭建教学辅助场景原型的开发者。直接拿包去交通常会翻车,但花两天把它跑通、改透,价值远大于重新造轮子。
2. 拆开这套系统的三层结构:微信小程序、SSM、MySQL 各自扛什么活
2.1 技术选型:教学辅助场景为什么绕不开 微信小程序 + SSM
教学辅助类业务,无论是高校的课程资料下发、作业提交、课堂签到,还是培训机构的讲义管理,本质都落在一组数据操作上:用户注册登录、课程查询、文件上传下载、作业记录、签到打卡。这类业务对并发要求不高,对事务要求集中在作业提交和成绩录入,用SSM这套经典组合完全够用。Spring负责业务对象和事务,SpringMVC负责把小程序发来的JSON请求路由到具体方法,MyBatis负责把Java对象映射成SQL。三者组合起来,是高校课程设计里最常见的Java后端样板。
那为什么不是Spring Boot?答案是:如果课题或项目标题写的是“基于SSM”,评审默认的参考系就是Spring+SpringMVC+MyBatis。Spring Boot当然开发效率更高,但在这类项目包的语境下,把SSM配置文件读明白,反而能在答辩时讲清楚“请求是怎么进去的、SQL是怎么查的”。下表是常见选型差异:
| 对比项 | SSM(Spring+SpringMVC+MyBatis) | Spring Boot |
|---|---|---|
| 配置方式 | XML + 注解混用,配置可见 | 自动配置为主,默认值多 |
| 入门曲线 | 偏陡,但要讲原理容易 | 上手快,黑匣子多 |
| 毕设/课程设计认可度 | 经典,参考书多 | 也行,但更偏企业 |
| 启动部署 | 打war包丢Tomcat | 内嵌Tomcat,jar直接跑 |
小程序端选原生而不是uniapp,通常也是出于同样的考量。原生小程序项目直接导入微信开发者工具就能调试,WXML、WXSS、JS的写法贴合官方文档;uniapp打包虽然能一套代码跑多端,但导入后多一层构建,改动页面还要重新编译,在这个项目里属于给自己加活。后面第4章会专门说前端结构和加载更多分页的实现。
2.2 数据库设计:用户、课程、作业、签到四类表怎么落
数据库是整个项目包里最好验证、也最容易看出水平的部分。拿到MySQL文件后不要急着导入,先对着ER图看清关系。教学辅助系统最常见的表划分是:用户表(学生/教师用role区分)、课程表、选课关联表、课程资料表、作业表、作业提交表、签到表、签到记录表。消息通知表可加可不加,看需求文档有没有“公告”模块。
我用MySQL 8语法给一个简化的建表样例,核心字段和索引都写在注释里:
CREATE DATABASE IF NOT EXISTS teaching_assistant DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE teaching_assistant; CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', username VARCHAR(64) NOT NULL UNIQUE COMMENT '学号/工号', password VARCHAR(128) NOT NULL COMMENT '加密后的密码', real_name VARCHAR(64) NOT NULL COMMENT '真实姓名', role TINYINT NOT NULL COMMENT '1学生 2教师 3管理员', avatar_url VARCHAR(255) DEFAULT '' COMMENT '头像URL', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', INDEX idx_role (role) ) COMMENT '用户表'; CREATE TABLE course ( id BIGINT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(128) NOT NULL, teacher_id BIGINT NOT NULL COMMENT '归属教师sys_user.id', course_desc TEXT COMMENT '课程简介', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_teacher (teacher_id) ) COMMENT '课程表'; CREATE TABLE course_student ( id BIGINT AUTO_INCREMENT PRIMARY KEY, course_id BIGINT NOT NULL, student_id BIGINT NOT NULL, UNIQUE KEY uk_course_student (course_id, student_id) ) COMMENT '选课关联表'; CREATE TABLE course_material ( id BIGINT AUTO_INCREMENT PRIMARY KEY, course_id BIGINT NOT NULL, title VARCHAR(128) NOT NULL COMMENT '资料标题', file_url VARCHAR(255) NOT NULL COMMENT '文件相对路径或对象存储URL', file_size INT DEFAULT 0 COMMENT '字节数', download_count INT DEFAULT 0 COMMENT '下载次数,统计用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_course (course_id) ) COMMENT '课程资料表';这段建表脚本有三个关键点。第一,字符集直接在建库时就指定utf8mb4,避免后续中文乱码,这在第5章会重点排查。第二,外键我没有物理添加,而是用逻辑关联,比如course.teacher_id指向sys_user.id,这是项目包里的常见做法,因为物理外键在分页查询和批量导入时反而碍事。第三,下载计数单独开download_count字段,避免每次统计都count一遍。
做数据库增删改查时,SSM合集的通用顺序是:先在sys_user里做登录验证,再根据role取课程列表,再通过course_student确认“这个学生确实选了这门课”,最后才能去查course_material或提交作业。这个权限链路通了,后面Controller和Mapper的代码才写得顺。
2.3 项目包里的文档怎么和源码对应着读
拿到压缩包后,不要先看源码,容易看晕。项目包里的文档通常按用途分成六类,和代码的对应关系是:
| 文档 | 内容 | 对应代码模块 |
|---|---|---|
| 说明文档 | 环境版本、导入步骤、账号密码 | 全局配置,jdbc.properties、app.json |
| 数据库文档 | 建表SQL、ER图、数据字典 | 数据库脚本、Mapper XML |
| 论文/设计文档 | 需求分析、系统设计、功能实现、测试 | 全部模块的书面化描述 |
| PPT | 答辩用梗概 | 论文的缩减版 |
| 源码 | Java后端 + 小程序前端 | 全部 |
| 数据库文件 | 含样例数据的sql或db文件 | 数据层 |
我的阅读顺序建议是:说明文档排在最前,确认JDK、Tomcat、MySQL版本,然后用Navicat或命令行跑数据库脚本,再启动后端,最后才打开微信开发者工具。如果你一上来就点开小程序代码,大概率会被appid、baseUrl这些配置绊住,分不清是代码问题还是环境问题。版本问题往后放,第5章专门讲。明白这三层结构之后,下面两章直接落到怎么把后端接口和前端页面跑通。
3. SSM后端从配置到接口:登录态、分页查询和常用注解落地
3.1 SSM整合的配置逻辑:三条线最后在web.xml汇合
SSM不是单一框架,而是三个框架的整合方案。Spring管对象和事务,SpringMVC管HTTP请求分发,MyBatis管SQL映射。三者通过配置文件串起来,最常见的目录结构是:web.xml定义DispatcherServlet和ContextLoaderListener;spring-mvc.xml扫描Controller;spring-dao.xml扫描Service和Mapper。下面这份配置是这个组合里最常见的样子:
<!-- spring-dao.xml 关键片段 --> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="com.example.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.dao"/> </bean>对应的jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/teaching_assistant?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456这里有三处最容易踩坑。driverClassName在MySQL 5.7和8.0之间不一样,8.0必须是com.mysql.cj.jdbc.Driver,老包里的com.mysql.jdbc.Driver在新驱动下直接报ClassNotFoundException;jdbc.url里必须带serverTimezone,否则报时区异常;Druid连接池的版本别混用,druid-1.1.x和1.2.x某些配置项有差别。如果项目包给的是c3p0,原理一样,关键还是那三个属性。
3.2 Controller接口设计:统一返回体、登录注解和SSM常用注解
小程序端发过来的请求,和网页端最大的区别是:几乎没有表单跳转,全部是wx.request发出的JSON。所以Controller层要做的事很固定——接收JSON参数,调用Service,返回统一结构的JSON。下面这个返回体是这类项目包最常见的代码:
public class Result<T> { private Integer code; // 0成功 1业务失败 401未登录 500系统异常 private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.msg = "success"; r.data = data; return r; } public static <T> Result<T> error(Integer code, String msg) { Result<T> r = new Result<>(); r.code = code; r.msg = msg; return r; } }Controller的写法直接影响小程序端解析。code含义固定下来,前端request封装就按这个来判断,不要有的接口返回success、有的返回status,否则后续改动面会非常大。下面是一个课程接口的Controller示例,覆盖了登录校验、请求参数接收、分页参数:
@RestController @RequestMapping("/api/course") public class CourseController { @Autowired private CourseService courseService; @GetMapping("/list") public Result<PageResult<CourseVO>> list( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { PageResult<CourseVO> page = courseService.pageQuery(pageNum, pageSize, keyword); return Result.ok(page); } @PostMapping("/submit") public Result<Void> submitHomework(@RequestBody HomeworkSubmitDTO dto) { courseService.submitHomework(dto); return Result.ok(null); } }SSM常用注解都在这个例子里露面了。@RestController把Controller对象交给SpringMVC管理并统一走JSON序列化;@RequestMapping定义类级路由,@GetMapping和@PostMapping是它的方法级子注解,分别对应wx.request的GET和POST;@RequestParam负责取URL查询参数,defaultValue保证了pageNum/pageSize不会因为前端漏传而变成null;@RequestBody回填POST体里的JSON;@Autowired做依赖注入。再往后是@Service标注业务类、@Transactional标注事务方法,这两者声明在Service层而不是Controller层。
3.3 MyBatis动态SQL与分页查询:只查当前页的数据
分页是第一高频需求。课程列表、作业列表、签到记录都是列表页,小程序端向上滑动就触发加载更多。手写分页比引入PageHelper插件更直观,也更容易在答辩时讲清楚。核心是两条SQL:一条count出总数,一条limit出当前页数据。MyBatis的Mapper XML里用动态SQL处理可选条件:
<select id="countMaterial" resultType="int"> SELECT COUNT(*) FROM course_material <where> <if test="courseId != null"> AND course_id = #{courseId} </if> <if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if> </where> </select> <select id="selectMaterialPage" resultType="com.example.entity.CourseMaterial"> SELECT * FROM course_material <where> <if test="courseId != null"> AND course_id = #{courseId} </if> <if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>参数这块要特别小心。#{offset}和#{pageSize}传进来之前,Service层要先把pageNum转换成offset,公式是(pageNum - 1) * pageSize,不要在Mapper里做乘法,MyBatis不会帮你算。另外<where>标签会把多余的AND去掉,这是MyBatis动态SQL最常见的写法;LIKE CONCAT('%', #{keyword}, '%')是为了防止参数直接拼进SQL的注入风险,不要学网上老写法写LIKE '%${keyword}%',那样等于把SQL注入口子打开了。
最后补一句数据库增删改查的走法。查询走上面这种带<where>的复杂SQL,新增和更新则保持简单:insert语句字段对齐DTO,update语句必须带<set>动态标签防止覆盖空值,删除用逻辑删除的时候update语句把deleted字段置1,而不是真的执行delete。事务方法记得在Service类上或方法上加@Transactional,比如作业提交时要同时写homework_submit和更新homework的提交人数,这两个操作不能一个成功一个失败。
4. 微信小程序端从零跑通:request封装、登录和列表加载更多
4.1 原生微信开发者工具 vs uniapp打包:为什么这类项目包默认原生
小程序前端在项目包里几乎都是原生写法。打开微信开发者工具,选择“导入项目”,目录要定位到包含app.json的那一层,而不是最外层文件夹。如果导入后一片空白,大概率就是目录层级选错。原生小程序的关键文件就这么几个:app.json管全局页面注册和窗口样式,app.js管全局生命周期,project.config.json管appid和编译设置。三者里app.json最容易出问题,每写一个页面都要在这里登记,漏了就报页面找不到。
| 对比项 | 原生微信小程序 | uniapp 微信小程序打包 |
|---|---|---|
| 开发工具 | 微信开发者工具直接跑 | HBuilderX/CLI转译后再打开 |
| 请求API | wx.request | uni.request,经转译 |
| 包体积 | 接近编译后体积 | 带框架运行时,偏大 |
| 调试体验 | 直接看WXML和请求 | 要过一层sourcemap |
uniapp的优势是多端复用,但在这个项目里,评审看的是“微信小程序开发”能力。原生写法下API调用、缓存、生命周期都直接用官方能力,讲起来也更痛快。项目包里如果混了uniapp代码,需要先npm install再打包,部署链会多一环,通常不是首选。
4.2 封装request.js:统一请求入口,别在页面里到处写wx.request
页面里直接写wx.request不是不行,但几十个页面都这么写,后端改个baseUrl就要全局替换。常见的做法是把wx.request包一层Promise,集中处理登录态、HTTP状态码和业务码。下面这段代码可以直接放进utils/request.js:
// utils/request.js const BASE_URL = 'http://127.0.0.1:8080'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'Content-Type': 'application/json', 'token': token ? token : '' }, success(res) { const body = res.data; if (body.code === 0) { // 业务成功 resolve(body.data); } else if (body.code === 401) { // 未登录,跳登录页 wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(new Error('未登录')); } else { wx.showToast({ title: body.msg, icon: 'none' }); reject(new Error(body.msg)); } }, fail(err) { wx.showToast({ title: '网络请求失败', icon: 'none' }); reject(err); } }); }); } module.exports = { request, BASE_URL };注意BASE_URL用的是127.0.0.1而不是localhost,原因是小程序开发者工具里两者虽然都能转回本机,但某些代理环境下localhost会被解析成IPv6,导致连接被拒,换成127.0.0.1少一个变量。token从storage里取,放进header的token字段,后端拦截器校验;401统一在这里处理,页面代码就干净很多。现在页面里调用就变成:
const { request } = require('../../utils/request'); request('/api/course/list?pageNum=1&pageSize=10', 'GET') .then(data => console.log(data)) .catch(() => {});加一点参数说明:BASE_URL要跟后端Tomcat启动后的实际端口一致,项目包说明文档里一般会写,如果后端改了端口号先改这里;wx.getStorageSync的key必须和后端登录接口写入的key一致;fail分支里我通常还加wx.hideLoading(),防止分页加载时loading一直转。
注意:开发阶段必须在微信开发者工具右上角“详情→本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,否则所有 http://127.0.0.1 的请求都会被拦下来,报“URL 不在合法域名列表中”。
这一步不做,后端跑得再好,小程序里也全是请求失败,别在这上面浪费时间。
4.3 课程列表与加载更多:onPullDownRefresh和onReachBottom的配合
列表加载更多是微信小程序页面里最常复用的交互,也是“微信小程序页面列表加载更多”对应的具体场景。课程资料列表页的要点是:数据分页游标由前端持有,data里维护pageNum、pageSize、list、hasMore、loading五个字段,onReachBottom触底判断要等上一次请求完成再发下一次。下面是一个可以直接套用的页面js骨架:
Page({ data: { courseId: null, pageNum: 1, pageSize: 10, hasMore: true, loading: false, materialList: [] }, onLoad(options) { this.setData({ courseId: options.courseId }); this.loadList(true); }, onPullDownRefresh() { this.setData({ pageNum: 1, hasMore: true }); this.loadList(true).then(() => wx.stopPullDownRefresh()); }, onReachBottom() { if (!this.data.hasMore || this.data.loading) return; this.setData({ pageNum: this.data.pageNum + 1 }); this.loadList(false); }, loadList(reset = false) { if (this.data.loading) return Promise.resolve(); this.setData({ loading: true }); const { courseId, pageNum, pageSize } = this.data; return request(`/api/material/list?courseId=${courseId}&pageNum=${pageNum}&pageSize=${pageSize}`, 'GET') .then(data => { const newList = reset ? data.records : this.data.materialList.concat(data.records); this.setData({ materialList: newList, hasMore: data.records.length === pageSize, loading: false }); }) .catch(() => this.setData({ loading: false })); } });这段代码有三个边界。第一,onPullDownRefresh重置pageNum为1,但要用hasMore保证触底后不再追加老数据。第二,data.records.length === pageSize这个判断意味着“只要这次返回了满页,就认为后面还有”,如果你的后端在最后一页恰好返回满10条,就会多一次空请求,但返回空数组后hasMore会变成false,属于可接受的代价。第三,loading标志防止onReachBottom在请求还没结束时连续触发多次。
页面json里要记得开两个开关:
{ "enablePullDownRefresh": true, "backgroundTextStyle": "dark" }如果你要用自定义顶部导航栏,微信小程序顶部导航栏高度适配的常见做法是:在app.json里配"navigationStyle": "custom",然后自己拿状态栏高度做适配。用wx.getWindowInfo()取statusBarHeight,给顶部view加padding-top,胶囊按钮高度和顶栏高度都在这个接口里能拿到。用系统默认导航栏就可以跳过这一步,多数教学辅助小程序用默认导航栏就够。
5. 从开发工具到真机预览:5个高频踩坑与排查记录
5.1 请求失败:URL 不在合法域名列表中
现象:后端Tomcat已经起来了,浏览器直接访问接口有返回,小程序端一请求就报url not in domain list,请求状态码404或直接失败。
原因:微信小程序对网络请求有域名白名单校验,开发工具默认不放开。项目包里给的说明文档如果没强调这一步,新手基本都会卡在这里。
解决:开发阶段在微信开发者工具里点“详情→本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。如果要在真机预览,把本地服务器映射成局域网IP,保证手机和电脑在同一WiFi下,后端打印实际监听IP而不是127.0.0.1。上线阶段则必须去小程序后台配置服务器域名,且要求HTTPS,这时需要给后端域名配SSL证书,并把request.js里的BASE_URL改成正式域名。如果你还要从公众号H5跳进小程序,那要走的是另一条链路,去配业务域名校验和跳转接口,别和这条混在一起排错。
5.2 登录态失效:code2session返回错误或频繁过期
现象:小程序启动后第一次请求接口成功,过几分钟再操作就跳回登录页;或者后端日志里出现code2Session相关报错。
原因:最常见三种。appid和secret不匹配,小程序后台的应用标识和后端代码里配置的常量对不上;后端把session存在了内存里,Tomcat一重启session全丢;前端只在用户主动登录时调wx.login,没有做启动时静默登录。
解决:先核对appid/secret,这个不一致百分百拿不到openid。然后把session缓存放到Redis或数据库里,至少要放在一个不随Tomcat重启丢失的地方。前端在app.js的onLaunch里调一次wx.login拿code,发给后端换登录态,后端返回自定义token,前端存到storage里。token有效期建议设7天,后续请求都通过header里的token字段校验,别再每次调code2session,微信接口有频控。
5.3 数据库中文乱码:连接串、建库、建表三层都要看
现象:后端接口返回的课程名变成问号或乱码字符,但数据库客户端里看又是正常中文。这种问题最头疼,因为数据库对、代码对,就是接口输出不对。
原因:乱码通常是三层里某一层字符集不一致。jdbc.url里没加characterEncoding=utf8;MySQL建库时用了默认的latin1;表或字段是utf8但客户端写入用了gbk。项目包自带的sql脚本如果是从别处导出的,第一行SET NAMES和库字符集往往对不上。
解决:按顺序排查。先改jdbc.url,加上useUnicode=true&characterEncoding=utf8;再改库和表:
ALTER DATABASE teaching_assistant CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE course_material CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;重新导入脚本时用命令行,避免图形工具隐藏字符集差异:
mysql -uroot -p --default-character-set=utf8mb4 teaching_assistant < teaching_assistant.sql改完之后重启Tomcat再测。还有一个坑:连接池连接在改库字符集后不会自动刷新,Tomcat必须彻底重启,不要只reload应用。
5.4 后端启动失败:JDK、Tomcat、MySQL驱动的版本匹配不是玄学
现象:Tomcat启动报ClassNotFoundException或Unable to load class,或者起来之后一访问接口就抛SQLException,日志指向Communications link failure。
原因:版本不匹配的组合太常见了。JDK 17配老版本Tomcat 8;MySQL 8的库用mysql-connector-java 5.x驱动;druid和mysql驱动版本互相冲突。这些报错第一次看觉得像玄学,其实全是版本问题。
解决:这类项目包我一般会先看pom.xml和说明文档,确认三个版本:JDK版本、Tomcat版本、MySQL驱动版本。比较稳的组合是JDK 8 + Tomcat 8.5/9.0 + mysql-connector-java 8.0.x。如果本机装的是新版JDK,建议直接装一个JDK 8并让IDE用这个JDK启动,不要在代码层面绕过,否则Tomcat反射机制报错很隐蔽。驱动类名按第3章写的,改成com.mysql.cj.jdbc.Driver。还有一类情况是jar包重复,Tomcat的lib里和WEB-INF/lib里各放了一份相同依赖,启动时类加载顺序会把老版本优先加载,删掉一份再试。
5.5 真机预览里图片加载不出来、文件下载404
现象:开发工具里课程资料封面显示正常,手机一扫码预览全部裂图;或者点击下载按钮提示404。
原因:项目包里数据库的file_url字段存的是/upload/avatar.jpg这种相对路径,或者直接写死http://127.0.0.1:8080/upload/...。开发工具把本地回环地址当本机处理,所以能显示;真机上这段URL指向的是学生自己的手机,自然404。
解决:统一改成后端完整URL拼相对路径的方式。接口返回file_url时,如果存的是相对路径,后端组装完整地址时用当前请求的host动态生成,而不是写死IP。小程序端显示图片时再用一个公共方法来拼接BASE_URL。如果图片量再大一点,可以放到MinIO或云对象存储,数据库里只存object key,这样不用等后端重启也能换域名。这一点在答辩时被问到的概率非常高,值得提前把方案想清楚。
6. 让这套项目变成你自己的:论文重写、PPT改造与核心功能验证清单
6.1 论文和PPT怎么改才不像直接下载的
项目包里带论文和PPT,最容易犯的错是只改封面就交。评审老师未必能看出源码是否原创,但很容易看出论文是不是套模板。先把目录结构重排:需求分析、系统设计、数据库设计、功能实现、测试,每个章节都按自己实际跑通的代码写。截图全部重截,用自己改过的页面和数据结构。重点章节比如系统架构图、时序图,用Visio或draw.io重画。PPT按“背景→技术选型→系统设计→功能演示→测试→结束页”推进,删掉原包里的水印和无关素材。答辩时真正被问的,是数据库为什么这么设计、分页怎么做,不是项目能不能跑。所以第3章和第4章的代码要能不看稿讲出来。
6.2 数据库文档怎么整理:导SQL、画ER图、补数据字典
数据库文档常见内容有建表SQL、ER图、数据字典和说明。我拿到手会先做三件事。第一,用Navicat对数据库反向生成ER图,确认表间关系是否和论文里的设计图一致。第二,按实际表结构重做数据字典,字段名、类型、是否为空、注释都要和代码对得上,表格格式固定为字段名、数据类型、主键、非空、默认值、注释六列。第三,检查是否需要ALTER TABLE调整表结构,比如course_material的course_id经常出现在where条件里就加上索引。数据库文档改完,论文里的数据库设计章节基本也就成了。
6.3 一个下午跑完的验证清单
| 功能模块 | 操作 | 验证点 |
|---|---|---|
| 登录 | 学生/教师账号登录 | 拿到token,storage里有值 |
| 课程列表 | 首页下拉刷新 | 分页正常,无重复数据 |
| 资料下载 | 点击下载按钮 | 微信能打开预览 |
| 作业提交 | 学生端提交一份作业 | 数据库homework_submit表多一条记录 |
前端用开发者工具的Network面板逐个看请求,不需要额外装抓包工具,请求里能看到是否带token、分页参数是否正确、返回体code是否为0。真机预览至少跑一遍登录、列表、下载这三个核心链路。如果时间还够,把小程序上传为体验版,生成二维码发给两三个同学让他们用几天,体验版二维码比开发工具的预览二维码稳定,适合收集几天试用反馈。我的血泪经验是:提交前一周再跑一遍完整流程,因为Tomcat、MySQL或者本地文件路径任何一个变了,都会让之前明明跑通过的项目翻车。这个方案值不值得做,我的答案是:只要你最后能把这套代码讲清楚,它就值得;如果只是解压交差,那再好的包都帮不了你。希望帮到你。
本文还有配套的精品资源,点击获取