xproj 的设计

1. 总体说明

1.1 技术设计

1.1.1 注意原则

  1. 不要过度设计,刚开始简单即可
  2. tenant,um_tenant_id来自于用户登录;
  3. 持续迭代:系统孱弱的很大原因,是因为没有迭代。由于要完成指标性的工作,导致交付即定型、上线即生产,只要上了生产,生成了生产数据的前提下,就几乎没有后续迭代的可能性
  4. 确定主数据库就定为postgres,CTE和Schema都使用了pg的特性,要留意未来可能的异构数据库迁移
  5. 好项目需要好产品,好产品需要打磨,打磨需要时间,但项目里,时间是稀缺项。如何解决这个问题?一个做法是将项目与产品分开

1.1.2关键点

  1. 以Project为核心:worker为一分支;客户为一个分支;进度(计划与实现)为一个分支;金钱(成本与费用)为一个分支
  2. org = liner 拥有多条船
  3. project = vessel/voyage/leg 船舶:带有进出港动态
  4. worker = container, 箱子:带有上下船动态;worker带有进出项目动态
  5. 用系统架构来解构现有业务问题,范围、进度、成本、质量、人力、沟通、风险
  6. 听DB会议,把现有项目用系统架构来分解
  7. 自上而下,自粗而细的设计

