From 8c635d6afeba4cbe99eb60c0b46eae57dcebca96 Mon Sep 17 00:00:00 2001 From: dxc Date: Fri, 22 May 2026 10:59:39 +0800 Subject: [PATCH] =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=98=E6=9B=B4V3.31?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AF=86=E5=9B=BE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/v1/common/image_search.py | 106 ++++++++++-------- .../app/services/inbound/base_service.py | 88 ++++----------- inventory-backend/requirements.txt | 4 +- inventory-backend/scripts/init_all_vectors.py | 19 +++- inventory-web/src/App.vue | 2 +- .../src/components/ImageSearchDialog.vue | 58 +++++++--- inventory-web/src/views/material/list.vue | 19 +++- msg.txt | 1 - 8 files changed, 154 insertions(+), 143 deletions(-) delete mode 100644 msg.txt diff --git a/inventory-backend/app/api/v1/common/image_search.py b/inventory-backend/app/api/v1/common/image_search.py index d1a33ae..7204bf9 100644 --- a/inventory-backend/app/api/v1/common/image_search.py +++ b/inventory-backend/app/api/v1/common/image_search.py @@ -80,73 +80,81 @@ def image_search(): SELECT id, name, spec_model, image_url, (1 - (vec <=> :query_vector)) AS similarity FROM ( - SELECT id, - COALESCE(name, '') AS name, - COALESCE(spec, '') AS spec_model, - COALESCE(product_image, '') AS image_url, - img_embedding AS vec + -- 1. 基础物料表 + SELECT id, name, spec_model, product_image AS image_url, img_embedding AS vec FROM material_base WHERE img_embedding IS NOT NULL UNION ALL - SELECT id, - '采购入库' AS name, - '到货照片' AS spec_model, - COALESCE(arrival_photo, '') AS image_url, - arrival_image_embedding AS vec - FROM stock_buy - WHERE arrival_image_embedding IS NOT NULL + -- 2. 采购入库表 (通过 base_id 关联拿真实物料) + SELECT mb.id, mb.name, mb.spec_model, sb.arrival_photo AS image_url, sb.arrival_image_embedding AS vec + FROM stock_buy sb + JOIN material_base mb ON sb.base_id = mb.id + WHERE sb.arrival_image_embedding IS NOT NULL UNION ALL - SELECT id, - '采购入库' AS name, - '质检报告' AS spec_model, - COALESCE(qc_report, '') AS image_url, - qc_report_image_embedding AS vec - FROM stock_buy - WHERE qc_report_image_embedding IS NOT NULL + -- 3. 半成品入库表 + SELECT mb.id, mb.name, mb.spec_model, ss.arrival_photo AS image_url, ss.arrival_image_embedding AS vec + FROM stock_semi ss + JOIN material_base mb ON ss.base_id = mb.id + WHERE ss.arrival_image_embedding IS NOT NULL + + UNION ALL + + -- 4. 成品入库表 + SELECT mb.id, mb.name, mb.spec_model, sp.product_photo AS image_url, sp.arrival_image_embedding AS vec + FROM stock_product sp + JOIN material_base mb ON sp.base_id = mb.id + WHERE sp.arrival_image_embedding IS NOT NULL ) AS combined - ORDER BY vec <=> :query_vector - LIMIT 10 + ORDER BY vec <=> :query_vector LIMIT 10 """) - result = db.session.execute(sql, {"query_vector": query_vector_str}) - rows = result.fetchall() + # 执行查询 + records = db.session.execute(sql, {"query_vector": query_vector_str}).fetchall() results = [] - for row in rows: - item_id = row[0] - item_name = row[1] or "" - spec_model = row[2] or "" - raw_image = row[3] + seen_product_ids = set() # 【新增】用来记录已经添加过的物料 ID - # 解析图片 URL 列表,取第一张 - image_url = "" - if raw_image: - try: - image_list = json.loads(raw_image) - if image_list and len(image_list) > 0: - image_url = image_list[0] - except Exception: - # 纯字符串直接使用 - image_url = str(raw_image) + for row in records: + # 【新增】如果这个物料已经在这个列表里了,直接跳过它 + if row.id in seen_product_ids: + continue + # 记录这个物料 ID,保证下次不会再重复添加 + seen_product_ids.add(row.id) + + # 1. 提取原始 URL + raw_url = row.image_url + clean_url = "" + + if raw_url: + if raw_url.startswith('[') and raw_url.endswith(']'): + import json + try: + url_list = json.loads(raw_url) + clean_url = url_list[0] if url_list else "" + except: + clean_url = raw_url + else: + clean_url = raw_url + + # 2. 组装返回结果 results.append({ - "id": item_id, - "name": item_name, - "spec_model": spec_model, - "image_url": image_url, - "similarity": round(float(row[4]), 4) + "product_id": row.id, + "product_name": row.name, + "spec_model": row.spec_model, + "image_url": clean_url, + "similarity": round(float(row.similarity), 4) }) - print(f"✅ [ImageSearch] 跨表检索完成,命中 {len(results)} 条结果") - return jsonify({ - "code": 200, - "msg": "检索成功", - "data": results - }) + # 【新增】只要凑够了 10 个完全不同的物料,就立刻结束循环 + if len(results) >= 10: + break + + return jsonify({"code": 200, "data": results}) except Exception as e: print(f"❌ [ImageSearch] 数据库检索失败: {e}") diff --git a/inventory-backend/app/services/inbound/base_service.py b/inventory-backend/app/services/inbound/base_service.py index a1f9fc6..120c857 100644 --- a/inventory-backend/app/services/inbound/base_service.py +++ b/inventory-backend/app/services/inbound/base_service.py @@ -1048,14 +1048,15 @@ class MaterialBaseService: @staticmethod def get_latest_specs(): """ - 获取所有规格型号的最大连号,按连续区间分组返回 + 获取所有规格型号的分组统计,按规则聚合后返回 - 前缀统一大写处理 - - 只有数字完全连续(N, N+1, N+2...)才认定为同一组 - - 数字不连续时断开,形成新组 - - 按每组数量降序排列 - - 返回每个连续区间的最大值 + - 匹配模式:(前缀)(单数字二级分类位)(纯数字部分),如 OPT12046 -> OPT, 1, 2046 + - OPT 系列:使用 前缀+二级分类位 作为分组 Key,如 OPT1, OPT2 + - 其他前缀:直接使用前缀作为分组 Key + - 返回每个分组的数量、最大号、完整规格名 """ import re + from collections import defaultdict # 1. 查询所有不为空的规格型号 specs = MaterialBase.query.filter( @@ -1063,8 +1064,8 @@ class MaterialBaseService: MaterialBase.spec_model != '' ).all() - # 2. 解析并收集所有有效的 (prefix, num, original_spec) - parsed = [] + # 2. 按分组收集所有数字 + groups = defaultdict(list) for material in specs: spec = material.spec_model @@ -1072,72 +1073,31 @@ class MaterialBaseService: continue base_spec = spec.split('/')[0] - - match = re.match(r'^([A-Za-z]+)(\d+)$', base_spec) + match = re.match(r'^([A-Za-z]+)(\d)(\d+)$', base_spec) if not match: continue - prefix, num_str = match.groups() + prefix, sub_cat, num_str = match.groups() prefix = prefix.upper() num = int(num_str) - parsed.append((prefix, num, spec)) + # OPT 系列使用 前缀+单数字二级分类 作为 Key + key = f"{prefix}{sub_cat}" if prefix == 'OPT' else prefix + groups[key].append((num, spec)) - # 3. 先按 prefix 升序,再按 num 升序排序 - parsed.sort(key=lambda x: (x[0], x[1])) - - # 4. 遍历切分连续区间 - # 核心逻辑:当 current_num != prev_num + 1 时,断开形成新组 - intervals = [] - current_prefix = None - current_start = None - current_end = None - current_last_spec = None - - for prefix, num, spec in parsed: - if current_prefix is None: - current_prefix = prefix - current_start = num - current_end = num - current_last_spec = spec - elif prefix == current_prefix and num == current_end + 1: - current_end = num - current_last_spec = spec - else: - intervals.append({ - 'prefix': current_prefix, - 'start': current_start, - 'end': current_end, - 'count': current_end - current_start + 1, - 'latest': current_last_spec - }) - current_prefix = prefix - current_start = num - current_end = num - current_last_spec = spec - - if current_prefix is not None: - intervals.append({ - 'prefix': current_prefix, - 'start': current_start, - 'end': current_end, - 'count': current_end - current_start + 1, - 'latest': current_last_spec - }) - - # 5. 按每组数量降序排列,再按前缀升序 - intervals.sort(key=lambda x: (-x['count'], x['prefix'])) - - # 6. 构建返回结果 + # 3. 生成展示用的统计数据 result = [] - for item in intervals: - prefix = item['prefix'] - start = item['start'] - end = item['end'] + for key, items in groups.items(): + sorted_items = sorted(items, key=lambda x: x[0]) + max_num, max_spec = sorted_items[-1] result.append({ - "group": f"{prefix}({start}-{end})", - "count": item['count'], - "latest": item['latest'] + 'group': key, + 'count': len(sorted_items), + 'latest': max_spec, + 'max_num': max_num }) + # 4. 按数量降序,再按分组名升序排列 + result.sort(key=lambda x: (-x['count'], x['group'])) + return result \ No newline at end of file diff --git a/inventory-backend/requirements.txt b/inventory-backend/requirements.txt index 5b0821e..7404dee 100644 --- a/inventory-backend/requirements.txt +++ b/inventory-backend/requirements.txt @@ -25,4 +25,6 @@ openpyxl>=3.1.2 # [新增] 定时任务调度器 (库存预警每日邮件) APScheduler==3.10.4 # [新增] 时区处理 (APScheduler 需要) -pytz \ No newline at end of file +pytz +# [新增] 进度条库 (脚本和任务所需) +tqdm>=4.66.0 \ No newline at end of file diff --git a/inventory-backend/scripts/init_all_vectors.py b/inventory-backend/scripts/init_all_vectors.py index 12d1460..3b250de 100644 --- a/inventory-backend/scripts/init_all_vectors.py +++ b/inventory-backend/scripts/init_all_vectors.py @@ -26,15 +26,26 @@ from app.extensions import db from app.utils.ai_vision import get_image_embedding, load_clip_model # ============================================================================ -# 业务配置:表 → 图片字段 → 向量字段 映射 +# 业务配置:表 → 图片字段 → 向量字段 映射 (已全面修复) # ============================================================================ TARGET_TABLES = [ - # 基础物料 + # 1. 基础物料 {"table": "material_base", "img_col": "product_image", "vec_col": "img_embedding"}, - # 采购入库 + # 2. 采购入库 {"table": "stock_buy", "img_col": "arrival_photo", "vec_col": "arrival_image_embedding"}, - {"table": "stock_buy", "img_col": "qc_report", "vec_col": "qc_report_image_embedding"}, + {"table": "stock_buy", "img_col": "inspection_report", "vec_col": "qc_report_image_embedding"}, # 已修复: qc_report -> inspection_report + + # 3. 半成品入库 (新增) + {"table": "stock_semi", "img_col": "arrival_photo", "vec_col": "arrival_image_embedding"}, + {"table": "stock_semi", "img_col": "quality_report_link", "vec_col": "qc_report_image_embedding"}, + + # 4. 成品入库 (新增) + {"table": "stock_product", "img_col": "product_photo", "vec_col": "arrival_image_embedding"}, + {"table": "stock_product", "img_col": "quality_report_link", "vec_col": "qc_report_image_embedding"} + + # 注意:成品入库表还有一个 inspection_report_link,但由于数据库中成品表目前只加了两个向量字段, + # 暂不将该字段加入遍历,以免覆盖 quality_report_link 的特征。 ] # 物理图片根目录(相对于 app 目录的相对路径 ../uploads/) diff --git a/inventory-web/src/App.vue b/inventory-web/src/App.vue index 3f2bb1d..08c8cf9 100644 --- a/inventory-web/src/App.vue +++ b/inventory-web/src/App.vue @@ -239,7 +239,7 @@ const handleLogout = () => { diff --git a/inventory-web/src/components/ImageSearchDialog.vue b/inventory-web/src/components/ImageSearchDialog.vue index 97395b8..f5a62a1 100644 --- a/inventory-web/src/components/ImageSearchDialog.vue +++ b/inventory-web/src/components/ImageSearchDialog.vue @@ -2,7 +2,8 @@
- -
点击或拖拽图片上传
-
支持 jpg/png/gif 等格式
+ +
点击拍照或选择图片
+
支持 jpg/png 格式
- 重新选择 + 重新拍照
@@ -79,12 +80,6 @@
- 使用此物料 - - @@ -104,7 +99,7 @@