-
Roach的集群级备份采用的是物理备份,即通过物理文件拷贝的方式对数据库进行备份,通过备份的数据文件及日志等文件,数据库可以进行完全恢复。全量备份则是将当前时间点数据库中所有的数据进行备份。当然,全备份可以备份整块硬盘、整个分区或某个具体的目录。全备份的好处是数据恢复方便,因为所有的数据都在同一个备份中,所以只要恢复全备份,所有的数据都会被恢复。 其优点是:物理备份速度快,通过合理规划,可以低成本进行备份和恢复; 其缺点是:相较于增量备份备份时间较长。Roach备份采用生产者-消费者模式,下图展现了备份流程各个线程及IO交互图。exec线程作为生产者,同时为减轻exec线程的压力,增加并行的reader线程读取小文件;sender线程作为消费者;中间则是通过一个256MB(可设置)的大buffer衔接。GaussDB(DWS)内核的备份组件为GaussRoach.py和gs_roach,需在集群内拉起备份任务。GaussRoach.py:Roach单集群全量备份入口为GaussRoach.py。每次命令行输入“python GaussRoach.py –t backup…”后,roach的python语言模式就开始运行。任意节点均可作为主节点拉起GaussRoach.py,然后每个节点都启动gs_roach进程负责本节点备份,各节点并行备份,节点内各DN并行备份。
-
CBM 文件保存在data 目录的pg_cbm 文件夹下,命名方式为:pg_xlog_seqnum_startlsn_endlsn.cbm。seqnum文件序号表示这是第几个cbm文件,当一个cbm文件的大小超过100M时,将会切换到下一个cbm文件,并将seqnum加1。 startlsn为本cbm文件内容对应xlog记录的起始lsn; endlsn为本cbm文件切换时最后一次解析的截止lsn,若一个cbm文件还没有切换,那么endlsn为0。pg_cbm_tracked_location 说明:用于查询cbm已经解析到的lsn位置 入参:无 返回值:cbm已经解析到的lsn位置 b. pg_cbm_get_merged_file 说明:用于将指定lsn范围之内的cbm文件合并成一个cbm文件 入参:startlsn,指定的起始lsn;endlsn,指定的结束lsn 返回值:合并完的cbm文件名 c. pg_cbm_get_changed_block 说明:用于将指定lsn范围之内的cbm文件合并一个表,并返回表的各行记录 入参:startlsn,指定的起始lsn;endlsn,指定的结束lsn 返回值:合并完的表的记录,表的结构如下
-
当系统运行时间较长的时候,由于操作较多,日志文件的数量也较多。如果每次利用日志进行恢复操作都会耗费大量的时间,为了节约时间同时减少不必要的恢复操作,引入了checkpoint的概念。checkpoint表示在此操作之前,相关数据已经被保存到永久存储中,即使系统故障,这部分数据也不会丢失,因此恢复的时候只要从checkpoint操作之后根据日志执行恢复操作就可以了。checkpoint本身也是一条xlog记录,该记录包含了redo点的位置,因此,每次恢复数据时,先从xloh记录里找到最近的一次checkpoint记录,并根据该记录找到相应的redo点位置,这就是执行本次恢复的起始点位置。checkpoint操作记录了redo点的位置。基于上述功能,由于数据的所有变化都被记录在了xlog中,GaussDB(DWS)数据库内核通过增加常驻的CBM writer线程,持续不断地对新增的xlog进行解析,识别并记录哪些数据数据页面被修改。进程启动时即开启CBM writer功能 在startup线程刚启动时,其根据已经解析出来的CBM文件,来决定CBM writer开始解析的起始LSN位置。每次执行到checkpoint末尾时,会设置CBM writer线程的latch。CBM writer线程等待latch被设置,然后进行一轮日志解析。 通过动态reload GUC参数,开启CBM writer功能 由于是动态开启的CBM writer功能,因此startup线程没有初始化CBM解析的起始位置。打开enable_cbm_writer开关的同时,会将CBM强制初始化的标志置为true。当CBM writer线程启动之后,其第一次解析中,会强制初始化获得解析的起始lsn。
-
如果一次全量备份后的多次增量备份,指定的prior-backup-key均为上一次备份(可能是全量备份也可能增量备份)的backup-key,即此次备份是基于上一次备份集来进行的,那么这些增量备份就是差分增量备份,差分增量备份均是基于最近一次备份进行的。增量备份只需要备份上一备份节点到当前时间发生变化的数据文件,为了实现备份数据的完整性与一致性,正确识别并备份增量数据文件是至关重要的,作为增量备份的核心GaussDB(DWS)数据库内核的事务日志功能与cbm设计可以帮助Roach工具快速准确识别增量期间数据文件的变换信息,为快速准确完成增量备份提供了有力保障。为了保证数据的一致性和完整性,在对数据进行相关操作之前都会将具体的操作记录下来,持久化到可靠存储中,然后再进行具体的数据操作,这就是所谓的WAL(Write Ahead Logging),记录的相关操作称为XLOG日志,每一条日志记录都由LSN进行唯一标识。这样做的好处是事务的记录被提前记录并保存起来,在因一些外部原因(比如断电、操作系统失败等)导致操作失败后,我们可以通过保存的事务日志将这些操作重新执行一遍,保证数据不会丢失。
-
增量备份指备份基于上一个备份集到当前阶段的数据变化文件,相比于全量备份所用的时间更少,能够节约大量的空间资源,因此一直是客户的常规备份手段。为了实现数据的完整性与一致性,正确识别并备份增量数据文件是至关重要的,Gauss(DWS)数据库内核的事务日志功能与CBM设计可以帮助Roach工具快速准确识别增量期间数据文件的变换信息,从而快速实现增量备份的任务。GaussDB(DWS)数仓的备份恢复工具Roach支持集群级增量备份。全量备份会将源数据完整备份,而增量备份仅将上次备份后所作的更改进行备份,这里的上次备份可以使全量备份,也可以是全量备份后的增量备份。需要注意的是,增量备份的基础始终是全量备份,如果一次全量备份之后进行了全量恢复,则不能再基于该全量备份进行增量备份,必须重新进行全量备份然后基于新的全量备份进行增量备份。 增量备份分为两种:累积增量备份和差分增量备份。如果一次全量备份后的多次增量备份,指定的prior-backup-key始终为全量备份的backup-key,即所有的增量备份都是基于全量备份来进行的,那么这些增量备份就是累积增量备份,累积增量备份均是基于最近一次全量备份进行的。
-
组合使用pgxc_stat_activity 、 pgxc_thread_wait_status 、pgxc_comm_send_stream、pgxc_comm_recv_stream视图定位问题 在pgxc_stat_activity 视图中找到问题查询的query_id; 根据query_id查询pgxc_thread_wait_status 视图,看到 CN 在等DN4的数据,而DN4在与DN42建连;查询pgxc_comm_send_stream看到DN4发起的与DN2的逻辑连接中有一个处于READY半连接状态;查询pgxc_comm_recv_stream看到DN2已经接受了DN4发起的4个逻辑连接,问题出在DN4逻辑连接应答上。DN间接收数据分两个动作 LibcommRecvPoll,等待数据到达; LibcommRecv,数据到达后接收数据。 Poll Time:等待数据到达所花费的时间; Send time:发送数据并接收对端quota所花费的时间。在GaussDB(DWS)通信运维中,很多情况是由于一些参数配置的不合理导致集群效率低、资源过载、连接异常等问题。对于通信库和操作系统两方面都提供了大量可供用户自定义的参数、视图、脚本,熟练掌握一些重要工具有助于快速定位现网通信故障问题。
-
CN 连接DN报错, 主要关注后面的detail信息 Already too many client:DN线程数超过max_connections上限; Connection timed out:TCP建连超时,网络不通或者网络质量差; Connection refused:TCP建连被拒绝,防火墙配置或者端口号错误; timeout expired:连接已经建立,但连接信息同步时对端响应超时。 数据收发过程中报错 cancel/terminate日志:外部信号中止查询; reset by peer/broken pipe:连接对端线程主动关闭连接; Stream closed by remote:对端关闭连接或发生重启,需查看对端日志; Peer address changed:对端DN存在主备切换。 DN 间通信性能差 pv_total_memory_detail中通信内存过大:Libcomm通信库降级运行; gsar监控有严重丢包重传:网络质量差,丢包过多导致连接断开; top查看CPU高或者ksoftirq进程CPU高:排查异常CPU占用。
-
CN 连接DN报错, 主要关注后面的detail信息 Already too many client:DN线程数超过max_connections上限; Connection timed out:TCP建连超时,网络不通或者网络质量差; Connection refused:TCP建连被拒绝,防火墙配置或者端口号错误; timeout expired:连接已经建立,但连接信息同步时对端响应超时。 数据收发过程中报错 cancel/terminate日志:外部信号中止查询; reset by peer/broken pipe:连接对端线程主动关闭连接; Stream closed by remote:对端关闭连接或发生重启,需查看对端日志; Peer address changed:对端DN存在主备切换。 DN 间通信性能差 pv_total_memory_detail中通信内存过大:Libcomm通信库降级运行; gsar监控有严重丢包重传:网络质量差,丢包过多导致连接断开; top查看CPU高或者ksoftirq进程CPU高:排查异常CPU占用。
-
对于256节点的集群来说,并发场景导致CN和DN之间存在大量连接,每个连接占用一个端口,则CN的端口号很容易受限。为解决此问题,设计了CN多流,即 CN与DN之间采用逻辑连接。comm_cn_dn_logic_conn参数默认值是off,在集群规模或并发达到一定程度时,需要将其开启为on,避免CN与DN之间由于端口号受限而无法建连。TCP 代理通信库采用 pull 模式进行流量控制,避免消息堵塞。两个DN 分别有一个buffer,当一条通道发送端数据量过大时,很容易造成buffer 填满,阻塞了其他通道的发送。此时,对于每条通道设置一个quota,接收端根据buffer剩余空间的大小发送给发送端一个合理quota值,发送端根据quota大小发送数据。 comm_quota_size 表示每个通道单次不间断发送数据量配额,默认值 1MB。当通道发送数据量达到配额时,发送端等待接收端重新发送配额,进而继续发送数据,从而实现流控功能。其取值为0时,表示不使用quota,在一些大流量等场景中,查询之间可能会有影响。在1GE网卡环境中,受网卡能力限制,应该调小该参数,推荐20KB~40KB。如果环境内存充足,参数comm_usable_memory设置较大,可以适当调大,从而提升性能。
-
视图展示所有DN的通信库状态。其中字段包括节点名称、节点通信库接收速率,单位为byte/s、节点通信库发送速率,单位为byte/s、节点通信库接收速率,单位为Kbyte/s、节点通信库发送速率,单位为Kbyte/s、cmailbox的buffer大小、libcomm进程通信内存的大小、libpq 进程通信内存的大小、postmaster 线程实时使用率、gs_sender_flow_controller 线程实时使用率、gs_receiver_flow_controller 线程实时使用率、多个gs_receivers_loop线程中最高的实时使用率、当前使用的逻辑连接总数。表示TCP代理通信库支持的最大DN数,最小值为1,最大值为8192。当DN数小于256时,默认值为256;否则,为大于等于DN数的2的N次方。在集群扩容、缩容场景下,要注意此参数的变更。 表示TCP代理通信库支持的最大并发Stream数量,默认值为1024,最 大 为 60000,此参数要保证大于并发数每并发平均Stream算子数(smp的平方),否则会报错Cannot get stream index, maybe comm_max_stream is not enough。此外,在设置此参数时需要考虑占用内存问题,其大小为256byte * comm_max_stream * comm_max_datanode ,可见在内存、comm_max_datanode 和comm_max_stream三者之间需要一个动态的规划。
-
视图展示所有DN上的通信库接收流状态。其中字段包括节点名称、使用此通信流的线程ID、连接对端节点名称、连接对端节点ID、通信对端DN在本DN内的标识编号、通信流在物理连接中的标识编号、通信流所使用的tpc通信socket、通信流当前的状态、通信流对应的debug_query_id 编号、通信流所执行查询的 plan_node_id 编号、通信流所执行查询send端的smpid编号、通信流所执行查询recv端的smpid编号、通信流接收的数据总量、通信流当前生命周期使用时长、通信流的平均接收速率、通信流当前的通信配额值、通信流当前缓存的数据大小。展示所有DN上的通信库发送流状态。其中字段包括节点名称、使用此通信流的线程ID、连接对端节点名称、连接对端节点ID、通信对端DN在本DN内的标识编号、通信流在物理连接中的标识编号、通信流所使用的tpc通信socket、通信流当前的状态、通信流对应的debug_query_id 编号、通信流所执行查询的 plan_node_id 编号、通信流所执行查询send端的smpid编号、通信流所执行查询recv端的smpid编号、通信流接收的数据总量、通信流当前生命周期使用时长、通信流的平均接收速率、通信流当前的通信配额值和通信流等待quota值产生的额外时间开销。
-
并发比较高时 producer 线程会一直往队列里push,如果此时对端 cunsumer1 线程正在处理别的数据导致接收 buffer1 满了的话,producer2 和 producer3 无法往网络上填充更多的数据,发送阶段就会阻塞,而此时可能consumer2和consumer3正在空闲状态,等待这个接收数据,但是因为发送端阻塞而接收不到,这种场景会严重影响性能。这个模型我们称之为push模型。因此我们需要通过另外一种流控机制来解决这个问题,我们称之为pull模型。 push模型:发送端不感知接收端状态。一直往无锁队列中push,直到push阻塞; poll 模型:发送端感知接收端状态。发送端一开始不会发送数据,当接收端里的buffer 池内存满足一定条件时,通知对应的发送端,并告知可以接收的数据量,发送端可以按照对端可以接收的数据量进行发送。 通过 poll 模型的实现,在本线程阻塞的情况下,其他的线程不会阻塞,以确保物理连接中数据永远不会阻塞,保证连接的通畅性。该视图中的字段包括节点名称、连接对端节点的节点名称、连接对端IP的对端地址、当前物理连接使用的 Stream 逻辑连接数量、当前物理连接一分钟内探测到的最小时延、当前物理连接一分钟内探测道德平均值和当前物理连接一分钟内探测到的最大时延。
-
GaussDB(DWS)为解决建联过多的问题,实现了 Libcomm 通信库(即逻辑连接通信库),在一个物理长连接上模拟n个逻辑连接,使得所有并发的数据跑在一个物理连接上。比如DN1需要给DN2发送数据,并发数1000,在原有逻辑下,DN1需要建立与DN2连接的1000个线程与之进行交互,消耗了大量的连接、内存、fd资源,而改造Libcomm通信库之后,DN1 与 DN2 仅需建立一个真正的物理连接,在这个物理连接上可以建立很多个逻辑链接,这样可以使得1000个并发就可以用同一个物理连接进行数据交互。 那么GaussDB(DWS)的逻辑连接是怎么实现的呢?首先我们从连接数据流入手,挖掘其实现逻辑。物理连接支持TCP、RDMA等协议连接,以TCP为例,其物理连接数据流可以分为两部分,即数据包头+数据。数据包头为固定长度,其中包含逻辑连接号和数据块长度,用来区别逻辑连接,并接收每个连接各自对应的数据。send queue:producer 发送线程将要发送的数据先push到一个无锁队列中,push完成之后,producer线程就可以继续做自己的事情了; send proxy thread:通信存在一个发送端代理线程,会统一将无锁队列中的数据,通过物理连接发送到对端。
-
求助,gaussdb集中式是不是不能创建 分布式键的表吗?CREATE TABLE tb_t3(c1 int,c2 int) DISTRIBUTE BY RANGE(c1)( SLICE s1 VALUES LESS THAN (100), SLICE s2 VALUES LESS THAN (200), SLICE s3 VALUES LESS THAN (MAXVALUE) );
-
Apache POI 是用Java编写的免费开源的跨平台的Java API,Apache POI提供API给Java程序对Microsoft Office格式档案读和写的功能,其中使用最多的就是使用POI操作Excel文件。POI为“Poor Obfuscation Implementation”的首字母缩写,意为“简洁版的模糊实现”。一、运营数据统计1.1 需求分析通过运营数据统计可以展示出体检机构的运营情况,包括会员数据、预约到诊数据、热门套餐等信息。本章节就是要通过一个表格的形式来展示这些运营数据。效果如下图: 1.2 完善页面运营数据统计对应的页面为/pages/report_business.html。1.2.1 定义模型数据定义数据模型,通过VUE的数据绑定展示数据<script> var vue = new Vue({ el: '#app', data:{ reportData:{ reportDate:null, todayNewMember :0, totalMember :0, thisWeekNewMember :0, thisMonthNewMember :0, todayOrderNumber :0, todayVisitsNumber :0, thisWeekOrderNumber :0, thisWeekVisitsNumber :0, thisMonthOrderNumber :0, thisMonthVisitsNumber :0, hotSetmeal :[] } } }) </script><div class="box" style="height: 900px"> <div class="excelTitle" > <el-button @click="exportExcel">导出Excel</el-button>运营数据统计 </div> <div class="excelTime">日期:{{reportData.reportDate}}</div> <table class="exceTable" cellspacing="0" cellpadding="0"> <tr> <td colspan="4" class="headBody">会员数据统计</td> </tr> <tr> <td width='20%' class="tabletrBg">新增会员数</td> <td width='30%'>{{reportData.todayNewMember}}</td> <td width='20%' class="tabletrBg">总会员数</td> <td width='30%'>{{reportData.totalMember}}</td> </tr> <tr> <td class="tabletrBg">本周新增会员数</td> <td>{{reportData.thisWeekNewMember}}</td> <td class="tabletrBg">本月新增会员数</td> <td>{{reportData.thisMonthNewMember}}</td> </tr> <tr> <td colspan="4" class="headBody">预约到诊数据统计</td> </tr> <tr> <td class="tabletrBg">今日预约数</td> <td>{{reportData.todayOrderNumber}}</td> <td class="tabletrBg">今日到诊数</td> <td>{{reportData.todayVisitsNumber}}</td> </tr> <tr> <td class="tabletrBg">本周预约数</td> <td>{{reportData.thisWeekOrderNumber}}</td> <td class="tabletrBg">本周到诊数</td> <td>{{reportData.thisWeekVisitsNumber}}</td> </tr> <tr> <td class="tabletrBg">本月预约数</td> <td>{{reportData.thisMonthOrderNumber}}</td> <td class="tabletrBg">本月到诊数</td> <td>{{reportData.thisMonthVisitsNumber}}</td> </tr> <tr> <td colspan="4" class="headBody">热门套餐</td> </tr> <tr class="tabletrBg textCenter"> <td>套餐名称</td> <td>预约数量</td> <td>占比</td> <td>备注</td> </tr> <tr v-for="s in reportData.hotSetmeal"> <td>{{s.name}}</td> <td>{{s.setmeal_count}}</td> <td>{{s.proportion}}</td> <td></td> </tr> </table> </div>1.2.2 发送请求获取动态数据在VUE的钩子函数中发送ajax请求获取动态数据,通过VUE的数据绑定将数据展示到页面<script> var vue = new Vue({ el: '#app', data:{ reportData:{ reportDate:null, todayNewMember :0, totalMember :0, thisWeekNewMember :0, thisMonthNewMember :0, todayOrderNumber :0, todayVisitsNumber :0, thisWeekOrderNumber :0, thisWeekVisitsNumber :0, thisMonthOrderNumber :0, thisMonthVisitsNumber :0, hotSetmeal :[] } }, created() { //发送ajax请求获取动态数据 axios.get("/report/getBusinessReportData.do").then((res)=>{ this.reportData = res.data.data; }); } }) </script>根据页面对数据格式的要求,我们发送ajax请求,服务端需要返回如下格式的数据:{ "data":{ "todayVisitsNumber":0, "reportDate":"2019-04-25", "todayNewMember":0, "thisWeekVisitsNumber":0, "thisMonthNewMember":2, "thisWeekNewMember":0, "totalMember":10, "thisMonthOrderNumber":2, "thisMonthVisitsNumber":0, "todayOrderNumber":0, "thisWeekOrderNumber":0, "hotSetmeal":[ {"proportion":0.4545,"name":"粉红珍爱(女)升级TM12项筛查体检套餐","setmeal_count":5}, {"proportion":0.1818,"name":"阳光爸妈升级肿瘤12项筛查体检套餐","setmeal_count":2}, {"proportion":0.1818,"name":"珍爱高端升级肿瘤12项筛查","setmeal_count":2}, {"proportion":0.0909,"name":"孕前检查套餐","setmeal_count":1} ], }, "flag":true, "message":"获取运营统计数据成功" }1.3 后台代码1.3.1 Controller在ReportController中提供getBusinessReportData方法@Reference private ReportService reportService; /** * 获取运营统计数据 * @return */ @RequestMapping("/getBusinessReportData") public Result getBusinessReportData(){ try { Map<String, Object> result = reportService.getBusinessReport(); return new Result(true,MessageConstant.GET_BUSINESS_REPORT_SUCCESS,result); } catch (Exception e) { e.printStackTrace(); return new Result(true,MessageConstant.GET_BUSINESS_REPORT_FAIL); } }1.3.2 服务接口在health_interface工程中创建ReportService服务接口并声明getBusinessReport方法package com.yunhe.service; import java.util.Map; public interface ReportService { /** * 获得运营统计数据 * Map数据格式: * todayNewMember -> number * totalMember -> number * thisWeekNewMember -> number * thisMonthNewMember -> number * todayOrderNumber -> number * todayVisitsNumber -> number * thisWeekOrderNumber -> number * thisWeekVisitsNumber -> number * thisMonthOrderNumber -> number * thisMonthVisitsNumber -> number * hotSetmeals -> List<Setmeal> */ public Map<String,Object> getBusinessReport() throws Exception; }1.3.3 服务实现类在health_service_provider工程中创建服务实现类ReportServiceImpl并实现ReportService接口package com.yunhe.service; import com.alibaba.dubbo.config.annotation.Service; import com.yunhe.dao.MemberDao; import com.yunhe.dao.OrderDao; import com.yunhe.utils.DateUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 统计报表服务 */ @Service(interfaceClass = ReportService.class) @Transactional public class ReportServiceImpl implements ReportService { @Autowired private MemberDao memberDao; @Autowired private OrderDao orderDao; /** * 获得运营统计数据 * Map数据格式: * todayNewMember -> number * totalMember -> number * thisWeekNewMember -> number * thisMonthNewMember -> number * todayOrderNumber -> number * todayVisitsNumber -> number * thisWeekOrderNumber -> number * thisWeekVisitsNumber -> number * thisMonthOrderNumber -> number * thisMonthVisitsNumber -> number * hotSetmeal -> List<Setmeal> */ public Map<String, Object> getBusinessReport() throws Exception{ //获得当前日期 String today = DateUtils.parseDate2String(DateUtils.getToday()); //获得本周一的日期 String thisWeekMonday = DateUtils.parseDate2String(DateUtils.getThisWeekMonday()); //获得本月第一天的日期 String firstDay4ThisMonth = DateUtils.parseDate2String(DateUtils.getFirstDay4ThisMonth()); //今日新增会员数 Integer todayNewMember = memberDao.findMemberCountByDate(today); //总会员数 Integer totalMember = memberDao.findMemberTotalCount(); //本周新增会员数 Integer thisWeekNewMember = memberDao.findMemberCountAfterDate(thisWeekMonday); //本月新增会员数 Integer thisMonthNewMember = memberDao.findMemberCountAfterDate(firstDay4ThisMonth); //今日预约数 Integer todayOrderNumber = orderDao.findOrderCountByDate(today); //本周预约数 Integer thisWeekOrderNumber = orderDao.findOrderCountAfterDate(thisWeekMonday); //本月预约数 Integer thisMonthOrderNumber = orderDao.findOrderCountAfterDate(firstDay4ThisMonth); //今日到诊数 Integer todayVisitsNumber = orderDao.findVisitsCountByDate(today); //本周到诊数 Integer thisWeekVisitsNumber = orderDao.findVisitsCountAfterDate(thisWeekMonday); //本月到诊数 Integer thisMonthVisitsNumber = orderDao.findVisitsCountAfterDate(firstDay4ThisMonth); //热门套餐(取前4) List<Map> hotSetmeal = orderDao.findHotSetmeal(); Map<String,Object> result = new HashMap<>(); result.put("reportDate",today); result.put("todayNewMember",todayNewMember); result.put("totalMember",totalMember); result.put("thisWeekNewMember",thisWeekNewMember); result.put("thisMonthNewMember",thisMonthNewMember); result.put("todayOrderNumber",todayOrderNumber); result.put("thisWeekOrderNumber",thisWeekOrderNumber); result.put("thisMonthOrderNumber",thisMonthOrderNumber); result.put("todayVisitsNumber",todayVisitsNumber); result.put("thisWeekVisitsNumber",thisWeekVisitsNumber); result.put("thisMonthVisitsNumber",thisMonthVisitsNumber); result.put("hotSetmeal",hotSetmeal); return result; } }1.3.4 Dao接口在OrderDao和MemberDao中声明相关统计查询方法package com.yunhe.dao; import com.yunhe.pojo.Order; import java.util.List; import java.util.Map; public interface OrderDao { public void add(Order order); public List<Order> findByCondition(Order order); public Map findById4Detail(Integer id); public Integer findOrderCountByDate(String date); public Integer findOrderCountAfterDate(String date); public Integer findVisitsCountByDate(String date); public Integer findVisitsCountAfterDate(String date); public List<Map> findHotSetmeal(); }package com.yunhe.dao; import com.github.pagehelper.Page; import com.yunhe.pojo.Member; import java.util.List; public interface MemberDao { public List<Member> findAll(); public Page<Member> selectByCondition(String queryString); public void add(Member member); public void deleteById(Integer id); public Member findById(Integer id); public Member findByTelephone(String telephone); public void edit(Member member); public Integer findMemberCountBeforeDate(String date); public Integer findMemberCountByDate(String date); public Integer findMemberCountAfterDate(String date); public Integer findMemberTotalCount(); }1.3.5 Mapper映射文件在OrderDao.xml和MemberDao.xml中定义SQL语句OrderDao.xml:<!--根据日期统计预约数--> <select id="findOrderCountByDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate = #{value} </select> <!--根据日期统计预约数,统计指定日期之后的预约数--> <select id="findOrderCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate >= #{value} </select> <!--根据日期统计到诊数--> <select id="findVisitsCountByDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate = #{value} and orderStatus = '已到诊' </select> <!--根据日期统计到诊数,统计指定日期之后的到诊数--> <select id="findVisitsCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate >= #{value} and orderStatus = '已到诊' </select> <!--热门套餐,查询前4条--> <select id="findHotSetmeal" resultType="map"> select s.name, count(o.id) setmeal_count , count(o.id)/(select count(id) from t_order) proportion from t_order o inner join t_setmeal s on s.id = o.setmeal_id group by o.setmeal_id order by setmeal_count desc limit 0,4 </select>MemberDao.xml:<!--根据日期统计会员数,统计指定日期之前的会员数--> <select id="findMemberCountBeforeDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime <= #{value} </select> <!--根据日期统计会员数--> <select id="findMemberCountByDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime = #{value} </select> <!--根据日期统计会员数,统计指定日期之后的会员数--> <select id="findMemberCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime >= #{value} </select> <!--总会员数--> <select id="findMemberTotalCount" resultType="int"> select count(id) from t_member </select>二、. 运营数据统计报表导出2.1 需求分析运营数据统计报表导出就是将统计数据写入到Excel并提供给客户端浏览器进行下载,以便体检机构管理人员对运营数据的查看和存档。2.2 提供模板文件本节我们需要将运营统计数据通过POI写入到Excel文件,对应的Excel效果如下: 通过上面的Excel效果可以看到,表格比较复杂,涉及到合并单元格、字体、字号、字体加粗、对齐方式等的设置。如果我们通过POI编程的方式来设置这些效果代码会非常繁琐。在企业实际开发中,对于这种比较复杂的表格导出一般我们会提前设计一个Excel模板文件,在这个模板文件中提前将表格的结构和样式设置好,我们的程序只需要读取这个文件并在文件中的相应位置写入具体的值就可以了。在本章节资料中已经提供了一个名为report_template.xlsx的模板文件,需要将这个文件复制到health_backend工程的template目录中2.3 完善页面在report_business.html页面提供导出按钮并绑定事件<div class="excelTitle" > <el-button @click="exportExcel">导出Excel</el-button>运营数据统计 </div>methods:{ //导出Excel报表 exportExcel(){ window.location.href = '/report/exportBusinessReport.do'; } }2.4 后台代码在ReportController中提供exportBusinessReport方法,基于POI将数据写入到Excel中并通过输出流下载到客户端。/** * 导出Excel报表 * @return */ @RequestMapping("/exportBusinessReport") public Result exportBusinessReport(HttpServletRequest request, HttpServletResponse response){ try{ //远程调用报表服务获取报表数据 Map<String, Object> result = reportService.getBusinessReport(); //取出返回结果数据,准备将报表数据写入到Excel文件中 String reportDate = (String) result.get("reportDate"); Integer todayNewMember = (Integer) result.get("todayNewMember"); Integer totalMember = (Integer) result.get("totalMember"); Integer thisWeekNewMember = (Integer) result.get("thisWeekNewMember"); Integer thisMonthNewMember = (Integer) result.get("thisMonthNewMember"); Integer todayOrderNumber = (Integer) result.get("todayOrderNumber"); Integer thisWeekOrderNumber = (Integer) result.get("thisWeekOrderNumber"); Integer thisMonthOrderNumber = (Integer) result.get("thisMonthOrderNumber"); Integer todayVisitsNumber = (Integer) result.get("todayVisitsNumber"); Integer thisWeekVisitsNumber = (Integer) result.get("thisWeekVisitsNumber"); Integer thisMonthVisitsNumber = (Integer) result.get("thisMonthVisitsNumber"); List<Map> hotSetmeal = (List<Map>) result.get("hotSetmeal"); //获得Excel模板文件绝对路径 String temlateRealPath = request.getSession().getServletContext().getRealPath("template") + File.separator + "report_template.xlsx"; //读取模板文件创建Excel表格对象 XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(new File(temlateRealPath))); XSSFSheet sheet = workbook.getSheetAt(0); XSSFRow row = sheet.getRow(2); row.getCell(5).setCellValue(reportDate);//日期 row = sheet.getRow(4); row.getCell(5).setCellValue(todayNewMember);//新增会员数(本日) row.getCell(7).setCellValue(totalMember);//总会员数 row = sheet.getRow(5); row.getCell(5).setCellValue(thisWeekNewMember);//本周新增会员数 row.getCell(7).setCellValue(thisMonthNewMember);//本月新增会员数 row = sheet.getRow(7); row.getCell(5).setCellValue(todayOrderNumber);//今日预约数 row.getCell(7).setCellValue(todayVisitsNumber);//今日到诊数 row = sheet.getRow(8); row.getCell(5).setCellValue(thisWeekOrderNumber);//本周预约数 row.getCell(7).setCellValue(thisWeekVisitsNumber);//本周到诊数 row = sheet.getRow(9); row.getCell(5).setCellValue(thisMonthOrderNumber);//本月预约数 row.getCell(7).setCellValue(thisMonthVisitsNumber);//本月到诊数 int rowNum = 12; for(Map map : hotSetmeal){//热门套餐 String name = (String) map.get("name"); Long setmeal_count = (Long) map.get("setmeal_count"); BigDecimal proportion = (BigDecimal) map.get("proportion"); row = sheet.getRow(rowNum ++); row.getCell(4).setCellValue(name);//套餐名称 row.getCell(5).setCellValue(setmeal_count);//预约数量 row.getCell(6).setCellValue(proportion.doubleValue());//占比 } //通过输出流进行文件下载 ServletOutputStream out = response.getOutputStream(); response.setContentType("application/vnd.ms-excel"); response.setHeader("content-Disposition", "attachment;filename=report.xlsx"); workbook.write(out); out.flush(); out.close(); workbook.close(); return null; }catch (Exception e){ return new Result(false, MessageConstant.GET_BUSINESS_REPORT_FAIL,null); } }
上滑加载中
推荐直播
-
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
华为云开发者AI素养直播课·第五期2026/09/04 周五 16:00-18:00
林华鼎-华为云AI开发者运营负责人;蒋春阳-华为云AI开发者案例开发专家
本期直播内容: AI工具体验营 · 第5-8课连讲。Agent-Team 多智能体协作完成毕业设计实践
回顾中 -
华为云开发者AI素养ClassRoom·第六期2026/09/08 周二 19:00-20:00
樊渊-2026华为软件挑战赛冠军
高手来了:看软挑高手解析二维排样问题—从工业难题到算法突破
回顾中
热门标签