diff --git a/furniture-3d-system/src/app/api/auth/register/route.test.ts b/furniture-3d-system/src/app/api/auth/register/route.test.ts index 7e9e6362..36881c10 100644 --- a/furniture-3d-system/src/app/api/auth/register/route.test.ts +++ b/furniture-3d-system/src/app/api/auth/register/route.test.ts @@ -164,6 +164,39 @@ describe('POST /api/auth/register', () => { ) }) + it('工号为空时创建待激活普通账号并等待管理员手动设置权限', async () => { + mockCreateUser.mockResolvedValue({ + id: 'user-external', + email: 'external@example.com', + name: '外部录入', + role: 'USER', + businessRoles: [], + employeeDirectoryId: null, + }) + + const response = await POST(createRequest({ + email: 'external@example.com', + name: '外部录入', + jobNumber: '', + password: 'abc12345', + })) + + expect(mockMatchByJobNumber).not.toHaveBeenCalled() + expect(mockFindByEmployeeIdentity).not.toHaveBeenCalled() + expect(mockResolveDepartmentIdByExternalId).not.toHaveBeenCalled() + expect(mockCreateUser).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'external@example.com', + name: '外部录入', + role: 'USER', + businessRoles: [], + status: 'PENDING', + isFirstLogin: false, + }), + ) + expect(response).toEqual(expect.objectContaining({ success: true, status: 201 })) + }) + it('姓名与员工库不一致时拒绝注册,不绑定员工身份', async () => { mockMatchByJobNumber.mockResolvedValue({ id: 'emp-1', diff --git a/furniture-3d-system/src/app/api/auth/register/route.ts b/furniture-3d-system/src/app/api/auth/register/route.ts index 0abc948a..d8bc06e9 100644 --- a/furniture-3d-system/src/app/api/auth/register/route.ts +++ b/furniture-3d-system/src/app/api/auth/register/route.ts @@ -21,7 +21,7 @@ interface RegisterRequest { email: string name: string password: string - jobNumber: string + jobNumber?: string // role 参数被忽略,注册只允许创建 USER 角色 departmentId?: string teamId?: string @@ -40,66 +40,83 @@ export async function POST(request: NextRequest) { request, async (ctx) => { const body: RegisterRequest = await request.json() - const { email, name, password, jobNumber } = body - - const employee = await externalEmployeeDirectoryService.matchByJobNumber(jobNumber) - if (!employee || employee.sourceStatus !== 'EMPLOYED') { - throw new AppError( - ErrorCode.VALIDATION_FAILED, - GENERIC_REGISTER_REVIEW_MESSAGE - ) - } + const { email, name, password } = body + const jobNumber = typeof body.jobNumber === 'string' ? body.jobNumber.trim() : '' const submittedName = normalizePersonName(name) - const employeeName = normalizePersonName(employee.name) - if (submittedName !== employeeName) { - throw new AppError( - ErrorCode.VALIDATION_FAILED, - GENERIC_REGISTER_REVIEW_MESSAGE + let canAutoActivate = false + let user: Awaited> + + if (jobNumber) { + const employee = await externalEmployeeDirectoryService.matchByJobNumber(jobNumber) + if (!employee || employee.sourceStatus !== 'EMPLOYED') { + throw new AppError( + ErrorCode.VALIDATION_FAILED, + GENERIC_REGISTER_REVIEW_MESSAGE + ) + } + + const employeeName = normalizePersonName(employee.name) + if (submittedName !== employeeName) { + throw new AppError( + ErrorCode.VALIDATION_FAILED, + GENERIC_REGISTER_REVIEW_MESSAGE + ) + } + + const existingEmployeeUser = await userService.findByEmployeeIdentity({ + employeeDirectoryId: employee.id, + dingtalkUserId: employee.dingtalkUserId, + dingtalkUnionId: employee.dingtalkUnionId, + }) + if (existingEmployeeUser) { + throw new AppError( + ErrorCode.VALIDATION_FAILED, + GENERIC_REGISTER_REVIEW_MESSAGE + ) + } + + const departmentId = await externalEmployeeDirectoryService.resolveDepartmentIdByExternalId( + employee.primaryDepartmentExternalId ) + const department = departmentId + ? await departmentService.findById(departmentId).catch(() => null) + : null + const provisioning = resolveEmployeeAccountProvisioning({ + sourceStatus: employee.sourceStatus, + departmentFullPath: department?.fullPath, + }) + canAutoActivate = provisioning.canAutoActivate + + // 调用服务层创建用户 + // 强制使用 USER 角色,忽略用户传入的 role 参数,防止权限提升攻击 + user = await userService.create({ + email, + name: employeeName, + password, + role: REGISTER_DEFAULT_ROLE, + businessRoles: provisioning.businessRoles, + status: provisioning.status, + departmentId: departmentId ?? undefined, + isFirstLogin: false, + phone: employee.mobile ?? undefined, + employeeDirectoryId: employee.id, + dingtalkUserId: employee.dingtalkUserId, + dingtalkUnionId: employee.dingtalkUnionId, + dingtalkBoundAt: employee.dingtalkUserId || employee.dingtalkUnionId ? new Date() : null, + }) + } else { + user = await userService.create({ + email, + name: submittedName, + password, + role: REGISTER_DEFAULT_ROLE, + businessRoles: [], + status: 'PENDING', + isFirstLogin: false, + }) } - const existingEmployeeUser = await userService.findByEmployeeIdentity({ - employeeDirectoryId: employee.id, - dingtalkUserId: employee.dingtalkUserId, - dingtalkUnionId: employee.dingtalkUnionId, - }) - if (existingEmployeeUser) { - throw new AppError( - ErrorCode.VALIDATION_FAILED, - GENERIC_REGISTER_REVIEW_MESSAGE - ) - } - - const departmentId = await externalEmployeeDirectoryService.resolveDepartmentIdByExternalId( - employee.primaryDepartmentExternalId - ) - const department = departmentId - ? await departmentService.findById(departmentId).catch(() => null) - : null - const provisioning = resolveEmployeeAccountProvisioning({ - sourceStatus: employee.sourceStatus, - departmentFullPath: department?.fullPath, - }) - - // 调用服务层创建用户 - // 强制使用 USER 角色,忽略用户传入的 role 参数,防止权限提升攻击 - const user = await userService.create({ - email, - name: employeeName, - password, - role: REGISTER_DEFAULT_ROLE, - businessRoles: provisioning.businessRoles, - status: provisioning.status, - departmentId: departmentId ?? undefined, - isFirstLogin: false, - phone: employee.mobile ?? undefined, - employeeDirectoryId: employee.id, - dingtalkUserId: employee.dingtalkUserId, - dingtalkUnionId: employee.dingtalkUnionId, - dingtalkBoundAt: employee.dingtalkUserId || employee.dingtalkUnionId ? new Date() : null, - }) - // 记录操作日志 await logService.create({ userId: user.id, @@ -115,7 +132,7 @@ export async function POST(request: NextRequest) { return jsonSuccess( { - message: provisioning.canAutoActivate ? '注册成功,账号已自动激活' : '注册成功,请等待管理员激活', + message: canAutoActivate ? '注册成功,账号已自动激活' : '注册成功,请等待管理员激活', user, }, 201, diff --git a/furniture-3d-system/src/app/draft-groups/DraftGroupsFormalClient.tsx b/furniture-3d-system/src/app/draft-groups/DraftGroupsFormalClient.tsx index e2e643cc..ef6b3cc2 100644 --- a/furniture-3d-system/src/app/draft-groups/DraftGroupsFormalClient.tsx +++ b/furniture-3d-system/src/app/draft-groups/DraftGroupsFormalClient.tsx @@ -33,6 +33,7 @@ import { createEmptyNonIntegratedPatentApplication, createEmptyNonIntegratedProjectReview, NON_INTEGRATED_DEFAULT_BRAND, + NON_INTEGRATED_MODULAR_PRODUCT_TAG, NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY, NON_INTEGRATED_PRODUCTS_UPDATED, type NonIntegratedSkuComponentDetail, @@ -754,6 +755,7 @@ function buildDraftGroupSeriesProductRecord({ sourceSeriesCode: draftGroup.assetSeriesCode?.trim().toUpperCase() || '', source: 'draft_group_modular_series', }, + productTag: NON_INTEGRATED_MODULAR_PRODUCT_TAG, spu: normalizedSeriesSpu, projectStatus: 'approved', projectReview: createEmptyNonIntegratedProjectReview(), @@ -3071,6 +3073,9 @@ function updateFormalProductSkuItems( const batchKey = String(batchNo) return { ...product, + productTag: Object.prototype.hasOwnProperty.call(product, 'productTag') + ? product.productTag ?? '' + : NON_INTEGRATED_MODULAR_PRODUCT_TAG, skuItems, skuBatchCount: Math.max(product.skuBatchCount ?? 1, batchNo), skuBatchFlowNos: { diff --git a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx index b639d864..e452f73c 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx @@ -264,6 +264,7 @@ import { NON_INTEGRATED_DUPLICATE_SPU_MESSAGE, NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES, NON_INTEGRATED_PLANNING_LAYOUT_WORKSPACE_CATEGORY, + NON_INTEGRATED_PRODUCT_TAG_OPTIONS, NON_INTEGRATED_WORKFLOW_NODE_CATEGORIES, getNonIntegratedFormalSpu, getNonIntegratedProductById, @@ -6650,6 +6651,7 @@ function buildProductPayload( createdBy?: string, updatedBy?: string, productDevelopmentKind: ProductDevelopmentKind = "product", + productTag = "", metadata?: Record, ): NonIntegratedProductRecord { const now = new Date().toISOString(); @@ -6901,6 +6903,7 @@ function buildProductPayload( id, productDevelopmentKind, ...(metadata && Object.keys(metadata).length > 0 ? { metadata } : {}), + productTag: productTag.trim(), projectStatus, projectReview: normalizeNonIntegratedProjectReview(projectReview), openingReference: normalizedOpeningReference, @@ -7182,6 +7185,11 @@ export default function NonIntegratedDetailClient({ const [formValues, setFormValues] = useState(() => createFormValuesFromProduct(existingProduct), ); + const [productTag, setProductTag] = useState(() => + Object.prototype.hasOwnProperty.call(existingProduct ?? {}, "productTag") + ? existingProduct?.productTag?.trim() ?? "" + : "", + ); const formValuesRef = useRef(formValues); useEffect(() => { formValuesRef.current = formValues; @@ -12187,6 +12195,7 @@ export default function NonIntegratedDetailClient({ existingProduct?.createdBy, currentUserNameNormalized || currentUserIdNormalized || undefined, effectiveProductDevelopmentKind, + productTag, nextProductMetadata, ), ); @@ -12279,6 +12288,7 @@ export default function NonIntegratedDetailClient({ productFirstQualityRows, productLimitRows, productPricingRows, + productTag, productFinishedSampleShipmentRows, productSampleMeasurementRows, projectReview, @@ -12461,6 +12471,13 @@ export default function NonIntegratedDetailClient({ }, AUTO_SAVE_DELAY_MS); } + function updateProductTag(value: string) { + if (isReadOnly) return; + setProductTag(value); + setSaveMessage(""); + setSaveMessageType("success"); + } + function handleTextInputCompositionStart(_: CompositionEvent) { isTextInputCompositionActiveRef.current = true; @@ -12789,6 +12806,7 @@ export default function NonIntegratedDetailClient({ productLimitRows, productPricingRows, productSampleMeasurementRows, + productTag, projectReview, projectStatus, qualityInspectionSheet, @@ -18429,6 +18447,7 @@ export default function NonIntegratedDetailClient({ function syncAiWhiteBackgroundSavedProduct(product: NonIntegratedProductRecord) { setUploadFiles(product.uploadFiles ?? []); setProductMetadata(product.metadata); + setProductTag(product.productTag?.trim() ?? ""); if (shouldUseRemoteAssetStorage()) { window.dispatchEvent(new Event(NON_INTEGRATED_PRODUCTS_UPDATED)); @@ -19909,6 +19928,7 @@ export default function NonIntegratedDetailClient({ currentUserNameNormalized || currentUserIdNormalized || undefined, currentUserNameNormalized || currentUserIdNormalized || undefined, effectiveProductDevelopmentKind, + productTag, seriesComponentMetadata ?? seriesProductMetadata, ), { attachToExistingProduct: false }, @@ -26584,6 +26604,30 @@ function renderFactoryQualityWorkspace() { description="当前还没有上传产品主图。" /> )} +
+ +
+ + +
+
diff --git a/furniture-3d-system/src/app/intake/non-integrated/nonIntegratedStorage.ts b/furniture-3d-system/src/app/intake/non-integrated/nonIntegratedStorage.ts index 8d9d79db..496ca4bf 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/nonIntegratedStorage.ts +++ b/furniture-3d-system/src/app/intake/non-integrated/nonIntegratedStorage.ts @@ -23,6 +23,10 @@ import { normalizeNonIntegratedAssetUrl } from "./nonIntegratedAssetUrl"; export const NON_INTEGRATED_PRODUCTS_UPDATED = "intake:nonIntegratedProductsUpdated"; +export const NON_INTEGRATED_MODULAR_PRODUCT_TAG = "模块化"; +export const NON_INTEGRATED_PRODUCT_TAG_OPTIONS = [ + NON_INTEGRATED_MODULAR_PRODUCT_TAG, +] as const; export const NON_INTEGRATED_DUPLICATE_SPU_MESSAGE = "SPU 已存在,不能重复新建。删除原记录后才可以重新新建。"; export const NON_INTEGRATED_DEFAULT_BRAND = "其他品牌-普通大货"; @@ -655,6 +659,7 @@ export type NonIntegratedProductRecord = { id: string; productDevelopmentKind?: "product" | "series"; metadata?: Record; + productTag?: string; spu: string; projectStatus: NonIntegratedProjectStatus; projectReview: NonIntegratedProjectReview; @@ -1187,6 +1192,18 @@ function normalizeMetadataRecord(value: unknown) { : undefined; } +function shouldDefaultModularProductTag( + metadata: Record | undefined, +) { + if (!metadata) return false; + + return Boolean( + typeof metadata.modularSpu === "string" || + metadata.source === "draft_group_modular_series" || + typeof metadata.componentDetailType === "string", + ); +} + function normalizeTupleMap( value: unknown, tupleLength: TLength, @@ -2229,12 +2246,20 @@ function toProductRecord( normalizedPreviewImage, normalizedPreviewImageName, ); + const metadata = normalizeMetadataRecord(item.metadata); + const hasProductTag = Object.prototype.hasOwnProperty.call(item, "productTag"); + const productTag = hasProductTag + ? normalizeString(item.productTag) + : shouldDefaultModularProductTag(metadata) + ? NON_INTEGRATED_MODULAR_PRODUCT_TAG + : undefined; return { id: item.id, productDevelopmentKind: item.productDevelopmentKind === "series" ? "series" : "product", - metadata: normalizeMetadataRecord(item.metadata), + metadata, + ...(productTag !== undefined ? { productTag } : {}), spu: item.spu, projectStatus: normalizeNonIntegratedProjectStatus(item.projectStatus), projectReview: normalizeNonIntegratedProjectReview(item.projectReview), diff --git a/furniture-3d-system/src/app/register/page.tsx b/furniture-3d-system/src/app/register/page.tsx index 74331fe1..4b7af995 100644 --- a/furniture-3d-system/src/app/register/page.tsx +++ b/furniture-3d-system/src/app/register/page.tsx @@ -166,11 +166,10 @@ export default function RegisterPage() { label="工号" id="jobNumber" type="text" - placeholder="请输入员工工号" + placeholder="内部员工可填写工号,外部人员可不填" value={jobNumber} onChange={(event) => setJobNumber(event.target.value)} - required - helperText="系统会用工号匹配员工库,匹配成功后仍需管理员激活" + helperText="填写工号会匹配员工库;不填写工号时,账号提交后等待管理员激活" variant="filled" size="large" containerClassName={authFieldClass} diff --git a/furniture-3d-system/src/server/services/asset-center.service.ts b/furniture-3d-system/src/server/services/asset-center.service.ts index 60d48aa2..e3215ab0 100644 --- a/furniture-3d-system/src/server/services/asset-center.service.ts +++ b/furniture-3d-system/src/server/services/asset-center.service.ts @@ -34,6 +34,7 @@ import { } from '@/lib/open-product/asset-coding' import { NON_INTEGRATED_DEFAULT_BRAND, + NON_INTEGRATED_MODULAR_PRODUCT_TAG, buildNonIntegratedProductId, createDefaultDocumentWorkspaceConfirmations, createEmptyNonIntegratedOpeningReference, @@ -1469,6 +1470,7 @@ function buildSeriesComponentDetailProductPayload(input: { componentSpu: spu, finishedProductSpu, }, + productTag: NON_INTEGRATED_MODULAR_PRODUCT_TAG, spu, projectStatus: 'draft', projectReview: createEmptyNonIntegratedProjectReview(), @@ -1575,6 +1577,7 @@ function buildSeriesComponentDetailProductPayloadFromExistingRecord(input: { }): Record { const payload = isRecord(input.record.payload) ? input.record.payload : {} const metadata = isRecord(payload.metadata) ? payload.metadata : {} + const hasProductTag = Object.prototype.hasOwnProperty.call(payload, 'productTag') const openingReference = isRecord(payload.openingReference) ? payload.openingReference : {} const spu = resolveSeriesComponentSpu(input.series) const finishedProductSpu = resolveSeriesFinishedProductSpu(input.series) @@ -1610,6 +1613,11 @@ function buildSeriesComponentDetailProductPayloadFromExistingRecord(input: { componentSpu: spu, finishedProductSpu, }, + productTag: hasProductTag + ? typeof payload.productTag === 'string' + ? payload.productTag + : '' + : NON_INTEGRATED_MODULAR_PRODUCT_TAG, spu: persistedSpu, productCode: spu, openingReference: { diff --git a/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.test.ts b/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.test.ts index 5d5f6a55..5f114038 100644 --- a/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.test.ts +++ b/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.test.ts @@ -105,11 +105,13 @@ function createMeasurementRow( function createProduct( skuItems: NonIntegratedSkuItem[], pricingRows = skuItems.map((sku) => createPricingRow(sku)), + productTag = '', ): JushuitanProductPricingPushInput['product'] { return { id: 'product-1', spu: 'GBGJ-CS0002', brand: '顾家', + productTag, productCategory: '茶几', productionType: '成品', skuItems, @@ -202,6 +204,62 @@ describe('JushuitanProductPricingPushService', () => { properties_value: '胡桃色;140/60/78', }, ]) + expect( + (readBiz(fetchMock, itemSkuCallIndex).items as Array>)[0], + ).not.toHaveProperty('labels') + }) + + it('商品标签有值时普通商品资料的每个 SKU 都推送 labels', async () => { + const fetchMock = createSuccessFetchMock() + global.fetch = fetchMock as unknown as typeof fetch + + const skuA = createSku({ + id: 'normal-a', + skuCode: 'CS000201', + barcode: 'CS000201N1280V01', + }) + const skuB = createSku({ + id: 'normal-b', + skuCode: 'CS000202', + barcode: 'CS000202N1280V01', + }) + + await new JushuitanProductPricingPushService().pushProductPricing({ + product: createProduct([skuA, skuB], undefined, '模块化'), + targetSkuIds: [skuA.id, skuB.id], + }) + + const itemSkuCallIndex = findCallIndex(fetchMock, '/open/jushuitan/itemsku/upload') + expect(readBiz(fetchMock, itemSkuCallIndex).items).toMatchObject([ + { + sku_id: skuA.barcode, + labels: ['模块化'], + }, + { + sku_id: skuB.barcode, + labels: ['模块化'], + }, + ]) + }) + + it('商品标签清空时不推 labels,也不使用旧核价行标签兜底', async () => { + const fetchMock = createSuccessFetchMock() + global.fetch = fetchMock as unknown as typeof fetch + + const normalSku = createSku({ id: 'normal-sku' }) + await new JushuitanProductPricingPushService().pushProductPricing({ + product: createProduct( + [normalSku], + [{ ...createPricingRow(normalSku), productTag: '旧标签' }], + '', + ), + targetSkuIds: [normalSku.id], + }) + + const itemSkuCallIndex = findCallIndex(fetchMock, '/open/jushuitan/itemsku/upload') + expect( + (readBiz(fetchMock, itemSkuCallIndex).items as Array>)[0], + ).not.toHaveProperty('labels') }) it('成本价按系统原值推送,不再除以 0.9', async () => { @@ -619,7 +677,7 @@ describe('JushuitanProductPricingPushService', () => { }) const result = await new JushuitanProductPricingPushService().pushProductPricing({ - product: createProduct([comboA, comboB, comboC]), + product: createProduct([comboA, comboB, comboC], undefined, '模块化'), targetSkuIds: [comboA.id], }) @@ -638,14 +696,17 @@ describe('JushuitanProductPricingPushService', () => { { sku_id: comboA.barcode, properties_value: '胡桃色;140/60/78', + labels: ['模块化'], }, { sku_id: comboB.barcode, properties_value: '胡桃色;140/60/78', + labels: ['模块化'], }, { sku_id: comboC.barcode, properties_value: '胡桃色;140/60/78', + labels: ['模块化'], }, ]) expect(readBiz(fetchMock, combineSkuCallIndex)).toMatchObject( @@ -654,7 +715,7 @@ describe('JushuitanProductPricingPushService', () => { i_id: 'GBGJ-CS000201', name: 'GBGJ-款式一 140/60/78 胡桃色', properties_value: '胡桃色;140/60/78', - labels: '双包/多包', + labels: ['模块化'], items: [ { src_sku_id: comboA.barcode, qty: 1, sale_price: 0 }, { src_sku_id: comboB.barcode, qty: 1, sale_price: 0 }, @@ -702,7 +763,7 @@ describe('JushuitanProductPricingPushService', () => { }) const result = await new JushuitanProductPricingPushService().pushProductPricing({ - product: createProduct([combineSku]), + product: createProduct([combineSku], undefined, '模块化'), targetSkuIds: [combineSku.id], }) @@ -721,7 +782,7 @@ describe('JushuitanProductPricingPushService', () => { sku_id: 'MKH-DNZ0384260600002', i_id: 'MKH-DNZ0384260600002', name: 'MKH-DNZ0384260600002', - labels: '模块化组合', + labels: ['模块化'], items: [ { src_sku_id: 'DNZ038502N1437V01', qty: 1, sale_price: 0 }, { src_sku_id: 'DNZ038401N1440V01', qty: 1, sale_price: 0 }, diff --git a/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.ts b/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.ts index 81023e24..fee93471 100644 --- a/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.ts +++ b/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.ts @@ -21,6 +21,7 @@ type JushuitanProductPricingPushProduct = { id: string spu: string brand: string + productTag?: string productCategory?: string productionType?: string openingReference?: { @@ -84,7 +85,7 @@ type JushuitanCombineItem = { i_id: string name: string properties_value: string - labels: string + labels?: string[] enabled: string items: Array<{ src_sku_id: string @@ -664,6 +665,7 @@ function buildNormalItem( product: JushuitanProductPricingPushProduct, row: NormalizedPushRow, ) { + const productTag = resolveProductTag(product) const item: Record = { sku_id: requireText(row.sku.barcode, '条码'), i_id: requireText(product.spu, 'SPU'), @@ -677,6 +679,7 @@ function buildNormalItem( setIfText(item, 'other_10', row.sku.material) setIfText(item, 'other_8', row.sku.setCount || '1') setIfText(item, 'brand', product.brand) + if (productTag) item.labels = [productTag] setIfText(item, 'properties_value', buildPropertiesValue(row.sku)) setIfText(item, 'other_1', `${row.sku.model.trim()}${row.factoryRow.supplierCode.trim()}`) setIfNumber(item, 'l', row.measurementRow?.lengthCm) @@ -1105,7 +1108,22 @@ function resolveComponentQuantity(component: { quantity?: string }) { return quantity && quantity > 0 ? quantity : 1 } -function buildModularCombineItems(rows: NormalizedPushRow[]): JushuitanCombineItem[] { +function resolveProductTag(product: JushuitanProductPricingPushProduct) { + if (Object.prototype.hasOwnProperty.call(product, 'productTag')) { + return product.productTag?.trim() ?? '' + } + + return firstNonEmpty( + product.pricingRows.map((row) => row.productTag?.trim() ?? ''), + ) +} + +function buildModularCombineItems( + product: JushuitanProductPricingPushProduct, + rows: NormalizedPushRow[], +): JushuitanCombineItem[] { + const productTag = resolveProductTag(product) + return rows .filter((row) => hasModularCombineComponents(row.sku)) .map((row) => { @@ -1127,7 +1145,7 @@ function buildModularCombineItems(rows: NormalizedPushRow[]): JushuitanCombineIt i_id: requireText(combineSkuId, '组合款式编码'), name: requireText(combineSkuId, '组合商品名称'), properties_value: '', - labels: '模块化组合', + ...(productTag ? { labels: [productTag] } : {}), enabled: '启用', items, } @@ -1138,7 +1156,8 @@ function buildCombineItems( product: JushuitanProductPricingPushProduct, rows: NormalizedPushRow[], ): JushuitanCombineItem[] { - const modularItems = buildModularCombineItems(rows) + const productTag = resolveProductTag(product) + const modularItems = buildModularCombineItems(product, rows) const rowBySkuId = new Map(rows.map((row) => [row.sku.id, row])) const packageGroupKeys = new Set( rows @@ -1181,7 +1200,7 @@ function buildCombineItems( i_id: requireText(combineSkuId, '组合款式编码'), name: requireText(combineName, '组合商品名称'), properties_value: buildPropertiesValue(firstSku), - labels: '双包/多包', + ...(productTag ? { labels: [productTag] } : {}), enabled: '启用', items: groupSkus.map((sku) => ({ src_sku_id: requireText(sku.barcode, '子商品编码'),