fix: support product tag push and optional register job number

This commit is contained in:
WuYongJie 2026-06-09 17:08:50 +08:00
parent 007dd0a986
commit b829efbaab
9 changed files with 281 additions and 70 deletions

View File

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

View File

@ -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<ReturnType<typeof userService.create>>
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,

View File

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

View File

@ -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<string, unknown>,
): 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>(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<HTMLElement>) {
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="当前还没有上传产品主图。"
/>
)}
<div className="mt-3 rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-2">
<FieldLabel label="商品标签" />
<div className="flex items-center gap-2">
<InlineSelect
ariaLabel="商品标签"
value={productTag}
options={NON_INTEGRATED_PRODUCT_TAG_OPTIONS}
wrapperClassName="min-w-0 flex-1"
readOnly={isReadOnly}
onChange={updateProductTag}
buttonClassName="h-8 w-full rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-2.5 text-xs text-apple-label-primary outline-none disabled:cursor-not-allowed disabled:bg-apple-bg-primary disabled:text-apple-label-secondary"
/>
<button
type="button"
aria-label="清空商品标签"
title="清空商品标签"
onClick={() => updateProductTag("")}
disabled={isReadOnly || !productTag.trim()}
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-apple-lg border border-apple-separator bg-apple-bg-secondary text-apple-label-secondary transition hover:bg-apple-bg-tertiary hover:text-apple-label-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-apple-bg-secondary disabled:hover:text-apple-label-secondary"
>
<X className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div className="space-y-3">
<div className="grid gap-2 md:grid-cols-3 xl:grid-cols-5">

View File

@ -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<string, unknown>;
productTag?: string;
spu: string;
projectStatus: NonIntegratedProjectStatus;
projectReview: NonIntegratedProjectReview;
@ -1187,6 +1192,18 @@ function normalizeMetadataRecord(value: unknown) {
: undefined;
}
function shouldDefaultModularProductTag(
metadata: Record<string, unknown> | undefined,
) {
if (!metadata) return false;
return Boolean(
typeof metadata.modularSpu === "string" ||
metadata.source === "draft_group_modular_series" ||
typeof metadata.componentDetailType === "string",
);
}
function normalizeTupleMap<TLength extends 3 | 4>(
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),

View File

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

View File

@ -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<string, unknown> {
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: {

View File

@ -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<Record<string, unknown>>)[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<Record<string, unknown>>)[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 },

View File

@ -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<string, unknown> = {
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, '子商品编码'),