diff --git a/apps/backend/src/services/worker-platform/consignment-spec-service.test.ts b/apps/backend/src/services/worker-platform/consignment-spec-service.test.ts index e1da3180..5af24930 100644 --- a/apps/backend/src/services/worker-platform/consignment-spec-service.test.ts +++ b/apps/backend/src/services/worker-platform/consignment-spec-service.test.ts @@ -13,6 +13,7 @@ import { normalizeConsignmentSpecValues, normalizeConsignmentSpecs, parseConsignmentSpecValues, + resolveConsignmentPublishQuantity, resolveLegacyConsignmentColumns, resolveWorkerConsignmentPrice, type ConsignmentPricingConfig, @@ -511,6 +512,17 @@ test('合并迁移 147 必须包含动态规格的 JSON、匹配 key、文本与 } }) +test('发布数量:关闭「允许修改寄售数量」时固定为 1,开启时按 1~1000 取整', () => { + assert.equal(resolveConsignmentPublishQuantity(false, 5), 1, '关闭时必须固定 1') + assert.equal(resolveConsignmentPublishQuantity(false, undefined), 1, '关闭时缺省 1') + assert.equal(resolveConsignmentPublishQuantity(false, 0), 1, '关闭时 0 也回落 1') + assert.equal(resolveConsignmentPublishQuantity(true, 3), 3, '开启时按填写值') + assert.equal(resolveConsignmentPublishQuantity(true, 0), 1, '非法值回落 1') + assert.equal(resolveConsignmentPublishQuantity(true, Number.NaN), 1, 'NaN 回落 1') + assert.equal(resolveConsignmentPublishQuantity(true, 1001), 1000, '上限 1000') + assert.equal(resolveConsignmentPublishQuantity(true, 1.6), 2, '四舍五入') +}) + test('合并迁移 147 必须为苏尔南手枪写入已发布的五个单选字段配置', async () => { const sql = await readFile( new URL('../../db/migrations/147_consignment_platform.sql', import.meta.url), diff --git a/apps/backend/src/services/worker-platform/consignment-spec-service.ts b/apps/backend/src/services/worker-platform/consignment-spec-service.ts index 7ab569be..1ada05fd 100644 --- a/apps/backend/src/services/worker-platform/consignment-spec-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-spec-service.ts @@ -678,6 +678,20 @@ function isNumericSpecField(field: ConsignmentSpecField): boolean { return field.type === 'number' || field.key === 'diamondLevel' || field.key === 'brillianceCount' } +/** + * 发布数量口径:分类关闭「允许修改寄售数量」时固定为 1; + * 否则按 1~1000 取整,非法值回落 1(防止旧客户端/直接调接口绕过前端禁用)。 + */ +export function resolveConsignmentPublishQuantity( + allowQuantityEdit: boolean, + rawQuantity: unknown, +): number { + if (!allowQuantityEdit) return 1 + const parsed = Number(rawQuantity) + if (!Number.isFinite(parsed) || parsed < 1) return 1 + return Math.min(1000, Math.max(1, Math.round(parsed))) +} + /** 只取参与匹配的字段生成稳定 key(按字段 key 排序,避免字段顺序影响)。 */ export function buildConsignmentSpecKey( specs: ConsignmentSpecField[], diff --git a/apps/backend/src/services/worker-platform/consignment-worker-service.ts b/apps/backend/src/services/worker-platform/consignment-worker-service.ts index 008b8feb..f0f2b819 100644 --- a/apps/backend/src/services/worker-platform/consignment-worker-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-worker-service.ts @@ -47,6 +47,7 @@ import { computeConsignmentPrice, normalizeConsignmentIntakeValues, normalizeConsignmentSpecValues, + resolveConsignmentPublishQuantity, resolveLegacyConsignmentColumns, resolveWorkerConsignmentPrice, type ConsignmentSpecField, @@ -314,7 +315,11 @@ export async function publishWorkerConsignment(workerId: number, payload: JsonOb errorCode: 'consignment_profile_unavailable', }) } - const quantity = Math.max(1, Math.min(1000, Math.round(Number(payload.quantity || 1)))) + // 「允许修改寄售数量」关闭时固定 1 件:前端会禁用输入框,这里再兜一层防绕过。 + const quantity = resolveConsignmentPublishQuantity( + profile?.config.fields.quantity !== false, + payload.quantity, + ) if (profile?.config.inventoryLimit.enabled) { const activeQuantity = await countWorkerActiveConsignmentQuantity(workerId) if (activeQuantity + quantity > profile.config.inventoryLimit.maxActiveQuantity) { diff --git a/apps/frontend/src/pages/admin/panels/ConsignmentConfigPanel.tsx b/apps/frontend/src/pages/admin/panels/ConsignmentConfigPanel.tsx index 69755f0a..35500f35 100644 --- a/apps/frontend/src/pages/admin/panels/ConsignmentConfigPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ConsignmentConfigPanel.tsx @@ -1134,11 +1134,12 @@ export default function ConsignmentConfigPanel() { - + -
+
)} - {selectedProfile?.config.fields.quantity !== false ? ( - - - - ) : null} + + + { + assert.ok(panel.includes('label="允许修改寄售数量"'), '文案必须改成是否允许修改') + assert.ok(panel.includes('extra="关闭后打手端数量固定为 1,输入框禁用"'), '必须说明关闭后的行为') + assert.ok(panel.includes('checkedChildren="可改"'), '开启文案应为「可改」') + assert.ok(panel.includes('unCheckedChildren="固定 1"'), '关闭文案应为「固定 1」') + assert.ok(!panel.includes('label="显示寄售数量"'), '旧的「显示寄售数量」必须移除') +}) + +test('打手端:数量字段必须始终渲染,关闭时禁用并灰显 1', () => { + assert.ok( + page.includes('const quantityLocked = selectedProfile?.config.fields.quantity === false'), + '必须由 fields.quantity === false 推导锁定状态', + ) + assert.ok(page.includes('disabled={quantityLocked}'), '锁定时输入框必须禁用') + assert.ok( + page.includes("extra={quantityLocked ? '该分类数量固定为 1,不可修改' : undefined}"), + '锁定时要给出固定 1 的提示', + ) + assert.ok( + !page.includes('selectedProfile?.config.fields.quantity !== false ? ('), + '不能再按开关隐藏数量字段', + ) + assert.ok(page.includes('initialValue={1}'), '数量默认值必须为 1') + assert.ok( + page.includes('const quantityForDeposit = quantityLocked ? 1 :'), + '锁定后押金必须按 1 件计算', + ) +}) + +test('打手端:数量始终显示,价格区固定三列,不再按数量切两列', () => { + assert.ok(page.includes('className="worker-consignment-price-grid"'), '价格区必须固定三列') + assert.ok( + !page.includes("selectedProfile?.config.fields.quantity === false ? ' is-two' : ''"), + '不能再用 is-two 布局', + ) +})