五月开发总结(一)

    整整一个五月份,外加上四月的后半部分和六月前几天,总算是完成了日本出差报销的开发,目前项目基本上线,总算松了口气。但是回顾整个过程,作为自己独立开发的第一个还算有点规模的功能模块,坑还是踩了不少的,所以记录一下,方便以后总结提高。

    1.不要一条路走到黑,合理的推翻原来的设计有时更加科学高效。

    还是得简述下业务逻辑,出差报销分为两步,第一步:创建计划单。第二步:待申请单通过后使用申请单进行具体的报销。大体就是一个报销单应该包含至少一个计划单,上不封顶。最初的设计是使用一个关联表,创建报销单的时候把报销单和计划单的id写入,这样,只要关联表中有的计划单,就代表此计划单已经被使用了,不能再次使用此计划单来申请报销。

    就这样,迎来了第一次需求变更:创建报销单并且保存之后,这个报销单中用到的计划单仍然可以被使用,等这个报销单被正式提交到审核后才不可以被使用。而如果A、B报销单都用到了计划单x,那么A报销单提交后,B报销单在提交的时候,会提示x已经被使用,必须在B中删除x,才能提交。针对这个需求,在计划单主表加了一个flag字段,当使用此计划单的报销单提交的时候,改变flag的值为1。创建新的报销单的时候,只能选择flag为0的计划单进行申请。

    第二次需求变更:当计划单已经审核完之后,增加一个功能,用户可以主动关闭这个计划单。关闭后的计划单不可以被使用;并且如果该计划单已经被报销单使用,并且报销单已提交,那么这个计划单不可以被关闭。针对这个需求,直接在计划单主表的flag增加一个状态码‘2’,在创建报销单的时候,flag为‘2’的计划单是不可用的。在关闭计划单的时候,状态为‘1’的计划单不可以被关闭。

    蛋疼的第三次需求变更来了:用户决定不再使用第一次变更的逻辑,决定还是在创建报销单后就把报销单中使用的计划单锁定,不可以再次使用此计划单进行报销申请,除非此报销单被删除,计划单被释放。此时,我的考虑是前边加入了两个状态码,而报销单的删除、提交、退回、撤回等功能都伴随着状态码的变化,因此没有选择回退到第一次变更之前的逻辑,而是加入的新的状态码‘3’-已锁定。这样,在申请报销单的时候,‘1’、‘2’、‘3’状态下的单据都是不可用的。至此,申请报销单的逻辑已经和关联表无关,单纯的取决于计划单主表的状态码。 这个设计的优点是数据库中保存的状态码可以很直观的看到现在计划单的状态,缺点是关联表的作用被大大降低了。而最最根本的逻辑还是,凡是被报销单使用的计划单,皆不可以进行其他的操作。我的设计却是,状态码不为‘0’的,不可以进行相关操作。当然状态码肯定是和关联表有密切关系的,只是设计上偏离了本质,不是那么简洁,但是可以用。然而,第四次需求变更直接点中了我这个设计的死穴。

    第四次需求变更:用户在创建完报销单后,仍然可以在报销单的编辑页面追加新的计划单或者删除原有的计划单。针对这个需求,我首先想到的是在前端打开一个dialog,选择要添加的计划单后,返回给父页面,父页面根据返回的json数组的长度添加datagrid行。无疑这是最符合之前设计的办法。但是在实现过程中,遇到了很大问题,让我放弃了这个方案。比如,用户要追加的计划单个数不确定,在添加多行的情况下,datagrid的appendrows方法效率会变得非常低;并且,报销单的编辑页面还有三个datagrid和计划单有关系,添加或者删除一个计划单,需要让其他datagrid的数据或者行数发生改变,这就导致了在前端处理这个问题变得非常复杂。所以我放弃了这个方案,决定使用后端处理,前台只做datagrid的局部刷新。前半段还是很顺利的,在dialog里选择完要追加的计划单后,点击确定,就把计划单的状态修改为‘3’,同时在关联表中插入n条新的数据。删除同理,利用ajax更新状态,然后局部刷新。但这带来了新的问题,那就是此时数据已经更新,而用户倘若没有点击保存,数据也无法回滚。当时决定一条路走到黑的我,决定就这么整下去。考虑到添加一条计划单后,这条计划单将不能被使用,我增加了一个状态码‘4’-临时锁定,若最后点击保存时,把状态为‘4’的更新为‘3’-正式锁定。而没点击保存,那么在进去编辑页面的上层页面,把状态为‘4’的计划单更新为‘0’;然而这还不是全部,假设用户先删除了一条,然后再把它添加进来,最后没点击保存,那么这个计划单的而最终状态应该是‘3’,而不是‘0’,因此我还要判断这个单据在追加之前的状态;删除当然也应该考虑那么多。这个方案在我构思的过程中已经把我绕晕了,我感觉5分钟后我可能就会忘掉‘4’这个状态码到底代表着什么。我尝试着把我的思路实现了,当然这在后边的测试中是没法通过的,因为我自己都搞不清楚了。现在想想,这个方案的确是可以的,但是也是十分复杂,不简洁,非常难以维护的。一个好的设计,应该是从问题的本质出发去分析,而不是你写了很多的代码去覆盖你认为的所有的情况。好的模型,应该让人一眼就看出来你想干什么,这也是建模需要解决的问题,那就是把问题抽象成易于大家理解的东西,而不是单纯的得到最后的答案。事实上,穷举总是能解决大部分问题,而它总是不那么好的方案。
    问题最后的解决是在宇航的建议下(不得不说姜还是老的辣),回归到第一次变更前,不考虑乱七八糟的状态码,单纯就是,在关联表中有的计划单,就不能被追加。至于没有点击保存时关联表如何回滚,就更是一个巧妙的设计(至少我在当时是没有想到的):复制一个新的关联表B,在进入编辑页面时,B表去同步A表中的数据,点击保存的时候,则A表同步B表的数据。这就保证了在编辑页面时,已经被追加的计划单无法被再次追加,而如果没有点击保存,那么在上一级页面创建报销单的时候,计划单仍然可用,因为这里查询的是A关联表。

    这第一条已经两千多字,似乎都可以单独成文了。总之,这是我在这次开发中印象最深刻的问题,似乎和技术关系不大,但是的确能总结出不少东西。无论是从看东西的高度,对待处理问题的态度,还是思考的角度,这都是一次难忘的经历。

