From ffe7c31a87f7a868c0a8c9f1ed08bd788c14d3cb Mon Sep 17 00:00:00 2001 From: zhaoxing <2558691827@qq.com> Date: Sun, 8 Nov 2026 11:11:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(consignment):=20=E5=AF=84=E5=94=AE=E4=BB=B7?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BD=8E=E4=BA=8E1=E5=85=83=E5=B9=B6?= =?UTF-8?q?=E6=8C=89=E5=88=86=E7=B1=BB=E6=A0=A1=E9=AA=8C=E5=AF=84=E5=94=AE?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/routes/worker/consignment.ts | 5 +- .../consignment-profile-service.test.ts | 17 ++++ .../consignment-profile-service.ts | 42 ++++++++- .../consignment-spec-service.test.ts | 26 ++++++ .../consignment-spec-service.ts | 4 +- .../consignment-worker-service.test.ts | 14 +++ .../consignment-worker-service.ts | 10 ++- .../pages/worker/WorkerConsignmentPage.tsx | 86 ++++++++++++++----- apps/frontend/src/types/consignment.ts | 2 + .../src/utils/consignment-publish-limits.ts | 39 +++++++++ .../tests/consignment-publish-limits.test.ts | 61 +++++++++++++ .../tests/consignment-quantity-edit.test.ts | 8 +- 12 files changed, 281 insertions(+), 33 deletions(-) create mode 100644 apps/frontend/src/utils/consignment-publish-limits.ts create mode 100644 apps/frontend/tests/consignment-publish-limits.test.ts diff --git a/apps/backend/src/routes/worker/consignment.ts b/apps/backend/src/routes/worker/consignment.ts index 870b6c82..17def610 100644 --- a/apps/backend/src/routes/worker/consignment.ts +++ b/apps/backend/src/routes/worker/consignment.ts @@ -19,9 +19,10 @@ const router = Router() * GET /api/v1/worker/consignment/profiles * 打手端寄卖页按已发布的分类配置动态生成 tab。 */ -router.get('/consignment/profiles', async (_req, res) => { +router.get('/consignment/profiles', async (req, res) => { try { - const result = await listWorkerConsignmentProfiles() + const session = getRequiredWorkerSession(req) + const result = await listWorkerConsignmentProfiles(session.workerId) res.json({ ok: true, data: { profiles: result } }) } catch (error) { sendRouteError(res, error, '获取寄卖配置失败', '[worker/consignment/profiles]') diff --git a/apps/backend/src/services/worker-platform/consignment-profile-service.test.ts b/apps/backend/src/services/worker-platform/consignment-profile-service.test.ts index 81fe8fef..c2ae2a69 100644 --- a/apps/backend/src/services/worker-platform/consignment-profile-service.test.ts +++ b/apps/backend/src/services/worker-platform/consignment-profile-service.test.ts @@ -138,6 +138,23 @@ test('打手端寄卖分类:苏尔南手枪固定排第一', async () => { ) }) +test('寄售数量上限按分类统计,并在打手配置接口返回 activeQuantity', async () => { + const source = await readFile( + new URL('./consignment-profile-service.ts', import.meta.url), + 'utf8', + ) + const countStart = source.indexOf('export async function countWorkerActiveConsignmentQuantity') + const countEnd = source.indexOf('/** 一次取回打手各分类', countStart) + const countBody = source.slice(countStart, countEnd) + assert.ok(countBody.includes('profile_id = $2'), '数量统计必须按 profile_id 过滤') + assert.ok(countBody.includes("status = 'pending_review'"), '待审核提报也必须计入当前占用量') + assert.ok( + source.includes('activeQuantity: activeByProfile.get(Number(row.id)) || 0'), + '打手配置接口必须回传每个分类的 activeQuantity', + ) + assert.ok(source.includes('GROUP BY profile_id'), '各分类数量必须一次分组取回') +}) + test('合并迁移 147 的苏尔南种子:降价提示默认开启、24 小时、按比例 10%', 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-profile-service.ts b/apps/backend/src/services/worker-platform/consignment-profile-service.ts index 3e5446af..76f978b2 100644 --- a/apps/backend/src/services/worker-platform/consignment-profile-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-profile-service.ts @@ -528,7 +528,7 @@ export async function archiveAdminConsignmentProfile(profileIdInput: number | st } /** 打手端:只返回已发布且分类启用的寄卖配置,按分类排序生成 tab。 */ -export async function listWorkerConsignmentProfiles() { +export async function listWorkerConsignmentProfiles(workerId?: number) { const result = await query( ` ${PROFILE_SELECT} @@ -537,6 +537,9 @@ export async function listWorkerConsignmentProfiles() { ORDER BY (c.category_key = 'shouqiang') DESC, c.sort_order ASC, p.id ASC `, ) + const activeByProfile = workerId + ? await countWorkerActiveConsignmentQuantities(workerId) + : new Map() return result.rows.map((row) => { const config = resolvePublishedProfileConfig(row) return { @@ -548,6 +551,7 @@ export async function listWorkerConsignmentProfiles() { description: row.description || '', tabTitle: row.tab_title || config.tabTitle || row.name, tabDescription: config.tabDescription, + activeQuantity: activeByProfile.get(Number(row.id)) || 0, config, } }) @@ -762,7 +766,12 @@ export async function getPublishedConsignmentProfileById( } /** 打手当前在售 + 待审核的寄卖数量(按件数,供单人寄售上限校验)。 */ -export async function countWorkerActiveConsignmentQuantity(workerId: number): Promise { +export async function countWorkerActiveConsignmentQuantity( + workerId: number, + profileId?: number | null, +): Promise { + const scopedProfileId = + Number.isInteger(Number(profileId)) && Number(profileId) > 0 ? Number(profileId) : null const result = await query<{ total: number | string }>( ` SELECT @@ -770,16 +779,43 @@ export async function countWorkerActiveConsignmentQuantity(workerId: number): Pr SELECT SUM(GREATEST(remaining_quantity, 0)) FROM consignment_inventories WHERE worker_id = $1 AND status = 'in_stock' + AND ($2::bigint IS NULL OR profile_id = $2) ), 0) + COALESCE(( SELECT SUM(GREATEST(quantity, 1)) FROM consignment_submissions WHERE worker_id = $1 AND status = 'pending_review' + AND ($2::bigint IS NULL OR profile_id = $2) ), 0) AS total `, + [workerId, scopedProfileId], + ) + return Math.max(0, Math.round(Number(result.rows[0]?.total || 0))) +} + +/** 一次取回打手各分类的在售+待审核件数,供打手端配置列表展示剩余额度。 */ +async function countWorkerActiveConsignmentQuantities( + workerId: number, +): Promise> { + const result = await query<{ profile_id: number | string; total: number | string }>( + ` + SELECT profile_id, SUM(quantity) AS total + FROM ( + SELECT profile_id, GREATEST(remaining_quantity, 0) AS quantity + FROM consignment_inventories + WHERE worker_id = $1 AND status = 'in_stock' AND profile_id IS NOT NULL + UNION ALL + SELECT profile_id, GREATEST(quantity, 1) AS quantity + FROM consignment_submissions + WHERE worker_id = $1 AND status = 'pending_review' AND profile_id IS NOT NULL + ) scoped + GROUP BY profile_id + `, [workerId], ) - return Math.max(0, Math.round(Number(result.rows[0]?.total || 0))) + return new Map( + result.rows.map((row) => [Number(row.profile_id), Math.max(0, Number(row.total || 0))]), + ) } export function buildDepositAmountFen(config: ConsignmentProfileConfig, quantity: number): number { 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 5af24930..d247b925 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 @@ -363,6 +363,32 @@ test('动态价格:打手改价只能低于系统建议价,且受最低价 assert.ok(belowMin.errors.some((error) => error.includes('不能少于'))) }) +test('动态价格:低于1元的寄售价允许发布,只受配置最低价约束', () => { + // 模板价 1 元 × 7.3 折 = 73 分,系统建议价本身就可以低于 1 元。 + const suggested = resolveWorkerConsignmentPrice({ + pricing: buildPricing({ discount: 7.3 }), + templatePriceFen: 100, + submittedPriceFen: 0, + }) + assert.deepEqual(suggested.errors, []) + assert.equal(suggested.priceFen, 73) + + const submitted = resolveWorkerConsignmentPrice({ + pricing: buildPricing(), + templatePriceFen: 100, + submittedPriceFen: 73, + }) + assert.deepEqual(submitted.errors, []) + assert.equal(submitted.priceFen, 73) + + const belowConfiguredMin = resolveWorkerConsignmentPrice({ + pricing: buildPricing({ discount: 7.3, minPriceFen: 80 }), + templatePriceFen: 100, + submittedPriceFen: 73, + }) + assert.ok(belowConfiguredMin.errors.some((error) => error.includes('不能少于'))) +}) + test('动态价格:没有模板价时一律阻止发布(不再允许手填)', () => { for (const pricing of [buildPricing(), buildPricing({ missingPricePolicy: 'manual' })]) { const blocked = resolveWorkerConsignmentPrice({ 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 1ada05fd..ca711910 100644 --- a/apps/backend/src/services/worker-platform/consignment-spec-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-spec-service.ts @@ -877,9 +877,7 @@ export function resolveWorkerConsignmentPrice(input: { if (priceFen > 0 && priceFen < minPriceFen) { errors.push(`寄售价格不能少于 ¥${(minPriceFen / 100).toFixed(2)}`) } - if (priceFen > 0) { - priceFen = Math.max(100, priceFen) - } + // 价格允许低于 1 元:下限只由配置的 minPriceFen 决定,0 元由发布侧拦截。 return { priceFen, capFen: computed.capFen, diff --git a/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts b/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts index c8004ba2..6cf77d86 100644 --- a/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts +++ b/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts @@ -218,3 +218,17 @@ test('未开通寄卖白名单权限的打手提报寄卖时被强校验拦截', '未开通必须返回专门的 consignment_permission_required 错误码', ) }) + +test('寄售价不再强制最低1元,数量上限按当前分类统计', async () => { + const [serviceSource, specSource] = await Promise.all([ + readServiceSource(), + readFile(new URL('./consignment-spec-service.ts', import.meta.url), 'utf8'), + ]) + assert.ok(!serviceSource.includes('Math.max(100'), '改价不能把 0.73 元静默抬到 1 元') + assert.ok(!specSource.includes('priceFen = Math.max(100, priceFen)'), '发布价不能硬编码最低 1 元') + assert.ok( + serviceSource.includes('countWorkerActiveConsignmentQuantity(') && + serviceSource.includes('profile.profileId'), + '数量上限校验必须按当前寄卖配置统计', + ) +}) 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 f0f2b819..0584f57a 100644 --- a/apps/backend/src/services/worker-platform/consignment-worker-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-worker-service.ts @@ -259,7 +259,13 @@ export async function updateWorkerConsignmentSalePrice( errorCode: 'consignment_price_edit_forbidden', }) } - const priceFen = Math.max(100, Math.round(Number(priceFenInput || 0))) + const priceFen = Math.round(Number(priceFenInput || 0)) + if (priceFen <= 0) { + throw createHttpError('寄售价格必须大于 0 元', { + statusCode: 400, + errorCode: 'consignment_price_invalid', + }) + } const capFen = Math.max(0, Math.round(Number(inventory.price_cap_fen || 0))) if (capFen > 0 && priceFen > capFen) { throw createHttpError(`寄售价格不能高于系统建议价 ¥${(capFen / 100).toFixed(2)}`, { @@ -321,7 +327,7 @@ export async function publishWorkerConsignment(workerId: number, payload: JsonOb payload.quantity, ) if (profile?.config.inventoryLimit.enabled) { - const activeQuantity = await countWorkerActiveConsignmentQuantity(workerId) + const activeQuantity = await countWorkerActiveConsignmentQuantity(workerId, profile.profileId) if (activeQuantity + quantity > profile.config.inventoryLimit.maxActiveQuantity) { throw createHttpError( `超出单人寄售数量上限(最多 ${profile.config.inventoryLimit.maxActiveQuantity} 件,当前在售/待审核 ${activeQuantity} 件)`, diff --git a/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx b/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx index a0c7051a..b8c6b493 100644 --- a/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx @@ -65,6 +65,10 @@ import { DEFAULT_CONSIGNMENT_INTAKE_FIELDS, buildSpecItemName } from '@/utils/co import { formatConsignmentContactSupporter } from '@/utils/consignment-contact' import { formatGiftCooldownRemaining } from '@/utils/consignment-cooldown' import { resolveConsignmentPageTitle } from '@/utils/consignment-page-title' +import { + resolveConsignmentPriceMinYuan, + resolveConsignmentQuantityLimit, +} from '@/utils/consignment-publish-limits' import { canWorkerPublishConsignment } from '@/utils/consignment-whitelist' import { formatDateTime } from '@/utils/date-time' import { useIsMobile } from '@/utils/use-is-mobile' @@ -206,7 +210,21 @@ export default function WorkerConsignmentPage() { consignmentProfiles[0] || null const selectedSpecs: ConsignmentSpecField[] = selectedProfile?.config.specs ?? [] + // 「允许修改寄售数量」关闭时:数量固定 1、输入框禁用,价格/押金一律按 1 件计算。 + const quantityLocked = selectedProfile?.config.fields.quantity === false const pageTitle = resolveConsignmentPageTitle(selectedProfile) + const priceMinYuan = resolveConsignmentPriceMinYuan(selectedProfile?.config.pricing.minPriceFen) + const quantityLimit = resolveConsignmentQuantityLimit( + selectedProfile?.config, + selectedProfile?.activeQuantity, + ) + const quantityLimitHint = quantityLimit.enabled + ? `当前在售/待审核 ${quantityLimit.activeQuantity} 件,上限 ${quantityLimit.maxActiveQuantity} 件,最多还可寄卖 ${quantityLimit.remainingQuantity} 件` + : '' + const quantityExtra = + [quantityLocked ? '该分类数量固定为 1,不可修改' : '', quantityLimitHint] + .filter(Boolean) + .join(';') || undefined const intakeFields: ConsignmentIntakeField[] = selectedProfile?.config.intake?.fields ?? DEFAULT_CONSIGNMENT_INTAKE_FIELDS useEffect(() => { @@ -247,8 +265,6 @@ export default function WorkerConsignmentPage() { finished: 0, } const watchedQuantity = Form.useWatch('quantity', form) - // 「允许修改寄售数量」关闭时:数量固定 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' @@ -487,6 +503,14 @@ export default function WorkerConsignmentPage() { ? (intakeValues.screenshots as string[]) : [] + const publishQuantity = quantityLocked ? 1 : Math.max(1, Number(values.quantity || 1)) + if (quantityLimit.enabled && publishQuantity > quantityLimit.remainingQuantity) { + message.error( + `最多可寄卖 ${quantityLimit.remainingQuantity} 件(当前在售/待审核 ${quantityLimit.activeQuantity} 件,上限 ${quantityLimit.maxActiveQuantity} 件)`, + ) + return + } + // 寄售价:必须命中接单模板价;允许改价时可下调,但不能高于建议价。 const minPriceFen = Math.max(0, selectedProfile?.config.pricing.minPriceFen || 0) if (priceState.status === 'loading') { @@ -506,15 +530,15 @@ export default function WorkerConsignmentPage() { message.error(`寄售价格不能高于系统建议价 ¥${(capPriceFen / 100).toFixed(2)}`) return } - if (submittedPriceFen < Math.max(100, minPriceFen)) { - message.error(`寄售价格不能少于 ¥${(Math.max(100, minPriceFen) / 100).toFixed(2)}`) + if (minPriceFen > 0 && submittedPriceFen < minPriceFen) { + message.error(`寄售价格不能少于 ¥${(minPriceFen / 100).toFixed(2)}`) return } askingPriceFen = submittedPriceFen } } - if (askingPriceFen < 100) { - message.error('寄售价格不能少于1元') + if (askingPriceFen <= 0) { + message.error('寄售价格必须大于 0 元') return } @@ -532,7 +556,7 @@ export default function WorkerConsignmentPage() { try { await publishWorkerConsignment({ profileId: selectedProfile?.profileId, - quantity: Math.max(1, Number(values.quantity || 1)), + quantity: publishQuantity, itemName, specValues, system: String(specValues.system || ''), @@ -871,7 +895,7 @@ export default function WorkerConsignmentPage() { label="寄售价格" tooltip={ manualPriceAllowed - ? `不能高于系统建议价${selectedProfile?.config.pricing.minPriceFen ? `;最低 ¥${(selectedProfile.config.pricing.minPriceFen / 100).toFixed(2)}` : ''}。` + ? `不能高于系统建议价;最低 ¥${priceMinYuan.toFixed(2)}。` : '由后台寄卖配置自动带出,卖家不可修改;需要调整请联系客服。' } className="worker-consignment-spec-card is-price" @@ -879,7 +903,7 @@ export default function WorkerConsignmentPage() { {manualPriceAllowed ? ( 0 ? priceState.capFen / 100 @@ -898,7 +922,7 @@ export default function WorkerConsignmentPage() { 计算建议价… ) : priceState.status === 'matched' ? ( - ¥{(priceState.fen / 100).toFixed(0)} + ¥{(priceState.fen / 100).toFixed(2)} ) : priceState.status === 'empty' ? ( 该规格暂无价格,请联系客服 ) : ( @@ -912,14 +936,30 @@ export default function WorkerConsignmentPage() { name="quantity" initialValue={1} className="worker-consignment-spec-card is-price" - extra={quantityLocked ? '该分类数量固定为 1,不可修改' : undefined} + extra={quantityExtra} + rules={[ + ({ getFieldValue }: { getFieldValue: (name: string) => unknown }) => ({ + validator(_: unknown, value: unknown) { + if (!quantityLimit.enabled) return Promise.resolve() + const quantity = Math.max(1, Number(value ?? getFieldValue('quantity') ?? 1)) + if (quantity > quantityLimit.remainingQuantity) { + return Promise.reject( + new Error( + `最多可寄卖 ${quantityLimit.remainingQuantity} 件(当前在售/待审核 ${quantityLimit.activeQuantity} 件,上限 ${quantityLimit.maxActiveQuantity} 件)`, + ), + ) + } + return Promise.resolve() + }, + }), + ]} > } > - {canPublish - ? `提交寄卖(暂扣押金 ¥${publishDepositYuan.toFixed(0)})` - : '余额不足,请先充值押金'} + {quantityLimit.blocked + ? '已达寄售数量上限' + : canPublish + ? `提交寄卖(暂扣押金 ¥${publishDepositYuan.toFixed(0)})` + : '余额不足,请先充值押金'} 提交后进入客服审核;押金在驳回或履约完成后释放 @@ -1811,12 +1853,14 @@ function ConsignmentRecordCard({ okText="保存" onOk={async () => { if (!record.submissionId) return + const nextPriceFen = Math.round(Number(priceYuan || 0) * 100) + if (nextPriceFen <= 0) { + message.error('寄售价格必须大于 0 元') + return + } setSavingPrice(true) try { - await updateWorkerConsignmentPrice( - record.submissionId, - Math.max(100, Math.round(Number(priceYuan || 0) * 100)), - ) + await updateWorkerConsignmentPrice(record.submissionId, nextPriceFen) message.success('寄卖售价已更新') setPriceOpen(false) await queryClient.invalidateQueries({ queryKey: ['worker-my-consignments'] }) @@ -1828,7 +1872,7 @@ function ConsignmentRecordCard({ }} > 0 ? record.priceCapFen / 100 : undefined} precision={2} addonBefore="¥" diff --git a/apps/frontend/src/types/consignment.ts b/apps/frontend/src/types/consignment.ts index 605531c1..2cce8161 100644 --- a/apps/frontend/src/types/consignment.ts +++ b/apps/frontend/src/types/consignment.ts @@ -504,6 +504,8 @@ export type WorkerConsignmentProfile = { description: string tabTitle: string tabDescription: string + /** 当前打手在该分类下「在售 + 待审核」的件数 */ + activeQuantity: number config: ConsignmentProfileConfig } diff --git a/apps/frontend/src/utils/consignment-publish-limits.ts b/apps/frontend/src/utils/consignment-publish-limits.ts new file mode 100644 index 00000000..c9a08d4b --- /dev/null +++ b/apps/frontend/src/utils/consignment-publish-limits.ts @@ -0,0 +1,39 @@ +import type { ConsignmentProfileConfig } from '../types/consignment' + +/** 寄售价允许低于 1 元:配置了最低价就按最低价,否则只要求大于 0(输入框用 0.01)。 */ +export function resolveConsignmentPriceMinYuan(minPriceFen: number | null | undefined): number { + const fen = Math.max(0, Math.round(Number(minPriceFen || 0))) + return fen > 0 ? fen / 100 : 0.01 +} + +export type ConsignmentQuantityLimit = { + enabled: boolean + maxActiveQuantity: number + activeQuantity: number + remainingQuantity: number + blocked: boolean +} + +/** + * 单个分类的寄售数量上限:后端按配置 profile_id 统计「在售 + 待审核」件数, + * 这里用接口返回的 activeQuantity 计算当前还能寄卖多少件。 + */ +export function resolveConsignmentQuantityLimit( + config: Pick | null | undefined, + activeQuantity: number | null | undefined, +): ConsignmentQuantityLimit { + const maxActiveQuantity = Math.max( + 0, + Math.round(Number(config?.inventoryLimit?.maxActiveQuantity || 0)), + ) + const active = Math.max(0, Math.round(Number(activeQuantity || 0))) + const enabled = config?.inventoryLimit?.enabled === true + const remainingQuantity = enabled ? Math.max(0, maxActiveQuantity - active) : 0 + return { + enabled, + maxActiveQuantity, + activeQuantity: active, + remainingQuantity, + blocked: enabled && remainingQuantity <= 0, + } +} diff --git a/apps/frontend/tests/consignment-publish-limits.test.ts b/apps/frontend/tests/consignment-publish-limits.test.ts new file mode 100644 index 00000000..1844435c --- /dev/null +++ b/apps/frontend/tests/consignment-publish-limits.test.ts @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import test from 'node:test' + +import { + resolveConsignmentPriceMinYuan, + resolveConsignmentQuantityLimit, +} from '../src/utils/consignment-publish-limits.ts' + +test('寄售价允许低于1元:未配置最低价时输入框下限为0.01元', () => { + assert.equal(resolveConsignmentPriceMinYuan(0), 0.01) + assert.equal(resolveConsignmentPriceMinYuan(null), 0.01) + assert.equal(resolveConsignmentPriceMinYuan(73), 0.73) + assert.equal(resolveConsignmentPriceMinYuan(100), 1) +}) + +test('寄售数量上限按当前分类的 activeQuantity 计算剩余额度', () => { + const config = { inventoryLimit: { enabled: true, maxActiveQuantity: 10 } } + const remaining = resolveConsignmentQuantityLimit(config, 3) + assert.equal(remaining.enabled, true) + assert.equal(remaining.activeQuantity, 3) + assert.equal(remaining.remainingQuantity, 7) + assert.equal(remaining.blocked, false) + + const full = resolveConsignmentQuantityLimit(config, 10) + assert.equal(full.remainingQuantity, 0) + assert.equal(full.blocked, true) + + const over = resolveConsignmentQuantityLimit(config, 12) + assert.equal(over.remainingQuantity, 0) + assert.equal(over.blocked, true) +}) + +test('寄售数量限制关闭时不校验剩余额度', () => { + const disabled = resolveConsignmentQuantityLimit( + { inventoryLimit: { enabled: false, maxActiveQuantity: 10 } }, + 100, + ) + assert.equal(disabled.enabled, false) + assert.equal(disabled.remainingQuantity, 0) + assert.equal(disabled.blocked, false) +}) + +test('打手端发布页:价格与数量限制必须走同一套规则', () => { + const page = readFileSync( + fileURLToPath(new URL('../src/pages/worker/WorkerConsignmentPage.tsx', import.meta.url)), + 'utf8', + ) + assert.ok(page.includes('resolveConsignmentPriceMinYuan('), '价格下限必须走配置解析函数') + assert.ok(page.includes('min={priceMinYuan}'), '价格输入框必须使用配置最低价') + assert.ok(!page.includes('Math.max(100, minPriceFen)'), '提交校验不得再强制最低 1 元') + assert.ok(!page.includes("message.error('寄售价格不能少于1元')"), '不得再提示最低 1 元') + assert.ok( + page.includes('resolveConsignmentQuantityLimit('), + '数量上限必须按当前分类的 activeQuantity 计算', + ) + assert.ok(page.includes('quantityLimit.remainingQuantity'), '必须展示剩余可寄卖数量') + assert.ok(page.includes('quantityLimit.blocked'), '达到上限时必须禁用提交') + assert.ok(page.includes('quantityLimit.activeQuantity'), '必须带入当前在售/待审核件数') +}) diff --git a/apps/frontend/tests/consignment-quantity-edit.test.ts b/apps/frontend/tests/consignment-quantity-edit.test.ts index 3b3c5326..4f343e2c 100644 --- a/apps/frontend/tests/consignment-quantity-edit.test.ts +++ b/apps/frontend/tests/consignment-quantity-edit.test.ts @@ -26,11 +26,15 @@ test('打手端:数量字段必须始终渲染,关闭时禁用并灰显 1', 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}"), + page.includes('disabled={quantityLocked || quantityLimit.blocked}'), + '锁定时或达到数量上限时输入框必须禁用', + ) + assert.ok( + page.includes("quantityLocked ? '该分类数量固定为 1,不可修改' : ''"), '锁定时要给出固定 1 的提示', ) + assert.ok(page.includes('extra={quantityExtra}'), '数量提示必须走统一文案') assert.ok( !page.includes('selectedProfile?.config.fields.quantity !== false ? ('), '不能再按开关隐藏数量字段',