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. 企业项目开发流程
- 产品组根据市场调研或商户同事的反馈提出 idea,设计出原型然后跟市场, 商户同事进行确认
- UI 设计组和开发组一起讨论,确定方案是否可行
- UI 组根据产品组提供的原型稿做出设计稿,与产品和开发确认
- 开发组根据产品的原型稿(看逻辑)和UI组的设计稿(看界面)编写代码其中当然也会来回跟设计, 产品 同学进行确认和沟通
- 代码编写完毕后提交给测试组. 然后再提交上线
- 后期的数据跟踪和优化
这就是一个产品研发的大致流程。其中开发的责任就是选用合适的框架技术来完成产品所提供的需求 以及设计所提供的效果。
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. 核心思路分析
前台
-
获取用户输入的数据
-
校验用户输入的数据
-
发送ajax请求到后台
-
接收后台返回的数据ResultInfo(封装UserModel,将数据存放在cookie中,保持登录状态)
后台
-
接收参数
-
校验参数是否为空 如果为空,抛异常
-
通过用户名查询数据库数据 如果未查到,抛异常(用户不存在)
-
校验前台传来的密码和数据库中的密码是否一致 (前台密码加密后再校验) 如果不一致,抛异常(密码错误)
-
封装ResultInfo对象给前台(根据前台需求:usermodel对象封装后传到前台使用)
分层思想
controller
- 接收参数,调用service
service
-
校验参数是否为空 如果为空,抛异常
-
调用dao层查询通过用户名查询数据库数据 如果未查到,抛异常(用户不存在)
-
校验前台传来的密码和数据库中的密码是否一致 (前台密码加密后再校验) 如果不一致,抛异常(密码错误)
-
封装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. 核心思路分析
-
确保用户是否是登录状态获取cookie中的id 非空 查询数据库
-
校验老密码 非空 老密码必须要跟数据库中密码一致
-
新密码 非空 新密码不能和原密码一致
-
确认密码 非空 确认必须和新密码一致
-
执行修改操作,返回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. 全局异常实现思路
控制层的方法返回的内容两种情况
-
视图:视图异常
-
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 });
}