CRM 客户管理系统(SpringBoot+MyBatis)-程序员宅基地

技术标签: spring  spring boot  java  数据库  大数据  

点击关注公众号,回复“1024”获取2TB学习资源!

一.对CRM的项目的简单描述

1.什么是CRM

CRM系统即客户关系管理系统,是指企业用CRM技术来管理与客户之间的关系。他的目标是缩减销售周期和销售成本,增加收入,寻找扩展业务所需的新的市场和渠道以及提高客户的价值,满意度,营利性和忠实度。CRM项目的实施可以分为3步,即应用业务集成。业务诗句分析和决策执行。

2.CRM开发环境和技术

<1> 项目业务介绍

客户关系管理是指企业为提高核心竞争力,利用相应的技术信息以及互联网技术协调企业与顾客间在消费,营销和服务上的交互,从而提升其管理方式,向客户提供创新式的个性化的客户交互和服务的过程,其最终目标是吸引新客户,保留老客户以及将已有客户转为忠实客户,增加市场

<2>开发环境

  • 项目名称:CRM客户管理系统

  • 系统作用:公司客户关系管理,潜在客户开发及订单合同管理

  • 开发环境:IDEA Windows10 jdk1.8 Maven Mysql8

  • 需要的工具:postman fiddler抓包工具或浏览器开发者工具

<3>开发技术

  • 前端:LayUI freeMaker

  • 后端:Spring SpringMVC SpringBoot MyBatis Maven MySQL8 Linux CentOS ECharts(折线和饼状图)权限管理 定时任务调度(quartz)CentOS Lombok

二.项目准备及模块分析

1.模块分析总览

1.基础模块:包含系统基本的用户登录,退出,记住我,密码修改等基本操作。

2.营销管理:

  • 营销机会管理:企业客户的质询需求所建立的信息录入功能

  • 客户开发计划:开发计划是根据营销机会而来,对于企业质询的客户,会有相应的销售人员对于该客户进行具体的沟通交流,此时对于整个 Crm 系统而言,通过营销开发计划来进行相应的信息管理,提高客户的购买企业产品的可能性。

3.客户管理:

  • 客户信息管理 :Crm 系统中完整记录客户信息来源的数据、企业与客户交往、客户订单查询等信息录入功能,方便企业与客户进行相应的信息交流与后续合作。

  • 客户流失管理 :Crm 通过一定规则机制所定义的流失客户(无效客户),通过该规则可以有效管理客户信息资源,提高营销开发的效率。

4.服务管理:

  • 服务管理是针对客户而开发的功能,针对客户要求,Crm 提供客户相应的信息质询,反馈与投诉功能,提高企业对于客户的服务质量。

5.数据报表:

  • Crm 提供的数据报表功能能够帮助企业了解客户整体分布,了解客户开发结果整体信息,从而帮助企业整体调整客户开发计划,提高企业的在市场中的竞争力度。

6.系统管理:

系统管理包含常量字典维护工作,以及权限管理模块,Crm 权限管理是基于角色的一种权限控制,基于RBAC 实现基于角色的权限控制,通过不同角色的用户登录该系统后展示系统不同的操作功能,从而达到对不同角色完成不同操作功能。

3d936c523502bd436b9681e17eb35170.png
2.项目前期环境的搭建
  • 1.创建SpringBoot项目,导入依赖(见源代码)

  • 2.在src/main/resources 目录下新建 application.yml 配置文件

## 端口号  上下文路径
server:
  port: 8080
  servlet:
    context-path: /crm
    
## 数据源配置
spring:
  datasource:
    type: com.mchange.v2.c3p0.ComboPooledDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/crm?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
    username: 
    password: 

## freemarker
  freemarker:
    suffix: .ftl
    content-type: text/html
    charset: UTF-8
    template-loader-path: classpath:/views/

## 启用热部署
  devtools:
    restart:
      enabled: true
      additional-paths: src/main/java

