fix: 分离视频详情与元数据编辑门禁
This commit is contained in:
parent
309e76989c
commit
128201944f
@ -144,6 +144,7 @@ export async function GET(
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
const isEditRequest = request.nextUrl.searchParams.get('intent') === 'edit';
|
||||
|
||||
let material;
|
||||
|
||||
@ -323,6 +324,60 @@ export async function GET(
|
||||
// 超级管理员不受限制
|
||||
const { isSuperAdmin } = await import('@/lib/department-permission');
|
||||
const userIsSuperAdmin = await isSuperAdmin(userId);
|
||||
const canBypassVideoPreviewGate = isSystemAdmin(userRole) || userIsSuperAdmin;
|
||||
const canEdit =
|
||||
(material as any).userId === userId ||
|
||||
canBypassVideoPreviewGate;
|
||||
const materialWithUser = material as any;
|
||||
const displayAuthorName =
|
||||
materialWithUser.user?.realName ||
|
||||
materialWithUser.user?.username ||
|
||||
materialWithUser.authorName;
|
||||
|
||||
if (isEditRequest) {
|
||||
if (!canEdit) {
|
||||
return NextResponse.json(
|
||||
{ error: '您没有权限编辑此素材' },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
const { rewriteMediaUrlForClient } = await import('@/lib/url-rewrite');
|
||||
const canShowPreview =
|
||||
!videoPreviewPolicy.videoPreviewBlocked ||
|
||||
canBypassVideoPreviewGate;
|
||||
|
||||
return NextResponse.json({
|
||||
id: materialWithUser.id,
|
||||
userId: materialWithUser.userId,
|
||||
productModel: materialWithUser.productModel,
|
||||
reportTitle: materialWithUser.reportTitle || null,
|
||||
categoryName: materialWithUser.categoryName,
|
||||
sceneName: materialWithUser.sceneName,
|
||||
platformName: materialWithUser.platformName || '',
|
||||
productCategoryName: materialWithUser.productCategoryName || '',
|
||||
materialType: materialWithUser.materialType,
|
||||
renderer: materialWithUser.renderer,
|
||||
authorName: displayAuthorName,
|
||||
previewUrl: canShowPreview
|
||||
? rewriteMediaUrlForClient(materialWithUser.previewUrl, request, 'image')
|
||||
: null,
|
||||
taskNo: materialWithUser.taskNo || null,
|
||||
dingtalkApprovalNo: materialWithUser.dingtalkApprovalNo || null,
|
||||
taskStatus: materialWithUser.taskStatus || null,
|
||||
taskFetchedAt: materialWithUser.taskFetchedAt || null,
|
||||
isFullSet: deriveTaskDetailIsFullSet(
|
||||
materialWithUser.materialType,
|
||||
materialWithUser.assets,
|
||||
Boolean(materialWithUser.isFullSet),
|
||||
),
|
||||
taskDetailSourceType: materialWithUser.taskDetailSourceType || null,
|
||||
taskDetailRequesterUserId: materialWithUser.taskDetailRequesterUserId || null,
|
||||
taskDetailRequesterName: materialWithUser.taskDetailRequesterName || null,
|
||||
canEdit: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!userIsSuperAdmin) {
|
||||
const canView = await canUserViewMaterial(
|
||||
userDepartment,
|
||||
@ -340,10 +395,6 @@ export async function GET(
|
||||
}
|
||||
}
|
||||
|
||||
const canBypassVideoPreviewGate = isSystemAdmin(userRole) || userIsSuperAdmin;
|
||||
const canEdit =
|
||||
(material as any).userId === userId ||
|
||||
canBypassVideoPreviewGate;
|
||||
if (videoPreviewPolicy.videoPreviewBlocked && !canBypassVideoPreviewGate) {
|
||||
return buildVideoPreviewBlockedResponse();
|
||||
}
|
||||
@ -379,12 +430,6 @@ export async function GET(
|
||||
}
|
||||
}
|
||||
|
||||
// 类型断言,确保 user 字段存在
|
||||
const materialWithUser = material as any;
|
||||
|
||||
// 优先使用用户的真实姓名或用户名,如果没有才使用 authorName
|
||||
const displayAuthorName = materialWithUser.user?.realName || materialWithUser.user?.username || materialWithUser.authorName;
|
||||
|
||||
const formattedAssets = (materialWithUser.assets || []).map((asset: any) =>
|
||||
formatTaskDetailAsset(asset, displayAuthorName),
|
||||
);
|
||||
|
||||
@ -250,7 +250,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
||||
setError('')
|
||||
setMaterial(null)
|
||||
|
||||
const materialResponse = await fetch(`/api/materials/${params.id}`, {
|
||||
const materialResponse = await fetch(`/api/materials/${params.id}?intent=edit`, {
|
||||
signal,
|
||||
})
|
||||
if (!materialResponse.ok) {
|
||||
|
||||
@ -299,6 +299,100 @@ describe('material edit routes', () => {
|
||||
expect(body.canEdit).toBe(true)
|
||||
})
|
||||
|
||||
test('视频等待期内,素材所有者仍可读取元数据编辑信息,但不能取得视频预览地址', async () => {
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||||
id: 'material-1',
|
||||
userId: 'user-1',
|
||||
productModel: 'VIDEO-001',
|
||||
reportTitle: null,
|
||||
categoryName: '店铺A',
|
||||
sceneName: '场景A',
|
||||
platformName: '',
|
||||
productCategoryName: '',
|
||||
materialType: '淘系-主图视频',
|
||||
isAiGenerated: false,
|
||||
taskDetailSourceType: null,
|
||||
taskDetailRequesterUserId: null,
|
||||
taskDetailRequesterName: null,
|
||||
renderer: '',
|
||||
authorName: '测试作者',
|
||||
previewUrl: 'https://s3.jiansun.vip/video-cover.webp',
|
||||
zipFileName: 'video.mp4',
|
||||
zipFilePath: 'uploads/video.mp4',
|
||||
zipFileSize: BigInt(123),
|
||||
downloadCount: 0,
|
||||
likeCount: 0,
|
||||
displayImages: [],
|
||||
createdAt: new Date(),
|
||||
videoSourceUrl: 'https://s3.jiansun.vip/video.mp4',
|
||||
videoPreview540Url: 'https://s3.jiansun.vip/video-540.mp4',
|
||||
videoPreviewClipUrl: 'https://s3.jiansun.vip/video-clip.mp4',
|
||||
videoCoverUrl: 'https://s3.jiansun.vip/video-cover.webp',
|
||||
videoDuration: 20,
|
||||
videoProcessingStatus: 'completed',
|
||||
taskNo: null,
|
||||
dingtalkApprovalNo: null,
|
||||
taskStatus: null,
|
||||
taskFetchedAt: null,
|
||||
isFullSet: false,
|
||||
user: {
|
||||
id: 'user-1',
|
||||
username: '测试用户',
|
||||
realName: '测试用户',
|
||||
},
|
||||
assets: [],
|
||||
})
|
||||
|
||||
const response = await getMaterialById(
|
||||
new NextRequest('http://localhost/api/materials/material-1?intent=edit'),
|
||||
{ params: { id: 'material-1' } },
|
||||
)
|
||||
const body = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(body.canEdit).toBe(true)
|
||||
expect(body.productModel).toBe('VIDEO-001')
|
||||
expect(body.previewUrl).toBeNull()
|
||||
expect(body).not.toHaveProperty('videoSourceUrl')
|
||||
expect(body).not.toHaveProperty('videoPreview540Url')
|
||||
expect(body).not.toHaveProperty('sourceFileUrl')
|
||||
expect(canUserViewMaterial).not.toHaveBeenCalled()
|
||||
|
||||
const detailResponse = await getMaterialById(
|
||||
new NextRequest('http://localhost/api/materials/material-1'),
|
||||
{ params: { id: 'material-1' } },
|
||||
)
|
||||
const detailBody = await detailResponse.json()
|
||||
|
||||
expect(detailResponse.status).toBe(403)
|
||||
expect(detailBody.code).toBe('VIDEO_WAIT_PERIOD')
|
||||
})
|
||||
|
||||
test('非素材所有者不能通过编辑读取意图绕过权限', async () => {
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||||
id: 'material-1',
|
||||
userId: 'other-user',
|
||||
materialType: '3D场景',
|
||||
createdAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
user: {
|
||||
id: 'other-user',
|
||||
username: '其他用户',
|
||||
realName: '其他用户',
|
||||
},
|
||||
assets: [],
|
||||
})
|
||||
|
||||
const response = await getMaterialById(
|
||||
new NextRequest('http://localhost/api/materials/material-1?intent=edit'),
|
||||
{ params: { id: 'material-1' } },
|
||||
)
|
||||
const body = await response.json()
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
expect(body.error).toBe('您没有权限编辑此素材')
|
||||
expect(canUserViewMaterial).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('PUT 路由应透传任务详情出图方式字段', async () => {
|
||||
const request = new NextRequest('http://localhost/api/materials/material-1', {
|
||||
method: 'PUT',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user