简介:这是一份基于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 Forms | MVC 5 (.NET Framework 4.8) | ASP.NET Core MVC |
|---|---|---|---|
| 页面模型 | 服务端控件+视图状态 | Razor 视图+路由 | Razor 视图+路由 |
| 前端介入 | 弱,控件生成的 HTML 难调整 | 强,视图里可直接写 HTML/JS | 强,更适合前后端分离 |
| 测试难度 | 难做单元测试 | Controller 可以单测 | 支持依赖注入,单测方便 |
| 部署环境 | Windows/IIS | Windows/IIS | Windows/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 表挂 CustomerId,Opportunity 表挂 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 List<Customer> GetPage(int pageIndex, int pageSize, string keyword, int ownerId, out int total) { var list = new List<Customer>(); 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 mode="Forms"> <forms loginUrl="~/Account/Login" timeout="60" slidingExpiration="true" /> </authentication>Controller 上的授权声明:
[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 name="CrmDB" connectionString="Server=192.168.1.10;Database=CrmDB;User Id=crm_app;Password=******;" providerName="System.Data.SqlClient" /> </connectionStrings>连接串有两个必须注意的坑:不要用 sa 账号,给应用单独建 crm_app 账号并只授予该数据库的读写权限,避免应用被注入后直接拿到整个数据库实例;Server 值在开发环境写 localhost,部署时要改成数据库服务器 IP,忘了改会报“用户登录失败”或“在与 SQL Server 建立连接时出现与网络相关的错误”,这个报错在刚发布时出现频率极高。
局域网访问还要做两步:Windows 防火墙在“入站规则”中放行 TCP 端口,比如 8086;IIS 网站绑定里不要用 localhost,要写服务器内网 IP 或主机头。做完这些,办公室同事才能在浏览器里输入 http://192.168.x.x:8086 访问到 CRM,也就是一直说的内部网里“永久在线”的业务系统。
注意:应用池高级设置里把“闲置超时”设为 0,避免一段时间没人访问、应用池被回收,导致第二天早上第一个人打开页面时卡顿十几秒。
5. 避坑指南:CRM 上线前后最容易翻车的五个问题
内部管理系统上线前后,真正让我熬夜改数据、写补偿脚本的,全在下面这几类问题里。每一条都是亲身踩过坑之后沉淀下来的经验。
5.1 搜索框变成 SQL 注入入口
现象:客户列表的搜索框输入' OR 1=1 --,页面直接返回了所有客户数据,敏感客户全部暴露。
原因:早期为了图快,用字符串拼接 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 条件带上旧的 UpdatedTime:
UPDATE 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 只盯着功能,跳过权限和并发控制,结果上线第二天就被人投诉数据错了,后来连续加班补数据才救回来。现在我做内部管理系统的底线就三条:参数化查询、角色权限、每日数据库备份。功能可以少一点,底线不能破。希望帮到你。
本文还有配套的精品资源,点击获取