简介:这是一套面向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 的 action=save,也可以走 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?action=list", 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 List<Dictionary<string, object>> DataTableToList(DataTable dt) { var list = new List<Dictionary<string, object>>(); foreach (DataRow row in dt.Rows) { var dict = new Dictionary<string, 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?action=list", // 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 href="javascript:void(0)" onclick="openEdit(' + row.CustomerID + ')">编辑</a>' + ' <a href="javascript:void(0)" onclick="deleteOne(' + 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 Server(Express 够用)、IIS Express,然后打开项目后右键检查目标框架是不是 4.8。
第一次编译见到几个典型错误很正常:找不到AjaxControlToolkit.dll,或者Newtonsoft.Json版本冲突。先看Web.config里有没有assemblyBinding节点,很多老项目没有,需要手动加:
<configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.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.config:
<connectionStrings> <add name="ConnString" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=CRMDB;User ID=sa;Password=你的密码;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>本机开发也可以把User ID/password换成Integrated Security=True。MultipleActiveResultSets=True这一项一定要加,老代码经常在一个连接上同时开着 DataReader 又执行另一条命令,不加会报“连接未关闭”。
3.3 第一次编译的翻车现场:缺失 DLL、目标框架、自定义错误
打开解决方案后直接按 F5 是新手最容易翻车的地方。正确顺序是:先生成解决方案,看错误列表,再处理 bin 目录里缺的 DLL。有些源码包为了体积清空了 bin,这时候要靠 NuGet 装回兼容版本。常见依赖参考:
| 缺失项 | 从哪里补 | 建议版本 |
|---|---|---|
| ligerUI | 源码包 /ligerUI 目录 | 一般自带的就能用 |
| jQuery | /Scripts 目录 | 1.8.2,不要换 3.x |
| Newtonsoft.Json | NuGet | 6.0 到 13.0 都行,注意绑定重定向 |
| AjaxControlToolkit | NuGet | 版本看代码里引用的命名空间 |
如果编译错误集中在System.Web.Extensions 找不到,说明目标框架太低;切到 .NET 4.8 以后这类错基本消失。还有一类错误是代码用了过时 API,编译只给警告不报错,不用管。
编译通过不代表能登录。第一次打开页面看到的往往不是登录框,而是 500。打开浏览器开发者工具看网络请求,把Web.config里的customErrors mode="Off"先改上,刷新再看,真正的异常信息会直接打在页面上。这一步能省掉大半排错时间。
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?action=list,我改造时通常会把拼 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("<script>parent.afterSave(true);</script>"); Response.End(); } }父页面在打开对话框前,必须先把afterSave挂到window上:
window.afterSave = function (success) { if (success) { $.ligerDialog.close(); $("#maingrid").ligerGetGridManager().reload(); } };提示:ligerDialog 打开的子页面默认是 iframe,
afterSave必须挂在父页面的window上,子页面里用parent.afterSave才能调得到。把函数写在$(function(){})里是典型的踩坑写法。
很多新业务方还会提“扫码采集”需求,比如用手机摄像头扫名片自动建客户。老架构里最常见的落地方式是在 CustomerEdit.aspx 单独做一个扫码入口,移动端浏览器调用摄像头识别以后,把结果通过 query string 带回表单并回填,再复用原有保存按钮。不要试图在旧源码里集成重型扫码 SDK,那会把这套薄前端拖垮。
5. 避坑清单:这类老 CRM 源码最容易翻车的五个位置
5.1 数据库脚本乱码和兼容性,属于解压前就要打的预防针
现象一:用 SSMS 打开crm_data.sql,SQL 能看懂但中文全是乱码,“客户名称”显示成“瀹㈡埛鍚嶇О”。第一反应是文件损坏,重新下载还是一样。
原因:老源码的 SQL 脚本保存编码是 GB2312 或 GBK,SSMS 默认按 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 转成List<Dictionary<string, 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 mode="InProc" timeout="480" cookieless="false" /> <httpRuntime executionTimeout="120" maxRequestLength="10240" /> </system.web>InProc模式在单台服务器上够用,但如果客户要求“永久在线”的稳定性,就得换 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 接口。希望帮到你。
本文还有配套的精品资源,点击获取