ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序音乐播放器毕业设计全攻略:从SSM架构到论文答辩

微信小程序音乐播放器毕业设计全攻略:从SSM架构到论文答辩 距离我当年选毕业设计题目那会儿已经过去挺久了。每年到这个节点总能看到一批同学被“一本正经的选题清单”安排得明明白白其中“基于微信小程序的音乐播放器”绝对是常青树般的存在——它看起来不偏门、有界面、有交互还带点前后端融合的气质。但说句实话真正把这个题目做出彩的人不多大多数版本都卡在了同样几道坎上播放功能怎么在小程序里稳定实现、SSM后端到底怎么跟前端配合、论文怎么写才能撑得起“设计”而不是“拼凑”。这篇文章我就拿这个经典题目来一次完整复盘从技术选型、数据库设计、前后端实现一直到论文写作和答辩准备一步步拆给你看。如果你正打算做这个题或者手上已经在写但进度停滞这篇文章应该能帮你把整条脉络理清楚少走不少弯路。1. 为什么这个题目年年有人选但年年有人翻车先说点实在话。微信小程序音乐播放器这个题目天生自带一个优势它很容易演示。打开小程序、点一下、歌响了评委和答辩老师一眼就能看到成品效果不用解释太多。再加上微信小程序本身就是这两年移动端开发的热点SSM又是学校里Java Web课程的老牌标配二者一组合技术栈上既有时髦元素又有课程呼应属于“安全牌”。但安全牌打不好一样会翻车。我见过很多同学的“毕设demo”长这样能登录能列个歌单点一首歌能放出一段音频再无其他。整个后台跟闹着玩似的数据库里两张大表糊弄完事。这样的项目拿去答辩老师问一句“用户的收藏表怎么设计的”“你分页用的什么方案”“多用户并发播放怎么考虑”人基本就懵了。这个题目的真正考点其实不在“做不做得出播放器”而在“你有没有把一个完整业务闭环做成系统”。播放器只是外壳里面的用户体系、歌曲管理、歌单组织、收藏评论、搜索播放记录这些才是论文里能写出“设计感”的地方。所以我说这个题想做出好结果至少要把下面这张功能表理清楚。用户端小程序微信登录、浏览歌单、歌曲搜索、播放控制、收藏歌曲、评论歌曲、播放历史管理端后台Web歌曲上传/编辑、歌手管理、歌单编排、用户数据概览、评论审核技术结构微信小程序前端 SSM架构后端 MySQL数据库有了这张功能清单你对整个项目的规模就有了底。接下来选技术路线的时候心里也不至于没谱。2. 技术栈选择与整体架构先搭骨架再谈细节2.1 前端原生微信小程序别为了省事乱上框架做小程序前端市面上流行两个方向原生小程序开发以及用uni-app、Taro这类跨端框架写完后编译成小程序。放在毕设场景里我的建议是除非你特别想展示“我掌握跨端框架”否则老老实实用原生微信小程序。原因有三个。第一论文里的技术介绍好写。原生小程序的WXML、WXSS、JS、JSON四件套拆开讲每一块都有官方文档支撑查重率也容易控制。第二调试方便。微信开发者工具对原生项目的支持永远是最好的你写一个API报错了社区里搜答案基本都是原生方案的帖子。第三跨端框架那层编译反而会给你引入一部分“解释不清的黑盒”答辩时万一被问到底层怎么映射容易卡壳。原生小程序的页面结构上我的习惯是这样划分。pages/index首页歌单瀑布流和推荐位pages/play播放页含唱片旋转、进度条、上下曲切换pages/search搜索页含搜索建议、结果列表pages/mine个人中心收藏列表、播放历史、登录状态pages/songListDetail歌单详情页展示歌单下的歌曲列表components/audio-player自定义播放器组件固定在底部底部那层常驻播放器我建议用自定义组件实现不要用原生audio标签。原生audio的样式在小程序里很受限自定义组件加上wx:if控制显示隐藏配合全局播放器的状态体验会更接近真实App。2.2 后端SSM这套组合到底谁在干什么论文要讲清楚SSM是Spring、SpringMVC、MyBatis三个框架的合称。很多同学写论文的时候把这仨名字一摆然后就开始抄概念其实根本没搞明白它们各自负责什么。这里我用一段大白话给你盘清楚。Spring是整个应用的容器骨架管理Service层的Bean处理事务和依赖注入。你写的UserService、SongService这些对象不需要自己new交给Spring容器去创建和维护。SpringMVC负责Web层请求进来先到DispatcherServlet它根据URL找到对应的Controller方法把前端传的参数绑定进去调用Service再把返回值渲染成JSON响应给前端。MyBatis是数据访问层把Mapper接口里的方法和XML里的SQL映射起来。你有多少条数据、怎么查、怎么改都靠它去跟数据库打交道。这一套链路对应到一次完整的请求上就是小程序发wx.request → Tomcat接收 → DispatcherServlet路由 → Controller处理 → 调Service业务逻辑 → 调Mapper执行SQL → MySQL返回结果 → 一层层往上返回最终以JSON格式回传给前端。论文里能把这条链路画清楚再配上你项目里的具体类名和接口路径比从百度百科抄十段框架特性都管用。2.3 架构分层三层还不够还得加上通用的响应体SSM项目做久了你会习惯一种写法包名按com.xxx.music来组织下面分controller、service、mapper、pojo、common。POJO用Map或者单独建的实体类承接数据库字段Controller只做参数接收和响应封装Service层写真正的业务判断Mapper只做数据查询。有一个细节非常影响代码规范和论文观感就是统一响应体。很多同学的接口返回特别随意——成功返回一堆字段失败就返回一个null或者一个字符串“error”。这在联调的时候特别痛苦前端根本没法判断到底哪一步出了问题。我自己在做这类项目的时候会单独建一个Result类结构大概是这样的。public class ResultT { private Integer code; // 200成功500失败 private String message; // 提示信息 private T data; // 实际返回数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }这样一来前端封装网络请求的时候不管哪个接口只要先判断code是不是200再决定要不要渲染data逻辑一下就统一了。论文里写这部分也好看一张响应结构表就讲完了。3. 数据库设计关系理不顺后面全是坑3.1 从一张用户表到五张核心表音频播放器这类系统数据库设计的核心难点不在“表多”而在“关系对”。一开始好多同学只建两张表用户表、歌曲表然后收藏和评论全塞到一个字段里用逗号隔开。这种设计在演示的时候能跑通但只要老师稍微追问“如果我收藏了100首歌这个字段会有多长”答辩就变得很难看。我做这个项目的时候用了五张核心表来支撑业务给你列一下结构和设计动机。表名主要字段作用userid, openid, nickname, avatar, create_time用户信息openid作为微信登录唯一的业务主键singerid, name, avatar, intro歌手信息歌曲表外键引用songid, title, singer_id, album, duration, url, cover, lyrics, play_count歌曲内容URL存网络可访问地址song_listid, name, cover, description, owner_id歌单基础信息song_list_detailid, song_list_id, song_id歌单与歌曲的多对多中间关系表favoriteid, user_id, song_id, create_time用户收藏唯一约束user_idsong_idcommentid, user_id, song_id, content, create_time, like_count歌曲评论这里的核心点在song_list_detail这张中间表。歌单和歌曲是一个多对多关系一张歌单里有好多歌一首歌可以出现在多张歌单里。如果不拆这张中间表在歌单里增删歌曲就只能靠字符串拼接处理既不好维护也没法统计。拆了之后往歌单里加一首歌就是在detail表插一行记录删歌就是删一行记录逻辑清楚得不行。3.2 字段设计的几个实战细节先说song表里的play_count。这个字段是用来支撑“热门推荐”和“排行榜”的每次播放接口被调用时执行一句UPDATE song SET play_count play_count 1 WHERE id ?就行千万不要先查再改并发高一点就会出现计数丢失的问题。虽然毕设系统的并发量基本可以忽略但这种意识写进论文里能显得你考虑过工程问题。再说create_time。MySQL5.7以上版本可以直接用datetime类型加DEFAULT CURRENT_TIMESTAMP让数据库帮我们生成代码里不需要手动set。这个细节在写Mapper的INSERT语句时很省事。最后说openid。微信登录拿到的openid是用户在当前小程序下的唯一标识很多同学直接把openid当主键用。我的建议还是保留自增的id主键openid单独建一个字段加唯一索引。一来后续如果要跑到不同小程序环境做迁移不会因为主键冲突头疼二来自增id做关联查询的时候比一长串openid高效不少。3.3 搜索和分页从哪来看接口才知道数据库表设计完后不要马上动手写页面先把接口文档列出来。前后端分离开发的习惯对于毕设团队来说可能有点夸张但对你一个人来说列一份接口清单却能省很多返工时间。我的接口规划大致是这样的。POST /user/login微信登录接收前端wx.login拿到的code返回自定义登录态GET /song/hot获取热门歌曲按play_count倒序取前十条GET /song/search?keywordxxxpageNum1pageSize20按关键词模糊搜索GET /songList/detail?idxx查询歌单详情包含歌曲列表POST /favorite/add收藏歌曲POST /favorite/remove取消收藏GET /favorite/list?userIdxx我收藏的歌曲列表POST /comment/add发表评论GET /comment/list?songIdxx查询某首歌的评论列表GET /song/playUrl?idxx获取播放地址同时触发play_count加一这些接口定义完你基本就知道每个页面该调哪个请求、需要传什么参、返回什么结构了。数据库里缺什么字段、漏什么表在这一步也都能补出来。4. 微信小程序端实现播放器不是简单放首歌4.1 登录流程wx.login拿到code只是第一步微信登录是几乎小程序项目都要先做的一环。流程说起来很简单前端调wx.login()拿到一个临时code这个code发给后端后端拿code去微信的接口换openid然后后端自己生成一个登录态比如token返回给小程序存起来。写论文的时候很多同学会把wx.login()到获取用户头像昵称这段写得很长但我建议你的实现重心放在后端如何用code换openid上因为这是SSM服务端真正干活的地方。前端这边代码量其实很少大概长这样。handleLogin() { wx.login({ success: async (res) { if (res.code) { const loginRes await request({ url: /user/login, method: POST, data: { code: res.code } }); if (loginRes.code 200) { wx.setStorageSync(token, loginRes.data.token); wx.setStorageSync(userInfo, loginRes.data.userInfo); } } } }); }这里的request是一个我常用的Promise化封装后面第4.4节会专门讲。后端拿到code后调微信接口参数是appid、secret、js_code、grant_type微信返回openid和session_key我们拿openid去查user表查到就直接登录查不到就自动注册一个新用户然后生成一个随机token存进redis或者数据库字段里返回给前端。有一点要注意微信的appsecret是不能暴露在小程序代码里的所以后端调微信接口这段是必须做的事加密串一定放在服务端。这里顺带吐槽一个经常踩的坑很多同学用code换完openid之后每次都重新调微信接口其实没有必要。你完全可以第一次登录之后把openid存进自己的user表token的有效期自己做校验不用每次都依赖微信的session。这样不仅减少了一次外网依赖论文里也能多写一层缓存设计。4.2 音频播放核心API与那些防不胜防的细节小程序里播放音频官方API从最早的wx.playBackgroundAudio到后来推荐使用的wx.createInnerAudioContext现在主流方案是后者。InnerAudioContext文档上叫“内部音频上下文”它比旧API好用的地方在于支持更多事件的监听比如自然播放结束、播放失败、进度更新而且你可以创建多个实例灵活控制。我自己在实现播放器的时候是把InnerAudioContext封装成了全局单例放在app.js里因为小程序页面切换时JS对象并不会被销毁所以全局播放器是实现“切到其他页面歌还在响”的基础。大概思路如下。// app.js globalData: { currentSong: null, isPlaying: false, audioCtx: null, playList: [], currentIndex: 0 }初始化的时候先判断globalData.audioCtx是否存在存在就直接用当前的实例不存在才创建新的。这样做的好处是你从首页进播放页播放页拿到的是一个已经存在的音频上下文你再从播放页退回首页底部组件还能通过对audioCtx的状态绑定继续更新播放暂停图标。播放器最关键的两个事件一个是onTimeUpdate每隔一定时间触发一次告诉你当前播放进度用来更新进度条的UI另一个是onEnded一首歌放完了自动切下一首根据playList和currentIndex计算下一首的索引。audioCtx.onTimeUpdate(() { this.setData({ currentTime: audioCtx.currentTime, progress: audioCtx.currentTime / audioCtx.duration * 100 }); }); audioCtx.onEnded(() { const nextIndex (this.data.currentIndex 1) % this.data.playList.length; this.playByIndex(nextIndex); });这里有个小坑需要提前说onTimeUpdate的频率很高你在setData里更新currentTime会让整个页面不断重渲染。但也没必要为这个过度优化正常的项目里把setData的频率控制在每秒三四次是合理的。你只需要在onTimeUpdate里做个简单节流比如用Date.now()记录上次更新时间超过250毫秒才set一次Data。关于音频格式我强烈建议后端存mp3地址。小程序对音频格式的兼容性比你想象中宽容但各类格式里mp3的兼容性和文件体积的平衡最好。如果你的音频来自第三方平台的在线播放地址要记得确认这个URL能不能外链播放很多平台有防盗链在小程序里会403。这也是很多同学“本地模拟器上能放真机上没声”的最常见原因。4.3 底部导航栏高度、胶囊按钮适配和页面间通信做小程序页面的同学有一半的时间耗在适配各种机型上。顶部导航栏的自定义、底部安全区的适配都是我实际开发中踩过坑的地方。如果你选择了自定义导航栏即在app.json里配置navigationStyle: custom就需要自己计算状态栏高度和胶囊按钮的位置。状态栏高度可以通过wx.getWindowInfo()里的statusBarHeight获取但我们没法直接拿到胶囊按钮的高度和间距解决办法是调用wx.getMenuButtonBoundingClientRect()获取胶囊按钮的位置信息。计算自定义导航栏高度的公式是const menuRect wx.getMenuButtonBoundingClientRect(); const statusBarHeight wx.getWindowInfo().statusBarHeight; const navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.height;这段代码模拟器上看着挺正常到了真机上也没啥大问题但你在写论文时最好别把适配逻辑写太复杂因为答辩老师不一定深究这个点但你自己得知道为什么这么算。页面跳转后播放器的状态同步我常用的方案是事件总线。用一个极简的EventBus在播放页切歌时emit一个事件首页底部播放器组件里on监听这个事件更新自己的正在播放文案和封面图。这个小技巧避免了在页面间通过url参数传递复杂的对象问题实现也不难。4.4 请求封装别让每个页面重复写loading和报错提示小程序发请求最原始的方式是每次写wx.request然后在success回调里判断statusCode。如果项目里有二十个接口这么写二十次后期你改个baseURL或者统一加个鉴权header得找二十个地方改动。所以我一般会在utils/request.js里封装一个方法。const BASE_URL https://your-api-domain.com; function request(options) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 200) { resolve(res.data); } else if (res.statusCode 401) { // 登录过期处理 wx.showToast({ title: 登录已过期, icon: none }); wx.navigateTo({ url: /pages/login/login }); reject(res); } else { wx.showToast({ title: 请求失败, icon: none }); reject(res); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports request;这里特别说一下Authorization。如果你没有做登录态只是随便登录了一下这个header里的token就是空的后端如果不校验也不会报错。但如果后端加了拦截器统一校验有一个小细节要注意request里header的字段值不能是undefined否则在某些Android机型上会抛异常所以取token的时候后面加一个|| 兜底。4.5 分页加载与触底刷新列表不是一次性灌进来首页歌单列表、搜索歌曲结果这种列表型的页面一定要做分页。不光是用户体验的问题论文里也值得写一次性返回100条数据对小程序渲染和网络传输都是负担。小程序实现触底加载就靠页面生命周期里的onReachBottom方法。我的实现思路是这样data里维护一个pageNum和pageSize第一次onLoad时加载第一页onReachBottom触发时pageNum加一然后请求下一页。拿到返回结果后如果是第一页就替换list否则用concat拼接。async loadSongs(reset false) { if (this.data.isLoading) return; if (reset) { this.setData({ pageNum: 1, songList: [] }); } const params { pageNum: this.data.pageNum, pageSize: this.data.pageSize, keyword: this.data.keyword }; const res await request({ url: /song/search, data: params }); if (res.code 200) { const newList this.data.songList.concat(res.data.list); this.setData({ songList: newList, hasMore: res.data.hasMore, isLoading: false }); } }, onReachBottom() { if (!this.data.hasMore) return; this.setData({ pageNum: this.data.pageNum 1 }); this.loadSongs(); }这里的isLoading是防止用户快速连续上滑时发出多个重复请求这个锁很关键不然你会发现列表偶尔会抽搐或折返。后端返回的data里要带回hasMore字段前端拿到它决定要不要再触发下一轮请求。另外有一个列表渲染细节在wx:for里一定要给每一项加wx:key否则小程序会警告而且列表更新时渲染效率极差。我自己在这个项目里是拿songId做key的因为它是稳定且唯一的。4.6 音频中断处理来电话和切后台时保命真机测试的时候你会遇到一个模拟器上完全没法复现的问题播放过程中来了个电话或者切到其他App再切回来音频可能停住或者再次播放时进度错乱。这个需要监听两个事件onAudioInterruptionBegin和onAudioInterruptionEnd。基本处理思路是在音频中断开始时暂停播放记录当前播放状态在中断结束时如果之前是播放状态就调用audioCtx.play()恢复同时要重新设定当前的播放位置。原理就是手机在来电时会暂时打断音频焦点小程序本身没法阻止这个行为只能配合系统的音频焦点机制做状态恢复。这一步在论文里可以作为“系统非功能性设计”的一部分来写标题叫“基于音频焦点变化的播放恢复策略”听着就很专业。5. 后端SSM实现Controller、Service、Mapper三兄弟的配合5.1 一次完整请求的处理器链条SSM项目里最容易被忽略的是“注解的作用”。我在论文初稿阶段写技术介绍时根本分不清Controller和RestController的区别网上一搜全是一堆框架概念。后来自己敲代码才慢慢明白RestController就是Controller加ResponseBody它的效果是让Controller里的每个方法返回值直接以JSON格式写在响应体里不需要再另外配置视图解析器。实际的项目里我是这么安排后端包的com.example.music ├── controller // 接收前端请求 ├── service // 业务接口与实现 ├── dao(mapper) // MyBatis的Mapper接口 ├── pojo(entity) // 数据库实体类 ├── common // 通用结果类、异常处理类 └── interceptor // 登录拦截器Controller里的方法一般只做三件事接收参数、调Service、封装Result返回。业务判断放到Service层这样后期如果想换技术栈Controller层基本不用动。比如“加入收藏”这个接口Controller收到userId和songId后直接调favoriteService.add(userId, songId)结果返回给前端。真正在Service里做的是先查你收藏过没有没收藏过才INSERT否则返回提示“请勿重复收藏”。这种分层方式谁都能写但很多同学并不理解为什么要这么分。我的理解是Controller的职责是“翻译”HTTP请求和Java调用Service的职责是“定义”业务规则Mapper的职责是“执行”数据操作。三个层的修改频率和复用程度都不一样拆开之后任何一个层的变化不会波及到其他层。这个认知在答辩时老师问“你的项目里Service为什么要有接口还有实现类”的时候能让你答得游刃有余。5.2 SSM常用注解清单论文里可以这样归纳如果你正在写SSM相关章节下面这些注解的基本用途和摆放位置建议你也归纳到论文里。注解作用典型使用位置Controller标记类为SpringMVC的控制器Controller类上RestController组合注解返回JSON数据Controller类上RequestMapping映射URL与方法/类的关系类和方法上ResponseBody方法返回值写入HTTP响应体Controller方法上Service声明一个Service Bean交给Spring管理Service实现类上Autowired按类型注入依赖Controller和Service里的字段Transactional开启事务管理Service类或方法上Repository声明Mapper BeanMapper接口上Param给Mapper方法参数起名字便于XML引用Mapper接口方法参数上我这里想重点说一个很容易被误解的Autowired。网上的教程经常直接在字段上标注Autowired就完事但你真上线跑大项目就会发现字段注入有一个不小的问题它和Spring容器强耦合且不方便做单元测试。很多现代规范推荐构造器注入但在毕设代码里用字段注入完全没问题因为项目复杂度不足以触发问题。只是你在论文里写的时候表述不要写得太满说“使用了依赖注入降低耦合”就够了。5.3 MyBatis的XML映射和动态SQL复杂的SQL查询我不会全部写在注解里量一大阅读性很差推荐配套的XML文件。举一个搜索接口的例子。select idsearchSongs resultTypecom.example.music.pojo.Song SELECT * FROM song WHERE 1 1 if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR album LIKE CONCAT(%, #{keyword}, %)) /if ORDER BY play_count DESC LIMIT #{offset}, #{pageSize} /select注意两个点。第一动态SQL里WHERE后面写的11看起来奇怪实际是为了后续拼接AND条件时不至于因为第一个条件不存在而语法错误。不过你也可以用官方推荐的 标签它更规范会自动处理前置AND。第二LIKE查询要用CONCAT拼接百分号不要直接在参数里传%那样容易出回显注入问题。分页我推荐手写LIMIT因为PageHelper这种插件虽然在SSM里很好用但它属于“框架里的框架”用不好还会出现慢查询或者count语句异常。毕设这种量级的项目手写LIMIT完全够用而且你能在论文里详细解释分页原理。5.4 登录拦截器和token校验后端既然给小程序发了token就得有人校验token。SSM里实现这个最标准的方式是HandlerInterceptor。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } // 查redis或查数据库校验token是否有效 User user tokenService.getUserByToken(token); if (user null) { response.setStatus(401); return false; } request.setAttribute(currentUser, user); return true; } }配置的时候把需要放行的路径排除掉登录接口、歌曲搜索接口、歌单详情接口这些可以匿名访问的其余全部拦截。这里最容易犯的错是忘了放行OPTIONS请求虽然小程序端大概率不会主动发OPTIONS预检但如果你表里加了自定义header某些场景是可能触发的。token的存储我建议放到Redis毕业论文里也好解释TTL自动过期不需要自己写定时清理。如果不想引入中间件存到数据库的token表也行但要在登录时手动清除过期token逻辑会多一些。5.5 音频文件存储与访问不过度方案关于音频文件本身有些同学会纠结要不要买对象存储服务。我的建议是毕设项目不要在这个地方过度投入。直接把mp3文件放到项目的static/music目录下SpringMVC配置静态资源映射暴露出去就行。mvc:resources mapping/music/** location/static/music/ /这样音频URL存的就是/music/song1.mp3这种相对路径本地联调时拼上IP端口就能访问。如果你的答辩环境是局域网内演示数据库里存相对路径就行。等到你真要部署到云服务器的时候再考虑把文件挪到对象存储那时候改的也只是一条URL前缀的事。封面图同理。这个方案最大的好处是你的论文不需要引入一段“对象存储选型”的大章节省下来的篇幅可以放到功能设计和测试用例上重量更集中在核心功。6. 联调与调试把Charles抓包、真机适配和性能优化的经验都用上6.1 用抓包工具拆解小程序请求小程序开发完并不是就万事大吉了。前后端联调阶段我强烈建议你学会用抓包工具看请求Charles是其中比较顺手的一款。在电脑上装好并配置好SSL代理之后手机连上同一个局域网把WiFi代理设到电脑的IP和端口这样小程序发出去的每一个请求在电脑上都能看到完整的URL、请求头、请求体和返回数据。它的价值在哪里最直接的一个场景前端说“我发请求了但页面不显示”你抓包一看发现请求根本没发出去或者发了但返回404。有一些SSM项目的404是因为接口路径漏了模块前缀比如你RequestMapping写的是/user/login但请求实际发到了/api/user/login这类问题在控制台里光看日志很难定位抓包一眼就能看到。抓包还能帮你做假数据联调。后端还没写完某个接口你可以在抓包工具里把某个请求的返回JSON改成你想要的结构前端继续开发不会阻塞。这个方法写进论文测试章节的“接口调试”部分能体现你具备常见开发工具的实操能力。6.2 真机调试与iPhone、Android的差异模拟器上一切正常真机上一塌糊涂这是小程序的常态。最容易出问题的几个点我列一下你们做个自查。音频播放被暂停真机切后台久一点音频可能会被系统杀掉需要加上文中提到的中断监听和恢复逻辑字体和高度适配自定义导航栏在部分老安卓机上的计算会偏差要保证所有布局都从计算出的常量值出发不要写死图片缓存问题开发环境下改了图片资源真机有时候不更新清除小程序缓存即可请求域名配置真机上发请求必须把后端域名加到小程序后台的request合法域名里开发者工具里勾选“不校验合法域名”只对本地开发有效。这一点年年有人忘记答辩现场翻车率极高还有一个必须讲的点真机调试时内存。如果你的列表加载了很多歌曲封面图而不做懒加载低端Android机型翻几页就会白屏或卡顿。小程序的image组件的lazy-load属性可以开启图片懒加载建议列表里的封面图都加上。6.3 本地缓存播放列表和进度的记性用户把App切走再回来如果播放进度全丢了体验会很差。所以播放进度和历史列表这些小数据我会存到小程序的Storage里。wx.setStorageSync是同步写入适合这种量小、频繁读写的场景。设计思路是每次onTimeUpdate节流后把当前歌曲的id、当前时间、总时长写进一个对象里setStorage保存。小程序下次启动时在启动页读取这个缓存如果有记录就把歌曲信息填充到底部播放器组件中用户点一下播放就能续播。这里存的是一个纯前端的状态不需要和后端交互。但如果做到“多端同步”那就得在数据库加一张播放进度表了。毕设不用做那么重有Storage版本的续播体验已经足够加分。6.4 列表和图片渲染的性能设计小程序性能优化这块论文里比较稳妥的写法是从“渲染层与逻辑层分离”讲起然后落到具体实现。我这里分享三个实在有效的优化手段。第一个是控制setData的体积。不要一次性把一大串不相关的数据set进data。页面用不到的字段比如后端返回接口里的时间戳、冗余字段在请求返回时做一个字段过滤再setData。第二个是分页之外还要做虚拟列表的构思毕设里可以不用真做但要在论文的改进与展望里提一句“当前采用分页加懒加载后续可引入recycle-view进行长列表虚拟滚动优化”导师看了会觉得你有延伸思考。第三个是图片尺寸问题。小程序里一张封面图没必要原图直出后端可以生成缩略图或者前端根据展示尺寸请求不同规格的URL。我实际做的时候用的是后端URL加参数的方式比如?size200这样列表页加载小图播放页加载大图流量省很多。7. 测试与部署不能只写“测试通过”要写出能说服人的数据7.1 功能性测试怎么设计用例很多同学的论文测试章节就是一张截图配一句“经测试系统功能正常满足需求”这基本等于白写。好的测试章节要有用例表每个用例包含前置条件、操作步骤、输入数据、预期结果、实际结果、结论这六项。我给你列一个示例。用例编号功能模块操作步骤预期结果实际情况TC-001微信登录点击微信登录按钮授权昵称头像系统自动注册或登录跳转首页与预期一致TC-002歌曲搜索输入关键词“晴天”点击搜索返回包含“晴天”的歌曲列表与预期一致TC-003播放控制点击歌曲播放再点击暂停音频播放开始暂停按钮图标切换与预期一致TC-004收藏功能点击歌曲右侧爱心图标图标变红收藏表中新增一条记录与预期一致TC-005评论发布输入评论内容点击发表评论出现在评论区数据库新增记录与预期一致TC-006触底加载在歌单列表页上滑到底部自动加载下一页20条数据与预期一致友情提示预期结果这一列不同用例不要全写“与预期一致”保持一定的变化表格观感更真实。7.2 非功能性测试与边界条件除了功能之外还能测什么我补充几个很实用且容易操作的响应时间测试用抓包工具或者浏览器的Network面板记录接口响应时间保证绝大多数请求在500毫秒内完成。如果后端没加索引导致歌曲搜索很慢你们要能发现并加索引并发梯度测试可以用JMeter模拟少量高并发访问登录接口和热门歌曲接口看数据库连接池是否有报错。毕设阶段不需要追求高性能但你得知道自己系统的上限在哪异常输入测试搜索框中输入超长字符串、评论内容输入为空、歌单id传个不存在的id系统应优雅提示而不是500不同设备兼容性测试至少2部Android机、1部iPhone、加上微信开发者工具模拟器统一走一遍核心功能我实际做完这个项目后发现最容易暴露问题的不是并发而是边界输入和手机兼容。很多崩溃其实都是自己在测试时随手乱按出来的。7.3 部署方案从本地到Linux服务器毕设答辩一般要求系统能现场演示。如果只在Windows上开着Eclipse或者IDEA跑后端演示当天电脑出问题一切就完了所以我建议提前部署到一台Linux云服务器上完成演示切换。部署基础步骤是装JDK和Tomcat或直接打包SpringBoot Jar包装MySQL导入建表SQL配置Nginx代理静态资源和反向代理后端接口。如果你用的还是传统SSM的war包部署那Tomcat的context路径别忘配置好小程序端的BASE_URL也要改成服务器IP或域名对应的小程序后台合法域名要同步配置。还有一个细节如果你用的小程序不在线上版本而是在微信开发者工具里运行真机预览那后端域名可以是IP或localhost但真机调试时必须用网络IP且保证端口开放。防火墙这块每年都有人被卡住演示当天一直在那找“为什么手机连不上后端”检查顺序建议是先ping服务器IP通不通再telnet端口通不通最后看后端日志有没有收到请求。按这个顺序排查不出三分钟绝对能找到问题。8. 论文写作要点从技术堆砌到设计逻辑8.1 论文结构怎么排每个学校给的论文模板可能不太一样但大概结构就是摘要、绪论、相关技术介绍、需求分析、总体设计、详细设计、系统测试、总结展望。这个题目下我建议的重点倾斜如下。相关技术介绍占整篇论文的10%15%即可不要抄框架文档抄一大段重点写清楚“这些技术在我这个项目里承担什么角色”需求分析要有用例图、功能性需求列表、非功能性需求列表。这里最容易空泛好一点的写法是从用户角色出发把“游客能干嘛、登录用户可以干嘛、管理员要干嘛”全用表格列出来总体设计架构图、功能模块图、数据库ER图、接口列表。画图用Visio或者Draw.io别用手画截图详细设计分模块描述实现过程每个模块包含业务逻辑说明、核心流程图或者时序图、核心代码片段。这里注意代码不要贴大段只挑关键逻辑贴系统测试见上文第7章总结写自己在设计过程中的收获和不足重点是“不足”要写得真实诚恳但又能圆回来8.2 “设计”和“开发”的区别论文里要把动机写出来很多毕设论文为什么一眼假因为从头到尾都在描述“怎么做的”从来没写“为什么这么做”。代码截图贴了一张又一张但老师看到后依然不知道你做这个表结构时的取舍判断。所以我在写论文时候给自己定了一条铁律每个设计决策都要跟一句解释。比如数据库的song_list_detail中间表你不能只写“建了一张中间表”而要写“由于歌单和歌曲是多对多关系为了避免数据冗余和维护困难引入中间表使多对多关系转化为两个一对多关系”。再比如前端为什么用自定义组件播放器而不是原生audio组件为什么全局持有InnerAudioContext实例后端为什么引入统一的Result响应类这些都值得用一两句话讲清楚设计动机。这样一来论文的每一节都自带逻辑链条不再是代码的罗列。答辩老师翻你的论文每一页都能读到判断和取舍印象分自然不一样。8.3 答辩准备提前想清楚老师爱问什么给你列几个这个题目下大概率会被问到的问题提前准备答案比现场编要好得多。问为什么选SSM而不选SpringBoot答SpringBoot其实是SpringMVC的简化封装从学习过程来说SSM的三层结构分界更清晰能深入到框架底层理解请求处理和事务机制对理解Java Web体系帮助更大。答辩这样说比一句“课程要求”体面得多。问你的登录态安全吗token过期怎么办答token是随机UUID存储到Redis并设置有效期前端每次请求都带header后端设置拦截器统一校验过期后前端收到401自动跳转登录。问如果同时有10000个人播放同一首歌你会怎么优化答当前系统面向小规模应用采用了简单计数策略如果要扩展高峰可引入Redis做计数缓存定时批量写回MySQL播放地址用CDN分发接口层可引入本地缓存。问分页为什么不用PageHelper答手动LIMIT便于理解且在这个量级下性能差异可忽略同时能减少框架插件的依赖。问这首歌的播放权限怎么设计的答歌曲URL接口做了登录校验匿名拿不到直接播放地址下载链接不直接暴露在代码里。这些问题的核心考点不是你答得多完美而是你能不能把自己的系统说得自圆其说。平时多梳理自己的代码心里有数了答辩就不会太慌。我在实际跑这个项目的时候最大的感受是“播放器”三个字看着简单真正串联起来之后前面涉及的登录、分页、缓存、适配、部署每一项都是独立的坑。但也是因为这些坑全踩了一遍你才敢说这个题目是“设计与实现”而不只是“照着教程敲了一遍”。如果你正在做类似的东西建议先把上面这个接口清单和数据库表结构落地再开始写页面。顺序反了的话你会发现写代码多数时候都在返工。希望这篇复盘对你有用。
返回列表