feat: implement BOM auto-extraction system
All checks were successful
NTFY Notification / notify (push) Successful in 11s

Add complete BOM auto-extraction system with the following modules:

- M06_ModelParser: Parse product model strings to extract parameters
  (azxs, bkxs, gclj, jycz, lcfw, fjgn)
  - Extracts header part from full model (ignores dial/attachment parts)
  - Splits process connection and material code (G123 -> G12, 3)
  - Supports multiple additional features with comma/dot separators

- M07_BOMMatcher: Match materials in BOM library
  - Exact match, wildcard (empty cell), negative match (!=)
  - Special fjgn contains matching logic
  - Array-based performance optimization for bulk operations

- M08_ComponentProcessor: Handle component material special logic
  - Component inventory check (interface reserved)
  - Sub-component extraction (joint + elastic element)
  - Combination validation rules (1 component OR 1 joint + 1 element)

- M09_BOMExtractor: Main extraction orchestrator
  - Reads input models from worksheet
  - Processes each model and matches all material types
  - Outputs to "BOM提取结果" worksheet
  - Error reporting and non-blocking design

- M06B_TestRunner: Comprehensive unit tests
  - 8 test cases for model parsing
  - 5 test cases for BOM matching
  - 5 test cases for component processing

- M04_Config: Add BOM extraction constants
  - BOM library filename and configuration
  - Input/output column definitions
  - Output column enumeration

- M01_Main: Add RunBOMExtraction entry point

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-02-12 13:02:14 +08:00
parent c6c31f81c8
commit 093a2d0a3b
7 changed files with 2405 additions and 3 deletions

View File

