news 2026/9/25 1:11:21

ASP.NET+SQL Server构建内部项目管理系统:从Gridview到部署避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ASP.NET+SQL Server构建内部项目管理系统:从Gridview到部署避坑

简介:这套基于ASP.NET的B/S模式项目管理系统,采用C#语言开发并搭配Access数据库,适合正在学习web开发或需要完成课程设计、毕业设计的读者。系统完整实现了管理员、员工、网管三种角色权限:管理员可维护员工、项目、日志及历史项目;员工可注册并参与项目、提交日志和回复建议;网管则可管理用户资料并执行数据库备份与还原。资源包内共297个文件,约5.09MB,主要包含cs源码、aspx页面、dll库、css样式及mdb数据库文件,目录结构清晰,便于定位与调试。目前已有505人学习下载,对于希望深入理解ASP.NET权限设计、数据库交互及项目分层结构的开发者来说,这套可运行源码具备很强的实践参考价值,可直接在VS2010中打开部署,并围绕角色功能继续扩展二次开发。

1. 内部项目管理系统,为什么我还在用 ASP.NET + SQL Server 的 Web 结构

一个公司内部要上项目管理系统,预算不高、时间又紧,需求无非是记录项目、拆分任务、跟踪进度。我一般直接推荐 ASP.NET + VS + SQL Server + C# 的 Web 结构组合:开发工具用 VS 就够了,数据库用 SQL Server 维护成本低,部署到一台 Windows Server 的 IIS 上就能跑,一个人从建表到上线,两三天能出第一个可用版本。这套方案不追新框架,但胜在资料多、接手快、坑基本都有前人踩过。我按实际交付过的内部系统写法,把从 SQL Server 表结构、VS 建项目、GridView 绑定数据到上线前排查的完整流程走一遍,适合做企业内部工具和中小团队管理系统的 .NET 开发者直接照着改。

2. SQL Server 表结构先行:三张核心表决定这个系统能撑多大

先动数据库,再动 VS。项目管理系统的核心数据关系很直白:一个项目下面挂多条任务,一条任务有一个负责人,负责人来自用户表。很多翻车现场都是上来就写页面,写到一半发现缺字段、状态值对不上,回头改表,连带改页面和 C# 代码,返工成本最高。我的习惯是先花半小时把三张表建好,字段语义固定下来,后面 GridView 绑定和 C# 逻辑都是顺着表走的。

2.1 项目表、任务表、用户表:最小可用的建表脚本

三张表足够跑通第一个版本:Project 存项目主信息,Task 存项目下的任务,User 存登录名和显示名。下面是 SQL Server 里直接可执行的建表脚本:

-- 项目表:一条项目记录对应一个内部项目 CREATE TABLE dbo.Project ( ProjectId INT IDENTITY(1,1) PRIMARY KEY, ProjectNo NVARCHAR(20) NOT NULL, ProjectName NVARCHAR(100) NOT NULL, ManagerId INT NULL, StartDate DATETIME NULL, EndDate DATETIME NULL, Status INT NOT NULL DEFAULT 0, Priority INT NOT NULL DEFAULT 1, CreatedTime DATETIME NOT NULL DEFAULT GETDATE() ); -- 任务表:一个项目下挂多条任务,ProjectId 做外键 CREATE TABLE dbo.Task ( TaskId INT IDENTITY(1,1) PRIMARY KEY, ProjectId INT NOT NULL FOREIGN KEY REFERENCES dbo.Project(ProjectId), TaskName NVARCHAR(200) NOT NULL, AssigneeId INT NULL, DueDate DATETIME NULL, Progress INT NOT NULL DEFAULT 0, Status INT NOT NULL DEFAULT 0 ); -- 用户表:User 是保留字,建表必须加中括号 CREATE TABLE dbo.[User] ( UserId INT IDENTITY(1,1) PRIMARY KEY, LoginName NVARCHAR(50) NOT NULL, DisplayName NVARCHAR(50) NOT NULL, DeptName NVARCHAR(50) NULL, IsActive BIT NOT NULL DEFAULT 1 );

