【SpringBoot 實戰】數據報表統計並定時推送用戶的手把手教程

本文節選自 《實戰演練專題》

【實戰系列】數據報表統計並定時推送用戶的手把手教程

通過一個小的業務點出發,搭建一個可以實例使用的項目工程,將各種知識點串聯起來; 實戰演練專題中,每一個項目都是可以獨立運行的,包含若干知識點,甚至可以不做修改直接應用於生產項目;

今天的實戰項目主要解決的業務需求爲:每日新增用戶統計,生成報表,並郵件發送給相關人

本項目將包含以下知識點:

  • 基於 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 如何讀取配置參數,SpringMVC 如何向模板中傳遞上下文,模板語法,靜態資源怎麼放等等

寫到這我自己都驚呆了好麼,一篇文章這麼多知識點,還有啥好猶豫的,一鍵三連走起啊,我是一灰灰,這可能是我這個假期內最後一篇實戰乾貨了,馬上要開學了,老婆孩子迴歸之後,後續的更新就靠各位讀友的崔更保持了

本文中所有知識點,都可以在我的個人站點獲取,歡迎關注: https://hhui.top/

說明:本文所有超鏈內容建議查看原文獲取

III. 不能錯過的源碼和相關知識點

0. 項目

1. 微信公衆號: 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章