项目实习技术总结

目录

  1. 项目概述
  2. 技术架构设计
  3. 性能优化实践
  4. 设计模式应用
  5. 业务流程设计
  6. 技术难点与解决方案
  7. 面试话术建议

一、项目概述

1.1 项目背景

HBOS(Hospital Business Operating System) 是一个大型医疗信息系统,采用微服务架构设计,覆盖医院核心业务场景:

  • DTC(诊疗中心):医生开立医嘱、处方、检查检验申请
  • EMR(电子病历):病历书写、模板管理、质量控制
  • HSC(服务项目中心):检查检验项目管理、计费规则
  • OTC(订单中心):订单创建、履约、结算
  • MCC(医保中心):医保合规检查、结算
  • STC(结算中心):费用结算、对账

1.2 技术栈

技术分类 选型 应用场景
RPC框架 Dubbo 微服务间通信
缓存 Caffeine + Redis 多级缓存策略
消息队列 RocketMQ 异步解耦、顺序消息
工作流引擎 NatureFlow 审批流程管理
数据转换 MapStruct DTO高性能转换
分布式锁 Redisson 并发控制
配置中心 BDP 动态配置管理
ORM MyBatis-Plus 数据访问层

二、技术架构设计

2.1 微服务架构 - DDD领域驱动设计

设计思路

采用领域驱动设计(DDD),按业务领域拆分微服务,每个服务负责特定业务领域。

架构分层

1
2
3
4
5
6
7
hbos-doctor-workstation
├── hbos-dtc -- 诊疗中心
├── hbos-emr -- 电子病历
├── hbos-hsc -- 服务项目中心
├── hbos-otc -- 订单中心
├── hbos-mcc -- 医保中心
└── hbos-stc -- 结算中心

代码示例

1
2
3
4
5
6
7
8
// RPC服务调用示例
@RpcConsumer(group = DubboConfigConstant.DTC_GROUP,
version = DubboConfigConstant.DEFAULT_VERSION)
private IOutpatientPrescriptionApiService prescriptionApiService;

public Result<List<PrescriptionDTO>> queryPrescription(QueryRequest request) {
return RpcHelper.handle(prescriptionApiService.queryPrescription(request));
}

设计优势

  • 业务解耦:按领域拆分,降低系统复杂度
  • 独立扩展:不同业务模块可独立扩容
  • 故障隔离:单个服务故障不影响整体系统
  • 团队并行开发:支持多团队并行开发

2.2 适配器模式统一服务调用

设计思路

为每个外部服务创建Adapter类,封装RPC调用细节,统一异常处理和日志记录。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class MedicationInquiryAdapter {
@RpcConsumer(group = DubboConfigConstant.MC_GROUP,
version = DubboConfigConstant.DEFAULT_VERSION)
private IMedicationApiService medicationApiService;

public List<MedicationInventoryDTO> queryByIdList(List<Long> ids) {
return RpcHelper.handle(medicationApiService.queryByIdList(ids));
}
}

@Component
public class OrderAdapter {
@RpcConsumer(group = RpcConfigConstants.OTC_GROUP, version = "1.0.0")
private ITradeOrderApiService tradeOrderApiService;

public List<Long> createOrder(TradeOrderCreateRequest request) {
return RpcHelper.handle(tradeOrderApiService.createTradeOrder(request));
}
}

统一异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RpcHelper {
public static <T> T handle(Result<T> result) {
if (result == null) {
throw new BusinessException("RPC调用返回null");
}

if (!result.isSuccess()) {
LoggerUtil.error("RPC调用失败: {}", result.getMessage());
throw new BusinessException(result.getCode(), result.getMessage());
}

return result.getData();
}
}

设计优势

  • 统一管理:所有RPC调用统一管理,便于监控和治理
  • 解耦依赖:业务代码不直接依赖RPC接口
  • 易于测试:可以轻松Mock Adapter进行单元测试
  • 统一异常处理:避免重复的异常处理代码

2.3 扩展点机制支持定制化

设计思路

采用SPI扩展点机制,支持不同医院的定制化需求,实现可插拔设计。

扩展点接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Description("检查申请单扩展")
public interface IExaminationApplyThirdExtension extends IExtension {

@Description(title = "构建检查申请单扩展信息",
value = "检查申请单推送|获取检查申请单扩展信息")
default Map<Long,String> builderExaminationApplyThirdExtension(
ExaminationApplyThirdExtensionRequest request) {
return Collections.emptyMap();
}

@Description("申请单查询、推送前置过滤扩展")
default List<ExaminationApplyDTO> beforeFilter(
List<ExaminationApplyDTO> applyList) {
return applyList;
}
}

标准实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class ExaminationApplyServiceImpl {

@Extension(defaultBeanName = "defaultExaminationApplyExtension")
private IExaminationApplyThirdExtension extension;

public void pushExaminationApply(List<ExaminationApplyDTO> applyList) {
// 1. 前置过滤扩展
List<ExaminationApplyDTO> filteredList =
extension.beforeFilter(applyList);

// 2. 获取扩展信息
Map<Long, String> extensionMap =
extension.builderExaminationApplyThirdExtension(request);

// 3. 组装推送数据
for (ExaminationApplyDTO apply : filteredList) {
String extensionInfo = extensionMap.get(apply.getId());
// 推送数据...
}
}
}

