不知不觉,做开发已有 2 年,传统行业的 IT 开发,需求的大概是全能手,SAP 上了一期,对接了 SAP,了解了一些 ABAP 语法,写了几个接口,慢慢的对语言不在执着,对技术也不再执着,因为出身工厂,对工厂业务还算熟悉,对解决方案的思考还算比较热衷。所以就沉浸在计算的一块,但是技术算法认知的先天不足,所以想请大佬把把脉,以后的学习方向。比如以下一题:
目前这个模块我是通过暴力遍历实现的,但是速度太慢了,逻辑如下
因为没到月末,基本收款及发票是不一定真实的,有可能会冲销掉。实时清账就有可能反清账,所以目前清账功能财务需求是预清账,点击就跑,期末定了再关账。
第二个问题是关于动态计算的表达式引擎问题,2W 条数据动态计算,用了 C#的库,居然还要几分钟。
大概需求如下
一个单据,有很多属性,然后不同的属性,会对应不同的成本及利润计算公式。所以我把它放在前端用动态表达式的方式配置,而不是代码写 IF (因为情况大概目前有 70+种,再加以后的新的营销活动啥的,财务提一个需求就要改一次代码太麻烦,所以找了一些动态表达式的库,通过前端配置触发条件)
for example.
1.目前逻辑是,遍历 2W 条,先匹配布尔条件生效判定(可能不止 1 个布尔条件),但终归 70+种情况组组合,最后得到的算法,然后计算提成。
因为用了 C#的表达式引擎,Flee ( Fast Lightweight Expression Evaluator ),目前用时 3~5 分钟,但是这 5 分钟不太适合体感使用,因为在结算的时候,表达式会调整的,然后重算验证就等的不耐烦了。
题外话:公司的技术大佬是中小方案很熟练,BS/CS 端直接上手,打印报表配置,运维都可以,中小企业不可少的人才,但是技术深度不行,喜欢代码一把梭,项目 MVC 还是我来了之后分的,以前直接控制器+静态类打天下,我想了一下我又不想成为绑定公司的人才,所以对多语言的学习比较抗拒,对技术逻辑比较感兴趣,对技术遇到的难题,也喜欢思考,但是非科班,所以很多时候都是暴力算法,然后因公司上市,业务发展的复杂度也越来越高,传统行业对 IT 技术人才是不太尊重,感觉目前的代码不重构,会因为复杂度,迟早黑盒子一样的存在.....
1
Easzz 2020-12-17 16:51:26 +08:00
可以每天 /月生成历史数据,出报表直接统计天 /月的数据即可,不用查询明细。
|
2
linksNoFound 2020-12-17 16:57:38 +08:00
你看看 io 和 cpu 哪个跑满了,都还空着就试试多进程吧,比改代码判断逻辑简单
|
3
MakeItGreat 2020-12-17 17:00:20 +08:00 via Android
我好像觉得 Excel 更简单
|
4
xzour OP @MakeItGreat 就是因为财务随着业务复杂度越来越高,汇总数据太麻烦,开始考虑上系统了呀。
|
5
whosesmile 2020-12-17 17:39:00 +08:00
我做前端的,看了第一个题目,瞎说下:
遍历每个客户感觉没必要,因为每个你无非是要找当前客户的的收款明细,倒不如直接一次性检索出所有的收款明细按用户 ID 分组,同样检索所有发票也按用户 ID 分组,然后这两个组都按用户 ID 排序,然后剩下的是算法层次的事情,找两个集合的对对碰。 前提是你的数据量不大,你的内存足够装下这么多数据,这里的优化是不需要每个 for 迭代都去查询 DB,全部换成内存操作了。 |
6
Bazingal 2020-12-17 17:39:59 +08:00 via Android
数组转字典,另外是不是可以遍历发票找对应的收款和客户
|
7
whileFalse 2020-12-17 17:40:01 +08:00
我估计你套了双层 /三层循环。请善用字典。
|
8
whosesmile 2020-12-17 17:41:15 +08:00
因为我理解你是每个月对账,所以这个用户的数据不是全库的,而是按时间维度收窄的,你的内存应该装得下的。
|
9
Bazingal 2020-12-17 17:43:55 +08:00 via Android
计算部分根据实际情况使用异步或者并行
|
10
whileFalse 2020-12-17 17:44:38 +08:00
另外,对“技术深度不行表现在没有 MVC”不能苟同。
|
11
xzour OP @whileFalse 一个微型的 ERP 没有 MVC 分层思想不严重吗?
|
12
xzour OP @whileFalse 具体表现为控制器方法都是为 3~1000 行的代码,db,业务逻辑耦合一起。
|
13
xzour OP @whileFalse 大佬举个数组转字典的优化例子,不太懂怎么用。
|
15
xzour OP @whosesmile 目前是一次取 DB,客户,收款,发票,然后是内存检索客户对应的收款发票对冲的。 你的思路是个好思路客户 ID 是可以排序的。谢谢
|
16
laminux29 2020-12-18 01:16:48 +08:00
1.C/Cpp 以及传统主流数据库,对于 1000 * ( 1000 + 1000 )规模的两层 for 循环或游标遍历,甚至 1000 * 1000 * 1000 规模的 3 层循环或游标遍历,根本毫无压力。
如果有瓶颈,需要分析瓶颈在哪。 比如常见的错误实践,在编程语言中这样写: for ----for ----.----for ----.----.----string SQL = "SELECT *...."; ----.----.----db.search(SQL); 那么主要的瓶颈就在 [db.search(SQL);] 这条语句这里,具体一点就是编程语言的数据库客户端驱动,与数据库的接口调用这里。这种问题需要把程序逻辑全部移动到数据库的存储过程里,然后编程语言这边对数据库进行一次调用,瓶颈就解决了。 2.就算不改这些东西,总体时间也只是 30 分钟的话,可以考虑增加 N 台服务器,把数据分为 N 组,每台跑一组,这样总体时间可以缩减到 30 分钟 ÷ N + 汇总时间。 |
17
xzour OP @laminux29 为什么要用存储过程这种方式?而不是在遍历之前把所有数据先查询好?是考虑内存限制的问题吗?在多数据源的情况下是不是不好处理了
|
18
l00t 2020-12-18 09:22:11 +08:00
@xzour #17 他是怀疑你每次都只读一条,这样大量时间耗费在数据库交互上。如果你一次全部读出来,那自然是无所谓,这个位置也不会是 db.search
|
19
no1xsyzy 2020-12-18 09:52:39 +08:00
@laminux29 6000*6000*6000 按 NOIP 估法,遍历总量除以 1e8 为秒数,大约 36 分钟
O(n^3) 增长挺快的。 |
20
no1xsyzy 2020-12-18 10:02:04 +08:00
第二个问题可以用局部表达式结果缓存来优化
动态修改的体感上的话,也可以通过每个被计算的对象依次计算,有值就推给前端来实现至少前几个能够立即响应(简单的 UX 设计) 甚至前端做动态滚动显示,滚动到的范围才会被求值(需要前端能力) |
21
laminux29 2020-12-18 10:12:08 +08:00
@xzour 我的意思是,只要不在程序里 for 循环然后一条一条去查就行了。存储过程是我建议的方式,你用其他方式也行。总之瓶颈不要被限制在数据库客户端驱动的 rpc call 就行。
|
22
jj783850915 2020-12-18 12:26:42 +08:00 via Android
善用表驱动 避免非必要的查找
|
23
tlday 2020-12-18 12:54:51 +08:00
第一个问题感觉本质上是数据库结构设计有问题,依我对财务极其粗浅的理解,收款和发票理想不该是一对一,最次不该是多对多的关系么。
第二个问题我建议你还是从根本上解决,从后端实现,不知道为什么你觉得改前端代码就不算改代码了...而且前端传表达式这种东西,漏洞,bug 会非常多。我不是很懂计算提成这种东西敢放在前端计算是什么操作。即便只是 bool 条件,也是很容易存在很大漏洞的。更别提如果后面上了权限管理,你可能需要 hack 表达式解析引擎了。我查了一下你说的这个 Flee,298 个 star,上一次提交是二月份合并了两个 pr,再上一次提交已经是去年三月份了,这种不仅用的人少而且 maintainer 不活跃的库真的不建议用在生产环境。 |
24
xzour OP @tlday 第一个问题:公司的业务收款发票不是一对一,但是给业务员的提成是实时回款结算,回多少结算多少,并且根据产品性质,营销活动,所在区域,下单日期等等落实到具体单据综合结算的。比如一个客户分别在一个月内买了 N 个商品(不同属性),前期预付款 30%,2 个月后付款 70%,这还算比较简单的。你也可以理解为 2W 多条数据,目前有 70 种提成条件这样子。
所以收款明细与发票,最多能保持总额账的话是相等的。但不代表每期是相等的。收款不是一笔收完的。可以理解为多对多关系。 第二条,是因为 C#的解决方案好像是没有 JAVA 解决方案多还是,个人能力有限,不能很好的找到替代方案,之前的公司方案是用 sql 来计算 BOOL 和数值的。公式解放给财务配置,我想这是未来财务自动化一个很大的方向。我是想参考 OA 自动工作流中如何自定义审批流来实现的。(云之家,泛微、企业微信的一些审批第三方件都支持),即拖拉思维导图,写上触发条件,即走哪一条线,(当然这次一条数据),比我 2W 条数据会少很多。体感也不错。 至于 eval 的问题,当然弊端多多,但是既然商业上有这种产品,说明他们是有克服前端传过去执行所产生的的问题。就比如这个 Flee,它分三步,会事先限定每个变量的类型,再渲染公式计算树,最后才计算 bool 或者值,也可以传入自定义的静态方法,反射调用?安全性比我用 sql,用 eval(javascript )安全多了,当然,表达式引擎 JAVA 好像比较多的解决方案,如果有人有接触过这种动态表达式的开发,不胜感激。 |
25
tlday 2020-12-18 20:13:49 +08:00
@xzour 如果我是你,就实时核销,他们要反清帐,对应核销记录就置 revoked 。怎么实时核销,想写了就每次创建收款记录 /发票记录时自己算,不想写了让财务人员自己选,自由还给他们,做错了是他们的问题,你的责任是保证系统不出错,逻辑没问题,尽量提示他们的低级疏忽,尽量简化他们的操作,但没有义务承担他们所有操作的责任。
如果要看客户期末余额,就收款 /发票两个表 group by user_id,sum 一下相减。 如果要看发票剩余金额,就发票 /核销两个表 filter by revoked, group by fapiao_id,sum 一下 filter 出值不为 0 的。 如果不想大改系统,就用 whosesmile 的办法。你要做的实际上是创建这样一个结构: [ { 用户 id: xxx, 收款记录: [],发票记录: [] }, { 用户 id: xxx, 收款记录: [],发票记录: [] }.... ] 然后遍历整个数组,每个项里面的两个数组采用类似“合并两个有序数组”的方法对对碰。 |
26
xzour OP @tlday 大概理解了 3N 遍历与 N^3 遍历的代码区别了,十分感谢!我应该是为了省事,没有重新组装数据结构而导致的,手动核销他们是不愿意的,因为核销逻辑大体思路上就是先进先销,都是重复工作,需要解放劳动力的,未来财务更多是检查,现在这种核销,这只是一种规则,未来有可能只是一种配置,据我了解,这也是一种财务自动化的一个具体实施点,类似 SAP 的自动核销,只是 SAP 目前的核销没有细节到发票构成,不满足我司的使用习惯。
楼上一些回复转字典,HashTable 的我倒现在还不太懂。很抱歉会产生 X-Y 的问题。 |