diff --git a/app/api/admin/data-stats/route.ts b/app/api/admin/data-stats/route.ts index 6769a30b..ae41030a 100644 --- a/app/api/admin/data-stats/route.ts +++ b/app/api/admin/data-stats/route.ts @@ -1,4 +1,4 @@ -import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access' +import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { getServerSession } from 'next-auth' @@ -38,7 +38,7 @@ export async function GET(request: NextRequest) { const endDate = searchParams.get('endDate') const departmentKey = searchParams.get('departmentKey') - const accessWhere = await buildMaterialOrganizationViewWhere((session.user as any).id) + const accessWhere = await buildMaterialOrganizationWhere((session.user as any).id) const stats = await getMaterialUploadStats({ accessWhere, startDate, diff --git a/app/api/admin/material-operation-logs/route.ts b/app/api/admin/material-operation-logs/route.ts index c6b75f53..040f5f25 100644 --- a/app/api/admin/material-operation-logs/route.ts +++ b/app/api/admin/material-operation-logs/route.ts @@ -1,4 +1,4 @@ -import { canViewOperationsGMaterials, getOperationsGMaterialIds } from '@/lib/material-organization-access' +import { canUseOperationsGMaterials, getOperationsGMaterialIds } from '@/lib/material-organization-access' import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' @@ -53,7 +53,7 @@ export async function GET(request: NextRequest) { const endDate = searchParams.get('endDate') const keyword = normalizeKeyword(searchParams.get('keyword')) - const excludedIds = await canViewOperationsGMaterials((session.user as any).id) ? [] : await getOperationsGMaterialIds() + const excludedIds = await canUseOperationsGMaterials((session.user as any).id) ? [] : await getOperationsGMaterialIds() const materialLogWhere: any = { ...(excludedIds.length ? { materialId: { notIn: excludedIds } } : {}) } const downloadLogWhere: any = { ...(excludedIds.length ? { materialId: { notIn: excludedIds } } : {}) } diff --git a/app/api/admin/materials/batch-delete/route.ts b/app/api/admin/materials/batch-delete/route.ts index 75df32df..dab23cc5 100644 --- a/app/api/admin/materials/batch-delete/route.ts +++ b/app/api/admin/materials/batch-delete/route.ts @@ -1,10 +1,11 @@ import { invalidateMaterialFacetsCache } from '@/lib/material-facets-cache' -import { canAccessOperationsGMaterials } from '@/lib/material-organization-access' +import { isRestrictedMaterial } from '@/lib/material-access-scope' +import { buildMaterialPrivacyWhere, TASK_DETAIL_PRIVACY_SELECT } from '@/lib/task-detail-privacy' import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' -import { isSystemAdmin } from '@/lib/department-permission' +import { isSuperAdmin, isSystemAdmin } from '@/lib/department-permission' import { createMaterialLog } from '@/lib/material-log' import { enqueueMaterialPurge, ensureMaterialPurgeWorkerStarted, MaterialPurgeError } from '@/lib/material-purge' @@ -25,19 +26,18 @@ export async function POST(request: NextRequest) { const session = await getServerSession(authOptions) // 使用统一的权限检查函数(与单条删除保持一致) - if (!session || !isSystemAdmin((session.user as any)?.role)) { + const operatorId = (session?.user as any)?.id + if (!session || (!isSystemAdmin(session.user?.role) && !(await isSuperAdmin(operatorId)))) { return NextResponse.json({ error: '无权限,需要系统管理员权限' }, { status: 403 }) } + if (!operatorId) { + return NextResponse.json({ error: '缺少操作人信息' }, { status: 401 }) + } const { ids, permanent } = await request.json() if (permanent !== undefined && typeof permanent !== 'boolean') { return NextResponse.json({ error: '删除方式无效' }, { status: 400 }) } - const operatorId = (session.user as any).id - - if (!operatorId) { - return NextResponse.json({ error: '缺少操作人信息' }, { status: 401 }) - } // 校验 ids 是数组且非空 if (!Array.isArray(ids) || ids.length === 0 || ids.length > 100) { @@ -63,15 +63,19 @@ export async function POST(request: NextRequest) { // 查询素材(需要处理任务号) const materials = await prisma.material.findMany({ where: { id: { in: ids } }, - select: { id: true, taskNo: true, productModel: true, materialType: true, accessScope: true } + select: { id: true, taskNo: true, productModel: true, materialType: true, ...TASK_DETAIL_PRIVACY_SELECT } }) if (materials.length === 0) { return NextResponse.json({ error: '未找到要删除的素材' }, { status: 404 }) } - if (materials.some((material) => material.accessScope === 'operations_g') && !(await canAccessOperationsGMaterials(operatorId))) { - return NextResponse.json({ error: '无权限操作运营中心 G 专属素材' }, { status: 403 }) + if (materials.some(isRestrictedMaterial)) { + const privacyWhere = await buildMaterialPrivacyWhere(operatorId) + const materialIds = materials.map((material) => material.id) + if (privacyWhere.OR && await prisma.material.count({ where: { id: { in: materialIds }, ...privacyWhere } }) !== materialIds.length) { + return NextResponse.json({ error: '无权限操作部分素材' }, { status: 403 }) + } } const deletedAt = new Date() diff --git a/app/api/admin/materials/purge-jobs/route.ts b/app/api/admin/materials/purge-jobs/route.ts index 080adfbe..d4033e1e 100644 --- a/app/api/admin/materials/purge-jobs/route.ts +++ b/app/api/admin/materials/purge-jobs/route.ts @@ -1,17 +1,18 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' -import { isSystemAdmin } from '@/lib/department-permission' +import { isSuperAdmin, isSystemAdmin } from '@/lib/department-permission' import { prisma } from '@/lib/prisma' -import { canViewOperationsGMaterials, getOperationsGMaterialIds } from '@/lib/material-organization-access' +import { canUseOperationsGMaterials, getOperationsGMaterialIds } from '@/lib/material-organization-access' import { ensureMaterialPurgeWorkerStarted, MaterialPurgeError, retryMaterialPurge, toPurgeJobView } from '@/lib/material-purge' export const dynamic = 'force-dynamic' async function operator() { const session = await getServerSession(authOptions) - if (!session?.user || !(session.user as any).id) return null - return isSystemAdmin((session.user as any).role) ? (session.user as any).id as string : null + const user = session?.user + if (!user?.id) return null + return isSystemAdmin(user.role) || await isSuperAdmin(user.id) ? user.id : null } export async function GET(request: NextRequest) { @@ -22,7 +23,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '清理任务无效' }, { status: 400 }) } try { - const excludedIds = await canViewOperationsGMaterials(operatorId) ? [] : await getOperationsGMaterialIds() + const excludedIds = await canUseOperationsGMaterials(operatorId) ? [] : await getOperationsGMaterialIds() const scopeWhere = excludedIds.length ? { materialId: { notIn: excludedIds } } : {} const [pending, tracked] = await Promise.all([ prisma.materialPurgeJob.findMany({ where: { ...scopeWhere, status: { not: 'completed' } }, orderBy: { createdAt: 'asc' }, take: 100 }), diff --git a/app/api/admin/regenerate-pdf-preview/route.ts b/app/api/admin/regenerate-pdf-preview/route.ts index 171098d1..1082b103 100644 --- a/app/api/admin/regenerate-pdf-preview/route.ts +++ b/app/api/admin/regenerate-pdf-preview/route.ts @@ -1,4 +1,4 @@ -import { canAccessOperationsGMaterials } from '@/lib/material-organization-access' +import { canAccessMaterialPrivacy } from '@/lib/task-detail-privacy' import { isRestrictedMaterial } from '@/lib/material-access-scope' import { createHash, timingSafeEqual } from 'crypto' import { lstat, realpath } from 'fs/promises' @@ -292,7 +292,7 @@ export async function POST(request: NextRequest) { throw new RegeneratePdfPreviewInputError('素材不存在', 404) } const session = isRestrictedMaterial(material) ? await getServerSession(authOptions) : null - if (isRestrictedMaterial(material) && !(await canAccessOperationsGMaterials((session?.user as any)?.id))) { + if (!(await canAccessMaterialPrivacy(material, (session?.user as any)?.id))) { throw new RegeneratePdfPreviewInputError('无权限操作此素材', 403) } if (material.materialType !== '品牌VI灵感') { diff --git a/app/api/admin/system-status/route.ts b/app/api/admin/system-status/route.ts index 58b30047..496d1b92 100644 --- a/app/api/admin/system-status/route.ts +++ b/app/api/admin/system-status/route.ts @@ -10,7 +10,7 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' -import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access' +import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access' export async function GET(req: NextRequest) { try { @@ -24,7 +24,7 @@ export async function GET(req: NextRequest) { ) } - const organizationWhere = await buildMaterialOrganizationViewWhere((session.user as any).id) + const organizationWhere = await buildMaterialOrganizationWhere((session.user as any).id) // 计算时间范围(30天前和7天前) const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) diff --git a/app/api/admin/trash/route.ts b/app/api/admin/trash/route.ts index 2a2db9b5..399a69df 100644 --- a/app/api/admin/trash/route.ts +++ b/app/api/admin/trash/route.ts @@ -1,4 +1,4 @@ -import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access' +import { buildMaterialPrivacyWhere } from '@/lib/task-detail-privacy' /** * 回收站 API - 获取已删除的素材 * @@ -9,13 +9,15 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' +import { isSuperAdmin, isSystemAdmin } from '@/lib/department-permission' import { rewriteMaterialStorageUrlsForClient } from '@/lib/url-rewrite' export async function GET(req: NextRequest) { try { const session = await getServerSession(authOptions) - if (!session || (session.user as any).role !== 'ADMIN') { + const user = session?.user + if (!user?.id || (!isSystemAdmin(user.role) && !(await isSuperAdmin(user.id)))) { return NextResponse.json( { error: '无权限' }, { status: 403 } @@ -27,7 +29,7 @@ export async function GET(req: NextRequest) { const pageSize = parseInt(searchParams.get('pageSize') || '20') const skip = (page - 1) * pageSize - const scopeWhere = await buildMaterialOrganizationViewWhere((session.user as any).id) + const scopeWhere = await buildMaterialPrivacyWhere(user.id) // 获取已删除的素材 const [deletedMaterials, total] = await Promise.all([ diff --git a/app/api/admin/video-transcode-stats/route.ts b/app/api/admin/video-transcode-stats/route.ts index 0fe75ed2..65497317 100644 --- a/app/api/admin/video-transcode-stats/route.ts +++ b/app/api/admin/video-transcode-stats/route.ts @@ -11,7 +11,7 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' -import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access' +import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access' import { LEGACY_COMPATIBLE_VIDEO_MATERIAL_TYPES } from '@/lib/video-material-types' import { USER_TRANSCODE_FAILURE_MESSAGE } from '@/lib/video-transcode-task' @@ -27,7 +27,7 @@ export async function GET(req: NextRequest) { ) } - const organizationWhere = await buildMaterialOrganizationViewWhere((session.user as any).id) + const organizationWhere = await buildMaterialOrganizationWhere((session.user as any).id) // 识别视频素材类型 const videoMaterialTypes = [...LEGACY_COMPATIBLE_VIDEO_MATERIAL_TYPES] diff --git a/app/api/categories/route.ts b/app/api/categories/route.ts index 9be47bc1..7dce702d 100644 --- a/app/api/categories/route.ts +++ b/app/api/categories/route.ts @@ -1,4 +1,4 @@ -import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access' +import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access' import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; @@ -12,7 +12,7 @@ export async function GET() { return NextResponse.json({ error: '未登录' }, { status: 401 }); } - const scopeWhere = await buildMaterialOrganizationViewWhere((session.user as any).id); + const scopeWhere = await buildMaterialOrganizationWhere((session.user as any).id); const categories = await prisma.category.findMany({ orderBy: { name: 'asc' }, diff --git a/app/api/materials/[id]/route.ts b/app/api/materials/[id]/route.ts index 9662d059..ae6cdcb6 100644 --- a/app/api/materials/[id]/route.ts +++ b/app/api/materials/[id]/route.ts @@ -323,7 +323,7 @@ export async function GET( canAccessMaterialPrivacy(material as any, userId), isSuperAdmin(userId), ]); - if (!hasPrivacyAccess && !userIsSuperAdmin) { + if (!hasPrivacyAccess) { return NextResponse.json({ error: '无权限查看此素材' }, { status: 403, headers: { 'Cache-Control': 'private, no-store' } }); } @@ -332,7 +332,7 @@ export async function GET( (material as any).materialType, (material as any).createdAt, new Date(), - { bypassWaitPeriod: userHasVideoEarlyAccess(session?.user) }, + { bypassWaitPeriod: userIsSuperAdmin || userHasVideoEarlyAccess(session?.user) }, ); const buildVideoPreviewBlockedResponse = () => @@ -348,7 +348,7 @@ export async function GET( ); const canBypassVideoPreviewGate = isSystemAdmin(userRole) || userIsSuperAdmin; - const canEdit = hasPrivacyAccess && ((material as any).userId === userId || canBypassVideoPreviewGate); + const canEdit = (material as any).userId === userId || canBypassVideoPreviewGate; const materialWithUser = material as any; const displayAuthorName = materialWithUser.user?.realName || @@ -429,9 +429,9 @@ export async function GET( return buildVideoPreviewBlockedResponse(); } - if (hasPrivacyAccess && canBypassVideoPreviewGate) { + if (canBypassVideoPreviewGate) { canAccessSource = true; - } else if (hasPrivacyAccess) { + } else { const userDownloadSetting = await prisma.user.findUnique({ where: { id: userId }, select: { canDownload: true }, diff --git a/app/api/materials/[id]/skus/[skuId]/download/route.ts b/app/api/materials/[id]/skus/[skuId]/download/route.ts index f1f79ca6..6b6c157c 100644 --- a/app/api/materials/[id]/skus/[skuId]/download/route.ts +++ b/app/api/materials/[id]/skus/[skuId]/download/route.ts @@ -90,10 +90,10 @@ export async function GET( const userId = (session.user as any).id const user = await prisma.user.findUnique({ where: { id: userId }, - select: { canDownload: true }, + select: { canDownload: true, isSuperAdmin: true }, }) - if (!user?.canDownload) { + if (!user?.canDownload && !user?.isSuperAdmin) { return NextResponse.json({ error: '您没有下载权限,请联系管理员' }, { status: 403 }) } diff --git a/app/api/materials/[id]/skus/[skuId]/route.ts b/app/api/materials/[id]/skus/[skuId]/route.ts index 97f87df8..17d7e27c 100644 --- a/app/api/materials/[id]/skus/[skuId]/route.ts +++ b/app/api/materials/[id]/skus/[skuId]/route.ts @@ -4,7 +4,6 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' -import { isSuperAdmin } from '@/lib/department-permission' import { createMaterialLog } from '@/lib/material-log' import { deleteFileIfUnreferenced, deleteLocalFile } from '@/lib/file-cleanup' import { @@ -54,8 +53,7 @@ export async function GET( } const session = await getServerSession(authOptions) const userId = (session?.user as any)?.id - const hasPrivacyAccess = await canAccessMaterialPrivacy(material, userId) - if (!hasPrivacyAccess && !(await isSuperAdmin(userId))) { + if (!(await canAccessMaterialPrivacy(material, userId))) { return NextResponse.json({ error: '无权限访问此素材' }, { status: session?.user ? 403 : 401, headers: { 'Cache-Control': 'private, no-store' }, @@ -87,7 +85,7 @@ export async function GET( skuName: sku.skuName, previewUrl: (sku as any).previewUrl || '', zipFileName: (sku as any).fileName || '', - zipFilePath: hasPrivacyAccess ? sku.filePath : '', + zipFilePath: sku.filePath, zipFileSize: sku.fileSize.toString(), sortOrder: sku.sortOrder, whiteBackgroundSourceType: sku.whiteBackgroundSourceType, diff --git a/app/api/materials/[id]/skus/authorization.ts b/app/api/materials/[id]/skus/authorization.ts index 0fe02b61..c5e3a65e 100644 --- a/app/api/materials/[id]/skus/authorization.ts +++ b/app/api/materials/[id]/skus/authorization.ts @@ -27,7 +27,7 @@ export async function canManageSkuMaterial( ): Promise { const stored = await prisma.material.findUnique({ where: { id: material.id }, select: TASK_DETAIL_PRIVACY_SELECT }) if (!stored || !(await canAccessMaterialPrivacy(stored, user.id))) return false - if (material.userId === user.id || isSystemAdmin(user.role) || user.isSuperAdmin === true) { + if (material.userId === user.id || isSystemAdmin(user.role)) { return true } diff --git a/app/api/materials/[id]/skus/batch-download/route.ts b/app/api/materials/[id]/skus/batch-download/route.ts index dbefb6e6..121d913c 100644 --- a/app/api/materials/[id]/skus/batch-download/route.ts +++ b/app/api/materials/[id]/skus/batch-download/route.ts @@ -80,10 +80,10 @@ export async function POST( const userId = (session.user as any).id const user = await prisma.user.findUnique({ where: { id: userId }, - select: { canDownload: true }, + select: { canDownload: true, isSuperAdmin: true }, }) - if (!user?.canDownload) { + if (!user?.canDownload && !user?.isSuperAdmin) { return NextResponse.json({ error: '您没有下载权限,请联系管理员' }, { status: 403 }) } diff --git a/app/api/materials/[id]/skus/route.ts b/app/api/materials/[id]/skus/route.ts index 848df937..1125c9f7 100644 --- a/app/api/materials/[id]/skus/route.ts +++ b/app/api/materials/[id]/skus/route.ts @@ -4,7 +4,6 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' -import { isSuperAdmin } from '@/lib/department-permission' import { rewriteMediaUrlForClient } from '@/lib/url-rewrite' import { createMaterialLog } from '@/lib/material-log' import { resolveWhiteBackgroundPreviewFileUrl } from '@/lib/white-background-preview' @@ -67,8 +66,7 @@ export async function GET( } const session = await getServerSession(authOptions) const userId = (session?.user as any)?.id - const hasPrivacyAccess = await canAccessMaterialPrivacy(material, userId) - if (!hasPrivacyAccess && (validateSkuName !== null || !(await isSuperAdmin(userId)))) { + if (!(await canAccessMaterialPrivacy(material, userId))) { return NextResponse.json({ error: '无权限访问该私密素材' }, { status: 403, headers: { 'Cache-Control': 'private, no-store' } }) } @@ -139,7 +137,6 @@ export async function GET( whiteBackgroundGroup?.other?.[0] || whiteBackgroundGroup?.zipAssets?.[0] const previewAssetUrl = resolveWhiteBackgroundPreviewFileUrl(previewAsset?.fileUrl || '') - const canShowWhiteBackgroundPreview = hasPrivacyAccess || /\.(webp|png|jpe?g)(?:[?#]|$)/i.test(previewAssetUrl || '') const uploader = sku.whiteBackgroundUploadedBy ? uploaderMap.get(sku.whiteBackgroundUploadedBy) || null : null @@ -150,7 +147,7 @@ export async function GET( skuName: sku.skuName, previewUrl: rewriteMediaUrlForClient((sku as any).previewUrl || '', request, 'image') || '', zipFileName: (sku as any).fileName || '', - zipFilePath: hasPrivacyAccess ? sku.filePath : '', + zipFilePath: sku.filePath, zipFileSize: sku.fileSize.toString(), sortOrder: sku.sortOrder, createdAt: sku.createdAt, @@ -165,9 +162,7 @@ export async function GET( whiteBackgroundUploadedAt: sku.whiteBackgroundUploadedAt, whiteBackgroundUploader: uploader, whiteBackgroundFileName: whiteBackgroundUploaded ? `${sku.skuName}_白底图.zip` : '', - whiteBackgroundFileUrl: canShowWhiteBackgroundPreview - ? rewriteMediaUrlForClient(previewAssetUrl || '', request, 'image') || '' - : '', + whiteBackgroundFileUrl: rewriteMediaUrlForClient(previewAssetUrl || '', request, 'image') || '', } }), total: skus.length, diff --git a/app/api/materials/download/route.ts b/app/api/materials/download/route.ts index 4ae4bb21..717fb6ae 100644 --- a/app/api/materials/download/route.ts +++ b/app/api/materials/download/route.ts @@ -14,6 +14,7 @@ import { lookup } from 'mime-types'; import { canUserDownloadMaterial, canUserDownloadSource, + isSuperAdmin, } from '@/lib/department-permission'; import { DownloadErrorCode, @@ -21,7 +22,7 @@ import { } from '@/lib/download-errors'; import { is3DOriginalType, isProductPackageType } from '@/lib/material-type-mapper'; import { ensureMaterialDownloadLogsTable } from '@/lib/material-download-log'; -import { resolveStoredUploadPath } from '@/lib/upload-paths'; +import { getUploadRootDir, resolveStoredUploadPath } from '@/lib/upload-paths'; import { getVideoDownloadAvailability, getVideoDownloadUnavailableMessage, @@ -47,9 +48,10 @@ export async function GET(request: NextRequest) { const userId = (session.user as any).id; const userRole = (session.user as any).role; + const userIsSuperAdmin = await isSuperAdmin(userId); // 检查用户级别的下载权限 - if (userRole !== 'ADMIN') { + if (userRole !== 'ADMIN' && !userIsSuperAdmin) { const user = await prisma.user.findUnique({ where: { id: userId }, select: { canDownload: true }, @@ -109,7 +111,7 @@ export async function GET(request: NextRequest) { material.materialType, material.createdAt, new Date(), - { bypassWaitPeriod: userHasVideoEarlyAccess(session.user) }, + { bypassWaitPeriod: userIsSuperAdmin || userHasVideoEarlyAccess(session.user) }, ); if (!videoDownloadAvailability.allowed) { return NextResponse.json( @@ -126,8 +128,7 @@ export async function GET(request: NextRequest) { const managedDepartment = (session.user as any).managedDepartment; // 超级管理员不受限制 - const { isSuperAdmin } = await import('@/lib/department-permission'); - if (!(await isSuperAdmin(userId))) { + if (!userIsSuperAdmin) { const canDownload = isRestrictedMaterial(material) || await canUserDownloadMaterial( userDepartment, userRole, @@ -261,7 +262,7 @@ async function serveFile(filePath: string, material: any, fileNameSuffix: string ); } - const allowedDirs = getAllowedFileDirectories(); + const allowedDirs = [getUploadRootDir(), ...getAllowedFileDirectories()]; if (!isPathInAllowedDirs(filePath, allowedDirs)) { console.error('[Download] 路径越界:', filePath, '允许的目录:', allowedDirs); return NextResponse.json( diff --git a/app/api/materials/restore/route.ts b/app/api/materials/restore/route.ts index 33beea39..bf6ee4c9 100644 --- a/app/api/materials/restore/route.ts +++ b/app/api/materials/restore/route.ts @@ -1,5 +1,6 @@ import { invalidateMaterialFacetsCache } from '@/lib/material-facets-cache' -import { canAccessOperationsGMaterials } from '@/lib/material-organization-access' +import { canAccessMaterialPrivacy } from '@/lib/task-detail-privacy' +import { isSuperAdmin, isSystemAdmin } from '@/lib/department-permission' import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; @@ -15,7 +16,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: '未登录' }, { status: 401 }); } const userRole = (session.user as any).role; - if (userRole !== 'ADMIN') { + const userId = (session.user as any).id; + if (!isSystemAdmin(userRole) && !(await isSuperAdmin(userId))) { return NextResponse.json({ error: '无权限' }, { status: 403 }); } @@ -34,8 +36,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: '素材不存在' }, { status: 404 }); } - if (material.accessScope === 'operations_g' && !(await canAccessOperationsGMaterials((session.user as any).id))) { - return NextResponse.json({ error: '无权限操作运营中心 G 专属素材' }, { status: 403 }); + if (!(await canAccessMaterialPrivacy(material, userId))) { + return NextResponse.json({ error: '无权限操作此素材' }, { status: 403 }); } const restoredTaskNo = getOriginalTaskNo((material as any).taskNo); @@ -143,7 +145,6 @@ export async function POST(request: NextRequest) { ]); // 记录操作日志 - const userId = (session.user as any).id; await createMaterialLog({ materialId: id, userId, diff --git a/app/api/materials/route.ts b/app/api/materials/route.ts index 73db6ecc..8f3ecfd1 100644 --- a/app/api/materials/route.ts +++ b/app/api/materials/route.ts @@ -1,12 +1,11 @@ import { invalidateMaterialFacetsCache } from '@/lib/material-facets-cache' -import { canAccessOperationsGMaterials } from '@/lib/material-organization-access' -import { isOperationsGMaterial, isPreviewAssetType, isRestrictedMaterial } from '@/lib/material-access-scope' -import { canAccessMaterialPrivacy, isPrivateTaskDetail, TASK_DETAIL_PRIVACY_SELECT } from '@/lib/task-detail-privacy'; +import { isRestrictedMaterial } from '@/lib/material-access-scope' +import { canAccessMaterialPrivacy, TASK_DETAIL_PRIVACY_SELECT } from '@/lib/task-detail-privacy'; import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { prisma } from '@/lib/prisma'; -import { isSystemAdmin } from '@/lib/department-permission'; +import { canUserViewMaterial, isSuperAdmin, isSystemAdmin } from '@/lib/department-permission'; import { createMaterialLog } from '@/lib/material-log'; import { enqueueMaterialPurge, ensureMaterialPurgeWorkerStarted, MaterialPurgeError } from '@/lib/material-purge'; import { normalizeUrl, normalizeMaterial } from '@/lib/utils'; @@ -176,9 +175,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ material: null }); } const userId = (session.user as any).id; - const { isSuperAdmin, canUserViewMaterial } = await import('@/lib/department-permission'); - const hasPrivacyAccess = await canAccessMaterialPrivacy(material, userId); - if (!hasPrivacyAccess && !(await isSuperAdmin(userId))) { + if (!(await canAccessMaterialPrivacy(material, userId))) { return NextResponse.json({ error: '无权限查看此素材' }, { status: 403, headers: { 'Cache-Control': 'private, no-store' } }); } @@ -266,7 +263,6 @@ export async function GET(request: NextRequest) { // 优先使用用户的真实姓名或用户名,如果没有才使用 authorName const displayAuthorName = material.user?.realName || material.user?.username || material.authorName; const formattedAssets = (material.assets || []) - .filter((asset: any) => hasPrivacyAccess || isPreviewAssetType(asset.assetType)) .map((asset: any) => formatTaskDetailAssetSummary( asset, displayAuthorName, @@ -275,9 +271,6 @@ export async function GET(request: NextRequest) { const normalizedMaterial = normalizeMaterial({ ...material, - isViewOnly: !hasPrivacyAccess, - zipFileName: hasPrivacyAccess ? material.zipFileName : '', - videoSourceUrl: hasPrivacyAccess ? material.videoSourceUrl : null, authorName: displayAuthorName, // 使用处理后的作者名 previewUrl, // 使用处理后的previewUrl zipFileSize: material.zipFileSize.toString(), @@ -773,10 +766,6 @@ export async function GET(request: NextRequest) { } const formattedMaterials = materials.map(material => { - const isViewOnly = context.isSuperAdmin && ( - (isOperationsGMaterial(material) && !context.canAccessOperationsGMaterials) || - (isPrivateTaskDetail(material) && !context.canAccessPrivateTaskDetails && material.taskDetailRequesterUserId !== user.id) - ); // 处理previewUrl的fallback逻辑 let previewUrl = material.previewUrl || material.videoCoverUrl || ''; @@ -814,7 +803,6 @@ export async function GET(request: NextRequest) { // 优先使用用户的真实姓名或用户名,如果没有才使用 authorName const displayAuthorName = material.user?.realName || material.user?.username || material.authorName; const formattedAssets = (material.assets || []) - .filter((asset: any) => !isViewOnly || isPreviewAssetType(asset.assetType)) .map((asset: any) => formatTaskDetailAssetSummary( asset, displayAuthorName, @@ -823,7 +811,6 @@ export async function GET(request: NextRequest) { const formatted = { ...material, - isViewOnly, authorName: displayAuthorName, // 使用处理后的作者名 previewUrl, // 使用处理后的previewUrl zipFileSize: material.zipFileSize.toString(), @@ -843,7 +830,7 @@ export async function GET(request: NextRequest) { // 处理音频文件的zipFileName(特殊处理) let processedZipFileName = formatted.zipFileName; - if (!isViewOnly && formatted.zipFileName && formatted.materialType === '音乐') { + if (formatted.zipFileName && formatted.materialType === '音乐') { const zipFileName = formatted.zipFileName; if (zipFileName.startsWith('http://') || zipFileName.startsWith('https://')) { // 老素材:S3 URL,使用统一规则(音频通常>1MB,会走代理) @@ -871,13 +858,13 @@ export async function GET(request: NextRequest) { // 视频:使用统一规则(内网用户直接访问内网MinIO,外网用户通常>1MB会走代理) videoPreview540Url: rewriteMediaUrlForClient(formatted.videoPreview540Url, request, 'video'), videoPreviewClipUrl: rewriteMediaUrlForClient(formatted.videoPreviewClipUrl, request, 'video'), - videoSourceUrl: isViewOnly ? null : rewriteMediaUrlForClient(formatted.videoSourceUrl, request, 'video'), + videoSourceUrl: rewriteMediaUrlForClient(formatted.videoSourceUrl, request, 'video'), // 音频文件:特殊处理 - zipFileName: isViewOnly ? '' : processedZipFileName, + zipFileName: processedZipFileName, assets: formattedAssets, }; - return applyVideoPreviewPolicy(rewrittenMaterial, new Date(), { bypassWaitPeriod: userHasVideoEarlyAccess(session?.user) }); + return applyVideoPreviewPolicy(rewrittenMaterial, new Date(), { bypassWaitPeriod: context.isSuperAdmin || userHasVideoEarlyAccess(session?.user) }); }); return NextResponse.json({ @@ -921,7 +908,8 @@ export async function DELETE(request: NextRequest) { ); } const userRole = (session.user as any).role; - if (!isSystemAdmin(userRole)) { + const userId = (session.user as any).id; + if (!isSystemAdmin(userRole) && !(await isSuperAdmin(userId))) { return NextResponse.json( { error: '无权限' }, { status: 403 } @@ -957,10 +945,8 @@ export async function DELETE(request: NextRequest) { ); } - const userId = (session.user as any).id; - - if (material.accessScope === 'operations_g' && !(await canAccessOperationsGMaterials(userId))) { - return NextResponse.json({ error: '无权限操作运营中心 G 专属素材' }, { status: 403 }); + if (!(await canAccessMaterialPrivacy(material, userId))) { + return NextResponse.json({ error: '无权限操作此素材' }, { status: 403 }); } // 软删除 diff --git a/app/api/materials/share/route.ts b/app/api/materials/share/route.ts index 1654837b..c461e77d 100644 --- a/app/api/materials/share/route.ts +++ b/app/api/materials/share/route.ts @@ -11,7 +11,6 @@ import { canUserDownloadSource, hasSystemAdminPermission, isSuperAdmin, - isSystemAdmin, } from '@/lib/department-permission'; import { isProductPackageType, is3DOriginalType, needsAssetsList } from '@/lib/material-type-mapper'; import { deriveTaskDetailIsFullSet } from '@/lib/task-detail-full-set'; @@ -19,6 +18,7 @@ import { canManageMaterial } from '@/lib/material-authorization'; import { detectFileKindFromName, getCertificateReportFileKindByAssetType, getDefaultCertificateExpiryDate, isCertificateReportMaterialType, isPublicCertificateReportPdf } from '@/lib/certificate-report'; import { DownloadErrorCode } from '@/lib/download-errors'; import { getVideoPreviewPolicyFields, userHasVideoEarlyAccess } from '@/lib/video-download-policy'; +import { isVideoMaterialType } from '@/lib/video-material-types'; // 解析 displayImages 字段 function parseDisplayImages(displayImages: any): string[] { @@ -160,8 +160,7 @@ export async function GET(request: NextRequest) { if (!material) { return NextResponse.json({ error: '素材不存在' }, { status: 404 }); } - const hasPrivacyAccess = await canAccessMaterialPrivacy(material as any, userId); - if (!hasPrivacyAccess && !(await isSuperAdmin(userId))) { + if (!(await canAccessMaterialPrivacy(material as any, userId))) { return NextResponse.json({ error: '请登录有权限的账号查看此素材' }, { status: userId ? 403 : 401, headers: { 'Cache-Control': 'private, no-store' } }); } @@ -169,7 +168,7 @@ export async function GET(request: NextRequest) { (material as any).materialType, (material as any).createdAt, new Date(), - { bypassWaitPeriod: userHasVideoEarlyAccess(session?.user) }, + { bypassWaitPeriod: userHasVideoEarlyAccess(session?.user) || (isVideoMaterialType((material as any).materialType) && await isSuperAdmin(userId)) }, ); if (videoPreviewPolicy.videoPreviewBlocked) { @@ -293,7 +292,7 @@ export async function GET(request: NextRequest) { // 任务-详情 PSD 和任务原图 ZIP 按各自素材类型检查源文件权限。 let canAccessSource = false; - if (hasPrivacyAccess && userId && currentUser) { + if (userId && currentUser) { if (hasSystemAdminPermission(currentUser.role, Boolean(currentUser.isSuperAdmin))) { canAccessSource = true; } else if (currentUser.canDownload) { @@ -321,9 +320,8 @@ export async function GET(request: NextRequest) { } const capabilities = { - canFavorite: Boolean(hasPrivacyAccess && userId && currentUser), + canFavorite: Boolean(userId && currentUser), canEdit: Boolean( - hasPrivacyAccess && userId && currentUser && ( @@ -331,14 +329,13 @@ export async function GET(request: NextRequest) { hasSystemAdminPermission(currentUser.role, Boolean(currentUser.isSuperAdmin)) ) ), - // 与 DELETE /api/materials 当前契约保持一致:仅系统管理员角色可删除。 - canDelete: Boolean(hasPrivacyAccess && currentUser && isSystemAdmin(currentUser.role)), + canDelete: Boolean(currentUser && hasSystemAdminPermission(currentUser.role, currentUser.isSuperAdmin)), }; // 任务-详情单图编辑(替换/改名/删除)与元数据编辑权限口径不同: // 走 canManageMaterial,托管部门的成员也能维护本部门素材。 let canManageTaskDetailAssets = false; - if (hasPrivacyAccess && userId && currentUser && isProductPackageType(material.materialType as unknown as string)) { + if (userId && currentUser && isProductPackageType(material.materialType as unknown as string)) { canManageTaskDetailAssets = await canManageMaterial( { id: userId, role: currentUser.role, isSuperAdmin: currentUser.isSuperAdmin }, { id: material.id, userId: material.userId } as any, @@ -352,7 +349,6 @@ export async function GET(request: NextRequest) { const response: any = { ...material, - isViewOnly: !hasPrivacyAccess, zipFileName: getSourceDisplayName(material.zipFileName || material.zipFilePath), zipFilePath: '', videoSourceUrl: canAccessSource ? material.videoSourceUrl : null, diff --git a/app/api/materials/stats/route.ts b/app/api/materials/stats/route.ts index 709a15eb..be33fe09 100644 --- a/app/api/materials/stats/route.ts +++ b/app/api/materials/stats/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { prisma } from '@/lib/prisma'; -import { buildMaterialViewWhere } from '@/lib/task-detail-privacy'; +import { buildMaterialPrivacyWhere } from '@/lib/task-detail-privacy'; // GET - 获取素材统计数据(只统计未删除的素材) export async function GET() { @@ -11,7 +11,7 @@ export async function GET() { if (!session?.user) { return NextResponse.json({ error: '未登录' }, { status: 401 }); } - const privacyWhere = await buildMaterialViewWhere((session.user as any).id); + const privacyWhere = await buildMaterialPrivacyWhere((session.user as any).id); // 获取总数(只统计未删除的素材) let total: number; diff --git a/app/api/materials/transcode/route.ts b/app/api/materials/transcode/route.ts index a3c057b6..d7364dc1 100644 --- a/app/api/materials/transcode/route.ts +++ b/app/api/materials/transcode/route.ts @@ -1,4 +1,4 @@ -import { canAccessOperationsGMaterials, canViewOperationsGMaterials } from '@/lib/material-organization-access' +import { canUseOperationsGMaterials } from '@/lib/material-organization-access' // 视频转码API - 完整版本(支持自动恢复) import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; @@ -6,7 +6,7 @@ import { prisma } from '@/lib/prisma'; import { transcodeQueue, VIDEO_MATERIAL_TYPES } from '@/lib/video-transcode-queue'; import { executeTranscodeTask, USER_TRANSCODE_FAILURE_MESSAGE } from '@/lib/video-transcode-task'; import { authOptions } from '@/lib/auth'; -import { canUserViewMaterial, hasSystemAdminPermission } from '@/lib/department-permission'; +import { canUserViewMaterial, hasSystemAdminPermission, isSuperAdmin } from '@/lib/department-permission'; export const dynamic = 'force-dynamic'; export const maxDuration = 3600; // 1小时 @@ -69,10 +69,7 @@ export async function GET(request: NextRequest) { } const sessionUser = session.user as any; - const isSystemAdministrator = hasSystemAdminPermission( - sessionUser.role, - Boolean(sessionUser.isSuperAdmin), - ); + const isSystemAdministrator = hasSystemAdminPermission(sessionUser.role) || await isSuperAdmin(sessionUser.id); const retryAfter = consumeRateLimit(`status:${String(sessionUser.id || 'unknown')}`, STATUS_RATE_LIMIT); if (retryAfter) { return rateLimitResponse(retryAfter); @@ -111,7 +108,7 @@ export async function GET(request: NextRequest) { { status: 404 } ); } - const canAccessG = await canViewOperationsGMaterials(sessionUser.id); + const canAccessG = await canUseOperationsGMaterials(sessionUser.id); if (material.accessScope === 'operations_g' && !canAccessG) { return NextResponse.json({ error: '无权限访问此素材' }, { status: 403, headers: { 'Cache-Control': 'private, no-store' } }); } @@ -173,10 +170,7 @@ export async function POST(request: NextRequest) { } const sessionUser = session.user as any; - const isSystemAdministrator = hasSystemAdminPermission( - sessionUser.role, - Boolean(sessionUser.isSuperAdmin), - ); + const isSystemAdministrator = hasSystemAdminPermission(sessionUser.role) || await isSuperAdmin(sessionUser.id); const retryAfter = consumeRateLimit(`start:${String(sessionUser.id || 'unknown')}`, START_RATE_LIMIT); if (retryAfter) { @@ -223,7 +217,7 @@ export async function POST(request: NextRequest) { ); } - const canAccessG = await canAccessOperationsGMaterials(sessionUser.id); + const canAccessG = await canUseOperationsGMaterials(sessionUser.id); if (material.accessScope === 'operations_g' && !canAccessG) { return NextResponse.json({ error: '无权限操作此素材' }, { status: 403 }); } diff --git a/app/api/platforms/route.ts b/app/api/platforms/route.ts index a316f2b4..87c95183 100644 --- a/app/api/platforms/route.ts +++ b/app/api/platforms/route.ts @@ -1,4 +1,4 @@ -import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access' +import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access' import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; @@ -13,7 +13,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '未登录' }, { status: 401 }); } - const scopeWhere = await buildMaterialOrganizationViewWhere((session.user as any).id); + const scopeWhere = await buildMaterialOrganizationWhere((session.user as any).id); const platforms = await prisma.platform.findMany({ orderBy: { createdAt: 'asc' }, diff --git a/app/api/product-categories/route.ts b/app/api/product-categories/route.ts index 0004fda1..1fb1acf8 100644 --- a/app/api/product-categories/route.ts +++ b/app/api/product-categories/route.ts @@ -1,4 +1,4 @@ -import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access' +import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access' import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; @@ -12,7 +12,7 @@ export async function GET() { return NextResponse.json({ error: '未登录' }, { status: 401 }); } - const scopeWhere = await buildMaterialOrganizationViewWhere((session.user as any).id); + const scopeWhere = await buildMaterialOrganizationWhere((session.user as any).id); const categories = await prisma.productCategory.findMany({ orderBy: { name: 'asc' }, diff --git a/app/api/proxy-media/route.ts b/app/api/proxy-media/route.ts index 7851e0f6..eeec4256 100644 --- a/app/api/proxy-media/route.ts +++ b/app/api/proxy-media/route.ts @@ -15,6 +15,7 @@ import { } from '@/lib/proxy-media-security' import { findBlockedVideoPreviewByUrl, isVideoMediaPath } from '@/lib/video-preview-access' import { userHasVideoEarlyAccess } from '@/lib/video-download-policy' +import { isSuperAdmin } from '@/lib/department-permission' export const dynamic = 'force-dynamic' export const runtime = 'nodejs' @@ -165,7 +166,7 @@ export async function GET(request: NextRequest) { targetUrl.pathname, ], new Date(), - { bypassWaitPeriod: userHasVideoEarlyAccess(videoSession?.user) }, + { bypassWaitPeriod: userHasVideoEarlyAccess(videoSession?.user) || await isSuperAdmin(videoSession?.user.id) }, ) if (blockedPreview) { diff --git a/app/api/scenes/route.ts b/app/api/scenes/route.ts index b7a04e89..5b3013be 100644 --- a/app/api/scenes/route.ts +++ b/app/api/scenes/route.ts @@ -1,4 +1,4 @@ -import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access' +import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access' import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; @@ -12,7 +12,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '未登录' }, { status: 401 }); } - const scopeWhere = await buildMaterialOrganizationViewWhere((session.user as any).id); + const scopeWhere = await buildMaterialOrganizationWhere((session.user as any).id); const { searchParams } = new URL(request.url); const category = searchParams.get('category'); diff --git a/app/api/uploads/[...path]/route.ts b/app/api/uploads/[...path]/route.ts index e8ff0d83..02e5d943 100644 --- a/app/api/uploads/[...path]/route.ts +++ b/app/api/uploads/[...path]/route.ts @@ -11,12 +11,13 @@ import { canUserDownloadMaterial, canUserDownloadSource, hasSystemAdminPermission, + isSuperAdmin, } from '@/lib/department-permission' import { DownloadErrorCode } from '@/lib/download-errors' import { is3DOriginalType, isProductPackageType } from '@/lib/material-type-mapper' import { findBlockedVideoPreviewByUrl, isVideoMediaPath } from '@/lib/video-preview-access' import { userHasVideoEarlyAccess } from '@/lib/video-download-policy' -import { canAccessMaterialPrivacy, canViewMaterialPrivacy, TASK_DETAIL_PRIVACY_SELECT, type TaskDetailPrivacyMaterial } from '@/lib/task-detail-privacy' +import { canAccessMaterialPrivacy, TASK_DETAIL_PRIVACY_SELECT, type TaskDetailPrivacyMaterial } from '@/lib/task-detail-privacy' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' @@ -269,8 +270,7 @@ async function handleFileRequest( } if (isRestrictedMaterial(access.material)) { const session = await getServerSession(authOptions) - const checkPrivacy = access.kind === 'preview' ? canViewMaterialPrivacy : canAccessMaterialPrivacy - if (!(await checkPrivacy(access.material, (session?.user as any)?.id))) { + if (!(await canAccessMaterialPrivacy(access.material, (session?.user as any)?.id))) { return new Response(headOnly ? null : '无权限访问此素材', { status: session?.user ? 403 : 401, headers: { 'Cache-Control': 'private, no-store', Vary: 'Cookie' }, @@ -301,7 +301,7 @@ async function handleFileRequest( normalizedRelativePath, ], new Date(), - { bypassWaitPeriod: userHasVideoEarlyAccess(videoSession?.user) }, + { bypassWaitPeriod: userHasVideoEarlyAccess(videoSession?.user) || await isSuperAdmin(videoSession?.user.id) }, ) if (blockedPreview) { diff --git a/app/api/user/favorites/route.ts b/app/api/user/favorites/route.ts index 140af8f7..db287d19 100644 --- a/app/api/user/favorites/route.ts +++ b/app/api/user/favorites/route.ts @@ -1,4 +1,4 @@ -import { buildMaterialViewWhere } from '@/lib/task-detail-privacy'; +import { buildMaterialPrivacyWhere } from '@/lib/task-detail-privacy'; import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; @@ -47,7 +47,7 @@ export async function GET(request: NextRequest) { const favorites = await prisma.materialFavorite.findMany({ where: { userId, - material: await buildMaterialViewWhere(userId), + material: await buildMaterialPrivacyWhere(userId), }, include: { material: { diff --git a/app/api/user/materials/route.ts b/app/api/user/materials/route.ts index 25db02a5..f6cfd668 100644 --- a/app/api/user/materials/route.ts +++ b/app/api/user/materials/route.ts @@ -1,4 +1,4 @@ -import { buildMaterialViewWhere } from '@/lib/task-detail-privacy'; +import { buildMaterialPrivacyWhere } from '@/lib/task-detail-privacy'; import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; @@ -43,7 +43,7 @@ export async function GET(request: NextRequest) { const materials = await prisma.material.findMany({ where: { userId, - AND: [await buildMaterialViewWhere(userId)], + AND: [await buildMaterialPrivacyWhere(userId)], deletedAt: null, }, select: { @@ -107,7 +107,7 @@ export async function GET(request: NextRequest) { const materials = await prisma.material.findMany({ where: { userId, - AND: [await buildMaterialViewWhere(userId)], + AND: [await buildMaterialPrivacyWhere(userId)], }, select: { id: true, diff --git a/app/assets/material-library/page.tsx b/app/assets/material-library/page.tsx index 9244d1dc..70c20b49 100644 --- a/app/assets/material-library/page.tsx +++ b/app/assets/material-library/page.tsx @@ -1492,7 +1492,7 @@ export default function MaterialLibraryPage() { setShowSkuEditModal(true)} onSelectionChange={setSelectedSkuIds} refreshKey={skuListRefreshKey} @@ -2160,7 +2157,7 @@ export default function SharePage() { )} {/* 操作按钮 - 统一样式 */} - {!material.isViewOnly &&
+
{material.materialType === '标准模型' ? (
} +
diff --git a/components/SkuList.tsx b/components/SkuList.tsx index b3573ae4..f2ef3a8c 100644 --- a/components/SkuList.tsx +++ b/components/SkuList.tsx @@ -41,13 +41,12 @@ interface Sku { interface SkuListProps { materialId: string canEdit: boolean - canDownload?: boolean onEditClick: () => void onSelectionChange?: (selectedIds: Set) => void refreshKey?: number } -export default function SkuList({ materialId, canEdit, canDownload = true, onEditClick, onSelectionChange, refreshKey = 0 }: SkuListProps) { +export default function SkuList({ materialId, canEdit, onEditClick, onSelectionChange, refreshKey = 0 }: SkuListProps) { const [skus, setSkus] = useState([]) const [loading, setLoading] = useState(true) const { toast, showToast } = useToast() @@ -207,7 +206,7 @@ export default function SkuList({ materialId, canEdit, canDownload = true, onEdi )} - {canDownload &&
+
} +
{skus.map((sku) => ( @@ -228,12 +227,12 @@ export default function SkuList({ materialId, canEdit, canDownload = true, onEdi key={sku.id} className="flex items-center gap-2 rounded-[8px] border border-[#e7e8ea] p-2 transition-colors hover:bg-[#fafafa]" > - {canDownload && handleSelectSku(sku.id)} className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 flex-shrink-0" - />} + />

{sku.skuName}

- 模型:{(canDownload ? sku.zipFilePath : sku.zipFileName)?.trim() ? '已上传' : '未上传'} + 模型:{sku.zipFilePath && sku.zipFilePath.trim() ? '已上传' : '未上传'} · 白底图:{sku.whiteBackgroundUploaded ? `${sku.whiteBackgroundCount || 0}张` : '未上传'} {sku.whiteBackgroundUploaded && sku.whiteBackgroundComplete === false ? '(未补齐)' : ''} @@ -269,7 +268,7 @@ export default function SkuList({ materialId, canEdit, canDownload = true, onEdi )}

- {canDownload &&
+
-
} +
))} diff --git a/components/detail/ProductPackageDetail.tsx b/components/detail/ProductPackageDetail.tsx index d4ffe33f..b0e0b5a5 100644 --- a/components/detail/ProductPackageDetail.tsx +++ b/components/detail/ProductPackageDetail.tsx @@ -52,7 +52,6 @@ interface ProductPackageDetailProps { assets: Asset[]; }; canDownloadSource: boolean; - canDownload?: boolean; onDownload: (type: 'all' | 'main' | 'sku' | 'detail' | 'psd', assetId?: string) => Promise; onBack?: () => void; showToast?: (message: string, options?: { variant?: 'success' | 'error' }) => void; @@ -218,7 +217,6 @@ function AssetEditMenu({ export default function ProductPackageDetail({ material, canDownloadSource, - canDownload = true, onDownload, onBack, showToast, @@ -639,7 +637,7 @@ export default function ProductPackageDetail({ )} {/* 下载按钮组 */} - {canDownload &&
+

批量下载

{downloading && (
@@ -772,7 +770,7 @@ export default function ProductPackageDetail({ 下载全部
-
} +
{/* 返回上一页按钮 */} {onBack && ( diff --git a/components/home/MaterialCard.tsx b/components/home/MaterialCard.tsx index e646586d..b86f6041 100644 --- a/components/home/MaterialCard.tsx +++ b/components/home/MaterialCard.tsx @@ -18,7 +18,6 @@ import { // 素材类型定义 export interface Material { accessScope?: string - isViewOnly?: boolean id: string productModel: string @@ -66,14 +65,15 @@ export interface Material { } // 用户会话类型 -interface UserSession { +export interface MaterialCardUser { id: string role: string + isSuperAdmin?: boolean } interface MaterialCardProps { material: Material - session: { user: UserSession } | null + session: { user: MaterialCardUser } | null // 音频播放状态 playingAudioId: string | null audioElement: HTMLAudioElement | null @@ -161,10 +161,13 @@ export default function MaterialCard({ const [shouldLoadVideoPreview, setShouldLoadVideoPreview] = useState(false) // 判断素材类型 + const canManageLibrary = session?.user.isSuperAdmin || session?.user.role === 'ADMIN' const isVideoType = isVideoMaterialType(material.materialType) const videoDownloadAvailability = getVideoDownloadAvailability( material.materialType, material.createdAt, + new Date(), + { bypassWaitPeriod: session?.user.isSuperAdmin }, ) const isDownloadWaiting = !videoDownloadAvailability.allowed const isPreviewWaiting = isVideoType && (material.videoPreviewBlocked || isDownloadWaiting) @@ -172,7 +175,7 @@ export default function MaterialCard({ material.videoPreviewUnavailableMessage || getVideoDownloadUnavailableMessage(videoDownloadAvailability).replace('下载', '查看详情') const isDownloadDisabled = downloadingId === material.id - const isAudioMaterial = material.materialType === '音乐' && !material.isViewOnly + const isAudioMaterial = material.materialType === '音乐' // 视频预览URL const videoPreviewUrl = isPreviewWaiting ? null : material.videoPreviewClipUrl || material.videoPreview540Url @@ -316,7 +319,7 @@ export default function MaterialCard({ >
{label} - {!material.isViewOnly && status === 'failed' && !material.videoPreviewClipUrl && !material.videoPreview540Url && ( + {status === 'failed' && !material.videoPreviewClipUrl && !material.videoPreview540Url && ( } + - {!material.isViewOnly && } - {!material.isViewOnly && session?.user && ( + + {session?.user && ( )} - {!material.isViewOnly && session?.user && (material.userId === session.user.id || session.user.role === 'ADMIN') && ( + {session?.user && (material.userId === session.user.id || canManageLibrary) && ( )} - {!material.isViewOnly && session?.user && session.user.role === 'ADMIN' && ( + {canManageLibrary && (