Java[工作小总结] 使用策略分页批量按天统计定时统计几千万级某消费记录大表(一))

前言

主要描述为了解决表的数据量比较大的时候,按天统计(按月或者按周同逻辑)一个表里面的多个类型做一个类型统计到另一张表,以方便升效率。
例如:表(例如消费记录)的某个字段 type(有多个类型):1:买菜 2:买肉 3:买海鲜 4. 买家具等等这样
原表(当数据大于几W多的时候或者亿W多的时候,查询运营在后台查询很慢,等待的时间较长,虽然有设置时间三个月内去查询,所以运营人员提出要做一次统计,这样他门就不用耗费太多时间去三个月内去查询并手动统计,所以这次的做法是将原表的数据统计沉淀在新表里面,并对某消费记录类型做按天统计。

CREATE TABLE `order_deatil` (
 `id` bigint(20) NOT NULL,
 `order_no` varchar(64) DEFAULT NULL COMMENT '订单详情id',
 `pay_member_id` varchar(64) DEFAULT NULL COMMENT '消费的用户',
 `pay_member_no` varchar(32) DEFAULT NULL COMMENT '消费会员的id',
 `pay_type` varchar(16) DEFAULT NULL COMMENT '消费类型',
 `consume_code` varchar(128) DEFAULT NULL COMMENT '消费功能编码',
 `create_date` datetime(3) DEFAULT NULL COMMENT '创建日期',
 `del_flag` tinyint(1) DEFAULT '0',
 PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单详情表';

新表(把原表要统计的类统计到新表上,便于快速查询)

CREATE TABLE `order_deatil_statistics_daily` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '数据ID',
  `buy_vegetables` int(11) DEFAULT '0' COMMENT '买菜',
  `buy_meat` int(11) DEFAULT '0' COMMENT '买肉',
  `buy_seafood` int(11) DEFAULT '0' COMMENT '买海鲜',
  `buy_furniture` int(11) DEFAULT '0' COMMENT '买家具',
  `create_date` datetime DEFAULT NULL COMMENT '创建时间',
  `update_date` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` tinyint(1) DEFAULT '0',
  PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='每天统计订单详情表';

一、涉及技术点和注意点

  1. 技术: Springboot + mybatis + mysql + quartz + Swagger
  2. 设计模式:策略模式
  3. 定时任务:应该要每天凌晨或什么时候统计(注意有没有跨天的问题)
  4. 按天查询,每次分页查询 例如:每页500条或更少,以此来减少对数据查询的压力(必要时候还要加上索引等)

二、搭建项目

简单搭建:Springboot + mybatis + mysql + quartz +Swagger

(1).附上pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- rabbitMQ -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>


        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>

        <!-- 引入Lombock依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

(2).application.yml配置

server:
  port: 8083

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://(ip地址):3306/(数据库名称)?characterEncoding=utf-8&useSSL=false
    username: 数据账号
    password: 数据密码

mybatis:
    mapper-locations: classpath:mapper/*.xml
    type-aliases-package: com.mi.entity
    configuration:
      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 配置mybatis log plugin
      map-underscore-to-camel-case: true
logging:
  level:
     com: debug

(3). SpringbootApplication 启动类

@SpringBootApplication
@MapperScan("com.xx.xx")//使用MapperScan批量扫描所有的Mapper接口;
public class SpringbootDemo1Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemo1Application.class, args);
    }
    /**
     * 需要加上这个上下文,以便后续使用策略模式的时候通过对应的容器加载对应的类
     * @return
     */
    @Bean
    public SpringContextHolder springContextHolder() {
        SpringContextHolder springContextHolder = new SpringContextHolder();
        return springContextHolder;
    }
}

三、config 配置包下准备

(1).SwaggerConfig (暂未体现用上)

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .pathMapping("/")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.mi.controller"))
                .paths(PathSelectors.any())
                .build().apiInfo(new ApiInfoBuilder()
                        .title("SpringBoot API 文档")
                        .description("SpringBoot整合Swagger,详细信息......")
                        .version("9.0")
                        .build());
    }
}

(2). QuartzConfig

@Component
@Configuration      //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling   // 2.开启定时任务
@Slf4j
public class QuartzConfig {

    @Autowired
    private OrderDeatilDao orderDeatilDao;

    @Autowired
    private OrderDeatilStatisticsDailyService orderDeatilStatisticsDailyService;

 @Autowired
    private OrderDeatilStatisticsDailyDao orderDeatilStatisticsDailyDao;

   // 3.添加定时任务 需求:每天凌晨0点执行一次,统计昨天的数据,以此类推
    @Scheduled(cron = "0 0 2 * * ?")
    //或直接指定时间间隔,例如:5秒
    //@Scheduled(fixedRate=5000)
    private void dayliyStatistcsTasks() {
        System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
        log.info(" ========== dayliyStatistcsTasks start ============");
        OrderDeatilDTO deatilDTO = new OrderDeatilDTO();
        //获取开始时间
        long startTime=System.currentTimeMillis();
        // 开始时间 按 00:00:00开始 例如  2021-09-01 00:00:00
        deatilDTO.setBeginDate(DateTool.getStartOfDay(DateUtils.addDays(new Date(),-4)));
        // 结束时间 按 23:59:59:59 结束   2021-09-03 23:59:59:59
         deatilDTO.setEndDate(DateTool.getEndOfDay(DateUtils.addDays(new Date(),-4)));
        log.info("   beginDate = {}   , endDate = {}  ", deatilDTO.getBeginDate(),deatilDTO.getEndDate());
        List<OrderDeatil>  orderDeatils = null;
        // 手动分页 当数据量大的时候每页按500条去分页去查
        PageInfo pageInfo = new PageInfo(0,500);
        do {
            pageInfo.setOffset((pageInfo.getPageNo() - 1) * pageInfo.getPageSize());
            orderDeatils = orderDeatilDao.findOrderDeatilByDay(pageInfo,deatilDTO);
            pageInfo.setPageNo(pageInfo.getPageNo() + 1);
            // 处理每天消费统计
            orderDeatilStatisticsDailyService.dayStatistcs(orderDeatils);
        }while (CollectionUtils.isNotEmpty(orderDeatils));

        // 处理当天查询数据已经存在了没, 没有就插入统计当天的日期
        String today = DateUtil.formatDate(deatilDTO.getBeginDate(), BaseConstant.DATE_FORMAT_YYYY_MM_DD);
        OrderDeatilStatisticsDaily daily = orderDeatilStatisticsDailyService.getOrderDeatilStatisticsDailyById(today);
        if (daily == null){
            OrderDeatilStatisticsDaily statisticsDaily = new OrderDeatilStatisticsDaily();
            statisticsDaily.setCreateDate(deatilDTO.getBeginDate());
            statisticsDaily.setUpdateDate(new Date());
            orderDeatilStatisticsDailyDao.save(statisticsDaily);
        }
        //计算耗时结束时间
        long endTime=System.currentTimeMillis();
        log.info(" ==========   dayliyStatistcsTasks end... ========== ");
        log.info("============== the  dayliyStatistcsTasks   spend = {}  ms  ============== ",(endTime-startTime));

    }
}

四、constant 包

(1) . 时间常量

public interface BaseConstant {

    String DATE_FORMAT_YYYY_MM_DD = "yyyy-MM-dd";
    String DATE_FORMAT_YYYY_MM = "yyyy-MM";
    String DATE_FORMAT_YEAR_MONTH= "yyyyMM";
}

(2). 类型常量(用于策略模式声明的常量)

public class TypeConstant {
    //  买菜
    public static final String TYPE_BUY_VEGETABLES = "vegetables";
    //  买肉
    public static final String TYPE_BUY_MEAT ="meat";
    // 买海鲜
    public static final String TYPE_BUY_SEAFOOD = "seafood";
     // 买家具
    public static final String TYPE_BUY_FURNITURE = "furniture";
    
}

五、dao包(Mapper)

1.1. 原表数据Mapper(只需要分页查询就行)

(1). dao接口

public interface OrderDeatilDao {


    /**
     * @Description: 查询当天的条数据
     * @param beginDate 开始时间  以  2021-11-19 00:00:00 
     * @param overDate  结束时间  以  2021-11-19 23:59:59               
     * @return OrderDeatil
     */
    List<OrderDeatil> findOrderDeatilList (@Param("beginDate") String beginDate, @Param("overDate") String overDate, @Param("pageInfo")PageInfo pageInfo);

    
    /**
     * 分页查询返回集合  按天以每页500调或更少条数查询 ,以此减少对数据量查询时间的压力
     * @param orderDeatilDTO   参数,开始时间和结束时间
     * @param pageInfo 分页  
     * @return
     */
    List<OrderDeatil> findOrderDeatilByDay(@Param("pageInfo") PageInfo pageInfo,@Param("deatilDTO")OrderDeatilDTO orderDeatilDTO);
}

(2). mapper.xml SQL文件

<mapper namespace="com.xx.xx.OrderDeatilDao">
       <!--通过实体作为筛选条件查询-->
    <select id="findOrderDeatilList" resultType="com.mi.entity.OrderDeatil">
        select
          id, order_no, pay_member_id, pay_type, actual_price,create_date, del_flag
        from order_deatil
        <where>
            <if test="id != null">
                and id = #{id}
            </if>
            <if test="createDate != null">
                and create_date = #{createDate}
            </if>
        </where>
    </select>
    <select id="findOrderDeatilByDay" resultType="com.mi.entity.OrderDeatil">
       select
          id, order_no,pay_member_id,pay_type,actual_price,create_date, del_flag
        from order_deatil
        where del_flag = 0
        <if test="deatilDTO.beginDate != null">
            and create_date &gt;= #{deatilDTO.beginDate}
        </if>
        <if test="deatilDTO.endDate != null">
            and create_date &lt;= #{deatilDTO.endDate}
        </if>
        order by create_date
        limit ${pageInfo.offset},${pageInfo.pageSize}
    </select>
</mapper>

1.2. 原表统计到新表的Mapper

(1). 接口

public interface OrderDeatilStatisticsDailyDao {
    /**
     * @Description: 查询今天统计的数据是否存在
     * @param today 当天时间
     * @return 实例对象
     */
    OrderDeatilStatisticsDaily getOrderDeatilStatisticsDailyById(@Param("today") String  today);

    /**
     * @Description: 新增数据
     * @param orderDeatilStatisticsDaily 实例对象
     */
    void save(OrderDeatilStatisticsDaily orderDeatilStatisticsDaily);

    /**
     * @Description: 更新统计数据
     * @param orderDeatilStatisticsDaily 实例对象
     */
    void update(OrderDeatilStatisticsDaily orderDeatilStatisticsDaily);
}

(2) mapper.xml SQL文件

<mapper namespace="com.mi.dao.OrderDeatilStatisticsDailyDao">

<!--查询单个-->
<select id="getOrderDeatilStatisticsDailyById" resultType="com.mi.entity.OrderDeatilStatisticsDaily">
    select
      id, buy_vegetables, buy_meat, buy_seafood, buy_furniture, create_date, update_date, del_flag
    from modular.order_deatil_statistics_daily
    where  DATE_FORMAT(create_date, '%Y-%m-%d') = #{today}
</select>

<!--新增所有列-->
<insert id="save" keyProperty="id" useGeneratedKeys="true">
    insert into order_deatil_statistics_daily(buy_vegetables, buy_meat, buy_seafood, buy_furniture, create_date, update_date, del_flag)
    values (#{buyVegetables}, #{buyMeat}, #{buySeafood}, #{buyFurniture}, #{createDate}, #{updateDate}, #{delFlag})
</insert>

<!--通过主键修改数据-->
<update id="update">
    update order_deatil_statistics_daily
    <set>
            buy_vegetables = #{buyVegetables} + buy_vegetables,
            buy_meat = #{buyMeat} + buy_meat ,
            buy_seafood = #{buySeafood} + buy_seafood,
            buy_furniture = #{buyFurniture} + buy_furniture,
            update_date = #{updateDate},
    </set>
    where id = #{id}
</update>

</mapper>

六、枚举类型(重点,通过查询数据对应的类型获取对应上下文的类)

public enum TypeEnum {
    // 买菜
    VEGETABLES_TYPE(TypeConstant.TYPE_BUY_VEGETABLES,"vegetablesStatisticsRealm"),
    // 买海鲜
    SEAFOOD_TYPE(TypeConstant.TYPE_BUY_SEAFOOD,"seaFoodStatisticsRealm"),
    // 买肉
    MEAT_TYPE(TypeConstant.TYPE_BUY_MEAT,"meatStatisticsRealm"),
    // 家具
    FURNITURE_TYPE(TypeConstant.TYPE_BUY_FURNITURE,"furnitureStatisticsRealm"),
    ;
    private final String code;
    private final String type;
    TypeEnum(String code, String type) {
        this.code = code;
        this.type = type;
    }

    public static String getRealmByType(String code){
        for (TypeEnum codeEnum: values()){
            if (codeEnum.code.equals(code)){
                return codeEnum.type;
            }
        }
        return  null;
    }
}

七、实体类

(1). 订单详情表原表实体类

public class OrderDeatil   implements Serializable {
    private static final long serialVersionUID = 665390835869509423L;
    /**
    * 数据ID
    */
    private Integer id;
    /**
    * 订单id
    */
    private String orderNo;
    /**
    * 消费的用户
    */
    private String payMemberId;
    /**
    * 消费类型
    */
    private String payType;
    /**
    * 创建日期
    */
    private Date createDate;

    /**
     * 消费的点数、价格
     */
    private Integer actualPrice;

    private Boolean delFlag;
....

(2). DTO 参数实体类

@Data
public class OrderDeatilDTO {

    private Date beginDate;    //创建开始时间

    private Date endDate;      //创建结束时间
}

(3). 从原表统计到新的实体类

public class OrderDeatilStatisticsDaily  implements Serializable {
    private static final long serialVersionUID = 698225930974140612L;
    /**
    * 数据ID
    */
    private Integer id;
    /**
    * 买菜
    */
    private Integer buyVegetables;
    /**
    * 买肉
    */
    private Integer buyMeat;
    /**
    * 买海鲜
    */
    private Integer buySeafood;
    /**
    * 买家具
    */
    private Integer buyFurniture;
    /**
    * 创建时间
    */
    private Date createDate;
    /**
    * 更新时间
    */
    private Date updateDate;
    /**
     * 消费的点数、价格
     */
    private Integer actualPrice;
    private Boolean delFlag;
....

八、策略模式的应用

(1). 新建一个接口类,声明要实现的方法,用来处理不同统计类型的方法入口

#public interface TypeStatisticsRealm {
    /**
     * 处理统计接口方法
     * @param statisticsDaily  存放统计后的实体类
     * @param orderDeatilList  获取集合第一条数据的时间、等
     * @return
     */
    OrderDeatilStatisticsDaily handleOrderDetail(OrderDeatilStatisticsDaily statisticsDaily, List<OrderDeatil> orderDeatilList);

}

(2). 声明抽象类实现,主要有两件事

 第一件事:实现接口的方法 handleOrderDetail
 第二件事:声明一个统计不同类型的方法,以便其它类去继承
public abstract class TypeStatisticsAbstractRealm   implements TypeStatisticsRealm 
    /**
     * 处理统计方法入口
     * @param statisticsDaily  存放统计后的实体类
     * @param orderDeatilList  获取集合第一条数据的时间、等
     * @return
     */
    @Override
    public OrderDeatilStatisticsDaily handleOrderDetail(OrderDeatilStatisticsDaily statisticsDaily, List<OrderDeatil> orderDeatilList) {
        int totalPoint = 0;
        if (CollectionUtils.isEmpty(orderDeatilList)){
            return statisticsDaily;
        }
        // 获取集合按类型分组的第一条数据
        OrderDeatil orderDeatil = orderDeatilList.get(0);
        // 获取统计的时间
        statisticsDaily.setCreateDate(orderDeatil.getCreateDate());
        statisticsDaily.setUpdateDate(new Date());
        // 统计不同类型的总点数
        totalPoint = orderDeatilList.stream().mapToInt(OrderDeatil::getActualPrice).sum();
        // 针对 不同  payType 方法 , 外遍历的时候就会利用策略模式将类注入容器里面,然后根据payType调用不同的方法
        statisticsDaily = this.handleDailyStatistics(totalPoint,statisticsDaily);

        return statisticsDaily;
    }

    /**
     * 将处理不同的类型的统计存在实体类里面
     * @param totalPoint   一天统计的总点数
     * @param statisticsDaily  统计的实体类
     * @return
     */
    protected OrderDeatilStatisticsDaily handleDailyStatistics(int totalPoint,OrderDeatilStatisticsDaily statisticsDaily){
        throw new AbstractMethodError();
    }
}

(3) . 声明策略模式对应类型的子类去继承TypeStatisticsAbstractRealm

1.1 蔬菜类

@Component
public class VegetablesStatisticsRealm  extends TypeStatisticsAbstractRealm{
    @Override
    protected OrderDeatilStatisticsDaily handleDailyStatistics(int totalPoint, OrderDeatilStatisticsDaily statisticsDaily) {
        statisticsDaily.setBuyVegetables(totalPoint);
        return  statisticsDaily;
    }
}

1.2 买肉类

@Component
public class MeatStatisticsRealm extends TypeStatisticsAbstractRealm{
    @Override
    protected OrderDeatilStatisticsDaily handleDailyStatistics(int totalPoint, OrderDeatilStatisticsDaily statisticsDaily) {
        statisticsDaily.setBuyMeat(totalPoint);
        return  statisticsDaily;
    }
}

1.3 买海鲜类

@Component
public class SeaFoodStatisticsRealm extends TypeStatisticsAbstractRealm {
    @Override
    protected OrderDeatilStatisticsDaily handleDailyStatistics(int totalPoint, OrderDeatilStatisticsDaily statisticsDaily) {
        statisticsDaily.setBuySeafood(totalPoint);
        return statisticsDaily;
    }
}

1.4 买家具类

@Component
public class FurnitureStatisticsRealm extends TypeStatisticsAbstractRealm{
    @Override
    protected OrderDeatilStatisticsDaily handleDailyStatistics(int totalPoint, OrderDeatilStatisticsDaily statisticsDaily) {
        statisticsDaily.setBuyFurniture(totalPoint);
        return  statisticsDaily;
    }
}

九、Service

(1). 原表订单详情 Service

public interface OrderDeatilService {
    /**
     * @Description: 查询多条数据
     * @param beginDate 开始时间
     * @param overDate  结束时间
     * @param pageInfo  分页
     * @return
     */
    List<OrderDeatil> findOrderDeatilList( String beginDate,String overDate,PageInfo pageInfo);
}

(2). 将原表统计新表的方法以及处理统计service

public interface OrderDeatilStatisticsDailyService {
    /**
     * 通过当天查询数据
     * @param today 当天统计昨天的时间 例如:2021-11-19 --》》 2021-11-18
     * @return
     */
    OrderDeatilStatisticsDaily getOrderDeatilStatisticsDailyById(String  today);
    /**
     * 按天消费统计接口入口
     * @param
     * @param orderDeatils
     */
    void dayStatistcs(List<OrderDeatil> orderDeatils);
    /**
     * 按天处理保存接口过程
     * @param value
     */
    void daySave(List<OrderDeatil> value);
    /**
     * 按天保存或更新数据
     * @param statisticsDaily
     */
    void saveDayStatisticsData(OrderDeatilStatisticsDaily statisticsDaily);
    /**
     * 保存统计方法入口
     * @param statisticsDaily
     * @return
     */
    OrderDeatilStatisticsDaily handleStatisticsInfo(OrderDeatilStatisticsDaily statisticsDaily);
    /**
     * 保存或更新
     * @param statisticsDaily
     */
    void saveOrUpdate(OrderDeatilStatisticsDaily statisticsDaily);
}

十、Impl

(1). 原表订单详情 Service实现类

@Service("orderDeatilService")
public class OrderDeatilServiceImpl implements OrderDeatilService {
    @Autowired
    private OrderDeatilDao orderDeatilDao;
    /**
     *
     * @param beginDate 开始时间
     * @param overDate  结束时间
     * @param pageInfo  分页
     * @return
     */
    @Override
    public List<OrderDeatil> findOrderDeatilList (String beginDate, String overDate, PageInfo pageInfo) {
        List<OrderDeatil> list=this.orderDeatilDao.findOrderDeatilList(beginDate,overDate,pageInfo);
        return list;
    }
}

(2). 将原表统计新表的方法以及处理统计实现类

(重点方法:dayStatistcs,daySave)
daySave:里面的

            //   核心代码:通过查询数据的pay_type类型,然后按天和类型分组,接着通过key获取对应注入实例
            String realmName = TypeEnum.getRealmByType(entry.getKey());
            if (realmName != null){
               //    核心代码:通过 不同的pay_type类型注入子类     
                TypeStatisticsRealm realm = SpringContextHolder.getBean(realmName);
                //    核心代码:通过对不同的pay_type类型,使用不同的策略类型处理总点数
                statisticsDaily = realm.handleOrderDetail(statisticsDaily,entry.getValue());
 .......
@Service("orderDeatilStatisticsDailyService")
@Slf4j
public class OrderDeatilStatisticsDailyServiceImpl implements OrderDeatilStatisticsDailyService {

    @Autowired
    private OrderDeatilStatisticsDailyDao orderDeatilStatisticsDailyDao;
    /**
     * 查询当天数据是否存在
     * @param today 统计昨天的天数
     * @return
     */
    @Override
    public OrderDeatilStatisticsDaily getOrderDeatilStatisticsDailyById(String today) {
        return this.orderDeatilStatisticsDailyDao.getOrderDeatilStatisticsDailyById(today);
    }
    /**
     * 对原表集合按天分组
     * @param orderDeatils
     */
    @Override
    @Transactional
    public void dayStatistcs(List<OrderDeatil> orderDeatils) {
        log.info("=========== start  dayStatistcs   orderDeatils of size = {} " ,orderDeatils.size());

        if (!CollectionUtils.isEmpty(orderDeatils)){
            // 按天分组 重点(这块其实可以和分类一起统计,省写了一个方法)
            Map<String, List<OrderDeatil>> dayliyMaps = orderDeatils.stream().collect(Collectors.groupingBy(deatil -> DateUtil.dateToString(deatil.getCreateDate(), BaseConstant.DATE_FORMAT_YYYY_MM_DD)));
            // 按天分组后遍历集合
            for (Map.Entry<String, List<OrderDeatil>> entry : dayliyMaps.entrySet()){
                // 处理保存入口
                this.daySave(entry.getValue());
            }
            log.info("=========== end  dayStatistcs  ===========  ");
        }

    }
    /**
     *  对原表集合按类型分组后对不同类型进行处理后的总点数set上统计实体类后去保存
     * @param orderDeatils
     */
    @Override
    @Transactional
    public void daySave(List<OrderDeatil> orderDeatils) {
        // 创建统计类型的实体类
        OrderDeatilStatisticsDaily statisticsDaily = new OrderDeatilStatisticsDaily();
        // 1. 按消费类型处理所有类型的统计
        Map<String, List<OrderDeatil>> typeListMap =  orderDeatils.stream().collect(Collectors.groupingBy(OrderDeatil :: getPayType));
        for (Map.Entry<String, List<OrderDeatil>> entry : typeListMap.entrySet()) {
            // 通过key获取对应注入实例
            String realmName = TypeEnum.getRealmByType(entry.getKey());
            if (realmName != null){
                TypeStatisticsRealm realm = SpringContextHolder.getBean(realmName);
                // 处理类型的统计值
                statisticsDaily = realm.handleOrderDetail(statisticsDaily,entry.getValue());
                List<String> values = entry.getValue().stream().map(s -> "id:" + s.getId() + ",point:" + s.getPayType()).collect(Collectors.toList());
                log.info("statisticsDaily statisticsDailyCodeEnum:[{}], values[{}]", entry.getKey(), values);
            }
        }
        this.saveDayStatisticsData(statisticsDaily);
    }
    /**
     * 保存统计数据,中间可以处理其它数据的方法
     * @param statisticsDaily
     */
    @Override
    @Transactional
    public void saveDayStatisticsData(OrderDeatilStatisticsDaily statisticsDaily) {
        // 处理保存
        this.handleStatisticsInfo(statisticsDaily);
    }
    /**
     * 处理保存或更新过程
     * @param statisticsDaily
     * @return
     */
    @Override
    @Transactional
    public OrderDeatilStatisticsDaily handleStatisticsInfo(OrderDeatilStatisticsDaily statisticsDaily) {
        // 保存或更新
        this.saveOrUpdate(statisticsDaily);
        return statisticsDaily;
    }
    /**
     * 查询当天存在就保存,否则更新
     * @param statisticsDaily
     */
    @Override
    @Transactional
    public void saveOrUpdate(OrderDeatilStatisticsDaily statisticsDaily) {
        // 处理当天时间为 yy-mm-dd
        String today = DateUtil.formatDate(statisticsDaily.getCreateDate(),BaseConstant.DATE_FORMAT_YYYY_MM_DD);
        // 1. 判断统计昨天有没有数据,没有的话就创建一条
        OrderDeatilStatisticsDaily daily = orderDeatilStatisticsDailyDao.getOrderDeatilStatisticsDailyById(today);
        if (daily == null){
            statisticsDaily.setUpdateDate(new Date());
            statisticsDaily.setDelFlag(true);
            orderDeatilStatisticsDailyDao.save(statisticsDaily);
        } else {
            // 否则就更新
            statisticsDaily.setId(daily.getId());
            statisticsDaily.setUpdateDate(new Date());
            orderDeatilStatisticsDailyDao.update(statisticsDaily);
        }
    }
}

十一、Utils 工具类

(1). 时间类

public class DateTool {

    /**
     * 获得某天最小时间 2019-7-17 00:00:00
     * @param date
     * @return
     */
    public static Date getStartOfDay(Date date) {
        LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
        LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN);
        return Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant());
    }

    /**
     * 获得某天最大时间 2019-7-17 23:59:59
     * @param date
     * @return
     */
    public static Date getEndOfDay(Date date) {
        LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
        LocalDateTime endOfDay = localDateTime.with(LocalTime.MAX);
        return Date.from(endOfDay.atZone(ZoneId.systemDefault()).toInstant());
    }
}
####(2). 
public class DateUtil {
    public static String formatDate(Date date, Object... pattern) {
        String formatDate = null;
        if (pattern != null && pattern.length > 0) {
            formatDate = DateFormatUtils.format(date, pattern[0].toString());
        } else {
            formatDate = DateFormatUtils.format(date, "yyyy-MM-dd");
        }

        return formatDate;
    }

    public static Date addDays(Date date, int amount) {
        return add(date, 5, amount);
    }


    private static Date add(Date date, int calendarField, int amount) {
        if (date == null) {
            throw new IllegalArgumentException("The date must not be null");
        } else {
            Calendar c = Calendar.getInstance();
            c.setTime(date);
            c.add(calendarField, amount);
            return c.getTime();
        }
    }

    // 按照格式将日期转化成字符串
    public static String dateToString(Date d, String format) {
        SimpleDateFormat formatter = new SimpleDateFormat(format);
        if (d == null) {
            return "";
        } else {
            return formatter.format(d);
        }
    }
}

十二:分页类

public class PageInfo implements Serializable {

    private int pageNo; //页码

    private int offset; // 开始条数

    private int pageSize; //页大小

    private String orderBy; //排序

    private int totalItem; //总条数

    public PageInfo () {
    }

    public PageInfo(int pageNo, int pageSize) {
        super();
        if (pageNo < 1) {
            pageNo = 1;
        }
        this.pageNo = pageNo;
        this.pageSize = pageSize;
    }

    public int getPageNo() {
        return pageNo;
    }

    public void setPageNo(int pageNo) {
        if (pageNo < 1) {
            pageNo = 1;
        }
        this.pageNo = pageNo;
    }

    @JsonIgnore
    public int getOffset() {
        return offset;
    }

    public void setOffset(int offset) {
        this.offset = offset;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    @JsonIgnore
    public String getOrderBy() {
        return orderBy;
    }

    public void setOrderBy(String orderBy) {
        this.orderBy = orderBy;
    }

    public int getTotalItem() {
        return totalItem;
    }

    public void setTotalItem(int totalItem) {
        this.totalItem = totalItem;
    }
}

十三. 获取getBean上下文工具

public class SpringContextHolder implements ApplicationContextAware , DisposableBean {

    private static ApplicationContext applicationContext = null;
    private static Logger logger = LoggerFactory.getLogger(SpringContextHolder.class);

    public SpringContextHolder() {
    }

    public static ApplicationContext getApplicationContext() {
        assertContextInjected();
        return applicationContext;
    }

    public static <T> T getBean(String name) {
        assertContextInjected();
        return (T) applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> requiredType) {
        assertContextInjected();
        return applicationContext.getBean(requiredType);
    }

    public static void clearHolder() {
        if (logger.isDebugEnabled()) {
            logger.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
        }

        applicationContext = null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextHolder.applicationContext = applicationContext;
    }

    @Override
    public void destroy() throws Exception {
        clearHolder();
    }

    private static void assertContextInjected() {
        Validate.validState(applicationContext != null, "applicaitonContext属性未注入, 请在applicationContext.xml中定义SpringContextHolder.", new Object[0]);
    }
}

结语

主要目的事为了解决某一张表数据量大(当超过几千万W级别这种方案可能不太适合)的时候做统计比较慢的方案,当中使用了一张新表去做原表统计后按天的总额,并用策略模式对不同类型进行处理(优点:遵守开闭原则,比if else更加有效,不需要在原有的类做修改,只需要增加类就行,缺点:类型越多,类也就会多,增加类的臃肿程度)。
最后:下篇会为新的业务要求新增一个导出功能,只能使用异步的方式导出(例如Rabbimt这种)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容