多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java+微信小程序毕设实战:从选型到部署的完整落地指南

Java+微信小程序毕设实战:从选型到部署的完整落地指南 简介本资源是一套面向计算机专业本科生的毕业设计实战项目聚焦绘画学习场景提供基于微信小程序的完整Java开源解决方案。项目采用WXML/WXSS/JS开发前端Spring Boot构建后端服务MySQL存储用户、课程与作品数据适配微信生态具备免安装、低内存占用、响应迅速等轻量化优势适合教学实践、课程设计及求职作品集搭建。压缩包共1409个文件含143个Java后端逻辑文件、216个JS交互脚本、152个Vue组件、114个WXML页面结构及116个WXSS样式文件辅以SQL建表语句、开题报告、论文、PPT答辩稿与详细使用说明总大小18.25MB。目前已有106人学习下载内容覆盖系统分析、数据库设计、前后端联调与测试全流程目录结构规范关键模块如用户中心、课程管理、画作上传与评论功能均完整可运行开箱即用。1. 这不是又一个“免费源码”搬运工微信小程序绘画学习平台为什么用 Java 做后端反而更稳、更易毕设答辩通过你搜“微信小程序 毕业设计”页面刷出来全是“含数据库论文ppt源码”的打包广告——点开一看90% 是 Node.js 或 PHP 写的轻量后台甚至直接用云开发硬扛用户上传和课程管理剩下那 10%才是真正敢用 Java 搭整套服务端的同学。但问题来了Java 后端配微信小程序前端真能跑通部署难不难答辩老师问“为什么不用 Spring Boot 而用传统 Servlet”你怎么答这个标题里的“基于微信小程序的绘画学习平台”核心不是炫技而是用 Java 技术栈落地一个真实可交互、有用户行为闭环注册→学课→交作业→教师批阅→作品展示、且能经得起答辩追问的毕业系统。它不追求高并发但必须逻辑清晰、分层合理、数据库设计规范、接口可测、日志可查——这才是导师眼里“工作量扎实、技术路线合理、代码有结构”的关键。我带过 17 届本科生做类似选题最后能顺利答辩、被评优的83% 都是 Java MySQL 微信小程序三件套原因很简单Java 工程化强、调试工具链成熟、Spring Boot 自动装配MyBatis-Plus CRUD 效率高且所有环节都有标准答案可查——比如登录态怎么跟微信 openid 绑定、作业图片怎么存、教师端如何批量下载学生作品 PDF……这些不是玄学是教科书级的 Web 开发实践。下面我们就从零开始把这套“能跑、能讲、能改、能交”的方案拆透。2. 后端选型与工程搭建为什么用 Spring Boot 2.7.x MyBatis-Plus 而不是 Spring Cloud 或纯 Servlet2.1 毕设场景下的 Java 后端技术栈取舍逻辑毕业设计不是企业级中台它要满足三个刚性约束① 单机可部署不依赖 Docker/K8s② 接口数量可控50 个核心 API③ 代码可读性强导师能一行行看懂业务逻辑。所以 Spring Cloud 直接排除——服务发现、熔断、网关对毕设是负优化纯 Servlet JDBC 虽然“原生”但 DAO 层重复代码多、事务难管、SQL 易出错答辩时被问“为什么不用 ORM”容易翻车而 Spring Boot 2.7.x注意不是 3.x因 JDK 17 兼容性在部分学校服务器上仍有风险 MyBatis-Plus 的组合恰好卡在黄金平衡点自动配置内嵌 Tomcatjava -jar app.jar一键启动MapperScanTableName让实体类与数据库表一一映射避免手写 XMLLambdaQueryWrapper写条件查询比拼字符串 SQL 更安全、更易 debugPageT分页插件天然支持微信小程序“列表加载更多”场景后面会细讲日志默认集成 Logbackapplication.yml里加两行就能输出 SQL 执行日志答辩演示时可现场打开控制台证明“数据确实查出来了”。提示别用 Spring Boot 3.x。很多高校实验室服务器仍为 CentOS 7 JDK 8/11Spring Boot 3.x 强制要求 JDK 17部署时UnsupportedClassVersionError会让你在答辩前夜崩溃。稳妥起见用spring-boot-starter-parent:2.7.182023 年 10 月 LTS 版本它对 JDK 8/11/17 全兼容。2.2 创建最小可运行工程5 分钟初始化一个带登录校验的后端骨架用 start.spring.io 选 Spring Boot 2.7.x勾选以下依赖✅ Spring Web✅ Spring Boot DevTools开发期热重载✅ Lombok省掉 getter/setter✅ MySQL Driver✅ MyBatis Framework注意选 MyBatis不是 MyBatis-Plus —— 后者需手动引入生成 ZIP 解压后在pom.xml中追加 MyBatis-Plus 和 HikariCP 连接池!-- MyBatis-Plus 核心依赖 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version !-- 适配 Spring Boot 2.7.x 的最新稳定版 -- /dependency !-- HikariCP 替代默认连接池性能更稳 -- dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId /dependency然后在application.yml中配置数据库以本地 MySQL 8.0 为例spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/paint_learn?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_password hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印 SQL global-config: db-config: id-type: assign_id # 使用雪花算法生成 Long 型 ID避免 int 溢出 table-prefix: t_ # 所有表加 t_ 前缀如 t_user → 符合毕设规范创建第一个实体类User.java对应t_user表import com.baomidou.mybatisplus.annotation.*; import lombok.Data; Data TableName(t_user) public class User { TableId(type IdType.ASSIGN_ID) // 雪花 ID private Long id; private String openid; // 微信唯一标识非用户名 private String nickname; private String avatarUrl; private Integer role; // 1-学生, 2-教师, 3-管理员 private Long createTime; }再建一个UserController.java暴露/api/user/login接口供小程序调用RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginDTO dto) { // 1. 校验 code 是否为空小程序 wx.login() 返回 if (StringUtils.isBlank(dto.getCode())) { return Result.fail(code 不能为空); } // 2. 调用微信接口换取 openid关键必须自己实现不能只存前端传来的 openid String openid wechatService.getOpenid(dto.getCode()); if (openid null) { return Result.fail(微信登录失败请重试); } // 3. 查询或创建用户 User user userService.getByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(dto.getNickname()); user.setAvatarUrl(dto.getAvatarUrl()); user.setRole(1); // 默认学生 user.setCreateTime(System.currentTimeMillis()); userService.save(user); } // 4. 生成自定义 token不用 JWT用简单时间戳随机数防重放 String token DigestUtils.md5Hex(openid System.currentTimeMillis() new Random().nextInt(1000)); return Result.success(Map.of(token, token, userInfo, user)); } }逻辑说明这里LoginDTO是一个仅含code、nickname、avatarUrl的 DTO 类用于接收小程序wx.login()后调用wx.getUserProfile()获取的用户信息。关键点在于第 2 步必须用 code 换取 openid这是微信安全机制——前端传来的 openid 可伪造只有后端用 code 向https://api.weixin.qq.com/sns/jscode2session请求才能拿到真实 openid。参数说明code是临时登录凭证有效期 5 分钟appid和secret需填入你小程序后台的配置这部分要写进application.yml的wechat.appid和wechat.secret并在WechatService中读取使用。3. 数据库设计与关键表关系为什么 t_lesson、t_work、t_comment 必须用逻辑外键而非物理外键3.1 毕设数据库的 4 张核心表及其设计依据毕业设计数据库不是生产环境不必追求极致范式但必须体现数据完整性意识和业务可追溯性。我们只建 4 张表足够支撑绘画学习平台主流程全部采用t_前缀 英文单数命名如t_user而非t_users符合高校数据库课程规范表名字段精简设计要点为什么这么设t_userid,openid,nickname,avatar_url,role,create_timeopenid为唯一索引role用 tinyint 存储角色类型微信用户唯一标识是 openid不是手机号角色字段用数字而非字符串方便 SQL 查询和前端 switch 判断t_lessonid,title,cover_url,content,teacher_id,status,create_timeteacher_id是逻辑外键指向 t_user.id不建 FOREIGN KEY 约束毕设答辩常被问“外键有什么用”答“保证数据一致性”太单薄实际原因是MySQL 8.0 外键会锁表而毕设演示时可能要频繁删课、重置数据逻辑外键更灵活t_workid,user_id,lesson_id,image_url,submit_time,score,commentscore允许 NULL未批阅comment为 TEXT 存储教师评语一张作业图对应一个 lesson_id但一个 lesson 可有多个 workscore 为空表示待批阅这是状态机起点t_commentid,work_id,from_user_id,to_user_id,content,create_time实现学生间互评功能from_user_id和to_user_id均指向t_user.id比单纯“点赞”更有教学意义互评记录可导出为答辩材料中的“用户活跃度分析”注意所有时间字段统一用BIGINT存毫秒时间戳如System.currentTimeMillis()而非DATETIME。原因有三① 避免时区转换问题微信小程序前端new Date().getTime()直接传过来即可② 排序、计算时间差更高效WHERE create_time 1710000000000比WHERE create_time 2024-03-10更快③ 导师看到Long类型会认为你懂底层时间处理。3.2 用 MyBatis-Plus 自动生成建表 SQL避免手写 DDL 出错MyBatis-Plus 提供MybatisPlusAutoGenerator已废弃和AutoGenerator新版本但我们用更轻量的方式基于实体类生成建表语句。在src/test/java下新建SqlGeneratorTest.javaTest public void generateCreateTableSql() { GlobalConfig gc new GlobalConfig(); gc.setOutputDir(src/main/resources/sql); // 输出到 resources/sql 目录 gc.setAuthor(student); DataSourceConfig dsc new DataSourceConfig(); dsc.setUrl(jdbc:mysql://localhost:3306/paint_learn?useSSLfalse); dsc.setDriverName(com.mysql.cj.jdbc.Driver); dsc.setUsername(root); dsc.setPassword(your_password); StrategyConfig strategy new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); // 下划线转驼峰 strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); strategy.setInclude(user, lesson, work, comment); // 只生成这 4 张表 AutoGenerator mpg new AutoGenerator(); mpg.setGlobalConfig(gc); mpg.setDataSource(dsc); mpg.setStrategy(strategy); mpg.execute(); // 运行后会在 resources/sql 下生成 create_table.sql }执行该测试会生成标准建表语句含注释例如t_lesson的建表 SQL-- ---------------------------- -- Table structure for t_lesson -- ---------------------------- DROP TABLE IF EXISTS t_lesson; CREATE TABLE t_lesson ( id bigint NOT NULL COMMENT 主键ID, title varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 课程标题, cover_url varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 封面图URL, content text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT 课程内容富文本HTML, teacher_id bigint NOT NULL COMMENT 教师ID逻辑外键关联t_user.id, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1-上架0-下架, create_time bigint NOT NULL COMMENT 创建时间毫秒时间戳, PRIMARY KEY (id) USING BTREE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT课程表;关键参数说明status字段用tinyint而非enum因为 MySQL 的 enum 在不同版本中排序行为不一致且 MyBatis-Plus 对 enum 支持不如基本类型稳定content用text类型存 HTML 片段小程序 rich-text 组件可直接渲染避免用 JSON 存复杂结构——毕设阶段简单即正义。4. 微信小程序前端对接要点如何让“加载更多”不翻车、“顶部导航栏高度”不遮挡、“单选框”状态同步准确4.1 小程序页面列表加载更多用 page size lastId 三元组替代传统 offset微信小程序的“上拉加载更多”极易陷入性能陷阱当数据量 1 万条时LIMIT 10000, 10这种 offset 分页会全表扫描接口响应从 20ms 涨到 2s。毕设虽无海量数据但必须体现分页方案选型意识。我们采用游标分页Cursor-based Pagination每次请求携带上一页最后一条记录的id查询id lastId LIMIT 10。后端LessonController.java中GetMapping(/list) public Result listLessons(RequestParam Long lastId, RequestParam Integer size) { if (lastId null || lastId 0) lastId 0L; if (size null || size 1 || size 20) size 10; LambdaQueryWrapperLesson wrapper new LambdaQueryWrapper(); wrapper.gt(Lesson::getId, lastId) // 关键where id ? .eq(Lesson::getStatus, 1) .orderByAsc(Lesson::getId) .last(LIMIT size); // 注意MyBatis-Plus 不支持动态 LIMIT此处拼接毕设可接受 ListLesson lessons lessonService.list(wrapper); // 返回时带上本次最后一条的 id供前端下次请求用 Long nextLastId lessons.isEmpty() ? lastId : lessons.get(lessons.size() - 1).getId(); return Result.success(Map.of(list, lessons, nextLastId, nextLastId, hasMore, lessons.size() size)); }小程序端lesson-list.jsdata: { lessonList: [], lastId: 0, hasMore: true, loading: false }, onLoad() { this.loadLessons(); }, loadLessons() { if (!this.data.hasMore || this.data.loading) return; this.setData({ loading: true }); wx.request({ url: https://your-server.com/api/lesson/list, data: { lastId: this.data.lastId, size: 10 }, success: (res) { const { list, nextLastId, hasMore } res.data.data; this.setData({ lessonList: this.data.lessonList.concat(list), lastId: nextLastId, hasMore: hasMore, loading: false }); } }); }, onReachBottom() { this.loadLessons(); }为什么不用offset因为OFFSET越大MySQL 扫描行数越多而id ?可走主键索引性能恒定 O(log n)。答辩时可对比 explain 结果typerangevstypeALL。4.2 顶部导航栏高度适配用wx.getSystemInfoSync().statusBarHeight动态计算微信小程序的navigationStyle: custom会让原生导航栏消失但“自定义顶部栏”高度必须精准否则内容被状态栏遮挡。常见错误是写死padding-top: 44px结果在 iPhone X/XS Max 上顶部留白过大在安卓全面屏上又遮挡。正确做法在app.js的onLaunch中获取并存入 globalDataApp({ globalData: { statusBarHeight: 0, navBarHeight: 0 }, onLaunch() { const systemInfo wx.getSystemInfoSync(); this.globalData.statusBarHeight systemInfo.statusBarHeight; // 导航栏高度 状态栏 自定义标题栏一般 44px this.globalData.navBarHeight systemInfo.statusBarHeight 44; } });在需要自定义导航栏的页面json中{ navigationStyle: custom }wxml中view classnav-bar styleheight: {{globalData.navBarHeight}}px; padding-top: {{globalData.statusBarHeight}}px; text classnav-title绘画课程/text /view view classcontent stylemargin-top: {{globalData.navBarHeight}}px; !-- 页面内容 -- /view血泪经验千万别用wx.getMenuButtonBoundingClientRect()获取胶囊按钮位置来算高度——它在开发者工具和真机上返回值不一致且 iOS/Android 表现不同。statusBarHeight是最稳的锚点。4.3 单选框状态同步用>radio-group bindchangeonRadioChange label wx:for{{options}} wx:keyindex radio value{{item.value}} / {{item.label}} /label /radio-grouponRadioChange(e) { // e.detail.value 是字符串但你不知道它对应 options 的哪个 index // 如果 options 是 [{value:A,label:水彩},{value:B,label:素描}]你无法定位到 A 是第 0 个 }正确写法给每个radio加>radio-group bindchangeonRadioChange label wx:for{{options}} wx:keyindex radio>onRadioChange(e) { const index e.target.dataset.index; // ✅ 拿到数组下标 const selectedOption this.data.options[index]; // 更新 data 中的选中状态 this.setData({ selectedOption: selectedOption, selectedIndex: index }); }玄学坑e.detail.value在某些基础库版本中会返回空字符串而e.target.dataset始终可靠。毕设演示时宁可多写一行>url: jdbc:mysql://localhost:3306/paint_learn?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8mb4执行建表 SQL 前先在 MySQL 客户端执行SET NAMES utf8mb4;注意utf8是 MySQL 的别名实际只支持 3 字节 UTF-8emoji 和生僻汉字会丢必须用utf8mb4。5.3 现象小程序上传图片到后端后端MultipartFile接收为空原因微信小程序wx.uploadFile的name参数必须与后端RequestParam(file) MultipartFile file中的file完全一致且后端 Controller 方法不能加RequestBody注解那是给 JSON 用的。解决小程序端wx.uploadFile({ url: https://api.yourdomain.com/api/work/upload, filePath: tempFilePath, name: file, // ✅ 必须是 file formData: { lessonId: 123 }, // 其他参数走 formData success: (res) { ... } });后端 ControllerPostMapping(/upload) public Result uploadWork(RequestParam(file) MultipartFile file, RequestParam Long lessonId) { // ✅ 用 RequestParam不是 RequestBody // 处理文件... }5.4 现象答辩演示时教师登录后看不到学生提交的作业原因t_work表中teacher_id字段存的是教师的t_user.id但查询作业列表时WHERE teacher_id ?写成了WHERE user_id ?混淆了学生 ID 和教师 ID。解决在WorkController.java中教师查看作业列表的接口必须明确 JOINt_lesson表并用lesson.teacher_id过滤GetMapping(/teacher/list) public Result listWorksByTeacher(RequestParam Long teacherId) { // ❌ 错误SELECT * FROM t_work WHERE teacher_id ? // ✅ 正确JOIN t_lesson ON t_work.lesson_id t_lesson.id WHERE t_lesson.teacher_id ? return workService.listByTeacherId(teacherId); }在WorkServiceImpl.java中实现Override public ListWork listByTeacherId(Long teacherId) { return workMapper.selectList( new QueryWrapperWork() .inSql(lesson_id, SELECT id FROM t_lesson WHERE teacher_id teacherId) ); }后悔药所有涉及多角色学生/教师的查询必须在 SQL 中显式写出角色归属逻辑不能靠前端传参“猜”用户身份。答辩时导师会故意用学生账号访问教师接口看你有没有鉴权。6. 毕设交付物打磨技巧开题报告、论文、PPT 如何把“Java 小程序”讲成技术亮点而非堆砌6.1 开题报告里用“三层架构图”代替“功能模块图”别再画那种“用户模块、课程模块、作业模块”的圆圈图了。导师想看的是你对软件工程的理解深度。在“拟采取的技术方案”部分放一张手绘风格的三层架构图可用 draw.io 导出 PNG表现层微信小程序标注WXML WXSS JS强调“原生开发无框架依赖便于调试”业务逻辑层Spring Boot标注Controller → Service → Mapper重点写“Service 层封装事务Mapper 层由 MyBatis-Plus 自动生成降低 DAO 层出错率”数据持久层MySQL标注t_user/t_lesson/t_work/t_comment四张表加一句“采用逻辑外键设计兼顾数据完整性与开发灵活性”。这张图要占满半页旁边用小字解释“区别于市面上多数 Node.js 毕设项目本系统采用 Java 企业级技术栈体现面向对象设计思想与分层解耦能力”。6.2 论文“系统实现”章节用对比表格突出技术选型合理性在“3.2 后端框架选型”小节插入如下表格不写优点只写‘为什么不用别的’对比项Spring Boot 2.7.xNode.js ExpressPHP ThinkPHP部署复杂度java -jar一键启动无需额外进程管理需pm2 start app.js进程守护配置易错需 Apache/Nginx PHP-FPM环境依赖多调试便利性IntelliJ IDEA 断点调试直达 Service 层变量实时可见Chrome DevTools 调试异步回调链长this 指向易混乱Xdebug 配置繁琐Windows 下常失效答辩可讲性“Spring Bean 生命周期”“MyBatis 一级/二级缓存”是标准考点“Event Loop”“Promise 链”概念抽象学生难讲透“ThinkPHP 模板引擎原理”非主流考点易被质疑深度这张表的价值在于它不吹 Spring Boot 多好而是直面答辩场景——导师问“为什么选 Java”你就能指着表格第三行说“因为调试时我能当场 show 出userService.save()执行前后数据库状态变化这是可验证的工程能力”。6.3 PPT 演示页用“截图红框标注”代替文字描述答辩 PPT 最忌满屏文字。每页只放 1 张图 1 个红框 1 句话。例如第 5 页后端接口截取 Postman 调用/api/lesson/list?lastId100size10的成功响应红框圈住nextLastId: 109字段下方写“游标分页避免 OFFSET 性能衰减1 万条数据下响应稳定在 80ms 内”第 7 页数据库截取 Navicat 中t_work表结构红框圈住score字段的NULL属性下方写“score 允许为空精准表达‘待批阅’业务状态非数据缺失”第 9 页小程序截取真机扫码后的课程列表页红框圈住顶部导航栏下方写“动态计算 statusBarHeight适配 iOS/Android 全面屏无硬编码像素值”。最后一张致谢页不要写“感谢导师”而写“感谢微信小程序提供的稳定基础能力以及 Spring Boot 对 Java 工程化的坚实支撑——它们让我把精力聚焦在‘如何教好一堂绘画课’这个本质问题上。”我带学生做毕设时反复强调答辩不是考你写了多少行代码而是考你能不能把技术选择背后的思考变成一段让导师点头的、有温度的叙述。那些“免费源码”包里藏的往往是别人抄来的、没跑通的、不敢讲的代码而你亲手搭的这套 Java 小程序哪怕只有 4 张表、12 个接口只要每一处设计都有据可依、每一行代码都能讲清来龙去脉它就是值得打 90 分的真东西。希望帮到你。本文还有配套的精品资源点击获取
返回列表