fix(stocktake): 盘库扫码改为精确匹配,修复漏匹配与性能问题
问题:
1. 准确性 bug: 前端用 getStockList({pageSize:10, keyword}) 模糊搜索 + find()
当 SKU 前缀相同、目标不在前10条时,误报'未找到该物料库存'
2. 性能: 每次扫码全表 ilike %x% 搜索 3 张表,不走索引
修复:
- 后端 get_stock_info 改为精确匹配(==)优先,未命中再回退模糊搜索
- 新增 GET /inbound/stock/scan 扫码精确匹配接口,返回唯一命中
- 前端 onScanSuccess/handleManualInput 改用 scanStockByBarcode
一次请求直接命中,不再依赖 pageSize:10 + find()
This commit is contained in:
@ -90,54 +90,70 @@ def get_stock_record(source_table, stock_id, for_update=False):
|
|||||||
|
|
||||||
def get_stock_info(uuid_or_barcode):
|
def get_stock_info(uuid_or_barcode):
|
||||||
"""
|
"""
|
||||||
根据 uuid 或 barcode 查询库存信息
|
根据 uuid 或 barcode 查询库存信息(★ 精确匹配优先,性能与准确性兼顾)
|
||||||
返回: (item, source_table, stock_id)
|
|
||||||
|
修复: 原来用 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:
|
exact_checks = [
|
||||||
print(f"🔍 [QUERY DEBUG] 正在成品表搜关键词: {uuid_or_barcode}")
|
(StockProduct, lambda c: db.or_(
|
||||||
item = StockProduct.query.filter(
|
StockProduct.barcode == c,
|
||||||
db.or_(
|
StockProduct.sku == c,
|
||||||
StockProduct.barcode.ilike(f"%{uuid_or_barcode}%"),
|
StockProduct.serial_number == c
|
||||||
StockProduct.sku.ilike(f"%{uuid_or_barcode}%"),
|
), 'stock_product'),
|
||||||
StockProduct.serial_number.ilike(f"%{uuid_or_barcode}%")
|
(StockSemi, lambda c: db.or_(
|
||||||
)
|
StockSemi.barcode == c,
|
||||||
).first()
|
StockSemi.sku == c,
|
||||||
if item:
|
StockSemi.serial_number == c
|
||||||
print(f"✅ [QUERY DEBUG] 命中成品! ID={item.id}, SKU={item.sku}")
|
), 'stock_semi'),
|
||||||
return (item, 'stock_product', item.id)
|
(StockBuy, lambda c: db.or_(
|
||||||
else:
|
StockBuy.barcode == c,
|
||||||
print(f"❌ [QUERY DEBUG] 成品表查询结束,无匹配项")
|
StockBuy.sku == c
|
||||||
|
), 'stock_buy'),
|
||||||
|
]
|
||||||
|
|
||||||
# 2. 半成品
|
for model, cond_fn, table_name in exact_checks:
|
||||||
if StockSemi:
|
if not model:
|
||||||
print(f"🔍 [QUERY DEBUG] 正在半成品表搜关键词: {uuid_or_barcode}")
|
continue
|
||||||
item = StockSemi.query.filter(
|
item = model.query.filter(cond_fn(code)).first()
|
||||||
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()
|
|
||||||
if item:
|
if item:
|
||||||
print(f"✅ [QUERY DEBUG] 命中半成品! ID={item.id}, SKU={item.sku}")
|
return (item, table_name, item.id)
|
||||||
return (item, 'stock_semi', item.id)
|
|
||||||
|
|
||||||
# 3. 采购件
|
# ===== 精确未命中 → 回退模糊搜索(保留旧行为兜底) =====
|
||||||
if StockBuy:
|
fuzzy_checks = [
|
||||||
print(f"🔍 [QUERY DEBUG] 正在采购件表搜关键词: {uuid_or_barcode}")
|
(StockProduct, lambda c: db.or_(
|
||||||
item = StockBuy.query.filter(
|
StockProduct.barcode.ilike(f"%{c}%"),
|
||||||
db.or_(
|
StockProduct.sku.ilike(f"%{c}%"),
|
||||||
StockBuy.barcode.ilike(f"%{uuid_or_barcode}%"),
|
StockProduct.serial_number.ilike(f"%{c}%")
|
||||||
StockBuy.sku.ilike(f"%{uuid_or_barcode}%")
|
), 'stock_product'),
|
||||||
)
|
(StockSemi, lambda c: db.or_(
|
||||||
).first()
|
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:
|
if item:
|
||||||
print(f"✅ [QUERY DEBUG] 命中采购件! ID={item.id}, SKU={item.sku}")
|
return (item, table_name, item.id)
|
||||||
return (item, 'stock_buy', item.id)
|
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
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')
|
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'])
|
@bp.route('/draft/list', methods=['GET'])
|
||||||
|
|||||||
@ -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
|
// 修改后: 去掉开头的 /api
|
||||||
export function printSelectionList(items: any[]) {
|
export function printSelectionList(items: any[]) {
|
||||||
|
|||||||
@ -413,7 +413,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { getStockList, getAllStocktakeItems, getDraftMergedList, updateStocktakeQuantity } from '@/api/inbound/stock'
|
import { getAllStocktakeItems, getDraftMergedList, updateStocktakeQuantity, scanStockByBarcode } from '@/api/inbound/stock'
|
||||||
import QrScanner from '@/components/QrScanner/index.vue'
|
import QrScanner from '@/components/QrScanner/index.vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close, WarningFilled } from '@element-plus/icons-vue'
|
import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close, WarningFilled } from '@element-plus/icons-vue'
|
||||||
@ -736,58 +736,51 @@ const onScanSuccess = async (code: string) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 实时查询后端匹配
|
// ★ 精确匹配查询后端(替代 pageSize:10 模糊搜索 + find(),避免漏匹配)
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await getStockList({
|
const res: any = await scanStockByBarcode(trimCode)
|
||||||
page: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
keyword: trimCode
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res || !res.data || !res.data.list || res.data.list.length === 0) {
|
if (!res || !res.data) {
|
||||||
ElMessage.error(`未找到该物料库存: ${trimCode}`)
|
|
||||||
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找匹配的物料
|
|
||||||
const foundItem = res.data.list.find((i: any) =>
|
|
||||||
i.uuid === trimCode || i.sku === trimCode || i.barcode === trimCode || i.bar_code === trimCode
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!foundItem) {
|
|
||||||
ElMessage.error(`未找到该物料库存: ${trimCode}`)
|
ElMessage.error(`未找到该物料库存: ${trimCode}`)
|
||||||
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const foundItem = res.data
|
||||||
if (navigator.vibrate) navigator.vibrate(100)
|
if (navigator.vibrate) navigator.vibrate(100)
|
||||||
|
|
||||||
// 关闭全屏扫码,弹出填数对话框
|
// 关闭全屏扫码,弹出填数对话框
|
||||||
showCamera.value = false
|
showCamera.value = false
|
||||||
|
|
||||||
// 处理数据格式
|
// 处理数据格式(后端已返回 source_table / stock_id / name / standard)
|
||||||
const stock = parseFloat(foundItem.stock_quantity || foundItem.qty_stock || 0)
|
const stock = parseFloat(foundItem.stock_quantity || foundItem.qty_stock || 0)
|
||||||
const type = foundItem.stock_type || foundItem.type || 'material'
|
const type = foundItem.stock_type || foundItem.type || 'material'
|
||||||
|
const sourceTable = foundItem.source_table || typeToSourceTable(type)
|
||||||
|
const stockId = foundItem.stock_id || foundItem.id
|
||||||
const item: StockItem = {
|
const item: StockItem = {
|
||||||
...foundItem,
|
...foundItem,
|
||||||
name: foundItem.name || foundItem.material_name || foundItem.product_name || '未知物品',
|
name: foundItem.name || foundItem.material_name || foundItem.product_name || '未知物品',
|
||||||
standard: foundItem.spec_model || foundItem.standard || foundItem.model || '',
|
standard: foundItem.standard || foundItem.spec_model || foundItem.model || '',
|
||||||
sku: foundItem.sku || '',
|
sku: foundItem.sku || '',
|
||||||
uuid: foundItem.uuid || foundItem.sku || '',
|
uuid: foundItem.uuid || foundItem.sku || '',
|
||||||
bar_code: foundItem.bar_code || foundItem.barcode || '',
|
bar_code: foundItem.bar_code || foundItem.barcode || '',
|
||||||
qty_stock: stock,
|
qty_stock: stock,
|
||||||
qty_actual: 1,
|
qty_actual: 1,
|
||||||
scanned: true,
|
scanned: true,
|
||||||
uniqueKey: `${type}_${foundItem.id}`,
|
uniqueKey: `${type}_${stockId}`,
|
||||||
source_table: typeToSourceTable(type),
|
source_table: sourceTable,
|
||||||
stock_id: foundItem.id
|
stock_id: stockId
|
||||||
}
|
}
|
||||||
|
|
||||||
openQtyDialog(item)
|
openQtyDialog(item)
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
ElMessage.error('查询库存失败')
|
const msg = e?.msg || e?.message || '查询库存失败'
|
||||||
|
if (msg.includes('未找到')) {
|
||||||
|
ElMessage.error(`未找到该物料库存: ${trimCode}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(msg)
|
||||||
|
}
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@ -820,48 +813,44 @@ const handleManualInput = async () => {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await getStockList({
|
// ★ 精确匹配查询后端(替代 pageSize:10 模糊搜索 + find())
|
||||||
page: 1,
|
const res: any = await scanStockByBarcode(code)
|
||||||
pageSize: 10,
|
|
||||||
keyword: code
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res || !res.data || !res.data.list || res.data.list.length === 0) {
|
if (!res || !res.data) {
|
||||||
ElMessage.error(`未找到该物料库存: ${code}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundItem = res.data.list.find((i: any) =>
|
|
||||||
i.uuid === code || i.sku === code || i.barcode === code || i.bar_code === code
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!foundItem) {
|
|
||||||
ElMessage.error(`未找到该物料库存: ${code}`)
|
ElMessage.error(`未找到该物料库存: ${code}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const foundItem = res.data
|
||||||
if (navigator.vibrate) navigator.vibrate(100)
|
if (navigator.vibrate) navigator.vibrate(100)
|
||||||
|
|
||||||
const stock = parseFloat(foundItem.stock_quantity || foundItem.qty_stock || 0)
|
const stock = parseFloat(foundItem.stock_quantity || foundItem.qty_stock || 0)
|
||||||
const type = foundItem.stock_type || foundItem.type || 'material'
|
const type = foundItem.stock_type || foundItem.type || 'material'
|
||||||
|
const sourceTable = foundItem.source_table || typeToSourceTable(type)
|
||||||
|
const stockId = foundItem.stock_id || foundItem.id
|
||||||
const item: StockItem = {
|
const item: StockItem = {
|
||||||
...foundItem,
|
...foundItem,
|
||||||
name: foundItem.name || foundItem.material_name || foundItem.product_name || '未知物品',
|
name: foundItem.name || foundItem.material_name || foundItem.product_name || '未知物品',
|
||||||
standard: foundItem.spec_model || foundItem.standard || foundItem.model || '',
|
standard: foundItem.standard || foundItem.spec_model || foundItem.model || '',
|
||||||
sku: foundItem.sku || '',
|
sku: foundItem.sku || '',
|
||||||
uuid: foundItem.uuid || foundItem.sku || '',
|
uuid: foundItem.uuid || foundItem.sku || '',
|
||||||
bar_code: foundItem.bar_code || foundItem.barcode || '',
|
bar_code: foundItem.bar_code || foundItem.barcode || '',
|
||||||
qty_stock: stock,
|
qty_stock: stock,
|
||||||
qty_actual: 1,
|
qty_actual: 1,
|
||||||
scanned: true,
|
scanned: true,
|
||||||
uniqueKey: `${type}_${foundItem.id}`,
|
uniqueKey: `${type}_${stockId}`,
|
||||||
source_table: typeToSourceTable(type),
|
source_table: sourceTable,
|
||||||
stock_id: foundItem.id
|
stock_id: stockId
|
||||||
}
|
}
|
||||||
|
|
||||||
openQtyDialog(item)
|
openQtyDialog(item)
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
ElMessage.error('查询库存失败')
|
const msg = e?.msg || e?.message || '查询库存失败'
|
||||||
|
if (msg.includes('未找到')) {
|
||||||
|
ElMessage.error(`未找到该物料库存: ${code}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(msg)
|
||||||
|
}
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|||||||
Reference in New Issue
Block a user