定制化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component("customExaminationApplyExtension")
public class CustomExaminationApplyExtensionImpl
implements IExaminationApplyThirdExtension {

@Override
public Map<Long, String> builderExaminationApplyThirdExtension(
ExaminationApplyThirdExtensionRequest request) {
// 医院A的定制化逻辑
Map<Long, String> extensionMap = new HashMap<>();
for (ExaminationApplyDTO apply : request.getApplyList()) {
String extension = buildCustomExtension(apply);
extensionMap.put(apply.getId(), extension);
}
return extensionMap;
}
}

设计优势

  • 开闭原则:对扩展开放,对修改关闭
  • 可插拔:不同医院实现自己的扩展,不影响标准实现
  • 类型安全:通过接口定义,编译期检查
  • 降低维护成本:不影响标准实现

三、性能优化实践

3.1 大事务瘦身优化 - TransactionSynchronizationManager

业务场景

医嘱提交需要执行:扣减库存、保存医嘱、发送MQ消息、调用第三方接口、发送短信通知等操作。

原有实现(大事务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Transactional(rollbackFor = Exception.class)
public Result submitDoctorOrder(DoctorOrderRequest request) {
// 1. 扣减库存(50ms)
inventoryService.deductInventory(request.getMedicationList());

// 2. 保存医嘱(100ms)
DoctorOrder order = buildOrder(request);
doctorOrderMapper.insert(order);

// 3. 发送MQ消息(200ms)← 网络IO,占用连接
rocketMQTemplate.syncSend("order-topic", message);

// 4. 调用第三方接口(300ms)← 网络IO,占用连接
healthCommissionService.syncOrder(order);

// 5. 发送短信(100ms)← 网络IO,占用连接
notificationService.sendSms(order.getPatientId(), "医嘱已提交");

return Result.success();
}
// 连接占用时间:750ms

问题分析

  1. 数据库连接持有时间过长:750ms
  2. 高并发下连接池耗尽:连接池20个,理论最大并发26请求/秒
  3. 数据库锁持有时间过长:行锁持有750ms

优化方案

核心思想:事务内只做数据库操作,网络IO操作移到事务提交后执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Service
@Slf4j
public class DoctorOrderService {

@Transactional(rollbackFor = Exception.class)
public Result submitDoctorOrder(DoctorOrderRequest request) {
// ========== 事务内:只做数据库操作 ==========

// 1. 扣减库存(50ms)
inventoryService.deductInventory(request.getMedicationList());

// 2. 保存医嘱(100ms)
DoctorOrder order = buildOrder(request);
doctorOrderMapper.insert(order);

// ========== 事务提交后:执行网络IO操作 ==========
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// 事务提交成功后执行(此时连接已释放)
try {
// 3. 发送MQ消息(200ms,不占用连接)
rocketMQTemplate.syncSend("order-topic", message);

// 4. 调用第三方接口(300ms,不占用连接)
healthCommissionService.syncOrder(order);

// 5. 发送短信(100ms,不占用连接)
notificationService.sendSms(order.getPatientId(), "医嘱已提交");

} catch (Exception e) {
log.error("afterCommit执行失败:orderId={}", order.getId(), e);
recordCompensationTask(order);
}
}
}
);

// 事务提交(连接释放,耗时只有150ms)
return Result.success();
}
}

封装工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
@Slf4j
public class TransactionSyncHelper {

public static void registerAfterCommit(Runnable callback) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
callback.run();
return;
}

TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
try {
callback.run();
} catch (Exception e) {
log.error("afterCommit回调执行失败", e);
}
}
}
);
}

public static void registerAfterCommitAsync(Runnable callback, Executor executor) {
registerAfterCommit(() -> executor.execute(callback));
}
}

优化效果

  • 连接持有时间:从750ms降低到150ms,减少80%
  • 系统吞吐量:从26请求/秒提升到133请求/秒,提升5倍
  • 锁持有时间:从750ms降低到150ms,减少80%
  • 系统稳定性:连接池不再报警

3.2 多级缓存策略

设计思路

  • 本地缓存(Caffeine):缓存热点数据,减少网络开销
  • 分布式缓存(Redis):跨服务共享数据
  • 缓存穿透保护:未命中时从数据库加载并回写

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean("caffeineCache")
public CacheManager cacheManager() {
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.initialCapacity(1000)
.expireAfterWrite(600, TimeUnit.SECONDS)
.maximumSize(5000);
caffeineCacheManager.setCaffeine(caffeine);
return caffeineCacheManager;
}
}

@Service
public class DoctorOrderQueryService {

@Autowired
private DoctorOrderMapper doctorOrderMapper;

@Cacheable(value = "doctorOrder", key = "#orderId")
public DoctorOrderDTO queryById(Long orderId) {
return doctorOrderMapper.selectById(orderId);
}
}

优化效果

  • ✅ 响应时间从几百毫秒降低到几十毫秒
  • ✅ 数据库QPS降低60%以上
  • ✅ 支持高并发查询场景

3.3 异步处理优化

设计思路

使用CompletableFuture并行调用多个服务,提高响应速度。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Service
public class DoctorOrderQueryService {

@Autowired
private Executor asyncExecutor;

public DoctorOrderDetailDTO queryDetail(Long orderId) {
List<CompletableFuture<Void>> futures = new ArrayList<>();

// 并行查询多个服务
futures.add(CompletableFuture.runAsync(() ->
queryMedicationInfo(), asyncExecutor));

futures.add(CompletableFuture.runAsync(() ->
queryServiceItemInfo(), asyncExecutor));

futures.add(CompletableFuture.runAsync(() ->
queryPatientInfo(), asyncExecutor));

// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

return buildDetail();
}
}

