简介这份资源是面向Java全栈开发者与中医药信息化方向学习者的中医药知识科普平台完整项目源码基于SpringBoot与Vue构建前后端分离架构整合MyBatis、MySQL、ElasticSearch与Redis解决中医药知识分散、检索效率低、传播方式传统的问题适合作为毕业设计、课程设计或企业级项目练手参考。压缩包共151个文件约327KB以124个Java源文件为核心涵盖控制器、服务层与业务逻辑实现另含12个JSON配置、7个XML映射文件、1个YML配置、1个SQL示例数据脚本及Dockerfile、说明文档等结构清晰便于按模块阅读。项目通过ElasticSearch实现药方、诊疗方法等资料的全文快速检索借助Redis缓存减轻数据库压力并应对高并发访问MyBatis负责数据持久化MySQL存储中医药资料。目前已有98人学习下载读者可从中获取前后端分离架构设计思路、搜索引擎与缓存集成方案及完整目录组织方式适合需要落地实践的中高级开发者参考。1. 中医药知识库系统从 SpringBoot 到 ElasticSearch 的完整落地拆解中医药领域的数字化有个很现实的痛点知识散落在古籍、药典、临床案例里术语体系复杂同一种药材在不同文献里可能有十几种叫法。用传统的关系型数据库做关键词匹配搜黄芪搜不到黄耆搜消渴匹配不到糖尿病相关的古方记载。这套基于 SpringBoot Vue 的中医药知识科普平台核心要解决的就是这个问题——用 ElasticSearch 做全文检索和分词把中医药术语的模糊匹配做扎实同时用 Redis 扛住高频查询MySQL 存结构化数据。技术栈覆盖了当前 Java 全栈开发的主流组合后端 SpringBoot MyBatis MySQL Redis ElasticSearch前端 Vue 做知识展示和后台管理。适合正在找毕业设计选题的计算机专业学生也适合想练手 ElasticSearch 中文分词和 SpringBoot 整合方案的初中级开发者。整个项目不是一个简单的 CRUD 演示检索模块和缓存策略有实际可调优的空间。2. 环境搭建与依赖版本别让版本冲突吃掉一整天2.1 后端依赖选型与版本锁定SpringBoot 和 ElasticSearch 的版本兼容是个老生常谈的坑。SpringBoot 2.x 默认集成的 ES 客户端版本和 ES 服务端版本必须对齐否则启动时直接报NoNodeAvailableException。我一般会先确定 ES 服务端版本再反推 SpringBoot 版本。常见做法是用 SpringBoot 2.7.x 配合 ElasticSearch 7.17.x这个组合经过大量项目验证踩坑概率最低。如果项目里用了spring-boot-starter-data-elasticsearch注意它默认走的是 REST Client 还是 Transport Client——7.x 之后 Transport Client 已经被标记废弃新项目一律用 REST Client。!-- pom.xml 关键依赖 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version !-- 与 ES 7.17 兼容性经过验证 -- /parent dependencies !-- Web 基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis 整合 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version scoperuntime/scope /dependency !-- Redis 缓存 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- ElasticSearch 客户端 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-elasticsearch/artifactId /dependency !-- 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency /dependencies这段 POM 里几个关键点mysql-connector-j是 MySQL 8.x 之后的新坐标老教程里写的mysql-connector-java在 8.0.31 之后已经改名不改会报找不到驱动。Druid 连接池不是必须的但中医药知识库查询并发不低用 Druid 的监控面板排查慢 SQL 会方便很多。2.2 ElasticSearch 安装与中文分词插件ES 的安装本身不复杂但中医药场景必须装中文分词插件否则默认的 standard 分词器会把黄芪切成黄和芪检索效果直接崩掉。常见做法是装 IK 分词器版本必须和 ES 服务端完全一致。# 下载 ES 7.17.18Linux 环境 wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.18-linux-x86_64.tar.gz tar -zxvf elasticsearch-7.17.18-linux-x86_64.tar.gz cd elasticsearch-7.17.18 # 安装 IK 分词器版本必须严格对应 ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.18/elasticsearch-analysis-ik-7.17.18.zip # 启动前调整 JVM 内存默认 1G 对知识库够用但生产环境建议 2G # 修改 config/jvm.options # -Xms2g # -Xmx2g # 启动 ./bin/elasticsearch -dIK 分词器装完后需要重启 ES 生效。验证方式是调分词接口curl -X POST http://localhost:9200/_analyze -H Content-Type: application/json -d { analyzer: ik_max_word, text: 黄芪桂枝五物汤 }如果返回结果里能看到黄芪桂枝五物汤这些完整词条说明分词器工作正常。ik_max_word是细粒度分词适合索引阶段ik_smart是粗粒度适合查询阶段。中医药术语建议索引用ik_max_word查询用ik_smart这样既能保证召回率又不会把查询词切得太碎导致噪音。2.3 Vue 前端环境与跨域配置前端用 Vue 2 还是 Vue 3 取决于项目实际代码但从热搜词里vue安装及环境配置vue devtools插件下载来看很多人卡在环境这一步。Node.js 版本建议用 16.x 或 18.x太新的版本可能和 node-sass 冲突。# 安装依赖 npm install # 如果 node-sass 报错换成 sassdart-sass npm uninstall node-sass npm install sass --save-dev # 启动开发服务器 npm run serve跨域问题在前后端分离项目里必现。开发阶段在vue.config.js里配代理// vue.config.js module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, // 后端地址 changeOrigin: true, pathRewrite: { ^/api: } } } } }changeOrigin: true这个参数不加后端拿到的 Host 头还是前端地址某些安全校验会拦截。pathRewrite看后端接口有没有统一前缀有的话要去掉。3. 核心模块实现检索、缓存与数据层3.1 ElasticSearch 索引设计与中医药字段映射中医药知识库的索引设计不能照搬通用方案。药材、方剂、症状、典籍这几个核心实体的字段类型和分词策略需要区别对待。比如药材名称用keyword做精确匹配同时用text IK 分词做模糊搜索功效描述用text做全文检索典籍出处用keyword做聚合筛选。// 药材索引映射定义通过 RestHighLevelClient 创建 // 实际项目中可以放在初始化脚本或 ApplicationRunner 里 String mapping { properties: { name: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart, fields: { keyword: { type: keyword } } }, alias: { type: text, analyzer: ik_max_word }, category: { type: keyword }, efficacy: { type: text, analyzer: ik_max_word }, meridian: { type: keyword }, description: { type: text, analyzer: ik_max_word }, source: { type: keyword }, createTime: { type: date, format: yyyy-MM-dd HH:mm:ss } } } ;name字段同时建了text和keyword两个类型这是 ES 的 multi-field 特性。text用于全文检索keyword用于排序和精确过滤。alias字段专门存别名比如黄芪的别名黄耆绵黄芪都放进去这样用户搜任何一个叫法都能命中。3.2 SpringBoot 整合 ES 的检索服务检索服务是这套系统的核心。用户输入关键词后需要同时匹配药材名、别名、功效描述还要支持按分类和出处筛选。用BoolQueryBuilder组合多个条件should控制召回filter控制筛选。Service public class HerbSearchService { Autowired private RestHighLevelClient client; private static final String INDEX_NAME herb_index; public PageResultHerbDoc search(String keyword, String category, int page, int size) throws IOException { SearchRequest request new SearchRequest(INDEX_NAME); SearchSourceBuilder builder new SearchSourceBuilder(); BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); if (StringUtils.hasText(keyword)) { // 名称权重最高别名次之功效描述最低 boolQuery.should(QueryBuilders.matchQuery(name, keyword).boost(3.0f)); boolQuery.should(QueryBuilders.matchQuery(alias, keyword).boost(2.0f)); boolQuery.should(QueryBuilders.matchQuery(efficacy, keyword).boost(1.0f)); boolQuery.minimumShouldMatch(1); // 至少命中一个字段 } else { boolQuery.must(QueryBuilders.matchAllQuery()); } // 分类筛选走 filter不参与评分性能更好 if (StringUtils.hasText(category)) { boolQuery.filter(QueryBuilders.termQuery(category, category)); } builder.query(boolQuery); builder.from((page - 1) * size); builder.size(size); builder.highlight(new HighlightBuilder() .field(name).field(efficacy) .preTags(em classhighlight).postTags(/em)); request.source(builder); SearchResponse response client.search(request, RequestOptions.DEFAULT); // 解析结果略核心是把 hits 转成业务对象 return parseResponse(response, page, size); } }boost参数控制字段权重名称匹配的得分是功效匹配的 3 倍这样搜黄芪时名称里带黄芪的药材排在最前面。minimumShouldMatch(1)保证至少命中一个should条件否则会返回全量数据。高亮用em标签包裹前端直接渲染。3.3 Redis 缓存策略与 MyBatis 二级缓存中医药知识库的读多写少特征很明显热门药材和方剂的查询频率极高。Redis 缓存层主要做两件事缓存检索结果和缓存详情页数据。检索结果用keyword category page做 key设置 5 分钟过期详情页数据用herb:{id}做 key设置 30 分钟过期。Service public class HerbCacheService { Autowired private RedisTemplateString, Object redisTemplate; private static final long SEARCH_TTL 5; // 分钟 private static final long DETAIL_TTL 30; public PageResultHerbDoc getSearchResult(String keyword, String category, int page, int size) { String cacheKey String.format(search:%s:%s:%d:%d, keyword null ? : keyword, category null ? : category, page, size); // 先查缓存 Object cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return (PageResultHerbDoc) cached; } // 缓存未命中查 ES PageResultHerbDoc result herbSearchService.search(keyword, category, page, size); // 写回缓存 redisTemplate.opsForValue().set(cacheKey, result, SEARCH_TTL, TimeUnit.MINUTES); return result; } }MyBatis 的二级缓存和 Redis 缓存是两套独立机制。MyBatis 二级缓存作用域是 Mapper 级别适合单表查询结果缓存Redis 是应用级缓存适合跨服务的共享数据。两者同时开启时要注意数据一致性——更新操作要同时清理两层缓存否则会出现数据库改了但页面还是旧数据的玄学问题。!-- MyBatis 二级缓存配置 -- cache evictionLRU flushInterval600000 size1024 readOnlytrue/ !-- 或者在 application.yml 里全局开启 -- !-- mybatis.configuration.cache-enabled: true --flushInterval设 10 分钟size设 1024 条readOnlytrue表示缓存对象不可修改性能更好但要求实体类不涉及并发写。4. 避坑与排查那些让我加班到凌晨的问题4.1 ES 启动报 max virtual memory areas 错误现象ES 启动直接退出日志里写max virtual memory areas vm.max_map_count [65530] is too low。原因ES 默认需要大量内存映射区域Linux 系统默认值不够。解决修改系统参数sudo sysctl -w vm.max_map_count262144永久生效写进/etc/sysctl.conf。Docker 环境要在宿主机改容器内改没用。4.2 IK 分词器装了但分词结果不对现象调_analyze接口返回的还是单字切分没有完整词条。原因要么插件版本和 ES 版本不一致要么索引创建时没指定analyzer要么 ES 没重启。解决先确认elasticsearch-plugin list能看到analysis-ik然后检查索引 mapping 里analyzer字段是否写了ik_max_word。如果 mapping 是动态生成的需要删掉索引重建。4.3 SpringBoot 启动报 Redis 连接超时现象本地开发正常部署到服务器后启动卡在 Redis 连接报Unable to connect to Redis。原因Redis 默认只监听127.0.0.1远程连不上或者防火墙没放行 6379 端口。解决改redis.conf里的bind 0.0.0.0和protected-mode no同时配置密码requirepass。生产环境不要裸奔密码和防火墙都要上。4.4 MyBatis 分页插件失效返回全量数据现象接口传了pageNum和pageSize但返回的还是全部数据。原因PageHelper 的startPage必须紧跟在查询语句之前中间不能插入其他数据库操作。解决检查代码里PageHelper.startPage(pageNum, pageSize)和实际查询之间有没有别的 Mapper 调用。如果有把startPage挪到紧挨着查询的位置。4.5 Vue 打包后接口 404现象开发环境正常npm run build之后部署所有接口请求 404。原因开发环境的代理配置只在devServer生效打包后是静态文件没有代理。解决生产环境用 Nginx 做反向代理把/api转发到后端服务。或者在axios的baseURL里写完整后端地址但这样跨域问题又回来了还是 Nginx 方案最稳。5. 检索调优与数据同步从能用到好用的最后一步检索效果调优是个持续过程。IK 分词器自带的主词典对中医药术语覆盖有限像消渴痹症君臣佐使这类词可能被切碎。常见做法是往 IK 的自定义词典里加词文件在config/ik/custom/mydict.dic每行一个词改完重启 ES 生效。# 追加中医药术语到自定义词典 cat config/ik/custom/mydict.dic EOF 消渴 痹症 君臣佐使 卫气营血 三焦辨证 EOF # 重启 ES ./bin/elasticsearch -d数据同步是另一个容易翻车的地方。MySQL 里的药材数据更新后ES 索引不会自动同步。我一般用两种方案一是业务代码里双写更新 MySQL 的同时更新 ES二是用 Logstash 或 Canal 做增量同步。双写方案简单但有一致性风险Canal 方案重但可靠。项目规模不大时双写加定时全量重建索引就够了。// 双写示例更新药材时同步更新 ES Transactional public void updateHerb(Herb herb) { // 更新 MySQL herbMapper.updateById(herb); // 同步更新 ES try { IndexRequest request new IndexRequest(herb_index) .id(String.valueOf(herb.getId())) .source(JSON.toJSONString(herb), XContentType.JSON); client.index(request, RequestOptions.DEFAULT); } catch (IOException e) { // ES 更新失败记日志走定时任务补偿 log.error(ES 同步失败, herbId{}, herb.getId(), e); } }ES 同步失败不能影响主流程所以 catch 住记日志靠定时任务扫 MySQL 的更新时间字段做补偿。这个补偿任务建议每小时跑一次扫最近两小时有更新的记录重新推送到 ES。验证检索效果有个笨办法但很管用准备一组测试查询词覆盖药材名、别名、功效、症状几个维度每次调完分词或权重就跑一遍看 Top 10 结果是否符合预期。我习惯把测试用例写成一个简单的 JUnit 测试改完参数就跑比手动点页面靠谱得多。从那以后我每次调 ES 相关参数都强制走一遍这个测试集确认召回和排序没有退化再提交代码。希望帮到你。本文还有配套的精品资源点击获取
