简介:面向计算机类毕业设计的微信小程序网络安全科普系统源码包,后端采用Spring Boot,搭配小程序端构成前后端分离项目,适合网络安全、人工智能、计算机科学与技术等专业用于毕业设计课题或课程作业参考。资源共985个文件,约63.35MB,主要包含Java后端源码、Vue管理端组件、微信小程序页面(WXML/WXSS/JS)、PNG/SVG素材图、SQL数据库脚本及BAT/CMD运行脚本,可按目录快速定位前后端各模块。当前已有64人学习。源码经严格测试,可放心运行,下载后借助README或各模块说明即可完成部署,便于研究小程序与Spring Boot的接口交互、系统分层设计以及科普内容展示逻辑;压缩包中同时含大量页面素材与多类型配置文件,有助于理解界面结构、接口参数和运行环境,适合作为论文写作、代码复现和答辩演示的完整参考。
1. 微信小程序与Spring Boot的网络安全科普系统:毕设选题怎么一次过
毕业设计选题表摆在面前,导师一句“做个微信小程序吧,最好跟网络安全沾点边”,于是你搜到了这个标题:微信小程序网络安全科普系统开发与设计 + Spring Boot后端毕业源码案例设计。说白了,这个项目就是把密码安全、防钓鱼、个人信息保护这些安全常识装进一个小程序,用户打开就能看科普文章、做安全答题、收藏感兴趣的内容;后端用Spring Boot提供数据和业务逻辑,小程序端只负责展示和交互。它适合计算机类、软件工程类学生做毕设,也适合想通过前后端分离项目实战练手的人。技术门槛不高,但五脏俱全,从登录鉴权到分页列表再到答题交互,每样都有东西可讲,工作量也控制在两三个月能完成的范围。
2. 系统架构拆解:小程序端、Spring Boot后端与接口设计
2.1 功能模块划分:内容展示、在线答题、用户中心三个模块就够答辩
毕业设计最怕的就是把系统设计得又大又空。这个标题里“科普系统”四个字,决定了核心功能就是内容展示,再加一个答题互动来体现“不只是静态资讯”的差异化。
我一般会把功能切成三块。第一块是科普内容浏览:文章按分类组织,比如密码安全、防钓鱼、个人信息保护、移动设备安全,用户进入首页看到轮播图和文章列表,点进详情页看正文。第二块是在线答题:从题库里随机拉一组题目,一题一题做,最后给出得分和错题解析,这个交互在答辩演示时比单纯翻列表有看点。第三块是用户中心:微信登录后能看到自己的答题记录和收藏列表。
收藏功能可以单独算一个小模块,也可以并在用户中心里。别小看这三块,它们覆盖了增删改查的绝大多数形态:列表查询、详情查询、用户关联数据写入、状态变更。答辩时老师问“你的系统有哪些功能”,你按这三块讲,逻辑是清晰的。
2.2 技术选型:原生小程序 + Spring Boot 2.7 + MyBatis-Plus
前后端分离是这个项目的默认姿势,但分离到什么程度要做好取舍。小程序端我推荐用原生微信小程序框架,不要用uni-app。原因很直接:uni-app帮你把WXML、WXSS、JS封装了一层,你答辩时被问到“页面是怎么渲染出来的”,如果只答得出“uni-app帮我们转了”,场面会很尴尬。原生框架虽然写起来啰嗦一点,但每一行代码对应的产物你都能讲清楚,这对毕设答辩是实打实的加分项。
后端方面,Spring Boot是标题里写死的,问题只在于版本。这里有一条硬经验:用Spring Boot 2.7.x,不要用3.x。3.x要求JDK17,而学校机房、大多数毕业生的本地环境还是JDK8。JDK8跑Spring Boot 3.x的项目会直接报UnsupportedClassVersionError,这种环境问题在答辩现场非常难解释。数据库用MySQL 8.0,ORM选MyBatis-Plus 3.5.x。
选MyBatis-Plus而不是Spring Data JPA有一个很实际的原因:分页。MP的分页插件写起来直观,而且答辩时老师大概率会问“分页怎么实现的”,你可以直接说用的是MyBatis的分页插件,它会在执行前拦截SQL并改写为带LIMIT的查询。这个话题比“JPA帮我封装好了”要扎实得多。
2.3 接口规范:统一返回格式与分页约定
前后端联调最怕各写各的,接口格式一定要在最开始定死。后端所有接口统一返回一个Result对象,结构是三个字段:code、msg、data。code为0表示成功,400表示参数错误,401表示未登录,500是服务端异常。小程序端拿到响应后只看code,不看HTTP状态码,这样错误处理逻辑可以收敛到一个地方。
分页参数统一叫pageNum和pageSize,pageNum从1开始。列表接口的响应里带上total字段,方便小程序端计算还有没有下一页。接口路径按模块分:/api/auth/开头的登录相关,/api/article/开头的文章相关,/api/quiz/开头的答题相关。统一前缀的好处是后端做拦截器时可以直接按路径过滤,比如拦截除了登录接口以外的所有/api/请求。
接口清单大致长这样:
| 接口 | 方法 | 入参 | 返回 |
|---|---|---|---|
| /api/auth/login | POST | code | token、用户信息 |
| /api/article/list | GET | pageNum、pageSize、category | 分页列表 |
| /api/article/detail | GET | id | 文章全文 |
| /api/quiz/list | GET | size | 题目数组 |
| /api/quiz/record | POST | score、total | 保存结果 |
| /api/favorite/add | POST | articleId | 收藏成功 |
这套接口文档不需要额外工具,写在代码注释里或者整理成一个Markdown表格放到项目根目录,答辩时拿出来给老师看,比直接在代码里翻要体面得多。
3. 数据库设计:从建表到后端登录与内容接口落地
3.1 五张表撑起整个系统:用户、文章、题目、答题记录、收藏
这个项目的数据量不大,五张表就够了,但每张表的字段选择都有讲究。用户表不要存密码,因为你走的是微信登录,后端只需要存openid,它才是微信体系里用户的唯一标识。文章表要单独存分类字段,不要在正文里打标签查——分类查询是列表页最频繁的操作。题库表里的选项我建议用一个字段以英文分号分隔储存,比单独建一张选项表更符合毕设的体量。
下面是建表SQL,可以直接跑在MySQL 8.0上:
CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信openid,唯一标识', `nickname` varchar(64) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `t_article` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(128) NOT NULL COMMENT '标题', `category` varchar(32) NOT NULL COMMENT '分类:pwd/phishing/privacy/device', `cover` varchar(255) DEFAULT NULL COMMENT '封面图URL', `content` text COMMENT '正文HTML', `view_count` int(11) DEFAULT 0 COMMENT '浏览量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间', PRIMARY KEY (`id`), KEY `idx_category` (`category`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科普文章表'; CREATE TABLE `t_quiz` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `question` varchar(255) NOT NULL COMMENT '题干', `options` varchar(255) NOT NULL COMMENT '选项,用英文分号分隔', `answer` tinyint(4) NOT NULL COMMENT '正确答案下标,0开始', `analysis` varchar(500) DEFAULT NULL COMMENT '题目解析', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='题库表'; CREATE TABLE `t_quiz_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '用户ID', `score` int(11) NOT NULL COMMENT '答对题数', `total` int(11) NOT NULL COMMENT '总题数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '答题时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='答题记录表'; CREATE TABLE `t_favorite` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '用户ID', `article_id` bigint(20) NOT NULL COMMENT '文章ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_article` (`user_id`, `article_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏表';这里有几个细节值得说明。第一,t_user表的openid加了唯一索引,因为微信登录时要用openid查用户,没有索引的话数据量一上来就会全表扫描。第二,t_favorite表加了联合唯一索引,防止同一个用户对同一篇文章重复收藏,接口层再判断一次会多一次查询,直接在数据库层兜底反而更稳。第三,t_quiz表的answer字段用下标而不是选项内容,因为选项内容可能修改,而下标是稳定的。第四个点是所有表的字符集都用utf8mb4,MySQL 8.0默认也是这个,不要用utf8,否则存emoji表情会报错。
3.2 Spring Boot工程初始化:pom.xml与分层目录
工程初始化建议直接用Spring Initializr生成,选Java 8、Spring Boot 2.7.x,依赖勾选Spring Web、MySQL Driver、Lombok。MyBatis-Plus和JWT的工具包之后手动加到pom.xml里。这一步如果选了Spring Boot 3.x,后面所有javax开头的包都要改成jakarta,对新手来说就是一堆莫名其妙的编译错误。
pom.xml里关键依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>版本号这里要特别确认一下:jjwt 0.9.1用的是javax包,和Spring Boot 2.7的javax体系一致,千万别配到Spring Boot 3.x项目里。MyBatis-Plus 3.5.3.1这个版本对Boot 2.x的支持是稳定的,分页插件需要额外加一个配置类,下面这段是MybatisPlusConfig,注意要加@MapperScan注解:
@Configuration @MapperScan("com.example.securityedu.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(50L); interceptor.addInnerInterceptor(pagination); return interceptor; } }setMaxLimit(50L)是个容易被忽略的细节,它限制了单页最大50条,防止有人把pageSize传成10000把你的接口拖垮。答辩时老师问“分页怎么做”,你指一下这个拦截器,然后说“它会在SQL执行前自动拼LIMIT”,这一问就过了。
3.3 微信登录实现:code2Session与JWT鉴权(个人主体别碰手机号授权)
小程序端登录流程是这样的:前端调用wx.login获取一个临时code,把它发给后端;后端拿这个code去微信的code2Session接口换openid和session_key;有了openid,查用户表,新用户就插入一条记录;最后用openid生成一个JWT返回给前端。
这里必须提醒一个毕设高频翻车点:微信小程序登录获取手机号的能力,个人主体小程序是不开放的。很多学生看教程里写着“获取手机号”,就照抄到毕业设计里,结果真机预览时按钮始终弹不出授权,原因是个人主体没有这个接口权限。所以毕设里登录只做到openid识别就够了,答辩时主动说一句“个人主体无法使用手机号快速验证组件,所以采用openid作为用户唯一标识”,这反而是加分项。
后端登录接口代码大致如下:
@RestController @RequestMapping("/api/auth") public class AuthController { @Value("${wx.appid}") private String appId; @Value("${wx.secret}") private String secret; @Autowired private UserMapper userMapper; @Autowired private JwtUtil jwtUtil; @PostMapping("/login") public Result login(@RequestBody LoginRequest req) { // 1. 用 code 换取 openid String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId + "&secret=" + secret + "&js_code=" + req.getCode() + "&grant_type=authorization_code"; // 这里用 HttpClient 或 Hutool 的 HttpUtil 发起 GET 请求 String response = HttpUtil.get(url); JSONObject json = JSONUtil.parseObj(response); String openid = json.getStr("openid"); if (StrUtil.isBlank(openid)) { return Result.error("登录失败:" + json.getStr("errmsg")); } // 2. 查用户,不存在则创建 User user = userMapper.selectOne( new LambdaQueryWrapper<User>().eq(User::getOpenid, openid)); if (user == null) { user = new User(); user.setOpenid(openid); user.setCreateTime(new Date()); userMapper.insert(user); } // 3. 签发 JWT String token = jwtUtil.generateToken(user.getId(), user.getNickname()); return Result.success(token); } }微信的code2Session接口返回的errmsg字段做了一次兜底,联调时如果code用错或者secret配错,这个errmsg会把具体原因带出来,省得瞎猜。appid和secret放在application.yml里,不要写死在代码中,这是最基本的习惯。JWT生成时以用户ID为主体,过期时间建议设7天,小程序端把token存到wx.setStorageSync,之后每次请求都在header里带上Authorization字段。
3.4 内容列表与详情接口:分页查询与SQL防注入
文章列表接口是整个系统里最常被调用也最容易被问到的接口。用MyBatis-Plus实现分页非常直接,不需要手写XML:
@GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String category) { Page<Article> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>(); if (StrUtil.isNotBlank(category)) { wrapper.eq(Article::getCategory, category); } // 列表页只需要这些字段,content大字段不要查出来 wrapper.select(Article::getId, Article::getTitle, Article::getCategory, Article::getCover, Article::getViewCount, Article::getCreateTime); wrapper.orderByDesc(Article::getCreateTime); articleMapper.selectPage(page, wrapper); return Result.success(page); }wrapper.select那行很多人会漏掉。文章表里有content字段,存的是一整篇HTML,可能几千字,列表页根本用不到它。如果每次都把content查出来,接口响应体积会大好几倍,小程序端渲染也会变慢。详情接口再单独把content带出来就行。页面上展示的浏览量和“xxx人已阅读”都是这个view_count字段,可以在详情接口里自增。
关于SQL注入这个问题,无论答辩老师问没问,你都要主动展示:MyBatis-Plus的条件构造器默认就是预编译的,不存在拼接风险。如果你在Mapper里手写SQL,记住一个死规矩——用#{}不用${}。前者是预编译占位符,后者是字符串拼接。网络安全科普系统的后端如果在SQL注入上栽跟头,答辩现场会很尴尬。
4. 小程序前端:把科普内容和题库装进手机
4.1 页面结构规划:底部TabBar加四个核心页面
小程序端目录结构按功能划分,不建议把页面堆成一团。我的习惯是底部TabBar放两个入口,首页和我的,文章列表和答题作为首页里的子页面跳转。如果你把四个页面全塞进TabBar,底部会显得挤,而且答题页和文章详情页这类二级页面不需要常驻入口。
典型的小程序目录如下:
miniprogram/ ├── app.js # 全局逻辑,存放globalData和登录入口 ├── app.json # 页面注册、window配置、tabBar配置 ├── app.wxss # 全局样式 ├── utils/ │ ├── request.js # 封装的请求方法 │ └── util.js # 工具函数 └── pages/ ├── index/ # 首页 ├── article/ # 文章列表和详情 ├── quiz/ # 在线答题 └── mine/ # 个人中心,含收藏和答题记录app.json里window的navigationBarTitleText会直接影响小程序顶部导航栏高度的表现。如果使用默认导航栏,文字内容不同高度不变,不用处理;如果为了视觉效果做自定义导航栏,就要用wx.getMenuButtonBoundingClientRect()获取胶囊按钮的位置,再结合状态栏高度计算导航栏高度。不同机型的胶囊位置不一样,iPhone的刘海屏和普通安卓机差了有20个像素左右,这块做适配是个独立的小难点,有这个心力的同学可以去调,默认导航栏方案可以完全避开。
4.2 封装request请求:域名白名单、Token传递与统一错误提示
在小程序里直接调wx.request会写出很多重复代码,而且有四个坑是绕不过去的:开发阶段域名校验会拦截本地请求、每个请求都要带token、后端返回401时要做统一处理、接口失败时要有统一的Toast提示。把这些逻辑收敛到一个工具函数里,是所有页面复用的基础。
我写的request封装长这样:
const BASE_URL = 'http://localhost:8080' const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: BASE_URL + url, method, data, header: { 'Authorization': token }, success(res) { if (res.data.code === 401) { // token失效,先清掉本地缓存,再重新走登录 wx.removeStorageSync('token') wx.showToast({ title: '请先登录', icon: 'none' }) reject(res.data) return } if (res.data.code === 0) { resolve(res.data.data) } else { wx.showToast({ title: res.data.msg, icon: 'none' }) reject(res.data) } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) } module.exports = { get: (url, data) => request(url, 'GET', data), post: (url, data) => request(url, 'POST', data) }header里挂Authorization字段,和后端拦截器对应。这里有个实际经验:后端拦截器只拦截/api/下的请求,并且放行/api/auth/login,否则第一次登录时token还没拿到就带着空token去访问登录接口,会被自己的拦截器拦下来。BASE_URL在开发阶段用http://localhost:8080,在开发者工具里勾选“不校验合法域名”,真机预览时改用电脑的局域网IP,这两条在第5章展开讲。
4.3 首页与内容列表:轮播图、分类切换、下拉刷新与触底加载
首页是用户看到的第一屏,也是答辩演示的封面。结构上从上到下依次是轮播图、分类导航、文章列表。轮播图可以用wx的swiper组件,数据可以从后端接口专门拉一个recommend字段,也可以直接取文章列表的前三篇。
文章列表用onPullDownRefresh和onReachBottom两个页面生命周期来做下拉刷新和触底加载。这里有一个必踩的坑:触底加载时如果上一个请求还没回来,又触发了下一次请求,列表会出现重复数据。我的做法是加一个loading状态,请求期间置true,请求回来置false,在onReachBottom里先判断这个状态。
data: { articleList: [], pageNum: 1, pageSize: 10, total: 0, loading: false, currentCategory: '' }, onPullDownRefresh() { this.setData({ pageNum: 1, articleList: [] }) this.fetchList().then(() => wx.stopPullDownRefresh()) }, onReachBottom() { if (this.data.loading) return // 防止重复请求 if (this.data.articleList.length >= this.data.total) return // 没有更多了 this.setData({ pageNum: this.data.pageNum + 1 }) this.fetchList() }, fetchList() { this.setData({ loading: true }) return api.get('/api/article/list', { pageNum: this.data.pageNum, pageSize: this.data.pageSize, category: this.data.currentCategory }).then(res => { this.setData({ articleList: this.data.articleList.concat(res.records), total: res.total, loading: false }) }) }concat不能写成直接覆盖,否则下拉刷新和翻页会互相打架。total判断放在触底加载的最前面,列表已经到头时不再发请求,这个小细节决定了你的接口不会被无效流量打爆。分类切换其实就是重新设置currentCategory,然后重置pageNum为1再fetchList。
4.4 在线答题页:单选交互、计分与结果保存
答题页的交互逻辑不复杂,但涉及多个状态:当前题号、用户选中的选项、得分、是否已提交。我建议大家用view加data-idx来实现单选交互,不要用radio组件。radio组件的样式定制麻烦,而且取值回传有一套自己的事件机制,对新手来说不如直接在一个view上绑定点击事件来得可控。
data: { quizList: [], current: 0, selected: -1, score: 0, finished: false }, onLoad() { api.get('/api/quiz/list', { size: 10 }).then(res => { this.setData({ quizList: res }) }) }, choose(e) { const idx = e.currentTarget.dataset.idx this.setData({ selected: idx }) }, next() { if (this.data.selected === -1) { wx.showToast({ title: '请先选择一个答案', icon: 'none' }) return } const quiz = this.data.quizList[this.data.current] if (this.data.selected === quiz.answer) { this.setData({ score: this.data.score + 1 }) } if (this.data.current + 1 >= this.data.quizList.length) { this.submitRecord() } else { this.setData({ current: this.data.current + 1, selected: -1 }) } }, submitRecord() { api.post('/api/quiz/record', { score: this.data.score, total: this.data.quizList.length }).then(res => { this.setData({ finished: true }) }) }答题页还有一个容易忽略的细节:最后一道题做完之后,要展示得分和错题解析。建议在finished状态为true时,用wx.showModal把得分弹出来,点击确定后跳转到结果页或回到首页。结果页如果单独做,就是一个展示所有题目和你所选答案的列表,技术含量不高但工作量多,时间不够可以不做。计分逻辑放前端有一个天然的理由:题目和答案都在前端包里,用户本来就能看到答案,这个系统的定位是科普不是考试,不存在防作弊问题。答辩时如果老师问“分数在前端算的靠谱吗”,你把这个理由一说,对方就能理解。
5. 联调与部署避坑指南:毕业设计最容易翻车的5个排查点
5.1 小程序连不上本地后端:域名校验与局域网IP
现象:开发者工具里wx.request一直报fail,或者转圈几秒后提示“request:fail url not found”,但后端控制台里根本没收到请求。原因:小程序对请求域名有校验,开发模式下默认不校验合法域名这个选项没勾选;真机调试时,手机访问不了电脑的localhost。
解决:开发者工具右上角“详情-本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”。注意这个选项只对开发者工具生效,真机预览时必须把BASE_URL从localhost改成电脑的局域网IP,比如http://192.168.1.108:8080,同时后端启动时配置server.address为0.0.0.0,这样才能从局域网访问。
5.2 Spring Boot版本太高导致JDK8启动失败
现象:pom.xml里填了spring-boot-starter-parent 3.2.0,启动时控制台报java.lang.UnsupportedClassVersionError: org/springframework/boot/loader/LaunchedURLClassLoader has been compiled by a more recent version of the Java Runtime。原因:Spring Boot 3.x要求JDK17,你用JDK8跑自然启动不了,而且3.x的javax.servlet全部改成了jakarta.servlet,即使强行编译也会有一堆包不存在。解决:回到Spring Boot 2.7.18,和JDK8是稳定搭配。如果导师要求用新版本,就换JDK17,但建议先把本地环境统一成一套版本,别在环境上耗费答辩时间。
5.3 微信登录code只能使用一次:写死code导致登录偶发失败
现象:登录接口第一次调用成功,之后反复调用同一个code时报错,错误信息是invalid code。原因:wx.login生成的code是一次性的,使用过一次之后立刻失效。很多学生为了方便测试,把控制台里拿到的code复制出来写死在代码里,前两次能通,再试就必然失败。解决:前端每次登录前都要重新调用wx.login,把新code传给后端。code的有效期只有5分钟,而且还是一次性的,这也是为什么登录接口不能设计成允许前端传入固定code的原因。
5.4 MyBatis-Plus自动填充失效:insert时create_time为空
现象:新用户插入t_user表后,create_time字段是null,但建表SQL里明明设置了DEFAULT CURRENT_TIMESTAMP。原因:MyBatis-Plus在执行insert时,如果实体类的createTime字段为null,会把这个字段的默认值null拼进SQL,覆盖了数据库的默认值策略。并非所有后端同学都知道这一点,新手最容易在这上面花掉半天。解决:实体类字段上配置@TableField(fill = FieldFill.INSERT),然后写一个MetaObjectHandler实现类,在insertFill方法里设置createTime为当前时间。
5.5 小程序列表渲染富文本HTML显示为源码标签
现象:文章详情页用{{article.content}}绑定数据,页面上显示的不是排版后的文章,而是一堆带着尖括号的HTML标签。原因:小程序没有webview那样的v-html机制,把HTML字符串直接当作文本插值渲染了。解决:用rich-text组件,把content绑定到nodes属性上:
<rich-text nodes="{{article.content}}"></rich-text>这个组件支持HTML标签部分渲染,图片、加粗、段落都能显示。唯一的坑是rich-text的nodes不支持的标签会被过滤,比如iframe、audio,所以录入文章时尽量只用基础的p、h1、h2、img、ul这几个标签。我在内容录入时还会做一次简单的HTML清洗,把用户可能粘贴进来的script标签去掉——作为网络安全科普系统,自己后台上传的内容里如果夹带脚本,那就成笑话了。
6. 交付与答辩:一篇能过审的论文和一场不出事故的演示
源码能跑只是第一步,毕业设计的最终载体是论文和答辩。我见过太多代码写得不错、却在论文和演示环节翻车的例子,这里把交付阶段的准备工作拆开讲。
论文的结构不要自己发明,按学校给的模板走,但各章节的落笔重点可以这样分配:绪论里讲清楚“为什么是微信小程序”和“为什么是网络安全科普”——小程序触达成本低,安全科普知识又恰恰是普通用户最缺乏的,这两个理由就构成了选题意义。需求分析部分把第2章的三个功能模块展开成用例图,每个用例写2到3条描述。系统设计部分重点放架构图和数据库表结构说明,ER图是必备的。系统实现部分是论文里最好写的,每个功能模块放一张小程序截图加一段核心代码,配3到4句说明。测试部分不要只写“测试通过”,列一张测试用例表,包含功能测试和兼容性测试,用不同机型跑一遍记录结果。
答辩演示的顺序比内容更重要。我建议的演示流程是:先打开小程序首页停留两秒,让老师看到整体界面;点一篇科普文章进详情页,展示富文本排版;回到首页切换分类,展示列表刷新;进入答题页,完整做一道题,让老师看到选中选项的反馈和下一题的切换;最后答完题展示得分弹窗,再去“我的”页面展示答题记录。全程最多5分钟,但每一个核心功能都过了一遍。
答辩被问频率最高的三个问题是:为什么选这个课题、系统有什么创新点、项目难点是什么。创新点建议从“科普形式+互动答题”入手,不要硬扯算法和技术架构。难点就答真的难点——小程序登录流程、触底加载的请求去重、富文本在小程序端的渲染兼容,这些都是你在开发中实际解决过的,讲细节反而最抗追问。另外,既然做的是网络安全科普系统,后端代码里要有意识地体现安全设计:登录用JWT而不是明文口令、SQL全部预编译、上传内容做HTML清洗,哪怕只做到其中两点,这也是系统与纯增删改查项目的本质区别。我在交付前最后做的一件事,是把数据库里预置的演示数据进行核对,确保每张表都有内容。见过有同学答辩前全库测试删得一干二净,打开小程序首页一片空白,那种沉默的十几秒是最难熬的。希望帮到你。
本文还有配套的精品资源,点击获取