fix(consignment): 下线 H5 收集链接渠道并封闭寄卖履约状态机
下线定向收集链接(H5 填报)整条线,提报统一走接单平台打手端: - 删除前端 ConsignmentCollectPage、consignment-public 服务与 /consignment-collect/:token 路由 - 删除后端公开路由 routes/consignment.ts、consignment-public-service 与管理端 /consignment/links 三接口 - 迁移 079 删表 consignment_collect_links、删列 consignment_submissions.link_id, source_type 的 collection_link 归并为 worker_portal 并替换 CHECK 修复手工入库/编辑的凭据与截图静默丢失:后端统一按 credentialsJson ?? credentials、 screenshotsJson ?? screenshots 读取(入库/编辑/审核入库三个入口)。 按"已验收一律禁止撤销"封闭状态机: - 撤销寄售履约加状态白名单(in_progress|pending_acceptance|problem)+ 事务内 FOR UPDATE 锁单 + UPDATE 带 status 条件(0 行 409)+ 原因必填 + 清空指派/时限/验收草稿痕迹 - 库存改状态一律带条件:撤销要求 bound_work_order_id 命中本工单;作废/回库要求未被 进行中工单占用(被挡时提示具体工单号);079 加部分唯一索引并清理历史重复绑定 - 工单终止统一解绑回库:打手撤单/取消指派/退回大厅/撤单/打手退单申请/超时处置接入 releaseConsignmentInventoryBindingForWorkOrderWithClient(售后不改工单状态,无需接入) - 回库路径保留运营的 allow_matching 开关设置(不再强制开启),前端文案同步更正 顺带修掉资金与归属的同源缺陷: - 押金归属与重冻金额改取不可变的冻结核销行,审核改派不再"退给错人/向错人扣款" - 新增 assigned_worker_id 拆开"卖家"与"被指派履约人",占库不再覆盖卖家 - PUT /inventories/:id 禁止直接改 status;发布接口补 requireActiveWorker - 押金冻结/释放后补推钱包实时事件;迁移 079 给发布冻结加台账幂等唯一索引 验证:后端 401 测试/398 通过/3 skip(新增 5 例回归),前端 16 通过; 两侧 typecheck、lint:check、format:check 与 check:sql、build 全绿。 迁移 079 未经真实数据库执行(本环境无 Postgres),上库前请先 dry-run。
This commit is contained in:
@@ -170,6 +170,36 @@ export async function releaseConsignmentPublishDepositWithClient(
|
||||
return releaseAmount
|
||||
}
|
||||
|
||||
/**
|
||||
* 押金归属与金额以「台账实际冻结核销行」为准。
|
||||
*
|
||||
* 提报单上的 `worker_id` / `deposit_amount_fen` 都是可变字段(审核可以改派打手、
|
||||
* 运营可以调整金额),若用它反查钱包,会在改派后把押金退给从未发布过的打手
|
||||
* (对原发布者而言是静默不退的挂账);反过来重冻时也可能向错误钱包扣款。
|
||||
* 台账行是唯一不可变的事实来源。
|
||||
*/
|
||||
async function getConsignmentDepositLedgerSnapshot(
|
||||
client: PoolClient,
|
||||
submissionId: number | string,
|
||||
): Promise<{ ownerWorkerId: number; frozenAmountFen: number } | null> {
|
||||
const res = await client.query<{ worker_id: number | string | null; amount: number | string }>(
|
||||
`
|
||||
SELECT worker_id, ABS(amount) AS amount
|
||||
FROM worker_wallet_ledgers
|
||||
WHERE ledger_type = 'deposit_freeze'
|
||||
AND COALESCE(payload_json->>'kind', '') = 'consignment_publish'
|
||||
AND COALESCE(payload_json->>'consignmentSubmissionId', '') = $1
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
`,
|
||||
[String(submissionId)],
|
||||
)
|
||||
const row = res.rows[0]
|
||||
const ownerWorkerId = Number(row?.worker_id || 0)
|
||||
if (!ownerWorkerId) return null
|
||||
return { ownerWorkerId, frozenAmountFen: Math.max(0, Math.round(Number(row?.amount || 0))) }
|
||||
}
|
||||
|
||||
async function getConsignmentSubmissionDepositTarget(
|
||||
client: PoolClient,
|
||||
submissionId: number | string,
|
||||
@@ -192,11 +222,15 @@ async function getConsignmentSubmissionDepositTarget(
|
||||
[submissionId],
|
||||
)
|
||||
const row = submission.rows[0]
|
||||
const workerId = Number(row?.worker_id || 0)
|
||||
const ledger = await getConsignmentDepositLedgerSnapshot(client, submissionId)
|
||||
|
||||
const workerId = ledger?.ownerWorkerId || Number(row?.worker_id || 0)
|
||||
if (!workerId) return null
|
||||
|
||||
return {
|
||||
workerId,
|
||||
depositAmountFen: Math.max(0, Math.round(Number(row?.deposit_amount_fen || 0))),
|
||||
depositAmountFen:
|
||||
ledger?.frozenAmountFen || Math.max(0, Math.round(Number(row?.deposit_amount_fen || 0))),
|
||||
submissionNo: String(row?.submission_no || ''),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,12 @@ export type ConsignmentInventoryRow = {
|
||||
bound_work_order_id: string | number | null
|
||||
bound_platform_order_id: string
|
||||
shop_name: string
|
||||
source_type: 'manual' | 'collection_link' | 'recycle'
|
||||
source_type: 'manual' | 'worker_portal' | 'recycle'
|
||||
source_submission_id: string | number | null
|
||||
worker_id?: string | number | null
|
||||
worker_name?: string
|
||||
worker_phone?: string
|
||||
assigned_worker_id?: string | number | null
|
||||
deposit_amount_fen?: number
|
||||
inbound_at: string
|
||||
consumed_at: string | null
|
||||
@@ -43,35 +44,9 @@ export type ConsignmentInventoryRow = {
|
||||
category_name?: string
|
||||
}
|
||||
|
||||
export type ConsignmentCollectLinkRow = {
|
||||
id: string | number
|
||||
token: string
|
||||
title: string
|
||||
game_name: string
|
||||
category_id: string | number | null
|
||||
system: string
|
||||
server_zone: string
|
||||
series: string
|
||||
diamond_level: number
|
||||
brilliance_count: number
|
||||
preset_price_fen: number
|
||||
inbound_batch: string
|
||||
buyer_name: string
|
||||
template_json: string | Record<string, unknown>
|
||||
is_multi_use: boolean
|
||||
expires_at: string | null
|
||||
enabled: boolean
|
||||
created_by: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
category_name?: string
|
||||
submission_count?: number
|
||||
}
|
||||
|
||||
export type ConsignmentSubmissionRow = {
|
||||
id: string | number
|
||||
submission_no: string
|
||||
link_id: string | number | null
|
||||
game_name: string
|
||||
category_id: string | number | null
|
||||
item_name: string
|
||||
@@ -99,7 +74,6 @@ export type ConsignmentSubmissionRow = {
|
||||
created_at: string
|
||||
updated_at: string
|
||||
category_name?: string
|
||||
link_title?: string
|
||||
}
|
||||
|
||||
export type ConsignmentStatsSummary = {
|
||||
@@ -340,7 +314,7 @@ export async function createConsignmentInventory(input: {
|
||||
screenshotsJson?: unknown[]
|
||||
remark?: string
|
||||
allowMatching?: boolean
|
||||
sourceType?: 'manual' | 'collection_link' | 'recycle'
|
||||
sourceType?: 'manual' | 'worker_portal' | 'recycle'
|
||||
sourceSubmissionId?: number | null
|
||||
workerId?: number | null
|
||||
workerName?: string
|
||||
@@ -421,7 +395,6 @@ export async function updateConsignmentInventory(
|
||||
screenshotsJson?: unknown[] | undefined
|
||||
remark?: string | undefined
|
||||
allowMatching?: boolean | undefined
|
||||
status?: 'in_stock' | 'consumed' | 'archived' | undefined
|
||||
allocationStatus?: string | undefined
|
||||
workerId?: number | null | undefined
|
||||
workerName?: string | undefined
|
||||
@@ -501,10 +474,6 @@ export async function updateConsignmentInventory(
|
||||
values.push(input.allowMatching)
|
||||
updates.push(`allow_matching = $${values.length}`)
|
||||
}
|
||||
if (input.status !== undefined) {
|
||||
values.push(input.status)
|
||||
updates.push(`status = $${values.length}`)
|
||||
}
|
||||
if (input.allocationStatus !== undefined) {
|
||||
values.push(input.allocationStatus)
|
||||
updates.push(`allocation_status = $${values.length}`)
|
||||
@@ -552,7 +521,16 @@ export async function updateConsignmentInventory(
|
||||
export async function claimConsignmentInventoryForWorkerWithClient(
|
||||
client: PoolClient,
|
||||
id: number | string,
|
||||
input: { workerId: number; now: string; allocationStatus?: string },
|
||||
input: {
|
||||
workerId: number
|
||||
now: string
|
||||
allocationStatus?: string
|
||||
/** 占库时同时写入工单绑定,便于撤单/超时/售后按工单解绑回库。 */
|
||||
boundOrderId?: number | null
|
||||
boundWorkOrderId?: number | null
|
||||
boundPlatformOrderId?: string
|
||||
shopName?: string
|
||||
},
|
||||
): Promise<ConsignmentInventoryRow | null> {
|
||||
const res = await client.query<ConsignmentInventoryRow>(
|
||||
`
|
||||
@@ -560,20 +538,41 @@ export async function claimConsignmentInventoryForWorkerWithClient(
|
||||
SET
|
||||
status = 'consumed',
|
||||
allocation_status = $1,
|
||||
worker_id = $2,
|
||||
assigned_worker_id = $2,
|
||||
consumed_at = $3,
|
||||
updated_at = $3
|
||||
WHERE id = $4 AND status = 'in_stock'
|
||||
updated_at = $3,
|
||||
bound_order_id = $4,
|
||||
bound_work_order_id = $5,
|
||||
bound_platform_order_id = $6,
|
||||
shop_name = $7
|
||||
WHERE id = $8 AND status = 'in_stock'
|
||||
RETURNING *
|
||||
`,
|
||||
[input.allocationStatus || '已指派打手(押金履约中)', input.workerId, input.now, id],
|
||||
[
|
||||
input.allocationStatus || '已指派打手(押金履约中)',
|
||||
input.workerId,
|
||||
input.now,
|
||||
input.boundOrderId ?? null,
|
||||
input.boundWorkOrderId ?? null,
|
||||
input.boundPlatformOrderId ?? '',
|
||||
input.shopName ?? '',
|
||||
id,
|
||||
],
|
||||
)
|
||||
return res.rows[0] || null
|
||||
}
|
||||
|
||||
export async function claimConsignmentInventoryForWorker(
|
||||
id: number | string,
|
||||
input: { workerId: number; now: string; allocationStatus?: string },
|
||||
input: {
|
||||
workerId: number
|
||||
now: string
|
||||
allocationStatus?: string
|
||||
boundOrderId?: number | null
|
||||
boundWorkOrderId?: number | null
|
||||
boundPlatformOrderId?: string
|
||||
shopName?: string
|
||||
},
|
||||
): Promise<ConsignmentInventoryRow | null> {
|
||||
return withTransaction((client) =>
|
||||
claimConsignmentInventoryForWorkerWithClient(client, id, input),
|
||||
@@ -591,7 +590,7 @@ export async function releaseConsignmentInventoryClaimWithClient(
|
||||
SET
|
||||
status = 'in_stock',
|
||||
allocation_status = '待安排',
|
||||
worker_id = NULL,
|
||||
assigned_worker_id = NULL,
|
||||
bound_order_id = NULL,
|
||||
bound_work_order_id = NULL,
|
||||
bound_platform_order_id = '',
|
||||
@@ -617,6 +616,19 @@ export async function releaseConsignmentInventoryClaim(
|
||||
return withTransaction((client) => releaseConsignmentInventoryClaimWithClient(client, id, input))
|
||||
}
|
||||
|
||||
/** 仅当库存未被"进行中的工单"占用时才允许人工改状态(作废 / 退回复库) */
|
||||
const NO_LIVE_BOUND_WORK_ORDER_SQL = `
|
||||
(
|
||||
ci.bound_work_order_id IS NULL
|
||||
OR NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM work_orders wo
|
||||
WHERE wo.id = ci.bound_work_order_id
|
||||
AND wo.status NOT IN ('accepted', 'cancelled')
|
||||
)
|
||||
)
|
||||
`
|
||||
|
||||
export async function archiveConsignmentInventoryWithClient(
|
||||
client: PoolClient,
|
||||
id: number | string,
|
||||
@@ -624,12 +636,13 @@ export async function archiveConsignmentInventoryWithClient(
|
||||
): Promise<ConsignmentInventoryRow | null> {
|
||||
const res = await client.query<ConsignmentInventoryRow>(
|
||||
`
|
||||
UPDATE consignment_inventories
|
||||
UPDATE consignment_inventories ci
|
||||
SET
|
||||
status = 'archived',
|
||||
allow_matching = FALSE,
|
||||
updated_at = $1
|
||||
WHERE id = $2
|
||||
WHERE ci.id = $2
|
||||
AND ${NO_LIVE_BOUND_WORK_ORDER_SQL}
|
||||
RETURNING *
|
||||
`,
|
||||
[now, id],
|
||||
@@ -645,22 +658,24 @@ export async function returnConsignmentInventoryToStockWithClient(
|
||||
): Promise<ConsignmentInventoryRow | null> {
|
||||
const res = await client.query<ConsignmentInventoryRow>(
|
||||
`
|
||||
UPDATE consignment_inventories
|
||||
UPDATE consignment_inventories ci
|
||||
SET
|
||||
status = 'in_stock',
|
||||
allocation_status = '待安排',
|
||||
assigned_worker_id = NULL,
|
||||
bound_order_id = NULL,
|
||||
bound_work_order_id = NULL,
|
||||
bound_platform_order_id = '',
|
||||
shop_name = '',
|
||||
consumed_at = NULL,
|
||||
allow_matching = TRUE,
|
||||
remark = CASE
|
||||
WHEN $1 <> '' THEN remark || ' [退库原因: ' || $1 || ']'
|
||||
ELSE remark
|
||||
END,
|
||||
updated_at = $2
|
||||
WHERE id = $3 AND status = 'consumed'
|
||||
WHERE ci.id = $3
|
||||
AND ci.status = 'consumed'
|
||||
AND ${NO_LIVE_BOUND_WORK_ORDER_SQL}
|
||||
RETURNING *
|
||||
`,
|
||||
[reason, now, id],
|
||||
@@ -668,6 +683,44 @@ export async function returnConsignmentInventoryToStockWithClient(
|
||||
return res.rows[0] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单终止 / 回退时解绑寄卖库存:把被该工单占用的现货放回在库池。
|
||||
*
|
||||
* 撤单、退回大厅、超时处置、售后取消都必须调用本函数,否则现货会永久停留在
|
||||
* `consumed`(占成本但不再参与匹配)。发货押金仍保持冻结:现货回到在库池即等于
|
||||
* 重新挂卖,符合"发布冻结、回库重冻"的既有口径。
|
||||
*/
|
||||
export async function releaseConsignmentInventoryBindingForWorkOrderWithClient(
|
||||
client: PoolClient,
|
||||
workOrderId: number | string,
|
||||
input: { now: string; reason?: string },
|
||||
): Promise<ConsignmentInventoryRow | null> {
|
||||
const res = await client.query<ConsignmentInventoryRow>(
|
||||
`
|
||||
UPDATE consignment_inventories
|
||||
SET
|
||||
status = 'in_stock',
|
||||
allocation_status = '待安排',
|
||||
assigned_worker_id = NULL,
|
||||
bound_order_id = NULL,
|
||||
bound_work_order_id = NULL,
|
||||
bound_platform_order_id = '',
|
||||
shop_name = '',
|
||||
consumed_at = NULL,
|
||||
remark = CASE
|
||||
WHEN $3 = '' THEN remark
|
||||
ELSE remark || ' [工单解绑回库: ' || $3 || ']'
|
||||
END,
|
||||
updated_at = $1
|
||||
WHERE bound_work_order_id = $2
|
||||
AND status = 'consumed'
|
||||
RETURNING *
|
||||
`,
|
||||
[input.now, workOrderId, input.reason || ''],
|
||||
)
|
||||
return res.rows[0] || null
|
||||
}
|
||||
|
||||
/** 退回复库(将已出库物品恢复为在库待匹配) */
|
||||
export async function returnConsignmentInventoryToStock(
|
||||
id: number | string,
|
||||
@@ -706,132 +759,10 @@ export async function listActiveConsignmentInventoriesForMatching(options?: {
|
||||
return res.rows
|
||||
}
|
||||
|
||||
// ---------------- 收集链接管理 ----------------
|
||||
|
||||
export async function listConsignmentCollectLinks(): Promise<ConsignmentCollectLinkRow[]> {
|
||||
const sql = `
|
||||
SELECT
|
||||
ccl.*,
|
||||
wc.name AS category_name,
|
||||
(SELECT COUNT(*)::integer FROM consignment_submissions cs WHERE cs.link_id = ccl.id) AS submission_count
|
||||
FROM consignment_collect_links ccl
|
||||
LEFT JOIN work_categories wc ON wc.id = ccl.category_id
|
||||
ORDER BY ccl.id DESC
|
||||
`
|
||||
const res = await query<ConsignmentCollectLinkRow>(sql)
|
||||
return res.rows
|
||||
}
|
||||
|
||||
export async function getConsignmentCollectLinkByToken(
|
||||
token: string,
|
||||
): Promise<ConsignmentCollectLinkRow | null> {
|
||||
const sql = `
|
||||
SELECT
|
||||
ccl.*,
|
||||
wc.name AS category_name
|
||||
FROM consignment_collect_links ccl
|
||||
LEFT JOIN work_categories wc ON wc.id = ccl.category_id
|
||||
WHERE ccl.token = $1 AND ccl.enabled = TRUE
|
||||
LIMIT 1
|
||||
`
|
||||
const res = await query<ConsignmentCollectLinkRow>(sql, [token])
|
||||
return res.rows[0] || null
|
||||
}
|
||||
|
||||
export async function createConsignmentCollectLink(input: {
|
||||
token: string
|
||||
title: string
|
||||
gameName?: string
|
||||
categoryId?: number | null
|
||||
system?: string
|
||||
serverZone?: string
|
||||
series?: string
|
||||
diamondLevel?: number
|
||||
brillianceCount?: number
|
||||
presetPriceFen?: number
|
||||
inboundBatch?: string
|
||||
buyerName?: string
|
||||
templateJson?: Record<string, unknown>
|
||||
isMultiUse?: boolean
|
||||
expiresAt?: string | null
|
||||
createdBy?: string
|
||||
now: string
|
||||
}): Promise<ConsignmentCollectLinkRow> {
|
||||
const sql = `
|
||||
INSERT INTO consignment_collect_links (
|
||||
token, title, game_name, category_id, system, server_zone,
|
||||
series, diamond_level, brilliance_count, preset_price_fen,
|
||||
inbound_batch, buyer_name, template_json, is_multi_use,
|
||||
expires_at, enabled, created_by, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9, $10,
|
||||
$11, $12, $13::jsonb, $14,
|
||||
$15, TRUE, $16, $17, $17
|
||||
)
|
||||
RETURNING *
|
||||
`
|
||||
const res = await query<ConsignmentCollectLinkRow>(sql, [
|
||||
input.token,
|
||||
input.title,
|
||||
input.gameName || '和平精英',
|
||||
input.categoryId || null,
|
||||
input.system || '',
|
||||
input.serverZone || '',
|
||||
input.series || '',
|
||||
input.diamondLevel || 1,
|
||||
input.brillianceCount || 0,
|
||||
input.presetPriceFen || 0,
|
||||
input.inboundBatch || '',
|
||||
input.buyerName || '',
|
||||
toJsonString(input.templateJson || {}),
|
||||
input.isMultiUse !== false,
|
||||
input.expiresAt || null,
|
||||
input.createdBy || '',
|
||||
input.now,
|
||||
])
|
||||
return res.rows[0]!
|
||||
}
|
||||
|
||||
export async function updateConsignmentCollectLink(
|
||||
id: number | string,
|
||||
input: {
|
||||
title?: string | undefined
|
||||
enabled?: boolean | undefined
|
||||
now: string
|
||||
},
|
||||
): Promise<ConsignmentCollectLinkRow | null> {
|
||||
const updates: string[] = []
|
||||
const values: unknown[] = []
|
||||
|
||||
if (input.title !== undefined) {
|
||||
values.push(input.title)
|
||||
updates.push(`title = $${values.length}`)
|
||||
}
|
||||
if (input.enabled !== undefined) {
|
||||
values.push(input.enabled)
|
||||
updates.push(`enabled = $${values.length}`)
|
||||
}
|
||||
|
||||
values.push(input.now)
|
||||
updates.push(`updated_at = $${values.length}`)
|
||||
|
||||
values.push(id)
|
||||
const sql = `
|
||||
UPDATE consignment_collect_links
|
||||
SET ${updates.join(', ')}
|
||||
WHERE id = $${values.length}
|
||||
RETURNING *
|
||||
`
|
||||
const res = await query<ConsignmentCollectLinkRow>(sql, values)
|
||||
return res.rows[0] || null
|
||||
}
|
||||
|
||||
// ---------------- 玩家提报与审核 ----------------
|
||||
// ---------------- 打手提报与审核 ----------------
|
||||
|
||||
export async function listConsignmentSubmissions(filters: {
|
||||
status?: string | undefined
|
||||
linkId?: number | undefined
|
||||
workerId?: number | undefined
|
||||
keyword?: string | undefined
|
||||
page?: number | undefined
|
||||
@@ -844,10 +775,6 @@ export async function listConsignmentSubmissions(filters: {
|
||||
values.push(filters.status)
|
||||
conditions.push(`cs.status = $${values.length}`)
|
||||
}
|
||||
if (filters.linkId) {
|
||||
values.push(filters.linkId)
|
||||
conditions.push(`cs.link_id = $${values.length}`)
|
||||
}
|
||||
if (filters.workerId) {
|
||||
values.push(filters.workerId)
|
||||
conditions.push(`cs.worker_id = $${values.length}`)
|
||||
@@ -872,11 +799,9 @@ export async function listConsignmentSubmissions(filters: {
|
||||
const listSql = `
|
||||
SELECT
|
||||
cs.*,
|
||||
wc.name AS category_name,
|
||||
ccl.title AS link_title
|
||||
wc.name AS category_name
|
||||
FROM consignment_submissions cs
|
||||
LEFT JOIN work_categories wc ON wc.id = cs.category_id
|
||||
LEFT JOIN consignment_collect_links ccl ON ccl.id = cs.link_id
|
||||
${whereClause}
|
||||
ORDER BY cs.id DESC
|
||||
LIMIT $${values.length - 1} OFFSET $${values.length}
|
||||
@@ -891,11 +816,9 @@ export async function getConsignmentSubmissionById(
|
||||
const sql = `
|
||||
SELECT
|
||||
cs.*,
|
||||
wc.name AS category_name,
|
||||
ccl.title AS link_title
|
||||
wc.name AS category_name
|
||||
FROM consignment_submissions cs
|
||||
LEFT JOIN work_categories wc ON wc.id = cs.category_id
|
||||
LEFT JOIN consignment_collect_links ccl ON ccl.id = cs.link_id
|
||||
WHERE cs.id = $1
|
||||
LIMIT 1
|
||||
`
|
||||
@@ -905,7 +828,6 @@ export async function getConsignmentSubmissionById(
|
||||
|
||||
export async function createConsignmentSubmission(input: {
|
||||
submissionNo: string
|
||||
linkId?: number | null
|
||||
gameName?: string
|
||||
categoryId?: number | null
|
||||
itemName: string
|
||||
@@ -934,7 +856,6 @@ export async function createConsignmentSubmissionWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
submissionNo: string
|
||||
linkId?: number | null
|
||||
gameName?: string
|
||||
categoryId?: number | null
|
||||
itemName: string
|
||||
@@ -959,25 +880,24 @@ export async function createConsignmentSubmissionWithClient(
|
||||
): Promise<ConsignmentSubmissionRow> {
|
||||
const sql = `
|
||||
INSERT INTO consignment_submissions (
|
||||
submission_no, link_id, game_name, category_id, item_name,
|
||||
submission_no, game_name, category_id, item_name,
|
||||
system, server_zone, series, diamond_level, brilliance_count,
|
||||
seller_game_id, seller_payment_info, seller_phone, contact_info,
|
||||
content_json, screenshots_json, asking_price_fen,
|
||||
worker_id, worker_name, worker_phone, deposit_amount_fen,
|
||||
status, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14,
|
||||
$15::jsonb, $16::jsonb, $17,
|
||||
$18, $19, $20, $21,
|
||||
'pending_review', $22, $22
|
||||
$1, $2, $3, $4,
|
||||
$5, $6, $7, $8, $9,
|
||||
$10, $11, $12, $13,
|
||||
$14::jsonb, $15::jsonb, $16,
|
||||
$17, $18, $19, $20,
|
||||
'pending_review', $21, $21
|
||||
)
|
||||
RETURNING *
|
||||
`
|
||||
const res = await client.query<ConsignmentSubmissionRow>(sql, [
|
||||
input.submissionNo,
|
||||
input.linkId || null,
|
||||
input.gameName || '和平精英',
|
||||
input.categoryId || null,
|
||||
input.itemName,
|
||||
@@ -1099,7 +1019,7 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
||||
$7, $8, $9, $10, $11,
|
||||
$12, $13, $14, $15,
|
||||
$16, $17::jsonb, $18::jsonb, $19,
|
||||
TRUE, 'in_stock', '待安排', 'collection_link',
|
||||
TRUE, 'in_stock', '待安排', 'worker_portal',
|
||||
$20, $21, $22, $23, $24,
|
||||
$25, $25, $25
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { withTransaction } from '../../db/client.js'
|
||||
import { isWorkOrderGiftDepositForfeitable } from '../../domain/work-order-acceptance-mode.js'
|
||||
import { releaseConsignmentInventoryBindingForWorkOrderWithClient } from './consignment-repo.js'
|
||||
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
||||
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
|
||||
import { getWorkOrderByIdWithClient } from './work-order-query-repo.js'
|
||||
@@ -78,6 +79,12 @@ export async function cancelWorkerWorkOrder(input: {
|
||||
)
|
||||
}
|
||||
|
||||
// 打手撤单:被本工单占用的寄卖现货放回在库池(否则现货永久停留在 consumed)。
|
||||
await releaseConsignmentInventoryBindingForWorkOrderWithClient(client, input.workOrderId, {
|
||||
now: input.now,
|
||||
reason: '打手撤单',
|
||||
})
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'worker',
|
||||
@@ -181,6 +188,12 @@ export async function unassignWorkOrder(input: {
|
||||
)
|
||||
}
|
||||
|
||||
// 取消指派:寄卖现货回到在库池等待重新匹配。
|
||||
await releaseConsignmentInventoryBindingForWorkOrderWithClient(client, input.workOrderId, {
|
||||
now: input.now,
|
||||
reason: '后台取消指派',
|
||||
})
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'admin',
|
||||
@@ -289,6 +302,12 @@ export async function returnAssignedWorkOrderToHall(input: {
|
||||
)
|
||||
}
|
||||
|
||||
// 退回大厅:寄卖现货回到在库池;若该单原本走内部现货履约,运营可重新指派。
|
||||
await releaseConsignmentInventoryBindingForWorkOrderWithClient(client, input.workOrderId, {
|
||||
now: input.now,
|
||||
reason: '后台退回大厅',
|
||||
})
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'admin',
|
||||
@@ -397,6 +416,12 @@ export async function cancelAssignedWorkOrder(input: {
|
||||
)
|
||||
}
|
||||
|
||||
// 撤单取消订单:寄卖现货回到在库池,避免占用成本却不再参与匹配。
|
||||
await releaseConsignmentInventoryBindingForWorkOrderWithClient(client, input.workOrderId, {
|
||||
now: input.now,
|
||||
reason: '后台撤单取消订单',
|
||||
})
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'admin',
|
||||
@@ -479,6 +504,13 @@ export async function reopenCancelledWorkOrder(input: {
|
||||
[input.now, input.workOrderId],
|
||||
)
|
||||
|
||||
// 重新启用已取消工单:历史绑定若未清理干净,这里兜底解绑,避免"重新履约再次占库"
|
||||
// 造成同一件现货被两张单子锁定。
|
||||
await releaseConsignmentInventoryBindingForWorkOrderWithClient(client, input.workOrderId, {
|
||||
now: input.now,
|
||||
reason: '已取消工单重新启用',
|
||||
})
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'admin',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { withTransaction } from '../../db/client.js'
|
||||
import { isWorkOrderGiftDepositForfeitable } from '../../domain/work-order-acceptance-mode.js'
|
||||
import { releaseConsignmentInventoryBindingForWorkOrderWithClient } from './consignment-repo.js'
|
||||
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
||||
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
|
||||
import { getWorkOrderByIdWithClient } from './work-order-query-repo.js'
|
||||
@@ -318,6 +319,14 @@ export async function settleOverdueWorkOrder(input: {
|
||||
[targetStatus, input.now, input.workOrderId],
|
||||
)
|
||||
|
||||
// 超时处置(重开/取消):被本工单占用的寄卖现货必须先放回在库池。
|
||||
// 内部现货履约单在赠送冷静期内同样带 deadline_at,若不解绑,现货会永久停留在 consumed:
|
||||
// 既不再参与匹配,发货押金也没有释放入口。
|
||||
await releaseConsignmentInventoryBindingForWorkOrderWithClient(client, input.workOrderId, {
|
||||
now: input.now,
|
||||
reason: `超时处置(${effectivePolicy})`,
|
||||
})
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'system',
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PoolClient } from 'pg'
|
||||
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { isWorkOrderGiftDepositForfeitable } from '../../domain/work-order-acceptance-mode.js'
|
||||
import { releaseConsignmentInventoryBindingForWorkOrderWithClient } from './consignment-repo.js'
|
||||
import {
|
||||
ensureWorkerWalletWithClient,
|
||||
getWorkerWalletWithClient,
|
||||
@@ -557,6 +558,11 @@ export async function reviewWorkerCancelRequest(input: {
|
||||
WHERE id = $2`,
|
||||
[input.now, workOrder.id],
|
||||
)
|
||||
// 打手退单申请通过:工单回到大厅,被本工单占用的寄卖现货同时放回在库池。
|
||||
await releaseConsignmentInventoryBindingForWorkOrderWithClient(client, workOrder.id, {
|
||||
now: input.now,
|
||||
reason: '打手退单申请通过',
|
||||
})
|
||||
if (releasedDepositAmount > 0) {
|
||||
const nextAvailable =
|
||||
Number(wallet?.available_amount || 0) + (forfeitGiftDeposit ? 0 : releasedDepositAmount)
|
||||
|
||||
Reference in New Issue
Block a user