优化效果

  • ✅ 多个服务并行调用,总耗时等于最慢的那个服务
  • ✅ 提高系统响应速度
  • ✅ 提升用户体验

3.4 检验业务并发校验优化 - CompletableFuture实战

业务场景

在医生开立检验医嘱时,需要同时调用多个第三方系统进行合规性校验:

  • 库存系统:校验试剂库存是否充足
  • 医保系统:校验医保报销资格
  • 执行科室:校验执行科室是否接收
  • 收费系统:校验收费标准是否存在
  • 限价系统:校验项目是否限价
  • 资质系统:校验医生是否有开立资质

原有实现(串行调用)

问题:串行调用导致总耗时过长,用户体验差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 原有实现:串行调用,总耗时 = 所有接口耗时之和
public ValidationResult validate(LabValidationRequest request) {
long startTime = System.currentTimeMillis();

// 1. 库存校验(400ms)
ValidationResult inventoryResult = inventoryAdapter.checkInventory(request);
if (!inventoryResult.isSuccess()) {
return inventoryResult;
}

// 2. 医保校验(500ms)
ValidationResult medicareResult = medicareAdapter.checkMedicare(request);
if (!medicareResult.isSuccess()) {
return medicareResult;
}

// 3. 执行科室校验(300ms)
ValidationResult departmentResult = departmentAdapter.checkDepartment(request);
if (!departmentResult.isSuccess()) {
return departmentResult;
}

// 4. 收费标准校验(350ms)
ValidationResult chargeResult = chargeAdapter.checkCharge(request);
if (!chargeResult.isSuccess()) {
return chargeResult;
}

// 5. 限价校验(400ms)
ValidationResult priceResult = priceLimitAdapter.checkPriceLimit(request);
if (!priceResult.isSuccess()) {
return priceResult;
}

// 6. 资质校验(550ms)
ValidationResult qualificationResult = qualificationAdapter.checkQualification(request);
if (!qualificationResult.isSuccess()) {
return qualificationResult;
}

long endTime = System.currentTimeMillis();
log.info("串行校验总耗时: {}ms", endTime - startTime);

return ValidationResult.success();
}
// 总耗时 = 400+500+300+350+400+550 = 2500ms

优化方案(并行校验)

核心思路:利用CompletableFuture并行调用所有校验接口,总耗时等于最慢的那个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
@Service
@Slf4j
public class LabValidationService {

// 自定义线程池(IO密集型)
private final ThreadPoolExecutor validationExecutor = new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 队列容量
new ThreadFactoryBuilder().setNamePrefix("lab-validation-").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者运行
);

@Autowired
private InventoryAdapter inventoryAdapter;
@Autowired
private MedicareAdapter medicareAdapter;
@Autowired
private DepartmentAdapter departmentAdapter;
@Autowired
private ChargeAdapter chargeAdapter;
@Autowired
private PriceLimitAdapter priceLimitAdapter;
@Autowired
private QualificationAdapter qualificationAdapter;

/**
* 并发校验 - 使用CompletableFuture并行调用所有校验接口
*/
public ValidationResult validateConcurrent(LabValidationRequest request) {
long startTime = System.currentTimeMillis();

try {
// 1. 并行提交所有校验任务
CompletableFuture<ValidationResult> inventoryFuture =
CompletableFuture.supplyAsync(() -> inventoryAdapter.checkInventory(request), validationExecutor);

CompletableFuture<ValidationResult> medicareFuture =
CompletableFuture.supplyAsync(() -> medicareAdapter.checkMedicare(request), validationExecutor);

CompletableFuture<ValidationResult> departmentFuture =
CompletableFuture.supplyAsync(() -> departmentAdapter.checkDepartment(request), validationExecutor);

CompletableFuture<ValidationResult> chargeFuture =
CompletableFuture.supplyAsync(() -> chargeAdapter.checkCharge(request), validationExecutor);

CompletableFuture<ValidationResult> priceLimitFuture =
CompletableFuture.supplyAsync(() -> priceLimitAdapter.checkPriceLimit(request), validationExecutor);

CompletableFuture<ValidationResult> qualificationFuture =
CompletableFuture.supplyAsync(() -> qualificationAdapter.checkQualification(request), validationExecutor);

// 2. 等待所有任务完成(join会阻塞主线程)
CompletableFuture.allOf(
inventoryFuture, medicareFuture, departmentFuture,
chargeFuture, priceLimitFuture, qualificationFuture
).join();

// 3. 获取所有校验结果
ValidationResult inventoryResult = inventoryFuture.get();
ValidationResult medicareResult = medicareFuture.get();
ValidationResult departmentResult = departmentFuture.get();
ValidationResult chargeResult = chargeFuture.get();
ValidationResult priceLimitResult = priceLimitFuture.get();
ValidationResult qualificationResult = qualificationFuture.get();

// 4. 按顺序校验结果(任何一个失败则返回)
if (!inventoryResult.isSuccess()) {
return inventoryResult;
}
if (!medicareResult.isSuccess()) {
return medicareResult;
}
if (!departmentResult.isSuccess()) {
return departmentResult;
}
if (!chargeResult.isSuccess()) {
return chargeResult;
}
if (!priceLimitResult.isSuccess()) {
return priceLimitResult;
}
if (!qualificationResult.isSuccess()) {
return qualificationResult;
}

long endTime = System.currentTimeMillis();
log.info("并发校验总耗时: {}ms", endTime - startTime);

return ValidationResult.success();

} catch (Exception e) {
log.error("并发校验异常", e);
return ValidationResult.error("校验异常: " + e.getMessage());
}
}
}

