简介:这份资源是一套基于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_name |
| formula | 方剂主表 | 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;dosage用VARCHAR而不是数值类型,是因为古籍里剂量单位五花八门,「三钱」「一两」「等分」都有,强行转数值会丢信息。真要统计时再在应用层做单位换算。pinyin字段单独建索引,是为了支持拼音首字母检索,用户输入gc能查到甘草,这个体验比只能打汉字好太多。
2.2 用 Spring Boot + MyBatis 搭最小可运行骨架
表建好后,Java 侧我一般用 Spring Boot 3 + MyBatis,不引 JPA。原因很直接:中医药查询大量是「多条件动态组合 + 关联多表」,MyBatis 的 XML 里写动态 SQL 比 JPA 的 Criteria 直观得多,也方便 DBA 直接看执行计划。
先看依赖和配置,pom.xml关键部分:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency>application.yml里把数据源和 MyBatis 的 mapper 路径配好:
spring: datasource: url: jdbc:mysql://localhost:3306/tcm_kb?useUnicode=true&characterEncoding=utf8mb4 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。连接串里的characterEncoding=utf8mb4也要写对,否则生僻字入库变问号,这个坑我见过不止一次。
实体类用普通 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 { List<Herb> 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 id="search" resultType="com.tcm.entity.Herb"> SELECT id, name, pinyin, nature, flavor FROM herb <where> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR pinyin LIKE CONCAT(#{keyword}, '%')) </if> <if test="nature != null"> AND nature = #{nature} </if> </where> ORDER BY id LIMIT #{offset}, #{size} </select><where>标签会自动处理第一个AND,避免拼接出WHERE AND的语法错误。pinyin LIKE CONCAT(#{keyword}, '%')用的是前缀匹配,能走索引;而name LIKE CONCAT('%', #{keyword}, '%')是前后模糊,走不了索引,数据量上万后要留意。分页这里用LIMIT offset, size手写,量小够用;量大了建议换 PageHelper 或游标分页,后面避坑章会讲。
3. 多条件组合检索和关联查询怎么写得又快又准
知识库的价值全在「查得准、查得快」。用户不会只按一个条件查,真实场景是「找归肺经、性温、含甘草的方剂」这种多跳查询。这一章讲清楚关联查询怎么写、索引怎么建、以及为什么有些查询慢得离谱。
3.1 方剂-药材多跳查询的 SQL 与索引
「含某味药材的方剂」是最典型的查询,走formula_herb中间表关联:
<select id="findFormulasByHerb" resultType="com.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 { List<Formula> findBySyndrome(@Param("syndromeId") Long syndromeId); List<FormulaHerbVO> findHerbsByFormulaIds(@Param("ids") List<Long> ids); }<select id="findHerbsByFormulaIds" resultType="com.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 collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </select><foreach>是 MyBatis 处理IN查询的标准写法,collection对应接口里的@Param("ids")。这里有个边界:如果ids为空列表,IN ()会直接报 SQL 语法错误。稳妥做法是在 Java 层先判空,为空直接返回空集合,别把空列表丢给 SQL。这个坑我在生产环境踩过,日志里只看到语法错误,排查半天才发现是上游传了空集合。
三级查询拆成两步后,第二步用IN批量查,一次网络往返搞定,比循环单查快一个数量级。返回的 VO 里带上formulaId,前端按 id 分组渲染即可。
4. 数据入库、校验和一致性怎么保证不翻车
中医药数据来源杂,有古籍录入、有 Excel 导入、有第三方接口,格式不统一是常态。入库环节做不好,后面查询全是脏数据。这一章讲批量导入、字段校验和事务处理。
4.1 用批量插入把上万条药材导进去
逐条INSERT导一万条药材,光网络往返就够呛。MyBatis 的批量插入配合rewriteBatchedStatements=true能把性能拉起来。连接串加参数:
url: jdbc:mysql://localhost:3306/tcm_kb?rewriteBatchedStatements=true&characterEncoding=utf8mb4Mapper 方法:
int batchInsert(@Param("list") List<Herb> list);<insert id="batchInsert"> INSERT INTO herb (name, pinyin, nature, flavor) VALUES <foreach collection="list" item="h" separator=","> (#{h.name}, #{h.pinyin}, #{h.nature}, #{h.flavor}) </foreach> </insert>rewriteBatchedStatements=true让 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,连接串加characterEncoding=utf8mb4,MyBatis 无需额外配置,三处对齐即可。
现象:拼音检索查不到,输入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 List<Herb> search(String keyword, Integer nature) { // 走数据库查询 }key用参数组合,保证不同查询条件命中不同缓存。但要注意,药材数据更新后必须清缓存,否则用户查到的是旧数据。我一般把@CacheEvict加在新增、修改、删除方法上,allEntries = true清空整个herb缓存区。缓存这玩意儿用好了是后悔药,用不好就是黑匣子——数据不一致时你根本不知道是库错了还是缓存错了,所以更新逻辑一定要和缓存清理绑死。
最后说个验证方法:写完检索接口后,别只用正常数据测。拿几条边界数据跑一遍——空关键字、超长关键字、含特殊字符的关键字、拼音首字母、生僻字,看返回是否符合预期。我吃过亏,上线后用户输入一个带单引号的药材名,SQL 直接报错,就是因为没做参数化测试。现在我的习惯是每个查询接口至少跑一遍边界用例,参数化查询是底线,但边界场景还得靠人想全。希望帮到你。
本文还有配套的精品资源,点击获取