记录一次业务逻辑的分析过程

首先简单描述一下业务上的需求:要计算出差的补助。
计算规则如下: 

1.有三组变量,分别为出差时间、规定劳动时间、出差地当地劳动时间(各自包含开始和结束时间) 

2.原来的需求描述为:
出差时间在规定劳动时间内的话不计算补助。
出差开始时间早于规定劳动时间的话,差的时间作为补助时间来计算。但是差的时间内包含出差地当地的劳动时间的话要将其前后的时间作为补助时间计算。
出差结束时间晚于规定劳动时间的话,差的时间作为补助时间来计算。但是差的时间内包含出差地当地的劳动时间的话要将其前后的时间作为补助时间计算。

3.在规定劳动时间之前的出差时间称为前补助时间,规定劳动时间之后的出差时间称为后补助时间。

4.补助计算以小时计,只舍不入。

5.工作日的补助计算是前后时间单算,补助金额相加;非工作日的补助计算是前补助时间加上后补助时间,一块算金额。

6.存在跨天选项,即有可能是从21点工作到第二天凌晨3点。

7.职务人员只考虑规定劳动时间。

乍一看这需求,字挺多,但是倒也不难理解。就是算出来出差时间既不在规定劳动时间也不在当地劳动时间的时间就ok了呗。
首先祭出if else大法。似乎有一些麻烦,算了,先想单一的,工作日的计算。
首先,获取各个时间。startTime,endTime,ruleStartTime,ruleEndTime,localStartTime,ruleEndTime
第一种情况,最简单的,规定劳动时间和当地劳动时间正好重合了;
if ruleStartTime==localStartTime&&ruleEndTime==ruleEndTime:
 beforeTime=ruleStartTime-startTime>0?ruleStartTime-startTime:startTime-ruleStartTime

继续,稍微复杂一点,规定劳动时间开始时间晚于当地劳动时间开始时间;
再来,规定劳动时间开始时间早于当地劳动时间开始时间,也不难;
等等,还有一种情况,规定劳动时间的结束时间也早于当地劳动时间的结束时间,
还有,当地劳动时间开始时间介于规定劳动时间开始时间和结束时间中间;
厉害了word哥,这还没算上工作日和休息日、跨天、职务的区分,光一个前补助时间就这么多条件了,并且最可怕的是,可能还没想全。
看来这个if else大法不太灵光,又傻,代码量还高,并且逻辑上不安全。

那就得想一下了,回到之前那句话:算出来出差时间既不在规定劳动时间也不在当地劳动时间的时间。
简单一想,这不就是求出来出差时间这个集合与(规定劳动时间与当地劳动时间的合集)的差集吗?
第一个念头是区间,有现成的可以表示区间的数据结构吗,仔细想了想想不着。凑合用Array[2]吧。也能达到目的。
这个方法好!是真好,我接下来只需要实现两个方法;
function  求合集(arr1,arr2)
function 求差集(arr1,arr2)
开始面向过程。。。
还是不行,why?还得考虑 规定劳动时间与当地劳动时间的合集 不连续的情况,不连续的话求差集要分开算。而分开算的差集也有可能不是连续的。最长可能为三段
那么还是得调用2的3次方次 function 求差集。太麻烦了,不想弄。还是得多动脑子少编码。
可是光想这几个方法的实现已经耗费了大半个晚上的时间,老婆喊着要睡觉了。
躺床上就在想,数学上的集合不好使,用编程上的集合,全列出来。
第二天起来,思路一写,哈哈,问题解决了。 

时间全部换算成分钟制,区间写入array,求合集,直接用set。求差集,直接for not in,set;
求出差集之后,判断一下是否连续,也很简单,首尾之差是否等于数组的长度。不连续就拆分一下。
拆分完了,每一段分别比较一下和规定劳动时间的大小,确定是前补助时间还是后补助时间。完事,除以60,得到小时数。美滋滋。
并且我这个只给你算出来时间,至于你要怎么用时间,怎么算补助,是什么职位,后边简单一算就行了。so easy。
编程真有趣,哈哈。     

现在看来这个抽象思想,建模思想真是好。不然我们小时候做应用题为什么要列方程而不是穷举呢?
并且也给我解决问题提供了一种思路,多总结规律,抽象出数学模型,问题就会简单的多。
最重要的,多思考,用脑子编程。