需求背景:存在狀態流轉的預約單
一.數據庫設計
CREATE TABLE `appointment` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵id',`appoint_type` int(11) NOT NULL COMMENT '預約類型(0:線下查房...)',`appoint_user_id` bigint(20) NOT NULL COMMENT '預約人userId',`appoint_store_id` bigint(20) NOT NULL COMMENT '預約門店',`appoint_service_type` int(11) DEFAULT NULL COMMENT '預約服務類型(9:兒科查房 7:產科查房 8:中醫查房)',`appoint_doctor_id` bigint(11) DEFAULT NULL COMMENT '預約醫生id',`appoint_date` date DEFAULT NULL COMMENT '預約日期(精確到日)',`appoint_time_start` time NOT NULL COMMENT '預約開始時間',`appoint_time_end` time NOT NULL COMMENT '預約結束時間',`status` int(11) NOT NULL COMMENT '狀態(-1:已取消 0:待接單 1:待分配(已拒絕) 2:待查房 3:待小結 4:待簽名 100:已完成 )',`drive_appointment_id` bigint(20) DEFAULT NULL COMMENT '驅動預約單id(null:代表驅動預約單)',`appointment_setting_id` bigint(20) DEFAULT NULL COMMENT '預約單-配置id',`create_id` bigint(20) DEFAULT NULL COMMENT '創建人id',`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',`modify_id` bigint(20) DEFAULT NULL COMMENT '修改人',`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',`deleted` tinyint(1) DEFAULT '0' COMMENT '刪除標記;0-正常 ;1-刪除',PRIMARY KEY (`id`),KEY `idx_drive_appointment_id` (`drive_appointment_id`) USING BTREE,KEY `idx_appointment_setting_id` (`appointment_setting_id`) USING BTREE,KEY `idx_appoint_user_id` (`appoint_user_id`) USING BTREE
) AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='預約單';-- 預約單-狀態流轉表CREATE TABLE `stbella-his`.appointment_status_log
( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵id',appointment_id bigint(20) not null COMMENT '預約單id',before_status int(11) NOT null COMMENT '前狀態',after_status int(11) NOT null COMMENT '后狀態',handle_type int(11) not null COMMENT '操作類型',handle_user_type int(11) not null COMMENT '操作人類型',create_id bigint(20) DEFAULT NULL COMMENT '創建人id',gmt_create datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',modify_id bigint(20) DEFAULT NULL COMMENT '修改人',gmt_modified datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',deleted tinyint(1) DEFAULT '0' COMMENT '刪除標記;0-正常 ;1-刪除',PRIMARY KEY (`id`),KEY `idx_appointment_id` (`appointment_id`) USING BTREE
) comment '預約單-狀態流轉表';
二.狀態枚舉類
@Getter
@AllArgsConstructor
public enum AppointStatusEnum {INIT(-100, "初始化"),CANCEL(-1, "已取消"),WAIT_RECEIVE(0, "待接單"),WAIT_DISTRIBUTE(1, "待分配"),WAIT_CHECK_ROOM(2, "待查房"),WAIT_SUMMARY(3, "待小結"),WAIT_SIGN(4, "待簽名"),COMPLETE(100, "已完成");private final int code;private final String name;public static AppointStatusEnum getEnum(int code) {for (AppointStatusEnum statusEnum : AppointStatusEnum.values()) {if (statusEnum.getCode() == code) {return statusEnum;}}return null;}}
三. 上下文參數類:參數傳遞
@Builder
@Data
public class AppointContext implements Serializable {private static final long serialVersionUID = 3542771730176821092L;private UserTokenInfoDTO userTokenInfoDTO;private ClientEnum clientEnum;private AppointPO appointPO;}
四.狀態機流轉上下文類:所有要執行的動作都在這里記錄
@Data
public class AppointHandleContext implements Serializable {private static final long serialVersionUID = 1658366511210864400L;private IAppointStatusHandler statusHandler = AppointStatusHandlerFactory.getStatusHandler(AppointStatusEnum.INIT);public AppointDetailVO detail(AppointHandleContext handleContext, public Boolean cancel(AppointHandleContext handleContext, AppointContext context) {return statusHandler.cancel(handleContext, context);}public String add(AppointHandleContext handleContext, AppointContext context) {return statusHandler.add(handleContext, context);}public Boolean update(AppointHandleContext handleContext, AppointContext context) {return statusHandler.update(handleContext, context);}}
五.狀態處理器工廠類
@Component
public class AppointStatusHandlerFactory implements ApplicationContextAware {private static final Map<AppointStatusEnum, IAppointStatusHandler> MAP = Maps.newHashMapWithExpectedSize(AppointStatusEnum.values().length);@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {Map<String, IAppointStatusHandler> beansOfType = applicationContext.getBeansOfType(IAppointStatusHandler.class);if (CollectionUtil.isEmpty(beansOfType)) {return;}beansOfType.forEach((key, statusHandler) -> MAP.put(statusHandler.getStatus(), statusHandler));}public static IAppointStatusHandler getStatusHandler(AppointStatusEnum appointStatusEnum) {return Optional.ofNullable(MAP.get(appointStatusEnum)).orElseThrow(() -> new BusinessException(ResultEnum.PARAM_ERROR, "預約單狀態異常"));}}
六.狀態接口類
public interface IAppointStatusHandler {AppointStatusEnum getStatus();AppointDetailVO detail(AppointHandleContext handleContext, AppointContext context);Boolean cancel(AppointHandleContext handleContext, AppointContext context);String add(AppointHandleContext handleContext, AppointContext context);Boolean update(AppointHandleContext handleContext, AppointContext context);」
七.狀態抽象實現類
@Slf4j
@Component
public abstract class AbstractAppointStatusHandler implements IAppointStatusHandler {private static final String LOG_PRE = "預約單狀態流轉異常,method:{},handleContext:{},context:{}";@Overridepublic AppointDetailVO detail(AppointHandleContext handleContext, AppointContext context) {log.error(LOG_PRE, "detail", JSONUtil.toJsonStr(handleContext), JSONUtil.toJsonStr(context));throw new BusinessException(ResultEnum.PARAM_ERROR, "當前狀態不允許查詢");}@Overridepublic Boolean cancel(AppointHandleContext handleContext, AppointContext context) {log.error(LOG_PRE, "cancel", JSONUtil.toJsonStr(handleContext), JSONUtil.toJsonStr(context));throw new BusinessException(ResultEnum.PARAM_ERROR, "當前狀態不允許取消");}@Overridepublic String add(AppointHandleContext handleContext, AppointContext context) {log.error(LOG_PRE, "add", JSONUtil.toJsonStr(handleContext), JSONUtil.toJsonStr(context));throw new BusinessException(ResultEnum.PARAM_ERROR, "當前狀態不允許新增");}@Overridepublic Boolean update(AppointHandleContext handleContext, AppointContext context) {log.error(LOG_PRE, "update", JSONUtil.toJsonStr(handleContext), JSONUtil.toJsonStr(context));throw new BusinessException(ResultEnum.PARAM_ERROR, "當前狀態不允許修改");}
}
八.每個狀態節點實現類
@Slf4j
@Component
public class AppointInitHandler extends AbstractAppointStatusHandler {@Overridepublic AppointStatusEnum getStatus() {return AppointStatusEnum.INIT;}@Overridepublic AppointDetailVO detail(AppointHandleContext handleContext, AppointContext context) {return appointSupport.detail(context);}@Overridepublic String add(AppointHandleContext handleContext, AppointContext context) {}}
@Slf4j
@Component
public class AppointWaitCheckRoomHandler extends AbstractAppointStatusHandler {@Overridepublic AppointStatusEnum getStatus() {return AppointStatusEnum.WAIT_CHECK_ROOM;}@Resourceprivate AppointSupport appointSupport;@Overridepublic AppointDetailVO detail(AppointHandleContext handleContext, AppointContext context) {AppointDetailVO detail = appointSupport.detail(context);ClientEnum clientEnum = context.getClientEnum();AppointPO appointPO = context.getAppointPO();switch (clientEnum) {case HIS_NURSE:this.nurseDetail(detail, appointPO);break;case HIS_DOCTOR:this.doctorDetail(detail, appointPO);break;default:break;}return detail;}@Overridepublic Boolean cancel(AppointHandleContext handleContext, AppointContext context) {return appointSupport.handleOnlyStatus(context, AppointStatusEnum.CANCEL, AppointHandleTypeEnum.CANCEL);}@Overridepublic Boolean startCheckRoom(AppointHandleContext handleContext, AppointContext context) {return appointSupport.handleOnlyStatus(context, AppointStatusEnum.WAIT_SUMMARY, AppointHandleTypeEnum.START_CHECK_ROOM);}
@Slf4j
@Component
public class AppointCompleteHandler extends AbstractAppointStatusHandler {@Overridepublic AppointStatusEnum getStatus() {return AppointStatusEnum.COMPLETE;}@Overridepublic AppointDetailVO detail(AppointHandleContext handleContext, AppointContext context) {AppointDetailVO detail = appointSupport.detail(context);ClientEnum clientEnum = context.getClientEnum();AppointPO appointPO = context.getAppointPO();switch (clientEnum) {case HIS_NURSE:this.nurseDetail(detail, appointPO);break;case HIS_DOCTOR:this.doctorDetail(detail, appointPO);break;default:break;}return detail;}@Overridepublic AppointCheckRoomSignNotifyVO getAppointCheckRoomSignNotifyVO(AppointHandleContext handleContext, AppointContext context) {}
}
九.具體行為調用代碼類
@Overridepublic void bindPicpCustomer(AppointClinicBindReq req, UserTokenInfoDTO userTokenInfoDTO) {AppointPO appointPO = Optional.ofNullable(appointRepository.getOnePOById(req.getAppointmentId())).orElseThrow(() ->new BusinessException(ResultEnum.PARAM_ERROR, "預約單數據不存在或已被刪除"));AppointHandleContext handleContext = new AppointHandleContext();handleContext.setStatusHandler(AppointStatusHandlerFactory.getStatusHandler(AppointStatusEnum.getEnum(appointPO.getStatus())));handleContext.bindPicpCustomer(handleContext, AppointContext.builder().appointPO(appointPO).clientEnum(appointSupport.getLoginClientEnum(userTokenInfoDTO)).userTokenInfoDTO(userTokenInfoDTO).picpUsers(req.getPicpUsers()).build());}