几个容易忽略的细节。主键用 INT IDENTITY 而不是 GUID,内部系统单表数据量到不了需要 GUID 的量级,INT 主键在 GridView 分页排序和 Join 时效率高,写起来也短。所有名称字段用 NVARCHAR,项目名、任务名里有中文也有特殊字符,NVARCHAR 按 Unicode 存储,不会因为排序规则不同出现乱码。User 表名带中括号是因为 USER 在 SQL Server 里是保留关键字,这个坑我见过不止一次。

Status 字段统一用 INT 而不是 VARCHAR。有人喜欢存"进行中"这种中文,但页面下拉框、C# 枚举、SQL 聚合统计全都得跟着字符串走,稍微多一个空格就匹配不上。INT 状态配合代码里的枚举,显示文案只在一处维护。任务表的 Progress 建议存 0 到 100 的整数,前端进度条直接拿这个值渲染,别存百分比字符串,后面查询统计会省很多事。

2.2 字段类型与状态枚举:两个最容易返工的设计点

第一点是负责人字段存什么。Project.ManagerId 和 Task.AssigneeId 都存 UserId,不要存姓名。内部系统最常遇到的就是组织结构调整,人员名字变了,如果负责人直接存字符串,所有历史数据都得 UPDATE;存 UserId 的话,只需改 User 表一行,页面 Join 出显示名,旧数据自动跟着变。Join 的开销在这种数据量下可以忽略。

第二点是时间字段。开始、结束时间用 DATETIME 够用,但如果你用的是 SQL Server 2016 以上版本,我建表时习惯直接写 DATETIME2,精度到微秒,默认值和比较行为更符合直觉。前端 GridView 显示时用 DataFormatString="{0:yyyy-MM-dd}" 格式化,不要在 SQL 里转 VARCHAR,否则排序会乱。CreatedTime 这种审计字段加上 DEFAULT GETDATE(),INSERT 时少写一个参数,也防止有人漏填。

状态枚举在 C# 侧用一个静态类集中定义,避免代码里到处是魔法数字:

public static class TaskStatus { public const int NotStarted = 0; public const int InProgress = 1; public const int Completed = 2; public const int Suspended = 3; }

页面、查询、统计都用这组常量,以后加状态只改这里和 RowDataBound 里的显示逻辑。字段设计还有个原则:能用约束在数据库挡住的脏数据,就不要指望页面输入框。比如 Progress 加 CHECK (Progress BETWEEN 0 AND 100),比在 C# 里写一堆校验更保险,让 SQL Server 在 INSERT 时直接拒绝非法值,省掉一层黑匣子式的排查。

2.3 建好表后顺手补两个索引:高频查询的索引选择

-- 任务按项目查是最频繁的访问路径 CREATE NONCLUSTERED INDEX IX_Task_ProjectId ON dbo.Task(ProjectId); -- 列表页常按状态过滤和统计 CREATE NONCLUSTERED INDEX IX_Task_Status ON dbo.Task(Status);

这两个索引覆盖项目管理系统的主要查询:项目详情页按 ProjectId 拉任务列表,首页按 Status 统计完成率。内部系统别过度索引,写多读少的场景下,每多一个索引就多一份 INSERT 开销,等系统跑起来看实际查询再补都来得及。建表的另一个注意点是控制权限:给应用账号只授 db_datareader 和 db_datawriter,别用 sa 连 Web 应用,否则一旦页面被注入,攻击者拿到的就是整个实例的权限。

3. 在 VS 里建 ASP.NET Web 项目:从模板选择到 GridView 显示第一页数据

数据库就位后打开 VS。网上搜 asp.net 入门教程,绝大部分还是拿 VS 建一个 Web 项目,拖一个 GridView 上去跑通,这个路径到今天依然是内部管理系统最快的开发方式。下面按 VS 2019 或 2022 的 Community 版操作,安装时勾选"ASP.NET 和 Web 开发"工作负载,SQL Server 用 2019 或 2022 都行,连接方式不影响后面的代码。

3.1 Web Forms 还是 MVC:内部管理系统选型不要纠结

