SpringBoot+Vue+MyBatis+MySQL:经方药食两用服务平台全栈实战复盘

发布时间:2026/9/9 11:27:46
SpringBoot+Vue+MyBatis+MySQL:经方药食两用服务平台全栈实战复盘 上个星期我一个做中医养生的朋友找我吐槽团队整理了三四年经方积累了上千条内容现在全靠Excel跟企业微信来回传要对一张方子里的十几味药材做注释、修订剂量版本直接乱掉。我听完第一反应是这不该是一个管理系统该干的事吗于是我用SpringBoot Vue MyBatis MySQL把这套“经方药食两用服务平台管理系统”从零搭了出来。这篇文章就是整个项目的复盘从建表思路到部署上线把我在实际开发中觉得真正有用的东西写出来适合正在做中医药信息化、内容管理类系统的开发者参考也适合想入门Java全栈项目的人用来理解一个真实项目是怎么落地的。1. 为什么做这个平台从Excel到在线协作的需求1.1 经方和药食两用数据比想象中难管先解释一下这个项目到底在管什么数据。经方指的是以张仲景《伤寒论》《金匮要略》为代表的经典方剂它的核心信息不只是方名和药物组成还包括每味药的剂量、煎服方法、主治病症、禁忌和加减化裁。药食两用物质是国家卫健委名单里明确列出的“既是食品又是中药材”的种类比如山药、枸杞、茯苓、陈皮这些。这些数据本身带有很强的结构性和关联性一位食材可能出现在几十首方剂里一首方剂也可能用十几味食材。如果只是拿Excel硬记一开始可能没感觉数据量一上来就全是问题一个别名改了所有方子里的引用都不同步加一个新食材要手动维护所有关联多人同时编辑根本分不清谁是最新版本。这个项目最开始的目标就是把“方剂—食材—药材—内容—用户”之间的多对多关系理清楚做成一个可以多人协作、在线审核、快速检索的管理后台。1.2 系统定位与三类核心用户我搭建这个平台时没有把它做成一个简单的内容展示站而是按“管理后台 用户端”的双层结构来设计。核心用户有三类第一类是系统管理员负责用户管理、基础分类、数据字典这类底层配置第二类是内容运营负责方剂和食材的录入、编辑、上下架以及健康文章的发布第三类是普通用户可以浏览方剂和食材的内容支持收藏、评论和检索。整个系统的功能闭环很清晰内容录入后进入待审核状态运营审核通过后才会在用户端展示。这样可以避免未经核对的中医内容直接流出去。这个审核流程在后端就是一个status字段的状态流转但在业务场景里作用很大也是我建议所有做内容管理类项目的人都保留的设计。开放给普通用户的检索需要支持模糊搜索、分类筛选、来源筛选这就要求后端在设计查询接口时提前把动态SQL的灵活性做出来。2. 技术栈选型SpringBootVueMyBatisMySQL是怎么定下来的2.1 后端为什么用SpringBoot MyBatis而不是MyBatis-Plus很多人在SpringBoot项目里一上来就选择MyBatis-Plus因为它自带CRUD用起来确实省事。但我最后还是用了原生MyBatis因为这类系统的核心查询并不都是一对一查表大量是方剂和食材的关联查询、标签和分类的筛选、管理员自定义条件的分页检索。MyBatis的XML映射文件面对这种需求反而更直接一条SQL语句写完关联关系清清楚楚出了问题也方便在数据库客户端里单独运行验证。SpringBoot这边我选择2.7.18版本因为稳定且社区生态成熟。SpringBoot帮我们省掉了大量XML配置内嵌Tomcat让部署变成一条Java命令。项目的结构就是标准的controller、service、mapper三层配合统一的返回结果类Result和全局异常处理器。这样前后端联调时所有接口返回格式都是固定的code、message、data前端axios拦截器里直接统一处理。2.2 前端用Vue3 Ant Design Vue的体验前端部分我用了Vue3和Vite。选择Vue3是因为组合式API写起来比Vue2的Options API更容易组织逻辑一个功能模块的变量和方法可以放在一个setup函数里不用在data、methods、computed之间来回切换。管理后台的UI我选的是Ant Design Vue它的表格、弹窗、表单、消息提示这些组件已经覆盖了后台管理系统的绝大多数场景最重要的是它对表单校验的支持很完整后面方剂编辑页的复杂校验就靠它撑起来。前端工作流也很标准用Vite创建项目目录按api、router、store、views、components划分。axios实例统一封装请求前自动带上token响应后统一处理401跳转登录。路由用Vue Router的懒加载方式每个页面组件独立打包首屏加载速度快很多。2.3 MySQL选型与字符集、存储引擎数据库我选择MySQL 8.0这也是目前SpringBoot项目里最常见的搭配。建库的时候有两个细节要注意一个是字符集使用utf8mb4而不是utf8。因为utf8在MySQL里最多支持3字节存不了emoji和一些特殊汉字药食同源内容里经常会有特殊符号用utf8mb4最保险。另一个是排序规则用utf8mb4_unicode_ci它在多语言场景下排序更符合预期。存储引擎直接选InnoDB行级锁和事务支持是这个系统的刚需。比如一个方剂在录入时要同时写入方剂表、食材关联表和标签关联表任何一步失败都必须全部回滚只有InnoDB才能保证这种事务能力。MyISAM虽然查询快但没有事务在这个项目里不可取。3. 数据库建模一张表也不多余的字段设计3.1 用户、角色与权限三表做管理系统的第一件事是先设计用户和权限。我这里的用户表设计得比较克制主要字段有id、username、password_hash、nickname、phone、avatar、status、role、create_time、update_time。这里没有单独拆角色表和权限表而是用一个role字段区分ADMIN和OPERATOR。很多教程喜欢上来就做用户-角色-权限的五张表但对一个内部管理平台来说初期用简单的角色字段完全够用后续真要细化再拆也不迟。password_hash字段存的是BCrypt加密后的密码不是明文。用户登录成功后后端会生成一个Token后续请求通过拦截器解析Token获取用户信息。这样设计的好处是接口服务天然无状态前端和后端分离部署时不会遇到Session共享的问题。密码字段长度我设为60因为BCrypt生成的标准哈希长度就是60。3.2 方剂、食材、方剂食材关系表方剂表是整个系统的核心。字段包括id、title、source、category_id、composition、dosage、method、indications、contraindications、image_url、status、created_by、create_time、update_time。这里的composition和dosage我选择用TEXT类型因为一首方剂的完整组成和剂量可能很长不能简单用一个VARCHAR存。status字段用tinyint0是草稿1是待审核2是已发布3是已下架。整个状态流转简单直接。食材表主要字段是id、name、alias、nature、flavor、meridian、efficacy、edible_method、notice、image_url、status。nature和flavor分别表示性味比如温、寒、甘、辛meridian表示归经这些字段在查询和统计时用得非常多。关键的是方剂和食材的关联表。这张表我命名为prescription_ingredient字段包括id、prescription_id、ingredient_id、dosage_usage。dosage_usage这个字段很实用它记录的是这味药在这个方子里的具体用量和用法比如“15g后下”。如果不建这张关联表而把食材ID直接塞进方剂表的一个字段里后续要做反向查询查某个食材出现在哪些方子里就会非常痛苦。3.3 内容扩展表文章、标签、收藏除了方剂和食材平台还需要健康文章。文章表的字段和方剂表类似title、content、cover_image、status、author_id、create_time、update_time。标签我单独建了一张tag表同时建了article_tag关联表。为什么标签要单独拆表而不是在文章表里加一个tag_ids字段因为标签需要单独管理比如统计某个标签下有多少篇文章、同时支持标签的重命名而不影响文章引用。用户收藏表也是必要的字段包括id、user_id、content_type、content_id、create_time。content_type用来区分收藏的是方剂还是食材这样一张表就能通用。每一条收藏记录都加唯一约束(user_id, content_type, content_id)避免用户重复收藏。收藏列表在用户端展示时需要关联查询最新的方剂或食材标题和封面这在前端看起来不难但在后端SQL里就是一次INNER JOIN的事。数据库设计完成之后我通常会用Navicat生成一份表结构文档方便团队成员直接查看字段注释。所有字段都要求带COMMENT不然半年后会连自己都搞不清楚这个字段是干什么用的。4. 后端核心实现权限、检索与事务4.1 JWT登录与接口鉴权这个项目的登录接口我直接用SpringBoot框架做没有引入过于复杂的Spring Security而是用拦截器 JWT解决。JWT的优势是无状态服务器不需要保存Session每台机器都能校验Token后面横向扩容也方便。登录成功后我会构建一个包含userId和role的Token并设置7天有效期。前端每次请求都在请求头带上Authorization: Bearer xxxxx。拦截器负责校验Token并把解析出来的用户信息放入ThreadLocal里方便Controller里随时获取当前登录用户。实现上只需要实现HandlerInterceptor在preHandle里判断请求路径是否在白名单内如果不需要登录则放行否则解析Token并处理异常。这样做比引入Security一整坨配置轻量得多而且对这个项目来说已经足够。密码加密使用BCrypt。比如用户注册或修改密码时不能直接把明文密码存库而是用BCryptPasswordEncoder.encode()加密。校验登录时用matches()方法验证。这个点很多人会忽略但属于数据安全的基本功必须明确写在代码里。4.2 方剂多条件检索动态SQL是MyBatis的强项方剂列表页的查询条件有关键字匹配方名或药物组成、来源如《伤寒论》、分类、状态、时间范围。前端把这些参数传给后端后端根据参数动态拼SQL。这种场景用MyBatis的 和 标签再合适不过。select idselectPageWithCondition resultTypecom.demo.entity.Prescription SELECT id, title, source, category_id, status, create_time FROM prescription where if testtitle ! null and title ! AND (title LIKE CONCAT(%, #{title}, %) OR composition LIKE CONCAT(%, #{title}, %)) /if if testsource ! null and source ! AND source #{source} /if if teststatus ! null AND status #{status} /if if teststartTime ! null AND create_time gt; #{startTime} /if if testendTime ! null AND create_time lt; #{endTime} /if /where ORDER BY create_time DESC /select这里有一个很常见的坑模糊查询不能直接用LIKE %${title}%因为${}是字符串拼接存在SQL注入风险。必须用#{}占位符配合CONCAT拼接百分号。如果把%直接写进参数里预处理就没法正确绑定查询结果会跟你预期的完全不同。这个语法细节我调整过很多次网上搜“MyBatis like”能遇到一堆相关问题。4.3 方剂详情关联查询与事务处理方剂详情接口需要返回三类数据方剂基本信息、关联的食材列表、对应的标签列表。我的实现是在Service层做“三步拼装”先根据id查询方剂基本信息再查关联食材表最后查标签表。这个过程中不需要写一个复杂的多表嵌套查询代码更清晰也更好维护。并不是所有场景都一定要用一条大SQL拆开调用只要不影响性能反而更好排查问题。添加或编辑方剂时就要用到事务了因为一次保存操作会同时操作三张表插入方剂主表记录、清空旧关联、插入新的食材关联记录、插入标签关联记录。只要中间任何一步失败前面插入的数据就会变成脏数据。我直接在Service实现方法上加Transactional注解让Spring把方法纳入事务管理。事务失效的情况值得多说一句如果这个方法被同类里的另一个方法直接调用Transactional是不生效的因为Spring事务默认走代理。还有如果方法内部把异常catch住不往外抛事务也会失效因为Spring只有在遇到RuntimeException时才会回滚。所以我的习惯是事务方法内部不做多余的异常吞掉统一交给全局异常处理器去处理。4.4 文件上传与图片管理方剂、食材、文章都要配图片。我在项目里采用最简单的本地存储方案上传接口接收MultipartFile把文件写入服务器指定目录文件名用UUID重命名后缀保持原样避免文件名冲突。然后把这个相对路径存进数据库前端通过Nginx或SpringBoot静态资源映射访问。需要注意文件名处理时一定不要信任前端传的原始文件名因为可能包含“../”这类路径穿越字符。我做了两层处理一层是强制重命名为UUID另一层是保存目录控制在配置项指定的路径内避免任意文件上传漏洞。这个项目用的是本地存储后续如果有条件可以换OSS或MinIO但接口设计上已经做了抽象Controller只负责接收和返回URL替换存储服务时不会影响上层业务。5. 前端搭建Vue3后台管理页面的具体落地5.1 项目初始化和目录设计前端我用Vite初始化Vue3项目依赖安装命令就是npm install vue-router4 pinia axios。目录设计上我按api、router、store、views、components五层来组织。api目录下面每个模块一个文件比如prescription.js、ingredient.js、article.js里面统一通过封装的axios实例发请求业务页面不直接操作axios这样当接口地址变化时只需要改动api文件。router里配置所有页面路由登录页和主要后台页面分开。后台页面统一挂在一个MainLayout组件下包含侧边栏菜单和顶栏。通过路由meta里的roles字段控制页面访问权限比如管理员才能进入用户管理页内容运营只能进入内容模块。前端权限只是控制显示真正严格的安全控制还是靠后端的角色字段。pinia用来存用户信息和TOKEN。我的做法是在登录成功后将用户信息和token一起写入localStorage和pinia页面刷新时从localStorage恢复避免登录状态丢失。5.2 方剂管理页面的表单校验与动态添加方剂管理是后台最复杂的页面。列表使用Ant Design Vue的Table加一个搜索表单区域。搜索表单提交时触发列表接口的重新查询同时把分页参数一起传过去。Table自带分页组件分页变化时更新current页码并重新请求接口。这里的联调要点是后端分页返回的格式要包含total和records前端才能正确渲染总条数和当前页数据。添加和编辑方剂用一个Drawer或Modal弹窗表单字段很多。需要校验必填项比如方剂名称、来源、内容不能为空。食物关联的部分用Select组件的multiple模式下拉选项从食材管理接口获取在提交前把选中的食材和对应用量组装成数组。Ant Design Vue的Form支持动态校验规则比如方剂名称最长50字、来源不能超过100字这些都可以配置在rules里。我实际开发中遇到过一个问题Modal里的表单组件如果关闭后不销毁再次打开时会保留上一次的数据。解决办法是为表单绑定初始值或者设置destroyOnClose属性让弹窗关闭后自动销毁内部组件状态。这个细节如果不处理用户会觉得自己填的内容被“拼接”了体验很差。5.3 数据看板用ECharts展示统计信息管理后台除了增删改查最好有一个数据看板让管理员能直观看到平台的内容规模和用户活跃度。我使用了ECharts库做图表展示。统计内容包括各分类下的方剂数量用药材性味分布近30天新增方剂趋势以及用户收藏最热的TOP10方剂。每次进入看板页面时通过axios请求多个统计接口拿到数据后组装成图表需要的格式再setOption到ECharts实例中。这里提醒一下使用ECharts时组件卸载前需要通过onUnmounted钩子销毁实例否则切换到其他页面再回来会出现图表重叠或者内存泄漏的问题。Vue3结合ECharts非常简单只需要封装成一个BaseChart组件接收option作为props内部负责初始化、更新和销毁。5.4 前端权限指令与按钮级控制后台系统经常会涉及按钮级权限。比如运营人员不能删除方剂只有管理员可以。我在项目里写了一个自定义指令v-permission用法类似v-if但它专门用来控制按钮是否渲染。指令内部读取store里的当前用户角色如果角色没有指定权限码就移除当前DOM元素。这个实现不到20行代码却让模板里不需要到处写v-ifrole ADMIN这种判断后端接口再做一层权限校验就双保险了。6. 部署与踩坑我终于解决了那几个线上问题6.1 环境配置JDK、Maven、Node、MySQL部署之前先把环境统一。我的服务器是LinuxCentOS 7风格安装的是JDK 8SpringBoot 2.x完全兼容Maven 3.6.3用于打包。前端使用Node 16和npm 8如果Node版本太高Vite的老版本可能会出现兼容告警建议直接用Node 16或Node 18 LTS。MySQL 8.0安装后需要注意root用户的密码策略初始化时设置为中等强度即可避免在命令行输入复杂密码被自动跳过。后端项目的application.yml配置里数据库连接串一定要加上时区和编码参数比如spring: datasource: url: jdbc:mysql://localhost:3306/yuyao_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: yourpassword如果漏掉serverTimezone连接MySQL 8.0时会因为时区无法识别直接启动报错。这个问题几乎每个新手都会遇到。6.2 前后端部署的具体步骤后端打包很简单在项目根目录执行mvn clean package -DskipTests会在target目录生成一个jar包。服务器上直接用nohup方式启动nohup java -jar /opt/yuyao/springboot-system-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod /opt/yuyao/logs/startup.log 21 前端打包需要先在本地或服务器上执行npm install然后npm run build生成dist目录。dist目录放到Nginx的html目录下Nginx配置里需要做两件事一是将/api开头的请求反向代理到后端8080端口二是配置history路由回退。server { listen 80; server_name yourdomain.com; root /opt/yuyao/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files这行是Vue Router使用history模式的关键没有它刷新页面时会直接404。这个问题我遇到的时候排查了很久最后才发现原来是Nginx没有回退到index.html。6.3 三个让我印象深刻的线上问题第一个是数据库连接池连接数耗尽。项目刚上线时因为后端的Druid连接池配置了最大20个连接但每个查询都很慢导致请求堆积连接全部被占满。后来通过阿里Druid监控发现是有几条SQL没有走索引全表扫描耗时太久。我把方剂表的关键字段补了索引之后连接数一下子降下来了。经验是连接池参数和环境强相关没有绝对正确的数值上线后一定要监控慢SQL。第二个是跨域问题。开发环境我通过Vite的proxy代理解决但生产环境前端和后端部署在不同端口时需要后端配置CorsFilter。这个项目里我选择在生产环境用Nginx统一反向代理让浏览器看到的跨域请求变为同源这比在后端开CORS更清洁。第三个是用户上传的图片在Nginx下无法访问。因为SpringBoot后端默认只允许Tomcat访问本地目录而Nginx静态资源配置没跟上。我的解决方法是把图片上传目录单独配置为Nginx的静态目录location /uploads/ { alias /opt/yuyao/uploads/; }。这个问题本身不难但如果不把前后端静态资源访问路径理清很容易在部署过程中绕圈。7. 项目复盘这些经验比源码本身更值钱7.1 “不过度设计”的原则整项目开发过程中我一直提醒自己不要过度设计。比如用户权限先用简单的role字段解决不做五张表的细粒度权限模型内容检索先用MySQL LIKE模糊查询不上Elasticsearch文件存储用本地磁盘不上对象存储。这些决定并不“高级”但在这个项目当前的数据量和使用规模下它们是最务实的方案。当项目真正跑起来之后你会发现80%的管理系统根本不需要微服务、消息队列、Redis这些重型组件。用一个单体SpringBoot应用把业务逻辑做扎实反而比一上来就分布式更容易维护。功能模块之间用清晰的Service边界分开将来真需要拆分也有重构的余地。7.2 给准备做同类系统的人的建议如果你也想做一个中医药管理类的系统我有几条具体建议。第一数据字典一定要提前定义好比如方剂来源、药材性味、状态码这些在数据库里统一用编码表示前端再做一次映射不要直接在页面上显示原始数字。第二内容审核流程不能省任何面向用户的中医信息都必须有审校机制不然一旦出了问题平台会承担不必要的责任。第三操作日志从第一天开始就要记录谁在什么时候修改了哪首方剂、改了什么字段全部留痕这样出了问题才能溯源。7.3 可以继续扩展的方向这个项目跑通只是第一步后续我给自己规划了几个扩展方向。第一内容量上来之后可以引入Elasticsearch做全文搜索把方剂和食材的名称、别名、功效做成索引查询速度会明显改善。第二如果想走内容工作流可以接入Flowable引擎把方剂的提交、审核、发布做成真正的审批流而不是简单的状态更新。第三面向C端用户做一个H5页面或小程序让普通用户直接用微信登录浏览和收藏这也能让平台的数据真正流动起来。如果让我重新做一遍我不会在项目启动第一天就去纠结界面样式而会把数据表字段和业务状态流转设计得更细。一个管理系统的灵魂是数据模型只要模型稳了后面的接口和页面都是往上添砖加瓦的事。这个基于SpringBoot Vue MyBatis MySQL的经方药食两用服务平台现在已经稳定运行在日常维护中希望我的这些踩坑记录和设计思路也能让你少走几条弯路。