需求分析
点击具体的科室,分页显示每天的挂号数量
getBookingScheduleRule逻辑
- 根据医院编号获取预约规则
Hospital hospital = (Hospital) hospitalService.getByHoscode(hoscode);
if(null == hospital) {
throw new YyghException(ResultCodeEnum.DATA_ERROR);
}
BookingRule bookingRule = hospital.getBookingRule();
- 获取可预约日期分页数据
- 通过bookRule得到当天的放号时间releaseTime以及预约周期cycle。
- 如果当天的放号时间已过,则预约周期后一天为即将放号时间,周期加1
- 可预约所有日期,最后一天显示即将放号倒计时。假如cycle=10 那么所有可预约时间3.1 -->3.2,3.3,3.4……3.9,3.10
- 要进行分页显示,先要判断limit和上一步得到的可预约所有日期dateList 哪个大,如果limit>dateList, end=dataList.size(),就直接显示不需要分页。
- 否则就要分页显示
IPage iPage = this.getListDate(page, limit, bookingRule);
- 得到当前页的可预约日期
List<Date> dateList = iPage.getRecords();
- 获取可预约日期科室剩余预约数
- 在dateList即可预约的日期中,根据hoscode,depcode和workdata作为条件得到availableNumber和reservedNumber
- 通过mongo的aggregate,得到上述条件的查询结果得到BookingScheduleRuleVo
- 将统计数据ScheduleVo根据“安排日期”合并到BookingRuleVo
//获取可预约日期科室剩余预约数
Criteria criteria = Criteria.where("hoscode").is(hoscode).and("depcode").is(depcode).and("workDate").in(dateList);
Aggregation agg = Aggregation.newAggregation(
Aggregation.match(criteria),
Aggregation.group("workDate")//分组字段
.first("workDate").as("workDate")
.count().as("docCount")
.sum("availableNumber").as("availableNumber")
.sum("reservedNumber").as("reservedNumber")
);
AggregationResults<BookingScheduleRuleVo> aggregationResults = mongoTemplate.aggregate(agg, Schedule.class, BookingScheduleRuleVo.class);
List<BookingScheduleRuleVo> scheduleVoList = aggregationResults.getMappedResults();
- 合并数据 将统计数据ScheduleVo根据“安排日期”合并到BookingRuleVo,map集合, key是日期,value是预约规则和剩余数量
- 用stream流的方法,将scheduleVoList 转成map集合
Map<Date, BookingScheduleRuleVo> scheduleVoMap = new HashMap<>();
if(!CollectionUtils.isEmpty(scheduleVoList)) {
scheduleVoMap = scheduleVoList.stream().collect(
Collectors.toMap(
BookingScheduleRuleVo::getWorkDate, BookingScheduleRuleVo -> BookingScheduleRuleVo));
}
- 获取可预约排班规则,通过遍历map给bookingScheduleRuleVo属性赋值
- 判断scheduleVoMap.get(date)是否为空,空则说明当天无医生
- 日期转为星期
- 最后一页最后一条记录为即将预约
- 当天预约如果过了停号时间, 不能预约
- 将每个date对应的结果放到bookingScheduleRuleVoList中
//获取可预约排班规则
List<BookingScheduleRuleVo> bookingScheduleRuleVoList = new ArrayList<>();
for(int i=0, len=dateList.size(); i<len; i++) {
Date date = dateList.get(i);
//从map集合根据key日期获取value值
BookingScheduleRuleVo bookingScheduleRuleVo = scheduleVoMap.get(date);
if(null == bookingScheduleRuleVo) {
// 说明当天没有排班医生
bookingScheduleRuleVo = new BookingScheduleRuleVo();
//就诊医生人数
bookingScheduleRuleVo.setDocCount(0);
//科室剩余预约数 -1表示无号
bookingScheduleRuleVo.setAvailableNumber(-1);
}
bookingScheduleRuleVo.setWorkDate(date);
bookingScheduleRuleVo.setWorkDateMd(date);
//计算当前预约日期为周几
String dayOfWeek = this.getDayOfWeek(new DateTime(date));
bookingScheduleRuleVo.setDayOfWeek(dayOfWeek);
//最后一页最后一条记录为即将预约 状态 0:正常 1:即将放号 -1:当天已停止挂号
if(i == len-1 && page == iPage.getPages()) {
bookingScheduleRuleVo.setStatus(1);
} else {
bookingScheduleRuleVo.setStatus(0);
}
//当天预约如果过了停号时间, 不能预约
if(i == 0 && page == 1) {
DateTime stopTime = this.getDateTime(new Date(), bookingRule.getStopTime());
if(stopTime.isBeforeNow()) {
//停止预约
bookingScheduleRuleVo.setStatus(-1);
}
}
bookingScheduleRuleVoList.add(bookingScheduleRuleVo);
}
- 最后把所有的信息封装到result中
//可预约日期规则数据
result.put("bookingScheduleList", bookingScheduleRuleVoList);
result.put("total", iPage.getTotal());
//其他基础数据
Map<String, String> baseMap = new HashMap<>();
//医院名称
baseMap.put("hosname", hospitalService.getHospName(hoscode));
//科室
Department department =departmentService.getDepartment(hoscode, depcode);
//大科室名称
baseMap.put("bigname", department.getBigname());
//科室名称
baseMap.put("depname", department.getDepname());
//月
baseMap.put("workDateString", new DateTime().toString("yyyy年MM月"));
//放号时间
baseMap.put("releaseTime", bookingRule.getReleaseTime());
//停号时间
baseMap.put("stopTime", bookingRule.getStopTime());
result.put("baseMap", baseMap);
return result;
最终效果:
- 红框就是查询预约信息排班接口对应的结果(就是一大堆东西)
- 绿框左边就是排班接口的结果
预约确认功能
- 根据排班id获取排班信息,在页面展示
getSchedule,不仅通过id得到排班信息,同时封装排班详情其他值 :医院名称、科室名称、日期对应星期。
//封装排班详情其他值 医院名称、科室名称、日期对应星期
private Schedule packageSchedule(Schedule schedule) {
//设置医院名称
schedule.getParam().put("hosname",hospitalService.getHospName(schedule.getHoscode()));
//设置科室名称
schedule.getParam().put("depname",departmentService.getDepName(schedule.getHoscode(),schedule.getDepcode()));
//设置日期对应星期
schedule.getParam().put("dayOfWeek",this.getDayOfWeek(new DateTime(schedule.getWorkDate())));
return schedule;
}
- 选择就诊人
调用patientApiController的findall方法,查询当前登陆用户的所有相关的就诊人。 - 预约下单
预约挂号下单+mq
下单参数:就诊人id与排班id,需要通过远程调用的方式得到
下单根据id我们要获取就诊人信息:然后service-user写接口,在patientController中实现,然后写远程调用(Feign) ,路径是inner
//根据就诊人id获取就诊人信息
//注意路径中的inner 是作为内部接口调用的!!在网关中进行了配置
@ApiOperation(value = "获取就诊人")
@GetMapping("inner/get/{id}")
public Patient getPatientOrder(
@ApiParam(name = "id", value = "就诊人id", required = true)
@PathVariable("id") Long id) {
return patientService.getById(id);
}
根据排班id获取下单信息与规则信息,封装到scheduleOrderVo。在service-hosp中的HosptialApiController,再写远程调用,调用的是排班信息,所以用scheduleService。 scheduleOrderVo封装了很全的属性,方便前端调用。
private Schedule getScheduleId(String scheduleId) {
Schedule schedule = scheduleRepository.findById(scheduleId).get();
return this.packageSchedule(schedule);
}
//根据排班id获取预约下单数据
@Override
public ScheduleOrderVo getScheduleOrderVo(String scheduleId) {
ScheduleOrderVo scheduleOrderVo = new ScheduleOrderVo();
//排班信息
Schedule schedule = this.getScheduleId(scheduleId);
if(null == schedule) {
throw new YyghException(ResultCodeEnum.PARAM_ERROR);
}
//获取预约规则信息
Hospital hospital = (Hospital) hospitalService.getByHoscode(schedule.getHoscode());
if(null == hospital) {
throw new YyghException(ResultCodeEnum.DATA_ERROR);
}
BookingRule bookingRule = hospital.getBookingRule();
if(null == bookingRule) {
throw new YyghException(ResultCodeEnum.PARAM_ERROR);
}
scheduleOrderVo.setHoscode(schedule.getHoscode());
scheduleOrderVo.setHosname(hospitalService.getHospName(schedule.getHoscode()));
scheduleOrderVo.setDepcode(schedule.getDepcode());
scheduleOrderVo.setDepname(departmentService.getDepName(schedule.getHoscode(), schedule.getDepcode()));
scheduleOrderVo.setHosScheduleId(schedule.getHosScheduleId());
scheduleOrderVo.setAvailableNumber(schedule.getAvailableNumber());
scheduleOrderVo.setTitle(schedule.getTitle());
scheduleOrderVo.setReserveDate(schedule.getWorkDate());
scheduleOrderVo.setReserveTime(schedule.getWorkTime());
scheduleOrderVo.setAmount(schedule.getAmount());
//退号截止天数(如:就诊前一天为-1,当天为0)
int quitDay = bookingRule.getQuitDay();
DateTime quitTime = this.getDateTime(new DateTime(schedule.getWorkDate()).plusDays(quitDay).toDate(), bookingRule.getQuitTime());
scheduleOrderVo.setQuitTime(quitTime.toDate());
//预约开始时间
DateTime startTime = this.getDateTime(new Date(), bookingRule.getReleaseTime());
scheduleOrderVo.setStartTime(startTime.toDate());
//预约截止时间
DateTime endTime = this.getDateTime(new DateTime().plusDays(bookingRule.getCycle()).toDate(), bookingRule.getStopTime());
scheduleOrderVo.setEndTime(endTime.toDate());
//当天停止挂号时间
DateTime stopTime = this.getDateTime(new Date(), bookingRule.getStopTime());
scheduleOrderVo.setStartTime(startTime.toDate());
return scheduleOrderVo;
}
根据hoscode获取医院签名信息封装到signInfoVo中,然后通过接口去医院预约下单
//获取医院签名信息
@Override
public SignInfoVo getSignInfoVo(String hoscode) {
QueryWrapper<HospitalSet> wrapper = new QueryWrapper<>();
wrapper.eq("hoscode",hoscode);
HospitalSet hospitalSet = baseMapper.selectOne(wrapper);
if(null == hospitalSet) {
throw new YyghException(ResultCodeEnum.HOSPITAL_OPEN);
}
SignInfoVo signInfoVo = new SignInfoVo();
signInfoVo.setApiUrl(hospitalSet.getApiUrl());
signInfoVo.setSignKey(hospitalSet.getSignKey());
return signInfoVo;
}
实现下单接口,维护更新下单order_info表,调用医院的接口成功后返回值,下单成功更新排班信息并发送短信
根据scheduleId和patientId维护order_info信息
- 获取就诊人信息
- 获取排班信息
- 判断当前的时间是否可以预约
if(new DateTime(scheduleOrderVo.getStartTime()).isAfterNow()
|| new DateTime(scheduleOrderVo.getEndTime()).isBeforeNow()) {
throw new YyghException(ResultCodeEnum.TIME_NO);
}
- 获取医院签名,为了调用对应医院的接口
JSONObject result = HttpRequestHelper.sendRequest(paramMap, signInfoVo.getApiUrl()+"/order/submitOrder");
- scheduleOrderVo数据复制到orderInfo。
- 向orderInfo设置其他数据,scheduleOrderVo只是把排班的信息赋值过去,还要就诊人的信息,然后更新order_Info表。
- 调用医院的接口,实现预约挂号/下单需求,设置调用医院接口需要的参数,参数放到map集合。
- 请求医院系统接口 hospitalController
JSONObject result = HttpRequestHelper.sendRequest(paramMap, signInfoVo.getApiUrl()+"/order/submitOrder");
- 成功请求医院的接口后得到返回的数据:如取号时间,地址,排号顺序,排班预约数,剩余数。
- 更新订单信息(除了可预约数和剩余数),可预约数和剩余数要用mq。OrderMqVo中包括可预约数剩余数,排班id和短信实体MSmvo。
- 短信提示:先封装短信模板参数
MsmVo msmVo = new MsmVo();
msmVo.setPhone(orderInfo.getPatientPhone());
String reserveDate =
new DateTime(orderInfo.getReserveDate()).toString("yyyy-MM-dd")
+ (orderInfo.getReserveTime()==0 ? "上午": "下午");
Map<String,Object> param = new HashMap<String,Object>(){{
put("title", orderInfo.getHosname()+"|"+orderInfo.getDepname()+"|"+orderInfo.getTitle());
put("amount", orderInfo.getAmount());
put("reserveDate", reserveDate);
put("name", orderInfo.getPatientName());
put("quitTime", new DateTime(orderInfo.getQuitTime()).toString("yyyy-MM-dd HH:mm"));
}};
msmVo.setParam(param);
orderMqVo.setMsmVo(msmVo);
rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_ORDER, MqConst.ROUTING_ORDER, orderMqVo);
- 发送mq消息,更新号源的操作交给mq,短信通知用户挂号成功。更新号源即更新剩余数和可预约数,为了提高并发性能,将更新的操作交给mq来管理。
rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_ORDER, MqConst.ROUTING_ORDER, orderMqVo);
- 在HospitalReceiver中,配置了RabbitListener,对应sendMessage中的order交换机,路由,队列。
- 判断队列即orderMqVo的可预约数是否为空,不空则更新mongo->schedule预约数量。
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = MqConst.QUEUE_ORDER, durable = "true"),
exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_ORDER),
key = {MqConst.ROUTING_ORDER}
))
public void receiver(OrderMqVo orderMqVo, Message message, Channel channel) throws IOException {
if(null != orderMqVo.getAvailableNumber()) {
//下单成功更新预约数
Schedule schedule = scheduleService.getById(orderMqVo.getScheduleId());
schedule.setReservedNumber(orderMqVo.getReservedNumber());
schedule.setAvailableNumber(orderMqVo.getAvailableNumber());
//update() : {scheduleRepository.save(schedule);},更新的是mongo中的剩余预约和可预约数量
scheduleService.update(schedule);
}else {
//取消预约更新预约数
Schedule schedule = scheduleService.getById(orderMqVo.getScheduleId());
int availableNumber = schedule.getAvailableNumber().intValue() + 1;
schedule.setAvailableNumber(availableNumber);
scheduleService.update(schedule);
}
//mq发送消息
MsmVo msmVo = orderMqVo.getMsmVo();
if(null != msmVo) {
rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_MSM, MqConst.ROUTING_MSM_ITEM, msmVo);
}
}
- 预约数量更新结束后,mq发送消息,跳转到MsmReceiver,同样配置了RabbitListener,也对应信息交换机,路由,队列,调用短信发送的接口。
public void send(MsmVo msmVo, Message message, Channel channel) {
msmService.send(msmVo);
}
- 调用第三方服务发送短信,步骤和发送验证码一直,不同之处是发送验证码的send(phone,code),发送挂号短信的send(msmVo.getPhone(), msmVo.getParam()),param中包括之前封装的短信模板参数,将这些信息发送给用户。
rabbitService
注入RabbitTemplate,实现sendMessage方法,在orderServiceImpl中调用rabbitService,在HosptialReceiver更新完预约数量后调用rabbitService的sendMessage方法,相应的HosptialReceiver和MsmReceiver中配置了RabbitListener,会发送mq消息。
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送消息
* @param exchange 交换机
* @param routingKey 路由键
* @param message 消息
*/
public boolean sendMessage(String exchange, String routingKey, Object message) {
rabbitTemplate.convertAndSend(exchange, routingKey, message);
return true;
}
预约成功后我们要更新预约数和短信提醒预约成功,为了提高下单的并发性,这部分逻辑我们就交给mq为我们完成,预约成功发送消息即可
典型应用场景:
- 异步处理。把消息放入消息中间件中,等到需要的时候再去处理。
- 流量削峰。例如秒杀活动,在短时间内访问量急剧增加,使用消息队列,当消息队列满了就拒绝响应,跳转到错误页面,这样就可以使得系统不会因为超负载而崩溃。
- 日志处理
- 应用解耦
订单详情显示
点击确认挂号后,会到订单详情页面
根据orderid,返回orderInfo给到前端
//根据订单id查询订单详情
@Override
public OrderInfo getOrder(String orderId) {
OrderInfo orderInfo = baseMapper.selectById(orderId);
return this.packOrderInfo(orderInfo);
}
private OrderInfo packOrderInfo(OrderInfo orderInfo) {
orderInfo.getParam().put("orderStatusString", OrderStatusEnum.getStatusNameByStatus(orderInfo.getOrderStatus()));
return orderInfo;
}
订单列表(条件查询带分页)
就诊人和订单状态的信息会封装到OrderQueryVo中
controller
//订单列表(条件查询带分页)
@GetMapping("auth/{page}/{limit}")
public Result list(@PathVariable Long page,
@PathVariable Long limit,
OrderQueryVo orderQueryVo,
HttpServletRequest request) {
//设置当前用户id
orderQueryVo.setUserId(AuthContextHolder.getUserId(request));
Page<OrderInfo> pageParam = new Page<>(page,limit);
IPage<OrderInfo> pageModel =
orderService.selectPage(pageParam,orderQueryVo);
return Result.ok(pageModel);
}
serviceImpl
实际情况中,一个认证的user可能有许多订单,包括自己的和patient的订单,我们根据前端传来的参数,就诊人和订单状态进行模糊查询,条件查询查出对应的订单信息
public IPage<OrderInfo> selectPage(Page<OrderInfo> pageParam, OrderQueryVo orderQueryVo) {
//orderQueryVo获取条件值
String name = orderQueryVo.getKeyword(); //医院名称
Long patientId = orderQueryVo.getPatientId(); //就诊人名称
String orderStatus = orderQueryVo.getOrderStatus(); //订单状态
String reserveDate = orderQueryVo.getReserveDate();//安排时间
String createTimeBegin = orderQueryVo.getCreateTimeBegin();
String createTimeEnd = orderQueryVo.getCreateTimeEnd();
//对条件值进行非空判断
QueryWrapper<OrderInfo> wrapper = new QueryWrapper<>();
if(!StringUtils.isEmpty(name)) {
wrapper.like("hosname",name);
}
if(!StringUtils.isEmpty(patientId)) {
wrapper.eq("patient_id",patientId);
}
if(!StringUtils.isEmpty(orderStatus)) {
wrapper.eq("order_status",orderStatus);
}
if(!StringUtils.isEmpty(reserveDate)) {
wrapper.ge("reserve_date",reserveDate);
}
if(!StringUtils.isEmpty(createTimeBegin)) {
wrapper.ge("create_time",createTimeBegin);
}
if(!StringUtils.isEmpty(createTimeEnd)) {
wrapper.le("create_time",createTimeEnd);
}
//调用mapper的方法
IPage<OrderInfo> pages = baseMapper.selectPage(pageParam, wrapper);
//编号变成对应值封装
pages.getRecords().stream().forEach(item -> {
this.packOrderInfo(item);
});
return pages;
}