本文作为初入ERP行业的新人的防坑指南,讲解了一些常见犯的错,这样也少走一些弯路,如果你是老鸟,请绕过 :-)

本文关联的代码使用kotlin编写,请自行转换为c#、java等你熟悉的语言,表述的坑在各个语言基本都是一样的。

不用使用单精度和双精度类型

     @Test
fun Test1(){
val a : Double = 0.3
var b : Double = 0.0
for (i in 0..9){
b += a
}
assert(b == 3.0)
}

你认为这个测试用例会通过吗?

是的,他的确不能通过,你仔细看看b的结果,你会发现b很接近3但不是3,关于这个问题有很多兄弟有详细的解释,我这里就不重复了。

在ERP中,这种错误可不能犯,不然可怜的财务人员就发现帐不平了,除非你是想让财务小妹天天找你套近乎。 囧。

所以凡是处理钱、数量、比例等等数值有关的,你应该用decimal类型(C#是decimal,java是BigDecimal),这里我想吐槽一下,java为什么要设计那么大一个BigDecimal?就不能设计一个折中的Decimal吗?(Java老鸟请指教)。

好吧,关键点来了,前端的小朋友注意了,javaScript中内置的number不是decimal哦,所以避免在前端算账了,甚至不能用number存储用户输入的数据,而是用字符串。或者用一些第三方库解决(比如decimal.js)。

要为数值检查范围

这个是我刚参加工作时,犯的一个错误,现在还记忆犹新,当时我用VB设计一个POS 收银程序,可是客户刚上线一个月后打电话来,统计报表出错,报:“数值溢出”,我一愣,这得多大的收入啊,能把数值溢出来。

后来经过仔细排查,得到问题的原因,在收银的收钱环节,有个界面收银员会录入用户付款多少,然后软件计算应该找零多少,有点像这样的:

总金额: 6 元

付款:  10   元

找零:4 元

就是在这个界面中,扫描枪经常无意间扫描到商品,你要知道,扫描枪对于电脑来说就是一个键盘,扫描一个商品条码就是模拟键盘录入一堆数字,并且帮你按回车键。然后就悲剧了,我们的POS机这个时候就可能变成了收款20亿,找零19亿9999万。。。(⊙o⊙)…你们好有钱哦。

这些信息也会进入数据库,虽然不会影响最后的收入,但是我们的统计报表中会用到这个字段,就“数值溢出”了。

所以后来的办法就是检查用户录入的数值不能超过总金额太多,即做范围检查。

可能你会认为,这是因为有条码枪这个特殊设备,我们做的普通软件都是在办公室用的,或者现在用的是高大上的手机,没有你说的事。那么我说,to yang to simple。

首先用户会自己加装条码枪,其次,你知道键盘会被诸如手机这样的东西丢在上面,然后不幸键入一排111111111111吗?更加不幸的是,好多领导是不看订单内容直接审核的。

so,本着对用户负责的太多,还是多做最大范围检查吧。

负数检查

我们刚才聊到要防止用户输入很大的值,其实我们也应该防止用户输入负数,我们说很大的值可能是用户无意间输入的,而负数就是用户故意输入的,有些用户不熟悉ERP软件,在处理退货等操作时,会很“聪明”的输入数量为负数,从而达到退货处理的目的,当然,你设计的软件也很“愚蠢”的通过了负数。可能用户觉得他很厉害,而你要为这个你没有考虑到的数值加班调整数据库了。

关于在ERP中是否允许使用负数,其实是存在争议的,有些ERP软件会利用负数实现对旧账的冲正处理,对此,我保留我的意见。我的观点是,让用户永远输入正数,然后用明确的冲正、退回等指令,让用户知道他在干什么,也让你在设计很多流程时不必处处小心负数。

溢出检查

这可能不算一个大坑,但作为知识,你还是需要知道这一点,上代码:

     @Test
fun Test2() {
val a = Int.MAX_VALUE - 3
val b = 5
val c = a + b
assert(c == -2147483647)
}

正如你看到的,一个很大的值,在加法超过边界后是不会出错的,而是“循环”到负数了。

我知道c#可用用checked{}来强制某段代码做溢出检查的,但似乎java没有内置的机制(请java老鸟指正)。

其实在ERP中,如果你做好了前面的范围检查,这个溢出检查基本上是不需要的,但如果你没有做好检查,就可能会造成计算结果不正确,比如累加的结果是负数。

作为额外的甜点,我们其实可以充分利用这个缺点,比如计算两个时间差多少毫秒时,就是利用操作系统的一个特定API,而那个API用的是int32,所以多少天后这个数值会不断循环的,而这不会影响我们用减法计算差额,不信你试试。

格式化小数点

在设计ERP时,很多界面是需要显示金额的,而需求会要求你按照当前币别格式化小数,比如,人民币应该显示到小数点后两位,即分,比如这个样子的: 3.14 元

如果你照做,你就会掉坑里了,因为我们刚从这个坑爬出来,☺

事情是这个样子的,我们的ERP允许为币别这个系统参数定义小数位数,而某个客户在刚上线时,出于小心的目的吧,将人民币设置到小数位数3位,我们在运算时也根据这个定义去四舍五入,比如:

0.31415 公斤(数量) * 10 元(单价) = 3.142 元

我们也将这个金额存入了数据库,在上线一年之后,客户觉得这个3位实在多余,而且造成单据有这个0.002元,没办法付钱或收款啊,所以就重新将人民币设置为2位,新建的单据工作正常。但是月底时,埋好的坑被踩到了 :-(

因为是中途修改的参数,所以可能上半个月的单据还存在 3.142 这样的数据,但月底的各种报表显示的结果可能就是3.14了,我们内部实际存储的是3.142,所以如果用户付款了3.14元的话,我们会说没有结算完毕的,关键是如果很多单据合计起来可能就差几元钱了。

所以说,这种小数点保留多少位,其实是两种需求,

一种需求是显示的格式化,我的观点是,数据库现在存放的是多少,就应该显示多少,3.142 就应该显示3.142。(当然,3.1420000 当然应该显示为3.142)

一种需求是录入和运算的四舍五入,例如上面的数量,如果数量的位数是5,当乘法运算后,其结果是 3.1415,但由于人民币的小数位数为2,这个时候就需要四舍五入为3.14。还有就是用户在录入数据时,如果用户录入3.1415时,就需要四舍五入或者提示用户数据有问题(依据业务设计的爱好)。

小心字符串

很多大型的ERP,在处理大任务很缓慢的时候,90%的可能是糟糕的SQL操作,还剩下7%可能就是滥用字符串了,不断的创建字符串、拼接、拼接再拼接,CPU说,我要抗议,GC说,我也要抗议,哪个龟儿子又在拼接字符串了。

如果你有段程序必须频繁的处理字符串,我们都知道可以使用StringBuilder,但如果StringBuilder都已经不能满足你了(怎么感觉怪怪的),那么你可以尝试一下 线程变量缓存 这样的写法,比如参考:.net framework的内部实现

数据库的超时

我们都知道,你在执行某个sql时,如果消耗太长的时间(比如ERP中的月底的结算、MRP计算等),可能会报超时错误的。同理,如果你开启一个事务,结果很长时间后你还没有提交事务,一样会报告超时的。

那么你想过,这些超时错误对数据库有什么影响吗?

//伪代码
val tran = Tran() //开启事务
tran.Begin() val cmd = SqlCommand()
cmd.sql = "....;..." //很多条sql,使用分号隔开
cmd.Execute() //很长,很长时间 tean.commit()

当事务超时了,而操作的命令没有超时时,SQL语句是继续执行的,效果就是事务超时前的数据被回滚了,而后面继续执行的sql是会被写入数据库的, 想想好恐怖吧。

所以,你的办法可以是很粗鲁的将超时时间设置很长的时间,讨巧的办法是让事务的超时时间总是大于命令的超时时间。

最好的办法是,优化你的sql吧,让他短时间执行完,别老霸占着数据库妹妹,实在不行的话,看看能不能拆分成很多的小事务,好事大家轮流转,你说是吧。

对异常的态度

有些新人,生怕自己的程序出现异常,或者从C、C++上带来一些“坏习惯”,在程序不能完成任务时,使用false、0或者""表示没有完成,然后你就发现调用他们写的库就是这个样子的:

 private fun DoSomething() : Int {
val data = GetData()
if (data !== null) {
var message = ChangeSomeData(data)
if (message != "") {
MessageBox(message)
return -1
} val number = SaveData(data)
if (number == 0) {
MessageBox("Error")
return -2
} return number
} return -3
}

如果你把这个函数公开出去,那就更有意思了,文档中需要说清楚返回的结果中有-1,-2,-3 三种情况。

囧,然后你就隔三差五打喷嚏,一定是新来的程序员调用你的代码时再骂你了。

为什么不能是这样用呢?

 fun DoSomething() : Int {
val data = GetData()
ChangeSomeData(data)
return SaveData(data)
}

事实上,你调用那些 .net framework或者jdk之类的都是这个感觉,对吧,这里的诀窍就是:你的函数没有搞定事情,就应该抛出异常。

以上面的GetData方法为例,如果你没有获取到数据,管他是数据错了,还是数据库连接不上了,还是其他任何错误,都应该以异常的方式抛出,只将你完成的结果作为返回值。

当然,世事无绝对,比如你看见.net framework就设计了 Int32.TryParse 这样的方法,因为这种操作是很关心是否成功的。再比如,Java和C#的枚举器中,hasNext()和MoveNext()都设计成bool返回值,表示是否成功移动到下一个位置。

小提示:Java没有out方式的参数,所以设计TryXX这样的方法就比较蹩脚,然后我看见一个帖子就贴心的设计了一个类解决这个问题。

 class Out<T>{
T s;
public void set(T value){
s = value;
}
public T get(){
return s;
}
public Out() {
}
} public static Boolean TryParse(String str, Out<Int32> result){
...

好吧,我承认,最后一个不能叫坑,应该叫 不能给被人挖坑的坑。

ERP新人防坑指南的更多相关文章

  1. 两百条微信小程序跳坑指南(不定时更新)

    微信小程序联盟出品 跳坑textarea<二百二十三>不显示文本及textarea相关问题集合跳坑<二百一十三> background-image无法获取本地资源图片....跳 ...

  2. ElasticSearch入坑指南之概述及安装

    ---恢复内容开始--- ElasticSearch入坑指南之概述及安装 了解ElasticSearch ElasticSearch(简称ES)基于Lucene的分布式全文检索引擎.使用ES可以实现近 ...

  3. C# -- HttpWebRequest 和 HttpWebResponse 的使用 C#编写扫雷游戏 使用IIS调试ASP.NET网站程序 WCF入门教程 ASP.Net Core开发(踩坑)指南 ASP.Net Core Razor+AdminLTE 小试牛刀 webservice创建、部署和调用 .net接收post请求并把数据转为字典格式

    C# -- HttpWebRequest 和 HttpWebResponse 的使用 C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebReq ...

  4. Kubernetes Fluentd+Elasticsearch+Kibana统一日志管理平台搭建的填坑指南

    在初步完成Kubernetes集群架构的建立后,通过搭建一些监控组件,我们已经能够实现 图形化的监控每个node,pod的状态信息和资源情况 通过scale进行replicateSet的扩展和伸缩 通 ...

  5. Rust入坑指南:核心概念

    如果说前面的坑我们一直在用小铲子挖的话,那么今天的坑就是用挖掘机挖的. 今天要介绍的是Rust的一个核心概念:Ownership.全文将分为什么是Ownership以及Ownership的传递类型两部 ...

  6. Rust入坑指南:鳞次栉比

    很久没有挖Rust的坑啦,今天来挖一些排列整齐的坑.没错,就是要介绍一些集合类型的数据类型."鳞次栉比"这个标题是不是显得很有文化? 在Rust入坑指南:常规套路一文中我们已经介绍 ...

  7. SpringBoot整合log4j2进行日志配置及防坑指南

    写在前面 最近项目经理要求将原先项目中的日志配置logBack,修改为log4j2,据说是log4j2性能更优于logback,具体快多少,网上有说快10多倍,看来还是很快的,于是新的一波挑战又开始了 ...

  8. 树莓派4B踩坑指南 - (15)搭建在线python IDE

    今天想在树莓派上自己搭一个在线的python IDE,于是找到了一篇教程--Fred913大神的从头开始制作OJ-在线IDE的搭建 自己尝试动手做了一下, 还是发现不少细节需要注意, 记录在此 如果不 ...

  9. 阿里巴巴泰山版《Java 开发者手册》,也是一份防坑指南

    我是风筝,公众号「古时的风筝」,一个不只有技术的技术公众号,一个在程序圈混迹多年,主业 Java,另外 Python.React 也玩儿的 6 的斜杠开发者. Spring Cloud 系列文章已经完 ...

随机推荐

  1. [转载]URI、 URL 和 URN 的区别

    1. URI URI = Universal Resource Identifier 统一资源标志符 URI采用一种特定语法标识一个资源的字符串.所标识的资源可能是服务器上的一个文件.不过,也可能是一 ...

  2. java 实现hex文件转换bin保存至内存中

    hex 文件的格式,以文件中某一行字符串(16进制)为例: :10 0830 00 020C5D0224B3FFFFFFFFFFFFFFFFFFFF 7E 10,长度,转换成10进制,也就是16B 大 ...

  3. python学习之文本文件上传

    最近用python的flask框架完成了一个最基本的文本文件上传,然后读取. 前端用的Angular的ng2-file-upload完成文件上传,后端用flask接收上传的文件,接着做处理. 在交互的 ...

  4. 用phantomjs进行web界面自动化测试的几个注意点

    貌似我以前说过不少界面自动化测试的坏话,哈哈.最近接触了phantomjs,发现用它进行web界面测试也挺有意思的,下面举几个我使用过程中发现的注意点. 1.需要指定phantomjs位置,否则使用时 ...

  5. Servlet3.0与springmvc那些事

    官方文档:https://docs.spring.io/spring/docs/5.0.2.RELEASE/spring-framework-reference/web.html#mvc-servle ...

  6. 最长(大)回文串的查找(字符串中找出最长的回文串)PHP实现

    首先还是先解释一下什么是回文串:就是从左到右或者从右到左读,都是同样的字符串.比如:上海自来水来自海上,bob等等. 那么什么又是找出最长回文串呢? 例如:字符串abcdefedcfggggggfc, ...

  7. Java程序设计第三次作业

    编写“学生”类及其测试类. 5.1 “学生”类: 类名:Student 属性:姓名.性别.年龄.学号.5门课程的成绩 方法1:在控制台输出各个属性的值. 方法2:计算平均成绩 方法3:输出各个属性的值 ...

  8. PL/SQL执行计划查看

    一.如何查看PLSQL的执行计划 在SQl Window窗口输入sql语句,然后按键"F5",就会进入执行计划查看界面. 二.界面说明 首先我们看第二行有几个属性可以选“Tree” ...

  9. Python day 02

    基础&运算符 今日概要 循环 字符串格式化 运算符 编码 内容回顾 & 补充 内容回顾 计算机基础 解释器python 2 和 python 3 语法 print input if / ...

  10. JS(JavaScript)的进一步了解2(更新中···)

    js数据类型 基本数据类型:string   undefined   null  boolean  number 引用数据类型  Object  array  function 二者的区别 基本数据类 ...