feat(consignment): 寄售数量支持关闭修改,固定为 1
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1134,11 +1134,12 @@ export default function ConsignmentConfigPanel() {
|
||||
<Input placeholder="展示在发布页顶部的一句话说明" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="显示寄售数量"
|
||||
label="允许修改寄售数量"
|
||||
name={['fields', 'quantity']}
|
||||
valuePropName="checked"
|
||||
extra="关闭后打手端数量固定为 1,输入框禁用"
|
||||
>
|
||||
<Switch checkedChildren="显示" unCheckedChildren="隐藏" />
|
||||
<Switch checkedChildren="可改" unCheckedChildren="固定 1" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item
|
||||
|
||||
@@ -245,7 +245,9 @@ export default function WorkerConsignmentPage() {
|
||||
finished: 0,
|
||||
}
|
||||
const watchedQuantity = Form.useWatch('quantity', form)
|
||||
const quantityForDeposit = Math.max(1, Number(watchedQuantity || 1))
|
||||
// 「允许修改寄售数量」关闭时:数量固定 1、输入框禁用,价格/押金一律按 1 件计算。
|
||||
const quantityLocked = selectedProfile?.config.fields.quantity === false
|
||||
const quantityForDeposit = quantityLocked ? 1 : Math.max(1, Number(watchedQuantity || 1))
|
||||
const publishDepositFen = selectedProfile?.config.deposit.enabled
|
||||
? selectedProfile.config.deposit.mode === 'per_quantity'
|
||||
? Math.max(0, selectedProfile.config.deposit.unitAmountFen) * quantityForDeposit
|
||||
@@ -862,11 +864,7 @@ export default function WorkerConsignmentPage() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`worker-consignment-price-grid${
|
||||
selectedProfile?.config.fields.quantity === false ? ' is-two' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="worker-consignment-price-grid">
|
||||
<Form.Item
|
||||
label="寄售价格"
|
||||
tooltip={
|
||||
@@ -907,16 +905,21 @@ export default function WorkerConsignmentPage() {
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
{selectedProfile?.config.fields.quantity !== false ? (
|
||||
<Form.Item
|
||||
label="寄售数量"
|
||||
name="quantity"
|
||||
initialValue={1}
|
||||
className="worker-consignment-spec-card is-price"
|
||||
>
|
||||
<InputNumber min={1} precision={0} addonAfter="件" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item
|
||||
label="寄售数量"
|
||||
name="quantity"
|
||||
initialValue={1}
|
||||
className="worker-consignment-spec-card is-price"
|
||||
extra={quantityLocked ? '该分类数量固定为 1,不可修改' : undefined}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
precision={0}
|
||||
addonAfter="件"
|
||||
style={{ width: '100%' }}
|
||||
disabled={quantityLocked}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="履约押金"
|
||||
tooltip="平台统一配置。发布成功后暂扣,驳回或履约完成后释放。"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import test from 'node:test'
|
||||
|
||||
function read(relativePath: string) {
|
||||
return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8').replace(
|
||||
/\r\n/g,
|
||||
'\n',
|
||||
)
|
||||
}
|
||||
|
||||
const panel = read('../src/pages/admin/panels/ConsignmentConfigPanel.tsx')
|
||||
const page = read('../src/pages/worker/WorkerConsignmentPage.tsx')
|
||||
|
||||
test('管理端:数量开关语义必须是「允许修改寄售数量」/「可改 / 固定 1」', () => {
|
||||
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 布局',
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user