CRM(客户关系管理)项目总结

1.摘要:

CRM客户关系管理系统用于管理与客户相关的信息与活动,包括企业与顾客间在销售、营销和服务上的交互,从而提升其管理方式,向客户提供创新式的个性化的客户交互和服务的过程。CRM不仅仅是一个软件,它是方法论、软件和IT能力综合,是商业策略。其最终目标是吸引新客户、保留老客户以及将已有客户转为忠实客户。

2.开发人员组成

管理员:项目经理,产品经理
开发者: Java开发人员5人

3.开发时长

共计:三个月,完成了项目的基础功能,扩展功能还要继续完善。

4.项目介绍

4.1前后端分离

后台:
在这里插入图片描述
前台:在这里插入图片描述

4.2项目主要模块

在这里插入图片描述

  • 1.营销管理
    营销管理:潜在客户管理、潜在客户开发计划
    营销的过程是开发新客户的过程。对老客户的销售行为不属于营销管理的范畴。所有的潜在客户由销售主管进行分配,每个潜在客户分配给一个市场专员。市场专员对分配给自己的潜在客户制定客户开发计划,计划好分几步开发,以及每个步骤的时间和具体事项。

  • 2.客户管理
    客户管理:客户信息管理、客户跟进历史、客户移交记录
    市场专员收集客户详细信息,建立客户表,后期市场专员跟进客户,市场部经理可以通过表进行客户移交到特定市场专员,潜在客户长期无法发展为正式客户的可以移到客户资源池管理。

  • 3.系统管理
    系统管理:角色管理、租户管理、套餐管理、权限管理、资源管理、菜单管理
    本系统是基于saas服务的。统一开放,维护,租户(注册付费的公司)需要在本系统中进行注册,并付费,然后根据付费情况使用系统功能。所有租户的数据都存放在一个数据库的同一套表中, 在表中增加tenant_id标志字段,表明该记录是属于哪个租户的。公司注册后、购买套餐然后分配权限再添加自己的员工管理等;然后租户和员工就拥有相应的权限进行增删改查等操作。

  • 4.订单合同管理
    订单合同管理:定金订单管理、合同管理
    在订单页面里面有一个按钮,可以生成合同,需要把部分订单的信息插入合同里面。

  • 5.售后管理
    公司对购买产品用户进行产品质量的保修。由合同完成自动生成。
    保单和保单明细组合关系。

  • 6.组织管理
    组织管理:部门管理、员工管理
    公司的部门和员工,可对进行增删改查,表中数据展示员工与部门的关系以及员工在职或者离职状态。

  • 7.数据管理
    数据管理:数据字典管理、数据字典明细
    系统各模块中,会有很多特别简单,且需要客户公司自己维护的信息。为简化对这类信息的维护,基于表的抽取设计思想,建立数据字典模块。

4.3项目分工

我负责的是营销管理,主要是对把采集到的潜在客户信息新增录入到系统中然后在对其进行开发计划实施。然后再建立客户的详情,建立客户表,然后把成功机率小的或者多次搞不定的客户可以分配到资源池,让其他专员再次开发。把已付费的客户进行移交,未付费的客户继续跟进。

4.4项目技术

  • saas平台
    本系统是基于saas服务的。统一开放,维护,租户(注册付费的公司)需要在本系统中进行注册,并付费,然后根据付费情况使用系统功能。所有租户的数据都存放在一个数据库的同一套表中, 在表中增加tenant_id标志字段,表明该记录是属于哪个租户的。租户在平台注册–>购买相应套餐–>具备对应的角色(购买不同的套餐,所含由角色不一样),也可以新增自己的员工 并可以对应自己员工分配角色(只能从自己购买的套餐里面选择存在的权限)。
  • Vue.js中使用百度地图
    在注册租户时候,调用百度地图接口,给公司选址。
    在这里插入图片描述

main.js中


import axios from 'axios'
import BaiduMap from 'vue-baidu-map'

/*使用百度地图*/
Vue.use(BaiduMap, {
    // ak 是在百度地图开发者平台申请的密钥 详见 http://lbsyun.baidu.com/apiconsole/key */
    ak: 'tt1KHvaLlRGxPxUS6TeYmIN4hyOIh800'
})

register.vue