1.1.2核心点

  • crud 前端javascript库【crud.js】

    1. 核心思想:界面层有很多形式,但主要功能是增/删/改/查/翻页,可以将这些功能从界面层抽取出来放在crud.js中
    2. 重要的贡献:主要功能与界面层分离,极大简化界面层的代码
    3. 结合后台系统的设计,对查询条件进行预处理,如"worker_name:like"与"worker_name:李"结合在一起,形成"worker_name:李:like"。这样的好处是,增加查询条件不需要改动后台系统;缺点是,目前查询预处理只支持查询条件的“与”操作,暂时不支持更复杂的查询,比如日期范围大于小于;另外,需要考虑到查询条件中,自带:的情况;
    4. 接口:在vue界面层文件中,调用useCRUD函数将界面vue与crud功能绑定
    5. 输入:数据表的基本要素、增删改查对应的api函数
    6. 输出:crud标准的增删改查函数(selectEntities等),供界面层绑定到表格、翻页控件、按钮
    7. 输出对话框、查询框、查询条件供界面层绑定
    8. 输入与界面层无关;输出与界面层相关
  • dataloader 前端javascript库【dataloader.js】

    1. 核心思想:将对通用数据,比如下拉框,树型结构的数据,从界面层抽取出来,放在各自业务板块的dataloader.js中
    2. 重要的贡献:主要功能与界面层分离,极大简化界面层的代码
    3. 结合后台系统,对接api的数据调用
    4. 接口:在vue界面层文件中,调用userDataLoader函数,将界面vue与dataloader功能绑定
    5. 输入:
    6. 输出:
  • 数据权限设计【um_data_perm】

    1. 数据权限区分成两大部分:1、操作者(op) 2、数据(data)
    2. 操作者op分为三个身份:1、组织 2、角色 3、用户
    3. 数据分为三个所有者:1、组织 2、角色 3、用户
    4. 数据权限表:通过建立操作者与数据的映射关系,来分配数据权限
    5. 操作者通过登录系统获得身份:组织、角色、用户
    6. 操作者通过系统写入数据,包括组织、角色、用户
    7. 操作者通过系统搜索数据,此时,权限系统根据操作者自身的组织、角色、用户来进行过滤,使用 and(um_org_id = xums.um_org_id or um_role_id= xums.um_role_id)
    8. 过滤规则在数据权限表里。如果没有在数据权限表里配置,操作者在系统里看不到数据
    9. 需要额外考虑的,是否数据权限里所有的表使用相同的权限,还是不同的表不一致?涉及到设计的复杂度。简化版的,就是所有表的数据权限都一致
    10. 实例,通过inner join关联业务表与权限表:
    SELECT * FROM public.cd_worker_status cda
    	inner join xums2.vw_db_perm vdp on cda.um_user_id = vdp.db_user_id
    where vdp.db_name='cd_worker_status'
    
  • 功能权限设计【um_op_perm】

    1. 功能权限分成两大部分:1、操作者(op) 2、功能(op)
    2. 操作者op分为三个身份:1、组织 2、角色 3、用户
    3. 功能有一个类型:功能点,可以是多级的功能点。功能点可以是系统、模块、功能,也可以是菜单、按钮、链接
    4. 功能权限表:通过建立操作者与功能的映射关系,来分配功能权限
    5. 操作者通过登录系统获得身份:组织、角色、用户
    6. 系统设计者在系统中埋功能点
    7. 操作者通过系统使用功能,此时,功能权限系统根据操作者自身的组织、角色、用户,再加上功能点来判断是否有权进行。判断时,使用3次:1、按组织;2、按角色;3、按用户搜索本表并对结果进行union,搜索到,就是有功能权限,搜索不到,就是无功能权限
    8. 判断规则在功能权限表里。如果没有在功能权限表里配置,操作者在系统里无权使用功能
  • 业务系统关联设计【pm_worker】

    1. 目的:一、保证um_user的正常权限控制;二、保证使用pm_worker的扩展属性;建立um_user与pm_worker的关系
    2. 要求:pm_worker正常与pm_worker的子表关联;复用表;复用界面;
    3. ~~比如pm_worker_id与um_user_id的关系:ChatGPT推荐外关键字的方式
    4. ~~可以考虑继承的方式,将um_user作为父表,pm_worker作为子表
    5. um_user与pm_worker根本就不是一回事,正好人的名字相同而已,
    6. um_user的目标是用户可以登录进系统,pm_worker的目标是作为业务系统的人员基础
    7. 因此,业务表直接使用xums的um_org, um_role, um_user作为业务表的权限字段,不再独立创建自己的权限表,即可实现权限系统的项目级别复用
    8. 业务字段要与权限字段彻底分开,权限字段只是用于记录是谁创建了这条记录,谁能看这条记录,谁能操作这条记录;业务字段应该不受限地创建任意字段;业务字段有可能从权限表的org/role/user/取值,仅此而已
    9. 应考虑pm_org与um_org的分离,因为pm_org可能与与um_org不一致,当um_org做组织变化的时候,不应该影响pm_org数据。pm_org本质上是业务表,应该由操作员任意指定,与权限没有直接关系。
  • 通用表设计

    1. name与code的关系:name用于外部人类辨识;code用于内部系统识别。
      • 比如user_name 张三,让人类能够方便地识别,user_code zhangsan,用于内部系统核对;一般user_name可以重复,但user_code不能重复。
      • 另一个例子是权限表中,op_name 项目列表,给人类识别与判断,op_code servicex.selectProjects 给系统判断
      • 需要增加code的表有um_user、um_op、um_db、
      • id是纯关联用,在编码中只用于建立连接
      • code用于配置,需要操作员根据code来判断
    2. 权限操作表op中,有必要加入app_code,用于区隔不同app的操作。因为op是支持多app的,如果没有app_code进行隔离,会存在不同的app,相同的op_code的情况;后续发现,使用um_app_id来作为app的主要关键字和外关键字
  • 前端树设计

    1. 要分清树节点扩展slot里的node和data的关系:node是tree上一个功能性的节点,用于扩展节点本身的功能;data是绑定在tree上data属性中的一个item,用于扩展节点的数据;使用时,根据需要选择两者之一,或者两者皆选;
    2. 新增节点:为了简化模型,只提供一种模式:新增子节点;非子节点用拖拽的方式进行升级;
    3. 新增节点:首先获取当前节点,作为父节点;向后台发送一个新增记录的请求,将应用程序ID um_app_id, 父节点parent_op_id, 传递给后台;后台需补充um_tenant_id, created_at;成功后返回um_op_id;前台收到后更新节点的value,更新op_tree,object的um_op_id;调用tree.js 来 rebuild ops;如有必要,重设后台updateOps;如果失败,则显示报错信息,不更新op_tree和ops和
    4. 删除节点:获取当前节点, 向后台发送一个删除记录的请求,将um_op_id传递给后台,后台需过滤um_tenant_id;后台删除时,需要考虑将所有子类级联删除,成功后返回;前台收到后在前台删除节点;更新op_tree,调用tree.js来rebuild ops;如有必要,重设后台updateOps;如果失败,则显示报错信息,不更新op_tree和ops。
    5. 树的四个实体:①前台el-tree ②前台item_tree ③前台items对象 ④后台平面数据表,带id和parent_id。数据流转的顺序是4-3-2-1。
    6. 树的操作原则:单点操作,包括树的新增、删除、修改节点、拖拽节点,都立刻发到后台,执行数据表更新操作;数据表操作成功,则执行界面操作;否则不执行界面操作
    7. 树的节点操作内容:新增、删除、修改、拖拽(包含了升级、降级、改变顺序)、选中、附加信息
    8. 树的初始化:
      • 后台数据表将表数据打包成一维的items,发往前台
      • 前台接收到items对象,根据id和parent_id展开成item_tree
      • 前台将item_tree绑定到el-tree
    9. 树节点的checked:合并usedChecked,增加toggleChecked函数,切换当前节点is_checked状态的同时,更新所有上级节点和下级节点的is_checked状态。
    10. 所有操作,尽量针对data,而不是node

  • 应用模型

    1. 从需要的结果倒推:面向系统管理员、项目经理、财务、质控;
    2. 基础数据包含:组织、项目、角色、动态、状态、员工
    3. 项目管理最终的产出是什么?项目人天数、项目绩效打分、员工进出日期、项目成本
    4. 项目的使用者:项目经理、HRBP
    5. 使用者相互之间的交流:通过消息、Excel(不能保证每个人都用到系统)
    6. 基础数据如组织、项目、角色、动态、状态、员工,在各个用户之间共享;
    7. 基础数据md与业务数据pm要区分开来;基础数据只有管理员可以修改;业务数据用户可以任意修改;
    8. 面向单一用户,而非全组织;每个用户只能看到自己的数据,看不到其他人的;在表这一级别,数据可以重复;在消息系统未完善前,业务数据可以通过excel导出/导入
    9. 绩效数据:可以用excel导入,也可以手工输入。输入后根据绩效进行排序,再根据S、A、B+、B、C进行区分
    10. 绩效数据作为一个列表,可以任意多个绩效文件,可以用项目组-年月来命名,比如 锦海捷亚-202405-绩效文件;也可以随时删除(很重要)
  • 我的项目设计

    1. 基于基础数据,生成我的项目,可以是多条
    2. 我的项目是核心,包括了项目人员、项目进度、人员绩效
    3. 项目进度是高价值功能,包括pv,ev,ac
    4. 人员绩效是有价值功能,包括打分、排序、分组、输出
    5. 人员进出记录,是项目基础数据,需要登记项目启动后所有进出人员姓名、时间;
    6. 人员考勤记录,是外部数据,用于检验工作量的合理性
    7. 客户关系是附加功能,包括客户组织架构(可以用树形结构显示)、职位、姓名,沟通计划、沟通记录,待办事项
    8. 最佳实践:项目经理新增项目,然后绑定公司项目,输入项目计划,往项目里添加人员(进入项目时间,离开项目时间)
  • 数据库设计

    1. 由于md、pm会出现相同的不同前缀下的相同表名,会导致Go和vue api都产生混乱,因此,需要从前往后都进行切分:Go用md、pm目录进行切分;vue通过api中的命名前缀和多级对象进行切分;
    2. 判断是service.md.selectProjects更合理,还是md.service.selectProjects更合理?vue调度方式区别不大,但Go因为只有两层,就会出现md.selectProjects分不清是service还是model的情况
    3. 还有一种选项,是将md与pm分为两个schema的情况(其实没有必要,白白增加了复杂度,这与xums的思路不同,那个是用来隔离权限xums的)。发现大多数原因是因为Go语言的限制引起的,pg本身没有问题。
  • tree与table tree的区别

    1. tree用于行数、列数有限的场景
    2. table tree用于行数、列数较多的场景
    3. tree 和 table tree一般都不分页
  • 用json字段进行通用表设计

    1. 目的是使用一个通用表来支持多种项目
    2. 表字段:id、app、module、key、version

