又是一年毕业设计季,看到不少人在问“有没有既好做、又有讲头、老师还不挑刺的题目”。今天要聊的这个题,算是同类里比较有代表性的:SpringBoot + Vue + MySQL 三大件,业务方向落在“经方药食两用服务平台”。你拿到手是一套能跑的源码、数据库脚本、毕业论文和部署文档,但光会跑还不够,折腾清楚里面的设计逻辑,答辩和以后工作才算真的学到东西。
这个平台说白了就是一个前后端分离的“经方 + 药食两用”信息管理展示站:用户能按分类、关键词检索经方和食材,看详情、做收藏、写笔记;管理员在后台上架内容、管理用户、发布公告。适合正在做Java Web毕设的同学参考,也适合想低成本搭一个中医药文化展示系统的个人开发者。下面我从设计拆解、数据库建模、核心实现、部署踩坑到答辩准备,把整个项目从头到尾捋一遍。
1. 项目价值拆解与整体设计思路
1.1 为什么选经方药食两用这个方向
毕设选题有个隐藏逻辑:技术难度要适中,业务要有辨识度,还得让答辩老师觉得这个题目“有意义”。
常见的图书管理系统、新闻发布系统、学生选课系统,技术上没问题,但每年交上去的太多了,老师看一眼标题就没了兴趣。经方药食两用服务平台不一样,它踩中了两个点:一是中医药文化这个热点话题,二是“药食同源”这个概念和日常饮食能联系起来。哪怕自己不学医,也能把内容管理做得像那么回事。
经方是中医经典方剂的统称,张仲景那一路传下来的方子,比如桂枝汤、麻黄汤这类。药食两用指的是既有食用价值又有药用价值的食材,比如山药、生姜、枸杞、大枣这些,国家层面还有药食同源目录可以参考。这个平台不是做在线问诊,也不开药方,它的核心定位是“知识的分类整理与查询”:把经方里涉及的药材、功效、禁忌和组织食用信息结构化,让用户能查、能看、能收藏。这个边界划清楚之后,技术上就好控制了。
1.2 技术栈选型的逻辑
很多人做毕设一上来就堆技术,Redis、RabbitMQ、微服务全套齐上,结果把自己卡死在中间件配置上。我一直觉得毕设阶段“够用且能讲清楚”比“多而杂”更重要。
后端选 SpringBoot 是没悬念的。内置 Tomcat,起步依赖简单,打包直接一个 jar,部署方便,写接口快。用 Maven 管理依赖,版本锁定清楚,另一台电脑也能复现环境。没有把微服务拆进来,因为单机业务场景根本用不上,拆了反而增加请求链路的复杂度。
前端选 Vue 同样是为了开发效率。Vue 的组件化写法适合这种“后台管理 + 用户展示”的场景,路由一套,页面之间的跳转很直接。构建工具用 npm 生态,开发环境跑 dev server,生产环境构建出静态文件放到 Nginx 或直接扔进 SpringBoot 的 static 目录,两头都能跑。
数据库选 MySQL 的理由更简单:关系型数据天然适合存用户、经方、药材这些有明确关联的记录;导出 sql 脚本就是交付物的一部分,老师开箱即验;网上资料多,出了问题能搜到答案。
1.3 前期设计避开了哪些坑
第一个坑是过度设计。一开始没有引入 Redis 缓存、没有搞分布式会话、没用消息队列。一个校园级并发量,单机完全扛得住。做起缓存和队列,部署文档、答辩问题、排错难度全都上一档,对毕设来说是吃力不讨好。
第二个坑是前后端联调时的数据格式不统一。项目里在早期就定了一个全局 Result 返回体,所有接口统一返回 code、message、data 三段式结构。这样做的好处是前端 axios 拦截器可以统一处理错误码,不用每个页面写一遍解析逻辑。后续如果有人要改接口,也只需要在统一返回体里调整,页面基本不动。
第三个坑是“经方和药材的关系”到底怎么建模。经方不是一段纯文本,一个方子由多味药材组成,每味药材有详细属性。如果只用一个“组成”字段存字符串,查询和统计会非常难做;但如果拆成多对多关联表,查询时又要联表。这个取舍一定要在开发前提想清楚,下文数据库设计部分会详细展开。
2. 核心功能拆解与数据库设计
2.1 用户端有哪些实用模块
用户端是平台的门面,功能设计上覆盖了“检索、阅读、收藏、记录”完整闭环。
经方库是这个平台的主干模块。列表页支持按分类筛选,比如解表方、清热方、补益方,也可以直接搜索方名。点击进入详情页,展示方剂组成、用法用量、功效主治、禁忌注意、经典出处。组成里每一味药材可以继续点击,跳转到对应药材详情。
药食两用食材库是另一个核心模块。每一味食材维护了性味、归经、功效、适用人群、禁忌等字段。这个模块特别适合做成图鉴风格,配一张食材图片,信息分卡片展示。
检索与筛选模块承担全站搜索功能。用户可以按名称模糊查,也可以按功效标签查,比如想找“健脾胃”的食材,直接点标签就能筛出来。检索结果分页展示,也可以按热度排序。
收藏与笔记模块帮助用户沉淀内容。用户登录后可以收藏经方和食材,收藏列表单独成页。用户还能给自己的收藏加备注,简单记录自己尝试过的食用方式或注意事项。这样平台就不只是查完即走的工具,还有一个轻量的“个人数据库”。
公告资讯模块负责展示平台动态,比如目录更新说明、饮食指南类内容。资讯走后台发布,用户端只需读取列表和详情。
2.2 管理端该做什么
管理端不做复杂权限体系,但至少要有两级:管理员和普通用户。管理员维护核心业务数据,普通用户正常浏览收藏。
管理端第一个核心功能是经方管理。列表、新增、编辑、删除、上下架。经方编辑页需要支持对组成的动态维护,一行就是一个药材加对应用量,可以增删行。这个交互在 Vue 里用动态表单实现,不复杂但要细心。
食材管理同样如此,只不过字段顺序和经方不同,多了性味、归经这种专属属性。分类管理用来维护经方分类、食材分类和标签,分类变化后,前端检索的筛选项也会跟着变。
用户管理模块做最简单的用户列表、状态禁用、角色设置。禁用用户后,该用户登录直接提示账号不可用。
数据统计是可选的加分模块。可以从收藏表里统计热门经方、热门食材,做成柱状图或饼图。毕设里用 ECharts 画两张图表,答辩时往屏幕上一放,老师会觉得你不是只做了一套增删改查。
2.3 核心数据库表设计思路
数据库设计是这个项目最重要的地基。表设计合理,后面写 Mapper 会很顺手;表设计不合理,改接口的时候想哭都来不及。
用户相关做最基础的两张表:用户表和角色表,再加一张用户角色关联表。
用户表 sys_user - id 主键 - username 用户名,唯一索引 - password 密码,存BCrypt加密串 - nickname 昵称 - avatar 头像地址,可空 - status 状态,1正常 0禁用 - create_time 创建时间经方相关建议至少做三张表:经方主表、药材表、经方药材关联表。
经方表 formula - id - name 方名 - category_id 分类id - origin 出处 - composition_text 组成速览(用于列表展示) - usage_text 用法 - function_text 功效 - indications_text 主治 - taboo_text 禁忌 - image 插图地址 - status 1上架 0下架 - view_count 浏览量 - create_time / update_time药材表 herb - id - name 药材名 - alias 别名 - flavor 性味 - meridian 归经 - efficacy 功效 - edible_method 食用方法 - suitable_people 适用人群 - taboo 禁忌 - image - status - create_time经方药材关联表 formula_herb - id - formula_id - herb_id - dosage 用量描述,比如“三两”“9克” - sort 排序字段这里把“组成”单独拆成关联表而不是塞一个字符串,是为了能做两件事:一是经方详情页展示结构化组成,二是将来做反向检索,比如用户选了几味药材,反查哪些经方包含它们。这是这套数据库相对真正系统的地方。
收藏表和笔记表可以考虑合并成一张用户收藏表,加一个 remark 字段既存收藏状态也存笔记内容。
用户收藏表 user_favorite - id - user_id - content_type 1经方 2食材 - content_id 对应业务表id - remark 笔记内容 - create_time - 唯一索引(user_id, content_type, content_id)分类表、标签表和公告表属于支撑表。分类表主要服务经方和食材的分类筛选,标签表服务于功效标签检索。
2.4 这套表结构怎么支撑业务逻辑
模糊搜索这块,后端用 LIKE 匹配 name、alias、function_text 等字段。数据量小,就能跑通;数据量大了,后面可以换全文检索方案,表结构本身不受影响。
收藏统计的维度落在 user_favorite 表上。按 content_id 分组计数,就可以得到热门经方排行榜;按 user_id 分组,可以得到用户的活跃数据。
权限控制的实现主要靠 sys_user 里的角色字段或单独的 user_role 表。后端接口用 Spring AOP 或拦截器做角色校验,管理员请求带上 token 解析出角色,再看是否放行。
创建时间和更新时间统一用 datetime 类型,所有业务主键用自增 id。没有引入逻辑删除框架,删除就走物理删除,毕设阶段保持直观更重要。
3. 关键实现过程与实操细节
3.1 后端:统一返回体、异常处理和鉴权
后端一切从 Result 开始。下面这段是项目里最基础的结构定义,所有接口都走这套返回格式。
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("操作成功"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.setCode(code); r.setMessage(message); return r; } }登录鉴权选 JWT 而不是分布式会话,主要原因是答辩时比较好讲明白。用户提交用户名和密码,后端校验后签发一个 token,里面带 userId。前端每次请求在请求头里带上 Authorization,后端写一个拦截器验证 JWT 是否有效,有效则放行,并从 token 里解析出用户信息。
@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录、注册等白名单接口 String uri = request.getRequestURI(); if (uri.startsWith("/api/auth/") || uri.startsWith("/api/public/")) { return true; } // 校验token逻辑 String token = request.getHeader("Authorization"); // 解析token,失败则抛出未登录异常 return true; } }异常处理不要在每个 Controller 里 try-catch,用全局异常处理器统一接,省得每个接口都写重复代码。
3.2 经方检索的核心 SQL 和业务逻辑
经方检索是本系统最有查询代表性的接口。一个典型场景是:用户在前端选择“解表方”分类,同时输入关键字“桂枝”,期望筛出包含该分类和关键字的所有经方。
后端使用 MyBatis-Plus 的 LambdaQueryWrapper 来拼条件,逻辑清晰,也方便后续维护。
@Override public IPage<FormulaVO> searchFormulas(Integer categoryId, String keyword, Integer current, Integer size) { Page<Formula> page = new Page<>(current, size); LambdaQueryWrapper<Formula> wrapper = new LambdaQueryWrapper<>(); if (categoryId != null) { wrapper.eq(Formula::getCategoryId, categoryId); } if (StrUtil.isNotBlank(keyword)) { wrapper.and(w -> w.like(Formula::getName, keyword) .or().like(Formula::getCompositionText, keyword) .or().like(Formula::getFunctionText, keyword)); } wrapper.eq(Formula::getStatus, 1); wrapper.orderByDesc(Formula::getCreateTime); return formulaMapper.selectPage(page, wrapper); }注意wrapper.and(w -> ...)这个写法。如果不用 and 嵌套,前面的 categoryId 条件和后面的 or 条件组合时,生成的 SQL 条件关系可能会错乱:categoryId=? AND name LIKE ? OR function LIKE ?和categoryId=? AND (name LIKE ? OR function LIKE ?)在语义上完全不同。这个细节在现场编码时很容易忽略,建议多打印 SQL 日志确认。
食材汇总的时候,如果要在列表里附带收藏量或浏览量统计,不要写循环查询,一次聚合查出来再 Map 填充,避免接口越写越慢。
3.3 前端:路由设计、状态管理和页面实现
Vue 项目目录结构建议这样组织,功能和职责一目了然:
src/ ├── api/ │ ├── formula.js │ ├── herb.js │ ├── user.js │ └── auth.js ├── router/ │ └── index.js ├── store/ │ └── user.js ├── views/ │ ├── home/ │ ├── formula/ │ ├── herb/ │ ├── favorite/ │ └── admin/ ├── components/ │ ├── FormulaCard.vue │ ├── HerbCard.vue │ └── Pagination.vue └── utils/ └── request.jsrouter 用路由守卫控制访问权限,核心逻辑在全局前置守卫里。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.path === '/login' && token) { next('/') } else { next() } })axios 请求层会统一注入 token,并处理 401 过期跳转。这里再补一段我实际用下来比较舒服的 axios 封装:
import axios from 'axios' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { // 统一错误提示 return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } ) export default request经方列表页是用户端的核心页面。卡片式布局比纯表格好看,每张卡片展示方名、分类、出处和功效摘要。详情页的动态路由配置为/formula/:id,进入详情时用 id 调后端接口拿完整数据。收藏按钮的交互是校验登录后调收藏接口,收藏成功状态存到前端变量里,刷新后通过“查询用户当前是否收藏”的接口还原。
3.4 经方知识库内容建设
内容建设是这类平台型毕设里最容易忽视、也最影响观感的部分。花两小时把数据库结构搭完,但如果表里只有三条测试数据,老师打开系统会觉得像空壳。建议前期就准备一套体面的数据。
经方数据可以从公开的经典文献梳理中整理,比如按主治功能分类的常见经方。药材数据可以从药食同源相关公开目录和常见食材资料整理。整理时要保留出处和来源,不要在系统里编造功效或禁忌结论。所有数据录入完成后,在前端页面显著位置放一句提示:平台内容仅供学习参考,不构成医疗建议。
数据导入建议写成 Excel 批量导入功能,后台提供一个模板下载,按模板填好再上传。直接把几十条数据写死在 sql 里,将来想调整数据时维护成本很高。批量导入功能还能给答辩加分,属于“数据导入导出”这个高频答辩考点。
3.5 经典推荐,如何做到“四两拨千斤”
很多同学毕设做得中规中矩,但缺少让人记住的亮点。这个平台的突破口在推荐逻辑上。
不需要上机器学习,一个简单的“热门推荐 + 分类热门 + 随机换一换”就能撑起首页推荐位。
public List<FormulaVO> recommendFormulas(Integer userId, Integer size) { if (userId != null) { // 优先取用户收藏分类下的热门经方 // 查 user_favorite 表,统计用户最常收藏的 content_type 和分类 // 再按该分类查 view_count 最高的前 size 条 } // 兜底逻辑:全站浏览最多的经方 return formulaMapper.selectTopByViewCount(size); }首页展示“热门推荐”和“猜你喜欢”两块,前者走全站热门,后者从用户收藏偏好出发。代码量不大,但演示的时候从普通列表一跃变成个性化推荐,观感完全不一样。
4. 常见问题、部署细节与答辩经验
4.1 我在运行和部署中踩过的坑
先说 MySQL 连接时区问题。本地开发容易踩这个坑,数据库连上了但时间字段查询结果差 8 小时或者干脆启动报错。解决方式是在 application.yml 里把连接字符串写完整。
spring: datasource: url: jdbc:mysql://localhost:3306/food_medicine?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver还有一个容易漏的地方是 MySQL 8 和旧版驱动的差异。MySQL 8.x 必须用com.mysql.cj.jdbc.Driver,如果还在用 5.x 的驱动类名,启动就会报错找不到驱动。
跨域配置也是一个高频坑。Vue 开发服务器默认跑在 5173 或 8080,后端跑在 8080,端口不同就存在跨域。最简单的方式是后端写一个 CORS 配置类,允许指定来源访问。
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }生产部署时还有一个前端路由刷新 404 的问题。前端路由是 history 模式,用户访问/formula/1后按 F5 刷新,Nginx 找不到对应文件,就会报 404。要在 Nginx 配置里加一个 try_files 规则。
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }如果你选择把前端构建产物放进 SpringBoot 的src/main/resources/static目录,刷新问题是另一种表现方式。SpringBoot 默认会把未知路由交给前端路由去处理,但最好也检查一下是否配置了 forward 到 index.html 的兜底规则,避免部署后手忙脚乱。
4.2 发布一篇能直接陪跑的部署文档
部署文档不要写成环境安装流水账,而是写一份新环境从零开始的完整操作手册。
最稳的框架是:前端构建、后端构建、数据库初始化、反向代理配置、验证访问。每一步都把命令、预期结果、常见报错写清楚。比如前端构建,要写清楚npm install安装的依赖来自 package-lock.json,避免版本漂移;再写清楚npm run build:prod输出的 dist 目录到底在哪。
后端构建要区分本地 jar 和 Docker 镜像两种方式。本地 jar 方式最简单,mvn clean package之后java -jar启动;Docker 方式则需要写好 Dockerfile。这两种方式二选一即可,不需要都写,但选中的那条路径一定要走通。
数据库初始化脚本不能只给一个裸 sql,要说明这个 sql 包含了表结构、初始管理员账号、基础经方和食材数据。提供一个便捷的初始化命令,比如mysql -uroot -p < init.sql。
4.3 答辩演示的顺序和讲解重点
答辩时间通常 10 到 15 分钟,演示顺序建议按照“业务理解、整体运行、核心流程、亮点挖掘”四层来走。
演示时先不看代码,直接打开系统正常使用的页面,从用户视角过一遍检索、查看详情、登录、收藏流程。走完流程后,再打开管理端后台,演示一条经方的录入和上下架。然后把数据库设计图或表结构调出来,讲清楚经方和药材为什么拆多对多关联,收藏表设计了哪些索引。最后把检索接口的 where 条件拼接和 JWT 鉴权拿出来讲一遍。
答辩老师最容易问的问题,提前准备好答案:
- “这个项目的难点在哪里?”可以回答多表关联查询和动态条件拼接,加上 token 鉴权和全局异常处理,这些是实际工程里一定会遇到的问题。
- “数据量大了还能不能跑?”老实讲分页和索引的意义,说明目前设计预留了优化空间,比如把 LIKE 查询换成全文检索引擎。
- “和普通内容管理系统有什么区别?”重点讲经方与药材的结构化关联,以及收藏偏好驱动的推荐逻辑。
- “为什么选 Vue 而不是 React?”从开发效率、中文生态、团队协作环境等角度回答,不要踩一捧一。
4.4 这个项目后续还能怎么扩展
数据库结构是典型的关系型设计,扩展空间其实不小。
第一档扩展是展示层的丰富,比如给药材详情页增加营养成分表,或给经方详情页增加同类方剂推荐。
第二档扩展是业务层的深化,比如为了体验优化增加一个“根据已选食材生成可选经方”的反向搜索。这建立在经方药材关联表的基础上,只需要把用户选择的 herb_id 列表传给后端,后端先查关联表找到同时包含这些药材的经方,再按覆盖率高到低排序。这个功能做出来,平台的实用性立刻上一个台阶。
第三档扩展是工程化层面,比如用 Redis 给热门经方加缓存、用 RabbitMQ 解耦文档导入流程,这是往简历项目方向靠的路线。但先把现有功能做到稳定、报表做好,再考虑这些比较稳妥。
5. 写在最后的一些实在话
做了这么多毕设类项目,最大的体会是:真正拉开差距的不是代码量,而是数据建模能力和对业务边界的判断。SpringBoot、Vue、MySQL 都是工具,工具大家都会装,但怎么把经方和药材组织成一套能查询、能统计、能推荐的结构,才是这个项目真正训练人的地方。
你复刻这套毕设的时候,建议按数据库设计、后端接口、前端页面、部署上线的顺序推进。先花一整块时间把表和字段敲定,再写接口,不要一上来就写前端。我见过很多同学卡壳,都是因为前端写到一半发现后端接口和数据结构对不上,又要回头改表。数据库稳了,后面的代码基本都是按部就班。
最后再分享一个提升体验的小技巧。把药食两用食材的图片做成懒加载,列表页滚动时图才加载,页面打开速度会快很多。别小看这个细节,老师打开系统的那几秒的加载速度,直接决定他对这个项目的第一印象。