## mybatis 配置
mybatis:
  mapper-locations: classpath:/mappers/*.xml
  type-aliases-package: org.example.crm.vo;org.example.crm.query;org.example.crm.dto
  configuration:
    map-underscore-to-camel-case: true

## pageHelper 分页
pagehelper:
  helper-dialect: mysql

## 设置 dao 日志打印级别
logging:
  level:
    org:
      example:
        crm:
          dao: debug
  • 3.新建 org.example.crm.controller 包,添加系统登录,主页面转发代码 。

  • 4.添加静态资源:在 src/main/resources 目录下新建 public 目录,存放系统相关静态资源文件,拷贝静态文件内容到

  • public 目录。

  • 5.添加视图模板:在 src/main/resources 目录下新建 views 目录,添加 index.ftl、main.ftl 等文件。(具体视图文件详见

  • 相关目录)

  • 6.添加启动类:在 org.example.crm 包下新建 Starter.java ,添加启动项目相关代码如下:

package org.example.crm;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@MapperScan("org.example.crm.dao")
//启用定时任务
@EnableScheduling
public class Starter {
    public static void main(String[] args) {
        SpringApplication.run(Starter.class);
    }
}
  • 7.添加Base包:主要用户对Controller,Service Dao层的统一控制,BaseQuery用于控制按条件搜索的对象,ResultInfo是后端返回的对象的统一封装

项目搭建结构:

677415a03ad95c47be6bd7a51f6d3cc1.png
  • 8.准备MyBatis代码统一生成工具(generatorConfig.xml)

这里注意:工具有点缺陷,每次需要改工具作用的表名

使用mybatis-generator生成Mybatis代码。能够生成 vo 类、能生成 mapper 映射文件(其中包括基本的增删改查功能)、能生成 mapper 接口。命令:mybatis-generator:generate -e

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

    <!--
        数据库驱动
            在左侧project边栏的External Libraries中找到mysql的驱动,右键选择copy path
    -->
    <classPathEntry  location="D:\Repository\Maven\mysql\mysql-connector-java\5.1.49\mysql-connector-java-5.1.49.jar"/>
    <context id="DB2Tables" targetRuntime="MyBatis3">

        <commentGenerator>
            <!-- 是否去除日期那行注释 -->
            <property name="suppressDate" value="false"/>
            <!-- 是否去除自动生成的注释 true:是 : false:否 -->
            <property name="suppressAllComments" value="false"/>
        </commentGenerator>

        <!-- 数据库链接地址账号密码 -->
        <jdbcConnection
                driverClass="com.mysql.jdbc.Driver"
                connectionURL="jdbc:mysql://localhost:3306/crm?serverTimezone=GMT%2B8"
                userId="root"
                password="sn20000904">
        </jdbcConnection>

        <!--
             java类型处理器
                用于处理DB中的类型到Java中的类型,默认使用JavaTypeResolverDefaultImpl;
                注意一点,默认会先尝试使用Integer,Long,Short等来对应DECIMAL和NUMERIC数据类型;
                true:使用 BigDecimal对应DECIMAL和NUMERIC数据类型
                false:默认,把JDBC DECIMAL和NUMERIC类型解析为Integer
        -->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>

        <!-- 生成Model类存放位置 -->
        <javaModelGenerator targetPackage="org.example.crm.model" targetProject="src/main/java">
            <!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false -->
            <property name="enableSubPackages" value="true"/>
            <!-- 设置是否在getter方法中,对String类型字段调用trim()方法 -->
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!--生成映射文件存放位置-->
        <sqlMapGenerator targetPackage="mappers" targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!--生成Dao类存放位置-->
        <javaClientGenerator type="XMLMAPPER" targetPackage="org.example.crm.dao" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

       <!--改表名 -->
        <table tableName="t_customer_serve" domainObjectName="CustomerServe"
               enableCountByExample="false" enableUpdateByExample="false"
               enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>

    </context>
</generatorConfiguration>
  • 9.导入工具类,主要有:根据Cookie获取作用域,登录成功返回userIdStr加密,Md5协议加密,判断电话号码的格式,userID加解密等

f56f5ba75fa64c0162c8129f51010d1b.png

三.项目的正式开发

主要讲解核心模块的核心代码

1.用户管理模块
  • <1>.表结构分析

bc52ae5ec9230becea119fbc3ebc57fb.png
  • <2>.用户登录

定义UserModel类,用于用户登录成功返回的用户信息,用来设置前端的Cookie

@Getter
@Setter
public class UserVo {
    //private Integer userId;
    //存放在前端cookie中加密后的Id
    private String userIdStr;
    private String userName;
    private String trueName;
}

设置cookie

layer.msg("登录成功!", function () {
                        // 判断用户是否选择记住密码(判断复选框是否被选中,如果选中,则设置cookie对象7天生效)
                        if ($("#rememberMe").prop("checked")) {
                            // 选中,则设置cookie对象7天生效
                            // 将用户信息设置到cookie中
                            $.cookie("userIdStr", result.result.userIdStr, {expires: 7});
                            $.cookie("userName", result.result.userName, {expires: 7});
                            $.cookie("trueName", result.result.trueName, {expires: 7});
                        } else {
                            // 将用户信息设置到cookie中
                            $.cookie("userIdStr", result.result.userIdStr);
                            $.cookie("userName", result.result.userName);
                            $.cookie("trueName", result.result.trueName);
                        }

退出登录时,删除前端Cookie即可

  • <3>全局统一的异常处理及非法请求的拦截

(1)统一的异常处理

全局异常实现思路:

控制层的方法返回的内容两种情况

  • 视图:视图异常

  • Json:方法执行错误 返回错误json信息

全局异常拦截器的实现,简化了try-catch代码

实现 HandlerExceptionResolver 接口 ,处理应用程序异常信息

(2)非法请求拦截

对于后端菜单资源,这里要求用户必须进行登录来保护 web 资源的安全性,此时引入非法请求拦截功能。

实现思路:

  • 判断用户是否是登录状态

  • 获取Cookie对象,解析用户ID的值

  • 如果用户ID不为空,且在数据库中存在对应的用户记录,表示请求合法。否则,请求不合法,进行拦截,重定向到登录页面

定义拦截器:在新建 interceptors 包,创建 NoLoginInterceptor 类,并继承 HandlerInterceptorAdapter 适配器,实现拦截器功能。

/**
 * 非法访问拦截
 * 继承HandlerInterceptorAdapter适配器
 */

