☰
SpringBoot+Vue3药食同源服务平台源码解析:知识库与推荐系统实现
2026/10/14 7:04:56 网站建设 项目流程

中医养生这几年越来越热,尤其“药食同源”这个概念被很多养生人群接受之后,线下调理馆、线上内容平台都在找一套好用的工具。前阵子我完整拆解了一套经方药食两用服务平台源码,技术栈是Java SpringBoot + Vue3 + MyBatis + MySQL,前后端分离的形式。这套系统把中医经典方剂的管理、药食同源食材的检索、个性化食养推荐、健康档案维护整合到了一起,用户在网页上完成注册登录后,可以按症状和体质查找适合的食材搭配方案,运营方则通过后台维护方剂和食材数据。

这类系统在健康管理、养生服务领域的价值很容易被低估。看起来只是“查询+推荐”,真正落地的时候你才会发现,中医知识结构化的难度、剂量换算的严谨性、食材与方剂多对多关联的建模,每一块都需要把业务规则吃透。这也是我写这篇文章的原因——把整条技术链路的思考过程、数据库设计、核心实现细节和踩过的坑完整记录一遍。适合谁看?给准备做医疗健康类管理系统的开发者、想把自己的经方知识库产品化的朋友,以及需要一套可复用前后端分离样板项目的Java程序员。

1. 业务拆解:经方、药食同源和平台形态

1.1 先搞清楚经方与药食两用的概念

在这套系统里,我绕不开两个业务术语:经方与药食两用食材。

“经方”指经典医学著作《伤寒论》《金匮要略》中记载的方剂,特点是药味少、组方精炼、主治明确。而“药食两用”指既是食物、又能作为药材使用的食材,这类目录按国家药食同源相关目录来界定,比如山药、大枣、枸杞、茯苓、百合、莲子、薏苡仁等就在其中。系统之所以要把两者绑定在一个平台里,是因为普通用户并不懂方剂组方的逻辑,但他们想知道“我最近睡不好、容易疲劳,可以吃点什么”。于是平台要做的事情就是把经方的“方义”拆出来,翻译成老百姓能直接看的食养推荐。

举个例子,针对气虚体质,传统经方里讲究补气健脾,平台就可以从食材库里匹配出党参、山药、大枣、莲子等食药物质,组合成一道炖汤方案。整个过程中,经方不是最终输出物,而是推荐逻辑的知识底座。搞懂这一点,你才能理解数据库为什么要设计成方剂表、食材表、关联表三件套。

1.2 平台的角色与核心流程

这种平台一般有四种角色:游客、注册用户、运营管理员、超管。游客只能看公开内容;注册用户可以维护健康档案、收藏方剂、获取推荐;运营管理员负责食材和方剂数据的录入审核;超管负责用户与权限管理。

核心业务闭环是这样的:用户输入症状标签和体质类型,系统把输入映射到证型(比如气虚、阴虚、湿热),再根据证型匹配到经方和食疗模板,过滤掉禁忌和过敏提示之后,返回一组食材搭配方案。这几步看似不复杂,落到代码里就是知识库、规则库和推荐逻辑的配合,后面我会重点展开。从产品形态上看,这个闭环既是内容展示工具,也是轻量级智能决策工具,比单纯做一个药材信息网站要有深度得多。

1.3 为什么这类项目值得做

从市场需求看,药膳餐饮、养生馆、居家食养、健康管理公司都在做内容数字化,但成熟平台很少,尤其是能把经方知识结构化、有据可查的就更少。从技术锻炼角度看,这套系统里有权限管理、复杂条件查询、知识库建模、推荐过滤、对象存储、日志审计等等,麻雀虽小五脏俱全。拿它当毕业设计、个人项目、企业内部系统原型都很合适,前后端分离的模式也能直接把手感迁移到真实工作中。

我身边就有朋友在这个源码基础上二次开发,把推荐模块改成了门店菜品推荐,前端几个页面一换,一套养生馆预约点餐系统就出来了。这就是这类项目的价值:业务边界清晰,技术方案通用,替换壳子就能适应新场景。

