ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

信息发布系统源码 C# 拆解:WebForms 权限审核与二次开发避坑

信息发布系统源码 C# 拆解:WebForms 权限审核与二次开发避坑

简介:信息发布系统源码包面向高校计算机相关专业学生,以及希望熟悉 C#、ASP.NET 与 .NET Web 开发的初中级开发者,可用于毕业设计、课程设计或自主研习。项目围绕信息发布、分类管理、检索和后台审核展开,涵盖用户注册登录、个人信息维护、信息增删改查、分类关联、关键词搜索、分页排序过滤及管理员审核等模块,便于理解数据库设计、身份验证与授权、前后端交互以及安全性和性能优化等要点。压缩包共 577 个文件,约 53.3MB,以 class、java、png、xml、jar 等类型为主,分别对应编译类文件、源码、界面图片、配置清单和依赖库,其中还包含 vsplayer 视频播放相关子项目,可参考视频内容嵌入思路。已有 713 人学习,读者可据此分析用户表、信息表与分类表之间的关联,借鉴身份验证、权限控制、分页查询和缓存优化的实现方式,也能结合响应式界面与视频子项目扩展展示形态。

1. 拿到这套信息发布系统源码先别急着打开解决方案:三个判断决定它值不值得跑

压缩包解开,通常是一个.sln、一份数据库备份、一个空的UploadFiles目录——这就是多数信息发布系统源码 C#版本的常见配置。它属于典型的 ASP.NET WebForms 三层结构项目:前台负责栏目导航和文章详情,后台负责栏目管理、稿件录入、审核发布和用户权限,数据库用 SQL Server。放在毕业设计场景里,它的价值不在代码写得多漂亮,而在于把“内容从录入到上线”这条链路的每个环节都摆出来了,包括权限、状态、附件、日志。

判断它值不值得花时间,先看三件事:目标框架是不是 .NET Framework 4.x(不是就别指望在旧版 VS 里直接打开);数据库是不是 SQL Server 且给了.bak或.mdf而不是只给建表语句;权限是写死在页面里的if (role == "admin"),还是落到角色-栏目-操作三张关联表上。前两条决定你能不能跑起来,第三条决定你后续改起来会不会被硬编码绑住手脚。

适合谁:需要一套能跑通、能改栏目、能加审核流的 CMS 底子的人;带学生做课题、要演示完整业务闭环的人。不适合谁:指望拿它直接扛生产流量的人——分页、缓存、富文本过滤这几块,默认实现基本都要重做。

2. 拆开解决方案看分层:ASP.NET WebForms 的目录职责与数据表映射

把项目在 Visual Studio 里加载完,先别按 F5。花二十分钟把目录和表结构对一遍,比后面 debug 半天更省事。这套代码的骨架不复杂,但目录命名各家不一样,有的叫BLL/DAL/Model,有的叫Business/DataAccess/Entity,还有的直接把 SQL 拼在.aspx.cs里。先摸清它到底分了几层,决定了你后面改一个字段要动几个文件。

2.1 从 .sln 到页面:五个目录的职责边界

按 WebForms 的惯例,一份能跑的信息发布系统源码大致会落到这几块:

