fix(consignment): 寄售价支持低于1元并按分类校验寄售数量上限

This commit is contained in:
2026-11-08 11:11:54 +08:00
parent 229eef31a8
commit ffe7c31a87
12 changed files with 281 additions and 33 deletions
@@ -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="¥"
+2
View File
@@ -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,
}
}