
简介这份资源是一套基于Java开发的传统中医药知识数据库源码面向中医药信息化开发者、计算机专业学生及需要构建知识库系统的技术人员用于解决中医药知识从纸质文献向数字化存储、检索与传播转型的问题。压缩包共1024个文件约43.24MB以472个JavaScript文件承担前端动态交互75个CSS与71个HTML文件完成页面布局与样式美化35个Java源码处理后端业务逻辑与数据库连接另有98个PNG、124个GIF等图像资源用于药材与穴位图示展示并包含PHP、ASP、SQL及配置文件支撑部署与数据维护。目前已有111人学习下载。项目涵盖药材、方剂、治疗方法、临床实践等多维数据模型设计目录结构完整适合作为课程设计、毕业设计或知识库系统二次开发的参考蓝本帮助读者理解多语言混合项目的组织方式与数据库设计思路。1. 中医药知识数据库为什么值得用 Java 重做一遍做中医药信息化的同行大多踩过同一个坑手里攒了几万条方剂、药材、证候数据散落在 Excel、Access 甚至纸质扫描件里想查一个「含甘草且主治咳嗽的方」得翻半天。传统中医药知识数据库要解决的就是这件事——把药材、方剂、证候、炮制方法、性味归经这些结构化知识存起来支持按多条件组合检索和关联查询。选 Java 来做不是因为别的语言不行而是这类系统天然是「后台管理 复杂查询 长期维护」的形态Spring Boot 生态成熟、MyBatis 对复杂 SQL 友好、JVM 在数据一致性上省心团队招人也容易。这篇笔记面向两类人想自己搭一套中医药知识库的后端开发者以及手里有数据但不知道怎么落库的中医药从业者。下面从表结构设计一路讲到查询优化和踩坑能照着复现。2. 中医药知识库的表结构怎么设计才不返工中医药数据的麻烦在于「关系多、别名多、层级深」。一味药材有性味、归经、功效、炮制方法一个方剂由多味药材组成还带剂量一个证候又对应多个方剂。如果一开始表结构拍脑袋定后面加一个「异名」字段就得改十几处代码。所以这一章先把数据模型立住再谈 Java 怎么落地。2.1 从药材、方剂、证候三个核心实体拆表我一般把核心实体拆成五张主表加若干关联表思路是「实体归实体关系归关系」避免把一堆字段塞进一张大宽表。表名作用关键字段herb药材主表id, name, pinyin, nature(性), flavor(味), meridian(归经)herb_alias药材异名id, herb_id, alias_nameformula方剂主表id, name, source(出处), effect(功效)formula_herb方剂-药材关联id, formula_id, herb_id, dosage, role(君臣佐使)syndrome证候主表id, name, description性味归经这类字段新手容易直接存成一个字符串「甘、平归心肺经」看着省事查的时候全是LIKE %甘%性能差还容易误匹配。正确做法是拆成枚举或独立字典表nature存「寒热温凉平」的编码meridian用关联表存多条归经。方剂和药材是多对多必须走中间表formula_herb剂量和君臣佐使这种「关系属性」就挂在中间表上而不是挂在药材表上——同一味药在不同方里剂量不同挂错地方数据就废了。建表 SQL 大致如下注意字符集用utf8mb4中医药里生僻字不少utf8存不下CREATE TABLE herb ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 药材正名, pinyin VARCHAR(128) COMMENT 拼音用于检索, nature TINYINT COMMENT 性:1寒2热3温4凉5平, flavor VARCHAR(32) COMMENT 味逗号分隔编码, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_name (name), KEY idx_pinyin (pinyin) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE formula_herb ( id BIGINT PRIMARY KEY AUTO_INCREMENT, formula_id BIGINT NOT NULL, herb_id BIGINT NOT NULL, dosage VARCHAR(32) COMMENT 剂量保留原文如三钱, role TINYINT COMMENT 1君2臣3佐4使, KEY idx_formula (formula_id), KEY idx_herb (herb_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;dosage用VARCHAR而不是数值类型是因为古籍里剂量单位五花八门「三钱」「一两」「等分」都有强行转数值会丢信息。真要统计时再在应用层做单位换算。pinyin字段单独建索引是为了支持拼音首字母检索用户输入gc能查到甘草这个体验比只能打汉字好太多。2.2 用 Spring Boot MyBatis 搭最小可运行骨架表建好后Java 侧我一般用 Spring Boot 3 MyBatis不引 JPA。原因很直接中医药查询大量是「多条件动态组合 关联多表」MyBatis 的 XML 里写动态 SQL 比 JPA 的 Criteria 直观得多也方便 DBA 直接看执行计划。先看依赖和配置pom.xml关键部分dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId /dependencyapplication.yml里把数据源和 MyBatis 的 mapper 路径配好spring: datasource: url: jdbc:mysql://localhost:3306/tcm_kb?useUnicodetruecharacterEncodingutf8mb4 username: root password: your_password mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case: true这行别漏它让数据库的herb_id自动映射到 Java 的herbId省掉一堆resultMap。连接串里的characterEncodingutf8mb4也要写对否则生僻字入库变问号这个坑我见过不止一次。实体类用普通 POJO 即可字段名和表字段驼峰对应public class Herb { private Long id; private String name; private String pinyin; private Integer nature; private String flavor; // getter/setter 省略 }Mapper 接口和 XML 是重点。下面这个查询支持「按药材名模糊 按性味筛选 分页」是知识库最常用的入口public interface HerbMapper { ListHerb search(Param(keyword) String keyword, Param(nature) Integer nature, Param(offset) int offset, Param(size) int size); long countSearch(Param(keyword) String keyword, Param(nature) Integer nature); }select idsearch resultTypecom.tcm.entity.Herb SELECT id, name, pinyin, nature, flavor FROM herb where if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR pinyin LIKE CONCAT(#{keyword}, %)) /if if testnature ! null AND nature #{nature} /if /where ORDER BY id LIMIT #{offset}, #{size} /selectwhere标签会自动处理第一个AND避免拼接出WHERE AND的语法错误。pinyin LIKE CONCAT(#{keyword}, %)用的是前缀匹配能走索引而name LIKE CONCAT(%, #{keyword}, %)是前后模糊走不了索引数据量上万后要留意。分页这里用LIMIT offset, size手写量小够用量大了建议换 PageHelper 或游标分页后面避坑章会讲。3. 多条件组合检索和关联查询怎么写得又快又准知识库的价值全在「查得准、查得快」。用户不会只按一个条件查真实场景是「找归肺经、性温、含甘草的方剂」这种多跳查询。这一章讲清楚关联查询怎么写、索引怎么建、以及为什么有些查询慢得离谱。3.1 方剂-药材多跳查询的 SQL 与索引「含某味药材的方剂」是最典型的查询走formula_herb中间表关联select idfindFormulasByHerb resultTypecom.tcm.entity.Formula SELECT f.id, f.name, f.source, f.effect FROM formula f JOIN formula_herb fh ON f.id fh.formula_id JOIN herb h ON h.id fh.herb_id WHERE h.name #{herbName} ORDER BY f.id /select这条 SQL 本身不复杂但性能全看索引。formula_herb上的idx_herb让h.name过滤后能快速定位到herb_id再通过idx_formula反查方剂。如果只建了主键没建这两个索引数据量到十万级就是全表扫描查询从毫秒变秒级。我一般会在建表时就带上这两个索引别等慢了再补。再复杂一点的是「多味药材同时出现」的方剂比如找同时含甘草和白术的方SELECT f.id, f.name FROM formula f JOIN formula_herb fh ON f.id fh.formula_id JOIN herb h ON h.id fh.herb_id WHERE h.name IN (甘草, 白术) GROUP BY f.id, f.name HAVING COUNT(DISTINCT h.id) 2;HAVING COUNT(DISTINCT h.id) 2是关键它保证两味药都出现而不是出现任意一味。这个写法比写两个EXISTS子查询更易读但要注意GROUP BY的字段要和SELECT一致否则在ONLY_FULL_GROUP_BY模式下会报错。3.2 用 MyBatis 动态 SQL 拼装证候-方剂-药材三级查询证候到方剂再到药材是三级关联用户在前端勾选「证候风寒感冒」后端要返回相关方剂及其组成药材。这种查询我一般分两步先查方剂列表再批量查药材避免一次 JOIN 出笛卡尔积导致结果集爆炸。public interface FormulaMapper { ListFormula findBySyndrome(Param(syndromeId) Long syndromeId); ListFormulaHerbVO findHerbsByFormulaIds(Param(ids) ListLong ids); }select idfindHerbsByFormulaIds resultTypecom.tcm.vo.FormulaHerbVO SELECT fh.formula_id AS formulaId, h.name AS herbName, fh.dosage, fh.role FROM formula_herb fh JOIN herb h ON h.id fh.herb_id WHERE fh.formula_id IN foreach collectionids itemid open( separator, close) #{id} /foreach /selectforeach是 MyBatis 处理IN查询的标准写法collection对应接口里的Param(ids)。这里有个边界如果ids为空列表IN ()会直接报 SQL 语法错误。稳妥做法是在 Java 层先判空为空直接返回空集合别把空列表丢给 SQL。这个坑我在生产环境踩过日志里只看到语法错误排查半天才发现是上游传了空集合。三级查询拆成两步后第二步用IN批量查一次网络往返搞定比循环单查快一个数量级。返回的 VO 里带上formulaId前端按 id 分组渲染即可。4. 数据入库、校验和一致性怎么保证不翻车中医药数据来源杂有古籍录入、有 Excel 导入、有第三方接口格式不统一是常态。入库环节做不好后面查询全是脏数据。这一章讲批量导入、字段校验和事务处理。4.1 用批量插入把上万条药材导进去逐条INSERT导一万条药材光网络往返就够呛。MyBatis 的批量插入配合rewriteBatchedStatementstrue能把性能拉起来。连接串加参数url: jdbc:mysql://localhost:3306/tcm_kb?rewriteBatchedStatementstruecharacterEncodingutf8mb4Mapper 方法int batchInsert(Param(list) ListHerb list);insert idbatchInsert INSERT INTO herb (name, pinyin, nature, flavor) VALUES foreach collectionlist itemh separator, (#{h.name}, #{h.pinyin}, #{h.nature}, #{h.flavor}) /foreach /insertrewriteBatchedStatementstrue让 JDBC 把多条INSERT重写成一条多值INSERT配合foreach拼接一万条数据通常几秒内完成。但要注意单条 SQL 不能太长MySQL 默认max_allowed_packet是 4MB数据量大时按每批 500 到 1000 条切分别一次全塞进去。4.2 字段校验和唯一约束的双保险药材正名重复是导入时最常见的问题。光靠 Java 层查重不够并发导入时两个线程可能同时查到「不存在」然后都插入。正确做法是数据库层加唯一约束Java 层捕获异常做友好提示try { herbMapper.batchInsert(list); } catch (DuplicateKeyException e) { // 定位重复项返回给前端让用户修正 throw new BizException(存在重复药材名请检查后重试); }herb表的UNIQUE KEY uk_name (name)就是那道兜底防线。Java 层可以先做一次批量查重把已存在的名字挑出来提示用户但最终一致性还得靠数据库约束。性味归经这类枚举字段在入库前用校验注解或手动校验非法值直接拒绝别让它进库——脏数据一旦进去后面清洗的成本远高于入库时拦一下。5. 中医药知识库开发中容易踩的坑这一章全是血泪经验每条都按「现象 → 原因 → 解决」写能帮你省下不少排查时间。现象生僻字入库变成问号或乱码。原因数据库、表、连接串三处字符集不一致只要有一处是utf8而非utf8mb4四字节字符就存不下。解决建库建表统一utf8mb4连接串加characterEncodingutf8mb4MyBatis 无需额外配置三处对齐即可。现象拼音检索查不到输入gc搜不出甘草。原因pinyin字段存的是全拼gancao而用户输入的是首字母缩写。解决入库时同时生成全拼和首字母两个字段或者存全拼后在应用层做首字母匹配。我一般加一个pinyin_abbr字段专门存首字母检索时两个字段都匹配。现象多条件查询时结果忽多忽少。原因动态 SQL 里AND和OR混用没加括号比如name LIKE ... OR pinyin LIKE ... AND nature ...运算优先级导致条件失效。解决用where标签包裹涉及OR的条件组手动加括号写成AND (name LIKE ... OR pinyin LIKE ...)。现象批量导入到一半报PacketTooBigException。原因单条批量 SQL 超过max_allowed_packet限制。解决按每批 500 条切分或者调大 MySQL 的max_allowed_packet参数。切分更稳妥不依赖数据库配置。现象并发导入时出现重复药材。原因Java 层查重和插入之间有竞态窗口。解决数据库唯一约束兜底捕获DuplicateKeyException后返回明确提示别指望应用层查重能百分百拦住。6. 让检索更聪明的两个进阶技巧基础功能跑通后真正拉开体验差距的是检索的「聪明程度」。这里分享两个我实际用过的技巧。第一个是拼音首字母检索的完整实现。前面提到加pinyin_abbr字段入库时用工具类生成public static String toAbbr(String pinyin) { StringBuilder sb new StringBuilder(); for (String s : pinyin.split(\\s)) { if (!s.isEmpty()) sb.append(s.charAt(0)); } return sb.toString(); }调用时toAbbr(gan cao)返回gc。检索 SQL 里同时匹配pinyin和pinyin_abbr用户打全拼或首字母都能命中。注意多音字问题比如「白术」的「术」读zhu不读shu拼音数据源要选带多音字标注的否则首字母会错。这个没有银弹只能靠数据源质量加人工校对。第二个是查询结果的缓存策略。药材和证候这类基础数据变动少、查询频繁适合加缓存。我一般用 Spring Cache Redis在 Mapper 上层加注解Cacheable(value herb, key #keyword : #nature) public ListHerb search(String keyword, Integer nature) { // 走数据库查询 }key用参数组合保证不同查询条件命中不同缓存。但要注意药材数据更新后必须清缓存否则用户查到的是旧数据。我一般把CacheEvict加在新增、修改、删除方法上allEntries true清空整个herb缓存区。缓存这玩意儿用好了是后悔药用不好就是黑匣子——数据不一致时你根本不知道是库错了还是缓存错了所以更新逻辑一定要和缓存清理绑死。最后说个验证方法写完检索接口后别只用正常数据测。拿几条边界数据跑一遍——空关键字、超长关键字、含特殊字符的关键字、拼音首字母、生僻字看返回是否符合预期。我吃过亏上线后用户输入一个带单引号的药材名SQL 直接报错就是因为没做参数化测试。现在我的习惯是每个查询接口至少跑一遍边界用例参数化查询是底线但边界场景还得靠人想全。希望帮到你。本文还有配套的精品资源点击获取