简介

Ohrb(Online hospital registration booking)即为某线上医院预约挂号系统,通过手机验证码登录或者微信登录实现,患者(用户)点击到相关的医院,选择相应的日期,可可实现简单的医院预约挂号操作。
系统的基本功能:

  • 实现手机验证码或微信登录
  • 实现微信支付/支付宝支付
  • 实现用户实名信息上传认证
  • 实现预约挂号订单短信通知

具体的功能页面如下图1.1、1.2所示,这是系统首页,展示医院、科室、医师以及挂号信息。
Ohrb项目介绍 - 图1
图1.1 首页界面图
Screenshot 2022-05-19 131529.png
图1.2 首页轮播界面图
用户点击登录/注册即即可实现登录/注册同时实现,用户可使用验证码或者微信登录进行登录,具体如图1.3所示。
Ohrb项目介绍 - 图3
图1.3 患者(用户)登录界面图
用户(患者)可在相应的医院例如,北京协和医院,选择多发性硬化专科,5月20日的号,点击选择,添加相应的就诊人。同时需要立刻支付,支付在稍后需要立刻支付。具体如图1.4所示。
Screenshot 2022-05-19 144523.png
图1.4 患者进行预约挂号操作
用户需要立刻支付,使用微信支付,扫描二维码,支付费用成功即为挂号成功。同时系统会将信息存储,以供管理端使用。具体如图1.5所示。
Ohrb项目介绍 - 图5
图1.5 支付成功,预约挂号成功
同时,用户手机端会收到验证码。具体如图1.6所示。
验证码.jpg
图1.6 手机收到验证码
同时系统还有管理端,用户可管理对医院、用户、订单上架、修改、删除等操作。如图1.7所示。
Screenshot 2022-05-19 155518.png
图1.7 管理端订单管理界面

技术点实现

系统主要使用Java语言、Spring框架、MySQL、MongoDB、Mybatis-plus、Vue、Redis等开发,其中登录、短信、文件上传使用阿里云等云服务。前端部分使用Vue开发。

  • Spring Cloud
  • Nacos
  • RabbitMQ
  • MySQL
  • Mybatis-plus
  • MongoDB
  • Redis
  • 联容云短信/腾讯云短信
  • 阿里云OSS
  • 微信登录/微信支付
  • 支付宝沙箱支付
  • Vue/Nuxt

    系统架构图

    Nginx作为系统的访问入口,对外部网络暴露,用户使用浏览器访问系统。在本系统中使用SpringCloud框架对各个服务模块进行管理,并且各个服务模块交由Nacos进行统一注册配置。JWT与Spring Cloud Gateway作为API路由网关并对服务进行统一认证;请求经过Nginx后,再经过服务网关,进入到Fegin Client服务集群。Fegin组件中使用了服务限流熔断Sentiel和负载均衡组件Ribbon。其中文件上传服务需要通过OSS实现,任务管理器对订单服务以及短信服务进行定时调用。Redis集群、MySQL数据库集群、MongoDB集群作为数据层提供数据源支持,而RabbitMQ集群作为消息交换器交换不同服务之间的消息队列。具体如下图1.8所示。
    Ohrb项目介绍 - 图8
    图1.8 系统架构图

    Spring Cloud 版本配置

    | ring Cloud Version | Spring Cloud Alibaba Version | Spring Boot Version | | —- | —- | —- | | Spring Cloud 2020.0.0 | 2021.1 | 2.4.2 | | Spring Cloud Hoxton.SR9 | 2.2.6.RELEASE | 2.3.2.RELEASE | | Spring Cloud Greenwich.SR6 | 2.1.4.RELEASE | 2.1.13.RELEASE | | Spring Cloud Hoxton.SR3 | 2.2.1.RELEASE | 2.2.5.RELEASE | | Spring Cloud Hoxton.RELEASE | 2.2.0.RELEASE | 2.2.X.RELEASE | | Spring Cloud Greenwich | 2.1.2.RELEASE | 2.1.X.RELEASE | | Spring Cloud Finchley | 2.0.4.RELEASE(停止维护,建议升级) | 2.0.X.RELEASE | | Spring Cloud Edgware | 1.5.1.RELEASE(停止维护,建议升级) | 1.5.X.RELEASE |

