ASP.NET CRM源码拆解:ligerUI框架到客户管理全流程实战
发布时间:2026/10/27 7:27:02 锦皓数字建站

简介这套基于ligerUI框架的ASP.NET客户关系管理系统完整源码围绕客户信息、员工合同、角色授权等企业常用业务展开适合有一定.NET基础、希望深入大型Web系统设计或二次开发的开发者。压缩包共47.93MB内含2000个文件类型覆盖用于界面展示的gif、png图片负责交互逻辑的js脚本定义样式的css与页面结构html以及服务器端处理的aspx、cs文件同时配置有sql数据库脚本、config配置文件、dll类库和chm帮助文档目录按功能模块组织便于快速定位。已有134人学习下载作为一份贴近真实企业项目的源码案例可用于自学、课程设计或工作参考。研读这套源码可以观察到ASP.NET页面生命周期、身份验证和角色授权在具体功能中的落地数据访问层则结合常见ORM框架或ADO.NET实现表结构和业务逻辑的划分值得细读。前端借助ligerUI表格完成数据排序、分页与异步加载在员工、合同、授权管理等模块中都有体现。通过梳理这些内容能帮助开发者提升.NET后端、数据库设计以及前端交互的综合能力为后续构建或改进同类管理系统积累经验。1. 拆开这套ASP.NET CRM源码前先想清楚你要的是什么一个写着“ASP.NET客户关系管理系统源码大型CRM源码ligerUI框架”的zip包在从业者眼里不只是代码而是一套后台管理系统的完整模板。ligerUI是国产的基于jQuery的UI框架专为管理系统而生配合ASP.NET Web Forms的服务器事件模型可以快速拼出客户列表、跟进记录、统计报表这类页面。这套源码能给你的是完整的客户管理、跟进记录、订单关联和权限框架适合需要快速交付后台管理系统的中小团队也适合想从老项目里拆业务模型的学习者。阅读后文之前先确认你的环境是Windows、Visual Studio 2015以上版本数据库用SQL Server 2012以上这条路后面会顺很多。2. ligerUI框架为什么老牌CRM系统押注在这套UI上一个成熟的CRM项目最早面对的不是功能而是“如何在最短时间搭出几十个长得差不多的后台页面”。ligerUI是国产开源UI框架从2009年左右开始流行在Web Forms时代是后台管理系统的事实标准之一。它不像DevExpress那样商业收费也不像今天的Vue、Element UI那样需要工程化构建直接在aspx页面引用三个文件就能工作这对ASP.NET老项目来说极具吸引力。用ligerUI可以完成布局、表格、表单、树、弹窗和页签几乎每个后台页面都能拼装出来。2.1 为什么选ligerUI而不是DevExpress或ElementUIDevExpress在Web Forms时代功能更强但License费用不低ElementUI虽然当下流行但基于Vue需要把整个aspx页面重构成前后端分离老项目一般不敢动。ligerUI的优势在于三点第一纯前端jQuery插件服务端不受约束ashx、aspx都能返回JSON供它渲染第二源码开放表格的列定义、工具栏按钮都可以在JS里直接定制第三对IE兼容性好很多企业内部后台至今还在用IE浏览器。缺点也明显在如今的高分屏下字体偏小、Switch按钮样式老气、项目长期不维护。不过CRM后台的核心是数据管理不是炫技ligerUI足够支撑五年以上的业务稳定期。2.2 用ligerLayout搭出CRM主界面布局参数与全屏坑CRM系统的默认界面是“顶部显示登录人信息 左侧菜单 右侧内容区”。用ligerLayout实现这个架子只需要一个div加几行配置。先看HTML结构div idlayout stylewidth:100%; height:100%;/div再看JavaScript初始化$(#layout).ligerLayout({ height: 100%, topHeight: 48, leftWidth: 180, allowLeftCollapse: true, centerBottomHeight: 0 });逻辑说明height设成100%并不会自动生效前提是html和body都要显式声明height:100%否则容器高度为0布局会塌陷成只有顶部一条线。topHeight决定顶部面板高度leftWidth是左侧菜单区宽度。allowLeftCollapse设为true后用户点击左侧栏边缘的箭头可收起菜单给内容区更多空间。参数说明centerBottomHeight:0表示中间内容区不再拆分上下两块如果业务需要在中区底部再挂一个客户详情面板把它调成比如180。ligerLayout的units配置方式更灵活但老源码更习惯这种简化写法维护起来直观。左侧菜单通常用ligerTree渲染选中后在中区打开页签$(#leftTree).ligerTree({ url: MenuHandler.ashx?actiontree, idFieldName: MenuId, parentIDFieldName: ParentId, onSelect: function (node) { var tab $(#centerPanel).ligerGetTabManager(); tab.addTab({ title: node.text, tabid: node.data.MenuId, url: node.data.Url }); } });逻辑说明菜单数据来自ashx接口返回树形JSON。选中节点后在中间区域添加一个ligerTab页签实现多页签工作模式。这比传统iframe切页面要合理也方便用户同时对比多个客户详情。参数说明idFieldName和parentIDFieldName告诉ligerTree哪个字段是主键、哪个是父级ID这两个名称必须与数据库字段名完全一致否则树渲染出来是一堆无法挂接的散节点。tab.addTab里的url对应一个aspx页面老系统普遍用frameset方式加载。2.3 ligerGrid渲染客户列表与ashx数据接口的对接客户列表是CRM里访问频率最高的页面。ligerGrid加载数据的方式是请求一个url后端返回灵活动态结构的JSON。这个结构包含两个固定字段Rows表示当前页数据行Total表示总条数。下面是一个典型的客户列表ashx接口public class CustomerHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType application/json; context.Response.Charset UTF-8; int pageIndex int.Parse(context.Request[page] ?? 1); int pageSize int.Parse(context.Request[pagesize] ?? 20); string keyword context.Request[keyword] ?? ; DataTable dt CustomerService.QueryPage(keyword, pageIndex, pageSize); int total CustomerService.QueryCount(keyword); var result new Dictionarystring, object(); result[Rows] dt; result[Total] total; context.Response.Write(JsonConvert.SerializeObject(result)); } public bool IsReusable false; }逻辑说明page和pagesize是ligerGrid分页时自动提交的查询参数keyword是搜索框传入的关键词。QueryPage和QueryCount是两个独立的数据层方法分别取当前页数据和总数。老项目很少引入ORM直接用ADO.NET执行SQL即可。参数说明page从1开始pagesize默认20Response.Charset设为UTF-8是为了避免中文乱码这一步容易被忽略。JsonConvert来自Newtonsoft.Json如果源码包里没有这个DLL可以用旧的JavaScriptSerializer替代但推荐统一用Newtonsoft.Json。前端配置ligerGrid的代码$(#maingrid).ligerGrid({ columns: [ { display: 客户名称, name: CustomerName, width: 200 }, { display: 联系人, name: ContactPerson, width: 100 }, { display: 电话, name: Phone, width: 120 }, { display: 客户状态, name: StatusName, width: 80 }, { display: 创建人, name: Creator, width: 80 } ], url: CustomerHandler.ashx?actionlist, rownumbers: true, pageSize: 20, usePager: true, checkbox: true, toolbar: { items: [ { text: 新增客户, click: function(){ openCustomerEditDialog(0); } } ] } });逻辑说明ligerGrid通过url读取数据rownumbers显示行号usePager启用分页。checkbox打开行选择配合toolbar里的“新增客户”按钮可以批量操作选中的行。openCustomerEditDialog(0)是弹窗新增的函数传0表示新增传客户ID表示编辑。参数说明name字段必须和JSON返回的数据字段名匹配display只控制表头显示文字width用像素值在后台管理系统这样的固定宽度场景里没有问题。若要在列表上方加搜索栏需要配置parms参数把keyword传给后端。3. 客户管理模块源码拆解从数据库到页面的完整链路CRM的核心是客户数据所有功能都围绕“谁能看到哪些客户、谁在什么时间跟进了谁”展开。用asp.net core mvc重写这套系统是近年讨论度很高的方向但这套源码本身是标准Web Forms三层结构页面层、业务层、数据层。下面按数据表、写入流程、权限控制的顺序拆开看。3.1 客户主表设计核心字段与下拉选项怎么存一套能落地的客户主表字段设计决定了后续所有模块的写法和查询速度。源码包里常见的核心表结构如下字段名类型说明CustomerIdint 自增客户主键CustomerNamenvarchar(200)客户名称ContactPersonnvarchar(50)联系人Phonenvarchar(20)联系电话Statusint1潜在 2已合作 3流失Sourceint来源渠道CreatorIdint创建人账号IDCreateTimedatetime创建时间LastFollowTimedatetime最近跟进时间建表SQL至少要有主键和两个索引否则数据量过十万后列表查询明显变慢CREATE TABLE [dbo].[Customer] ( [CustomerId] INT IDENTITY(1,1) PRIMARY KEY, [CustomerName] NVARCHAR(200) NOT NULL, [ContactPerson] NVARCHAR(50) NULL, [Phone] NVARCHAR(20) NULL, [Status] INT NOT NULL DEFAULT 1, [Source] INT NOT NULL DEFAULT 0, [CreatorId] INT NOT NULL, [CreateTime] DATETIME NOT NULL DEFAULT GETDATE(), [LastFollowTime] DATETIME NULL ); GO CREATE INDEX IX_Customer_CreatorId ON [dbo].[Customer] (CreatorId); CREATE INDEX IX_Customer_Status ON [dbo].[Customer] (Status);逻辑说明Status和Source用int存储前端ligerGrid显示时翻译成中文。常见做法是在列表SQL中用LEFT JOIN一张字典表或者在C#后端从缓存字典里映射。推荐后者因为减少一次查询字典变更不用改SQL。CreatorId必须建索引“只能看自己的客户”的过滤条件就是它。参数说明NVARCHAR统一使用避免中文显示为问号CreateTime用DATETIME默认值GETDATE()C#代码里不需要赋时间。LastFollowTime留空表示尚未跟进列表页可以按此字段排序让“今天该联系谁”的客户排在最前面。3.2 跟进记录的写入与时间线展示跟进记录与客户表是多对一关系。设计时要把“新增记录”和“更新客户最近跟进时间”放在一次操作里完成否则容易出现记录写了但列表排序时间没变的脏数据。下面是写入接口的代码string sql INSERT INTO FollowRecord (CustomerId, Content, FollowType, FollowUserId, FollowTime) VALUES (CustomerId, Content, FollowType, FollowUserId, GETDATE()); UPDATE Customer SET LastFollowTime GETDATE() WHERE CustomerId CustomerId;; using (SqlConnection conn new SqlConnection(connStr)) { SqlCommand cmd new SqlCommand(sql, conn); cmd.Parameters.AddWithValue(CustomerId, customerId); cmd.Parameters.AddWithValue(Content, content); cmd.Parameters.AddWithValue(FollowType, followType); cmd.Parameters.AddWithValue(FollowUserId, userId); conn.Open(); cmd.ExecuteNonQuery(); }逻辑说明通过一次连接、两条SQL语句完成“新增跟进记录”和“更新客户最近跟进时间”。这里没有包事务因为UPDATE客户表失败的概率很低而且即使失败跟进记录已写入下次拉取时LastFollowTime为空也不会丢数据。如果想更严谨可以包一层SqlTransaction代价是多写几行代码。参数说明FollowType用int表示电话、拜访、微信、邮件Content用nvarchar建议长度500以内FollowUserId记录跟进人方便后续统计“本周每人跟进了多少条”。时间线展示用ligerGrid也能做但更推荐直接用aspx页面拼接HTML时间轴视觉效果更直观ul classtimeline li span classtime2025-06-03 10:22/span p张三电话沟通报价客户对价格有疑虑/p /li li span classtime2025-05-28 15:40/span p李四发送产品白皮书/p /li /ul逻辑说明用Repeater控件遍历数据源生成li节点按FollowTime倒序排列。日期格式建议在后端ToString(yyyy-MM-dd HH:mm)避免在浏览器端用JavaScript处理时区。3.3 销售只能看自己的客户基于Session的权限过滤CRM系统里最敏感的权限规则是“销售只能看自己的客户经理看全部”。这套源码的登录模块会把用户ID和角色ID写入Session列表查询根据角色拼接条件protected void Page_Load(object sender, EventArgs e) { if (Session[UserId] null) { Response.Redirect(Login.aspx); return; } bool isManager Session[RoleId].ToString() 2; if (!isManager) { int userId Convert.ToInt32(Session[UserId]); string sql SELECT * FROM Customer WHERE CreatorId UserId; // 绑定数据源 } else { string sql SELECT * FROM Customer; } }逻辑说明这是一种朴素但有效的权限实现。经理角色的查询条件不加过滤销售角色的查询条件固定带CreatorId。更严谨的做法是把过滤条件收口到数据层统一处理用存储过程接收UserId参数避免每个页面重复写这段逻辑。参数说明Session[RoleId].ToString() 2里的角色ID要与数据库角色表保持一致。多租户场景在此基础上还要加CompanyId条件单公司内部CRM不需要。Session过期后这个判断会直接跳登录页因此上一章里提到的Session持久化方案是权限模块稳定运行的前提。4. ASP.NET CRM项目避坑指南老源码常见的五处翻车点老源码拿到手直接按F5能跑起来的概率不到一半。最常见的问题集中在机器环境、数据库脚本和代码里的硬编码配置。以我拆过源码包的经验下面五个坑几乎每个人都会撞上。4.1 连接字符串指向了旧SQL Server实例现象项目启动后页面报“System.Data.SqlClient.SqlException: 无法打开登录所请求的数据库”。原因web.config里的Data Source还是开发者的本机实例名或IP形如Data SourcePC-2019;Initial CatalogCRM;User IDsa;Password123456。这个实例名在你机器上根本不存在数据库也没有还原。解决先在本机SQL Server里还原数据库备份再改连接字符串。建议统一改成connectionStrings add nameCrmConn connectionStringData Source.;Initial CatalogCRM;Integrated SecurityTrue;User IDsa;Password你的密码; providerNameSystem.Data.SqlClient / /connectionStrings逻辑说明Data Source.表示本机默认实例Integrated SecurityTrue在开发环境使用Windows认证比较方便。但部署到服务器后如果IIS应用池账户没有SQL权限就会失败所以生产环境要保留User ID和Password。参数说明providerName必须与代码中SqlConnection的创建方式匹配修改web.config后IIS会自动回收应用池不需要手动重启站点。4.2 Session丢失导致登录状态不定现象用户登录后跳转首页刷新一次又回到登录页或者过几分钟就掉线。原因IIS应用池默认Idle Timeout是20分钟超过20分钟没有请求工作进程被回收内存中的Session全部清空。另外web.config里SessionState默认是InProc进程内存储应用池回收后Session就没了。解决将Session模式改为StateServer并修改应用池的空闲超时system.web sessionState modeStateServer stateConnectionStringtcpip127.0.0.1:42424 timeout60 / /system.web逻辑说明StateServer模式把Session存到独立的ASP.NET状态服务进程应用池回收时Session不丢这是小规模CRM最稳定的方案。同时还要把IIS应用池的“空闲超时”从默认20分钟改成0也就是永不回收才能实现真正意义上的永久在线CRM网站。参数说明timeout60单位是分钟Windows服务里的“ASP.NET State Service”默认状态是手动要改成自动并启动。SQLServer模式更可靠但要额外创建ASPState数据库小项目不太划算。4.3 自增ID与批量操作的坑现象在客户列表里勾选多条记录删除后新导入的客户ID变成了几百上千看起来像“ID跳跃”业务人员误以为数据被破坏。原因SQL Server自增列在任何删除操作后都不会回填这是正常行为。但从业务角度看列表中删除中间记录会导致分页跳变体验很差。解决不要做硬删除改用软删除字段给Customer表加IsDeleted bit字段默认0。列表查询加WHERE IsDeleted0删除操作变成UPDATEUPDATE Customer SET IsDeleted 1 WHERE CustomerId IN (1, 2, 3);逻辑说明软删除避免了自增ID跳跃和关联数据被连带删除的风险。客户被软删除后跟进记录、合同、订单仍能通过CustomerId关联回溯只是不在默认列表里出现。后续加一个“回收站”页面把IsDeleted改回0即可恢复。参数说明如果要有审计需求再加上LastModifyUserId和LastModifyTime字段每次修改都更新这两个值。4.4 中文乱码问题现象ashx接口返回的列表里中文显示成“???”或者变成“测试”这类乱码。原因Response没有设置Charset为UTF-8或者aspx页面本身以GBK编码保存而浏览器按UTF-8解析。老项目的源文件很多是GBK编码。解决三层统一处理。ashx接口设置Response.CharsetUTF-8aspx页面在% Page %指令里加ResponseEncodingutf-8如果源码文件是GBK编码用Visual Studio的“文件-高级保存选项”把文件重新保存为带签名的UTF-8格式。逻辑说明C#字符串编译进DLL后源文件编码不影响编译结果影响的是.aspx直接渲染到HTML的中文。因此只要页面指令和HTTP头设置一致乱码基本能解决。参数说明ResponseEncodingutf-8和Response.CharsetUTF-8要统一。如果页面指令写utf-8Response只写text/html浏览器可能仍按默认编码解析。4.5 JSON序列化循环引用现象用Newtonsoft.Json序列化实体对象时抛异常“Self referencing loop detected”。原因数据库实体类的导航属性形成循环引用最典型的是Customer集合里有FollowRecords每条FollowRecord又引用Customer。JSON序列化时无限循环。解决序列化时设置ReferenceLoopHandling.Ignore或直接在实体导航属性上加[JsonIgnore]特性var settings new JsonSerializerSettings(); settings.ReferenceLoopHandling ReferenceLoopHandling.Ignore; string json JsonConvert.SerializeObject(result, settings);逻辑说明Ignore会跳过循环引用的属性丢失的只是导航属性对客户列表场景没有影响。另一个可选做法是定义DTO输出类只暴露需要的字段更推荐但要写更多映射代码。参数说明settings.DateFormatString设为yyyy-MM-dd HH:mm:ss避免时间字段序列化成/Date(...)/的奇怪格式前端显示也更友好。5. 大型CRM源码的模块边界订单、合同、统计报表怎么拆“大型”在源码里通常体现在模块数量上客户、跟进、订单、合同、产品、统计、权限、工作台加起来二十多张表。如果每个页面直接写死SQL后期维护成本很高。好的源码会在业务层做边界控制让订单模块不直接操作客户表而是通过公共数据访问基类获取信息。5.1 订单与合同模块如何复用客户主表订单表和合同表都依赖CustomerId但不应该各自重复建立到数据库的连接逻辑。公共的数据访问基类能把最基础的增删改查收拢成几个方法各模块DAL继承它即可public class BaseDAL { protected string connStr ConfigurationManager.ConnectionStrings[CrmConn].ConnectionString; protected DataTable ExecuteTable(string sql, params SqlParameter[] ps) { using (SqlConnection conn new SqlConnection(connStr)) { SqlCommand cmd new SqlCommand(sql, conn); if (ps ! null) cmd.Parameters.AddRange(ps); SqlDataAdapter da new SqlDataAdapter(cmd); DataTable dt new DataTable(); da.Fill(dt); return dt; } } protected int ExecuteNonQuery(string sql, params SqlParameter[] ps) { using (SqlConnection conn new SqlConnection(connStr)) { SqlCommand cmd new SqlCommand(sql, conn); if (ps ! null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } }逻辑说明BaseDAL把数据库连接打开、执行、释放封装成两个方法。订单DAL和合同DAL继承它复用的是连接字符串、参数化执行和异常释放逻辑。这种基类加继承的模式在老项目里很常见不需要引入ORM的映射关系。参数说明ExecuteTable返回DataTable便于绑定GridView或ligerGridExecuteNonQuery用于INSERT、UPDATE、DELETE。所有SQL一律使用SqlParameter参数化防止拼接SQL注入这个习惯在改造老代码时务必保留。5.2 统计报表用视图还是C#内存计算统计报表通常有三个维度本月新增客户数、各销售跟进次数、订单金额排名。如果全部读取原始数据在C#内存里分组数据量大时会卡死页面。正确做法是把聚合逻辑下推到数据库复杂统计建视图CREATE VIEW vw_CustomerSummary AS SELECT c.CreatorId, u.RealName AS CreatorName, COUNT(c.CustomerId) AS CustomerCount, COUNT(f.FollowId) AS FollowCount, MAX(c.CreateTime) AS LastCreateTime FROM dbo.Customer c LEFT JOIN dbo.SysUser u ON c.CreatorId u.UserId LEFT JOIN dbo.FollowRecord f ON c.CustomerId f.CustomerId WHERE c.IsDeleted 0 GROUP BY c.CreatorId, u.RealName;逻辑说明视图把客户数和跟进数按销售汇总页面直接SELECT视图即可不需要在C#里遍历循环。LEFT JOIN保证即使某销售跟进次数为零也照样出现在结果集中。参数说明SQL Server对视图会缓存执行计划性能比每回动态拼接SQL更好。表结构后续频繁变更时记得用EXEC sp_refreshview vw_CustomerSummary刷新视图元数据否则可能出现查询报错。5.3 模块间的耦合控制与公共类大型CRM最容易出现的问题是Global.asax里堆满各业务模块的公共方法。推荐的边界划分如下数据层每个模块一个DAL文件不跨模块调用业务层按客户、订单、合同、系统权限划分只允许上层调用公共工具把缓存、JSON序列化、Excel导出放到Common目录。如果客户模块要删除一个客户不要在页面上直接DELETE订单表和合同表的数据。正确的做法是先调用订单DAL检查是否存在未完成订单再调用合同DAL检查合同最后才删除客户主数据。模块边界清晰后回溯问题时日志点才找得准。另外如果你后续想把功能扩展到crm管理系统结合扫码采集的场景也要按这个边界新增独立模块而不是在客户页面里堆代码块。6. 让这套CRM源码跑起来的验证方法最小闭环与调优拿到源码后不要急着改界面。先跑通“最小闭环”闭环的三条标准是数据库脚本能在你的SQL Server上建出库项目能编译客户列表页能加载数据。跑通之后再谈模块扩展和性能优化。6.1 本地跑通的最小步骤先确认源码是Web Site还是Web Application。Web Site以文件夹为单位改动后IIS直接编译Web Application需要先编译再运行推荐后者。最小步骤如下解压到非中文路径用Visual Studio打开解决方案恢复NuGet包修改web.config连接字符串执行数据库脚本设置启动项目后按F5。如果NuGet还原失败不要轻易升级包版本先检查packages目录是否存在老源码的DLL版本一旦升级很容易引发不兼容。6.2 初步性能验证页面能正常显示后打开浏览器开发者工具看ligerGrid接口返回时间。客户数据超过十万条时分页查询和索引基本够用。重点检查三件事所有列表查询是否过滤了IsDeleted0FollowRecord大表是否建了CustomerId索引有没有哪条SQL漏了分页条件一次查全表。这些SQL层的小改动往往比引入缓存框架收益更大。6.3 上线前的自检清单部署到IIS前花半小时把如下清单过一遍应用池托管管道模式选经典模式站点目录转为应用程序web.config里customErrors先设为Off方便看真实报错Windows事件查看器里ASP.NET日志是排错最重要的来源。我自己拆这套源码的习惯是先跑一个小团队试用三个月再根据实际使用情况决定是否改造多租户。CRM这类系统真正沉淀价值的是业务数据和流程不是UI框架。每改动一处都用同一份测试数据验证回归。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。