CRM - 用户管理

1. 学习目标

在这里插入图片描述

2. CRM 系统概念与项目开发流程

2. 1. CRM 基本概念

圈内存在这么一句话:“世上本来没有 CRM,大家的生意越来越难做了,才有了 CRM。” 在同质化竞 争时代,顾客资产尤为重要,新时代在呼唤 CRM。
CRM 系统即客户关系管理系统, 顾名思义就是管理公司与客户之间的关系。 是一种以"客户关系一对 一理论"为基础,旨在改善企业与客户之间关系的新型管理机制。客户关系管理的定义是:企业为提高核心竞争力,利用相应的信息技术以及互联网技术来协调企业与顾客间在销售、营销和服务上的交互,从而提升其管理方式,向客户提供创新式的个性化的客户交互和服务的过程。 其最终目标是吸引新客户、 保留老客户以及将已有客户转为忠实客户,增加公司市场份额。
CRM 的实施目标就是通过全面提升企业业务流程的管理来降低企业成本,通过提供更快速和周到的优质服务来吸引和保持更多的客户。作为一种新型管理机制,CRM 极大地改善了企业与客户之间的关系, 应用于企业的市场营销、销售、服务与技术支持等与客户相关的领域。

2. 2. CRM 分类

根据客户的类型不同,CRM 可以分为 B to B CRM 及 B to C CRM。 BtoB CRM 中管理的客户是企业 客户,而 B to C CRM 管理的客户则是个人客户。提供企业产品销售和服务的企业需要的 B to B 的CRM,也就是市面上大部分 CRM 的内容。而提供个人及家庭消费的企业需要的是 B to C 的 CRM。
根据 CRM 管理侧重点不同又分为操作性和分析型 CRM。大部分 CRM 为操作型 CRM,支持CRM的日 常作业流程的每个环节,而分析型 CRM 则偏重于数据分析。

2. 3. 企业项目开发流程

  1. 产品组根据市场调研或商户同事的反馈提出 idea,设计出原型然后跟市场, 商户同事进行确认
  2. UI 设计组和开发组一起讨论,确定方案是否可行
  3. UI 组根据产品组提供的原型稿做出设计稿,与产品和开发确认
  4. 开发组根据产品的原型稿(看逻辑)和UI组的设计稿(看界面)编写代码其中当然也会来回跟设计, 产品 同学进行确认和沟通
  5. 代码编写完毕后提交给测试组. 然后再提交上线
  6. 后期的数据跟踪和优化

这就是一个产品研发的大致流程。其中开发的责任就是选用合适的框架技术来完成产品所提供的需求 以及设计所提供的效果。

3. CRM 系统模块划分

3. 1. 系统功能模块图

3. 2. 模块功能描述

3. 2. 1. 基础模块

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

3. 2. 2. 营销管理

营销机会管理 :企业客户的质询需求所建立的信息录入功能,方便销售人员进行后续的客户需求跟 踪。

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

客户信息管理 :Crm 系统中完整记录客户信息来源的数据、企业与客户交往、客户订单查询等信息录 入功能,方便企业与客户进行相应的信息交流与后续合作。
客户流失管理 :Crm 通过一定规则机制所定义的流失客户(无效客户),通过该规则可以有效管理客 户信息资源,提高营销开发的效率。

3. 2. 4. 服务管理

服务管理是针对客户而开发的功能,针对客户要求,Crm 提供客户相应的信息质询,反馈与投诉功能,提高企业对于客户的服务质量。
营销开发计划 :开发计划是根据营销机会而来,对于企业质询的客户,会有相应的销售人员对于该客户进行具

3. 2. 5. 数据报表

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

3. 2. 6. 系统管理

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

4. CRM 系统数据库设计

CRM 系统根据产品的原型稿以及UI组的设计稿,接下来就要设计数据库, 一般在大公司通常会有专门的DBA, 这时我们可以不要考虑数据库表设计, 但是也要能够读懂或者了解DBA的设计思路方便在程序开发阶段不会出现问题,一般关系型数据库表设计满足三范式的设计即可,表名设计做到见名知意最 好。

5. 项目环境搭建与测试

5. 1. 项目技术栈

5. 2. 环境搭建与测试

5. 2. 1. 新建项目

在 IDEA 中,新建 SpringBoot 项目,项目名设置为 crm

5. 2. 2. 引入坐标 & 插件

