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:8080servlet: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%2B8username: root password:123456## freemarkerfreemarker:suffix: .ftl content-type: text/html charset: UTF-8template-loader-path: classpath:/views/ ## 启用热部署devtools:restart:enabled:trueadditional-paths: src/main/java ## mybatis 配置mybatis:mapper-locations: classpath:/mappers/*.xmltype-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 包,详细文件 见相关目录)packagecom.xxxx.crm.controller;importcom.xxxx.crm.base.BaseController;importcom.xxxx.crm.service.UserService;importcom.xxxx.crm.utils.LoginUserUtil;importcom.xxxx.crm.vo.User;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importjavax.annotation.Resource;importjavax.servlet.http.HttpServletRequest;@ControllerpublicclassIndexControllerextendsBaseController{@ResourceprivateUserService userService;/** * 系统登录页 * @return */@RequestMapping("index")publicStringindex(){return"index";}// 系统界面欢送页@RequestMapping("welcome")publicStringwelcome(){return"welcome";}/** * 后端管理主页面 * @return */@RequestMapping("main")publicStringmain(){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 ,添加启动项目相关代码如下:packagecom.xxxx;importorg.mybatis.spring.annotation.MapperScan;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/** * Hello world! */@SpringBootApplication@MapperScan("com.xxxx.crm.dao")publicclassApp{publicstaticvoidmain(String[] args){SpringApplication.run(App.class);}} 复制代码 5. 2. 8. 项目目录构造
5. 2. 9. 阅读器访问
CHR ome阅读器访问登录页地址: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"?><!DOCTYPEgeneratorConfigurationPUBLIC"-//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 --><classPathEntrylocation="E:\.m2\maven_repository\mysql\mysql-connector-java\8.0.18\mysql-connector-java-8.0.18.jar"/><!-- context 是逆向工程的主要配置信息,id:起个名字,targetRuntime:设置生成的文件适用于哪个mybatis版本 --><contextid="DB2Tables"targetRuntime="MyBatis3"><!--optional,指在创建class时,对注释停止控制--><commentGenerator><!-- 是否去除日期那行注释 --><propertyname="suppressDate"value="true"/><!-- 是否去除自动生成的注释 true:是 : false:否 --><propertyname="suppressAllComments"value="true"/></commentGenerator><!-- 数据库链接地址账号密码 --><jdbcConnectiondriverClass="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><propertyname="forceBigDecimals"value="false"/></javaTypeResolver><!-- 生成Model类寄存位置 --><javaModelGeneratortargetPackage="com.xxxx.crm.vo"targetProject="src/main/java"><!-- 在targetPackage的根底上,根据数据库的schema再生成一层package,生成的类放在这个package下,默认为false --><propertyname="enableSubPackages"value="true"/><!-- 设置是否在getter方法中,对String类型字段调用trim()方法 --><propertyname="trimStrings"value="true"/></javaModelGenerator><!--生成映射文件寄存位置--><sqlMapGeneratortargetPackage="mappers"targetProject="src/main/resources"><propertyname="enableSubPackages"value="true"/></sqlMapGenerator><!--生成Dao类寄存位置--><javaClientGeneratortype="XMLMAPPER"targetPackage="com.xxxx.crm.dao"targetProject="src/main/java"><propertyname="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
校验参数是否为空 假设为空,抛异常
调用dao层查询通过用户名查询数据库数据 假设未查到,抛异常(用户不存在)
校验前台传来的密码和数据库中的密码是否一致 (前台密码加密后再校验) 假设不一致,抛异常(密码错误)
封装ResultInfo对象给前台(根据前台需求:usermodel对象封装后传到前台使用)
dao
通过用户名查询数据库数据
6. 3. 核心代码实现
6. 3. 1. UserModel
定义 UserModel 实体类,用来返回登录胜利后的用户信息packagecom.xxxx.crm.query;publicclassUserModel{//private Integer userId;privateString userId;privateString userName;privateString trueName;/*public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } */publicStringgetUserId(){return userId;}publicvoidsetUserId(String userId){this.userId = userId;}publicStringgetUserName(){return userName;}publicvoidsetUserName(String userName){this.userName = userName;}publicStringgetTrueName(){return trueName;}publicvoidsetTrueName(String trueName){this.trueName = trueName;}} 复制代码 6. 3. 2. UserService
用户登录详细的业务逻辑的实现packagecom.xxxx.crm.service;importcom.xxxx.crm.base.BaseService;importcom.xxxx.crm.base.ResultInfo;importcom.xxxx.crm.dao.UserMapper;importcom.xxxx.crm.query.UserModel;importcom.xxxx.crm.utils.AssertUtil;importcom.xxxx.crm.utils.Md5Util;importcom.xxxx.crm.utils.UserIDBase64;importcom.xxxx.crm.vo.User;importorg.apache.commons.lang3.StringUtils;importorg.springframework.stereotype.Service;importjavax.annotation.Resource;importjava.util.Date;@ServicepublicclassUserServiceextendsBaseService<User,Integer>{@ResourceprivateUserMapper userMapper;/** * 用户登录 * 2.校验参数是否为空 * 假设为空,抛异常 * 3.调用dao层查询通过用户名查询数据库数据 * 假设未查到,抛异常(用户不存在) * 4.校验前台传来的密码和数据库中的密码是否一致 (前台密码加密后再校验) * 假设不一致,抛异常(密码错误) * 5.封装ResultInfo对象给前台(根据前台需求:usermodel对象封装后传到前台使用) */publicResultInfologinCheck(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 */privateResultInfobuildResultInfo(User user){ResultInfo resultInfo =newResultInfo();//封装userMdel cookie需要的数据UserModel userModel =newUserModel();//将userid加密String id =UserIDBase64.encoderUserID(user.getId()); userModel.setUserId(id); userModel.setUserName(user.getUserName()); userModel.setTrueName(user.getTrueName()); resultInfo.setResult(userModel);return resultInfo;}privatevoidcheckLoginPwd(String dbPwd,String userPwd){//将传来的密码加密再校验String encodePwd =Md5Util.encode(userPwd);//校验AssertUtil.isTrue(!encodePwd.equals(dbPwd),"用户密码错误");}/** * 用户登录参数非空校验 * * @param userName * @param userPwd */privatevoidcheckLoginData(String userName,String userPwd){AssertUtil.isTrue(StringUtils.isBlank(userName),"用户名不能为空");AssertUtil.isTrue(StringUtils.isBlank(userPwd),"密码不能为空");}} 复制代码 6. 3. 3. UserMapper
在 UserMapper 接口类中定义对应的查询方法packagecom.xxxx.crm.dao;importcom.xxxx.crm.base.BaseMapper;importcom.xxxx.crm.vo.User;publicinterfaceUserMapperextendsBaseMapper<User,Integer>{//通过用户名称查询数据publicUserqueryUserByName(String name);} 复制代码 6. 3. 4. UserMapper.xml
配置查询对应的 SQL 语句<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.xxxx.crm.dao.UserMapper"><resultMapid="BaseResultMap"type="com.xxxx.crm.vo.User"><idcolumn="id"property="id"jdbcType="INTEGER"/><resultcolumn="user_name"property="userName"jdbcType="VARCHAR"/><resultcolumn="user_pwd"property="userPwd"jdbcType="VARCHAR"/><resultcolumn="true_name"property="trueName"jdbcType="VARCHAR"/><resultcolumn="email"property="email"jdbcType="VARCHAR"/><resultcolumn="phone"property="phone"jdbcType="VARCHAR"/><resultcolumn="is_valid"property="isValid"jdbcType="INTEGER"/><resultcolumn="create_date"property="createDate"jdbcType="TIMESTAMP"/><resultcolumn="update_date"property="updateDate"jdbcType="TIMESTAMP"/></resultMap><sqlid="Base_Column_List"> id, user_name, user_pwd, true_name, email, phone, is_valid, create_date, update_date </sql><selectid="selectByPrimaryKey"resultMap="BaseResultMap"parameterType="java.lang.Integer"> select <includerefid="Base_Column_List"/> from t_user where id = #{id,jdbcType=INTEGER} </select><deleteid="deleteByPrimaryKey"parameterType="java.lang.Integer"> delete from t_user where id = #{id,jdbcType=INTEGER} </delete><insertid="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><insertid="insertSelective"parameterType="com.xxxx.crm.vo.User"> insert into t_user <trimprefix="("suffix=")"suffixOverrides=","><iftest="id != null"> id, </if><iftest="userName != null"> user_name, </if><iftest="userPwd != null"> user_pwd, </if><iftest="trueName != null"> true_name, </if><iftest="email != null"> email, </if><iftest="phone != null"> phone, </if><iftest="isValid != null"> is_valid, </if><iftest="createDate != null"> create_date, </if><iftest="updateDate != null"> update_date, </if></trim><trimprefix="values ("suffix=")"suffixOverrides=","><iftest="id != null"> #{id,jdbcType=INTEGER}, </if><iftest="userName != null"> #{userName,jdbcType=VARCHAR}, </if><iftest="userPwd != null"> #{userPwd,jdbcType=VARCHAR}, </if><iftest="trueName != null"> #{trueName,jdbcType=VARCHAR}, </if><iftest="email != null"> #{email,jdbcType=VARCHAR}, </if><iftest="phone != null"> #{phone,jdbcType=VARCHAR}, </if><iftest="isValid != null"> #{isValid,jdbcType=INTEGER}, </if><iftest="createDate != null"> #{createDate,jdbcType=TIMESTAMP}, </if><iftest="updateDate != null"> #{updateDate,jdbcType=TIMESTAMP}, </if></trim></insert><updateid="updateByPrimaryKeySelective"parameterType="com.xxxx.crm.vo.User"> update t_user <set><iftest="userName != null"> user_name = #{userName,jdbcType=VARCHAR}, </if><iftest="userPwd != null"> user_pwd = #{userPwd,jdbcType=VARCHAR}, </if><iftest="trueName != null"> true_name = #{trueName,jdbcType=VARCHAR}, </if><iftest="email != null"> email = #{email,jdbcType=VARCHAR}, </if><iftest="phone != null"> phone = #{phone,jdbcType=VARCHAR}, </if><iftest="isValid != null"> is_valid = #{isValid,jdbcType=INTEGER}, </if><iftest="createDate != null"> create_date = #{createDate,jdbcType=TIMESTAMP}, </if><iftest="updateDate != null"> update_date = #{updateDate,jdbcType=TIMESTAMP}, </if></set> where id = #{id,jdbcType=INTEGER} </update><updateid="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><selectid="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 格式响应给客户端。packagecom.xxxx.crm.controller;importcom.xxxx.crm.base.BaseController;importcom.xxxx.crm.base.ResultInfo;importcom.xxxx.crm.exceptions.ParamsException;importcom.xxxx.crm.service.UserService;importcom.xxxx.crm.utils.LoginUserUtil;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.Mapping;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.ResponseBody;importjavax.annotation.Resource;importjavax.servlet.http.HttpServletRequest;@Controller@RequestMapping("user")publicclassUserControllerextendsBaseController{@ResourceprivateUserService userService;/** * 用户登录 * @param userName * @param userPwd */@PostMapping("login")@ResponseBodypublicResultInfologin(String userName,String userPwd){// return userService.loginCheck(userName, userPwd);ResultInfo resultInfo =newResultInfo();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 注解,设置扫描包范围。packagecom.xxxx;importorg.mybatis.spring.annotation.MapperScan;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/** * Hello world! */@SpringBootApplication@MapperScan("com.xxxx.crm.dao")publicclassApp{publicstaticvoidmain(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#onsubmitlayui.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});}}});returnfalse;//阻止表单跳转。假设需要表单跳转,去掉这段即可。});}); 复制代码 6.3.9. 修改 Cookie 的数据
将 Cookie 中的 userId 的值加密存储。
6.3.10. 主页面显示用户名信息
6.3.11. 启动程序测试登录效果
使用测试账号执行登录操作。(用户名:admin ,密码:123)
7. 密码修改功能实现
7. 1. 核心思路分析
确保用户是否是登录状态获取cookie中的id 非空 查询数据库
校验老密码 非空 老密码必需要跟数据库中密码一致
新密码 非空 新密码不能和原密码一致
确认密码 非空 确认必需和新密码一致
执行修改操作,返回ResultInfo
7. 2. UserService
updateUsERP assword 方法实现/** 修改密码 */publicvoiduserUpdate(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(newDate());//判断是否修改胜利AssertUtil.isTrue(userMapper.updateByPrimaryKeySelective(user)<1,"密码修改失败");}/**密码校验 * 1.确保用户是否是登录状态获取cookie中的id 非空 查询数据库 * 2.校验老密码 非空 老密码必需要跟数据库中密码一致 * 3.新密码 非空 新密码不能和原密码一致 * 4.确认密码 非空 确认必需和新密码一致 * 5.执行修改操作,返回ResultInfo * @param oldPassword * @param newPassword * @param confirmPassword * @param dbPassword */privatevoidcheckUpdateData(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")@ResponseBodypublicResultInfoupdate(HttpServletRequest request,String oldPassword,String newPassword,String confirmPassword){ResultInfo resultInfo =newResultInfo();//int i = 1/0;//获取登录用户的idint 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 接口 ,处置应用程序异常信息packagecom.xxxx.crm;importcom.alibaba.fastjson.JSON;importcom.xxxx.crm.base.ResultInfo;importcom.xxxx.crm.exceptions.NoLoginException;importcom.xxxx.crm.exceptions.ParamsException;importorg.springframework.stereotype.Component;importorg.springframework.web.bind.annotation.ResponseBody;importorg.springframework.web.method.HandlerMethod;importorg.springframework.web.servlet.HandlerExceptionResolver;importorg.springframework.web.servlet.ModelAndView;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;importjava.io.PrintWriter;@ComponentpublicclassGlobalExceptionResolverimplementsHandlerExceptionResolver{/** * 控制层的方法返回的内容两种情况 * 1. 视图:视图异常 * 2. Json:方法执行错误 返回错误json信息 * @param request * @param response * @param handler * @param ex * @return */@OverridepublicModelAndViewresolveException(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex){ModelAndView mv =newModelAndView();if(ex instanceofNoLoginException){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 instanceofHandlerMethod){//转换成controller方法对象HandlerMethod handlerMethod =(HandlerMethod)handler;//获取responsebody注解对象ResponseBody reponsebody = handlerMethod.getMethod().getDeclaredAnnotation(ResponseBody.class);//判断当前方法是否存在responsebody注解if(reponsebody ==null){//返回视图的接口异常处置if(ex instanceofParamsException){ParamsException pe =(ParamsException)ex; mv.addObject("code",pe.getCode()); mv.addObject("msg",pe.getMsg());}return mv;}else{//返回json的接口异常处置ResultInfo resultInfo =newResultInfo(); resultInfo.setCode(500); resultInfo.setMsg("系统异常请重试");//判断是否是自定义异常if(ex instanceofParamsException){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();}}returnnull;}}return mv;}} 复制代码 9. 3. 消除 try-catch 代码
系统引入全局异常,简化控制层 try-catch 代码packagecom.xxxx.crm.controller;importcom.xxxx.crm.base.BaseController;importcom.xxxx.crm.base.ResultInfo;importcom.xxxx.crm.exceptions.ParamsException;importcom.xxxx.crm.service.UserService;importcom.xxxx.crm.utils.LoginUserUtil;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.Mapping;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.ResponseBody;importjavax.annotation.Resource;importjavax.servlet.http.HttpServletRequest;@Controller@RequestMapping("user")publicclassUserControllerextendsBaseController{@ResourceprivateUserService userService;/** * 用户登录 * @param userName * @param userPwd */@PostMapping("login")@ResponseBodypublicResultInfologin(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")@ResponseBodypublicResultInfoupdate(HttpServletRequest request,String oldPassword,String newPassword,String confirmPassword){//ResultInfo resultInfo = new ResultInfo();//int i = 1/0;//获取登录用户的idint 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("修改密码失败"); }*/returnsuccess();//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")publicStringtoPasswordPage(){//int i = 1/0;return"user/password";}} 复制代码 10. 非法恳求拦截
对于后端菜单资源,这里要求用户必需停止登录来维护 web 资源的安全性,此时引入非法恳求拦截功
能。
10. 1. 实现思路
判断用户是否是登录状态
获取Cookie对象,解析用户ID的值
假设用户ID不为空,且在数据库中存在对应的用户记录,表示恳求合法
否则,恳求不合法,停止拦截,重定向到登录页面
10. 2. 定义拦截器
在新建 interceptors 包,创建 NoLoginInterceptor 类,并继承 HandlerInterceptorAdapter 适配器,
实现拦截器功能。packagecom.xxxx.crm.interceptors;importcom.xxxx.crm.exceptions.NoLoginException;importcom.xxxx.crm.service.UserService;importcom.xxxx.crm.utils.LoginUserUtil;importorg.springframework.web.servlet.HandlerInterceptor;importorg.springframework.web.servlet.handler.HandlerInterceptorAdapter;importjavax.annotation.Resource;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;publicclassLoginInterceptorimplementsHandlerInterceptor{@ResourceprivateUserService userService;/** * 在恳求到达目的接口之前,拦截 * @param request * @param response * @param handler * @return * @throws Exception */@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{//通过 cookie中的userIdStr 判断用户是否是登录状态int id =LoginUserUtil.releaseUserIdFromCookie(request);if(id ==0||null== userService.selectByPrimaryKey(id)){thrownewNoLoginException();}returntrue;//放行 执行目的接口方法}} 复制代码 10. 3. 全局异常类配置
在全局异常处置类中引入未登录异常判断packagecom.xxxx.crm.exceptions;/** * 自定义参数异常 */publicclassNoLoginExceptionextendsRuntimeException{privateInteger code=300;privateString msg="用户未登录!";publicNoLoginException(){super("用户未登录!");}publicNoLoginException(String msg){super(msg);this.msg = msg;}publicNoLoginException(Integer code){super("用户未登录!");this.code = code;}publicNoLoginException(Integer code,String msg){super(msg);this.code = code;this.msg = msg;}publicIntegergetCode(){return code;}publicvoidsetCode(Integer code){this.code = code;}publicStringgetMsg(){return msg;}publicvoidsetMsg(String msg){this.msg = msg;}} 复制代码 10. 4. 拦截器生效配置
新建 config 包,添加拦截器生效的配置类packagecom.xxxx.crm.config;importcom.xxxx.crm.interceptors.LoginInterceptor;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;@ConfigurationpublicclassMvcConfigimplementsWebMvcConfigurer{@BeanpublicLoginInterceptorcreateLoginInterceptor(){returnnewLoginInterceptor();}@OverridepublicvoidaddInterceptors(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
在用户登录表单中添加记住密码的复选框<#--记住我--><divclass="layui-form-item"><inputtype="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});} 复制代码