目录
1. 确认需求
(1)未支付取消订单,直接通知医院更新取消预约状态
(2)已支付取消订单,先退款给用户,然后通知医院更新取消预约状态
参考文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
该接口需要使用证书,详情参考文档并下载证书
2. 确认证书
3. 添加配置证书地址,解开相关注释
- #双向证书
- weixin.cert=E:\\apiclient_cert.p12

4. 获取支付记录方法
PaymentService
- //获取支付记录
- PaymentInfo getPaymentInfo(Long orderId, Integer paymentType);
-
- //获取支付记录
- @Override
- public PaymentInfo getPaymentInfo(Long orderId, Integer paymentType) {
- QueryWrapper
wrapper = new QueryWrapper<>(); - wrapper.eq("order_id", orderId);
- wrapper.eq("payment_type", paymentType);
- PaymentInfo paymentInfo = baseMapper.selectOne(wrapper);
- return paymentInfo;
- }
1. 确认库表 refund_info
2. 创建相关接口、类
3. 接口实现
- public interface RefundInfoService extends IService
{ - //保存退款记录
- RefundInfo saveRefundInfo(PaymentInfo paymentInfo);
- }
-
- @Service
- public class RefundInfoServiceImpl extends ServiceImpl
implements RefundInfoService { - //保存退款记录
- @Override
- public RefundInfo saveRefundInfo(PaymentInfo paymentInfo) {
- //1.查询退款记录,如果存在直接返回
- QueryWrapper
queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("order_id", paymentInfo.getOrderId());
- queryWrapper.eq("payment_type", paymentInfo.getPaymentType());
- RefundInfo refundInfo = baseMapper.selectOne(queryWrapper);
- if(null != refundInfo) return refundInfo;
-
- //2.如果不存在,新增退款记录
- refundInfo = new RefundInfo();
- refundInfo.setCreateTime(new Date());
- refundInfo.setOrderId(paymentInfo.getOrderId());
- refundInfo.setPaymentType(paymentInfo.getPaymentType());
- refundInfo.setOutTradeNo(paymentInfo.getOutTradeNo());
- refundInfo.setRefundStatus(RefundStatusEnum.UNREFUND.getStatus());//1 退款中
- refundInfo.setSubject(paymentInfo.getSubject());
- //paymentInfo.setSubject("test");
- refundInfo.setTotalAmount(paymentInfo.getTotalAmount());
- baseMapper.insert(refundInfo);
- return refundInfo;
- }
- }
1. 接口实现
weixinService
- //退款
- Boolean refund(Long orderId);
-
- //退款
- @Override
- public Boolean refund(Long orderId) {
- try {
- //1.根据参数查询交易记录
- PaymentInfo paymentInfo = paymentService.getPaymentInfo(orderId, PaymentTypeEnum.WEIXIN.getStatus());
- if (paymentInfo == null) {
- throw new YyghException(20001, "交易记录有误");
- }
-
- //2.根据交易记录添加退款记录,确认退款状态
- RefundInfo refundInfo = refundInfoService.saveRefundInfo(paymentInfo);
- if (refundInfo.getRefundStatus() ==
- RefundStatusEnum.REFUND.getStatus()) {
- return true; //已完成退款
- }
-
- //3.封装调用接口参数
- Map
paramMap = new HashMap<>(8); - 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"); //退款多少,小于总金额
-
-
- //4.创建客户端(设置url) 参考文档
- HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/secapi/pay/refund");
-
- //5.设置参数 (map=>xml),开启读取证书开关
- String paramXml = WXPayUtil.generateSignedXml(paramMap,ConstantPropertiesUtils.PARTNERKEY);
- client.setXmlParam(paramXml);
- client.setHttps(true);
- client.setCert(true); //开启读取证书开关
- client.setCertPassword(ConstantPropertiesUtils.PARTNER); //整证密码(商户编号)
- //6.客户端发送请求
- client.post();
-
- //7.获取响应,转化响应类型(xml=>map)
- String xml = client.getContent();
- System.out.println("退款xml = " + xml);
- Map
resultMap = WXPayUtil.xmlToMap(xml); -
- //8.如果退款成功,更新退款记录信息
- 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 false;
- }
1. OrderController 方法
*参数:orderId
*返回值:R.ok()
- @ApiOperation(value = "取消预约")
- @GetMapping("auth/cancelOrder/{orderId}")
- public R cancelOrder(
- @ApiParam(name = "orderId", value = "订单id", required = true)
- @PathVariable("orderId") Long orderId) {
-
- Boolean flag = orderService.cancelOrder(orderId);
- return R.ok().data("flag",flag);
- }
2. service
注释掉医院取消预约 校验签名
- //取消预约
- @Override
- public Boolean cancelOrder(Long orderId) {
- //1.查询订单信息
- OrderInfo orderInfo = baseMapper.selectById(orderId);
- if(orderInfo==null){
- throw new YyghException(20001,"订单信息有误");
- }
-
- //2.判断是否已过退号时间
- DateTime quitDateTime = new DateTime(orderInfo.getQuitTime());
- if(quitDateTime.isBeforeNow()){
- throw new YyghException(20001,"已过取消预约截止时间");
- }
-
- //3.调用医院系统接口,取消预约
- Map
reqMap = new HashMap<>(); - reqMap.put("hoscode",orderInfo.getHoscode());
- reqMap.put("hosRecordId",orderInfo.getHosRecordId());
- reqMap.put("timestamp", HttpRequestHelper.getTimestamp());
- reqMap.put("sign", "");
-
- JSONObject result = HttpRequestHelper.sendRequest(reqMap, "http://localhost:9998/order/updateCancelStatus");
-
- //4.医院取消预约成功
- if(result.getInteger("code")!=200){
- throw new YyghException(20001,"取消预约失败");
- }else {
- //判断是否已支付
- if(orderInfo.getOrderStatus() == OrderStatusEnum.PAID.getStatus()) {
- //5.如果已支付,调用微信退款
- Boolean refund = weixinService.refund(orderId);
- if(!refund){
- throw new YyghException(20001,"微信退款失败");
- }
- }
- //6.更新订单状态
- orderInfo.setOrderStatus(OrderStatusEnum.CANCLE.getStatus());
- this.updateById(orderInfo);
-
- //7.发送MQ消息,更新号源,通知就诊人
- OrderMqVo orderMqVo = new OrderMqVo();
- orderMqVo.setScheduleId(orderInfo.getHosScheduleId());
- orderMqVo.setHoscode(orderInfo.getHoscode());
- //短信提示
- MsmVo msmVo = new MsmVo();
- msmVo.setPhone(orderInfo.getPatientPhone());
- orderMqVo.setMsmVo(msmVo);
- rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_ORDER, MqConst.ROUTING_ORDER, orderMqVo);
- return true;
- }
- }
3. 改造HospitalReceiver 监听器,判断取消预约还是创建订单
- @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 {
- //1.取出参数
- String hoscode = orderMqVo.getHoscode();
- String hosScheduleId = orderMqVo.getScheduleId();
- Integer reservedNumber = orderMqVo.getReservedNumber();
- Integer availableNumber = orderMqVo.getAvailableNumber();
- MsmVo msmVo = orderMqVo.getMsmVo();
-
- //2.根据参数查询排班信息
- Schedule schedule = scheduleService.getScheduleByIds(hoscode, hosScheduleId);
-
- //2.5判断创建订单,还是取消预约 (剩余预约数是否为空 取消订单医院接口没有返回值)
- if(StringUtils.isEmpty(availableNumber)){
- //取消预约,更新号源
- availableNumber = schedule.getAvailableNumber().intValue() + 1;
- schedule.setAvailableNumber(availableNumber);
- }else {
- //创建订单,更新号源
- schedule.setReservedNumber(reservedNumber);
- schedule.setAvailableNumber(availableNumber);
- }
- //3.更新排班信息
- schedule.setUpdateTime(new Date()); //MongoDB不会自动更新时间
- scheduleService.update(schedule);
-
- //4.发送短信相关MQ消息
- if(msmVo!=null){
- rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_MSM, MqConst.ROUTING_MSM_ITEM, msmVo);
- }
- }
- }
1. wx.js 创建API接口方法
- //取消预约订单
- cancelOrder(orderId) {
- return request({
- url: `/api/order/orderInfo/auth/cancelOrder/${orderId}`,
- method: 'get'
- })
- },
2. JS实现
- //取消预约
- cancelOrder() {
- this.$confirm('此操作将取消预约,是否继续?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- }).then(() => {
- return weixinApi.cancelOrder(this.orderId).then(response => {
- this.$message({
- type: "success",
- message: "取消预约成功!"
- })
- window.location.reload()
- })
- }).catch(() => {
- this.$message({
- type: "info",
- message: "已取消操作"
- })
- })
- }
5. 测试,收到微信退款、短信
我们通过定时任务,每天8点执行,提醒就诊
1. 新建service_task 模块
- <dependencies>
- <dependency>
- <groupId>com.atguigugroupId>
- <artifactId>rabbit_utilartifactId>
- <version>0.0.1-SNAPSHOTversion>
- dependency>
- dependencies>
- # 服务端口
- server.port=8208
- # 服务名
- spring.application.name=service-task
- # 环境设置:dev、test、prod
- spring.profiles.active=dev
-
- # nacos服务地址
- spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
-
- #rabbitmq地址
- spring.rabbitmq.host=192.168.86.86
- spring.rabbitmq.port=5672
- spring.rabbitmq.username=guest
- spring.rabbitmq.password=guest
- @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
- @EnableDiscoveryClient
- @ComponentScan(basePackages = {"com.atguigu"})
- public class ServiceTaskApplication {
- public static void main(String[] args) {
- SpringApplication.run(ServiceTaskApplication.class, args);
- }
- }
2. 添加常量配置
在rabbit-util模块MqConst类添加
- //定时任务
- public static final String EXCHANGE_DIRECT_TASK = "exchange.direct.task";
- public static final String ROUTING_TASK_8 = "task.8";
- //队列
- public static final String QUEUE_TASK_8 = "queue.task.8";
3. 七域表达式 (七个时间区域)
4. ScheduledTask 实现
- @Component
- @EnableScheduling //开启定时任务
- public class ScheduledTask {
-
- @Autowired
- private RabbitService rabbitService;
-
- //@Scheduled(cron = "0 0 8 * * ?")
- @Scheduled(cron = "0/5 * * * * ?")
- public void test() {
- System.out.println("定时任务启动");
- }
-
- //每天8点执行 提醒就诊
- @Scheduled(cron = "0 0 8 * * ?")
- public void task1() {
- System.out.println(new Date().toLocaleString());
- rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK, MqConst.ROUTING_TASK_8, "");
- }
- }
1. OrderService 添加方法 就诊提醒
- //就诊提醒
- void patientTips();
- //就诊提醒
- @Override
- public void patientTips() {
- //1.查询符合条件订单集合
- QueryWrapper
queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("reserve_date",
- new DateTime().toString("yyyy-MM-dd"));
- List
orderInfoList = baseMapper.selectList(queryWrapper); -
- //2.遍历集合,拼写短信,发送mq消息
- for(OrderInfo orderInfo : orderInfoList) {
- //短信提示
- MsmVo msmVo = new MsmVo();
- msmVo.setPhone(orderInfo.getPatientPhone());
- String reserveDate = new DateTime(orderInfo.getReserveDate()).toString("yyyy-MM-dd") + (orderInfo.getReserveTime()==0 ? "上午": "下午");
- Map
param = new HashMap(){{ - put("title", orderInfo.getHosname()+"|"+orderInfo.getDepname()+"|"+orderInfo.getTitle());
- put("reserveDate", reserveDate);
- put("name", orderInfo.getPatientName());
- }};
- msmVo.setParam(param);
- rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_MSM, MqConst.ROUTING_MSM_ITEM, msmVo);
- }
2. 创建监听器
receiver/OrderReceiver
- @ComponentScan
- public class OrderReceiver {
-
- @Autowired
- OrderService orderService;
-
- //定时任务:就医提醒
- @RabbitListener(bindings = @QueueBinding(
- value = @Queue(value = MqConst.QUEUE_TASK_8, durable = "true"),
- exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_TASK),
- key = {MqConst.ROUTING_TASK_8}
- ))
- public void patientTips(Message message, Channel channel) throws IOException {
- orderService.patientTips();
- }
- }
我们统计医院每天的预约情况,通过图表的形式展示,统计的数据都来自订单模块,因此我们在该模块封装好数据,在统计模块通过feign的形式获取数据。
(1) 创建远程接口统计数据
优势:实时查询
劣势:影响各个模块使用的性能 (占用服务器资源)
(2) 每天固定时间,统计分析各个指标数据,生成前一天统计报表
优势:各个模块使用影响相对小
劣势:不是实时数据
(3) 使用脚本语言、数据库编程、数据统计分析
优势: 数据统计块,对资源占用少
劣势:不能使用JAVA实现 (学习成本高)
1. 查询数据sql
SELECT o.`reserve_date`,COUNT(o.`id`) FROM order_info o
WHERE XXXXX
GROUP BY o.`reserve_date`;
2. 接口分析
*参数:OrderCountQueryVo
*返回值:Map (x轴,y轴数据)
3. OrderApiController 新增方法
- @ApiOperation(value = "获取订单统计数据")
- @PostMapping("inner/getCountMap")
- public Map
getCountMap(@RequestBody OrderCountQueryVo orderCountQueryVo) { - Map
map = orderService.getCountMap(orderCountQueryVo); - return map;
- }
4. 实现mapper
由于MP(MybatisPlus) 无法实现统计sql,需要自行实现
- public interface OrderInfoMapper extends BaseMapper
{ - //统计每天平台预约数据
- List
selectOrderCount(OrderCountQueryVo orderCountQueryVo); - }
创建 mapper/xml/OrderInfoMapper.xml
- mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.atguigu.yygh.order.mapper.OrderInfoMapper">
-
- <select id="selectOrderCount" resultType="com.atguigu.yygh.vo.order.OrderCountVo">
- select reserve_date as reserveDate, count(reserve_date) as count
- from order_info
- <where>
- <if test="hosname != null and hosname != ''">
- and hosname like CONCAT('%',#{hosname},'%')
- if>
- <if test="reserveDateBegin != null and reserveDateBegin != ''">
- and reserve_date >= #{reserveDateBegin}
- if>
- <if test="reserveDateEnd != null and reserveDateEnd != ''">
- and reserve_date <= #{reserveDateEnd}
- if>
- and is_deleted = 0
- where>
- group by reserve_date
- order by reserve_date
- select>
- mapper>
5. 添加配置
*service.pom.xml 添加 maven插件,部署xml文件
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-maven-pluginartifactId>
- plugin>
- plugins>
- <resources>
- <resource>
- <directory>src/main/javadirectory>
- <includes>
- <include>**/*.ymlinclude>
- <include>**/*.propertiesinclude>
- <include>**/*.xmlinclude>
- includes>
- <filtering>falsefiltering>
- resource>
- <resource>
- <directory>src/main/resourcesdirectory>
- <includes> <include>**/*.ymlinclude>
- <include>**/*.propertiesinclude>
- <include>**/*.xmlinclude>
- includes>
- <filtering>falsefiltering>
- resource>
- resources>
- build>
*order 配置文件添加相关配置
- #mapper xml文件扫描
- mybatis-plus.mapper-locations=classpath:com/atguigu/yygh/order/mapper/xml/*.xml
6. 实现service
- //获取订单统计数据
- @Override
- public Map
getCountMap(OrderCountQueryVo orderCountQueryVo) { - //1.查询统计信息
- List
orderCountVoList = baseMapper.selectOrderCount(orderCountQueryVo); -
- //2.收集x轴、y轴数据
- List
dateList //日期列表 - =orderCountVoList.stream()
- .map(OrderCountVo::getReserveDate)
- .collect(Collectors.toList());
- List
countList //统计列表 - =orderCountVoList.stream()
- .map(OrderCountVo::getCount)
- .collect(Collectors.toList());
-
- //3.封装数据返回
- Map
map = new HashMap<>(); - map.put("dateList", dateList);
- map.put("countList", countList);
- return map;
- }
7. 测试
1. 创建模块 service_order_client

2. 创建目录、接口
- @FeignClient(value = "service-orders")
- @Repository
- public interface OrderFeignClient {
- //获取订单统计数据
- @PostMapping("/api/order/orderInfo/inner/getCountMap")
- Map
getCountMap(@RequestBody OrderCountQueryVo orderCountQueryVo); -
- }
1. service下创建service_statistics模块

- <dependencies>
- <dependency>
- <groupId>com.atguigugroupId>
- <artifactId>service_order_clientartifactId>
- <version>0.0.1-SNAPSHOTversion>
- dependency>
- dependencies>
- # 服务端口
- server.port=8209
- # 服务名
- spring.application.name=service-sta
- # 环境设置:dev、test、prod
- spring.profiles.active=dev
-
- # nacos服务地址
- spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
- @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
- @EnableDiscoveryClient
- @EnableFeignClients(basePackages = {"com.atguigu"})
- @ComponentScan(basePackages = {"com.atguigu"})
- public class ServiceStatisticsApplication {
- public static void main(String[] args) {
- SpringApplication.run(ServiceStatisticsApplication.class, args);
- }
- }
添加网关配置
- #设置路由id
- spring.cloud.gateway.routes[7].id=service-sta
- #设置路由的uri
- spring.cloud.gateway.routes[7].uri=lb://service-sta
- #设置路由断言,代理servicerId为auth-service的/auth/路径
- spring.cloud.gateway.routes[7].predicates= Path=/*/statistics/**
2. 实现 controller
- @Api(tags = "统计管理接口")
- @RestController
- @RequestMapping("/admin/statistics")
- public class StatisticsController {
-
- @Autowired
- private OrderFeignClient orderFeignClient;
-
- @ApiOperation(value = "获取订单统计数据")
- @GetMapping("getCountMap")
- public R getCountMap(OrderCountQueryVo orderCountQueryVo) {
- Map
map = orderFeignClient.getCountMap(orderCountQueryVo); - return R.ok().data(map);
- }
- }
ECharts是百度的一个项目,后来百度把Echart捐给apache,用于图表展示,提供了常规的折线图、柱状图、散点图、饼图、K线图,用于统计的盒形图,用于地理数据可视化的地图、热力图、线图,用于关系数据可视化的关系图、treemap、旭日图,多维数据可视化的平行坐标,还有用于 BI 的漏斗图,仪表盘,并且支持图与图之间的混搭。

1. 后台前端 安装echarts
npm install --save echarts@4.1.0
2. 添加路由、创建页面
- {
- path: '/statistics',
- component: Layout,
- redirect: '/statistics/order/index',
- name: 'BasesInfo',
- meta: { title: '统计管理', icon: 'table' },
- alwaysShow: true,
- children: [
- {
- path: 'order/index',
- name: '预约统计',
- component: () => import('@/views/yygh/sta/order/index'),
- meta: { title: '预约统计' }
- }
- ]
- },

3. 添加API接口方法 sta.js
- import request from '@/utils/request'
-
- const api_name = '/admin/statistics'
-
- export default {
- //获取统计数据
- getCountMap(searchObj) {
- return request({
- url: `${api_name}/getCountMap`,
- method: 'get',
- params: searchObj
- })
- }
- }
4. 实现相关页面
- <div class="app-container">
-
- <el-form :inline="true" class="demo-form-inline">
- <el-form-item>
- <el-input v-model="searchObj.hosname" placeholder="点击输入医院名称" />
- el-form-item>
-
- <el-form-item>
- <el-date-picker v-model="searchObj.reserveDateBegin" type="date" placeholder="选择开始日期"
- value-format="yyyy-MM-dd" />
- el-form-item>
- <el-form-item>
- <el-date-picker v-model="searchObj.reserveDateEnd" type="date" placeholder="选择截止日期"
- value-format="yyyy-MM-dd" />
- el-form-item>
- <el-button :disabled="btnDisabled" type="primary" icon="el-icon-search" @click="showChart()">查询el-button>
- el-form>
-
- <div class="chart-container">
- <div id="chart" ref="chart" class="chart" style="height:500px;width:100%" />
- div>
- div>
- <script>
- import echarts from 'echarts'
- import statisticsApi from '@/api/yygh/sta'
-
- export default {
-
- data() {
- return {
- searchObj: {
- hosname: '',
- reserveDateBegin: '',
- reserveDateEnd: ''
- },
- btnDisabled: false,
- chart: null,
- title: '',
- xData: [], // x轴数据
- yData: [] // y轴数据
- }
- },
-
- methods: {
- // 初始化图表数据
- showChart() {
- statisticsApi.getCountMap(this.searchObj).then(response => {
- this.yData = response.data.countList
- this.xData = response.data.dateList
- this.setChartData()
- })
- },
-
- setChartData() {
- // 基于准备好的dom,初始化echarts实例
- var myChart = echarts.init(document.getElementById('chart'))
- // 指定图表的配置项和数据
- var option = {
- title: {
- text: this.title + '挂号量统计'
- },
- tooltip: {},
- legend: {
- data: [this.title]
- },
- xAxis: {
- data: this.xData
- },
- yAxis: {
- minInterval: 1
- },
- series: [{
- name: this.title,
- type: 'line',
- data: this.yData
- }]
- }
-
- // 使用刚指定的配置项和数据显示图表。
- myChart.setOption(option)
- },
- }
- }
- script>
5. 实现相关页面
