拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于jfinal与Vue的CRM系统前后端分离开发与二次实践

简介悟空CRM是一套基于JFinal与VueElementUI构建的前后端分离CRM系统面向需要快速搭建或学习企业级客户关系管理的开发者、实施人员及学生群体。资源压缩包共含2001个文件其中png图片资源数量最多达1057个可用于界面设计参考源码部分以274个Java后端类、259个Vue页面组件为核心辅以207个JavaScript脚本、91个CSS样式、53个SQL数据库脚本以及SCSS、HTML、XML、JSON、Git等配置与说明文件整体约32.26MB能够完整呈现系统前后端分离的模块划分与工程构建结构。该资源目前已有394人学习下载适合作为从零部署到二次开发的学习样例。借助这套代码可以重点分析客户、商机、权限等核心模块的数据表设计与接口实现梳理从数据库初始化、后端业务逻辑到前端页面渲染的完整调用链路同时工程中的构建脚本与目录组织方式也便于用作毕业设计或企业信息化项目的参考基线在现有功能基础上扩展定制。1. 悟空CRM的技术选型jfinal 做 API、vue 做界面免费 CRM 的另一种样本很多做销售的团队最初都用在线表格记客户等到要按权限分客户池、按线索阶段算转化率时才会意识到需要一套自带客户、线索、商机、合同链路的 CRM。悟空CRM 走的是一条偏工程化的路线后端用 jfinal 提供 REST 接口前端用 vue2 ElementUI 构建界面全站前后端分离部署人和账号数量都不卡死。云服务版本免费私有化部署也保留了完整数据表结构和接口文档适合那种想把数据放回自己服务器、又不愿意为每个账号单独付费的成长型企业。从 Java 生态看jfinal 属于重量极轻的框架路由、插件、ActiveRecord 都按“少写样板代码”设计。对比 Spring Boot 一套注解加依赖注入的上下文成本jfinal 的入门曲线短得多CRM 这种以单表读写为主、权限过滤和状态流转清晰的业务放在 jfinal 上并不会出现规模瓶颈反而更容易让新人接手。前端选 vue2 ElementUI组件覆盖了表格、表单弹窗、树形选择、日期范围这些后台管理系统的高频控件二次开发时按组件 props 对接接口即可。这套技术栈放在国内中小企业管理软件实践里属于能快速落地、也方便后续改数据字典的那一类。2. 后端实现链路jfinal 路由注册、ActiveRecord 映射与登录拦截2.1 从 72crm.bat 看启动方式与路由骨架资源清单里那份72crm.bat很醒目它通常是 Windows 下负责拉起后端 jar 的启动脚本指定 JVM 堆内存和运行端口。常见内容是这样echo off rem 指定 jfinal 运行内存与编码避免中文乱码 set JAVA_OPTS-Xms256m -Xmx1024m -Dfile.encodingutf-8 java %JAVA_OPTS% -jar 72crm.jar --server.port8080这个脚本里有两个参数最值得关注-Xmx1024m限制了堆上限客户量在几千级别的单体 CRM 部署完全够用--server.port8080决定了后端监听端口前端 devServer 的 proxy 必须指向同一端口否则联调时接口直接 404。jfinal 的路由注册继承自JFinalConfig每个 Controller 通过Routes注册到固定路径下。悟空CRM 的接口按业务模块拆分大致结构如下public class MainConfig extends JFinalConfig { public void configRoute(Routes me) { me.add(/api/customer, CustomerController.class); me.add(/api/leads, LeadsController.class); me.add(/api/business, BusinessController.class); me.add(/api/contract, ContractController.class); } }路由前缀对应前端 axios 请求里的 URL 第一段。me.add的第二个参数是 Controller 类类中每个 public 方法默认就是一个 action通过ActionKey或方法名来映射具体路径。路由前缀Controller在 CRM 中承担的功能/api/customerCustomerController客户列表、客户详情、客户分配与转移/api/leadsLeadsController线索导入、线索转客户/api/businessBusinessController商机阶段推进、赢单/输单/api/contractContractController合同创建、回款计划与开票记录这种路由即服务的方式好处是接口路径和 Controller 方法一一对应部署后可以直接用/api/customer/list验证后端是否存活排查链路比反射注入式的框架直观得多。2.2 登录态与拦截器AuthInterceptor 里发生了什么CRM 系统的所有业务接口都必须先过登录态校验。悟空CRM 在 jfinal 里用全局拦截器处理这一层业务 Controller 本身不写重复的鉴权代码public class AuthInterceptor implements Interceptor { public void intercept(Invocation inv) { HttpServletRequest req inv.getController().getRequest(); Object userId req.getSession().getAttribute(loginUserId); if (userId null) { inv.getController().renderJson(new ApiResult(401, 登录已过期, null)); return; } // 把当前用户放入 ThreadLocal后续 Service 直接取避免反复读 session UserHolder.set((Integer) userId); try { inv.invoke(); } finally { UserHolder.remove(); // 防止线程复用导致的数据串号 } } }这段代码有三个关键设计。第一会话保存在服务端 session 里前端通过 cookie 携带会话标识和 vue 端 axios 的withCredentials配置必须一致否则前端每次请求都拿不到登录态。第二UserHolder.set用 ThreadLocal 存储当前用户这样 Service 层各方法不需要把 userId 作为参数传来传去。第三finally里的 remove 是必要的jfinal 的 Handler 体系会复用线程不清理 ThreadLocal 就可能让下一个请求读到上一个用户的数据这是排查“A 账号看到 B 客户列表”时首先要检查的位置。登录成功后写 session、记录登录 IP 和最后活跃时间这部分通常在 LoginService 里完成接口只返回 userId、用户名和 token 快照。token 本身在悟空这套分离架构里更多起到“前端存储凭证、后端校验会话”的作用真正有效的是 session 中的loginUserId。2.3 CRM 核心数据模型的 ActiveRecord 映射与分页查询悟空CRM 的客户、线索、商机、合同分别落在这几张核心表上crm_customer、crm_leads、crm_business、crm_contract。jfinal 的 ActiveRecord 模式让每张表对应一个 Model 子类字段自动映射不需要像 MyBatis 那样写 XML resultMappublic class Customer extends ModelCustomer { public static final Customer dao new Customer().dao(); public PageCustomer pageByOwner(int pageNum, int pageSize, int userId) { return paginate( pageNum, pageSize, select c.customer_id, c.customer_name, u.realname as owner_name, c.create_time, from crm_customer c left join admin_user u on c.owner_user_id u.user_id where c.owner_user_id ? and c.is_deleted 0 order by c.create_time desc, userId ); } }分页查询返回 jfinal 的Page对象里面带list、totalPage、totalRow三个字段前端 ElementUI 表格的el-pagination可以直接对接。这套写法看上去简单实际工程里要留意两个约束。第一Model 的字段类型校验在写入时是宽散的日期字段通常以字符串传入数据库datetime字段如果不做格式校验非法值会在写库时报错建议在 Service 层统一对create_time、update_time做格式化。第二左连接查询会查出只读字段比如这里的owner_name是admin_user表里的realname如果直接对查询结果调用update()会把整行非主键字段一起更新容易覆盖不想改的列。因此只读查询的 Model 建议单独put字段或者用Db.template写专用 SQL 模板把列表查询和详情查询拆开。3. 前端工程化拆解vue2 ElementUI 的请求层、路由与表格性能3.1 dist 里那些 app..css 和 chunk-.css 能反推出什么资源清单里看到app.2d20ef02.css、chunk-libs.0651b7d6.css、chunk-d1e2.609582a4.css这类文件名说明前端工程经过 webpack 生产构建文件名带内容哈希指纹。.2d20ef02是根据文件内容生成的 8 位 hash内容不变则文件名不变浏览器可以放心长缓存chunk-libs这个前缀一般来自 splitChunks 的 cacheGroups 配置把 node_modules 里的第三方依赖独立打包。从文件名还能看出路由懒加载的痕迹chunk-d1e2、chunk-53f9这类单个异步路由页面只要有独立 CSS就说明构建时 vue-router 用了动态 import页面组件被拆成独立 chunk访问对应路由时才加载对应样式。这套机制对 CRM 这种左侧菜单多、每个页面组件大的后台系统很实用首屏只加载登录页和仪表盘所需资源。3.2 axios 拦截器请求注入 token 与 401 统一处理vue 端所有接口统一走 axios 实例登录态在请求拦截器里挂在 Header 上响应拦截器负责处理 token 失效import request from /utils/request request.interceptors.request.use(config { const token Vue.ls.get(Admin-Token) if (token) { config.headers[Authorization] Bearer token } return config }, error Promise.reject(error)) request.interceptors.response.use(response { const res response.data if (res.code 401) { // token 过期清除本地缓存并跳转登录页 Vue.ls.remove(Admin-Token) location.href /login return Promise.reject(new Error(登录已过期)) } return res }, error Promise.reject(error))这里有几个细节值得展开。Vue.ls是 vue-ls 插件把 token 存在 localStorage 里刷新页面不会丢Authorization: Bearer token是前后端约定好的格式后端一般从请求头里截取Bearer后面的部分解析。响应层只判断code字段HTTP 状态码仍保持 200这是很多后台管理系统的通用做法方便把业务异常比如“客户名称已存在”和网络异常区分开。面试里常提到的取消重复请求不建议在这个拦截器里做因为 CRM 列表页的筛选条件变化频繁同一接口短时间内多次请求是正常交互真正的幂等控制应该放在业务层比如提交按钮的 loading 状态。3.3 ElementUI 表格在 CRM 列表页的取舍分页、树形勾选与溢出提示客户列表页是整个系统最核心的页面ElementUI 的el-table加后端分页是标准姿势。数据量超过一万行时不要一次性把全量数据塞进:dataElementUI 的虚拟滚动是通过el-table的el-table-virtual-scroll扩展实现的直接在 2.x 版本上用全量渲染会卡。客户分组或组织架构场景下树形表格的勾选是高频问题。el-table的row-key加tree-props开启树形后勾选事件返回的 selection 只包含当前已勾选的节点父节点选中时子节点不会自动进入 selection。通用处理方式是维护一个对象作为选中集合handleSelectionChange(selection) { // 按 id 重组选中集解决树形表格父子节点联动勾选的数据丢失 const selectedMap {} selection.forEach(row { selectedMap[row.id] true }) this.selectedMap selectedMap }另一个实际体验优化是字段文字超长。客户名称、备注这些字段在固定列宽下会挤压布局建议在所有内容可能超长的列加show-overflow-tooltip鼠标悬浮时显示完整文字这是 ElementUI 自带行为不需要额外写 CSS。el-table-column propcustomerName label客户名称 show-overflow-tooltip min-width140 /需要提醒的是show-overflow-tooltip在多个表格同时渲染时会消耗较多 DOM 节点只建议加在真正可能超长的列上不要全表所有列都开。4. 从下载到跑通的一线过程前端依赖、后端数据库与联调排错4.1 前端 npm 依赖安装与 dev proxy 配置这套工程的前端依赖在package.json里锁定安装前先确认 Node 环境vue2 工程建议用 Node 14 或 16Node 18 在部分 webpack4 的构建链上会出现digital envelope routines报错。安装依赖时切换国内镜像能省不少时间node -v npm install --registryhttps://registry.npmmirror.com npm run devdev 模式启动后vue-cli 默认起在localhost:9528悟空CRM 的常见配置端口所有/api请求需要代理到后端的 8080vue.config.js里配 devServermodule.exports { devServer: { port: 9528, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }changeOrigin: true会把请求头里的 Host 改成 target 域名避免后端出现跨域判断错误。如果后端不在 8080只改 target 即可前端代码里的接口路径不需要动。4.2 后端数据库初始化与 72crm.bat 的执行后端起跑前必须先把数据库初始化完。悟空CRM 的 SQL 脚本在sql/目录下文件按模块拆开比如crm.sql、admin.sql导入顺序有依赖先 admin 后 crm。数据库连接配置一般在resources/下的配置文件中jfinal 通过 DruidPlugin 连接 MySQLdbUrljdbc:mysql://localhost:3306/72crm?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai dbUserroot dbPassword你的密码这里最容易踩坑的是serverTimezone。MySQL 8.x 驱动要求显式指定时区不配或少配成UTC导入 SQL 时可能不报错但查询create_time和服务器本地时间对不上。characterEncodingutf8mb4对应 MySQL 建表时的字符集如果库表是 utf8这里也不要改改了反而会告警。数据库就绪后直接双击72crm.bat或者到 jar 包目录执行java -Xms256m -Xmx1024m -Dfile.encodingutf-8 -jar 72crm.jar --server.port8080看到控制台输出JFinal start success且无异常堆栈后端就算起来了。验证接口可以直接用浏览器打开/api/customer/list不登录时应该返回 401 JSON而不是 HTML 404这一步能确认路由和拦截器都生效。4.3 联调中最常遇到的三个异常与排查顺序前端 dev server 起来、后端 8080 也通了联调阶段最常见的问题是以下三种报错现象排查命令/配置根因说明前端请求 404Network 面板看到请求到了 9528检查 vue.config.js 的 proxy 是否匹配/api前缀代理没生效或 target 端口写错后端日志报The server time zone valueALTER TABLE 或改 dbUrl 的 serverTimezoneMySQL 驱动与系统时区不一致接口返回 401但浏览器看 cookie 已写入检查 axios 是否设置withCredentials: true跨域场景下 session cookie 不被携带排查顺序建议先看浏览器 Network 面板的请求 URL确认走了代理没有再看后端日志有没有对应请求进来最后看数据库连接池状态。前端 401 不一定都是密码错先把登录接口的请求头展开确认Authorization字段值非空再用curl -I单独打登录接口验证。curl -X POST http://localhost:8080/api/login \ -H Content-Type: application/json \ -d {username:admin,password:123456} \ -c cookies.txt后端接口通了之后再回前端点登录能少绕很多弯。5. 二次开发前必须处理的三个生产化问题路由懒加载、分块与接口签名5.1 路由懒加载与分包配置直接影响首屏指标悟空CRM 自带的路由集中在src/router/index.js二次开发新增页面时如果继续用静态 import每扩展一个模块首屏构建产物体积都会膨胀。改造点是把组件引用换成动态 importconst CustomerList () import(/views/customer/index.vue) const BusinessDetail () import(/views/business/detail.vue)配合vue.config.js的 splitChunks 单独提取 ElementUI 依赖configureWebpack: { optimization: { splitChunks: { chunks: all, cacheGroups: { elementUI: { name: element-ui, test: /[\\/]node_modules[\\/]element-ui/, priority: 20 } } } } }ElementUI 全量打包接近 600KB独立成 chunk 后浏览器会自动长缓存后续业务代码更新时不用重复下载组件库。这个分包策略对 5 年以上经验的开发来说可能不算新东西但放在这套老工程上是收益最明显的改造点。5.2 数据权限下沉到 SQL 层owner_user_id 与 dept_id 的组合CRM 客户数据不能只靠前端菜单隐藏来控制真正要防的是有人直接调接口拉数据。客户表里的owner_user_id记录了归属人查询时必须在 SQL 层过滤不能在 Service 里查出全量再 filter否则数据量上来后内存和接口响应同时崩。常见做法是把当前用户 id 和部门 id 作为查询条件select c.customer_id, c.customer_name, c.deal_status from crm_customer c where c.is_deleted 0 and ( c.owner_user_id #{userId} or c.owner_dept_id #{deptId} or exists ( select 1 from crm_customer_share s where s.customer_id c.customer_id and s.user_id #{userId} ) )数据权限控制到 SQL 层后业务逻辑测试也要按角色切换账号验证避免“管理员能看全部”的 SQL 里误写成了select *。5.3 接口签名防重放给免费系统加一道生产级保障私有化部署的 CRM 往往没有专门的网关接口裸奔在网络上容易被抓包重放。给写操作接口加签名是成本最低的加固手段。前端对请求参数做排序拼接再用 MD5 生成签名const params { customerId: 101, transferTo: 55, timestamp: Date.now() } const signStr Object.keys(params).sort().map(k ${k}${params[k]}).join() params.sign md5(signStr secret你的盐)后端拦截器验签同时校验时间戳防止重放long ts Long.valueOf(getPara(timestamp)); if (Math.abs(System.currentTimeMillis() - ts) 3 * 60 * 1000) { renderJson(new ApiResult(403, 请求已过期, null)); return; } String expect Md5.toMd5(buildSignStr(getParaMap())); if (!expect.equals(getPara(sign))) { renderJson(new ApiResult(403, 签名校验失败, null)); return; }签名默认有效期 3 分钟超过直接拒收如果需要防同一次请求在有效期内重复提交可以在 redis 里记一个sign键第二次出现相同签名直接拒绝。这套方案不依赖 HTTPS也能挡住大部分抓包改包行为对一套给人管理客户资产的后台管理系统来说这笔投入非常值得。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门