星空标准功能提供编码规则来生成单据编号,当设置了默认编码规则后,一般是保存时,自动生成单据编号。由于星空的机制是,先生成单据编号。提供给各个业务使用,各个业务提供插件在校验库存或者其他逻辑。当这些逻辑校验不通过时,对应编号的流水号已经被占用,生成下一单时,不再使用原编号。
因为以上机制,星空保存或者其他逻辑,生成编号与保存不是事务一致性,后续保存失败时,编号的流水号不会回滚。因此,星空不保证生成的编号一定连续。这种机制类似与,你到银行办理业务,到取号机取号(流水号)。但是最终没有真的去办理业务(异常情况),银行系统按照最终办理业务的流水号来看,有一部分流水号断开了。
又因为,星空的上级操作日志仅记录成功的日志,失败回滚的数据,无法在上机操作日志上查询。所以导致一部分业务要求单据编号严格连续场景无法支持。
为了支持以上场景,可以考虑在编码规则开启自动补号,延时计算(考虑并发设计的方案)。因为自动补号是延时的,那么按照每天作为编码依据生成的流水号,可能是断开的。比如今天20230525001一次生成,当生成到20230525005时,业务校验失败,导致最终20230525005没有保存到数据库里面。这个时候,补号自动计算服务还没有计算。下一次生成的流水号是20230525006,并且保存到数据库。当第二天,我们来查看20230525的数据时,发现20230525005这个流水号没有在系统中,而20230525006在系统中。当时上机操作日志中,没有任何20230525005日志(因为保存失败, 不会记录上机操作日志)。
基于以上场景,如果系统保留日志,即可以在上机操作日志中查看到所有单据生成记录,可以在对应的单据上,增加以下服务插件,参考代码如下,在保存服务插件,增加此插件后,那么保存失败的单据编号也会出现在上机操作日志里面,提供给审计等要求单据编号严格连续的情况。
using Kingdee.BOS.App; using Kingdee.BOS.Contracts; using Kingdee.BOS.Core.DynamicForm.PlugIn; using Kingdee.BOS.Core.DynamicForm.PlugIn.Args; using Kingdee.BOS.Core.Log; using System.ComponentModel; using Kingdee.BOS.Util; using Kingdee.BOS.Orm.DataEntity; using System.Text; using System; namespace Cloud.BOS.Support.AppService { /// <summary> /// 记录可能跳号的日志服务插件 /// </summary> [Description("记录单跳号日志")] public class SkipBillNoLogAppPlugIn : AbstractOperationServicePlugIn { public override void BeginOperationTransaction(BeginOperationTransactionArgs e) { base.BeginOperationTransaction(e); WriteLog(e.DataEntitys, false); } public override void EndOperationTransaction(EndOperationTransactionArgs e) { base.EndOperationTransaction(e); } public override void RollbackData(OperationRollbackDataArgs e) { base.RollbackData(e); WriteLog(e.DataEntitys, true); } private void WriteLog(DynamicObject[] objs, bool isRollback) { if (objs == null || objs.Length == 0) return; var dataObj = objs[0]; var billNoField = BusinessInfo.GetBillNoField(); if (billNoField == null) return; if (dataObj.DynamicObjectType.Properties.Contains(billNoField.PropertyName) && !dataObj[billNoField.PropertyName].IsNullOrEmptyOrWhiteSpace()) { var strDesc = string.Format("编号日志,编号:{0}", dataObj[billNoField.PropertyName]); if (isRollback) { strDesc += string.Format(" 详细堆栈信息:{0}", GetStackTrace()); } LogObject logObj = new LogObject() { pkValue = dataObj["Id"].ToString(), Description = strDesc, OperateName = FormOperation.OperationName, ObjectTypeId = BusinessInfo.GetForm().Id, SubSystemId = BusinessInfo.GetForm().SubsysId, Environment = OperatingEnvironment.BizOperate }; ILogService logService = ServiceHelper.GetService<ILogService>(); logService.WriteLog(Context, logObj); } } private string GetStackTrace() { System.Diagnostics.StackTrace st = new System.Diagnostics.StackTrace(); System.Diagnostics.StackFrame[] sfs = st.GetFrames(); if (sfs == null) return string.Empty; StringBuilder sb = new StringBuilder(); for (int i = 1; i < sfs.Length; ++i) { if (System.Diagnostics.StackFrame.OFFSET_UNKNOWN == sfs[i].GetILOffset()) continue; var method = sfs[i].GetMethod(); var fullName = method.DeclaringType == null ? string.Empty : method.DeclaringType.FullName; sb.AppendFormat("{0} {1}{2}", fullName, method.Name, Environment.NewLine); } var str = sb.ToString(); if (str.Length > 1990) { str = str.Substring(0, 1900); } return str; } } }
推荐阅读