feat(consignment): 匹配日志新增寄卖单号列并中文化原因
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Input, Modal, Select, Space, Tag, Typography } from 'antd'
|
||||
import { Input, Modal, Select, Space, Tag, Tooltip, Typography } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
@@ -8,6 +8,8 @@ import { fetchAdminConsignmentMatchLogs } from '@/services/admin/consignment'
|
||||
import type { ConsignmentMatchLogItem } from '@/types/consignment-match-log'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
|
||||
import { resolveConsignmentMatchReasonLabel } from './consignment-match-log-labels'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const TRIGGER_LABELS: Record<string, string> = {
|
||||
@@ -68,6 +70,28 @@ export default function ConsignmentMatchLogsModal({
|
||||
render: (_: string, record: ConsignmentMatchLogItem) =>
|
||||
record.workOrderNo || (record.workOrderId ? `#${record.workOrderId}` : '-'),
|
||||
},
|
||||
{
|
||||
title: '寄卖单号',
|
||||
dataIndex: 'consignmentNos',
|
||||
width: 160,
|
||||
render: (_: string[], record: ConsignmentMatchLogItem) => {
|
||||
const consignmentNos = Array.isArray(record.consignmentNos)
|
||||
? record.consignmentNos.filter(Boolean)
|
||||
: []
|
||||
if (consignmentNos.length === 0) return '-'
|
||||
const text = consignmentNos.join('、')
|
||||
const inventoryNos = Array.isArray(record.inventoryNos)
|
||||
? record.inventoryNos.filter(Boolean)
|
||||
: []
|
||||
return inventoryNos.length > 0 ? (
|
||||
<Tooltip title={`库存号:${inventoryNos.join('、')}`}>
|
||||
<span>{text}</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
text
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '触发',
|
||||
dataIndex: 'triggerType',
|
||||
@@ -96,16 +120,17 @@ export default function ConsignmentMatchLogsModal({
|
||||
{
|
||||
title: '原因',
|
||||
dataIndex: 'reasonText',
|
||||
render: (value: string, record: ConsignmentMatchLogItem) => (
|
||||
<div style={{ display: 'grid', gap: 2 }}>
|
||||
<Text>{value || '-'}</Text>
|
||||
{record.reasonCode ? (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{record.reasonCode}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
render: (value: string, record: ConsignmentMatchLogItem) => {
|
||||
const label = resolveConsignmentMatchReasonLabel(record.reasonCode, value)
|
||||
// 英文 reasonCode 只做排查用途,收进悬浮提示,列表里始终显示中文。
|
||||
return record.reasonCode ? (
|
||||
<Tooltip title={record.reasonCode}>
|
||||
<Text style={{ cursor: 'help' }}>{label}</Text>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Text>{label}</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 寄卖匹配日志原因码 → 中文文案。
|
||||
*
|
||||
* 后端的 `reason_code` 有两种来源:匹配引擎固定写入的流程原因,以及
|
||||
* `extractConsignmentErrorCode` 透传的业务错误码。新增登记点时必须同步补齐,
|
||||
* `tests/consignment-match-log-reasons.test.ts` 会扫描后端源码校验覆盖率。
|
||||
*/
|
||||
export const CONSIGNMENT_MATCH_REASON_LABELS: Record<string, string> = {
|
||||
// 自动 / 重新匹配流程
|
||||
before_effective_from: '早于匹配生效时间',
|
||||
no_candidate: '无可用寄卖库存',
|
||||
quantity_insufficient: '库存数量不足',
|
||||
auto_matched_before_hall: '发布前已自动匹配',
|
||||
apply_not_applied: '匹配未生效(库存已变化)',
|
||||
exception: '匹配异常',
|
||||
rematch_started: '已重新发起匹配',
|
||||
// 拼单
|
||||
auto_sharing_matched_before_hall: '发布前已自动拼单匹配',
|
||||
sharing_quantity_insufficient: '拼单份数不足',
|
||||
sharing_not_applied: '拼单匹配未生效',
|
||||
// 手动匹配
|
||||
manual_matched: '手动匹配成功',
|
||||
manual_not_applied: '手动匹配未生效',
|
||||
// 业务校验错误码
|
||||
work_order_not_found: '工单不存在',
|
||||
work_order_not_matchable: '工单状态不允许匹配',
|
||||
consignment_inventory_required: '未选择可用寄卖库存',
|
||||
consignment_spec_mismatch: '所选库存与订单规格不一致',
|
||||
consignment_match_reason_required: '非精确匹配未填写原因',
|
||||
consignment_sharing_quantity_insufficient: '所选库存不够一次拼满',
|
||||
consignment_sharing_not_applied: '工单状态或库存已变化',
|
||||
consignment_multiple_inventory_not_allowed: '整单匹配只能选一条库存',
|
||||
consignment_inventory_quantity_insufficient: '库存剩余数量不足',
|
||||
consignment_inventory_not_found: '寄卖库存不存在',
|
||||
consignment_inventory_unavailable: '寄卖库存不可用',
|
||||
consignment_inventory_race_condition: '库存被并发占用',
|
||||
consignment_deposit_insufficient: '押金不足',
|
||||
work_order_conflict: '工单状态冲突',
|
||||
}
|
||||
|
||||
/**
|
||||
* 原因列展示文案:优先用后端带的详细说明,其次用中文映射,未知码兜底原文
|
||||
* (英文码只在前端未覆盖时出现,正常情况全部命中映射)。
|
||||
*/
|
||||
export function resolveConsignmentMatchReasonLabel(
|
||||
reasonCode: string,
|
||||
reasonText?: string,
|
||||
): string {
|
||||
const text = String(reasonText || '').trim()
|
||||
if (text) return text
|
||||
const code = String(reasonCode || '').trim()
|
||||
if (!code) return '-'
|
||||
return CONSIGNMENT_MATCH_REASON_LABELS[code] || code
|
||||
}
|
||||
@@ -21,6 +21,10 @@ export type ConsignmentMatchLogItem = {
|
||||
actorId: string
|
||||
durationMs: number
|
||||
payload: Record<string, unknown>
|
||||
/** 本次日志涉及的寄卖单号(提报单号优先,无则库存号),由后端按 payload 库存 ID 反查。 */
|
||||
consignmentNos: string[]
|
||||
/** 对应库存号,用于悬浮提示。 */
|
||||
inventoryNos: string[]
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import test from 'node:test'
|
||||
|
||||
import ts from 'typescript'
|
||||
|
||||
import {
|
||||
CONSIGNMENT_MATCH_REASON_LABELS,
|
||||
resolveConsignmentMatchReasonLabel,
|
||||
} from '../src/components/admin/consignment-match-log-labels.ts'
|
||||
|
||||
const ENGINE_PATH = fileURLToPath(
|
||||
new URL(
|
||||
'../../backend/src/services/worker-platform/consignment-match-engine-service.ts',
|
||||
import.meta.url,
|
||||
),
|
||||
)
|
||||
const MODAL_PATH = fileURLToPath(
|
||||
new URL('../src/components/admin/ConsignmentMatchLogsModal.tsx', import.meta.url),
|
||||
)
|
||||
|
||||
function collectStringLiterals(expr: ts.Expression, result: Set<string>) {
|
||||
if (ts.isStringLiteralLike(expr)) {
|
||||
result.add(expr.text)
|
||||
return
|
||||
}
|
||||
if (ts.isParenthesizedExpression(expr)) {
|
||||
collectStringLiterals(expr.expression, result)
|
||||
return
|
||||
}
|
||||
if (ts.isConditionalExpression(expr)) {
|
||||
// 只取分支结果,不把条件里的字符串当成原因码。
|
||||
collectStringLiterals(expr.whenTrue, result)
|
||||
collectStringLiterals(expr.whenFalse, result)
|
||||
return
|
||||
}
|
||||
if (
|
||||
ts.isBinaryExpression(expr) &&
|
||||
[
|
||||
ts.SyntaxKind.BarBarToken,
|
||||
ts.SyntaxKind.QuestionQuestionToken,
|
||||
ts.SyntaxKind.PlusToken,
|
||||
].includes(expr.operatorToken.kind)
|
||||
) {
|
||||
collectStringLiterals(expr.left, result)
|
||||
collectStringLiterals(expr.right, result)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/** 匹配日志的 reasonCode 只有两种来源:固定流程原因 + createHttpError 的 errorCode。 */
|
||||
function collectEngineReasonCodes() {
|
||||
const source = readFileSync(ENGINE_PATH, 'utf8')
|
||||
const sourceFile = ts.createSourceFile(ENGINE_PATH, source, ts.ScriptTarget.Latest, true)
|
||||
const result = new Set<string>()
|
||||
|
||||
function visit(node: ts.Node) {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = node.expression.getText(sourceFile).split('.').pop() || ''
|
||||
if (callee === 'recordConsignmentMatchLog') {
|
||||
const objectArg = node.arguments.find((arg): arg is ts.ObjectLiteralExpression =>
|
||||
ts.isObjectLiteralExpression(arg),
|
||||
)
|
||||
for (const property of objectArg?.properties || []) {
|
||||
if (
|
||||
ts.isPropertyAssignment(property) &&
|
||||
property.name.getText(sourceFile) === 'reasonCode'
|
||||
) {
|
||||
collectStringLiterals(property.initializer, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ts.isPropertyAssignment(node) && node.name.getText(sourceFile) === 'errorCode') {
|
||||
collectStringLiterals(node.initializer, result)
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
|
||||
visit(sourceFile)
|
||||
return result
|
||||
}
|
||||
|
||||
test('寄卖匹配日志:后端写入的每个原因码都必须有中文映射', () => {
|
||||
const reasonCodes = collectEngineReasonCodes()
|
||||
assert.ok(reasonCodes.size > 0, '没有扫描到任何原因码,测试逻辑可能失效')
|
||||
|
||||
const missing = [...reasonCodes].filter((code) => !CONSIGNMENT_MATCH_REASON_LABELS[code]).sort()
|
||||
|
||||
assert.deepEqual(
|
||||
missing,
|
||||
[],
|
||||
`以下原因码缺少中文映射,会在匹配日志里直接显示英文:${missing.join(', ')}`,
|
||||
)
|
||||
})
|
||||
|
||||
test('寄卖匹配日志:原因列优先显示中文,未知码兜底原文', () => {
|
||||
assert.equal(resolveConsignmentMatchReasonLabel('manual_matched', ''), '手动匹配成功')
|
||||
assert.equal(resolveConsignmentMatchReasonLabel('manual_matched', undefined), '手动匹配成功')
|
||||
assert.equal(
|
||||
resolveConsignmentMatchReasonLabel(
|
||||
'consignment_spec_mismatch',
|
||||
'所选库存与订单规格不一致:INV1',
|
||||
),
|
||||
'所选库存与订单规格不一致:INV1',
|
||||
)
|
||||
assert.equal(resolveConsignmentMatchReasonLabel('future_unknown_code', ''), 'future_unknown_code')
|
||||
assert.equal(resolveConsignmentMatchReasonLabel('', ''), '-')
|
||||
})
|
||||
|
||||
test('寄卖匹配日志弹窗:工单右侧新增寄卖单号列', () => {
|
||||
const modalSource = readFileSync(MODAL_PATH, 'utf8')
|
||||
|
||||
assert.ok(modalSource.includes("title: '寄卖单号'"), '必须新增「寄卖单号」列')
|
||||
assert.ok(
|
||||
modalSource.includes("dataIndex: 'consignmentNos'"),
|
||||
'「寄卖单号」列必须读取后端返回的 consignmentNos',
|
||||
)
|
||||
assert.ok(
|
||||
modalSource.indexOf("title: '寄卖单号'") > modalSource.indexOf("title: '工单'"),
|
||||
'「寄卖单号」列必须在「工单」列右侧',
|
||||
)
|
||||
assert.ok(modalSource.includes('resolveConsignmentMatchReasonLabel'), '原因列必须使用中文映射')
|
||||
})
|
||||
Reference in New Issue
Block a user