表1.1 Spring Cloud版本配置

Nacos

image.png

E-R图

image.png
图1.9 E-R图
通过对数据字典、医院设置、科室排班等字段添加索引,实现数据库等数据快速查询,实现一定的性能优化。

短信调用

短信使用云通讯的服务,申请后,得到AppID秘钥,在yaml文件中进行配置,根据SDK,进行开发。具体如代码所示。

  1. private CCPRestSmsSDK getSdk()
  2. {
  3. CCPRestSmsSDK sdk = new CCPRestSmsSDK();
  4. sdk.init(lryConfig.getServerIp(), lryConfig.getServerPort());
  5. sdk.setAccount(lryConfig.getAccountSId(), lryConfig.getAccountToken());
  6. sdk.setAppId(lryConfig.getAppId());
  7. sdk.setBodyType(BodyType.Type_JSON);
  8. return sdk;
  9. }
  10. @Override
  11. public boolean send(String phone)
  12. {
  13. //判断手机号是否为空
  14. return !StringUtils.isEmpty(phone);
  15. }
  16. /**
  17. *
  18. * @param phone 手机号
  19. * @param codes 内容数据,用于替换模板中{序号}
  20. * @return
  21. */
  22. public boolean sendTemplate(String phone, String[] codes)
  23. {
  24. HashMap<String, Object> result = this.getSdk().sendTemplateSMS(phone, lryConfig.getTemplateId(), codes);
  25. log.info("SDKTestGetSubAccounts result=" + result);
  26. if ("000000".equals(result.get("statusCode"))) {
  27. //正常返回输出data包体信息(map) 打印出来看看
  28. HashMap<String, Object> data = (HashMap<String, Object>) result.get("data");
  29. Set<String> keySet = data.keySet();
  30. for (String key : keySet) {
  31. Object object = data.get(key);
  32. System.out.println(key + " = " + object);
  33. }
  34. } else {
  35. //异常返回输出错误码和错误信息
  36. System.out.println("错误码=" + result.get("statusCode") + " 错误信息= " + result.get("statusMsg"));
  37. return false;
  38. }
  39. return true;
  40. }
  41. @Override
  42. public boolean send(String phone, String code)
  43. {
  44. String[] codes = {code, "2"};
  45. return sendTemplate(phone, codes);
  46. }
  47. @Override
  48. public boolean send(MsmVo msmVo)
  49. {
  50. String name = (String) msmVo.getParam().get("name");
  51. String date = (String) msmVo.getParam().get("reserveDate");
  52. //手机号不为空且就医提醒不为空,走提醒就医的短信模板!
  53. if (!StringUtils.isEmpty(msmVo.getPhone()) && !StringUtils.isEmpty((CharSequence) msmVo.getParam().get("jiuyitixing"))) {
  54. String code1 = Sms.SUCCESS + name;
  55. String code2 = Sms.DATE + date;
  56. String[] codes = {code1, code2};
  57. return this.sendTemplate(msmVo.getPhone(), codes);
  58. }
  59. //手机号不为空,进行发送,这个走的是预约订单成功的短信模板
  60. if (!StringUtils.isEmpty(msmVo.getPhone())) {
  61. String code1 = Sms.INFO + name;
  62. String code2 = Sms.DATE + date;
  63. String[] codes = {code1, code2};
  64. return this.sendTemplate(msmVo.getPhone(), codes);
  65. }
  66. return false;
  67. }

文件上传

使用阿里云OSS,实现文件上传的功能。具体实现如代码所示。

