文章目录
1.摘要:2.开发人员组成3.开发时长4.项目介绍
4.1前后端分别4.2项目主要模块4.3项目分工4.4项目技术4.5遇到问题和总结:
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(newError('请再次输入密码'))}elseif(value !==this.form.password){callback(newError('两次输入密码不一致!'))}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:0025px #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> 复制代码 /** * 加密工具类 */publicclassMd5Util{//加密方式publicstaticfinal String ALGORITHMNAME ="MD5";//盐值publicstaticfinal String SALT ="itsource";//加密次数publicstaticfinal Integer HASHITERATIONS =10;publicstatic String createMd5(String source){ SimpleHash hash =newSimpleHash(ALGORITHMNAME, source, SALT, HASHITERATIONS);return hash.toString();}publicstaticvoidmain(String[] args){ System.out.println(createMd5("admin"));}} 复制代码 EmployeeController中@Overridepublic 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@CrossOriginpublicclassLoginController{@Autowiredprivate IPermissionService permissionService;/** * 登录核心代码 * @param employee * @return */@RequestMapping(value ="login",method = RequestMethod.POST)@ResponseBodypublic AjaxResult login(@RequestBody Employee employee, HttpSession session){//获取当前用户 Subject subject = SecurityUtils.getSubject();//假设没有认证则开端认证if(!subject.isAuthenticated()){try{ UsernamePasswordToken token =newUsernamePasswordToken(employee.getUsername(), employee.getPassword()); subject.login(token);}catch(UnknownAccountException e){ e.printStackTrace();returnnewAjaxResult(false,"用户名不存在!");}catch(IncorrectCredentialsException e){ e.printStackTrace();returnnewAjaxResult(false,"密码错误!");}catch(Exception e){ e.printStackTrace();returnnewAjaxResult(false,"网络繁忙请稍后再试!!");}} AjaxResult result =newAjaxResult();//把登录用户放到session中(存储到web 中的session中) UserContext.setUser(session);//登陆用户 Employee e = UserContext.getEmployee();//拿到效劳器返回的sessionId Serializable sessionId = subject.getSession().getId();//创建一个map集合,该集合装sessionId和登录用户 Map<String, Object> map =newHashMap<>(); map.put("sessionId", sessionId); map.put("object", subject.getPrincipal()); result.setObject(map);return result;}/*登陆胜利获取租户信息*/@RequestMapping(value="/myTenant",method = RequestMethod.GET)@ResponseBodypublic Tenant myTenant(){return UserContext.getEmployee().getTenant();}/*根据当前登录用户id获取所拥有的权限*/@RequestMapping(value="/allUserPermissions",method = RequestMethod.PATCH)@ResponseBodypublic Set<String>selectCurrentUserPermissions(){return permissionService.selectCurrentUserPermissions();}/** * 根据当前租户id获取当前租户套餐所有权限 * @return */@RequestMapping(value="/allTenantPermissions",method = RequestMethod.PATCH)@ResponseBodypublic Set<Permission>selectCurrentTenantPermissions(){return permissionService.selectCurrentTenantPermissions();}/*获取当前登录用户*/@RequestMapping(value="/subject",method = RequestMethod.GET)@ResponseBodypublic 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!!');returnfalse;}});}}}.... 复制代码 4.5遇到问题和总结:
登录胜利后无法访问
登录胜利之后 无法查询部门 。这个什么原因导致的?
就是cookie的管理机制导致
前后端分别项目中,ajax恳求没有携带cookie,所以后台无法通过cookie获取到SESSIONID,从而无法获取到session对象。而shiro的认证与受权都是通过session实现的,我们要想办法处置这个问题。
前后端需要建立会话机制
通过token的机制建立前端和后端的会话管理机制
1)登录胜利后返回token,并以后每次ajax恳求都要携带token
LoginControllerEmployee employee1 =(Employee) currentUser.getPrincipal(); employee.setPassword(null); Map<String,Object> result =newHashMap<>(); 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.vuethis.$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方法 * */publicclassCrmSessionManagerextendsDefaultWebSessionManager{privatestaticfinal String AUTHORIZATION ="X-TOKEN";privatestaticfinal String REFERENCED_SESSION_ID_SOURCE ="Stateless request";publicCrmSessionManager(){super();}@Overrideprotected Serializable getSessionId(ServletRequest request, ServletResponse response){//取到jessionid String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION); HttpServletRequest request1 =(HttpServletRequest) request;//假设恳求头中有 X-TOKEN 则其值为sessionIdif(!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取sessionIdreturnsuper.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> 复制代码 /** * 自定义身份认证过滤器 */publicclassMyAuthenticationFilterextendsFormAuthenticationFilter{@OverrideprotectedbooleanisAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue){//假设是OPTIONS恳求,直接放行 HttpServletRequest httpServletRequest =(HttpServletRequest) request; String method = httpServletRequest.getMethod(); System.out.println(method);if("OPTIONS".equalsIgnoreCase(method)){returntrue;}returnsuper.isAccessAllowed(request, response, mappedValue);}} 复制代码 本次团队合作项目在前期时候就要理清楚逻辑思路,然后建表时候把关系外键设计好,一般字段是对象就是外键,在写mapper.xml时候要认真点多对一,一对多等关系配置。前端的elementui要多加练习,小组要互相讨论,不用一意孤行。