最近把一套基于Java SpringBoot + Vue3 + MyBatis的经方药食两用服务平台源码完整梳理了一遍,前后端分离,MySQL做持久化,算是一个业务方向比较清晰、技术栈也足够主流的全栈项目。这类平台现在很有实际意义:大众对药食同源、经典方剂调理的关注度越来越高,但真正能把“经方知识”和“日常饮食建议”结合起来的产品并不多,很多还停留在静态展示页面。这套系统源码的价值就在于把经方查询、药食两用食材库、体质建议、收藏笔记等业务完整落地,适合正在学Java全栈的开发者用来练手,也适合业务方拿去快速搭建一个健康服务平台的雏形。
1. 项目定位与业务设计思路
1.1 为什么聚焦“经方药食两用”
“经方”通常指经典方剂中的代表配伍,讲究组方思路、适应证和禁忌;而“药食两用”指的是既可作为食品、又可作为中药材使用的品类,比如山药、枸杞、茯苓、山楂这类常见品种。把这两者结合起来,平台的核心不是做一个冷冰冰的中药材数据库,而是解决一个很现实的问题:用户想知道“我这种情况适合吃什么、怎么搭配”,同时又需要可信的来源和专业的参考依据。
这个定位决定了系统不能只是增删改查演示,还要有领域上的逻辑。比如食材不仅要有名称和功效,还要区分性味归经、适用体质、禁忌人群;经方不能只存方名和组成,还要有主治方向、服用注意。业务建模一旦想清楚,后面的表结构、接口设计、前端页面都会顺很多。
我实际梳理时发现,很多类似项目败在“表设计太简单”,比如食材表只放一个名称和图片,后续想加“功效标签”就得改表。这套源码在这方面用了标准的关联表设计,把食材、经方、体质、标签拆开,扩展空间大得多。
1.2 角色与核心业务流程
平台涉及的核心角色有三类:普通用户、健康顾问(内容维护者)和管理员。普通用户负责浏览经方知识、按体质或症状筛选食材、查看推荐食谱、收藏内容、写健康笔记;健康顾问负责维护食材库和经方数据;管理员偏系统层面,管用户状态、内容审核和基础配置。
整个核心业务闭环大概是:用户进入平台后先做一次体质倾向确认,系统根据体质标签推荐对应的经方和食材;用户点击经方详情可以看到组成、功效、适用人群和注意事项;遇到合适的可以收藏,并在个人中心记录自己的使用笔记;健康顾问在后台上传新的食材或经方内容,经过管理员审核后发布。这个流程不算复杂,但覆盖了内容管理、用户交互、推荐匹配三个关键点。
这个流程落在系统实现上,就是几大模块:用户认证模块、体质标签管理模块、经方管理模块、食材管理模块、收藏笔记模块、内容审核模块。前端路由和菜单也按这个模块来组织,结构非常清晰,二次开发时不会迷路。
1.3 数据模型概览
数据模型是这套源码里值得先看的部分。核心表大致包括:用户表、角色表、菜单权限表、经方表、经方组成明细表、食材表、体质表、食材体质关联表、收藏表、笔记表、审核记录表。
以经方表为例,除了方名、出处、组成、用法之外,还有主治方向、适用体质、禁忌提示这几个业务字段;组成明细单独拆一张表是为了支持一组多药、一药多量的数据,不会出现把整套组成塞在一个文本字段里的尴尬。食材表则包含名称、别名、性味、归经、功效、食用建议、禁忌人群、图片地址、是否药食两用标记等字段。
体质表和数据字典表配合使用,比如“气虚质”“湿热质”这样的体质标签本身是稳定字典,通过中间表关联到食材和经方,比在业务表里直接存字符串规范得多。后面做筛选和推荐,直接基于关联表联查就行,不需要正则匹配,性能也更好。
2. 技术选型解析:SpringBoot + Vue3 + MyBatis 如何协作
2.1 后端为什么选 SpringBoot 而不是更重的框架
SpringBoot在Java后端项目里基本是事实标准,选它不需要太多理由,但值得说明的是它给这类项目带来了什么。服务平台的核心是快速交付业务接口,SpringBoot的自动装配和起步依赖把很多样板配置省掉了,一个内嵌Tomcat跑起来就能出接口,配合YAML格式的配置文件,团队上手成本很低。
这套源码后端结构也很常规:controller、service、mapper三层,controller层只做参数接收和响应封装,service层处理业务逻辑,mapper层通过MyBatis操作数据库。和传统SSM相比,省掉大量XML配置,但保留了分层思想,对学习者来说特别友好。实际开发中我习惯在service层记录业务日志,这套源码也在关键位置做了操作日志切面,排错时省事不少。
SpringBoot的版本选择方面,这套源码基于2.7.x或者3.x都可以跑通。如果是学习,建议先用2.7.x,网上资料多、踩坑少;如果是新项目起步,可以考虑3.x。差异主要在javax到jakarta的命名空间迁移,以及部分自动配置类的调整,整体迁移成本不高。
2.2 Vue3 + Vite 带来的前端开发体验
前端选择Vue3与Vite的组合在目前看来是很成熟的选择。Vue3的组合式API(Composition API)让组件逻辑复用变得直接,比如用户信息获取、Token读取、接口状态处理,都可以抽成hook函数统一调用。这套源码里大量使用了setup语法糖,页面代码比Vue2的Options API更紧凑,逻辑相关的内容也更容易集中。
Vite做开发服务器,热更新速度比Webpack时代的体验好太多。尤其像这种后台管理平台,页面多、组件多,改了样式或接口后浏览器基本秒级刷新,开发效率明显提升。生产构建用vite build生成静态文件,部署到Nginx即可,和后端完全独立。
前端UI组件库用的是Element Plus,表格、表单、弹窗、标签这些后台管理常用件开箱即用。模板项目里已经把axios请求封装好,统一处理了Token附带、响应码拦截、错误提示,新增一个接口只需要在api模块里写一个函数,避免每个页面都重复处理请求逻辑。
2.3 MyBatis 与 MySQL:可控SQL和常用配置
MyBatis在这类系统中最大的优势是SQL可控。业务平台往往有报表、统计、分页查询、多表关联查询,用Hibernate或Spring Data JPA虽然写起来快,但遇到复杂查询时要么拼JPQL,要么绕来绕去,反而不如直接写SQL直观。MyBatis的Mapper XML里可以像写普通SQL一样处理多表关联,配合resultMap完成字段映射,对熟悉SQL的开发者来说几乎没有额外心智负担。
MySQL方面,这套源码的核心配置都集中在application.yml里,包括连接地址、用户名密码、连接池参数、MyBatis的mapper-locations和驼峰映射开关。需要注意的一个细节是数据库连接串必须带serverTimezone=Asia/Shanghai,否则连接时容易因为时区问题报错。字符集统一使用utf8mb4,因为平台内容里有生僻字和特殊符号,用utf8能存但排序和展示都可能出问题。
数据库连接池方面建议关注maximum-pool-size的配置,内嵌HikariCP默认是10,对于这种体量的服务平台完全够用。但如果在调试时频繁手动重启应用,连接池里的旧连接可能会失效,遇到“Connection is not available”的报错,优先怀疑连接池和MySQL的wait_timeout配置,这个点在后面排错部分我会细说。
3. 核心功能模块的实操拆解
3.1 用户登录与JWT权限控制
用户模块是前后端分离架构里最容易出问题的部分。这套源码采用的是JWT方案:用户带用户名和密码请求/api/auth/login,后端校验通过后签发Token,前端把Token存到localStorage,后续每个请求通过axios拦截器在请求头带上Authorization: Bearer <token>,后端再用拦截器或过滤器统一校验。
后端实现上,关键是两个点。第一是登录接口的密码校验,用户表里存的是BCrypt加密后的密文,校验时调用BCryptPasswordEncoder.matches(),不会出现明文密码落库的问题。第二是Token校验的过滤器,重写doFilterInternal方法时先取出请求头里的Token,解析失败或过期直接返回401,不放行到Controller。这里有一个很常见的坑:如果某些接口是白名单(比如登录、注册、获取验证码),一定要在过滤器中明确放行,否则会把正常请求也拦截掉。
// 自定义JWT过滤器的核心逻辑,简化展示 String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); try { Long userId = jwtUtil.getUserIdFromToken(token); request.setAttribute("currentUserId", userId); } catch (Exception e) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return; } } chain.doFilter(request, response);前端配合的是vue-router的全局前置守卫,每次跳转路由前检查本地是否有Token,如果没有且目标路由需要登录,就重定向到登录页。同时在前端axios响应拦截器里统一处理401:弹出重新登录提示、清空本地Token、跳回登录页。这样后端只管出401,前端统一响应,不会每个页面都写重复逻辑。
3.2 经方检索与食材列表分页
经方检索和食材列表是内容型平台的核心流量入口,也是这套源码里前后端配合最典型的模块。后端的查询接口支持三个主要条件:关键字模糊搜索(匹配方名或食材名)、体质标签筛选、分类筛选。参数通过@RequestParam接收,service层组装查询条件,mapper里写好动态SQL。
MyBatis里做动态查询最常用的是<where>和<if>标签。比如根据体质标签筛选时,如果参数不为空才拼入关联表的查询条件;关键字模糊搜索用LIKE CONCAT('%', #{keyword}, '%')而不是直接LIKE #{keyword},这样能避免用户传特殊符号把SQL搞错。分页可以采用PageHelper插件,也可以手写LIMIT #{offset}, #{pageSize}。这套源码用的是PageHelper,一行PageHelper.startPage(pageNum, pageSize)就能拿到分页信息,返回给前端时再包一层总记录数。
前端列表页的核心套路基本一致:进入页面请求一次列表,筛选条件变化时重置页码并重新请求,分页组件触发切换页码事件。接口返回的数据结构建议统一,比如{ code: 200, data: { list: [], total: 100 } },这样前端分页只需要绑定total和list,不用每个页面单独适配。
这里特别想提醒一个查询性能的问题:如果食材表已经有一两千条数据,直接SELECT *再传给前端问题不大,但如果未来数据量大,后台列表查询要注意只查必要的字段,不要每行都把大文本字段(比如食材的详细介绍)查出来。可以在列表接口用精简字段视图,详情接口再查完整内容,这套源码虽然没有做极端优化,但字段拆分的设计是朝着这个方向去的。
3.3 药食推荐逻辑与收藏笔记
药食推荐是这个平台比较有特色的地方。推荐逻辑没有上复杂的算法,而是基于用户选择的体质标签,和食材表、经方表的关联关系做匹配。用户进入推荐页面时先选择一个或多个体质倾向,后端拿到体质标签ID后,联查关联表找出匹配的食材和经方,再按推荐值排序返回。
这个设计很务实,因为平台早期数据量有限,用协同过滤或深度学习模型没有足够的数据基础,反而基于规则匹配结果更可解释、可调整。后端业务逻辑大致是:体质标签ID数组作为参数传入,SELECT DISTINCT f.* FROM food f INNER JOIN food_constitution fc ON f.id = fc.food_id WHERE fc.constitution_id IN (...),然后按关联数量倒序,这样匹配多个体质标签的内容天然排在前面。
收藏和笔记模块相对简单但很实用。收藏表记录用户ID和内容ID,同时冗余一个内容类型字段区分是经方还是食材;笔记表存储用户自定义的心得、食用记录和感受。这两个模块对权限有要求:查询接口必须校验当前登录用户,而且只能查自己的数据。后端从JWT过滤器设置currentUserId属性中取用户ID,不允许前端传入用户ID来查询,这是权限设计上的一条底线。
4. 数据库设计与初始化脚本
4.1 核心表的字段设计思路
数据库设计直接决定项目能不能顺利二开,这套源码的表结构设计有几点值得借鉴。先看用户表,除了常规的用户名、密码、昵称、头像之外,还加了状态字段(启用、禁用)、注册来源和最后登录时间。状态字段特别有用,管理员封禁用户不需要删除记录,改状态就行,保留历史行为数据。
经方表字段包含方名、出处、类别、组成说明、功效主治、服用方法、禁忌事项、适用体质ID、状态和审核人ID。这里“适用体质ID”其实是一个JSON数组字段或逗号分隔的字符串,方便前端直接展示标签。虽然关系正规化角度应该拆关联表,但在实际业务中这种“冗余”反而减少了查询复杂度,取舍很合理。
食材表是另一个重点,字段包括名称、拼音首字母(用于字母索引)、别名、性味、归经、功效、食用建议、禁忌人群、图片URL、适宜体质、是否药食两用。拼音首字母字段很多人会忽略,但后台管理列表的字母快速定位全靠它。有效成分和营养价值这类大段文本可以单独放扩展表,避免主表查询变慢。
收藏表和笔记表都带了unique key防止重复,收藏表用user_id + content_id + content_type做联合唯一索引。笔者团队做类似项目时曾因为漏掉唯一索引导致用户重复收藏,服务端又没做幂等校验,结果数据库里出现好几条一模一样的记录,后续维护成本很高。这点值得专门记一下。
4.2 初始化数据和常用SQL
初始化数据这块,平台至少要预置几类内容:菜单权限、体质标签、常用食材、示例经方。体质标签建议做成一等数据字典表而不是直接写死在代码里,因为“湿热质”“痰湿质”这类标签后续可能会调整名称或增删,数据字典管理起来更灵活。
在MySQL里手动执行初始化脚本时要注意字符集,建库语句最好显式指定:
CREATE DATABASE IF NOT EXISTS food_platform DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;脚本执行顺序也很重要:先建表,再插入基础数据,最后再插入关联数据。如果先插入关联数据但基础表没数据,外键约束会直接报错。建议把整个初始化脚本拆成01_schema.sql、02_data.sql、03_seed_relation.sql三个文件,按顺序执行,排错时也容易定位。我用这套源码时踩过的一个问题是:直接用图形化工具导入整个脚本,中途因为某条数据重复就中断了,后面再执行时出现重复主键,非常容易造成混淆,分开执行反而更安全。
5. 前后端联调与部署落地
5.1 开发环境代理配置
前后端分离项目在本地开发时,最大的痛点是跨域。前端页面跑在5173端口,后端接口跑在8080端口,直接请求会产生跨域问题。这套源码在开发环境用Vite的proxy配置来解决,不开启后端CORS,这样生产环境行为更一致。
Vite配置代理的方式很简单,在vite.config.js里加:
server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这样前端请求/api/auth/login时,Vite开发服务器会把它转发到后端的http://localhost:8080/api/auth/login,浏览器感知不到跨域存在。.env.development里的接口基础地址设置为/api即可,到生产环境时再改为完整域名或用Nginx继续代理。
后端CORS其实也可以配置,但我不建议在正式环境完全放开。如果后端配置了addAllowedOriginPattern("*"),意味着任何域名都能请求接口,安全隐患较大。开发时用前端代理,生产用Nginx同域代理,是更干净的做法。
5.2 构建打包与部署顺序
前端打包后端部署的顺序有讲究。前端执行npm run build后会生成dist目录,里面是纯静态文件,需要放到Nginx的html目录或自定义站点目录。后端执行Maven打包mvn clean package -DskipTests,生成可执行的jar包,通过java -jar xxx.jar启动。
部署顺序建议是先确认后端能正常启动,再用Nginx代理静态页面。如果顺序反了,用户访问页面时接口全挂,体验很糟糕。后端启动成功后应该先测试几个接口是否返回预期数据,再处理前端静态资源。我见过不少新手把前后端一起上线,结果前端样式加载出来了但登录接口504,排查半天才发现后端服务没起来。
生产环境还可以考虑用systemd管理Java进程,配置一个简单的unit文件,实现开机自启和异常自动重启。虽然这套源码没有自带部署脚本,但自己补一个也很简单:
[Unit] Description=Food Platform Backend After=network.target [Service] ExecStart=/usr/bin/java -jar /opt/app/food-platform.jar Restart=always RestartSec=10 [Install] WantedBy=multi-user.target5.3 跨域问题与Nginx反向代理
生产环境我强烈建议用Nginx做反向代理,把前端和后端统一到一个域名下。比如用户访问https://example.com,Nginx把根路径指向dist静态文件,把/api路径转发到本机的8080端口:
server { listen 80; server_name example.com; root /opt/app/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这段配置里try_files $uri $uri/ /index.html就是Vue Router的history模式必须的兜底配置,否则刷新子路由页面会404。这个问题几乎每个做前后端分离项目的人都会遇到,Nginx配好后基本告别跨域烦恼。
后端配置文件里的上下文路径也建议统一,比如server.servlet.context-path=/api,这样前端请求路径和后端映射路径天然对齐,Nginx代理转发也不需要重写路径。要改代理路径时只动Nginx一处,维护成本低。
6. 常见问题排查与避坑实录
6.1 数据库连接不可用和时区问题
我梳理这套源码时遇到的第一个高频问题就是数据库连接失败。症状很典型:后端启动时报Access denied for user或Communications link failure。前者一般是用户名密码配置不对,后者大概率是连接串里的数据库地址端口写错、MySQL没启动、或者账号密码对但字符集问题。
还有一个经常被忽略的坑是时区问题。MySQL 8.x 默认时区设置可能导致驱动报Server returns invalid timezone,解决办法是在连接串上追加serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8mb4。这套源码的配置文件里已写好,但如果你从旧项目拷贝配置,很可能漏掉这一项。
如果应用运行一段时间后突然出现连接超时的报错,别急着重启,先看MySQL的wait_timeout和interactive_timeout配置。默认值是8小时,连接池里的连接如果长时间空闲会被MySQL断开,但连接池还认为连接有效,一取就报错。HikariCP可以通过配置connection-test-query或开启test-while-idle来规避,更适合的做法是缩短连接池的max-lifetime(建议小于数据库wait_timeout)。
6.2 MyBatis 绑定异常定位
Invalid bound statement (not found)是使用MyBatis时另一个高频报错。这个问题的本质是Mapper接口和XML文件没有正确对应。检查顺序就三个:第一,application.yml里的mapper-locations路径是否覆盖XML存放位置;第二,XML文件的namespace是否和Mapper接口全限定名一致;第三,Mapper接口方法名和XML里的语句ID是否对得上。
这套源码在mapper目录放XML文件时结合了包扫描,路径配置是classpath:mapper/*.xml。如果你自己调整过目录层级,一定要同步修改这个配置。还有一种情况是XML文件没被编译进target/classes,在IDEA里表现为target目录下找不到对应的XML,这是构建配置遗漏导致的,需要检查pom.xml的resource配置是否包含src/main/java下的xml(如果XML放在Java目录下)或src/main/resources。
排查顺序有讲究:先在target目录里找XML是否存在,再打开XML看namespace和id,最后检查接口方法签名。很多人绕了一大圈,最后发现只是方法参数没有加@Param注解导致SQL里取不到值,生成一个格式错误的语句,这个错误在启动时往往不明显,运行查询才爆出来。
6.3 前端请求404/401问题
前端页面打开没问题,但登录时请求/api/auth/login返回404,这是一个很容易迷惑人的情况。从现象看像是接口不存在,但我排查过多数是代理路径没对上。前端请求的是/api/auth/login,后端实际接口路径是/auth/login且没有context-path配置,代理转发时把/api前缀也带着转发过去了,后端自然匹配不上。
解决办法有几种:后端设置server.servlet.context-path=/api;或者Nginx/Vite代理配置里重写路径,比如proxy_pass http://127.0.0.1:8080/(末尾加斜杠去掉前缀)。我在这套源码里推荐第二种方式更灵活,但要注意如果后端配置了context-path,重写时就不能去掉前缀,否则反而404。前置条件是前后端约定好一套规则,别一会儿有前缀一会儿没前缀。
401问题相对好查,多数是Token失效、前端没把Token附带在请求头、或者请求白名单配置不对。前端检查一下请求头的Authorization字段是否存在,后端看过滤器日志里是否校验通过。不建议把Token失效时间设太长,安全意识还是要有的,七天比较合理。
6.4 容易忽略的细节清单
运行这套源码时还有些小地方容易卡住。第一个是端口冲突:后端默认8080、前端默认5173,如果本机有服务占用了端口,启动会报Port already in use,改配置或者杀掉占用进程即可。第二个是前端依赖安装失败:使用npm install时如果遇到网络问题或版本冲突,建议清掉node_modules和package-lock.json重装,Element Plus和Vue3版本之间的兼容问题多半出在旧缓存上。
第三是数据库初始化时的大小写问题。MySQL里表名和字段名在Linux下区分大小写,写SQL时尽量统一用小写,否则可能出现“Table doesn't exist”的诡异报错。第四是图片上传路径的配置,如果前后端分离部署在不同的服务器,上传图片的存储路径要单独配置,并且前端展示时要能正确拼接出完整的URL,这个模块常被当成“本地能跑就行”而忽略。
7. 后续扩展方向与个人实操体会
这套平台在技术和业务上都有明显的扩展空间。技术上,可以把当前的手写SQL查询升级为更灵活的筛选方案,可以做Redis缓存热点经方和食材详情,也可以引入Elasticsearch做全文检索,让用户搜索体验更好。业务上,可以增加在线咨询模块,把健康顾问和用户之间的交流接进来;也可以增加周期性的健康档案记录,让用户看到自己一段时间内的饮食调整记录和体质变化趋势。
我在跑这套源码的过程中最深的一个体会是:项目能否顺利二次开发,很大程度取决于表设计和接口约定是否清晰,而不是代码量多少。这套源码在这些方面做得足够规范,Controller统一返回Result对象,Service层不把SQL逻辑散落得到处都是,数据库字段命名统一为下划线风格,前端接口方法按模块集中管理。这些看似基础的约定,恰恰是最省心的部分。
最后一个想分享的小技巧是:二次开发前,先把数据库初始化脚本完整跑一遍,再启动后端,最后启动前端,三个环节依次绿灯之后再开始改代码。否则你改到一半才发现环境本身有问题,排查问题时会分不清楚是自己改出的bug还是环境本身就没通。先跑通基线版本,再动手改造,是这类全栈项目最稳的起步方式。