diff --git a/inventory-backend/app/api/v1/inbound/stock.py b/inventory-backend/app/api/v1/inbound/stock.py index d1f27f7..fa81a16 100644 --- a/inventory-backend/app/api/v1/inbound/stock.py +++ b/inventory-backend/app/api/v1/inbound/stock.py @@ -90,54 +90,70 @@ def get_stock_record(source_table, stock_id, for_update=False): def get_stock_info(uuid_or_barcode): """ - 根据 uuid 或 barcode 查询库存信息 - 返回: (item, source_table, stock_id) + 根据 uuid 或 barcode 查询库存信息(★ 精确匹配优先,性能与准确性兼顾) + + 修复: 原来用 ilike %x% 全表模糊搜索 + .first(), + 在 SKU 前缀相同的场景会命中错误记录或漏匹配。 + 改为: 精确匹配(==)优先,命中即返回;无精确命中再回退模糊搜索。 + + 返回: (item, source_table, stock_id) 或 (None, None, None) """ # 清洗输入:去掉前后空格和换行符 - uuid_or_barcode = str(uuid_or_barcode).strip() + code = str(uuid_or_barcode).strip() + if not code: + return None, None, None - # 1. 成品 - if StockProduct: - print(f"🔍 [QUERY DEBUG] 正在成品表搜关键词: {uuid_or_barcode}") - item = StockProduct.query.filter( - db.or_( - StockProduct.barcode.ilike(f"%{uuid_or_barcode}%"), - StockProduct.sku.ilike(f"%{uuid_or_barcode}%"), - StockProduct.serial_number.ilike(f"%{uuid_or_barcode}%") - ) - ).first() - if item: - print(f"✅ [QUERY DEBUG] 命中成品! ID={item.id}, SKU={item.sku}") - return (item, 'stock_product', item.id) - else: - print(f"❌ [QUERY DEBUG] 成品表查询结束,无匹配项") + # ===== 精确匹配优先(走索引,快且准) ===== + exact_checks = [ + (StockProduct, lambda c: db.or_( + StockProduct.barcode == c, + StockProduct.sku == c, + StockProduct.serial_number == c + ), 'stock_product'), + (StockSemi, lambda c: db.or_( + StockSemi.barcode == c, + StockSemi.sku == c, + StockSemi.serial_number == c + ), 'stock_semi'), + (StockBuy, lambda c: db.or_( + StockBuy.barcode == c, + StockBuy.sku == c + ), 'stock_buy'), + ] - # 2. 半成品 - if StockSemi: - print(f"🔍 [QUERY DEBUG] 正在半成品表搜关键词: {uuid_or_barcode}") - item = StockSemi.query.filter( - db.or_( - StockSemi.barcode.ilike(f"%{uuid_or_barcode}%"), - StockSemi.sku.ilike(f"%{uuid_or_barcode}%"), - StockSemi.serial_number.ilike(f"%{uuid_or_barcode}%") - ) - ).first() + for model, cond_fn, table_name in exact_checks: + if not model: + continue + item = model.query.filter(cond_fn(code)).first() if item: - print(f"✅ [QUERY DEBUG] 命中半成品! ID={item.id}, SKU={item.sku}") - return (item, 'stock_semi', item.id) + return (item, table_name, item.id) - # 3. 采购件 - if StockBuy: - print(f"🔍 [QUERY DEBUG] 正在采购件表搜关键词: {uuid_or_barcode}") - item = StockBuy.query.filter( - db.or_( - StockBuy.barcode.ilike(f"%{uuid_or_barcode}%"), - StockBuy.sku.ilike(f"%{uuid_or_barcode}%") - ) - ).first() + # ===== 精确未命中 → 回退模糊搜索(保留旧行为兜底) ===== + fuzzy_checks = [ + (StockProduct, lambda c: db.or_( + StockProduct.barcode.ilike(f"%{c}%"), + StockProduct.sku.ilike(f"%{c}%"), + StockProduct.serial_number.ilike(f"%{c}%") + ), 'stock_product'), + (StockSemi, lambda c: db.or_( + StockSemi.barcode.ilike(f"%{c}%"), + StockSemi.sku.ilike(f"%{c}%"), + StockSemi.serial_number.ilike(f"%{c}%") + ), 'stock_semi'), + (StockBuy, lambda c: db.or_( + StockBuy.barcode.ilike(f"%{c}%"), + StockBuy.sku.ilike(f"%{c}%") + ), 'stock_buy'), + ] + + for model, cond_fn, table_name in fuzzy_checks: + if not model: + continue + item = model.query.filter(cond_fn(code)).first() if item: - print(f"✅ [QUERY DEBUG] 命中采购件! ID={item.id}, SKU={item.sku}") - return (item, 'stock_buy', item.id) + return (item, table_name, item.id) + + return None, None, None return (None, None, None) @@ -411,6 +427,42 @@ def get_stock_list(): return _do_get_stock_list(permission_prefix='outbound_selection') +# -------------------------------------------------------- +# 盘库/出库/借库 扫码精确匹配接口 +# GET /api/v1/inbound/stock/scan?barcode=xxx +# 精确匹配优先,替代前端 pageSize:10 模糊搜索 + find() 的漏匹配问题 +# -------------------------------------------------------- +@bp.route('/scan', methods=['GET']) +@jwt_required() +def scan_stock_by_barcode(): + """根据条码精确匹配库存记录(一次返回唯一命中,性能好且准确)""" + try: + barcode = request.args.get('barcode', '').strip() + if not barcode: + return jsonify({'code': 400, 'msg': 'barcode 不能为空'}), 400 + + item, source_table, stock_id = get_stock_info(barcode) + if not item: + return jsonify({'code': 404, 'msg': f'未找到该物料库存: {barcode}'}), 404 + + d = item.to_dict() + d['stock_type'] = source_table.replace('stock_', '') + d['type'] = source_table.replace('stock_', '') + d['source_table'] = source_table + d['stock_id'] = stock_id + # 兼容前端字段 + if hasattr(item, 'base') and item.base: + d['name'] = d.get('material_name') or item.base.name or '' + d['standard'] = d.get('spec_model') or item.base.spec_model or '' + d['stock_quantity'] = float(d.get('stock_quantity') or d.get('qty_stock') or 0) + d['available_quantity'] = float(d.get('available_quantity') or d.get('qty_available') or 0) + + return jsonify({'code': 200, 'msg': 'success', 'data': d}), 200 + except Exception as e: + traceback.print_exc() + return jsonify({'code': 500, 'msg': str(e)}), 500 + + # --- 草稿箱接口 --- @bp.route('/draft/list', methods=['GET']) diff --git a/inventory-web/src/api/inbound/stock.ts b/inventory-web/src/api/inbound/stock.ts index cb7c45e..798bbbd 100644 --- a/inventory-web/src/api/inbound/stock.ts +++ b/inventory-web/src/api/inbound/stock.ts @@ -19,6 +19,15 @@ export function getStockList(params: { page?: number; pageSize?: number; keyword }) } +// 扫码精确匹配库存(盘库/出库/借库通用,替代模糊搜索漏匹配问题) +export function scanStockByBarcode(barcode: string) { + return request({ + url: '/v1/inbound/stock/scan', + method: 'get', + params: { barcode } + }) +} + // 打印出库选单 // 修改后: 去掉开头的 /api export function printSelectionList(items: any[]) { diff --git a/inventory-web/src/views/stock/stocktake/index.vue b/inventory-web/src/views/stock/stocktake/index.vue index aeb1ab2..b97f37e 100644 --- a/inventory-web/src/views/stock/stocktake/index.vue +++ b/inventory-web/src/views/stock/stocktake/index.vue @@ -413,7 +413,7 @@