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.
11 KiB
11 KiB
项目素材引用计数维护实现
日期:2026-02-04
类型:功能实现
影响范围:后端服务层
变更概述
在 StoryboardService 中实现项目素材引用计数(usage_count)的自动维护逻辑,确保素材被分镜使用时计数准确更新。
变更详情
1. 修改 StoryboardService
文件:server/app/services/storyboard_service.py
1.1 添加导入
from sqlalchemy import update
from app.models.project_resource import ProjectResource
1.2 修改 add_element_to_storyboard() 方法
在添加项目素材到分镜时,自动增加引用计数:
async def add_element_to_storyboard(self, ...):
# ... 创建关联记录 ...
created_item = await self.storyboard_repo.create_item(item)
# 如果是项目素材,增加引用计数
if item_type == ItemType.RESOURCE and resource_id:
await self.session.execute(
update(ProjectResource)
.where(ProjectResource.project_resource_id == resource_id)
.values(usage_count=ProjectResource.usage_count + 1)
)
logger.debug(
"项目素材引用计数 +1: resource_id=%s",
resource_id
)
await self.session.commit()
# ...
1.3 修改 remove_element_from_storyboard() 方法
在从分镜移除项目素材时,自动减少引用计数:
async def remove_element_from_storyboard(self, ...):
# ... 获取元素信息 ...
# 如果是项目素材,减少引用计数
if item.item_type == ItemType.RESOURCE and item.resource_id:
await self.session.execute(
update(ProjectResource)
.where(ProjectResource.project_resource_id == item.resource_id)
.values(usage_count=ProjectResource.usage_count - 1)
)
logger.debug(
"项目素材引用计数 -1: resource_id=%s",
item.resource_id
)
await self.storyboard_repo.delete_item(item_id)
await self.session.commit()
# ...
实现特点
1. 事务安全
引用计数更新和关联记录的创建/删除在同一事务中完成:
# 创建关联记录
created_item = await self.storyboard_repo.create_item(item)
# 更新引用计数
await self.session.execute(update(...))
# 一起提交
await self.session.commit()
优点:
- 确保数据一致性
- 失败时自动回滚
- 避免计数不准确
2. 仅对项目素材生效
只有当元素类型为 ItemType.RESOURCE 时才更新计数:
if item_type == ItemType.RESOURCE and resource_id:
# 更新引用计数
原因:
ItemType.ELEMENT_TAG(剧本元素标签)不需要引用计数- 避免不必要的数据库操作
3. 日志记录
使用 DEBUG 级别记录引用计数变化:
logger.debug(
"项目素材引用计数 +1: resource_id=%s",
resource_id
)
用途:
- 便于调试
- 审计追踪
- 问题排查
数据库字段
project_resources.usage_count
| 属性 | 值 |
|---|---|
| 类型 | INTEGER |
| 默认值 | 0 |
| 约束 | NOT NULL, CHECK (usage_count >= 0) |
| 索引 | idx_project_resources_usage_count |
| 说明 | 被多少个分镜使用 |
维护规则:
- 添加素材到分镜:
usage_count+1 - 从分镜移除素材:
usage_count-1 - 软删除分镜:不影响
usage_count - 硬删除分镜:批量减少所有关联素材的
usage_count
使用场景
场景 1:添加素材到分镜
POST /api/v1/storyboards/{storyboard_id}/items
Content-Type: application/json
{
"item_type": 2,
"resource_id": "019d1234-5678-7abc-def0-123456789abc"
}
执行流程:
- 验证分镜存在
- 验证项目权限
- 创建
storyboard_items记录 - 执行
UPDATE project_resources SET usage_count = usage_count + 1 - 提交事务
结果:
- 素材成功关联到分镜
usage_count从 0 变为 1
场景 2:从分镜移除素材
DELETE /api/v1/storyboards/{storyboard_id}/items/{item_id}
执行流程:
- 获取元素信息
- 验证项目权限
- 执行
UPDATE project_resources SET usage_count = usage_count - 1 - 删除
storyboard_items记录 - 提交事务
结果:
- 素材从分镜移除
usage_count从 1 变为 0
场景 3:删除保护(待实现)
当 usage_count > 0 时,禁止删除素材:
# ProjectResourceService.delete_resource()
if resource.usage_count > 0 and not force:
raise ValidationError(
"该素材正在被 %d 个分镜使用,无法删除。" % resource.usage_count
)
技术栈符合性
✅ 完全符合 jointo-tech-stack 规范:
-
事务管理
- 使用
async with self.session.begin()或手动commit() - 关联操作和计数更新在同一事务
- 使用
-
日志系统
- 使用标准库
logging - 使用 %-formatting 格式化
- DEBUG 级别记录计数变化
- 使用标准库
-
异步编程
- 所有数据库操作使用
async/await - 使用
AsyncSession
- 所有数据库操作使用
-
应用层约束
- 无物理外键
- 应用层保证引用完整性
测试建议
单元测试
async def test_add_element_increases_usage_count():
"""测试添加素材到分镜时增加引用计数"""
# 创建项目素材
resource = await create_project_resource(usage_count=0)
# 添加到分镜
await service.add_element_to_storyboard(
user_id=user_id,
storyboard_id=storyboard_id,
item_type=ItemType.RESOURCE,
resource_id=resource.project_resource_id
)
# 验证引用计数
updated_resource = await repo.get_by_id(resource.project_resource_id)
assert updated_resource.usage_count == 1
async def test_remove_element_decreases_usage_count():
"""测试从分镜移除素材时减少引用计数"""
# 创建项目素材并添加到分镜
resource = await create_project_resource(usage_count=1)
item = await create_storyboard_item(resource_id=resource.project_resource_id)
# 从分镜移除
await service.remove_element_from_storyboard(
user_id=user_id,
item_id=item.item_id
)
# 验证引用计数
updated_resource = await repo.get_by_id(resource.project_resource_id)
assert updated_resource.usage_count == 0
async def test_usage_count_transaction_rollback():
"""测试事务回滚时引用计数不变"""
resource = await create_project_resource(usage_count=0)
# 模拟添加失败
with pytest.raises(Exception):
await service.add_element_to_storyboard(
user_id=user_id,
storyboard_id=invalid_storyboard_id, # 不存在的分镜
item_type=ItemType.RESOURCE,
resource_id=resource.project_resource_id
)
# 验证引用计数未变化
resource = await repo.get_by_id(resource.project_resource_id)
assert resource.usage_count == 0
集成测试
async def test_usage_count_api_integration(client):
"""测试通过 API 操作时引用计数正确更新"""
# 创建项目素材
resource = await create_project_resource()
# 通过 API 添加到分镜
response = await client.post(
f"/api/v1/storyboards/{storyboard_id}/items",
json={
"item_type": 2,
"resource_id": str(resource.project_resource_id)
}
)
assert response.status_code == 200
# 验证引用计数
resource = await repo.get_by_id(resource.project_resource_id)
assert resource.usage_count == 1
# 通过 API 移除
item_id = response.json()["data"]["item_id"]
response = await client.delete(
f"/api/v1/storyboards/{storyboard_id}/items/{item_id}"
)
assert response.status_code == 200
# 验证引用计数
resource = await repo.get_by_id(resource.project_resource_id)
assert resource.usage_count == 0
后续优化
1. 批量操作优化
当批量添加/移除素材时,使用单个 SQL 语句更新:
# 批量增加引用计数
await self.session.execute(
update(ProjectResource)
.where(ProjectResource.project_resource_id.in_(resource_ids))
.values(usage_count=ProjectResource.usage_count + 1)
)
2. 引用计数修复工具
创建脚本修复不一致的引用计数:
# scripts/fix_usage_count.py
async def fix_usage_count():
"""修复项目素材的引用计数"""
resources = await session.execute(select(ProjectResource))
for resource in resources.scalars():
# 统计实际引用次数
actual_count = await session.execute(
select(func.count(StoryboardItem.item_id))
.where(
StoryboardItem.resource_id == resource.project_resource_id,
StoryboardItem.item_type == ItemType.RESOURCE
)
)
actual_count = actual_count.scalar_one()
# 更新引用计数
if resource.usage_count != actual_count:
await session.execute(
update(ProjectResource)
.where(ProjectResource.project_resource_id == resource.project_resource_id)
.values(usage_count=actual_count)
)
logger.info(
"修复引用计数: resource_id=%s, old=%d, new=%d",
resource.project_resource_id,
resource.usage_count,
actual_count
)
3. 删除保护实现
在 ProjectResourceService 中添加:
async def delete_resource(
self,
user_id: UUID,
resource_id: UUID,
force: bool = False
) -> None:
"""删除项目素材"""
resource = await self.repo.get_by_id(resource_id)
if not resource:
raise NotFoundError("素材不存在")
# 检查引用计数
if resource.usage_count > 0 and not force:
raise ValidationError(
"该素材正在被 %d 个分镜使用,无法删除。"
"请先从分镜中移除该素材,或使用强制删除。" % resource.usage_count
)
# 删除素材
await self.repo.delete(resource_id)
await self.session.commit()
相关文档
总结
项目素材引用计数维护功能已完整实现:
- ✅ 添加素材到分镜时自动 +1
- ✅ 从分镜移除素材时自动 -1
- ✅ 事务安全保证
- ✅ 日志记录
- ✅ 符合技术栈规范
待实现:
- ⏸️ 删除保护逻辑
- ⏸️ 批量操作优化
- ⏸️ 引用计数修复工具
引用计数功能可以准确追踪素材的使用情况,为素材管理和删除保护提供数据支持。
变更作者:Kiro AI
审核状态:待审核
部署状态:待部署