解析大型.NET ERP系统架构设计 Framework+ Application 设计模式
我对大型系统的理解,从数量上面来讲,源代码超过百万行以上,系统有超过300个以上的功能,从质量上来讲系统应该具备良好的可扩展性和可维护性,系统中的功能紧密关联。除去业务上的复杂性,如何设计这样的一个协作良好的系统,搭建开发人员基础平台,一直是我研究的方向。
SouceCounter(版本3.3.91.79)对源代码的统计信息如下:
下面来详细解析一下这个系统的设计架构,纯.NET技术架构方案,C/S WinForms系统。
系统分为Framework和Application两个部分,前者是框架(Framework),包含核心的基础功能,如公共类库,许可授权,数据字典,公共控件,公共窗体,多线程组件,通信基础,会话管理等基础部分,后者是应用部分,根据业务逻辑的不同,对于ERP系统而言,可分为进销存(Distribution),工程(Engineering),生产(Production),生产计划(Production Planning),财务(Account Receivable,Account Payable,General Ledger),客户关系(CRM)等模块。
Framework 开发框架
1. Common 公共类库
- 定义异常类型,用于系统异常处理:LicenseException 许可授权异常,字段验证异常FieldValidationException,实体验证异常,应用程序异常AppException,水晶报表异常CrystalReportException。
- 定义版本信息,设定四个版本字段的值
public const string Major = "3"; public const string Minor = "4"; public const string Build = "0"; public const string Revision = "0";
- 定义许可授权方案, SystemLicense系统许可文件,HaredwareInformation硬件信息。SystemLicense中包含一个核心的方法Verify,用于验证授权许可,比如过期时间,版本(Standard标准版,企业版Enterprise),硬件信息,参数设置(最大用户数,可用模块数量,可用数据库)。
public enum LicenseType { Internal, Standard, Enterprise }
- 优化.NET DataSet传输,主要是AdoNetHelper,这个类型来自CodeProject上的一篇文章,标题是Optimize .NET DataSet,完全自定义DataSet在.NET Remoting服务器和客户端传输方法。
- 定义帮助类库,DataSetHelper数据集操作,CrystalReportHelper水晶报表帮助类,WmiHelper操作系统WMI帮助类,加密与解密帮助类RSACryptionHelper,SafeInvokeHelper 用于COM操作帮助类,数据库操作帮助类SqlHelper,StatisticsHelper数据统计帮助类,StrongNameSignatureHelper强命名验证帮助类。
- FTP文件传输 System.Net.FtpClient,这个引用于netftp,有很多例子代码可快速入手。
- 类型转换 Type Converter,在设计ASP.NET控件的时候会经常遇到这个类型,用于将ASPX中的文本定义转化为.NET类型,比较常用的如bool值转换,0转换为false,1或其它值转化为true,字符Y转化true,字符N转化为false,MSDN中很多TypeConverter的例子可以参考。
- Excel导入类,对于Excel导入类,对功能的唯一的要求是不能一定要求安装Microsoft Excel,客户的服务器中可以安装的软件非常少,所以全盘否定微软自带的Office Interop。
- 定义系统常用的资源文件,全部以嵌入的资源内置在程序集中。
2. Component 公共控件与组件
- Editor 编辑控件,文本编辑控件TextEditor, 日期编辑控件DateTimeEditor,下拉选择控件ComboEditor,多行明细数据编辑控件Grid,单选控件CheckBoxEditor,图片编辑控件PictureBox,富文本编辑控件RichTextBoxEditor,数字编辑控件NumericEditor,时间编辑控件TimeEditor,甘特图编辑控件GanttaEditor。
- Calendar日期控件 日期控件用于显示当前时间范围内的任务(工作单,生产计划,送货计划),可参考Outlook中的日期控件,代码来自于CodeProject中的文章。
- Form 窗体基类,这是很核心的部件,子类窗体的代码能否简洁高效,全在于这个地方。定义FormBase类型,包含基本的功能:多语言(可读取界面上所有控件进行Text属性重设置),布局可持久化(Layout Persistence),用于将用户修改过的界面偏号保存,常用于Grid列顺序修改,SplittContainer上下面板位置大小调整,自定义布局(Custom Layout) 用于窗体设计器修改界面布局,窗体快捷键定义,比如将Enter键重写成Tab键方便跳转。
- Query Form/Enquiry Form 查询窗体基类,用于定义通用查询界面,用户将一个SQL语句,或存储过程,或是通过查询生成工具生成一个查询,通过这个界面基类解析产生界面和产数据。
- Crystal Report Form 水晶报表基类,用户设计完报表,放置到配置的目录中,即可查看水晶报表,方便打印设定,报表多语言,报表参数传递,报表导出为Excel或PDF,报表生成为指定格式发送到用户邮件。
- Error Dialog 异常对话框,CodeProject中有一篇文章是讲解如何获取.NET堆栈的信息,当系统业务抛出异常时,需要将此异常信息封装,比如获取当前程序的版本,构建时间等封装传递到异常对话框,在异常对话框中点一个简单的复制到剪贴版按钮即可将出错的信息收集起来,这样方便程序员诊断问题。
- Form Designer 窗体设计器,用于自定义布局Layout,修改之后将布局保存为Xml文件,FormBase会预先在自定义布局表(FormLayout)中搜索此功能的布局定义,如发现有则加载。.NET CLR Form Host中有文章介绍如何生成Xml文件定义的内容,简单的理解可以将Visual Studio定义为一个代码生成工具,拖放控件到窗体中会生成不同的源代码,C#,VB.NET,还有Xml,Visual Studio不支持生成Xml格式界面配置,但CLR Form Host可以实现这一点。
- Graph 图表基类 用于以图表的方式呈现数据,柱子图/饼图/点状图/曲线图/K线图,打开Excel的图表功能即可看到各种类型的图表,Visual Studio 2009内置了丰富的图表控件,进行简单的封装即可。
- Field Range Control 查询组件 涉及到ORM的查询,只需要给这个控件填写到实体名称和属性名称,它即可为我生成查询条件,这样在设计时很方便设计查询条件,节省了大量的拼接条件的代码。
- Condition Editor 条件编辑控件,主要用于工作流中的IF-ELSE活动的条件编辑。
- BackgroundWorker 多线程组件,用于多线程处理,.NET系统已经包含很实用的后台线程控件BackgroundWorker,避免主界面卡死,也实现了多线程处理,但是这个控件需要依托在Form窗体中,需要增加一个WorkerThreadBase,参考Code Project中的文章。
- Misc 杂项 SQL编辑器控件,系统中比如生成查询语句,工作流的条件设置,预警提醒的查询语句等地方会涉及到SQL显示或书写,有一个语法高亮的编辑控件会给系统增色不少,Code Project上有大量的SQL Hightlight Editor控件可供使用,Auto CAD图纸显示控件,工程模块的物料清单可能会增加图纸附件,需要联机在线查看DWG格式的图纸,桌面提醒控件 Desktop Alert 这个的参考例子是MSN的消息提醒,Outlook新邮件到时的提醒,参考Code Project上有许多类似控件。Tab MDI布局控件(DockManager),这个组件也相当重要,因Infragistics中就有一个这样的组建,开箱即用,可以将传统的MDI布局改成Tab MDI,不需要修改代码。图表控件Image Control用于给图片设定缩放模式,摄像头控件,用于人事系统中的员工主档给员工拍摄相片,浏览器控件Web Browser用于显示网页,重写.NET系统提供的控件,修改几个属性,关闭错误脚本提示。
3. 预定义基础功能 Administration UI
设计精良的系统应该先预定义好一系列的基础界面,用于管理框架功能中的元数据,这一节分两个部分讲解,一是框架要定义什么,二是如何去实现这些功能。先来看一下框架数据库Framework有哪些基础的元数据表定义:
先写一个基础查询语句,用于查询框架系统数据库的所有表。
SELECT * FROM sys.tables ORDER BY name
数据表 | 定义 |
Attachment | 系统中所有功能的附件,可直接存储文件或是存放一个FTP文件路径 |
Branch / BranchDetail |
实现多库存组织 |
Company | 实现多公司帐套 |
Configuration | 系统参数配置 |
ContextFunction ContextFunctionDetail |
系统上下文菜单项定义 |
Dictionary |
可变的数据字典定义,用于可修改的数据字典 |
FormLayout
|
自窗义界面(Form Designer设计之后保存的Xml文件) |
FormProfile | 用户偏号(网格排序,控件位置或大小拖动) |
LanguageTranslation | 多国语言翻译 |
Component | 系统启动时读取的程序集 |
Lookup | 查找数据对话框 |
Message
MessageDetail |
消息盒子 |
Report | 报表参数定义 |
ScheduledTask | 系统预置的计划任务 |
SystemModule
SystemFunction |
系统包含的模块与功能 |
User | 系统用户 |
UserDefinedQuery | 预定义查询 |
UserGroup | 用户组别 |
UserGroupAuthorization | 用户组别授权 |
UserGroupMenu | 用户组别菜单 |
UserGroupMenuBitmap | 用户组别导航图 |
UserLog | 用户日志 |
Workflow | 工作流定义 |
- 可修改的数据字典,比如员工工作等级,从T1到T4,再到M1,T表示Technican技术员,M表示Manaer经理,这些都可以根据实现需要增减。不可修改的数据字典如性员Gender,只有Male或Female两种选项,不能修改,这种不可变的数据字典直接用代码写死。
- 系统预置的计划任务,常用于工作流中的消息提醒或报表生成需求。前者如当库存不够时发送消息提醒采购人员,后者用于每周定期生成老板报表发送到老板邮箱中。
- 工作流定义保存工作流设计器生成的XOML文件。Visual Studio不支持生成XOML格式的工作流定义,但.NET Workflow Host可通过修改输出获取工作流设计器生成的工作流定义文件。
- 单据编码/单据序列号 有的客户偏好于放在业务数据库中,在上表中没有提到。
通过上面框架数据库表的定义即可看到框架的基础功能,也就是对以上数据进行读写。我按照窗体的类别简单介绍。
- 登入退出类功能:LoginDialog用户登入,登入成功后可显示Splash Screen,之后进入主界面Main Form,用户退出时,还要检测当前是否有窗体的数据没有保存(dirty)逐个提醒用户保存数据或是放弃修改,用户可能会修改登入密码,用户也可能做一些菜单或功能的自定义。
- 设计类功能:Query Designer 设计查询,将查询文本保存到数据表UserDefinedQuery中。Report Dialog Designer用于维护报表参数,报表的设计由水晶报表设计完成,实在没有精力去维护一个Report Designer,所以直接用功能强大的水晶报表,在报表对话框中只需要维护报表的参数,这样不用写代码即可完成将参数值传递到报表中。Workflow Designer用于设计工作流,保存结果XOML到Workflow表中。Form Designer用于修改自定义布局,隐藏或调整控件位置,或修改查询条件。Lookup Designer 用于设计查找窗体,直观的解释是当光标放到客户编号控件中时,此控件会显示一个小按钮,点击按钮会弹出客户编号选择对话框,这里的设计查找窗体,也就是设计这个客户编号选择对话框。
- 应用程序类功能:控制应用程序的启动,关闭,控制通信层,实现Application Recovery模式,这个功能可以从Windows 7 Code Package中获取源代码参考。当应用程序崩溃时,可以提示重新启动再恢复崩溃前的数据。
- 自动更新类功能:支持从FTP,HTTP,XCOPY等方式下载最新版本的文件更新系统。目前没有实现调用微软的Background Intelligent Transfer Service,也没有实现调用第三方下载API的功能,不支持断点续传或是快速的大文件下载。调用迅雷API速度很快的原因可能是要求服务器中有备份文件,这对于企业应用来说不可行,另外第三方的下载API会遭遇限速等困扰,还是老老实实的实现局域网内的快速稳定更新。
- 通用附件类功能:实现一个通用的附件管理模块,其它功能只需要设置一个SupportAttachment=true属性即可拥有附件管理功能。附件可保存在数据库或FTP文件服务器中。
4 工作流基础 Workflow Essential
工作流实现的四大基础功能:通知提醒,批核,计划任务,调用自定义代码。
通知提醒:ERP系统中包含大量的提醒功能,每加一个业务功能就写一遍提醒功能的代码调用显得有些繁琐,在此只做一个设置即可实现通知设置,包括要通知的人员,通知的内容。
批核:框架应该抽象出所有单据的批核需求,建立一个独立的批核系统,任何功能只需要简单设置一下即可调用工作流代码,实现批核流程。
计划任务: 系统中有一些定期执行的任务,比如员工生日提醒,库存余额报警,待收货记录提醒,老板报表定时发送。
调用自定义代码:如果系统中的功能存在缺陷defect,软件公司不愿意修改的情况下,可以考虑增加自定义的.NET代码解决问题。只需要在合理的事件点上插入合适代码,完成重复的数据修复工作。
基于微软工作流的解决方案,先看一下包含的基础组件:
Activities 活动库,活动是工作流定义中一个基本的代码执行单元,可固化执行的代码均可封装到一个活动中。包含文档批核活动,发送报表活动,查询活动,调用.NET代码活动等。调用.NET代码活动的设定方法参考如下:
assembly=Microsoft.Applications.MyReportDll; class=Microsoft.Applications.MyReportDll.EmployeelistingDAL; method=GetEmployeeListing();
熟悉.NET框架反射调用方法的朋友一看就明白上面定义的含义,这个活动的源代码可以简化如下所示:
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { assembly = Assembly.Load(segments[0]); type = assembly.GetType(segments[1]); method = segments[2]; type.GetMethod(method, BindingFlags.Public | BindingFlags.Static).Invoke(null, null); }
Contracts 接口与实现 在第一步活动定义中,大量的调用了WF中的CallExternalMethod活动,这个活动用于调用外部自定义方法,实现接口与实现的分离。在设计活动时只需要指定要调用的接口和方法,具体的实现可根据需要变化。
.NET WF要求在启动WF Runtime时,需要先注册要执行的服务,代码参考如下:
SqlWorkflowPersistenceService persistenceService=new SqlWorkflowPersistenceService (ConnectionString);
runtime.AddService(persistenceService);
在活动的代码中,可通过以下的方式引用经过注册的服务:
SqlWorkflowPersistenceService persistenceService=context.GetService<SqlWorkflowPersistenceService>(); //调用 persistenceService服务的方法
这样,服务中可调用ERP代码的接口,实现了ERP逻辑与.NET WF工作流的整合。
Workflows 给每种常见的流程定义一个工作流类型,方便做持久化和验证工作。
Workflow Designer Control re-host工作流定义组件,直接参考借用MSDN中的例子。
Monitor 工作流监控,查看流程的执行情况,当前执行结点,执行路径。
5 服务器体系 Server Series
从功能上来讲,系统应该具备以下四个基础服务器,实现数据读写分离。
Application Server 业务逻辑服务器,.NET Remoting服务器端。
Report Server 报表服务器,用于报表呈现,减低Application Server的压力。
Workflow Server 工作流服务器 执行工作流。
Scheduling Server 计划任务服务器,减低Application Server的压力。
每种服务器都配置Console版和Service版,代码完全一样,Console以控制台程序呈现,Service以Windows 服务应用形式实现,前者方便开发,后者用于部署和实际使用。
Application 应用程序
根据ERP项目的功能分类,分为进销存(Distribution),工程(Engineering),生产(Production),生产计划(Production Planning),财务(Account Receivable,Account Payable,General Ledger),客户关系(CRM)等模块。每个模块独立为一个Visual Studio Project,编译成一个程序集。通过插件式结构,实现使用时只需要在Component表中插入一行记录,即可让系统识别到此程序集,运用反射方法调用程序集中的功能。
1 业务实体与业务逻辑 Business Logic
如果是用LLBL Gen Pro开发系统,则业务实体层具备以下文件夹层次结构。
以销售单表头为例子,数据库表SalesOrder,生成实体为SalesOrderEntity,设计读写接口文件为ISalesOrderManager, 接口的实现类为SalesOrderManager,用简单的图表示如下:
SalesOrder –> SalesOrderEntity –> ISalesOrderManager –> SalesOrderManager
系统强制执行以上约定,并且设计了Code Smith模板代码生成来减少出错的可能。既提供强制性约束,又提供工具辅助开发人员遵守约定,系统开发效率成倍提升。
业务实体层还实现了数据审计(Audit)功能,记录表的每个记录的修改值。刚毕业参加工作时,常常混淆Audit和Approval的区别,现在一些系统还存在用Audit作为批核的意义。
2 数据字典 Enum
定义系统中不可变的数据字典,虽然用代码写死字典的方法值得商议,但它的好处也是非常明显的。
定义一个劳动合同的枚举,分固定期限和无固定期限的合同,参考下面的代码。
public enum ContractType { [StringValue("F")] [DisplayText("Fixed Time")] FixedTime, [StringValue("U")] [DisplayText("Unlimited Time")] UnlimitedTime }
获取它的值用如下方法,值用于存储到数据库中或程序代码使用:
StringEnum<ContractType>.GetStringValue(ContractType.FixedTime)
获取它的描述用如下方法,描述用于界面中呈现:
StringEnum<ContractType>.GetDisplayText(ContractType.FixedTime)
3 业务实现 Business Logic Implementation
一部分逻辑在Business Logic中实现,比如类型初始化值,自动带值,值验证等逻辑。复杂的逻辑比如进出仓,涉及到的关联表会多一些,可能要扣减库存,修改物料库存余额,批号生成,生成出仓平均单价等,复杂的逻辑要单独放在一个项目中设计源代,这样方便维护。
4 报表 Report
从形式上来分,分为图形报表和数据报表。图形报表用微软.NET 自带的图形控件完成,MSDN上包含Samples Environments for Microsoft Chart Controls的大量例子。数据报表用水晶报表完成,600多个水晶报表文件,涵盖单据的打印,列表查询打印,主档数据打印等类别。丰富的水晶报表功能为系统增色不少。
5 系统维护 System Maintenance
集成一些常用的功能,不需要进入系统即可完成系统维护。比如数据库升级,数据库备份,数据库还原,新帐套创建,系统参数设定,数据库性能优化(主要是索引重建),这些实用工具程序减轻了系统管理员的负担。
6 功能模块/界面 Presentation
Distribution 进销存,包含销售,采购,仓库模块。
模块 | 功能 |
Sales 销售 | 销售合同,销售订单,送货,退货,销售包装,销售发票 |
Purchasing 采购 | 采购申请,采购订单,采购收货,采购退货,供应商发票 |
Inventory 仓库 | 进仓(Receipt),出仓(Issue),转仓(Transfer),盘点(Cycle-count) |
Engineering 工程 物料清单,标准成本设定。
Production 生产 工作单,工作单发料,倒冲与组装,物料退回,发散料。
Production Planning 生产计划 主生产计划MPS,物料需求计划MRP, 能力需求计划CRP
Finance 财务 Account Receivable应收,Account Payable应付,General Ledger总帐。
CRM 客户关系 销售线索,销售日报,销售月报,出差申请与费用报销。
在我的从业经历以来,我认为搭建一套开发框架对企业开发是很有用处的。前期所花费的精力和时间在后期都会得到充分的回报。然而搭建框架所花费的时间和精力,值得商榷。公司一直都是对股东负责,能用最少的时间做完项目,收回合同款即可,大费周折的去做产品基础功能,对于小公司而言生存都是问题,精心设计的框架需要大量的精力去维护和改善,抛开公司因素,学习一个大型系统对个人的职业发展和成长也是相当重要的。