VS 新建项目时有两个常见模板:ASP.NET Web Forms (.NET Framework) 和 ASP.NET MVC。内部管理系统核心是几十个列表页加编辑页,Web Forms 的 GridView、DetailsView 自带绑定、分页、编辑模板,写一个列表页只要几十行代码;MVC 需要自己配路由、控制器、视图和表单验证,代码量大约是 Web Forms 的两到三倍。我一般直接选 Web Forms,目标框架 .NET Framework 4.7.2,部署到 IIS 时应用程序池选 v4.0 集成模式就能跑。

这不是说 MVC 不好,而是投入产出比的问题。项目管理系统没有复杂的 REST API 和前端框架需求,服务端渲染加 GridView 足够。如果团队后续想让前端用 Vue 或 React 分离开发,那是另一个选型故事。选 .NET Framework 4.7.2 还有一个实际好处:Windows Server 2012 R2 以上的系统内置支持,不用单独装运行时,部署服务器时少一个变量。

3.2 Web.config 连接字符串:SQL Server 认证与端口写法

项目建好后第一件事是配置数据库连接。打开 Web.config,在 connectionStrings 节点里加一条连接字符串:

<configuration> <connectionStrings> <add name="PMSConnection" connectionString="Data Source=127.0.0.1,1433;Initial Catalog=ProjectManageDB;User ID=pms_user;Password=pms_pass;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>

Data Source 写成 127.0.0.1,1433 而不是 localhost 或机器名,是为了避开 SQL Server 命名实例解析的坑。默认实例的端口是 1433,显式写端口后,C# 连接时直接走 TCP/IP,不依赖 SQL Browser 服务。这个细节在新装的 SQL Server 上尤其重要,因为默认配置下 TCP/IP 协议常常是禁用状态,等部署到服务器再排查就要花半天。

连接方式我建议用 SQL Server 认证(User ID/Password),而不是 Windows 集成认证。内部局域网虽然可以开 Integrated Security=True,但同事电脑连服务器时,IIS 应用程序池的运行账户如果不是域账户,认证就会失败,日志还看不明白。SQL 认证只要在 SQL Server 里建一个 pms_user 账号,授予 ProjectManageDB 的 db_datareader 和 db_datawriter 权限,问题边界清楚,好排查。MultipleActiveResultSets=True 建议加上,一个连接上同时开多个 SqlDataReader 时不会报错,GridView 绑定和页面里嵌套查询场景下能少踩一个运行时异常。

提示:SQL Server 配置管理器里启用 TCP/IP 后,必须重启 SQL Server 服务,只点"启用"不重启不生效。

3.3 最小可用页面:Default.aspx + GridView 绑定项目列表

配置完连接字符串,写第一个页面验证整条链路。在 VS 里添加 Web 窗体,命名 Default.aspx,拖一个 GridView 到页面:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="PMS.Web.Default" %> <!DOCTYPE html> <html> <head runat="server"> <title>项目列表</title> </head> <body> <form id="form1" runat="server"> <asp:GridView ID="gvProjects" runat="server" AutoGenerateColumns="False" AllowPaging="True" PageSize="10" OnPageIndexChanging="gvProjects_PageIndexChanging"> <Columns> <asp:BoundField DataField="ProjectNo" HeaderText="项目编号" /> <asp:BoundField DataField="ProjectName" HeaderText="项目名称" /> <asp:BoundField DataField="ManagerName" HeaderText="负责人" /> <asp:BoundField DataField="StartDate" HeaderText="开始日期" DataFormatString="{0:yyyy-MM-dd}" HtmlEncode="False" /> </Columns> </asp:GridView> </form> </body> </html>

页面后置代码 Default.aspx.cs:

