feat(consignment): 停用客服手动入库,寄卖货源只走平台提报

背景:手动入库是唯一会造出「没有提报单的现货」的入口,这类货没有寄卖单号,
统一状态机/凭证链/搜索/卖家端单号锚定都串不起来,只能在界面回退显示内部仓库号 INV,
正是"一件货两个号"的残留。业务定调:寄卖货源只走「打手平台提报 → 客服审核入库」。

- 前端寄售仓库的「手动入库」入口整块删除(按钮 + 入库弹窗 + 提交逻辑 + 服务函数)
- 后端同时挡住,防旧页面缓存或直接调接口绕过:
  POST /worker-platform/consignment/inventories 返回 409 consignment_manual_inbound_disabled;
  死代码一并清掉(服务层原实现、仓库层 createConsignmentInventory)
- 提报审核入库路径不受影响,照旧生成内部仓库号 INV 并写 source_submission_id,
  即每件货都必然挂着寄卖单号,INV 只作为内部编号存在、界面不展示

真实库验证:接口被拒(409),且「没有提报单的现货行数」前后均为 0。
This commit is contained in:
yml2213
2026-09-17 11:41:46 +08:00
parent 47e09d72ce
commit 0c10f0e135
6 changed files with 50 additions and 454 deletions
@@ -1,10 +1,4 @@
import {
EyeOutlined,
PlusOutlined,
ReloadOutlined,
RollbackOutlined,
StopOutlined,
} from '@ant-design/icons'
import { EyeOutlined, ReloadOutlined, RollbackOutlined, StopOutlined } from '@ant-design/icons'
import { useQuery } from '@tanstack/react-query'
import {
Alert,
@@ -35,7 +29,6 @@ import { useState } from 'react'
import ConsignmentEvidenceGallery from '@/components/admin/ConsignmentEvidenceGallery'
import {
archiveAdminConsignmentInventory,
createAdminConsignmentInventory,
fetchAdminConsignmentInventories,
fetchAdminConsignmentStats,
returnAdminConsignmentInventory,
@@ -147,13 +140,11 @@ export default function ConsignmentInventoryPanel() {
const [pageSize, setPageSize] = useState<number>(20)
// 弹窗状态
const [inboundModalOpen, setInboundModalOpen] = useState(false)
const [editingItem, setEditingItem] = useState<ConsignmentInventory | null>(null)
const [viewingItem, setViewingItem] = useState<ConsignmentInventory | null>(null)
const [returnItem, setReturnItem] = useState<ConsignmentInventory | null>(null)
const [returnReason, setReturnReason] = useState<string>('')
const [inboundForm] = Form.useForm()
const [editForm] = Form.useForm()
// 写操作按钮统一按 consignment.manage 门控:后端全部挂了 requireAdminPermission,
@@ -230,60 +221,6 @@ export default function ConsignmentInventoryPanel() {
}
}
// 手动入库提交
async function handleInboundSubmit() {
try {
const values = await inboundForm.validateFields()
const count = Number(values.count || 1)
const costPriceFen = Math.round(Number(values.costPrice || 0) * 100)
const depositAmountFen = values.depositAmount
? Math.round(Number(values.depositAmount) * 100)
: undefined
const selectedWorker = workerOptions.find((w) => w.value === values.workerId)
for (let i = 0; i < count; i++) {
await createAdminConsignmentInventory({
itemName: values.itemName,
system: values.system,
serverZone: values.serverZone,
series: values.series,
diamondLevel: Number(values.diamondLevel || 1),
brillianceCount: Number(values.brillianceCount || 0),
costPriceFen,
buyerName: values.buyerName,
shopName: values.shopName,
inboundBatch: values.inboundBatch,
sellerContact: values.sellerContact,
sellerGameId: values.sellerGameId,
sellerPaymentInfo: values.sellerPaymentInfo,
sellerPhone: values.sellerPhone,
workerId: values.workerId || undefined,
workerName: selectedWorker?.name || undefined,
workerPhone: selectedWorker?.phone || undefined,
depositAmountFen,
credentialsJson: {
loginMethod: values.loginMethod,
account: values.account,
password: values.password,
authNote: values.authNote,
},
screenshotsJson: values.screenshots ? [values.screenshots] : [],
remark: values.remark,
allowMatching: values.allowMatching !== false,
sourceType: 'manual',
})
}
message.success(`成功入库 ${count} 件寄售物品!`)
setInboundModalOpen(false)
inboundForm.resetFields()
await refresh()
} catch (err: any) {
if (err.errorFields) return
message.error(err.message || '创建寄售物品失败')
}
}
// 编辑更新提交
async function handleEditSubmit() {
if (!editingItem) return
@@ -807,28 +744,6 @@ export default function ConsignmentInventoryPanel() {
<Button icon={<ReloadOutlined />} onClick={refresh}>
刷新
</Button>
{canManageConsignment ? (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
inboundForm.resetFields()
inboundForm.setFieldsValue({
system: '苹果',
serverZone: '微信区',
series: '暗潮',
diamondLevel: 1,
brillianceCount: 0,
count: 1,
allowMatching: true,
itemName: '苹果微信暗潮1钻',
})
setInboundModalOpen(true)
}}
>
手动入库
</Button>
) : null}
</Space>
</Space>
</Card>
@@ -873,245 +788,6 @@ export default function ConsignmentInventoryPanel() {
/>
</Card>
{/* 手动入库弹窗 */}
<Modal
title="寄售物品入库 (和平精英)"
open={inboundModalOpen}
width={720}
onCancel={() => setInboundModalOpen(false)}
onOk={handleInboundSubmit}
destroyOnClose
>
<Form form={inboundForm} layout="vertical">
<Row gutter={16}>
<Col span={6}>
<Form.Item name="system" label="系统" rules={[{ required: true }]}>
<Select
options={[
{ label: '苹果 (iOS)', value: '苹果' },
{ label: '安卓 (Android)', value: '安卓' },
{ label: '通用/双端', value: '通用' },
]}
onChange={() => autoFillItemName(inboundForm)}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="serverZone" label="区服" rules={[{ required: true }]}>
<Select
options={[
{ label: '微信区', value: '微信区' },
{ label: 'QQ区', value: 'QQ区' },
]}
onChange={() => autoFillItemName(inboundForm)}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="series" label="系列/枪系" rules={[{ required: true }]}>
<Select
options={[
{ label: '暗潮', value: '暗潮' },
{ label: '怒嚎', value: '怒嚎' },
{ label: '血宴', value: '血宴' },
]}
onChange={() => autoFillItemName(inboundForm)}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="diamondLevel" label="钻石等级" rules={[{ required: true }]}>
<Select
options={[
{ label: '1 钻', value: 1 },
{ label: '2 钻', value: 2 },
{ label: '3 钻', value: 3 },
{ label: '4 钻', value: 4 },
{ label: '5 钻', value: 5 },
{ label: '6 钻', value: 6 },
{ label: '7 钻', value: 7 },
]}
onChange={() => autoFillItemName(inboundForm)}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={6}>
<Form.Item name="brillianceCount" label="璀璨数">
<InputNumber
min={0}
max={7}
style={{ width: '100%' }}
onChange={() => autoFillItemName(inboundForm)}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="itemName"
label="物品显示名称"
rules={[{ required: true, message: '请输入物品名称' }]}
>
<Input placeholder="如:苹果微信暗潮1钻" />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="costPrice"
label="收购价 (元)"
rules={[{ required: true, message: '请输入收购价格' }]}
>
<InputNumber min={0} step={1} style={{ width: '100%' }} placeholder="如 80" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item
name="buyerName"
label="收购人 (客服姓名)"
rules={[{ required: true, message: '请填写收购人' }]}
>
<Input placeholder="如:小明" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="shopName" label="关联店铺">
<Input placeholder="如:淘宝旗舰店 / 拼多多" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="inboundBatch" label="收购批次/日期">
<Input placeholder="如:202609-01" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="sellerContact" label="卖家微信号/联系方式">
<Input placeholder="微信号或QQ" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="sellerPhone" label="卖家电话">
<Input placeholder="手机号" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="sellerGameId" label="卖家游戏ID/角色名">
<Input placeholder="游戏内名称" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={14}>
<Form.Item
name="workerId"
label="走打手接单平台 (供货/履约打手)"
tooltip="关联打手账号后,工单匹配推荐时将直接指派该打手接单丢枪并冻结押金"
>
<Select
allowClear
showSearch
filterOption={(input, option) =>
String(option?.label ?? '')
.toLowerCase()
.includes(input.toLowerCase())
}
placeholder="选择关联的接单平台打手账号 (选填)"
options={workerOptions}
/>
</Form.Item>
</Col>
<Col span={10}>
<Form.Item
name="depositAmount"
label="履约押金金额 (元)"
tooltip="指派该打手接单交付手枪时,平台自动冻结的押金金额(默认100元)"
>
<InputNumber
min={0}
step={10}
addonBefore="¥"
style={{ width: '100%' }}
placeholder="如 100"
/>
</Form.Item>
</Col>
</Row>
<Form.Item name="sellerPaymentInfo" label="卖家收款信息 (支付宝/微信号/银行卡)">
<Input placeholder="如:支付宝 13800000000 (张三)" />
</Form.Item>
<Card
type="inner"
size="small"
title="账号与上号凭据信息 (仅内部及打手履约可见)"
style={{ marginBottom: 16 }}
>
<Row gutter={16}>
<Col span={6}>
<Form.Item name="loginMethod" label="登录方式" style={{ marginBottom: 8 }}>
<Select
options={[
{ label: '微信扫码', value: 'wechat_qrcode' },
{ label: 'QQ扫码', value: 'qq_qrcode' },
{ label: '账号密码', value: 'account_pwd' },
{ label: '游戏内转赠/投喂', value: 'in_game_transfer' },
]}
/>
</Form.Item>
</Col>
<Col span={9}>
<Form.Item name="account" label="账号/手机" style={{ marginBottom: 8 }}>
<Input placeholder="账号" />
</Form.Item>
</Col>
<Col span={9}>
<Form.Item name="password" label="密码/令牌" style={{ marginBottom: 8 }}>
<Input.Password placeholder="密码" />
</Form.Item>
</Col>
</Row>
<Form.Item name="authNote" label="上号说明/安全码" style={{ marginBottom: 0 }}>
<Input placeholder="备用验证码/二次验证说明等" />
</Form.Item>
</Card>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="count"
label="入库数量 (批量录入同类物品)"
tooltip="如果大于1,系统将自动创建对应数量的独立库存物品编号"
>
<InputNumber min={1} max={50} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="allowMatching"
label="是否立即允许匹配"
valuePropName="checked"
tooltip="开启后,发布新工单时系统会自动检测此库存进行推荐或一键核销"
>
<Switch defaultChecked checkedChildren="允许匹配" unCheckedChildren="禁止匹配" />
</Form.Item>
</Col>
</Row>
<Form.Item name="remark" label="内部备注">
<Input.TextArea rows={2} placeholder="如:来源某老客户置换、质押等" />
</Form.Item>
</Form>
</Modal>
{/* 编辑物品弹窗 */}
<Modal
title="编辑寄售物品"
@@ -23,13 +23,6 @@ export function getAdminConsignmentInventory(id: number | string) {
return apiGet<ConsignmentInventory>(`/api/v1/admin/worker-platform/consignment/inventories/${id}`)
}
export function createAdminConsignmentInventory(payload: Record<string, unknown>) {
return apiPost<ConsignmentInventory>(
'/api/v1/admin/worker-platform/consignment/inventories',
payload,
)
}
export function updateAdminConsignmentInventory(
id: number | string,
payload: Record<string, unknown>,