摘要:本系统旨在设计一款基于MVC的web系统,以产品经理和项目经理为目标用户,针对EXCEL表格统计软件的不足,提出一套轻量级、易操作的解决方案,搭建了一个存储在云端的项目资源管理网站。系统围绕企业中人与项目这两个资源该如何搭配这个主题,提供了项目资源的编辑与统计服务等定制的项目管理功能,能够让管理人员在网页上管理员工与项目之间的工时安排,编辑、统计每个项目对每个部门的资源需求以及每个部门给每个项目提供的人力资源数等具体功能。本系统以material design为UI主题,以SPA应用程序为设计模式,以函数式编程为代码风格,实现一个高可用,易扩展的网站。
关键词:Web;Material Design;MVC;项目管理;资源分配
1绪论
1.1 选题目的
目前,很多中小型企业有着“上云”的趋势,很多公司将内部的内容管理系统的建设任务外包给软件公司,本次设计的目标是以产品经理和项目经理为目标用户,搭建一个存储在云端的项目资源管理网站,提供高效的项目资源分配分析服务。Windows上提供的管理软件比如office,Outlook都不够灵活,无法满足日益增长的需求,尤其是Excel统计工具在某些场景下异常臃肿,本系统主要就是针对表格统计软件的不足,提出一套轻量级,易操作的解决方案。
本次选题的意义在于实现云端系统给企业管理带来批量增删改查的功能,并且开放源码推广给其他用户,同时还要保证系统在保持高可扩展的同时是否能做到高内聚低耦合。
1.2 研究内容
目前很多企业管理者感受到Windows上提供的管理软件比如office,Outlook都有不够灵活,无法多人同时编辑的缺陷,无法满足日益增长的需求,尤其是传统的Excel统计工具更是异常臃肿,本系统主要就是针对表格统计软件的不足,提出一套自己的解决方案,能够轻松的扩充功能并且向下兼容[1]。
以产品经理和项目经理为目标用户,设计开发一款项目资源管理网站。要求给出合理的需求分析、详尽的总体设计方案以及详细设计说明,编程实现基于web的项目资源分配系统,具有云端数据存储、在线编辑资源、监控项目资源、数据统计分析及图表绘制等功能,软件最终要完成测试。
2系统需求分析
本系统以产品经理和项目经理为目标用户,设计开发一款项目资源管理网站。要求给出合理的需求分析、详尽的总体设计方案以及详细设计说明,编程实现基于web的项目资源分配系统,具有云端数据存储、在线编辑资源、监控项目资源、数据统计分析及图表绘制等功能,软件最终要完成测试。要求系统界面美观大方,操作模式友好,用户体验优良。
2.1 功能性分析
如今,所有的桌面软件都有一种“上浏览器端”的趋势,比如许多开发工具IDE现在都出现了相应的ODE(online IDE),很多绘图制表的工具也从传统的app变成在线使用的工具,当然还有各种web管理系统,都上web了。在这个趋势的背后,Web平台的发展起到了至关重要的作用:web平台由浏览器和HTTP(s)组成,在20多年的发展过程中衍生出了许多分支,其中nodejs和webassembly是2个里程碑式的技术。其中nodejs将web技术拓展到了服务器端,webassembly则让所有客户端的app上web成为可能。
本次的项目管理系统需要的功能主要是方便项目经理或部门经理规划公司某地区内所有的项目和人力资源,以及如何将人力资源(以时间为单位)合理的分配到不同的项目上。基础功能之上还有一些进阶的功能需求如统计的功能,包括排序、过滤、索引、制图,还有UI上的“隐含“要求比如动画、遮罩层、弹窗、字体。除此之外,网站还要解决excel“无法多人同时编辑”的缺陷,即要求可以多人同时登录,同时修改数据库。
从用户的角度,可以画出一个用户需求图:
图2.1 用户需求图
图2.1中,用户希望能够直观,方便的编辑项目对部门的需求,以及部门对项目的实际分配,最好是在熟悉的表格中进行操作,然后需要能生成一个统计图,最后可以将这个表格备份到本机。
2.2 非功能性需求
web平台能做的事情越来越多,也越来越强大,本系统的关键来自于,如何合理的利用web平台,将传统企业基于Excel的管理模式搬移至web上面,满足用户对性能的需求。
众所周知,许多传统制造业,尤其是外企的工作站一直采用微软公司的“全套服务”,即Windows企业版,Outlook邮件,Skype for business通讯工具,Office办公四件套,甚至IE浏览器。
从用户的角度,很多企业管理者感受到Windows上提供的管理软件比如office,Outlook都有不够灵活,无法多人同时编辑的缺陷,无法满足日益增长的需求,尤其是传统的Excel统计工具更是异常臃肿。
让用户浏览网页理论上要比使用传统的excel快,因为以前通常是打开一个体积庞大的excel文件,一次读取所有的数据,但网站则是读取自己需要的一部分数据,网络带宽和前端渲染上都会好很多。因为是通过网络传输数据,用户还要求保证数据安全性,保证传输中数据的加密和认证。
除此之外,还要求系统页面美观,交互性强。
3 开发环境以及相关技术
系统前端基于现代浏览器,以Chrome60 为准,后端是nodejs服务器,开发语言都是JavaScript,数据库使用超越关系型的mongodb。
3.1 前端框架
首先考虑到整个UI风格,系统更倾向于使用目前比较流行的material design风格,所以UI框架采用Google的MDC(material design component)。MDC是一个轻量级的UI框架,提供了用户友好的图标,动画,尤其是动画可以带来很好的操作暗示[3],如图3.1所示。
图3.1 material design提供的各种UI元素
除此之外还需要一个表格驱动引擎Ag-grid,这是一个重量级的网格插件,可以在表格的基础上提供丰富的操作,表格的主题也支持material,如图3.2所示。Ag-grid并不是由社区维护,而是一家公司,版本更新的力度很足,issue反馈率也非常高,所以Ag-gird非常可靠。
为了方便用户更好的编辑数据,尤其是具体的json对象比如人的姓名部门等属性,本系统引入了一个json-editor插件来渲染编辑器,并将他制作成一个异步模块以方便调用。
最后,为了能够生成统计图,使用Chart.js来渲染雷达图或者柱状图。
图3.2 ag-grid的material主题的大致风格
3.2 后端框架
Web容器采用最流行的express。express是一个非常精简但是强大的web容器,在node的异步光环下完美实现了路由,session,中间件,静态文件,模板引擎等核心功能,同时安全性也做得非常好。express还是nodejs基金会的成员,某种意义上,express可以和node标准库相提并论
数据库采用了和nodejs搭配极佳的mongodb,后者支持易扩展的数据结构[4],后者是一个存储类json对象的一种超越关系型的数据库,但和SQL类似,mongo也支持关系代数和集合论,也拥有索引和过滤器,创新的地方在于它可以储存嵌套的数据结构,为此还专门设计了一个叫做BSON的二进制协议格式,用于底层的存储和传输。Mongodb非常适合数据结构复杂的企业级管理系统,也就是本系统。
此外还用到了一些小插件,比如用于打包备份的压缩模块archiver,用于给密码进行sha1加密的crypto模块,以及标准库的https模块(提供ssl认证服务[5])。
3.3 全栈设计模式
前端并没有使用经典的mvvm框架,同时ag-grid提供的状态栏和context菜单内置了数据绑定的功能,所以前端间接性的使用了MVVM设计模式。
后端仍然是经典的MVC设计模式,结合express严格分离了数据层,业务逻辑层以及显示层[6]。
中间http的设计,选择业界最流行的restful api[2]设计模式。Rest严格意义上并不是设计模式,而是一个http的使用教程,旨在让开发者更好的使用http。
3.4 开发环境和工具
图3.3 开发者工具
3.4.1 vscode
Vscode是微软公司基于electron制作的IDE(集成开发环境),特点是轻量级但功能强大,插件系统非常丰富,同时还支持git,debug等有用的功能。本系统所有的前后端代码都在vscode上完成。
3.4.2 chrome
调试前端代码基本上在Chrome内置的devtool(开发者工具)中进行。前端开发者都知道,devtool是开发高性能页面的必备,它内置了html元素查看器,调试终端,资源管理器,网络工具,性能监视器,以及一堆开发者常用的工具。甚至Chrome还可以调试node程序[7],但本次只用它来测试前端的代码。
3.4.3 mongodb compass
Compass是mongodb官方推出的一款可视化客户端,界面友好,反应快捷,也是基于electron制作而成。Compass不仅可以作为开发工具,还可以作为项目上线后网站管理员的数据库管理工具。
3.4.4 github desktop
Github desktop是GitHub的可视化客户端工具,工具集成了所有的git命令和GitHub专有的功能,在本项目的开发过程中,它起到了本机与云端同步代码,保存历史版本记录的功能。
4 系统设计
根据之前的需求分析,本系统的基本功能是让用户(项目经理)维护每一个项目对不同部门的需求量,这里的需求量主要指的是人头数或者更准确的说是时间数,还有一个是与之相对应的数值:实际花费值,即部门内每个人每季度在项目X上贡献的时间(单位天)的总和。
前者的需求量通常由项目经理来填写,后者实际值部门经理来维护每个人所花费的时间,即资源数,最后将项目对部门资源的需求量和实际值进行对比,并绘制成雷达图的形式观察是否满足需求。
根据用户需求图2.1,制定一个详细的系统功能图,其中将用户需要的主要功能分类为“编辑与设置”与“统计”,“管理员”属于管理员用户需要的功能,“业务逻辑与界面”属于系统层面的一些功能。
图4.1 系统功能图
图4.1中,“统计”的目的旨在帮助用户更好的分析数据,利用表格和图表这2种表达方式合理的表现用户所需要的“资源数据”,“编辑”则从增删改的角度帮助用户更好的对业务逻辑上的数据进行操作,原则是尽可能的满足大众用户操作表格的习惯,“其他”则包含了其他一些很重要但用户默认的功能,比如登录注册登出机制,还有方便管理员维护的功能包括查看日志,远程调试。
4.1业务逻辑与界面
4.1.1 GUI设计
虽然传统的管理系统有一套标准的UI模板来构建整个页面,基本是灰白蓝主题,框架式的页面布局,但是本次系统希望引入material design的元素,让界面更美观,动画效果更明显,色彩更鲜明,给企业用户带来焕然一新的感觉,所以本系统设计UI上要额外花费一些精力,考虑添加一些遮罩层,动画,折叠与收缩等UI元素。
首先利用MDC框架搭起一个大的框架,包括标题栏,侧边栏,主体,然后在主体中嵌入aggrid框架提供的表格,表格在主体中可以自由滚动,变形,除此之外还需要一些悬浮在页面最上方的元素包括弹窗,提示框,对话框。
GUI设计需要使用到的模块包括schema封装模块,渲染循环模块,UI布局模块。
4.1.2 用户权限划分
考虑到许多应用场景,本系统划分了4种级别用户(正常情况下只使用其中2种),分别是:inactive user,common user,super user,root user,如表4.1所示。
表4.1 4种用户权限说明
权限等级 | 权限名称 | 权限范围 | 标识符 | 说明 |
---|---|---|---|---|
4 | Root | 全部 | _root | 管理员用户,拥有所有权限 |
3 | Super | 所有地区(全公司) | 无_common | Boss/总监级别(大部分用户) |
2 | Common | 本地区 | _common | 小的部门领导 |
1 | Inactive | 自身 | 无_pwd | 普通员工 |
表中root user指的是管理员用户,也就是网站的运维人员,在接手开发者的使用文档后,拥有远程debug,数据库备份,添加/删除部门和项目分类等权限。Super user则是大多数部门经理/产品经理的权限,可以添加/删除员工和项目,增删查改员工数据,项目需求等。Common user适用于一些特定的场景,这个场景中一些用户权限有限,不能查看其他用户所在的地区的信息,即只能在自己所管辖的范围内进行增删查改。Inactive user指的是那些非经理级别的普通员工,也就是出现在表格当中被当做资源分配的员工。
不同用户通过表中“标识符”一列的数据库字段来区分。
和用户权限划分有关的模块主要是数据过滤模块。
4.1.3 MVC设计
MVC是web后端设计的经典模式,MVC分别代表数据模型层,前端表现层,业务逻辑层。这三层在应用上分别对应着数据库,前端,后端,但都是在后端代码中连接在一起,这表示,虽然三层分工明确,但一定通过其中的业务逻辑层(controller)将剩下2层联系起来。本系统自然也遵循了MVC的原则:将mongodb的连接库封装而成的读写模块作为模型层挂载在全局对象上,将前端静态文件目录的检索接口放在路由器最前端的位置作为显示层,将所有的路由模块以http方法分类放在路由器的核心位置作为逻辑层。
图4.2 MVC之间的关系
和MVC设计有关的模块包括模型层模块,页面跳转模块。
4.1.4 编辑器功能
本系统的编辑器包括用户登录框和更新对象的框,都使用对话框加上json-editor来设计。从用户打开编辑器开始,到用户点击提交按钮这个过程封装成一个promise。
和编辑器功能相关的模块包括异步封装模块,UI布局模块,表格交互模块。
4.1.5 数据库功能
Mongodb是nosql数据库,nosql不是“非关系型”的意思,而是“不止关系型(not only)”,也就是说mongodb包含关系型[8]的表格结构,在类型上比传统sql数据库多了2种,分别是列表和字典(对象),本项目总共建立了4张表(mongo中叫做集合),外加sessions表是由session模块自动创建的,4个表都是管理员来维护。
图4.3:E-R图(属性参考下面的表格)
图4.3中3个主要对象中,person和project的关系(工作)就是某人在某项目上花费的时间(spent time),这个关系属性选择存放在person集合中以节省空间。腻歪一个关系属性是demand,代表项目对部门的时间需求,存储在department集合中。图中还能看到2个一对多的包含关系(belong)。
1) person集合
表4.2:person集合的结构
序号 | 字段 | 类型 | 说明 | 索引 | 验证 |
---|---|---|---|---|---|
1 | _id | String | 人名 | 是 | 正则表达式 |
2 | _about | String | 备注信息 | 长度限制 | |
3 | _root | Boolean | 是否有root权限 | ||
4 | _common | Boolean | 是否是普通用户 | ||
5 | _role | String | 所属部门 | 是 | 正则表达式 |
6 | _pwd | String | 密码(sha1加密) | ||
7 | _owner | String | 该用户的创建人 | ||
8 | _capacity | Number | 该用户的“余额” | ||
[project] | Object | 关于某个项目的信息,多个字段 | |||
…… | 同上一个字段 |
Person集合是存放所有员工,以及领导的集合,用户的登录,增删改查都会对这张表进行读写。除了前8个字段外,如果需要建立某个人和某个项目之间的联系,即通过实际分配的时间数,备注,当前状态来表示一个人在某个项目上的数据,这时候就可以通过一个以项目名(_id)为key的字段插在person对象之上,value是一个新的json对象,其中包括spent,comment,status等字段。
为了不和前面几个特殊字段名冲突,项目名首字符不允许是“_”。
2) project集合
表4.3:project集合的结构
序号 | 字段 | 类型 | 说明 | 索引 | 验证 |
---|---|---|---|---|---|
1 | _id | String | 项目名 | 是 | 正则表达式 |
2 | _about | String | 备注信息 | 长度限制 | |
3 | _owner | String | 该项目的创建人 | ||
4 | _type | String | 所属分类 | 是 | 正则表达式 |
5 | _sub_type | String | 所属子分类 |
Project集合存放了所有的项目。其中_type字段和_sub_type字段是为了某些地区用户服务,他们希望对项目进行二级分类,以方便管理,但这个特性并不常用。和person集合一样,利用_id作为项目名,类型索引_type作为批量操作的入口点。_owner是一个记录字段,记录上次修改这个项目的人,其余的2个字段都是描述字段。
3) department集合
表4.4:department集合的结构
序号 | 字段 | 类型 | 说明 | 索引 | 验证 |
---|---|---|---|---|---|
1 | _id | String | 部门名 | 是 | 正则表达式 |
[project] | number | 关于某个项目的信息,多个字段 | 整形数,大小限制 | ||
…… |
Department集合是一个只为存储项目对部门的公共需求(总需求)而存在的集合。这个集合比较简单,除了_id作为部门名,剩下若干个字段分别对应一个项目:键为项目名,值为部门被这个项目所要求的时间(数字类型)。
4) log集合
表4.5:log集合的结构
序号 | 字段 | 类型 | 说明 | 索引 | 验证 |
---|---|---|---|---|---|
1 | _id | Number | Log创建的时间 | 是 | |
2 | Method | string枚举 | http方法,get、post、put、delete… | 枚举类型 | |
3 | Ip | String | 用户的ip地址 | Ipv4、ipv6格式验证 | |
4 | User | String | 用户的_id | ||
5 | Person | String | 某操作所涉及的person的_id | ||
6 | Department | String | 某操作所涉及的department的_id | ||
7 | Project | String | 某操作所涉及的project的_id | ||
8 | About | String | 备注信息 | 长度限制 |
Log集合是一个辅助集合,记录某一个用户操作所产生的影响,也就是日志。这样有利于系统出现异常时的错误定位和责任追踪,查看日志也是网站管理员所必须的习惯。除了更新员工的分配时间这种高频操作外,其余所有的增删改操作都会计入log集合。关于如何实现在接下来的“系统实现一章”讲解。
和数据库功能有关的模块包括索引模块,模型层模块,数据验证模块。
4.2 统计
4.2.1 表格变形
表格变形指的是在数据层面不变的情况下,对view层面的表现形式进行变换[9],从而达到更好的数据展示效果。在context menu中直接提供了6种批量的变形操作。
1.Collapse All。将所有的索引子表折叠起来,只展现索引列表,这个操作和Expand All同样可以通过一个个点击表格中的箭头号来实现。
2.Expand All。是上面Collapse All的逆操作,即展开所有的索引子表。
3.Autosize All Columns。,根据当前可视窗口的内容自动调整每一列的宽度。
4.Reset Columns。重置表格到最开始的形状。
5.Group(only)by [column]。对当前聚焦的列进行一级索引。
6.Ungroup All。取消所有的索引。
除了这6种批量的变形操作,还可以对某一列某一行进行单独操作,比如在侧边栏可以过滤行或隐藏列,表头位置可以排序,手动调整列宽和顺序,手动折叠/展开索引等。所有的变形因为不影响数据,都是可逆操作,也不会触发请求。
和表格变形有关的模块包括表格交互模块,表格变形配置模块。
4.2.2 雷达图
绘制统计图是为了更直观的表现表格数据,主要是为了表现项目需求与部门的付出之间的对比。本来根据数据结构,有4种统计方式,分别是:
1)person/project
2)person/type
3)department/project
4)department/type
他们分别代表每一个person对应所有project或type,每一个department对应所有project或type,并以此为基础画出需求值和实际使用值的多边形(雷达图),然后进行面积的比较,从而达到观察需求是否满足的目的。至于选择雷达图是为了更好的表现面积。
但是考虑到实际情况,本系统只需要第3种方式,也就是对于某一个部门,将所有项目每7个一组绘制成一个雷达图,图中分别是demand线和actual(spent)线。
和雷达图有关的模块包括雷达图模块。
4.2.3 Excel导出
这个功能可以让用户将当前编辑过的表格导出成excel文件[10],这个功能设计在右键菜单中。
和excel导出有关的模块是表格交互模块。
4.2 编辑与设置
4.2.1 对象增删改查
更新包含增,删,改,其中增与改对应的http方法是post和put[11]。
本系统需要考虑3种对象的添加与修改,但主要考虑由用户操作直接引起的person对象和project对象。在允许操作前需要经过层层的验证和过滤方可通行,其中包括:
1)用户是否已经登录,检查request.session.user是否存在。
2)是否有权限,检查用户的_departmentList和_typeList字段。
3)用户_capacity字段是否>0。
4)确保person对象的_department或project对象的_type存在。
5)所有string类型字段需要满足相应的正则表达式。
删除对象操作对应的是delete方法,过滤器只需要判断是否有权限即可允许通过。删除project对象的时候要注意同时删除掉person和department集合中存在的相应的project字段,以避免数据库黑洞。
更新对象的流程如图4.4所示。
图4.4 update对象的流程图
主页面初始化的时候,客户端会主动请求“所有数据”,但并不是数据库中的所有数据,而是自身需要的所有,比如某个测试账户只需要1个部门下所有员工信息以及4个type下的所有project信息,这样对整个数据库表的读取只需要过滤成1行或者4行,其他行通常是别的地区的员工信息。这种过滤操作对应的api是/get/filteredAll。
但是在account setting界面初始化有点不同,这时候需要所有的部门员工以及所有的project,但又要屏蔽掉表格中不需要的许多列比如员工的时间分配信息,这样是为了避免读取整个数据库,那是很可怕的。所以这时候需要关系代数中的“投影(project)”操作,即过滤掉不需要的属性(列)。这种投影操作对应的api是/get/projectedAll。
2种关系型操作可以用图4.5来简单理解:filter是“过滤行”,project是“过滤列”。
图4.5:2种使用到的关系的api请求范围
对象增删改查有关的模块包括增删改查模块,数据过滤模块。
4.2.2 账户设置
账户设置功能指用户登录以后可以设置自己的账号信息[12],包括修改密码,调整自己的管辖范围或者切换部门。账户设置将放在一个单独的页面里来展现,页面中将呈现数据库中所有的员工和项目的信息,并高亮出用户自己所管辖的那部分,然后所有的数据拥有多选框可供用户选择,已达到“切换”部门的功能。
和账户设置有关的模块包括数据过滤模块,增删改查模块。
4.2.3 登录登出
登录登出即最基本的用户会话连接的维护功能。
和登录登出有关的模块是会话控制模块。
4.4 管理员
4.4.1 日志访问
系统日志是为了排错[13]而设计的,通常用户的每一个操作都会被记录下,同时还要求管理员有权限来访问。
和日志访问有关的模块是日志模块。
4.4.2 远程调试
远程调试模块叫做debug remotely,也是给管理员调试后端程序服务的,这个功能希望能够给管理员用户提供一个接口,通过网络传输,在服务端的nodejs环境下执行命令,从而达到直接操作服务器的目的。
和远程调试有关的模块是远程调试模块。
4.4.3 用户注册
由于本系统用户数量稳定,同时每个用户还需要和他对应的公司个人信息认证,实名认证稍有麻烦,再加上用户注册的频率很低,本系统没有设计自动注册账户的功能。取而代之的是让管理员去添加新用户或删除用户。
5 系统实现
图5.1根据前后端分类,列举出了本系统为了实现所有的功能(系统功能图4.1),所需要的所有模块。
图5.1 前后端模块一览
5.1 前端
5.1.1 表格交互模块
context菜单是鼠标右键弹出的菜单。在表格的任意位置点击都可以触发菜单,但是点击的位置会影响当前聚焦的对象。聚焦对象指的是,在某一时刻,进行增删查改的对象聚焦到唯一的person,唯一的project,以及唯一的department。当菜单触发以后,触发点所在的那一行中相关的对象就会覆盖上一次的焦点。这样设计的目的是,想要修改某一个数据,比如某一个人,只要将鼠标移动到他附近再右键就好了。
其中,update和remove分别对应着更新和删除操作,而且会弹出包含编辑器的对话框,还可以在表格右半部分的嵌入式编辑器中直接修改员工的数据。chart则表示绘制图[14],transform是在view层面对表格进行变形,但不影响数据。Export可以将本表格导出成csv或者Excel文件。
Aggrid框架提供了许多功能与事件,功能可以提供更友好的用户操作[15],事件可以和开发者自己的脚本对接起来。下面一一列举所使用到的功能和事件的配置方法和作用。
1.侧边栏。表格侧边栏是分布在右侧的一个多标签页的窗口,提供一些快捷键,虽然这些快捷键被context menu里的选项替代,但放在右侧更直观。
2.动画。框架提供了动画效果,当用户拖拽,缩放表格的时候都会出现相应的渐变动画,使得操作效果更友好直观。
3.整行嵌入式编辑。除了group行,每一行都可以直接在行内使用文本框和选择器编辑文本和数字。双击或者任意字符键打开编辑器,编辑完整行数据后回车或失去焦点即完成编辑,触发事件,发送到服务器更新。
4.允许分组。分组功能指对所有行进行分类,类似数据库表中的索引操作。系统加载时默认只对人名来索引,用户可以通过查找某人快速定位到某一行。和数据库索引不同的是,这里的分组是有层级关系的,比如对部门进行一级索引,再对人名进行二级索引。
5.单击打印本行对象。当主键单击某一行,都会打印这一行所对应的内存对象,方便debug。
6.允许排序。排序的作用不言而喻,尤其是对索引列的排序至关重要。
7.允许搜索。允许在某一列当中通过关键字搜索某一行。
8.编辑器滤镜。用户编辑完某一单元格数据后,数据并不会立即更新,新数据以文本串的形式传递到parser函数,经过一定的规则验证或“修订”后再写入新数据。比如数字类型的spent time一列编辑完后需要审查是否是一个合法的数字,还要将string转成number类型。
5.1.2 schema封装模块
本系统的表格拥有以下几列,并且从采用平铺的方式导入数据,比如一个人对应多个项目的话,就生成多行。本系统初始化表格的时候是通过每个人----每个项目的形式遍历的,及总行数(不包含group的行)为人数*项目数,然后默认将demand为空的那些行给隐藏起来,这样做的目的是既照顾到所有的资源又可以自动屏蔽不需要的数据。
1)Group:当前表格汇总行所占据的一列,由系统自动生成,类似于索引值所单独占据的一列。
2)Person:员工的姓名(ID)。
3)Department:员工的部门。
4)Personal Info:员工的备注信息。(默认隐藏)
5)Project:某一个项目名(ID)。
6)Project Type:该项目的类型。
7)Sub Project Type:项目子分类。(默认隐藏)
8)Department Demand:该项目对该部门要求的资源数(number类型)。
9)Spent Time:该员工在该项目上花费的时间数(number类型)。
10)Action:该员工在该项目上的所作所为。
11)Status:该员工在该项目上的进度(枚举类型)。
12)Comment:该员工在该项目上所作所为的备注信息。
5.1.3 异步封装模块
主界面”/”引入了2个js文件,分别是主线剧情/public/my/main.js和异步方法/public/my/async.js。其中async.js提供了所有封装好的异步操作,从任务的开始到结束都封装在一个promise内,等待调用。根据类型,这些异步模块分为编辑器UI异步工具和网络类异步工具。
1)编辑器UI异步。在很多情况下,UI的变化是异步完成的,比如UI的加载有时候就很慢,还有比如某一个对话框需要等侧边栏隐藏起来后才能打开。所以本系统准备了3个编辑器对话框的异步函数,分别是登录框,person编辑框以及project编辑框。Promise从对话框打开的一瞬间开始,到用户点击确定或取消时结束。
2)网络层面的异步[16]模块则较多,包含登录,初始化数据请求,debug remotely,下载日志,CRUD对象,申请账户设置,拷贝用户模板。网络层的promise则从接收一个原始数据开始,发送http,返回结果并解析返回数据完成。
就网络层的异步模块来说就有多达13种:
代码语言:javascript复制window.curd = {
login,
getAll,
debug,
log,
getExtraData,
setHuman,
dropHuman,
addHuman,
addSkill,
setSkill,
dropSkill,
setRole,
dropRole,
};
5.1.4 前端自动化脚本
与debug remotely相对应,debug locally可以在本地解析执行浏览器环境下的JavaScript脚本。通过localstorage api接口在用户浏览器本地存储一段用户自制的代码段,每次页面加载时自动执行,由于本地执行的原因,无需担心安全性。
不过debug locally的应用场景并不多,主要适合一些极端用户的个性需求,比如调整界面的主题颜色,自动隐藏不想看到的表格行等等。
前端自动化脚本模块对应的函数是debug_locally:
代码语言:javascript复制// onclick
function debug_local() {
let cmd = prompt('Overwrite current command:', localStorage.getItem('debug') || '');
if (cmd !== null) {
if (cmd.trim() === '') localStorage.removeItem('debug');
else localStorage.setItem('debug', cmd);
if (confirm('Command updated, reload?'))
location.reload();
}
}
5.1.5 渲染循环模块
渲染循环(render loop)是对UI刷新的常见函数,在本系统中有一个repaint函数专门用来对表格“重画”,也就是全部更新(局部更新的情况不适用)。Repaint的作用是将挂载在全局变量下的所有数据列表渲染进表格里。
5.1.6 UI布局模块
主界面和account setting界面都是基于material的扁平化布局,布局方向是上下,左中右结构。首先上方是top app bar或者标题栏,下方从左到右分别是抽屉,网格,侧边栏,其中抽屉可以隐藏到页面左边,侧边栏的功能由aggrid配置决定,本系统设置了3个侧边标签页,可以非常方便的对网格进行变形。
还有3个临时出现的UI元素,分别是snackbar,progress bar,tooltip。Snackbar是一个偶尔弹出的消息框,用于提示用户操作的结果,progress bar出现在界面下方,用于显示当前网络请求的状态,tooltip是鼠标提示框,当鼠标悬浮在某个按钮上,可以提示一些帮助信息。这三个“隐藏”元素如图5.7所示。
图5.7 3种隐藏UI----snackbar,progress bar,tooltip
另一边,setting页面的布局则是常见的顶部标签页分布,外加动画切换和tooltip提示。Json列表的编辑器采用复选框checkboxes的模式来实现。
5.1.7 表格变形配置模块
图5.8 6种批量表格变形操作
如图5.8所示,本系统在context菜单中提供了6种表格变形的功能,分别是折叠所有索引,展开所有索引,自动调整列宽,重置所有列,对当前列索引,取消所有索引。配置代码如下。
代码语言:javascript复制const menu_transform = {
name: 'Transform',
icon: 'transform',
subMenu: [
'contractAll',
'expandAll',
'autoSizeAll',
'separator',
'resetColumns',
{
name: `Group(only) by ${event.column.colDef.headerName}`,
action: () => {
let col = gridOptions.columnApi.getColumn(event.column.colDef.field);
gridOptions.columnApi.setRowGroupColumns([col]);
gridOptions.api.expandAll();
gridOptions.columnApi.autoSizeAllColumns();
}
}, {
name: 'Ungroup All',
action: () => {
gridOptions.columnApi.setRowGroupColumns([]);
}
},
]
}
5.1.8 雷达图模块
雷达图的具体实现方法是,先对表格进行变形,以department作为一级分类(通常也就一个部门),project作为2级分类,如图5.9所示。然后对spent time一列进行求和运算汇总到汇总行上,对department demand一列进行first运算(选择第一个值作为聚合值,因为都一样)汇总到汇总行上,最后提取这些汇总的数据画出雷达图。
图5.9 雷达图一一对应的表格结构示例(变形后)
图5.10 雷达图示例
5.2 后端
5.2.1 目录设计模块
开始实现之前先确定好代码的目录结构,添加必要的文件和目录,然后开始编辑内容,目录结构图如图5.11。
图5.11 本系统中文件与目录结构
1)README.md:GitHub默认的帮助文件。
2)model/:该目录存放了MVC模型层必要的工具文件,主要是针对project表,person表,department表,log表进行高效CRUD的调用函数。
3)package.json:node项目说明文件,从开发环境的角度描述了整个项目。
4)LICENSE:项目开源许可证。
5)index.js:项目的入口执行文件,通过node命令解释执行。
6)cfg.js:配置文件,存储了所有的配置数据。只有配置文件和mongodb数据库储存了所有必要的和本系统相关的数据,其余地方和文件都储存的是代码信息。
7)其他/:该目录存放了一些不重要的文件,比如前期开发文档,用户使用说明书,自动化测试脚本等。
8)view/:该目录存放了MVC视图层的模板引擎ejs文件,一个ejs对应一个html页面,总共设计了3个页面,分别是main.ejs,error.ejs,setting.ejs,分别是主界面,错误跳转界面和用户设置界面。为了满足SPA单页应用的设计原则,绝大部分的应用任务在主页面上完成。
9)ssl/:该目录存放了https网站必须的ssl证书以及私钥,但由于本项目没有购买证书的必要,这里存放了以localhost为common name的自签名证书,所以网站访问的时候会显示“不安全”。
10)route/:该目录存放了MVC路由层的handler文件,根据restful设计模式分为log,set,get,add,drop这5个文件。本项目的所有业务逻辑基本都在这一层实现。
11)public/:该目录存放了所有直接扔给前端的静态文件,包括前端框架,必要的js脚本,图片等。同时这个目录的文件还会被浏览器缓存30天以提高web app的效率。
12)node_modules/:该目录存放了所有后端使用的nodejs第三方库,比如archiver,body-parser,ejs,express,session,mongodb等。
13)mongo_backup/:该目录存放了数据库的备份文件。管理员登录系统后可以使用“一键备份”的功能,将mongodb中重要的3个表分别导出json文件,再打包下载。
5.2.2 入口设计模块
后端提供的可交互api的具体设计方式是,首先用户打开主界面后要登录(7天的缓存免登录),然后向后端请求相关地区所有的person对象和project对象,得到之先遍历所有的人再遍历所有项目,将遍历之后的二层嵌套结构渲染进表格,同时department demand为空的那些行可以过滤掉,因为他们被认为是不需要参与的项目。
整个系统的入口程序主要是/index.js和/cfg.js。
为了不滥用顶级对象global的属性,本系统将所有的全局对象挂载在global.cfg下,这个cfg对象来自/cfg.js导出。Cfg对象包含server的信息,数据库mongodb的信息,app的信息,前端文件映射的信息,数据库对象的字段验证信息,常用工具类函数等。
/index.js分成3个promise,分别是初始化全局变量,初始化MongoDB,初始化expressjs。3个任务并发进行,全部结束后打印url,即可正常访问,如若失败则终止进程。
1)初始化nodejs全局变量。首先require /cfg.js文件,挂在在global.cfg下,然后在标准库上自定义一些实用的方法,比如RegExp.escape用于将用户发来的一段文本转译成普通文本再构建成正则表达式,比如Date和Function原型链上的toJSON方法设计成一个可读的文本串,方便前后端传递json数据。
2)初始化expressjs。首先生成express对象,然后指定模板引擎为ejs并指定模板地址。开始路由,设置静态文件目录为/public/并挂载在/public/下,之后经过session过滤器,再然后设置入口地址“/”返回渲染后的main.ejs,再引入之前定义好的5个路由模块,最后保留一条缺省路由指向404的错误页面,结束路由。路由配置完成后,require https模块生成服务器对象,同时导入/ssl/目录中的私钥和证书,以及之前创建的express对象,最后申请443端口号,开启服务。
3)初始化mongodb。连接到指定的数据库主机需要经过一系列步骤:首先利用Mongo官方开发的node连接器连接指定url,将得到的数据库对象挂载在global.DB下,同时将MVC model层的4个集合对象挂载在global.model下;然后检查当前数据库中是否已经存在所需要的5个集合,如果没有则创建出来;之后对person的_department和project集合的_type列添加索引(如果存在则不变);最后根据cfg中保存的schema对必要的集合列添加验证器,完成任务。
等待3个promise任务完成后打印url或者报错时退出程序的代码如下:
代码语言:javascript复制Promise.all([initGlobal(), initExpress(), initMongo()])
.then(() => {
console.log('[init OK]');
console.log(` {cfg.app.protocol}://{cfg.server.domain || cfg.server.ip}:
}).catch(err => {
console.log(err.message);
process.exit(0);
});
5.2.3 增删改查模块
增删改查模块提供一个完整的请求回调函数,对用户的请求进行验证,执行,记录,返回,所有的CRUD函数都在路由层实现。函数接收request对象,返回一个response对象。
5.2.4 页面跳转模块
虽然应该遵守SPA的原则,但是现代浏览器并不是完全为app而设计的虚拟机,有时候系统需要“子窗口”的功能,又不能频繁使用对话框,所以就采用了第二章页面作为账户设置界面,出错处理界面,数据库备份(临时页面)和logout页面(临时页面),以兼容现在的前端技术。
除了主页面所在的根路径“/”,其余2个页面都挂载在/get/下。
1)/get/error_page?status&msg:跳转到错误页面,还可以定制http返回码以及错误提示,通常访问/get/setting和/get/mongo_backup的时候出现权限问题会跳转到这个页面,如果前端发现后端数据有致命漏洞或者浏览器有兼容性问题的时候也会停止渲染,跳转到错误页面。
2)/get/setting:用户账户设置界面。这个界面可以重置自己的密码,切换到别的地区(针对总监级别拥有该权限),查看所有地区的数据。进入account setting界面有2种方式:点击抽屉里的按钮或者点击右上角自己的ID。界面如图5.12所示。
3)【临时页面】本系统从逻辑上总共4个跳转url,除了get/error_page和/get/setting,还有/get/mongo_backup和/log/logout。这两个页面真实情况并不存在,前者在备份文件下载完成后界面自动消失,后者访问后会被重定向到根“/”。只是为了逻辑上处理方便而设置了这2个虚拟页面。
图5.12 account setting是一个单独的界面
5.2.5 远程调试模块
Debug remotely模块是一个非常实用的,供管理员用户使用的功能。原理是通过root user从前端发来的一串明文nodejs代码,在后端通过eval函数解释执行,理论上通过sudo出来的node进程可以有权限对服务器进行任何操作,所以eval是非常危险的,debug remotely的使用必须谨慎。效果如图5.13所示。
图5.13 debug remotely的效果示例
远程调试模块代码较简单,整个接收函数如下。
代码语言:javascript复制// _root用户only
// 接收string of code, 返回json或者错误msg
router.put('/debug', bodyParser.text(), (req, res, next) => {
new Promise((res, rej) => {
assert(req.session.user && req.session.user._root, 'Permission denied, root user required');
res();
}).then(() => model.log.insertOne({
ip: req.ip,
user: req.session.user._id,
method: req.method,
extra: req.body
})).then(() => {
res.set('Content-Type', `application/json`);
res.end(JSON.stringify(eval(req.body)));
}).catch(err => {
res.status(400).json(err.message);
})
});
5.2.6 数据库备份模块
为了方便管理员定期备份数据库,本系统设计了一个“一键备份”的功能,即一键导出数据库中3个主要集合,分别导出json文件再将文件夹打包成zip下载下来。
首先建立一个mongodb的可读流和文件系统的可写流,将person,department,project集合分别导向/mongo_backup目录下的person.json,department.json以及project.json。事件完成后再将/mongo_backup目录打包成zip文件,同样以流的形式返回给用户,整个过程共享一个http来回。
因为数据库备份相比其他操作更消耗资源,所以设置一个时长5分钟的最小间隔,以避免频繁备份,同时只有管理员有权限执行此操作。
5.2.7 数据过滤模块
数据过滤模块是在MVC的业务逻辑层中的请求回调函数中放置一些assert断言方法对request对象中携带参数进行验证和过滤,比如最常使用的验证是否登录:
assert(req.session.user, 'session expired, login first');
5.2.8 会话控制模块
系统需要一个用户登录登出的功能。登录的本质是认证,并且利用cookie-session机制建立一个中程的连接,后端存储session有3种方式:
1)存储在内存中。
2)存储在文件系统中。
3)存储在各种类型的数据库中。
其中第一种方式是不可取的,因为将session存放在宝贵的内存中很容易被ddos攻击,剩下2种方式都是存在外存当中,相对合理得多,又由于本项目已经使用mongodb数据库了,就统一将session也存入数据库中的sessions集合。
每次用户访问都会刷新cookie的到期时间,添加或者维持与之对应的session消息。每次用户登录都会检查session看用户是否已经登录过,如果有就直接返回session种存放的user对象,如果没有就检查person集合进行认证(request对象中包含用户发来的user对象)。由于本系统使用https,传输中密码无需加密,但是数据库中的密码统一采用sha1加密,由node标准库中的crypto模块简洁实现。
登出模块的实现只要删除用户session中的user对象即可。代码详见目录中/route/log.js文件。
5.2.9 日志模块
日志系统的设计分为读与写,其中写入日志需要路由到model层的log模块来调用,直接传入log对象即可写入,同时还会主动分配_id作为写入的时间以便排序,当然该字段写入前会做一个防抖,以防同一ms级出现2个相同的_id。读取日志时则选择最新的200条日志返回给用户,同时还要定期清除过期(30天)的日志。插入一条log的代码实例:
代码语言:javascript复制model.log.insertOne({
ip: req.ip,
user: req.session.user._id,
human: human._id,
method: req.method
})
5.3 数据库
5.3.1 索引模块
数据库索引是最常见的优化操作,在本次的mongodb数据库中,除了本身默认的对_id字段的索引外,本系统还需要对person集合和project集合的分类字段进行索引,也就是_department和_type字段。
代码语言:javascript复制Promise.all([
DB.collection('human').createIndex({
_role: -1
}), DB.collection('skill').createIndex({
_type: -1
})
]);
5.3.2 模型层模块
MVC中模型层提供的数据接口,提供一些常用的数据库增删改查操作的封装api,比如登录方法login:
代码语言:javascript复制(user) => new Promise((res, rej) => {
// 更新并返回更新前的数据
coll.findOne(user, (err, result) => {
if (err) rej(err);
// 否则返回null
else if (result) res(result);
else rej(new Error(`Login failed: n username or password not right`));
});
})
5.3.3 数据验证模块
数据验证模块是mongodb数据库在添加或更新时经过的一层验证器,对字段进行一些条件审查,比如对_type字段强制存在:
代码语言:javascript复制DB.command({
collMod: "skill",
validator: {
_type: {
$exists: true
}
},
validationLevel: "strict",
validationAction: "error",
})
6 测试
6.1 功能性测试(用户数据验证)
本次采用黑盒测试法,选取一个具有代表性的功能,通过添加恶意project对象来测试后端的字段验证器是否正常工作。
图6.1 黑盒测试示例
表6.1 黑盒测试结果
类型 | 输入 | 输出 | 预期结果 | 结果 |
---|---|---|---|---|
添加大写person_id | JinHengyu | Jinhengyu | 人名全部转成小写 | 符合预期 |
添加恶意的project_id | -test-test$test | -test-test-test | 特殊字符全部被转换成‘-’ | 符合预期 |
更新500字符以上的project_about | [500 的任意文本] | […..] failed the Regular Expression /^[sS]{0,500}$/,try another one pls | 提示正则表达式验证失败,promise被rejected。 | 符合预期 |
添加重名project | 两次“-” | E11000 duplicate key error collection: graduation.skill index: _id_ dup key: { : "-" } | Mongodb提示唯一索引_id重复了,promise被rejected。 | 符合预期 |
结果表明,每次添加的project对象的属性都在后端被过滤和转换了,没有出现恶意字符逃逸的情况,所以本次测试结果全部符合预期,后端数据过滤器测试通过。
6.2 安全性测试
6.2.1 请求未知资源(404)
请求一个错误的url:/nothing,测试错误是否捕捉到,预期会得到404页面,结果如下。
图6.2 404界面
结果表明,当请求404资源的时候,系统会捕获异常,并且提示用户错误,不会因为异常请求导致系统出错。测试通过。
6.2.2 权限测试
非root user使用debug remotely(远程调试)功能,预期被拦截:
图6.3 浏览器提示用户权限不够
结果表明,低级别用户使用高级别功能的时候,无论在系统处理上,还是在UI上都进行了拦截。测试通过。
7 总结与展望
本系统为产品经理和项目经理服务,提供了对“员工”和“项目”这两种资源之间关系的增删改查操作,其中的“查”指的是统计功能。
本系统设计上的特色在于,全面实现了material design主题,SPA单页面应用程序,底层上通过函数式编程风格,结合promise模块化的理念实现了高可用同时易扩展的特点。本系统的设计从功能,性能与安全性上考虑:功能上考虑了整体UI和表格的各种操作,性能上主要考虑算法优化,控制内存的使用,以及减少静态文件体积,安全性上利用https协议防止外部攻击,利用后端数据验证防止内部的操作失误。
经过了对本系统的功能和性能测试,本系统的使用效果做到了尽可能的舒适,安全性上也足够健壮。
本系统仍然有一些可以提升的地方,比如数据库中department集合和project集合可以合并以提高内聚性;使用w3c最新的web component组件标准可以减少框架带来的压力;可以同时采用除雷达图之外其他的图表,如柱状图和线形图;升级http1.1至二进制传输的http2.0可以大幅提升网络资源的利用率。
参考文献
[1] 韩万江.软件项目管理案例教程(第三版).北京机械工业出版社,2015,101-102
[2] HTTP设计模式RESTful 维基百科. https://ssl123d1063af181fe606e55ed93dd5b867169.vpn.nuist.edu.cn/wiki/Representational_state_transfer [3] 李洪海,石爽.交互界面设计(第一版).化学工业出版社出版,2011,45-51 [4] Rick Copeland.MongoDB应用设计模式(第一版).中国电力出版社,2015,94-96
[6] https://expressjs.com/. Express web框架的官方网站. 2019.4-1
[7] http://nodejs.cn/ . NodeJS中文网. 2019.3-1
[8] https://www.mongodb.com/docs . mongodb数据库开发文档. 2019.3-1
[9] https://material.io/develop/web/ . google material design components for web 官方网站. 2019.4-1
[10] https://www.ag-grid.com/ . ag-grid前端框架官网. 2019.4-1
[11] https://developer.mozilla.org/zh-CN/ . mozilla前端开发者网站(MDN). 2019.2-1
[12] Mingmin Zhang,Zhigeng Pan.EasyHome:an online virtual home decoration system[J].Computer Animation and Virtual Worlds,2014,25(2):101-105
[13] Williams ME,Consolazio GR,Hoit MI.Advances in Engineering Software [J].2005;36
[14] Elver G.Geometric texture modeling[J].IEEE Computer Graphics and Applications 2005;25(4):66-67.
[15] Nashat Mansour,Manal Houri.Testing web application[J].Infomation and Software Technology,2006,48(1):31-42
[16] MATTSON RLR,GHOSH S.HTTP-MPLEX:An enhanced hypertext transfer protocol and its performance evalution [J],Journal of Network and Compjter /applications,2009,32(4):925-939
致谢
在论文的末尾,我想对帮助过我的老师以及同学,说声谢谢!感谢这段时间你们不厌其烦的帮助,在知识上对我有莫大的推动力,让我有了质的提升,非常的感谢。
首先诚挚的感谢我的论文指导老师XXX老师。她在忙碌的教学工作中挤出时间来审查、修改我的论文。徐老师对我们特别负责任,很早就告诉我们该如何切入自己的论文研究点,可以通过哪些途径查询到较新的资料,我从老师身上学到了很多:完成一件事就要把它做好,用最认真的态度去完成每一个细节,尤其最后写论文的时候,老师指导了我们很多次关于如何排版和画图,真的由衷感谢老师这段时间的付出,老师辛苦了。
还要感谢教过我的所有老师们,你们严谨细致、一丝不苟的作风一直是我工作、学习中的榜样。能够完成我大学里最后一道作业,我感觉很荣幸,感谢学院里的每一位老师,给予我这个机会来证明自己是可以的,事实证明只要努力就会有成果,希望下一届的学弟学妹们不要松懈。祝福大学里遇到的所有人,曾经帮助过我的人,非常感谢!