简介:这是一份基于.NET Core 3.1与EFCore构建的C# CRM客户管理系统完整源码,面向中高级.NET开发人员及需要企业级客户关系管理参考的学员,项目覆盖用户管理、权限管理、营销管理、客户管理与服务管理五大核心模块,权限管理贯穿服务分配、新客户开拓与老客户维护等关键链路,业务边界清晰。压缩包共1087个文件,大小约39.97MB,以dll程序集、png/gif界面资源、js/css前端脚本、cs后端源码及cshtml视图为主,另含4个sql数据库脚本、sln工程文件等,可在VS2019中直接还原。数据库采用SQL2014,整体基于API与MVC分层交互,适合毕业设计、企业内部培训或CRM二次开发参考,目前已有1215人学习下载。从源码中可梳理权限分配与客户服务流转的落地实现,对照sql脚本可快速搭建数据库环境;同时目录结构清晰,dll与缓存等编译产物保留完整,便于直接运行调试并理解前后端交互机制。
1. C# CRM 客户管理系统源码:先决定它值不值得你解压
“C# CRM 客户管理系统源码”这个包,我拆开后的第一反应不是看登录界面,而是先确认一个问题:它能不能接住我手里的业务现状。客户信息散在 Excel、微信群和销售各自的记事本里,主管要查一个客户的跟进历史得挨个问人,这套源码解决的就是这件事。它走的是 C# WinForms 客户端 + SQL Server 2014 数据库的经典组合,解压后能看到解决方案、数据库脚本和界面工程。适合做二次开发的中小团队、正在写 .NET 课程设计的学生,以及想在增删改查之外看一套完整业务流的 C# 开发者。但先说清楚边界:它大概率不是打开浏览器就能用的“永久在线的 CRM 网站”,而是典型 C/S 桌面程序。
如果你手里是想快速搭一个给三五个人用的小系统,这套源码价值很高;如果期待开箱即产、连表结构都不用改,那就得降低预期。下面我从数据库、架构、核心模块三个维度拆,最后给你一份避坑清单。
2. 数据库先行:SQL2014 脚本还原与 12 张核心业务表的外键设计
2.1 解压后先摸清目录,别急着双击 .sln
大多数人的习惯是拿到压缩包直接双击 .sln 编译,然后被一堆报错淹死。我的习惯相反:先打开文件目录,把数据库脚本跑起来再说。原因很简单——这套系统的所有界面,最终都落在 SQL Server 2014 的数据表上,数据起不来,界面跑通了也没意义。
这里说的文件名以实际解压为准,但典型结构通常长这样:
| 文件/目录 | 作用 | 打开方式 |
|---|---|---|
| CrmSystem.sln | 解决方案文件 | Visual Studio 2015 及以上 |
| DB/CrmDb.sql | 建库建表脚本 | SSMS 直接执行 |
| DB/CrmDb.bak | 数据库备份文件 | SSMS 还原 |
| Model/ | 实体类工程 | Visual Studio |
| DAL/ | 数据访问层工程 | Visual Studio |
| UI/ | WinForms 界面层工程 | Visual Studio |
| App.config | 数据库连接串等配置 | 记事本即可 |
先打开App.config看一眼连接串,能省后面很多事。WinForms 项目里连接串通常长这样:
<connectionStrings> <add name="CrmDb" connectionString="Data Source=.;Initial Catalog=CrmDb;User ID=sa;Password=你的密码;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" /> </connectionStrings>Data Source=.表示本机默认实例,Initial Catalog是数据库名,MultipleActiveResultSets=true这个参数建议保留,否则后续在 DataReader 未关闭时再执行另一条查询会报“连接忙于另一命令”。这一行配置,是整包最容易改错的地方,第 5 章我会专门展开。
2.2 数据库还原:脚本优先于 .bak
DB 目录里如果同时有 .sql 和 .bak,我建议先用脚本方式建库。原因很简单:脚本可以反复执行、能看到每张表的字段定义,而且不会受备份文件物理路径影响。下面这段是这类源码里最常见的可重复执行写法:
IF DB_ID(N'CrmDb') IS NULL CREATE DATABASE CrmDb; GO USE CrmDb; GO IF OBJECT_ID(N'dbo.Customer', N'U') IS NULL BEGIN CREATE TABLE dbo.Customer ( CustomerId INT IDENTITY(1,1) PRIMARY KEY, CustomerName NVARCHAR(80) NOT NULL, ContactName NVARCHAR(50) NULL, Phone NVARCHAR(30) NULL, Area NVARCHAR(100) NULL, Source NVARCHAR(30) NULL, Level NVARCHAR(20) NULL, Creator NVARCHAR(30) NULL, CreateTime DATETIME NULL DEFAULT(GETDATE()) ); END GOIF OBJECT_ID(...) IS NULL是幂等建表,脚本跑第二遍不会报“对象已存在”。NVARCHAR不是随手写的——它按 Unicode 存储,中文、生僻字、繁体字都能存住,CRM 里客户姓名和公司名经常有生僻字,用VARCHAR迟早出乱码。这段脚本跑完后,CustomerId自增主键就位,后面所有客户关联表都拿它做外键。
如果 DB 里只有 .bak,那就用还原方式:
RESTORE DATABASE CrmDb FROM DISK = N'D:\DB\CrmDb.bak' WITH REPLACE, MOVE N'CrmDb' TO N'D:\Data\CrmDb.mdf', MOVE N'CrmDb_log' TO N'D:\Data\CrmDb_log.ldf';REPLACE允许覆盖同名数据库,MOVE负责把备份里的逻辑文件名映射到你机器上的物理路径。很多人还原失败,就是少了MOVE这一段,源机器的数据文件路径和你本机不一样,SQL Server 找不到地方放文件。还原完建议立刻执行一次SELECT * FROM dbo.Customer,确认表里有初始化数据。
2.3 核心业务表:客户、跟进、用户三张表串起主流程
这套源码的表数量一般在 12 张上下,但真正跑通业务流程的是三张:SysUser(用户)、Customer(客户)、FollowRecord(跟进记录)。其余像Contract(合同)、SysRole(角色)、LoginLog(登录日志)都是外围支撑。典型表清单如下:
| 表名 | 职责 | 关键字段 |
|---|---|---|
| SysUser | 系统用户 | UserId, UserName, PwdHash, RealName, RoleId, IsLocked |
| SysRole | 角色 | RoleId, RoleName |
| Customer | 客户主表 | CustomerId, CustomerName, ContactName, Phone, Area, Source, Level, Creator |
| FollowRecord | 跟进记录 | FollowId, CustomerId, FollowType, FollowContent, NextTime, Creator |
| Contract | 合同登记 | ContractId, CustomerId, ContractNo, Amount, SignDate |
| LoginLog | 登录日志 | LogId, UserName, LoginTime, LoginIP |
跟进记录表是最容易在设计上翻车的。有的源码把跟进内容直接塞进Customer表里加一个LastFollowContent字段,那等于只能存最后一次,历史全丢。正确做法是独立一张表,用外键挂到客户上:
CREATE TABLE dbo.FollowRecord ( FollowId INT IDENTITY(1,1) PRIMARY KEY, CustomerId INT NOT NULL, FollowType NVARCHAR(20) NULL, -- 电话/拜访/微信 FollowContent NVARCHAR(500) NULL, NextTime DATETIME NULL, -- 下次跟进时间 Creator NVARCHAR(30) NULL, CreateTime DATETIME NULL DEFAULT(GETDATE()), CONSTRAINT FK_Follow_Customer FOREIGN KEY(CustomerId) REFERENCES dbo.Customer(CustomerId) );外键的意义不只是约束,它让“查一个客户的完整跟进历史”变得顺理成章:
SELECT f.FollowType, f.FollowContent, f.NextTime, f.Creator, f.CreateTime FROM FollowRecord f WHERE f.CustomerId = 1 ORDER BY f.CreateTime DESC;这套源码如果表设计合理,CustomerId上一定会有索引,否则数据量到几万条时这条查询会明显变慢。你拿到包后可以顺手检查一下FollowRecord表有没有IX_Follow_CustomerId索引,没有就自己补上,这是后文第 6 章会讲到的优化点。
3. 三层架构怎么用:SqlHelper、实体模型与参数化调用的完整链路
3.1 为什么 WinForms 项目仍然值得用三层
这套源码大概率是按Model(实体)、DAL(数据访问)、UI(界面)三层组织的。现在流行各种新框架,但 C/S 桌面客户管理系统里,三层架构依然是性价比最高的选择。原因不复杂:界面层只负责显示和收集输入,业务逻辑在中间,数据操作在最底下,哪一层出了问题都不用牵一发动全身。
我拆过不少 CRM 源码,发现一个规律:凡是把SqlConnection、SqlCommand直接写在窗体按钮事件里的,后期维护一定痛苦。改一个字段名,满屏报错。而三层结构下,UI 层只认Customer实体,不知道 SQL 长什么样;DAL 层只知道怎么存取数据,不关心数据在界面上怎么展示。这套源码你拿到手,先看命名空间是不是这个套路:
CrmSystem.Model -> 实体类 CrmSystem.DAL -> SqlHelper + 各业务数据类 CrmSystem.UI -> 窗体 + 控件逻辑如果是这样,后面做二次开发会轻松很多。如果发现 UI 层里直接拼 SQL,那你就得做好心理准备,改起来会费点劲。
3.2 SqlHelper:整个 DAL 的地基
不管源码拆成多少层,最底下通常都有一个SqlHelper静态类,封装连接、执行、查询这些重复动作。它做得好的话,DAL 层每个方法只需要写 SQL 和参数。典型实现长这样:
public static class SqlHelper { private static readonly string connStr = ConfigurationManager.ConnectionStrings["CrmDb"].ConnectionString; public static DataTable ExecuteDataTable(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) { using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); SqlDataAdapter adapter = new SqlDataAdapter(cmd); DataTable table = new DataTable(); adapter.Fill(table); return table; } } } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) { using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } } }这段代码的核心是using块——连接和命令对象用完后自动释放,不用手写conn.Close()。忘关连接是桌面程序最常见的资源泄漏点,跑一两天后客户端越用越卡,就是连接没回收。params SqlParameter[]这个设计让调用方可以按需传任意数量的参数,DAL 层写起来很简洁。
参数化是这里最重要的一环。不拼字符串、只用@参数名占位,能挡掉 SQL 注入攻击。客户名字里如果带个单引号,比如“O'Neil”,直接拼 SQL 会把语句炸掉,参数化则完全没这个问题。拿到源码后先检查 DAL 层里有没有人用string.Format拼 SQL,只要有,优先改成参数化。
3.3 实体模型与参数化插入:字段变更时的后悔药
Model层的实体类,是对应数据库表的内存映射。比如Customer实体,字段应该和表结构一一对应:
public class Customer { public int CustomerId { get; set; } public string CustomerName { get; set; } public string ContactName { get; set; } public string Phone { get; set; } public string Area { get; set; } public string Source { get; set; } public string Level { get; set; } public string Creator { get; set; } public DateTime CreateTime { get; set; } }有了实体后,插入客户的核心代码就变成参数化调用:
public int InsertCustomer(Customer c) { string sql = @"INSERT INTO Customer (CustomerName, ContactName, Phone, Area, Source, Level, Creator, CreateTime) VALUES (@name, @contact, @phone, @area, @source, @level, @creator, GETDATE()); SELECT SCOPE_IDENTITY();"; object result = SqlHelper.ExecuteScalar(sql, new SqlParameter("@name", c.CustomerName), new SqlParameter("@contact", c.ContactName), new SqlParameter("@phone", c.Phone), new SqlParameter("@area", c.Area), new SqlParameter("@source", c.Source), new SqlParameter("@level", c.Level), new SqlParameter("@creator", c.Creator)); return Convert.ToInt32(result); }注意这里用的是SCOPE_IDENTITY()而不是@@IDENTITY。这是血泪经验换来的——@@IDENTITY返回的是当前会话最后生成的标识值,如果表上有触发器,它返回的可能是触发器里那张表的 ID,而不是你插入的CustomerId。SCOPE_IDENTITY()限定在当前作用域内,才是你真正想要的自增值。
这里我一般会留个心眼:如果实体类字段和数据库表字段对不上,运行期才会报列名无效。建议每次改完表结构,顺手刷新实体类,别等到编译过了、运行到插入时才翻车。
4. 登录与客户跟进:这套源码里最值得抄的两个模块
4.1 登录鉴权:MD5 哈希只是及格线
登录模块是每套 CRM 都有的模块,但实现质量差别很大。差的写法是把密码明文存数据库,登录时直接WHERE UserName='xx' AND Password='xx'。这套源码如果稍微有点底线,会用哈希存储。常见做法是 MD5 加盐:
public static string Md5Hash(string input) { using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) { byte[] bytes = Encoding.UTF8.GetBytes(input); byte[] hash = md5.ComputeHash(bytes); StringBuilder sb = new StringBuilder(32); foreach (byte b in hash) { sb.Append(b.ToString("x2")); } return sb.ToString(); } } public SysUser Login(string userName, string password) { // 实际项目中 salt 通常存在用户表里,每个用户一个随机值 string salt = "Crm2014"; string pwdHash = Md5Hash(password + salt); string sql = @"SELECT UserId, UserName, RealName, RoleId FROM SysUser WHERE UserName=@name AND PwdHash=@pwd AND IsLocked=0"; DataTable dt = SqlHelper.ExecuteDataTable(sql, new SqlParameter("@name", userName), new SqlParameter("@pwd", pwdHash)); if (dt.Rows.Count == 1) { return new SysUser { UserId = Convert.ToInt32(dt.Rows[0]["UserId"]), UserName = dt.Rows[0]["UserName"].ToString(), RealName = dt.Rows[0]["RealName"].ToString(), RoleId = Convert.ToInt32(dt.Rows[0]["RoleId"]) }; } return null; }从安全角度讲,MD5 已经不算强哈希,攻破成本很低。但现实是存量系统里大量代码还是 MD5,因为它兼容性好、性能高、改造成本低。如果你要二次开发,至少做到加盐——盐值不要写死在代码里,放用户表单独字段,每个用户随机生成。更稳妥的方案是升级成 PBKDF2 或 BCrypt,但那需要引入额外算法库,看你的改造意愿。
登录方法里IsLocked=0这个条件容易被忽略。它表示被锁定的用户不允许登录,这是账号管理的基本操作。很多初版源码没有这个字段,导致离职员工账号还能进系统,这属于安全隐患,建议保留。
4.2 客户跟进:UI 线程与数据库写入的闭环
跟进记录是整个 CRM 使用频率最高的功能。销售每次打完电话、拜访完客户,都要往FollowRecord表里写一条。这个模块的核心在保存按钮事件里:
private void btnSaveFollow_Click(object sender, EventArgs e) { // UI 层的输入校验,能挡住八成无效提交 if (string.IsNullOrWhiteSpace(txtContent.Text)) { MessageBox.Show("跟进内容不能为空"); return; } string sql = @"INSERT INTO FollowRecord (CustomerId, FollowType, FollowContent, NextTime, Creator, CreateTime) VALUES (@customerId, @followType, @content, @nextTime, @creator, GETDATE())"; int rows = SqlHelper.ExecuteNonQuery(sql, new SqlParameter("@customerId", currentCustomerId), new SqlParameter("@followType", cboType.SelectedItem.ToString()), new SqlParameter("@content", txtContent.Text.Trim()), new SqlParameter("@nextTime", dtpNext.Value), new SqlParameter("@creator", LoginUser.UserName)); if (rows > 0) { RefreshFollowGrid(); // 重新加载 DataGridView MessageBox.Show("跟进已保存"); } }ExecuteNonQuery返回受影响行数,大于 0 才提示成功。txtContent.Text.Trim()去掉了首尾空格,避免存进去一堆空白字符。dtpNext.Value是下次跟进时间,这是销售最常看的字段——今天该跟谁,全靠它排序。
注意保存按钮事件里的RefreshFollowGrid(),它负责重新查询并绑定 DataGridView。很多新人在这里会犯一个错:直接用dataGridView1.Rows.Add()往表格里加行,结果和数据库脱节。正确姿势是重新跑一次查询,拿全新的 DataTable 赋值给DataSource。这套源码如果事件命名规范,你会看到所有增删改完成后都走同一套刷新逻辑,这个习惯值得抄。
4.3 客户统计:GROUP BY 之后如何安全绑定表格
统计模块是给管理层看的。按来源分组统计客户数量,是最典型的需求:
SELECT Source, COUNT(*) AS CustomerCount FROM Customer WHERE CreateTime >= @beginDate AND CreateTime <= @endDate GROUP BY Source ORDER BY CustomerCount DESC;接到 UI 层时,直接把结果 DataTable 绑定到 DataGridView 就行。但要注意两点。第一,分组查询结果的列名是CustomerCount,不是Count,绑定前确认列名匹配。第二,如果Source字段允许为空,GROUP BY会把空值归成一组,显示成空行,体验很差,建议查询时加ISNULL(Source, '未知来源')。
这个模块的代码量不大,但它是管理层判定系统“有没有用”的关键。客户分布、跟进次数、合同金额这几张报表如果跑得顺,系统落地率会高很多。源码包里如果带了报表查询,优先看它写没写时间范围参数——很多 MVP 版本只做全量统计,数据一多就卡死。
5. 避坑手册:编译失败、连接串和中文乱码的 5 条踩坑记录
这 5 条是我拆类似 C# 源码包时反复见过的翻车现场,每一条都按“现象 → 原因 → 解决”写清楚,拿去对照排查。
5.1 还原数据库报“无法打开物理文件,拒绝访问”
- 现象:SSMS 执行
RESTORE DATABASE后报错,提示无法打开物理文件CrmDb.mdf,后面跟着“操作系统错误 5(拒绝访问)”。 - 原因:SQL Server 服务账号对目标目录没有写入权限。Windows 下 SQL Server 默认以
MSSQLSERVER服务账号运行,它要往 D 盘写 mdf 文件,但 D 盘目录权限没给它。 - 解决:先在 D 盘建好
Data目录,右键目录 → 属性 → 安全 → 编辑 → 添加MSSQLSERVER或NETWORK SERVICE账号,给完全控制权限。然后重新执行还原语句。图形界面里就是在“还原数据库”弹窗里指定目标路径,一样要保证该目录权限够。
5.2 客户端能启动,但登录时报“用户 'sa' 登录失败”
- 现象:程序编译没问题,窗体也能弹出来,点登录就报 “用户 'sa' 登录失败” 或 “无法连接到 SQL Server”。
- 原因:SQL Server 装的时候可能只开了 Windows 身份验证模式,
sa账号根本没法用;也可能是App.config里的密码和实际sa密码不一致。这是 C/S 程序最常见的连接问题,80% 都是连接串配置不对,不是代码问题。 - 解决:先确认 SQL Server 的认证模式。用 SSMS 以 Windows 身份登录服务器,右键服务器 → 属性 → 安全性 → 选择“SQL Server 和 Windows 身份验证模式”。然后执行下面这段脚本开启混合认证:
EXEC xp_instance_regwrite N'HKEY_LOCAL_MACHINE', N'SOFTWARE\Microsoft\MSSQLServer\MSSQLServer', N'LoginMode', REG_DWORD, 2;记得重启 SQL Server 服务。接着设置sa密码,再到App.config里把Password改成一致。这里有个习惯值得养成:改完连接串必须重新编译整个解决方案,不是保存就生效。
5.3 编译报错“目标框架不受支持”或“需要 .NET Framework 4.x”
- 现象:用新版本 Visual Studio 打开解决方案,编译时提示某个项目需要更高的 .NET Framework 版本,或者直接提示“不受支持的目标框架”。
- 原因:源码工程写的目标框架和你本机安装的运行时不一致。老一点的项目可能定在
.NET Framework 4.0,新项目可能要求4.6以上,VS 默认安装的组件对不上就会报这个错。 - 解决:右键报错项目 → 属性 → 应用程序 → 目标框架,改为本机已装的版本。改完重新生成解决方案。注意如果源码里用了 C# 6.0 语法(比如
?.空条件运算符),目标框架至少得是 4.6,否则语法检查过不去,这时候就得装对应 Developer Pack。
5.4 数据库里中文正常,界面却显示“????”
- 现象:直接用 SSMS 查询表数据,中文完全正常。但客户端界面上显示客户名全是问号,或者保存后再查变成问号。
- 原因:建表 SQL 脚本里字段用了
VARCHAR而不是NVARCHAR,中文在代码页转换时丢字符。另一种可能更隐蔽——.sql脚本文件本身是 ANSI 编码,里面有中文默认值或注释,执行时编码没对上,写入数据库就已经乱了。 - 解决:表字段统一用
NVARCHAR,这个在建表阶段定下来,后面改成本高。已经建错的表,用ALTER TABLE改列类型再更新数据。脚本文件用记事本打开后另存为“Unicode (UTF-8 带签名)”格式,再重新执行一遍初始化脚本。
5.5 改了 DAL 层代码,运行起来界面还是老行为
- 现象:明明在
InsertCustomer方法里改了逻辑,F5 调试跑起来,操作界面还是旧效果,像是改了另一个项目的代码。 - 原因:解决方案里多个项目没有一起重新生成,UI 项目引用的 DLL 还是旧的。VS 默认“启动项目”只编译自身,不会强制重新编译所有依赖项目。bin 目录里的旧 DLL 被占用时,生成失败也会出现这种情况。
- 解决:菜单栏“生成 → 重新生成解决方案”,如果报 DLL 被占用,关掉正在运行的程序再重新生成。养成习惯:改任何底层代码后,先重新生成整个解决方案,再 F5,免得对着旧程序集调试半小时。
6. 跑通之后:从本机到局域网多用户部署的验证清单
单机跑通只算完成一半。真正在公司多人用的场景下,局域网部署才是验收环节。我的习惯是跑通后按下面四步走一遍验证,每步都能挡住一类实际问题。
第一步,做全功能回归。用测试账号把“新增客户 → 写跟进 → 查统计 → 退出登录”完整走一遍,确认流程闭环。不要只登录看个界面就认为部署完成,CRM 的核心价值在数据流转,不在登录页。
第二步,建独立数据库账号,别用sa跑业务。给客户端一个最小权限账号,避免程序连接串泄露导致管理员密码暴露。常见做法是:
CREATE LOGIN crm_app WITH PASSWORD = N'Crm@2024'; CREATE USER crm_app FOR LOGIN crm_app; ALTER ROLE db_datareader ADD MEMBER crm_app; ALTER ROLE db_datawriter ADD MEMBER crm_app;db_datareader和db_datawriter组合起来够日常增删改查用。但注意,如果系统首次运行需要自动建表,这俩角色是做不到的,部署阶段先用管理员账号完成初始化,再把连接串切到应用账号。
第三步,改连接串指向服务器 IP,并放行防火墙端口:
netsh advfirewall firewall add rule name="SQL1433" dir=in action=allow protocol=TCP localport=1433这是 Windows 防火墙放行 1433 端口的命令,需要管理员权限。连接串从Data Source=.改成:
<add name="CrmDb" connectionString="Data Source=192.168.1.10,1433;Initial Catalog=CrmDb;User ID=crm_app;Password=Crm@2024;MultipleActiveResultSets=true" providerName="System.Data.SqlClient"/>改完用客户机上跑一遍登录,能通过才算部署成功。SQL Server 默认实例通常监听 1433,如果改过端口,这里要跟着改。
第四步,用活动监视器看慢查询。多人同时操作后,Customer表和FollowRecord表的数据量会快速增长。打开 SSMS 的“活动监视器”,或者直接查 SQL Server 的动态管理视图:
SELECT TOP 10 total_elapsed_time / execution_count AS AvgMs, text FROM sys.dm_exec_query_stats CROSS APPLY sys.dm_exec_sql_text(sql_handle) ORDER BY total_elapsed_time DESC;看到哪条查询平均耗时超过几百毫秒,就给它的 WHERE 字段补索引。跟进记录表最常见的性能瓶颈在CustomerId外键上,按前文说的补一个非聚集索引即可:
CREATE NONCLUSTERED INDEX IX_Follow_CustomerId ON dbo.FollowRecord(CustomerId);从那以后我每次拿到新的源码包,都强制自己先跑数据库脚本、再改连接串、最后才动业务代码。这个顺序帮我挡掉了至少一半的无意义报错。这次拆这套 C# CRM 客户管理系统源码也是一样,数据库、架构、核心模块都过了一遍,剩下的就是在你真实业务里把字段补齐、报表调好,希望帮到你。
本文还有配套的精品资源,点击获取