• 22-08-18 西安 尚医通(07)支付流程、rabbitmq的使用


    这一天以来真的是有种劫后余生的感觉,体会到了为什么电视剧里那些人在经历一些事情之后,点根烟连打火机都点不着的感觉了,就是还没缓过来!


     挂号流程

    大体流程是这样的

    1、用户系统选中某一个科室下的某个排班,点击剩余按钮,跳转到一个页面然后选择就诊人,点击确认挂号。

    2、现在状态就变为预约成功,未支付状态。我们点击支付,之后就是扫描二维码付款了。


    1、确认挂号(创建订单)

    rabbitmq的使用

    mq在这里充当了异步调用的作用,可以提高下单的响应速度。

    1.1 调用医院端 的程序在医院端保存一条订单和在医院端扣减号源。

    1. //使用httpclient发送请求,请求医院接口
    2. //医院端 1.扣减号源 2.保存医院端自己的订单
    3. JSONObject result =
    4. HttpRequestHelper.sendRequest(paramMap, "http://localhost:9998/order/submitOrder");

    1.2 在我们的平台端也保存一条我们自己的订单,之后发送消息到mq,异步处理。

    1.3 mq在本项目的异步处理的体现:

     平台端的医院服务更新mongodb中的号源(数据来自医院端的返回)

     平台端医院服务再发送消息到mq,短信服务消费这些消息发送短信通知用户挂号成功。

    1. OrderMqVo orderMqVo = new OrderMqVo();
    2. //这2个数量都是医院端返给我们平台端的
    3. orderMqVo.setReservedNumber(reservedNumber);//总的号源数量
    4. orderMqVo.setAvailableNumber(availableNumber);//剩余可预约数量
    5. orderMqVo.setScheduleId(scheduleId);//mongondb中排班id
    6. MsmVo msmVo = new MsmVo();
    7. msmVo.setPhone(orderInfo.getPatientPhone());
    8. Map map = new HashMap(){{
    9. put("message","请于"+orderInfo.getReserveDate()+"到"
    10. +orderInfo.getHosname()+orderInfo.getDepname()+"就诊");
    11. }};
    12. msmVo.setParam(map);
    13. orderMqVo.setMsmVo(msmVo);
    14. //发送消息到mq,异步处理
    15. rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_ORDER,MqConst.ROUTING_ORDER,orderMqVo);

    发送短信的话,我们模拟一下,在控制台输出。上一篇我们已经讲过了如何调用阿里云发送短信,在短信服务控制台中输出如下:

    2、点击支付(生成二维码)

    在线微信支付开发文档:

    https://pay.weixin.qq.com/wiki/doc/api/index.html

    --------------------------------------

    生成二维码

    使用Redis判断5分钟内是否生成过二维码

    1. Map m = (Map)redisTemplate.opsForValue().get(orderId.toString());
    2. //防止5分钟之内重复向微信端发请求获取支付链接
    3. if(m!=null){
    4. return m;
    5. }

    1、创建支付记录,处于未支付状态

    2、向微信端统一下单 ” 发请求,拿到二维码连接,并把map存储到Redis

    paramMap参数的设置就多了去了,,, 

    1. //2、HTTPClient来根据URL访问第三方接口并且传递参数
    2. HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
    3. //client设置参数
    4. client.setXmlParam(WXPayUtil.generateSignedXml(paramMap, ConstantPropertiesUtils.PARTNERKEY));
    5. client.setHttps(true);
    6. client.post();
    7. //3、微信端返回数据xml格式数据
    8. String xml = client.getContent();
    9. Map result = WXPayUtil.xmlToMap(xml);
    10. String return_code = result.get("return_code");
    11. String result_code = result.get("result_code");
    12. if (return_code.equals("SUCCESS") && result_code.equals("SUCCESS")) {
    13. Map map = new HashMap();
    14. map.put("orderId", orderId);
    15. map.put("totalFee", order.getAmount());
    16. map.put("resultCode", result.get("result_code"));//返回状态码 SUCCESS/FAIL
    17. map.put("codeUrl", result.get("code_url"));//二维码链接
    18. redisTemplate.opsForValue().set(orderId.toString(),map,5, TimeUnit.MINUTES);
    19. return map;
    20. }

    ----------------------------------------------

    查询支付状态

    打开二维码后,前端每隔3秒去调用查询支付状态接口

    1、查询微信端“查询订单”接口

    代码如下,没啥可说的。写法很固定!

    代码里面有很多工具类,比如发送请求的、map和xml格式互转的。

    1. //根据订单号去微信第三方查询支付状态
    2. @Override
    3. public Map queryPayStatus(Long orderId) {
    4. OrderInfo orderInfo = orderService.getById(orderId);
    5. //1、封装参数
    6. Map paramMap = new HashMap<>();
    7. paramMap.put("appid", ConstantPropertiesUtils.APPID);
    8. paramMap.put("mch_id", ConstantPropertiesUtils.PARTNER);//商户号
    9. paramMap.put("out_trade_no", orderInfo.getOutTradeNo());//订单的out_trade_no
    10. paramMap.put("nonce_str", WXPayUtil.generateNonceStr());//随机字符串
    11. try {
    12. //2、设置请求,调用微信端“查询订单”接口
    13. HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
    14. //微信端都是xml作为参数和返回值
    15. //generateSignedXml会自动添加签名
    16. client.setXmlParam(WXPayUtil
    17. .generateSignedXml(paramMap, ConstantPropertiesUtils.PARTNERKEY));//商户号签名
    18. client.setHttps(true); //是否支持指定协议
    19. client.post();
    20. //3、获取返回值,转成Map
    21. String xml = client.getContent();
    22. Map resultMap = WXPayUtil.xmlToMap(xml);
    23. //4、返回
    24. return resultMap;
    25. } catch (Exception e) {
    26. e.printStackTrace();
    27. return null;
    28. }
    29. }

    2、支付成功,需要做的事情有修改订单的状态为已支付,修改或填充支付记录中的4个字段

    比较重要的就是 trade_no,这个是微信端返回给我们的支付流水号。另外一个out_trade_no是我们订单表中自己的交易记录号,out_trade_no会伴随着整个支付流程

     paymentInfo.setTradeNo(paramMap.get("transaction_id"))


    3、取消预约 (取消订单)

    取消预约 有2种情况

    (1)未支付取消订单,直接通知医院更新取消预约状态

    (2)已支付取消订单,先退款给用户,然后通知医院更新取消预约状态

    取消预约需要配置证书

    拷贝证书到c盘下:

    在配置文件中:

    weixin.cert=C:\\apiclient_cert.p12 

    1、根据支付记录创建退款记录

    2、调用微信端接口“申请退款”

    1. String paramXml = WXPayUtil.generateSignedXml(paramMap,ConstantPropertiesUtils.PARTNERKEY);
    2. //调用微信接口,进行退款
    3. HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/secapi/pay/refund");
    4. client.setXmlParam(paramXml);
    5. client.setHttps(true);
    6. client.setCert(true);
    7. client.setCertPassword(ConstantPropertiesUtils.PARTNER);//商户id
    8. client.post();
    9. //返回第三方的数据
    10. String xml = client.getContent();
    11. Map resultMap = WXPayUtil.xmlToMap(xml);
    12. //暂且认为是退款成功,实际要调用退款查询接口
    13. if (null != resultMap && WXPayConstants.SUCCESS.equalsIgnoreCase(resultMap.get("result_code"))) {
    14. //调用微信端接口 查询退款状态
    15. refundInfo.setCallbackTime(new Date());
    16. refundInfo.setTradeNo(resultMap.get("refund_id"));//微信退款单号
    17. refundInfo.setRefundStatus(RefundStatusEnum.REFUND.getStatus());//已退款
    18. refundInfo.setCallbackContent(JSONObject.toJSONString(resultMap));
    19. refundInfoService.updateById(refundInfo);
    20. return true;
    21. }
    22. return false;

    3、查询退款成功则修改第一步创建的退款记录,并把平台端的订单状态也要改为-1

    4、mq异步实现

    • 平台端号源数量加1,(相比创建订单少发俩个参数)
    • 通知就诊人退号成功
    1. //4.向mq发送消息,完成mongondb中号源数量+1,短信通知就诊人
    2. OrderMqVo orderMqVo = new OrderMqVo();
    3. orderMqVo.setScheduleId(orderInfo.getScheduleId());
    4. MsmVo msmVo =new MsmVo();
    5. msmVo.setPhone(orderInfo.getPatientPhone());
    6. Map param=new HashMap<>();
    7. param.put("message","退号成功");
    8. msmVo.setParam(param);
    9. orderMqVo.setMsmVo(msmVo);
    10. rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_ORDER,MqConst.ROUTING_ORDER,msmVo);

    就医提醒【定时任务、mq实现】

    请夸我灵魂画手,低调低调哈哈

    1、在定时任务模块发送消息到task队列

    这个消息可以是空的,本来就是起一个提示的作用。

    要注意的点就是 @Scheduled(cron = "0/50 * * * * ?") //等步增长

    1. /**
    2. * 每天8点执行 提醒就诊
    3. */
    4. //@Scheduled(cron = "0 0 8 * * ?")
    5. @Scheduled(cron = "0/5 * * * * ?") //等步增长
    6. public void task() {
    7. System.out.println(new Date().toLocaleString());
    8. rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK, MqConst.ROUTING_TASK_8, "");
    9. }

    RabbitService是我们自己封装好的一个操作rabbitmq的服务

    1. @Component
    2. public class RabbitService {
    3. @Autowired
    4. private RabbitTemplate rabbitTemplate;
    5. /**
    6. * 发送消息
    7. * @param exchange 交换机
    8. * @param routingKey 路由键
    9. * @param message 消息
    10. */
    11. public boolean sendMessage(String exchange, String routingKey, Object message) {
    12. rabbitTemplate.convertAndSend(exchange, routingKey, message);
    13. return true;
    14. }
    15. }

    2、订单服务去消费“task队列”的消息

    2.1、去查询当天的就诊人,实际上是查询 就诊日期是当天的订单,

    2.2、然后在订单服务遍历当天的这些订单,给“msm队列”里发送消息。

    1. public void patientTips() {
    2. //1、查询当天已支付所有的订单
    3. QueryWrapper queryWrapper = new QueryWrapper<>();
    4. queryWrapper.eq("reserve_date", new DateTime().toString("yyyy-MM-dd"));
    5. queryWrapper.eq("order_status",1);
    6. List orderInfos = baseMapper.selectList(queryWrapper);
    7. //2、循环遍历发送消息到mq
    8. for (OrderInfo orderInfo : orderInfos) {
    9. //短信提示
    10. MsmVo msmVo = new MsmVo();
    11. //就诊人手机号
    12. msmVo.setPhone(orderInfo.getPatientPhone());
    13. //短信内容
    14. Map param = new HashMap(){{
    15. put("message", orderInfo.getPatientName()+"请于今天就诊");
    16. }};
    17. msmVo.setParam(param);
    18. //发送消息到msm队列
    19. rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_MSM, MqConst.ROUTING_MSM_ITEM, msmVo);
    20. }
    21. }

    3、短信服务 去消费“msm队列”的消息

    1. @RabbitListener(bindings = @QueueBinding(
    2. value = @Queue(value = MqConst.QUEUE_MSM_ITEM, durable = "true"),
    3. exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_MSM),
    4. key = {MqConst.ROUTING_MSM_ITEM}
    5. ))
    6. public void send(MsmVo msmVo) {
    7. Map param = msmVo.getParam();
    8. System.out.println("[尚医通]: "+param.get("message"));
    9. }

    控制台打印如下:当然你还可以让提示更加丰富,我这里就只取了“就诊人的名字”


    预约统计 Echart

    在实际的生产环境中,有很多种各式统计,数据来源于各个服务模块,我们得有一个统计模块来专门管理。

    ECharts是百度的一个项目,后来百度把Echart捐给apache,用于图表展示,提供了常规的折线图柱状图散点图饼图K线图,用于统计的盒形图,用于地理数据可视化的地图热力图线图,用于关系数据可视化的关系图treemap旭日图,多维数据可视化的平行坐标,还有用于 BI 的漏斗图仪表盘,并且支持图与图之间的混搭。

    官方网站:https://echarts.apache.org/zh/index.html

    需求:统计每天平台预约数据(即订单数量)

    实际上返回给前端的就是横坐标集合和纵坐标集合。横坐标表示日期,纵坐标表示预约数量

    项目中安装 echarts组件   
    npm install --save echarts@4.1.0
    大体流程

    我们要做的事,对应到mysql,差不多就是这样

    1. SELECT `reserve_date`,COUNT(0) AS 'count' FROM `order_info`
    2. GROUP BY `reserve_date` ORDER BY `reserve_date`

    核心代码就是

    1、调用mapper接口查询

    1. <select id="selectOrderCount" resultType="com.atguigu.yygh.vo.order.OrderCountVo">
    2. select reserve_date as reserveDate, count(reserve_date) as count
    3. from order_info
    4. <where>
    5. <if test="hosname != null and hosname != ''">
    6. and hosname like CONCAT('%',#{hosname},'%')
    7. if>
    8. <if test="reserveDateBegin != null and reserveDateBegin != ''">
    9. and reserve_date >= #{reserveDateBegin}
    10. if>
    11. <if test="reserveDateEnd != null and reserveDateEnd != ''">
    12. and reserve_date <= #{reserveDateEnd}
    13. if>
    14. and is_deleted = 0
    15. where>
    16. group by reserve_date
    17. order by reserve_date
    18. select>

    2、把查询的结果封装横坐标集合和纵坐标集合

    1. //获取订单统计数据
    2. @Override
    3. public Map getCountMap(OrderCountQueryVo orderCountQueryVo) {
    4. List orderCountVos = orderInfoMapper.selectOrderCount(orderCountQueryVo);
    5. List dateList= orderCountVos.stream()
    6. .map(OrderCountVo::getReserveDate).collect(Collectors.toList());
    7. List countList= orderCountVos.stream()
    8. .map(OrderCountVo::getCount).collect(Collectors.toList());
    9. HashMap map = new HashMap<>();
    10. //给前端返回横坐标集合(日期)和纵坐标集合(预约数量)
    11. map.put("dateList",dateList);
    12. map.put("countList",countList);
    13. return map;
    14. }

    最后,就可以在页面看到好看的效果了,当然省略了前端的代码


    项目总结

    1、项目流程图


    2、项目功能

    医院端

    医院设置管理

    医院管理

    数据字典

    用户管理

     统计管理 

  • 相关阅读:
    响应格式规范
    archlinux ssdm的使用
    Win11右键菜单怎么改为Win10?
    【MyBatis-Plus】简介 入门案例
    2024年CSP-J暑假冲刺训练营(1):枚举
    金仓数据库 MySQL 至 KingbaseES 迁移最佳实践(2. 概述)
    Go RabbitMQ简介 使用
    PCL点云库 点云拼接
    Sora引发安全新挑战
    C++实现有理数类 四则运算和输入输出
  • 原文地址:https://blog.csdn.net/m0_56799642/article/details/126409716