using System; using System.Data; public partial class Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { // IsPostBack 为 false 才做首次绑定,避免每次回发重置列表 if (!IsPostBack) { BindProjects(); } } private void BindProjects() { string sql = @" SELECT p.ProjectId, p.ProjectNo, p.ProjectName, u.DisplayName AS ManagerName, p.StartDate FROM dbo.Project p LEFT JOIN dbo.[User] u ON p.ManagerId = u.UserId ORDER BY p.CreatedTime DESC"; DataTable dt = SqlHelper.ExecuteQuery(sql); gvProjects.DataSource = dt; gvProjects.DataBind(); } protected void gvProjects_PageIndexChanging(object sender, GridViewPageEventArgs e) { gvProjects.PageIndex = e.NewPageIndex; BindProjects(); } }

Page_Load 里的 if (!IsPostBack) 是 Web Forms 的核心习惯。页面每次回发都会执行 Page_Load,如果每次都绑定 GridView,翻页、删除后数据源被重置,用户体验和状态都不对。GridView 的 AllowPaging 和 PageSize 控制每页行数,翻页触发 OnPageIndexChanging 事件,把 PageIndex 换成目标页再重新绑定。ManagerName 来自 User 表 LEFT JOIN,如果某个项目没设负责人,左边表字段为 NULL,GridView 对应单元格显示为空,页面不会崩。如果改成 INNER JOIN,没负责人的项目直接消失,用户会以为系统丢了数据,这种细节在内部系统里很影响信任度。

按 F5 启动调试后,VS 会拉起 IIS Express,浏览器访问 localhost 端口下的 Default.aspx,页面从 SQL Server 把项目列表查出来渲染成 HTML,浏览器、IIS Express、C# 代码、SQL Server 这四层链路就是标题里 web 结构的完整含义。能看到列表,说明连接字符串、表结构和页面绑定都通了,后面的增删改查都是在这个骨架上加肉。

4. C# 增删改查落地:SqlHelper、GridView 行操作与状态显示

页面能显示列表只是开始,项目管理系统必须能加任务、改状态、删记录。这一章把数据访问层和 GridView 的事件写法定下来,后面每个模块都是复制这套模式。

4.1 一个够用的 SqlHelper:参数化查询与连接释放

常见做法是单独建一个 SqlHelper.cs,把连接字符串、查询、执行封装成静态方法。内部系统用不着 Entity Framework 的完整能力,一个 SqlHelper 加 DataTable 足够应付 GridView 绑定场景,也方便新人理解数据是怎么来的。

using System; using System.Configuration; using System.Data; using System.Data.SqlClient; public static class SqlHelper { private static readonly string connStr = ConfigurationManager.ConnectionStrings["PMSConnection"].ConnectionString; public static DataTable ExecuteQuery(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); } using (SqlDataAdapter da = new SqlDataAdapter(cmd)) { DataTable dt = new DataTable(); da.Fill(dt); return dt; } } } 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 语句保证 SqlConnection 和 SqlCommand 用完即释放,连接归还连接池,SQL Server 不会因为连接句柄没关而耗尽 worker 线程,这是内部系统跑一段时间后"数据库突然连不上"的头号原因。第二,所有外部传入的值一律走 SqlParameter,不要字符串拼接。拼 SQL 除了 SQL 注入风险,还有一个容易忽略的问题:文本框里输入带单引号的文本,比如项目名"客户'满意度'调研",拼接 SQL 直接语法错误,参数化后这类问题彻底消失。参数的类型和长度由 SqlParameter 推导,C# 侧传 string、int、DateTime 都对应到 SQL Server 的正确类型。

4.2 GridView 行操作统一走 RowCommand:别在模板里挂控件事件

给 GridView 加删除按钮,常见做法是在列集合里加一个 ButtonField,CommandName 指定动作名,CommandArgument 绑定主键。页面上的按钮点击后,事件统一在 GridView 的 RowCommand 里处理:

<asp:ButtonField Text="删除" CommandName="DeleteTask" ButtonType="Button" />

对应的事件代码:

