merge: 合并 main 到 dev(含订单来源快照与寄售改名同步)

# Conflicts:
#	README.md
#	apps/backend/src/db/migrate.ts
This commit is contained in:
2026-11-12 15:35:42 +08:00
38 changed files with 860 additions and 289 deletions
@@ -7,6 +7,7 @@ import {
applyOrderItemRevenueAllocationWithExecutor,
replaceOrderItemsWithExecutor,
} from '../../repositories/order-item-repo.js'
import { appendOrderSourceSnapshotWithExecutor } from '../../repositories/order-source-snapshot-repo.js'
import { withTransaction } from '../../db/client.js'
import { syncDeliveryTasksForOrder } from './delivery-task-service.js'
import { resolveOrderItemForFulfillment } from '../fulfillment/product-resolution-service.js'
@@ -189,6 +190,34 @@ export async function upsertOrderFromSource(
})
}
await appendOrderSourceSnapshotWithExecutor(executor, {
orderId: Number(order.id),
provider: event.provider,
platform: event.platform,
shopId: String(order.shop_id || event.shopId || '').trim(),
platformOrderId: event.platformOrderId,
sourceLabel,
payload: {
sourceLabel,
provider: event.provider,
platform: event.platform,
shopId: event.shopId,
shopName: event.shopName,
platformOrderId: event.platformOrderId,
orderStatus: event.orderStatus,
payStatus: event.payStatus,
buyerId: event.buyerId,
buyerName: event.buyerName,
receiverContact: event.receiverContact,
totalAmount: event.totalAmount,
currency: event.currency,
paidAt: event.paidAt || null,
rawPayload: event.rawPayload,
items: event.items,
},
recordedAt: now,
})
const orderItems = await replaceOrderItemsWithExecutor(
executor,
order.id,
@@ -25,9 +25,8 @@ import type { OrderItemRow, OrderRow } from '../../../types/repository/rows.js'
export const OPEN_91_PENDING_CONFIG_STATUS = 'pending_config'
export const OPEN_91_MANUAL_FAILED_STATUS = 'manual_failed'
export async function markOpen91OrderPendingConfig(order: OrderRow, reason = '') {
export async function markOpen91OrderPendingConfig(order: OrderRow, _reason = '') {
const now = nowIso()
const rawPayload = parseJsonObject(order.raw_payload_json)
return updateOrder(order.id, {
provider: order.provider,
platform: order.platform,
@@ -41,11 +40,6 @@ export async function markOpen91OrderPendingConfig(order: OrderRow, reason = '')
receiverContact: order.receiver_contact,
totalAmount: Number(order.total_amount) || 0,
currency: order.currency,
rawPayloadJson: JSON.stringify({
...rawPayload,
pendingReason: String(reason || '').trim() || 'fulfillment_not_ready',
pendingAt: now,
}),
paidAt: order.paid_at,
updatedAt: now,
})
@@ -154,11 +148,6 @@ export async function upsertOpen91PendingOrder(payload: JsonObject = {}, config:
platformOrderId: event.platformOrderId,
})
const rawPayloadJson = JSON.stringify({
...event.rawPayload,
pendingReason: 'unconfigured_items',
pendingAt: now,
})
const orderPayload = {
provider: event.provider,
platform: event.platform,
@@ -172,7 +161,6 @@ export async function upsertOpen91PendingOrder(payload: JsonObject = {}, config:
receiverContact: '',
totalAmount: event.totalAmount,
currency: 'CNY',
rawPayloadJson,
paidAt: existing?.paid_at || now,
}
@@ -245,10 +233,9 @@ export async function retryOpen91Order(orderId: string | number) {
}
}
export async function failOpen91Order(orderId: string | number, reason = '') {
export async function failOpen91Order(orderId: string | number, _reason = '') {
const order = await getRequiredOpen91Order(orderId)
const now = nowIso()
const rawPayload = parseJsonObject(order.raw_payload_json)
const updated = await updateOrder(order.id, {
provider: order.provider,
platform: order.platform,
@@ -262,11 +249,6 @@ export async function failOpen91Order(orderId: string | number, reason = '') {
receiverContact: order.receiver_contact,
totalAmount: order.total_amount,
currency: order.currency,
rawPayloadJson: JSON.stringify({
...rawPayload,
manualFailedAt: now,
manualFailedReason: String(reason || '').trim() || '商家手动标记无法履约',
}),
paidAt: order.paid_at,
updatedAt: now,
})
@@ -3,12 +3,14 @@ import type { Response } from 'express'
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
import {
type ConsignmentInventoryRow,
getLatestMaterialEventAtByOrderIds,
getPendingCancelRequestOrderIds,
getPendingFeedbackOrderIds,
getWorkerFinanceRequestById,
getWorkerUserById,
getWorkOrderById,
listConsignmentInventoriesByIds,
listWorkerTimeoutEventsByOrderIds,
listWorkerWorkOrderNotes,
listWorkerWorkOrderViews,
@@ -23,6 +25,7 @@ import {
type RealtimeEventRow,
} from '../../repositories/realtime-event-repo.js'
import { getAdminDashboardSummary } from '../admin/admin-dashboard-service.js'
import { safeParseJson } from '../admin/admin-query-utils.js'
import { mapAdminOrderListItem } from '../admin/admin-order-read-helpers.js'
import { mapAdminTaskListItem } from '../admin/admin-task-read-helpers.js'
import {
@@ -298,8 +301,27 @@ async function publishWorkOrderSnapshots(input: {
return
}
const pendingUnfreezeByOrderId = await sumPendingUnfreezeByOrderIds([workOrderId])
const shares = await listWorkOrderShares(workOrderId)
const material = safeParseJson(workOrder.material_json)
const consignmentMaterial = material.consignment
const consignmentInventoryId =
consignmentMaterial &&
typeof consignmentMaterial === 'object' &&
!Array.isArray(consignmentMaterial)
? Math.max(
0,
Math.floor(Number((consignmentMaterial as Record<string, unknown>).inventoryId) || 0),
)
: 0
const [pendingUnfreezeByOrderId, shares, consignmentInventories] = await Promise.all([
sumPendingUnfreezeByOrderIds([workOrderId]),
listWorkOrderShares(workOrderId),
consignmentInventoryId > 0
? listConsignmentInventoriesByIds([consignmentInventoryId])
: Promise.resolve([] as ConsignmentInventoryRow[]),
])
// 寄售工单的快照也要带库存实时供货人:前端会整行替换,
// 若只给 material 里的旧快照,改名后的列表会被实时事件刷回旧名。
const consignmentInventory = consignmentInventories[0] || null
// 事件驱动缓存失效:这里已经拿到的工单与拼单快照恰好给出完整的受影响打手集合
// (当前指派 + 历史保留 + 全部拼单人 + 调用方显式指定的)。
@@ -317,7 +339,7 @@ async function publishWorkOrderSnapshots(input: {
.reduce((sum, share) => sum + Number(share.quantity || 0), 0)
const pendingSubmissionCount = shares.filter((share) => share.status === 'joined').length
const adminSnapshot = {
...mapWorkOrderAdmin(workOrder, shares),
...mapWorkOrderAdmin(workOrder, shares, consignmentInventory),
pendingUnfreezeAmount: pendingUnfreezeByOrderId.get(workOrderId) || 0,
sharingProgress: {
joinedQuantity,
@@ -6,21 +6,25 @@ async function readJobSource() {
return readFile(new URL('./data-retention-job.ts', import.meta.url), 'utf8')
}
test('保留清理必须覆盖 work_order_events,否则这张只增不减的表会一直拖慢历史单检索', async () => {
test('数据维护不得覆盖原始报文或删除永久业务事实', async () => {
const src = await readJobSource()
assert.ok(
src.includes("deleteOldRowsInBatches('work_order_events'"),
'work_order_events 未纳入保留清理(生产上 71 万行 / 269MB 且只增不减)',
)
assert.ok(
src.includes("'task_events' | 'webhook_events' | 'admin_audit_logs' | 'work_order_events'"),
'删除助手需要显式允许 work_order_events(表名是白名单联合类型,避免拼接注入)',
)
assert.ok(src.includes('retention.workOrderEventDays'), '保留天数必须走配置项,便于按环境调整')
assert.ok(
src.includes('workOrderEventRows: number') && src.includes('workOrderEventRows,'),
'清理结果要带上工单事件的删除行数,方便定时任务面板核对',
)
assert.equal(src.includes('raw_payload_json ='), false)
assert.equal(src.includes('DELETE FROM'), false)
for (const table of [
'orders',
'kuaishou_industry_vouchers',
'task_events',
'webhook_events',
'admin_audit_logs',
'work_order_events',
]) {
assert.equal(
new RegExp(`(?:UPDATE|DELETE)\\s+${table}\\b`, 'i').test(src),
false,
`${table} 属于永久业务事实,不能在维护任务中清理或覆盖`,
)
}
assert.ok(src.includes('永久业务事实未清理'))
})
test('成本异常只归档已处理的行,open 队列不能被藏起来', async () => {
@@ -64,29 +68,28 @@ test('保留任务要回填漏写的付款派生列,并在有命中时打日
)
})
test('保留清理默认窗口:敏感报文/事件 180 天,审计 365 天,工单事件 180 天', async () => {
test('数据维护只保留可派生数据的成本异常归档窗口', async () => {
const defaults = await readFile(new URL('../../config/defaults.ts', import.meta.url), 'utf8')
assert.ok(defaults.includes('costExceptionDays: 60'))
for (const line of [
'rawPayloadDays: 180',
'taskEventDays: 180',
'webhookEventDays: 180',
'auditLogDays: 365',
'workOrderEventDays: 180',
'costExceptionDays: 60',
'rawPayloadDays:',
'taskEventDays:',
'webhookEventDays:',
'auditLogDays:',
'workOrderEventDays:',
]) {
assert.ok(defaults.includes(line), `默认保留策略缺少 ${line}`)
assert.equal(defaults.includes(line), false, `永久事实不能存在清理窗口 ${line}`)
}
const overrides = await readFile(
new URL('../../config/env-overrides.ts', import.meta.url),
'utf8',
)
assert.ok(
overrides.includes(
"integerEnv('WORK_ORDER_EVENT_RETENTION_DAYS', ['retention', 'workOrderEventDays'])",
),
'WORK_ORDER_EVENT_RETENTION_DAYS 需要环境变量出口,否则生产无法调整',
)
assert.equal(overrides.includes('RAW_PAYLOAD_RETENTION_DAYS'), false)
assert.equal(overrides.includes('TASK_EVENT_RETENTION_DAYS'), false)
assert.equal(overrides.includes('WEBHOOK_EVENT_RETENTION_DAYS'), false)
assert.equal(overrides.includes('AUDIT_LOG_RETENTION_DAYS'), false)
assert.equal(overrides.includes('WORK_ORDER_EVENT_RETENTION_DAYS'), false)
assert.ok(
overrides.includes(
"integerEnv('COST_EXCEPTION_RETENTION_DAYS', ['retention', 'costExceptionDays'])",
@@ -8,49 +8,29 @@ import { pruneWorkProductMatchLogs } from '../../repositories/worker-platform/pr
export type DataRetentionResult = {
status: 'ok'
message: string
rawPayloadRows: number
taskEventRows: number
webhookEventRows: number
auditLogRows: number
workOrderEventRows: number
matchLogRows: number
matchLogDeletedRows: number
sourceOrderAmountBackfillRows: number
costExceptionArchivedRows: number
}
/** 清理敏感原始报文并删除已过保留期的事件/审计记录。 */
/**
* 维护可重建的派生数据。
*
* 原始订单、平台报文、工单/任务事件、审计日志和资金记录都是永久事实,
* 不在这里按时间清理或覆盖。
*/
export async function runDataRetentionJob(): Promise<DataRetentionResult> {
const retention = runtimeConfig.retention || {}
const rawPayloadDays = positiveDays(retention.rawPayloadDays, 180)
const taskEventDays = positiveDays(retention.taskEventDays, 180)
const webhookEventDays = positiveDays(retention.webhookEventDays, 180)
const auditLogDays = positiveDays(retention.auditLogDays, 365)
// 工单事件表以前完全没纳入保留:717k 行 / 269MB 且只增不减,会一直拖慢历史单检索。
const workOrderEventDays = positiveDays(retention.workOrderEventDays, 180)
// 成本异常只归档「已处理」的行;open 行是待办队列,不能因为超期就藏起来。
const costExceptionDays = positiveDays(retention.costExceptionDays, 60)
// 1. 按配置清理超出上限或过期的打手商品匹配日志
// 商品匹配日志是诊断数据,不是订单或履约事实。
const matchConfig = getWorkerProductMatchConfig()
const matchLogPruneResult = await pruneWorkProductMatchLogs({
maxRows: matchConfig.maxMatchLogs,
retentionDays: matchConfig.matchLogRetentionDays,
})
// 2. 清理其他报文与日志
const rawPayloadRows =
(await scrubJsonColumnInBatches('orders', 'updated_at', rawPayloadDays)) +
(await scrubJsonColumnInBatches('kuaishou_industry_vouchers', 'updated_at', rawPayloadDays))
const matchLogRows = await scrubJsonColumnInBatches(
'work_product_match_logs',
'created_at',
rawPayloadDays,
)
const taskEventRows = await deleteOldRowsInBatches('task_events', taskEventDays)
const webhookEventRows = await deleteOldRowsInBatches('webhook_events', webhookEventDays)
const auditLogRows = await deleteOldRowsInBatches('admin_audit_logs', auditLogDays)
const workOrderEventRows = await deleteOldRowsInBatches('work_order_events', workOrderEventDays)
// 统计热路径已不再读 material_json。若将来有第三个写入点忘了维护派生列,
// 金额会静默回退 orders.total_amount。这里把漏写补上并打日志,让口径变化可见。
const costExceptionArchive = await archiveResolvedCostExceptions({
@@ -66,14 +46,8 @@ export async function runDataRetentionJob(): Promise<DataRetentionResult> {
return {
status: 'ok',
message: '数据保留清理完成',
rawPayloadRows,
matchLogRows,
message: '派生数据维护完成;永久业务事实未清理',
matchLogDeletedRows: matchLogPruneResult.totalDeleted,
taskEventRows,
webhookEventRows,
auditLogRows,
workOrderEventRows,
sourceOrderAmountBackfillRows,
costExceptionArchivedRows,
}
@@ -92,59 +66,6 @@ async function backfillMissingSourceOrderAmount() {
return result.rowCount || 0
}
async function scrubJsonColumnInBatches(
table: 'orders' | 'kuaishou_industry_vouchers' | 'work_product_match_logs',
dateColumn: 'created_at' | 'updated_at',
retentionDays: number,
) {
let total = 0
while (true) {
const result = await query(
`
WITH candidates AS (
SELECT ctid
FROM ${table}
WHERE ${dateColumn} < NOW() - make_interval(days => $1::int)
AND raw_payload_json <> '{}'::jsonb
LIMIT 1000
)
UPDATE ${table} AS target
SET raw_payload_json = '{}'::jsonb
FROM candidates
WHERE target.ctid = candidates.ctid
`,
[retentionDays],
)
total += result.rowCount || 0
if ((result.rowCount || 0) < 1000) return total
}
}
async function deleteOldRowsInBatches(
table: 'task_events' | 'webhook_events' | 'admin_audit_logs' | 'work_order_events',
retentionDays: number,
) {
let total = 0
while (true) {
const result = await query(
`
WITH candidates AS (
SELECT ctid
FROM ${table}
WHERE created_at < NOW() - make_interval(days => $1::int)
LIMIT 1000
)
DELETE FROM ${table} AS target
USING candidates
WHERE target.ctid = candidates.ctid
`,
[retentionDays],
)
total += result.rowCount || 0
if ((result.rowCount || 0) < 1000) return total
}
}
function positiveDays(value: unknown, fallback: number) {
const days = Number(value)
return Number.isInteger(days) && days > 0 ? days : fallback
@@ -169,12 +169,9 @@ async function runJob(job: JsonObject, { manual = false }: { manual?: boolean }
status,
...(job.type === 'data_retention'
? {
rawPayloadRows: Number(summary.rawPayloadRows || 0),
matchLogRows: Number(summary.matchLogRows || 0),
taskEventRows: Number(summary.taskEventRows || 0),
webhookEventRows: Number(summary.webhookEventRows || 0),
auditLogRows: Number(summary.auditLogRows || 0),
matchLogDeletedRows: Number(summary.matchLogDeletedRows || 0),
sourceOrderAmountBackfillRows: Number(summary.sourceOrderAmountBackfillRows || 0),
costExceptionArchivedRows: Number(summary.costExceptionArchivedRows || 0),
}
: {}),
})
@@ -59,6 +59,7 @@ function buildWorkOrderStatisticsCacheKey(filters: JsonObject): string {
filters.keyword || '',
filters.shopNames || [],
filters.workerSharingId || 0,
filters.consignmentSupplierWorkerId || 0,
filters.workerId || 0,
filters.workerLevelIds || [],
filters.categoryId || 0,
@@ -149,6 +150,9 @@ export function resolveAdminWorkOrderListFilters(query: JsonObject = {}): ListIn
keyword,
shopNames,
workerSharingId: workerId,
// 「接单人员」筛选同时覆盖寄售供货人:内部寄售直接履约不写 assigned_worker_id,
// 供货关系只存在 consignment_inventories.worker_id,必须补进可见集合。
...(workerId > 0 ? { consignmentSupplierWorkerId: workerId } : {}),
workerLevelIds,
vipEvidencePending,
...(acceptanceMode ? { acceptanceMode } : {}),
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { normalizeInventoryDate } from './consignment-admin-service.js'
import { mapConsignmentInventory, normalizeInventoryDate } from './consignment-admin-service.js'
test('寄售仓库入库日期筛选只接受合法的 YYYY-MM-DD', () => {
assert.equal(normalizeInventoryDate('2026-11-05'), '2026-11-05')
@@ -10,3 +10,61 @@ test('寄售仓库入库日期筛选只接受合法的 YYYY-MM-DD', () => {
assert.equal(normalizeInventoryDate('2026/11/05'), undefined)
assert.equal(normalizeInventoryDate(''), undefined)
})
test('寄售库存改名后优先显示实时打手昵称,打手被删才回退快照', () => {
const base = {
id: 1,
inventory_no: 'INV1',
game_name: '和平精英',
category_id: null,
item_name: '测试物品',
system: '',
server_zone: '',
series: '',
diamond_level: 1,
brilliance_count: 0,
cost_price_fen: 0,
buyer_name: '',
inbound_batch: '',
seller_contact: '',
seller_game_id: '',
seller_payment_info: '',
seller_phone: '',
credentials_json: {},
screenshots_json: [],
remark: '',
allow_matching: true,
status: 'in_stock',
allocation_status: '待安排',
bound_order_id: null,
bound_work_order_id: null,
bound_platform_order_id: '',
shop_name: '',
source_type: 'manual',
source_submission_id: null,
inbound_at: '2026-11-01T00:00:00.000Z',
consumed_at: null,
created_at: '2026-11-01T00:00:00.000Z',
updated_at: '2026-11-01T00:00:00.000Z',
worker_id: 7,
worker_name: '旧昵称',
worker_phone: '111',
}
const renamed = mapConsignmentInventory({
...base,
live_worker_name: '新昵称',
live_worker_phone: '222',
} as never)
assert.equal(renamed.workerName, '新昵称')
assert.equal(renamed.workerPhone, '222')
// 打手被删除时 join 不到实时行,仍要能显示登记时快照
const deleted = mapConsignmentInventory({
...base,
live_worker_name: null,
live_worker_phone: null,
} as never)
assert.equal(deleted.workerName, '旧昵称')
assert.equal(deleted.workerPhone, '111')
})
@@ -309,8 +309,9 @@ export function mapConsignmentInventory(row: ConsignmentInventoryRow) {
})
: null,
workerId: row.worker_id ? Number(row.worker_id) : null,
workerName: row.worker_name || '',
workerPhone: row.worker_phone || '',
// 改名后以当前昵称为准;快照只用于打手被删除等无法实时关联的兜底。
workerName: row.live_worker_name || row.worker_name || '',
workerPhone: row.live_worker_phone || row.worker_phone || '',
assignedWorkerId: row.assigned_worker_id ? Number(row.assigned_worker_id) : null,
depositAmountFen: Number(row.deposit_amount_fen || 0),
contactSupporterId: row.contact_supporter_id ? Number(row.contact_supporter_id) : null,
@@ -371,8 +372,9 @@ export function mapConsignmentSubmission(row: ConsignmentSubmissionRow) {
),
askingPriceFen: Number(row.asking_price_fen || 0),
workerId: row.worker_id ? Number(row.worker_id) : null,
workerName: row.worker_name || '',
workerPhone: row.worker_phone || '',
// 改名后以当前昵称为准;快照只用于打手被删除等无法实时关联的兜底。
workerName: row.live_worker_name || row.worker_name || '',
workerPhone: row.live_worker_phone || row.worker_phone || '',
depositAmountFen: Number(row.deposit_amount_fen || 0),
contactSupporterId: row.contact_supporter_id ? Number(row.contact_supporter_id) : null,
contactSupporterName: row.contact_supporter_name || '',
@@ -41,6 +41,7 @@ test('后台:显式单号检索时忽略状态等排他筛选,只按单号
assert.equal(filters.vipEvidencePending, false)
// 接单人员:目标单可能根本不在这名打手的可见集合里
assert.equal(filters.workerSharingId, 0)
assert.equal(filters.consignmentSupplierWorkerId, undefined)
// 分类 / 等级 / 验收模式 / 赠送阶段 / 创建时间 / 定位工单
assert.equal(filters.categoryId, 0)
assert.deepEqual(filters.workerLevelIds, [])
@@ -77,6 +78,7 @@ test('后台:普通关键词(商品名/昵称)仍按原样下推全部筛
assert.deepEqual(filters.statuses, ['open', 'in_progress'])
assert.equal(filters.vipEvidencePending, true)
assert.equal(filters.workerSharingId, 7)
assert.equal(filters.consignmentSupplierWorkerId, 7)
assert.equal(filters.categoryId, 12)
assert.deepEqual(filters.workerLevelIds, [1, 2])
assert.equal(filters.acceptanceMode, 'friend_gift')
@@ -4,6 +4,7 @@ import test from 'node:test'
import type { WorkOrderShareRow } from '../../repositories/worker-platform/types.js'
import {
isWorkerOrderVipEvidencePending,
mapWorkOrderMaterialWithConsignmentWorker,
pickWorkerRepresentativeShare,
resolveWorkerOrderStatusTabs,
stripWorkerPersonalShareFields,
@@ -264,3 +265,36 @@ test('大厅广播快照必须剔除打手私有份额字段', () => {
assert.deepEqual(snapshot.sharingProgress, { joinedQuantity: 2, pendingSubmissionCount: 1 })
assert.equal(snapshot.workOrderId, 1)
})
test('寄售供货人改名后,工单 material 展示使用库存实时昵称', () => {
const material = {
consignment: {
inventoryId: 9,
workerName: '旧昵称',
workerPhone: '111',
itemName: '测试物品',
},
}
const renamed = mapWorkOrderMaterialWithConsignmentWorker(material, {
live_worker_name: '新昵称',
live_worker_phone: '222',
} as never) as { consignment: { workerName: string; workerPhone: string; itemName: string } }
assert.equal(renamed.consignment.workerName, '新昵称')
assert.equal(renamed.consignment.workerPhone, '222')
// 其它寄售字段不能被覆盖
assert.equal(renamed.consignment.itemName, '测试物品')
const fallback = mapWorkOrderMaterialWithConsignmentWorker(material, null) as {
consignment: { workerName: string }
}
assert.equal(fallback.consignment.workerName, '旧昵称')
// 历史上没有登记供货人的内部现货单,不能因为库存关联到打手就改标成「供货打手」
const noSnapshot = mapWorkOrderMaterialWithConsignmentWorker(
{ consignment: { inventoryId: 9, workerName: '' } },
{ live_worker_name: '新昵称', live_worker_phone: '222' } as never,
) as { consignment: { workerName: string } }
assert.equal(noSnapshot.consignment.workerName, '')
})
@@ -68,6 +68,40 @@ export function mapWorkOrderMaterial(value: unknown) {
}
}
/**
* 工单 material.consignment 里保存的是入库/履约时的打手昵称快照。
* 打手改名后,后台列表仍要显示当前昵称;有实时库存行时用它覆盖展示字段,
* 历史 JSON 本身不改写(保留登记时痕迹)。
*/
export function mapWorkOrderMaterialWithConsignmentWorker(
value: unknown,
consignmentInventory?: ConsignmentInventoryRow | null,
) {
const material = mapWorkOrderMaterial(value) as JsonObject
const liveWorkerName = String(
consignmentInventory?.live_worker_name || consignmentInventory?.worker_name || '',
).trim()
if (!liveWorkerName) return material
const consignment = material.consignment
if (!consignment || typeof consignment !== 'object' || Array.isArray(consignment)) {
return material
}
const consignmentRecord = consignment as JsonObject
// 只刷新「原本就登记过供货人」的单据,避免把历史上没有供货人快照的
// 内部现货单改标成「供货打手」(那会改变历史单的展示语义)。
if (!String(consignmentRecord.workerName || '').trim()) return material
return {
...material,
consignment: {
...consignmentRecord,
workerName: liveWorkerName,
workerPhone: String(
consignmentInventory?.live_worker_phone || consignmentInventory?.worker_phone || '',
).trim(),
},
}
}
/** 读取好友赠送模式的打手本人资料;主页截图访问链接需要读取时重签。 */
export function mapWorkOrderBoosterMaterial(value: unknown) {
const material = safeParseJson(value)
@@ -283,6 +317,12 @@ export function mapWorkOrderAdmin(
const giftPhase = String(workOrder.gift_phase || '').trim()
const resolvedGiftPhase = resolveWorkOrderGiftPhase(workOrder)
const internalConsignment = hasInternalConsignmentFulfillment(workOrder)
const consignmentMaterial = safeParseJson(
safeParseJson(workOrder.material_json).consignment,
) as JsonObject
const consignmentWorkerIdRaw =
consignmentInventory?.worker_id || consignmentMaterial.workerId || 0
const consignmentWorkerId = Math.max(0, Math.floor(Number(consignmentWorkerIdRaw) || 0))
const hallAvailable =
workOrder.status === WORK_ORDER_STATUS.OPEN &&
(!workOrder.sharing_enabled || activeShareQuantity < sharingTotalQuantity)
@@ -437,7 +477,10 @@ export function mapWorkOrderAdmin(
depositThresholdAmount: Number(
workOrder.deposit_threshold_amount || DEFAULT_DEPOSIT_THRESHOLD_AMOUNT,
),
material: mapWorkOrderMaterial(workOrder.material_json),
material: mapWorkOrderMaterialWithConsignmentWorker(
workOrder.material_json,
consignmentInventory,
),
requirement: {
fields: resolveRequirementFields(workOrder),
},
@@ -462,6 +505,8 @@ export function mapWorkOrderAdmin(
displayName: workOrder.worker_display_name || '',
}
: null,
/** 寄售供货打手:后台「接单人员」筛选也认这个 id(内部寄售直接履约没有 assigned_worker_id)。 */
consignmentWorkerId: consignmentWorkerId > 0 ? consignmentWorkerId : null,
...(shares ? { sharingWorkers: mapWorkOrderSharingWorkers(shares) } : {}),
publishedAt: workOrder.published_at,
pinnedAt: workOrder.pinned_at,