fix(consignment): 寄售价支持低于1元并按分类校验寄售数量上限
This commit is contained in:
@@ -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]')
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<ConsignmentProfileRow>(
|
||||
`
|
||||
${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<number, number>()
|
||||
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<number> {
|
||||
export async function countWorkerActiveConsignmentQuantity(
|
||||
workerId: number,
|
||||
profileId?: number | null,
|
||||
): Promise<number> {
|
||||
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<Map<number, number>> {
|
||||
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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
'数量上限校验必须按当前寄卖配置统计',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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} 件)`,
|
||||
|
||||
@@ -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 ? (
|
||||
<Form.Item name="askingPrice" noStyle>
|
||||
<InputNumber
|
||||
min={Math.max(1, Number(selectedProfile?.config.pricing.minPriceFen || 0) / 100)}
|
||||
min={priceMinYuan}
|
||||
max={
|
||||
priceState.status === 'matched' && priceState.capFen > 0
|
||||
? priceState.capFen / 100
|
||||
@@ -898,7 +922,7 @@ export default function WorkerConsignmentPage() {
|
||||
<Spin size="small" /> 计算建议价…
|
||||
</span>
|
||||
) : priceState.status === 'matched' ? (
|
||||
<span className="is-amount">¥{(priceState.fen / 100).toFixed(0)}</span>
|
||||
<span className="is-amount">¥{(priceState.fen / 100).toFixed(2)}</span>
|
||||
) : priceState.status === 'empty' ? (
|
||||
<span className="is-empty">该规格暂无价格,请联系客服</span>
|
||||
) : (
|
||||
@@ -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()
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
precision={0}
|
||||
addonAfter="件"
|
||||
style={{ width: '100%' }}
|
||||
disabled={quantityLocked}
|
||||
disabled={quantityLocked || quantityLimit.blocked}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -1040,12 +1080,14 @@ export default function WorkerConsignmentPage() {
|
||||
htmlType="submit"
|
||||
size="large"
|
||||
loading={submitting}
|
||||
disabled={!canPublish}
|
||||
disabled={!canPublish || quantityLimit.blocked}
|
||||
icon={<CloudUploadOutlined />}
|
||||
>
|
||||
{canPublish
|
||||
? `提交寄卖(暂扣押金 ¥${publishDepositYuan.toFixed(0)})`
|
||||
: '余额不足,请先充值押金'}
|
||||
{quantityLimit.blocked
|
||||
? '已达寄售数量上限'
|
||||
: canPublish
|
||||
? `提交寄卖(暂扣押金 ¥${publishDepositYuan.toFixed(0)})`
|
||||
: '余额不足,请先充值押金'}
|
||||
</Button>
|
||||
<Typography.Text type="secondary">
|
||||
提交后进入客服审核;押金在驳回或履约完成后释放
|
||||
@@ -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({
|
||||
}}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
min={0.01}
|
||||
max={record.priceCapFen && record.priceCapFen > 0 ? record.priceCapFen / 100 : undefined}
|
||||
precision={2}
|
||||
addonBefore="¥"
|
||||
|
||||
@@ -504,6 +504,8 @@ export type WorkerConsignmentProfile = {
|
||||
description: string
|
||||
tabTitle: string
|
||||
tabDescription: string
|
||||
/** 当前打手在该分类下「在售 + 待审核」的件数 */
|
||||
activeQuantity: number
|
||||
config: ConsignmentProfileConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ConsignmentProfileConfig, 'inventoryLimit'> | 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,
|
||||
}
|
||||
}
|
||||
@@ -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'), '必须带入当前在售/待审核件数')
|
||||
})
|
||||
@@ -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 ? ('),
|
||||
'不能再按开关隐藏数量字段',
|
||||
|
||||
Reference in New Issue
Block a user