
简介这套ASPNET自动办公系统源码面向.NET开发者基于VS2008SQL2005环境采用三层架构与标签式操作界面整合DsoFramer在线编辑、Ckeditor/Ckfinder文件管理及VML画图模块覆盖个人事务、审批流转、内部邮件和系统设置等完整OA功能。压缩包共2069个文件以js、gif、cs、html、aspx、css等为主分别对应前端脚本、界面图标、业务逻辑、静态页面、服务端页面与样式定义并包含mdf/ldf数据库文件体积仅8.86MB目录结构清晰便于快速部署与二次开发。已有1171人学习下载。审批流转模块支持起草、查询、待办、监控与归档全流程管理公文模板可自定义控件适合需要了解企业级审批流设计、办公系统模块划分及在线编辑集成方案的中高级.NET程序员参考也适用于课程设计或项目原型搭建。1. OA源码真正的价值是那套能改的审批流骨架不是报表页面拿到一份 ASPNET自动办公系统OA审批流源码我先不打开页面代码而是直接去翻数据库脚本里的 FlowNode 表数一数它支持哪些节点类型。这是多年接手老系统的习惯OA 里真正值钱的资产是审批流骨架不是公告、考勤这些能跑能看的模块。一批批 Java 系的 OA 解决方案在 Windows Server 加 SQL Server 的老环境里水土不服反而是这套老 ASP.NET 源码能匹配现场。这篇文章写给要二次开发而不是从头造轮子的 .NET 工程师它是什么、怎么改、坑在哪、值不值得投入我用实际接手经验一次讲清楚。2. 技术选型与架构为什么这套OA源码还在用WebForms而不是ASP.NET Core2.1 老OA的技术栈画像WebForms、.NET Framework、SQL Server看到 ASP.NET 自动办公系统源码先别急着问“是不是 Core”。现实中能下载、能流传的老 OA 源码绝大多数是 ASP.NET WebForms 加 .NET Framework 4.x 加 SQL Server 2008/2012 的组合少部分是 MVC 4/5 改写。原因不复杂这批系统的生命周期起点大多在 2008 到 2015 年之间那时 WebForms 是微软主推的快速开发模型拖控件、事件驱动、ViewState 自动维护状态对企业内部管理系统来说开发效率确实高。ASP.NET Core 是 2016 年之后才逐步成熟的老源码不可能用一门还没发布的框架去写。这不是说 WebForms 好而是说接手源码前先认清现实。WebForms 的 ViewState 让页面体积偏大服务端控件和前端框架的配合也别扭但它的优点同样明确开发快、状态管理省心、对低版本 IE 兼容好。企业内部 OA 的使用场景恰恰需要这种“能用就行”的稳定性。那些在技术选型阶段纠结 Core 的人往往忽略了一个事实大部分老 OA 的部署环境根本没有 Docker没有 Linux 服务器只有一台跑着 IIS 的 Windows Server迁 Core 的收益并没有想象中高。从现代视角看这批源码的价值点不在框架新旧而在业务模型。真正的资产是里面的部门树、用户权限、审批流、表单绑定这套逻辑。把 WebForms 页面迁到 Razor 页面并不难把审批流的状态机逻辑完整抽出来重写才要命。所以别一上来就喊“推倒重写”先评估业务逻辑的复用度。顺便说一句和 aspnet zero 这类商业模板相比免费流出的 OA 源码虽然框架老、代码糙但审批场景贴合国内企业习惯反而是很多定制项目的好底子。维度WebForms 老源码MVC 改写版ASP.NET Core 重写版上手门槛拖控件、事件驱动老团队熟悉前端友好、结构清晰跨平台、部署灵活部署环境Windows Server IIS同左仍需 IISLinux / Docker 可跑审批流逻辑复用度高拿过来能改中要迁移事件代码低几乎等于重写适合的接盘场景维护老系统、内部使用愿意小幅重构全新项目、云原生这套源码里最常被忽视的是它预置的审批流数据模型。国内团队看惯了泛微、致远、蓝凌这类商用 OA 的操作习惯总觉得老源码功能单薄但换个角度理解商用 OA 的表单设计器、会签/非会签机制是平台级内置能力源码项目给你的是基础引擎剩下靠二次开发补。这个区别决定了你该怎么用这份源码而不是照搬商用 OA 的期待去要求它。2.2 源码目录怎么读三层架构与三个必看位置常见做法是这套源码按三层架构组织UI 层放页面.aspx业务层放 .cs 类库数据层放 SQL 访问类。我接手时第一件事不是打开 .aspx 页面而是先看解决方案资源管理器里的项目结构。典型目录大概长这样/OA.Web UI 层aspx 页面、母版页、用户控件 /OA.BLL 业务层审批、公告、考勤等业务类 /OA.DAL 数据层SQLHelper、各实体的数据访问 /OA.Model 实体类UserInfo、DeptInfo、FlowInstance /OA.Common 公共类加密、日志、分页、Session 操作 /OA.DB 数据库脚本建表、初始化数据三个必看位置一是 /OA.DB 里的建表脚本能直接看出系统有没有审批流表、怎么设计的二是 /OA.Common 里的权限校验基类所有页面的安全检查都汇聚在这里三是 /OA.BLL 里的工作流相关类这是整个系统能不能改的关键。页面代码反而不急因为大部分 .aspx 是复制粘贴的业务操作可读性普遍一般。目录结构确认后用一条命令快速确认框架版本避免在 Visual Studio 里加载失败才想起来排查Get-Content OA.Web\OA.Web.csproj | Select-String TargetFrameworkVersion输出类似TargetFrameworkVersionv4.5/TargetFrameworkVersion一眼确认目标框架。如果是 v2.0 或 v3.5新版 Visual Studio 大概率会直接卸载项目到时候要么改版本号要么迁移项目格式。数据访问层通常有个 SQLHelper 类所有数据库操作都走它遇到 SQL 执行问题先在这个类里加日志比在几十个页面里逐个排查快得多。2.3 web.config 里的三个关键位连接串、认证模式、程序集源码跑起来之前先把 web.config 读一遍。三个位置必须搞清楚缺一个都会让部署阶段反复折腾。第一个是数据库连接串。默认配置多半是connectionStrings add nameOAConnection connectionStringData Source.;Initial CatalogOADB;User IDsa;Password123456; providerNameSystem.Data.SqlClient / /connectionStrings这个配置里的 Data Source 是全英文句点代表本机默认实例。放到正式服务器上要改成服务器 IP 或实例名比如Data Source192.168.1.10\SQLEXPRESS。Initial Catalog 是数据库名OADB 是默认名字导入脚本时如果改了库名这里必须同步。密码不要用初始的 123456 这类弱口令尤其是暴露在内网的运维后台默认口令被扫到就是整个系统的入口。第二个是认证模式。老代码大部分是 Forms 认证authentication modeForms forms loginUrl~/Login.aspx timeout30 slidingExpirationtrue / /authenticationloginUrl 指向登录页timeout 是登录态超时分钟数slidingExpiration 为 true 表示用户一直在操作就不掉线。如果系统出现过一会儿就跳回登录页的问题优先查这里其次才是 Session 配置。注意这里 timeout 和后面 Session 的 timeout 是两个独立配置很多人只改一处结果还是掉线。第三个是 machineKey。单独一台服务器部署时可以省略但只要以后打算做负载均衡或多台 IIS 前端就必须显式配置machineKey validationKey换成随机生成的64位密钥 decryptionKey换成随机生成的32位密钥 validationSHA1 /不配置 machineKey 的后果很隐蔽两台服务器各自生成不同的表单认证票据用户在第一台登录第二台不认表现为“间歇性要求重新登录”。这个坑在 OA 这类长期运行的系统里特别伤人因为没有明显报错只会觉得 Session 不稳定。顺手把编译节点里的debugtrue也关掉老源码经常带着调试开关上生产页面报错会把完整堆栈暴露给访问者。3. OA高频模块的落地写法登录校验、部门树、公告回执3.1 用BasePage基类统一做登录校验别在每个页面重复写OA 系统页面多最忌讳每个页面各自写登录判断。正常源码里会有一个公共基类所有业务页面继承它在 Page_Load 之前完成校验。我一般会这样实现public class BasePage : System.Web.UI.Page { protected override void OnLoad(EventArgs e) { if (Session[UserInfo] null) { Response.Redirect(~/Login.aspx?returnUrl Server.UrlEncode(Request.Url.PathAndQuery)); return; } base.OnLoad(e); } }这段代码的逻辑不复杂Session 里没有用户信息就跳登录页并把当前页面地址作为 returnUrl 带过去登录成功后跳回原页面。这里的时机很关键OnLoad 的执行在 Page_Load 之前能拦下后续所有初始化逻辑。参数说明Session[UserInfo] 是登录成功时写入的用户实体有的源码写成 UserId 或 UserName具体看公共类里的定义returnUrl 一定要做 UrlEncode否则参数里有中文或特殊字符会被截断。基类只负责“有没有登录”具体“有没有权限”要放在业务层判断。审批提交这类敏感操作页面按钮隐藏只是第一步后端必须再验证一次protected bool CheckAuth(string permissionCode) { UserInfo user Session[UserInfo] as UserInfo; return permissionService.HasPermission(user.UserId, permissionCode); }这个方法放在审批按钮的 Click 事件开头返回 false 就提示“无权限执行此操作”。老源码最大的安全漏洞之一就是只做前端按钮权限没做后端校验随便拼个 URL 就能越权操作。如果这套系统要接公网这一步是必须补的。3.2 部门与审批人选择树递归加载与懒加载的取舍OA 的部门和审批人选择是高频交互几乎每个审批单都要选“下一个处理人”。老源码里常见做法是后端递归拼接树形 HTML再塞进前端控件。代码不算优雅但可靠private void BuildTree(int parentId, StringBuilder sb) { DataTable dt deptBll.GetDeptsByParent(parentId); foreach (DataRow row in dt.Rows) { int id Convert.ToInt32(row[DeptId]); string name row[DeptName].ToString(); sb.AppendFormat(li iddept_{0}{1}, id, name); sb.Append(ul); BuildTree(id, sb); // 递归进入子部门 sb.Append(/ul/li); } }递归的出口是 GetDeptsByParent 返回空表这一层自然结束。拼接出来的 ul/li 结构配合 jQuery 插件显示为可展开的树点击叶子节点时再触发人员列表的加载。这里有两个容易踩的参数位一是 parentId 为 0 的约定顶级部门用 0 表示建表脚本里最好写死别搞出 null二是递归层数组织架构如果超过七八层递归写法栈深度没问题但前端渲染会明显变慢几千人规模时用户体验很差。点击部门加载人员的写法通常是这样private void LoadPersons(int deptId, StringBuilder sb) { DataTable dt userBll.GetUsersByDept(deptId); foreach (DataRow row in dt.Rows) { sb.AppendFormat(input typecheckbox value{0} /{1}, row[UserId], row[UserName]); // 审批人选择场景value 存 UserId提交时取选中的值 } }参数说明deptId 是当前部门的主键GetUsersByDept 只查该部门直属人员要不要含子部门人员要看业务。报销这类场景通常按部门选审批人含子部门反而会选错人。如果部门层级超过四层或人员超过几千最好改成懒加载点击节点时再用 Ajax 请求下一层别一次性拼全量树。这是老源码最常见的性能瓶颈功能没错就是慢。3.3 公告发布与已读回执一张幂等表解决统计需求公告是 OA 里看似简单、实际容易做错的功能。很多源码只做了“发布”和“列表展示”没有回执导致无法知道谁没看。加已读回执的核心是两张表公告表和已读记录表。发布公告时插入公告主表用户第一次打开详情时插入一条已读记录public void MarkAsRead(int noticeId, int userId) { string sql IF NOT EXISTS ( SELECT 1 FROM NoticeRead WHERE NoticeIdNoticeId AND UserIdUserId) INSERT INTO NoticeRead(NoticeId, UserId, ReadTime) VALUES(NoticeId, UserId, GETDATE()); SqlParameter[] p { new SqlParameter(NoticeId, noticeId), new SqlParameter(UserId, userId) }; sqlHelper.ExecuteNonQuery(CommandType.Text, sql, p); // 参数化查询防止拼接 SQL 注入 }这段 SQL 的关键是 IF NOT EXISTS保证同一用户重复打开时不产生重复回执记录。这是典型的幂等写法在回执、审批记录这类写入操作里应该成为习惯。参数说明NoticeId 对应公告主键UserId 对应登录用户的 UserIdReadTime 用 GETDATE()避免应用服务器和数据库服务器时间不一致。有了回执表“已读未读统计”就是一条 GROUP BY 查询SELECT n.NoticeId, n.Title, COUNT(r.Id) AS ReadCount, (SELECT COUNT(*) FROM SysUser) AS TotalUser FROM Notice n LEFT JOIN NoticeRead r ON n.NoticeId r.NoticeId GROUP BY n.NoticeId, n.Title;统计需求出来后按部门催办就有依据了查某个部门哪些人没读直接对 NoticeRead 取反即可。老源码里这一步常有坑比如 ReadTime 用应用服务器时间、重复插入回执导致统计翻倍这两处改掉整个公告模块才能支撑行政日常使用。4. 审批流的实现四张表、一个状态机、一份表单绑定4.1 审批流的数据模型流程定义、节点、实例、审批记录审批流是整个 OA 源码里最值得研究的部分也是标题里“审批流”三个字的落点。先看表设计一般至少四张表流程定义表 FlowDefine、流程节点表 FlowNode、流程实例表 FlowInstance、审批记录表 FlowLog。关键字段对应关系如下表名关键字段作用FlowDefineFlowId, FlowName, CreatorId, CreateTime定义“请假审批”这类流程的元数据FlowNodeNodeId, FlowId, NodeName, NodeType, ApprovalRoleId, NextNodeId定义流程节点、处理角色、下一节点FlowInstanceInstanceId, FlowId, ApplicantId, CurrentNodeId, Status, SubmitTime一条具体的审批单据当前走到哪一步FlowLogLogId, InstanceId, NodeId, OperatorId, ActionType, Comment, LogTime每一步通过/驳回/转交的原始记录建表脚本大致这样CREATE TABLE FlowNode ( NodeId INT IDENTITY(1,1) PRIMARY KEY, FlowId INT NOT NULL, NodeName NVARCHAR(50) NOT NULL, NodeType INT DEFAULT 0, -- 0普通审批 1会签 2抄送 ApprovalRoleId INT NOT NULL, -- 处理角色关联角色表 NextNodeId INT DEFAULT 0 -- 0 表示流程结束 ); CREATE TABLE FlowInstance ( InstanceId INT IDENTITY(1,1) PRIMARY KEY, FlowId INT NOT NULL, ApplicantId INT NOT NULL, CurrentNodeId INT DEFAULT 0, Status NVARCHAR(20) DEFAULT RUNNING, -- RUNNING/COMPLETED/REJECTED Content NTEXT, -- 表单内容的 JSON/XML 序列化 SubmitTime DATETIME DEFAULT GETDATE() ); CREATE TABLE FlowLog ( LogId INT IDENTITY(1,1) PRIMARY KEY, InstanceId INT NOT NULL, NodeId INT NOT NULL, OperatorId INT NOT NULL, ActionType NVARCHAR(20) NOT NULL, -- PASS/REJECT/TRANSFER Comment NVARCHAR(500), LogTime DATETIME DEFAULT GETDATE() );四张表的关系要记牢FlowDefine 是模板FlowNode 是模板里的节点FlowInstance 是模板被实例化后的单据FlowLog 是单据在每个节点的操作轨迹。最常见的建模错误是把节点直接写成流程表里的一列比如“第一个审批人”“第二个审批人”看起来简单一旦要调整节点顺序表结构就得动。泛微、致远这类商业 OA 里讲的会签、非会签在这套模型里对应的就是 NextNodeId 指向同一个节点多人会签还是指向不同节点顺序审批。看源码时先找这个字段能快速判断流程引擎的复杂度。4.2 核心流转方法通过、驳回、转交与并发保护审批流的动作归纳起来就三个通过、驳回、转交。这三个动作共用同一个方法骨架先鉴权再改状态再写日志。我写一个简化但不失真的核心方法public int ProcessFlow(int instanceId, int nodeId, int operatorId, string action, string comment) { FlowInstance fi flowDal.GetInstance(instanceId); // 1. 鉴权只有当前节点的处理人才能操作 if (fi.CurrentNodeId ! nodeId || !IsOperator(operatorId, nodeId)) return -1; // 无权操作 // 2. 按动作更新流程状态 if (action PASS) { int nextNode flowDal.GetNextNode(nodeId); if (nextNode 0) { fi.Status COMPLETED; // 没有下一节点流程结束 fi.CurrentNodeId 0; } else { fi.CurrentNodeId nextNode; } } else if (action REJECT) { fi.Status REJECTED; fi.CurrentNodeId -1; } else if (action TRANSFER) { fi.CurrentNodeId transferTargetId; // 转交指定人节点 } // 3. 写日志先落日志再改状态状态变更前保留完整证据 flowDal.InsertLog(instanceId, nodeId, operatorId, action, comment); // 4. 持久化实例状态 flowDal.UpdateInstance(fi); return 0; }这段代码最要紧的是第一步鉴权。很多审批流系统“卡死”不是引擎坏了而是非当前处理人点了一下提交按钮把状态改乱了。加个 IsOperator 判断让非法操作直接返回错误码再配合前端提示能挡掉九成误操作。参数说明action 的三个取值 PASS、REJECT、TRANSFER 建议定义成常量类而不是魔法字符串否则后期容易拼错nextNode 0 表示没有下一节点时流程置为 COMPLETED这个 0 的约定必须和 4.1 表里的数据一致CurrentNodeId -1 是驳回终态-1 这个值别随意改。再说并发保护。两个审批人同时在两个页面点“通过”后提交的请求可能把先提交的状态覆盖掉。解决方式是在 UpdateInstance 时加上条件public int UpdateInstanceIfRunning(FlowInstance fi) { string sql UPDATE FlowInstance SET StatusStatus, CurrentNodeIdCurrentNodeId WHERE InstanceIdInstanceId AND StatusRUNNING; // 返回受影响行数0 表示流程已被其他请求改动 }调用时判断返回值如果影响行数为 0说明流程在并发下已经变更直接返回 -2 让页面提示“单据已被处理请刷新”。这个 -2 分支看着小实际是线上审批流程不会出乱子的保障。我遇到过真实案例部长和分管副总同时批同一张单状态互相覆盖最后单据显示了两个审批结果数据彻底对不上只能手工修库。驳回后重新提交也是高频改动点。很多老源码的 REJECT 就是终态单子废了重新填实际业务里“驳回修改后重新提交”更常见。改法不复杂驳回时不置 -1而是把 CurrentNodeId 退回上一节点Status 改成 EDITING重新提交时从 EDITING 回到该节点继续走。这个逻辑一旦放开状态机就从一条直线变成带环的图测试用例必须覆盖“反复驳回三次后再通过”的路径。4.3 审批表单与流程绑定Content 大字段还是单独加列审批单除了流程状态还要承载表单内容请假天数、报销金额、事由说明。老源码里最常见的方案是“流程实例表加一个 Content 大字段”把表单内容序列化成 JSON 存进去。页面提交时收集表单控件走流程时原样读取// 提交时把表单内容序列化存进流程实例 var formData new Dictionarystring, string { { leaveDays, txtDays.Text.Trim() }, { leaveType, ddlType.SelectedValue }, { reason, txtReason.Text.Trim() } }; System.Web.Script.Serialization.JavaScriptSerializer js new System.Web.Script.Serialization.JavaScriptSerializer(); fi.Content js.Serialize(formData);读取展示时反序列化var result js.DeserializeDictionarystring, string(fi.Content); if (result.ContainsKey(leaveDays)) lblShow.Text 请假天数 result[leaveDays];这种设计的好处是流程引擎与表单结构解耦新增一种审批类型不需要改流程表只要换一套表单页坏处是无法用 SQL 统计表单字段比如“查所有请假超过 5 天的单子”必须把 Content 取出来反序列化后再过滤。泛微、致远那类商用 OA 的做法是做成表单设计器字段配置存数据库、页面动态渲染源码项目做不到那么完整但保留 JSON 序列化方案已经能支撑绝大多数审批场景。如果准备在这套源码上长期做二次开发建议给高频的审批类型单独加字段列比如 FlowInstance 里加 LeaveDays、Amount 这类常用列Content 只放低频业务。这样报表查询走索引性能不会掉。这是从“能跑”到“好用”的一个关键改造点也是同源码接不同企业时最能体现工作量的地方。顺带提一个安全点有些源码把表单内容序列化成 XML 存档反序列化时要注意 XXE 类风险。XML 解析器如果没关外部实体恶意提交的表单内容可能读取服务器本地文件。老系统没暴露公网时可以不管一旦挂了公网入口这个位置必须按 JSON 方案重写或显式禁用外部实体。5. 从源码到上线部署与二次开发避坑指南5.1 项目打不开Framework版本与解决方案格式不匹配现象双击 .sln 文件Visual Studio 提示“项目文件被卸载”或“此版本不兼容”连编译的机会都不给。 原因老 OA 源码多数基于 .NET Framework 4.0 或 4.5 编写新版 VS 默认模板可能不带对应 targetFramework也可能是解决方案文件格式太老VS 2008 时代的 .sln 文件在新版里不再直接支持。 解决先用记事本打开 .csproj 文件找到TargetFrameworkVersion节点确认版本v4.0 可以直接改成 v4.8 后重新加载通常能编译。如果 .csproj 是 VS 2008 时代格式最省事的办法是新建一个同类型 Web 项目把 .aspx、.cs、web.config、样式目录整个拷进去再引用一天内能恢复编译。改版本前先备份这是翻车现场最常见的救急手段改崩了还能还原。5.2 IIS部署后全站500托管管道模式选错了现象本地编译发布后访问任何页面都是 “Server Error / 500”静态 html 正常动态页全炸。 原因应用池的托管管道模式不对。老 WebForms 项目对经典模式Classic兼容最好新版 IIS 默认集成模式Integrated某些 HttpModule 在集成模式下直接冲突。 解决在 IIS 对应网站的应用池上右键“高级设置”把“托管管道模式”从 Integrated 改成 Classic回收应用池再试。如果改成 Classic 还是 500去 Windows 事件查看器看应用程序日志拿到具体异常类型再定位常见还有目录权限问题给网站目录加 IIS_IUSRS 读取权限数据库连接串里的账号在服务器上有访问权限。这个坑几乎是老 ASP.NET 项目部署的必踩项遇到 500 先别翻代码多半是环境配置。5.3 连不上SQL ServerTCP/IP、防火墙与实例名现象页面提示数据库连接错误或报表模块超时本机能跑服务器上不行。 原因本机开发时 Data Source 用点号代表本机默认实例换服务器后没改成服务器地址老 SQL Server 2008/2012 默认不开 TCP/IP 协议远程连接会被拒防火墙也可能挡了 1433 端口。 解决在 SQL Server 配置管理器里启用 TCP/IP 协议重启 SQL 服务防火墙放行 1433 端口连接串里写成IP,端口或IP\实例名的格式。测试连通性别用 ping用处不大直接一行命令验证sqlcmd -S 192.168.1.10 -U sa -P 密码 -Q SELECT 1能返回结果说明网络和账号都通。如果用了混合身份验证确认 SQL Server 的登录模式选的是“SQL Server 和 Windows 身份验证模式”而不是仅 Windows否则 sa 永远连不上。这一步排查完数据库层的坑基本就清干净了。5.4 审批卡在“审批中”数据对不上与鉴权失败现象提交审批单后当前处理人点“通过”没反应列表里单据仍显示“审批中”。 原因常见是 ProcessFlow 里鉴权失败返回了 -1但页面代码只弹了个“操作失败”没暴露具体错误或者是流程实例里 CurrentNodeId 和 FlowNode 表的 NodeId 对不上GetNextNode 查不到下一节点。 解决先在数据库里跑两条查询对比SELECT InstanceId, CurrentNodeId, Status FROM FlowInstance WHERE InstanceId 123; SELECT NodeId, NextNodeId FROM FlowNode WHERE FlowId (SELECT FlowId FROM FlowInstance WHERE InstanceId 123);如果 CurrentNodeId 在节点表里根本不存在就是流程实例数据被改坏了手动把 CurrentNodeId 修正为合法节点值再继续走。如果数据没问题就在 ProcessFlow 的鉴权处加日志输出把 operatorId 和 nodeId 打出来确认是不是处理人身份判断错了。这类问题的通病是接口异常被页面吞掉修复的同时把异常输出到日志文件以后能少很多猜测。5.5 Session频繁掉线应用池回收与进程内Session的坑现象用户操作频繁时还好停顿十来分钟再点页面就跳登录页有时上午登录下午就掉了。 原因进程内 Session 存的是应用池进程的内存应用池一回收就全没了。老源码默认SessionState不配置IIS 应用池的“空闲超时”默认 20 分钟用户不操作触发回收Session 清空。 解决先把应用池的“空闲超时”调大比如 120 分钟或设为 0不回收。再看 web.config 的 forms 认证 timeout 与 Session 超时是否一致。更彻底的方案是把 Session 改成 SQLServer 模式sessionState modeSQLServer sqlConnectionStringdata source192.168.1.10;user idsa;passwordxxx cookielessfalse timeout60 /用 SQLServer 模式后 Session 存进数据库应用池怎么回收都不丢。但要留意一个隐藏陷阱放进 Session 的实体类必须加[Serializable]标签否则启动后第一个写 Session 的操作直接抛异常。这个问题在改造时经常遇到老源码的实体类大多没打这个标签改动量不大但容易让人误以为是配置问题排查半天。6. 进阶把审批流状态机做成可自检的再谈二次开发搜整套 ASP.NET OA 源码的人目标大多不是原样部署而是要在这个骨架上长出自己公司的业务。动手改任何业务前我建议先给审批流状态机写一段自检代码。所谓自检就是把一条审批单从发起、通过、驳回到重新提交的完整路径跑一遍看每个节点的状态是否符合预期。// 用一个动作序列模拟整条链路提交 - 两级审批 - 驳回 - 重新提交 - 通过 string[] actions { SUBMIT, PASS, PASS, REJECT, SUBMIT, PASS, PASS }; foreach (string act in actions) { int result ProcessFlow(instanceId, currentNodeId, currentNodeOperator, act, 自检); if (result -1 || result -2) { Trace.WriteLine($自检失败动作{act}当前节点{currentNodeId}返回{result}); break; } } // 最后断言流程应结束状态为 COMPLETED不要小看这段模拟执行它能在一分钟内暴露状态机最隐蔽的问题驳回后是否允许重新提交、重新提交后是否回到正确节点、并发更新时会不会返回 -2。我用这个套路修过一条卡了半年的审批链原因就是“驳回即终态”被写死后来改成“驳回后可提交并回到上一节点”才让业务真正流转起来。如果这套源码要用于合同、用印、报销等其他场景也是同一个步骤先画状态图再把每个动作的期望结果写成断言最后让测试数据跑一遍。验证完状态机再谈个性化配置把写死的审批链改成可配置是这套源码最值得投入的方向。做法是做一个流程设计页让管理员用下拉框把节点顺序维护进 FlowNode 表业务代码里去掉固定链路判断全部走 NextNodeId。这一步做完系统才算从“固定审批流”升级成“可配置审批流”后续接新流程就不用动代码了。其他像“按部门抄送”“金额超过一万自动跳到总经理”这类规则也可以在状态机自检稳定之后再叠加避免一步到位把老骨架改散架。我自己的习惯是每次改完流程代码直接复制一份线上真实数据跑一遍回归场景再上生产这比什么保障都实在。希望帮到你。本文还有配套的精品资源点击获取