1.1.3功能子模块

  • 范围:
    • 范围:范围ID、项目ID、范围类型(合同范围、新范围、范围变更、Bug、扩展范围)、范围名称、范围状态(已确认、未确认)、确认时间、干系人ID、父范围ID、顺序
  • 进度:
    • 任务:任务ID、项目ID、任务名称、范围ID、负责人ID、开始日期、结束日期、工期(天)、进度(0-100)、状态(已完成、未完成)、父任务ID、顺序
    • 任务更新:任务更新ID、任务ID、进度(0-100)、更新日期、备注
  • 成本:
    • 成本ID、项目ID、成本类型、成本名称、发生时间、单价、数量、总额、成本单位、备注、顺序
  • 质量:
    • 质量ID、质量名称、范围ID、质量类型(规范、性能、功能)、质量目标值、质量实际值、父质量ID、顺序
  • 成员:
    • 成员ID、项目ID、人员ID、成员类型(项目经理、SA、开发)、开始日期、结束日期、个人贡献、备注、顺序;【子表-进出动态、假期】
  • 干系人:干系人ID、干系人职位、干系人类型(1级-所有者、2级-决策人、3级-影响者、4级-受影响者)、干系人名称、联系方式、干系人状态(正面、中立、负面)、顺序、父干系人ID、顺序
  • 沟通:沟通ID、成员ID、干系人ID、沟通类型(商务、项目、混合、日常)、沟通目的、沟通时间、沟通内容、沟通状态(正面、中立、负面)、父沟通ID、顺序
  • 风险:风险ID、风险类型(客户、己方)、风险内容、干系人ID、人员ID、解决方案、风险状态(事前、事中、事后)、父风险ID、顺序
  • 采购:采购ID、采购类型(软件、硬件、服务)、采购内容、采购时间、采购数量、采购金额、采购状态(计划、招标、开标、采购、结束)、父采购ID、顺序

