feat(audit): 提升可读性 —— 操作对象补全物料名、详情降噪、字段字典补齐
业务方反馈两点:操作对象太干瘪、详情快照噪音太多。
1. 操作对象补全为「SKU - 物料名称 (规格型号)」
库里 target_name 大多只存了 SKU('0000002270'),入库类甚至存的是内部
标识('stock_buy ID:1667'),业务人员完全看不懂。
补全走两条路径,**正确性优先**:
· 快照里的 base_id(CREATE/DELETE 的库存行)—— 权威,零歧义
· target_id 唯一命中一张股票表 —— 实测与上一条 1230/1230 完全一致
★ 撞号一律放弃:target_id 同时命中 2 张股票表时解出的 base_id
24/27 是错的、命中 3 张时 44/44 全错。补一个**错的**物料名比不补更糟 ——
那是"看起来完全可信的错误答案",业务方会照着它去找不相干的物料。
实测 300 条撞号行 0 条被补,300 条唯一命中行全部补上。
target_name 原值保留(target_keyword 搜索仍按它匹配),新增 target_display
供展示。前端去掉灰显的 #target_id —— 业务人员不需要看数据库主键。
2. 详情快照降噪
· 前端过滤空值:null / '' / [] / {} / '-'。判据**严格**:false 和 0
不算空(`is_returned: false`、`quantity: 0` 是明确的业务事实,
用真值判断会把它们一起吃掉,那是在篡改数据)。
· 过滤纯技术字段:id / created_at / updated_at / target_id / module_name
及 pgvector 的 `*embedding`(单条可达数 KB)。清单由后端下发
(labels.hiddenFields),前端不硬编 —— 它会随新表增长,再存一份必然漂移。
embedding 类按后缀拦截,新表加向量列不必改代码。
· 变更对比表过滤「等于没改」的行(null ↔ 空串)。
3. 字段字典补齐:168 项
实测快照里出现过但字典没有的字段全部补上(buyer_email / currency /
in_date / exchange_rate / dosage / loss_rate / child / parent /
production_* / return_* / *_threshold 等 60+ 个)。
现在快照字段缺中文名的数量为 **0** —— 详情页不会再裸露英文。
4. 摘要优化
· changes_of 丢掉无意义变更(null ↔ 空串)。库里 98 条记录带这种变更,
写进摘要就是「备注:空→空」,纯噪音还挤占截断长度。
· CREATE/DELETE 摘要改为核心属性**按槽位取值**:
新增(入库):入库数量 10 件、库位 ZZTEST
而不是「新增:SKU、base、状态… 等 29 个字段」。
· 槽位只有数量与位置两个,**不含物料** —— 物料由「操作对象」列承担,
同一屏里再来一遍是重复。数量字段也按槽位只取一个:
in_quantity / stock_quantity / available_quantity 值往往相同,
取三个会得到三个一样的数字。
· 数字去掉无意义的 .0(10.0 → 10);单位取自物料主数据,
纯数字的占位单位(库里有一批 unit='1')丢弃。
验证:33 + 16 项断言全过,含「补全的物料与快照 base_id 零冲突」「撞号行
一律不补」「快照字段缺中文名 0 个」「摘要无空→空」「导出口径与列表一致」;
vue-tsc --noEmit 与 vite build 均 exit=0。
日报回归:三天附件 490.5K / 193.4K / 10.3K,6 列结构未变。
This commit is contained in:
@ -134,11 +134,14 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 强化操作对象:名称 + 主键,便于按 ID 追单条记录 -->
|
||||
<el-table-column label="操作对象" min-width="180" show-overflow-tooltip>
|
||||
<!--
|
||||
★ 操作对象只显示名称,**不显示数据库主键** —— 业务人员不需要它。
|
||||
★ 优先用后端补全的 target_display(「SKU - 物料名称 (规格型号)」);
|
||||
后端解不出物料的记录回落原始 target_name。
|
||||
-->
|
||||
<el-table-column label="操作对象" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.target_name || '-' }}</span>
|
||||
<span v-if="row.target_id" class="muted">#{{ row.target_id }}</span>
|
||||
<span>{{ row.target_display || row.target_name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@ -225,8 +228,7 @@
|
||||
<el-tag :type="getActionType(currentDetail.action)">{{ actionLabel(currentDetail.action) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作对象">
|
||||
{{ currentDetail.target_name || '-' }}
|
||||
<span v-if="currentDetail.target_id" class="muted">#{{ currentDetail.target_id }}</span>
|
||||
{{ currentDetail.target_display || currentDetail.target_name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作时间" :span="2">{{ currentDetail.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="IP地址">{{ currentDetail.ip_address || '-' }}</el-descriptions-item>
|
||||
@ -351,7 +353,8 @@ const statusMapByModule = ref<Record<string, Record<string, string>>>({})
|
||||
const defaultStatusMap = ref<Record<string, string>>({})
|
||||
const enumFields = ref<string[]>(['status'])
|
||||
const booleanFields = ref<string[]>([])
|
||||
// 模块展示映射 {原始值: 展示名} 定义在下方「模块名展示」一节(后端下发)
|
||||
// 详情快照里不展示的纯技术字段(后端下发,见「详情降噪」一节)
|
||||
const hiddenFields = ref<string[]>([])
|
||||
|
||||
// 字段中文名解析:命中映射取中文,否则原样返回
|
||||
const fieldLabel = (key: string | number): string => {
|
||||
@ -395,6 +398,7 @@ const loadLabels = async () => {
|
||||
enumFields.value = res.data.enumFields || ['status']
|
||||
booleanFields.value = res.data.booleanFields || []
|
||||
moduleDisplay.value = res.data.moduleDisplay || {}
|
||||
hiddenFields.value = res.data.hiddenFields || []
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[AuditLog] 审计中文映射拉取失败,字段名与码值将原样显示', e)
|
||||
@ -458,24 +462,55 @@ const OBJECT_REPR_RE = /^<[A-Za-z_][\w.]*(\s+[^<>]*)?>$/
|
||||
const isObjectRepr = (v: any): boolean =>
|
||||
typeof v === 'string' && OBJECT_REPR_RE.test(v.trim())
|
||||
|
||||
// ============================================================
|
||||
// 详情降噪
|
||||
//
|
||||
// 目标:只给用户看「真正有值」的业务数据。
|
||||
//
|
||||
// ★ 「空」的判据必须**严格**:null / '' / [] / {} / '-' 才算空。
|
||||
// **false 和 0 不算空** —— `is_returned: false`、`quantity: 0` 是明确的
|
||||
// 业务事实;用 JS 的真值判断(`if (!v)`)会把它们一起吃掉,
|
||||
// 那是在悄悄篡改业务数据。
|
||||
//
|
||||
// ★ 纯技术字段清单由**后端**下发(hiddenFields),前端不硬编:
|
||||
// 它会随新表增长(尤其 pgvector 的 `*embedding` 列,单条几 KB),
|
||||
// 前端再存一份必然漂移。这里只额外按后缀拦一层 embedding,
|
||||
// 与后端 is_hidden_snapshot_field 的规则一致。
|
||||
// ============================================================
|
||||
const isBlankValue = (v: any): boolean => {
|
||||
if (v === null || v === undefined) return true
|
||||
if (typeof v === 'string') return ['', '-'].includes(v.trim())
|
||||
if (Array.isArray(v)) return v.length === 0
|
||||
if (typeof v === 'object') return Object.keys(v).length === 0
|
||||
return false
|
||||
}
|
||||
|
||||
const isHiddenField = (key: string): boolean => {
|
||||
const k = String(key || '').trim().toLowerCase()
|
||||
return hiddenFields.value.includes(k) || k.endsWith('embedding')
|
||||
}
|
||||
|
||||
// UPDATE:字段对比行
|
||||
// 过滤「等于没改」的行(两侧都空)—— 后端 changes_of 也会滤,但详情走的是
|
||||
// 原始 details,这里必须再滤一次,否则会看到「备注:空 → 空」这种行。
|
||||
const diffRows = computed(() => {
|
||||
const changes = currentDetail.value?.details?.changes
|
||||
if (!changes || typeof changes !== 'object') return []
|
||||
return Object.entries(changes).map(([key, v]: [string, any]) => ({
|
||||
key,
|
||||
old: v?.old,
|
||||
new: v?.new,
|
||||
}))
|
||||
return Object.entries(changes)
|
||||
.filter(([key, v]: [string, any]) =>
|
||||
!isHiddenField(key) && !(isBlankValue(v?.old) && isBlankValue(v?.new)))
|
||||
.map(([key, v]: [string, any]) => ({ key, old: v?.old, new: v?.new }))
|
||||
})
|
||||
|
||||
// CREATE / DELETE:快照属性行(跳过对象 repr 脏值)
|
||||
// CREATE / DELETE:快照属性行
|
||||
// 过滤:空值、纯技术字段、对象 repr 脏值(见 isObjectRepr)
|
||||
const snapshotRows = computed(() => {
|
||||
const d = currentDetail.value?.details || {}
|
||||
const snap = d.deleted_snapshot || d.created
|
||||
if (!snap || typeof snap !== 'object') return []
|
||||
return Object.entries(snap)
|
||||
.filter(([, v]) => !isObjectRepr(v))
|
||||
.filter(([key, v]) =>
|
||||
!isBlankValue(v) && !isHiddenField(key) && !isObjectRepr(v))
|
||||
.map(([key, value]) => ({ key, value }))
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user