ERP新人防坑指南

本文作爲初入ERP行業的新人的防坑指南,講解了一些常見犯的錯,這樣也少走一些彎路,如果你是老鳥,請繞過 :-)

本文關聯的代碼使用kotlin編寫,請自行轉換爲c#、java等你熟悉的語言,表述的坑在各個語言基本都是一樣的。

不用使用單精度和雙精度類型

1     @Test
2     fun Test1(){
3         val a : Double = 0.3
4         var b : Double = 0.0
5         for (i in 0..9){
6             b += a
7         }
8         assert(b == 3.0)
9     }

你認爲這個測試用例會通過嗎?

是的,他的確不能通過,你仔細看看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軟件會利用負數實現對舊賬的衝正處理,對此,我保留我的意見。我的觀點是,讓用戶永遠輸入正數,然後用明確的衝正、退回等指令,讓用戶知道他在幹什麼,也讓你在設計很多流程時不必處處小心負數。

溢出檢查

這可能不算一個大坑,但作爲知識,你還是需要知道這一點,上代碼:

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

正如你看到的,一個很大的值,在加法超過邊界後是不會出錯的,而是“循環”到負數了。

我知道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或者""表示沒有完成,然後你就發現調用他們寫的庫就是這個樣子的:

 1 private fun DoSomething() : Int {
 2     val data = GetData()
 3     if (data !== null) {
 4         var message = ChangeSomeData(data)
 5         if (message != "") {
 6             MessageBox(message)
 7             return -1
 8         }
 9 
10         val number = SaveData(data)
11         if (number == 0) {
12             MessageBox("Error")
13             return -2
14         }
15         
16         return  number
17     }
18     
19     return -3
20 }

如果你把這個函數公開出去,那就更有意思了,文檔中需要說清楚返回的結果中有-1,-2,-3 三種情況。

囧,然後你就隔三差五打噴嚏,一定是新來的程序員調用你的代碼時再罵你了。

爲什麼不能是這樣用呢?

1 fun DoSomething() : Int {
2     val data = GetData()
3     ChangeSomeData(data)
4     return SaveData(data)
5 }

事實上,你調用那些 .net framework或者jdk之類的都是這個感覺,對吧,這裏的訣竅就是:你的函數沒有搞定事情,就應該拋出異常。

以上面的GetData方法爲例,如果你沒有獲取到數據,管他是數據錯了,還是數據庫連接不上了,還是其他任何錯誤,都應該以異常的方式拋出,只將你完成的結果作爲返回值。

當然,世事無絕對,比如你看見.net framework就設計了 Int32.TryParse 這樣的方法,因爲這種操作是很關心是否成功的。再比如,Java和C#的枚舉器中,hasNext()和MoveNext()都設計成bool返回值,表示是否成功移動到下一個位置。

小提示:Java沒有out方式的參數,所以設計TryXX這樣的方法就比較蹩腳,然後我看見一個帖子就貼心的設計了一個類解決這個問題。

 1 class Out<T>{
 2     T s;
 3     public void set(T value){
 4         s =  value;
 5     }
 6     public T get(){
 7         return s;
 8     }
 9     public Out() {
10     }
11 }
12 
13 public static Boolean TryParse(String str, Out<Int32> result){
14 ...

 好吧,我承認,最後一個不能叫坑,應該叫 不能給被人挖坑的坑。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章