certflow.models.caliber_mapping 源代码

"""口径映射模型

将各种格式的口径原始值(如 "DN15"、"1/2\""、"G1/2"、"3/8\"" 等)映射为标准口径。
支持公制和英制两种表示。
"""

from __future__ import annotations

from sqlalchemy import Boolean, Column, DateTime, Integer, String
from sqlalchemy.orm import Session
from sqlalchemy.sql import func

from certflow.models.base import Base


[文档] class CaliberMapping(Base): """口径字符串映射表 对标 VBA function.vb 确认口径() 第164-166行: .. code-block:: text Arr_KouJingDuiYing = CanShuSheet.Range("口径替换数据表") For i = LBound(Arr_KouJingDuiYing, 1) To UBound(Arr_KouJingDuiYing, 1) If VBA.Trim(Arr_KouJingDuiYing(i, 1)) = VBA.Trim(口径字符串) Then ... Attributes: id: 主键ID raw_text: 原始口径字符串(如 "DN100", "2\"", "2-1/2\"", "DN100*80") caliber_value: 标准化口径值(如 "100", "50", "65", "100*80") is_imperial: 是否英制口径 source: 来源(manual/auto-learned/import) usage_count: 使用次数 """ __tablename__ = "caliber_mappings" id = Column(Integer, primary_key=True, autoincrement=True) raw_text = Column(String(50), nullable=False, unique=True, index=True, comment="原始口径文本") caliber_value = Column(String(20), nullable=False, comment="口径值(mm或英寸表示)") is_imperial = Column(Boolean, default=False, comment="是否英制(True=英寸,False=毫米)") source = Column(String(20), default="manual", comment="来源:manual/auto_learn/import") usage_count = Column(Integer, default=0, comment="使用次数") created_at = Column(DateTime, server_default=func.now(), comment="创建时间") updated_at = Column( DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间" ) # raw_text 已设为 unique=True,SQLite 自动生成唯一索引,无需重复显式索引(阶段1b 清理) __table_args__ = () def __repr__(self) -> str: return f"<CaliberMapping(raw='{self.raw_text}' → {self.caliber_value}{'英寸' if self.is_imperial else 'mm'})>"
[文档] def to_mm(self) -> int | None: """转换为毫米值 Returns: int | None: 标准化后的毫米整数值;无法解析时返回 None。 """ if self.is_imperial: try: if "/" in self.caliber_value: num, den = self.caliber_value.split("/") inch = float(num) / float(den) else: inch = float(self.caliber_value) return round(inch * 25.4) except (ValueError, ZeroDivisionError): return None else: try: return int(float(self.caliber_value)) except ValueError: return None
class CaliberMappingService: """口径映射服务""" # 内置正则规则(作为数据库映射的补充/回退) BUILTIN_PATTERNS = [ # DN格式:DN15, DN 15, DN15A (r"DN\s*(\d+)", lambda m: ("mm", int(m.group(1)))), # 数字+mm:15mm, 15 mm (r"(\d+)\s*mm", lambda m: ("mm", int(m.group(1)))), # 纯数字(毫米) (r"^(\d+)$", lambda m: ("mm", int(m.group(1)))), # 英寸分数:1/2", 1/2″ (r"(\d+)/(\d+)[\"″']?", lambda m: ("inch", f"{m.group(1)}/{m.group(2)}")), # 英寸整数:2", 2″ (r"(\d+(?:\.\d+)?)[\"″']", lambda m: ("inch", m.group(1))), # G螺纹:G1/2 → 1/2英寸 (r"G(\d+)/(\d+)", lambda m: ("inch", f"{m.group(1)}/{m.group(2)}")), (r"G(\d+)", lambda m: ("inch", m.group(1)) if int(m.group(1)) <= 4 else None), # 带PN后缀:DN15 PN315 → 15mm (r"DN(\d+)\s*PN", lambda m: ("mm", int(m.group(1)))), # 带抗硫后缀:DN80抗硫 → 80mm (r"DN(\d+)抗硫", lambda m: ("mm", int(m.group(1)))), # 范围格式:DN65/40 → 65mm (r"DN(\d+)/\d+", lambda m: ("mm", int(m.group(1)))), ] def __init__(self, session: Session) -> None: """初始化口径映射服务。 Args: session: SQLAlchemy 会话,用于查询与持久化。 """ self.session = session def resolve_caliber(self, raw_text: str) -> tuple[str, str] | None: """解析口径,返回 (单位, 值) Args: raw_text: 原始口径文本 Returns: (单位, 值) 如 ("mm", "15") 或 ("inch", "3/8"),无法解析返回 None """ if not raw_text or not isinstance(raw_text, str): return None raw_text = raw_text.strip() if not raw_text: return None # 1. 精确匹配数据库 mapping = ( self.session.query(CaliberMapping).filter(CaliberMapping.raw_text == raw_text).first() ) if mapping: mapping.usage_count = (mapping.usage_count or 0) + 1 self.session.commit() unit = "inch" if mapping.is_imperial else "mm" return (unit, mapping.caliber_value) # 2. 标准化后匹配(去空格、转大写) normalized = self._normalize(raw_text) mapping = ( self.session.query(CaliberMapping).filter(CaliberMapping.raw_text == normalized).first() ) if mapping: mapping.usage_count = (mapping.usage_count or 0) + 1 self.session.commit() unit = "inch" if mapping.is_imperial else "mm" return (unit, mapping.caliber_value) # 3. 内置规则 import re for pattern, extractor in self.BUILTIN_PATTERNS: match = re.search(pattern, raw_text, re.IGNORECASE) if match: try: result = extractor(match) if result: unit, value = result self.auto_learn(raw_text, unit, str(value)) return (unit, str(value)) except Exception: continue return None def resolve_to_mm(self, raw_text: str) -> int | None: """解析口径并返回毫米值 Args: raw_text: 原始口径文本。 Returns: int | None: 换算后的毫米整数值;无法解析时返回 None。 """ result = self.resolve_caliber(raw_text) if not result: return None unit, value = result if unit == "mm": try: return int(float(value)) except ValueError: return None else: try: if "/" in value: num, den = value.split("/") inch = float(num) / float(den) else: inch = float(value) return round(inch * 25.4) except (ValueError, ZeroDivisionError): return None def _normalize(self, text: str) -> str: """标准化文本""" if not text: return "" text = text.strip() text = text.replace("″", '"').replace("′", "'") return text.upper() def add_mapping( self, raw_text: str, unit: str, value: str, source: str = "manual" ) -> CaliberMapping | None: """添加口径映射 Args: raw_text: 原始口径文本。 unit: 单位(mm/inch)。 value: 口径值。 source: 来源。 Returns: CaliberMapping | None: 新建或更新后的映射对象;raw_text 或 value 为空时返回 None。 """ if not raw_text or not value: return None raw_text = raw_text.strip() is_imperial = unit == "inch" existing = ( self.session.query(CaliberMapping).filter(CaliberMapping.raw_text == raw_text).first() ) if existing: existing.caliber_value = value existing.is_imperial = is_imperial existing.updated_at = func.now() self.session.commit() return existing mapping = CaliberMapping( raw_text=raw_text, caliber_value=value, is_imperial=is_imperial, source=source, usage_count=0, ) self.session.add(mapping) self.session.commit() return mapping def auto_learn(self, raw_text: str, unit: str, value: str) -> None: """自动学习新映射 Args: raw_text: 原始口径文本。 unit: 单位(mm/inch)。 value: 口径值。 """ if not raw_text: return raw_text = raw_text.strip() existing = ( self.session.query(CaliberMapping).filter(CaliberMapping.raw_text == raw_text).first() ) if existing: return mapping = CaliberMapping( raw_text=raw_text, caliber_value=value, is_imperial=(unit == "inch"), source="auto_learn", usage_count=1, ) self.session.add(mapping) self.session.commit() def get_all_mappings(self) -> list[dict]: """获取所有映射(按使用次数降序) Returns: list[dict]: 每条映射的字典列表,按 usage_count 降序、raw_text 升序排列。 """ mappings = ( self.session.query(CaliberMapping) .order_by(CaliberMapping.usage_count.desc(), CaliberMapping.raw_text) .all() ) return [ { "id": m.id, "raw_text": m.raw_text, "caliber_value": m.caliber_value, "is_imperial": m.is_imperial, "display": f"{m.caliber_value}{'英寸' if m.is_imperial else 'mm'}", "source": m.source, "usage_count": m.usage_count, } for m in mappings ]