简介
Ohrb(Online hospital registration booking)即为某线上医院预约挂号系统,通过手机验证码登录或者微信登录实现,患者(用户)点击到相关的医院,选择相应的日期,可可实现简单的医院预约挂号操作。
系统的基本功能:
- 实现手机验证码或微信登录
- 实现微信支付/支付宝支付
- 实现用户实名信息上传认证
- 实现预约挂号订单短信通知
具体的功能页面如下图1.1、1.2所示,这是系统首页,展示医院、科室、医师以及挂号信息。
图1.1 首页界面图
图1.2 首页轮播界面图
用户点击登录/注册即即可实现登录/注册同时实现,用户可使用验证码或者微信登录进行登录,具体如图1.3所示。
图1.3 患者(用户)登录界面图
用户(患者)可在相应的医院例如,北京协和医院,选择多发性硬化专科,5月20日的号,点击选择,添加相应的就诊人。同时需要立刻支付,支付在稍后需要立刻支付。具体如图1.4所示。
图1.4 患者进行预约挂号操作
用户需要立刻支付,使用微信支付,扫描二维码,支付费用成功即为挂号成功。同时系统会将信息存储,以供管理端使用。具体如图1.5所示。
图1.5 支付成功,预约挂号成功
同时,用户手机端会收到验证码。具体如图1.6所示。
图1.6 手机收到验证码
同时系统还有管理端,用户可管理对医院、用户、订单上架、修改、删除等操作。如图1.7所示。
图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所示。
图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 |
Nacos
E-R图
图1.9 E-R图
通过对数据字典、医院设置、科室排班等字段添加索引,实现数据库等数据快速查询,实现一定的性能优化。
短信调用
短信使用云通讯的服务,申请后,得到AppID秘钥,在yaml文件中进行配置,根据SDK,进行开发。具体如代码所示。
private CCPRestSmsSDK getSdk()
{
CCPRestSmsSDK sdk = new CCPRestSmsSDK();
sdk.init(lryConfig.getServerIp(), lryConfig.getServerPort());
sdk.setAccount(lryConfig.getAccountSId(), lryConfig.getAccountToken());
sdk.setAppId(lryConfig.getAppId());
sdk.setBodyType(BodyType.Type_JSON);
return sdk;
}
@Override
public boolean send(String phone)
{
//判断手机号是否为空
return !StringUtils.isEmpty(phone);
}
/**
*
* @param phone 手机号
* @param codes 内容数据,用于替换模板中{序号}
* @return
*/
public boolean sendTemplate(String phone, String[] codes)
{
HashMap<String, Object> result = this.getSdk().sendTemplateSMS(phone, lryConfig.getTemplateId(), codes);
log.info("SDKTestGetSubAccounts result=" + result);
if ("000000".equals(result.get("statusCode"))) {
//正常返回输出data包体信息(map) 打印出来看看
HashMap<String, Object> data = (HashMap<String, Object>) result.get("data");
Set<String> keySet = data.keySet();
for (String key : keySet) {
Object object = data.get(key);
System.out.println(key + " = " + object);
}
} else {
//异常返回输出错误码和错误信息
System.out.println("错误码=" + result.get("statusCode") + " 错误信息= " + result.get("statusMsg"));
return false;
}
return true;
}
@Override
public boolean send(String phone, String code)
{
String[] codes = {code, "2"};
return sendTemplate(phone, codes);
}
@Override
public boolean send(MsmVo msmVo)
{
String name = (String) msmVo.getParam().get("name");
String date = (String) msmVo.getParam().get("reserveDate");
//手机号不为空且就医提醒不为空,走提醒就医的短信模板!
if (!StringUtils.isEmpty(msmVo.getPhone()) && !StringUtils.isEmpty((CharSequence) msmVo.getParam().get("jiuyitixing"))) {
String code1 = Sms.SUCCESS + name;
String code2 = Sms.DATE + date;
String[] codes = {code1, code2};
return this.sendTemplate(msmVo.getPhone(), codes);
}
//手机号不为空,进行发送,这个走的是预约订单成功的短信模板
if (!StringUtils.isEmpty(msmVo.getPhone())) {
String code1 = Sms.INFO + name;
String code2 = Sms.DATE + date;
String[] codes = {code1, code2};
return this.sendTemplate(msmVo.getPhone(), codes);
}
return false;
}
文件上传
使用阿里云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