最初它的名字为 BugHound,主要功能是 bug 提交,EC 同学已经造出了它最初的样子,它最初的目标用户是移动用户,能在提 bug 的同时收集设备的相关信息。主要用到的技术点:

  • php
  • mongodb

我开始熟悉原代码并尝试在此基础上进行迭代开发,但是由于功能上有较大的变更,而我没有用过 Mongodb 和 php,这时我更倾向于使用我熟悉的 MySql 和 Nodejs,在得到 EC 同学的允许后,我决定从零开始搭建系统。本文选择其中几点进行阐述。

数据设计

第一步是建表,下图是最初的表设计,基本上按照原 mongodb 中的数据关系进行定义,设计好数据表,再把它们写进数据字典,最后才开始建表。

(数据字典对开发和协作的作用非常大,它能让你和别人知道表中有哪些字段,某字段代表什么等信息,在接下来的编码工作中还会经常去查看它。)

数据设计

随着开发的深入,数据不断拓展,有些字段被淘汰或被拆分到新表,有些字段新加进来,比如代表“文件”的数据,原来以字符串的形式写在 demand 表的 files 字段,作为经常被读写的数据,以字符串的形式保存显然不正确,我们需要把每个文件当作一个数据元,于是把 files 字段拆分成新表 demandfiles,并增加了上传人、上传时间和下载次数这些信息。

为了让数据库设计更加合理,使用了外键约束,确保在删除修改数据的同时能级联删除修改,外键统一命名为 fk_table_field 的形式。修改后的 ER 图如下所示:
数据库设计

技术实践

  1. 第一阶段

    起初,项目后端使用了 ejs 模板引擎,拆分头尾在后端渲染模板后发回给前端,前端用 jQuery 进行逻辑的处理。

  2. 第二阶段

    前端开始使用 Angular 替代 jQuery,但是 ejs 跟 Angular 好像有点格格不入,前者意在后端渲染数据,后者通常在前端通过 $http 获取数据来填充,这时的 ejs 的存在主要是为了组装拆分出来的头尾,不好去掉它。但是,接着发现了 ng-include,这让组装头尾的工作交给了前端,从而摒弃掉了 ejs。

  3. 第三阶段

    接着,切换页面的时候头部闪烁的问题引起了注意,原来是 Angular 使用方式不对,我的每个页面都是一个单独的 Angular app,每次切换页面时都引入一次头尾导致闪烁,于是引入 ui-router 把系统修改成一个单页面应用,更新 ui-view 即可切换页面。

    这又引出了一个新的问题,所有页面的入口都是 index.html,也就是说不能再简单地在后端通过路由来控制页面的访问权限了,这样另一个概念就登场了——本地身份验证。简单点说,就是在进入应用主入口 index.html 的时候,从服务器请求身份信息,身份信息将保存在 service 中(类似 session storage),然后在状态(页面)切换事件 stateChangeStartstateChangeSuccess 中,对页面请求进行拦截或重定向等操作。

    然而,网络请求都有一定的延迟,在身份信息返回来之前,某些页面的内容不应该被呈现出来,所以要在获取到身份信息后通知到页面,于是就加进了事件广播和接收器 $broadcast$on:获取到身份信息后通过 $broadcast 广播一个事件,在子页面中通过 $on 接收到该事件后执行相关逻辑。

