整整一个五月份,外加上四月的后半部分和六月前几天,总算是完成了日本出差报销的开发,目前项目基本上线,总算松了口气。但是回顾整个过程,作为自己独立开发的第一个还算有点规模的功能模块,坑还是踩了不少的,所以记录一下,方便以后总结提高。
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关联表。
这第一条已经两千多字,似乎都可以单独成文了。总之,这是我在这次开发中印象最深刻的问题,似乎和技术关系不大,但是的确能总结出不少东西。无论是从看东西的高度,对待处理问题的态度,还是思考的角度,这都是一次难忘的经历。