category: schema title: Parse Server 数据范式文档 subtitle: 项目管理系统数据表结构 name: 'project-schema'
本文档详细描述了 nova-project 项目管理系统中使用的 Parse Server 数据表结构,包括表结构、字段说明、关系映射和使用场景。
系统采用多租户架构,以 Company 为核心,支持项目管理、任务管理、产品需求管理、OKR目标管理和知识文档管理。
@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
用途: 多租户系统的核心,所有数据通过 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 |
关系:
使用场景:
// 获取当前企业ID
const companyId = localStorage.getItem("Parse/CompanyId") || localStorage.getItem("company");
// 数据隔离查询
const query = new Parse.Query("Project");
query.equalTo("company", companyId);
索引建议:
objectId
(主键,自动索引)name
用途: 存储用户在企业内的档案信息,关联 Parse 内置 _User 表。
表结构:
字段名 | 类型 | 必填 | 说明 | 示例值 |
---|---|---|---|---|
objectId | String | 是 | 主键ID | "abc123xyz" |
name | String | 是 | 用户姓名 | "张三" |
mobile | String | 否 | 手机号 | "13800138000" |
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
: 开发者权限关系:
使用场景:
// 获取当前用户档案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
用途: 组织架构管理,支持树状结构(多层级部门)。
表结构:
字段名 | 类型 | 必填 | 说明 | 示例值 |
---|---|---|---|---|
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 |
关系:
使用场景:
// 查询企业所有部门
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
用途: 项目管理的核心表,支持多种类型项目。
表结构:
字段名 | 类型 | 必填 | 说明 | 示例值 |
---|---|---|---|---|
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
: 功能模块关系:
使用场景:
// 查询项目
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
(降序)用途: 存储项目任务,支持甘特图、看板等多种视图,支持父子任务。
表结构:
字段名 | 类型 | 必填 | 说明 | 示例值 |
---|---|---|---|---|
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
: 测试关系:
使用场景:
// 查询项目任务
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
用途: 管理项目成员关系,实现项目与用户的多对多关联。
表结构:
字段名 | 类型 | 必填 | 说明 | 示例值 |
---|---|---|---|---|
objectId | String | 是 | 主键ID | "team001" |
project | Pointer | 是 | 所属项目 | → Project |
profile | Pointer | 是 | 团队成员 | → Profile |
role | String | 否 | 成员角色 | "开发" / "测试" / "PM" |
isDeleted | Boolean | 否 | 软删除标记 | false |
createdAt | Date | 自动 | 加入时间 | 2024-01-01T00:00:00.000Z |
关系:
使用场景:
// 查询项目成员
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
(联合唯一索引)用途: 项目下的产品管理。
表结构:
字段名 | 类型 | 必填 | 说明 | 示例值 |
---|---|---|---|---|
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 |
关系:
使用场景:
// 查询项目的产品列表
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
(降序)用途: 支持模块化的需求管理,可构建树状结构(模块→子模块→需求)。
表结构:
字段名 | 类型 | 必填 | 说明 | 示例值 |
---|---|---|---|---|
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
: 具体需求(归属于某个模块)树状结构说明:
关系:
使用场景:
// 查询项目的模块列表
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
(降序)用途: 管理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列表
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
(降序)用途: 存储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 平衡计分卡维度:
财务
: 财务指标(如:营收、利润等)客户
: 客户相关指标(如:客户满意度、客户数等)内部运营
: 内部流程指标(如:交付效率、质量等)学习成长
: 学习与创新指标(如:培训时长、新技能等)三级架构说明:
关系:
使用场景:
// 查询公司级指标树
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
用途: 文档空间管理,通常与项目关联,作为项目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
: 项目空间关系:
使用场景:
// 创建项目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
用途: 存储具体的文档笔记,支持树状结构(目录层级)。
表结构:
字段名 | 类型 | 必填 | 说明 | 示例值 |
---|---|---|---|---|
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 |
关系:
使用场景:
// 查询空间的一级笔记(根目录)
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);
query.limit(pageSize);
query.skip(pageSize * (pageIndex - 1));
query.descending("createdAt"); // 降序
query.ascending("index"); // 升序
query.addDescending("priority"); // 添加额外排序
// 单层级关联
query.include("assignee", "owner");
// 多层级关联
query.include(["project", "assignee.user", "assignee.department"]);
// 只返回指定字段
query.select("title", "startDate", "assignee");
const count = await query.count();
// 带计数的查询
query.withCount();
const result = await query.find();
console.log(result.count); // 总数
console.log(result.results); // 结果列表
// 查询根节点
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)
}));
}
必建索引:
company + isDeleted
updatedAt
(降序)复合索引示例:
// 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 }
避免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();
项目中实现了简单的内存缓存:
// 检查是否需要更新数据
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 字段在应用层需要保证引用完整性:
基准工时: 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"
待分配 → 进行中 → 测试中 → 已完成 → 已上线
状态映射:
const stateColorMap = {
'待分配': 'default',
'进行中': 'processing',
'测试中': 'warning',
'已完成': 'success',
'已上线': 'gold',
};
const priorityMap = {
1: { text: '最低', color: 'default' },
50: { text: '普通', color: 'blue' },
70: { text: '较高', color: 'orange' },
90: { text: '重要', color: 'red' },
100: { text: '严重', color: '#f50' },
};
// 公司级 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 可选
const bscList = [
{ label: "财务", value: "财务" },
{ label: "客户", value: "客户" },
{ label: "内部运营", value: "内部运营" },
{ label: "学习成长", value: "学习成长" },
];
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
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"
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
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
src/modules/okr/page-okr-panel/page-okr-panel.component.ts
src/modules/okr/okr-dashboard/okr-dashboard.component.ts
序号 | 表名 | 中文名 | 用途 |
---|---|---|---|
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 | 文档笔记表 | 笔记管理 |
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