在 pom.xml 文件中,添加项目集成环境所需要的依赖坐标与插件

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.2.RELEASE</version>
  </parent>

  <dependencies>

    <!-- web 环境 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- aop -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- freemarker -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
    <!-- 测试环境 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

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

    <!-- 分页插件 -->
    <dependency>
      <groupId>com.github.pagehelper</groupId>
      <artifactId>pagehelper-spring-boot-starter</artifactId>
      <version>1.2.13</version>
    </dependency>

    <!-- mysql -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>

    <!-- c3p0 -->
    <dependency>
      <groupId>com.mchange</groupId>
      <artifactId>c3p0</artifactId>
      <version>0.9.5.5</version>
    </dependency>

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

    <!-- json -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.47</version>
    </dependency>

    <!-- DevTools 热部署 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <optional>true</optional>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>11</source>
          <target>11</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.mybatis.generator</groupId>
        <artifactId>mybatis-generator-maven-plugin</artifactId>
        <version>1.3.2</version>
        <configuration>
          <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
          <verbose>true</verbose>
          <overwrite>true</overwrite>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <!-- 如果没有该配置,热部署的devtools不生效 -->
          <fork>true</fork>
        </configuration>
      </plugin>
    </plugins>
  </build>
5. 2. 3. 添加配置文件

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: root
    password: 123456

  ## 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: com.xxxx.crm.vo;com.xxxx.crm.query;com.xxxx.crm.dto
  configuration:
    map-underscore-to-camel-case: true

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

## 设置 dao 日志打印级别
logging:
  level:
    com:
      xxxx:
        crm:
          dao: debug
5. 2. 4. 添加视图转发

新建 com.xxxx.crm.controller 包,添加系统登录,主页面转发代码 (这里先引入 base 包,具体文件 见相关目录)

package com.xxxx.crm.controller;

import com.xxxx.crm.base.BaseController;
import com.xxxx.crm.service.UserService;
import com.xxxx.crm.utils.LoginUserUtil;
import com.xxxx.crm.vo.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

@Controller
public class IndexController extends BaseController {

    @Resource
    private UserService userService;

    /**
     * 系统登录页
     * @return
     */
    @RequestMapping("index")
    public String index(){
        return "index";
    }

    
    // 系统界面欢迎页
    @RequestMapping("welcome")
    public String welcome(){
        return "welcome";
    }

    /**
     * 后端管理主页面
     * @return
     */
    @RequestMapping("main")
    public String main() {
        return "main";
    }
}
5. 2. 5. 添加静态资源

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

5. 2. 6. 添加视图模板

在 src/main/resources 目录下新建 views 目录,添加 index.ftl、main.ftl 等文件。 (具体视图文件详见 相关目录)

5. 2. 7. 添加应用启动类

在 com.xxxx.crm 包下新建 Starter.java ,添加启动项目相关代码如下:

package com.xxxx;

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

/**
 * Hello world!
 */
@SpringBootApplication
@MapperScan("com.xxxx.crm.dao")
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}
5. 2. 8. 项目目录结构
5. 2. 9. 浏览器访问

Chrome浏览器访问登录页地址:http://localhost:8080/crm/index
Chrome浏览器访问系统主页地址:http://localhost:8080/crm/main

6. 用户登录功能实现

6. 1. 准备工作

6. 1. 1. 工具类与自定义异常类

将工具类与自定义异常类,拷贝到项目中。 (这里拷贝 utils 包和 exceptions 包,具体文件见相关目录)

6. 1. 2. 自动生成代码
6. 1. 2. 1. generatorConfig.xml

在 src/main/resources 目录下,添加 generatorConfig.xml 配置文件。(需要修改数据库驱动路径、数据库账号密码等信息。)