public class NoLoginInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private UserMapper userMapper;

    /**
     * 拦截用户是否是登录状态
     * 在目标方法(资源)执行前执行的方法
     * 返回boolean
     * 如果为true,表示目标方法可用被执行
     * 如果为false,表示阻止目标方法执行
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取cookie中的用户Id
        Integer userId = LoginUserUtil.releaseUserIdFromCookie(request);
        //判断用户Id是否为空,且数据库中是否存在改userId的记录
        if (userId == null || userMapper.selectByPrimaryKey(userId) == null) {
            //抛出未登录异常
            throw new NoLoginException();
        }
        return true;
    }
}

全局异常类配置:在全局异常处理类中引入未登录异常判断

/**
 * 全局异常统一处理
 */
@Component
public class GlobalExceptionResolver implements HandlerExceptionResolver {
    /**
     * 异常处理方法
     * 方法的返回值:
     * 1. 返回视图
     * 2. 返回数据(JSON数据)
     * <p>
     * 如何判断方法的返回值?
     * 通过方法上是否声明@ResponseBody注解
     * 如果未声明,则表示返回视图
     * 如果声明了,则表示返回数据
     *
     * @param request  request请求对象
     * @param response response响应对象
     * @param handler  方法对象
     * @param ex       异常对象
     * @return org.springframework.web.servlet.ModelAndView
     */
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