2. 技术选型解析:SpringBoot、Vue3、MyBatis、MySQL的搭配逻辑

2.1 SpringBoot为什么够用

后端选择SpringBoot,不是因为它新,而是因为它把Spring配置的繁琐操作做了大量收敛。项目用SpringBoot 2.7.x版本起步,内嵌Tomcat,只需要一个main方法就能启动。配合spring-boot-starter-web、spring-boot-starter-security、mybatis-spring-boot-starter、mysql-connector-java这些依赖,一个完整服务端工程几分钟就能初始化完成,具体依赖版本在pom.xml里统一管理。

Spring Security在这里承担了登录认证、接口鉴权、密码加密三件事。BCryptPasswordEncoder加密用户密码,JWT作为无状态令牌,登录后前端拿到token,每次请求放在Authorization头里,后端通过拦截器统一校验。无状态的好处很多,服务可以水平扩展,sticky session也完全不需要考虑。早期做单体应用时那种基于Session的登录方式,在前后端分离架构里维护成本高、跨域麻烦,换成JWT之后整体清爽很多。

2.2 Vue3的前端工程结构

前端选择Vue3主要看重Composition API的代码组织能力,用起来比Options API更适合中小团队的协作。工程用Vite脚手架初始化,开发时热更新速度比老配置快好几倍,生产构建也简单。UI组件库我选了Element Plus,表格、表单、弹窗开箱即用,非常适合做管理系统。

前端目录大致分成三层:api层封装接口请求,views层放页面组件,store层用Pinia管理全局状态。路由用Vue Router 4,注册守卫做登录拦截,什么页面可以匿名访问、什么页面必须登录,在beforeEach里一次搞定。axios实例里做baseURL统一配置和token注入,响应后对业务状态码统一处理。这样结构的前端工程不管你做推荐页面还是后台管理,替换个壳就能复用到别的项目上。

Vue3项目里还有一个容易被忽略的选择:组合式函数。推荐页面里需要处理症状标签勾选、滚动加载、收藏状态变化这些逻辑,把它抽成useRecommend、useFavorite这样的hook,页面模板里只留展示代码,维护起来特别清晰。

2.3 MyBatis与MySQL的组合取舍

数据访问层没有跟风用JPA,而是选MyBatis,因为这种知识库类系统的查询条件非常多、非常动态。用户可以按方剂名称、出处、主治、症状标签、食材性味任意组合筛选,JPA的派生查询面对这种复杂动态条件会很痛苦,而MyBatis的XML动态SQL能把if、where、foreach写到极致。加上这种系统的SQL本来就比较复杂,MyBatis保留SQL可控性对后期性能调优帮助很大。

数据库选MySQL 8.0,理由同样简单直接:这种业务的关系模型很清晰,用户、方剂、食材、标签、归档天然就是一张张表。MySQL的事务能力让推荐记录、收藏操作、内容审核这些写到库里的逻辑不必担心数据异常。如果哪天单库扛不住,垂直拆库、读写分离这套路线也成熟,不至于把自己锁死。之前我也想过用MongoDB存食材文档,但很快放弃——因为食材和方剂之间的关联关系随时要联表统计,文档模型处理这种多对多关系远没有关系型数据库顺手。

2.4 前后端分离的工程目录设计

前后端分离不只是把前端工程和后端工程分开,更重要的是接口契约和发布方式。后端只提供JSON接口,前端页面通过Nginx做静态资源服务,所有 /api 前缀请求由Nginx反向代理到SpringBoot端口。这样做的好处是团队可以并行开发,前端不会受后端服务进程影响,上线时两边单独发版。

工程目录我习惯分成backend和frontend两个模块。后端按controller/service/mapper/entity四层分包,数据库连接、Redis缓存、日志这些配置再独立成config包。前端把components拆成业务组件和通用组件,公共请求、工具函数、常量配置分别放在utils、api、constants目录里。这个目录规范一旦定下来,后续加功能基本只是按模板填空。接口文档我用Swagger自动生成,每次后端改完entity或者controller,接口文档同步更新,前端联调时不用反复来问字段含义。