<?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的驱动下的jar包,右键选择copy path -->
    <classPathEntry
            location="E:\.m2\maven_repository\mysql\mysql-connector-java\8.0.18\mysql-connector-java-8.0.18.jar"/>

    <!-- context 是逆向工程的主要配置信息,id:起个名字,targetRuntime:设置生成的文件适用于哪个mybatis版本 -->
    <context id="DB2Tables" targetRuntime="MyBatis3">

        <!--optional,指在创建class时,对注释进行控制-->
        <commentGenerator>
            <!-- 是否去除日期那行注释 -->
            <property name="suppressDate" value="true"/>
            <!-- 是否去除自动生成的注释 true:是 : false:否 -->
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <!-- 数据库链接地址账号密码 -->
        <jdbcConnection
                driverClass="com.mysql.cj.jdbc.Driver"
                connectionURL="jdbc:mysql://127.0.0.1:3306/crm?serverTimezone=GMT%2B8"
                userId="root"
                password="123456">
        </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="com.xxxx.crm.vo" 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="com.xxxx.crm.dao" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>


        <!-- 数据库的表名与对应的实体类的名称,tableName是数据库中的表名,domainObjectName是生成的JAVA模型名 -->
        <!--用完可以注释掉,防止错按-->
       <!-- <table tableName="t_user" domainObjectName="User"
               enableCountByExample="false" enableUpdateByExample="false"
               enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false">
        </table>-->

    </context>
</generatorConfiguration>
6. 1. 2. 2. 执行命令

使用mybatis-generator生成Mybatis代码。能够生成 vo 类、能生成 mapper 映射文件(其中包括基

本的增删改查功能)、能生成 mapper 接口。

命令:mybatis-generator:generate -e

6. 2. 核心思路分析

前台

  1. 获取用户输入的数据

  2. 校验用户输入的数据

  3. 发送ajax请求到后台

  4. 接收后台返回的数据ResultInfo(封装UserModel,将数据存放在cookie中,保持登录状态)

后台

  1. 接收参数

  2. 校验参数是否为空 如果为空,抛异常

  3. 通过用户名查询数据库数据 如果未查到,抛异常(用户不存在)

  4. 校验前台传来的密码和数据库中的密码是否一致 (前台密码加密后再校验) 如果不一致,抛异常(密码错误)

  5. 封装ResultInfo对象给前台(根据前台需求:usermodel对象封装后传到前台使用)

分层思想

controller

  1. 接收参数,调用service

service

  1. 校验参数是否为空 如果为空,抛异常

  2. 调用dao层查询通过用户名查询数据库数据 如果未查到,抛异常(用户不存在)

  3. 校验前台传来的密码和数据库中的密码是否一致 (前台密码加密后再校验) 如果不一致,抛异常(密码错误)

  4. 封装ResultInfo对象给前台(根据前台需求:usermodel对象封装后传到前台使用)

dao

通过用户名查询数据库数据

6. 3. 核心代码实现

6. 3. 1. UserModel

定义 UserModel 实体类,用来返回登录成功后的用户信息

package com.xxxx.crm.query;

public class UserModel {

    //private Integer userId;
    private String userId;
    private String userName;
    private String trueName;

    /*public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }
*/

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getTrueName() {
        return trueName;
    }

    public void setTrueName(String trueName) {
        this.trueName = trueName;
    }
}
6. 3. 2. UserService

用户登录具体的业务逻辑的实现

package com.xxxx.crm.service;

import com.xxxx.crm.base.BaseService;
import com.xxxx.crm.base.ResultInfo;
import com.xxxx.crm.dao.UserMapper;
import com.xxxx.crm.query.UserModel;
import com.xxxx.crm.utils.AssertUtil;
import com.xxxx.crm.utils.Md5Util;
import com.xxxx.crm.utils.UserIDBase64;
import com.xxxx.crm.vo.User;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Date;

@Service
public class UserService extends BaseService<User, Integer> {

    @Resource
    private UserMapper userMapper;

    /**
     * 用户登录
     * 2.校验参数是否为空
     * 如果为空,抛异常
     * 3.调用dao层查询通过用户名查询数据库数据
     * 如果未查到,抛异常(用户不存在)
     * 4.校验前台传来的密码和数据库中的密码是否一致 (前台密码加密后再校验)
     * 如果不一致,抛异常(密码错误)
     * 5.封装ResultInfo对象给前台(根据前台需求:usermodel对象封装后传到前台使用)
     */
    public ResultInfo loginCheck(String userName, String userPwd) {
        //校验参数是否为空
        checkLoginData(userName, userPwd);
        //调用dao层查询通过用户名查询数据库数据,判断账号是否存在
        User user = userMapper.queryUserByName(userName);
        AssertUtil.isTrue(user == null, "账号不存在");

        //校验前台传来的密码和数据库中的密码是否一致 (前台密码加密后再校验)
        checkLoginPwd(user.getUserPwd(),userPwd);

        //封装ResultInfo对象给前台(根据前台需求:usermodel对象封装后传到前台使用)
        ResultInfo resultInfo = buildResultInfo(user);


        //封装ResultInfo对象给前台(根据前台需求:usermodel对象封装后传到前台使用)
        /*ResultInfo resultInfo = new ResultInfo();
        UserModel userModel = new UserModel();
        userModel.setUserId(user.getId());
        userModel.setUserName(user.getUserName());
        userModel.setTrueName(user.getTrueName());
        resultInfo.setResult(userModel);*/
        return resultInfo;
    }