        /**
         * 非法请求拦截
         *  判断是否抛出未登录异常
         *      如果抛出该异常,则要求用户登录,重定向跳转到登录页面
         */
        if (ex instanceof NoLoginException) {
            // 重定向到登录页面
            ModelAndView mv = new ModelAndView("redirect:/index");
            return mv;
        }


        /**
         * 设置默认异常处理(返回视图)
         */
        ModelAndView modelAndView = new ModelAndView("error");
        // 设置异常信息
        modelAndView.addObject("code", 500);
        modelAndView.addObject("msg", "系统异常,请重试...");


        // 判断HandlerMethod
        if (handler instanceof HandlerMethod) {
            // 类型转换
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            // 获取方法上声明的@ResponseBody注解对象
            ResponseBody responseBody = handlerMethod.getMethod().getDeclaredAnnotation(ResponseBody.class);

            // 判断ResponseBody对象是否为空 (如果对象为空,则表示返回的事视图;如果不为空,则表示返回的事数据)
            if (responseBody == null) {
                /**
                 * 方法返回视图
                 */
                // 判断异常类型
                if (ex instanceof ParamsException) {
                    ParamsException p = (ParamsException) ex;
                    // 设置异常信息
                    modelAndView.addObject("code", p.getCode());
                    modelAndView.addObject("msg", p.getMsg());

                } else if (ex instanceof AuthException) { // 认证异常
                    AuthException a = (AuthException) ex;
                    // 设置异常信息
                    modelAndView.addObject("code", a.getCode());
                    modelAndView.addObject("msg", a.getMsg());
                }

                return modelAndView;

            } else {
                /**
                 * 方法返回数据
                 */
                // 设置默认的异常处理
                ResultInfo resultInfo = new ResultInfo();
                resultInfo.setCode(500);
                resultInfo.setMsg("异常异常,请重试!");

                // 判断异常类型是否是自定义异常
                if (ex instanceof ParamsException) {
                    ParamsException p = (ParamsException) ex;
                    resultInfo.setCode(p.getCode());
                    resultInfo.setMsg(p.getMsg());

                } else if (ex instanceof AuthException) { // 认证异常
                    AuthException a = (AuthException) ex;
                    resultInfo.setCode(a.getCode());
                    resultInfo.setMsg(a.getMsg());
                }

                // 设置响应类型及编码格式(响应JSON格式的数据)
                response.setContentType("application/json;charset=UTF-8");
                // 得到字符输出流
                PrintWriter out = null;
                try {
                    // 得到输出流
                    out = response.getWriter();
                    // 将需要返回的对象转换成JOSN格式的字符
                    String json = JSON.toJSONString(resultInfo);
                    // 输出数据
                    out.write(json);

                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    // 如果对象不为空,则关闭
                    if (out != null) {
                        out.close();
                    }
                }

                return null;

            }
        }
        return modelAndView;
    }
}

拦截器生效配置:

@Configuration//配置类
public class MvcConfig extends WebMvcConfigurerAdapter {
    @Bean//将方法的返回值交给IOC
    public NoLoginInterceptor noLoginInterceptor() {
        return new NoLoginInterceptor();
    }