2.业务

2.1 业务设计

2.1.1 项目范围

  • 项目范围面向可交付的成果
  • 下一级的范围之和必须100%代表上一级元素
  • 范围变更或者是需求变更,当作范围的一个类型

2.1.2 项目进度

  • 进度包含任务编制任务更新
  • 任务的增删改,与任务进度更新分开来作为两个不同的功能
  • 任务:任务ID、任务名称、负责人ID、开始日期、结束日期、进度progress、状态;
  • 任务历史(任务快照):任务历史ID、任务ID、更新日期、进度progress、备注

2.1.3 项目成本

  • 项目成本分为成本估算、成本预算、成本控制
  • 成本估算PV体现为进度计划中人力资源安排所产生的值duration * 资源数(缺省为1),成本AC体现为人员动态的变化产生的日期差值,EV体现为进度完成百分比的值(duration * 资源数 * percentage)
  • 单独的成本列表AC包括:员工类型、员工姓名、状态类型(在岗、离岗)、开始时间、结束时间、人天数;

2.1.4 项目人员

  • 项目人员的来源,是人员
  • 人员信息包括人员ID、加入时间、离开时间、备注、顺序

3. 关注事项

  1. 基础数据导入,删除旧有数据还是仅更新?
    • 根据关键字来更新,关键字有可能是ID,有可能是手机号,也可能是email等
    • 完全删除旧数据的弊端是,自增长id被删除,导致关联这张表key的其他表数据失效
  2. 分析市场上的项目管理软件,尤其是Microsoft Project

4. 关键代码

  1. 创建xums的SQL语句
-- 首先创建 xums schema(如果尚未存在)
CREATE SCHEMA IF NOT EXISTS xums;