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

文章详情

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

校园服务平台毕设实战:微信小程序+Spring Boot完整落地

校园服务平台毕设实战:微信小程序+Spring Boot完整落地 简介这是一份面向计算机专业毕业生与课程设计者的校园服务平台毕业设计项目技术栈为微信小程序前端、Java后端与MySQL数据库完整覆盖用户、卖家、管理员三类角色的业务闭环包含商品发布、订单管理、校园公告、后台管理等典型模块适合用于毕设参考、答辩演示或二次开发学习。压缩包内包含1112个文件、约27.76MB其中png/svg/jpg为界面与图标素材js/vue/java构成前后端核心逻辑wxml/wxss为小程序页面骨架与样式sql为数据库初始化脚本另有mp4演示视频、docx说明文档及一键启动/构建脚本目录结构清晰便于对照部署与功能梳理。目前已有140人浏览学习。这份资源提供完整的项目源码、操作演示录屏、部署说明和数据库文件并附带项目构建脚本与常见配置既能直接运行查看效果也可帮助读者快速理解小程序与Java后端的交互方式适合从0到1搭建同类校园服务平台时参考。1. 校园服务平台毕设看起来是“买源码”实际是“拆项目”拆什么决定答辩成绩一句话说透这套题目的本质它不是让你从零设计并实现一个高并发校园中台而是让你在毕业设计的半年周期里用一套结构清晰的微信小程序前端和 Java 后端把校园二手交易、活动报名、失物招领、场地预约这类真实业务串成一条完整链路。源码、演示视频、说明文档、数据库文件打包好放在面前真正拉开学生差距的不在“能不能跑”而在“能不能把这个项目讲成自己的方案”。适合谁Java 基础薄弱但需要一份能运行、能演示、能答得上追问的工程代码的人。能解决什么一个接口文档、实体关系、权限校验都齐全的闭环项目以及从环境重建到真机联调全流程的实战经验。能让你少熬几个通宵但前提是你愿意把源码当成一份“施工图纸”而不是期末答案。2. 技术选型与总体结构为什么“原生微信小程序 Spring Boot”是这套毕设最稳的组合2.1 前端为什么选原生微信小程序而不是 uniapp你在热搜里会看到“uniapp 微信小程序打包”这类词但如果毕设题目明确写的是“微信小程序Java 后端”我建议核心页面全部用原生 WXML、WXSS 与 JavaScript 实现而不是套 uniapp。原生小程序在微信开发者工具里能直接运行和断点不需要经过 HBuilderX 的编译链和云打包环节少一层黑匣子答辩时被问“这个页面是怎么渲染的”也能直接指向真实代码。uniapp 适合你有多端上线需求但毕业设计的验收主体是学校老师他们要看到的是你对一个平台的完整掌控而不是多端适配能力。原生小程序的另一个好处是 API 语义与文档一一对应wx.request、wx.navigateTo、wx.getLocation、wx.openBluetoothAdapter。这些接口的入参、回调、失败分支都有官方说明可查出问题时你能准确描述“哪个接口在什么机型上报什么错误”这本身就是答辩加分项。2.2 后端模块划分与源码包的读法Java 后端在这个项目里几乎可以确定是 Spring Boot 体系常见结构是 controller、service、mapper、entity、config 五层。拿到源码包我建议不要从页面开始读而是按这个顺序pom.xml 里看依赖粒度application.yml 里看数据源和端口然后找 SQL 脚本重建数据库最后用演示视频里的操作路径反向定位到对应的 Controller 接口。这个顺序能让你在半天内建立“按钮 → 接口 → SQL”的完整映射而我们最容易犯的错是直接在 WXML 里找事件绑定然后顺着 JS 一层层找下去最后在十几个文件里迷失。合理分层对于这个项目的意义不只是代码整洁。答辩时老师大概率会问“订单状态流转写在哪”你需要能立刻说出“状态变量在 service 层更新mapper 只负责持久化”这类话。如果源码的 controller 里直接拼 SQL也不是不能跑但你很难把这种设计讲得让人信服所以拿到源码后先把业务逻辑是否在 service 层这件事核实清楚。2.3 数据库在“微信小程序 Java”里的连接点校园服务平台的数据量远不到大数据级别MySQL 8.x 加 MyBatis-Plus 是常见做法。数据库在这个架构里的角色不是“存储容器”那么简单它承担了业务规则的落地用户表与角色表决定谁能发布闲置物品预约表的时间段唯一索引防止同一个自习座位被两个人占用订单表的状态字段驱动交易流程。这些约束如果只靠 Java 代码判断会出现并发场景下的重复插入所以在建表时就要通过索引和唯一键把业务边界立住。表与表之间的关系也要按真实校园场景来建模。一个学生可以发布多件闲置物品这是用户表与商品表的一对多一个订单必须关联一个买家和一个卖家这是订单表与用户表的两次外键关联失物招领和活动报名各自独立成表不与二手交易混在一起。关系清晰后端 service 层写起来就不拧巴。2.4 前后端接口约定与状态码设计前后端分离项目实战中最容易扯皮的就是接口约定。这个项目里约定全凭一份接口设计文档来约束常见做法是统一 JSON 结构业务状态码 code 为 200 表示成功、401 表示未登录、403 表示无权限、500 表示服务端异常data 字段返回业务对象或列表msg 字段返回给用户看得懂的提示。分页接口的返回体建议固定为 total、current、size、records 四个字段这样小程序端做“加载更多”时不需要关心数据库方言。还有一个约定常被忽略时间格式。后端返回的时间如果格式不统一小程序端 new Date 解析会出兼容性问题。建议接口层把所有时间统一为字符串“yyyy-MM-dd HH:mm:ss”前端不自行转换格式。接口方法请求参数返回 data 示例/user/loginPOSTcode、nickname、avatar{ token, userId }/goods/pageGETcurrent、size、keyword、categoryId{ total, records[] }/booking/addPOSTgoodsId、startTime、endTime{ bookingId }/found/publishPOSTtitle、description、images{ foundId }这个表格不是让你背下来而是提醒你拿到源码后的第一件事就是把所有接口按这种粒度整理成一页纸。整理完整个系统的数据流就立体了。3. Java 后端从零跑通配置、鉴权、跨域三件套3.1 最小可运行的依赖与配置文件后端能不能在本地快速启动依赖选择比代码本身更关键。pom.xml 里常见的核心依赖是 spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、jjwt、lombok。不要贪多不要引入 dubbo、xxl-job 这类与校园平台无关的组件依赖越多版本冲突概率越大而版本冲突是毕业设计启动阶段最容易遇见的坑。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency /dependencies需要说明的是 mysql-connector-j 是 MySQL 8.x 驱动的新坐标旧写法 mysql-connector-java 在新版本里已经迁移。如果你导入源码后报驱动类找不到先检查这里是不是版本没对齐。resource 目录下的 application.yml 是第二个要核对的文件主要看数据源和 MyBatis-Plus 配置。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_service?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有两个参数对新手最关键。第一个是 serverTimezoneAsia/Shanghai不加这个参数MySQL 8 在连接时会报时区错误项目直接起不来。第二个是 logic-delete-field这是 MyBatis-Plus 的逻辑删除配置字段值为 1 表示已删除0 表示未删除。如果你删一条数据却查不到记录变少多半是逻辑删除生效了不是代码出 bug。3.2 建表脚本与核心表设计下面是一份比较典型的校园服务平台建表脚本核心片段覆盖用户、商品、订单、预约、失物招领五张主表。细心的读者会发现我没有写活动报名表原因是很多校园服务平台的“活动”业务已经合并到预约体系里报名一次活动本质就是预约一个名额。CREATE TABLE tb_user ( id bigint NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信openid, nickname varchar(64) DEFAULT COMMENT 昵称, avatar varchar(255) DEFAULT COMMENT 头像, role tinyint NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, deleted tinyint NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE tb_goods ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 发布者ID, title varchar(100) NOT NULL, description text, price decimal(10,2) NOT NULL DEFAULT 0.00, images varchar(1000) DEFAULT COMMENT 图片路径,逗号分隔, status tinyint NOT NULL DEFAULT 0 COMMENT 0在售 1下架 2已卖出, deleted tinyint NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT闲置商品表; CREATE TABLE tb_booking ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, target_type tinyint NOT NULL COMMENT 1自习座位 2器材 3活动, target_id bigint NOT NULL, start_time datetime NOT NULL, end_time datetime NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待审核 1已确认 2已取消, deleted tinyint NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), UNIQUE KEY uk_target_time (target_type, target_id, start_time, end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约表;标注一下需要留意的设计细节。tb_user 的 uk_openid 唯一索引很关键微信登录拿到 openid 后用“先查再插”或“直接插入冲突后查询”都能防止同一个人创建多个账号。tb_booking 的 uk_target_time 唯一索引是防止座位被重复预约的兜底手段如果后端代码里的并发校验失效数据库会直接拒绝第二条重复预约这是数据正确性的最后防线。images 字段存的是逗号分隔的相对路径不塞 base64否则单条记录可以超过几兆数据库同步和备份都会很痛苦。CREATE TABLE tb_order ( id bigint NOT NULL AUTO_INCREMENT, order_sn varchar(32) NOT NULL COMMENT 订单号, goods_id bigint NOT NULL, buyer_id bigint NOT NULL, seller_id bigint NOT NULL, amount decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待付款 1已付款 2已完成 3已取消, deleted tinyint NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_sn (order_sn), KEY idx_buyer_id (buyer_id), KEY idx_seller_id (seller_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE tb_found ( id bigint NOT NULL AUTO_INCREMENT, publisher_id bigint NOT NULL, type tinyint NOT NULL COMMENT 1寻物 2招领, title varchar(100) NOT NULL, description text, images varchar(1000) DEFAULT , status tinyint NOT NULL DEFAULT 0 COMMENT 0未解决 1已解决, deleted tinyint NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_type (type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT失物招领表;建表脚本执行完后用 show tables 和 desc tb_user 验证一下结构。很多配有数据库文件的毕设会直接把建表语句写成一个 .sql 脚本你需要确认里面的表名和实体类里的注解是否完全一致包括大小写。MySQL 在 Linux 下表名大小写敏感Windows 上默认不敏感这种环境差异会带来非常隐蔽的报错。3.3 JWT 登录与拦截器微信小程序登录的常规链路是wx.login 拿到临时 code后端用 code 调微信接口换 openid然后签发 token 返回小程序。毕设项目通常不会自己维护 session因为 JWT 无状态特性正好匹配前后端分离架构后端不需要存登录态token 过期时间由签发时决定这能省掉 Redis 的依赖。Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; public String createToken(Long userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }这段代码里的 secret 不要写死在类里放在 application.yml 的 jwt.secret 配置项中每位同学部署时改成自己的随机字符串。expire 参数建议设 7 天太短会导致小程序用户频繁重新登录太长会有安全风险毕设场景 7 天是一个比较平衡的值。有了 token 生成工具还需要一个拦截器来保护接口。注意要放行登录接口和图片静态资源否则小程序第一次请求就会被 401 挡住。public class AuthInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { try { Claims claims jwtUtil.parseToken(token.substring(7)); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { // 解析失败说明token已过期或非法 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; } }代码里有一个容易被忽略的细节token 写在请求头 Authorization 字段并以“Bearer ”开头。这是业界的通用约定小程序端的 request 封装必须把这一行拼对否则后端永远解析不到 token。拦截器里把 userId 塞进 request attribute后续 controller 方法里直接取值避免每个接口都重复解析 token。3.4 CORS 跨域配置与联调微信小程序请求后端时不会触发浏览器跨域因为小程序的运行环境不是浏览器。但你仍然需要配 CORS原因是至少有三个场景会用到一是你用浏览器调试 Swagger 或 Knife4j 接口文档二是后期如果做一个简单的 Web 管理后台三是在微信开发者工具里使用“不校验合法域名”模式时某些版本的开发者工具会带 Origin 头。后端不配 CORS这些场景全都会被浏览器拦截。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }联调时最典型的报错是“Access-Control-Allow-Origin”与请求头里的 Origin 不一致。如果你的项目里同时存在前端页面直连和调试工具连两种方式allowedOriginPatterns 用“*”会省很多事。有些同学会把 allowCredentials 设为 false 来避开“Cannot use wildcard with credentials”的报错但那样小程序里携带 token 的自定义请求头会被浏览器丢掉得不偿失。4. 微信小程序端实现顶部导航栏高度、列表加载更多与蓝牙定位落地4.1 自定义顶部导航栏用系统信息算高度别写死 64px微信小程序的顶部导航栏高度不是一个固定值。iPhone 带刘海屏的机型状态栏高度通常在 44px 到 47px 之间Android 机型普遍是 24px 到 30px胶囊按钮的位置也随系统版本变化。如果你在 app.json 里把 navigationStyle 设成 custom就必须自己处理这部分高度否则自定义的标题栏会顶到状态栏里或者离胶囊按钮太近。获取导航栏高度的稳定做法是用 wx.getWindowInfo 拿到 statusBarHeight用 wx.getMenuButtonBoundingClientRect 拿到胶囊按钮的位置信息。具体适配逻辑如下。const getNavbarHeight () { const windowInfo wx.getWindowInfo(); const menuButton wx.getMenuButtonBoundingClientRect(); const statusBarHeight windowInfo.statusBarHeight || 24; const menuButtonTop menuButton.top; const menuButtonHeight menuButton.height; // 导航栏高度 (胶囊顶部 - 状态栏底部) 胶囊高度 下方留白 const navBarHeight (menuButtonTop - statusBarHeight) * 2 menuButtonHeight; return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight navBarHeight }; };计算思路不复杂状态栏和胶囊按钮之间的间隙通常等同于胶囊按钮到导航栏底部的间隙所以用胶囊顶部减去状态栏高度的两倍再加上胶囊高度就是自定义导航栏的完整高度。把这组数值存到全局变量里页面上的自定义头部组件统一读取不要在每个页面重复计算。如果你在真机上发现标题文字偏上或偏下多半是自己算的高度少了或多了 4px用 iPhone 13 和一台 Android 低端机分别测一遍就能定位。4.2 列表加载更多分页参数、onReachBottom 与请求锁校园服务平台里二手商品列表和失物招领列表都要用“下拉加载更多”的交互。微信小程序已经提供了 onReachBottom 页面生命周期事件不需要自己监听滚动事件。但真正实现时容易犯的两个错误是一是在 onReachBottom 里重复发起请求导致数据重复二是在 setData 里直接往数组尾部拼接数据造成渲染层数据量无限增长。// pages/goods/list.js const app getApp(); Page({ data: { goodsList: [], current: 1, size: 10, total: 0, hasMore: true, loading: false }, onLoad() { this.loadGoods(true); }, onReachBottom() { if (this.data.hasMore !this.data.loading) { this.loadGoods(false); } }, async loadGoods(init) { if (this.data.loading) return; const current init ? 1 : this.data.current 1; this.setData({ loading: true }); try { const res await app.request({ url: /goods/page, data: { current, size: this.data.size } }); if (res.code 200) { const records res.data.records || []; const total res.data.total || 0; // init时替换列表loadMore时追加 const goodsList init ? records : this.data.goodsList.concat(records); const hasMore goodsList.length total; this.setData({ goodsList, current, total, hasMore, loading: false }); } } catch (err) { this.setData({ loading: false }); wx.showToast({ title: 加载失败, icon: none }); } } });关键点有三个。第一loading 标志位必须放在请求之前否则用户快速滚动到底部时会同时发起多个分页请求数据顺序错乱。第二current 的更新逻辑是核心初始化时 current 为 1加载更多时先自增再赋值如果顺序写反点击进入详情再返回时列表会从头开始。第三hasMore 的判断标准是“当前已加载数量小于后端返回的 total”不是“current 小于 total/size”因为总条数可能正好被 size 整除后一种写法会漏掉最后一页。4.3 蓝牙定位场景用蓝牙做校园室内定位辅助先扫到设备再上报校园服务平台里经常会加一个“室内容纳量查询”或“设备状态巡检”的功能。既然热词里有微信小程序蓝牙定位这里展开讲一个落地场景在图书馆自习室或实验室门口放几个低功耗蓝牙信标小程序扫描到信标后通过信标 ID 向服务端上报当前位置实现“入室打卡 座位推荐”。这个方案的业务价值是GPS 在室内完全失效而蓝牙信标部署成本低特别适合图书馆、体育馆这类场景。const startScan () { wx.openBluetoothAdapter({ success() { wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: true, interval: 1000, powerLevel: high, success() { wx.onBluetoothDeviceFound((res) { const devices res.devices || []; const beacons devices.filter(item item.name item.name.startsWith(CAMPUS_) ); if (beacons.length 0) { // 取扫描结果中RSSI最强的信标上报位置 const target beacons.reduce((prev, cur) (cur.RSSI || -100) (prev.RSSI || -100) ? cur : prev ); wx.request({ url: ${app.globalData.baseUrl}/checkin/beacon, method: POST, data: { beaconId: target.deviceId, rssi: target.RSSI, timestamp: Date.now() } }); } }); } }); }, fail() { wx.showToast({ title: 蓝牙未开启, icon: none }); } }); };这里有两个参数值得研究。allowDuplicatesKey 设为 true 表示允许同一个信标重复上报这是为了实时性如果做的是“进入区域触发一次”的业务应该设为 false并在拿到首次结果后主动调用 wx.stopBluetoothDevicesDiscovery。interval 是扫描间隔单位毫秒值越小越耗电但对信标响应越及时毕设演示建议用 1000。最大的坑在于蓝牙扫描在微信开发者工具里根本扫不到任何设备必须用手机真机调试同时把手机蓝牙和微信定位权限都打开否则 wx.openBluetoothAdapter 永远走向 fail 分支。5.4 请求封装baseURL、token 注入与统一错误提示如果每个页面都直接写 wx.request你的代码会被“请求头拼接、401 处理、错误码提示”这三块重复逻辑塞满。正确做法是封装一个全局 request返回 Promise页面里用 async/await 调用。// utils/request.js const app getApp(); const request (options) { return new Promise((resolve, reject) { wx.request({ url: ${app.globalData.baseUrl}${options.url}, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: Bearer ${wx.getStorageSync(token)} }, success(res) { if (res.statusCode 401) { // 登录态失效跳转登录页并清空本地token wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(res); return; } if (res.data res.data.code 200) { resolve(res.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }; module.exports request;baseURL 的值在开发阶段建议用 http://127.0.0.1:8080并在微信开发者工具中勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”这样本地后端可以直接连。真机调试时127.0.0.1 指向的是手机自身你需要把 baseURL 改成电脑的局域网 IP并确保手机与电脑在同一 Wi-Fi。request 封装里的 Authorization 头是后端拦截器能放进来的唯一渠道改 token 的存储 key 时前后端要一起改否则会出现“有 token 但后端不认识”的灵异问题。5. 从源码到可演示系统数据库初始化、打包部署与性能边界5.1 数据库初始化与演示数据核对拿到数据库文件后第一步不是在微信开发者工具里预览而是把数据库先还原起来。用 MySQL 命令行或 Navicat 执行你手上的 .sql 脚本执行完毕后打开三张核心表检查演示数据的完整度。mysql -u root -p campus_service campus_service.sql如果导入时报错大概率是 SQL 文件里的表已经存在可以在命令行前加上 drop database if exists campus_service; create database campus_service; 两句保证导入环境是全新的。导入完成后执行下面三条查询分别核对商品、订单、预约的演示数据量。SELECT COUNT(*) FROM tb_goods; SELECT COUNT(*) FROM tb_order; SELECT COUNT(*) FROM tb_booking;这里有一个很实用的检查技巧演示视频里展示的每个功能都要能在数据库里找到对应数据。比如视频里演示了“发布二手手机”你就应该在 tb_goods 里找到一条 title 为“二手 iPhone”且 user_id 对应某个演示账号的记录。如果数据库里根本没有这条数据说明源码包里的 SQL 与演示视频不是同一套数据这时候需要按视频里的路径重新走一遍业务流程来造数据否则答辩现场很容易出现信息对不上的尴尬。5.2 后端启动与小程序导入顺序后端和前端有严格的启动顺序。先启动后端再用微信开发者工具导入小程序项目。如果顺序反过来开发者工具会在编译时请求后端接口超时后报了错你会误以为代码有问题。mvn spring-boot:run后端启动成功的标志不是控制台打出一串日志而是能看到类似“Tomcat started on port(s): 8080”的地方并且用浏览器直接访问 http://127.0.0.1:8080/goods/page?current1size10 能返回 JSON。如果此接口返回 401属于正常现象说明拦截器生效如果返回 404请检查 RequestMapping 里的路径是否以 /goods 开头。小程序端导入时AppID 先用测试号等真机调试时再换成自己的个人 AppID。测试号与正式 AppID 在 wx.login 获取 code 的流程上没有区别但测试号无法调用部分开放接口比如需要用户手机号授权的场景需要提前说明这一点。5.3 答辩演示功能对照与数据准备演示环节最怕的不是功能报错而是操作路径与源码实现不一致。拿到项目后按照演示视频把全部业务流程自己走一遍并把每一条流程拆成“页面 → 按钮 → 接口 → 后端逻辑 → 数据变化”五个步骤形成一页纸的对照表。这张表既是你的排练脚本也是老师追问时的答题提纲。演示功能操作路径关键接口数据变化微信登录首页 → 点击登录POST /user/logintb_user 新增记录发布闲置个人中心 → 发布闲置POST /goods/addtb_goods 新增记录预约自习座位首页 → 座位预约 → 提交POST /booking/addtb_booking 新增记录确认订单订单列表 → 点击确认收货POST /order/confirmtb_order.status 变为 2演示前把数据库里状态为“1”的订单改成“0”把可能被误删的失物招领数据恢复好给自己留一条干净的数据链路。最好准备两个演示账号一个普通用户账号用于走发布与预约流程一个管理员账号用于演示权限控制比如下架违规商品。双账号能同时展示前端交互和后端权限校验比单账号更有说服力也更容易撑起答辩时间。5.4 性能边界分页、图片、接口响应应该做到什么标准毕业设计不要求你做高并发压测但基础性能规范还是要守住否则演示时界面会卡顿到让你不敢操作。前端列表的分页 size 建议设在 10 到 20 之间超过 20 条在小程序里渲染的时间明显变长图片必须压缩后再上传常见做法是将图片控制在 500KB 以内、分辨率不超过 1280px后端存储用相对路径而非 base64。后端接口在本地环境下的响应时间应该控制在 200ms 以内如果你发现某个接口超过 1 秒先看 SQL 有没有走全表扫描再看循环里有没有阻塞调用。并发场景只建议做一层防护预约表靠数据库唯一索引兜底订单状态更新用乐观锁或版本号不引入 Redis 分布式锁。你用不上也讲不清楚反而容易被追问到漏洞百出。性能测试也不要引入 JMeter 这类重型工具打开开发者工具的 Network 面板观察每个请求的耗时和资源体积就能写出“接口平均响应 150ms、首页资源加载 300KB”这类可量化结论。6. 避坑与常见问题拿到毕设源码后的 5 个翻车现场6.1 现象导入数据库脚本时报语法错误原因数据库文件可能是用 MySQL 5.7 导出你的本地环境是 MySQL 8.0两者在 utf8mb4 排序、timestamp 默认值、索引命名等细节上有差异。解决优先用源码包说明文档推荐的数据库版本如果必须用新版把 SQL 文件里的 ENGINEInnoDB DEFAULT CHARSETutf8mb4 附近的注释和多余字符清掉再手工执行 CREATE TABLE 语句片段直到定位到报错的那一行。不要整文件反复导入浪费时间且遮蔽真实错误。6.2 现象小程序请求后端一直转圈最终报“request:fail”原因开发环境中 90% 是地址不通。常见场景是电脑开了防火墙阻止了 8080 端口的入站连接或者真机调试时 baseURL 写成了 127.0.0.1。解决先用电脑浏览器访问 http://127.0.0.1:8080 确认后端存活再用开发者工具请求 http://localhost:8080看是网络层错误还是 HTTP 状态码错误最后在真机调试时把 baseURL 改成电脑的局域网 IP并在手机浏览器里访问该 IP 加端口验证连通性。如果你用的是云服务器还要检查安全组是否放行了 8080 端口这一步属于服务器配置不是代码问题。6.3 现象蓝牙相关功能在开发者工具里永远扫不到设备原因微信开发者工具基于 PC 的蓝牙适配能力是模拟的不支持真实 BLE 扫描。解决所有蓝牙功能必须用手机真机调试。操作路径是点击开发者工具右上角“真机调试”按钮手机会自动打开项目此时 wx.openBluetoothAdapter 才能正常工作。如果你的手机型号较老还要在系统设置里给微信打开“位置权限”因为 Android 系统的蓝牙扫描会间接依赖定位权限缺了它不会报权限错误但回调里一个设备都收不到。6.4 现象列表加载更多的数据与总条数对不上首页出现重复数据原因onReachBottom 触发频率高于 setData 更新频率或者你在加载数据时 current 自增与接口返回不是原子操作导致后端返回了同一页数据。解决在 loadGoods 函数开头加 loading 判断并在请求结束后统一更新 current。另一个隐蔽原因是有多条记录 create_time 相同order by create_time 的分页顺序不稳定解决方法是排序条件改为 order by create_time desc, id desc用主键打破时间戳的并列问题。6.5 现象后端返回的图片在小程序里显示不出来控制台没有报错原因图片字段存的是相对路径比如 /upload/2025/04/01/a.jpg小程序拿到的完整地址变成了 http://127.0.0.1:8080/upload/2025/04/01/a.jpg。此时大概率是后端的静态资源映射没有配置或配置的路径与图片实际存放目录不一致。解决在 Spring Boot 里加一个资源映射把 /upload/** 指向本地磁盘目录再确认上传图片的代码里保存路径与返回路径一致。如果图片是 MySQL 中历史写入的检查 images 字段开头是否有斜杠前后端拼接 baseURL 时多一个斜杠会拼出双斜杠地址再次请求就 404。7. 进阶与验收从“能运行”到“讲得清”的一周验收清单项目能跑通只是第一关真正拉开答辩差距的是你能不能把一个功能从页面讲到数据库再从数据库讲回页面。我的个人习惯是验收时用的不是代码行数而是一张逐项打勾的清单。第一项是登录链路前端 wx.login 获取 code后端换成 openid签发 token之后每次请求携带 token拦截器放行或拦截这条线每个环节都要能画出图。第二项是列表链路首页商品分页如何加载更多、字段如何组装、加载更多时 loading 如何阻止重复请求、hasMore 如何计算。第三项是状态流转订单从待付款到已付款再到已完成每一步修改哪张表的哪个字段谁有权限修改。验证技巧上我在毕业设计阶段用过两个效果很好的方案。第一个是给关键接口加一个简单的耗时统计用一个 OncePerRequestFilter 记录每个请求的处理时间把它输出到日志里。这样答辩时你能拿出“商品分页接口平均耗时 80ms”这种硬数据比“系统很流畅”更有说服力。第二个方案是给所有查询接口加一个 explain 分析确认没有全表扫描用这个结果说明你对索引和 SQL 优化有真实理解。有时间的话再把项目里的一个小模块改成另一种实现比如把 JWT 的解析结果用 AOP 切面注入到方法参数里或者把 wx.request 封装换成基于 await 的版本。这种改动不会破坏现有功能但会让你在回答“你做了什么优化”时有真正的差异点可以讲而不是复述别人的代码。我做毕业设计时最深的体会是源码最难的部分不是读代码而是把代码从“别人写的”变成“我能讲清楚的”。给数据库文件加一个字段注释给 Controller 加一段接口说明给小程序页面加一段数据流注释这些动作做下来源码里哪怕只有三五行是你亲手写的答辩时你都更有底气。希望帮到你拿去用跑不通再回来查排查思路。本文还有配套的精品资源点击获取
返回列表