    /**
     * 添加拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //需要实现了拦截器功能的实例对象 NoLoginInterceptor
        registry.addInterceptor(noLoginInterceptor())
                //设置需要被拦截的资源
                .addPathPatterns("/**")
                // 设置不需要被拦截的资源
                .excludePathPatterns("/css/**", "/images/**", "/js/**", "/lib/**")
                .excludePathPatterns("/index", "/user/login");
    }
}

测试拦截效果:

当 Cookie 中的用户ID不存在时,访问 main 页面,会自动跳转到登录页面

<3>记住我功能

记住我功能核心在于当用户上次登录时如果点击了记住我,下次在重新打开浏览器时可以不用选择登录,此时可以借助拦截器 + cookie 来实现,当用户在登录时,如果用户点击了记住我功能,默认设置cookie存储时间为7天即可。

2.营销管理模块(CRUD操作,见源码)
  • <1>功能开发及表结构分析

功能开发:

4d09bb737feff29ad606b6180b992007.png

表结构:

45943229360051ce33853ed0af2e3037.png

这里注意:时间格式化:

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd") // 如果传递的参数是Date类型,要求传入的时间字符串的格式
    private Date planDate;
3.权限管理模块(CRUD操作见源码)

基本概念:RBAC是基于角色的访问控制( Role-Based Access Control )在RBAC中,权限与角色相关联,用户通过扮演适当的角色从而得到这些角色的权限。这样管理都是层级相互依赖的,权限赋予给角色,角色又赋予用户,这样的权限设计很清楚,管理起来很方便。

  • <1>.模块功能及表的结构设计

功能模块:

292d985f6aa272d752b7047ea604b492.png

表结构设计:

从上面实体对应关系分析,权限表设计分为以下基本的五张表结构:用户表(t_user)、角色表(t_role)、t_user_role(用户角色表)、资源表(t_module)、权限表(t_permission)用户和角色间一对一关系,角色和权限间一对一关系,建立t_user_role和t_permission中间表表结构关系如下:

5bd19aea641dadaeec86e170c88f1dbf.png 1f93e5e32e242e56cb9d7385eaa8826e.png
  • <2>角色权限功能

当完成角色权限添加功能后,下一步就是对角色操作的资源进行认证操作,这里对于认证包含两块:

  • 菜单级别显示控制

  • 后端方法访问控制

查询出改用户所拥有的角色,然后根据角色查询出拥有的权限码,具体实现如下:

@RequestMapping("main")
    public String main(HttpServletRequest request) {
        //通过获取cookie用户ID
        Integer userId = LoginUserUtil.releaseUserIdFromCookie(request);
        //查询用户对象,设置session作用域
        User user = userService.selectByPrimaryKey(userId);
        request.getSession().setAttribute("user", user);

        //通过当前登录用户ID,查询当前登录用户拥有的资源列表(查询对应的资源授权码)
        List<String> permissions = null;
        permissions = permissionService.queryUserHasRoleHasPermissionByUserId(userId);
        //将集合设置作用域中(Session作用域)
        request.getSession().setAttribute("permissions", permissions);

        return "main";
    }
(1).菜单级别显示控制

系统根据登录用户扮演的不同角色来对登录用户操作的菜单进行动态控制显示操作,这里显示的控制使用freemarker指令+内建函数实现,例如:

926a09ef2e5dfdcc6f06de8d6cb7f3a0.png

会根据权限码,来显示菜单内容:

1141e10e6141413e486b8d356e55f2b5.png
(2).后端方法级别访问控制(AOP+注解实现)

实现了菜单级别显示控制,但最终客户端有可能会通过浏览器来输入资源地址从而越过ui界面来访问后端资源,所以接下来加入控制方法级别资源的访问控制操作,这里使用aop+自定义注解实现自定义注解类:表示资源所需的权限码

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
 * 定义方法需要的对应资源的权限码
 */
public @interface RequiredPermission {
    //权限码
    String code() default "";
}

方法级别使用注解:

例如:

17694344011090999fcb4ae7eef34b50.png

定义aop切面类 拦截指定注解标注的方法:

@Component
@Aspect
public class PermissionProxy {

    @Resource
    private HttpSession session;

