拆解电商老系统CRMOnline:ExtJS+Spring+Ibatis架构与缓存优化

发布时间:2026/9/17 4:15:55
拆解电商老系统CRMOnline:ExtJS+Spring+Ibatis架构与缓存优化 简介B2C电商CRM客户关系管理系统CRMOnlineRelease20100806-V16终结版是一套完整的客户关系管理解决方案面向Java Web开发人员、电商公司技术团队及需要毕设参考的高校学生可直接部署或修改后商用。系统采用ExtJSServletSpringIbatis技术栈B/S架构实现Web层与逻辑层分离涵盖广告、机会、客户、订单、投诉、会员卡、计划、组织、试用装、呼叫及通话记录、使用记录、问卷调查、系统、缓存等模块并提供API调用、代码模板、代码检测以及系统集成和人事对接等扩展接口业务覆盖较全面。压缩包约6.19MB共2000个文件其中包含838个Java源文件、428个JS脚本、240个XML配置、138个JSP页面、123个SQL脚本及116个CSS样式等代码结构清晰便于按模块检索学习。已有53人学习适合需要完整CRM系统源码用于二次开发、业务分析与毕业设计的人群。1. 电商仓库里翻出的CRMOnline为什么一套2010年的代码仍值得拆从RAR里解压出这套CRMOnline时第一反应是“又是老古董”。但把ExtJS页面、Servlet控制器和Ibatis映射铺开之后会发现它比很多新项目更完整。这套来自B2C电商公司内部的客户关系管理系统覆盖广告投放、销售机会、客户档案、订单、投诉、会员卡、试用装、呼叫及通话记录、问卷调查和系统缓存等模块页面与数据库脚本齐全。对正在做电商后台管理系统的人它是一份反例与正例混合的参考你能看到没有微服务、没有前后端分离时电商CRM如何靠分层和缓存扛住日常运营。对刚入行的开发者这套代码可以直接导入作为毕业设计或课程设计对5年以上的工程师值得关注的是它“Web层和逻辑层分离”的实现方式以及OSCache、Log4j如何嵌进业务代码。2. ExtJS Spring IbatisCRMOnline 的分层与模块清单2.1 前端的ExtJS组件样式只是一部分从资源文件的命名能看出来前端不是随手写的HTML而是ExtJS 3.x时代的界面体系ext-all.css、extjs.css、panel.css、tabs.css、tree.css、grid.css、form.css再加上app.newedit.css这种业务定制样式。app.newedit.css是对ExtJS默认外观的覆盖专门用于“新建/编辑”类型弹窗比如用户资料编辑、订单备注、投诉跟进。实际页面里这些CSS配合ExtJS的Ext.Window和Ext.form.Panel会让弹窗里的字段排版统一而不会像普通HTML表单那样在各浏览器里变形。如果直接用浏览器打开静态页面会发现样式加载不了。原因是ExtJS的组件样式需要由Ext.BLANK_IMAGE_URL指向一个1像素透明GIF同时页面必须通过Tomcat这类Servlet容器来访问否则组件宽度和布局计算会乱。建议把前端代码部署在同一个Web应用的根目录下通过index.html或main.jsp进入而不是直接用file://协议打开。2.2 请求流转Servlet控制器与Spring业务层后端不是Spring MVC而是“Servlet Spring Ibatis”的经典组合。一个典型流程是浏览器点击客户列表页ExtJS的Ext.data.Store发起AJAX请求请求打到web.xml注册的ServletServlet负责解析请求参数和响应格式Spring容器里的Service对象负责事务和业务规则Ibatis的SqlMapClient负责最终SQL执行。这里需要重点看web.xml中的ServletMapping。通常每个功能模块有一个对应的Servlet路径比如/customer.do、/order.do、/membercard.do。我一般会在部署前把web.xml里所有url-pattern扫描一遍先搞清楚前端请求对应哪一个Servlet再往下追Service实现。如果只追到DAO层很容易漏掉Servlet里对返回格式的封装。对应的映射片段一般是这样的servlet servlet-namecustomerServlet/servlet-name servlet-classcom.crmonline.web.CustomerServlet/servlet-class /servlet servlet-mapping servlet-namecustomerServlet/servlet-name url-pattern/customer.do/url-pattern /servlet-mapping这段配置把/customer.do请求交给CustomerServlet处理。servlet-class必须写成完整类名部署时如果项目改包名这里会最先报ClassNotFoundException。url-pattern里的.do只是约束不是Spring MVC的DispatcherServlet所以Servlet内部自己要处理参数解析和Json输出。在Spring配置里常见的做法是把SqlMapClientFactoryBean配置成Ibatis的入口让Service通过Spring拿到SqlMapClientTemplate。数据库连接池可以使用DBCP或C3P0这里不强制。重点是事务边界要放在Service方法上而不是Servlet里。bean idsqlMapClient classorg.springframework.orm.ibatis.SqlMapClientFactoryBean property nameconfigLocation value/WEB-INF/sqlmap-config.xml/ property namedataSource refdataSource/ /bean bean idcustomerService classcom.crmonline.service.CustomerService property namesqlMapClient refsqlMapClient/ /bean这段配置把sqlmap-config.xml作为Ibatis的总入口。CustomerService直接持有SqlMapClient在Service方法里通过queryForList(Customer.queryList, param)查数据。好处是Service层不感知具体SQL坏处是SQL字符串一旦改名或者参数类型变化Service层只有在运行时才会报错。调试时优先看sqlmap-config.xml里的namespace和statement id是否拼写一致。2.3 模块地图从广告到会员卡数据流如何串起来把摘要里提到的功能模块映射到实际业务表可以得到一张模块关系表模块典型数据实体关联主流程广告Ad, AdPosition广告计划带来机会机会SalesOpportunity机会转客户客户Customer, CustomerContact客户下订单订单Order, OrderItem订单触发投诉/会员卡投诉Complaint投诉关联客户与订单会员卡MemberCard, MemberRule会员卡绑定客户试用装TrialSample试用装推动客户回访呼叫CallRecord呼叫记录关联机会与客户问卷Questionnaire, Answer问卷沉淀客户偏好系统集成AccessToken, ApiLog对外API鉴权供人事系统拉取用户与组织这张表是我逆向梳理代码时最常用的起点。不要在任何一个Service里钻太久先拿这张表去对照sqlmap目录下的映射文件确认每个模块对应的SQL statement id再决定要改哪一层。以订单模块为例订单表和订单明细表是主从关系。在CRMOnline里订单列表左侧是订单主表数据右侧是选中订单的商品明细。这个交互由两个独立的ExtJS Grid完成后端接口也拆成/order/list.do和/order/detail.do两个Servlet。如果我需要新增一个“订单备注”字段就要同时改主表的SQL映射、Servlet的返回VO和前端Grid的ColumnModel三层缺一不可。RAR里的代码模板目录和代码检测目录属于工程工具部署时不需要进Tomcat但二次开发时按模板扩展会省很多事。尤其是“调用API”目录里面预留了鉴权参数和签名示例对接人事系统时可以直接参考不需要重新设计加解密方案。2.4 OSCache和Log4j在代码里的实际位置OSCache在CRMOnline里一般用来缓存字典数据和统计报表。常见配置是oscache.properties中设置cache.memorytrue和cache.capacity5000然后在Service方法外包装一层GeneralCacheAdministrator的存取判断。注意OSCache的key要能区分不同查询参数否则不同条件的查询结果会互相污染。建议key使用“模块名查询条件JSON串”的形式比如customer_list_ condition.toString()。Log4j的作用更直接在Servlet入口、Service入口、Ibatis执行后分别打印入参、耗时和SQL结果行数。线上排查问题时级别不要调到DEBUG否则Ibatis会打出大量结果集内容。使用INFO级别记录请求路径和响应状态即可。用log4j.properties配置log4j.logger.com.ibatisWARN避免SQL日志灌满磁盘。这个模块梳理算是一层地基。下一章专门说数据库脚本里最值得抄的几张表。3. 从数据库脚本读业务客户、订单、会员卡与呼叫记录的表关系3.1 客户主表和联系人表CRMOnline的客户模型不是单表结构而是“客户主表 联系人表 地址表”。主表保存customer_id、客户等级、来源渠道、注册时间。联系人表保存联系人姓名、手机号、email、是否主联系人。这种设计在B2C场景下尤其重要一个家庭客户可能有两个联系人一个是下单人一个是收货人。如果只把联系人字段放进客户主表后续做短信营销时很难按联系人角色分组。这里需要注意的是原数据库脚本里通常会把地址表单独存放地址信息包含country_code、province、city、detail_address。如果做跨境电商country_code是必填项只做本地电商时这个字段可能被忽略但保留它并不会带来负担。示例建表脚本从原库脚本简化而来CREATE TABLE customer ( customer_id INT PRIMARY KEY AUTO_INCREMENT, customer_no VARCHAR(32) NOT NULL, customer_name VARCHAR(64), customer_level TINYINT DEFAULT 0, source_chan VARCHAR(32), register_date DATETIME, status TINYINT DEFAULT 1 ); CREATE TABLE customer_contact ( contact_id INT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL, contact_name VARCHAR(64), mobile VARCHAR(20), email VARCHAR(128), is_primary TINYINT DEFAULT 0, KEY idx_customer_id (customer_id) );customer表保存客户生命周期状态customer_contact表保存变动频繁的联系信息。实际数据库脚本里可能还有create_time、modify_time但核心逻辑就是这两张表。查询客户时先按customer_no定位customer再联查contact表取出主联系人。常见错误是只关联所有联系人导致一条客户记录被拆成多行返回ExtJS Grid里出现重复行。解决方法是使用子查询或者MAX(CASE WHEN is_primary1 ...)把主联系人字段折叠到客户主查询里。3.2 订单表和会员卡交易与营销两套持久化订单表结构比较常规但有三个字段值得留意order_status、pay_status、ship_status。很多电商项目把支付和发货状态杂糅在一个字段里CRMOnline把它们分开方便做售后退款统计。会员卡表和订单表之间不是直接外键而是通过customer_id和手机号进行关联。会员卡表里会有卡号、余额、积分、等级、到期时间。B2C电商的会员运营通常把订单的支付金额回写到会员卡积分这一步由定时任务或下单事务完成。订单状态字段的取值建议如下字段取值含义order_status0/1/2/3待支付/已支付/已关闭/已取消pay_status0/1/2未支付/已支付/退款ship_status0/1/2未发货/已发货/已签收这里的实践要点是不要轻易在订单表上增加“会员卡号”字段而是通过客户ID和卡类型关联。不然同一个客户换卡之后历史订单查询会产生歧义。CRMOnline里会员卡和订单解耦卡挂失换卡只影响会员卡表历史订单不变。在二次开发时如果要加“订单是否使用积分”建议在订单扩展表中增加used_points字段不改主表结构。3.3 销售机会和呼叫记录CRM的核心过程数据销售机会表保存从广告点击到成交之前的所有跟进节点。机会表的关键字段是expected_amount、probability、next_contact_time、follow_status。呼叫记录表保存每次通话的电话号码、通话时长、通话类型呼入/呼出、关联机会ID、通话录音文件名。这套结构和互联网公司的呼叫中心系统非常像但更精简。使用Ibatis查询呼叫记录和机会关联时常见的方法是嵌套结果映射resultMap idcallWithOpportunity classCallRecord result propertycallId columncall_id/ result propertymobile columnmobile/ result propertyduration columnduration/ association propertyopportunity columnopp_id selectOpportunity.queryById/ /resultMap这里selectOpportunity.queryById表示对每条呼叫记录再按机会ID查一次机会表。如果呼叫记录量很大N1查询问题会非常严重。线上优化时建议改成一次LEFT JOIN查询将机会主题直接映射进CallRecord对象。3.4 问卷调查表一份可以复用的动态表单设计问卷模块采用了“问卷-题目-回答”三张表而不是为每道题建一个字段。问卷表保存问卷编号和标题题目表保存题目类型和选项JSON回答表保存userId和题干快照。这种设计在电商售后回访里特别实用因为运营人员可以随时改题目不需要改表结构。CREATE TABLE survey_answer ( answer_id INT PRIMARY KEY AUTO_INCREMENT, survey_id INT NOT NULL, customer_id INT NOT NULL, question_id INT NOT NULL, answer_value TEXT, answer_time DATETIME );answer_value用TEXT类型保存用户回答对单选题存选项值对多选题存逗号分隔的选项ID。配合问卷模块的导出功能能直接生成统计报表。缺点是统计时需要脚本解析文本不能直接SQL聚合。如果需要在数据库层做分析我一般会增加冗余字段question_type在回答时同步写入避免每次统计都去关联题目表。把这些查询案例放进Ibatis配置后会发现CRMOnline的表设计遵循“主从分开、过程独立、营销冗余”的思路。下一部分进入部署重点说怎么把脚本和配置落到本地环境里。4. 解压、配置、跑起CRMOnline 的本地化启动步骤4.1 确认环境边界这套系统发布年代决定了它更适合放在JDK 1.6或1.7容器里运行。如果本机只有JDK 8大概率会碰到ClassNotFound或反射异常因为老的CGLIB版本和JDK 8的强反射访问有兼容问题。建议准备一个独立的Tomcat 6或Tomcat 7实例。MySQL版本推荐5.1到5.5之间5.7以上需要手动处理GROUP BY的ONLY_FULL_GROUP_BY模式。不用盲目升级依赖。常见做法是在lib目录里先看spring.jar、ibatis.jar等文件保持原jar不变。只要JDK和Tomcat兼容系统就能起。升级Ibatis到MyBatis可以后面做不要在启动阶段混着改。4.2 初始化数据库RAR里通常包含一个.sql数据库脚本。导入步骤如下在MySQL中创建数据库crmonline字符集选定utf8。使用命令行执行脚本mysql -uroot -p crmonline crmonline.sql导入后检查核心表和菜单表SELECT table_name FROM information_schema.tables WHERE table_schemacrmonline;mysql -uroot -p crmonline crmonline.sql这条命令用用户名root连接本地MySQL-p会交互提示输入密码crmonline是目标库输入重定向将SQL脚本顺序执行。实际部署时建议创建专用账号避免把root密码写进jdbc.properties。脚本文件可能较大如果导入时报Got a packet bigger than max_allowed_packet需要修改my.ini中的max_allowed_packet。不要只改MySQL客户端服务端也要改。否则后续写入问卷答案这类长文本会失败。4.3 修改数据源、缓存和日志配置Spring数据源通常配置在applicationContext.xml或jdbc.properties中。重点检查四项配置项建议值说明jdbc.driverClassNamecom.mysql.jdbc.Driver老驱动支持5.xjdbc.urljdbc:mysql://localhost:3306/crmonline?useUnicodetruecharacterEncodingutf8避免中文乱码jdbc.usernamecrmonline_user不要用rootjdbc.password自定义明文存储改后不要提交到仓库改完数据库配置后检查oscache.properties中的cache.memory。如果开发机内存较小可以设置cache.memoryfalse、cache.path/tmp/oscache把缓存刷到磁盘。cache.capacity是缓存条目数默认5000如果查询字典频繁建议提高到10000。4.4 启动和验证把项目打成war包或者直接把WebContent目录拷贝到Tomcat的webapps下启动Tomcat。启动后观察logs/catalina.out出现Server startup in xxx ms即成功。验证的首个页面不是登录页而是登录页之前的健康检查接口。在浏览器访问某个不加权限的Servlet比如/system/cacheManager.do?actionshowStatus看能否返回JSON或JSP内容。这一步能快速区分“没启动成功”和“登录后才出错”两种情况。如果访问请求出现404先检查Web应用根路径。Tomcat中项目目录如果叫crmonline访问地址就是http://localhost:8080/crmonline/。ExtJS的Store请求路径如果是绝对路径/customer.do则这里的斜杠会指向Tomcat根需要改为crmonline/customer.do或使用相对路径。4.5 高频报错对照表报错原因处理java.sql.SQLException: Unknown character setjdbc.url缺characterEncoding按上面url配置补全Invalid bound statement (not found)Ibatis namespace或id拼写错检查sqlmap配置文件Ext.widget is not a functionExtJS版本被替换恢复原ext-all.js版本ORA-00942用Oracle脚本错导到MySQL重新匹配对应数据库脚本OutOfMemoryError: PermGen spaceJDK1.6的PermGen限制在catalina.sh里增加-XX:MaxPermSize256m最后一条在Mac和Linux上很常见。老版本Tomcat和JDK搭配运行时PermGen不足会导致系统不定期宕机。调整JVM参数后重启即可。提示换JDK版本后如果Tomcat一直报UnsupportedClassVersionError先执行java -version确认JAVA_HOME指向的是1.6而不是在IDE里编译成了1.8的class。5. 用OSCache日志和Log4j验证CRMOnline查询缓存的命中质量5.1 开启逐条SQL日志OSCache能掩盖不少数据库压力但也会掩盖代码里的缓存key设计问题。验证的方法不是看监控面板而是打开Log4j的Ibatis日志。log4j.logger.com.ibatisDEBUG log4j.logger.org.apache.ibatisDEBUG如果项目用的Ibatis 2.x第一条生效如果已经部分升级到MyBatis第二条生效。重启后在Tomcat控制台观察每次后台查询都会打印出PreparedStatement的SQL和执行参数。这时重复点击客户列表的下一页如果第二次点击没有打印新的SQL日志说明上次结果被OSCache命中如果每次都打印就要检查查询条件里是不是带了时间戳或随机数。注意操作窗口不要开太久DEBUG日志会很快撑到几百MB。测完立刻改回WARN。5.2 临时打印缓存命中点日志只能看到SQL看不到缓存层。为了定位是OSCache没进还是进了没命中可以在Service方法临时加一段判断代码String cacheKey customer_list_ customerName; try { Object cached admin.getFromCache(cacheKey); return (List) cached; } catch (NeedsRefreshException e) { admin.cancelUpdate(cacheKey); List result queryFromDatabase(customerName); admin.putInCache(cacheKey, result); return result; }这段代码的关键是NeedsRefreshException。第一次缓存未命中时OSCache会抛出这个异常并不是程序错误。很多初学者把它当成普通Exception catch掉后不更新缓存导致每次查询都穿透到数据库。真正要做的是在异常分支里主动调用cancelUpdate释放条目锁然后回填数据库查询结果。如果回填过程耗时较长锁期间其他请求会等待这在高并发下会拖慢接口需要同步关注。5.3 低命中率修复方向如果发现某个列表页命中率低于预期先看key的构造。常见问题出现在把new Date()、随机ID或者HTTP Session值拼进key里。纠正后可以给不同模块设置不同过期时间字典数据refreshPeriod设置300秒订单列表设置30秒客户详情设置60秒。改完再开SQL日志压一次对比相同参数请求的日志条数。源码里的调用API目录是给二次开发用的里面预留了接口鉴权参数对接外部系统时可以直接复用代码模板和代码检测目录属于工程工具部署时不用管但扩展新模块前先看一眼模板能帮你省掉重复写ExtJS窗口和Ibatis映射的时间。调整完缓存策略后再次压一下重复查询接口对比SQL日志条数以此确认OSCache在控制业务查询开销时没有把数据锁死。本文还有配套的精品资源点击获取