最新动态
- 项目前后期关系算法问题
项目表中存在如下3个字段: 项目编号:当前项目的编号。 前期项目编号:当前项目的所有前期项目的项目编号,包括前期的前期。 后期项目编号:当前项目的所有后期项目的项目编号,包括后期的后期。 数据示例:有A、B、C、D四个项目,关系如下图,存储结构如下表。 前期项目编号 项目编号 后期项目编号 - A B,C,D A B C,D A,B C - A,B D - 需要对系统中现存的项目前后期关系数据做校验、修正、简化、分析。 校验 校验前后期关系数据的完整性、一致性。 算法思想:如果A在B的前期中,则B一定在A的后期中。 # 获取项目列表 projects = ... # 映射表:项目编号 → 项目 map = {project["code"]: project for project in projects} for project in projects: code = project["code"] for previous_code in project["previous_codes"]: if previous_code not in map: print(f"{code} 的前期项目 {previous_code} 不存在") elif code not in map[previous_code]["next_codes"]: print(f"{code} 不在前期项目 {previous_code} 的后期项目中") for next_code in project["next_codes"]: if next_code not in map: print(f"{code} 的后期项目 {next_code} 不存在") elif code not in map[next_code]["previous_codes"]: print(f"{code} 不在后期项目 {next_code} 的前期项目中") 修正 针对校验出的问题,修正办法: 项目编号不存在:可能是项目编号改变了但未同步更新,修正项目编号,修正同步更新机制;如果真的不存在,则确认后删除。 B不在其前期A的后期中:在A的后期中补充B。 A不在其后期B的前期中:在B的前期中补充A。 多次运行校验,直到无错误。 简化 前期、后期字段这么设计,是为了做链接,方便从一个项目跳转到有前后期关系的任意项目,但这给我们分析前后期关系情况带来了麻烦。实际上,项目表中只需要存储项目编号+直接后期项目编号(不包括后期的后期)这两个字段即可,直接前期、所有前期、所有后期都可以通过这两个字段动态计算出来。因此,在校验并修正之后,我们对关系的存储结构做简化。 算法思想:首先确认不存在隔代后期关系,即在A→B→C的场景下,A→C一定是无效的。那么,对于任意一条弧A→B,如果存在顶点P,且同时存在P→A、P→B,则P→B一定是无效的。 # 遍历每一条弧 for project in projects: code = project["code"] for next_code in project["next_codes"]: # 找出同时满足p→code和p→next_code的顶点 for p in projects: if code in p["next_codes"] and next_code in p["next_codes"]: # 删除弧p→next_code p["next_codes"].remove(next_code) 分析 聚类 将有前后期关系的所有项目聚类在一起,构成一个个独立的项目群。 算法思想:求有向图的所有极大连通子图,用并查集实现。 # 项目编号的并查集,简单实现 # 初始每个项目编号是一个集合 disjoint_set = [{project["code"]} for project in projects] for project in projects: code = project["code"] for next_code in project["next_codes"]: # 找出code所在的集合set1 for set1 in disjoint_set: if code in set1: # 找出next_code所在的集合set2 for set2 in disjoint_set: if next_code in set2: # 如果set1不是set2,则合并为一个集合 if set1 is not set2: set1.update(set2) disjoint_set.remove(set2) 算法结束时,并查集中的每个集合就是一个项目群。 图形化 我们希望以图形的方式直观地看到每个项目群中各项目的前后期关系。 Python的networkx+matplotlib库可以实现图(graph)的绘制。绘制时,需要计算顶点的摆放层号,使得绘图结果清晰、美观。 算法思想:对图做拓扑排序,入度为0的顶点被取出的批次,就是顶点的层号。 # 映射表:项目编号 → 项目 project_map = {project["code"]: project for project in projects} # 项目群序号 order = 1 # 遍历项目群 for codes in disjoint_set: # codes表示顶点集合 # 只关注多顶点子图,不关注单顶点子图 if len(codes) > 1: # 弧集合 arcs = [ (code, next_code) for code in codes for next_code in project_map[code]["next_codes"] ] # 映射表:顶点 → 弧尾顶点集合 in_map = {code: set() for code in codes} for arc in arcs: in_map[arc[1]].add(arc[0]) # 映射表:顶点 → 绘图时的层号 layer_map = {} # 拓扑排序,入度为0的顶点被取出的批次,就是层号 layer = 0 while in_map: # 找出入度为0的顶点,得到层号 zero_degree_codes = [ code for code, values in in_map.items() if len(values) == 0 ] for code in zero_degree_codes: layer_map[code] = layer layer += 1 # 删除入度为0的节点 for code in zero_degree_codes: del in_map[code] for values in in_map.values(): values.discard(code) # 绘图 fig, ax = plt.subplots(figsize=(24, 13.5)) graph = nx.DiGraph() graph.add_nodes_from(codes) graph.add_edges_from(arcs) for code in codes: graph.nodes[code]["subset"] = layer_map[code] positions = nx.multipartite_layout(graph) nx.draw_networkx_nodes(graph, positions, ax=ax, node_size=10000) nx.draw_networkx_edges(graph, positions, ax=ax, arrowsize=20, min_target_margin=50) nx.draw_networkx_labels(graph, positions, ax=ax, font_color="white") ax.axis("off") plt.savefig(f"output/项目前后期关系图/{order}.png") plt.close(fig) order += 1 算法缺陷:由于networkx按顺序绘制顶点,因此存在弧交叉、弧穿越顶点的问题,但总体上绘图结果还是很好的。
- 分析复杂的Power Query实现的Excel文件的逻辑
公司的财务顾问使用Power Query技术制作了几个非常复杂的Excel文件,用于业务数据分析。业务人员知道如何使用这些Excel,可无法读懂其中的逻辑细节,出现问题时也无法解决。考虑到未来这些逻辑都需要我做到系统中,那么这些其实就是第一手的需求资料,于是我决定,主动帮助业务人员分析这些Excel的逻辑。 一、学习 首先了解并学习Power Query是什么,能做什么,有什么特性和限制,并做一些实验和demo来加深理解。 我理解到,Power Query其实是一种表处理语言,从一张表计算出另一张表。非技术人员通过Excel软件中的图形化界面来编排计算过程,但背后其实就是代码(M语言代码)。 二、提取元素和关系 将每个Excel中的主要元素逐个提取出来,包括: Excel文件(工作簿) 工作表(Sheet) 表格(命名单元格区域) 单元格区域(非命名单元格区域) 数据透视表 数据透视表的切片器 Power Query的查询 Power Pivot的数据模型 这些就是要分析的问题对象。我将它们用不同的图形表示出来,画在一张PPT上。 然后分析它们之间的关系,包括: 元素之间的包含关系,例如Excel文件与工作表、查询,工作表与表格、数据透视表。 Excel文件的输入关系,即Excel文件从哪个Excel文件取数。 查询的输入关系,即查询从哪些Excel文件、表格、其他查询取数。 查询的输出关系,即查询的结果被哪个查询取数,或输出到哪个表格,或输出到哪个数据模型。 数据模型的关系,例如数据模型从哪个查询取数、数据模型间的连接关系。 将这些关系用连线或箭头表示出来,同样画在PPT上。 整理PPT中的元素和关系,就得到了整体结构图。 三、理解查询代码 整体上的计算过程是: 输入:从表格或Excel文件读取数据。 计算:Power Query执行计算。 输出:将计算结果输出到表格。 那么接下来要重点分析各Power Query查询的计算过程。 首先将每个查询的代码都提取出来,逐个放到文本文件中。然后逐个阅读这些代码文件,尝试理解,并写注释。 这个过程的主要困难在于,代码是最末端、最具体的执行步骤。一个宏观上的意图,可能会表现为多条代码。限于表处理语言的机制(只能表算表),计算过程的表达形式跟我们常用的编程语言很不一样,很简单的一个操作,Power Query经常不得不写得很绕、很晦涩。此外,步骤都是图形化编排出来的,没有有意义的命名。所以,基本上跟读反编译代码差不多,主要靠推理和猜测。这里我采用自底向上的思路,先用自然语言描述出每一个步骤做了什么,再联系上下文猜测它的宏观意图是什么,最后跟业务人员同步信息确认理解是否正确。 我对每个查询都进行了仔细分析,阅读并注释了每一行代码,画出了数据流转关系图,写出了对宏观意图的理解,写出了对主要计算过程的理解,写出了对计算结果的描述(形式和内容)。 四、报告与讲解 我将分析出来的所有成果,写成了一个报告文档。文档先总述Power Query的基础知识、各个Excel文件之间的关系,然后逐个Excel详细描述分析成果,包括Excel文件内的各结构图、每个查询的分析、每个数据透视表的分析等。 我将报告文档提交给了领导和相关业务同事,然后在领导的要求下,给领导和同事现场清楚地讲解了整个逻辑。 五、价值 后来的事实证明,我的工作是非常有价值的。 我清楚地理解了这里面的每一个细节,成为了最了解这一块业务的人之一(甚至某些方面还要超过业务人员),能参与进跟财务顾问的讨论,能在后续上系统时帮助产品经理纠正理解上的不足和偏差,能在开发时有更高的效率。
- OA系统实现复制建模布局
做一个复杂的需求,要求对于同一份建模数据,不同的用户在新建、编辑、查看时使用不同的布局。实现时,先做一个完整的母版布局,然后复制出用户布局并修改。Ecology 9没有提供复制建模布局的功能,因此我们自己实现。 为了避免使OA系统出现未知的问题,我们限定,只允许在同一个模块内,复制一个已存在的布局到另一个已存在的布局。 从接口/formmode/exceldesign/excelLayoutSave.jsp?operation=saveExcel出发,逆向分析OA系统保存建模布局的机制,得出结论,只需复制以下表的数据即可实现复制建模布局: 表 数据内容 复制方式 modehtmllayout 布局自身的信息,例如布局设计、代码块等 update,注意保留布局名称 modeformfield 布局的字段信息,例如是否必填等 先delete再insert,注意复制到显示布局时字段全部变为只读 modefieldattr 布局的字段属性信息,例如计算日期差等 先delete再insert modeformgroup 布局的明细表信息,例如是否允许增行等 先delete再insert,注意复制到显示布局时关闭修改的相关配置 mode_layout_querysql 布局的明细表的固定查询条件 先delete再insert mode_layout_sortfield 布局的明细表的字段排序信息 先delete再insert
- oa-codeblock项目架构设计 - JS文件包含
最近在开发一个非常复杂的需求,代码量巨大,为了清晰起见,必须拆分成多个JS文件,然后有序地组合起来。开发框架目前只支持将代码写在单个JS文件script.js中,因此需要修改。 考虑历史代码兼容性和开发周期,我决定采用一种简单的实现方案:文件包含。类似于C语言中的#include,我们在JS文件中做特殊标记,表明要将另一个JS文件的内容包含进来,然后在构建时用指定JS文件的内容替换这个特殊标记,这样就实现了分开编写、组合运行。 文件包含标记:在JS文件中用如下标记表明要包含哪个JS文件: //#include: A/B/C/xxx.js 解析文件包含: 迭代解析:A包含B,B包含C,通过迭代算法可以实现正确的链式包含。算法:循环检查A中是否还有包含标记,有则对第一个包含标记(B)执行替换,此时,如果替换的内容中带进来了新的包含标记(C),则会在下一次循环中被解析。 防止循环包含:迭代解析的过程中,要记住走过的文件有哪些,一旦发现要包含的文件之前已经走过,就说明出现了循环包含,报错。 动态监听:一个脚本文件现在可以拆分为多个文件,这多个文件又随时可能修改、增加、删除,则对于持续构建模式来说,要监听变化的文件范围是动态的。每次构建一个脚本文件时,都要重新计算并监听涉及的文件。 有了文件包含机制,我们就可以获得以下好处: 将单个复杂的任务分解为多个简单的任务,提高了清晰度、可维护性。 可以实现公共代码,免于复制粘贴,提高了代码复用度。 在OA系统的代码块机制中,多个<script src="...">之间的执行顺序是随机的,通过在同一个文件中控制文件包含的顺序,我们就可以严格控制多个脚本间的执行顺序。 文件包含机制也有其缺点: IDE不识别:标记是我们自己定的,IDE并不知道要包含哪个文件、文件间的顺序等信息,也就无法提供文件跳转、准确的代码提示等功能。 作用域不隔离:文件包含本质上是将代码拼在一起,是同一个作用域,并没有实现真正的JS模块化。编写代码时需要对这一点有清晰的认识,如果需要模块化,则必须自己用IIFE等手段实现。 不太容易理解:我开发的这个非常复杂的需求,其他同事在阅读代码时,不太容易跟踪和理解同一作用域、多层包含下的执行过程。 这些缺点,未来可以通过用Webpack实现真正的JS模块化来解决。
- OA系统前端逆向工程
泛微OA系统Ecology 9的前端程序是经过编译、压缩后的JavaScript文件,为了深入研究其机制细节,我们对其做逆向工程,将压缩的JS代码还原为非压缩的JS代码,并对其进行分析、理解。 建立一个项目oa-frontend-reverse-engineering,将线上的JS文件下载下来,按相同的目录结构放在项目中。然后在本地用nginx做反向代理:如果一个JS文件在项目中,则取项目中的,否则取线上的。nginx配置如下: server { listen 80; listen [::]:80; server_name oa-frontend-reverse-engineering; root "<项目目录>"; location / { # 如果本地存在文件,则取本地文件 # 否则,通过反向代理,取OA系统上的文件 try_files $uri $uri/ @reverse_proxy; } location @reverse_proxy { proxy_pass http://<OA系统的IP地址>$request_uri; } } 浏览器通过http://oa-frontend-reverse-engineering/可以正常访问OA系统,其中部分JS文件取自本地,于是,我们不仅可以阅读代码,还可以通过实际执行、断点调试来深入理解细节。 注:后来Chrome系浏览器在DevTools的“源代码”功能中增加了“替换”功能,可以直接用本地JS文件替换指定的线上JS文件,这比做nginx反向代理更简便。以下是阅读、还原压缩的JavaScript代码的方法和经验: 第一步:用代码格式化工具(例如VSCode)将压缩的JS代码格式化为缩进良好的版本。 写注释。 利用编辑器、IDE的重构功能,将变量改为有意义的名字。 利用编辑器、IDE的代码跟踪功能,跳转到相关的上下文。但JS代码由于动态性高,通常难以跳转,转而利用搜索功能,用函数名、特殊字符串等关键信息查找上下文。 在分析过程中,熟悉!0、&&、||、模块化等常见的编译结果模式,理解JS语法原理,还原为对应的正常写法。 断点调试,跟踪执行。 临时改写代码,将某些对象、数据、函数暴露到window上,在控制台中查看、修改、执行之,这对状态存储数据尤其有用。 注意:还原出来的JS文件只允许用于分析,不允许替换线上的JS文件。
- OA系统后端逆向工程
泛微OA系统Ecology 9的后端程序使用Java语言开发,为了深入研究其机制细节,我们对其做逆向工程,将class文件反编译为Java源代码,并对源代码进行分析、理解。 反编译 Ecology 9的class文件都存放在classbean目录中。从服务器上将classbean目录整体下载到本地,然后使用JetBrains IntelliJ IDEA附带的反编译器FernFlower,将classbean目录整体反编译到src目录: java -cp '<IDEA安装目录>\plugins\java-decompiler\lib\java-decompiler.jar' org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true .\classbean\ .\src\ 代码分析 反编译出来的源代码并不是原始代码,它的类名、属性名、方法名、字符串等都保留了意义,但所有的方法参数、局部变量都是形如var1、var2的无意义形式,也没有任何注释。 我们建立一个Git仓库oa-backend-reverse-engineering,将反编译出来的源代码放入其中管理,因为我们会在阅读的过程中修改它。我们同时将WEB-INF/lib目录(OA系统的三方库)放入其中,使类库整体完整,以便IDE识别。 以下是阅读、分析反编译出来的Java源代码的方法和经验: 写注释,包括文档注释、行注释等。 利用IDE的重构功能,将局部变量改为有意义的名字。 利用IDE的代码跟踪功能,跳转到相关的上下文。 在浏览器DevTools中提取API路径的关键词,在IDE中搜索,定位到起始执行点。 对最常见的类,如BaseBean、RecordSet,做完整分析,加深理解。 对最常见的架构设计,如命令(command)设计模式,做架构分析,加深理解。 注意:反编译出来的Java源代码只允许用于分析,不允许重新编译后替换线上的class文件。
- OA系统热更新Java类
我们的Java类在OA系统中运行时,如果发现bug,因为白天有大量用户在使用,无法重启JVM,就只能半夜更新代码并重启,故障处理时间很长。经研究,部分bug完全可以在白天通过热更新手段迅速修复。 工具:Arthas(阿里巴巴开源的JVM热更新工具)。 热更新要求:类不能增减字段、方法,只能修改方法的执行过程。 热更新过程: 本地修改Java代码,编译成class文件,上传到OA系统中,覆盖问题class文件(避免下次重启仍加载问题class文件)。 启动Arthas,选择名称为Resin的Java进程(OA系统的Web容器)。Resin进程可能有多个,在Arthas连接到进程后,通过jad com.company.<Tab键>来判断进程中是否有我们的Java类,从而确定是OA系统的进程。 在Arthas中执行命令retransform <class文件的路径>,尝试热更新。如果看到输出success,则成功,否则失败。注意不要使用redefine命令。
- 清除OA系统的表缓存
OA系统在读写数据库时,会对各表维护缓存,以提高性能。实际业务中,我们经常要在OA系统之外写库,此时OA系统中的表缓存可能是陈旧的,这会导致数据不一致。因此,我们需要清除OA系统中指定表的表缓存的功能。OA系统未提供这种功能,我们要自己想办法开发出来。 原理 通过逆向工程分析OA系统的表缓存机制。 RecordSet的executeQuery()和executeUpdate()最终都是执行executeSql()。在executeSql()中,以"select"开头的SQL会先尝试从缓存获取结果,非"select"开头的SQL不会。在未命中缓存的情况下,SQL执行成功后,会调用CacheFactory的refreshCache()更新缓存。 在CacheFactory的refreshCache()中,如果SQL以"select"开头,则调用putCache()更新指定表的缓存的一部分;如果SQL非"select"开头,则调用removeCacheForSql()清除指定表的所有缓存。 那么,想要清除指定表的缓存,就只需要执行一条非"select"开头的、包含指定表名的SQL语句,比如写库SQL,但更安全的办法是执行desc 表名。 实现 实现APIclearTableCache,接收参数tableNames。遍历表名,对每个表名通过RecordSet的executeQuery()执行SQLdesc 表名。
- 分析复杂的公式实现的Excel文件的逻辑
背景 公司有一个非常复杂的Excel文件,用于填报数据并做一些基本的分析。这个Excel文件设计得很精巧,充分利用了Excel的各种功能,通过颜色区分、基础数据区域、下拉选项、公式、条件样式等手段,实现了基础数据配置、数据填报、数据校验、统计分析等功能,几乎就是一个独立的业务系统了,设计者一定很厉害。 我们接到的需求是,将这个Excel的功能做上系统,毕竟线下使用不方便,数据也不通。开发时,有一个业务人员跟我们对接(非Excel设计者),我们做到哪一块,就问她这一块有什么逻辑,然后她去看公式,描述给我们。我发现,这样太低效了:① 她没有编程背景,自然语言描述经常不够准确;② 多个有互相联系的块,逐个描述,容易漏掉重要信息;③ 对于特别复杂的公式,她也看不太懂;④ 没有落到纸面,后面不清楚了还得重复问。我觉得,想把这个事情干好干扎实,对Excel中的逻辑进行系统、全面、细致的分析是必不可少的,这个事情,我来做。 分析过程 将每个Sheet导出为PDF,然后导入到我的iPad的GoodNotes软件中。 逐个Sheet,逐个单元格,阅读公式,理解并标注: 对于简单的单元格或公式,直接在单元格旁边手写标注,例如常量还是变量,输入形式是什么,从哪里取数,用伪代码重新表达逻辑等。 对于复杂的公式,先将其复制到VSCode中,加缩进格式化,使之更易读,然后再粘贴到GoodNotes中,一点一点地分析、标注计算过程,理解其意图。 至此,我已经充分理解了Excel中的所有逻辑,并得到了一份详细的分析手稿。限于开发周期,此时完成手稿就结束了。之后所有的逻辑改为由我来描述和检查,就这样,我们完成了第一版的开发。 进行第二版迭代时,我进一步将分析成果编写成了一个形式良好的文档,除了手稿中的内容(有不少变化),还考虑了目前已在系统上实现的功能模块划分、数据结构、填写控制等设计,使用更规范的描述方式(例如字段的多级命名、用颜色区分元素类型、更严谨的伪代码等),对每一个字段的逻辑进行了详细的描述。 至此,我们的软件迭代彻底从Excel文件脱离了出来,后续逻辑的更新只需要在这份文档的基础上迭代即可。 价值 对问题充分的理解,是解决问题的重要基础。我自己去理解了这些,后面做起事情来就踏实、从容很多了。 这也说明,事情没有一定之规,合适的方法要自己去找出来。
- oa-codeblock项目架构设计 - 支持JSX和SCSS
使用代码块开发复杂的功能时,经常需要写React组件和复杂的CSS来实现界面,目前的开发框架只支持原生JS和CSS,因此书写很繁琐,需要修改。 支持编译JSX:在JS业务代码被替换到template.js之前,先用Babel库进行编译(presets为@babel/preset-react)。 支持编译SCSS:如果样式文件的文件名为style.scss,则先用Sass库将SCSS代码编译为CSS代码。style.css则无需编译。 支持JSX和SCSS语法,显著提高了React组件和样式的开发效率。