From d0a237625c3fcb237d2acf2c81181cb2c01d920b Mon Sep 17 00:00:00 2001 From: dxc Date: Wed, 11 Feb 2026 11:28:15 +0800 Subject: [PATCH] feat: improve fuzzy search for buy inbound material Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) --- .../app/services/inbound/buy_service.py | 151 ++++++++++++------ 1 file changed, 104 insertions(+), 47 deletions(-) diff --git a/inventory-backend/app/services/inbound/buy_service.py b/inventory-backend/app/services/inbound/buy_service.py index ff4b0cd..f421afd 100644 --- a/inventory-backend/app/services/inbound/buy_service.py +++ b/inventory-backend/app/services/inbound/buy_service.py @@ -41,7 +41,7 @@ class BuyInboundService: raise ValueError(f"该物料已存在批号【{batch_number}】,请勿重复录入,可直接在该批次下追加库存。") # ============================================================ - # 1. 基础物料搜索 (参照 Base 模块:全量模糊匹配) + # 1. 基础物料搜索 (增强模糊匹配:多字段、多关键词、分词匹配) # ============================================================ @staticmethod def search_base_material(keyword): @@ -49,26 +49,56 @@ class BuyInboundService: query = MaterialBase.query.filter(MaterialBase.is_enabled == True) if keyword: - # [参照 Base 逻辑] 简单直接的模糊匹配:LIKE %keyword% - k_str = f'%{keyword.strip()}%' + # 1. 清理关键词:去除首尾空格,并将多个连续空格替换为单个 + import re + keyword_clean = re.sub(r'\s+', ' ', keyword.strip()) + + # 2. 支持两种搜索模式: + # a) 精确短语匹配:用双引号包裹,如 "蓝色电阻" + # b) 多关键词 AND 匹配:空格分隔,如 "蓝色 电阻" + # c) 单关键词模糊匹配:如 "蓝色" + + if keyword_clean.startswith('"') and keyword_clean.endswith('"'): + # 精确短语匹配 + exact_phrase = keyword_clean[1:-1] + if exact_phrase: + k_str = f'%{exact_phrase}%' + conditions = [ + MaterialBase.name.ilike(k_str), + MaterialBase.spec_model.ilike(k_str), + MaterialBase.pinyin.ilike(k_str), + MaterialBase.category.ilike(k_str), + MaterialBase.material_type.ilike(k_str) + ] + if hasattr(MaterialBase, 'brand'): + conditions.append(MaterialBase.brand.ilike(k_str)) + if hasattr(MaterialBase, 'manufacturer'): + conditions.append(MaterialBase.manufacturer.ilike(k_str)) + query = query.filter(or_(*conditions)) + else: + # 多关键词 AND 匹配 + keywords = keyword_clean.split() + if keywords: + and_conditions = [] + for word in keywords: + k_str = f'%{word}%' + word_conditions = [ + MaterialBase.name.ilike(k_str), + MaterialBase.spec_model.ilike(k_str), + MaterialBase.pinyin.ilike(k_str), + MaterialBase.category.ilike(k_str), + MaterialBase.material_type.ilike(k_str) + ] + if hasattr(MaterialBase, 'brand'): + word_conditions.append(MaterialBase.brand.ilike(k_str)) + if hasattr(MaterialBase, 'manufacturer'): + word_conditions.append(MaterialBase.manufacturer.ilike(k_str)) + and_conditions.append(or_(*word_conditions)) + # 使用 AND 连接所有关键词条件 + if and_conditions: + query = query.filter(and_(*and_conditions)) - conditions = [ - MaterialBase.name.ilike(k_str), # 名称 - MaterialBase.spec_model.ilike(k_str), # 规格 - MaterialBase.pinyin.ilike(k_str), # 拼音 - MaterialBase.category.ilike(k_str), # 类别 - MaterialBase.material_type.ilike(k_str) # 类型 - ] - - # 安全地添加可能存在的扩展字段 (品牌/厂家) - if hasattr(MaterialBase, 'brand'): - conditions.append(MaterialBase.brand.ilike(k_str)) - if hasattr(MaterialBase, 'manufacturer'): - conditions.append(MaterialBase.manufacturer.ilike(k_str)) - - query = query.filter(or_(*conditions)) - - # [参照 Base 逻辑] 移除 limit,返回所有结果 + # 按 ID 倒序排序 query = query.order_by(MaterialBase.id.desc()) results = [] @@ -80,7 +110,6 @@ class BuyInboundService: 'category': item.category, 'unit': item.unit, 'type': item.material_type, - # 使用 getattr 防止字段不存在报错 'brand': getattr(item, 'brand', ''), 'manufacturer': getattr(item, 'manufacturer', ''), 'pinyin': getattr(item, 'pinyin', ''), @@ -261,7 +290,7 @@ class BuyInboundService: raise e # ============================================================ - # 5. 获取列表 (参照 Base 逻辑:全量模糊匹配) + # 5. 获取列表 (增强模糊匹配:多字段、多关键词、分词匹配) # ============================================================ @staticmethod def get_list(page, limit, keyword=None, statuses=None): @@ -269,30 +298,58 @@ class BuyInboundService: query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id) if keyword: - # [简单匹配] 只要任一字段包含该字符串,即匹配 - k_str = f'%{keyword.strip()}%' - - conditions = [ - # 库存业务字段 - StockBuy.sku.ilike(k_str), - StockBuy.barcode.ilike(k_str), - StockBuy.batch_number.ilike(k_str), - StockBuy.serial_number.ilike(k_str), - StockBuy.supplier_name.ilike(k_str), - StockBuy.buyer_name.ilike(k_str), - # 基础物料关联字段 - MaterialBase.name.ilike(k_str), - MaterialBase.spec_model.ilike(k_str), - MaterialBase.pinyin.ilike(k_str), - MaterialBase.category.ilike(k_str) - ] - - if hasattr(MaterialBase, 'brand'): - conditions.append(MaterialBase.brand.ilike(k_str)) - if hasattr(MaterialBase, 'manufacturer'): - conditions.append(MaterialBase.manufacturer.ilike(k_str)) - - query = query.filter(or_(*conditions)) + # 1. 清理关键词 + import re + keyword_clean = re.sub(r'\s+', ' ', keyword.strip()) + + if keyword_clean.startswith('"') and keyword_clean.endswith('"'): + # 精确短语匹配 + exact_phrase = keyword_clean[1:-1] + if exact_phrase: + k_str = f'%{exact_phrase}%' + conditions = [ + StockBuy.sku.ilike(k_str), + StockBuy.barcode.ilike(k_str), + StockBuy.batch_number.ilike(k_str), + StockBuy.serial_number.ilike(k_str), + StockBuy.supplier_name.ilike(k_str), + StockBuy.buyer_name.ilike(k_str), + MaterialBase.name.ilike(k_str), + MaterialBase.spec_model.ilike(k_str), + MaterialBase.pinyin.ilike(k_str), + MaterialBase.category.ilike(k_str) + ] + if hasattr(MaterialBase, 'brand'): + conditions.append(MaterialBase.brand.ilike(k_str)) + if hasattr(MaterialBase, 'manufacturer'): + conditions.append(MaterialBase.manufacturer.ilike(k_str)) + query = query.filter(or_(*conditions)) + else: + # 多关键词 AND 匹配 + keywords = keyword_clean.split() + if keywords: + and_conditions = [] + for word in keywords: + k_str = f'%{word}%' + word_conditions = [ + StockBuy.sku.ilike(k_str), + StockBuy.barcode.ilike(k_str), + StockBuy.batch_number.ilike(k_str), + StockBuy.serial_number.ilike(k_str), + StockBuy.supplier_name.ilike(k_str), + StockBuy.buyer_name.ilike(k_str), + MaterialBase.name.ilike(k_str), + MaterialBase.spec_model.ilike(k_str), + MaterialBase.pinyin.ilike(k_str), + MaterialBase.category.ilike(k_str) + ] + if hasattr(MaterialBase, 'brand'): + word_conditions.append(MaterialBase.brand.ilike(k_str)) + if hasattr(MaterialBase, 'manufacturer'): + word_conditions.append(MaterialBase.manufacturer.ilike(k_str)) + and_conditions.append(or_(*word_conditions)) + if and_conditions: + query = query.filter(and_(*and_conditions)) if not statuses: statuses = ['在库', '借库'] @@ -447,4 +504,4 @@ class BuyInboundService: locs = [row[0] for row in query.all()] return locs except Exception: - return [] \ No newline at end of file + return []