    /**
     * 准备前台cookie需要的数  usermodel
     * @param user
     */
    private ResultInfo buildResultInfo(User user) {
        ResultInfo resultInfo = new ResultInfo();

        //封装userMdel  cookie需要的数据
        UserModel userModel = new UserModel();
        //将userid加密
        String id = UserIDBase64.encoderUserID(user.getId());
        userModel.setUserId(id);
        userModel.setUserName(user.getUserName());
        userModel.setTrueName(user.getTrueName());

        resultInfo.setResult(userModel);
        return resultInfo;
    }

    private void checkLoginPwd(String dbPwd, String userPwd) {
        //将传来的密码加密再校验
        String encodePwd = Md5Util.encode(userPwd);
        //校验
        AssertUtil.isTrue(!encodePwd.equals(dbPwd), "用户密码错误");
    }

    /**
     * 用户登录参数非空校验
     *
     * @param userName
     * @param userPwd
     */
    private void checkLoginData(String userName, String userPwd) {
        AssertUtil.isTrue(StringUtils.isBlank(userName), "用户名不能为空");
        AssertUtil.isTrue(StringUtils.isBlank(userPwd), "密码不能为空");
    }
}
6. 3. 3. UserMapper

在 UserMapper 接口类中定义对应的查询方法

package com.xxxx.crm.dao;

import com.xxxx.crm.base.BaseMapper;
import com.xxxx.crm.vo.User;


public interface UserMapper extends BaseMapper<User,Integer> {
    //通过用户名称查询数据
    public User queryUserByName(String name);
}
6. 3. 4. UserMapper.xml

配置查询对应的 SQL 语句

<?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.xxxx.crm.dao.UserMapper" >
  <resultMap id="BaseResultMap" type="com.xxxx.crm.vo.User" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="user_name" property="userName" jdbcType="VARCHAR" />
    <result column="user_pwd" property="userPwd" jdbcType="VARCHAR" />
    <result column="true_name" property="trueName" jdbcType="VARCHAR" />
    <result column="email" property="email" jdbcType="VARCHAR" />
    <result column="phone" property="phone" jdbcType="VARCHAR" />
    <result column="is_valid" property="isValid" jdbcType="INTEGER" />
    <result column="create_date" property="createDate" jdbcType="TIMESTAMP" />
    <result column="update_date" property="updateDate" jdbcType="TIMESTAMP" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, user_name, user_pwd, true_name, email, phone, is_valid, create_date, update_date
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from t_user
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from t_user
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.xxxx.crm.vo.User" >
    insert into t_user (id, user_name, user_pwd, 
      true_name, email, phone, 
      is_valid, create_date, update_date
      )
    values (#{id,jdbcType=INTEGER}, #{userName,jdbcType=VARCHAR}, #{userPwd,jdbcType=VARCHAR}, 
      #{trueName,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR}, #{phone,jdbcType=VARCHAR}, 
      #{isValid,jdbcType=INTEGER}, #{createDate,jdbcType=TIMESTAMP}, #{updateDate,jdbcType=TIMESTAMP}
      )
  </insert>
  <insert id="insertSelective" parameterType="com.xxxx.crm.vo.User" >
    insert into t_user
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="userName != null" >
        user_name,
      </if>
      <if test="userPwd != null" >
        user_pwd,
      </if>
      <if test="trueName != null" >
        true_name,
      </if>
      <if test="email != null" >
        email,
      </if>
      <if test="phone != null" >
        phone,
      </if>
      <if test="isValid != null" >
        is_valid,
      </if>
      <if test="createDate != null" >
        create_date,
      </if>
      <if test="updateDate != null" >
        update_date,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="userName != null" >
        #{userName,jdbcType=VARCHAR},
      </if>
      <if test="userPwd != null" >
        #{userPwd,jdbcType=VARCHAR},
      </if>
      <if test="trueName != null" >
        #{trueName,jdbcType=VARCHAR},
      </if>
      <if test="email != null" >
        #{email,jdbcType=VARCHAR},
      </if>
      <if test="phone != null" >
        #{phone,jdbcType=VARCHAR},
      </if>
      <if test="isValid != null" >
        #{isValid,jdbcType=INTEGER},
      </if>
      <if test="createDate != null" >
        #{createDate,jdbcType=TIMESTAMP},
      </if>
      <if test="updateDate != null" >
        #{updateDate,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.xxxx.crm.vo.User" >
    update t_user
    <set >
      <if test="userName != null" >
        user_name = #{userName,jdbcType=VARCHAR},
      </if>
      <if test="userPwd != null" >
        user_pwd = #{userPwd,jdbcType=VARCHAR},
      </if>
      <if test="trueName != null" >
        true_name = #{trueName,jdbcType=VARCHAR},
      </if>
      <if test="email != null" >
        email = #{email,jdbcType=VARCHAR},
      </if>
      <if test="phone != null" >
        phone = #{phone,jdbcType=VARCHAR},
      </if>
      <if test="isValid != null" >
        is_valid = #{isValid,jdbcType=INTEGER},
      </if>
      <if test="createDate != null" >
        create_date = #{createDate,jdbcType=TIMESTAMP},
      </if>
      <if test="updateDate != null" >
        update_date = #{updateDate,jdbcType=TIMESTAMP},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.xxxx.crm.vo.User" >
    update t_user
    set user_name = #{userName,jdbcType=VARCHAR},
      user_pwd = #{userPwd,jdbcType=VARCHAR},
      true_name = #{trueName,jdbcType=VARCHAR},
      email = #{email,jdbcType=VARCHAR},
      phone = #{phone,jdbcType=VARCHAR},
      is_valid = #{isValid,jdbcType=INTEGER},
      create_date = #{createDate,jdbcType=TIMESTAMP},
      update_date = #{updateDate,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=INTEGER}
  </update>
  <select id="queryUserByName" parameterType="String" resultType="user">
    select * from t_user where is_valid = 1 and user_name=#{name}
  </select>
