携手创造,一起生长!这是我参与「日新计划 8 月更文应战」的第13天,点击检查活动概况

【SpringBoot实战系列】数据报表统计并定时推送用户的手把手教程

本文节选自 《实战演练专题》

【实战系列】数据报表计算并守时推送用户的手把手教程

经过一个小的事务点出发,建立一个能够实例运用的项目工程,将各种常识点串联起来; 实战演练专题中,每一个项目都是能够独立运转的,包含若干常识点,甚至能够不做修改直接应用于出产项目;

今天的实战项目首要处理的事务需求为:每日新增用户计算,生成报表,并邮件发送给相关人

本项目将包含以下常识点:

  • 依据 MySql 的每日新增用户报表计算(怎样计算每日新增用户,若日期不接连怎样主动补 0?)
  • 守时履行报表计算使命
  • MyBatis + MySql 数据操作
  • 邮件发送
  • Thymeleaf 引擎完成报表模板烘托

I. 需求拆解

需求相对来说属于比较明确的了,目的便是完成一个主动报表计算的使命,查询出每日的用户新增情况,然后推送给指定的用户

因此咱们将很明晰的知道,咱们需求干的事情

守时使命

这儿要点放在怎样来支撑这个使命的守时履行,一般来说守时使命会区分为固守时刻履行 + 距离时长履行两种(注意这种区分首要是为了便利理解,如每天五点履行的使命,也能够理解为每隔 24h 履行一次)

前者常见于一次性使命,如本文中的每天计算一次,这种便是相对典型的固守时刻履行的使命;

后者常见于轮询式使命,如常见的应用探活(每隔 30s 发一个 ping 消息,判别服务是否健在)

守时使命的计划十分多,有爱好的小伙伴能够重视一波“一灰灰 blog”公众号,蹲守一个后续

本文将直接选用 Spring 的守时使命完成需求场景,对这块不了解的小伙伴能够看一下我之前的分享的博文

  • 180801-Spring 之守时使命根本运用篇 – 一灰灰 Blog
  • 180803-Spring 守时使命高档运用篇 – 一灰灰 Blog

每日新增用户计算

每日新增用户计算,完成方法挺多的,比如举几个简略的完成思路

  • 依据 redis 的计数器:一天一个 key,当天有新用户时,同步的完成计数器+1
  • 依据数据库,新增一个计算表,包含如日期 + 新增用户数 + 活泼用户数 等字段
    • 有新用户注册时,对应日期的新增用户数,活泼用户数 + 1
    • 老用户今天首次运用时,活泼用户数 + 1

上面两个计划都需求凭借额定的库表来辅助支撑,本文则选用直接计算用户表,依据注册时刻来聚合计算每日的新增用户数

  • 长处:简略,无额定要求,适用于数据量小的场景(比如用户量小于百万的)
  • 缺陷:用户量大时,数据库压力大

关于怎样运用 mysql 进行计算每日新增用户,不了解的小伙伴,引荐参考博主之前的分享文章

    • 220707-MySql 准时、天、周、月进行数据计算 – 一灰灰 Blog

报表生成&推送用户

接下来便是将上面计算的数据,生成报表然后推送给用户;首先是怎样将数据生成报表?其次则是怎样推送给指定用户?

将数据组装成报表的方法一般取决于你挑选的推送方法,如飞书、钉钉之类的,有对应的开发 api,能够直接推送富文本;

本文的完成姿态则挑选的是经过邮件的方法进行发送,why?

  • 飞书、钉钉、微信之类的,需求授权,关于不运用这些作为办公软件的小伙伴没什么意义
  • 短信需求钱….

关于邮件,咱们应该都有,无论是 qq 邮箱,还是作业邮箱;根本上关于想要直接跑本文的小伙伴来说,没有什么额定的门槛

关于 java/spring 怎样运用邮箱,对此不太了解的小伙伴,能够参考博主之前的分享文章

    • 【中间件】SpringBoot 系列之邮件发送姿态介绍 | 一灰灰 Blog

上面文章中介绍的是 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}hui@email.com', '{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: xhhuiblog@163.com
    # 运用自己的发送方用户名 + 授权码填充
    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);
    //邮件发送人,早年面的装备参数中拿,若没有装备,则运用默许的xhhuiblog@163.com
    mimeMessageHelper.setFrom(environment.getProperty("spring.mail.from", "xhhuiblog@163.com"));
    //邮件接纳人,能够是多个
    mimeMessageHelper.setTo("bangzewu@126.com");
    //邮件主题
    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 的中间图

【SpringBoot实战系列】数据报表统计并定时推送用户的手把手教程

打开的内容展现

【SpringBoot实战系列】数据报表统计并定时推送用户的手把手教程

此外,源码除了完成了守时推送之外,也供给了一个 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 怎样向模板中传递上下文,模板语法,静态资源怎样放等等

写到这我自己都惊呆了好么,一篇文章这么多常识点,还有啥好犹豫的,一键三连走起啊,我是一灰灰,这可能是我这个假期内最终一篇实战干货了,马上要开学了,老婆孩子回归之后,后续的更新就靠各位读友的崔更坚持了

本文中一切常识点,都能够在我的个人站点获取,欢迎重视: hhui.top/

阐明:本文一切超链内容主张检查原文获取

III. 不能错失的源码和相关常识点

0. 项目

  • 工程:github.com/liuyueyi/sp…
  • 源码:github.com/liuyueyi/sp…

1. 微信公众号: 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,不免有遗漏和错误之处,如发现 bug 或许有更好的主张,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录一切学习和作业中的博文,欢迎咱们前去逛逛

  • 一灰灰 Blog 个人博客 blog.hhui.top
  • 一灰灰 Blog-Spring 专题博客 spring.hhui.top