大型.NET ERP系统高质量代码设计模式
1 缓存 Cache
系统中大量的用到缓存设计模式,对系统登入之后不变的数据进行缓存,不从数据库中直接读取。耗费一些内存,相比从SQL Server中再次读取数据要划算得多。缓存的基本设计模式参考下面代码:
private static ConcurrentDictionary_cachedLookupDialogEntities = new ConcurrentDictionary (); if (!_cachedLookupDialogEntities.ContainsKey(key)) lookupDialog = _cachedLookupDialogEntities.GetOrAdd(key, lookupDialog); else _cachedLookupDialogEntities[key] = lookupDialog;
主要用到的数据结构是字典,字典中的项目不存在时,向其增加,以后再调用时,直接从内存中取值。
列举一下,我可以看到的ERP系统中应用缓存设计模式的地方,主要分数据缓存和对象缓存,资源缓存:
1) 系统翻译 ERP系统中的文句翻译内容保存在数据库表中,只需要在系统登入时读取一次,缓存到DataTable中。
2) 系统参数 登入系统之后,当前的财年,会计期间,采购单批核流程,物料编码长度,是否实施批号和序号,记帐凭证过帐前是否需要审核,成本核算的来源(物料成本,物料成本+人工成本,物料成本+人工成本+机器成本),这些参数都可以缓存在Entity中,用户修改这些参数值,需要提醒或是强制用户退出重新登入。
3) 系统查询 系统中可预定义一组查询语句,在代码中将查询语句转化为查询对象,将查询对象缓存,节省SQL语句到查询对象的转化时间。
4) 对象实例 以插件方式在搜索程序集中包含的系统功能时,搜索到后,会将程序功能对应的类型缓存,所以第二次执行功能的速度会相当快。参考下面的例子代码加深印象:
public void OpenFunctionForm(string functionCode) { functionCode = functionCode.ToUpper().Trim(); Type formBaseType = null; if (!_formBaseType.TryGetValue(functionCode, out formBaseType)) { Assembly assembly = Assembly.GetExecutingAssembly(); foreach (Type type in assembly.GetTypes()) { try { object[] attributes = type.GetCustomAttributes(typeof(FunctionCode), true); foreach (object obj in attributes) { FunctionCode attribute = (FunctionCode)obj; if (!string.IsNullOrEmpty(attribute.Value)) { if (!_formBaseType.ContainsKey(attribute.Value)) _formBaseType.Add(attribute.Value, type); if (formBaseType == null && attribute.Value.Equals(functionCode,StringComparison.InvariantCultureIgnoreCase)) formBaseType = type; } if (formBaseType != null) { goto Found; } } } catch { } } } Found: if (formBaseType != null) { object entryForm = Activator.CreateInstance(formBaseType) as Form; Form functionForm = (Form)entryForm; OpenFunctionForm(functionForm); } }
在我的通用应用程序开源框架中,有上面这个例子的完整代码。
5) 资源缓存 系统中会用到一些以嵌入方式编译到程序集中的资源文件,在搜索到资源文件后,也是以字典的方式缓存资源(图标Icon,图片Image,文本Text,查询语句Query)。
2 查询优化 Query Optimize
这是个很容易理解的设计模式,贵在坚持。我们在读取数据时,只读取最少的可用的数据,避免读取不需要的数据。用查询语句表达如下,下面是没有效率的查询数据:
SELECT * FROM Company
经过改善之后的语句,改成只读需要使用的数据,改善后的查询如下:
SELECT CompanyCode, CompanyName FROM Company
后者的性能会好很多。对于我使用的LLBL Gen Pro,把上面的代码转化为程序代码,也就是下面的例子程序所示:
IncludeFieldsList fieldList = new IncludeFieldsList(); fieldList.Add(FiscalPeriodFields.Period); fieldList.Add(FiscalPeriodFields.FiscalYear); fieldList.Add(FiscalPeriodFields.PeriodNo); IFiscalPeriodManager fiscalPeriodManager = ClientProxyFactory.CreateProxyInstance(); FiscalPeriodEntity fiscalPeriodEntity = fiscalPeriodManager.GetFiscalPeriod(Shared.CurrentUserSessionId, this.VoucherDate, null, fieldList); this.Period = fiscalPeriodEntity.Period; this.FiscalYear = fiscalPeriodEntity.FiscalYear; this.PeriodNo = fiscalPeriodEntity.PeriodNo;
即使没有接触过LLBL Gen Pro,也可感受到类型IncludeFieldsList 的作用是为了挑选要读取的数据列,也就是要使用什么字段,就读什么字段,避免读取不需要的字段。
对于上面的程序,它的性能开销主要在读取数据和创建对象方面,为了性能再快一点,考虑读取数据转化为DataTable,可读性上有所降低但性能又提升了一些。
IRelationPredicateBucket filterBucket = new RelationPredicateBucket(); filterBucket.PredicateExpression.Add(ShipmentFields.CustomerNo == this.CustomerNo); filterBucket.PredicateExpression.Add(ShipmentFields.Posted == true); filterBucket.Relations.Add(new EntityRelation(ShipmentDetailFields.OrderNo, SalesOrderDetailFields.OrderNo, RelationType.ManyToMany)); filterBucket.PredicateExpression.Add(ShipmentDetailFields.QtyShipped == SalesOrderDetailFields.Qty); ResultsetFields fields = new ResultsetFields(4); fields.DefineField(ShipmentFields.RefNo, 0); fields.DefineField(ShipmentFields.PayTerms, 1); fields.DefineField(ShipmentFields.Ccy, 2); fields.DefineField(ShipmentFields.ShipmentDate, 3); System.Data.DataTable shipments = userDefinedQueryManager.GetQueryResult(Shared.CurrentUserSessionId, fields, filterBucket, null, null, false, false);
继续改善查询的性能,假设场景是销售订单表要读取客户编号和客户名称,我们直接在销售订单表中增加客户名称字段,这样每次加载销售订单时,可直接读取到销售订单表自身的客户名称字段,而不用左连接关联到客户表读取客户名称。
Entity Framework或是第三方的ORM 查询接口,应该都具备上面列举的特性。
ORM查询不推荐使用LINQ,性能是主要考虑的方面。ORM框架将查询转化为实体对象时,因为不能预料到后面会用到实体的哪些属性,预先读取所有的字段绑定到属性中,性能难以接受,这跟前面提到的SELECT * 读取所有字段是同样的意思,延迟绑定属性,用到属性时再读取相应的数据库字段,每用一个属性都去读取一次数据库,对数据库的连接次数过于频繁,也不可接受。
下面的写法是我最不能忍受的查询写法,参考代码中的例子:
EntityCollection
AccountsReceivableJournalEntity lastJournal = journalCollection[journalCollection.Count-1];
为了取一个表中的最后一笔记录,居然将整个表都读取到内存中,再取最后一条记录。
这种查询可以改善成SELECT TOP 1 + ORDER BY,读一笔数据的性能肯定优于读取未知笔数据记录。
3 延迟加载 Delay Load
在使用对象时,只有当需要使用对象的方法或属性,我们才实例化对象。设计模式的代码例子如下:
PayTermEntity payTerm = null; payTerms.TryGetValue(dataRow["PayTerms"].ToString(), out payTerm); if (payTerm == null) { payTerm = payTermManager.GetPayTerm(Shared.CurrentUserSessionId, dataRow["PayTerms"].ToString()); payTerms.Add(payTerm.PayTerms, payTerm); }
突然想到这种模式就是系统缓存的实现方法。在类型中定义一个私有静态变量,使用这个变量时我们才去初始化它的实例。延迟加载避免了系统启动时创建所有缓存对象耗费的内存和时间,有些对象或许根本不会用到,也就不应该去创建。
比如用户仅登入进系统,没有做任何业务单据操作然后退出。如果在登入时就创建货币或付款条款的缓存,而用户又没有使用这些数据,影响了系统性能。
4 后台线程与多线程 BackgroundWorker/WorkerThreadBase
.NET 提供了后台线程控件,解决了长时间操作避免主界面卡死的问题。在系统中,凡是涉及到数据库操作,不能在很短时间内完成的,都放到BackgroundWorker后台线程中执行。系统中大量使用BackgroundWorker的地方:
1) 单据增删查改 所有单据对数据的Insert,Delete,Update都用BackgroundWorker操作。
2) 查询 所有关于数据的查询封装到BackgroundWorker中执行。
3) 数据操作类功能:数据初始化,数据再开始,核算供应商帐,核算客户帐,数据存档,数据备份,数据还原。
4) 业务单据过帐,业务单据完成,业务单据取消,业务单据修改。
当没有界面时,无法使用BackgroundWorker,可以用多线程组件改善性能。参考下面的例子代码:
private sealed class LoadItemsWorker : WorkerThreadBase { private MrpEntity _mrp; private ConcurrentBag_itemMasterRows; protected override void Work() { //long time operation }
调用上面的多线程组件,参看下面的例子代码:
Listworkers = new List (); for (int i = 0; i < MAX_RUNNING_THREAD; i++) { LoadItemsWorker worker = new LoadItemsWorker(sessionId, this, mrp); workers.Add(worker); } WorkerThreadBase.StartAndWaitAll(workers.ToArray());
多线程组件WorkerThreadBase可以在Code Project上找到源代码和讲解文章。
5 数据字典 Data Dictionary
主要介绍不可变的数据字典的设计模式,先看一下性别Gender的数据字典设计:
public enum Gender { [StringValue("M")] [DisplayValue("Male")] Male, [StringValue("F")] [DisplayValue("Female")] Female }
为枚举类型增加了二个特性,StringValue用于存储,DisplayValue用于界面控件中显示,这跟数据绑定中的介绍的数据源的ValueMember和DisplayMember是一样的原理。再来看使用代码:
Employee employee=...
employee.Gender=StringEnum
也可以这样调用获取显示的值DisplayValue:
string displayValue=StringEnum
这样设计模式解决了数据字典的文档更新的烦恼。编写源代码同时就设计好了文档,想知道数据字典的值,直接打开枚举类型定义即可。
6 校验-执行-验证 Validate-Post-Verify
对业务逻辑的业务操作,遵守校验-执行-验证设计约定,来看一段代码加深印象:
try { adapter.StartTransaction(IsolationLevel.ReadCommitted, "PostInvoice"); this.ValidateBeforePost(sessionId, accountsReceivableAllocation); this.Post(sessionId, accountsReceivableAllocation); this.VerifyGeneratedVoucher(sessionId, accountsReceivableAllocation); adapter.Commit(); } catch { adapter.Rollback(); throw; }
先校对要执行操作的数据,再对数据进行操作,操作完成之后,再对期望的数据进行验证。
比如发票生成凭证,先要验证发票上的金额是否大于零,开发票的时间是否是当前期间等业务逻辑,再执行凭证生成(Voucher)动作,最后验证生成的凭证的借贷方是否一致,是否考虑到小数点进位导致的借货方不一致,生成的凭证金额是否与原发票上的金额相等。
7 执行前-执行-执行后 OnBefore-Perform-OnAfter
第六条讲解是的业务记帐方法,第七条这里讲解的是公共框架与应用程序互动的方法。继承的.NET窗体或派生类要能改变基类的行为,需要设计一种方法来达到此目的。先看一段代码熟悉这种设计模式:
CancelableRecordEventArgs e = new CancelableRecordEventArgs(this.CurrentEntity); this.OnBeforeCancelEdit(e); if (this._beforeCancelEdit != null) this._beforeCancelEdit(this, e); if (e.Cancel) return false; bool flag = this.DoPerformCancelEdit(this.CurrentEntity); RecordEventArgs args2 = new RecordEventArgs(this.CurrentEntity); this.OnAfterCancelEdit(args2); if (this._afterCancelEdit != null) this._afterCancelEdit(this, args2);
为了加深了解这种设计模式,我对上面的代码段用两行空格分开成三个部分,下面详细讲解这三个部分:
OnBefore 在执行操作前,派生类可以设定参数到基类中,影响基类的行为。比如可以执行一个事件,也可以向基类传递取消条件,派生类向基类传递Cancel=true的标志位,完全取消当前的操作。这是派生类影响基类行为的一种设计方式。另一种方法是抛出异常,异常会导致整个堆栈回滚。
Perform 执行要做的操作,这个命名是按照.NET的规范。比如我们想在代码中直接执行按钮的点击事件,可以这样写调用代码的方法:btnOK.PerformClick();
OnAfter 在执行完成后。可以对执行的结果重写,也可以调用派生类中的事件。
8 元数据 Metadata
框架能完成很多应用程序一句话调用就能完成的功能,元数据的功劳最大。系统中的实体对象的每个字段都有一张附加属性表,参考下面的代码定义:
private static void SetupCustomPropertyHashtables() { _customProperties = new Dictionary(); _fieldsCustomProperties = new Dictionary >(); _customProperties.Add("SupportDocumentApproval", @""); _customProperties.Add("SupportExternalAttachment", @""); Dictionary fieldHashtable; fieldHashtable = new Dictionary (); _fieldsCustomProperties.Add("Recnum", fieldHashtable); fieldHashtable = new Dictionary (); fieldHashtable.Add("AllowEditForNewOnly", @""); fieldHashtable.Add("CapsLock", @""); _fieldsCustomProperties.Add("RefNo", fieldHashtable); fieldHashtable = new Dictionary (); fieldHashtable.Add("ReadOnly", @"");
看到上面的代码,当前实体的每一个属性都可以绑定一个Dictionary对象,这段代码是用代码生成器完成。于是发挥想象力,将字段的特殊属性放到实体属性的附加属性中,框架可完成很多基础功能。
看到上面的RefNo属性中增加了AllowEditForNewOnly和CapsLock两条元数据。在系统框架部分,代码参考如下:
DictionaryfieldsCustomProperties = GetFieldsCustomProperties(boundEntity, bindingMemberInfo.BindingField); if (fieldsCustomProperties != null) { if (fieldsCustomProperties.ContainsKey("CapsLock")) { base.CharacterCasing = CharacterCasing.Upper; } else if (!(this.AlwaysReadOnly || !fieldsCustomProperties.ContainsKey("AllowEditForNewOnly"))) { this._allowEditForNewOnly = true; }
元数据通过代码生成器的实体设计完成,框架获取实体代码的元数据,做一些控件属性上的公共设置,节省了大量的重复的代码。以上是属性上的元数据,也可以增加实体层级上的元数据,元数据的存在给框架设计带来了便利。
如果正在设计一套ORM框架,考虑给实体和实体的属性增加元数据(自定义属性),它会为系统的可扩展带来诸多方便。
解析大型.NET ERP系统 20条数据库设计规范
当系统越来越庞大,严格控制数据库的设计人员,并且有一份规范书供执行参考。在程序框架中,也有一份强制性的约定,当不遵守规范时报错误。
以下20个条款是我从一个超过1000个数据库表的大型ERP系统中提炼出来的设计约定,供参考。
1 所有的表的第一个字段是记录编号Recnum,用于数据维护
[Recnum] [decimal] (8, 0) NOT NULL IDENTITY(1, 1)
在进行数据维护的时候,我们可以直接这样写:
UPDATE Company SET Code='FLEX' WHERE Recnum=23
2 每个表增加4个必备字段,用于记录该笔数据的创建时间,创建人,最后修改人,最后修改时间
[CreatedDate] [datetime] NULL,
[CreatedBy] [nvarchar] (10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[RevisedDate] [datetime] NULL,
[RevisedBy] [nvarchar] (10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
框架程序中会强制读取这几个字段,默认写入值。
3 主从表的主外键设计
主表用参考编号RefNo作为主键,从表用RefNo,EntryNo作为主键。RefNo是字符串类型,可用于单据编码功能中自动填写单据流水号,从表的EntryNo是行号,LineNo是SQL Server 的关键字,所以用EntryNo作为行号。
如果是三层表,则第三层表的主键依次是RefNo,EntryNo,DetailEntryNo,第三个主键用于自动增长行号。
4 设计单据状态字段
字段 含义
Posted 过帐,已确认
Closed 已完成
Cancelled 已取消
Approved 已批核
Issued 已发料
Finished 已完成
Suspended 已取消
5 字段含义相近,把相同的单词调成前缀。
比如工作单中的成本核算,人工成本,机器成本,能源成本,用英文表示为LaborCost,MachineCost,EnergyCost
但是为了方便规组,我们把Cost调到字段的前面,于是上面三个字段命名为CostLabor,CostMachine,CostEnergy。
可读性后者要比前者好一点,Visual Studio或SQL Prompt智能感知也可帮助提高字段输入的准确率。
6 单据引用键命名 SourceRefNo SourceEntryNo
销售送货Shipment会引用到是送哪张销售单据的,可以添加如下引用键SourceRefNo,SourceEntryNo,表示送货单引用的销售单的参考编号和行号。Source开头的字段一般用于单据引用关联。
7 数据字典键设计
比如员工主档界面的员工性别Gender,我的方法是在源代码中用枚举定义。性别枚举定义如下:
public enum Gender
{
[StringValue("M")]
[DisplayText("Male")]
Male,
[StringValue("F")]
[DisplayText("Female")]
Female
}
在代码中调用枚举的通用方法,读取枚举的StringValue写入到数据库中,读取枚举的DisplayText显示在界面中。
经过这一层设计,数据库中有关字典方面的设计就规范起来了,避免了数据字典的项的增减给系统带来的问题。
8 数值类型字段长度设计
Price/Qty 数量/单价 6个小数位 nnnnnnnnnn.nnnnnn 格式 (10.6)
Amount 金额 2个小数位 nnnnnnnnnnnn.nn 格式(12.2)
Total Amt 总金额 2个小数位 nnnnnnnnnnnnnn.nn 格式(14.2)
参考编号默认16个字符长度,不够用的情况下增加到30个字符,再不够用增加到60个字符。这样可以保证每张单据的第一个参考编号输入控件看起来都是一样长度。
除非特别需求,一般而言,界面中控件的长度取自映射的数据库中字段的定义长度。
9 每个单据表头和明细各增加10个自定义字段,基础资料表增加20个自定义字段
参考供应商主档的自定义字段,自定义字段的名称统一用UserDefinedField。
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_1] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_2] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_3] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_4] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_5] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_6] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_7] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_8] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_9] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_10] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_11] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_12] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_13] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_14] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_15] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_16] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_17] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_18] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_19] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
ALTER TABLE Vendor ADD COLUMN [USER_DEFINED_FIELD_20] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL
10 多货币(本位币)转换字段的设计
金额或单价默认是以日记帐中的货币为记录,当默认货币与本位币不同时需要同时记录下本位币的值。
销售单销售金额 SalesAmount或SalesAmt,本位币字段定义为SalesAmountLocal或SalesAmtLocal
通常是在原来的字段后面加Local表示本位币的值。
11 各种日期字段的设计
字段名称 含义
TranDate 日期帐日期 Tran是Transaction的简写
PostedDate 过帐日期
ClosedDate 完成日期
InvoiceDate 开发票日期
DueDate 截止日期
ScheduleDate 计划日期,这个字段用在不同的单据含义不同。比如销售单是指送货日期,采购单是指收货日期。
OrderDate 订单日期
PayDate 付款日期
CreatedDate 创建日期
RevisedDate 修改日期
SettleDate 付款日期
IssueDate 发出日期
ReceiptDate 收货日期
ExpireDate 过期时间
12 财务有关的单据包含三个标准字段
FiscalYear 财年,PeriodNo 会计期间,Period 前面二个的组合。以国外的财年为例子,FiscalYear是2015,PeriodNo是4,Period是2015/04。
欧美会计期间是从每年的4月份开始,需要注意的是会计期间与时间没有必然的联系,看到会计期间是2015/04,不一定是表示2015的4月份,它只是说这是2015财年的第四期,具体在哪个时间段需要看会计期间定义。
13 单据自动生成 DirectEntry
有些单据是由其它单据生成过来的,逻辑上应该不支持编辑。比如销售送货Shipment单会产生出仓单,出仓单应该不支持编辑,只能做过帐扣减库存操作。这时需要DirectEntry标准字段来表示。当手工创建一张出仓单时,将DirectEntry设为true,表示可编辑单据中的字段值,当由其它单据传递产生过来产生的出仓单,将DirectEntry设为false,表示不能编辑此单据。这种情况还发生在业务单据产生记帐凭证(Voucher)的功能中,如果可以修改由原始单据传递过来的数量金额等字段,则会导致与源单不匹配,给系统对帐产生困扰。
14 百分比值字段的设计
Percentage百分比值,用于折扣率,损耗率等相关比率设定的地方。推荐用数值类型表示,用脚本表示是
[ScrapRate] [decimal] (5, 2) NULL
预留两位小数,整数部分支持1-999三位数。常常是整数部分2位就可以,用3位也是为了支持一些特殊行业(物料损耗率超过100)的要求。
15 日志表记录编号LogNo字段设计
LogNo字段的设计有些巧妙,以出仓单为例子,一张出仓单有5行物料明细,每一行物料出仓都会扣减库存,再写物料进出日记帐,因为这五行物料出仓来自同一个出仓单,于是将这五行物料的日记帐中的LogNo都设为同一个值。于在查询数据时,以这个字段分组即可看到哪些物料是在同一个时间点上出仓的,对快速查询有很重要的作用。
16 基础资料表增加名称,名称长写,代用名称三个字段
比如供应商Vendor表,给它加以下三个字段:
Description 供应商名称,比如微软公司。
ExtDescription 供应商名称长写,比如电气行业的南网的全名是南方国家电网有限公司。
AltDescription 供应商名称替代名称,用在报表或是其它单据引用中。比如采购单中的供应商是用微软,还是用代用名称Microsoft,由参数(是否用代用名称)控制。
17 文件类表增加MD5 Hash字段
比如产品数据管理系统要读取图纸,单据功能中增加的附件文件,这类涉及文件读写引用的地方,考虑存放文件的MD5哈希值。文件的MD5相当于文件的唯一识别身份,在网上下载文件时,网站常常会放出文件的MD5值,以方便对比核对。当下载到本机的文件的MD5值与网站上给出的值不一致时,有可能这个文件被第三方程序修改过,不可信任。
18 数据表的主键用字符串而不是数字
比如销售单中的货币字段,是存放货币表的货币字符串值RMB/HKD/USD,还是存放货币表的数字键,1/2/3。
存放前者对于报表制作相对容易,但是修改起来相对麻烦。存放后者对修改数据容易,但对报表类或查询类操作都需要增加一个左右连接来看数字代表的货币。金蝶使用的是后者,它的BOS系统也不允许数据表之间有直接的关联,而是间接通过Id值来关联表。
在我看到的系统中,只有一个会计期间功能(财年Fiscal Year)用到数字值作主键,其余的单据全部是字符串做主键。
19 使用约定俗成的简写
模块Module 简写
简写 全名
SL Sales 销售
PU Purchasing 采购
IC Inventory 仓库
AR Account Receivable 应收
AP Account Payable 应付
GL General Ledger 总帐
PR Production 生产
名称Name 简写
简写 全名
Uom Unit of Measure 单位
Ccy Currency 货币
Amt Amount 金额
Qty Quantity 数量
Qty Per Quantity Per 用量
Std Output Standard Output 标准产量
ETA Estimated Time of Arrival 预定到达时间
ETD Estimated Time of Departure 预定出发时间
COD Cash On Delivery 货到付款
SO Sales Order 销售单
PO Purchase Order 采购单
20 库存单据数量状态
Qty On Hand 在手量
Qty Available 可用量
Qty On Inspect 在验数量
Qty On Commited 提交数量
Qty Reserved 预留数量
以上每个字段都有标准和行业约定的含义,不可随意修改取数方法。