|
|
|
@@ -2,16 +2,26 @@ import { withTransaction } from '../../db/client.js'
|
|
|
|
|
import { freezeConsignmentPublishDepositWithClient } from '../../repositories/worker-platform/consignment-deposit-repo.js'
|
|
|
|
|
import {
|
|
|
|
|
createConsignmentSubmissionWithClient,
|
|
|
|
|
getConsignmentInventoryById,
|
|
|
|
|
getConsignmentInventoryBySubmissionId,
|
|
|
|
|
getConsignmentSubmissionById,
|
|
|
|
|
listConsignmentInventories,
|
|
|
|
|
listConsignmentSubmissions,
|
|
|
|
|
} from '../../repositories/worker-platform/consignment-repo.js'
|
|
|
|
|
import type { ConsignmentInventoryRow } from '../../repositories/worker-platform/consignment-repo.js'
|
|
|
|
|
import { getWorkerUserById } from '../../repositories/worker-platform/index.js'
|
|
|
|
|
import { listWorkOrdersByIds } from '../../repositories/worker-platform/work-order-query-repo.js'
|
|
|
|
|
import { deriveConsignmentItemStage } from '../../domain/consignment-item-stage.js'
|
|
|
|
|
import { hasInternalConsignmentFulfillment } from '../../repositories/worker-platform/work-order-gift-repo.js'
|
|
|
|
|
import type { JsonObject } from '../../types/json.js'
|
|
|
|
|
import { createHttpError } from '../../utils/http.js'
|
|
|
|
|
import { generateConsignmentSubmissionNo } from '../../utils/random.js'
|
|
|
|
|
import { nowIso } from '../../utils/time.js'
|
|
|
|
|
import { publishWorkerWalletRealtimeChange } from '../realtime/realtime-event-service.js'
|
|
|
|
|
import { getConsignmentPublishDepositFen as getConfiguredConsignmentPublishDepositFen } from './worker-finance-config-service.js'
|
|
|
|
|
import { confirmAdminWorkOrderFriendAdded } from './admin-work-order-gift-service.js'
|
|
|
|
|
import { submitWorkerOrderAcceptance } from './worker-acceptance-service.js'
|
|
|
|
|
import type { WorkerSession } from './worker-session-context-service.js'
|
|
|
|
|
|
|
|
|
|
export function getConsignmentPublishDepositFen() {
|
|
|
|
|
return getConfiguredConsignmentPublishDepositFen()
|
|
|
|
@@ -182,48 +192,246 @@ export async function publishWorkerConsignment(workerId: number, payload: JsonOb
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 「我的寄卖」列表。
|
|
|
|
|
*
|
|
|
|
|
* 一件货以前会返回两条记录(提报单 SUB… + 库存 INV…),前端也只能分两个列表渲染,
|
|
|
|
|
* 卖家看到同一件货两行、两个号。现在**以提报单号为唯一锚点**聚合:一个提报单一条记录,
|
|
|
|
|
* 库存与绑定工单的信息挂上去,阶段由统一状态机推导(三套底层状态 → 一个卖家看得懂的阶段)。
|
|
|
|
|
*/
|
|
|
|
|
export async function listWorkerConsignments(workerId: number) {
|
|
|
|
|
const [submissionsRes, inventoriesRes] = await Promise.all([
|
|
|
|
|
listConsignmentSubmissions({ workerId, pageSize: 100 }),
|
|
|
|
|
listConsignmentInventories({ workerId, pageSize: 100 }),
|
|
|
|
|
listConsignmentSubmissions({ workerId, pageSize: 200 }),
|
|
|
|
|
listConsignmentInventories({ workerId, pageSize: 200 }),
|
|
|
|
|
])
|
|
|
|
|
const publishDepositFen = getConsignmentPublishDepositFen()
|
|
|
|
|
|
|
|
|
|
const inventoryBySubmission = new Map<number, (typeof inventoriesRes.items)[number]>()
|
|
|
|
|
const orphanInventories: typeof inventoriesRes.items = []
|
|
|
|
|
for (const inv of inventoriesRes.items) {
|
|
|
|
|
const submissionId = Number(inv.source_submission_id || 0)
|
|
|
|
|
if (submissionId > 0 && !inventoryBySubmission.has(submissionId)) {
|
|
|
|
|
inventoryBySubmission.set(submissionId, inv)
|
|
|
|
|
} else {
|
|
|
|
|
// 客服手工入库的现货没有提报单,用库存号自己当锚点
|
|
|
|
|
orphanInventories.push(inv)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const boundWorkOrderIds = [...inventoryBySubmission.values(), ...orphanInventories]
|
|
|
|
|
.map((inv) => Number(inv.bound_work_order_id || 0))
|
|
|
|
|
.filter((id) => id > 0)
|
|
|
|
|
const workOrders = await listWorkOrdersByIds(boundWorkOrderIds)
|
|
|
|
|
const workOrderById = new Map(workOrders.map((wo) => [Number(wo.id), wo]))
|
|
|
|
|
|
|
|
|
|
const viewer = { role: 'seller', workerId: Number(workerId) } as const
|
|
|
|
|
const resolveStage = (
|
|
|
|
|
submissionStatus: string,
|
|
|
|
|
inv: (typeof inventoriesRes.items)[number] | undefined,
|
|
|
|
|
) => {
|
|
|
|
|
const workOrder = inv?.bound_work_order_id
|
|
|
|
|
? workOrderById.get(Number(inv.bound_work_order_id))
|
|
|
|
|
: undefined
|
|
|
|
|
return deriveConsignmentItemStage({
|
|
|
|
|
submissionStatus,
|
|
|
|
|
inventoryStatus: inv?.status ?? null,
|
|
|
|
|
viewer,
|
|
|
|
|
workOrder: workOrder
|
|
|
|
|
? {
|
|
|
|
|
status: String(workOrder.status || ''),
|
|
|
|
|
acceptance_mode: workOrder.acceptance_mode,
|
|
|
|
|
gift_phase: workOrder.gift_phase,
|
|
|
|
|
gift_available_at: workOrder.gift_available_at,
|
|
|
|
|
friend_added_at: workOrder.friend_added_at,
|
|
|
|
|
internalStock: hasInternalConsignmentFulfillment(workOrder),
|
|
|
|
|
assignedWorkerId: Number(workOrder.assigned_worker_id || 0) || null,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const items = [
|
|
|
|
|
...submissionsRes.items.map((sub) => {
|
|
|
|
|
const inv = inventoryBySubmission.get(Number(sub.id))
|
|
|
|
|
const stage = resolveStage(String(sub.status || ''), inv)
|
|
|
|
|
return {
|
|
|
|
|
/** 唯一锚点:提报单号贯穿发布→审核→在库→匹配→冷却→赠送→结算 */
|
|
|
|
|
consignmentNo: sub.submission_no,
|
|
|
|
|
submissionId: sub.id,
|
|
|
|
|
inventoryId: inv?.id ?? null,
|
|
|
|
|
workOrderId: inv?.bound_work_order_id ?? null,
|
|
|
|
|
itemName: sub.item_name,
|
|
|
|
|
system: sub.system,
|
|
|
|
|
serverZone: sub.server_zone,
|
|
|
|
|
series: sub.series,
|
|
|
|
|
diamondLevel: sub.diamond_level,
|
|
|
|
|
brillianceCount: sub.brilliance_count,
|
|
|
|
|
askingPriceFen: sub.asking_price_fen,
|
|
|
|
|
depositAmountFen: sub.deposit_amount_fen,
|
|
|
|
|
reviewNote: sub.review_note,
|
|
|
|
|
screenshotsJson: sub.screenshots_json,
|
|
|
|
|
createdAt: sub.created_at,
|
|
|
|
|
...stage,
|
|
|
|
|
/** 冷却截止时间,前端拿它显示倒计时 */
|
|
|
|
|
giftAvailableAt: inv?.bound_work_order_id
|
|
|
|
|
? (workOrderById.get(Number(inv.bound_work_order_id))?.gift_available_at ?? null)
|
|
|
|
|
: null,
|
|
|
|
|
friendAddedAt: inv?.bound_work_order_id
|
|
|
|
|
? (workOrderById.get(Number(inv.bound_work_order_id))?.friend_added_at ?? null)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
...orphanInventories.map((inv) => {
|
|
|
|
|
const stage = resolveStage('approved', inv)
|
|
|
|
|
return {
|
|
|
|
|
consignmentNo: inv.inventory_no,
|
|
|
|
|
submissionId: null,
|
|
|
|
|
inventoryId: inv.id,
|
|
|
|
|
workOrderId: inv.bound_work_order_id ?? null,
|
|
|
|
|
itemName: inv.item_name,
|
|
|
|
|
system: inv.system,
|
|
|
|
|
serverZone: inv.server_zone,
|
|
|
|
|
series: inv.series,
|
|
|
|
|
diamondLevel: inv.diamond_level,
|
|
|
|
|
brillianceCount: inv.brilliance_count,
|
|
|
|
|
askingPriceFen: inv.cost_price_fen,
|
|
|
|
|
depositAmountFen: inv.deposit_amount_fen,
|
|
|
|
|
reviewNote: '',
|
|
|
|
|
screenshotsJson: [],
|
|
|
|
|
createdAt: inv.created_at,
|
|
|
|
|
...stage,
|
|
|
|
|
giftAvailableAt: inv.bound_work_order_id
|
|
|
|
|
? (workOrderById.get(Number(inv.bound_work_order_id))?.gift_available_at ?? null)
|
|
|
|
|
: null,
|
|
|
|
|
friendAddedAt: inv.bound_work_order_id
|
|
|
|
|
? (workOrderById.get(Number(inv.bound_work_order_id))?.friend_added_at ?? null)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
].sort((a, b) => Date.parse(String(b.createdAt || '')) - Date.parse(String(a.createdAt || '')))
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
publishDepositFen,
|
|
|
|
|
submissions: submissionsRes.items.map((sub) => ({
|
|
|
|
|
id: sub.id,
|
|
|
|
|
submissionNo: sub.submission_no,
|
|
|
|
|
itemName: sub.item_name,
|
|
|
|
|
system: sub.system,
|
|
|
|
|
serverZone: sub.server_zone,
|
|
|
|
|
series: sub.series,
|
|
|
|
|
diamondLevel: sub.diamond_level,
|
|
|
|
|
brillianceCount: sub.brilliance_count,
|
|
|
|
|
askingPriceFen: sub.asking_price_fen,
|
|
|
|
|
status: sub.status,
|
|
|
|
|
reviewNote: sub.review_note,
|
|
|
|
|
resultInventoryId: sub.result_inventory_id,
|
|
|
|
|
screenshotsJson: sub.screenshots_json,
|
|
|
|
|
contentJson: sub.content_json,
|
|
|
|
|
depositAmountFen: sub.deposit_amount_fen,
|
|
|
|
|
createdAt: sub.created_at,
|
|
|
|
|
})),
|
|
|
|
|
inventories: inventoriesRes.items.map((inv) => ({
|
|
|
|
|
id: inv.id,
|
|
|
|
|
inventoryNo: inv.inventory_no,
|
|
|
|
|
itemName: inv.item_name,
|
|
|
|
|
system: inv.system,
|
|
|
|
|
serverZone: inv.server_zone,
|
|
|
|
|
series: inv.series,
|
|
|
|
|
diamondLevel: inv.diamond_level,
|
|
|
|
|
brillianceCount: inv.brilliance_count,
|
|
|
|
|
costPriceFen: inv.cost_price_fen,
|
|
|
|
|
status: inv.status,
|
|
|
|
|
allocationStatus: inv.allocation_status,
|
|
|
|
|
boundPlatformOrderId: inv.bound_platform_order_id,
|
|
|
|
|
depositAmountFen: inv.deposit_amount_fen,
|
|
|
|
|
createdAt: inv.created_at,
|
|
|
|
|
})),
|
|
|
|
|
items,
|
|
|
|
|
stageCounts: items.reduce<Record<string, number>>((acc, item) => {
|
|
|
|
|
acc[item.stage] = (acc[item.stage] || 0) + 1
|
|
|
|
|
return acc
|
|
|
|
|
}, {}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 卖家侧「确认加好友」与「提交赠送截图」的公共入口。
|
|
|
|
|
*
|
|
|
|
|
* 两个动作本质上就是工单赠送流程里的既有环节,这里只做**归属校验**后复用同一套仓库/服务:
|
|
|
|
|
* 谁在履约谁才能操作,避免卖家去动别人的订单。
|
|
|
|
|
*/
|
|
|
|
|
async function resolveSellerConsignmentWorkOrder(
|
|
|
|
|
workerId: number,
|
|
|
|
|
anchor: { submissionId: number | string } | { inventoryId: number | string },
|
|
|
|
|
): Promise<{ workOrderId: number; inventoryId: number }> {
|
|
|
|
|
let inventory: ConsignmentInventoryRow | null = null
|
|
|
|
|
|
|
|
|
|
if ('submissionId' in anchor) {
|
|
|
|
|
const submission = await getConsignmentSubmissionById(anchor.submissionId)
|
|
|
|
|
if (!submission || Number(submission.worker_id) !== Number(workerId)) {
|
|
|
|
|
throw createHttpError('寄卖单不存在或不属于当前账号', {
|
|
|
|
|
statusCode: 404,
|
|
|
|
|
errorCode: 'consignment_item_not_found',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
inventory = await getConsignmentInventoryBySubmissionId(submission.id)
|
|
|
|
|
} else {
|
|
|
|
|
// 客服手工入库的现货没有提报单,只能按库存号定位(提报单线不走这里,避免 id 撞号误操作)
|
|
|
|
|
inventory = await getConsignmentInventoryById(anchor.inventoryId)
|
|
|
|
|
if (!inventory || Number(inventory.worker_id || 0) !== Number(workerId)) {
|
|
|
|
|
throw createHttpError('寄卖单不存在或不属于当前账号', {
|
|
|
|
|
statusCode: 404,
|
|
|
|
|
errorCode: 'consignment_item_not_found',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!inventory) {
|
|
|
|
|
throw createHttpError('该寄卖单还没有生成库存,暂无可执行的操作', {
|
|
|
|
|
statusCode: 409,
|
|
|
|
|
errorCode: 'consignment_inventory_missing',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const workOrderId = Number(inventory.bound_work_order_id || 0)
|
|
|
|
|
if (!workOrderId) {
|
|
|
|
|
throw createHttpError('该寄卖单当前没有关联订单,无需操作', {
|
|
|
|
|
statusCode: 409,
|
|
|
|
|
errorCode: 'consignment_work_order_missing',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [workOrder] = await listWorkOrdersByIds([workOrderId])
|
|
|
|
|
if (!workOrder) {
|
|
|
|
|
throw createHttpError('关联订单不存在', {
|
|
|
|
|
statusCode: 404,
|
|
|
|
|
errorCode: 'work_order_not_found',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (Number(workOrder.assigned_worker_id || 0) !== Number(workerId)) {
|
|
|
|
|
throw createHttpError('该订单不是由你履约,无法在此操作', {
|
|
|
|
|
statusCode: 403,
|
|
|
|
|
errorCode: 'consignment_fulfillment_not_owned',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { workOrderId, inventoryId: Number(inventory.id) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 卖家在「我的寄卖」确认已在游戏内加好买家好友。
|
|
|
|
|
*
|
|
|
|
|
* 确认即进入赠送冷却期,冷却结束前不能赠送;且按寄卖规则,
|
|
|
|
|
* **确认加好友之后未完成赠送的押金不退**。
|
|
|
|
|
*/
|
|
|
|
|
export async function confirmWorkerConsignmentFriendAdded(
|
|
|
|
|
workerId: number,
|
|
|
|
|
submissionId: number | string,
|
|
|
|
|
workerName = '',
|
|
|
|
|
) {
|
|
|
|
|
const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, { submissionId })
|
|
|
|
|
return confirmAdminWorkOrderFriendAdded(workOrderId, workerName || `worker:${workerId}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 客服手工入库现货(没有提报单)的同一动作:按库存号定位 */
|
|
|
|
|
export async function confirmWorkerConsignmentInventoryFriendAdded(
|
|
|
|
|
workerId: number,
|
|
|
|
|
inventoryId: number | string,
|
|
|
|
|
workerName = '',
|
|
|
|
|
) {
|
|
|
|
|
const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, { inventoryId })
|
|
|
|
|
return confirmAdminWorkOrderFriendAdded(workOrderId, workerName || `worker:${workerId}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 卖家提交赠送完成后的截图给客服审核。
|
|
|
|
|
*
|
|
|
|
|
* 直接复用打手验收提交链路:它会校验「必须是本人订单」「冷却必须已结束」「必须带图」,
|
|
|
|
|
* 提交成功后工单进入待验收,客服在工单面板验收。
|
|
|
|
|
*/
|
|
|
|
|
export async function submitWorkerConsignmentGiftEvidence(
|
|
|
|
|
workerId: number,
|
|
|
|
|
submissionId: number | string,
|
|
|
|
|
payload: JsonObject,
|
|
|
|
|
session: WorkerSession,
|
|
|
|
|
) {
|
|
|
|
|
const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, { submissionId })
|
|
|
|
|
return submitWorkerOrderAcceptance(workOrderId, payload, session)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 客服手工入库现货(没有提报单)的同一动作:按库存号定位 */
|
|
|
|
|
export async function submitWorkerConsignmentInventoryGiftEvidence(
|
|
|
|
|
workerId: number,
|
|
|
|
|
inventoryId: number | string,
|
|
|
|
|
payload: JsonObject,
|
|
|
|
|
session: WorkerSession,
|
|
|
|
|
) {
|
|
|
|
|
const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, { inventoryId })
|
|
|
|
|
return submitWorkerOrderAcceptance(workOrderId, payload, session)
|
|
|
|
|
}
|
|
|
|
|