```java
<template>
    <div>
        <!--
        导航菜单-->
        <el-menu
                mode="horizontal"
                background-color="#545c64"
                text-color="#fff"
                active-text-color="#ffd04b"
        >
            <el-menu-item index="1" style="margin-left: 120px" @click="index">首页</el-menu-item>
            <el-submenu index="2" style="margin-left:120px">
                <template slot="title">产品</template>
                <el-menu-item index="2-1">CRM客户管理系统</el-menu-item>
                <el-menu-item index="2-2">智慧能力销售系统</el-menu-item>
                <el-menu-item index="2-3">CMS内容管理系统</el-menu-item>
            </el-submenu>
            <el-menu-item index="3" style="margin-left: 120px">定制</el-menu-item>
            <el-menu-item index="4"style="margin-left: 120px">关于我们</el-menu-item>
            <el-menu-item index="5" style="margin-left: 120px">
                <el-button @click="refresh()" type="text">公司入驻</el-button>
            </el-menu-item>
            <el-menu-item index="5" style="margin-left: 100px">
                <el-button @click="login" type="text">登录</el-button>
            </el-menu-item>
        </el-menu>

        <el-form :model="form" ref="tenantForm" :rules="formRules" label-position="left" label-width="100px"
                 class="demo-ruleForm login-container" style="margin-top: 0px">
            <h3 class="title">公司入驻</h3>
            <el-form-item prop="logoUrl" label="*公司Logo">
                <el-upload class="upload-demo" action="http://localhost/file/logoUpload"
                        :on-success="handlePreview" :limit="1" :on-remove="handleRemove" :file-list="fileList" list-type="picture">
                    <el-button size="small" type="primary">点击上传</el-button>
                    <!--<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>-->
                </el-upload>
            </el-form-item>
            <el-form-item prop="companyName"label="公司名称">
                <el-input type="text" v-model="form.companyName" auto-complete="off" placeholder="请输入公司名称!"></el-input>
            </el-form-item>
            <el-form-item prop="sysName" label="公司账号">
                <el-input type="text" v-model="form.sysName" auto-complete="off" placeholder="请输入账号!"></el-input>
            </el-form-item>
            <el-form-item prop="companyNum" label="公司座机">
                <el-input type="text" v-model="form.companyNum" auto-complete="off" placeholder="请输入座机!"></el-input>
            </el-form-item>
            <el-form-item prop="companyTel" label="联系电话">
                <el-input type="text" v-model="form.companyTel" auto-complete="off" placeholder="请输入联系电话!"></el-input>
            </el-form-item>
            <el-form-item prop="email" label="电子邮件">
                <el-input type="text" v-model="form.email" auto-complete="off" placeholder="请输入邮件!"></el-input>
            </el-form-item>
            <el-form-item prop="address" label="公司地址">
                <el-input type="text" v-model="form.address" auto-complete="off" placeholder="请输入地址" style="width:70%"></el-input>
                <el-button @click="baiduDialog">选择地址</el-button>
            </el-form-item>
            <el-form-item prop="password" label="登录密码">
                <el-input type="password" v-model="form.password" auto-complete="off" placeholder="请输入密码!"></el-input>
            </el-form-item>
            <el-form-item prop="comfirmPassword" label="确认密码">
                <el-input type="password" v-model="form.comfirmPassword" auto-complete="off" placeholder="请输入确认密码!"></el-input>
            </el-form-item>
            <el-form-item style="width:100%;">
                <el-button type="primary" style="width:100%;" @click="settledIn" >入驻</el-button>
            </el-form-item>
        </el-form>
        <!--地图展示-->
        <el-dialog title="百度地图" :visible.sync="baiduVisible" >
            <baidu-map :center="center" :zoom="11" :position="keyword">
                <bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']" anchor="BMAP_ANCHOR_TOP_LEFT"></bm-map-type>
                <bm-scale anchor="BMAP_ANCHOR_TOP_RIGHT"></bm-scale>
                <!--<bm-navigation anchor="BMAP_ANCHOR_TOP_RIGHT"></bm-navigation>-->
                <bm-auto-complete v-model="keyword" :sugStyle="{zIndex: 2100}">
                    <div style="margin-bottom:10px">
                        <input id="searchInput" type="text" placeholder="请输入关键字" class="searchinput"/>
                        <el-button type="success" @click="selectAdrressConfirm">确定</el-button>
                    </div>
                </bm-auto-complete>
                <bm-view class="map"/>
                <bm-local-search :keyword="keyword" :auto-viewport="true" :panel="false"></bm-local-search>
            </baidu-map>
        </el-dialog>
    </div>
</template>
<script>
    export default {
        name: "Register",
        data() {
            var validatePass2 = (rule, value, callback) => {
                if (value === '') {
                    callback(new Error('请再次输入密码'))
                } else if (value !== this.form.password) {
                    callback(new Error('两次输入密码不一致!'))
                } else {
                    callback()
                }
            }
            return {
                //form:tenant 为了做数据表单校验不要嵌套对象
                form: {},
                keyword:'',
                center:"成都",
                baiduVisible:false,
                fileList:[],
                formRules: {
                    companyName: [
                        { required: true, message: '请输入公司名称!', trigger: 'blur' }
                    ],
                    companyNum: [
                        { required: true, message: '请输入公司座机!', trigger: 'blur' }
                    ],
                    address: [
                        { required: true, message: '请输入公司地址!', trigger: 'blur' }
                    ],
                    /*coordinate: [
                        { required: true, message: '请输入公司坐标!', trigger: 'blur' }
                    ],*/
                    sysName: [
                        { required: true, message: '请输入账号!', trigger: 'blur' }
                    ],
                    companyTel: [
                        { required: true, message: '请输入联系电话!', trigger: 'blur' }
                    ],
                    email: [
                        { type: 'email',required: true, message: '请输入邮箱!', trigger: 'blur' }
                    ],
                    password: [
                        { required: true, message: '请输入密码!', trigger: 'blur' }
                    ],
                    comfirmPassword: [
                        {required: true,validator: validatePass2, trigger: 'blur' } //自定义校验规则
                    ]
                }
            };
        },
        methods: {
            baiduDialog(){
                this.baiduVisible=true;
            },
            selectAdrressConfirm(){
                this.form.address=this.keyword;
                this.baiduVisible=false;
            },
            login(){
                this.$router.replace('/login');
            },
            refresh(){
                location.reload();
            },
            handleRemove(file, fileList) {
                //console.log(file, fileList);
            },
            handlePreview(response, file, fileList) {
                this.form.logoUrl = response;
            },
            settledIn(){
                this.$refs.tenantForm.validate((valid) => {
                    //校验表单成功后才做一下操作
                    if (valid) {
                        this.$confirm('确认入驻吗?', '提示', {}).then(() => {
                            //拷贝后面对象的值到新对象,防止后面代码改动引起模型变化
                            let url = "http://api.map.baidu.com/geocoding/v3/?address="+this.keyword+"&output=json&ak=tt1KHvaLlRGxPxUS6TeYmIN4hyOIh800"
                            this.$jsonp(url).then(res=>{
                                this.form.coordinate="(lat"+res.result.location.lat.toFixed(1)+",lng"+res.result.location.lng.toFixed(1)+")";
                                if(this.form.coordinate){
                                    let para = Object.assign({}, this.form);
                                    para.coordinate=this.form.coordinate;
                                    this.$http.put("/tenant/save",para).then((res) => {
                                        if(res.data.success){
                                            this.$message({
                                                message: '操作成功!',
                                                type: 'success'
                                            });
                                            //重置表单
                                            this.$refs['tenantForm'].resetFields();
                                            //跳转登录页面
                                            this.$router.push({ path: '/login' });
                                        }
                                        else{
                                            this.$message({
                                                message: res.data.msg,
                                                type: 'error'
                                            });
                                        }
                                    });
                                }
                            })
                        });
                    }
                })
            },
            //页面跳转
            index(){
                this.$router.replace('/');
            }
        }
    }
</script>
<style lang="scss" scoped>

    .login-container {
        -webkit-border-radius: 5px;
        border-radius: 5px;
        -moz-border-radius: 5px;
        background-clip: padding-box;
        margin: 180px auto;
        width: 500px;
        padding: 35px 35px 15px 35px;
        background: #fff;
        border: 1px solid #eaeaea;
        box-shadow: 0 0 25px #cac6c6;
    .title {
        margin: 0px auto 40px auto;
        text-align: center;
        color: #505458;
    }
    .remember {
        margin: 0px 0px 35px 0px;
    }
    }
    .map{
        width:100%;
        height:500px;
    }
    .searchinput{
        width: 300px;
        box-sizing: border-box;
        padding: 9px;
        border: 1px solid #dddee1;
        line-height: 20px;
        font-size: 16px;
        height: 38px;
        color: #333;
        position: relative;
        border-radius: 4px;
    }
</style>
  • Shiro
    1.员工密码加密保存
 /**
 * 加密工具类
 */
public class Md5Util {
    //加密方式
    public static final  String ALGORITHMNAME = "MD5";
    //盐值
    public static final String SALT = "itsource";
    //加密次数
    public static final Integer HASHITERATIONS = 10;


    public static String createMd5(String source){
        SimpleHash hash = new SimpleHash(ALGORITHMNAME, source, SALT, HASHITERATIONS);
        return hash.toString();
    }

    public static void main(String[] args) {
        System.out.println(createMd5("admin"));
    }
}

EmployeeController中

 @Override
    public AjaxResult addOrUpdate(Employee employee) {
        if (employee.getId()==null){
            employee.setPassword(MD5Util.encrypt(employee.getPassword()));
            employeeService.add(employee);
        }else{
            employeeService.update(employee);
        }
        return AjaxResult.me();
    }

LoginController类

/**
 *  后台登录接收控制器
 */
@Controller
@CrossOrigin
public class LoginController {
    @Autowired
    private IPermissionService permissionService;
    /**
     * 登录核心代码
     * @param employee
     * @return
     */
    @RequestMapping(value = "login",method = RequestMethod.POST)
    @ResponseBody
    public AjaxResult login(@RequestBody Employee employee, HttpSession session){
        //获取当前用户
        Subject subject = SecurityUtils.getSubject();
        //如果没有认证则开始认证
        if(!subject.isAuthenticated()){
            try {
                UsernamePasswordToken token = new UsernamePasswordToken(employee.getUsername(), employee.getPassword());
                subject.login(token);
            } catch (UnknownAccountException e) {
                e.printStackTrace();
                return new AjaxResult(false, "用户名不存在!");
            }catch (IncorrectCredentialsException e){
                e.printStackTrace();
                return new AjaxResult(false, "密码错误!");
            }catch (Exception e){
                e.printStackTrace();
                return new AjaxResult(false, "网络繁忙请稍后再试!!");
            }
        }
        AjaxResult result = new AjaxResult();

        //把登录用户放到session中(存储到web  中的session中)
        UserContext.setUser(session);
        //登陆用户
        Employee e = UserContext.getEmployee();
        //拿到服务器返回的sessionId
        Serializable sessionId = subject.getSession().getId();
        //创建一个map集合,该集合装sessionId和登录用户
        Map<String, Object> map = new HashMap<>();
        map.put("sessionId", sessionId);
        map.put("object", subject.getPrincipal());
        result.setObject(map);
        return result;
    }
    /*登陆成功获取租户信息*/
    @RequestMapping(value="/myTenant",method = RequestMethod.GET)
    @ResponseBody
    public Tenant myTenant(){
        return UserContext.getEmployee().getTenant();
    }
    /*根据当前登录用户id获取所拥有的权限*/
    @RequestMapping(value="/allUserPermissions",method = RequestMethod.PATCH)
    @ResponseBody
    public Set<String> selectCurrentUserPermissions(){
        return permissionService.selectCurrentUserPermissions();
    }

    /**
     * 根据当前租户id获取当前租户套餐所有权限
     * @return
     */
    @RequestMapping(value="/allTenantPermissions",method = RequestMethod.PATCH)
    @ResponseBody
    public Set<Permission> selectCurrentTenantPermissions(){
        return permissionService.selectCurrentTenantPermissions();
    }



    /*获取当前登录用户*/
    @RequestMapping(value="/subject",method = RequestMethod.GET)
    @ResponseBody
    public Employee getSubject(){
        return UserContext.getEmployee();
    }
}

login.vue

...
methods: {

        //页面跳转
        register(){
            this.$router.replace('/register');
        },
        login(){
            this.$router.replace('/login');
        },

      handleReset2() {
        this.$refs.ruleForm2.resetFields();
      },
      handleSubmit2(ev) {
        var _this = this;
        this.$refs.ruleForm2.validate((valid) => {
          if (valid) {
            this.$router.replace('/index');
            this.logining = true;
            //NProgress.start();
            var loginParams = { username: this.ruleForm2.account, password: this.ruleForm2.checkPass };
            this.$http.post("/login",loginParams).then(data =>{
              this.logining = false;
              //NProgress.done();
              let { msg, success, object } = data.data;
              if (!success) {
                this.$message({
                  message: msg,
                  type: 'error'
                });
              } else {
                sessionStorage.setItem('user', JSON.stringify(object.object));
                //存储sessionID
                  sessionStorage.setItem("sessionId",object.sessionId);
                  //登录成功跳转的页面
                this.$router.push({ path: '/main' });
              }
            });
          }else{
            console.log('error submit!!');
            return false;
          }
        });
      }
    }
  }
  ....

4.5遇到问题和总结:

登录成功后无法访问
登录成功之后 无法查询部门 。这个什么原因导致的?
就是cookie的管理机制导致

前后端分离项目中,ajax请求没有携带cookie,所以后台无法通过cookie获取到SESSIONID,从而无法获取到session对象。而shiro的认证与授权都是通过session实现的,我们要想办法解决这个问题。

前后端需要建立会话机制
通过token的机制建立前端和后端的会话管理机制
在这里插入图片描述

1)登录成功后返回token,并以后每次ajax请求都要携带token
LoginController

Employee employee1 = (Employee) currentUser.getPrincipal();
        employee.setPassword(null);

        Map<String,Object> result = new HashMap<>();
        result.put("user",employee1);
        System.out.println(currentUser.getSession().getId()+"xxxx"); 登录成功后把会话id返回,会后作为token使用
        result.put("token",currentUser.getSession().getId());

        return AjaxResult.me().setResultObj(result);

Longin.vue

this.$http.post("/login",loginParams).then(data => {
              this.logining = false;
              let { success, message, resultObj } = data.data;
              if (!success) {
                this.$message({
                  message: message,
                  type: 'error'
                });
              } else {

                  console.log(resultObj)
                  //登录成功跳转/table的路由地址
                sessionStorage.setItem('user', JSON.stringify(resultObj.user));
                sessionStorage.setItem('token', resultObj.token); //不要加字符串转换了巨大的坑
                  //修改登录成功后跳转到首页
                this.$router.push({ path: '/echarts' });
              }
Home.vue
	//退出登录
			logout: function () {
				var _this = this;
				this.$confirm('确认退出吗?', '提示', {
					//type: 'warning'
				}).then(() => {
					sessionStorage.removeItem('user');
					sessionStorage.removeItem('token');
					_this.$router.push('/login');
				}).catch(() => {

				});

Main.js

//拦截器 
axios.interceptors.request.use(config => {
    if (sessionStorage.getItem('token')) {
        // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
        config.headers['X-Token'] = sessionStorage.getItem('token')
    }
    console.debug('config',config)
    return config
}, error => {
    // Do something with request error
    Promise.reject(error)
})

2)服务端变为通过token来唯一标识session
Shirospring配置文件

  <!--session管理器-->
    <bean id="sessionManager" class="cn.itsource.shiro.util.CrmSessionManager"/>

    <!--shiro的核心对象-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="sessionManager" ref="sessionManager"/>
        <!--配置realm-->
        <property name="realm" ref="authRealm"/>
    </bean>

```java
/**
 *
 * 传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,
 * 在前后端分离的项目中(也可在移动APP项目使用),我们选择在ajax的请求头中传递sessionId,
 * 因此需要重写shiro获取sessionId的方式。
 * 自定义CrmSessionManager类继承DefaultWebSessionManager类,重写getSessionId方法
 *
 */