    /**
     * 切面会拦截指定包下的指定注解
     * 拦截com.xxxx.crm.annoation的RequiredPermission注解
     *
     * @param pjp
     * @return java.lang.Object
     */
    @Around(value = "@annotation(org.example.crm.annotation.RequiredPermission)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object result = null;
        // 得到当前登录用户拥有的权限 (session作用域)
        List<String> permissions = (List<String>) session.getAttribute("permissions");
        // 判断用户是否拥有权限
        if (null == permissions || permissions.size() < 1) {
            // 抛出认证异常
            throw new AuthException();
        }

        // 得到对应的目标
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        // 得到方法上的注解
        RequiredPermission requiredPermission = methodSignature.getMethod().getDeclaredAnnotation(RequiredPermission.class);
        // 判断注解上对应的状态码
        if (!(permissions.contains(requiredPermission.code()))) {
            // 如果权限中不包含当前方法上注解指定的权限码,则抛出异常
            throw new AuthException();
        }

        result = pjp.proceed();
        return result;
    }
}
4.客户管理模块(CRUD操作见源码)
  • <1>.模块功能及表结构设计

模块功能:

821056f19517552475b5dfcac54e62fe.png

表结构设计:

t_customer 客户表、t_customer_contact 客户交往记录表、t_customer_linkman 客户联系人表、t_customer_order 客户订单表、t_order_details 订单详情表

b642fd34a09d8488a86a80715b490c29.png dd621b8ead0823ab1e02931777d3fd72.png
  • <2>.定时器

当实现了客户数据转移业务逻辑代码后,这里需要思考一个问题:客户数据量的问题随着时间的积累,流失的客户数据可能就比较大,如果数据的获取在用户查询时进行,此时后端对于数据的查询就会变得很慢,此时可以使用我们之前讲到的定时任务来处理,后台通过定时器来对流失客户数据定时进行转移处理,从而当前端用户查询时只需到客户流失表查询流失数据即可。

增加定时器服务:

/**
 * 定时任务的执行
 */
@Component
public class JobTask {
    @Autowired
    private CustomerService customerService;

    //cron表达式
    //每两秒执行一次
    //@Scheduled(cron = "0/2 * * * * ?")

    //从六月开始,每个月执行一次
    @Scheduled(cron = "* * * * 6/1 ? ")
    public void job() {
        //调用需要被执行的方法
        //开始执行定时任务
        System.out.println("开始执行定时器任务");
        customerService.updateCustomerState();
        System.out.println("定时器任务执行完成");
    }
}

Starter开启定时任务环境配置:

@SpringBootApplication
@MapperScan("org.example.crm.dao")
//启用定时任务
@EnableScheduling
public class Starter {
    public static void main(String[] args) {
        SpringApplication.run(Starter.class);
    }
}
5.服务管理(CRUD操作见源码)
  • <1>功能实现及表结构设计

功能实现:

956b6cae7ce2c02cf2355f6f08e20370.png

表结构设计:

71473ed2138b0e87945a6f3e4ef1c921.png
  • <2>服务实现

这里对于服务管理服务的创建,分配,处理与反馈后端代码实现放在同一个方法中进行处理,同时方便对于服务状态值统一处理,这里定义 CustomerServeStatus 枚举类来实现。

/**
 * 客户服务状态枚举类
 */
public enum CustomerServeStatus {
    // 创建
    CREATED("fw_001"),
    // 分配
    ASSIGNED("fw_002"),
    // 处理
    PROCED("fw_003"),
    // 反馈
    FEED_BACK("fw_004"),
    // 归档
    ARCHIVED("fw_005");

    private String state;

