project.md 45 KB


category: schema title: Parse Server 数据范式文档 subtitle: 项目管理系统数据表结构 name: 'project-schema'

label: database

Parse Server 数据范式 - 项目管理系统

概述

本文档详细描述了 nova-project 项目管理系统中使用的 Parse Server 数据表结构,包括表结构、字段说明、关系映射和使用场景。

系统采用多租户架构,以 Company 为核心,支持项目管理、任务管理、产品需求管理、OKR目标管理和知识文档管理。


数据表关系图

整体架构(PlantUML)

@startuml
!define TABLE(name,desc) class name as "desc" << (T,#FFAAAA) >>
!define FIELD(name,type) name : type

skinparam classAttributeIconSize 0
skinparam class {
    BackgroundColor LightYellow
    BorderColor Black
    ArrowColor Black
}

' ============ 核心实体 ============
TABLE(Company, "Company\n企业表") {
    FIELD(objectId, String)
    FIELD(name, String)
    FIELD(project, Pointer)
    FIELD(devModule, Array)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

TABLE(Profile, "Profile\n用户档案表") {
    FIELD(objectId, String)
    FIELD(name, String)
    FIELD(mobile, String)
    FIELD(email, String)
    FIELD(company, Pointer→Company)
    FIELD(department, Pointer→Department)
    FIELD(user, Pointer→_User)
    FIELD(identyType, String)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

TABLE(Department, "Department\n部门表") {
    FIELD(objectId, String)
    FIELD(name, String)
    FIELD(type, String)
    FIELD(company, Pointer→Company)
    FIELD(parent, Pointer→Department)
    FIELD(num, Number)
    FIELD(index, Number)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

' ============ 项目模块 ============
TABLE(Project, "Project\n项目表") {
    FIELD(objectId, String)
    FIELD(title, String)
    FIELD(type, String)
    FIELD(company, Pointer→Company)
    FIELD(owner, Pointer→Profile)
    FIELD(defaultTab, String)
    FIELD(isClosed, Boolean)
    FIELD(isStar, Boolean)
    FIELD(isStarted, Boolean)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

TABLE(ProjectTask, "ProjectTask\n任务表") {
    FIELD(objectId, String)
    FIELD(title, String)
    FIELD(project, Pointer→Project)
    FIELD(company, Pointer→Company)
    FIELD(assignee, Pointer→Profile)
    FIELD(owner, Pointer→Profile)
    FIELD(parent, Pointer→ProjectTask)
    FIELD(priority, Number)
    FIELD(stateList, String)
    FIELD(stateLane, String)
    FIELD(startDate, Date)
    FIELD(endDate, Date)
    FIELD(deadline, Date)
    FIELD(duration, Number)
    FIELD(estimate, Number)
    FIELD(progress, Number)
    FIELD(tag, Array)
    FIELD(desc, String)
    FIELD(cate, String)
    FIELD(isClosed, Boolean)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

TABLE(ProjectTeam, "ProjectTeam\n项目团队表") {
    FIELD(objectId, String)
    FIELD(project, Pointer→Project)
    FIELD(profile, Pointer→Profile)
    FIELD(role, String)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
}

TABLE(Product, "Product\n产品表") {
    FIELD(objectId, String)
    FIELD(title, String)
    FIELD(project, Pointer→Project)
    FIELD(company, Pointer→Company)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

TABLE(ProductRequire, "ProductRequire\n产品需求表") {
    FIELD(objectId, String)
    FIELD(title, String)
    FIELD(type, String)
    FIELD(project, Pointer→Project)
    FIELD(company, Pointer→Company)
    FIELD(product, Pointer→Product)
    FIELD(module, Pointer→ProductRequire)
    FIELD(parent, Pointer→ProductRequire)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

' ============ OKR模块 ============
TABLE(OKR, "OKR\nOKR周期表") {
    FIELD(objectId, String)
    FIELD(title, String)
    FIELD(company, Pointer→Company)
    FIELD(startDate, Date)
    FIELD(endDate, Date)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

TABLE(OKRObject, "OKRObject\nOKR对象表") {
    FIELD(objectId, String)
    FIELD(title, String)
    FIELD(type, String)
    FIELD(okr, Pointer→OKR)
    FIELD(company, Pointer→Company)
    FIELD(department, Pointer→Department)
    FIELD(profile, Pointer→Profile)
    FIELD(tree, Pointer→OKRObject)
    FIELD(parent, Pointer→OKRObject)
    FIELD(bsc, String)
    FIELD(value, Number)
    FIELD(unit, String)
    FIELD(index, Number)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

' ============ 知识库模块 ============
TABLE(NoteSpace, "NoteSpace\n知识空间表") {
    FIELD(objectId, String)
    FIELD(title, String)
    FIELD(type, String)
    FIELD(company, Pointer→Company)
    FIELD(project, Pointer→Project)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

TABLE(NotePad, "NotePad\n文档笔记表") {
    FIELD(objectId, String)
    FIELD(title, String)
    FIELD(space, Pointer→NoteSpace)
    FIELD(profile, Pointer→Profile)
    FIELD(parent, Pointer→NotePad)
    FIELD(content, String)
    FIELD(isDeleted, Boolean)
    FIELD(createdAt, Date)
    FIELD(updatedAt, Date)
}

' ============ 关系连线 ============

' Company 一对多关系
Company "1" --> "n" Profile : 企业员工
Company "1" --> "n" Department : 企业部门
Company "1" --> "n" Project : 企业项目
Company "1" --> "n" OKR : OKR周期

' Profile 关系
Profile "n" --> "1" Department : 所属部门
Profile "1" --> "n" ProjectTask : assignee\n执行任务
Profile "1" --> "n" ProjectTask : owner\n创建任务
Profile "1" --> "n" NotePad : 创建笔记

' Department 树状结构
Department "1" --> "n" Department : parent\n上下级

' Project 相关
Project "n" --> "1" Profile : owner\n项目负责人
Project "1" --> "n" ProjectTask : 项目任务
Project "1" --> "n" Product : 项目产品
Project "1" --> "1" NoteSpace : Wiki空间
Project "n" <--> "n" Profile : ProjectTeam\n团队成员

' ProjectTask 树状结构
ProjectTask "1" --> "n" ProjectTask : parent\n父子任务

' Product 相关
Product "1" --> "n" ProductRequire : 产品需求

' ProductRequire 树状结构
ProductRequire "1" --> "n" ProductRequire : module/parent\n层级结构

' OKR 相关
OKR "1" --> "n" OKRObject : OKR对象
OKRObject "n" --> "1" Department : 部门OKR
OKRObject "n" --> "1" Profile : 个人OKR
OKRObject "1" --> "n" OKRObject : tree/parent\n指标树

' NoteSpace 相关
NoteSpace "1" --> "n" NotePad : 空间笔记

' NotePad 树状结构
NotePad "1" --> "n" NotePad : parent\n目录层级

@enduml

数据表详细说明

1. Company(企业表)

用途: 多租户系统的核心,所有数据通过 company 字段进行租户隔离。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "1AiWpTEDH9"
name String 企业名称 "科技有限公司"
project Pointer 关联项目 → Project
devModule Array 开发模块配置 ["module1", "module2"]
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

关系:

  • 一对多: Company → Profile(企业员工)
  • 一对多: Company → Department(企业部门)
  • 一对多: Company → Project(企业项目)
  • 一对多: Company → OKR(OKR周期)

使用场景:

// 获取当前企业ID
const companyId = localStorage.getItem("Parse/CompanyId") || localStorage.getItem("company");

// 数据隔离查询
const query = new Parse.Query("Project");
query.equalTo("company", companyId);

索引建议:

  • objectId (主键,自动索引)
  • name

2. Profile(用户档案表)

用途: 存储用户在企业内的档案信息,关联 Parse 内置 _User 表。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "abc123xyz"
name String 用户姓名 "张三"
mobile String 手机号 "13800138000"
email String 邮箱 "zhangsan@example.com"
company Pointer 所属企业 → Company
department Pointer 所属部门 → Department
user Pointer 关联Parse用户 → _User
identyType String 身份类型 "admin" / "coders"
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

identyType 枚举值:

  • admin: 管理员权限
  • coders: 开发者权限
  • 其他: 普通用户权限

关系:

  • 多对一: Profile → Company(所属企业)
  • 多对一: Profile → Department(所属部门)
  • 多对一: Profile → _User(关联用户账号)
  • 一对多: Profile ← ProjectTask (assignee, 执行任务)
  • 一对多: Profile ← ProjectTask (owner, 创建任务)
  • 多对多: Profile ←→ Project(通过 ProjectTeam)

使用场景:

// 获取当前用户档案ID
const profileId = localStorage.getItem("Parse/ProfileId");

// 查询用户创建的任务
const query = new Parse.Query("ProjectTask");
query.equalTo("owner", profileId);

// 查询用户执行的任务
const query2 = new Parse.Query("ProjectTask");
query2.equalTo("assignee", profileId);

// 搜索用户(模糊查询)
const query3 = new Parse.Query("Profile");
query3.matches("name", keyword, "i"); // 不区分大小写
query3.matches("mobile", keyword, "i");

索引建议:

  • company + isDeleted
  • name + company
  • mobile + company
  • user

3. Department(部门表)

用途: 组织架构管理,支持树状结构(多层级部门)。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "dept001"
name String 部门名称 "技术部"
type String 类型 "depart"
company Pointer 所属企业 → Company
parent Pointer 父部门 → Department
num Number 排序号 1
index Number 索引 100
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

关系:

  • 多对一: Department → Company(所属企业)
  • 树状结构: Department → Department (parent, 支持多层级)
  • 一对多: Department ← Profile(部门员工)
  • 一对多: Department ← OKRObject(部门OKR)

使用场景:

// 查询企业所有部门
const query = new Parse.Query("Department");
query.equalTo("company", companyId);
query.notEqualTo("isDeleted", true);
query.ascending("num"); // 按排序号排序

// 查询一级部门(没有父级)
const rootQuery = new Parse.Query("Department");
rootQuery.equalTo("company", companyId);
rootQuery.doesNotExist("parent"); // parent 为空

// 查询某部门的子部门
const childQuery = new Parse.Query("Department");
childQuery.equalTo("parent", parentDeptId);

索引建议:

  • company + isDeleted
  • parent + company
  • num + company

4. Project(项目表)

用途: 项目管理的核心表,支持多种类型项目。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "proj001"
title String 项目标题 "CRM系统开发"
type String 项目类型 "project" / "book" / "plan" / "module"
company Pointer 所属企业 → Company
owner Pointer 项目负责人 → Profile
defaultTab String 默认Tab页 "gantt" / "kanban" / "wiki"
isClosed Boolean 是否已关闭 false
isStar Boolean 是否星标 false
isStarted Boolean 是否已启动 true
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

type 枚举值:

  • project: 项目平台
  • book: 电子书籍
  • plan: 行动策略
  • module: 功能模块

关系:

  • 多对一: Project → Company(所属企业)
  • 多对一: Project → Profile (owner, 项目负责人)
  • 一对多: Project → ProjectTask(项目任务)
  • 一对多: Project → Product(项目产品)
  • 一对多: Project → ProductRequire(项目需求)
  • 一对一: Project → NoteSpace(Wiki空间)
  • 多对多: Project ←→ Profile(通过 ProjectTeam)

使用场景:

// 查询项目
const query = new Parse.Query("Project");
await query.get(projectId);

// 查询用户创建的项目
const myQuery = new Parse.Query("Project");
myQuery.equalTo("owner", profileId);
myQuery.notEqualTo("isDeleted", true);

// 查询星标项目
const starQuery = new Parse.Query("Project");
starQuery.equalTo("isStar", true);
starQuery.equalTo("company", companyId);

// 按类型筛选
const typeQuery = new Parse.Query("Project");
typeQuery.containedIn("type", ["project", "module"]);

// 查询用户参与的项目(通过 ProjectTeam)
const teamQuery = new Parse.Query("ProjectTeam");
teamQuery.equalTo("profile", profileId);
teamQuery.include("project");
const teams = await teamQuery.find();
const projects = teams.map(team => team.get("project"));

索引建议:

  • company + isDeleted
  • owner + company
  • isStar + company
  • isStarted + company
  • type + company
  • updatedAt (降序)

5. ProjectTask(项目任务表)

用途: 存储项目任务,支持甘特图、看板等多种视图,支持父子任务。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "task001"
title String 任务标题 "实现用户登录功能"
desc String 任务描述 "包括前端页面和后端API"
project Pointer 所属项目 → Project
company Pointer 所属企业 → Company
assignee Pointer 任务执行人 → Profile
owner Pointer 任务创建人 → Profile
parent Pointer 父任务 → ProjectTask
priority Number 优先级 1 / 50 / 70 / 90 / 100
stateList String 任务状态 "待分配" / "进行中" / "测试中" / "已完成" / "已上线"
stateLane String 看板泳道 "默认泳道"
startDate Date 开始时间 2024-01-01T00:00:00.000Z
endDate Date 结束时间 2024-01-10T00:00:00.000Z
deadline Date 截止时间 2024-01-15T00:00:00.000Z
duration Number 实际工时(小时) 8.5
estimate Number 预估工时(小时) 10
progress Number 完成进度(0-100) 75
tag Array 标签数组 ["前端", "紧急"]
cate String 工作分类 "frontend" / "backend" / "design" / "testing"
target String 排序目标ID 用于甘特图排序
remark String 备注 "需要先完成设计稿"
approver Pointer 审批人 → Profile
approvalDate Date 审批日期 2024-01-10T00:00:00.000Z
isClosed Boolean 是否已关闭 false
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

priority 枚举值:

  • 1: 最低
  • 50: 普通(默认)
  • 70: 较高
  • 90: 重要
  • 100: 严重

stateList 枚举值:

待分配 → 进行中 → 测试中 → 已完成 → 已上线

cate 枚举值:

  • requirement: 需求
  • frontend: 前端开发
  • backend: 后端开发
  • design: 设计
  • testing: 测试

关系:

  • 多对一: ProjectTask → Project(所属项目)
  • 多对一: ProjectTask → Company(所属企业)
  • 多对一: ProjectTask → Profile (assignee, 执行人)
  • 多对一: ProjectTask → Profile (owner, 创建人)
  • 多对一: ProjectTask → Profile (approver, 审批人)
  • 树状结构: ProjectTask → ProjectTask (parent, 父子任务)

使用场景:

// 查询项目任务
const query = new Parse.Query("ProjectTask");
query.equalTo("project", projectId);
query.notEqualTo("isDeleted", true);
query.notEqualTo("isClosed", true);
query.descending("startDate");
query.include("assignee", "owner");

// 查询用户的任务
const myTaskQuery = new Parse.Query("ProjectTask");
myTaskQuery.equalTo("assignee", profileId);
myTaskQuery.notEqualTo("isDeleted", true);

// 按状态筛选
const stateQuery = new Parse.Query("ProjectTask");
stateQuery.containedIn("stateList", ["进行中", "测试中"]);

// 查询即将到期的任务
const today = new Date();
const endDate = new Date();
endDate.setDate(endDate.getDate() + 7); // 7天内到期
const deadlineQuery = new Parse.Query("ProjectTask");
deadlineQuery.greaterThanOrEqualTo("deadline", today);
deadlineQuery.lessThan("deadline", endDate);
deadlineQuery.notContainedIn("stateList", ["已完成", "已上线"]);

// 查询用户参与的所有项目(SQL方式)
const sql = `
SELECT DISTINCT(task."project"), pro."title" AS "name"
FROM "ProjectTask" task
LEFT JOIN "Project" pro ON pro."objectId" = task."project"
WHERE task."isDeleted" IS NOT TRUE
  AND task."isClosed" IS NOT TRUE
  AND (task."assignee" = '${profileId}' OR task."owner" = '${profileId}')
  AND task."project" IS NOT NULL
`;

工时统计(SQL):

-- 用户工时统计
SELECT
    pt."assignee",
    SUM(pt."d") as duration,      -- 实际工时
    SUM(pt."e") as estimate        -- 预估工时
FROM (
    SELECT *,
    CASE WHEN "ProjectTask"."duration" IS NOT NULL
         THEN "ProjectTask"."duration" ELSE 0 END as d,
    CASE WHEN "ProjectTask"."estimate" IS NOT NULL
         THEN "ProjectTask"."estimate" ELSE 0 END as e
    FROM "ProjectTask"
    WHERE "company" = $1
      AND ("isDeleted" != true OR "isDeleted" IS NULL)
      AND "startDate" >= $2
      AND "startDate" < $3
) as pt
GROUP BY pt."assignee"

索引建议:

  • project + isDeleted
  • assignee + isDeleted
  • owner + isDeleted
  • company + startDate
  • priority (降序)
  • stateList + project
  • deadline + stateList

6. ProjectTeam(项目团队表)

用途: 管理项目成员关系,实现项目与用户的多对多关联。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "team001"
project Pointer 所属项目 → Project
profile Pointer 团队成员 → Profile
role String 成员角色 "开发" / "测试" / "PM"
isDeleted Boolean 软删除标记 false
createdAt Date 自动 加入时间 2024-01-01T00:00:00.000Z

关系:

  • 多对一: ProjectTeam → Project
  • 多对一: ProjectTeam → Profile
  • 实现: Project ←→ Profile 的多对多关系

使用场景:

// 查询项目成员
const query = new Parse.Query("ProjectTeam");
query.equalTo("project", projectId);
query.notEqualTo("isDeleted", true);
query.include("profile");
const members = await query.find();

// 查询用户参与的项目
const myProjectQuery = new Parse.Query("ProjectTeam");
myProjectQuery.equalTo("profile", profileId);
myProjectQuery.notEqualTo("isDeleted", true);
myProjectQuery.include("project");
myProjectQuery.select("project");
const teams = await myProjectQuery.find();
const projects = teams.map(team => team.get("project"));

// 添加项目成员
const ProjectTeam = Parse.Object.extend("ProjectTeam");
const team = new ProjectTeam();
team.set("project", project.toPointer());
team.set("profile", profile.toPointer());
team.set("role", "开发");
await team.save();

索引建议:

  • project + isDeleted
  • profile + isDeleted
  • project + profile (联合唯一索引)

7. Product(产品表)

用途: 项目下的产品管理。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "prod001"
title String 产品名称 "移动端APP"
project Pointer 所属项目 → Project
company Pointer 所属企业 → Company
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

关系:

  • 多对一: Product → Project(所属项目)
  • 多对一: Product → Company(所属企业)
  • 一对多: Product → ProductRequire(产品需求/模块)

使用场景:

// 查询项目的产品列表
const query = new Parse.Query("Product");
query.equalTo("project", projectId);
query.addDescending("updatedAt");
const products = await query.find();

// 创建产品
const Product = Parse.Object.extend("Product");
const product = new Product();
product.set("title", "Web管理后台");
product.set("project", project.toPointer());
product.set("company", company.toPointer());
await product.save();

索引建议:

  • project + isDeleted
  • company + isDeleted
  • updatedAt (降序)

8. ProductRequire(产品需求表)

用途: 支持模块化的需求管理,可构建树状结构(模块→子模块→需求)。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "req001"
title String 需求/模块名称 "用户管理模块"
type String 类型 "module" / "require"
project Pointer 所属项目 → Project
company Pointer 所属企业 → Company
product Pointer 所属产品 → Product
module Pointer 所属模块 → ProductRequire
parent Pointer 父级 → ProductRequire
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

type 枚举值:

  • module: 功能模块(可以有子模块)
  • require: 具体需求(归属于某个模块)

树状结构说明:

  • 一级模块: type="module", module=null, parent=null
  • 二级模块: type="module", module=一级模块ID, parent=一级模块ID
  • 具体需求: type="require", module=所属模块ID

关系:

  • 多对一: ProductRequire → Project(所属项目)
  • 多对一: ProductRequire → Company(所属企业)
  • 多对一: ProductRequire → Product(所属产品)
  • 树状结构: ProductRequire → ProductRequire (module/parent)

使用场景:

// 查询项目的模块列表
const moduleQuery = new Parse.Query("ProductRequire");
moduleQuery.equalTo("project", projectId);
moduleQuery.equalTo("type", "module");
moduleQuery.addDescending("updatedAt");
const modules = await moduleQuery.find();

// 查询某产品下的需求
const requireQuery = new Parse.Query("ProductRequire");
requireQuery.equalTo("project", projectId);
requireQuery.equalTo("type", "require");
requireQuery.equalTo("product", productId);
requireQuery.limit(1000);
const requires = await requireQuery.find();

// 查询某模块及其子模块的所有需求
function getModulePathIds(module, allModules) {
  let ids = [module.id];
  allModules.forEach(mitem => {
    if (mitem.get("module")?.id == module.id) {
      ids.push(mitem.id);
    }
    if (mitem.get("parent")?.id == module.id) {
      ids.push(mitem.id);
    }
  });
  return ids;
}

const query = new Parse.Query("ProductRequire");
query.equalTo("type", "require");
query.containedIn("module", modulePathIds);

索引建议:

  • project + type + isDeleted
  • product + type
  • module + type
  • parent + type
  • updatedAt (降序)

9. OKR(目标管理周期表)

用途: 管理OKR周期(年度/季度),支持公司级目标管理。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "okr001"
title String OKR名称 "2024年度OKR"
company Pointer 所属企业 → Company
startDate Date 开始日期 2024-01-01T00:00:00.000Z
endDate Date 结束日期 2024-12-31T23:59:59.999Z
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

关系:

  • 多对一: OKR → Company(所属企业)
  • 一对多: OKR → OKRObject(OKR对象/目标)

使用场景:

// 查询企业的OKR列表
const query = new Parse.Query("OKR");
query.equalTo("company", companyId);
query.notEqualTo("isDeleted", true);
query.addDescending("updatedAt");
const okrs = await query.find();

// 创建年度OKR
const OKR = Parse.Object.extend("OKR");
const okr = new OKR();
okr.set("title", "2024年度OKR");
okr.set("company", company.toPointer());
okr.set("startDate", new Date("2024-01-01"));
okr.set("endDate", new Date("2024-12-31"));
await okr.save();

索引建议:

  • company + isDeleted
  • startDate + endDate
  • updatedAt (降序)

10. OKRObject(OKR对象表)

用途: 存储OKR的指标树和具体目标,支持公司/部门/个人三级架构,采用BSC平衡计分卡。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "okrobj001"
title String 目标/指标树名称 "年度营收目标"
type String 对象类型 "tree" / "objective"
okr Pointer 所属OKR周期 → OKR
company Pointer 所属企业 → Company
department Pointer 所属部门 → Department
profile Pointer 所属个人 → Profile
tree Pointer 所属指标树 → OKRObject
parent Pointer 父级指标树 → OKRObject
bsc String 平衡计分卡维度 "财务" / "客户" / "内部运营" / "学习成长"
value Number 目标值 1000000
unit String 单位 "万元" / "%"
index Number 排序索引 1
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

type 枚举值:

  • tree: 指标树节点(用于分类组织)
  • objective: 具体目标(O或KR)

BSC 平衡计分卡维度:

  • 财务: 财务指标(如:营收、利润等)
  • 客户: 客户相关指标(如:客户满意度、客户数等)
  • 内部运营: 内部流程指标(如:交付效率、质量等)
  • 学习成长: 学习与创新指标(如:培训时长、新技能等)

三级架构说明:

  1. 公司级OKR: department=null, profile=null
  2. 部门级OKR: department不为空, profile=null
  3. 个人级OKR: department可选, profile不为空

关系:

  • 多对一: OKRObject → OKR(所属OKR周期)
  • 多对一: OKRObject → Company(所属企业)
  • 多对一: OKRObject → Department(部门OKR,可选)
  • 多对一: OKRObject → Profile(个人OKR,可选)
  • 树状结构: OKRObject → OKRObject (tree/parent, 指标树层级)

使用场景:

// 查询公司级指标树
const treeQuery = new Parse.Query("OKRObject");
treeQuery.equalTo("okr", okrId);
treeQuery.equalTo("type", "tree");
treeQuery.equalTo("department", null);
treeQuery.equalTo("profile", null);
treeQuery.ascending("index");
const trees = await treeQuery.find();

// 查询部门OKR
const deptOKRQuery = new Parse.Query("OKRObject");
deptOKRQuery.equalTo("okr", okrId);
deptOKRQuery.equalTo("department", departmentId);
deptOKRQuery.addDescending("updatedAt");
const deptOKRs = await deptOKRQuery.find();

// 查询个人OKR
const personalOKRQuery = new Parse.Query("OKRObject");
personalOKRQuery.equalTo("okr", okrId);
personalOKRQuery.equalTo("profile", profileId);
const personalOKRs = await personalOKRQuery.find();

// 查询某指标树下的目标
const objectiveQuery = new Parse.Query("OKRObject");
objectiveQuery.equalTo("okr", okrId);
objectiveQuery.equalTo("tree", treeId);
objectiveQuery.equalTo("type", "objective");
const objectives = await objectiveQuery.find();

// 按BSC维度筛选
const bscQuery = new Parse.Query("OKRObject");
bscQuery.equalTo("bsc", "财务");
bscQuery.equalTo("okr", okrId);

创建目标示例:

// 创建公司级指标树
const OKRObject = Parse.Object.extend("OKRObject");
const tree = new OKRObject();
tree.set("title", "财务指标");
tree.set("type", "tree");
tree.set("okr", okr.toPointer());
tree.set("company", company.toPointer());
tree.set("bsc", "财务");
tree.set("index", 1);
await tree.save();

// 创建具体目标
const objective = new OKRObject();
objective.set("title", "实现营收1000万");
objective.set("type", "objective");
objective.set("okr", okr.toPointer());
objective.set("company", company.toPointer());
objective.set("tree", tree.toPointer());
objective.set("bsc", "财务");
objective.set("value", 1000);
objective.set("unit", "万元");
await objective.save();

// 创建部门OKR(继承指标树)
const deptOKR = new OKRObject();
deptOKR.set("title", "技术部营收贡献500万");
deptOKR.set("type", "objective");
deptOKR.set("okr", okr.toPointer());
deptOKR.set("company", company.toPointer());
deptOKR.set("department", department.toPointer());
deptOKR.set("tree", tree.toPointer());
deptOKR.set("bsc", "财务");
deptOKR.set("value", 500);
deptOKR.set("unit", "万元");
await deptOKR.save();

索引建议:

  • okr + type + isDeleted
  • okr + department
  • okr + profile
  • tree + type
  • bsc + okr
  • index + okr

11. NoteSpace(知识空间表)

用途: 文档空间管理,通常与项目关联,作为项目Wiki。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "space001"
title String 空间标题 "CRM系统的Wiki空间"
type String 空间类型 "project"
company Pointer 所属企业 → Company
project Pointer 关联项目 → Project
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

type 枚举值:

  • project: 项目空间

关系:

  • 多对一: NoteSpace → Company(所属企业)
  • 一对一: NoteSpace → Project(关联项目)
  • 一对多: NoteSpace → NotePad(空间笔记)

使用场景:

// 创建项目Wiki空间(自动)
const NoteSpace = Parse.Object.extend("NoteSpace");
const space = new NoteSpace();
space.set("title", project.get("title") + "的Wiki空间");
space.set("type", "project");
space.set("company", company.toPointer());
space.set("project", project.toPointer());
await space.save();

// 查询项目的Wiki空间
const query = new Parse.Query("NoteSpace");
query.equalTo("project", projectId);
query.equalTo("type", "project");
query.notEqualTo("isDeleted", true);
const space = await query.first();

// 查询用户有权访问的空间(通过项目团队)
const teamQuery = new Parse.Query("ProjectTeam");
teamQuery.equalTo("profile", profileId);
const teams = await teamQuery.find();
const projectIds = teams.map(t => t.get("project").id);

const spaceQuery = new Parse.Query("NoteSpace");
spaceQuery.containedIn("project", projectIds);
const spaces = await spaceQuery.find();

索引建议:

  • project + type
  • company + isDeleted

12. NotePad(文档笔记表)

用途: 存储具体的文档笔记,支持树状结构(目录层级)。

表结构:

字段名 类型 必填 说明 示例值
objectId String 主键ID "note001"
title String 笔记标题 "API接口设计"
space Pointer 所属空间 → NoteSpace
profile Pointer 创建者 → Profile
parent Pointer 父笔记 → NotePad
content String 笔记内容 Markdown格式
isDeleted Boolean 软删除标记 false
createdAt Date 自动 创建时间 2024-01-01T00:00:00.000Z
updatedAt Date 自动 更新时间 2024-01-01T00:00:00.000Z

关系:

  • 多对一: NotePad → NoteSpace(所属空间)
  • 多对一: NotePad → Profile(创建者)
  • 树状结构: NotePad → NotePad (parent, 支持多层级目录)

使用场景:

// 查询空间的一级笔记(根目录)
const query = new Parse.Query("NotePad");
query.equalTo("space", spaceId);
query.doesNotExist("parent"); // 或 query.equalTo("parent", null)
query.notEqualTo("isDeleted", true);
const rootNotes = await query.find();

// 查询某笔记的子笔记
const childQuery = new Parse.Query("NotePad");
childQuery.equalTo("parent", parentNoteId);
childQuery.notEqualTo("isDeleted", true);
const children = await childQuery.find();

// 创建笔记
const NotePad = Parse.Object.extend("NotePad");
const note = new NotePad();
note.set("title", "接口文档");
note.set("space", space.toPointer());
note.set("profile", profile.toPointer());
note.set("content", "# API接口设计\n\n...");
await note.save();

// 递归查询笔记层级结构(SQL方式)
const sql = `
WITH RECURSIVE notepad_hierarchy AS (
  SELECT np."objectId", np."parent", np."title", 1 AS "level"
  FROM "NotePad" np
  WHERE np."parent" IS NULL
    AND np."isDeleted" IS NOT TRUE
    AND np."space" = $1
  UNION ALL
  SELECT np2."objectId", np2."parent", np2."title", nh."level" + 1
  FROM "NotePad" np2
  JOIN notepad_hierarchy nh ON nh."objectId" = np2."parent"
  WHERE np2."isDeleted" IS NOT TRUE
)
SELECT * FROM notepad_hierarchy
ORDER BY "level", "createdAt"
`;

索引建议:

  • space + isDeleted
  • parent + space
  • profile + space
  • updatedAt (降序)

通用字段说明

所有 Parse Server 表都包含以下系统字段:

字段名 类型 说明
objectId String 唯一标识符,自动生成,主键
createdAt Date 创建时间,自动生成
updatedAt Date 更新时间,自动维护
ACL ACL 访问控制列表(权限控制)

软删除模式

系统采用软删除模式,几乎所有表都包含 isDeleted 字段:

// 标准查询模式(排除已删除数据)
query.notEqualTo("isDeleted", true);

// 或
query.equalTo("isDeleted", null);

优势:

  • 数据可恢复
  • 保持关联完整性
  • 支持审计追踪
  • 避免级联删除问题

多租户隔离

系统采用多租户架构,所有业务表都包含 company 字段:

// 获取当前企业ID
const companyId = localStorage.getItem("Parse/CompanyId") || localStorage.getItem("company");

// 所有查询都需要加上企业隔离
query.equalTo("company", companyId);

关键查询模式

1. 分页查询

query.limit(pageSize);
query.skip(pageSize * (pageIndex - 1));

2. 排序

query.descending("createdAt");  // 降序
query.ascending("index");       // 升序
query.addDescending("priority"); // 添加额外排序

3. 关联查询(Include)

// 单层级关联
query.include("assignee", "owner");

// 多层级关联
query.include(["project", "assignee.user", "assignee.department"]);

4. 字段筛选

// 只返回指定字段
query.select("title", "startDate", "assignee");

5. 计数查询

const count = await query.count();

// 带计数的查询
query.withCount();
const result = await query.find();
console.log(result.count); // 总数
console.log(result.results); // 结果列表

6. 树状结构查询

// 查询根节点
const roots = list.filter(item => !item.get("parent"));

// 查询子节点
const children = list.filter(item => item.get("parent")?.id === parentId);

// 递归构建树
function buildTree(nodes, parentId = null) {
  return nodes
    .filter(node => node.get("parent")?.id === parentId)
    .map(node => ({
      ...node.toJSON(),
      children: buildTree(nodes, node.id)
    }));
}

性能优化建议

1. 索引策略

必建索引:

  • 所有表: company + isDeleted
  • 所有表: updatedAt (降序)
  • 关联字段: 所有 Pointer 字段
  • 查询字段: 经常用于筛选的字段

复合索引示例:

// Project表
{ company: 1, isDeleted: 1, isStar: 1 }
{ company: 1, owner: 1, isDeleted: 1 }

// ProjectTask表
{ project: 1, isDeleted: 1, stateList: 1 }
{ assignee: 1, isDeleted: 1, deadline: 1 }
{ company: 1, startDate: -1 }

// OKRObject表
{ okr: 1, type: 1, isDeleted: 1 }
{ okr: 1, department: 1, profile: 1 }

2. 查询优化

避免N+1查询:

// ❌ 不好的做法
const tasks = await query.find();
for (const task of tasks) {
  const assignee = await task.get("assignee").fetch(); // N次查询
}

// ✅ 好的做法
query.include("assignee");
const tasks = await query.find(); // 1次查询

批量操作:

// 批量保存
await Parse.Object.saveAll(objects);

// 批量删除
await Parse.Object.destroyAll(objects);

使用 select 限制字段:

// ❌ 返回所有字段
const tasks = await query.find();

// ✅ 只返回需要的字段
query.select("title", "startDate", "stateList");
const tasks = await query.find();

3. 缓存策略

项目中实现了简单的内存缓存:

// 检查是否需要更新数据
async hasUpdateObject(list: Parse.Object[], querySrc: Parse.Query, refresh = false) {
  if (refresh) { return true; }

  if (list?.length > 0) {
    // 获取最新的一条数据
    const query = Parse.Query.fromJSON(querySrc.className, querySrc.toJSON());
    query.addDescending("updatedAt");
    query.withCount();
    query.limit(1);
    const data = await query.find();

    // 比较更新时间和数量
    if (data.results[0]?.updatedAt > list[0].updatedAt) {
      return true; // 有更新
    }
    if (data.count !== list.length) {
      return true; // 数量变化
    }
    return false; // 无需更新
  }

  return true; // 首次加载
}

数据完整性约束

必填字段(应用层校验)

表名 必填字段
Company name
Profile name, company
Department name, type, company
Project title, company, owner
ProjectTask title, project, company, owner, startDate
ProjectTeam project, profile
Product title, project, company
ProductRequire title, type, project, company
OKR title, company
OKRObject title, type, okr, company
NoteSpace title, type, company
NotePad space, profile

外键约束

所有 Pointer 字段在应用层需要保证引用完整性:

  • 删除操作应采用软删除
  • 级联更新应谨慎处理
  • 关联查询前应检查对象是否存在

特殊业务逻辑

1. 工时管理

基准工时: 26天 × 7小时 = 182小时/月

// ProjectTask 字段
duration: Number  // 实际工时(小时)
estimate: Number  // 预估工时(小时)

工时统计SQL:

SELECT
    pt."assignee",
    SUM(COALESCE(pt."duration", 0)) as actual_hours,
    SUM(COALESCE(pt."estimate", 0)) as estimated_hours,
    SUM(COALESCE(pt."duration", 0)) / 182.0 * 100 as completion_rate
FROM "ProjectTask" pt
WHERE pt."company" = $1
  AND (pt."isDeleted" != true OR pt."isDeleted" IS NULL)
  AND pt."startDate" >= $2
  AND pt."startDate" < $3
GROUP BY pt."assignee"

2. 任务状态流转

待分配 → 进行中 → 测试中 → 已完成 → 已上线

状态映射:

const stateColorMap = {
  '待分配': 'default',
  '进行中': 'processing',
  '测试中': 'warning',
  '已完成': 'success',
  '已上线': 'gold',
};

3. 优先级映射

const priorityMap = {
  1:   { text: '最低', color: 'default' },
  50:  { text: '普通', color: 'blue' },
  70:  { text: '较高', color: 'orange' },
  90:  { text: '重要', color: 'red' },
  100: { text: '严重', color: '#f50' },
};

4. OKR 三级架构

// 公司级 OKR
okrObject.set("department", null);
okrObject.set("profile", null);

// 部门级 OKR
okrObject.set("department", department.toPointer());
okrObject.set("profile", null);

// 个人级 OKR
okrObject.set("profile", profile.toPointer());
// department 可选

5. 平衡计分卡(BSC)

const bscList = [
  { label: "财务", value: "财务" },
  { label: "客户", value: "客户" },
  { label: "内部运营", value: "内部运营" },
  { label: "学习成长", value: "学习成长" },
];

SQL 查询示例

1. 用户参与项目统计

SELECT DISTINCT(task."project"), pro."title" AS "name"
FROM "ProjectTask" task
LEFT JOIN "Project" pro ON pro."objectId" = task."project"
WHERE task."isDeleted" IS NOT TRUE
  AND task."isClosed" IS NOT TRUE
  AND (task."assignee" = $1 OR task."owner" = $1)
  AND task."project" IS NOT NULL
ORDER BY pro."updatedAt" DESC

2. 笔记层级结构查询

WITH RECURSIVE notepad_hierarchy AS (
  -- 查询根节点
  SELECT
    np."objectId",
    np."parent",
    np."title",
    np."space",
    1 AS "level"
  FROM "NotePad" np
  WHERE np."parent" IS NULL
    AND np."isDeleted" IS NOT TRUE
    AND np."space" = $1

  UNION ALL

  -- 递归查询子节点
  SELECT
    np2."objectId",
    np2."parent",
    np2."title",
    np2."space",
    nh."level" + 1
  FROM "NotePad" np2
  JOIN notepad_hierarchy nh ON nh."objectId" = np2."parent"
  WHERE np2."isDeleted" IS NOT TRUE
)
SELECT * FROM notepad_hierarchy
ORDER BY "level", "createdAt"

3. 月度工时统计

SELECT
    p."name",
    p."objectId" as profile_id,
    COALESCE(SUM(pt."duration"), 0) as actual_hours,
    COALESCE(SUM(pt."estimate"), 0) as estimated_hours,
    COUNT(pt."objectId") as task_count
FROM "Profile" p
LEFT JOIN "ProjectTask" pt ON pt."assignee" = p."objectId"
    AND pt."isDeleted" IS NOT TRUE
    AND pt."startDate" >= $1
    AND pt."startDate" < $2
WHERE p."company" = $3
  AND p."isDeleted" IS NOT TRUE
GROUP BY p."objectId", p."name"
ORDER BY actual_hours DESC

文件位置索引

核心服务文件:

  • 项目服务: src/modules/project/project.service.ts
  • OKR服务: src/modules/okr/okr.service.ts
  • 部门服务: src/modules/okr/depart.service.ts

主要组件:

  • 项目管理: src/modules/project/project-manage/project-manage.component.ts
  • 甘特图: src/modules/project/project-gantt/project-gantt.component.ts
  • 看板: src/modules/project/project-kanban/project-kanban.component.ts
  • 产品管理: src/modules/project/project-product/project-product.component.ts
  • OKR面板: src/modules/okr/page-okr-panel/page-okr-panel.component.ts
  • OKR仪表板: src/modules/okr/okr-dashboard/okr-dashboard.component.ts

附录

A. 数据表清单

序号 表名 中文名 用途
1 Company 企业表 多租户核心
2 Profile 用户档案表 用户信息
3 Department 部门表 组织架构
4 Project 项目表 项目管理
5 ProjectTask 任务表 任务管理
6 ProjectTeam 项目团队表 成员关系
7 Product 产品表 产品管理
8 ProductRequire 产品需求表 需求管理
9 OKR OKR周期表 目标周期
10 OKRObject OKR对象表 目标管理
11 NoteSpace 知识空间表 文档空间
12 NotePad 文档笔记表 笔记管理

B. 关系类型说明

  • 一对多 (1:N): 一个父记录对应多个子记录
  • 多对多 (N:N): 通过中间表实现
  • 一对一 (1:1): 一个记录对应另一个记录
  • 树状结构: 通过 parent 字段实现自引用

C. 数据类型说明

Parse 类型 说明 示例
String 字符串 "项目名称"
Number 数字 100
Boolean 布尔值 true / false
Date 日期时间 ISO 8601 格式
Array 数组 ["tag1", "tag2"]
Object JSON对象 { key: "value" }
Pointer 关联引用 { __type: "Pointer", className: "Profile", objectId: "abc" }
Relation 多对多关系 Parse.Relation
File 文件 Parse.File

文档版本: v1.0 最后更新: 2025-10-13 维护者: Nova Project Team