本文节选自 《实战演练专题》
通过一个小的业务点出发,搭建一个可以实例使用的项目工程,将各种知识点串联起来; 实战演练专题中,每一个项目都是可以独立运行的,包含若干知识点,甚至可以不做修改直接应用于生产项目;
今天的实战项目主要解决的业务需求为:每日新增用户统计,生成报表,并邮件发送给相关人
本项目将包含以下知识点:
基于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应用,相信这一步大家都很熟悉了;若有不懂的小伙伴,请点赞、评论加博主好友,手把手教你,不收费
最终的项目依赖如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <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. 数据准备
文末的源码包含库表结构,初始化数据,可以直接使用
既然模拟的是从数据库中读取每日新增用户,所以我们准备了一张表
1 2 3 4 5 6 7 8 9 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不熟的小伙伴,可以到博主的站点进补一下,超链 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import datetimedef 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
文件内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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来实现数据库操作,定义一个统计的接口
1 2 3 4 5 6 7 8 9 10 11 12 public interface UserStatisticMapper { List<UserStatisticPo> statisticUserCnt (int days) ; }
接口中定义了一个PO对象,就是我们希望返回的数据,其定义就非常清晰简单了,时间 + 数量
1 2 3 4 5 @Data public class UserStatisticPo { private String day; private Integer count; }
上面定义的知识接口,具体首先,当然是放在mybatis的传统xml文件中,根据前面application.yml配置,我们的xml文件需要放在 resources/mapper
目录下,具体实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?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的思考时间
好的3s时间到,现在公布答案,当某一天一个新增用户都没有的时候,会发生什么事情?会出现这一天的数据空缺,即返回的列表中,少了一天,不连续了,如果前段的小伙伴基于这个列表数据进行绘图,很有可能出现异常
所以出于系统的健壮性考虑(即传说中的鲁棒性),我们希望若某一天没有数据,则对应的计数设置为0
具体的sql说明就不展开了,请查看博文获取更多: MySql按时、天、周、月进行数据统计
5. 报表生成实现 数据统计出来之后,接下来就是基于这些数据来生成我们报表,我们借助Thymleaf来实现,因此先写一个html模板,resources/templates/report.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 <!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对象,我们定义如下
1 2 3 4 5 6 7 8 9 @Data public class StatisticVo { private List<UserStatisticPo> list; private String htmlTitle; private String tableTitle; }
接下来就是拿到数据之后,将它与模板渲染得到我们希望的数据,这里主要借助的是org.thymeleaf.spring5.SpringTemplateEngine
核心实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @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生产环境发送排雷指南,你值得拥有 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void sendMail (String title, String content) throws MessagingException { MimeMessage mimeMailMessage = javaMailSender.createMimeMessage(); MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMailMessage, true ); mimeMessageHelper.setFrom(environment.getProperty("spring.mail.from" , "xhhuiblog@163.com" )); mimeMessageHelper.setTo("bangzewu@126.com" ); mimeMessageHelper.setSubject(title); mimeMessageHelper.setText(content, true ); Thread.currentThread().setContextClassLoader(javax.mail.Message.class .getClassLoader ()) ; javaMailSender.send(mimeMailMessage); }
上面的实现,直接写死了收件人邮箱,即我本人的邮箱,各位大佬在使用的时候,请记得替换一下啊
上面的实现除了发送邮件这个知识点之外,还有一个隐藏的获取配置参数的知识点,即environment#getProperty()
,有兴趣的小伙伴翻博主的站点吧
6. 定时任务 上面几部基本上就把我们的整个任务功能都实现了,从数据库中统计出每日新增用户,然后借助Thymleaf来渲染模板生成报告,然后借助email进行发送
最后的一步,就是任务的定时执行,直接借助Spring的Schedule来完成我们的目标,这里我们希望每天4:15分执行这个任务,如下配置即可
1 2 3 4 5 6 7 8 9 @Scheduled (cron = "0 15 4 * * ?" )public void autoCalculateUserStatisticAndSendEmail () throws MessagingException { StatisticVo vo = statisticAddUserReport(); String content = renderReport(vo); sendMail("新增用户报告" , content); }
7. 测试 最后测试演练一下,启动方法如下,除了基本的启动注解之外,还指定了mapper接口位置,开启定时任务;感兴趣的小伙伴可以试一下干掉这两个注解会怎样,评论给出你的实测结果吧
1 2 3 4 5 6 7 8 @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接口,访问之后直接可以查看报表内容,方便大家调样式,实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 @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
数据库统计每日新增
模板渲染
邮件发送
除了上面比较突出的知识点之外,当然还有其他的,如Spring如何读取配置参数,SpringMVC如何向模板中传递上下文,模板语法,静态资源怎么放等等
写到这我自己都惊呆了好么,一篇文章这么多知识点,还有啥好犹豫的,一键三连走起啊,我是一灰灰,这可能是我这个假期内最后一篇实战干货了,马上要开学了,老婆孩子回归之后,后续的更新就靠各位读友的崔更保持了
本文中所有知识点,都可以在我的个人站点获取,欢迎关注: https://hhui.top/
III. 不能错过的源码和相关知识点 0. 项目
1. 微信公众号: 一灰灰Blog 尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
一灰灰blog
Be the first person to leave a comment!