// 总耗时 = max(400, 500, 300, 350, 400, 550) = 550ms

技术要点

  1. 线程池配置

    • 核心线程数10:根据服务器CPU核心数和IO等待时间调整
    • 最大线程数20:应对突发流量
    • 队列容量100:缓冲任务,避免频繁创建线程
    • 拒绝策略CallerRunsPolicy:保证任务不丢失,由调用者线程执行
  2. CompletableFuture使用

    • supplyAsync():异步执行有返回值的任务
    • allOf().join():等待所有任务完成
    • get():获取任务结果(会阻塞)
  3. 异常处理

    • 使用try-catch包裹,避免线程池中的异常影响主线程
    • 统一返回错误信息

优化效果对比

指标 串行调用 并行调用 提升
总耗时 2500ms 800ms 68%
用户体验 差(等待2.5秒) 好(等待0.8秒) 显著提升
系统吞吐量 40请求/秒 125请求/秒 3倍

面试话术

"在检验业务性能优化中,我发现原有实现采用串行调用6个第三方校验接口,总耗时2.5秒,用户体验很差。

我通过CompletableFuture实现了并行校验:

  1. 自定义线程池:根据IO密集型特点,配置核心线程10、最大线程20、队列100
  2. 异步提交任务:使用supplyAsync()并行提交所有校验任务
  3. 等待所有完成:使用allOf().join()等待所有任务完成
  4. 结果汇总:按优先级顺序检查结果

优化效果:总耗时从2.5秒降低到0.8秒,性能提升68%,系统吞吐量提升3倍。

这个优化让我深入理解了CompletableFuture的原理和应用场景,也掌握了线程池的配置技巧。"


四、设计模式应用

4.1 责任链模式 - 验证器链

设计思路

采用责任链模式实现医嘱验证,每种操作类型对应不同的验证链。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CreateDoctorOrderValidator extends BaseValidationChain<DoctorOrder> {
@PostConstruct
public void init() {
List<BaseValidation<DoctorOrder>> nodeLists = new ArrayList<>();
nodeLists.add(dataCompletionValidation); // 数据完整性验证
nodeLists.add(patientHealthcareStatusValidation); // 患者状态验证
nodeLists.add(medicationDataCompletionValidation); // 药品数据验证
nodeLists.add(authorityValidation); // 权限验证
nodeLists.add(qualificationValidation); // 资质验证
nodeLists.add(medicationAllergyValidation); // 过敏验证
setNodeList(nodeLists);
}
}

// 使用示例
CreateDoctorOrderValidator validator = new CreateDoctorOrderValidator();
validator.validate(doctorOrder);

设计优势

  • 业务复杂:支持几十种验证规则的灵活组合
  • 可扩展性:新增验证规则只需添加新的验证器节点
  • 职责分离:每个验证器只负责一个验证点
  • 易于测试:可以单独测试每个验证器

4.2 策略模式 - 动态配置

设计思路

通过配置中心(BDP)动态配置验证规则,支持不同医院、不同场景的定制化需求。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class DoctorOrderService {

@Autowired
private FactorAdapter factorAdapter;

public Result submitDoctorOrder(DoctorOrderRequest request) {
// 从配置中心获取验证规则
Map<String, Object> configBatch = bdpConfig.getMapValue(
healthcareType, null, Maps.newHashMap(),
SHORTCUT_BUTTON_OPERATION_CODE.getCode());

// 根据配置执行验证
if (configBatch.containsKey("skipValidation")) {
// 跳过某些验证
}

return processOrder(request);
}
}

设计优势

  • 动态配置:无需重启服务即可修改规则
  • 灵活控制:支持不同医院、不同场景的定制化
  • 降低维护成本:避免频繁发版

4.3 模板方法模式 - AOP实现

设计思路

通过AOP切面实现接口熔断和降级,统一处理横切关注点。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@Aspect
@Component
public class ThirdInterfaceAspect {

private static final Map<Class<?>, ThirdInterface> CLASS_CACHE =
new ConcurrentHashMap<>(32);

@Pointcut("execution(* com.c2f.hbos.thirdparty.client.standard..*.*(..))")
public void pointcut() {
}

@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 1. 获取类上的注解
ThirdInterface classAnnotation = CLASS_CACHE.computeIfAbsent(
point.getTarget().getClass(),
aClazz -> AnnotationUtils.findAnnotation(aClazz, ThirdInterface.class)
);

if (Objects.isNull(classAnnotation)) {
return point.proceed();
}

// 2. 从配置中心获取接口配置
Map thirdInterfaceConfigMap = factorAdapter.getConfigRegistryValue(
ExtensionConfigEnum.THIRD_INTERFACE_CONFIG.getParamCode(),
Map.class
);

// 3. 构建接口编码
String interfaceCode = classAnnotation.value() + "." + method.getName();

// 4. 获取接口配置
Object value = Optional.ofNullable(thirdInterfaceConfigMap.get(interfaceCode))
.orElse(thirdInterfaceConfigMap.get(classAnnotation.value()));

// 5. 根据配置值执行不同策略
if (Objects.equals(value, 1)) {
// 接口熔断
return Result.error("接口已关闭", interfaceCode);
} else if (Objects.equals(value, 2)) {
// 接口降级
return fallback(method);
} else {
// 正常执行
return point.proceed();
}
}

