fix(intake): allow merchandisers to download factory files

This commit is contained in:
WuYongJie 2026-06-11 12:43:06 +08:00
parent d5aba614fe
commit 5745abf0ae
5 changed files with 105 additions and 50 deletions

View File

@ -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(

View File

@ -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(

View File

@ -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 = [
{

View File

@ -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',

View File

@ -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) {