</mapper>
6. 3. 5. UserController

控制层定义接口,对接前台。Controller 层调用 Service 层 userLogin 方法,捕获 service 方法的异

常,获取登录结果,并将 ResultInfo 对象通过 JSON 格式响应给客户端。

package com.xxxx.crm.controller;

import com.xxxx.crm.base.BaseController;
import com.xxxx.crm.base.ResultInfo;
import com.xxxx.crm.exceptions.ParamsException;
import com.xxxx.crm.service.UserService;
import com.xxxx.crm.utils.LoginUserUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.Mapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("user")
public class UserController extends BaseController {

    @Resource
    private UserService userService;



/**
     *  用户登录
     * @param userName
     * @param userPwd
     */

    @PostMapping("login")
    @ResponseBody
    public ResultInfo login(String userName,String userPwd) {
       // return userService.loginCheck(userName, userPwd);

        ResultInfo resultInfo = new ResultInfo();
        try {
            resultInfo = userService.loginCheck(userName, userPwd);
        } catch (ParamsException e) {
            e.printStackTrace();
            resultInfo.setCode(400);
            resultInfo.setMsg(e.getMsg());
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo.setCode(500);
            resultInfo.setMsg("登录失败");
        }
        return resultInfo;
    }

}
6. 3. 6. Starter

修改启动类,在启动类上添加 @MapperScan 注解,设置扫描包范围。

package com.xxxx;

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

/**
 * Hello world!
 */
@SpringBootApplication
@MapperScan("com.xxxx.crm.dao")
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}
6.3.7. PostMan 测试

利用 Postman 工具,对用户登录的接口进行测试。

6.3.8. 前端登录功能实现

index.ftl 添加对应 index.js,使用 layui 表单组件实现表单提交操作。登录成功后,如果

参考API:https://www.layui.com/doc/modules/form.html#onsubmit

