|
|
89db1d14d3
|
feat(audit): BOM 摘要带出子件,消除批量插入的刷屏
问题:BOM 一次新建会批量插入几十条子件关联记录,同一秒出现几十行
target_name 与 summary 完全相同的日志。实测同一 bom_no 同一秒最多 58 条,
整页 200 条只折叠出 16 种摘要。UPDATE 更严重 —— 批量归档时一种摘要
(「是否启用:是→否;是否归档:否→是」)重复了 135 次。
改动:changes_summary 增加子件分支,load_child_lookup 解析子件。
两条解析路径:
1. 快照里的 child_id —— CREATE/DELETE(实测 BOM CREATE 覆盖 100%)
2. 否则若 module 是 BOM,用 target_id 反查 bom_table —— UPDATE
(实测 584/634 可解)
★ 为什么必须按 module 门控,不能靠「target_id 命中 bom_table」这个特征:
后者实测**大量误命中** —— 入库管理 3655 条、系统管理的 /permissions/assign
1415 条、image_embeddings 1187 条、出库管理 726 条,它们的 target_id 都会
撞上某条 bom_table 记录。照着补子件名就是把毫不相干的零件安到别的记录上。
而 module 由**表名**推得(audit_listener: bom_table → 'BOM管理'),
按 module 判定等价于按表判定,是可靠的。
★ 子件名直接可用,不必退回 #ID:child_id 已在 load_ref_maps 的批量解析
范围内,且 load_child_lookup 自身也只做一次 MaterialBase 批量查
(实测 200 行 1 次查询、20 行 1 次查询,与行数无关,无 N+1)。
摘要格式:
新增(BOM管理):添加子件 SF-9000 机加工配件(用量 1)
删除(BOM管理):移除子件 四代一体-侧面壳(用量 1)
子件 9-36V输入5V输出隔离模块15W:是否启用:是→否;是否归档:否→是(来源:/bom/archive)
用量只在快照里有;UPDATE 行拿不到就不显示 —— 宁可不显示,也不去猜。
效果(实测):
· 同一秒 58 条 → 58 种不同摘要
· 整页 200 条 CREATE:改造前 16 种 → 改造后 76 种
· UPDATE:改造前 300 条折叠成 5 种(最多重复 135)→ 去重种类显著提升
· 非 BOM 记录零污染(系统管理/入库管理/出库管理 各 200 条,含「子件」0 条)
验证:16 + 18 项断言全过;日报三天附件 490.5K / 193.4K / 10.3K 与 6 列结构未变;
导出口径在 4 组筛选下与列表一致;权限与凭据过滤未松动;
字典 182 个键零缺失;全库 0 条退化到原始 JSON。vue-tsc 与 vite build exit=0。
|
2026-09-28 10:52:25 +08:00 |
|
|
|
956d4acb5f
|
feat(audit): 快照嵌套分层渲染 + 凭据字段从接口剥离(安全修复)
起因:借库管理等单据的详情页仍显示原始 JSON 代码块。
根因:details 有**第三种**存法 {'payload': {...}},且内部嵌着对象数组
(items 明细行),而旧实现只认平铺的 created/deleted_snapshot,
遇到非平铺结构就退化成原始 JSON。
1. 快照键收敛(后端)
created / deleted_snapshot / payload 三种键与展示标题收进
audit_labels.SNAPSHOT_VIEWS,经 /audit/labels 下发 snapshotViews,
前端不再硬编键名 —— 将来加第四种只改一处。
audit_export_service 与 daily_report_service 改为引用同一份。
changes_summary 也改用 snapshot_of_any:payload 型记录的新增摘要
此前整列为空,现在有内容了(如「新增(借库管理):物料明细 1 项、
备注、借用人、签名、预计归还时间」)。
2. 分层渲染(前端)
· 标量字段 → el-descriptions(保留字典翻译与空值/技术字段过滤)
· 数组字段 → el-table:列取**所有元素键的并集**(同一数组内元素键
并不完全一致,只取首个元素会漏字段),跳过技术字段,列头走 fieldLabel
· 标量数组(arrival_photo 等图片 URL 列表)→ 单列表格
· details / payload 本身是数组或标量的情况也一并处理
· 只有确实无可识别结构时才回落到原始 JSON
实测全库 58728 条:**0 条**会退化到原始 JSON 兜底
(payload 1495 条全部拆出结构)。
3. ★ 安全修复:审计快照里存有**明文密码**,本次从所有出口剥离
实测 `用户管理/新增` 的 payload 快照记着哈希前的原始密码(27 条,
形如 '123456'、'shili0823'),因为写入路径把请求体整包记进了审计。
· sanitize_details():递归剥掉凭据类字段,应用于列表与详情接口
· changes_of / snapshot_of 在**源头**滤掉凭据:逐个出口去补必然漏掉
某一个,而漏掉的那个就是泄漏点(日报附件、导出、摘要都走这两个函数)
· 凭据判定用**关键词**(password/passwd/secret/token/private_key)
而非逐个登记 —— 今天漏的是 payload 里的 password,明天可能是
reset_token。新表加凭据字段也自动被挡住。
★ 第一版我只做了前端隐藏(hiddenFields),被测试抓出来:原始响应里
密码照样在,打开 devtools 就读得到。**要挡的数据必须在接口出口剥掉,
前端隐藏不是防线。**
验证:14 + 15 项断言全过,含「列表/详情响应无密码原文与 password 键」
「全量导出的表头与单元格均无凭据」「日报三天附件无凭据」
「sanitize_details 递归进嵌套数组且不原地改原对象」。
6 列结构与附件大小未变(490.5K / 193.4K / 10.3K);vue-tsc 与 vite build exit=0。
⚠️ 数据库中仍存有明文密码(16 条非空),本次只挡展示与导出,**未改数据** ——
改数据不可逆,需单独立项决定。
|
2026-09-28 10:40:00 +08:00 |
|
|
|
91672214c8
|
feat(audit): 操作对象补全改为按特征全局解析,摘要联动物料名
此前补全只覆盖「入库/采购」,报废、退回、BOM 仍只显示光秃秃的 SKU 或 bom_no。
根因是解析走的是「快照 base_id / target_id 命中股票表」两条硬路径,
而这几类记录的可用线索完全不同(报废快照有 sku 无 base_id、BOM 只有
parent_id、UPDATE 行两样都没有)。
改为**按特征五级解析,不看 module 名单**(同一 module 内不同记录的线索
本来就不一样,按 module 判断必然覆盖不全):
1. 快照 base_id —— 库存行的权威线索
2. 快照 sku → SKU 索引 —— 报废/退回/借还(单据表有 sku、无 base_id)
3. target_name 是 SKU —— 库存/退回/报废 的 UPDATE 行(无快照)
4. 快照 parent_id —— BOM 关系行
5. target_id 唯一命中股票表 —— target_name 为 'stock_buy ID:N' 的入库行
★ 「按 SKU 匹配」不能直接查 material_base —— 那张表**没有 sku 列**,
SKU 在 7 张流水/库存表上(stock_buy/semi/product、stock_adjustment、
stock_service、trans_defective_goods、trans_repair)。故建 SKU→base_id
索引:把这些表 UNION 后要求一个 SKU 只对应一个物料。
实测并集 2178 个 SKU、0 歧义;审计里 432/438 个 SKU 型 target_name 可解。
表名从 information_schema 动态枚举并缓存,**不硬编** —— 硬编清单在加表时
会静默漏掉(本项目反复踩过的漂移)。
★ SKU 形状护栏 ^\d{10}$:实测库里 2178 个 SKU **全部**是 10 位纯数字,
而字母开头的 target_name(bom_no/单号/角色码)与 SKU 集合命中数为 0 ——
护栏既不误伤也不漏,挡掉了"拿单号当 SKU 查"的误配风险。
★ 第 4 条取 parent_id 而非 child_id:BOM 行的 bom_no(操作对象)属于父件,
实测同一 bom_no 的所有子行 parent_id 一致('IH-L2V1J' → parent 44)。
用子件会显示成只在 BOM 里出现一次的小零件名。
★ 编码段优先用库里的 target_name:单号(BOR-…)、bom_no(IH-L2V1J)本身
就是业务标识,比 SKU 更能定位记录 —— 把「BOR-…」换成 SKU 是**换掉**了
有用信息。只有 target_name 是内部兜底形态('stock_buy ID:1667')时才用 SKU。
★ 批量查询:先扫一遍收集线索再批量查,全程恒为 5 次查询(SKU 索引 1 +
股票表 3 + material_base 1),与页大小无关。实测 20 条与 200 条都是 5 次。
效果(真实数据):
OUT-20260915-0909-0001 - SIF金属箱门包边 (061工)
BOR-20260922-0001 - 防水余弦接收器 (FOV0008/FOV0008)
IH-L2V1J - 穹顶光源V1J (IH-L2V1J/类A)
0000001180 - 华为路由AX3 wifi6路由器3000M (Det0001/Det0001)
覆盖率 972/4000 → 2985/4000,与快照 base_id **零冲突**。
摘要联动:CREATE/DELETE 摘要的「物料」段取自解析结果(快照里只有 sku,
名称在 material_base 上),形如
「新增(报废管理):华为路由AX3 wifi6路由器3000M、数量 1 件」。
物料名排在数量/位置之前;短名如「防水余弦接收器」不加截断。
验证:24 + 16 项断言全过(五级路径逐条覆盖、错误率为 0、业务单号保留为
编码段、查询次数恒定、导出口径与列表一致、权限 401/403/200、字典零遗漏);
vue-tsc --noEmit 与 vite build 均 exit=0。
日报回归:三天附件 490.5K / 193.4K / 10.3K,6 列结构未变。
|
2026-09-28 10:25:55 +08:00 |
|
|
|
46aec0b954
|
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 列结构未变。
|
2026-09-28 10:19:58 +08:00 |
|
|
|
6a9e41f53c
|
feat(audit-ui): 审计页 UI 重构 —— 干净模块下拉、变更摘要、抽屉详情、触发来源
后端:GET /audit/modules 与 /logs 的 modules 改为下发最终选项 [{value,label}]
· 历史命名(入库管理/采购入库/成品入库/库存管理)折叠进「入库」一项且
**不再单独列出**,下拉里看不到历史包袱。展开仍只由后端 expand_modules
负责 —— 前端若自己再拼一份成员表,将来后端加成员会静默失效(本项目
已经踩过两次这种漂移)。
· 英文历史 module(image_embeddings 2719 条 / purchase_request 75 条 /
sys_element 6 条)在这里翻成中文 label,value 保留英文原值否则筛选
匹配不上。映射表 MODULE_LABELS 从**前端** AuditLog.vue 的 moduleMap
收进 audit_labels.py —— 那份硬编副本历史上已经漂移过一次。
· /audit/labels 新增 moduleDisplay {原始值 → 展示名},同时覆盖聚合成员与
英文历史值:只覆盖后者会出现「下拉显示『入库』、表格显示『库存管理』」,
用户会以为筛选没生效。
后端:新增「触发来源」
· 摘要追加 (来源:/outbound/request) 形式的 URL 尾段(去掉 /api/vN 前缀)。
解决一个具体误解:业务方看到「入库/库存管理」里有大量非库管人员的
UPDATE,以为越权改库存,实际是出库/借还/盘点等单据流转触发的自动扣减。
实测该类 UPDATE 的来源:outbound 1582、outbound/request 424、
borrow/dispatch 102、stocktake/update-quantity 95 —— 光看"谁改的"永远
解释不清,必须能看出"哪个流程触发的"。
· 来源拼在**截断之后**,保证这条关键信息永远不会被截掉。
· 列表与导出共用 changes_summary,故 Excel 台账同样带来源。
后端:其余
· 新增 target_keyword(ilike 同时匹配 target_id / target_name),
放进共用的 _build_audit_query,导出自动支持。
· action 在响应里归一化为 CREATE/UPDATE/DELETE(复用 audit_labels
.canon_action,覆盖批量删除/分配/归还等全部历史别名)。库里还有 1573 条
中文 action,归一后前端不必再为每种历史写法兜底。
· 新增 summary 字段。不算在模型 @property 上:摘要要把 user_id/base_id
翻成人名/物料名,需要查库,模型属性里发查询就是 N+1;改为整页一次
load_ref_maps 批量解析。
· 过滤对象 repr 脏值(<MaterialBase 3030>)。快照收集早期漏跳关系属性,
库里留了 1529 条这种值 —— 不是业务数据,真正的值在对应 _id 字段里。
判据收窄到「<类名 空格 内容>」,避免误伤备注里的「<急件>」。
前端 AuditLog.vue
· 模块下拉改平铺 [{value,label}],删除硬编的 moduleMap
· 新增「操作对象」模糊搜索
· 合并「操作人」「姓名」两列(判据 username==='system' —— 响应里没有
operator_type 字段,那是请求参数名,照搬会永远不显示系统标签)
· 新增「变更摘要」列;操作对象显示为「名称 #id」
· 操作时间改为恒显示北京时间,不再按浏览器时区换算,与数据库/日报/导出一致
· 详情弹窗改 el-drawer,按 action 分支渲染:UPDATE 出对比表,
CREATE/DELETE 出快照属性表,并跳过对象 repr
· 抽屉顶部高亮展示触发来源(METHOD + URL 告警条)
验证:后端 18 + 22 + 10 项断言全过(模块选项无历史名且「入库」=四值之和、
英文 value 仍可筛选、url_source 边界、来源不被截断、导出口径与列表一致、
权限 401/403/200 矩阵);vue-tsc --noEmit 与 vite build 均 exit=0。
日报回归:三天附件 490.5K/193.5K/10.3K,6 列结构与改动前一致。
|
2026-09-28 10:10:05 +08:00 |
|
|
|
1d6beaa376
|
feat(daily-report): 正文保持摘要,新增 Excel 附件给全量明细
日报此前把明细压到极简(修改只列变更字段≥3 的记录、新增只给模块计数),
"199 条新增只看到 3 个模块名",查不到某条具体记录改了什么。
但也不能全塞进正文:实测某日 354 条修改展开近 3000 行,客户端渲染卡顿,
部分企业邮件网关会把超长自动邮件判为垃圾或截断 —— 发件方照样收到 250,
表面看一切正常,这比"信息少"更难排查。
故拆成两路:正文保持摘要(render),附件给全量明细(render_excel),
六个工作表不设 DETAIL_MIN_FIELDS 之类的阈值。
- email_service: send_email/send_email_async 支持 attachments。数据是内存
字节、不落盘;中文文件名走 RFC 2231;.xlsx 用官方 MIME 类型,写错会让
Outlook 当成未知二进制、附件无法直接打开。
- audit_export_service: 新增。审计日志 → 可读值/Excel 的共用层。
「同一条「实际审批人ID: 7」一边翻了人名一边没翻」这类漂移,比不翻译更
难发现,故取值/翻译/排版逻辑集中在此,由日报与审计页导出共用。
- daily_report_service: 私有副本改为消费上述共用层(纯重构)。
附件大小与重构前逐字节一致(490.5K / 193.9K / 10.3K),行为未变。
附件生成失败时降级为纯文本正文继续发送,但记 error 级日志 —— 降级后邮件
看起来完全正常,不留显眼日志的话附件静默丢失可以持续几个月没人发现。
|
2026-09-28 09:46:25 +08:00 |
|