@@ -0,0 +1,299 @@
' ==============================================================================
' 模块: M06_ModelParser
' 职责: 产品型号解析,从完整产品型号中提取关键参数
'
' 产品型号结构: [表头]|[表盘]|[附件]|[法兰隔膜]
' 表头结构: [型号]-[公称外径].[安装形式].[壳体形式].[过程连接&接液材质].[量程范围].[仪表特性]
'
' 示例: YTHN-100.A0.531.G123.M04.Y3|BP-088.2312.B09.0A3
' 表头: YTHN-100.A0.531.G123.M04.Y3
' azxs: A0, bkxs: 531, gclj: G12, jycz: 3, lcfw: M04, fjgn: Y3
'
' 注意: 仅处理表头部分,其他部分(表盘、附件、法兰隔膜)暂时丢弃
' ==============================================================================
Option Explicit
' 模块级变量 - 错误记录器
Private g_Logger As clsErrorLogger
' ------------------------------------------------------------------------------
' 初始化型号解析器
' ------------------------------------------------------------------------------
Public Sub InitModelParser(logger As clsErrorLogger)
Set g_Logger = logger
End Sub
' ------------------------------------------------------------------------------
' 主入口: 解析产品型号
'
' 输入:
' modelString - 完整的产品型号字符串
'
' 输出:
' Object (Scripting.Dictionary) - 包含提取的参数
' 键值对: "xh"->型号, "gcwj"->公称外径, "azxs"->安装形式,
' "bkxs"->表壳形式, "gclj"->过程连接, "jycz"->接液材质,
' "lcfw"->量程范围, "fjgn"->附加功能
'
' 示例:
' Set params = ParseProductModel("YTHN-100.A0.531.G123.M04.Y3|BP-088.2312.B09.0A3")
' ' params("azxs") = "A0"
' ' params("gclj") = "G12"
' ' params("jycz") = "3"
' ------------------------------------------------------------------------------
Public Function ParseProductModel(ByVal modelString As String) As Object
On Error GoTo ErrorHandler
Dim params As Object
Set params = CreateObject("Scripting.Dictionary")
' 预处理:去除前后空格
modelString = Trim(modelString)
' 如果型号为空,返回空字典
If Len(modelString) = 0 Then
Set ParseProductModel = params
Exit Function
End If
' 步骤1: 提取表头部分(第一个管道符之前的部分)
Dim headerPart As String
headerPart = ExtractHeaderPart(modelString)
If Len(headerPart) = 0 Then
If Not g_Logger Is Nothing Then
g_Logger.Record 0, "M06.ParseProductModel", "ModelParseError", _
"无法提取表头部分,型号可能为空或格式错误", modelString
End If
Set ParseProductModel = params
Exit Function
End If
' 步骤2: 分割表头为段
Dim segments As Variant
segments = SplitHeaderPart(headerPart)
If Not IsArray(segments) Then
If Not g_Logger Is Nothing Then
g_Logger.Record 0, "M06.ParseProductModel", "ModelParseError", _
"表头分割失败", headerPart
End If
Set ParseProductModel = params
Exit Function
End If
' 步骤3: 验证段数是否足够
If UBound(segments) - LBound(segments) + 1 < MODEL_HEADER_MIN_SEGMENTS Then
If Not g_Logger Is Nothing Then
g_Logger.Record 0, "M06.ParseProductModel", "ModelParseError", _
"表头段数不足,需要至少" & MODEL_HEADER_MIN_SEGMENTS & "段,实际" & _
(UBound(segments) - LBound(segments) + 1) & "段", headerPart
End If
End If
' 步骤4: 提取型号和公称外径预留暂不参与BOM匹配
Call ExtractModelAndSize(headerPart, params)
' 步骤5: 提取各参数
' 注意segments(0)是"型号-公称外径"需要从segments(1)开始提取参数
If UBound(segments) - LBound(segments) + 1 >= 2 Then
params("azxs") = ExtractAzxs(segments(1))
End If
If UBound(segments) - LBound(segments) + 1 >= 3 Then
params("bkxs") = ExtractBkxs(segments(2))
End If
If UBound(segments) - LBound(segments) + 1 >= 4 Then
Dim gclj As String, jycz As String
Call ExtractGcljAndJycz(segments(3), gclj, jycz)
params("gclj") = gclj
params("jycz") = jycz
End If
If UBound(segments) - LBound(segments) + 1 >= 5 Then
params("lcfw") = ExtractLcfw(segments(4))
End If
If UBound(segments) - LBound(segments) + 1 >= 6 Then
params("fjgn") = ExtractFjgn(segments, 5)
End If
Set ParseProductModel = params
Exit Function
ErrorHandler:
If Not g_Logger Is Nothing Then
g_Logger.Record 0, "M06.ParseProductModel", "SystemError", _
"解析过程发生错误: " & Err.Description, modelString
End If
Set ParseProductModel = CreateObject("Scripting.Dictionary")
End Function
' ------------------------------------------------------------------------------
' 提取表头部分(管道符之前)
'
' 输入: YTHN-100.A0.531.G123.M04.Y3|BP-088.2312.B09.0A3
' 输出: YTHN-100.A0.531.G123.M04.Y3
' ------------------------------------------------------------------------------
Private Function ExtractHeaderPart(ByVal fullModel As String) As String
Dim pipePos As Long
pipePos = InStr(fullModel, MODEL_SEPARATOR_PIPELINE)
If pipePos > 0 Then
ExtractHeaderPart = Left(fullModel, pipePos - 1)
Else
ExtractHeaderPart = fullModel
End If
End Function
' ------------------------------------------------------------------------------
' 分割表头部分为段数组
'
' 输入: YTHN-100.A0.531.G123.M04.Y3
' 输出: Array("YTHN-100", "A0", "531", "G123", "M04", "Y3")
' ------------------------------------------------------------------------------
Private Function SplitHeaderPart(ByVal headerPart As String) As Variant
' 按点号分割
Dim rawSegments As Variant
rawSegments = Split(headerPart, MODEL_SEPARATOR_DOT)
' 如果分割失败或结果为空
If Not IsArray(rawSegments) Then
SplitHeaderPart = Null
Exit Function
End If
' 去除每个段的空格
Dim i As Long
For i = LBound(rawSegments) To UBound(rawSegments)
rawSegments(i) = Trim(CStr(rawSegments(i)))
Next i
SplitHeaderPart = rawSegments
End Function
' ------------------------------------------------------------------------------
' 提取型号和公称外径(预留字段)
'
' 输入: YTHN-100
' 输出: params("xh")="YTHN", params("gcwj")="100"
' ------------------------------------------------------------------------------
Private Sub ExtractModelAndSize(ByVal firstSegment As String, ByRef params As Object)
Dim dashPos As Long
dashPos = InStr(firstSegment, "-")
If dashPos > 1 Then
params("xh") = Left(firstSegment, dashPos - 1)
Dim afterDash As String
afterDash = Mid(firstSegment, dashPos + 1)
' 提取-号后的数字部分(公称外径)
' 因为firstSegment是"YTHN-100.A0...",需要去掉后面的点号内容
Dim dotPos As Long
dotPos = InStr(afterDash, ".")
If dotPos > 0 Then
params("gcwj") = Left(afterDash, dotPos - 1)
Else
params("gcwj") = afterDash
End If
End If
End Sub
' ------------------------------------------------------------------------------
' 提取安装形式
'
' 输入: A0
' 输出: A0
'
' 规则: 直接返回第一段的值
' ------------------------------------------------------------------------------
Private Function ExtractAzxs(ByVal segment As String) As String
ExtractAzxs = Trim(segment)
End Function
' ------------------------------------------------------------------------------
' 提取表壳形式
'
' 输入: 531
' 输出: 531
'
' 规则: 直接返回第二段的值
' ------------------------------------------------------------------------------
Private Function ExtractBkxs(ByVal segment As String) As String
ExtractBkxs = Trim(segment)
End Function
' ------------------------------------------------------------------------------
' 提取过程连接和接液材质
'
' 输入: G123
' 输出: gclj="G12", jycz="3"
'
' 规则:
' - 过程连接: 去除最后一位
' - 接液材质: 最后一位
' ------------------------------------------------------------------------------
Private Sub ExtractGcljAndJycz(ByVal segment As String, ByRef gclj As String, ByRef jycz As String)
segment = Trim(segment)
If Len(segment) >= 1 Then
jycz = Right(segment, 1)
If Len(segment) > 1 Then
gclj = Left(segment, Len(segment) - 1)
Else
gclj = ""
End If
Else
gclj = ""
jycz = ""
End If
End Sub
' ------------------------------------------------------------------------------
' 提取量程范围
'
' 输入: M04
' 输出: M04
'
' 规则: 直接返回第四段的值
' ------------------------------------------------------------------------------
Private Function ExtractLcfw(ByVal segment As String) As String
ExtractLcfw = Trim(segment)
End Function
' ------------------------------------------------------------------------------
' 提取附加功能
'
' 输入: segments数组起始索引为4
' 输出: Y3 或 N1,N2 或 N1.N2
'
' 规则:
' - 从第五段开始,所有段合并为附加功能
' - 用逗号或点号分隔的多个功能,保留原分隔符
' - 示例: Y3 -> Y3
' - 示例: N1,N2.Y3 -> N1,N2,Y3
' ------------------------------------------------------------------------------
Private Function ExtractFjgn(ByRef segments As Variant, ByVal startIndex As Long) As String
Dim result As String
result = ""
Dim i As Long
For i = startIndex To UBound(segments)
Dim segment As String
segment = Trim(CStr(segments(i)))
If Len(segment) > 0 Then
' 替换点号为逗号(统一分隔符)
segment = Replace(segment, MODEL_SEPARATOR_DOT, ",")
If Len(result) > 0 Then
result = result & "," & segment
Else
result = segment
End If
End If
Next i
ExtractFjgn = result
End Function