补全工单流转记录中文事件映射

- 补齐内部寄售、电子凭证核销、自动发布等 30+ 事件文案
- 新增扫描测试:后端写入的 eventType 必须在前端有中文映射
- 覆盖生产库历史事件类型,避免工单详情露英文 key
This commit is contained in:
2026-11-14 15:41:35 +08:00
parent 09bfb3ea14
commit e2863286c3
2 changed files with 311 additions and 0 deletions
@@ -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} 必须有中文映射`)
}
})