protected void gvTasks_RowCommand(object sender, GridViewCommandEventArgs e) { if (e.CommandName == "DeleteTask") { // 表头按钮触发时 CommandArgument 为 null,必须先判空 if (e.CommandArgument == null) { return; } int taskId = Convert.ToInt32(e.CommandArgument); string sql = "DELETE FROM dbo.Task WHERE TaskId = @TaskId"; SqlHelper.ExecuteNonQuery(sql, new SqlParameter("@TaskId", taskId)); BindTasks(); } }

为什么建议统一走 RowCommand 而不是在 TemplateField 里放一个 LinkButton 再挂 OnClick:TemplateField 里的控件事件需要 FindControl 或写 OnClientClick,回发链路长,ViewState 稍微不对就报"回发或回调参数无效",这是 Web Forms 里最著名的报错之一,错误信息还特别误导。RowCommand 是 GridView 层面的标准事件,CommandArgument 把主键从行上下文里带出来,代码集中在同一处,调试时打断点也方便。CommandArgument 传出来的是字符串,用 Convert.ToInt32 转换前先判断是否为空,否则表头按钮触发 RowCommand 时直接抛异常。

4.3 RowDataBound 把状态数字翻译成前端文案

数据库里 Status 是 0、1、2,页面上不能直接显示数字。在不改造 GridView 的情况下,用 RowDataBound 事件在每一行绑定完成后改写 Status 列。先在 GridView 的模板列里放一个 Literal:

<asp:TemplateField HeaderText="状态"> <ItemTemplate> <asp:Literal ID="litStatus" runat="server"></asp:Literal> </ItemTemplate> </asp:TemplateField>

RowDataBound 里找到这个控件并赋值:

protected void gvTasks_RowDataBound(object sender, GridViewRowEventArgs e) { // 跳过表头和表尾行,它们没有 DataItem if (e.Row.RowType != DataControlRowType.DataRow) return; DataRowView row = (DataRowView)e.Row.DataItem; // 历史数据里可能混入 NULL,先判空再转换 if (row["Status"] == DBNull.Value) { return; } int status = Convert.ToInt32(row["Status"]); Literal lit = (Literal)e.Row.FindControl("litStatus"); if (lit != null) { switch (status) { case 0: lit.Text = "未开始"; break; case 1: lit.Text = "<span style='color:#d97706'>进行中</span>"; break; case 2: lit.Text = "<span style='color:#16a34a'>已完成</span>"; break; default: lit.Text = "未知"; break; } } }

RowType 判断是为了跳过表头和表尾行,表头行没有 DataItem,强转 DataRowView 会抛异常。row["Status"] 先判断 DBNull,这条是我吃过亏的经验:某个版本允许任务不设置状态,历史数据里出现 NULL,Convert.ToInt32 直接崩,页面整页翻车,日志里只有一句空引用异常,排查起来特别费劲。如果你从别的系统导入任务数据,比如 Excel 导入时把进度写成"80%",导入到 SQL Server 的 Progress 字段就会报转换失败,第 5 章排查清单里专门有一条,这里先记住原则:数据库字段类型在导入前先 TRY_CONVERT 验证,别让脏数据进门。

5. 上线避坑:SQL Server 连不上、分页越界、字符串转数字报错的排查清单

这一章是我做这个方向踩过的坑汇总。每条按"现象、原因、解决"写,新手上线前照着过一遍,能省下至少一个通宵。

5.1 现象:本机能跑,同事浏览器访问报数据库连接错误

现象:在开发机上 F5 调试一切正常,发布到服务器后自己访问没问题,同事访问同样的地址,页面报"建立与 SQL Server 的连接时发生网络相关错误或特定于实例的错误"。

原因:通常三选一。SQL Server 的 TCP/IP 协议没启用,默认只开了 Shared Memory;Windows 防火墙没放行 1433 端口;连接字符串用了机器名而服务器解析不了。

解决:打开 SQL Server 配置管理器,找到"SQL Server 网络配置"下的实例,启用 TCP/IP,重启 SQL Server 服务。确认 telnet 服务器IP 1433 能通,不通就加防火墙入站规则。连接字符串统一写成 IP 加端口,别写 localhost,也别写服务器主机名。我见过最隐蔽的情况是服务器上有多个 SQL Server 实例,默认实例里没建库,数据都在命名实例里,连接字符串指向默认实例自然报错。排查这类问题先从 SQL Server 配置管理器和防火墙下手,不要一上来就怀疑代码。

5.2 现象:GridView 翻页到第 3 页后删除最后一条,页面报索引越界

现象:列表第 1 页删数据正常,翻到最后一页,把该页最后一条记录删除后,BindTasks() 一执行就报"索引超出范围"。

原因:删除后重新绑定,GridView 的 PageIndex 还停留在原来的页码,但总页数已经减少了,当前页索引超过 PageCount - 1,GridView 内部取行时索引越界。

解决:重新绑定前做一次钳制,在 DataBind 之前判断:

private void BindTasks() { // ... 查 DataTable、赋 DataSource 的代码不变 // 删除或筛选后总页数可能减少,钳制到最后一页 if (gvTasks.PageIndex >= gvTasks.PageCount) { gvTasks.PageIndex = gvTasks.PageCount - 1; } gvTasks.DataBind(); }

这个 bug 只在"最后一页删最后一条"的边界条件下出现,测试数据不够多时根本发现不了,属于典型的黑匣子式报错,报错信息也指向不明。顺带一提,翻页事件里修改 PageSize 后也要重新 DataBind,否则 GridView 显示的行数和新 PageSize 不一致。写 GridView 相关逻辑时,把"页面索引变化"和"数据源变化"这两条链路分开想,能少踩一半这类坑。

5.3 现象:sqlserver 字符串转数字报错,导入任务数据时 Progress 字段直接失败

现象:从 Excel 导入任务清单,SQL Server 提示数据无效或报"将 nvarchar 转换为数据类型 int 的语法错误",整批数据导入失败。

原因:Progress 是 INT,源数据里有的单元格是"80%",有的为空,甚至混着"高/中/低"这类文本。SQL Server 做隐式转换时遇到非数字字符直接抛错,而且不是跳过那几行,是整批失败。

解决:导入前用 TRY_CONVERT 过滤。SQL Server 2012 以上版本支持 TRY_CONVERT,转换失败返回 NULL,配合 ISNULL 给默认值:

-- ProgressText 是导入表的临时字段,转换失败时归 0 SELECT TaskName, ISNULL(TRY_CONVERT(INT, REPLACE(ProgressText, '%', '')), 0) AS Progress FROM dbo.TaskImport;

C# 侧也用 int.TryParse 兜底,不用 Convert.ToInt32 直接转,避免输入为空字符串时抛 FormatException。这条原则对所有从 Web 页面拿到的东西都适用:凡是从 GridView 的 CommandArgument、TextBox 文本、Excel 导入来的值,先 TryParse 再进数据库。SQL Server 的隐式转换在数据量大的表上还会引发额外的类型转换开销,能显式 CAST 就不要靠自动转。

5.4 现象:IIS 发布后页面报 500.19 或"由于扩展配置问题无法提供请求的页面"

现象:VS 里发布成功,文件也拷到服务器了,浏览器访问 .aspx 页面直接 500.19,IIS 日志里写"无法读取配置节"。

原因:服务器 IIS 没装 ASP.NET 功能模块。Windows Server 默认只装静态网页功能,.NET 的请求处理模块没注册;或者应用程序池选的经典模式,与 Web Forms 的集成管线不兼容。

解决:服务器管理器里添加角色和功能,勾选"应用程序开发"下的 ASP.NET 4.x(对应 .NET Framework 版本),安装后重启 IIS。再检查应用程序池:右键默认网站对应的池,基本设置里".NET CLR 版本"选 v4.0.30319,"托管管道模式"选"集成"。手动搭建 IIS 时经常忘记这步,报错信息又没直接说"没装 ASP.NET",很容易在 web.config 里白找半天。发布时如果用了 Web Deploy,还要确认目标服务器装了 Web Deploy 代理,否则 VS 里"发布"按钮能点,但文件根本没传全。

5.5 现象:SQL Server 事务日志把磁盘撑满,数据库进入只读状态

现象:系统跑了两个月,磁盘告警,检查发现数据库的 .ldf 日志文件涨到几十 GB,数据库变成只读,业务页面全部报错。

原因:数据库恢复模式是"完整",又没有配任何日志备份作业。完整恢复模式下,日志只有在备份之后才会截断,没有备份就无限增长。内部项目管理系统对数据恢复粒度没有苛刻要求,完全不需要完整恢复模式的按时间点还原能力。

解决:右键数据库属性,恢复模式改成"简单",日志空间自动收缩;再配一个每周完整备份的维护计划。想看日志文件占用和恢复模式,用这条 SQL:

-- 查看数据库恢复模式和日志文件空间占用 SELECT name, recovery_model_desc, log_recovery_size_desc FROM sys.databases WHERE name = 'ProjectManageDB';

简单恢复模式损失的是"最近一次完整备份之后到故障点之间的日志",对内部系统来说,最坏情况丢一天数据,完全可以接受。这个决策要记录在部署文档里,不然下一位维护者看到恢复模式是"简单"会以为是配置错误又改回去,日志再次暴涨。日志问题排查时,右键数据库"报表-标准报表-磁盘使用情况"也能直接看到日志和数据的空间占比,比数文件大小直观。

6. 让系统真正被用起来:进度统计、GridView 的 jQuery 体验与上线验证

系统没人用往往不是功能不够,而是看不到价值。项目管理系统最能说服用户的是项目进度一眼可见。给首页加一个统计视图,SQL 直接聚合:

6.1 一条 SQL 算出项目进度:先乘再除避免整数除法

SELECT p.ProjectName, COUNT(t.TaskId) AS TotalTask, SUM(CASE WHEN t.Status = 2 THEN 1 ELSE 0 END) AS DoneTask, CASE WHEN COUNT(t.TaskId) = 0 THEN 0 ELSE CAST(SUM(CASE WHEN t.Status = 2 THEN 1 ELSE 0 END) * 100 / COUNT(t.TaskId) AS INT) END AS Progress FROM dbo.Project p LEFT JOIN dbo.Task t ON p.ProjectId = t.ProjectId GROUP BY p.ProjectId, p.ProjectName;

注意先乘 100 再除,SQL Server 的整数除法直接舍去小数,先除后乘会把 8/10 得 0 再乘还是 0。COUNT(t.TaskId) 为 0 的项目单独兜底返回 0,否则除零报错。这条 SQL 返回的结果集可以直接绑定到首页的 GridView,也可以在 C# 里读出来画进度条。

6.2 GridView 加一点 jQuery 交互:ClientID 定位和行高亮

GridView 渲染出来就是一个带 id 的 table,给它的行加 hover 高亮和点击选中,不需要任何插件。网上搜"asp.net 的 gridview 的 jquery 插件",很多方案绕了一圈,其实原生的够用:

$(function () { // ClientID 保证嵌套页面里也能定位到 GridView $('#<%= gvTasks.ClientID %> tr').hover( function () { $(this).addClass('row-hover'); }, function () { $(this).removeClass('row-hover'); } ); });

用 ClientID 而不是写死 ID,是因为 Web Forms 在主页面、用户控件嵌套时控件 ID 会被改写成 ctl00_ 前缀,写死 gvTasks 在深层页面里定位不到。CSS 里定义 .row-hover 的背景色即可,这段脚本放在页面底部或者单独的 js 文件里,不依赖 UpdatePanel 就能工作。如果页面里有 UpdatePanel 局部刷新,scriptManager 注册的脚本会在每次回发后重新执行,不需要额外处理。

6.3 上线前的验证清单:发布后 10 分钟过一遍

发布后我习惯按下面这张表过一遍,全绿才算交付:

检查项验证方法失败时看哪里
数据库连接访问一个列表页,确认数据能显示SQL Server 配置管理器 TCP/IP、防火墙 1433
IIS 应用池aspx 页面不报 500.19应用池 .NET CLR 版本、集成模式
删除/翻页边界删最后一页最后一条记录GridView PageIndex 钳制逻辑
日志与备份查看数据库恢复模式sys.databases、维护计划任务

这套组合开发内部项目管理系统,我走了不止一遍,现在的习惯是先画表结构再写页面,状态全部用枚举,外部输入一律参数化,上线前把第五章的清单完整过一遍。这几个习惯帮我避开了大部分返工,也希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 1:10:47

Linux PCI驱动框架详解:从设备匹配到中断处理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:09:55

烧录良率上不去?从物理层到系统层的逐级排查框架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:09:46

九联UNT403HS强刷安卓9教程:从U盘刷机到救砖全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:09:29

STM32CubeMX与Keil5安装避坑指南:版本匹配与系统级配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:09:19

嵌入式Debug本质:硬件-编译器-运行时全栈信任链重建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:08:58

VMware虚拟机启用摄像头全指南:USB直通与UVC驱动配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华