@Service
public class FileServiceImpl implements FileService
{
    /**
     * @param file 文件
     * @return
     */
    @Override
    public String upload(MultipartFile file)
    {
        String endpoint = ConstantOssPropertiesUtils.EDNPOINT;
        String accessKeyId = ConstantOssPropertiesUtils.ACCESS_KEY_ID;
        String accessKeySecret = ConstantOssPropertiesUtils.SECRECT;
        String bucketName = ConstantOssPropertiesUtils.BUCKET;


        try {
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
            // 上传文件流。
            InputStream inputStream = file.getInputStream();
            String fileName = file.getOriginalFilename();
            //生成随机唯一值,使用uuid,添加到文件名称里面
            String uuid = UUID.randomUUID().toString().replaceAll("-", "");
            fileName = uuid + fileName;

            //按照当前日期,创建文件夹,上传到创建文件夹里面
            //  2022/04/02/01.jpg
            String timeUrl = new DateTime().toString("yyyy/MM/dd");
            fileName = timeUrl + "/" + fileName;

            //调用方法实现上传
            // 1.jpg    /a/b/1.jpg
            ossClient.putObject(bucketName, fileName, inputStream);
            // 关闭OSSClient。
            ossClient.shutdown();
            //上传之后文件路径
            // https://yygh-zhoujk.oss-cn-beijing.aliyuncs.com/01.jpg
            String url = "https://" + bucketName + "." + endpoint + "/" + fileName;
            //返回
            return url;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

支付

调用支付宝支付API,实现系统的支付功能。具体实现如代码所示。

package com.zhoujk.yygh.order.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradePrecreateModel;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.request.AlipayTradePrecreateRequest;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.response.AlipayTradePrecreateResponse;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.zhoujk.yygh.hosp.enums.PaymentTypeEnum;
import com.zhoujk.yygh.hosp.enums.RefundStatusEnum;
import com.zhoujk.yygh.hosp.model.order.OrderInfo;
import com.zhoujk.yygh.hosp.model.order.PaymentInfo;
import com.zhoujk.yygh.hosp.model.order.RefundInfo;
import com.zhoujk.yygh.order.service.AlipayService;
import com.zhoujk.yygh.order.service.OrderService;
import com.zhoujk.yygh.order.service.PaymentService;
import com.zhoujk.yygh.order.service.RefundInfoService;
import com.zhoujk.yygh.order.util.AlipayConfig;
import com.zhoujk.yygh.order.util.ParamsUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author : zhoujiankang
 * @Desc:
 * @since : 2022/5/7 21:48
 */
@Slf4j
@Service
public class AlipayServiceImpl implements AlipayService {

    Logger logger = LoggerFactory.getLogger(AlipayServiceImpl.class);

    @Autowired private OrderService orderService;

    @Autowired private PaymentService paymentService;

    @Autowired private RedisTemplate redisTemplate;

    @Autowired private RefundInfoService refundInfoService;


    /**
     * 生成微信支付二维码
     *
     * @param orderId
     * @return
     */
    @Override
    public Map createNative(Long orderId) {
        //从redis获取数据
        Map payMap = (Map) redisTemplate.opsForValue().get(orderId.toString());
        if (payMap != null) {
            return payMap;
        }
        //1 根据orderId获取订单信息
        OrderInfo order = orderService.getById(orderId);
        //2 向支付记录表添加信息
        paymentService.savePaymentInfo(order, PaymentTypeEnum.ALIPAY.getStatus());
        //3设置参数
        String params = order.getReserveDate() + "就诊" + order.getDepname();

        //4.获取请求客户端
        AlipayClient alipayClient = getAlipayClient();

        //5.设置交易预创建支付参数模型
        AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
        model.setBody(params);
        model.setTotalAmount(String.valueOf(order.getAmount()));
        model.setOutTradeNo(order.getOutTradeNo());
        model.setSubject("支付测试项目");

        //6.获取请求对象
        AlipayTradePrecreateRequest alipayRequest = new AlipayTradePrecreateRequest();
        //7.设置请求参数
        alipayRequest.setBizModel(model);
        alipayRequest.setNotifyUrl(AlipayConfig.NOTIFY_URL);
        alipayRequest.setReturnUrl(AlipayConfig.RETURN_URL);
        AlipayTradePrecreateResponse alipayResponse = null;
        try {
            alipayResponse = alipayClient.execute(alipayRequest);
        } catch (AlipayApiException e) {
            e.printStackTrace();
            return null;
        }
        //TODO 如果测试正常,就注释下面两行
        String body = alipayResponse.getBody();
        logger.info("请求的响应二维码信息====>" + body);
        Map<String, Object> map = new HashMap<>();
        if (alipayResponse.isSuccess()) {

            map.put("orderId", orderId);
            map.put("totalFee", order.getAmount());
            /**
             *   在微信支付返回的resultCode的值为SUCCESS或FAIL
             */
            map.put("resultCode", "SUCCESS");
            //二维码地址
            map.put("codeUrl", alipayResponse.getQrCode());

            redisTemplate.opsForValue().set(orderId.toString(), map, 120, TimeUnit.MINUTES);
            return map;
        } else {
            //  失败则为空
            return null;
        }
    }

    //调用支付宝接口实现支付状态查询
    @Override
    public Map<String, String> queryPayStatus(Long orderId) {
        //1 根据orderId获取订单信息
        OrderInfo orderInfo = orderService.getById(orderId);

        //2.获取客户端
        AlipayClient alipayClient = getAlipayClient();


        //3.设置交易查询参数
        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
        JSONObject bizContent = new JSONObject();
        bizContent.put("out_trade_no", orderInfo.getOutTradeNo());
        request.setBizContent(bizContent.toJSONString());
        request.setNotifyUrl(AlipayConfig.NOTIFY_URL);
        request.setReturnUrl(AlipayConfig.RETURN_URL);
        AlipayTradeQueryResponse alipayResponse = null;
        try {
            alipayResponse = alipayClient.execute(request);
        } catch (AlipayApiException e) {
            e.printStackTrace();
            return null;
        }

        if (alipayResponse.isSuccess()) {
            Map<String, String> resultMap = new HashMap<>();
            resultMap.put("trade_state", "SUCCESS");
            resultMap.put("out_trade_no", alipayResponse.getOutTradeNo());
            return resultMap;
        } else {
            //失败返回空
            return null;
        }
    }

    /**
     * 退款
     *
     * @param orderId 订单id
     * @return Y?N退款是否成功
     */
    @Override
    public Boolean refund(Long orderId) {

        //获取支付记录信息
        PaymentInfo paymentInfo = paymentService.getPaymentInfo(orderId, PaymentTypeEnum.WEIXIN.getStatus());
        //添加信息到退款记录表
        RefundInfo refundInfo = refundInfoService.saveRefundInfo(paymentInfo);
        //判断当前订单数据是否已经退款
        if (refundInfo.getRefundStatus().intValue() == RefundStatusEnum.REFUND.getStatus().intValue()) {
            return true;
        }

        //2.获取客户端
        AlipayClient alipayClient = getAlipayClient();


        AlipayTradeRefundModel model = new AlipayTradeRefundModel();
        model.setOutTradeNo(paymentInfo.getOutTradeNo());
        model.setRefundAmount("1");


        AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
        request.setBizModel(model);
        request.setNotifyUrl(AlipayConfig.NOTIFY_URL);
        request.setReturnUrl(AlipayConfig.RETURN_URL);

        AlipayTradeRefundResponse alipayResponse = null;
        try {
            alipayResponse = alipayClient.execute(request);
        } catch (AlipayApiException e) {
            e.printStackTrace();
            return false;
        }
        if (alipayResponse.isSuccess()) {
            refundInfo.setCallbackTime(new Date());
            refundInfo.setTradeNo(alipayResponse.getTradeNo());
            refundInfo.setRefundStatus(RefundStatusEnum.REFUND.getStatus());
            refundInfo.setCallbackContent(alipayResponse.getBody());
            return true;
        }
        return false;
    }

    /**
     * 回调
     *
     * @param request 回调请求
     * @return
     */
    @Override
    public Boolean alipayCallback(HttpServletRequest request) {
        try {
            Map<String, String> params = ParamsUtil.ParamstoMap(request);
            logger.info("回调参数=========>" + params);
            String out_trade_no = params.get("trade_no");
            String body = params.get("body");
            logger.info("交易的流水号和交易信息=======>",out_trade_no, body);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            logger.info("异常====>", e.toString());
            return false;
        }

    }
    /**
     * 获取Alipay客户端
     *
     * @return
     */

    @Autowired
    public AlipayClient getAlipayClient() {
        return new DefaultAlipayClient(AlipayConfig.APPID, AlipayConfig.RSA_PRIVATE_KEY, AlipayConfig.FORMAT, AlipayConfig.CHARSET, AlipayConfig.ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE);
    }

}

由于篇幅所限,不可能将所有的代码展示。只展示核心功能的代码的实现。
更多浏览:https://github.com/zhou431615/ohrb