--- 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