public class CrmSessionManager extends DefaultWebSessionManager {

    private static final String AUTHORIZATION = "X-TOKEN";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public CrmSessionManager() {
        super();
    }

    @Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
    //取到jessionid
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        HttpServletRequest request1 = (HttpServletRequest) request;
        //如果请求头中有 X-TOKEN 则其值为sessionId
        if (!StringUtils.isEmpty(id)) {
            System.out.println(id+"jjjjjjjjj"+request1.getRequestURI()+request1.getMethod());
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }

}

跨域预检查放行 OPTIONS每次跨域
cors跨域处理时,每次都要跨域预检查,也就是发一个options请求,这种请求shiro应该放行.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">

<bean id="myAuthc" class="cn.itsource.shiro.util.MyAuthenticationFilter"/>

    <!--shiro的过滤器配置-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/s/login"/>
        <property name="successUrl" value="/s/index"/>
        <property name="unauthorizedUrl" value="/s/unauthorized"/>
        <property name="filters">
            <map>
                <entry key="myAuthc" value-ref="myAuthc"/>
            </map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /login = anon
                /** = myAuthc
            </value>
        </property>
    </bean>
/**
 * 自定义身份认证过滤器
 */
public class MyAuthenticationFilter extends FormAuthenticationFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //如果是OPTIONS请求,直接放行
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String method = httpServletRequest.getMethod();
        System.out.println(method);
        if("OPTIONS".equalsIgnoreCase(method)){
            return true;
        }
        return super.isAccessAllowed(request, response, mappedValue);
    }
}

本次团队合作项目在前期时候就要理清楚逻辑思路,然后建表时候把关系外键设计好,一般字段是对象就是外键,在写mapper.xml时候要仔细点多对一,一对多等关系配置。前端的elementui要多加练习,小组要相互讨论,不用一意孤行。

  • 4
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值