You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
15 KiB
15 KiB
Projects 表 V1/V2 设计方案
方案名称:Projects 表多态所有者与预算积分设计
创建时间:2025-01-14
版本:v1.0
1. 背景与目标
1.1 业务需求
V1 阶段(当前):
- 只支持个人用户
- 项目归属于个人
- 积分从个人账户扣除
V2 阶段(未来):
- 支持企业账号
- 项目可以归属于企业
- 企业可以为项目充值预算积分
- 个人在企业项目中工作时,使用项目预算积分
1.2 设计目标
- V1 实现简单:只处理个人用户场景,复杂度低
- V2 平滑升级:无需修改表结构,只需启用预留字段
- 扩展性强:支持未来更多所有者类型
- 性能优秀:索引优化,查询高效
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
-- 项目类型枚举
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 业务规则
-
项目创建:
owner_type固定为 'user'owner_id设置为当前用户的 user_idai_credits_budget和budget_consumed保持为 0
-
积分消耗:
- 从项目所有者的
users.ai_credits_balance扣除 - 不使用项目预算
- 从项目所有者的
-
项目查询:
- 我的项目:
owner_type='user' AND owner_id=当前用户ID - 协作项目:通过
project_members表查询
- 我的项目:
4.2 代码示例
# 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 新增功能
-
企业账号:
- 创建
organizations表 - 企业有独立的积分余额
- 创建
-
企业项目:
- 支持
owner_type='organization' - 企业可以创建项目
- 支持
-
项目预算:
- 企业可以为项目充值预算
- 启用
ai_credits_budget和budget_consumed字段
-
积分消耗优先级:
- 优先使用项目预算
- 预算不足时使用所有者积分
5.2 数据迁移
-- 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 代码示例
# 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 查询
-- 查询我的项目
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 查询
-- 查询企业的所有项目
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 索引策略
- 复合索引:
(owner_type, owner_id)支持按所有者查询 - 部分索引:只索引未删除的记录,减少索引大小
- GIN 索引:支持 JSONB 字段和全文搜索
- 预算索引:只索引有预算的项目(
ai_credits_budget > 0)
7.2 查询优化
-- 使用 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 技术优势
- 扩展性强:多态所有者模式,轻松支持新类型
- 前期简单:V1 只用个人用户,复杂度低
- 平滑升级:V2 引入企业无需重构数据
- 性能优秀:索引优化,查询高效
- 审计完整:预留字段,便于追踪
8.2 业务优势
- 灵活计费:支持个人付费和企业付费
- 预算控制:企业可以为项目设置预算上限
- 成本透明:项目消耗清晰可见
- 协作友好:个人在企业项目中工作,使用企业积分
9. 注意事项
9.1 V1 阶段
-
字段约束:
owner_type必须为 'user'ai_credits_budget和budget_consumed必须为 0
-
业务逻辑:
- 创建项目时自动设置
owner_type='user' - 积分消耗只从用户账户扣除
- 创建项目时自动设置
-
数据验证:
- 应用层校验
owner_type只能是 'user' - 数据库层使用 CHECK 约束保证数据一致性
- 应用层校验
9.2 V2 升级
-
数据迁移:
- 现有项目无需迁移(owner_type 已经是 'user')
- 只需创建 organizations 表
-
代码升级:
- 修改创建项目逻辑,支持 owner_type='organization'
- 修改积分消耗逻辑,支持项目预算优先
-
测试验证:
- 测试企业项目创建
- 测试项目预算充值
- 测试积分消耗优先级
10. 相关文档
方案版本:v1.0
最后更新:2025-01-14