diff --git a/furniture-3d-system/src/app/api/intake/non-integrated/[id]/contracts/route.ts b/furniture-3d-system/src/app/api/intake/non-integrated/[id]/contracts/route.ts index f5dcca7b..f5488608 100644 --- a/furniture-3d-system/src/app/api/intake/non-integrated/[id]/contracts/route.ts +++ b/furniture-3d-system/src/app/api/intake/non-integrated/[id]/contracts/route.ts @@ -7,28 +7,15 @@ import { normalizeProductFactoryContractBatch, normalizeProductFactoryContractType, } from "@/server/services/intake-contract-docx.service"; -import { isIntakeNonIntegratedSupplyChainUser } from "@/lib/non-integrated-permissions"; -import type { AuthUser } from "@/server/core/types"; export const runtime = "nodejs"; +const PRODUCT_FACTORY_WORKSPACE_CATEGORY = "产品定厂"; + type RouteContext = { params: Promise<{ id: string }>; }; -function canDownloadProductFactoryContract(user: AuthUser) { - if (user.role === "ADMIN" || user.role === "SUPER_ADMIN") return true; - - return isIntakeNonIntegratedSupplyChainUser({ - id: user.id, - name: user.name, - role: user.role, - businessRoles: user.businessRoles, - department: user.department, - departmentPaths: user.departmentPaths, - }); -} - function buildContentDisposition(fileName: string) { const fallbackName = fileName.replace(/[^\x20-\x7E]+/g, "_"); return `attachment; filename="${fallbackName}"; filename*=UTF-8''${encodeURIComponent( @@ -40,15 +27,6 @@ export async function GET(request: NextRequest, context: RouteContext) { return handleRequest( request, async (ctx) => { - if (!canDownloadProductFactoryContract(ctx.user)) { - return jsonError( - ErrorCode.AUTH_PERMISSION_DENIED, - "当前账号没有下载产品定厂合同的权限", - 403, - ctx.requestId, - ); - } - const { id } = await context.params; const productId = decodeURIComponent(id); const searchParams = request.nextUrl.searchParams; @@ -69,9 +47,10 @@ export async function GET(request: NextRequest, context: RouteContext) { } try { - const product = await intakeNonIntegratedProductService.getById( + const product = await intakeNonIntegratedProductService.getDownloadableById( productId, ctx.user, + PRODUCT_FACTORY_WORKSPACE_CATEGORY, ); if (!product) { return jsonError( diff --git a/furniture-3d-system/src/app/api/intake/non-integrated/[id]/order-sheet/route.ts b/furniture-3d-system/src/app/api/intake/non-integrated/[id]/order-sheet/route.ts index 3cba193c..ebf9bda4 100644 --- a/furniture-3d-system/src/app/api/intake/non-integrated/[id]/order-sheet/route.ts +++ b/furniture-3d-system/src/app/api/intake/non-integrated/[id]/order-sheet/route.ts @@ -6,28 +6,15 @@ import { createIntakeProductFactoryOrderSheetXlsx, normalizeProductFactoryOrderSheetBatch, } from "@/server/services/intake-order-sheet-xlsx.service"; -import { isIntakeNonIntegratedSupplyChainUser } from "@/lib/non-integrated-permissions"; -import type { AuthUser } from "@/server/core/types"; export const runtime = "nodejs"; +const PRODUCT_FACTORY_WORKSPACE_CATEGORY = "产品定厂"; + type RouteContext = { params: Promise<{ id: string }>; }; -function canDownloadProductFactoryOrderSheet(user: AuthUser) { - if (user.role === "ADMIN" || user.role === "SUPER_ADMIN") return true; - - return isIntakeNonIntegratedSupplyChainUser({ - id: user.id, - name: user.name, - role: user.role, - businessRoles: user.businessRoles, - department: user.department, - departmentPaths: user.departmentPaths, - }); -} - function buildContentDisposition(fileName: string) { const fallbackName = fileName.replace(/[^\x20-\x7E]+/g, "_"); return `attachment; filename="${fallbackName}"; filename*=UTF-8''${encodeURIComponent( @@ -39,15 +26,6 @@ export async function GET(request: NextRequest, context: RouteContext) { return handleRequest( request, async (ctx) => { - if (!canDownloadProductFactoryOrderSheet(ctx.user)) { - return jsonError( - ErrorCode.AUTH_PERMISSION_DENIED, - "当前账号没有下载产品定厂下单表的权限", - 403, - ctx.requestId, - ); - } - const { id } = await context.params; const productId = decodeURIComponent(id); const batch = normalizeProductFactoryOrderSheetBatch( @@ -55,9 +33,10 @@ export async function GET(request: NextRequest, context: RouteContext) { ); try { - const product = await intakeNonIntegratedProductService.getById( + const product = await intakeNonIntegratedProductService.getDownloadableById( productId, ctx.user, + PRODUCT_FACTORY_WORKSPACE_CATEGORY, ); if (!product) { return jsonError( diff --git a/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-product.service.test.ts b/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-product.service.test.ts index a12cc090..e40a11b4 100644 --- a/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-product.service.test.ts +++ b/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-product.service.test.ts @@ -846,6 +846,41 @@ describe('intakeNonIntegratedProductService', () => { expect(product?.spu).toBe('DNZ0001') }) + it('allows merchandiser users to get product factory files by download permission', async () => { + mockedRepository.findActiveById.mockResolvedValue( + createRecord({ + createdBy: 'creator-2', + departmentId: 'dept-2', + teamId: 'team-2', + }) as never, + ) + + const product = await intakeNonIntegratedProductService.getDownloadableById( + 'non-integrated-1', + { + ...baseUser, + id: 'merchandiser-user-id', + email: 'merchandiser@example.com', + name: '槐米', + businessRoles: [], + departmentId: 'merchandiser-dept', + teamId: 'merchandiser-team', + department: { + id: 'merchandiser-dept', + name: '跟单组', + fullPath: '江苏简尚品牌管理有限公司 > 供应链中心 > 品牌采购部 > 跟单组', + }, + departmentPaths: [ + '江苏简尚品牌管理有限公司 > 供应链中心 > 品牌采购部 > 跟单组', + ], + }, + '产品定厂', + ) + + expect(product).not.toBeNull() + expect(product?.spu).toBe('DNZ0001') + }) + it('hydrates pushed series SKU 3D model snapshots from the source pre SKU when reading details', async () => { const carriedNodes = [ { diff --git a/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-scope.service.test.ts b/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-scope.service.test.ts index 2dcafc34..faa7d7aa 100644 --- a/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-scope.service.test.ts +++ b/furniture-3d-system/src/server/services/__tests__/intake-non-integrated-scope.service.test.ts @@ -219,6 +219,45 @@ describe('IntakeNonIntegratedScopeService node permissions', () => { ).resolves.toBe(false) }) + it('allows merchandiser users to download product factory files', async () => { + const record = { + id: 'factory-download-record', + createdBy: 'designer-id', + designer: '设计同事', + operator: '', + purchaser: '采购同事', + visibility: 'DEPARTMENT', + departmentId: null, + teamId: null, + payload: {}, + profile: null, + } + const merchandiserUser: AuthUser = { + id: 'merchandiser-user-id', + email: 'merchandiser@example.local', + name: '槐米', + role: 'USER', + status: 'ACTIVE', + businessRoles: [], + department: { + id: 'merchandiser-dept', + name: '跟单组', + fullPath: '江苏简尚品牌管理有限公司 > 供应链中心 > 品牌采购部 > 跟单组', + }, + departmentPaths: [ + '江苏简尚品牌管理有限公司 > 供应链中心 > 品牌采购部 > 跟单组', + ], + } + + await expect( + service.canDownloadWorkspaceRecordWithSettings( + record, + merchandiserUser, + '产品定厂', + ), + ).resolves.toBe(true) + }) + it('allows space layout writes when only the space visual department name is available', async () => { const record = { id: 'stock-space-record', diff --git a/furniture-3d-system/src/server/services/intake-non-integrated-product.service.ts b/furniture-3d-system/src/server/services/intake-non-integrated-product.service.ts index 2374aeab..5ad7e8b0 100644 --- a/furniture-3d-system/src/server/services/intake-non-integrated-product.service.ts +++ b/furniture-3d-system/src/server/services/intake-non-integrated-product.service.ts @@ -5014,6 +5014,29 @@ class IntakeNonIntegratedProductService { return hydrateProductSkuModelSnapshots(hydratedProduct) } + async getDownloadableById( + id: string, + user: AuthUser, + category: string, + ) { + const record = await this.repository.findActiveById(id) + if (!record) { + return null + } + + await intakeNonIntegratedScopeService.assertCanDownloadWorkspaceRecord( + record, + user, + category, + ) + + const product = buildStructuredProductRecordFromRecord(record) + if (!product) return null + + const hydratedProduct = await hydrateSeriesProductDisplayName(product) + return hydrateProductSkuModelSnapshots(hydratedProduct) + } + async assertCanWrite(id: string, user: AuthUser, category?: string | null) { const existingRecord = await this.repository.findActiveById(id) if (!existingRecord) {