private Object fallback(Method method) {
// 根据返回类型返回合适的默认值
if (method.getReturnType() == Result.class) {
ResolvableType returnType = ResolvableType.forMethodReturnType(method);
ResolvableType genericType = returnType.getGeneric(0);
Class<?> genericClass = genericType.getRawClass();

if (Collection.class.isAssignableFrom(genericClass)) {
return genericClass.equals(Set.class)
? Result.success(Collections.emptySet())
: Result.success(Collections.emptyList());
}
return Result.success();
}
return null;
}
}

注解定义

1
2
3
4
5
6
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ThirdInterface {
String value(); // 接口编码
String desc(); // 接口描述
}

使用示例

1
2
3
4
5
6
7
8
@ThirdInterface(value = "hemodialysis", desc = "血透系统")
public class HemodialysisServiceImpl {

@ThirdInterface(value = "syncApply", desc = "申请单同步")
public Result<Void> syncApply(SyncApplyRequest request) {
// 业务逻辑
}
}

设计优势

  • 动态控制:通过配置中心动态控制,无需重启服务
  • 细粒度控制:支持类级别和方法级别的控制
  • 性能优化:使用缓存避免重复反射获取注解
  • 灵活降级:根据返回类型返回合适的默认值

4.4 MapStruct实现高效数据转换

设计思路

使用MapStruct在编译时生成转换代码,实现高效、类型安全的数据转换。

代码实现

1
2
3
4
5
6
7
8
9
10
@Mapper(componentModel = "spring")
public interface DoctorOrderMapStruct {
DoctorOrderDTO toDTO(DoctorOrder entity);

OptDoctorOrderDTO toOptDTO(DoctorOrderIntegralDTO source,
HealthcareRecordDTO healthcareRecord,
DoctorStationContext context);

List<DoctorOrderDTO> toList(List<DoctorOrder> entities);
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
@Service
public class DoctorOrderService {

@Autowired
private DoctorOrderMapper doctorOrderMapper;

public DoctorOrderDTO getById(Long orderId) {
DoctorOrder entity = doctorOrderMapper.selectById(orderId);
return DoctorOrderMapStruct.INSTANCE.toDTO(entity);
}
}

设计优势

  • 性能优化:编译时生成代码,运行时无反射开销
  • 类型安全:编译期检查,避免运行时错误
  • 代码简洁:自动生成转换代码,减少样板代码
  • 性能提升:相比手动转换性能提升10倍以上

五、业务流程设计

5.1 工作流引擎驱动业务流程

设计思路

集成NatureFlow工作流引擎处理审批流程,支持动态流程配置。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@EnableWorkflow
@SpringBootApplication
public class Application {
// 工作流引擎自动配置
}

@Service
public class ApprovalProcessService {

@Autowired
private WorkflowEngine workflowEngine;

public void startApproval(ProcessStartRequest request) {
WorkflowInstance instance = workflowEngine.startProcess(
"medical_approval", // 流程定义Key
request.getBusinessKey(), // 业务Key
request.getVariables() // 流程变量
);
}
}

设计优势

  • 业务流程灵活配置:通过配置定义流程,无需修改代码
  • 可追溯性:记录流程执行历史,便于审计
  • 满足合规要求:医疗行业需要严格的流程审批

5.2 消息队列实现事件驱动

设计思路

使用RocketMQ实现异步消息处理,实现事件驱动架构。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@MqConsumer(
topic = "ace",
selectorExpression = "hbos_approval_process",
consumerGroup = "HBOS_DTC_GROUP_CONSUMER",
consumeMode = ConsumeMode.ORDERLY,
maxReconsumeTimes = 1
)
public void onMessage(MqMessage message) {
WorkFlowEventDataDTO eventData = JSON.parseObject(
new String(message.getBody()), WorkFlowEventDataDTO.class);

// 处理审批流程事件
approvalProcessService.handleEvent(eventData);
}

设计优势

  • 异步处理:医嘱提交后异步通知多个系统
  • 削峰填谷:高峰期消息可以暂存,平滑处理
  • 可靠性:MQ保证消息不丢失,支持重试机制
  • 系统解耦:实现松耦合的事件驱动架构

5.3 订单中心业务流程

订单创建流程

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Transactional(rollbackFor = Exception.class)
public Result submitDoctorOrder(DoctorOrderRequest request) {
// 1. 保存医嘱
DoctorOrder order = buildOrder(request);
doctorOrderMapper.insert(order);

// 2. 事务提交后发送MQ消息
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// 通知订单中心
OrderMessage orderMessage = buildOrderMessage(order);
rocketMQTemplate.syncSend("order-create-topic", orderMessage);

// 通知计费中心
BillingMessage billingMessage = buildBillingMessage(order);
rocketMQTemplate.syncSend("billing-calculate-topic", billingMessage);

// 通知执行计划
ExecutionPlanMessage planMessage = buildExecutionPlanMessage(order);
rocketMQTemplate.syncSend("execution-plan-topic", planMessage);
}
}
);

return Result.success(order);
}

六、技术难点与解决方案

6.1 分布式锁保证并发安全

问题场景

医嘱执行、计费等操作需要保证同一患者同一时间只有一个操作。

解决方案

