# Projects 表 V1/V2 设计方案 > **方案名称**:Projects 表多态所有者与预算积分设计 > **创建时间**:2025-01-14 > **版本**:v1.0 --- ## 1. 背景与目标 ### 1.1 业务需求 **V1 阶段(当前)**: - 只支持个人用户 - 项目归属于个人 - 积分从个人账户扣除 **V2 阶段(未来)**: - 支持企业账号 - 项目可以归属于企业 - 企业可以为项目充值预算积分 - 个人在企业项目中工作时,使用项目预算积分 ### 1.2 设计目标 1. **V1 实现简单**:只处理个人用户场景,复杂度低 2. **V2 平滑升级**:无需修改表结构,只需启用预留字段 3. **扩展性强**:支持未来更多所有者类型 4. **性能优秀**:索引优化,查询高效 --- ## 2. 核心设计 ### 2.1 多态所有者模式 **设计思路**: - 使用 `owner_type` + `owner_id` 组合表示所有者 - `owner_type` 可以是 'user' 或 'organization' - `owner_id` 根据 `owner_type` 指向不同的表 **优势**: - ✅ 灵活:支持多种所有者类型 - ✅ 扩展:新增类型只需添加枚举值 - ✅ 统一:查询和权限逻辑统一处理 ### 2.2 项目预算积分 **设计思路**: - 项目可以有独立的积分预算(`ai_credits_budget`) - 记录已消耗的预算(`budget_consumed`) - 消耗优先级:项目预算 > 所有者积分 **优势**: - ✅ 灵活:企业可以为项目单独充值 - ✅ 可控:项目预算独立管理,不影响企业总积分 - ✅ 透明:消耗记录清晰,便于审计 --- ## 3. 表结构设计 ### 3.1 完整 DDL ```sql -- 项目类型枚举 CREATE TYPE project_type AS ENUM ('mine', 'collab'); -- 项目表 CREATE TABLE projects ( project_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- 基本信息 name TEXT NOT NULL, description TEXT, type project_type NOT NULL DEFAULT 'mine', -- 多态所有者(V1: user, V2: user | organization) owner_type TEXT NOT NULL DEFAULT 'user' CHECK (owner_type IN ('user', 'organization')), owner_id BIGINT NOT NULL, -- 文件夹归属 folder_id BIGINT REFERENCES folders(folder_id) ON DELETE SET NULL, -- 封面图片 thumbnail_url TEXT, cover_image_id BIGINT REFERENCES attachments(attachment_id) ON DELETE SET NULL, -- 项目预算积分(V1: 0, V2: 企业充值) ai_credits_budget INTEGER NOT NULL DEFAULT 0, budget_consumed INTEGER NOT NULL DEFAULT 0, -- 项目设置 settings JSONB NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(settings) = 'object'), -- 状态 status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived', 'deleted')), -- 时间戳 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ, -- 唯一约束 CONSTRAINT projects_name_unique UNIQUE (folder_id, name) NULLS NOT DISTINCT ); -- 索引 CREATE INDEX idx_projects_owner ON projects (owner_type, owner_id) WHERE deleted_at IS NULL; CREATE INDEX idx_projects_folder_id ON projects (folder_id) WHERE deleted_at IS NULL AND folder_id IS NOT NULL; CREATE INDEX idx_projects_type ON projects (type) WHERE deleted_at IS NULL; CREATE INDEX idx_projects_status ON projects (status) WHERE deleted_at IS NULL; CREATE INDEX idx_projects_created_at ON projects (created_at) WHERE deleted_at IS NULL; CREATE INDEX idx_projects_updated_at ON projects (updated_at) WHERE deleted_at IS NULL; CREATE INDEX idx_projects_cover_image_id ON projects (cover_image_id) WHERE cover_image_id IS NOT NULL; CREATE INDEX idx_projects_settings_gin ON projects USING GIN (settings); CREATE INDEX idx_projects_name_trgm ON projects USING GIN (name gin_trgm_ops) WHERE deleted_at IS NULL; CREATE INDEX idx_projects_budget ON projects (ai_credits_budget) WHERE deleted_at IS NULL AND ai_credits_budget > 0; -- 注释 COMMENT ON TABLE projects IS '项目表,支持个人和企业项目'; COMMENT ON COLUMN projects.owner_type IS 'V1: 固定为 user, V2: 支持 organization'; COMMENT ON COLUMN projects.owner_id IS '所有者ID,根据 owner_type 指向 users.user_id 或 organizations.organization_id'; COMMENT ON COLUMN projects.ai_credits_budget IS 'V1: 保持为 0, V2: 企业为项目充值的专属积分预算'; COMMENT ON COLUMN projects.budget_consumed IS 'V1: 保持为 0, V2: 已消耗的项目预算积分'; -- 触发器 CREATE TRIGGER update_projects_updated_at BEFORE UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` ### 3.2 字段说明 | 字段名 | 类型 | 说明 | V1 使用 | V2 使用 | | ------------------- | ----------- | ----------------------- | ---------------- | -------------------------------- | | `project_id` | BIGINT | 主键 | ✅ | ✅ | | `name` | TEXT | 项目名称 | ✅ | ✅ | | `description` | TEXT | 项目描述 | ✅ | ✅ | | `type` | ENUM | 项目类型(mine/collab) | ✅ | ✅ | | `owner_type` | TEXT | 所有者类型 | ✅ (固定 'user') | ✅ ('user' 或 'organization') | | `owner_id` | BIGINT | 所有者ID | ✅ (指向 users) | ✅ (指向 users 或 organizations) | | `folder_id` | BIGINT | 文件夹ID | ✅ | ✅ | | `thumbnail_url` | TEXT | 缩略图URL | ✅ | ✅ | | `cover_image_id` | BIGINT | 封面图片ID | ✅ | ✅ | | `ai_credits_budget` | INTEGER | 项目预算积分 | ❌ (保持 0) | ✅ (企业充值) | | `budget_consumed` | INTEGER | 已消耗预算 | ❌ (保持 0) | ✅ (记录消耗) | | `settings` | JSONB | 项目设置 | ✅ | ✅ | | `status` | TEXT | 项目状态 | ✅ | ✅ | | `created_at` | TIMESTAMPTZ | 创建时间 | ✅ | ✅ | | `updated_at` | TIMESTAMPTZ | 更新时间 | ✅ | ✅ | | `deleted_at` | TIMESTAMPTZ | 删除时间 | ✅ | ✅ | --- ## 4. V1 实现(当前) ### 4.1 业务规则 1. **项目创建**: - `owner_type` 固定为 'user' - `owner_id` 设置为当前用户的 user_id - `ai_credits_budget` 和 `budget_consumed` 保持为 0 2. **积分消耗**: - 从项目所有者的 `users.ai_credits_balance` 扣除 - 不使用项目预算 3. **项目查询**: - 我的项目:`owner_type='user' AND owner_id=当前用户ID` - 协作项目:通过 `project_members` 表查询 ### 4.2 代码示例 ```python # V1: 创建项目 async def create_project(self, user_id: int, data: dict) -> Project: project = Project( name=data['name'], description=data.get('description'), type=data.get('type', 'mine'), owner_type='user', # V1 固定为 'user' owner_id=user_id, folder_id=data.get('folder_id'), ai_credits_budget=0, # V1 不使用 budget_consumed=0, # V1 不使用 settings=data.get('settings', {}) ) return await self.repository.create(project) # V1: 消耗积分 async def consume_credits(self, project_id: int, amount: int) -> bool: project = await self.repository.get_by_id(project_id) # V1: 只从用户积分扣除 if project.owner_type == 'user': user = await self.user_repo.get_by_id(project.owner_id) if user.ai_credits_balance < amount: raise InsufficientCreditsError("积分不足") await self.user_repo.update(project.owner_id, { 'ai_credits_balance': user.ai_credits_balance - amount, 'total_credits_consumed': user.total_credits_consumed + amount }) return True ``` --- ## 5. V2 升级(未来) ### 5.1 新增功能 1. **企业账号**: - 创建 `organizations` 表 - 企业有独立的积分余额 2. **企业项目**: - 支持 `owner_type='organization'` - 企业可以创建项目 3. **项目预算**: - 企业可以为项目充值预算 - 启用 `ai_credits_budget` 和 `budget_consumed` 字段 4. **积分消耗优先级**: - 优先使用项目预算 - 预算不足时使用所有者积分 ### 5.2 数据迁移 ```sql -- V2 升级:无需修改 projects 表结构 -- 只需创建 organizations 表 CREATE TABLE organizations ( organization_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, display_name TEXT, created_by BIGINT NOT NULL REFERENCES users(user_id), ai_credits_balance INTEGER NOT NULL DEFAULT 0, total_recharged_amount NUMERIC(10, 2) NOT NULL DEFAULT 0.00, total_credits_earned INTEGER NOT NULL DEFAULT 0, total_credits_consumed INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ ); -- 现有项目数据无需迁移,owner_type 已经是 'user' ``` ### 5.3 代码示例 ```python # V2: 消耗积分(支持项目预算) async def consume_credits(self, project_id: int, amount: int) -> bool: project = await self.repository.get_by_id(project_id) # 优先使用项目预算 if project.ai_credits_budget >= amount: await self.repository.update(project_id, { 'ai_credits_budget': project.ai_credits_budget - amount, 'budget_consumed': project.budget_consumed + amount }) return True # 项目预算不足,使用所有者积分 if project.owner_type == 'user': user = await self.user_repo.get_by_id(project.owner_id) if user.ai_credits_balance < amount: raise InsufficientCreditsError("积分不足") await self.user_repo.update(project.owner_id, { 'ai_credits_balance': user.ai_credits_balance - amount, 'total_credits_consumed': user.total_credits_consumed + amount }) elif project.owner_type == 'organization': org = await self.org_repo.get_by_id(project.owner_id) if org.ai_credits_balance < amount: raise InsufficientCreditsError("企业积分不足") await self.org_repo.update(project.owner_id, { 'ai_credits_balance': org.ai_credits_balance - amount, 'total_credits_consumed': org.total_credits_consumed + amount }) return True # V2: 为项目充值预算 async def recharge_project_budget( self, project_id: int, amount: int, user_id: int ) -> bool: project = await self.repository.get_by_id(project_id) # 只有企业项目支持预算充值 if project.owner_type != 'organization': raise ValidationError("只有企业项目支持预算充值") # 从企业积分转移到项目预算 org = await self.org_repo.get_by_id(project.owner_id) if org.ai_credits_balance < amount: raise InsufficientCreditsError("企业积分不足") # 扣除企业积分 await self.org_repo.update(project.owner_id, { 'ai_credits_balance': org.ai_credits_balance - amount }) # 增加项目预算 await self.repository.update(project_id, { 'ai_credits_budget': project.ai_credits_budget + amount }) return True ``` --- ## 6. 查询示例 ### 6.1 V1 查询 ```sql -- 查询我的项目 SELECT * FROM projects WHERE owner_type = 'user' AND owner_id = $current_user_id AND deleted_at IS NULL ORDER BY created_at DESC; -- 查询协作项目 SELECT p.* FROM projects p INNER JOIN project_members pm ON p.project_id = pm.project_id WHERE pm.user_id = $current_user_id AND pm.role != 'owner' AND p.deleted_at IS NULL ORDER BY p.created_at DESC; ``` ### 6.2 V2 查询 ```sql -- 查询企业的所有项目 SELECT * FROM projects WHERE owner_type = 'organization' AND owner_id = $organization_id AND deleted_at IS NULL ORDER BY created_at DESC; -- 查询有预算的项目 SELECT * FROM projects WHERE ai_credits_budget > 0 AND deleted_at IS NULL ORDER BY ai_credits_budget DESC; -- 查询预算即将用完的项目(剩余 < 10%) SELECT project_id, name, ai_credits_budget, budget_consumed, (ai_credits_budget - budget_consumed) AS remaining, ROUND((budget_consumed::NUMERIC / NULLIF(ai_credits_budget, 0)) * 100, 2) AS usage_percent FROM projects WHERE ai_credits_budget > 0 AND deleted_at IS NULL AND (budget_consumed::NUMERIC / NULLIF(ai_credits_budget, 0)) > 0.9 ORDER BY usage_percent DESC; ``` --- ## 7. 性能优化 ### 7.1 索引策略 1. **复合索引**:`(owner_type, owner_id)` 支持按所有者查询 2. **部分索引**:只索引未删除的记录,减少索引大小 3. **GIN 索引**:支持 JSONB 字段和全文搜索 4. **预算索引**:只索引有预算的项目(`ai_credits_budget > 0`) ### 7.2 查询优化 ```sql -- 使用 EXPLAIN ANALYZE 分析查询性能 EXPLAIN ANALYZE SELECT * FROM projects WHERE owner_type = 'user' AND owner_id = 123 AND deleted_at IS NULL; -- 预期使用索引:idx_projects_owner ``` --- ## 8. 优势总结 ### 8.1 技术优势 1. **扩展性强**:多态所有者模式,轻松支持新类型 2. **前期简单**:V1 只用个人用户,复杂度低 3. **平滑升级**:V2 引入企业无需重构数据 4. **性能优秀**:索引优化,查询高效 5. **审计完整**:预留字段,便于追踪 ### 8.2 业务优势 1. **灵活计费**:支持个人付费和企业付费 2. **预算控制**:企业可以为项目设置预算上限 3. **成本透明**:项目消耗清晰可见 4. **协作友好**:个人在企业项目中工作,使用企业积分 --- ## 9. 注意事项 ### 9.1 V1 阶段 1. **字段约束**: - `owner_type` 必须为 'user' - `ai_credits_budget` 和 `budget_consumed` 必须为 0 2. **业务逻辑**: - 创建项目时自动设置 `owner_type='user'` - 积分消耗只从用户账户扣除 3. **数据验证**: - 应用层校验 `owner_type` 只能是 'user' - 数据库层使用 CHECK 约束保证数据一致性 ### 9.2 V2 升级 1. **数据迁移**: - 现有项目无需迁移(owner_type 已经是 'user') - 只需创建 organizations 表 2. **代码升级**: - 修改创建项目逻辑,支持 owner_type='organization' - 修改积分消耗逻辑,支持项目预算优先 3. **测试验证**: - 测试企业项目创建 - 测试项目预算充值 - 测试积分消耗优先级 --- ## 10. 相关文档 - [项目管理服务](../需求/backend/04-services/project/project-service.md) - [用户管理服务](../需求/backend/04-services/user/user-service.md) - [数据库设计](../需求/backend/04-database-design.md) --- **方案版本**:v1.0 **最后更新**:2025-01-14