1. 项目背景与核心价值
这个经方药食两用服务平台系统采用了当前企业级开发中最主流的全栈技术组合:SpringBoot+Vue3+MyBatis。作为一名长期从事医疗健康类系统开发的工程师,我发现这种技术栈特别适合需要快速迭代的中小型项目。SpringBoot的后端高效稳定,Vue3的前端灵活轻量,再加上MyBatis对复杂SQL的优秀控制能力,三者结合能很好地满足医药健康类系统常见的业务需求变化。
这个平台最核心的价值在于将传统经方与现代药食同源理念数字化。在实际开发中,我们遇到过很多医疗机构和养生机构的需求——他们需要一个能管理经方配伍、药食搭配,同时又能支持用户自主查询和搭配的系统。这个项目正好填补了这个市场空白,而且采用了前后端分离架构,使得后期维护和功能扩展变得非常方便。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构解析
2.1 后端技术选型
SpringBoot 2.7.x版本是这个项目后端的基石。选择这个版本而非最新的3.x系列,主要是考虑到国内医疗行业系统对JDK版本的兼容性要求。在实际部署中,我们发现很多医院的服务器环境还停留在JDK8,而SpringBoot 2.7.x对JDK8的支持最为完善。
MyBatis-Plus 3.5.x作为ORM框架,极大地简化了数据库操作。在经方数据管理这个核心功能上,我们设计了多张关联表:
- 药材基础表(herb_base)
- 经方配方表(prescription)
- 药食搭配表(food_combo)
- 用户收藏表(user_favorite)
MyBatis的动态SQL特性在这里发挥了巨大作用。比如在复杂查询场景下,我们使用<if>标签实现了条件组合查询:
xml复制<select id="searchHerbs" resultType="HerbVO">
SELECT * FROM herb_base
<where>
<if test="name != null and name != ''">
AND herb_name LIKE CONCAT('%',#{name},'%')
</if>
<if test="category != null">
AND category_id = #{category}
</if>
<if test="isFood != null">
AND is_food = #{isFood}
</if>
</where>
ORDER BY use_count DESC
</select>
2.2 前端技术方案
Vue3的组合式API是这个项目前端层的亮点。相比Options API,组合式API在复杂业务逻辑的组织上更具优势。比如在经方详情页面,我们需要同时处理:
- 基础信息展示
- 配伍禁忌检查
- 相似推荐计算
- 用户收藏状态
使用setup语法糖可以让相关代码保持内聚:
javascript复制// 经方详情组件
<script setup>
const { prescriptionId } = useRoute().params
const { data: detail } = useFetchPrescriptionDetail(prescriptionId)
const { checkConflict } = useHerbConflict()
const { similarPrescriptions } = useSimilarRecommend()
const conflictWarning = computed(() => {
return checkConflict(detail.value.herbs)
})
</script>
Element Plus作为UI组件库,其Table组件经过二次封装后,很好地满足了医药数据表格的特殊需求——支持药材图片预览、功效标签渲染等定制化功能。
3. 数据库设计与优化
3.1 核心表结构
MySQL 8.0是这个项目的数据库选择,主要利用了它的JSON字段功能和窗口函数。数据库设计中最大的挑战是处理经方配伍关系的多对多关联。我们最终采用的方案是:
sql复制CREATE TABLE `prescription` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`source` VARCHAR(50) COMMENT '典籍来源',
`indications` TEXT COMMENT '适应症',
`contraindications` JSON COMMENT '禁忌人群(JSON数组)',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FULLTEXT INDEX `idx_ft_indications` (`indications`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `prescription_herb` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`prescription_id` BIGINT NOT NULL,
`herb_id` BIGINT NOT NULL,
`dosage` VARCHAR(20) COMMENT '用量',
`processing` VARCHAR(50) COMMENT '炮制方法',
`role` ENUM('chief','deputy','assistant','envoy') COMMENT '君臣佐使',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_prescription_herb` (`prescription_id`,`herb_id`)
);
3.2 性能优化实践
在用户量测试中,我们发现经方搜索接口的响应时间随着数据量增加而明显变长。通过EXPLAIN分析发现,模糊查询是全表扫描的瓶颈。最终采用了三重优化方案:
- 对高频搜索字段建立全文索引:
sql复制ALTER TABLE herb_base ADD FULLTEXT INDEX `idx_ft_name_property` (name, property);
-
引入Elasticsearch作为辅助搜索引擎,定时从MySQL同步数据
-
对复杂查询结果实现Redis缓存,设置合理的过期策略:
java复制@Cacheable(value = "prescription", key = "#id", unless = "#result == null")
public PrescriptionDetailVO getPrescriptionDetail(Long id) {
// 数据库查询逻辑
}
4. 关键业务实现
4.1 药食配伍算法
平台的核心功能是药食搭配推荐,其算法实现分为几个步骤:
- 基于药材性味归经计算相似度:
java复制public double calculateHerbSimilarity(Herb h1, Herb h2) {
// 性味相似度(0-1)
double propertyScore = calculatePropertyScore(h1.getProperty(), h2.getProperty());
// 归经相似度(0-1)
double meridianScore = calculateMeridianScore(h1.getMeridians(), h2.getMeridians());
// 综合加权得分
return 0.6 * propertyScore + 0.4 * meridianScore;
}
- 配伍禁忌检查:
java复制public List<ConflictResult> checkConflicts(List<Herb> herbs) {
List<ConflictResult> results = new ArrayList<>();
for (int i = 0; i < herbs.size(); i++) {
for (int j = i + 1; j < herbs.size(); j++) {
ConflictRule rule = conflictRuleRepository
.findByHerbPair(herbs.get(i).getId(), herbs.get(j).getId());
if (rule != null) {
results.add(new ConflictResult(
herbs.get(i),
herbs.get(j),
rule.getConflictLevel(),
rule.getDescription()
));
}
}
}
return results;
}
4.2 前后端交互设计
采用RESTful风格API设计,但针对复杂业务场景做了适度调整。例如经方保存接口:
java复制@PostMapping("/prescriptions")
public Result<Long> savePrescription(
@RequestBody @Valid PrescriptionSaveDTO dto,
@RequestHeader("X-User-Id") Long userId
) {
// 1. 基础校验
if (dto.getHerbs().size() > 20) {
throw new BusinessException("单方药材数量不能超过20味");
}
// 2. 配伍禁忌检查
List<ConflictResult> conflicts = herbService.checkConflicts(
dto.getHerbs().stream().map(h -> h.getHerbId()).collect(Collectors.toList())
);
if (!conflicts.isEmpty()) {
return Result.successWithWarning(
"检测到配伍禁忌",
conflicts.stream().map(ConflictResult::getMessage).collect(Collectors.toList())
);
}
// 3. 保存逻辑
Long id = prescriptionService.save(dto, userId);
return Result.success(id);
}
前端采用axios拦截器统一处理响应,特别是对带有warning的成功响应做了特殊处理:
javascript复制// 响应拦截器
instance.interceptors.response.use(response => {
if (response.data.code === 200 && response.data.warnings) {
// 成功但有警告信息
response.data.warnings.forEach(msg => {
ElMessage.warning(msg)
})
}
return response.data
}, error => {
// 错误处理
})
5. 部署与运维实践
5.1 多环境配置
SpringBoot的profile机制很好地支持了多环境配置。我们在项目中设计了四套环境:
- dev - 开发环境,连接本地数据库
- test - 测试环境,使用内网测试数据库
- pre - 预发布环境,配置与生产一致
- prod - 生产环境,使用阿里云RDS
通过application-{profile}.yml文件管理不同环境的配置,关键配置项包括:
- 数据库连接池参数
- Redis地址
- 文件上传路径
- 短信/邮件服务商配置
5.2 健康检查与监控
系统集成了SpringBoot Actuator,但出于安全考虑,我们做了以下定制:
- 自定义健康检查指标:
java复制@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(1)) {
return Health.up().build();
}
} catch (Exception e) {
return Health.down(e).build();
}
return Health.unknown().build();
}
}
- 敏感端点保护:
java复制@Configuration
@ConditionalOnProperty(name = "management.endpoint.health.enabled", havingValue = "true")
public class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher(EndpointRequest.toAnyEndpoint())
.authorizeRequests()
.requestMatchers(EndpointRequest.to("health")).permitAll()
.anyRequest().hasRole("ADMIN")
.and().httpBasic();
}
}
6. 典型问题排查实录
6.1 MyBatis缓存引发的问题
在用户反馈系统中,出现过经方详情显示不一致的问题。经过排查发现是MyBatis的一级缓存导致的。当同一个会话中多次查询同一经方,且中间有更新操作时,缓存未及时失效。
解决方案:
- 在更新操作后手动清除缓存:
java复制@Transactional
public void updatePrescription(PrescriptionUpdateDTO dto) {
prescriptionMapper.updateById(dto);
// 清除关联缓存
sqlSession.clearCache();
herbConflictCache.evict(dto.getId());
}
- 对关键查询添加flushCache配置:
xml复制<select id="selectById" resultMap="prescriptionMap" flushCache="true">
SELECT * FROM prescription WHERE id = #{id}
</select>
6.2 Vue3响应式数据陷阱
在开发药材多选组件时,遇到过选中状态不同步的问题。原因是直接对响应式数组进行了非响应式操作:
错误写法:
javascript复制const selectedHerbs = ref([])
function addHerb(herb) {
// 直接push不会触发更新
selectedHerbs.value.push(herb)
}
正确写法:
javascript复制function addHerb(herb) {
selectedHerbs.value = [...selectedHerbs.value, herb]
}
或者使用Vue3提供的响应式API:
javascript复制import { reactive } from 'vue'
const state = reactive({
selectedHerbs: []
})
function addHerb(herb) {
state.selectedHerbs.push(herb) // 这样写是响应式的
}
7. 项目扩展方向
基于现有架构,平台还可以向以下几个方向扩展:
- 智能推荐增强
- 接入用户健康档案数据
- 实现个性化推荐算法
- 增加季节时令推荐维度
- 移动端适配
- 基于现有API开发微信小程序
- 使用Uniapp打包多端应用
- 增加扫码识药功能
- 运营功能扩展
- 经方知识付费模块
- 医师在线咨询系统
- 用户成长体系设计
在技术架构层面,可以考虑引入:
- SpringCloud实现微服务化
- RabbitMQ处理异步任务
- 分布式事务保证数据一致性
这个项目最让我满意的部分是前后端分离带来的开发效率提升。后端团队可以专注于业务逻辑和API设计,前端团队可以自由尝试各种交互方案,双方通过Swagger文档和Mock数据并行开发,大大缩短了迭代周期。