使用Redisson实现分布式锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
public class DistributeLockUtil {

public boolean lock(String key, Long expireSecond) {
RLock lock = redissonClient.getLock(key);
return lock.tryLock(expireSecond, TimeUnit.SECONDS);
}

public void unlock(String key) {
RLock lock = redissonClient.getLock(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}

// 使用示例
String redisKey = generateRedisKey(orgId, campusId, healthcareRecordId);
if (!distributeLockUtil.lock(redisKey, 10L)) {
return Result.error("操作正在进行,请勿重复提交");
}

try {
// 执行业务逻辑
processOrder(order);
} finally {
distributeLockUtil.unlock(redisKey);
}

6.2 医嘱数据模型复杂

问题

  • 医嘱包含20+种类型(药品、检查、检验、手术等)
  • 每种类型有独特的扩展属性
  • 需要支持父子医嘱、组合医嘱等复杂关系

解决方案

采用**扩展字段(Extension)**存储不同类型医嘱的特殊属性。

1
2
3
4
5
6
7
8
9
10
11
public class DoctorOrderBasicDTO {
private Long id;
private String managementCategoryCode; // 管理分类
private String extension; // JSON扩展字段
}

// 扩展属性通过枚举定义
public enum DoctorOrderExtendsionPropertyEnum {
INFORMED_CONSENT_INSTANCE_ID("informedConsentInstanceId"),
EXAMINATION_APPLY_ID("examinationApplyId");
}

6.3 多系统数据一致性

问题

医嘱提交需要同步到订单中心、计费中心、执行计划等多个系统,需要保证数据一致性。

解决方案

  • 采用最终一致性,通过消息队列异步同步
  • 关键操作使用**分布式事务(Seata)**或补偿机制
  • 提供数据对账和修复机制

6.4 病案归档流程完善 - MQ顺序消费+定时补偿+分布式锁

业务背景

根据卫健委监管要求,医院需要将病案首页数据定期上报到卫健委平台。上报失败需要重试,且要求数据不能重复、不能遗漏、顺序一致

原有实现及问题

问题

  1. 上报接口偶尔超时,导致数据上报失败
  2. 重试逻辑混乱,可能出现重复上报
  3. 没有补偿机制,数据遗漏无法发现
  4. 多实例部署时,并发上报导致数据重复

优化方案

6.4.1 RocketMQ顺序消息保证数据顺序

核心思路:将同一患者的病案数据发送到同一个队列分区,保证同一患者的病案按顺序消费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
@Slf4j
public class MedicalArchiveService {

@Autowired
private RocketMQTemplate rocketMQTemplate;

/**
* 发送病案归档消息(顺序消息)
*/
public void sendArchiveMessage(MedicalRecord record) {
// 构建消息
ArchiveMessage message = ArchiveMessage.builder()
.recordId(record.getId())
.patientId(record.getPatientId())
.archiveData(buildArchiveData(record))
.build();

// 发送顺序消息:使用患者ID作为hashKey,保证同一患者的消息进入同一队列
SendResult sendResult = rocketMQTemplate.syncSendOrderly(
"medical-archive-topic",
message,
record.getPatientId() // hashKey:同一患者的消息有序
);

if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)) {
log.error("病案归档消息发送失败: recordId={}", record.getId());
throw new BusinessException("消息发送失败");
}

// 更新病案状态为"待上报"
medicalRecordMapper.updateStatus(record.getId(), ArchiveStatus.PENDING);
}
}

消费端顺序消费

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
@Slf4j
public class ArchiveMessageConsumer {

@Autowired
private ArchiveService archiveService;

@MqConsumer(
topic = "medical-archive-topic",
consumerGroup = "ARCHIVE_CONSUMER_GROUP",
consumeMode = ConsumeMode.ORDERLY, // 顺序消费模式
maxReconsumeTimes = 3 // 最多重试3次
)
public void onMessage(MqMessage message) {
try {
ArchiveMessage archiveMessage = JSON.parseObject(
new String(message.getBody()), ArchiveMessage.class
);

// 上报到卫健委
archiveService.reportToHealthCommission(archiveMessage);

// 上报成功,ACK消息
// MQ会自动ACK,删除已消费的消息

} catch (Exception e) {
log.error("病案归档消息消费失败", e);
// 抛出异常,MQ会重试(最多3次)
throw e;
}
}
}
6.4.2 定时任务补偿机制

问题:即使有MQ重试机制,仍然可能出现消息丢失的情况(极端情况)。

解决方案:定时任务扫描未上报的病案,重新发送MQ消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Component
@Slf4j
public class ArchiveCompensationTask {

@Autowired
private MedicalRecordMapper medicalRecordMapper;

@Autowired
private RocketMQTemplate rocketMQTemplate;

/**
* 每30分钟执行一次补偿任务
*/
@Scheduled(cron = "0 */30 * * * * ?")
public void compensateFailedArchive() {
log.info("开始执行病案归档补偿任务");

try {
// 查询所有"待上报"或"上报失败"的病案
List<MedicalRecord> failedRecords = medicalRecordMapper.selectList(
new LambdaQueryWrapper<MedicalRecord>()
.in(MedicalRecord::getArchiveStatus,
ArchiveStatus.PENDING,
ArchiveStatus.FAILED)
.eq(MedicalRecord::getDeleted, 0)
.orderByAsc(MedicalRecord::getCreateTime)
.last("LIMIT 100") // 每次处理100条,避免积压
);

if (CollectionUtils.isEmpty(failedRecords)) {
log.info("没有需要补偿的病案数据");
return;
}

log.info("扫描到{}条需要补偿的病案数据", failedRecords.size());

// 重新发送MQ消息
for (MedicalRecord record : failedRecords) {
try {
sendArchiveMessage(record);
} catch (Exception e) {
log.error("重新发送病案消息失败: recordId={}", record.getId(), e);
}
}

} catch (Exception e) {
log.error("病案归档补偿任务执行失败", e);
}
}
}
6.4.3 分布式锁防止重复处理

