尚医通-预约挂号
# 尚医通-预约挂号
# 预约挂号详情
# 需求分析
- 接口分析
- 根据预约周期,展示可预约日期数据,按分页展示
- 选择日期展示当天可预约列表(该接口后台已经实现过)
- 页面展示分析
- 分页展示可预约日期,根据有号、无号、约满等状态展示不同颜色,以示区分
- 可预约最后一个日期为即将放号日期,根据放号时间页面展示倒计时
# api 接口
# 添加 service 接口以及实现类
- 在 ScheduleService 类添加接口
//获取可预约的排版数据
Map<String,Object> getBookingScheduleRule(Integer page, Integer limit, String hoscode, String depcode);
- 在 ScheduleServiceImpl 类添加接口实现
//获取可预约的排班数据
@Override
public Map<String, Object> getBookingScheduleRule(Integer page, Integer limit, String hoscode, String depcode) {
Map<String, Object> result = new HashMap<>();
//获取预约规则
//根据医院的编号获取预约的规则
Hospital hospital = hospitalService.getByHosCode(hoscode);
if(hospital==null){
throw new YyghException(ResultCodeEnum.DATA_ERROR);
}
BookingRule bookingRule = hospital.getBookingRule();
//获取可预约的日期的数据(分页)
IPage iPage = this.getListDate(page,limit,bookingRule);
//获取可预约的日期
List<Date> dateList = iPage.getRecords();
//获取可预约日期里面科室剩余预约数
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> aggregateResult =
mongoTemplate.aggregate(agg, Schedule.class, BookingScheduleRuleVo.class);
List<BookingScheduleRuleVo> scheduleRuleVoList = aggregateResult.getMappedResults();
//合并数据 map集合 key日期 value预约规则和数量信息
Map<Date, BookingScheduleRuleVo> scheduleVoMap = new HashMap<>();
if(!CollectionUtils.isEmpty(scheduleRuleVoList)){
scheduleVoMap = scheduleRuleVoList.stream()
.collect(
Collectors.toMap(BookingScheduleRuleVo::getWorkDate,
BookingScheduleRuleVo -> BookingScheduleRuleVo));
}
//获取可预约排班规则
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(bookingScheduleRuleVo == null){
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(0);
} 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.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;
}
//获取可预约的日志分页数据
private IPage getListDate(Integer page, Integer limit, BookingRule bookingRule) {
//获取当天放号的一个时间 年 月 日 小时 分钟
DateTime releaseTime = this.getDateTime(new Date(), bookingRule.getReleaseTime());
//获取预约周期
Integer cycle = bookingRule.getCycle();
//如果当天放号时间已经过去了,预约周期从后一天开始计算,周期+1
if(releaseTime.isBeforeNow()){
cycle += 1;
}
//获取可预约的所有的日期,最后一天显示即将放号
List<Date> dateList = new ArrayList<>();
for (int i = 0; i < cycle; i++) {
DateTime currentDateTime = new DateTime().plusDays(i);
String dataSting = currentDateTime.toString("yyyy-MM-dd");
dateList.add(new DateTime(dataSting).toDate());
}
//因为预约周期是不同的,每页显示的日期最多七天的数据,超过七天分页
List<Date> pageDateList = new ArrayList<>();
int start = (page-1)*limit;
int end = (page-1)*limit+limit;
//如果可以显示的数据小于7,直接显示
if(end > dateList.size()){
end = dateList.size();
}
for (int i = start; i < end ; i++) {
pageDateList.add(dateList.get(i));
}
//如果可以显示的数据大于7,进行分页
IPage<Date> iPage =
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, 7, dateList.size());
iPage.setRecords(pageDateList);
return iPage;
}
/**
* 将Date日期(yyyy-MM-dd HH:mm)转换为DateTime
*/
private DateTime getDateTime(Date date, String timeString) {
String dateTimeString = new DateTime(date).toString("yyyy-MM-dd") + " "+ timeString;
DateTime dateTime = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm").parseDateTime(dateTimeString);
return dateTime;
}
# 获取科室信息
# 添加 service 接口以及实现类
- 在DepartmentService类添加接口
//根绝科室编号,和医院编号,查询科室
Department getDepartment(String hoscode, String depcode);
- 在DepartmentImpl类实现接口
@Override
public Department getDepartment(String hoscode, String depcode) {
return departmentRepository.getDepartemntByHoscodeAndDepcode(hoscode,depcode);
}
# 添加 Controller 接口
在HospitalApiController类添加方法
//获取可预约的排班数据
@ApiOperation(value = "获取可预约排版数据")
@GetMapping("/auth/getBookingScheduleRule/{page}/{limit}/{hoscode}/{depcode}")
public Result getBookingScheduleRule(@PathVariable Integer page,
@PathVariable Integer limit,
@PathVariable String hoscode,
@PathVariable String depcode){
return Result.ok(scheduleService.getBookingScheduleRule(page,limit,hoscode,depcode));
}
//获取排班的具体数据
@ApiOperation(value = "获取排班数据")
@GetMapping("/auth/findScheduleList/{hoscode}/{depcode}/{workData}")
public Result findScheduleList(@PathVariable String hoscode,
@PathVariable String depcode,
@PathVariable String workData){
return Result.ok(scheduleService.getDetailSchedule(hoscode,depcode,workData));
}
- 前端访问结果
# 预约确认
- 根据排班id获取排班信息,在页面展示
- 选择就诊人
- 预约下单
# api 接口
# 添加 service 接口以及实现类
- 在ScheduleService类添加接口
//根绝排班id获取排班数据
Schedule getScheduleById(String scheduleId);
- 在ScheduleServiceImpl类实现接口
//根绝排班id获取排班数据
@Override
public Schedule getScheduleById(String scheduleId) {
Schedule schedule = scheduleRepository.findById(scheduleId).get();
return this.packageSchedule(schedule);
}
# 添加 Controller 接口
//根绝排班id获取排班数据
@ApiOperation(value = "根绝排班id获取排班数据")
@GetMapping("/getSchedule/{scheduleId}")
public Result getSchedule(@PathVariable String scheduleId){
Schedule schedule = scheduleService.getScheduleById(scheduleId);
return Result.ok(schedule);
}
- 前端访问测试
# 预约下单
由于预约下单后台api接口相对复杂,我们先实现前端,前端配合调试api接口。
# 需求分析
# 订单表结构
# 下单分析
下单参数:就诊人id与排班id
- 下单我们要获取就诊人信息
- 获取排班下单信息与规则信息
- 获取医院签名信息,然后通过接口去医院预约下单
- 下单成功更新排班信息与发送短信
# 搭建 service-order 模块
# 修改配置
- 修改 pom.xml,引入依赖
<dependencies>
<dependency>
<groupId>com.frx01</groupId>
<artifactId>service-cmn-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- 添加配置文件 application.properties
# 服务端口
server.port=8206
# 服务名
spring.application.name=service-order
# 环境设置:dev、test、prod
spring.profiles.active=dev
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/yygh_hosp?characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=hsp
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
spring.data.mongodb.uri=mongodb://192.168.91.166:27017/yygh_hosp
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 启动类
@SpringBootApplication
@ComponentScan(basePackages = {"com.frx01"})
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.frx01"})
public class ServiceOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceOrderApplication.class, args);
}
}
# 配置网关
#设置路由id
spring.cloud.gateway.routes[6].id=service-order
#设置路由的uri
spring.cloud.gateway.routes[6].uri=lb://service-order
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[6].predicates= Path=/*/order/**
# 添加订单基础类
# 添加 model
说明:由于实体对象没有逻辑,我们已经统一导入
com.frx01.yygh.model.order.OrderInfo
# 添加 Mapper
添加com.frx01.yygh.order.mapper.OrderInfoMapper,添加配置类,扫描mapper包
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
}
# 添加 service 接口及实现类
- 添加com.frx01.yygh.order.service.OrderService接口
public interface OrderService extends IService<OrderInfo> {
//生成挂号的订单
Long saveOrder(String scheduleId, String patientId);
}
- 添加com.frx01.yygh.order.service.impl.OrderServiceImpl接口实现
@Service
public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderService {
@Override
public Long saveOrder(String scheduleId, String patientId) {
return null;
}
}
# 添加 Controller
@RestController
@RequestMapping("/api/order/orderInfo")
public class OrderApiController {
@Autowired
private OrderService orderService;
//生成挂号的订单
@PostMapping("/auth/submitOrder/{scheduleId}/{patientId}")
public Result saveOrders(@PathVariable String scheduleId,
@PathVariable String patientId){
Long orderId = orderService.saveOrder(scheduleId,patientId);
return Result.ok(orderId);
}
}
# 封装 Feign 调用获取就诊人接口
# 获取就诊人信息 api 接口
操作模块:service-user
在PatientApiController类添加方法
//根绝就诊人id获取就诊人信息
@GetMapping("/inner/get/{id}")
public Patient getPatientOrder(@PathVariable Long id){
Patient patient = patientService.getPatientId(id);
return patient;
}
# 搭建 service-user-client 模块
# 添加 Feign 接口类
@FeignClient(value = "service-user")
@Repository
public interface PatientFeignClient {
//根绝就诊人id获取就诊人信息
@GetMapping("/api/user/patient/inner/get/{id}")
public Patient getPatientOrder(@PathVariable("id") Long id);
}
# 封装Feign调用获取排班下单信息接口
# 获取排班下单信息api接口
# 添加 service 接口与实现
- 在 ScheduleService 类添加接口
//根据排班id获取预约下单数据
ScheduleOrderVo getScheduleOrderVo(String scheduleId);
- 在ScheduleServiceImpl类 添加实现
@Override
public ScheduleOrderVo getScheduleOrderVo(String scheduleId) {
ScheduleOrderVo scheduleOrderVo = new ScheduleOrderVo();
//获取排班的信息
Schedule schedule = baseMapper.selectById(scheduleId);
if(schedule == null){
throw new YyghException(ResultCodeEnum.PARAM_ERROR);
}
//获取预约规则信息
Hospital hospital = hospitalService.getByHosCode(schedule.getHoscode());
if(hospital==null){
throw new YyghException(ResultCodeEnum.PARAM_ERROR);
}
BookingRule bookingRule = hospital.getBookingRule();
if(bookingRule == null){
throw new YyghException(ResultCodeEnum.PARAM_ERROR);
}
//把获取数据设置到ScheduleOrderVo中去
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.setStopTime(stopTime.toDate());
return scheduleOrderVo;
}
# 添加 Controller 方法
@ApiOperation(value = "根据排班id获取预约下单数据")
@GetMapping("/inner/getScheduleOrderVo/{scheduleId}")
public ScheduleOrderVo getScheduleOrderVo(@PathVariable("scheduleId") String scheduleId){
return scheduleService.getScheduleOrderVo(scheduleId);
}
# 添加 Feign 接口类
@FeignClient(value = "service-hosp")
@Repository
public interface HospitalFeignClient {
/**
* 根据排班id获取预约下单数据
* @param scheduleId
* @return
*/
@ApiOperation(value = "根据排班id获取预约下单数据")
@GetMapping("/api/hosp/hospital/inner/getScheduleOrderVo/{scheduleId}")
ScheduleOrderVo getScheduleOrderVo(@PathVariable("scheduleId") String scheduleId);
}
# 获取下单引用签名信息接口
# 添加 service 接口以及实现类
- 在HospitalSetService类添加接口
//获取医院签名信息
SignInfoVo getSignInfoVo(SignInfoVo hoscode);
- 在 HospitalSetServiceImpl 类实现
//根据医院编号获取医院签名信息
@Override
public SignInfoVo getSignInfoVo(SignInfoVo hoscode) {
QueryWrapper<HospitalSet> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("hoscode",hoscode);
HospitalSet hospitalSet = baseMapper.selectOne(queryWrapper);
if(null == hospitalSet){
throw new YyghException(ResultCodeEnum.HOSPITAL_OPEN);
}
SignInfoVo signInfoVo = new SignInfoVo();
signInfoVo.setSignKey(hospitalSet.getSignKey());
signInfoVo.setApiUrl(hospitalSet.getApiUrl());
return signInfoVo;
}
# 添加 Controller 方法
@ApiOperation(value = "获取医院签名信息")
@GetMapping("/inner/getSignInfoVo/{hoscode}")
public SignInfoVo getSignInfoVo(@PathVariable("hoscode") SignInfoVo hoscode){
return hospitalSetService.getSignInfoVo(hoscode);
}
# 添加 Feign 接口类
/**
* 获取医院签名信息
* @param hoscode
* @return
*/
@ApiOperation(value = "获取医院签名信息")
@GetMapping("/api/hosp/hospital/inner/getSignInfoVo/{hoscode}")
SignInfoVo getSignInfoVo(@PathVariable("hoscode") String hoscode);
# 实现下单接口
操作模块:service-order
# 引入依赖
<dependencies>
<dependency>
<groupId>com.frx01</groupId>
<artifactId>service-cmn-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.frx01</groupId>
<artifactId>service-hosp-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.frx01</groupId>
<artifactId>service-user-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
# 封装下单工具类
封装HttpRequestHelper类,添加签名请求方法
@Slf4j
public class HttpRequestHelper {
/**
*
* @param paramMap
* @return
*/
public static Map<String, Object> switchMap(Map<String, String[]> paramMap) {
Map<String, Object> resultMap = new HashMap<>();
for (Map.Entry<String, String[]> param : paramMap.entrySet()) {
resultMap.put(param.getKey(), param.getValue()[0]);
}
return resultMap;
}
/**
* 请求数据获取签名
* @param paramMap
* @param signKey
* @return
*/
public static String getSign(Map<String, Object> paramMap, String signKey) {
if(paramMap.containsKey("sign")) {
paramMap.remove("sign");
}
TreeMap<String, Object> sorted = new TreeMap<>(paramMap);
StringBuilder str = new StringBuilder();
for (Map.Entry<String, Object> param : sorted.entrySet()) {
str.append(param.getValue()).append("|");
}
str.append(signKey);
log.info("加密前:" + str.toString());
String md5Str = MD5.encrypt(str.toString());
log.info("加密后:" + md5Str);
return md5Str;
}
/**
* 签名校验
* @param paramMap
* @param signKey
* @return
*/
public static boolean isSignEquals(Map<String, Object> paramMap, String signKey) {
String sign = (String)paramMap.get("sign");
String md5Str = getSign(paramMap, signKey);
if(!sign.equals(md5Str)) {
return false;
}
return true;
}
/**
* 获取时间戳
* @return
*/
public static long getTimestamp() {
return new Date().getTime();
}
/**
* 封装同步请求
*/
public static JSONObject sendRequest(Map<String, Object> paramMap, String url){
String result = "";
try {
//封装post参数
StringBuilder postdata = new StringBuilder();
for (Map.Entry<String, Object> param : paramMap.entrySet()) {
postdata.append(param.getKey()).append("=")
.append(param.getValue()).append("&");
}
log.info(String.format("--> 发送请求:post data %1s", postdata));
byte[] reqData = postdata.toString().getBytes("utf-8");
byte[] respdata = HttpUtil.doPost(url,reqData);
result = new String(respdata);
log.info(String.format("--> 应答结果:result data %1s", result));
} catch (Exception ex) {
ex.printStackTrace();
}
return JSONObject.parseObject(result);
}
}
# 预约成功后处理逻辑
预约成功后我们要更新预约数和短信提醒预约成功,为了提高下单的并发性,这部分逻辑我们就交给mq为我们完成,预约成功发送消息即可。
- RabbitMQ简介
以商品订单场景为例,
如果商品服务和订单服务是两个不同的微服务,在下单的过程中订单服务需要调用商品服务进行扣库存操作。按照传统的方式,下单过程要等到调用完毕之后才能返回下单成功,如果网络产生波动等原因使得商品服务扣库存延迟或者失败,会带来较差的用户体验,如果在高并发的场景下,这样的处理显然是不合适的,那怎么进行优化呢?这就需要消息队列登场了。
消息队列提供一个异步通信机制,消息的发送者不必一直等待到消息被成功处理才返回,而是立即返回。消息中间件负责处理网络通信,如果网络连接不可用,消息被暂存于队列当中,当网络畅通的时候在将消息转发给相应的应用程序或者服务,当然前提是这些服务订阅了该队列。如果在商品服务和订单服务之间使用消息中间件,既可以提高并发量,又降低服务之间的耦合度。
RabbitMQ就是这样一款消息队列。RabbitMQ是一个开源的消息代理的队列服务器,用来通过普通协议在完全不同的应用之间共享数据。
- 典型应用场景:
异步处理。把消息放入消息中间件中,等到需要的时候再去处理。
流量削峰。例如秒杀活动,在短时间内访问量急剧增加,使用消息队列,当消息队列满了就拒绝响应,跳转到错误页面,这样就可以使得系统不会因为超负载而崩溃。
日志处理
应用解耦
- 安装RabbitMQ
docker pull rabbitmq:management
docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management
管理后台:http://IP:15672
- 访问
# rabbit-util 模块封装
由于后续可能多个模块都会使用mq,所以我们把它封装成一个模块,需要的地方直接引用即可
# 修改 pom.xml
<dependencies>
<!--rabbitmq消息队列-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
# 封装 service 方法
@Service
public class RabbitService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送消息
* @param exchange 交换机
* @param rootingKey 路由键
* @param message 消息
* @return
*/
public boolean sendMessage(String exchange,String rootingKey,Object message){
rabbitTemplate.convertAndSend(exchange,rootingKey,message);
return true;
}
}
# 配置 mq 消息转换器
@Configuration
public class MQconfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
说明:默认是字符串转换器
# 封装短信接口
操作模块:service-msm
# 引入依赖
<dependency>
<groupId>com.frx01</groupId>
<artifactId>rabbit-util</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
# 添加配置
在resources/application.properties添加
#rabbitmq地址
spring.rabbitmq.host=192.168.91.166
spring.rabbitmq.port=15672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 添加常量配置
在rabbit-util模块com.frx01.yygh.common.constant.MqConst类添加
public class MqConst {
/**
* 预约下单
*/
public static final String EXCHANGE_DIRECT_ORDER
= "exchange.direct.order";
public static final String ROUTING_ORDER = "order";
//队列
public static final String QUEUE_ORDER = "queue.order";
/**
* 短信
*/
public static final String EXCHANGE_DIRECT_MSM = "exchange.direct.msm";
public static final String ROUTING_MSM_ITEM = "msm.item";
//队列
public static final String QUEUE_MSM_ITEM = "queue.msm.item";
}
# 在model模块封装短信实体
@Data
@ApiModel(description = "短信实体")
public class MsmVo {
@ApiModelProperty(value = "phone")
private String phone;
@ApiModelProperty(value = "短信模板code")
private String templateCode;
@ApiModelProperty(value = "短信模板参数")
private Map<String,Object> param;
}
说明:已统一引入
# 封装 service 接口
- 在 MsmService 类添加接口
boolean send(MsmVo msmVo);
- 在 MsmServiceImpl 类添加接口实现
//使用mq发送短信
@Override
public boolean send(MsmVo msmVo) {
if(StringUtils.isEmpty(msmVo.getPhone())){
String code = (String) msmVo.getParam().get("code");
boolean isSend = this.send(msmVo.getPhone(), code);
return isSend;
}
return false;
}
# 封装 mq 监听器
@Component
public class MsmReceiver {
@Autowired
private MsmService msmService;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = MqConst.QUEUE_MSM_ITEM,durable = "true"),
exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_MSM),
key = {MqConst.ROUTING_MSM_ITEM}
))
public void send(MsmVo msmVo, Message message , Channel channel){
msmService.send(msmVo);
}
}
# 封装更新排班数量
操作模块:service-hosp
# 引入依赖
<dependency>
<groupId>com.frx01</groupId>
<artifactId>rabbit-util</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
# 添加配置
在resources/application.properties添加
#rabbitmq地址
spring.rabbitmq.host=192.168.91.166
spring.rabbitmq.port=15672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 添加常量配置
在rabbit-util模块com.frx01.yygh.common.constant.MqConst类添加
/**
* 预约下单
*/
public static final String EXCHANGE_DIRECT_ORDER = "exchange.direct.order";
public static final String ROUTING_ORDER = "order";
//队列
public static final String QUEUE_ORDER = "queue.order";
# 在model模块封装更新排班实体
@Data
@ApiModel(description = "短信实体")
public class MsmVo {
@ApiModelProperty(value = "phone")
private String phone;
@ApiModelProperty(value = "短信模板code")
private String templateCode;
@ApiModelProperty(value = "短信模板参数")
private Map<String,Object> param;
}
说明:已统一引入,该对象放一个短信实体,预约下单成功后,我们发送一条消息,让mq来保证两个消息都发送成功。
# 封装 service 接口
- 在ScheduleService类添加接口
//更新排版数据
void update(Schedule schedule);
- 在ScheduleServiceImpl类添加接口实现
//更新排班信息,用于mq的操作
@Override
public void update(Schedule schedule) {
schedule.setUpdateTime(new Date());
scheduleRepository.save(schedule);
}
# 封装mq监听器
@Component
public class HospitalReceiver {
@Autowired
private ScheduleService scheduleService;
@Autowired
private RabbitService rabbitService;
@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 {
//下单成功更新预约数
Schedule schedule = scheduleService.getScheduleById(orderMqVo.getScheduleId());
schedule.setReservedNumber(orderMqVo.getReservedNumber());
schedule.setAvailableNumber(orderMqVo.getAvailableNumber());
scheduleService.update(schedule);
//发送短信
MsmVo msmVo = orderMqVo.getMsmVo();
if(null != msmVo) {
rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_MSM, MqConst.ROUTING_MSM_ITEM, msmVo);
}
}
}
# 调整下单接口
操作模块:service-order
# 引入依赖
<dependency>
<groupId>com.frx01</groupId>
<artifactId>service-order</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
# 添加配置
在resources/application.properties添加
#rabbitmq地址
spring.rabbitmq.host=192.168.91.166
spring.rabbitmq.port=15672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 修改下单接口
//生成挂号订单
@Override
public Long saveOrder(String scheduleId, Long patientId) {
//获取就诊人信息
Patient patient = patientFeignClient.getPatientOrder(patientId);
//获取排相关的信息
ScheduleOrderVo scheduleOrderVo = hospitalFeignClient.getScheduleOrderVo(scheduleId);
//判断当前是否还可以预约
if(new DateTime(scheduleOrderVo.getStartTime()).isAfterNow()
|| new DateTime(scheduleOrderVo.getEndTime()).isBeforeNow()){
throw new YyghException(ResultCodeEnum.TIME_NO);
}
//获取签名的信息
SignInfoVo signInfoVo = hospitalFeignClient.getSignInfoVo(scheduleOrderVo.getHoscode());
//添加到订单的表中
OrderInfo orderInfo = new OrderInfo();
//把 scheduleOrderVo 中的数据 复制到 orderInfo中去
BeanUtils.copyProperties(scheduleOrderVo,orderInfo);
// 向orderInfo设置其他的数据
String outTradeNo = System.currentTimeMillis() + ""+ new Random().nextInt(100);
orderInfo.setOutTradeNo(outTradeNo);
orderInfo.setScheduleId(scheduleId);
orderInfo.setUserId(patient.getUserId());
orderInfo.setPatientId(patientId);
orderInfo.setPatientName(patient.getName());
orderInfo.setPatientPhone(patient.getPhone());
orderInfo.setOrderStatus(OrderStatusEnum.UNPAID.getStatus());
this.save(orderInfo);
//调用医院的接口,实现预约挂号操作
//设置调用医院接口需要参数,参数放到map集合
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("hoscode",orderInfo.getHoscode());
paramMap.put("depcode",orderInfo.getDepcode());
paramMap.put("hosScheduleId",orderInfo.getScheduleId());
paramMap.put("reserveDate",new DateTime(orderInfo.getReserveDate()).toString("yyyy-MM-dd"));
paramMap.put("reserveTime", orderInfo.getReserveTime());
paramMap.put("amount",orderInfo.getAmount());
paramMap.put("name", patient.getName());
paramMap.put("certificatesType",patient.getCertificatesType());
paramMap.put("certificatesNo", patient.getCertificatesNo());
paramMap.put("sex",patient.getSex());
paramMap.put("birthdate", patient.getBirthdate());
paramMap.put("phone",patient.getPhone());
paramMap.put("isMarry", patient.getIsMarry());
paramMap.put("provinceCode",patient.getProvinceCode());
paramMap.put("cityCode", patient.getCityCode());
paramMap.put("districtCode",patient.getDistrictCode());
paramMap.put("address",patient.getAddress());
//联系人
paramMap.put("contactsName",patient.getContactsName());
paramMap.put("contactsCertificatesType", patient.getContactsCertificatesType());
paramMap.put("contactsCertificatesNo",patient.getContactsCertificatesNo());
paramMap.put("contactsPhone",patient.getContactsPhone());
paramMap.put("timestamp", HttpRequestHelper.getTimestamp());
String sign = HttpRequestHelper.getSign(paramMap, signInfoVo.getSignKey());
paramMap.put("sign", sign);
//请求医院系统接口
JSONObject result =
HttpRequestHelper.sendRequest(paramMap, signInfoVo.getApiUrl() + "/order/submitOrder");
if(result.getInteger("code")==200){
JSONObject jsonObject = result.getJSONObject("data");
//预约记录唯一标识(医院预约记录主键)
String hosRecordId = jsonObject.getString("hosRecordId");
//预约序号
Integer number = jsonObject.getInteger("number");;
//取号时间
String fetchTime = jsonObject.getString("fetchTime");;
//取号地址
String fetchAddress = jsonObject.getString("fetchAddress");;
//更新订单
orderInfo.setHosRecordId(hosRecordId);
orderInfo.setNumber(number);
orderInfo.setFetchTime(fetchTime);
orderInfo.setFetchAddress(fetchAddress);
baseMapper.updateById(orderInfo);
//排班可预约数
Integer reservedNumber = jsonObject.getInteger("reservedNumber");
//排班剩余预约数
Integer availableNumber = jsonObject.getInteger("availableNumber");
//发送mq消息,号源更新和短信通知
OrderMqVo orderMqVo = new OrderMqVo();
orderMqVo.setScheduleId(scheduleId);
orderMqVo.setReservedNumber(reservedNumber);
orderMqVo.setAvailableNumber(availableNumber);
//短息提示
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);
} else {
throw new YyghException(result.getString("message"),ResultCodeEnum.FAIL.getCode());
}
return orderInfo.getId();
}
- 前端测试访问
- 得到就诊人信息
- 得到排班信息
- 得到相关信息
- 调用医院接口
2022-11-13 08:48:29.151 INFO 7572 --- [nio-8206-exec-1] com.frx01.helper.HttpRequestHelper : --> 应答结果:result data {"code":200,"message":"成功","data":{"number":12,"fetchAddress":"一层114窗口","reservedNumber":33,"availableNumber":21,"resultCode":"0000","fetchTime":"2022-11-1409:00前","hosRecordId":11,"resultMsg":"预约成功"}}
2022-11-13 08:48:29.437 INFO 7572 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty : Flipping property: service-user.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2022-11-13 08:48:29.593 INFO 7572 --- [erListUpdater-1] c.netflix.config.ChainedDynamicProperty : Flipping property: service-hosp.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
# 订单详情功能
# 添加 servcie 接口以及实现类
- 在OrderInfoService类添加接口
//根据订单的id查询订单详情
OrderInfo getOrder(String orderId);
- 在OrderInfoServiceImpl类添加接口实现
//根据订单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;
}
# 添加 Controller
//根据订单的id查询订单详情
@GetMapping("/auth/getOrders/{orderId}")
public Result getOrders(@PathVariable String orderId){
OrderInfo orderInfo = orderService.getOrder(orderId);
return Result.ok(orderInfo);
}
- 前端测试访问
# 订单列表功能
# 添加 service 接口以及实现
- 在 OrderInfoSerice 接口及实现类
//订单列表(条件查询带分页)
IPage<OrderInfo> selectPage(Page<OrderInfo> pageParam, OrderQueryVo orderQueryVo);
- 在 OrderInfoServiceImpl 类添加接口实现
//订单列表(条件查询带分页)
@Override
public IPage<OrderInfo> selectPage(Page<OrderInfo> pageParam, OrderQueryVo orderQueryVo) {
//OrderQueryVo获取条件值
String name = orderQueryVo.getKeyword();//医院名称
String patientName = orderQueryVo.getPatientName();//就诊人的名称
String orderStatus = orderQueryVo.getOrderStatus();//订单的状态
String reserveDate = orderQueryVo.getReserveDate();//安排日期
String createTimeBegin = orderQueryVo.getCreateTimeBegin();//开始时间
String createTimeEnd = orderQueryVo.getCreateTimeEnd();//结束时间
//对条件值进行非空的判断
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
if(!StringUtils.isEmpty(name)){
queryWrapper.like("hosname",name);
}
if(!StringUtils.isEmpty(patientName)){
queryWrapper.eq("patient_name",patientName);
}
if(!StringUtils.isEmpty(orderStatus)){
queryWrapper.eq("order_status",orderStatus);
}
if(!StringUtils.isEmpty(reserveDate)){
queryWrapper.ge("reserce_date",reserveDate);
}
if(!StringUtils.isEmpty(createTimeBegin)){
queryWrapper.ge("create_time",createTimeBegin);
}
if(!StringUtils.isEmpty(createTimeEnd)){
queryWrapper.le("create_time",createTimeEnd);
}
//调用mapper的方法
Page<OrderInfo> pages = baseMapper.selectPage(pageParam, queryWrapper);
pages.getRecords().stream().forEach(item -> {
this.packOrderInfo(item);
});
return pages;
}
private OrderInfo packOrderInfo(OrderInfo orderInfo){
orderInfo.getParam().put("orderStatusString",OrderStatusEnum.getStatusNameByStatus(orderInfo.getOrderStatus()));
return orderInfo;
}
# 添加 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);
}
//获取订单状态
@GetMapping("/auth/getStatusList")
public Result getStatusList(){
return Result.ok(OrderStatusEnum.getStatusList());
}
说明:订单状态我们是封装到枚举中的,页面搜索需要一个下拉列表展示,所以我们通过接口返回页面
- 前端访问测试
# 订单支付(生成二维码)
# 微信支付介绍
# 微信扫码支付申请
微信扫码支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。
申请步骤:(了解)
第一步:注册公众号(类型须为:服务号)
请根据营业执照类型选择以下主体注册:个体工商户 (opens new window)| 企业/公司 (opens new window)| 政府 (opens new window)| 媒体 (opens new window)| 其他类型 (opens new window)。
第二步:认证公众号
公众号认证后才可申请微信支付,认证费:300元/年。
第三步:提交资料申请微信支付
登录公众平台,点击左侧菜单【微信支付】,开始填写资料等待审核,审核时间为1-5个工作日内。
第四步:开户成功,登录商户平台进行验证
资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。
第五步:在线签署协议
本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。
# 开发文档
微信支付接口调用的整体思路:
按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。
在线微信支付开发文档:
https://pay.weixin.qq.com/wiki/doc/api/index.html
- appid:微信公众账号或开放平台APP的唯一标识
- mch_id:商户号 (配置文件中的partner)
- partnerkey:商户密钥
- sign:数字签名, 根据微信官方提供的密钥和一套算法生成的一个加密信息, 就是为了保证交易的安全性
# 微信支付SDK
添加依赖
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
我们主要会用到微信支付SDK的以下功能
- 获取随机字符串
WXPayUtil.generateNonceStr()
- MAP转换为XML字符串(自动添加签名)
WXPayUtil.generateSignedXml(param, partnerkey)
- XML字符串转换为MAP
WXPayUtil.xmlToMap(result)
# 微信支付开发
# api 接口
场景:用户扫描商户展示在各种场景的二维码进行支付
使用案例:
线下:家乐福超市、7-11便利店、上品折扣线下店等
线上:大众点评网站、携程网站、唯品会、美丽说网站等
开发模式:
模式一:商户在后台给你生成二维码,用户打开扫一扫
模式二:商户后台系统调用微信支付【统一下单API (opens new window)】生成预付交易,将接口返回的链接生成二维码,用户扫码后输入密码完成支付交易。注意:**该模式的预付单有效期为2小时,**过期后无法支付。
微信支付:生成xml发送请求
操作模块:service-order
# 引入依赖
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
# 添加配置
在application.properties中添加商户信息
spring.redis.host=192.168.91.166
spring.redis.port=6379
spring.redis.database= 0
spring.redis.password=mima
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#关联的公众号appid
weixin.pay.appid=wx74862e0dfcf69954
#商户号
weixin.pay.partner=1558950191
#商户key
weixin.pay.partnerkey=T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
# 引入工具类
@Component
public class ConstantPropertiesUtils implements InitializingBean {
@Value("${weixin.appid}")
private String appid;
@Value("${weixin.partner}")
private String partner;
@Value("${weixin.partnerkey}")
private String partnerkey;
public static String APPID;
public static String PARTNER;
public static String PARTNERKEY;
@Override
public void afterPropertiesSet() throws Exception {
APPID = appid;
PARTNER = partner;
PARTNERKEY = partnerkey;
}
}
/**
* http请求客户端
*/
public class HttpClient {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
private boolean isCert = false;
//证书密码 微信商户号(mch_id)
private String certPassword;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
public boolean isCert() {
return isCert;
}
public void setCert(boolean cert) {
isCert = cert;
}
public String getXmlParam() {
return xmlParam;
}
public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}
public HttpClient(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}
public HttpClient(String url) {
this.url = url;
}
public String getCertPassword() {
return certPassword;
}
public void setCertPassword(String certPassword) {
this.certPassword = certPassword;
}
public void setParameter(Map<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
param.put(key, value);
}
public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}
public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}
public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst)
url.append("?");
else
url.append("&");
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}
/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet())
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}
private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
if(isCert) {
//TODO 需要完善
FileInputStream inputStream = new FileInputStream(new File(""));
KeyStore keystore = KeyStore.getInstance("PKCS12");
char[] partnerId2charArray = certPassword.toCharArray();
keystore.load(inputStream, partnerId2charArray);
SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(keystore, partnerId2charArray).build();
SSLConnectionSocketFactory sslsf =
new SSLConnectionSocketFactory(sslContext,
new String[] { "TLSv1" },
null,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
} else {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
}
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null)
statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}
public int getStatusCode() {
return statusCode;
}
public String getContent() throws ParseException, IOException {
return content;
}
}
# 添加交易记录接口
# 添加 Mapper
public interface PaymentMapper extends BaseMapper<PaymentInfo> {
}
# 添加service接口与实现
- 添加PaymentService 类
public interface PaymentService extends IService<PaymentInfo> {
//向支付记录表添加信息
void savePaymentInfo(OrderInfo orderInfo, Integer status);
}
- 添加PaymentServiceImpl实现类
@Service
public class PaymentServiceImpl extends ServiceImpl<PaymentMapper, PaymentInfo> implements PaymentService {
//向支付记录表添加信息
@Override
public void savePaymentInfo(OrderInfo orderInfo, Integer status) {
//根据订单id和支付类型,查询支付记录表里面是否存在相同的订单
QueryWrapper<PaymentInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_id",orderInfo.getId());
queryWrapper.eq("payment_type",status);
Integer count = baseMapper.selectCount(queryWrapper);
if(count>0){
return;
}
//添加记录
PaymentInfo paymentInfo = new PaymentInfo();
paymentInfo.setCreateTime(new Date());
paymentInfo.setOrderId(orderInfo.getId());
paymentInfo.setPaymentType(status);
paymentInfo.setOutTradeNo(orderInfo.getOutTradeNo());
paymentInfo.setPaymentStatus(PaymentStatusEnum.UNPAID.getStatus());
String subject = new DateTime(orderInfo.getReserveDate()).toString("yyyy-MM-dd")+"|"+orderInfo.getHosname()+"|"+orderInfo.getDepname()+"|"+orderInfo.getTitle();
paymentInfo.setSubject(subject);
paymentInfo.setTotalAmount(orderInfo.getAmount());
baseMapper.insert(paymentInfo);
}
}
# 添加支付service接口与实现
- 添加com.frx01.yygh.order.service.WeixinService类
public interface WeiXinService {
//生成微信支付扫描的二维码
Map createNative(Long orderId);
}
- 添加com.frx01.yygh.order.service.impl.WeixinServiceImpl类
@Service
public class WeiXinServiceImpl implements WeiXinService {
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@Autowired
private RedisTemplate redisTemplate;
//生成微信支付扫描的二维码
@Override
public Map createNative(Long orderId) {
//1.根据orderId获取订单信息
OrderInfo orderInfo = orderService.getById(orderId);
//2.向支付记录表添加信息
paymentService.savePaymentInfo(orderInfo, PaymentTypeEnum.WEIXIN.getStatus());
try {
//从redis获取数据
Map payMap = (Map)redisTemplate.opsForValue().get(orderId.toString());
if(payMap!=null){
return payMap;
}
//3.设置参数,调用微信生成二维码的接口
//把参数转换成xml格式,使用商户key进行加密
Map paramMap = new HashMap();
paramMap.put("appid", ConstantPropertiesUtils.APPID);
paramMap.put("mch_id", ConstantPropertiesUtils.PARTNER);
paramMap.put("nonce_str", WXPayUtil.generateNonceStr());
String body = orderInfo.getReserveDate() + "就诊" + orderInfo.getDepname();
paramMap.put("body", body);
paramMap.put("out_trade_no", orderInfo.getOutTradeNo());
//paramMap.put("total_fee", order.getAmount().multiply(new BigDecimal("100")).longValue()+"");
paramMap.put("total_fee", "1");
paramMap.put("spbill_create_ip", "127.0.0.1");
paramMap.put("notify_url", "http://guli.shop/api/order/weixinPay/weixinNotify");
paramMap.put("trade_type", "NATIVE");
//4.调用微信生成二维码的接口
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
//设置map参数
client.setXmlParam(WXPayUtil.generateSignedXml(paramMap, ConstantPropertiesUtils.PARTNERKEY));
client.setHttps(true);
client.post();
//5.返回相关的数据
String xml = client.getContent();
//转换成map集合
Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
System.out.println("resultMap=:" + resultMap);
//6.封装返回结果集
Map map = new HashMap<>();
map.put("orderId", orderId);
map.put("totalFee", orderInfo.getAmount());
map.put("resultCode", resultMap.get("result_code"));
map.put("codeUrl", resultMap.get("code_url"));//二维码地址
if(resultMap.get("result_code")!=null){
redisTemplate.opsForValue().set(orderId.toString(),map,120, TimeUnit.MINUTES);
}
return map;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
# 添加 Controller 方法
@Api(tags = "微信支付接口")
@RestController
@RequestMapping("/api/order/weixin")
public class WeiXinController {
@Autowired
private WeiXinService weiXinService;
//生成微信支付扫描的二维码
@GetMapping("/createNative/{orderId}")
public Result createNative(@PathVariable Long orderId){
Map map = weiXinService.createNative(orderId);
return Result.ok(map);
}
}
- 前端支付测试
# 处理支付结果
# 添加 service 接口以及实现
- 在PaymentService类添加接口
//更新订单状态
void paySucess(String out_trade_no, Map<String, String> resultMap);
- 在PaymentServiceImpl类添加实现
//更新订单状态
@Override
public void paySucess(String out_trade_no, Map<String, String> resultMap) {
//1.根据订单编号得到支付记录
QueryWrapper<PaymentInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("out_trade_no",out_trade_no);
queryWrapper.eq("payment_type", PaymentTypeEnum.WEIXIN.getStatus());
PaymentInfo paymentInfo = baseMapper.selectOne(queryWrapper);
//2.更新支付记录信息
paymentInfo.setPaymentStatus(PaymentStatusEnum.PAID.getStatus());
paymentInfo.setCallbackTime(new Date());
paymentInfo.setTradeNo(resultMap.get("transaction_id"));
paymentInfo.setCallbackContent(resultMap.toString());
baseMapper.updateById(paymentInfo);
//3.根据订单号得到订单信息
//4.更新订单信息
OrderInfo orderInfo = orderService.getById(paymentInfo.getOrderId());
orderInfo.setOrderStatus(OrderStatusEnum.PAID.getStatus());
orderService.updateById(orderInfo);
//5.调用医院接口,更新订单支付信息
}
# 更新医院支付状态
//5.调用医院接口,更新订单支付信息
SignInfoVo signInfoVo = hospitalFeignClient.getSignInfoVo(orderInfo.getHoscode());
Map<String,Object> reqMap = new HashMap<>();
reqMap.put("hoscode",orderInfo.getHoscode());
reqMap.put("hosRecordId",orderInfo.getHosRecordId());
reqMap.put("timestamp", HttpRequestHelper.getTimestamp());
String sign = HttpRequestHelper.getSign(reqMap, signInfoVo.getSignKey());
reqMap.put("sign", sign);
JSONObject result
= HttpRequestHelper.sendRequest(reqMap, signInfoVo.getApiUrl() + "/order/updatePayStatus");
- 前端访问测试
- 查看控制台
支付状态resultMap:{transaction_id=4200001670202211168362613899, nonce_str=mDMDU1ap2Lqlg9QR, trade_state=SUCCESS, bank_type=OTHERS, openid=oHwsHuL_t99-ri2ZsWbBVi4CNGXI, sign=6BB001DF5E713892D5D5142615D56718, return_msg=OK, fee_type=CNY, mch_id=1558950191, cash_fee=1, out_trade_no=16685284490991, cash_fee_type=CNY, appid=wx74862e0dfcf69954, total_fee=1, trade_state_desc=支付成功, trade_type=NATIVE, result_code=SUCCESS, attach=, time_end=20221116000748, is_subscribe=N, return_code=SUCCESS}
- 查看数据库
# 取消预约
# 需求描述
取消订单分两种情况:
- 未支付取消订单,直接通知医院更新取消预约状态
- 已支付取消订单,先退款给用户,然后通知医院更新取消预约状态
# 开发微信退款接口
该接口需要使用证书,详情参考文档并下载证书
# 配置证书
请下载的证书放在service-order模块/resources/cert文件夹下
在application.properties文件配置证书路径
#退款证书
weixin.cert=cert/apiclient_cert.p12
# 添加获取支付记录接口
退款我们是根据支付记录发起退款的
- 在PaymentService类添加接口
//获取支付记录
PaymentInfo getPaymentInfo(Long orderId,Integer paymentType);
- 在PaymentServiceImpl类添加实现
//获取支付记录
@Override
public PaymentInfo getPaymentInfo(Long orderId, Integer paymentType) {
QueryWrapper<PaymentInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_id",orderId);
queryWrapper.eq("payment_type",paymentType);
PaymentInfo paymentInfo = baseMapper.selectOne(queryWrapper);
return paymentInfo;
}
# 添加退款记录
# 添加 mapper
public interface RefundInfoMapper extends BaseMapper<RefundInfo> {
}
# 添加service接口与实现
- 添加service接口
public interface RefundInfoService extends IService<RefundInfo> {
//保存退款记录
RefundInfo saveRefundInfo(PaymentInfo paymentInfo);
}
- 添加service接口实现
@Service
public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundInfo> implements RefundInfoService {
//保存退款记录
@Override
public RefundInfo saveRefundInfo(PaymentInfo paymentInfo) {
//判断是否有重复数据添加
QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_id",paymentInfo.getOrderId());
queryWrapper.eq("payment_type",paymentInfo.getPaymentType());
RefundInfo refundInfo = baseMapper.selectOne(queryWrapper);
if(refundInfo != null){//有相同数据
return refundInfo;
}
//添加记录
refundInfo = new RefundInfo();
refundInfo.setCreateTime(new Date());
refundInfo.setOrderId(paymentInfo.getOrderId());
refundInfo.setPaymentType(paymentInfo.getPaymentType());
refundInfo.setOutTradeNo(paymentInfo.getOutTradeNo());
refundInfo.setRefundStatus(RefundStatusEnum.UNREFUND.getStatus());
refundInfo.setSubject(paymentInfo.getSubject());
//paymentInfo.setSubject("test");
refundInfo.setTotalAmount(paymentInfo.getTotalAmount());
baseMapper.insert(refundInfo);
return refundInfo;
}
}
# 添加微信退款接口
- 在WeixinService添加接口
//退款
Boolean refund(Long orderId);
- 在WeixinServiceImpl添加实现
//微信退款
@Override
public Boolean refund(Long orderId) {
try {
//获取支付记录相关信息
PaymentInfo paymentInfo = paymentService.getPaymentInfo(orderId, PaymentTypeEnum.WEIXIN.getStatus());
//添加信息到退款记录表
RefundInfo refundInfo = refundInfoService.saveRefundInfo(paymentInfo);
//判断当前订单数据是否已经退款
if(refundInfo.getRefundStatus().intValue()== RefundStatusEnum.REFUND.getStatus().intValue()){
return true;
}
//调用微信接口实现退款
//封装需要参数
Map<String,String> paramMap = new HashMap<>();
paramMap.put("appid",ConstantPropertiesUtils.APPID); //公众账号ID
paramMap.put("mch_id",ConstantPropertiesUtils.PARTNER); //商户编号
paramMap.put("nonce_str",WXPayUtil.generateNonceStr());
paramMap.put("transaction_id",paymentInfo.getTradeNo()); //微信订单号
paramMap.put("out_trade_no",paymentInfo.getOutTradeNo()); //商户订单编号
paramMap.put("out_refund_no","tk"+paymentInfo.getOutTradeNo()); //商户退款单号
//paramMap.put("total_fee",paymentInfoQuery.getTotalAmount().multiply(new BigDecimal("100")).longValue()+"");
//paramMap.put("refund_fee",paymentInfoQuery.getTotalAmount().multiply(new BigDecimal("100")).longValue()+"");
paramMap.put("total_fee","1");
paramMap.put("refund_fee","1");
String paramXml = WXPayUtil.generateSignedXml(paramMap,ConstantPropertiesUtils.PARTNERKEY);
//设置调用接口内容
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/secapi/pay/refund");
client.setXmlParam(paramXml);
client.setHttps(true);
//设置证书的信息
client.setCert(true);
client.setCertPassword(ConstantPropertiesUtils.PARTNER);
client.post();
//接受返回数据
String xml = client.getContent();
Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
if (null != resultMap && WXPayConstants.SUCCESS.equalsIgnoreCase(resultMap.get("result_code"))) {
refundInfo.setCallbackTime(new Date());
refundInfo.setTradeNo(resultMap.get("refund_id"));
refundInfo.setRefundStatus(RefundStatusEnum.REFUND.getStatus());
refundInfo.setCallbackContent(JSONObject.toJSONString(resultMap));
refundInfoService.updateById(refundInfo);
return true;
}
return false;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
# 完成取消预约
# 添加service接口与实现
- 在OrderService添加接口
//取消预约
boolean cancelOrder(Long orderId);
- 在OrderServiceImpl添加实现
//取消预约
@Override
public boolean cancelOrder(Long orderId) {
//先获取订单的信息
OrderInfo orderInfo = baseMapper.selectById(orderId);
//判断是否可以取消
DateTime quitTime = new DateTime(orderInfo.getQuitTime());
if(quitTime.isBeforeNow()){
throw new YyghException(ResultCodeEnum.CANCEL_ORDER_NO);
}
//调用医院接口实现预约取消
SignInfoVo signInfoVo = hospitalFeignClient.getSignInfoVo(orderInfo.getHoscode());
if(null == signInfoVo) {
throw new YyghException(ResultCodeEnum.PARAM_ERROR);
}
Map<String, Object> reqMap = new HashMap<>();
reqMap.put("hoscode",orderInfo.getHoscode());
reqMap.put("hosRecordId",orderInfo.getHosRecordId());
reqMap.put("timestamp", HttpRequestHelper.getTimestamp());
String sign = HttpRequestHelper.getSign(reqMap, signInfoVo.getSignKey());
reqMap.put("sign", sign);
JSONObject result = HttpRequestHelper.sendRequest(reqMap, "http://localhost:9998/order/updateCancelStatus");
//根据医院接口返回的数据
if(result.getInteger("code")!=200){
throw new YyghException(result.getString("message"),ResultCodeEnum.FAIL.getCode());
} else {
//判断当前的订单是否可以取消
if(orderInfo.getOrderStatus().intValue() == OrderStatusEnum.PAID.getStatus().intValue()){
Boolean isRefund = weiXinService.refund(orderId);
if(!isRefund){
throw new YyghException(ResultCodeEnum.CANCEL_ORDER_FAIL);
}
//更新订单的状态
orderInfo.setOrderStatus(OrderStatusEnum.CANCLE.getStatus());
baseMapper.updateById(orderInfo);
//发送mq更新预约数量
OrderMqVo orderMqVo = new OrderMqVo();
orderMqVo.setScheduleId(orderInfo.getScheduleId());
//短信提示
MsmVo msmVo = new MsmVo();
msmVo.setPhone(orderInfo.getPatientPhone());
msmVo.setTemplateCode("SMS_194640722");
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("reserveDate", reserveDate);
put("name", orderInfo.getPatientName());
}};
msmVo.setParam(param);
orderMqVo.setMsmVo(msmVo);
rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_ORDER, MqConst.ROUTING_ORDER, orderMqVo);
}
return true;
}
# 添加 Controller 方法
//取消预约
@GetMapping("/auth/cancelOrder/{orderId}")
public Result cancelOrder(@PathVariable Long orderId){
boolean isOrder = orderService.cancelOrder(orderId);
return Result.ok(isOrder);
}
相关文章
- Jgit的使用笔记
- 利用Github Action实现Tornadofx/JavaFx打包
- 叹息!GitHub Trending 即将成为历史!
- 微软软了?开源社区讨论炸锅,GitHub CEO 亲自来答
- GitHub Trending 列表频现重复项,前后端都没去重?
- Photoshop Elements 2021版本软件安装教程(mac+windows全版本都有)
- (ps全版本)Photoshop 2020的安装与破解教程(mac+windows全版本都有)
- (ps全版本)Photoshop cc2018的安装与破解教程(mac+windows全版本,包括2023
- 环境搭建:Oracle GoldenGate 大数据迁移到 Redshift/Flat file/Flume/Kafka测试流程
- 每个开发人员都要掌握的:最小 Linux 基础课
- 来撸羊毛了!Windows 环境下 Hexo 博客搭建,并部署到 GitHub Pages
- 超实用!手把手入门 MongoDB:这些坑点请一定远离
- 【GitHub日报】22-10-09 zustand、neovim、webtorrent、express 等4款App今日上新
- 【GitHub日报】22-10-10 brew、minio、vite、seaweedfs、dbeaver 等8款App今日上新
- 【GitHub日报】22-10-11 cobra、grafana、vue、ToolJet、redwood 等13款App今日上新
- Photoshop 2018 下载及安装教程(mac+windows全版本都有,包括最新的2023)
- Photoshop 2017 下载及安装教程(mac+windows全版本都有,包括最新的2023)
- Photoshop 2020 下载及安装教程(mac+windows全版本都有,包括最新的2023)
- Photoshop 2023 资源免费下载(mac+windows全版本都有,包括最新的2023)
- 最新版本Photoshop CC2018软件安装教程(mac+windows全版本都有,包括2023