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
@@ -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
},
)
})