目录 / 文件职责改动频率是否建议动
*.sln/*.csproj解决方案与程序集引用低只在加第三方库时动
Model/或App_Code/实体类,字段与表列一一对应中加字段时必动
DAL/拼 SQL、执行SqlCommand、返回DataTable高改查询、加索引必动
BLL/业务判断,如“待审才能改”“已发布不能删”高审核流改造的主战场
Admin/后台页面,.aspx+.aspx.cs+.designer.cs高界面与交互
Web/或根目录前台栏目页、列表页、详情页中模板改造
UploadFiles/附件与图片落盘位置低权限配置重点

WebForms 的一个特点是每个页面三个文件:.aspx写服务器控件和绑定表达式,.aspx.cs写生命周期事件(Page_Load、btnSave_Click),.designer.cs是设计器自动生成的控件声明。改界面时只动.aspx,改逻辑只动.aspx.cs,.designer.cs手改容易和设计器打架,一般不碰。

判断分层是否真实存在,最快的办法是搜一遍SqlConnection。如果它只出现在DAL目录里,说明分层是干净的;如果Admin下的每个btnSave_Click里都有一份,那这套代码本质上是“页面直连数据库”,后面加一个字段你要改十几个页面。这种情况不用急着重构,先把新功能用DAL写,老的慢慢替换。

2.2 数据表与实体的对应关系

信息发布系统的表不多,但关联关系是全的。把下面这几张表认全,后面调权限和状态就不会迷路:

表名(常见命名)主键关键字段说明
CMS_ColumnColumnIdColumnName、ParentId、Sort、IsNav栏目树,ParentId=0为顶级
CMS_ArticleArticleIdTitle、Content、ColumnId、State、AuthorId稿件主表
CMS_AttachmentAttIdArticleId、FileName、SavePath、FileSize一稿多附件
Sys_UserUserIdUserName、Pwd、RoleId、IsLock密码常见是 MD5,后面要换
Sys_RoleRoleIdRoleName编辑、审核、管理员
Sys_RoleColumn联合主键RoleId、ColumnId、CanAdd/CanEdit/CanAudit数据级授权
Sys_LogLogIdUserId、Action、TargetId、CreateTime操作留痕

实体类字段和表列对不上是常见现象:Content在实体里可能叫Body,State可能叫Status,还有的把CreateTime写成字符串类型,取出来再Convert.ToDateTime。这不会报错,但会让你在写条件判断时踩空。做法是先打开一个实体类,对照表结构逐个核一遍类型,尤其是decimal、DateTime、bit这三种,映射错了在 GridView 里显示正常,写回数据库时才炸。

栏目树还有一个隐藏约定:ParentId是不是允许无限层级。多数实现只支持两级,前台导航写死了“一级栏目 + 二级栏目”的循环。你如果加三级,前台模板不会自动展开,需要改绑定逻辑。这一点在改造前先用数据试:手动插一条ParentId指向二级栏目的记录,看前台是否显示。

2.3 一次发布请求的完整链路

把“编辑点保存”这条线走通,整套代码的运行方式就清楚了。典型链路如下:

  1. Admin/ArticleEdit.aspx加载,Page_Load里判断Session["UserId"]是否为空,空则跳登录页;
  2. 用户填写标题、选栏目、富文本编辑器写入正文;
  3. 点保存触发btnSave_Click,从控件取值组装实体;
  4. 调BLL.Article.Save(entity),内部先做状态判断,再调DAL;
  5. DAL用参数化 SQL 执行INSERT或UPDATE,返回受影响行数;
  6. 页面根据返回值ClientScript.RegisterStartupScript弹提示并跳列表页。

其中第 4 步是整套代码的业务核心。判断逻辑通常长这样:

// BLL 层保存:区分新增与修改,并限制已发布稿件不能直接覆盖 public int Save(Article model) { if (model.ArticleId == 0) { model.State = 0; // 新增一律进草稿 model.CreateTime = DateTime.Now; return dal.Insert(model); } var old = dal.GetById(model.ArticleId); if (old == null) throw new Exception("稿件不存在"); if (old.State == 2) // 已发布,先下架再改 throw new Exception("已发布稿件请先下架"); return dal.Update(model); }

这段代码里有两个参数值得注意:State = 0是硬编码的草稿状态,如果后面要加“直接送审”的入口,这个赋值就得改成从页面传入;old.State == 2用的是魔法数字,改造时建议先把它换成枚举,否则状态一多就没法读了。dal.Insert返回的是@@IDENTITY或SCOPE_IDENTITY(),前者在表上有触发器时会拿到错误的自增值,这是老代码里常见的隐患,看到就换成后者。

链路走通之后再看一个细节:前台的列表页是不是每次都实时查库。如果Page_Load里没有Cache相关代码,那么后台一发布,前台刷新就能看到,调试方便但性能一般;如果用了Cache.Insert且没设过期策略,就会出现“改了看不到”的经典问题,排查时先去Global.asax里找有没有统一的缓存清理逻辑。

3. 把环境跑起来:数据库附加、连接串与目录权限的三处必改项

代码看得差不多了,开始动手。这一步的目标很明确:让登录页出现在浏览器里。整个过程会卡在三个地方——依赖版本、数据库附加、目录权限,每一个都有明确的报错特征,认出来就好办。

3.1 依赖清单与版本对齐

先确认本机环境。多数这类项目是 .NET Framework 4.5 到 4.8 之间,需要装对应版本的开发工具包;数据库用 SQL Server Express 就够,但要注意实例名。

依赖项常见要求检查方式不满足时的表现
.NET Framework4.5 / 4.6.1 / 4.8注册表或 VS 安装器项目显示“不兼容”
Visual Studio2017 及以上打开.sln提示需要迁移
SQL Server2012 及以上SSMS 连接附加时报版本号错误
IIS Express随 VS 安装项目属性里的 Web 选项卡启动时无端口
第三方 DLLAjaxControlToolkit、 Newtonsoft.Json 等bin/与packages/对照“未能加载程序集”

版本不对齐是第一个坑。.sln文件开头有一行Format Version,还有TargetFrameworkVersion写在.csproj里,用记事本就能看。如果显示v4.8而本机只有 4.5,要么装 4.8 的开发工具包,要么把目标框架降下来重新编译——但降框架可能导致某些语法特性不可用,所以优先升级环境。

第三方 DLL 的处理有个省事的做法:先看bin目录里有没有编译好的 DLL,有的话直接跑,不用管packages里的源码。只有在你需要改第三方库或者它引用了错误版本时,才去 NuGet 里还原。判断依据是页面报错里那句Could not load file or assembly ... Version=x.x.x.x,报错里的版本号和bin里实际的版本号对一下就知道差在哪。

3.2 附加数据库与初始化脚本

数据库给的形式通常两种:.mdf+.ldf文件,或者一个.bak备份。前者用FOR ATTACH,后者用RESTORE。把路径换成你本机解压后的实际目录再执行。

-- 方式一:只有 mdf/ldf,直接附加 -- 注意:mdf 与 ldf 必须同时存在,且 SQL Server 服务账号要有读取权限 CREATE DATABASE InfoPub ON (FILENAME = N'D:\Src\App_Data\InfoPub.mdf'), (FILENAME = N'D:\Src\App_Data\InfoPub_log.ldf') FOR ATTACH; GO
-- 方式二:给了 .bak 备份,用 RESTORE 并指定新路径 -- WITH MOVE 的两个逻辑名要与备份里的逻辑文件名一致,写错会报“无法覆盖” RESTORE DATABASE InfoPub FROM DISK = N'D:\Src\db\InfoPub.bak' WITH MOVE 'InfoPub' TO N'D:\DbData\InfoPub.mdf', MOVE 'InfoPub_log' TO N'D:\DbData\InfoPub_log.ldf', REPLACE; GO

两段脚本的关键参数说明:FILENAME必须写绝对路径,写成相对路径会报操作系统错误 5(拒绝访问),本质是 SQL Server 服务账号没权限访问你用户目录下的文件;MOVE后面的逻辑名不是数据库名,要看备份内部的名字,用RESTORE FILELISTONLY FROM DISK = N'...'先列一遍最稳妥。REPLACE表示覆盖同名库,第一次执行可以不加,重复执行时加。

如果只给了.sql建表脚本,那就先CREATE DATABASE InfoPub,再在 SSMS 里切到该库执行脚本。注意脚本里的GO是 SSMS 的批处理分隔符,不是 T-SQL 语句,用代码执行时要按GO切分。

附加完成后做一次数据核对:SELECT COUNT(*) FROM CMS_Article,看有没有演示数据。有数据的话,后台登录账号通常藏在Sys_User表里,密码是 MD5,直接改一个新值比猜原密码快——用SELECT HASHBYTES('MD5', 'NewPass123')生成后UPDATE进去。

3.3 Web.config 三处改动与目录授权

数据库通了,接下来改配置。Web.config里必须动的是三处:

<configuration> <connectionStrings> <!-- 改动点 1:数据源改成自己的实例名,Express 版通常是 .\SQLEXPRESS --> <add name="ConnStr" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=InfoPub;User ID=sa;Password=YourStrongPwd;" providerName="System.Data.SqlClient" /> </connectionStrings> <system.web> <!-- 改动点 2:调试期开 true,交付前必须改回 false --> <compilation debug="true" targetFramework="4.8" /> <!-- 改动点 3:富文本正文较大,默认 4MB 不够;单位是 KB --> <httpRuntime maxRequestLength="20480" requestValidationMode="2.0" /> </system.web> </configuration>

三处的含义分别是:Data Source里的点代表本机,如果用的是命名实例必须带反斜杠;debug="true"会让每个请求多做一些检查,交付前改成false是基本要求,否则出错页会把堆栈直接吐给访问者;maxRequestLength单位是 KB,20480 就是 20MB,配合system.webServer下的maxAllowedContentLength(单位是字节)一起改才生效,只改一处会出现“小文件能传、大文件 404”的怪现象。

接着给上传目录授权。IIS Express 跑的时候用的是当前用户身份,问题不大;换成完整 IIS 之后,应用程序池的虚拟账号需要对UploadFiles有写权限:

# 给 IIS_IUSRS 组授予修改权限,/T 递归到子目录 # (OI)(CI) 表示对象继承与容器继承,M 表示修改 icacls "D:\Src\UploadFiles" /grant "IIS_IUSRS:(OI)(CI)M" /T # 查看授权结果,确认输出里有 IIS_IUSRS 一行 icacls "D:\Src\UploadFiles"

如果授权后仍然上传失败,去看应用程序池的“标识”是什么。默认是ApplicationPoolIdentity,对应的虚拟账号是IIS AppPool\池名称,也可以直接给这个账号授权,比给整个IIS_IUSRS组更收敛。

三处改完,在 VS 里把起始页设成Admin/Login.aspx或Default.aspx,F5。能看到登录框,这一步就算过了。看不到的话按报错类型分:黄页是编译错误,看具体行号;白页多半是Session或数据库连接超时;404 是路由或处理程序没注册,检查system.webServer里的handlers节。

4. 权限与审核流程落地:角色表、状态机与两道校验

跑起来之后,真正决定这套源码能不能改成可用系统的,是权限和状态这两块。毕业设计里这两块常常是半成品:权限靠页面顶部的角色判断,审核靠一个IsPass字段。跑演示够用,一加栏目、一加角色就散架。

4.1 表驱动的三级权限:角色-栏目-操作

先分清三层概念。功能权限决定你能看到哪些菜单,数据权限决定你能操作哪些栏目的稿件,操作权限决定你能新增还是只能审核。三层里最容易缺的是第二层——很多代码只做了菜单控制,结果编辑 A 栏目的账号能改 B 栏目的稿子。

判断现有实现够不够用,看Sys_RoleColumn这张表在不在。在的话,授权粒度就到位了;不在的话,建议补上,改动量不大:

层级载体控制对象校验时机
功能权限Sys_RoleMenu后台菜单项、页面入口菜单渲染、页面Page_Load
数据权限Sys_RoleColumn栏目、稿件集合列表查询条件、保存前判断
操作权限Sys_RoleColumn的四个bit列增、改、审、删按钮点击事件内

补表的时候有个细节要注意:Sys_RoleColumn的主键应该是RoleId + ColumnId的联合主键,而不是自增 ID。用自增 ID 的话,同一条授权重复插入不会报错,权限表里会出现重复行,判断时取到哪一条全看运气,这种玄学问题排查起来很费时间。加联合主键之后,重复插入直接抛主键冲突,问题在插入那一步就暴露了。

菜单权限的校验方式通常是页面基类或母版页里做一次判断。如果代码里每个页面开头都是重复的三行if,那说明没抽基类,这时候新增页面就必须记得复制这三行,漏一个就是一个越权入口。改造思路后面 4.3 会讲。

4.2 发布状态机:四个状态与合法迁移

审核流的核心不是“有没有审核按钮”,而是状态之间能不能随便跳。一个够用的状态机至少四个状态:

状态值名称谁可以进入允许迁往
0草稿编辑新增或驳回后1 待审
1待审编辑提交0 草稿(驳回)、2 已发布
2已发布审核通过1 待审(修改重审)、3 已下架
3已下架管理员下架2 已发布(重新上架)

用表格把规则写死,代码就只是翻译这张表。常见的翻车点是允许“已发布”直接跳回“草稿”,表面上是撤回,实际上前台缓存和静态页不会同步失效,用户看到的是旧内容,而数据库里已经是草稿状态,两边对不上。所以合法迁移里必须把这条排除掉,改稿走“下架 → 修改 → 重新提交”的路径。

另一个细节是驳回要不要记原因。至少加一列AuditRemark,否则被驳回的编辑只能猜哪里不合格。这列在CMS_Article上加就行,nvarchar(200)够用。

4.3 把校验收口到一个静态入口

规则清楚了,代码就要收口。别在每个按钮事件里重复写判断,抽两个静态类:一个管权限,一个管状态迁移。

// 状态迁移守卫:规则集中在这里,页面只问“能不能”,不自己写 if public enum ArticleState { Draft = 0, Pending = 1, Published = 2, Offline = 3 } public static class ArticleFlow { // 字典的键是当前状态,值是可到达的目标状态集合 private static readonly Dictionary<ArticleState, ArticleState[]> Map = new Dictionary<ArticleState, ArticleState[]> { { ArticleState.Draft, new[] { ArticleState.Pending } }, { ArticleState.Pending, new[] { ArticleState.Published, ArticleState.Draft } }, { ArticleState.Published, new[] { ArticleState.Offline, ArticleState.Pending } }, { ArticleState.Offline, new[] { ArticleState.Published } } }; public static bool CanTransit(ArticleState from, ArticleState to) { ArticleState[] targets; if (!Map.TryGetValue(from, out targets)) return false; return Array.IndexOf(targets, to) >= 0; } }

Map这个字典就是 4.2 表格的代码化。TryGetValue比先ContainsKey再取值的写法少一次哈希查找,执行频率高的地方值得这么写。返回false时页面要给出明确提示,比如“已发布稿件请先下架”,而不是笼统的“操作失败”。

权限部分同理:

// 栏目级权限校验:action 取 add / edit / audit / delete public static class Auth { public static bool HasColumnRight(int userId, int columnId, string action) { int roleId = UserRepo.GetRoleId(userId); // 可加 60 秒缓存 var right = RightRepo.Get(roleId, columnId); // 查 Sys_RoleColumn if (right == null) return false; switch (action) { case "add": return right.CanAdd; case "edit": return right.CanEdit; case "audit": return right.CanAudit; case "delete": return right.CanDelete; default: return false; } } }

页面里的用法是把权限判断放在数据库操作之前,而不是放在按钮的Enabled属性上——前端隐藏按钮只是体验优化,服务端不校验等于没校验,直接构造请求照样能提交。

protected void btnSubmit_Click(object sender, EventArgs e) { int uid = Convert.ToInt32(Session["UserId"]); int columnId = Convert.ToInt32(ddlColumn.SelectedValue); if (!Auth.HasColumnRight(uid, columnId, "add")) { ClientScript.RegisterStartupScript(GetType(), "noauth", "alert('无权在该栏目发布');", true); return; } var model = new Article { ColumnId = columnId, AuthorId = uid, Title = txtTitle.Text }; // 后续保存逻辑…… }

ClientScript.RegisterStartupScript比Response.Write("<script>")靠谱,后者在页面有 AJAX 局部刷新时可能不执行。第三个参数true表示自动加<script>标签,别写成false又自己拼标签,容易拼出两个嵌套标签导致整页脚本失效。GetType()作为注册键,同一页面注册多个脚本时用不同的键,否则后面的会覆盖前面的。

5. 避坑与排查:这套源码最容易翻车的五处

下面五条都是复现过程中真实会撞上的,按“看到什么现象、为什么、怎么处理”记录。前三条偏环境,后两条偏数据与权限。

5.1 跑不起来的三个现场

现象一:附加数据库时报“数据库版本为 869,无法打开,此服务器支持 852 及更低版本”。原因是.mdf来自更高版本的 SQL Server,而本机实例版本偏低,文件头里的内部版本号对不上。解决办法有两条:装一个同版本或更高版本的实例来附加;或者找提供方导出一份.sql脚本,在本机建库后执行。后者更稳,因为脚本里通常是纯 T-SQL,不携带版本信息。如果只能拿到 mdf,别尝试用兼容级别参数去糊,那个参数控制的是语法行为,不是文件格式。

现象二:页面能打开,但一进后台就报“未能加载文件或程序集 xxx,版本=x.x.x.x”。原因是bin目录里的 DLL 实际版本与web.config的bindingRedirect或.csproj里的引用版本不一致。老项目升级过 NuGet 包,但bin里留着旧的,编译时又不覆盖。处理办法:先看报错里的版本号,再对bin下同名 DLL 右键看属性版本,两者不一致就删掉旧的重新生成。如果项目引用了packages目录,确认该目录存在且路径正确——HintPath写的是相对路径,换个解压位置就失效了。

现象三:前台首页 404,但后台能进。原因是默认文档没配对,或者前台的.aspx文件不在站点根目录而在子目录里。先看项目属性里的“起始页”设的是什么,再检查 IIS 的“默认文档”列表里有没有Default.aspx。还有一种情况是用了 URL 重写但没装对应模块,web.config里有rewrite节而服务器没有URL Rewrite模块时会整站 500.19,报错信息里会写明缺少哪个模块。

5.2 数据与权限的两类隐性错误

现象四:富文本编辑器里存进去的正文,保存后只剩前几百字,或者直接报“检测到有潜在危险的 Request.Form 值”。原因有两层。第一层是请求验证:ASP.NET 默认拦截含 HTML 标签的表单值,富文本提交的正是标签,所以要在该页面的<%@ Page %>指令里加ValidateRequest="false",同时在web.config的httpRuntime上设requestValidationMode="2.0",缺一个都不生效。第二层是字段长度:Content列如果建成了nvarchar(4000),超出部分会被截断或抛异常,正确类型是nvarchar(max)。关掉请求验证之后必须补输入过滤,否则等于自己开了个存储型 XSS 的口子,白名单方案在第 6 章讲。

现象五:后台显示“发布成功”,前台刷新还是旧内容,等十几分钟才变。原因通常是缓存没失效。列表页在Page_Load里用了Cache.Insert("article_list_" + columnId, dt),但没有对应的清理逻辑,后台发布只更新了数据库。排查顺序是先看Global.asax有没有Application_Start里注册的缓存依赖,再全局搜Cache.Insert找到所有键名,在发布、下架、删除三个动作后面统一调一次清理。临时验证的办法是改web.config的compilation节加个空格触发应用重启,缓存清空后如果内容正常,就确认是缓存问题。长期方案是把键名收敛到一个CacheKey静态类里,避免手写字符串拼错导致漏清。

还有一种更隐蔽的情况:稿件状态字段存的是"2"(字符串)而查询条件写的是State = 2(数字),SQL Server 做隐式转换后能查到,但如果该列上有索引,可能因为类型转换不走索引,数据量一大列表页就变慢。核对办法是看表设计器里State的实际类型,tinyint或int是正常的,varchar就要改。

6. 二次开发验收:把毕业设计底子改到能交付的四项收口

跑通、改完权限,剩下的工作是把“能演示”变成“能交付”。这一步我一般盯四个点,每个点都有明确的验证方式,不是靠感觉。

第一是富文本的输入过滤。关掉请求验证之后,正文是原样入库的,必须加白名单。做法是保存前用正则或 HTML 解析器只保留允许的标签,其余转义:

// 白名单过滤:只保留排版常用标签,其余标签与事件属性一律清掉 private static readonly Regex SafeTag = new Regex(@"<(?!\/?(p|br|strong|em|ul|ol|li|img|a)\b)[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static string CleanHtml(string html) { if (string.IsNullOrEmpty(html)) return string.Empty; // 先干掉 script/style 整块,再干不掉白名单外的标签 html = Regex.Replace(html, @"<(script|style)[^>]*>.*?</\1>", string.Empty, RegexOptions.Singleline | RegexOptions.IgnoreCase); html = SafeTag.Replace(html, string.Empty); // on 开头的事件属性单独清一遍,避免 img onerror 这类 return Regex.Replace(html, @"\son\w+\s*=\s*[""'][^""']*[""']", string.Empty, RegexOptions.IgnoreCase); }

RegexOptions.Compiled适合调用频繁的静态正则,代价是首次编译稍慢;Singleline让点号匹配换行,否则跨行的script块清不掉。这套过滤不追求完备,够挡住最常见的注入就行,真要严格就用成熟的 HTML 净化库。

第二是分页。默认实现常见两种:一次性查全表再DataTable分页,或者用GridView自带的AllowPaging。前者在几千条数据时就开始卡。改成数据库端分页:

-- SQL Server 2012 及以上:按发布时间倒序,取第 @page 页 -- @page 从 1 开始,@size 是每页条数 SELECT ArticleId, Title, PublishTime FROM CMS_Article WHERE State = 2 AND ColumnId = @columnId ORDER BY PublishTime DESC, ArticleId DESC OFFSET (@page - 1) * @size ROWS FETCH NEXT @size ROWS ONLY;

ORDER BY里的第二排序键ArticleId不能省,发布时间相同的稿件如果没有稳定次序,翻页时会出现重复或漏条,这种问题在测试数据少的时候完全看不出来。配套索引:

CREATE INDEX IX_Article_Column_State_Time ON CMS_Article (ColumnId, State, PublishTime DESC) INCLUDE (Title);

列顺序按“等值条件在前、范围与排序在后”排,INCLUDE把列表页要显示的Title带上,避免回表。如果实例是 2008 版本,OFFSET/FETCH不支持,得换成ROW_NUMBER() OVER (ORDER BY ...)再套一层筛选。

第三是密码存储。老代码里 MD5 很常见,改造时至少加盐,SHA256加每用户随机盐也比裸 MD5 强,改动只在注册和登录两个方法里。

第四是回归验收。我习惯用一张固定清单过一遍,比凭记忆靠谱:

验收项操作预期
越权新增用只读角色账号调新增接口返回无权,库中无新记录
状态回退对已发布稿件执行“撤回草稿”操作被拒,状态不变
大附件上传 15MB 文件成功,且maxAllowedContentLength生效
分页边界总数正好为每页条数的整数倍最后一页无空白页
缓存失效发布后在另一浏览器刷新5 秒内可见新内容

这套源码我本地一直留着,改新项目时拿它当骨架比从零搭快得多。血泪经验是:每次动手改之前,先把原版压缩包另存一份,改坏了能退回去——这个后悔药成本几乎为零,但真出事的时候能省一整天。从那以后我每次拆这类老项目,都先跑一遍上面的验收清单再动代码。希望帮到你。

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

返回列表