    CustomerServeStatus(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }
}
6.统计报表管理(CRUD见源码)
  • <1>.功能实现

功能实现:

44794c879ab1b191f0fb5cf8f3ea7a60.png
  • <2>.使用ECharts对数据进行分析

折线图数据返回实现:

/**
     * 查询客户构成 (折线图)
     * @return
     */
    public Map<String, Object> countCustomerMake() {
        Map<String, Object> map = new HashMap<>();
        // 查询客户构成数据的列表
        List<Map<String, Object>> dataList = customerMapper.countCustomerMake();
        // 折线图X轴数据  数组
        List<String> data1 = new ArrayList<>();
        // 折线图Y轴数据  数组
        List<Integer> data2 = new ArrayList<>();

        // 判断数据列表 循环设置数据
        if (dataList != null && dataList.size() > 0) {
            for (int i = 0; i < dataList.size(); i++) {
                data1.add(dataList.get(i).get("level").toString());
                data2.add(Integer.parseInt(dataList.get(i).get("total").toString()));
            }
        }
        // 将X轴的数据集合与Y轴的数据集合,设置到map中
        map.put("data1", data1);
        map.put("data2", data2);

        return map;
    }
a8fe7390f905f3f1733b7808a0d9e05b.png

饼状图数据返回实现:

public Map<String, Object> countCustomerMake02() {
     Map<String, Object> map = new HashMap<>();
     // 查询客户构成数据的列表
     List<Map<String, Object>> dataList = customerMapper.countCustomerMake();
     // 饼状图数据   数组(数组中是字符串)
     List<String> data1 = new ArrayList<>();
     // 饼状图的数据  数组(数组中是对象)
     List<Map<String, Object>> data2 = new ArrayList<>()
     // 判断数据列表 循环设置数据
     if (dataList != null && dataList.size() > 0) {
         // 遍历集合
         for (int i = 0; i < dataList.size(); i++) {
             //饼状图数据, 数组(数组中是字符串
             data1.add(dataList.get(i).get("level").toString());
             //饼状图数据 数组(数组中是对象)
             Map<String, Object> dataMap = new HashMap<>();
             dataMap.put("name", dataList.get(i).get("level"));
             dataMap.put("value", dataList.get(i).get("total"));
             data2.add(dataMap);
         }
     
     // 将X轴的数据集合与Y轴的数据集合,设置到map中
     map.put("data1", data1);
     map.put("data2", data2)
     return map;
 }
4a997d47e6d6bb6f097fde9a04d4e406.png

作者:Serendipity  sn 链接:

blog.csdn.net/qq_45704528/article/details/117451506

1cfcfdd2e909ac16b973eec327ffc2a8.png

6368acd116ea50abb3e02de2e2bf852e.png

推荐阅读 点击标题可跳转

再这样下去,半年内团队就要散了。。

暴力拒绝白嫖!著名开源项目作者删库跑路

谁再说学不会 MySQL 数据库,就把这个给他扔过去!

12年的良心国产 PDF 软件开源了!永久免费!不流氓

如何让手机 1 秒打开健康码,任何机型!

Nginx+Redis 搭建高性能缓存利器

干掉工具类!一个 MyBatis 简单配置搞定数据加密解密

图解 MySQL 索引,清晰易懂,写得太好了!

5afc2dba8be89b4db7093d01f219b539.png

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。

随手在看、转发是最大的支持!9f54973120fd8f501f40996b2140f968.gif

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/mingongge/article/details/122504895

智能推荐

基于Python的Climate Indices库计算SPEI(标准化降水蒸散发指数)01—Climate Indices库的下载和安装_climate-indices包-程序员宅基地

文章浏览阅读4.6k次,点赞4次,收藏45次。SPEI, Python, Climate_Indices库下载安装_climate-indices包

d3.js 旋转图形_《几何画板》画阴影图形-程序员宅基地

文章浏览阅读318次。《几何画板》画阴影图形 《几何画板》功能强大,但究其作图本质,其原理不过是提供了尺规作图的工具,当然其它的浮点运算的强大的功能另说,那个与尺规作图无关.精确的作图一是为了美观,比如电子文档或试卷的建立,二是有些平几性质的探究,本文只说一下阴影部分如何建立.比如下面的图形题:是个小奥题,虽然没有文字说明,想必大家也知道就是求其阴影部分的面积,但在电子文档中如何绘出这个图形呢?下面在《几何画板》中给出..._几何画板旋转区域阴影

Foxmail 邮件内链接无法打开问题_foxmail点击链接地址打不开-程序员宅基地

文章浏览阅读5.5k次。这两天新装了系统,然后Foxmail邮箱的邮件链接就打不开了。以前很方便就直接自动跳转到谷歌游览器上。在网上扒了半天也没解决,发现很多人都在问,今天突然折腾出来了。是我们把电脑设置里的默认游览器改了但是第三方管家里边 的默认游览器还没有改。第一种:把电脑默认游览器改为IE 游览器,就可以直接跳转到IE游览器了。但是我想大家都不愿意。第二种就是去管家(我的是腾讯电脑管家)里把默认游览器改..._foxmail点击链接地址打不开

Flutter控件——布局控件:层叠_flutter 绝对定位 在不同手机上-程序员宅基地

文章浏览阅读335次。Stack 层叠布局_flutter 绝对定位 在不同手机上

GEM5中利用Pydot输出系统配置图_contos7系统中gem5中安装了pydothou 生成了.dot文件怎么转成pdf或者图片-程序员宅基地

文章浏览阅读1.4k次,点赞3次,收藏4次。GEM5中利用Pydot输出系统配置图安装python的Pydot包之后,当运行gem5进行模拟仿真之后,即可在m5out目录下找到config.pdf之类的配置图,如图1所示。还可以通过dot命令将config.dot文件转换成自己需要的格式pip install pydotcd m5outdot -Tpng -o config.png config.dot图1 gem5输..._contos7系统中gem5中安装了pydothou 生成了.dot文件怎么转成pdf或者图片

硬件设计规范_硬件详细设计规格书的目的怎么写-程序员宅基地

文章浏览阅读4.3k次,点赞2次,收藏14次。1 硬件需求说明书 2 硬件总体设计报告 3 单板硬件总体设计方案 4 单板硬件详细设计 5 单板硬件过程调试文档 6 单板硬件系统调试报告 7 单板硬件测试文档 8 硬件总体方案归档详细文档 9 硬件单板总体方案归档详细文档 10 硬件信息库 2.2._硬件详细设计规格书的目的怎么写

随便推点

Linux内核堆栈实现分析 save_stack_trace_save_stack_trace_tsk-程序员宅基地

文章浏览阅读8.3k次。1 内核线程Linux 内核为每个线程分配THREAD_SIZE(16k)的栈空间, 在每个堆栈的顶部放着struct thread_info 结构体,用来保存线程相关信息.其中有几个重要变量:Preempt_count :此变量分为四部分 0-7bit :当前进程是否能抢占的标志8-15bit:softirq 使能标志16-23bit :hardirq 使能标志..._save_stack_trace_tsk

深入MyBatis源码,理解Java设计模式之适配器模式-程序员宅基地

文章浏览阅读215次。2019独角兽企业重金招聘Python工程师标准>>> ..._mybatis源码中运用到的适配器模式

终止端口进程命令_停止端口进程命令-程序员宅基地

文章浏览阅读590次。【代码】终止端口进程命令。_停止端口进程命令

51单片机c语言俩个按键启停,51单片机跑步机启停速度控制模块仿真+源程序+电路原理图...-程序员宅基地

文章浏览阅读1.8k次。#include unsigned char code SEGtable[ ]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; //字符编码sbit SEKey =P1 ^ 0; //启动、暂停和停止按键sbit INCKey = P1 ^ 4; //速度增加键sbit DE..._顺启逆停c语言程序图

关于tomcat成功启动但访问不了欢迎界面的问题_tomcat部署成功以后服务起来了,访问不到-程序员宅基地

文章浏览阅读4.6w次,点赞16次,收藏59次。eclipse中安装tomcat的时候出现了问题。JDK已经按照网上的说法配好了,然后tomcat也成功启动。点击tomcat安装目录bin下的startup.bat后并没有出现闪退的现象。可是我在地址栏输入http://localhost:8080/却出现404错误。确认了端口号并没有被占用。百度网上一大堆也没啥用。后来发现有个帖子说是因为Root文件夹里没有index.html和index.j..._tomcat部署成功以后服务起来了,访问不到

android命令截屏_screencap /mnt/card/test.png-程序员宅基地

文章浏览阅读406次。adb shell screencap /mnt/card/test.png_screencap /mnt/card/test.png

推荐文章

热门文章

相关标签