简介这是一套面向ASP.NET开发者的企业级客户关系管理系统完整源码整合了大型CRM业务逻辑与ligerUI前端框架。源码涵盖销售、市场、服务等典型业务场景包含员工管理、合同管理、角色授权等模块适合希望深入理解ASP.NET Web Forms与前端交互的中高级开发者参考学习。压缩包共2000个文件约47.93MB其中以gif、js、png、css等前端资源为主配合aspx页面、cs后台代码以及sql数据库脚本便于对照研究页面呈现与服务端处理的完整链路。已有134人学习下载。通过学习这套代码可掌握ASP.NET平台下分层架构设计、数据访问与权限控制的具体实现也能从ligerUI的表格、下拉、日期控件应用中学到富客户端界面的搭建技巧为后续二次开发或自主构建CRM系统提供扎实参考。1. 这套带 ligerUI 的 ASP.NET CRM 源码值不值先解压看三大件遇到“ASP.NET客户关系管理系统源码大型CRM源码ASP.NET源码ligerUI框架.zip”这种命名基本都来自源码站打包或网盘分享。标题把技术栈ASP.NET、业务CRM、UI组件ligerUI一次说全可以断定这是 WebForms 时代的老项目不是 ASP.NET Core MVC。它解决的问题很实际中小团队需要一个能管客户、联系人、跟进记录和销售机会的系统又不愿意从零开发。适合有 C# 基础、打算做二次开发的后端工程师完全没有 .NET 背景的人拿到这包源码很难跑通。先说清楚“三大件”数据库脚本、ashx 接口层、ligerUI 前端文件。缺了哪一件这个项目都起不来。下面按解压到上线的顺序把这条路走完。2. 源码结构拆解ASP.NET WebForms 与 ligerUI 的前后端约定2.1 先分三层aspx 页面、ashx 处理程序、App_Code 里的 DBHelper老式 CRM 源码的目录通常长这样/Web /bin 编译后的 DLL包里可能已经带了一套 /App_Code DbHelper.cs ADO.NET 封装SqlConnection 都集中在这里 Customer.cs 客户实体和数据处理类 /ashx customer.ashx 客户列表、保存、删除的 HTTP 入口 customer.ashx.cs /Modules /Customer CustomerList.aspx 客户列表页主区域只放 ligerGrid 的 div CustomerEdit.aspx 新增/编辑客户页 /System UserList.aspx RoleList.aspx MenuList.aspx /ligerUI /js liger.all.js ligerGrid.js ligerForm.js /css /Scripts jquery-1.8.2.min.js /login.aspx /Site.master 母版页,左侧菜单在这里按角色渲染 /Web.config拿到源码先别急着点运行先把目录过一遍弄清楚哪里是页面、哪里是数据出口。这类项目最常见的分工是列表页用普通 aspx 放一个 div页面加载时由 ligerGrid 请求 ashx 接口拿 JSON新增和编辑单独开一个 aspx 页面在 ligerDialog 弹窗里打开保存既可以走 ashx 的 actionsave也可以走 aspx 自身的 PostBack。这套混合模式看着不现代但它绕开了 WebForms 服务器控件回发带来的 ViewState 负担是当年做“轻前后端分离”最稳妥的折中方案。接口层骨架可以统一写成这样// customer.ashx.cs public class customer : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType application/json; string action context.Request[action] ?? ; switch (action) { case list: GetList(context); break; case save: Save(context); break; case delete: Delete(context); break; } } private void GetList(HttpContext context) { int page int.Parse(context.Request[page] ?? 1); int pagesize int.Parse(context.Request[pagesize] ?? 20); string sort context.Request[sort] ?? CustomerID; string order context.Request[order] ?? desc; // 调用 DbHelper 查 DataTable再拼成 ligerGrid 要的 JSON } public bool IsReusable { get { return false; } } }先解释两个参数。page和pagesize是 ligerGrid 每次翻页自动带给后端的入参sort和order是点击列头排序时追加的参数大多数老源码全靠这几个参数拼 SQL 分页而不是 ORM。后面改任何查询接口都必须守住这几个入参否则前端表格要么不分页要么点排序就报错。2.2 ligerGrid 的 JSON 协议Rows 和 Total 一个都不能少ligerUI 在那个年代流行是因为它的网格几乎不写 HTML 表格只需要一个 div所有配置丢给 JS 即可。前端配置是这样的$(#maingrid).ligerGrid({ url: /ashx/customer.ashx?actionlist, pageSize: 20, columns: [ { display: 客户名称, name: CustomerName, width: 180 }, { display: 联系人, name: Contact, width: 100 }, { display: 电话, name: Phone, width: 120 }, { display: 创建时间, name: CreateTime, width: 140 } ], root: Rows, record: Total, pageParmName: page, rowsParmName: pagesize, sortnameParmName: sort, sortorderParmName: order });ligerGrid 默认向后台要的不是普通数组而是带Rows和Total两个顶层字段的对象。Total是总记录数网格靠它算总页数Rows是当前页的行数组。如果你直接把 DataTable 序列化后输出行数据能出来但结构不对网格会一直转圈。我一般在数据访问层外面做一层转换把 DataTable 变成字典列表private static ListDictionarystring, object DataTableToList(DataTable dt) { var list new ListDictionarystring, object(); foreach (DataRow row in dt.Rows) { var dict new Dictionarystring, object(); foreach (DataColumn col in dt.Columns) { dict[col.ColumnName] row[col] DBNull.Value ? : row[col].ToString(); } list.Add(dict); } return list; } // 拼最终结果 var result new { Rows DataTableToList(dt), Total totalCount }; context.Response.Write(new JavaScriptSerializer().Serialize(result));这是老源码里的标准动作。要特别留意日期类型JavaScriptSerializer 会把 DateTime 序列化成/Date(1599999999999)/这种格式ligerGrid 显示出来是一串毫秒数所以我转换时先把日期统一转成字符串前端不用再做格式化。另外row[col].ToString()会把 NULL 变成空字符串避免 JSON 里出现 null 导致前端取字段报错。这个细节值一条血泪经验。2.3 权限链路五张表串起来的菜单和登录态标题里写“大型CRM源码”通常指的不只是客户表而是带完整系统管理模块。典型权限设计是五张表Sys_User用户、Sys_Role角色、Sys_UserRole用户角色关系、Sys_Menu菜单、Sys_RoleMenu角色菜单关系。登录时把用户主键和角色主键写进 Session母版页加载时按角色查菜单// Site.master.cs 片段 protected void Page_Load(object sender, EventArgs e) { if (Session[UserId] null) { Response.Redirect(/login.aspx, false); return; } int roleId Convert.ToInt32(Session[RoleId]); DataTable menus DbHelper.ExecuteDataTable( SELECT MenuName, MenuUrl FROM Sys_Menu m JOIN Sys_RoleMenu rm ON m.MenuID rm.MenuID WHERE rm.RoleID roleId ORDER BY m.SortNo, new SqlParameter(roleId, roleId)); // 循环 menus 生成 li绑定到母版页的菜单容器 }这个位置有个细节母版页的Page_Load先于内容页执行所以登录判断写在这里一次就够。注意Response.Redirect最好带上第二个参数false否则系统会先抛一个 ThreadAbortException在某些 IIS 配置下会导致响应被中断。老源码通常在 ashx 里也各自判断一次Session[UserId]这看起来啰嗦但这是安全感的来源——你改接口时千万别顺手删掉这段判断。2.4 操作按钮的通用模式toolbar、行内按钮和对话框列表页下半部分一般是工具栏和行内操作。ligerGrid 的 toolbar 设置会绑定新增、删除、导出按钮行内操作则在render列里返回按钮 HTML$(#maingrid).ligerGrid({ url: /ashx/customer.ashx?actionlist, // columns、root、record 等配置省略 toolbar: { items: [ { text: 新增, click: openAdd, icon: add }, { text: 删除, click: deleteSelected, icon: delete } ] }, columns: [ // 省略业务列 { display: 操作, name: op, width: 120, render: function (row) { return a hrefjavascript:void(0) onclickopenEdit( row.CustomerID )编辑/a a hrefjavascript:void(0) onclickdeleteOne( row.CustomerID )删除/a; }} ] });新增和编辑按钮通常都走同一个对话框入口只是传不传当前行 id 的区别。这种模式贯穿整个系统包括用户管理、角色管理、字典管理逻辑完全一致。你只要理解了一个模块的前后端联动剩下模块都是复制粘贴后改业务字段。这也是这种老源码最大的学习价值。3. 在本地把项目跑起来环境匹配、数据库还原与一次编译3.1 环境版本匹配是玄学但先装齐这几样就不慌老 CRM 源码多数是 VS2008 或 VS2010 时代创建的目标框架 .NET Framework 3.5 到 4.0。现在拿 VS2022 打开会弹项目升级向导默认往 .NET Framework 4.8 迁移这个过程通常能过偶尔卡在某些程序集引用上。我的建议是先装好 .NET Framework 4.8 Developer Pack、SQL ServerExpress 够用、IIS Express然后打开项目后右键检查目标框架是不是 4.8。第一次编译见到几个典型错误很正常找不到AjaxControlToolkit.dll或者Newtonsoft.Json版本冲突。先看Web.config里有没有assemblyBinding节点很多老项目没有需要手动加configuration runtime assemblyBinding xmlnsurn:schemas-microsoft-com:asm.v1 dependentAssembly assemblyIdentity nameNewtonsoft.Json publicKeyToken30ad4fe6b2a6aeed cultureneutral / bindingRedirect oldVersion0.0.0.0-13.0.0.0 newVersion13.0.0.0 / /dependentAssembly /assemblyBinding /runtime /configuration不要一上来就升级所有 NuGet 包也不要手痒把 System.Web.Mvc 从 3 升到 5。老代码里很多 API 在新版本里能编译但运行时行为不一样。能编译通过就先编译源码改造讲究小步快跑先求跑通再求重构。3.2 数据库还原先建库、再导脚本、最后改连接串源码包里通常带两个 SQL 文件一个建库建表比如crm_db.sql一个初始化数据比如crm_data.sql。如果直接用 SSMS 打开最常遇到的是中文乱码具体原因见第 5 章。命令行执行更稳sqlcmd -S .\SQLEXPRESS -i D:\crm\crm_db.sql sqlcmd -S .\SQLEXPRESS -d CRMDB -i D:\crm\crm_data.sql如果脚本里已经写了USE CRMDB-d参数可以省略但我习惯带着防止脚本没切库初始化数据写进 master。执行完以后打开Web.configconnectionStrings add nameConnString connectionStringData Source.\SQLEXPRESS;Initial CatalogCRMDB;User IDsa;Password你的密码;MultipleActiveResultSetsTrue providerNameSystem.Data.SqlClient / /connectionStrings本机开发也可以把User ID/password换成Integrated SecurityTrue。MultipleActiveResultSetsTrue这一项一定要加老代码经常在一个连接上同时开着 DataReader 又执行另一条命令不加会报“连接未关闭”。3.3 第一次编译的翻车现场缺失 DLL、目标框架、自定义错误打开解决方案后直接按 F5 是新手最容易翻车的地方。正确顺序是先生成解决方案看错误列表再处理 bin 目录里缺的 DLL。有些源码包为了体积清空了 bin这时候要靠 NuGet 装回兼容版本。常见依赖参考缺失项从哪里补建议版本ligerUI源码包 /ligerUI 目录一般自带的就能用jQuery/Scripts 目录1.8.2不要换 3.xNewtonsoft.JsonNuGet6.0 到 13.0 都行注意绑定重定向AjaxControlToolkitNuGet版本看代码里引用的命名空间如果编译错误集中在System.Web.Extensions 找不到说明目标框架太低切到 .NET 4.8 以后这类错基本消失。还有一类错误是代码用了过时 API编译只给警告不报错不用管。编译通过不代表能登录。第一次打开页面看到的往往不是登录框而是 500。打开浏览器开发者工具看网络请求把Web.config里的customErrors modeOff先改上刷新再看真正的异常信息会直接打在页面上。这一步能省掉大半排错时间。3.4 启动以后先验证什么登录、菜单、列表三个页面跑起来以后先不要点遍所有菜单。按“登录 → 首页菜单 → 选一个带 ligerGrid 的列表页”这个顺序验证。登录页能进说明数据库连接字符串和 Session 基础配置没问题首页菜单能按角色显示说明五张权限表的数据是通的列表页能出数据说明 ashx JSON 协议链路完整。这三关过了这个项目在你机器上就是活的。如果列表页能打开但没有数据先看数据库里客户表是不是空表初始化数据脚本可能只建了表没插数。如果菜单渲染出来但点了没反应先看 URL 是不是用了绝对路径老项目部署在虚拟目录下时经常因为少一层路径导致页面跳不过去。这些都不涉及改业务代码但排查起来最耗时间。4. 按业务改客户模块数据表、handler 与 ligerGrid 的联动4.1 给客户表加两个业务字段表、代码、页面要一起改实际业务里最常见的需求是加“客户等级”和“客户来源”。先找到客户表名称一般是Customer或T_Customer执行加字段ALTER TABLE dbo.Customer ADD CustomerLevel int NOT NULL DEFAULT 0, SourceName nvarchar(50) NULL;字段加完很多源码的数据访问层是直接写 SQL 的你要到App_Code/Customer.cs里找到 Insert、Update、GetList 等方法把字段拼进去。如果源码用的是存储过程那还得用 ALTER PROCEDURE 重新定义入参和写入列。这是老架构最磨人的地方改了表不改代码列表不报错但新字段永远是空改了代码不改存储过程保存直接报参数不对。我的习惯是拿到项目先搜“INSERT INTO”、再搜“UPDATE”把所有涉及客户表的地方列成清单改完一张划一张。4.2 改查询接口分页排序的 SQL 不能破坏协议约定列表查询走customer.ashx?actionlist我改造时通常会把拼 SQL 的逻辑重写一遍保证分页稳定。以 SQL Server 2008 以上版本为例分页用ROW_NUMBER()private DataTable GetCustomerPage(int page, int pagesize, string sort, string order, string keyword) { int start (page - 1) * pagesize 1; int end page * pagesize; string sortField GetSafeSortField(sort); // 白名单映射列名不能直接拼 string sortOrder order asc ? ASC : DESC; string sql SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY sortField sortOrder ) AS RowNo, CustomerID, CustomerName, Contact, Phone, CustomerLevel, SourceName, CreateTime FROM Customer WHERE (Keyword OR CustomerName LIKE % Keyword %) ) AS T WHERE RowNo BETWEEN Start AND End; SqlParameter[] ps { new SqlParameter(Keyword, keyword ?? ), new SqlParameter(Start, start), new SqlParameter(End, end) }; return DbHelper.ExecuteDataTable(sql, ps); }GetSafeSortField必须做它把传入的 sort 映射到固定白名单防止前端把恶意列名拼进 SQL。动态 SQL 用了参数化注入风险压下去了但列名没法参数化所以只能用白名单。加了业务字段以后列表查询的 SELECT 列也要跟着加不然网格里永远看不到新列。4.3 新增和编辑页面ligerDialog 打开 aspx保存后回调刷新客户编辑页CustomerEdit.aspx是老源码里最典型的“独立页面 弹窗”结构。列表页里定义新增和编辑按钮function openAdd() { $.ligerDialog.open({ url: /Modules/Customer/CustomerEdit.aspx, width: 520, height: 380, title: 新增客户, isResize: true }); } function openEdit(row) { $.ligerDialog.open({ url: /Modules/Customer/CustomerEdit.aspx?id row.CustomerID, width: 520, height: 380, title: 编辑客户, isResize: true }); }编辑页Page_Load里读取Request[id]有值就查单条数据并回填文本框没有就是新增模式。保存时把控件值收集起来调Customer.Save()然后输出一段脚本通知父页面刷新protected void btnSave_Click(object sender, EventArgs e) { int id string.IsNullOrEmpty(Request[id]) ? 0 : int.Parse(Request[id]); bool ok Customer.Save(id, txtName.Text.Trim(), ...); if (ok) { Response.Write(scriptparent.afterSave(true);/script); Response.End(); } }父页面在打开对话框前必须先把afterSave挂到window上window.afterSave function (success) { if (success) { $.ligerDialog.close(); $(#maingrid).ligerGetGridManager().reload(); } };提示ligerDialog 打开的子页面默认是 iframeafterSave必须挂在父页面的window上子页面里用parent.afterSave才能调得到。把函数写在$(function(){})里是典型的踩坑写法。很多新业务方还会提“扫码采集”需求比如用手机摄像头扫名片自动建客户。老架构里最常见的落地方式是在 CustomerEdit.aspx 单独做一个扫码入口移动端浏览器调用摄像头识别以后把结果通过 query string 带回表单并回填再复用原有保存按钮。不要试图在旧源码里集成重型扫码 SDK那会把这套薄前端拖垮。5. 避坑清单这类老 CRM 源码最容易翻车的五个位置5.1 数据库脚本乱码和兼容性属于解压前就要打的预防针现象一用 SSMS 打开crm_data.sqlSQL 能看懂但中文全是乱码“客户名称”显示成“瀹㈡埛鍚嶇О”。第一反应是文件损坏重新下载还是一样。原因老源码的 SQL 脚本保存编码是 GB2312 或 GBKSSMS 默认按 UTF-8 解码。一个中文字符在 GBK 里是两个字节用 UTF-8 去解析就成了两个乱码字符。文件没坏是编码识别错位。解决优先用 SQLCMD 执行它会按 Windows 系统代码页识别中文不乱码。如果必须要改脚本在 SSMS 里选“文件 → 打开 → 选择编码 → 中文(GB2312)”改完另存时也保持 GB2312不要顺手改成 UTF-8 带 BOM否则 SQLCMD 第一行会因为 BOM 报语法错误。我碰到最坑的一次是全套初始化数据在 SSMS 里全乱一度以为作者丢错了文件最后 SQLCMD 一把过。现象二建库脚本执行到一半报“text is not a recognized data type”或者脚本里用了sp_dboption这类早就移除的系统存储过程直接中断。原因脚本是 SQL Server 2000/2005 时代写的新版本数据库引擎对部分旧语法做了严格处理。text、ntext、image这些大对象类型虽然还保留但有些上下文已经不允许直接使用尤其不能用于变量声明和表值函数。解决新建数据库以后先执行ALTER DATABASE CRMDB SET COMPATIBILITY_LEVEL 100;把兼容级别设成 SQL Server 2008。这能解决大部分“语法不认”的报错同时不影响业务代码。如果脚本里还有RULE、DEFAULT这类 2008 之后继续废弃的对象只能手动改成标准约束没有自动化后悔药。5.2 Session、JSON 协议、jQuery 版本三个运行期深坑现象三登录页输对账号密码点击登录后浏览器立刻又跳回登录页或者列表页的 ajax 请求一直返回 401怎么都进不了系统。原因这套源码的 ashx 大多这样声明public class customer : IHttpHandler。但 ASP.NET 里普通 IHttpHandler 默认不给访问 Session代码里读context.Session[UserId]不报编译错运行时只拿到 null。于是登录成功写了 Session下一个 ajax 请求里 ashx 读 Session 读出来 null判定未登录前端被踢回登录页。这是 WebForms 老项目里最著名的“幽灵 bug”。解决在所有 ashx 类声明上补接口改成public class customer : IHttpHandler, IRequiresSessionState。IRequiresSessionState是标记接口不需要实现任何方法只要类头写上ASP.NET 就允许 handler 读写当前 Session。我拿到老项目的第一件事就是全项目搜IHttpHandler把所有类补上这个接口。补完以后登录跳转和权限校验立刻正常。现象四列表页能打开网格区域却一直转圈不显示数据。F12 看 Network返回的是{Message:列名 xxx 无效}或者Rows是嵌套对象而不是数组。原因多半是改了数据库表结构以后查询 SQL 没跟着改列名对不上报异常异常被 try/catch 或全局 Application_Error 吞掉输出成了错误字符串。另一个原因是 DataTable 被直接塞进匿名对象序列化Rows变成了嵌套的{Columns, Rows}结构ligerGrid 不认。解决按第 2.2 节把 DataTable 转成ListDictionarystring, object确保Rows是数组再把 ashx 的 catch 块统一返回标准 JSON 结构catch (Exception ex) { context.Response.Write( new JavaScriptSerializer().Serialize( new { Rows new object[0], Total 0, Error ex.Message })); }前端拿到Rows: []就不会无限转圈同时可以在 Network 面板看到Error字段排查效率翻倍。现象五把 jQuery 从 1.8.2 升级到 3.x 以后页面渲染正常但翻页、排序、弹窗全部失灵控制台报$.browser is undefined或$.live is not a function。原因ligerUI 2.x 依赖 jQuery 早期 API$.browser用来判断浏览器类型$.live用来做事件委托这两个在 jQuery 2.0 以后被删除。老框架不会自动适配新 jQuery属于兼容断层。解决不要升级 jQuery。源码包自带的 jquery-1.8.2 就是 ligerUI 最稳的组合。如果某个业务模块非用新 jQuery 不可引入 jquery-migrate-1.4.1 把老 API 补回去。但我的建议是保持原样老项目里“稳定”比“新”值钱。真到了非升级不可的地步说明整个前端也该重写了。6. 上线前最后一步会话超时、SQL 注入与部署验证改造完客户模块后别急着把项目丢到生产服务器。我自己的习惯是上线前固定做三件事任何一件都能拦住一次事故。第一检查会话超时。ASP.NET Session 默认 20 分钟业务人员填半天表单回来发现要重新登录体验直接崩。在Web.config里把超时拉长到业务合理范围比如一个工作日system.web sessionState modeInProc timeout480 cookielessfalse / httpRuntime executionTimeout120 maxRequestLength10240 / /system.webInProc模式在单台服务器上够用但如果客户要求“永久在线”的稳定性就得换 StateServer 或 SQLServer否则应用池一回收全员掉线。把 Session 从 InProc 拿出来是这类常驻型 CRM 系统的关键一步。第二把项目里所有拼接 SQL 的地方抓一遍。老代码到处都是string sql SELECT * FROM Customer WHERE Name name 开发环境没事生产环境被注入就脱库。没有精力彻底重构就用最笨的办法全项目搜字符串相加拼 SQL 的写法全部换成 SqlParameter 参数化。我在实操里会改DbHelper.ExecuteDataTable统一入口只收参数化 SQL非参数化语句直接拦截并记日志。宁可功能报错也不能把裸 SQL 放上线。第三部署后的验证。发布到 IIS 后开一个 PowerShell 窗口跑一轮检查Get-Website | Select-Object Name, State, PhysicalPath Test-NetConnection localhost -Port 80 Invoke-WebRequest -Uri http://localhost/login.aspx -UseBasicParsing第一条看站点状态第二条看端口监听第三条确认首页能返回 200。如果第三条返回 500马上开 IIS 日志目录C:\inetpub\logs\FailedReqLogFiles和事件查看器里的应用程序日志把Web.config的customErrors临时设为Off能看到具体哪一行抛异常排完再改回RemoteOnly。这套源码能不能用我的结论是能用但它是“原料”不是“半成品”。你要接受 WebForms 的老架构接受 ligerUI 停留在 jQuery 1.8 的时代然后把精力花在业务字段、权限和数据安全上而不是纠结要不要把前端换成 Vue。等系统跑稳、业务数据积累起来再考虑用 ASP.NET Core 重写接口层前端页面逐步替换。这是我一直坚持的习惯接手老 CRM 源码第一周不写任何新功能只跑通、抓裸 SQL、补 Session 接口。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?