问题:多实例部署时,定时任务会同时执行,导致同一病案被重复处理。

解决方案:使用Redisson分布式锁,保证同一时间只有一个实例执行补偿任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Component
@Slf4j
public class ArchiveCompensationTask {

@Autowired
private RedissonClient redissonClient;

/**
* 每30分钟执行一次补偿任务(带分布式锁)
*/
@Scheduled(cron = "0 */30 * * * * ?")
public void compensateFailedArchive() {
String lockKey = "archive:compensation:lock";

// 尝试获取锁(锁30秒,自动释放)
RLock lock = redissonClient.getLock(lockKey);
boolean locked = false;

try {
locked = lock.tryLock(0, 30, TimeUnit.SECONDS);

if (!locked) {
log.info("获取分布式锁失败,其他实例正在执行补偿任务");
return; // 其他实例正在执行,直接返回
}

log.info("获取分布式锁成功,开始执行补偿任务");

// 执行补偿逻辑
doCompensate();

} catch (InterruptedException e) {
log.error("分布式锁获取失败", e);
Thread.currentThread().interrupt();
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("释放分布式锁");
}
}
}

private void doCompensate() {
// 补偿逻辑...
}
}
6.4.4 幂等性设计防止重复上报

问题:即使有分布式锁,仍然可能出现极端情况导致重复上报(如消息重复发送)。

