
简介一套基于微信小程序的新闻资讯系统完整毕设/课程设计方案面向Java后端与小程序开发初学者可辅助在本地配置运行。系统后台采用SSM框架MySQL作为数据库小程序端使用微信开发者工具涵盖用户、新闻分类、新闻信息、视频分类与新闻视频等管理模块具备界面清晰、操作简单的特点。资源包共1136个文件约25.46MB包含java后端源码、vue后台管理页面、wxml/wxss小程序页面、js前端逻辑、png/svg图标素材、json配置文件以及SQL数据库脚本、环境配置与启动脚本结构完整便于部署。目前已有56人学习适合毕业设计选题、课程设计与就业项目练习可直接运行查看效果也可按注释和目录快速定位模块进行二次开发是快速搭建新闻资讯类小程序的实用参考。1. 为什么这个毕设题目值得被反复推荐把「微信小程序 Spring Boot 新闻资讯」三个词拆开看它其实覆盖了一条完整的移动端全栈链路前端要处理列表渲染、下拉刷新、页面跳转与登录态后端要面对接口设计、数据分页、图片存储和定时推送。大多数毕设题目只碰了其中一段而这个题目把两段都占了所以它天然适合用来展示「我能独立完成一个前后端分离的完整应用」这个叙事在开题和答辩时都很有说服力。另一个现实原因是它的资料密度高不管是从零写还是拿现成包改微信小程序的组件生态和 Spring Boot 的脚手架能力都能把工作量压到可控范围不会出现做不完的风险。学长学姐留下的项目包大多是这套组合也证明了它经得起答辩现场的追问。这篇文章就顺着「后端接口 → 小程序端对接 → 联调排错 → 答辩扩展」这条线把这个题目从项目包变成你能讲清楚的东西。2. 后端 Spring Boot把新闻数据变成小程序能读的接口毕设包里的 Spring Boot 项目本质上是一个数据服务端。小程序不能直连 MySQL所以后端要做的事情就三件把新闻数据从数据库里查出来、按前端需要的结构处理成分页JSON、通过 REST 接口暴露出去。下面从项目结构开始说。2.1 先看清一个新闻资讯系统的后端骨架如果你拿到的是一个标准包它的包结构一般长这样com.example.news ├── controller # 接收请求返回数据 ├── service # 业务逻辑 ├── mapper # 数据库查询MyBatis / MyBatis-Plus ├── entity # 数据实体类 ├── config # 跨域、拦截器等配置 └── common # 统一的返回体、异常处理这个分层不是随便分的答辩时候老师大概率会问「为什么要分 controller 和 service」。你得答得上来controller 只负责参数接收和结果包装不写具体业务service 处理新闻分类筛选、分页逻辑、点击量更新这类操作mapper 屏蔽 SQL 细节。这样无论前端是新闻列表、搜索还是分类页都调用同一套 service 方法不会出现同样的查询逻辑散落在多个接口里。项目包的依赖通常在pom.xml里核心是这几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency这里spring-boot-starter-web提供接口能力MyBatis-Plus 简化单表 CRUDLombok 通过注解省掉 getter / setter 的编写。如果你的包用的不是 MyBatis-Plus 而是原生 MyBatis也不要慌原理一样只是 SQL 要自己写在 XML 里。MyBatis-Plus 的核心优势是新闻表这种单表操作频率高的场景你甚至不用写任何 SQL继承一个BaseMapperT就能完成增删改查。2.2 新闻表怎么设计才扛得住答辩追问新闻资讯系统的数据量不可能大到哪里去所以表设计不用追求极端范式但字段是否合理、索引有没有加是答辩时的高频检查点。我常用的表结构是这样的CREATE TABLE news ( id int NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 新闻标题, summary varchar(500) DEFAULT NULL COMMENT 摘要, content text COMMENT 正文内容, cover_image varchar(500) DEFAULT NULL COMMENT 封面图URL, category_id int DEFAULT NULL COMMENT 分类ID, author varchar(50) DEFAULT NULL COMMENT 作者, view_count int DEFAULT 0 COMMENT 浏览量, is_top tinyint(1) DEFAULT 0 COMMENT 是否置顶, status tinyint(1) DEFAULT 1 COMMENT 状态0下架 1上架, publish_time datetime DEFAULT NULL COMMENT 发布时间, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category_publish (category_id, publish_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;cover_image为什么用 URL 而不是存图片因为小程序的image组件只能加载 URL你不能把图片二进制塞进 JSON 响应里。在开发阶段图片可以直接放在 Spring Boot 的static目录下通过http://localhost:8080/images/xxx.jpg访问上线后再换成对象存储或者服务器绝对路径映射这样设计的好处是后端代码一行不用改只改配置就行。category_id和publish_time联合索引是为了支撑首页最常见的场景按分类刷列表、按时间倒序排。如果你在列表接口里写了WHERE category_id ? ORDER BY publish_time DESC没有这个联合索引数据量超过几万条之后响应速度会肉眼可见地变慢。新闻类应用对实时性要求高所以这个索引值得加。2.3 Controller 层怎么写前端用起来最顺手前端小程序一般会这样调用接口GET /api/news/list?categoryId2pageNum1pageSize10。所以后端接口要固定返回格式不能有的接口返回数组、有的返回对象。我统一用这种结构Data public class ResultT { private Integer code; private String message; private T data; }然后新闻列表接口写成RestController RequestMapping(/api/news) public class NewsController { Autowired private NewsService newsService; GetMapping(/list) public ResultPageResultNews getNewsList( RequestParam(required false) Integer categoryId, RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize) { PageResultNews page newsService.getNewsPage(categoryId, pageNum, pageSize); return Result.success(page); } }categoryId用required false是因为首页需要「全部」分类这时候不带这个参数后端返回全部分类的数据pageNum和pageSize带默认值防止前端漏传导致报错。PageResult里我一般放四条total总数、pageNum当前页码、pageSize每页条数、list当前页数据。小程序端拿到这个结构才能正确渲染「上拉加载更多」时的页码累加。另外跨域问题会在联调阶段第一个冒出来。小程序开发者工具里发请求域名和端口和本地后端不同所以要在配置类里放开跨域Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns注意要用*通配如果只写http://localhost:8080真机预览时小程序的请求源不同接口会报 403。这个小配置卡住过不少刚跑毕设包的人。3. 微信小程序端从请求封装到新闻列表渲染后端接口就绪后小程序端要做的是把接口数据变成页面。微信小程序和普通网页最大的区别是它没有axios、没有fetch的完整实现、也不能直接操作 DOM一切数据驱动都是靠setData完成的。3.1 小程序目录结构和页面规划小程序页面通常落在pages/目录下一个新闻资讯系统的页面规划一般是miniprogram/ ├── app.js # 全局逻辑 ├── app.json # 全局配置注册页面和 window 样式 ├── app.wxss # 全局样式 ├── utils/ │ └── api.js # 封装 wx.request统一管理接口地址 └── pages/ ├── index/ # 首页新闻列表 轮播图 分类tab ├── detail/ # 新闻详情页 ├── category/ # 分类页 ├── search/ # 搜索页 └── my/ # 个人中心可做收藏列表app.json里要特别注意pages数组的第一项是启动页如果你看到的包启动后白屏先检查这一项是不是指向了pages/index/index{ pages: [ pages/index/index, pages/detail/detail, pages/category/category ], window: { navigationBarTitleText: 新闻资讯, navigationBarBackgroundColor: #d43c33, navigationBarTextStyle: white } }navigationBarTitleText会直接显示在顶部导航栏不同页面需要单独设置时可以在页面的.json文件里覆盖这是小程序和 H5 不一样的地方每个页面都可以有自己的配置文件粒度更细。3.2 封装 wx.request别让每个页面都写一遍 URL在utils/api.js里做一层请求封装是所有小程序项目的第一步。直接在每个页面的onLoad里调用wx.request不是不能跑但你的毕设要是只有一个列表页还好一旦加上搜索、分类、详情三个页面重复代码量就失控了答辩老师一眼就能看出来。// utils/api.js const BASE_URL http://localhost:8080 const request (url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${url}, method, data, header: { Content-Type: application/json }, success: (res) { if (res.data.code 200) { resolve(res.data.data) } else { wx.showToast({ title: res.data.message, icon: none }) reject(res.data) } }, fail: (err) { wx.showToast({ title: 网络请求失败请检查后端是否启动, icon: none }) reject(err) } }) }) } // 接口方法汇总页面只需 import 这些方法 export const getNewsList (data) request(/api/news/list, GET, data) export const getNewsDetail (id) request(/api/news/${id}, GET) export const getNewsSearch (keyword) request(/api/news/search, GET, { keyword })request函数返回的是Promise这样页面里就能用async/await替代回调嵌套。res.data.code 200这个判断对应后端 Result 结构里的 code如果后端包里的成功码不是 200 而是 0记得改这里这是项目包最常见的「逻辑不一致」坑——后端Result.success()写的是 code 0前端判断的是 200接口就一直走失败分支。3.3 首页新闻列表的完整渲染流程新闻列表页的核心逻辑是进入页面加载第一页数据触底时加载下一页下拉时刷新重置。下面是pages/index/index.js的关键部分Page({ data: { newsList: [], categoryList: [], currentCategory: 0, pageNum: 1, pageSize: 10, total: 0, hasMore: true, loading: false }, onLoad() { this.getCategoryList() this.getNewsList(true) }, async getNewsList(reset) { if (this.data.loading) return this.setData({ loading: true }) const pageNum reset ? 1 : this.data.pageNum try { const data await getNewsList({ pageNum, pageSize: this.data.pageSize, categoryId: this.data.currentCategory 0 ? undefined : this.data.currentCategory }) const list reset ? data.list : this.data.newsList.concat(data.list) this.setData({ newsList: list, total: data.total, pageNum: pageNum 1, hasMore: list.length data.total, loading: false }) } catch (e) { this.setData({ loading: false }) } }, onReachBottom() { if (this.data.hasMore) { this.getNewsList(false) } }, onPullDownRefresh() { this.getNewsList(true).finally(() wx.stopPullDownRefresh()) } })这里几个细节值得在答辩现场讲pageNum在成功响应后加一下一次触底用新页码newsList.concat(data.list)是追加下一页数据而不是覆盖hasMore通过list.length total判断因为最后一页 list 长度小于每页条数且累计长度不会是 total 的整数倍。onLoad里加载分类列表是为了顶部的 tab 切换切换分类时把currentCategory设置成对应 id重置页码从第一页重新拉数据。页面上拉加载更多不需要写任何scroll监听微信小程序提供了onReachBottom生命周期函数但前提是在页面配置里开启{ enablePullDownRefresh: true, backgroundColor: #eeeeee }如果下拉刷新和触底刷新都不生效最多的情况就是配置没写这个小坑卡住过大半跑毕设包的人。4. 联调与排错从 404 到真机预览的关键路径前后端都写完后第一个要面对的问题就是「数据能不能跑通」。因为小程序开发工具默认有域名校验本地调试时还会碰上局域网地址不通的问题。这一章按出现顺序排一下最常见的错误和处理方式。4.1 本地跑通的最小配置组合如果你用的是真实的小程序 AppID本地请求http://localhost:8080会被拦下来报错信息是「不在以下 request 合法域名列表中」。日常开发调通的组合是在开发者工具右上角「详情 → 本地设置」里勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」。勾上这一步本地开发才能发出请求。但这条捷径有副作用项目预览到手机时这部分勾选是不生效的所以在答辩前要么把接口换成 HTTPS 域名要么用「真机调试」功能临时绕过。常见做法是后者因为毕设里很少有人真的去买域名和证书。接口地址如果是http://localhost:8080后端本地启动没问题但真机预览就废了因为手机访问不到你电脑的 localhost。需要修改 BASE_URL 为局域网 IP比如const BASE_URL http://192.168.1.7:8080在命令行用ipconfigWindows或ifconfigmacOS查到电脑的局域网 IP然后替换掉。同时要确认后端application.yml里没有只绑定 127.0.0.1server: address: 0.0.0.0 port: 80800.0.0.0表示监听所有网卡接口这样局域网内手机才能访问到。只配port不配address时默认已经是 0.0.0.0但如果包里有特殊配置就要改回来。这个操作在手机端调试时是必做的否则真机打开所有列表都是空数据。4.2 三个高频报错按症状直接定位我在联调阶段遇到最多的报错集中在下面三类第一类是「request:fail」或者「errno: 600001」。这个报错说的是请求根本没发出去或者发出去了没收到响应。先确认后端有没有启动再去后端控制台看有没有请求日志。如果后端日志里压根没有记录问题基本出在BASE_URL本身——也许 URL 后面多了一个/或者端口写错了。我用一个笨办法验证直接在电脑浏览器里访问http://localhost:8080/api/news/list?pageNum1pageSize10看到 JSON 返回就说明后端没问题问题一定在小程序端到了配置或路径上。第二类是「errno: 600002」。域名不在合法列表里也就是前面说的没勾选「不校验合法域名」。或者是勾选了但代码在真机上调试真机不认这个勾选。临时做法是「真机调试」模式正式做法是注册开发者账号后把域名配置到小程序管理后台。毕业设计答辩一般用前者就够了。第三类是接口有响应但列表渲染出来是空白的。这时候用开发者工具右上角的「Network」面板看返回数据如果data.data.list确实有内容问题在 wxml 绑定的字段名对不上。比如后端返回的是coverImage前端写的却是cover_img渲染出来自然空白。遇到这种问题我一般把返回 JSON 在浏览器里格式化后对着字段名逐一核对 wxml 绑定表达式比盯着代码看有效得多。在联调过程中你最好保留一份「先用 Postman 或浏览器测后端再用小程序测前端」的流程这样能快速定位问题出在哪一端。5. 答辩现场才能用到的进阶打磨到了这一步系统已经能跑了但你要在答辩时拿到高分还需要针对「新闻资讯系统」这个业务场景做几个强化点。5.1 给系统加一个搜索防抖和状态恢复搜索页是新闻系统绕不开的模块但很多项目包里的搜索是「点击搜索按钮才发请求」的笨实现。实际上你可以直接用小程序提供的input事件配合防抖let searchTimer null handleInput(e) { const keyword e.detail.value if (searchTimer) clearTimeout(searchTimer) searchTimer setTimeout(() { this.performSearch(keyword) }, 500) }防抖的意图很直接用户在输入过程中每敲一个字符都会触发input事件如果每个事件都发请求后端就会被无效请求塞满。500ms 的延时确保用户停顿下来才真正发请求体验上有「即输即搜」的顺畅感性能上又可控。这里只有 30 行代码的差距但答辩老师对你的代码风评会立刻不同。搜索页还有一个常见需求从详情页返回时希望搜索关键词还在输入框里。这个用wx.setStorageSync在输入时存下来页面onLoad时读出来填回input的值就行。5.2 把新闻详情页的「点击量」做成实时更新新闻详情页几乎每个项目包都有浏览量展示但多数是写死的假数据。你可以加一个接口调用逻辑详情页onLoad时拉取详情数据正常展示内容在onHide或onUnload时静默调一次浏览量 1 的接口。这个设计在答辩时可以说成「用户打开行为埋点」听起来专业度上升一个档次。技术上只需要在wx.request的失败回调里写空函数不打扰用户即可。值得注意的边界是onHide在用户切后台或者跳转到微信其他页面时也会触发如果这个系统未来要做精确统计需要在小程序后端加 session 或标识去重但对毕设场景就不用考虑那么深了。你可以在答辩时说清楚这个限制在哪里反而是加分项。另一个可以讲清楚的点是新闻详情页跳转到相关新闻。小程序内页面跳转用wx.navigateTo或navigator组件比如navigator url/pages/detail/detail?id{{item.id}} hover-classnone text{{item.title}}/text /navigator在小程序里页面栈最多只能叠十层如果你的跳转链条是「首页 → 详情 → 相关新闻 → 相关新闻」超过十层时navigateTo会静默失败。要么在跳转前用getCurrentPages()检查栈深度要么直接把navigator的delta配置好。这也是为什么新闻资讯类小程序很少做深层跳转这就是真正把业务和技术结合在一起来聊的例子老师会觉得你真的在做一个产品而不只是调通了接口。最后确认一点整个系统跑通后把后端打成 jar 包启动小程序勾掉「不校验合法域名」后自然失败这是正常的。毕设演示前用「真机调试」或者把后端部署到一台 HTTPS 服务器它们之间二选一作为演示方案就好。本文还有配套的精品资源点击获取