拆分打手平台映射器
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,311 @@
|
|||||||
|
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||||
|
import {
|
||||||
|
type WorkCategoryRow,
|
||||||
|
type WorkOrderCapabilities,
|
||||||
|
type WorkOrderRow,
|
||||||
|
type WorkOrderShareRow,
|
||||||
|
} from '../../repositories/worker-platform/index.js'
|
||||||
|
import type { JsonObject } from '../../types/json.js'
|
||||||
|
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||||
|
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||||
|
import { DEFAULT_DEPOSIT_THRESHOLD_AMOUNT } from './worker-finance-mappers.js'
|
||||||
|
import { normalizeUploadedFiles } from './worker-input-normalizers.js'
|
||||||
|
import { resolveFreezeDepositAmount } from './worker-permission-utils.js'
|
||||||
|
import {
|
||||||
|
mapAcceptanceForResponse,
|
||||||
|
resolveCollectSubmittedFieldValues,
|
||||||
|
} from './work-order-material-mappers.js'
|
||||||
|
import { resolveRequirementFields } from './work-order-requirement-fields.js'
|
||||||
|
|
||||||
|
export function mapWorkCategory(category: WorkCategoryRow | null | undefined) {
|
||||||
|
if (!category) return null
|
||||||
|
return {
|
||||||
|
categoryId: Number(category.id),
|
||||||
|
categoryKey: category.category_key,
|
||||||
|
name: category.name,
|
||||||
|
sortOrder: Number(category.sort_order || 100),
|
||||||
|
status: category.status,
|
||||||
|
createdAt: category.created_at,
|
||||||
|
updatedAt: category.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取工单资料:刷新买家主页截图等文件的访问链接(签名 15 分钟过期,读取时必须重签) */
|
||||||
|
export function mapWorkOrderMaterial(value: unknown) {
|
||||||
|
const material = safeParseJson(value)
|
||||||
|
const collect =
|
||||||
|
material.collect && typeof material.collect === 'object' && !Array.isArray(material.collect)
|
||||||
|
? (material.collect as JsonObject)
|
||||||
|
: null
|
||||||
|
if (!collect) {
|
||||||
|
return material
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...material,
|
||||||
|
collect: {
|
||||||
|
...collect,
|
||||||
|
screenshots: normalizeUploadedFiles(collect.screenshots).map(refreshUploadedFileUrls),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统一计算后台使用的工单业务状态,避免拼单订单继续直接暴露母工单 open。 */
|
||||||
|
export function resolveWorkOrderOperationalStatus(workOrder: WorkOrderRow): string {
|
||||||
|
const activeShareQuantity = Math.max(0, Number(workOrder.active_share_quantity || 0))
|
||||||
|
const pendingShareCount = Math.max(0, Number(workOrder.pending_share_count || 0))
|
||||||
|
const sharingTotalQuantity = Math.max(1, Number(workOrder.sharing_total_quantity || 1))
|
||||||
|
const hasActiveShares = activeShareQuantity > 0
|
||||||
|
const sharingFilled =
|
||||||
|
workOrder.sharing_enabled === true && activeShareQuantity >= sharingTotalQuantity
|
||||||
|
|
||||||
|
if (workOrder.status === WORK_ORDER_STATUS.OPEN && workOrder.sharing_enabled && hasActiveShares) {
|
||||||
|
return sharingFilled && pendingShareCount === 0
|
||||||
|
? WORK_ORDER_STATUS.PENDING_ACCEPTANCE
|
||||||
|
: WORK_ORDER_STATUS.IN_PROGRESS
|
||||||
|
}
|
||||||
|
return workOrder.status
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 汇总拼单参与者,已撤销份额不应继续展示为当前打手。 */
|
||||||
|
export function mapWorkOrderSharingWorkers(shares: WorkOrderShareRow[]) {
|
||||||
|
const workers = new Map<
|
||||||
|
number,
|
||||||
|
{ workerId: number; username: string; displayName: string; quantity: number }
|
||||||
|
>()
|
||||||
|
for (const share of shares) {
|
||||||
|
if (share.status === 'cancelled') continue
|
||||||
|
const workerId = Number(share.worker_id || 0)
|
||||||
|
if (workerId <= 0) continue
|
||||||
|
const existing = workers.get(workerId)
|
||||||
|
if (existing) {
|
||||||
|
existing.quantity += Number(share.quantity || 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const username = String(share.worker_username || '').trim()
|
||||||
|
workers.set(workerId, {
|
||||||
|
workerId,
|
||||||
|
username,
|
||||||
|
displayName: String(share.worker_display_name || username || `打手#${workerId}`).trim(),
|
||||||
|
quantity: Number(share.quantity || 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return [...workers.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderShareRow[]) {
|
||||||
|
const activeShareQuantity = Math.max(0, Number(workOrder.active_share_quantity || 0))
|
||||||
|
const pendingShareCount = Math.max(0, Number(workOrder.pending_share_count || 0))
|
||||||
|
const sharingTotalQuantity = Math.max(1, Number(workOrder.sharing_total_quantity || 1))
|
||||||
|
const hasActiveShares = activeShareQuantity > 0
|
||||||
|
const sharingFilled =
|
||||||
|
workOrder.sharing_enabled === true && activeShareQuantity >= sharingTotalQuantity
|
||||||
|
const status = resolveWorkOrderOperationalStatus(workOrder)
|
||||||
|
const hallAvailable =
|
||||||
|
workOrder.status === WORK_ORDER_STATUS.OPEN &&
|
||||||
|
(!workOrder.sharing_enabled || activeShareQuantity < sharingTotalQuantity)
|
||||||
|
const editableBeforeClaim =
|
||||||
|
!workOrder.assigned_worker_id &&
|
||||||
|
(workOrder.status === WORK_ORDER_STATUS.PENDING_MATERIAL ||
|
||||||
|
workOrder.status === WORK_ORDER_STATUS.UNASSIGNED ||
|
||||||
|
(workOrder.status === WORK_ORDER_STATUS.OPEN && !hasActiveShares))
|
||||||
|
const capabilities: WorkOrderCapabilities = {
|
||||||
|
canEdit: editableBeforeClaim,
|
||||||
|
canConfigureSharing: editableBeforeClaim,
|
||||||
|
canAssign:
|
||||||
|
!workOrder.assigned_worker_id &&
|
||||||
|
(workOrder.status === WORK_ORDER_STATUS.UNASSIGNED ||
|
||||||
|
(workOrder.status === WORK_ORDER_STATUS.OPEN && !hasActiveShares)),
|
||||||
|
canPin: hallAvailable || Boolean(workOrder.pinned_at),
|
||||||
|
canUnpublish:
|
||||||
|
workOrder.status === WORK_ORDER_STATUS.OPEN &&
|
||||||
|
!hasActiveShares &&
|
||||||
|
!workOrder.assigned_worker_id,
|
||||||
|
canUpdateMaterial:
|
||||||
|
workOrder.status === WORK_ORDER_STATUS.PENDING_MATERIAL ||
|
||||||
|
workOrder.status === WORK_ORDER_STATUS.UNASSIGNED ||
|
||||||
|
workOrder.status === WORK_ORDER_STATUS.OPEN ||
|
||||||
|
workOrder.status === WORK_ORDER_STATUS.IN_PROGRESS,
|
||||||
|
canAccept: status === WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||||
|
canMarkProblem:
|
||||||
|
status === WORK_ORDER_STATUS.IN_PROGRESS || status === WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||||
|
canDelete:
|
||||||
|
!workOrder.assigned_worker_id &&
|
||||||
|
!hasActiveShares &&
|
||||||
|
[
|
||||||
|
WORK_ORDER_STATUS.PENDING_MATERIAL,
|
||||||
|
WORK_ORDER_STATUS.UNASSIGNED,
|
||||||
|
WORK_ORDER_STATUS.CANCELLED,
|
||||||
|
].includes(workOrder.status as never),
|
||||||
|
canReopen: workOrder.status === WORK_ORDER_STATUS.CANCELLED && !hasActiveShares,
|
||||||
|
canPublish:
|
||||||
|
workOrder.status === WORK_ORDER_STATUS.UNASSIGNED && Number(workOrder.reward_amount || 0) > 0,
|
||||||
|
canCancel: status === WORK_ORDER_STATUS.IN_PROGRESS && Boolean(workOrder.assigned_worker_id),
|
||||||
|
canResolveProblem: workOrder.status === WORK_ORDER_STATUS.PROBLEM,
|
||||||
|
canDeduct: workOrder.status === WORK_ORDER_STATUS.ACCEPTED,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
orderId: workOrder.order_id ? Number(workOrder.order_id) : null,
|
||||||
|
orderItemId: workOrder.order_item_id ? Number(workOrder.order_item_id) : null,
|
||||||
|
workOrderNo: workOrder.work_order_no,
|
||||||
|
platformOrderId: workOrder.platform_order_id,
|
||||||
|
productName: workOrder.product_name,
|
||||||
|
categoryId: workOrder.category_id ? Number(workOrder.category_id) : null,
|
||||||
|
categoryName: workOrder.category_name || '',
|
||||||
|
status,
|
||||||
|
rawStatus: workOrder.status,
|
||||||
|
hallAvailable,
|
||||||
|
sharingProgress: {
|
||||||
|
joinedQuantity: activeShareQuantity,
|
||||||
|
pendingSubmissionCount: pendingShareCount,
|
||||||
|
remainingQuantity: Math.max(0, sharingTotalQuantity - activeShareQuantity),
|
||||||
|
filled: sharingFilled,
|
||||||
|
},
|
||||||
|
capabilities,
|
||||||
|
rewardAmount: Number(workOrder.reward_amount || 0),
|
||||||
|
requiredDepositAmount: Number(workOrder.required_deposit_amount || 0),
|
||||||
|
sharing: {
|
||||||
|
enabled: workOrder.sharing_enabled === true,
|
||||||
|
totalQuantity: Number(workOrder.sharing_total_quantity || 1),
|
||||||
|
unitReward: Number(workOrder.sharing_unit_reward || 0),
|
||||||
|
},
|
||||||
|
timeoutMinutes: Number(workOrder.timeout_minutes || 0),
|
||||||
|
timeoutPolicy: String(workOrder.timeout_policy || 'reopen').trim(),
|
||||||
|
deadlineAt: workOrder.deadline_at,
|
||||||
|
depositThresholdAmount: Number(
|
||||||
|
workOrder.deposit_threshold_amount || DEFAULT_DEPOSIT_THRESHOLD_AMOUNT,
|
||||||
|
),
|
||||||
|
material: mapWorkOrderMaterial(workOrder.material_json),
|
||||||
|
requirement: {
|
||||||
|
fields: resolveRequirementFields(workOrder),
|
||||||
|
},
|
||||||
|
acceptance: mapAcceptanceForResponse(workOrder.acceptance_json),
|
||||||
|
draftAcceptance: mapAcceptanceForResponse(workOrder.draft_acceptance_json),
|
||||||
|
problemNote: workOrder.problem_note || '',
|
||||||
|
worker: workOrder.assigned_worker_id
|
||||||
|
? {
|
||||||
|
workerId: Number(workOrder.assigned_worker_id),
|
||||||
|
username: workOrder.worker_username || '',
|
||||||
|
displayName: workOrder.worker_display_name || '',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
...(shares ? { sharingWorkers: mapWorkOrderSharingWorkers(shares) } : {}),
|
||||||
|
publishedAt: workOrder.published_at,
|
||||||
|
pinnedAt: workOrder.pinned_at,
|
||||||
|
assignedAt: workOrder.assigned_at,
|
||||||
|
submittedAt: workOrder.submitted_at,
|
||||||
|
acceptedAt: workOrder.accepted_at,
|
||||||
|
createdAt: workOrder.created_at,
|
||||||
|
updatedAt: workOrder.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWorkOrderForWorker(
|
||||||
|
workOrder: WorkOrderRow,
|
||||||
|
permissions: { depositFreeAmount: number },
|
||||||
|
myShare?: WorkOrderShareRow | null,
|
||||||
|
workerNote = '',
|
||||||
|
viewedAt?: string | null,
|
||||||
|
) {
|
||||||
|
const mapped = mapWorkOrderAdmin(workOrder)
|
||||||
|
const {
|
||||||
|
requiredDepositAmount: _requiredDepositAmount,
|
||||||
|
depositThresholdAmount: _depositThresholdAmount,
|
||||||
|
worker: _worker,
|
||||||
|
orderId: _orderId,
|
||||||
|
orderItemId: _orderItemId,
|
||||||
|
rawStatus: _rawStatus,
|
||||||
|
hallAvailable: _hallAvailable,
|
||||||
|
capabilities: _capabilities,
|
||||||
|
...visibleOrder
|
||||||
|
} = mapped
|
||||||
|
const personalStatus = resolveWorkerShareStatus(workOrder.status, myShare?.status)
|
||||||
|
return {
|
||||||
|
...visibleOrder,
|
||||||
|
productName: myShare
|
||||||
|
? resolveWorkerShareProductName(workOrder.product_name, Number(myShare.quantity || 0))
|
||||||
|
: visibleOrder.productName,
|
||||||
|
status: personalStatus,
|
||||||
|
rewardAmount: myShare ? Number(myShare.share_reward || 0) : visibleOrder.rewardAmount,
|
||||||
|
freezeDepositAmount: myShare
|
||||||
|
? Number(myShare.share_deposit || 0)
|
||||||
|
: resolveFreezeDepositAmount(workOrder, permissions),
|
||||||
|
assignedAt: myShare?.created_at || visibleOrder.assignedAt,
|
||||||
|
submittedAt: myShare?.submitted_at || visibleOrder.submittedAt,
|
||||||
|
acceptedAt: myShare?.accepted_at || visibleOrder.acceptedAt,
|
||||||
|
workerNote,
|
||||||
|
myShare: myShare ? mapWorkOrderShare(myShare) : null,
|
||||||
|
...(viewedAt === undefined ? {} : { isUnread: !viewedAt }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拼单参与者看到个人份额规格,而不是母工单的原始总规格。 */
|
||||||
|
export function resolveWorkerShareProductName(productName: unknown, quantity: number): string {
|
||||||
|
const text = String(productName || '').trim()
|
||||||
|
const normalizedQuantity = Math.max(1, Number(quantity) || 1)
|
||||||
|
return text.replace(
|
||||||
|
/(\d+|[零一二三四五六七八九十百千两]+)(\s*)(个|份|张|枚)/,
|
||||||
|
(_matched, _oldQuantity: string, spacing: string, unit: string) =>
|
||||||
|
`${normalizedQuantity}${spacing}${unit}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把份额状态映射成打手端订单状态,避免母工单 open 被误显示为已超时。 */
|
||||||
|
export function resolveWorkerShareStatus(workOrderStatus: string, shareStatus: unknown): string {
|
||||||
|
if (!shareStatus) return workOrderStatus
|
||||||
|
if (['accepted', 'cancelled', 'problem'].includes(workOrderStatus)) return workOrderStatus
|
||||||
|
if (shareStatus === 'joined') return WORK_ORDER_STATUS.IN_PROGRESS
|
||||||
|
if (shareStatus === 'submitted') return WORK_ORDER_STATUS.PENDING_ACCEPTANCE
|
||||||
|
if (shareStatus === 'accepted') return WORK_ORDER_STATUS.ACCEPTED
|
||||||
|
if (shareStatus === 'cancelled') return WORK_ORDER_STATUS.CANCELLED
|
||||||
|
return workOrderStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWorkOrderShare(share: WorkOrderShareRow) {
|
||||||
|
return {
|
||||||
|
shareId: Number(share.id),
|
||||||
|
workOrderId: Number(share.work_order_id),
|
||||||
|
workerId: Number(share.worker_id),
|
||||||
|
quantity: Number(share.quantity || 0),
|
||||||
|
unitReward: Number(share.unit_reward || 0),
|
||||||
|
shareReward: Number(share.share_reward || 0),
|
||||||
|
shareDeposit: Number(share.share_deposit || 0),
|
||||||
|
status: share.status,
|
||||||
|
acceptance: mapAcceptanceForResponse(share.acceptance_json),
|
||||||
|
draftAcceptance: mapAcceptanceForResponse(share.draft_acceptance_json),
|
||||||
|
submittedAt: share.submitted_at,
|
||||||
|
acceptedAt: share.accepted_at,
|
||||||
|
createdAt: share.created_at,
|
||||||
|
updatedAt: share.updated_at,
|
||||||
|
worker: share.worker_id
|
||||||
|
? {
|
||||||
|
workerId: Number(share.worker_id),
|
||||||
|
username: share.worker_username || '',
|
||||||
|
displayName: share.worker_display_name || share.worker_username || '',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWorkOrderPublic(workOrder: WorkOrderRow) {
|
||||||
|
return {
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workOrderNo: workOrder.work_order_no,
|
||||||
|
platformOrderId: workOrder.platform_order_id,
|
||||||
|
productName: workOrder.product_name,
|
||||||
|
status: workOrder.status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapCollectLookupOrder(workOrder: WorkOrderRow) {
|
||||||
|
const material = mapWorkOrderMaterial(workOrder.material_json)
|
||||||
|
return {
|
||||||
|
...mapWorkOrderPublic(workOrder),
|
||||||
|
fields: resolveRequirementFields(workOrder),
|
||||||
|
values: resolveCollectSubmittedFieldValues(workOrder),
|
||||||
|
screenshots:
|
||||||
|
material.collect && Array.isArray(material.collect.screenshots)
|
||||||
|
? material.collect.screenshots
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
import {
|
||||||
|
type WorkProductRuleMappingRow,
|
||||||
|
type WorkProductRuleRow,
|
||||||
|
} from '../../repositories/worker-platform/index.js'
|
||||||
|
import type { JsonObject } from '../../types/json.js'
|
||||||
|
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||||
|
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||||
|
import { normalizeRequirementFields } from './work-order-requirement-fields.js'
|
||||||
|
|
||||||
|
export function mapWorkProductRule(rule: WorkProductRuleRow | null | undefined) {
|
||||||
|
if (!rule) return null
|
||||||
|
const requirement = safeParseJson(rule.requirement_json)
|
||||||
|
const match = safeParseJson(rule.match_json)
|
||||||
|
return {
|
||||||
|
ruleId: Number(rule.id),
|
||||||
|
ruleKey: rule.rule_key,
|
||||||
|
provider: rule.provider || '',
|
||||||
|
platform: rule.platform || '',
|
||||||
|
shopId: rule.shop_id || '',
|
||||||
|
skuCode: rule.sku_code || '',
|
||||||
|
productName: rule.product_name || '',
|
||||||
|
matchType: rule.match_type || 'contains',
|
||||||
|
match: {
|
||||||
|
sellerId: String(match.sellerId || '').trim(),
|
||||||
|
itemId: String(match.itemId || '').trim(),
|
||||||
|
relItemId: String(match.relItemId || '').trim(),
|
||||||
|
skuId: String(match.skuId || '').trim(),
|
||||||
|
relSkuId: String(match.relSkuId || '').trim(),
|
||||||
|
itemTitle: String(match.itemTitle || '').trim(),
|
||||||
|
skuNick: String(match.skuNick || '').trim(),
|
||||||
|
itemTitleMatchType: match.itemTitleMatchType === 'exact' ? 'exact' : 'contains',
|
||||||
|
skuNickMatchType: match.skuNickMatchType === 'exact' ? 'exact' : 'contains',
|
||||||
|
},
|
||||||
|
categoryId: rule.category_id ? Number(rule.category_id) : null,
|
||||||
|
categoryName: rule.category_name || '',
|
||||||
|
enabled: Boolean(rule.enabled),
|
||||||
|
autoCreate: Boolean(rule.auto_create),
|
||||||
|
rewardAmount: Number(rule.reward_amount || 0),
|
||||||
|
unitPriceFen: Number(rule.unit_price_fen || 0),
|
||||||
|
sharing: {
|
||||||
|
enabled: rule.sharing_enabled === true,
|
||||||
|
totalQuantity: Number(rule.sharing_total_quantity || 1),
|
||||||
|
unitReward: Number(rule.sharing_unit_reward || 0),
|
||||||
|
autoFromOrder: rule.sharing_auto_from_order === true,
|
||||||
|
},
|
||||||
|
timeoutMinutes: Number(rule.timeout_minutes || 0),
|
||||||
|
timeoutPolicy: String(rule.timeout_policy || 'reopen').trim(),
|
||||||
|
requirement: {
|
||||||
|
fields: normalizeRequirementFields(requirement.fields),
|
||||||
|
},
|
||||||
|
sortOrder: Number(rule.sort_order || 100),
|
||||||
|
createdAt: rule.created_at,
|
||||||
|
updatedAt: rule.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMatchingProductRule(
|
||||||
|
order: OrderRow,
|
||||||
|
item: OrderItemRow,
|
||||||
|
rules: WorkProductRuleRow[],
|
||||||
|
mappings: WorkProductRuleMappingRow[] = [],
|
||||||
|
options: { legacyFallbackEnabled?: boolean } = {},
|
||||||
|
) {
|
||||||
|
return resolveMatchingProductRuleDecision(order, item, rules, mappings, options).rule
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkProductRuleMatchDecision = {
|
||||||
|
rule: WorkProductRuleRow | null
|
||||||
|
mappingId: number | null
|
||||||
|
reason: 'matched' | 'unmatched' | 'ambiguous'
|
||||||
|
score: number
|
||||||
|
candidates: Array<{ ruleKey: string; score: number; mappingId: number | null }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KuaishouWorkProductMatchContext = {
|
||||||
|
sellerId: string
|
||||||
|
itemId: string
|
||||||
|
relItemId: string
|
||||||
|
skuId: string
|
||||||
|
relSkuId: string
|
||||||
|
itemTitle: string
|
||||||
|
skuNick: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMatchingProductRuleDecision(
|
||||||
|
order: OrderRow,
|
||||||
|
item: OrderItemRow,
|
||||||
|
rules: WorkProductRuleRow[],
|
||||||
|
mappings: WorkProductRuleMappingRow[] = [],
|
||||||
|
options: { legacyFallbackEnabled?: boolean } = {},
|
||||||
|
): WorkProductRuleMatchDecision {
|
||||||
|
const context = resolveKuaishouWorkProductMatchContext(item)
|
||||||
|
const rulesById = new Map(rules.map((rule) => [Number(rule.id), rule]))
|
||||||
|
// 快手发码订单默认只允许确定性商品映射;非快手订单保留原有规则,避免影响其他来源。
|
||||||
|
const useLegacyFallback = !context.sellerId || options.legacyFallbackEnabled === true
|
||||||
|
const candidates = [
|
||||||
|
...mappings.map((mapping) => scoreWorkProductRuleMapping(context, mapping, rulesById)),
|
||||||
|
...(useLegacyFallback ? rules.map((rule) => scoreWorkProductRule(order, item, rule)) : []),
|
||||||
|
]
|
||||||
|
.filter(
|
||||||
|
(candidate): candidate is { rule: WorkProductRuleRow; score: number; mappingId?: number } =>
|
||||||
|
Boolean(candidate),
|
||||||
|
)
|
||||||
|
.sort((left, right) => right.score - left.score || Number(right.rule.id) - Number(left.rule.id))
|
||||||
|
|
||||||
|
const top = candidates[0]
|
||||||
|
if (!top) {
|
||||||
|
return { rule: null, mappingId: null, reason: 'unmatched', score: 0, candidates: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tied = candidates.filter((candidate) => candidate.score === top.score)
|
||||||
|
if (tied.length > 1) {
|
||||||
|
return {
|
||||||
|
rule: null,
|
||||||
|
mappingId: null,
|
||||||
|
reason: 'ambiguous',
|
||||||
|
score: top.score,
|
||||||
|
candidates: tied.map((candidate) => ({
|
||||||
|
ruleKey: candidate.rule.rule_key,
|
||||||
|
score: candidate.score,
|
||||||
|
mappingId: candidate.mappingId || null,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rule: top.rule,
|
||||||
|
mappingId: top.mappingId || null,
|
||||||
|
reason: 'matched',
|
||||||
|
score: top.score,
|
||||||
|
candidates: candidates.slice(0, 5).map((candidate) => ({
|
||||||
|
ruleKey: candidate.rule.rule_key,
|
||||||
|
score: candidate.score,
|
||||||
|
mappingId: candidate.mappingId || null,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveKuaishouWorkProductMatchContext(
|
||||||
|
item: Pick<OrderItemRow, 'item_snapshot_json'>,
|
||||||
|
): KuaishouWorkProductMatchContext {
|
||||||
|
const snapshot = safeParseJson(item.item_snapshot_json)
|
||||||
|
const source = safeParseJson(snapshot.kuaishouSendCode)
|
||||||
|
return {
|
||||||
|
sellerId: String(source.sellerId || '').trim(),
|
||||||
|
itemId: String(source.itemId || '').trim(),
|
||||||
|
relItemId: normalizeExternalMatchId(source.relItemId),
|
||||||
|
skuId: String(source.skuId || '').trim(),
|
||||||
|
relSkuId: normalizeExternalMatchId(source.relSkuId),
|
||||||
|
itemTitle: String(source.itemTitle || '').trim(),
|
||||||
|
skuNick: String(source.skuNick || source.skuName || snapshot.externalSkuName || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreWorkProductRuleMapping(
|
||||||
|
context: KuaishouWorkProductMatchContext,
|
||||||
|
mapping: WorkProductRuleMappingRow,
|
||||||
|
rulesById: Map<number, WorkProductRuleRow>,
|
||||||
|
) {
|
||||||
|
if (!mapping.enabled) return null
|
||||||
|
const rule = rulesById.get(Number(mapping.rule_id))
|
||||||
|
if (!rule || !rule.enabled) return null
|
||||||
|
if (
|
||||||
|
!mappingSellerIds(mapping.seller_ids_json).some((sellerId) =>
|
||||||
|
sameMatchId(context.sellerId, sellerId),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappingType =
|
||||||
|
mapping.mapping_type === 'sku_series'
|
||||||
|
? 'sku_series'
|
||||||
|
: mapping.mapping_type === 'sku_exact' || mapping.mapping_type === 'sku_override'
|
||||||
|
? 'sku_exact'
|
||||||
|
: 'product_default'
|
||||||
|
const mappedItemIds = normalizeMatchIdList(mapping.rel_item_id)
|
||||||
|
const mappedTitle = normalizeMatchText(mapping.item_title)
|
||||||
|
const hasContextItemId = Boolean(normalizeMatchId(context.relItemId))
|
||||||
|
const itemIdMatched = mappedItemIds.some((mappedItemId) =>
|
||||||
|
sameMatchId(context.relItemId, mappedItemId),
|
||||||
|
)
|
||||||
|
const titleMatched = Boolean(
|
||||||
|
mappedTitle && matchNameCondition(normalizeMatchText(context.itemTitle), mappedTitle, true),
|
||||||
|
)
|
||||||
|
const hasProductScope = Boolean(mappedItemIds.length > 0 || mappedTitle)
|
||||||
|
if (mappingType === 'product_default' || hasProductScope) {
|
||||||
|
// 配置了商品 ID 时必须要求订单也提供商品 ID,避免缺少 ID 时误落入同店其他商品。
|
||||||
|
if (
|
||||||
|
(mappedItemIds.length > 0 && (!hasContextItemId || !itemIdMatched)) ||
|
||||||
|
(mappedItemIds.length === 0 && !titleMatched)
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let score = mappingType === 'sku_exact' ? 10_000 : mappingType === 'sku_series' ? 8_000 : 5_000
|
||||||
|
if (itemIdMatched) score += 400
|
||||||
|
if (titleMatched) score += 200
|
||||||
|
if (mappingType === 'sku_exact') {
|
||||||
|
const mappedSkuId = normalizeMatchId(mapping.rel_sku_id)
|
||||||
|
const mappedSkuNick = normalizeMatchText(mapping.sku_nick)
|
||||||
|
if (!mappedSkuId && !mappedSkuNick) return null
|
||||||
|
const skuIdMatched = Boolean(mappedSkuId && sameMatchId(context.relSkuId, mappedSkuId))
|
||||||
|
const skuNickMatched = Boolean(
|
||||||
|
mappedSkuNick && matchNameCondition(normalizeMatchText(context.skuNick), mappedSkuNick, true),
|
||||||
|
)
|
||||||
|
if (!skuIdMatched && !skuNickMatched) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (skuIdMatched) score += 400
|
||||||
|
if (skuNickMatched) score += 200
|
||||||
|
}
|
||||||
|
if (mappingType === 'sku_series') {
|
||||||
|
const skuMatchMode = mapping.sku_match_mode === 'exact' ? 'exact' : 'fuzzy'
|
||||||
|
if (!matchSkuSeriesName(context.skuNick, mapping.sku_nick, skuMatchMode)) return null
|
||||||
|
score += 200
|
||||||
|
}
|
||||||
|
return { rule, score, mappingId: Number(mapping.id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreWorkProductRule(order: OrderRow, item: OrderItemRow, rule: WorkProductRuleRow) {
|
||||||
|
if (!matchesOptionalText(rule.provider, order.provider)) return null
|
||||||
|
if (!matchesOptionalText(rule.platform, order.platform)) return null
|
||||||
|
|
||||||
|
const snapshot = safeParseJson(item.item_snapshot_json)
|
||||||
|
const source = safeParseJson(snapshot.kuaishouSendCode)
|
||||||
|
const context = {
|
||||||
|
sellerId: normalizeMatchId(source.sellerId),
|
||||||
|
itemId: normalizeMatchId(source.itemId),
|
||||||
|
relItemId: normalizeMatchId(source.relItemId),
|
||||||
|
skuId: normalizeMatchId(source.skuId),
|
||||||
|
relSkuId: normalizeMatchId(source.relSkuId),
|
||||||
|
itemTitle: normalizeMatchText(source.itemTitle),
|
||||||
|
skuNick: normalizeMatchText(source.skuNick || source.skuName || snapshot.externalSkuName),
|
||||||
|
}
|
||||||
|
const ruleShopId = normalizeMatchId(rule.shop_id)
|
||||||
|
if (
|
||||||
|
ruleShopId &&
|
||||||
|
ruleShopId !== normalizeMatchId(order.shop_id) &&
|
||||||
|
ruleShopId !== context.sellerId
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const match = safeParseJson(rule.match_json)
|
||||||
|
const hasStructuredMatch = Object.entries(match).some(
|
||||||
|
([key, value]) =>
|
||||||
|
!['itemTitleMatchType', 'skuNickMatchType'].includes(key) && String(value || '').trim(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasStructuredMatch) {
|
||||||
|
let score = 100
|
||||||
|
if (!matchCondition(context.sellerId, match.sellerId)) return null
|
||||||
|
if (match.sellerId) score += 1000
|
||||||
|
if (!matchCondition(context.itemId, match.itemId)) return null
|
||||||
|
if (match.itemId) score += 800
|
||||||
|
if (!matchCondition(context.relItemId, match.relItemId)) return null
|
||||||
|
if (match.relItemId) score += 700
|
||||||
|
if (!matchCondition(context.skuId, match.skuId)) return null
|
||||||
|
if (match.skuId) score += 1000
|
||||||
|
if (!matchCondition(context.relSkuId, match.relSkuId)) return null
|
||||||
|
if (match.relSkuId) score += 900
|
||||||
|
|
||||||
|
const itemTitle = normalizeMatchText(match.itemTitle)
|
||||||
|
if (itemTitle) {
|
||||||
|
const exact = match.itemTitleMatchType === 'exact'
|
||||||
|
if (!matchNameCondition(context.itemTitle, itemTitle, exact)) return null
|
||||||
|
score += exact ? 600 : 250
|
||||||
|
}
|
||||||
|
const skuNick = normalizeMatchText(match.skuNick)
|
||||||
|
if (skuNick) {
|
||||||
|
const exact = match.skuNickMatchType === 'exact'
|
||||||
|
if (!matchNameCondition(context.skuNick, skuNick, exact)) return null
|
||||||
|
score += exact ? 800 : 350
|
||||||
|
}
|
||||||
|
return { rule, score }
|
||||||
|
}
|
||||||
|
|
||||||
|
const skuCodes = [item.sku_code, snapshot.externalSkuCode, snapshot.externalItemId]
|
||||||
|
.map(normalizeMatchId)
|
||||||
|
.filter(Boolean)
|
||||||
|
const skuNames = [
|
||||||
|
item.sku_name,
|
||||||
|
snapshot.externalSkuName,
|
||||||
|
snapshot.externalSkuNameNormalized,
|
||||||
|
source.skuNick,
|
||||||
|
source.itemTitle,
|
||||||
|
]
|
||||||
|
.map(normalizeMatchText)
|
||||||
|
.filter(Boolean)
|
||||||
|
const ruleSkuCode = normalizeMatchId(rule.sku_code)
|
||||||
|
const ruleProductName = normalizeMatchText(rule.product_name)
|
||||||
|
|
||||||
|
if (ruleSkuCode && !skuCodes.includes(ruleSkuCode)) return null
|
||||||
|
if (!ruleProductName) {
|
||||||
|
return ruleSkuCode ? { rule, score: 120 } : null
|
||||||
|
}
|
||||||
|
if (rule.match_type === 'exact') {
|
||||||
|
return skuNames.includes(ruleProductName) || skuCodes.includes(ruleProductName)
|
||||||
|
? { rule, score: ruleSkuCode ? 300 : 150 }
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
return skuNames.some((value) => value.includes(ruleProductName)) ||
|
||||||
|
skuCodes.some((value) => value.includes(ruleProductName))
|
||||||
|
? { rule, score: ruleSkuCode ? 220 : 100 }
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchCondition(actual: string, expected: unknown) {
|
||||||
|
const value = normalizeMatchId(expected)
|
||||||
|
return !value || actual === value
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchNameCondition(actual: string, expected: string, exact: boolean) {
|
||||||
|
return Boolean(actual) && (exact ? actual === expected : actual.includes(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMatchId(value: unknown) {
|
||||||
|
const normalized = String(value || '').trim()
|
||||||
|
return normalized === '0' ? '' : normalized.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMatchIdList(value: unknown) {
|
||||||
|
const rawValues = Array.isArray(value) ? value : [value]
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
rawValues
|
||||||
|
.flatMap((entry) => String(entry || '').split(/[,,;;\s\n\r]+/))
|
||||||
|
.map(normalizeMatchId)
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExternalMatchId(value: unknown) {
|
||||||
|
const normalized = String(value || '').trim()
|
||||||
|
return normalized === '0' ? '' : normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameMatchId(left: unknown, right: unknown) {
|
||||||
|
const normalizedLeft = normalizeMatchId(left)
|
||||||
|
const normalizedRight = normalizeMatchId(right)
|
||||||
|
return Boolean(normalizedLeft) && normalizedLeft === normalizedRight
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMatchText(value: unknown) {
|
||||||
|
return String(value || '')
|
||||||
|
.normalize('NFKC')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[\s\u3000]+/g, '')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function mappingSellerIds(value: unknown) {
|
||||||
|
const parsed = Array.isArray(value) ? value : safeParseJson(value)
|
||||||
|
const values = Array.isArray(parsed) ? parsed : []
|
||||||
|
return Array.from(new Set(values.map(normalizeMatchId).filter(Boolean)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 去除 SKU 中的数量规格后得到系列名。
|
||||||
|
* 例如“指挥官密钥10个”和“1个盛夏礼卡”分别归一为“指挥官密钥”“盛夏礼卡”。
|
||||||
|
*/
|
||||||
|
function normalizeSkuSeriesName(value: unknown) {
|
||||||
|
return normalizeMatchText(value).replace(
|
||||||
|
/(?:\d+|[零一二三四五六七八九十百千两]+)(?:个|份|张|枚)/g,
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchSkuSeriesName(actual: unknown, expected: unknown, matchMode: 'fuzzy' | 'exact') {
|
||||||
|
const normalizedActual = normalizeSkuSeriesName(actual)
|
||||||
|
const normalizedExpected = normalizeSkuSeriesName(expected)
|
||||||
|
if (!normalizedActual || !normalizedExpected) return false
|
||||||
|
if (matchMode === 'exact') return normalizedActual === normalizedExpected
|
||||||
|
|
||||||
|
const fuzzyActual = normalizeFuzzySkuSeriesName(normalizedActual)
|
||||||
|
const fuzzyExpected = normalizeFuzzySkuSeriesName(normalizedExpected)
|
||||||
|
return fuzzyActual.includes(fuzzyExpected) || fuzzyExpected.includes(fuzzyActual)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFuzzySkuSeriesName(value: string) {
|
||||||
|
return value.replace(/秘钥|密匙|秘匙/g, '密钥')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeWorkProductRuleMatch(payload: JsonObject): JsonObject {
|
||||||
|
const match =
|
||||||
|
payload.match && typeof payload.match === 'object' && !Array.isArray(payload.match)
|
||||||
|
? (payload.match as JsonObject)
|
||||||
|
: {}
|
||||||
|
const source = {
|
||||||
|
sellerId: payload.sellerId ?? payload.seller_id ?? match.sellerId,
|
||||||
|
itemId: payload.itemId ?? payload.item_id ?? match.itemId,
|
||||||
|
relItemId: payload.relItemId ?? payload.rel_item_id ?? match.relItemId,
|
||||||
|
skuId: payload.skuId ?? payload.sku_id ?? match.skuId,
|
||||||
|
relSkuId: payload.relSkuId ?? payload.rel_sku_id ?? match.relSkuId,
|
||||||
|
itemTitle: payload.itemTitle ?? payload.item_title ?? match.itemTitle,
|
||||||
|
skuNick: payload.skuNick ?? payload.sku_nick ?? match.skuNick,
|
||||||
|
}
|
||||||
|
const result: JsonObject = {}
|
||||||
|
for (const [key, value] of Object.entries(source)) {
|
||||||
|
const normalized = key.endsWith('Id') ? normalizeMatchId(value) : normalizeMatchText(value)
|
||||||
|
if (normalized) result[key] = normalized
|
||||||
|
}
|
||||||
|
result.itemTitleMatchType =
|
||||||
|
(payload.itemTitleMatchType ?? payload.item_title_match_type ?? match.itemTitleMatchType) ===
|
||||||
|
'exact'
|
||||||
|
? 'exact'
|
||||||
|
: 'contains'
|
||||||
|
result.skuNickMatchType =
|
||||||
|
(payload.skuNickMatchType ?? payload.sku_nick_match_type ?? match.skuNickMatchType) === 'exact'
|
||||||
|
? 'exact'
|
||||||
|
: 'contains'
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesOptionalText(ruleValue: unknown, sourceValue: unknown) {
|
||||||
|
const ruleText = String(ruleValue || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
if (!ruleText) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
ruleText ===
|
||||||
|
String(sourceValue || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
import {
|
||||||
|
addWorkerWalletCredit,
|
||||||
|
type WorkerFinanceRequestRow,
|
||||||
|
type WorkerLevelRow,
|
||||||
|
type WorkerUserRow,
|
||||||
|
type WorkerWalletLedgerRow,
|
||||||
|
} from '../../repositories/worker-platform/index.js'
|
||||||
|
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||||
|
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||||
|
import { createWorkerSessionToken } from './worker-auth-utils.js'
|
||||||
|
import {
|
||||||
|
normalizeBoolean,
|
||||||
|
normalizeInteger,
|
||||||
|
normalizePositiveInteger,
|
||||||
|
} from './worker-normalizers.js'
|
||||||
|
import {
|
||||||
|
resolveVisibleDelaySeconds,
|
||||||
|
resolveWorkerPermissions,
|
||||||
|
} from './worker-permission-utils.js'
|
||||||
|
import { normalizeUploadedFiles } from './worker-input-normalizers.js'
|
||||||
|
import { mapAcceptanceForResponse } from './work-order-material-mappers.js'
|
||||||
|
|
||||||
|
export const DEFAULT_LEVEL_KEY = 'vip1'
|
||||||
|
export const DEFAULT_LEVEL_NAME = 'VIP1'
|
||||||
|
export const DEFAULT_CATEGORY_KEY = 'default'
|
||||||
|
export const DEFAULT_DEPOSIT_THRESHOLD_AMOUNT = 20_000
|
||||||
|
export const INVITE_CODE_LENGTH = 8
|
||||||
|
export const INVITE_CODE_RETRY_LIMIT = 5
|
||||||
|
export const WORKER_CANCEL_LIMIT_PER_WINDOW = 5
|
||||||
|
export const WORKER_CANCEL_LIMIT_WINDOW_MS = 30 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
export function createWorkerSession(worker: WorkerUserRow) {
|
||||||
|
const session = createWorkerSessionToken(worker)
|
||||||
|
return {
|
||||||
|
...session,
|
||||||
|
worker: mapWorkerUser(worker),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWorkerLevel(level: WorkerLevelRow | null | undefined) {
|
||||||
|
if (!level) return null
|
||||||
|
const permission = safeParseJson(level.permission_json)
|
||||||
|
return {
|
||||||
|
levelId: Number(level.id),
|
||||||
|
levelKey: level.level_key,
|
||||||
|
name: level.name,
|
||||||
|
sortOrder: Number(level.sort_order || 100),
|
||||||
|
status: level.status,
|
||||||
|
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
|
||||||
|
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
|
||||||
|
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
|
||||||
|
autoAcceptWithoutEvidence: normalizeBoolean(permission.autoAcceptWithoutEvidence, false),
|
||||||
|
visibleDelaySeconds: resolveVisibleDelaySeconds(
|
||||||
|
level.level_key,
|
||||||
|
permission.visibleDelaySeconds,
|
||||||
|
),
|
||||||
|
createdAt: level.created_at,
|
||||||
|
updatedAt: level.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeWorkerType(value: unknown): 'internal' | 'external' {
|
||||||
|
return String(value || '').trim() === 'internal' ? 'internal' : 'external'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWorkerUser(worker: WorkerUserRow) {
|
||||||
|
return {
|
||||||
|
workerId: Number(worker.id),
|
||||||
|
username: worker.username,
|
||||||
|
displayName: worker.display_name || worker.username,
|
||||||
|
phone: worker.phone,
|
||||||
|
status: worker.status,
|
||||||
|
workerType: normalizeWorkerType(worker.worker_type),
|
||||||
|
reviewNote: worker.review_note || '',
|
||||||
|
freezeReason: worker.frozen_reason || '',
|
||||||
|
frozenAt: worker.frozen_at,
|
||||||
|
inviteCode: worker.invite_code || '',
|
||||||
|
inviter: worker.inviter_id
|
||||||
|
? {
|
||||||
|
workerId: Number(worker.inviter_id),
|
||||||
|
username: worker.inviter_username || '',
|
||||||
|
displayName: worker.inviter_display_name || worker.inviter_username || '',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
level: worker.level_id
|
||||||
|
? {
|
||||||
|
levelId: Number(worker.level_id),
|
||||||
|
levelKey: worker.level_key || '',
|
||||||
|
name: worker.level_name || '',
|
||||||
|
permissions: resolveWorkerPermissions(worker),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
wallet: {
|
||||||
|
availableAmount: Number(worker.available_amount || 0),
|
||||||
|
frozenDepositAmount: Number(worker.frozen_deposit_amount || 0),
|
||||||
|
pendingUnfreezeAmount: Number(worker.pending_unfreeze_amount || 0),
|
||||||
|
totalCreditedAmount: Number(worker.total_credited_amount || 0),
|
||||||
|
totalSettledAmount: Number(worker.total_settled_amount || 0),
|
||||||
|
},
|
||||||
|
createdAt: worker.created_at,
|
||||||
|
updatedAt: worker.updated_at,
|
||||||
|
reviewedAt: worker.reviewed_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWorkerCancelRequest(request: {
|
||||||
|
id: number
|
||||||
|
work_order_id: number
|
||||||
|
work_order_share_id: number | null
|
||||||
|
worker_id: number
|
||||||
|
reason: string
|
||||||
|
status: string
|
||||||
|
review_note: string
|
||||||
|
reviewed_by: string
|
||||||
|
created_at: string
|
||||||
|
reviewed_at: string | null
|
||||||
|
platform_order_id?: string
|
||||||
|
shop_name?: string
|
||||||
|
worker_username?: string
|
||||||
|
worker_display_name?: string
|
||||||
|
share_quantity?: number | null
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
requestId: Number(request.id),
|
||||||
|
workOrderId: Number(request.work_order_id),
|
||||||
|
shareId: request.work_order_share_id ? Number(request.work_order_share_id) : null,
|
||||||
|
workerId: Number(request.worker_id),
|
||||||
|
platformOrderId: String(request.platform_order_id || ''),
|
||||||
|
shopName: String(request.shop_name || ''),
|
||||||
|
workerName: String(request.worker_display_name || request.worker_username || ''),
|
||||||
|
shareQuantity: request.share_quantity ? Number(request.share_quantity) : null,
|
||||||
|
reason: String(request.reason || ''),
|
||||||
|
status: String(request.status || ''),
|
||||||
|
reviewNote: String(request.review_note || ''),
|
||||||
|
reviewedBy: String(request.reviewed_by || ''),
|
||||||
|
createdAt: request.created_at,
|
||||||
|
reviewedAt: request.reviewed_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWorkerOrderFeedback(feedback: {
|
||||||
|
id: number
|
||||||
|
work_order_id: number
|
||||||
|
work_order_share_id: number | null
|
||||||
|
worker_id: number
|
||||||
|
content: string
|
||||||
|
status: string
|
||||||
|
reply: string
|
||||||
|
handled_by: string
|
||||||
|
created_at: string
|
||||||
|
handled_at: string | null
|
||||||
|
work_order_no?: string
|
||||||
|
product_name?: string
|
||||||
|
shop_name?: string
|
||||||
|
worker_username?: string
|
||||||
|
worker_display_name?: string
|
||||||
|
share_quantity?: number | null
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
feedbackId: Number(feedback.id),
|
||||||
|
workOrderId: Number(feedback.work_order_id),
|
||||||
|
shareId: feedback.work_order_share_id ? Number(feedback.work_order_share_id) : null,
|
||||||
|
workerId: Number(feedback.worker_id),
|
||||||
|
workOrderNo: String(feedback.work_order_no || ''),
|
||||||
|
productName: String(feedback.product_name || ''),
|
||||||
|
shopName: String(feedback.shop_name || ''),
|
||||||
|
workerName: String(feedback.worker_display_name || feedback.worker_username || ''),
|
||||||
|
shareQuantity: feedback.share_quantity ? Number(feedback.share_quantity) : null,
|
||||||
|
content: String(feedback.content || ''),
|
||||||
|
status: String(feedback.status || ''),
|
||||||
|
reply: String(feedback.reply || ''),
|
||||||
|
handledBy: String(feedback.handled_by || ''),
|
||||||
|
createdAt: feedback.created_at,
|
||||||
|
handledAt: feedback.handled_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWorkerAfterSalesCase(afterSalesCase: {
|
||||||
|
id: number
|
||||||
|
case_no: string
|
||||||
|
work_order_id: number
|
||||||
|
work_order_share_id: number | null
|
||||||
|
worker_id: number
|
||||||
|
problem_type: string
|
||||||
|
complaint_note: string
|
||||||
|
complaint_files_json: unknown
|
||||||
|
acceptance_snapshot_json: unknown
|
||||||
|
status: string
|
||||||
|
worker_response: string
|
||||||
|
worker_response_files_json: unknown
|
||||||
|
responded_at: string | null
|
||||||
|
resolution_action: string
|
||||||
|
resolution_note: string
|
||||||
|
recovery_amount: number
|
||||||
|
pending_deposit_deduction_amount: number
|
||||||
|
available_deduction_amount: number
|
||||||
|
debt_amount: number
|
||||||
|
created_by: string
|
||||||
|
resolved_by: string
|
||||||
|
resolved_at: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
work_order_no?: string
|
||||||
|
platform_order_id?: string
|
||||||
|
product_name?: string
|
||||||
|
shop_name?: string
|
||||||
|
worker_username?: string
|
||||||
|
worker_display_name?: string
|
||||||
|
share_quantity?: number | null
|
||||||
|
}) {
|
||||||
|
const snapshot = safeParseJson(afterSalesCase.acceptance_snapshot_json)
|
||||||
|
return {
|
||||||
|
caseId: Number(afterSalesCase.id),
|
||||||
|
caseNo: String(afterSalesCase.case_no || ''),
|
||||||
|
workOrderId: Number(afterSalesCase.work_order_id),
|
||||||
|
shareId: afterSalesCase.work_order_share_id ? Number(afterSalesCase.work_order_share_id) : null,
|
||||||
|
workerId: Number(afterSalesCase.worker_id),
|
||||||
|
workOrderNo: String(afterSalesCase.work_order_no || ''),
|
||||||
|
platformOrderId: String(afterSalesCase.platform_order_id || ''),
|
||||||
|
productName: String(afterSalesCase.product_name || ''),
|
||||||
|
shopName: String(afterSalesCase.shop_name || ''),
|
||||||
|
workerName: String(afterSalesCase.worker_display_name || afterSalesCase.worker_username || ''),
|
||||||
|
shareQuantity: afterSalesCase.share_quantity ? Number(afterSalesCase.share_quantity) : null,
|
||||||
|
problemType: String(afterSalesCase.problem_type || ''),
|
||||||
|
complaintNote: String(afterSalesCase.complaint_note || ''),
|
||||||
|
complaintFiles: normalizeUploadedFiles(safeParseJson(afterSalesCase.complaint_files_json)).map(
|
||||||
|
refreshUploadedFileUrls,
|
||||||
|
),
|
||||||
|
acceptanceSnapshot: {
|
||||||
|
...snapshot,
|
||||||
|
acceptance: mapAcceptanceForResponse(snapshot.acceptance),
|
||||||
|
},
|
||||||
|
status: String(afterSalesCase.status || ''),
|
||||||
|
workerResponse: String(afterSalesCase.worker_response || ''),
|
||||||
|
workerResponseFiles: normalizeUploadedFiles(
|
||||||
|
safeParseJson(afterSalesCase.worker_response_files_json),
|
||||||
|
).map(refreshUploadedFileUrls),
|
||||||
|
respondedAt: afterSalesCase.responded_at,
|
||||||
|
resolutionAction: String(afterSalesCase.resolution_action || ''),
|
||||||
|
resolutionNote: String(afterSalesCase.resolution_note || ''),
|
||||||
|
recoveryAmount: Number(afterSalesCase.recovery_amount || 0),
|
||||||
|
pendingDepositDeductionAmount: Number(afterSalesCase.pending_deposit_deduction_amount || 0),
|
||||||
|
availableDeductionAmount: Number(afterSalesCase.available_deduction_amount || 0),
|
||||||
|
debtAmount: Number(afterSalesCase.debt_amount || 0),
|
||||||
|
createdBy: String(afterSalesCase.created_by || ''),
|
||||||
|
resolvedBy: String(afterSalesCase.resolved_by || ''),
|
||||||
|
resolvedAt: afterSalesCase.resolved_at,
|
||||||
|
createdAt: afterSalesCase.created_at,
|
||||||
|
updatedAt: afterSalesCase.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWalletLedger(ledger: WorkerWalletLedgerRow | null | undefined) {
|
||||||
|
if (!ledger) return null
|
||||||
|
return {
|
||||||
|
ledgerId: Number(ledger.id),
|
||||||
|
ledgerType: ledger.ledger_type,
|
||||||
|
amount: Number(ledger.amount || 0),
|
||||||
|
balanceAfter: Number(ledger.balance_after || 0),
|
||||||
|
frozenAfter: Number(ledger.frozen_after || 0),
|
||||||
|
relatedWorkOrderId: ledger.related_work_order_id ? Number(ledger.related_work_order_id) : null,
|
||||||
|
relatedPlatformOrderId: String(ledger.related_platform_order_id || '').trim() || null,
|
||||||
|
auditStatus: ledger.audit_status || '',
|
||||||
|
note: ledger.note || '',
|
||||||
|
payload: safeParseJson(ledger.payload_json),
|
||||||
|
createdAt: ledger.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapFinanceRequest(request: WorkerFinanceRequestRow | null | undefined) {
|
||||||
|
if (!request) return null
|
||||||
|
const payload = safeParseJson(request.payload_json)
|
||||||
|
const alipayQrCodeImage = refreshUploadedFileUrls(payload.alipayQrCodeImage)
|
||||||
|
if (String(alipayQrCodeImage.url || '').trim()) {
|
||||||
|
payload.alipayQrCodeImage = alipayQrCodeImage
|
||||||
|
}
|
||||||
|
const wechatQrCodeImage = refreshUploadedFileUrls(payload.wechatQrCodeImage)
|
||||||
|
if (String(wechatQrCodeImage.url || '').trim()) {
|
||||||
|
payload.wechatQrCodeImage = wechatQrCodeImage
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
requestId: Number(request.id),
|
||||||
|
requestType: request.request_type || '',
|
||||||
|
status: request.status || '',
|
||||||
|
amount: Number(request.amount || 0),
|
||||||
|
accountChannel: request.account_channel || '',
|
||||||
|
accountName: request.account_name || '',
|
||||||
|
accountNo: request.account_no || '',
|
||||||
|
note: request.note || '',
|
||||||
|
reviewedNote: request.reviewed_note || '',
|
||||||
|
payload,
|
||||||
|
createdAt: request.created_at,
|
||||||
|
updatedAt: request.updated_at,
|
||||||
|
reviewedAt: request.reviewed_at,
|
||||||
|
worker: request.worker_id
|
||||||
|
? {
|
||||||
|
workerId: Number(request.worker_id),
|
||||||
|
username: request.worker_username || '',
|
||||||
|
displayName: request.worker_display_name || request.worker_username || '',
|
||||||
|
phone: request.worker_phone || '',
|
||||||
|
workerType: normalizeWorkerType(request.worker_type),
|
||||||
|
levelKey: request.worker_level_key || '',
|
||||||
|
levelName: request.worker_level_name || '',
|
||||||
|
withdrawRequestCount: Number(request.worker_withdraw_request_count || 0),
|
||||||
|
totalWithdrawAmount: Number(request.worker_total_withdraw_amount || 0),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWallet(wallet: Awaited<ReturnType<typeof addWorkerWalletCredit>>) {
|
||||||
|
return {
|
||||||
|
workerId: Number(wallet?.worker_id || 0),
|
||||||
|
availableAmount: Number(wallet?.available_amount || 0),
|
||||||
|
frozenDepositAmount: Number(wallet?.frozen_deposit_amount || 0),
|
||||||
|
pendingUnfreezeAmount: Number(wallet?.pending_unfreeze_amount || 0),
|
||||||
|
totalCreditedAmount: Number(wallet?.total_credited_amount || 0),
|
||||||
|
totalSettledAmount: Number(wallet?.total_settled_amount || 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user