简介这是一份基于ASP.NET(C#)技术的客户关系管理系统CRM源码适合学习B/S开发与业务系统的程序员、在校学生使用。项目围绕客户信息录入、编辑修改、服务派工、流失反馈等典型功能展开通过阅读页面文件、后台业务类和数据访问代码可快速理解Web表单事件驱动与分层开发思路。压缩包共297个文件其中含100个C#源文件、38个aspx页面、32个dll以及gif/jpg图标、CSS脚本与数据库文件等整体大小仅1.15MB目录层次明确便于按模块检索学习。目前已有439人学习下载利用源码可快速搭建可运行的CRM原型适合课程设计、毕业设计或二次开发参考。1. ASP.NET(C#) CRM 是什么销售数据不该在 Excel 里烂掉我见过太多销售团队客户名单在 Excel 里躺了三年同一个客户被三个销售同时跟谁都没跟成。后来用 ASP.NET(C#) 搭了一套内部的客户关系管理系统CRM把客户、联系人、跟进记录、商机阶段全部收进系统里Excel 再也没人维护。这套东西不复杂核心就是几张表加一组增删改查但它解决的是客户归属、跟进沉淀和商机流失的问题。适合做这类系统的是已经会一点 C# 和 SQL、想在公司内部落地一套正经 CRM 的 .NET 开发。本文按我实际的开发顺序讲选型、建表、编码、部署、排错最后补几个让系统更好用的技巧。2. 选型与架构从 Web Forms 到 MVC三层架构怎么切CRM 项目拿到手最忌讳一上来就建表写代码。先花半天时间把技术栈和项目结构定下来否则后面补权限、补日志的时候改起来会非常痛苦。下面说的是我做 asp.net(c#) 类管理系统时的惯用选型思路同时覆盖老项目维护的场景。2.1 Web Forms 与 MVC 怎么选先看团队再谈技术ASP.NET 体系下做 CRM主要有三条路Web Forms、ASP.NET MVC 5、ASP.NET Core MVC。老运维系统里 Web Forms 存量很大拖控件绑定数据源开发速度确实快但它把页面渲染和事件处理耦合在一起后面要给 CRM 加移动端扫码页或者做前端调整时改动成本会成倍增加。MVC 5 的 View 和 Controller 分离适合多人分工这也是“asp.net c# CRM”这个检索词下最常见的一组示例代码的技术底座。ASP.NET Core MVC 在跨平台和性能上有优势但很多公司内网服务器还是 Windows Server 加 .NET Framework 环境直接升 Core 要考虑迁移成本和第三方组件兼容性。如果是在新环境、新团队从零开始我一般会直接选 ASP.NET Core MVC如果是给已有系统做二次开发或者维护老代码就继续用 MVC 5 或 Web Forms不要为了新而新。下面这个表可以拿来给项目相关方讲清楚为什么这样选。对比项Web FormsMVC 5 (.NET Framework 4.8)ASP.NET Core MVC页面模型服务端控件视图状态Razor 视图路由Razor 视图路由前端介入弱控件生成的 HTML 难调整强视图里可直接写 HTML/JS强更适合前后端分离测试难度难做单元测试Controller 可以单测支持依赖注入单测方便部署环境Windows/IISWindows/IISWindows/Linux/容器适合场景老企业内部系统现有 Framework 环境新模块新项目、考虑长期演进选型不是越新越好。我之前见过一个团队把老 Web Forms 项目强行迁到 Core结果第三方报表控件不兼容迁移花的时间比重写还长。要是你也遇到这种环境老实留在 MVC 5 并不丢人CRM 真正值钱的是里面的客户数据和业务规则不是用了哪个版本。2.2 CRM 数据库设计从客户到商机的 5 张核心表CRM 再怎么包装核心数据模型跳不出这几类用户、客户、联系人、跟进记录、商机。我第一次做的时候设计了二十多张表后来砍到核心五张再加上几张扩展表反而更好维护。以 SQL Server 为例先看客户和跟进记录两张表CREATE TABLE [dbo].[Customer] ( Id INT IDENTITY(1,1) PRIMARY KEY, -- 客户主键 CustomerName NVARCHAR(100) NOT NULL, -- 客户公司名 Phone NVARCHAR(50) NULL, -- 联系电话 Source NVARCHAR(20) NULL, -- 来源展会/网络/转介绍 OwnerId INT NOT NULL, -- 归属销售用户ID Status TINYINT NOT NULL DEFAULT(1), -- 1启用 0停用(用于软删除) CreatedTime DATETIME NOT NULL DEFAULT(GETDATE()), UpdatedTime DATETIME NOT NULL DEFAULT(GETDATE()) ); CREATE TABLE [dbo].[FollowUp] ( Id INT IDENTITY(1,1) PRIMARY KEY, CustomerId INT NOT NULL, -- 关联客户 UserId INT NOT NULL, -- 跟进人 Content NVARCHAR(500) NOT NULL, -- 跟进内容 FollowTime DATETIME NOT NULL DEFAULT(GETDATE()) );这里有几个选型细节容易踩坑。第一公司名用 NVARCHAR 而不是 VARCHAR因为 SQL Server 里的 VARCHAR 遇到中文和排序规则不对的库容易出乱码NVARCHAR 能统一处理 Unicode。第二状态字段用 TINYINT 存 0 和 1不要用字符的“Y/N”代码里判断更直接也便于以后扩展冻结、合并状态。第三客户删除要走软删除即把 Status 置 0而不是直接 DELETE否则销售好不容易积累的跟进记录就彻底找不回来了这也是给数据留后悔药。联系人表和商机表按同样的思路设计Contact 表挂 CustomerIdOpportunity 表挂 CustomerId、Stage、Amount、预计成交时间。要注意给 Customer 表的 OwnerId 建索引因为销售列表页最常用的查询条件就是“当前登录用户 ID 状态”这个索引能直接决定列表页在数据量大之后还跑不跑得动。2.3 三层架构在 CRM 里的切分边界项目结构我通常按四个工程组织Crm.Models -- 实体类对应数据库表 Crm.DAL -- 数据访问层SqlHelper 各Repository Crm.BLL -- 业务层处理状态机、权限校验、防重复 Crm.Web -- Controller、View、Model绑定实体类示例public class Customer { public int Id { get; set; } public string CustomerName { get; set; } public string Phone { get; set; } public string Source { get; set; } public int OwnerId { get; set; } public byte Status { get; set; } public DateTime UpdatedTime { get; set; } }边界就一条DAL 只做 SQL 和实体映射BLL 只做业务判断Controller 只做参数接收和页面装配。新手最容易犯的毛病是在 Controller 里直接写 SQL或者把状态判断散落在多个 ActionResult 里短期看着快后面加一条“商机不能从成交状态再改回报价”的规则时就要满项目找代码。三层不是为了让代码变多而是给规则一个固定的家。对于 c# 入门阶段的开发者照这个结构从客户模块写到商机模块基本就能掌握一套可以复用的内部系统骨架。还有一点要提醒不要为了架构而架构CRM 这种体量用三四个工程是常态不要引入微服务、消息队列这些重东西除非后面真有对接第三方系统的需求。存储过程也建议少用把业务规则写在 C# 里比分散在数据库里更容易做单元测试和版本管理。3. 客户与商机模块落地分页查询、跟进记录与阶段状态机确认好结构和表之后按客户管理、跟进记录、商机三个阶段把模块一个个打通。这一章给出可以直接抄到项目里的核心实现覆盖最常见的销售场景。3.1 客户列表的分页与搜索参数化查询的正确姿势客户列表是每个 CRM 的首页级功能销售要看自己名下的客户、要按公司名搜索、要翻页。常见的错误做法是直接拼接 SQL然后小数据量时没感觉数据量一上来要么慢要么存在注入风险。下面这段 DAL 方法把列表和总数放在同一个批次返回避免查两次数据库public ListCustomer GetPage(int pageIndex, int pageSize, string keyword, int ownerId, out int total) { var list new ListCustomer(); var sql SELECT Id, CustomerName, Phone, OwnerId FROM Customer WHERE OwnerId OwnerId AND Status 1 AND (Keyword OR CustomerName LIKE % Keyword %) ORDER BY Id DESC OFFSET Offset ROWS FETCH NEXT PageSize ROWS ONLY; SELECT COUNT(*) FROM Customer WHERE OwnerId OwnerId AND Status 1 AND (Keyword OR CustomerName LIKE % Keyword %); ; using (var conn new SqlConnection(_connStr)) using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(OwnerId, ownerId); cmd.Parameters.AddWithValue(Keyword, string.IsNullOrWhiteSpace(keyword) ? : keyword); cmd.Parameters.AddWithValue(Offset, (pageIndex - 1) * pageSize); cmd.Parameters.AddWithValue(PageSize, pageSize); conn.Open(); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new Customer { Id reader.GetInt32(0), CustomerName reader.GetString(1), Phone reader.IsDBNull(2) ? : reader.GetString(2), OwnerId reader.GetInt32(3) }); } if (reader.NextResult() reader.Read()) { total reader.GetInt32(0); } else { total 0; } } } return list; }这段代码里有几个参数值得说明。OwnerId 用来限定数据归属销售只能看自己的客户这是 CRM 权限最小单元的体现。Keyword 为空时用 OR 条件放行全表搜索避免在 C# 里拼出两条 SQL。Offset 和 PageSize 直接配合 OFFSET/FETCH 分页SQL Server 2012 以上版本都支持。参数化查询不仅防注入还能让 SQL Server 缓存执行计划数据量上来之后比字符串拼接可靠得多。补充一点AddWithValue 在传 NVARCHAR 参数时存在类型推断问题字段是 NVARCHAR(100) 时更稳妥的写法是cmd.Parameters.Add(Keyword, SqlDbType.NVarChar, 100).Value keyword避免隐式转换导致索引失效。这个细节在数据量过百万之后影响很大。Controller 对应的调用代码[Authorize] public ActionResult Index(string keyword, int page 1) { int pageSize 20; var currentUser (UserIdentity)HttpContext.User.Identity; var list _customerBll.GetPage(page, pageSize, keyword, currentUser.UserId, out int total); ViewBag.Total total; ViewBag.PageIndex page; ViewBag.Keyword keyword; return View(list); }注意视图里分页链接必须带上 keyword 参数否则搜索后翻到第二页条件就丢了。这个问题会放在第 5 章展开讲是列表页最容易忽略的翻车点。3.2 跟进记录时间线、必填校验与防重复跟进记录是 CRM 里内容最杂的部分销售每天填的跟进要能按客户串成时间线。表结构上面已经给了关键是业务层要把三件事管住内容不能为空长度要限制同一人同一客户在同一自然天不能重复提交同样的跟进。校验放在业务层public bool AddFollowUp(int customerId, int userId, string content) { if (string.IsNullOrWhiteSpace(content)) throw new ApplicationException(跟进内容不能为空); if (content.Length 500) throw new ApplicationException(跟进内容不能超过500字); var today DateTime.Today; var sql SELECT COUNT(*) FROM FollowUp WHERE CustomerId CustomerId AND UserId UserId AND FollowTime StartTime AND FollowTime EndTime AND Content Content; // 当前用户当天是否已有相同内容有则报错 // 校验通过后再执行INSERT插入新记录 }这里先查 COUNT 再 INSERT 属于业务层防重严格意义上存在并发窗口但内部 CRM 的场景下先查后插已经能挡住九成误操作没必要为了极端并发把代码搞复杂。如果后面真遇到两个销售同时提交相同跟进可以考虑给 FollowUp 表增加联合索引或者在 SQL 层做原子写入。跟进记录的展示一般是按客户倒序排列时间线页面上要显示用户名、时间、内容三要素。运营上容易忽略的是时间字段必须默认取服务器当前时间不要交给前端传否则终端时钟不准会导致时间线错乱。从第一天就给 FollowUp 加上默认时间值后面做周报、月报统计的时候按天分组直接用。3.3 商机阶段状态机用枚举管住销售漏斗商机在 CRM 里是价值密度最高的数据它要经历“潜在客户 → 意向确认 → 报价中 → 商务谈判 → 成交”也可能在任何一步变成“丢单”。如果不做状态约束业务上就会出现“成交之后再改回报价”这种诡异数据。C# 的枚举加一个迁移方法就能把状态机收住public enum OpportunityStage { Lead 0, // 潜在客户 Qualified 1, // 意向确认 Quotation 2, // 报价中 Negotiation 3, // 商务谈判 Won 4, // 成交 Lost 5 // 丢单 } public static class OpportunityFlow { public static bool CanTransit(OpportunityStage from, OpportunityStage to) { if (to OpportunityStage.Lost) return true; // 任何阶段都可以丢单丢单必须填原因 switch (from) { case OpportunityStage.Lead: return to OpportunityStage.Qualified; case OpportunityStage.Qualified: return to OpportunityStage.Quotation; case OpportunityStage.Quotation: return to OpportunityStage.Negotiation; case OpportunityStage.Negotiation: return to OpportunityStage.Won; default: return false; // 成交/丢单是终态不能再迁移 } } }这套状态机的核心不是代码量而是让所有人对“什么阶段可以走到什么阶段”有统一认知。调用侧在业务层的 UpdateStage 方法里先判断 CanTransit不允许就直接抛异常保证页面和接口都走同一套规则。丢单单独允许从任意状态进入但要同时写入丢单原因字段否则销售会为了图省事直接丢单不填原因月底复盘丢单原因时全是空数据。商机的金额、预计成交日期这些业务字段应当放在 Opportunity 表而不是跟进记录里不要把关键业务字段塞进 Content 字符串否则后面做销售漏斗统计时会非常痛苦。状态机加独立金额字段这两件事能支撑起 CRM 里最核心的销售漏斗报表。4. 登录、权限与部署把 CRM 变成真正在线的内部系统单个模块跑通只完成一半。CRM 要真正用起来必须让销售在浏览器里安全登录、各角色只能看到各自的数据最后部署到服务器让整个办公室都能访问。这一章讲权限与上线的落地配置。4.1 登录与表单认证Cookie 票据里应该存什么ASP.NET 体系下最常见的是 Forms 认证登录成功后把用户信息加密进 Cookie。很多新手把 UserId 和 RoleCode 直接用明文写到 Cookie这会成为数据泄露的口子。正确做法是用 FormsAuthenticationTicket 生成加密票据[HttpPost] public ActionResult Login(LoginViewModel model) { var user _userBll.ValidateUser(model.UserName, model.Password); if (user null) { ModelState.AddModelError(, 用户名或密码错误); return View(model); } var ticket new FormsAuthenticationTicket( 1, user.UserName, DateTime.Now, DateTime.Now.AddHours(8), true, user.RoleCode | user.Id); var cookie new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket)); Response.Cookies.Add(cookie); return RedirectToAction(Index, Customer); }票据的 userData 部分我习惯用“角色|用户ID”的方式拼起来登录后从 HttpContext.User.Identity 里解析时用 Split(|) 取出避免每次操作都查一次数据库。注意过期时间这里写的是 AddHours(8)后面部署时要和 web.config 里的 timeout 保持一致否则就会出现“登录一小时就被踢出来”的高频事故。用户密码不要明文存数据库至少做一次加盐哈希ASP.NET 自带的 SimpleCrypto 或 BCrypt 都可以这一步没有偷懒的余地。4.2 基于角色的授权Sales、Manager、Admin 三层权限小团队 CRM 不需要复杂的权限引擎三种角色基本够用Sales 只能看自己名下客户Manager 能看本组所有客户和跟进Admin 负责用户、字典和系统配置。web.config 里的认证方式配置authentication modeForms forms loginUrl~/Account/Login timeout60 slidingExpirationtrue / /authenticationController 上的授权声明[Authorize(Roles Admin,Manager)] public ActionResult AllCustomer() { var customers _customerBll.GetAll(); return View(customers); }角色名必须以数据库 User 表里的 RoleCode 为准别用中文显示名当角色标识否则角色一改名权限就失效。管理员账号的创建要单独写在部署文档里第一次上线时先用 SQL 直接插入一个 Admin 用户再通过该用户去后台建其他账号。授权粒度上Controller 级用 [Authorize(Roles...)] 管页面数据级归属用 OwnerId 管两者缺一不可。只有页面级权限但没有数据归属过滤销售换个 URL 就能看到全部客户只有数据过滤但没有页面授权普通销售能打开管理后台。这两个边界都检查一遍权限体系才算真正闭环。4.3 IIS 部署与局域网访问发布、连接串与防火墙检查把 CRM 部署给公司内部人员使用IIS 是最常见做法。发布流程不复杂在 Visual Studio 里对 Web 项目执行“发布”选择文件夹模式输出到服务器目录然后在 IIS 里建网站、指向该目录、应用程序池选 .NET v4.0 集成模式。关键检查点是连接字符串web.config 里的 Server 要和 SQL Server 所在机器对应connectionStrings add nameCrmDB connectionStringServer192.168.1.10;DatabaseCrmDB;User Idcrm_app;Password******; providerNameSystem.Data.SqlClient / /connectionStrings连接串有两个必须注意的坑不要用 sa 账号给应用单独建 crm_app 账号并只授予该数据库的读写权限避免应用被注入后直接拿到整个数据库实例Server 值在开发环境写 localhost部署时要改成数据库服务器 IP忘了改会报“用户登录失败”或“在与 SQL Server 建立连接时出现与网络相关的错误”这个报错在刚发布时出现频率极高。局域网访问还要做两步Windows 防火墙在“入站规则”中放行 TCP 端口比如 8086IIS 网站绑定里不要用 localhost要写服务器内网 IP 或主机头。做完这些办公室同事才能在浏览器里输入 http://192.168.x.x:8086 访问到 CRM也就是一直说的内部网里“永久在线”的业务系统。注意应用池高级设置里把“闲置超时”设为 0避免一段时间没人访问、应用池被回收导致第二天早上第一个人打开页面时卡顿十几秒。5. 避坑指南CRM 上线前后最容易翻车的五个问题内部管理系统上线前后真正让我熬夜改数据、写补偿脚本的全在下面这几类问题里。每一条都是亲身踩过坑之后沉淀下来的经验。5.1 搜索框变成 SQL 注入入口现象客户列表的搜索框输入 OR 11 --页面直接返回了所有客户数据敏感客户全部暴露。原因早期为了图快用字符串拼接 SQL用户输入被当成 SQL 指令执行。这是 asp.net 老项目里最常见的漏洞尤其是在搜索、筛选项、排序字段这些位置。解决把所有数据访问改成参数化查询禁止在任何数据访问方法里拼接用户输入。上线前在代码库里搜索string.Format或拼 SQL 的位置逐个替换成参数化写法具体见 3.1 节的示例。参数化是基础防线再配合 4.3 节的独立应用账号即使有漏洞也无法拖库。5.2 会话超时导致销售反复登录现象销售刚登录没一会儿刷新页面就跳回登录页或者 Cookie 还在但页面报“未授权”错误。原因web.config 里 forms timeout 设了 60 分钟但 Session 默认超时只有 20 分钟或者 IIS 应用池默认 20 分钟回收一次导致票据还在而会话对象丢失。解决把表单认证超时和 Session 超时统一都设为 8 小时IIS 应用程序池高级设置里把固定回收时间改到凌晨业务低峰期避免上班时间内被回收。如果代码里大量依赖 Session 来存用户信息建议改成从认证票据里解析用户标识减少对 Session 的依赖。这样调整后内部网络的 CRM 基本能做到长期自动登录。5.3 并发更新谁后点保存谁赢现象销售 A 和销售 B 同时打开同一个客户A 修改了电话保存B 后来修改了公司名保存A 的电话被覆盖成了旧值。原因更新语句没有带任何旧值校验最后保存的人直接覆盖先保存的人这是典型丢失更新问题。解决用乐观锁给 Customer 表加一个 UpdatedTime 字段更新时 WHERE 条件带上旧的 UpdatedTimeUPDATE Customer SET CustomerName CustomerName, UpdatedTime GETDATE() WHERE Id Id AND UpdatedTime OldUpdatedTime影响行数为 0 时说明这条记录在编辑期间已经被别人改过此时提示用户刷新页面再操作。这是 CRM 并发冲突里最典型的场景加了这一行 WHERE 能挡住九成以上的“数据被覆盖”投诉。5.4 分页翻车数据重复且搜索条件丢失现象列表按创建时间排序翻到第二页发现有一条第一页出现过的记录或者某些记录根本翻不到搜索后点第二页结果变成了全部客户。原因第一ORDER BY 列不是唯一键比如按 CreatedTime 排序时同一秒创建了多条数据OFFSET/FETCH 在并列键上不保证顺序稳定。第二分页链接没有带上搜索关键字点击下一页时参数丢失。解决排序改成ORDER BY CreatedTime DESC, Id DESC或直接ORDER BY Id DESC保证排序完全确定。分页链接生成时把 keyword 拼回 URL视图里保持“当前搜索上下文”。这两个点单独看都是小事一起出现时会让用户直接认为系统有 Bug。5.5 时间字段的时区与格式隐患现象统计周报时数据少了 8 小时导出 Excel 里时间显示成“2025/01/01 12:00:00”和业务口径看到的“2025-01-01”不一致。原因服务器时区、SQL Server 时区和客户端浏览器时区不一致多个页面各写各的格式化规则没有统一格式化入口。解决所有时间写入统一用服务器本地时间 DateTime.Now数据库字段用 datetime展示层用统一的ToString(yyyy-MM-dd HH:mm)格式。涉及统计的查询按业务日期范围计算不要用FollowTime 当天这种写法要用FollowTime 当天0点 AND FollowTime 明天0点。这两条能覆盖绝大多数报表对不上的情况。6. 让 CRM 更有生产力跟进提醒、Word 导出与扫码扩展系统上线只是开始真正让销售愿意天天用的是那些帮他们省时间的增强功能。这里讲三个性价比最高的方向。6.1 跟进提醒用后台线程定时扫数据销售经常忘跟进可以在 Global.asax 的 Application_Start 里启动一个轻量级 Timer每天固定时间扫描“超过 3 天没有跟进记录”的客户给归属销售发站内信。这个场景用 c# 的 System.Threading.Timer 就够不要为了一个提醒功能引入全套任务调度框架。要注意的是Web 应用里的定时任务会随应用池回收而停止重要的提醒最好放到 Windows 服务里执行Web 里最多做“提醒”而不是“必达通知”。6.2 导出 Word 报价单模板替换比手写文档更稳给客户发报价单是销售的高频动作。与其在浏览器里排版不如准备一个 Word 模板在模板里写{客户名}、{产品}、{金额}占位符导出时用代码读取模板并替换变量另存为新文件。排版样式统一在模板里维护应用层只负责填充数据客户改格式时完全不用动代码这是 c# 生成 Word 文档插入变量最省力的落地方式。6.3 移动端扫码手机浏览器加扫码枪就够仓库扫物料号、展会现场扫名片这类移动场景在 CRM 里越来越多。做法是把客户编辑页做成响应式布局手机浏览器直接打开配一把蓝牙扫码枪或直接用摄像头扫码把条码内容填进客户编号输入框。服务端只需要额外提供几个 JSON 接口供扫码设备调用前端页面和后端接口共用同一套登录状态不需要单独开发原生 App。我早期做 CRM 只盯着功能跳过权限和并发控制结果上线第二天就被人投诉数据错了后来连续加班补数据才救回来。现在我做内部管理系统的底线就三条参数化查询、角色权限、每日数据库备份。功能可以少一点底线不能破。希望帮到你。本文还有配套的精品资源点击获取