3. 数据库设计与建表实操

3.1 从业务概念梳理实体关系

建表不急着写SQL,先把实体画出来。我梳理下来的核心实体有这么几个:用户(sys_user)、角色(sys_role)、方剂(formula)、食材(medicinal_food)、方剂与食材的关联(formula_food_rel)、症状标签(symptom_tag)、健康档案(health_profile)、收藏记录(favorite_record)、推荐记录(recommend_log)。

实体关系最核心的就是方剂和食材之间的多对多关系。一个经方里包含多种食材方向,一种食材可以出现在多个经方推荐组合中,所以必须引入一张关联表。症状标签和证型之间是多对多,健康档案则要与用户一对一关联,保存最新的体质与过敏信息。这类系统不需要过度抽象,业务流程里出现一次的数据就建一张表,宁可表多也不要在一个字段里塞一堆逗号分隔的JSON,否则后面统计、筛选都会非常痛苦。

3.2 核心数据表的建表SQL

这里给出实际用到的几个核心表结构,注释写得很详细,直接照着建就行:

CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', password VARCHAR(100) NOT NULL COMMENT 'BCrypt密文', nickname VARCHAR(50) COMMENT '昵称', phone VARCHAR(20) COMMENT '手机号', gender TINYINT DEFAULT 0 COMMENT '性别:0未知1男2女', status TINYINT DEFAULT 1 COMMENT '状态:1正常0禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE formula ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL COMMENT '方剂名称', source_book VARCHAR(100) COMMENT '出处典籍', syndrome VARCHAR(255) COMMENT '主治证型', ingredients TEXT COMMENT '原方组成描述', dosage_text TEXT COMMENT '原方剂量文字描述', usage_text TEXT COMMENT '煎服法说明', taboo TEXT COMMENT '禁忌提示', category VARCHAR(50) COMMENT '分类:补气/滋阴/清热等', status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='经方资料表'; CREATE TABLE medicinal_food ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT '食材名称', aliases VARCHAR(200) COMMENT '别名集合,逗号分隔', nature VARCHAR(20) COMMENT '性:寒凉温平热', flavor VARCHAR(50) COMMENT '味:甘苦酸辛咸', channel VARCHAR(50) COMMENT '归经', efficacy VARCHAR(500) COMMENT '功效', is_food_medicinal TINYINT DEFAULT 1 COMMENT '是否属于药食同源目录', picture_url VARCHAR(255), status TINYINT DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药食同源食材表'; CREATE TABLE formula_food_rel ( id BIGINT AUTO_INCREMENT PRIMARY KEY, formula_id BIGINT NOT NULL, food_id BIGINT NOT NULL, weight TINYINT DEFAULT 50 COMMENT '匹配权重0-100', remark VARCHAR(255) COMMENT '配伍说明', UNIQUE KEY uk_formula_food (formula_id, food_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='方剂食材关联表'; CREATE TABLE health_profile ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, constitution VARCHAR(30) COMMENT '体质:气虚/阳虚/阴虚等', allergy VARCHAR(200) COMMENT '过敏食材', diseases VARCHAR(500) COMMENT '基础疾病描述', age INT, gender TINYINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户健康档案表'; CREATE TABLE recommend_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, symptom_tags VARCHAR(500) COMMENT '用户勾选的症状标签', result_ids VARCHAR(1000) COMMENT '推荐返回的食材ID集合', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐记录表';

这些表看着不多,但已经覆盖了平台全部核心业务。药食同源目录更新后,管理员在后台改medicinal_food表数据即可,不用动代码。注意formula表里的dosage_text字段一定要用TEXT类型而不是VARCHAR,因为原文里经常有几百字符的描述,裁短了反而丢失原始信息。

3.3 建表时的几个细节

字符集一定要用utf8mb4,不是utf8,否则生僻字和emoji符号在保存时会乱码。所有表都加上create_time和update_time两个通用字段,后续排查数据和做审计都会方便很多。索引上,formula表的name字段、medicinal_food表的name字段、recommend_log表的user_id字段都需要建索引,否则数据量过万之后推荐历史查询会开始变慢。