layui.use(['form','jquery','jquery_cookie'], function () {
    var form = layui.form,
        layer = layui.layer,
        $ = layui.jquery,
        $ = layui.jquery_cookie($);



    /**
     * 监听表单的提交
     *     on监听 submit事件
     */
    form.on("submit(login)",function (data){
        /*console.log(data.elem);
        console.log(data.form);*/
        console.log(data.field) //当前容器的全部表单字段,名值对形式:{name: value}

        //数据校验 TODO
        //使用了lay-verify表单验证
        //发送请求
        $.ajax({
            type:"post",
            url: ctx + "/user/login",
            data:{
                userName:data.field.username,
                userPwd:data.field.password
            },
            dataType:'json',
            success:function (data){
                if(data.code == 200){

                    //存储cookie
                   /* $.cookie("userId",data.result.userId);
                    $.cookie("userName",data.result.userName);
                    $.cookie("trueName",data.result.trueName);*/
                    $.cookie("userIdStr",data.result.userId);
                    $.cookie("userName",data.result.userName);
                    $.cookie("trueName",data.result.trueName);

                    //记住密码
                    if($("#rememberMe").prop("checked")){
                        $.cookie("userIdStr", data.result.userId, { expires: 7 });
                        $.cookie("userName", data.result.userName, { expires: 7 });
                        $.cookie("trueName", data.result.trueName, { expires: 7 });
                    }

                    //跳转到首页
                    window.location.href = ctx + "/main";
                }else{
                    layer.msg(data.msg,{icon:5});
                }
            }
        });

        return false; //阻止表单跳转。如果需要表单跳转,去掉这段即可。
    });
});
6.3.9. 修改 Cookie 的数据

将 Cookie 中的 userId 的值加密存储。

6.3.10. 主页面显示用户名信息
6.3.11. 启动程序测试登录效果

使用测试账号执行登录操作。(用户名:admin ,密码:123)

7. 密码修改功能实现

7. 1. 核心思路分析

  1. 确保用户是否是登录状态获取cookie中的id 非空 查询数据库

  2. 校验老密码 非空 老密码必须要跟数据库中密码一致

  3. 新密码 非空 新密码不能和原密码一致

  4. 确认密码 非空 确认必须和新密码一致

  5. 执行修改操作,返回ResultInfo

7. 2. UserService

updateUserPassword 方法实现

/** 修改密码
     */
    public void userUpdate(Integer userId,String oldPassword,String newPassword,String confirmPassword){
        //确保用户是否是登录状态获取cookie中的id 非空 查询数据库
        AssertUtil.isTrue(userId == null,"用户未登录");
        User user = userMapper.selectByPrimaryKey(userId);
        AssertUtil.isTrue(user == null,"用户状态异常");

        //校验密码数据
        checkUpdateData(oldPassword,newPassword,confirmPassword,user.getUserPwd());
        // 执行修改操作,返回ResultInfo
        user.setUserPwd(Md5Util.encode(newPassword));
        user.setUpdateDate(new Date());
        //判断是否修改成功
        AssertUtil.isTrue(userMapper.updateByPrimaryKeySelective(user) < 1,"密码修改失败");
    }

    /**密码校验
     *          1.确保用户是否是登录状态获取cookie中的id 非空 查询数据库
     *          2.校验老密码 非空  老密码必须要跟数据库中密码一致
     *          3.新密码    非空  新密码不能和原密码一致
     *          4.确认密码  非空  确认必须和新密码一致
     *          5.执行修改操作,返回ResultInfo
     * @param oldPassword
     * @param newPassword
     * @param confirmPassword
     * @param dbPassword
     */
    private void checkUpdateData(String oldPassword, String newPassword, String confirmPassword, String dbPassword) {
        //校验老密码  非空  老密码必须要跟数据库中密码一致
        AssertUtil.isTrue(StringUtils.isBlank(oldPassword),"原始密码不存在");
        AssertUtil.isTrue(!dbPassword.equals(Md5Util.encode(oldPassword)),"原始密码错误");

        //新密码    非空  新密码不能和原密码一致
        AssertUtil.isTrue(StringUtils.isBlank(newPassword),"新密码不能为空");
        AssertUtil.isTrue(oldPassword.equals(newPassword),"新密码不能和原密码一致");

        //确认密码  非空  确认必须和新密码一致
        AssertUtil.isTrue(StringUtils.isBlank(confirmPassword),"确认密码不能为空");
        AssertUtil.isTrue(!confirmPassword.equals(newPassword),"确认密码必须和新密码一致");
    }

7.3. UserController

updateUserPassword 方法实现