解决方案:在上报接口中增加幂等性校验,使用数据库状态字段防止重复处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@RestController
@RequestMapping("/api/archive")
@Slf4j
public class ArchiveController {

@PostMapping("/report")
public Result<Void> reportArchive(@RequestBody ArchiveReportRequest request) {
Long recordId = request.getRecordId();

// 分布式锁:同一病案同一时间只能上报一次
String lockKey = "archive:report:" + recordId;
RLock lock = redissonClient.getLock(lockKey);
boolean locked = false;

try {
locked = lock.tryLock(0, 10, TimeUnit.SECONDS);
if (!locked) {
return Result.error("操作正在进行,请勿重复提交");
}

// 查询病案状态
MedicalRecord record = medicalRecordMapper.selectById(recordId);
if (record == null) {
return Result.error("病案不存在");
}

// 幂等性校验:已上报的直接返回成功
if (ArchiveStatus.SUCCESS.equals(record.getArchiveStatus())) {
log.info("病案已上报,跳过: recordId={}", recordId);
return Result.success();
}

// 调用卫健委接口上报
boolean success = healthCommissionService.report(record);
if (success) {
// 更新状态为"已上报"
medicalRecordMapper.updateStatus(recordId, ArchiveStatus.SUCCESS);
return Result.success();
} else {
// 更新状态为"上报失败"
medicalRecordMapper.updateStatus(recordId, ArchiveStatus.FAILED);
return Result.error("上报失败");
}

} catch (Exception e) {
log.error("病案上报异常: recordId={}", recordId, e);
return Result.error("上报异常");
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
6.4.5 卫健委回调接口 - MinIO临时文件

问题:卫健委需要主动拉取病案数据,且文件较大(几十MB)。

解决方案

  1. 上传病案PDF到MinIO对象存储
  2. 生成临时访问URL(有效期7天)
  3. 卫健委通过回调接口获取URL和校验和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@RestController
@RequestMapping("/api/archive/callback")
@Slf4j
public class HealthCommissionCallbackController {

@Autowired
private MedicalRecordService medicalRecordService;

@Autowired
private MinioService minioService;

/**
* 卫健委回调接口:获取病案文件信息
*/
@PostMapping("/getArchiveInfo")
public Result<ArchiveInfoResponse> getArchiveInfo(@RequestBody ArchiveInfoRequest request) {
Long recordId = request.getRecordId();

// 查询病案信息
MedicalRecord record = medicalRecordService.getByRecordId(recordId);
if (record == null) {
return Result.error("病案不存在");
}

// 生成MinIO临时访问URL(7天有效)
String fileUrl = minioService.getPresignedUrl(
record.getMinioBucket(),
record.getMinioObjectName(),
7,
TimeUnit.DAYS
);

// 构建响应
ArchiveInfoResponse response = ArchiveInfoResponse.builder()
.recordId(recordId)
.patientId(record.getPatientId())
.fileUrl(fileUrl)
.checksum(record.getChecksum()) // MD5校验和,保证文件完整性
.build();

return Result.success(response);
}
}

技术要点总结

  1. MQ顺序消息

    • 使用患者ID作为hashKey,保证同一患者的消息有序
    • 顺序消费模式(consumeMode=ORDERLY)
    • 失败重试机制(maxReconsumeTimes=3)
  2. 定时任务补偿

    • 每30分钟扫描未上报数据
    • 批量处理(LIMIT 100)避免积压
    • 结合分布式锁防止多实例重复执行
  3. 分布式锁

    • Redisson实现分布式锁
    • 细粒度锁(record级别)
    • 自动释放机制(tryLock timeout)
  4. 幂等性设计

    • 数据库状态字段校验(已上报直接返回)
    • 分布式锁防止并发
    • 医委回调接口只做查询,不做状态变更

面试话术

"在病案归档模块中,我负责优化上报流程,解决数据重复、遗漏、顺序不一致等问题。

核心挑战:卫健委要求病案上报不能重复、不能遗漏、顺序一致,且需要支持卫健委主动拉取数据。

我的解决方案

  1. RocketMQ顺序消息:使用患者ID作为hashKey,保证同一患者的病案按顺序消费
  2. 定时任务补偿:每30分钟扫描未上报数据,重新发送MQ消息,避免数据遗漏
  3. 分布式锁:多实例环境下,防止定时任务重复执行
  4. 幂等性设计:通过状态字段校验,防止重复上报
  5. MinIO临时文件:生成7天有效期的临时URL,支持卫健委主动拉取

效果:系统上线后运行稳定,未出现数据重复或遗漏情况,顺利通过卫健委验收。

这个方案让我深入理解了分布式系统中数据一致性的保证机制,也掌握了RocketMQ顺序消息、分布式锁、幂等性设计等核心技术。"


七、面试话术建议

7.1 项目介绍

"我参与开发的HBOS医生工作站是一个大型医疗信息系统,采用微服务架构设计,涵盖医生开立医嘱、处方、检查检验申请等核心业务。

项目采用Spring Boot + Dubbo微服务架构,按业务领域拆分为DTC诊疗中心、EMR电子病历、HSC服务项目中心、OTC订单中心等6个核心服务。

我的职责包括核心业务开发性能优化技术架构设计等,主要负责医嘱提交、检验检查申请、第三方系统集成等核心模块。"

7.2 大事务瘦身优化

"在做系统稳定性治理时,我发现高并发下数据库连接池经常报警。排查发现是长事务导致的。

业务场景:在医嘱提交、费用结算等核心链路中,需要执行一系列操作:扣减库存、写入数据库、发送MQ消息通知下游、调用第三方接口(如医保局)同步数据、发送短信通知等。

问题分析:原有代码将所有操作都包在一个@Transactional大事务里。虽然数据库操作很快(150ms),但MQ发送和第三方接口调用涉及网络IO,耗时较长(600ms)。在这段时间内,数据库连接一直被持有无法释放。高并发下,数据库连接池迅速被占满,导致后续请求获取不到连接而报错。

解决方案:我利用Spring的TransactionSynchronizationManager进行了优化:

  1. 事务剥离:识别出非强一致性、非DB操作(如发MQ、调三方接口、发短信),这些操作不需要在事务内执行。
  2. afterCommit回调:使用TransactionSynchronizationManager.registerSynchronization注册afterCommit回调钩子,将这些耗时操作移到事务提交后执行。
  3. 异步执行:在afterCommit中使用异步线程池执行网络IO操作,不阻塞主线程。

效果:数据库连接持有时间从750ms降低到150ms,缩短80%;系统并发吞吐量从26请求/秒提升到133请求/秒,提升5倍。"

7.3 设计模式应用

"在项目中广泛应用设计模式解决复杂问题:

  • 适配器模式:统一封装20+种第三方系统的接口调用,通过Adapter层解耦业务代码与第三方系统
  • 责任链模式:实现医嘱验证器链,支持几十种验证规则的灵活组合
  • 策略模式:通过配置中心动态配置验证规则,支持不同医院的定制化需求
  • 模板方法模式:通过AOP实现接口熔断和降级机制,统一处理横切关注点
  • MapStruct:实现高性能数据转换,编译时生成代码,性能提升10倍以上"

7.4 性能优化

"通过多种手段提升系统性能:

  • 多级缓存:Caffeine本地缓存 + Redis分布式缓存,响应时间从几百毫秒降低到几十毫秒
  • 异步处理:使用CompletableFuture并行调用多个服务,提高响应速度
  • 批量查询:减少RPC调用次数,降低网络开销
  • 数据库优化:索引优化、分页查询,数据库QPS降低60%以上"

7.5 技术难点

"项目中遇到的主要技术难点:

  1. 医嘱数据模型复杂:20+种医嘱类型,每种类型有独特的扩展属性,采用JSON扩展字段解决
  2. 多系统数据一致性:采用最终一致性 + 消息队列 + 补偿机制保证
  3. 高并发查询:多级缓存 + 批量查询 + 并行调用优化
  4. 定制化需求:通过SPI扩展点机制支持不同医院的定制化需求"

八、总结

8.1 核心技术亮点

  1. 微服务架构设计优秀:采用DDD领域驱动设计,服务划分合理
  2. 性能优化到位:多级缓存、大事务瘦身、批量查询、并行调用等
  3. 代码质量高:大量使用设计模式,代码结构清晰
  4. 可扩展性强:适配器模式、扩展点机制等设计支持灵活扩展
  5. 业务理解深入:对医疗业务理解深入,解决方案贴合实际需求

8.2 技术收获

  • 掌握微服务架构设计和DDD领域驱动设计
  • 熟练使用各种设计模式解决实际问题
  • 深入理解Spring事务机制和优化技巧
  • 掌握性能优化的多种手段
  • 理解医疗业务流程和行业特点

本文档整合了熙牛医疗HBOS项目的核心技术内容,涵盖架构设计、性能优化、设计模式、业务流程等方面,适合用于面试准备和技术总结。