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:
@@ -321,88 +321,6 @@ export async function getConsignmentInventoryById(
|
||||
return res.rows[0] || null
|
||||
}
|
||||
|
||||
export async function createConsignmentInventory(input: {
|
||||
inventoryNo: string
|
||||
gameName?: string
|
||||
categoryId?: number | null
|
||||
itemName: string
|
||||
system?: string
|
||||
serverZone?: string
|
||||
series?: string
|
||||
diamondLevel?: number
|
||||
brillianceCount?: number
|
||||
costPriceFen?: number
|
||||
buyerName?: string
|
||||
inboundBatch?: string
|
||||
sellerContact?: string
|
||||
sellerGameId?: string
|
||||
sellerPaymentInfo?: string
|
||||
sellerPhone?: string
|
||||
credentialsJson?: Record<string, unknown>
|
||||
screenshotsJson?: unknown[]
|
||||
remark?: string
|
||||
allowMatching?: boolean
|
||||
sourceType?: 'manual' | 'worker_portal' | 'recycle'
|
||||
sourceSubmissionId?: number | null
|
||||
workerId?: number | null
|
||||
workerName?: string
|
||||
workerPhone?: string
|
||||
depositAmountFen?: number
|
||||
now: string
|
||||
}): Promise<ConsignmentInventoryRow> {
|
||||
const sql = `
|
||||
INSERT INTO consignment_inventories (
|
||||
inventory_no, game_name, category_id, item_name, system, server_zone,
|
||||
series, diamond_level, brilliance_count, cost_price_fen, buyer_name,
|
||||
inbound_batch, seller_contact, seller_game_id, seller_payment_info,
|
||||
seller_phone, credentials_json, screenshots_json, remark,
|
||||
allow_matching, status, allocation_status, source_type,
|
||||
source_submission_id, worker_id, worker_name, worker_phone, deposit_amount_fen,
|
||||
inbound_at, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9, $10, $11,
|
||||
$12, $13, $14, $15,
|
||||
$16, $17::jsonb, $18::jsonb, $19,
|
||||
$20, 'in_stock', '待安排', $21,
|
||||
$22, $23, $24, $25, $26,
|
||||
$27, $27, $27
|
||||
)
|
||||
RETURNING *
|
||||
`
|
||||
const values = [
|
||||
input.inventoryNo,
|
||||
input.gameName || '和平精英',
|
||||
input.categoryId || null,
|
||||
input.itemName,
|
||||
input.system || '',
|
||||
input.serverZone || '',
|
||||
input.series || '',
|
||||
input.diamondLevel || 1,
|
||||
input.brillianceCount || 0,
|
||||
input.costPriceFen || 0,
|
||||
input.buyerName || '',
|
||||
input.inboundBatch || '',
|
||||
input.sellerContact || '',
|
||||
input.sellerGameId || '',
|
||||
input.sellerPaymentInfo || '',
|
||||
input.sellerPhone || '',
|
||||
toJsonString(input.credentialsJson || {}),
|
||||
toJsonString(input.screenshotsJson || []),
|
||||
input.remark || '',
|
||||
input.allowMatching !== false,
|
||||
input.sourceType || 'manual',
|
||||
input.sourceSubmissionId || null,
|
||||
input.workerId || null,
|
||||
input.workerName || '',
|
||||
input.workerPhone || '',
|
||||
input.depositAmountFen || 0,
|
||||
input.now,
|
||||
]
|
||||
const res = await query<ConsignmentInventoryRow>(sql, values)
|
||||
return res.rows[0]!
|
||||
}
|
||||
|
||||
export async function updateConsignmentInventory(
|
||||
id: number | string,
|
||||
input: {
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
type ConsignmentInventoryRow,
|
||||
type ConsignmentSubmissionRow,
|
||||
approveConsignmentSubmissionWithInbound,
|
||||
createConsignmentInventory,
|
||||
getConsignmentInventoryById,
|
||||
getConsignmentInventoryStats,
|
||||
getConsignmentSubmissionById,
|
||||
@@ -279,46 +278,19 @@ export async function getAdminConsignmentInventory(id: number | string) {
|
||||
return mapConsignmentInventory(item)
|
||||
}
|
||||
|
||||
export async function createAdminConsignmentInventory(payload: JsonObject, actorName = '') {
|
||||
const itemName = String(payload.itemName || '').trim()
|
||||
if (!itemName) {
|
||||
throw createHttpError('物品名称不能为空', { statusCode: 400, errorCode: 'invalid_item_name' })
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const inventoryNo = generateConsignmentInventoryNo()
|
||||
const costPriceFen = Math.max(0, Math.round(Number(payload.costPriceFen || 0)))
|
||||
|
||||
const row = await createConsignmentInventory({
|
||||
inventoryNo,
|
||||
gameName: String(payload.gameName || '和平精英').trim(),
|
||||
categoryId: payload.categoryId ? Number(payload.categoryId) : null,
|
||||
itemName,
|
||||
system: String(payload.system || '').trim(),
|
||||
serverZone: String(payload.serverZone || '').trim(),
|
||||
series: String(payload.series || '').trim(),
|
||||
diamondLevel: Math.max(1, Math.min(7, Number(payload.diamondLevel || 1))),
|
||||
brillianceCount: Math.max(0, Math.min(7, Number(payload.brillianceCount || 0))),
|
||||
costPriceFen,
|
||||
buyerName: String(payload.buyerName || actorName || '').trim(),
|
||||
inboundBatch: String(payload.inboundBatch || '').trim(),
|
||||
sellerContact: String(payload.sellerContact || '').trim(),
|
||||
sellerGameId: String(payload.sellerGameId || '').trim(),
|
||||
sellerPaymentInfo: String(payload.sellerPaymentInfo || '').trim(),
|
||||
sellerPhone: String(payload.sellerPhone || '').trim(),
|
||||
credentialsJson: pickCredentialsJson(payload) ?? {},
|
||||
screenshotsJson: pickScreenshotsJson(payload) ?? [],
|
||||
remark: String(payload.remark || '').trim(),
|
||||
allowMatching: payload.allowMatching !== false,
|
||||
sourceType: 'manual',
|
||||
workerId: payload.workerId ? Number(payload.workerId) : null,
|
||||
workerName: String(payload.workerName || '').trim(),
|
||||
workerPhone: String(payload.workerPhone || '').trim(),
|
||||
depositAmountFen: payload.depositAmountFen ? Number(payload.depositAmountFen) : 0,
|
||||
now,
|
||||
/**
|
||||
* 客服手动入库——**业务已停用**。
|
||||
*
|
||||
* 寄卖的货源只允许走「打手在平台提报 → 客服审核入库」这一条路:手动入库的货没有提报单,
|
||||
* 也就没有寄卖单号,全流程(状态机/凭证链/搜索/卖家端)都无法用同一个号锚定。
|
||||
* 这里保留一个明确的拒绝而不是静默删除,是为了挡住旧页面缓存或直接调接口的写入,
|
||||
* 避免又冒出没有提报单的货;将来要恢复也只需回退这个守卫。
|
||||
*/
|
||||
export async function createAdminConsignmentInventory(_payload: JsonObject, _actorName = '') {
|
||||
throw createHttpError('客服手动入库已停用:寄卖货源请统一走打手在平台提报后审核入库', {
|
||||
statusCode: 409,
|
||||
errorCode: 'consignment_manual_inbound_disabled',
|
||||
})
|
||||
|
||||
return mapConsignmentInventory(row)
|
||||
}
|
||||
|
||||
export async function updateAdminConsignmentInventory(
|
||||
|
||||
@@ -144,3 +144,17 @@ test('提报行同样带出赠送凭证(审核界面要看这件货有没有
|
||||
assert.equal(mapped.giftEvidenceReviewStatus, 'accepted')
|
||||
assert.equal(mapped.giftWorkOrderNo, 'WO1')
|
||||
})
|
||||
|
||||
test('客服手动入库已停用:接口必须明确拒绝,不能再造出没有提报单的货', async () => {
|
||||
const { createAdminConsignmentInventory } = await import('./consignment-admin-service.js')
|
||||
// 手动入库的货没有提报单 → 没有寄卖单号 → 全流程无法用一个号锚定,业务已改为只允许平台提报
|
||||
await assert.rejects(
|
||||
() => createAdminConsignmentInventory({ itemName: '不该被创建的货' }, 'admin'),
|
||||
(error: unknown) => {
|
||||
const err = error as { errorCode?: string; code?: string; statusCode?: number }
|
||||
assert.equal(err.errorCode || err.code, 'consignment_manual_inbound_disabled')
|
||||
assert.equal(err.statusCode, 409)
|
||||
return true
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
| `632dddc4` | 上一轮:H5 渠道整体下线 + 凭据字段修复 + 状态机封闭(撤销守卫 / 库存绑定唯一性 / 生命周期解绑)+ 押金归属取台账 | 迁移 `079`、`consignment-*` 五个文件、`work-order-*` 三个仓库、两个前端面板 |
|
||||
| `5d11bf20` | 批次 C+D:指派部分成功补偿、凭证核销如实反馈、空规格不放行 + 履约/指派服务端规格复检 | `consignment-fulfillment-service.ts`、`consignment-matching-service.ts`、`consignment-repo.ts`、履约路由、`WorkOrdersPanel.tsx` |
|
||||
| `d6f6219f` | 批次 F1:内部现货履约停在「待加好友」,客服确认后才进赠送冷却 + 备注快捷项 | `work-order-gift-repo.ts`、履约服务、`WorkOrdersPanel.tsx`、`utils/gift-phase.ts` |
|
||||
| 本批提交(批次 M,同一提交内含本文档,故不写自身 hash) | 批次 M:停用客服手动入库(前端入口删除 + 后端 409 守卫),寄卖货源只走平台提报 | `consignment-admin-service.ts`、`consignment-repo.ts`、`ConsignmentInventoryPanel.tsx` |
|
||||
| 本批提交(批次 L,同一提交内含本文档,故不写自身 hash) | 批次 L:匹配弹窗两个动作一律二次确认 + 按钮分区/图标/后果说明,防点错 | `WorkOrdersPanel.tsx` |
|
||||
| 本批提交(批次 K,同一提交内含本文档,故不写自身 hash) | 批次 K:匹配弹窗改用寄卖单号(候选查询补 join),客服界面不再展示内部仓库号 | `consignment-repo.ts`、`WorkOrdersPanel.tsx`、`ConsignmentInventoryPanel.tsx` |
|
||||
| 本批提交(批次 J,同一提交内含本文档,故不写自身 hash) | 批次 J:凭证链可视化(提报凭证 + 赠送凭证),客服审核/仓库面板统一展示 | `consignment-admin-service.ts`、`consignment-repo.ts`、`ConsignmentSubmissionsPanel.tsx`、`ConsignmentInventoryPanel.tsx` |
|
||||
@@ -282,6 +283,28 @@
|
||||
前端在"部分匹配"候选上先 `modal.confirm` 危险确认,再带 `allowPartial: true` 重试,高配充低配的合法玩法保留但不可误触。
|
||||
3. ⏳ 匹配下推 SQL + 批量匹配接口 + 工单列表角标(P2-8)未做:当前仍是"取全量在库 → 内存比对"。
|
||||
|
||||
### 批次 M · 停用客服手动入库(本轮)
|
||||
|
||||
**背景**:手动入库是唯一会造出「没有提报单的现货」的入口,而这类货**没有寄卖单号**,
|
||||
于是全流程(统一状态机、凭证链、搜索、卖家端的单号锚定)都没法用一个号串起来——
|
||||
它只能在界面上回退显示内部仓库号 INV…,正是"一件货两个号"的残留。
|
||||
|
||||
**业务定调**:寄卖货源**只走「打手在平台提报 → 客服审核入库」这一条路**。
|
||||
|
||||
**改法**:
|
||||
|
||||
1. ✅ 前端「寄售仓库」的「手动入库」入口整块删除(按钮 + 入库弹窗 + 提交逻辑 + 服务函数),
|
||||
客服无法再建无提报单的货。
|
||||
2. ✅ 后端同时挡住,避免旧页面缓存或直接调接口绕过:
|
||||
`POST /worker-platform/consignment/inventories` 返回 409 `consignment_manual_inbound_disabled`
|
||||
(保留明确的拒绝而不是静默删除,将来要恢复只需回退这个守卫)。死代码一并清掉:
|
||||
服务层的原实现与仓库层的 `createConsignmentInventory` 都已删除(要恢复从 git 历史取)。
|
||||
3. ✅ 提报审核入库(正常路径)不受影响:它照旧生成内部仓库号 INV… 并写入 `source_submission_id`,
|
||||
即**每件货都必然挂着一个寄卖单号**,INV 只作为内部编号存在、界面不展示。
|
||||
|
||||
**真实库验证**:调用手动入库接口被拒(409 `consignment_manual_inbound_disabled`),
|
||||
且库里「没有提报单的现货行数」前后均为 0(没有偷偷写入)。
|
||||
|
||||
### 批次 L · 匹配弹窗防点错(本轮)
|
||||
|
||||
**问题**:匹配弹窗里「走接单平台指派打手(冻结押金交付)」与「内部现货履约」两个按钮
|
||||
|
||||
Reference in New Issue
Block a user