/**
     *  修改密码

     */
    @PostMapping("update")
    @ResponseBody
    public ResultInfo update(HttpServletRequest request, String oldPassword, String newPassword, String confirmPassword){
        ResultInfo resultInfo = new ResultInfo();
        //int i = 1/0;
        //获取登录用户的id
        int id = LoginUserUtil.releaseUserIdFromCookie(request);
        //userService.userUpdate(id,oldPassword,newPassword,confirmPassword);
        try {
            userService.userUpdate(id,oldPassword,newPassword,confirmPassword);
        } catch (ParamsException e) {
            e.printStackTrace();
            resultInfo.setCode(400);
            resultInfo.setMsg(e.getMsg());
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo.setCode(500);
            resultInfo.setMsg("修改密码失败");
        }
       // return success();
        return resultInfo;
    }

7.4. PostMan 测试

7.4.1. Postman 中添加 Cookie

7.5. 前端核心代码

8. 用户退出功能实现

8. 1. 退出登录

找到 “退出登录” 的元素,并绑定点击事件。当用户点击退出时,清空cookie信息

在 main.js 中,通过类选择器绑定元素的点击事件

9. 全局异常统一处理

9. 1. 全局异常实现思路

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

  1. 视图:视图异常

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

9. 2. 全局异常拦截器实现

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

package com.xxxx.crm;

import com.alibaba.fastjson.JSON;
import com.xxxx.crm.base.ResultInfo;
import com.xxxx.crm.exceptions.NoLoginException;
import com.xxxx.crm.exceptions.ParamsException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Component
public class  GlobalExceptionResolver implements HandlerExceptionResolver {

    /**
     * 控制层的方法返回的内容两种情况
     * 	    1. 视图:视图异常
     *   	2. Json:方法执行错误 返回错误json信息
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @return
     */
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ModelAndView mv = new ModelAndView();

        if(ex instanceof NoLoginException){
            NoLoginException ne = (NoLoginException)ex;
            // mv.setViewName("index");   目前是直接去找视图
            //目的是跳转到登录页面   必须通过接口才能显示
            mv.setViewName("redirect:index");
            return mv;
        }

        //设置默认的异常处理
        mv.setViewName("error");
        mv.addObject("code",300);
        mv.addObject("msg","数据异常,请重试");

        //判断目标方法返回的是视图还是json数据
        if(handler instanceof HandlerMethod){
            //转换成controller方法对象
            HandlerMethod handlerMethod = (HandlerMethod)handler;
            //获取responsebody注解对象
            ResponseBody reponsebody = handlerMethod.getMethod().getDeclaredAnnotation(ResponseBody.class);

            //判断当前方法是否存在responsebody注解
            if(reponsebody == null){
                //返回视图的接口异常处理

                if(ex instanceof ParamsException){
                    ParamsException pe = (ParamsException)ex;
                    mv.addObject("code",pe.getCode());
                    mv.addObject("msg",pe.getMsg());
                }
                return mv;
            }else{
                //返回json的接口异常处理
                ResultInfo resultInfo = new ResultInfo();
                resultInfo.setCode(500);
                resultInfo.setMsg("系统异常请重试");

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

                //将resultinfo数据传给前台的ajax回调函数

                //设置数据传输的类型和编码格式
                response.setContentType("application/json;charset=utf-8");

                PrintWriter writer = null;
                try {
                    //获取输出流
                    writer = response.getWriter();
                    //将数据对象转换成json格式的,传输出去
                    writer.write(JSON.toJSONString(resultInfo));
                    writer.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    if(writer != null){
                        writer.close();
                    }
                }
                return null;
            }
        }

        return mv;
    }
}

9. 3. 消除 try-catch 代码

系统引入全局异常,简化控制层 try-catch 代码

package com.xxxx.crm.controller;

import com.xxxx.crm.base.BaseController;
import com.xxxx.crm.base.ResultInfo;
import com.xxxx.crm.exceptions.ParamsException;
import com.xxxx.crm.service.UserService;
import com.xxxx.crm.utils.LoginUserUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.Mapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("user")
public class UserController extends BaseController {

    @Resource
    private UserService userService;



/**
     *  用户登录
     * @param userName
     * @param userPwd
     */

    @PostMapping("login")
    @ResponseBody
    public ResultInfo login(String userName,String userPwd) {
        return userService.loginCheck(userName, userPwd);

       /* ResultInfo resultInfo = new ResultInfo();
        try {
            resultInfo = userService.loginCheck(userName, userPwd);
        } catch (ParamsException e) {
            e.printStackTrace();
            resultInfo.setCode(400);
            resultInfo.setMsg(e.getMsg());
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo.setCode(500);
            resultInfo.setMsg("登录失败");
        }
        return resultInfo;*/
    }

