补全工单流转记录中文事件映射
- 补齐内部寄售、电子凭证核销、自动发布等 30+ 事件文案 - 新增扫描测试:后端写入的 eventType 必须在前端有中文映射 - 覆盖生产库历史事件类型,避免工单详情露英文 key
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import test from 'node:test'
|
||||
|
||||
import ts from 'typescript'
|
||||
|
||||
/** 这些调用最终都会写入 work_order_events,事件类型必须在前端有中文映射。 */
|
||||
const WORK_ORDER_EVENT_CALLS = new Set([
|
||||
'createWorkOrderEvent',
|
||||
'createWorkOrderEventWithClient',
|
||||
'parkWorkOrderWaitingFriendAddWithClient',
|
||||
'startWorkOrderGiftCountdownWithClient',
|
||||
])
|
||||
|
||||
/**
|
||||
* 生产库仍存在、但当前代码已经不再写入的历史事件类型。
|
||||
* 前端必须继续映射,否则历史工单详情会露英文 key。
|
||||
*/
|
||||
const PRODUCTION_ONLY_EVENT_TYPES = [
|
||||
'auto_publish_succeeded',
|
||||
'status_repaired',
|
||||
'problem_cancel_restored',
|
||||
'consignment_internal_matching_entered',
|
||||
'consignment_internal_matching_reentered',
|
||||
'consignment_internal_matching_expired',
|
||||
'consignment_internal_matching_migrated',
|
||||
]
|
||||
|
||||
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)) {
|
||||
// 只取分支结果,不把条件里的 'queued' / 'failed' 当成事件类型。
|
||||
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
|
||||
}
|
||||
if (ts.isTemplateExpression(expr) && expr.templateSpans.length === 0) {
|
||||
result.add(expr.head.text)
|
||||
}
|
||||
}
|
||||
|
||||
function collectEventTypesFromFile(filePath: string, result: Set<string>) {
|
||||
const source = readFileSync(filePath, 'utf8')
|
||||
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true)
|
||||
|
||||
function visit(node: ts.Node) {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = node.expression.getText(sourceFile).split('.').pop() || ''
|
||||
if (WORK_ORDER_EVENT_CALLS.has(callee)) {
|
||||
const objectArg = node.arguments.find((arg): arg is ts.ObjectLiteralExpression =>
|
||||
ts.isObjectLiteralExpression(arg),
|
||||
)
|
||||
if (objectArg) {
|
||||
for (const property of objectArg.properties) {
|
||||
if (
|
||||
ts.isPropertyAssignment(property) &&
|
||||
property.name.getText(sourceFile) === 'eventType'
|
||||
) {
|
||||
collectStringLiterals(property.initializer, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
|
||||
visit(sourceFile)
|
||||
}
|
||||
|
||||
function walkBackendSources(dir: string, result: Set<string>) {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const filePath = join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
walkBackendSources(filePath, result)
|
||||
continue
|
||||
}
|
||||
if (!entry.isFile() || !filePath.endsWith('.ts') || filePath.endsWith('.test.ts')) continue
|
||||
collectEventTypesFromFile(filePath, result)
|
||||
}
|
||||
}
|
||||
|
||||
test('工单事件时间线:后端写入的每个 eventType 都必须有中文映射', () => {
|
||||
const backendRoot = fileURLToPath(new URL('../../backend/src', import.meta.url))
|
||||
const timelinePath = fileURLToPath(
|
||||
new URL('../src/components/WorkOrderEventTimeline.tsx', import.meta.url),
|
||||
)
|
||||
|
||||
const eventTypes = new Set<string>()
|
||||
for (const type of PRODUCTION_ONLY_EVENT_TYPES) eventTypes.add(type)
|
||||
walkBackendSources(backendRoot, eventTypes)
|
||||
|
||||
const timelineSource = readFileSync(timelinePath, 'utf8')
|
||||
const mappedTypes = new Set(
|
||||
[...timelineSource.matchAll(/case '([a-z_]+)'/g)].map((match) => match[1]!),
|
||||
)
|
||||
|
||||
const missing = [...eventTypes].filter((type) => !mappedTypes.has(type)).sort()
|
||||
assert.deepEqual(
|
||||
missing,
|
||||
[],
|
||||
`以下工单事件缺少中文映射,会在流转记录里直接显示英文 key:${missing.join(', ')}`,
|
||||
)
|
||||
})
|
||||
|
||||
test('工单事件时间线:截图中的内部寄售与凭证事件必须命中中文映射', () => {
|
||||
const timelineSource = readFileSync(
|
||||
fileURLToPath(new URL('../src/components/WorkOrderEventTimeline.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
)
|
||||
for (const eventType of [
|
||||
'consignment_gift_countdown_started',
|
||||
'publish_voucher_consume_schedule_skipped',
|
||||
'consignment_manual_matched',
|
||||
]) {
|
||||
assert.ok(timelineSource.includes(`case '${eventType}'`), `${eventType} 必须有中文映射`)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user