另外我踩过一个坑:关联表的唯一键一定要加上,否则后台重复提交导致同一条方剂食材关系出现两条数据,推荐的时候一个方子会重复返回同一种食材。这个在运营测试时很难发现,数据量一大就会冒出来。

建好表结构之后,向表里填充数据的时候,我建议运营侧按“食材库优先、方剂库其次、关联关系最后”的顺序录入。食材库先录常用药食同源物质,确认名字、别名、性味归经都对;方剂库按典籍分类录入,尽量保留原文;所有基础数据稳定后再去维护formula_food_rel关联和推荐模板,否则边录边调容易数据混乱,一旦上线后回头清理脏数据,成本比多录几天的数据高得多。

4. 核心业务逻辑:推荐、换算与权限控制

4.1 登录认证与用户权限

前端登录页提交用户名密码,后端调用LoginService的login方法,先用BCryptPasswordEncoder匹配密码,成功之后生成JWT返回给前端。JWT里我放user_id和role两个核心字段,过期时间设置为24小时。后续每次请求都通过JwtFilter过滤器里解析token反查用户状态,被禁用用户直接拒绝访问。

权限这块用简单的角色字符串判断。Spring Security里配置了三个规则:/api/auth/**和公开接口直接放行,/api/admin/**要求ROLE_ADMIN,其余接口只要登录即可。因为平台早期就是健康知识和推荐服务,这个粒度已经够用。如果你后面要管到多角色多菜单,可以把菜单表、角色菜单关联表一起加进来,做成动态菜单权限,结构上也不冲突。实际开发中,我见过很多人一上来就做权限设计的大而全,结果半年都用不上,反而增加了前后端联调成本,所以初期够用、中期好扩展,这才是合理的演进思路。

4.2 经方检索的多条件动态SQL

方剂列表页支持按名称模糊搜索、按出处筛选、按主治证型筛选、按标签筛选的组合查询。这个需求如果写死SQL会非常痛苦,所以直接用MyBatis XML的dynamic SQL做了。核心片段长这样:

<select id="searchFormula" resultType="com.example.entity.FormulaVO"> SELECT f.id, f.name, f.source_book, f.syndrome, f.category FROM formula f <where> <if test="keyword != null and keyword != ''"> AND (f.name LIKE CONCAT('%', #{keyword}, '%') OR f.syndrome LIKE CONCAT('%', #{keyword}, '%') OR f.ingredients LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="sourceBook != null and sourceBook != ''"> AND f.source_book = #{sourceBook} </if> <if test="category != null and category != ''"> AND f.category = #{category} </if> </where> ORDER BY f.id DESC </select>

这里注意,like里的%不要直接在Java代码里拼,而是用CONCAT函数处理,可以避免参数绑定出问题,也防止因为漏掉单引号引发SQL异常。如果条件很多, 标签会自动把多余的AND去掉,不用自己去裁SQL,这个细节很多人都吃过亏。多条件查询是MyBatis最常用的场景,把这种写法吃透了,你再去写报表查询、统计查询都会顺手很多。

4.3 推荐引擎的实现思路:从证型到食材

推荐引擎是这套系统的业务核心,我把它设计成三个步骤逐步缩小范围的过程。

第一步,收集用户输入。用户在前端勾选症状标签,比如“易疲劳”“失眠”“口干”,同时选择体质(可选),系统把这些标签拼接起来。

第二步,证型映射。症状标签和证型做一次匹配,得到一个证型集合,比如“阴虚”+“气虚”。这里用一张symptom_tag表和tag_formula_rel表做桥接,每个证型再关联到若干经方模板和应用于食物搭配的模板组。

第三步,食材匹配与过滤。每个模板组下面绑定了若干食材组合,这些组合数据来自运营在后台维护的formula_food_rel表。过滤规则依次是:是否属于药食同源目录、是否与用户过敏项冲突、是否存在孕妇哺乳期禁忌、是否与用户健康档案里的基础疾病冲突。最后按食材匹配权重的加权得分排序,输出Top N食材。

用一句话概括这个推荐算法:它不是做机器学习那种黑盒,而是把中医知识显性化成规则,让每条推荐结果都能从知识库里追溯依据。这样做的好处是用户追问“为什么推荐这个”的时候,系统可以直接把对应经方和食材功效翻出来解释,这一点对健康类产品的信任感影响极大。

做健康类平台,用户对推荐结果的第一反应往往是“为什么给我推这个”。这个项目在返回食材列表时,同步返回每条结果对应的recommend_reason字段,来自运营维护的推荐理由模板,比如“对应脾胃气虚经典方向”或者“食材性平味甘,适合阴虚体质的日常调养”。前端把理由展示在卡片下方,用户能看懂也就不容易产生疑惑。运营在日常维护中发现某种食材推荐理由太生硬,可以直接在后台改文案,不需要发版。

4.4 剂量换算与方剂安全校验

经方里的剂量是历史词汇单位,跟现代克数并不一致。平台整理数据时特别保留了“原方剂量”字符串,同时在换算表里记录了多种算法:按一两约15克、按一两约3克、按一两约10克的临床习惯分别进行加工。实际展示给用户时,推荐方案直接使用现代计量单位,比如“干山药15g、大枣3颗、莲子10g”,不展示一刀切的历史换算结果,这一点非常关键,避免用户自己在家按古代剂量抓药出错。

安全校验的另一个工作是禁忌词匹配。食材库和方剂库都维护了taboo字段,推荐时与用户健康档案做字符串匹配。虽然药食同源目录里的物质整体上相对安全,但个体差异仍然存在,比如糖尿病患者要控制大枣、龙眼这类含糖食材的推荐量,易过敏人群对海鲜类、菌菇类食材要特别小心。这些规则不复杂,就是一个一个积累进规则表的过程。我一开始也没想到要做这么细,直到测试阶段一位用户录入过敏食材“花生”后,推荐页仍然出现了花生制品,才意识到过滤模块必须从一开始就接入,而不是等上线后再补。

4.5 前端核心页面与交互细节

前端页面里最有特点的是推荐结果页。用户选完症状标签后,页面会先用骨架屏展示计算过程,然后返回卡片式结果:每张卡片包含食材图片、名称、性味归经、推荐理由、用法用量建议、“加入收藏”按钮。推荐理由这一栏直接把后台匹配到的经方名称和核心功效写进去,既有知识属性又有说服力。

另外方剂详情页用了标签式布局,基本信息、组成解析、对应的药食同源食材、注意事项各占一个tab。基本信息区展示出处、主治、禁忌原文,组成解析区把原方每味药和平台推荐的食材方向放在一起对比。后台管理页则用了表格+抽屉的形态,运营人员点开某条方剂,抽屉里直接编辑关联食材、调整权重和禁忌说明,保存后立即生效。这样运营修改知识库的路径很顺,不需要开发介入,平台内容更新速度就有了保障。

5. 实操环节:项目搭建、接口实现与部署

5.1 后端工程初始化和关键配置

我建后端工程习惯直接开一个SpringBoot项目,然后在pom.xml里加入web、security、validation、mybatis、mysql这些starter。application.yml里三个必配项别忘:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/food_medicinal?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true

这里大学问在两点:一是连接串里必须带serverTimezone=Asia/Shanghai,很多人在部署到云服务器后发现数据库时间差8小时,就是因为这个参数没配;二是mybatis的map-underscore-to-camel-case必须打开,数据库字段的下划线风格和Java实体的驼峰风格才能自动对齐,否则你每个查询都要写别名。这种配置项目早期不注意到,等到接口联调时返回字段全是null,再回头排查就要花不少时间。

5.2 推荐接口的完整实现路径

推荐接口的逻辑我放在RecommendService里,核心代码流程可以概括成四步:同步用户最新健康档案、解析症状标签、查询匹配模板、过滤禁忌并组装结果。实际使用时查询次数较多,所以我对核心查询都加了本地缓存,标签到证型的映射表在系统启动时加载到内存。

这里给出推荐过滤环节的伪代码,帮助理解整条链路:

List<FoodVO> recommend(userId, List<String> tags) { UserProfile profile = profileMapper.findByUserId(userId); List<String> syndromeTypes = tagSyndromeMapper.findSyndromes(tags); List<TemplateVO> templates = templateMapper.findBySyndrome(syndromeTypes); List<FoodVO> foods = new ArrayList<>(); for (TemplateVO tpl : templates) { for (FoodVO food : tpl.getFoods()) { if (!safeForProfile(food, profile)) continue; food.setScore(calcWeight(food, tpl.getWeight())); foods.add(food); } } return foods.stream() .sorted(Comparator.comparingInt(FoodVO::getScore).reversed()) .limit(10) .collect(Collectors.toList()); }

排名的关键在calcWeight这个方法,它综合了模板匹配度、食材性味与证型的适配度、关联表里的管理员权重。权重值我建议直接存数据库,不要在代码里写死,运营人员调整推荐效果的时候只需要在后台改一个数字,不用重新发布版本。这里还要注意安全漏洞:用户传入的症状标签列表一定要做白名单校验,防止恶意传入大流量数据或者注入特殊符号,我直接在Controller层加了参数长度限制和枚举匹配,超长或者不合法直接返回参数错误。

5.3 前端接口封装与联调经验

前端axios请求封装我统一放在utils/request.js里,baseURL指向/api,请求拦截器添加token,响应拦截器根据code字段做全局提示。在这个项目里,后端自定义了统一的返回体结构,包括code、message、data三个字段,前端所有接口都按这个结构去解包。这个契约从第一天就要定好,越早定越省事,否则后端改一个字段名,前端十个页面都要跟着动。

接口联调时还常见一个token过期问题,JWT通常有24小时有效期,后台管理员修改用户密码或禁用用户后,旧的token不会立即失效。如果系统对安全要求高,可以在Redis里维护一个token黑名单,但早期版本保持简单即可,等相关数据量增长后再去升级。联调阶段跨域问题几乎必踩。开发时前端跑在5173端口,后端跑在8080端口,不配置CORS一步都走不动。我的处理办法是在后端加一个WebMvcConfigurer配置类,允许本机开发地址的跨域访问,生产环境则完全交给Nginx反向代理,不让后端直接暴露跨域策略。配置类里注意要把allowedMethods、allowedHeaders都配全,否则前端发PUT、DELETE请求会莫名失败。

5.4 打包部署流程

部署时后端用mvn clean package -DskipTests打jar包,上传到服务器后用nohup java -jar命令后台运行。前端在工程目录执行npm run build,产物放到Nginx根目录的dist文件夹。Nginx配置里把 /api 前缀的请求proxy_pass到后端8080端口,location中同时处理history模式路由的try_files。这样一套下来,用户访问域名看到的是Vue3单页应用,接口请求自动转发到SpringBoot服务,前后端完全隔离,单独更新任何一端都不会影响另一端。

我给个配置片段供参考:

server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

try_files $uri $uri/ /index.html这行必须要有,否则刷新Vue Router的history路由页面会报404。部署时还要注意改配置文件:数据库连接串、JWT密钥这类环境相关配置不要写死在代码里,用Spring Profile区分开发、测试、生产三个环境,上线时只需要指定active profile,代码不用重新编译。

6. 开发中的常见问题与排查技巧

6.1 登录后接口全部401

新手最常见的问题是登录拿到了token,但后续请求还是401。排查顺序基本是:先看浏览器请求头里有没有带Authorization字段,再看后端过滤器有没有放行登录接口本身,最后查看token解析时签名密钥是否一致。最容易出问题的点是JWT密钥写死在前端或配置中心不同步,前端打包后密钥改了后端还在用旧密钥。还有一个隐蔽情况:JwtFilter注册顺序不正确,过滤链里还没有执行就已经被认证逻辑拦截了。遇到401别急着改代码,先看后端日志里拦截失败的具体异常,往往比猜更高效。

6.2 时间差8小时与日期格式不对

部署到云上发现推荐记录创建时间错8小时,十有八九是MySQL连接串缺了serverTimezone=Asia/Shanghai。前端显示日期变成数组或者其他怪格式,则是Jackson配置没指定date-format,返回的LocalDateTime字段被序列化成了默认形式。这两个问题在一开始就把配置配全,能省下很多排查时间。如果你用了Redis做缓存,存储时间也建议统一以UTC时间戳为主,展示时再转本地时区,这样多实例部署时时间不会因为服务器时区不同而对不上。

6.3 中文乱码与emoji保存失败

数据库连接串没有characterEncoding=utf8的时候,前端录入食材别名里的生僻字会乱码。表结构不是utf8mb4时,遇到emoji直接报错。这两条都算老生常谈,但我确实见过不止一个团队在项目上线后才暴露,到时候改表字符集成本高得多。建库第一时间用utf8mb4,连接串三件套写全,万事大吉。另外在后端接口的@RequestParam上,如果有必要可以显式声明produces和consumes的字符编码,虽然在SpringBoot里一般不会出问题,但遇到自定义HttpMessageConverter时还是要留意。

6.4 多条件查询返回结果不对

业务性更强的一个坑是MyBatis的if条件里判断空字符串。比如用户不输入关键词时,把keyword设置了空字符串,这时如果只判断keyword != null,就会出现一条永远为真的WHERE子句,查出一堆不该出现的数据。正确的写法是keyword != null and keyword != '',标签数组也要用list != null and list.size() > 0判断。还有一个小坑是CDATA标签的使用,XML里写比较运算符要转义,比如小于号直接写会报错。像时间范围查询这种场景,尽量把比较操作写在SQL里,配合CDATA包裹,既避免语法错误也方便阅读。

6.5 全表扫描导致的慢查询

推荐记录查询、用户收藏列表这些功能在数据量上来后会变慢。我建议在常用筛选字段上建组合索引,比如recommend_log表加(user_id, create_time)组合索引,favorite_record表加(user_id, formula_id)唯一索引。开启MySQL慢日志,把超过两秒的SQL捞出来看看执行计划,大部分慢查询原因就是没有走索引。每次排查慢查询,我习惯先看查询条件里哪些字段有索引、哪些字段被函数包住了,比如在日期字段上用了DATE()函数就无法走索引,这种时候改成范围查询会快很多。

6.6 经方数据录入中的细节坑

最后单独说一个业务数据坑:同一种食材在不同典籍里别名很多,比如“山药”也叫“薯蓣”,“大枣”在不同地方叫法也不同。录入时如果不把别名维护进aliases字段,用户按习惯用语搜索就搜不到这条食材。这个工作看似琐碎,其实是决定产品体验的关键,运营侧需要把常见的搜索词提前盘点进去。另外推荐模板里的“match_tags”字段不要只写一个证型关键词,要多维护几个相近检索词,用户哪天换了一个词表达同一个症状,推荐结果才不会一下子就空掉。

整套经方药食两用服务平台捋完之后,我最大的感觉是这套系统的技术门槛并不高,真正的价值在业务建模和知识库数字化。SpringBoot+Vue3+MyBatis+MySQL这一组合非常成熟,四者配套使用开发效率极高,而经方与药食同源内容的合理组织才是让系统从“管理系统”变成“服务平台”的原因。如果我自己接着在这个项目上迭代,下一步会加入食材季节性推荐、饮食日记记录和更细粒度的证型匹配,这样用户粘性和运营深度还会再上一个台阶。

如果你打算用这套源码起步,我建议先别急着加花哨的功能,而是先把方剂、食材、关联关系这三张表的数据录扎实,再谈推荐效果。最后说句实在的:这类项目的核心是数据质量和规则沉淀,技术永远只是工具。有了方向,你完全可以从这份源码里把大半个健康服务平台拆出来,变成属于自己的产品。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询