    /**
     *  修改密码

     */
    @PostMapping("update")
    @ResponseBody
    public ResultInfo update(HttpServletRequest request, String oldPassword, String newPassword, String confirmPassword){
        //ResultInfo resultInfo = new ResultInfo();
        //int i = 1/0;
        //获取登录用户的id
        int id = LoginUserUtil.releaseUserIdFromCookie(request);
        userService.userUpdate(id,oldPassword,newPassword,confirmPassword);
        /*try {
            userService.userUpdate(id,oldPassword,newPassword,confirmPassword);
        } catch (ParamsException e) {
            e.printStackTrace();
            resultInfo.setCode(400);
            resultInfo.setMsg(e.getMsg());
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo.setCode(500);
            resultInfo.setMsg("修改密码失败");
        }*/
        return success();
        //return resultInfo;
    }





/**
     *  修改密码

     */

   /* @PostMapping("update")
    @ResponseBody
    public ResultInfo update(HttpServletRequest request, String oldPassword, String newPassword, String confirmPassword){
        int i = 1/0;
        //获取登录用户的id
        int id = LoginUserUtil.releaseUserIdFromCookie(request);
            userService.userUpdate(id,oldPassword,newPassword,confirmPassword);
        return success();
    }*/


    //打开修改密码页面
    @RequestMapping("toPasswordPage")
    public String toPasswordPage(){
        //int i = 1/0;
        return "user/password";
    }
}

10. 非法请求拦截

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

能。

10. 1. 实现思路

判断用户是否是登录状态

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

如果用户ID不为空,且在数据库中存在对应的用户记录,表示请求合法

否则,请求不合法,进行拦截,重定向到登录页面

10. 2. 定义拦截器

在新建 interceptors 包,创建 NoLoginInterceptor 类,并继承 HandlerInterceptorAdapter 适配器,

实现拦截器功能。

package com.xxxx.crm.interceptors;

import com.xxxx.crm.exceptions.NoLoginException;
import com.xxxx.crm.service.UserService;
import com.xxxx.crm.utils.LoginUserUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class LoginInterceptor implements HandlerInterceptor {

    @Resource
    private UserService userService;

    /**
     * 在请求到达目标接口之前,拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //通过 cookie中的userIdStr 判断用户是否是登录状态
        int id = LoginUserUtil.releaseUserIdFromCookie(request);
        if(id == 0 || null == userService.selectByPrimaryKey(id)){
            throw new NoLoginException();
        }

        return true; //放行 执行目标接口方法
    }
}

10. 3. 全局异常类配置

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

package com.xxxx.crm.exceptions;

/**
 * 自定义参数异常
 */
public class NoLoginException extends RuntimeException {
    private Integer code=300;
    private String msg="用户未登录!";


    public NoLoginException() {
        super("用户未登录!");
    }

    public NoLoginException(String msg) {
        super(msg);
        this.msg = msg;
    }

    public NoLoginException(Integer code) {
        super("用户未登录!");
        this.code = code;
    }

    public NoLoginException(Integer code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

10. 4. 拦截器生效配置

新建 config 包,添加拦截器生效的配置类

package com.xxxx.crm.config;

import com.xxxx.crm.interceptors.LoginInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Bean
    public LoginInterceptor createLoginInterceptor(){
        return new LoginInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(createLoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/index","/user/login","/css/**","/images/**","/js/**","/lib/**");
    }
}

10. 5. 拦截测试

10. 6. 测试拦截效果

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

11. 记住我功能实现

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

11. 1. 修改 index.ftl

在用户登录表单中添加记住密码的复选框

<#-- 记住我 -->
<div class="layui-form-item">
  <input type="checkbox" name="rememberMe" id="rememberMe" value="true" lay-skin="primary" title="记住密码">
</div>

11. 2. 修改 index.js

如果用户在登录时,勾选了 “记住我” 的复选框,则在登录成功之后,设置 cookie 的有效期

//记住密码
if($("#rememberMe").prop("checked")){
  $.cookie("userIdStr", data.result.userId, { expires: 7 });
  $.cookie("userName", data.result.userName, { expires: 7 });
  $.cookie("trueName", data.result.trueName, { expires: 7 });
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值