排期表设计

  1. v1 版

    背景:需求排期时间不能冲突,排期由排期起始和结束时间决定。

    从服务器获取的源数据形式如下,已按 startDate 排好序,每个用户数据中包含排期数据(data):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [
    {
    erp:xx,
    name:xx,
    ... ,
    data[
    {demandID:xx, startDate:xx, endDate:xx, ...},
    {demandID:xx, startDate:xx, endDate:xx, ...},
    {demandID:xx, startDate:xx, endDate:xx, ...},
    ...
    ]
    },
    ...
    ]

    在前端计算好周起始时间 ws 和周结束时间 we,对每条排期数据进行判断,如果排期时间跟本周的时间有交集,则计算该排期都出现在了哪几天,比如下图的情形,这里有两个排期:

    周期

    数据将会按下面的形式(blocks)附加到用户数据中,其中 [0,1,2] 和 [4,5] 的数字就分别表示了两个排期在周排期表中占据的位置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    [
    {
    erp:xx,
    name:xx,
    ... ,
    data[
    {demandID:xx, startDate:xx, endDate:xx, ...},
    {demandID:xx, startDate:xx, endDate:xx, ...},
    {demandID:xx, startDate:xx, endDate:xx, ...},
    ...
    ],
    blocks:[
    {
    demandID:xx,
    demandName:xx,
    dictator:xx,
    block: [0, 1, 2]
    },
    {
    demandID:xx,
    demandName:xx,
    dictator:xx,
    block: [4, 5]
    },
    ...
    ]
    },
    ...
    ]

    接着,处理排期表中的空闲的天,给用户数据附加以下数据:

    1
    2
    3
    4
    5
    6
    schedule: [
    {demandID:xxx, ...}, // blocks[0]
    {date:xxx}, //周三,时间用于点进申请页自动填好时间
    {demandID:xxx, ...}, // blocks[1]
    {date:xxx} //周六,时间用于点进申请页自动填好时间
    ]

    然后在页面上由 schedule 循环输出 td,blocks[0] 和 blocks[1] 的长度就是 td 横跨的 td 个数。

  2. v2 版

    背景:v1 版的排期表设计中,由于排期的最小单位是“天”,而存在数据库中的 startDate 和 endDate 的最小单位却是毫秒,在计算上无疑增加了大量纷繁的计算,受移动组 task 系统的启发,把排期数据改为 startDate 和 days(所需天数),这样可以减少一半的计算,也更好理解。另外较大的改动,排期时间将允许有重叠(即同一天可以排多个需求)。

    从服务器获取的源数据的形式不变,这次我在计算之前把时间都转化成了天(1970-01-01至今的天数)以方便计算,为了解决排期堆叠的问题,添加了两个关键的变量:

    1
    2
    var weekpile = [0,0,0,0,0,0,0]; //记录排期在该天已经堆到第几层了
    var wee = [[], [], [], [], [], [], []]; //数组中的每个数组代表星期里的一天,存储由该天开始的需求,下面有详解

    weekpile 原理如下图,每个排期排在第几层由它在排期表中的第一天所在层级决定。

    weekpile

    层数记录附加到用户数据中,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    blocks: [
    {
    id:xx, demandID:xx, startDate:xx, days:xx, ... ,
    weekdays: [0, 1, 2],
    pilefloor: n // 所在层级
    },
    ...
    ]

    另外,上文提到的 wee 变量存储的是指向 blocks 索引的数字,把它也附加到用户数据中。如图,外层 div 的 margin-topwee 所指向的第一个排期的 pilefloor 决定,比如下图假设这是星期六,那么外层 div 的 margin-topblocks[wee[6][0]].pilefloor 决定。

    wee

    因为已知排期的天数 n,给每个排期的宽度设置相应的 n*100% 即可。

    然后在页面上由 wee 循环输出 td,blocks[n] 中的 weekdays 的 length 决定了排期的长度。

    v2 版排期表如图所示:

    排期

协作

最初 UFT 主要由我进行开发,后来另一个同事加入开发,变成我负责 UFT 的功能开发,她负责 UFT 的 UI 优化,项目源码放到 github 上了,我们要在 github 共同进行开发,我们两人琢磨了好久,面对冲突问题时开始还采取过删除后重新拉取暴力做法,几经尝试,我们最终探索出两种较好的协作方式:

  1. 我们各自拉取分支修改,push 上去后发起合并请求,我负责合并分支;
  2. 在本地修改后,先从服务器上拉取更新合并:

    git fetch origin master

    然后进行合并

    git merge FETCH_HEAD

    一般情况下,合并会顺利进行,但难免会有冲突,一旦有冲突,就调用 mergetool 进行合并,或者直接在原文件上手动合并。最后,就是正常的提交过程了。

    关于 mergetool,自从同事分享过一次,回去后自己装了 p4merge,配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [diff]
    tool = p4merge
    [difftool "p4merge"]
    cmd = "p4merge $LOCAL $REMOTE"
    [merge]
    tool = p4merge
    [mergetool "p4merge"]
    cmd = "p4merge $BASE $LOCAL $REMOTE $MERGED"
    trustExitCode = true
    keepBackup = false

    但是,没有冲突是调用不了 mergetool 的。

其他

  1. 临时文件的处理

    临时文件指的是异步上传到 temp 文件夹的文件,用户上传文件后如果不打算进一步提交了,这些临时文件就成了“弃儿”,如果不对这些文件进行处理,temp 文件夹会越来越大。于是加进了定期任务,每隔一段时间对临时文件夹中的文件进行清理,为了防止把用户刚上传的文件清理掉了,被清理的文件的最后访问时间要在3天之前。

总结

UFT 从无到有,很多东西边学边用:第一次尝试用 Express,配设置、配路由等过程遇到不少坑;第一次用 Angular,用得并不规范,多亏涛哥从中相助,解决我不少困惑;Angular 各种各样插件的学习使用过程,就是不断试错,不断筛选满意插件的过程。

除了知识上吸收,最大的收获是意识到项目规划的重要性。开始接手后就马上开始写代码,需要什么功能就写什么,以至于最后需要经常较大地改动代码,比如,列表之前用 Bootstrap-table 写,然后改成 Angular,又比如将页面合并成单页面 Angular 应用时,需要把大量的插件都换成 Angular 插件,还有项目的结构也几经变换。折腾这几番足见项目前期的规划还是非常重要且必要的。

未来规划

UFT 原来的目标是供部门内部使用,用户访问系统需要先经过内部员工帐号的身份认证过程,然而,随着内部推出了新的需求管理平台并推广到部门,UFT 被闲置,为了让 UFT 重新焕发活力,在接下来,把源码开源,并将把 UFT 改造成通用的需求管理平台。目前平台仍有不少可以优化的地方,例如:

  • MySql 使用上 View 和 Function 会简化很多工作
  • Models 不是真正的 MVC 中的 M,可以考虑让这部分更远离业务逻辑更接近数据
  • 需求状态的多样化,需求状态变更的系统通知(非邮件)
  • ……

源码地址:https://github.com/o2team/UFT