本文節選自 《實戰演練專題》
【實戰系列】數據報表統計並定時推送用戶的手把手教程
通過一個小的業務點出發,搭建一個可以實例使用的項目工程,將各種知識點串聯起來; 實戰演練專題中,每一個項目都是可以獨立運行的,包含若干知識點,甚至可以不做修改直接應用於生產項目;
今天的實戰項目主要解決的業務需求爲:每日新增用戶統計,生成報表,並郵件發送給相關人
本項目將包含以下知識點:
- 基於 MySql 的每日新增用戶報表統計(如何統計每日新增用戶,若日期不連續如何自動補 0?)
- 定時執行報表統計任務
- MyBatis + MySql 數據操作
- 郵件發送
- Thymeleaf 引擎實現報表模板渲染
I. 需求拆解
需要相對來說屬於比較明確的了,目的就是實現一個自動報表統計的任務,查詢出每日的用戶新增情況,然後推送給指定的用戶
因此我們將很清晰的知道,我們需要乾的事情
定時任務
這裏重點放在如何來支持這個任務的定時執行,通常來說定時任務會區分爲固定時刻執行 + 間隔時長執行兩種(注意這種區分主要是爲了方便理解,如每天五點執行的任務,也可以理解爲每隔 24h 執行一次)
前者常見於一次性任務
,如本文中的每天統計一次,這種就是相對典型的固定時刻執行的任務;
後者常見於輪詢式任務
,如常見的應用探活(每隔 30s 發一個 ping 消息,判斷服務是否健在)
定時任務的方案非常多,有興趣的小夥伴可以關注一波“一灰灰 blog”公衆號,蹲守一個後續
本文將直接採用 Spring 的定時任務實現需求場景,對這塊不熟悉的小夥伴可以看一下我之前的分享的博文
每日新增用戶統計
每日新增用戶統計,實現方式挺多的,比如舉幾個簡單的實現思路
- 基於 redis 的計數器:一天一個 key,當天有新用戶時,同步的實現計數器+1
- 基於數據庫,新增一個統計表,包含如日期 + 新增用戶數 + 活躍用戶數 等字段
- 有新用戶註冊時,對應日期的新增用戶數,活躍用戶數 + 1
- 老用戶今日首次使用時,活躍用戶數 + 1
上面兩個方案都需要藉助額外的庫表來輔助支持,本文則採用直接統計用戶表,根據註冊時間來聚合統計每日的新增用戶數
- 優點:簡單,無額外要求,適用於數據量小的場景(比如用戶量小於百萬的)
- 缺點:用戶量大時,數據庫壓力大
關於如何使用 mysql 進行統計每日新增用戶,不熟悉的小夥伴,推薦參考博主之前的分享文章
報表生成&推送用戶
接下來就是將上面統計的數據,生成報表然後推送給用戶;首先是如何將數據生成報表?其次則是如何推送給指定用戶?
將數據組裝成報表的方式通常取決於你選擇的推送方式,如飛書、釘釘之類的,有對應的開發 api,可以直接推送富文本;
本文的實現姿勢則選擇的是通過郵件的方式進行發送,why?
- 飛書、釘釘、微信之類的,需要授權,對於不使用這些作爲辦公軟件的小夥伴沒什麼意義
- 短信需要錢....
對於郵件,大家應該都有,無論是 qq 郵箱,還是工作郵箱;基本上對於想要直接跑本文的小夥伴來說,沒有什麼額外的門檻
關於 java/spring 如何使用郵箱,對此不太熟悉的小夥伴,可以參考博主之前的分享文章
上面文章中介紹的是 FreeMaker 來實現模板渲染,本文則介紹另外一個知識點,藉助 Thymleaf 來實現數據報表的生成 (一篇文章獲取這麼多知識點,就問你開不開心 O(∩_∩)O)
II. 分佈實現
1. 項目搭建
首選搭建一個基本的 SpringBoot 應用,相信這一步大家都很熟悉了;若有不懂的小夥伴,請點贊、評論加博主好友,手把手教你,不收費
最終的項目依賴如下
<dependencies>
<!-- 郵件發送的核心依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
別看上面好像依賴了不少包,實際上各有用處
spring-boot-starter-web
: 提供 web 服務spring-boot-starter-mail
: 發郵件就靠它mybatis-spring-boot-starter
: 數據庫操作
我們的用戶存在 mysql 中,這裏使用 mybatis 來實現 db 操作(又一個知識點來了,收好不謝)
2. 數據準備
文末的源碼包含庫表結構,初始化數據,可以直接使用
既然模擬的是從數據庫中讀取每日新增用戶,所以我們準備了一張表
CREATE TABLE `u1` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`name` varchar(64) NOT NULL DEFAULT '' COMMENT 'name',
`email` varchar(512) NOT NULL DEFAULT '' COMMENT 'email',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生成時間',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='u1測試';
接下來準備寫入一些數據;爲了模擬某些天沒有新增用戶,貼心的一灰灰博主給大家提供基於 python 的數據生成腳本,源碼如下 (python3+,對 python 不熟的小夥伴,可以到博主的站點進補一下,超鏈)
import datetime
def create_day_time(n):
now = datetime.datetime.now()
now = now - datetime.timedelta(days = n)
return now.strftime("%Y-%m-%d %H:%S:%M")
vals = []
for i in range(0, 100):
if (i % 32 % 6) == 0:
# 模擬某一天沒有用戶的場景
continue
vals.append(f"('{i}_灰灰', '{i}[email protected]', '{create_day_time(i % 32)}', '{create_day_time(i % 32)}')")
values = ',\n\t'.join(vals)
sqls = f"INSERT INTO story.u1 (name, email, create_time, update_time) VALUES \n{values};"
print(sqls)
3. 全局配置
數據準備完畢之後,接下來配置一下 db、email 相關的參數
resources/application.yml
文件內容如下
spring:
#郵箱配置
mail:
host: smtp.163.com
from: [email protected]
# 使用自己的發送方用戶名 + 授權碼填充
username:
password:
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
datasource:
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
thymeleaf:
mode: HTML
encoding: UTF-8
servlet:
content-type: text/html
cache: false
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.git.hui.demo.report.dao.po
上面的配置分爲三類
- 數據庫相關:連接信息,用戶名密碼, mybatis 配置
- thymleaf:模板渲染相關
- email: 郵箱配置相關,請注意若使用博主的源碼,在本地運行時,請按照前面介紹的郵箱博文中手把手的教程,獲取您自己的郵箱授權信息,填在上面的 username, password 中
4. 數據報表統計實現
接下來就正式進入大家喜聞樂見的編碼實現環節,我們直接使用 mybaits 來實現數據庫操作,定義一個統計的接口
/**
* @author YiHui
*/
public interface UserStatisticMapper {
/**
* 統計最近多少天內的新增用戶數
*
* @param days 統計的天數,從當前這一天開始
* @return
*/
List<UserStatisticPo> statisticUserCnt(int days);
}
接口中定義了一個 PO 對象,就是我們希望返回的數據,其定義就非常清晰簡單了,時間 + 數量
@Data
public class UserStatisticPo {
private String day;
private Integer count;
}
上面定義的知識接口,具體首先,當然是放在 mybatis 的傳統 xml 文件中,根據前面 application.yml 配置,我們的 xml 文件需要放在 resources/mapper
目錄下,具體實現如下
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.git.hui.demo.report.dao.UserStatisticMapper">
<resultMap id="countMap" type="com.git.hui.demo.report.dao.po.UserStatisticPo">
<result column="day" property="day"/>
<result column="count" property="count"/>
</resultMap>
<!-- 統計用戶新增 -->
<select id="statisticUserCnt" resultMap="countMap">
SELECT date_table.day as `day`, IFNULL(data.cnt, 0) as `count`
from
(select DATE_FORMAT(create_time, '%Y-%m-%d') day, count(id) cnt from u1 GROUP BY day) data
right join
(SELECT @date := DATE_ADD(@date, interval - 1 day) day from (SELECT @date := DATE_ADD(CURDATE(), interval 1 day) from u1) days limit #{days}) date_table
on date_table.day = data.day
</select>
</mapper>
重點看一下上面的 sql 實現,爲什麼會一個 join 邏輯?
那我們稍稍思考,若我們直接通過日期進行 format 之後,再 group 一下統計計數,會有什麼問題?給大家 3s 的思考時間
- 1s
- 2s
- 3s
好的 3s 時間到,現在公佈答案,當某一天一個新增用戶都沒有的時候,會發生什麼事情?會出現這一天的數據空缺,即返回的列表中,少了一天,不連續了,如果前段的小夥伴基於這個列表數據進行繪圖,很有可能出現異常
所以出於系統的健壯性考慮(即傳說中的魯棒性),我們希望若某一天沒有數據,則對應的計數設置爲 0
具體的 sql 說明就不展開了,請查看博文獲取更多: MySql 按時、天、周、月進行數據統計
5. 報表生成實現
數據統計出來之後,接下來就是基於這些數據來生成我們報表,我們藉助 Thymleaf 來實現,因此先寫一個 html 模板,resources/templates/report.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${vo.htmlTitle}">每日用戶統計</title>
</head>
<style>
.title22 {
font: 16px/24px bold;
position: relative;
display: block;
padding: 0 6px;
margin-left: -6px;
margin-bottom: 12px;
font-size: 22px;
font-weight: 550;
}
.container {
background: #fff;
overflow: auto;
padding: 6px;
margin: 6px;
font-family: 'Microsoft YaHei UI', 'Microsoft YaHei', '微軟雅黑', SimSun, '宋體';
}
.content {
overflow: auto;
padding: 6px 12px;
margin: 6px;
}
table {
border: none;
border-collapse: collapse;
table-layout: fixed;
}
.thead {
font: 14px/20px bold;
font-weight: 550;
background: #eaeaea;
line-height: 1.5em;
}
.tbody {
font: 15px/20px normal;
font-weight: 540;
background: #fff;
}
tr > td {
padding: 6px 12px;
border: 1px solid #d8d8d8;
max-width: 600px;
}
</style>
<body>
<div class="container">
<div class="content">
<div class="title22" style="color: red;" th:text="${vo.tableTitle}">統計標題</div>
<table>
<thead class="thead">
<tr>
<td class="thead" style="background:#eaeaea;">日期</td>
<td style="min-width: 50px; color: #4040e1">新增用戶</td>
</tr>
</thead>
<tbody class="tbody">
<tr th:each="item: ${vo.list}">
<td class="thead" style="background:#eaeaea;" th:text="${item.day}">2022-08-01</td>
<td style="min-width: 50px; color: #4040e1" th:text="${item.count}">1</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
一個非常簡單的 table 模板,需要接收三個數據,與之對應的 vo 對象,我們定義如下
@Data
public class StatisticVo {
// 表格數據項,即日期 + 數量的列表
private List<UserStatisticPo> list;
// 網頁的標題
private String htmlTitle;
// 表格標題
private String tableTitle;
}
接下來就是拿到數據之後,將它與模板渲染得到我們希望的數據,這裏主要藉助的是org.thymeleaf.spring5.SpringTemplateEngine
核心實現如下
@Service
public class StatisticAndReportService {
@Autowired
private UserStatisticMapper userStatisticMapper;
@Autowired
private JavaMailSender javaMailSender;
@Autowired
private Environment environment;
@Autowired
private SpringTemplateEngine templateEngine;
public StatisticVo statisticAddUserReport() {
List<UserStatisticPo> list = userStatisticMapper.statisticUserCnt(30);
StatisticVo vo = new StatisticVo();
vo.setHtmlTitle("每日新增用戶統計");
vo.setTableTitle(String.format("【%s】新增用戶報表", LocalDate.now()));
vo.setList(list);
return vo;
}
public String renderReport(StatisticVo vo) {
Context context = new Context();
context.setVariable("vo", vo);
String content = templateEngine.process("report", context);
return content;
}
}
模板渲染就一行templateEngine.process("report", context)
,第一個參數爲模板名,就是上面的 html 文件名(對於模板文件、靜態資源怎麼放,放在那兒,這個知識點當然也可以在一灰灰的站點獲取,超鏈)
第二個參數用於封裝上下文,傳遞模板需要使用的參數
5. 郵件發送
報表生成之後,就是將它推送給用戶,我們這裏選定的是郵箱方式,具體實現也比較簡單,但是在最終部署到生產環境(如阿里雲服務器時,可能會遇到坑,同樣明顯的知識點,博主會沒有分享麼?當然不會沒有了,Email 生產環境發送排雷指南,你值得擁有)
/**
* 發送郵件的邏輯
*
* @param title
* @param content
* @throws MessagingException
*/
public void sendMail(String title, String content) throws MessagingException {
MimeMessage mimeMailMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMailMessage, true);
//郵件發送人,從前面的配置參數中拿,若沒有配置,則使用默認的[email protected]
mimeMessageHelper.setFrom(environment.getProperty("spring.mail.from", "[email protected]"));
//郵件接收人,可以是多個
mimeMessageHelper.setTo("[email protected]");
//郵件主題
mimeMessageHelper.setSubject(title);
//郵件內容
mimeMessageHelper.setText(content, true);
// 解決linux上發送郵件時,拋出異常 JavaMailSender no object DCH for MIME type multipart/mixed
Thread.currentThread().setContextClassLoader(javax.mail.Message.class.getClassLoader());
javaMailSender.send(mimeMailMessage);
}
上面的實現,直接寫死了收件人郵箱,即我本人的郵箱,各位大佬在使用的時候,請記得替換一下啊
上面的實現除了發送郵件這個知識點之外,還有一個隱藏的獲取配置參數的知識點,即environment#getProperty()
,有興趣的小夥伴翻博主的站點吧
6. 定時任務
上面幾部基本上就把我們的整個任務功能都實現了,從數據庫中統計出每日新增用戶,然後藉助 Thymleaf 來渲染模板生成報告,然後藉助 email 進行發送
最後的一步,就是任務的定時執行,直接藉助 Spring 的 Schedule 來完成我們的目標,這裏我們希望每天 4:15 分執行這個任務,如下配置即可
// 定時發送,每天4:15分統計一次,發送郵件
@Scheduled(cron = "0 15 4 * * ?")
// 下上面這個是每分鐘執行一次,用於本地測試
// @Scheduled(cron = "0/1 * * * * ?")
public void autoCalculateUserStatisticAndSendEmail() throws MessagingException {
StatisticVo vo = statisticAddUserReport();
String content = renderReport(vo);
sendMail("新增用戶報告", content);
}
7. 測試
最後測試演練一下,啓動方法如下,除了基本的啓動註解之外,還指定了 mapper 接口位置,開啓定時任務;感興趣的小夥伴可以試一下幹掉這兩個註解會怎樣,評論給出你的實測結果吧
@EnableScheduling
@MapperScan(basePackages = "com.git.hui.demo.report.dao")
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
當然我再實際測試的時候,不可能真等到早上四點多來看是否執行,大晚上還是要睡覺的;因此本地測試的時候,可以將上面定時任務改一下,換成每隔一分鐘執行一次
接一個 debug 的中間圖
打開的內容展示
此外,源碼除了實現了定時推送之外,也提供了一個 web 接口,訪問之後直接可以查看報表內容,方便大家調樣式,實現如下
@Controller
public class StatisticReportRest {
@Autowired
private StatisticAndReportService statisticAndReportSchedule;
@GetMapping(path = "report")
public String view(Model model) {
StatisticVo vo = statisticAndReportSchedule.statisticAddUserReport();
model.addAttribute("vo", vo);
return "report";
}
}
8.一灰灰的乾貨總結
最後進入一灰灰的保留環節,這麼“大”一個項目坐下來的,當然是得好好盤一盤它的知識點了,前面的各小節內容中有穿插的指出相應的知識點,接下來如雨的知識點將迎面襲來,不要眨眼
- Spring 定時任務@Schedule
- 怎麼用? -> 180801-Spring 之定時任務基本使用篇 - 一灰灰 Blog
- 多個任務串行並行,是否會相互影響?自定義線程池怎麼整?一個異常會影響其他麼?-> Spring 定時任務高級使用篇 - 一灰灰 Blog
- 數據庫統計每日新增
- mysql 直接統計日新增,sql 怎麼寫? 時間不連續,如何規避? -> MySql 按時、天、周、月進行數據統計 - 一灰灰 Blog
- mybatis 操作 db 怎麼玩? -> Mybatis 系列教程
- 模板渲染
- 數據報表生成,直接字符串拼接?還是模板引擎的渲染?
- 更多的 spring web 知識點 -> 一灰灰的 SpringWeb 專欄 | 免費
- 郵件發送
- 怎麼發郵件? -> SpringBoot 無障礙使用郵箱服務
- 如何避免上線不採坑 -> Email 生產環境發送排雷指南,你值得擁有
除了上面比較突出的知識點之外,當然還有其他的,如 Spring 如何讀取配置參數,SpringMVC 如何向模板中傳遞上下文,模板語法,靜態資源怎麼放等等
寫到這我自己都驚呆了好麼,一篇文章這麼多知識點,還有啥好猶豫的,一鍵三連走起啊,我是一灰灰,這可能是我這個假期內最後一篇實戰乾貨了,馬上要開學了,老婆孩子迴歸之後,後續的更新就靠各位讀友的崔更保持了
本文中所有知識點,都可以在我的個人站點獲取,歡迎關注: https://hhui.top/
說明:本文所有超鏈內容建議查看原文獲取
III. 不能錯過的源碼和相關知識點
0. 項目
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源碼:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/431-mail-report
1. 微信公衆號: 一灰灰 Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰 Blog 個人博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 專題博客 http://spring.hhui.top