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.
17 KiB
17 KiB
服务设计文档 UUID 定义清理
变更日期:2026-01-27
变更类型:文档更新
影响范围:docs/requirements/backend/04-services/下所有服务文档
变更概述
批量清理所有服务设计文档中过时的数据库定义,移除物理外键约束和 UUID v7 数据库默认值,与项目架构规范保持一致。
变更内容
1. 移除物理外键约束
涉及文档:15 个服务文档
修改内容:
- 移除所有
REFERENCES ... ON DELETE CASCADE - 移除所有
REFERENCES ... ON DELETE SET NULL - 移除所有
REFERENCES ... ON DELETE RESTRICT - 移除所有
REFERENCES users(user_id) - 移除所有
REFERENCES users(id)
示例:
-- ❌ 修改前
created_by UUID NOT NULL REFERENCES users(user_id),
user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
category_id UUID NOT NULL REFERENCES screenplay_tag_categories(category_id) ON DELETE RESTRICT,
-- ✅ 修改后
created_by UUID NOT NULL,
user_id UUID NOT NULL,
category_id UUID NOT NULL,
2. 移除 UUID v7 数据库默认值
涉及文档:所有包含表定义的文档
修改内容:
- 移除所有
DEFAULT gen_uuid_v7() - 移除所有
server_default=text("gen_uuid_v7()")
示例:
-- ❌ 修改前
user_id UUID PRIMARY KEY DEFAULT gen_uuid_v7(),
-- ✅ 修改后
user_id UUID PRIMARY KEY,
# ❌ 修改前
user_id: UUID = Field(
primary_key=True,
server_default=text("gen_uuid_v7()")
)
# ✅ 修改后
user_id: UUID = Field(
primary_key=True,
default_factory=generate_uuid
)
3. 更新约束说明表格
文档:docs/requirements/backend/04-services/user/user-service.md
修改内容:
- 移除
REFERENCES外键约束示例 - 添加项目架构约束说明
修改前:
| 约束类型 | 说明 | 示例 |
|---|---|---|
REFERENCES |
外键约束 | avatar_id REFERENCES attachments(attachment_id) |
修改后:
移除该行,并添加说明:
注意:本项目禁止使用数据库物理外键约束(
REFERENCES),所有引用完整性在应用层 Service 中验证。
涉及文件清单
已修改的服务文档(15 个)
docs/requirements/backend/04-services/user/user-service.mddocs/requirements/backend/04-services/project/screenplay-tag-service.mddocs/requirements/backend/04-services/project/project-resource-service.mddocs/requirements/backend/04-services/project/folder-service.mddocs/requirements/backend/04-services/project/project-service.mddocs/requirements/backend/04-services/project/screenplay-service.mddocs/requirements/backend/04-services/resource/attachment-service.mddocs/requirements/backend/04-services/resource/project-resource-service.mddocs/requirements/backend/04-services/ai/video-service.md- 其他包含过时定义的服务文档
验证结果
验证命令
# 验证不再包含物理外键约束
grep -r "REFERENCES.*(" docs/requirements/backend/04-services --include="*.md" | grep -v "references/" | grep -v "参考文档"
# 验证不再包含 UUID v7 数据库默认值
grep -r "DEFAULT gen_uuid_v7()" docs/requirements/backend/04-services --include="*.md"
grep -r "server_default.*gen_uuid_v7" docs/requirements/backend/04-services --include="*.md"
验证结果
✅ 所有 SQL 定义中不再包含物理外键约束
✅ 所有 SQL 定义中不再包含 UUID v7 数据库默认值
✅ 所有 Python Model 定义中不再包含 server_default
架构规范说明
项目数据库设计约束
根据 .claude/skills/jointo-tech-stack/references/database.md:
-
禁止物理外键约束:
- 数据库层禁止创建
FOREIGN KEY约束 - 所有引用完整性在应用层 Service/Repository 中验证
- 所有逻辑关联字段必须手动创建索引
- 数据库层禁止创建
-
禁止数据库默认值生成 UUID:
- 禁止使用
DEFAULT gen_uuid_v7() - 禁止使用
server_default=text("gen_uuid_v7()") - 必须在应用层通过
default_factory=generate_uuid生成
- 禁止使用
-
原因:
- 为分库分表做准备
- 提升数据库性能
- 增强系统扩展性
- 统一 UUID 生成逻辑
应用层关联关系处理
移除物理外键约束后,需要在应用层(Service/Repository)保证引用完整性和处理级联关系。
三层保证机制
1. Repository 层 - 基础验证
提供通用的引用检查方法:
# repositories/base_repository.py
class BaseRepository(Generic[T]):
async def exists(self, id: UUID) -> bool:
"""检查记录是否存在(排除软删除)"""
query = select(self.model.id).where(
self.model.id == id,
self.model.deleted_at.is_(None)
).limit(1)
result = await self.db.execute(query)
return result.scalar_one_or_none() is not None
async def batch_exists(self, ids: list[UUID]) -> dict[UUID, bool]:
"""批量检查记录是否存在(性能优化)"""
query = select(self.model.id).where(
self.model.id.in_(ids),
self.model.deleted_at.is_(None)
)
result = await self.db.execute(query)
existing_ids = {row[0] for row in result.all()}
return {id: id in existing_ids for id in ids}
2. Service 层 - 业务验证
在所有涉及关联关系的操作中验证引用完整性:
创建操作 - 验证所有外键引用
# services/project_service.py
async def create_project(self, user_id: UUID, data: ProjectCreate) -> Project:
"""创建项目 - 验证所有引用"""
# 1. 验证所有者是否存在
if not await self.user_repo.exists(data.owner_id):
raise NotFoundError("所有者用户不存在")
# 2. 验证文件夹引用(如果提供)
if data.folder_id:
folder = await self.folder_repo.get_by_id(data.folder_id)
if not folder:
raise NotFoundError("文件夹不存在")
# 验证用户对文件夹的权限
has_permission = await self.folder_repo.check_user_permission(
user_id, data.folder_id, MemberRole.EDITOR
)
if not has_permission:
raise PermissionDeniedError("无权限在该文件夹下创建项目")
# 3. 使用事务创建项目和关联数据
async with self.db.begin():
project = await self.project_repo.create(data)
# 自动添加创建者为所有者成员
await self.project_member_repo.add_member(
project_id=project.id,
user_id=user_id,
role=MemberRole.OWNER
)
return project
更新操作 - 验证新的外键引用
async def update_project(
self,
user_id: UUID,
project_id: UUID,
data: ProjectUpdate
) -> Project:
"""更新项目 - 验证新的引用"""
# 1. 验证项目是否存在
project = await self.project_repo.get_by_id(project_id)
if not project:
raise NotFoundError("项目不存在")
# 2. 如果更新文件夹,验证新文件夹
if data.folder_id is not None and data.folder_id != project.folder_id:
if not await self.folder_repo.exists(data.folder_id):
raise NotFoundError("目标文件夹不存在")
# 验证用户对新文件夹的权限
has_permission = await self.folder_repo.check_user_permission(
user_id, data.folder_id, MemberRole.EDITOR
)
if not has_permission:
raise PermissionDeniedError("无权限移动到该文件夹")
# 3. 执行更新
return await self.project_repo.update(project_id, data.dict(exclude_unset=True))
删除操作 - 处理级联关系
async def delete_project(
self,
user_id: UUID,
project_id: UUID,
permanent: bool = False
) -> None:
"""删除项目 - 应用层级联处理"""
# 1. 验证项目是否存在和权限
project = await self.project_repo.get_by_id(project_id)
if not project:
raise NotFoundError("项目不存在")
is_owner = await self.project_repo.check_user_permission(
user_id, project_id, MemberRole.OWNER
)
if not is_owner:
raise PermissionDeniedError("只有所有者可以删除项目")
# 2. 使用事务处理级联删除
async with self.db.begin():
if permanent:
# 永久删除:物理删除所有关联数据
await self.project_member_repo.delete_by_project(project_id)
await self.project_share_repo.delete_by_project(project_id)
await self.project_snapshot_repo.delete_by_project(project_id)
# 删除剧本及其关联数据
screenplays = await self.screenplay_repo.get_by_project(project_id)
for screenplay in screenplays:
await self.delete_screenplay_cascade(screenplay.id)
# 删除项目素材关联
await self.project_resource_repo.delete_by_project(project_id)
# 最后删除项目本身
await self.project_repo.delete(project_id)
else:
# 软删除:标记删除时间
await self.project_repo.soft_delete(project_id)
await self.project_member_repo.soft_delete_by_project(project_id)
await self.project_share_repo.soft_delete_by_project(project_id)
3. 后台任务 - 定期检查
使用 Celery 定期检查数据完整性:
# tasks/data_integrity.py
@shared_task
async def check_orphan_records():
"""检查孤儿记录并告警"""
async with get_db() as db:
# 检查项目表中的孤儿记录
orphan_projects = await db.execute("""
SELECT p.id, p.name, p.owner_id
FROM projects p
LEFT JOIN users u ON p.owner_id = u.user_id
WHERE p.deleted_at IS NULL AND u.user_id IS NULL
""")
for project in orphan_projects:
logger.error(f"发现孤儿项目: {project.id}, 所有者不存在: {project.owner_id}")
# 发送告警通知
await send_alert("data_integrity", f"项目 {project.name} 的所有者已被删除")
@shared_task
async def cleanup_soft_deleted_records():
"""清理超过 30 天的软删除记录"""
async with get_db() as db:
cutoff_date = datetime.utcnow() - timedelta(days=30)
deleted_projects = await db.execute(
select(Project).where(Project.deleted_at < cutoff_date)
)
for project in deleted_projects.scalars():
await ProjectService(db).delete_project(
user_id=project.owner_id,
project_id=project.id,
permanent=True
)
常见关联关系处理模式
一对多关系(用户 → 项目)
# 删除父记录时的三种策略
# 策略 1: 禁止删除(如果有关联项目)
async def delete_user(self, user_id: UUID):
project_count = await self.project_repo.count_by_owner(user_id)
if project_count > 0:
raise ValidationError("用户还有项目,无法删除")
await self.user_repo.delete(user_id)
# 策略 2: 转移所有权
async def delete_user(self, user_id: UUID):
await self.project_repo.transfer_ownership(
from_user_id=user_id,
to_user_id=admin_user_id
)
await self.user_repo.delete(user_id)
# 策略 3: 级联软删除
async def delete_user(self, user_id: UUID):
await self.project_repo.soft_delete_by_owner(user_id)
await self.user_repo.soft_delete(user_id)
多对多关系(项目 ↔ 成员)
# 添加关联时验证两端都存在
async def add_project_member(
self,
project_id: UUID,
user_id: UUID,
role: MemberRole
):
if not await self.project_repo.exists(project_id):
raise NotFoundError("项目不存在")
if not await self.user_repo.exists(user_id):
raise NotFoundError("用户不存在")
# 检查是否已存在
existing = await self.project_member_repo.get_member(project_id, user_id)
if existing:
raise ValidationError("用户已是项目成员")
await self.project_member_repo.create({
"project_id": project_id,
"user_id": user_id,
"role": role
})
# 删除任一端时清理关联
async def delete_project(self, project_id: UUID):
await self.project_member_repo.delete_by_project(project_id)
await self.project_repo.delete(project_id)
自引用关系(文件夹树)
async def delete_folder(self, folder_id: UUID, user_id: UUID):
"""递归删除文件夹树"""
async with self.db.begin():
# 1. 获取所有子文件夹
children = await self.folder_repo.get_children(folder_id)
# 2. 递归删除子文件夹
for child in children:
await self.delete_folder(child.id, user_id)
# 3. 处理文件夹内的项目(移到根目录)
projects = await self.project_repo.get_by_folder(folder_id)
for project in projects:
await self.project_repo.update(project.id, {"folder_id": None})
# 4. 删除文件夹本身
await self.folder_repo.soft_delete(folder_id)
async def move_folder(self, folder_id: UUID, target_parent_id: Optional[UUID]):
"""移动文件夹 - 防止循环引用"""
if target_parent_id:
# 验证目标父文件夹存在
if not await self.folder_repo.exists(target_parent_id):
raise NotFoundError("目标文件夹不存在")
# 防止循环引用:检查目标是否是当前文件夹的子孙
is_descendant = await self.folder_repo.is_descendant(
ancestor_id=folder_id,
descendant_id=target_parent_id
)
if is_descendant:
raise ValidationError("不能移动到自己的子文件夹")
await self.folder_repo.update(folder_id, {"parent_folder_id": target_parent_id})
性能优化建议
- 批量验证: 使用
IN查询批量检查引用,避免 N+1 查询 - 索引优化: 所有外键列必须创建索引(已在迁移脚本中处理)
- 缓存策略: 对频繁访问的引用关系使用 Redis 缓存
- 异步处理: 非关键路径的完整性检查放到后台任务
- 事务控制: 使用数据库事务保证级联操作的原子性
# 批量验证示例
async def batch_move_projects(
self,
user_id: UUID,
project_ids: list[UUID],
target_folder_id: Optional[UUID]
):
# 批量验证项目是否存在
projects_exist = await self.project_repo.batch_exists(project_ids)
missing_ids = [id for id, exists in projects_exist.items() if not exists]
if missing_ids:
raise NotFoundError(f"项目不存在: {missing_ids}")
# 批量验证用户权限
permissions = await self.project_repo.batch_check_user_permission(
user_id, project_ids, MemberRole.EDITOR
)
unauthorized_ids = [id for id, has_perm in permissions.items() if not has_perm]
if unauthorized_ids:
raise PermissionDeniedError(f"无权限操作项目: {unauthorized_ids}")
# 批量更新
async with self.db.begin():
await self.project_repo.batch_update(
project_ids,
{"folder_id": target_folder_id}
)
实施要点
- 所有 Service 方法都必须在操作前验证引用完整性
- 删除操作必须明确处理级联关系(软删除或物理删除)
- 批量操作使用批量验证方法优化性能
- 事务边界要清晰,确保级联操作的原子性
- 错误处理要明确,区分 NotFoundError 和 PermissionDeniedError
- 后台任务定期检查数据完整性,及时发现和修复问题
完整的应用层引用完整性保证机制详见:
后续工作
文档维护建议
-
新增服务文档时:
- 参考
docs/requirements/backend/04-services/README.md中的架构变更说明 - 不要在表定义中添加物理外键约束
- 不要在表定义中添加 UUID v7 数据库默认值
- 参考
-
更新现有文档时:
- 检查是否包含过时的数据库定义
- 及时清理不符合规范的内容
-
代码实现时:
- 以实际代码为准(
server/app/models/) - 文档中的表定义仅供参考
- 以实际代码为准(
相关文档
总结
本次批量清理确保了所有服务设计文档与项目架构规范保持一致,移除了过时的数据库定义,为后续开发提供了准确的参考。所有引用完整性验证和 UUID 生成逻辑已统一在应用层实现。