|
@@ -0,0 +1,1552 @@
|
|
|
+---
|
|
|
+category: schema
|
|
|
+title: Parse Server 数据范式文档
|
|
|
+subtitle: 项目管理系统数据表结构
|
|
|
+name: 'project-schema'
|
|
|
+label: database
|
|
|
+---
|
|
|
+
|
|
|
+# Parse Server 数据范式 - 项目管理系统
|
|
|
+
|
|
|
+## 概述
|
|
|
+
|
|
|
+本文档详细描述了 nova-project 项目管理系统中使用的 Parse Server 数据表结构,包括表结构、字段说明、关系映射和使用场景。
|
|
|
+
|
|
|
+系统采用**多租户架构**,以 Company 为核心,支持项目管理、任务管理、产品需求管理、OKR目标管理和知识文档管理。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 数据表关系图
|
|
|
+
|
|
|
+### 整体架构(PlantUML)
|
|
|
+
|
|
|
+```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周期)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 获取当前企业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)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 获取当前用户档案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)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 查询企业所有部门
|
|
|
+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)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 查询项目
|
|
|
+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, 父子任务)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 查询项目任务
|
|
|
+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)**:
|
|
|
+```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 的多对多关系
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 查询项目成员
|
|
|
+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(产品需求/模块)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 查询项目的产品列表
|
|
|
+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)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 查询项目的模块列表
|
|
|
+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对象/目标)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 查询企业的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, 指标树层级)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 查询公司级指标树
|
|
|
+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);
|
|
|
+```
|
|
|
+
|
|
|
+**创建目标示例**:
|
|
|
+```typescript
|
|
|
+// 创建公司级指标树
|
|
|
+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(空间笔记)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 创建项目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, 支持多层级目录)
|
|
|
+
|
|
|
+**使用场景**:
|
|
|
+```typescript
|
|
|
+// 查询空间的一级笔记(根目录)
|
|
|
+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` 字段:
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 标准查询模式(排除已删除数据)
|
|
|
+query.notEqualTo("isDeleted", true);
|
|
|
+
|
|
|
+// 或
|
|
|
+query.equalTo("isDeleted", null);
|
|
|
+```
|
|
|
+
|
|
|
+**优势**:
|
|
|
+- 数据可恢复
|
|
|
+- 保持关联完整性
|
|
|
+- 支持审计追踪
|
|
|
+- 避免级联删除问题
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 多租户隔离
|
|
|
+
|
|
|
+系统采用**多租户架构**,所有业务表都包含 `company` 字段:
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 获取当前企业ID
|
|
|
+const companyId = localStorage.getItem("Parse/CompanyId") || localStorage.getItem("company");
|
|
|
+
|
|
|
+// 所有查询都需要加上企业隔离
|
|
|
+query.equalTo("company", companyId);
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 关键查询模式
|
|
|
+
|
|
|
+### 1. 分页查询
|
|
|
+```typescript
|
|
|
+query.limit(pageSize);
|
|
|
+query.skip(pageSize * (pageIndex - 1));
|
|
|
+```
|
|
|
+
|
|
|
+### 2. 排序
|
|
|
+```typescript
|
|
|
+query.descending("createdAt"); // 降序
|
|
|
+query.ascending("index"); // 升序
|
|
|
+query.addDescending("priority"); // 添加额外排序
|
|
|
+```
|
|
|
+
|
|
|
+### 3. 关联查询(Include)
|
|
|
+```typescript
|
|
|
+// 单层级关联
|
|
|
+query.include("assignee", "owner");
|
|
|
+
|
|
|
+// 多层级关联
|
|
|
+query.include(["project", "assignee.user", "assignee.department"]);
|
|
|
+```
|
|
|
+
|
|
|
+### 4. 字段筛选
|
|
|
+```typescript
|
|
|
+// 只返回指定字段
|
|
|
+query.select("title", "startDate", "assignee");
|
|
|
+```
|
|
|
+
|
|
|
+### 5. 计数查询
|
|
|
+```typescript
|
|
|
+const count = await query.count();
|
|
|
+
|
|
|
+// 带计数的查询
|
|
|
+query.withCount();
|
|
|
+const result = await query.find();
|
|
|
+console.log(result.count); // 总数
|
|
|
+console.log(result.results); // 结果列表
|
|
|
+```
|
|
|
+
|
|
|
+### 6. 树状结构查询
|
|
|
+```typescript
|
|
|
+// 查询根节点
|
|
|
+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 字段
|
|
|
+- 查询字段: 经常用于筛选的字段
|
|
|
+
|
|
|
+**复合索引示例**:
|
|
|
+```javascript
|
|
|
+// 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查询**:
|
|
|
+```typescript
|
|
|
+// ❌ 不好的做法
|
|
|
+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次查询
|
|
|
+```
|
|
|
+
|
|
|
+**批量操作**:
|
|
|
+```typescript
|
|
|
+// 批量保存
|
|
|
+await Parse.Object.saveAll(objects);
|
|
|
+
|
|
|
+// 批量删除
|
|
|
+await Parse.Object.destroyAll(objects);
|
|
|
+```
|
|
|
+
|
|
|
+**使用 select 限制字段**:
|
|
|
+```typescript
|
|
|
+// ❌ 返回所有字段
|
|
|
+const tasks = await query.find();
|
|
|
+
|
|
|
+// ✅ 只返回需要的字段
|
|
|
+query.select("title", "startDate", "stateList");
|
|
|
+const tasks = await query.find();
|
|
|
+```
|
|
|
+
|
|
|
+### 3. 缓存策略
|
|
|
+
|
|
|
+项目中实现了简单的内存缓存:
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 检查是否需要更新数据
|
|
|
+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小时/月
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ProjectTask 字段
|
|
|
+duration: Number // 实际工时(小时)
|
|
|
+estimate: Number // 预估工时(小时)
|
|
|
+```
|
|
|
+
|
|
|
+**工时统计SQL**:
|
|
|
+```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. 任务状态流转
|
|
|
+
|
|
|
+```
|
|
|
+待分配 → 进行中 → 测试中 → 已完成 → 已上线
|
|
|
+```
|
|
|
+
|
|
|
+**状态映射**:
|
|
|
+```typescript
|
|
|
+const stateColorMap = {
|
|
|
+ '待分配': 'default',
|
|
|
+ '进行中': 'processing',
|
|
|
+ '测试中': 'warning',
|
|
|
+ '已完成': 'success',
|
|
|
+ '已上线': 'gold',
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### 3. 优先级映射
|
|
|
+
|
|
|
+```typescript
|
|
|
+const priorityMap = {
|
|
|
+ 1: { text: '最低', color: 'default' },
|
|
|
+ 50: { text: '普通', color: 'blue' },
|
|
|
+ 70: { text: '较高', color: 'orange' },
|
|
|
+ 90: { text: '重要', color: 'red' },
|
|
|
+ 100: { text: '严重', color: '#f50' },
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### 4. OKR 三级架构
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 公司级 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)
|
|
|
+
|
|
|
+```typescript
|
|
|
+const bscList = [
|
|
|
+ { label: "财务", value: "财务" },
|
|
|
+ { label: "客户", value: "客户" },
|
|
|
+ { label: "内部运营", value: "内部运营" },
|
|
|
+ { label: "学习成长", value: "学习成长" },
|
|
|
+];
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## SQL 查询示例
|
|
|
+
|
|
|
+### 1. 用户参与项目统计
|
|
|
+
|
|
|
+```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" = $1 OR task."owner" = $1)
|
|
|
+ AND task."project" IS NOT NULL
|
|
|
+ORDER BY pro."updatedAt" DESC
|
|
|
+```
|
|
|
+
|
|
|
+### 2. 笔记层级结构查询
|
|
|
+
|
|
|
+```sql
|
|
|
+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. 月度工时统计
|
|
|
+
|
|
|
+```sql
|
|
|
+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
|