diff --git a/inventory-backend/app/services/inbound/base_service.py b/inventory-backend/app/services/inbound/base_service.py
index 7fe2ea1..c021777 100644
--- a/inventory-backend/app/services/inbound/base_service.py
+++ b/inventory-backend/app/services/inbound/base_service.py
@@ -114,6 +114,10 @@ class MaterialBaseService:
"""
获取基础信息列表 (带分页、高级筛选和全字段排序)
支持库存预警功能(如果用户有 view_warning 权限)
+
+ 修复说明:
+ 1. 使用子查询方案确保聚合列(total_inv)能正确参与预警排序计算
+ 2. 高级筛选中针对聚合字段使用 HAVING 而非 WHERE
"""
try:
# 检查用户是否有查看预警的权限
@@ -144,7 +148,7 @@ class MaterialBaseService:
func.sum(StockProduct.available_quantity).label('prod_avail')
).group_by(StockProduct.base_id).subquery()
- # 总库存和可用数的 SQL 表达式
+ # 总库存和可用数的 SQL 表达式(用于后续计算)
total_inv = func.coalesce(buy_sub.c.buy_inv, 0) + \
func.coalesce(semi_sub.c.semi_inv, 0) + \
func.coalesce(prod_sub.c.prod_inv, 0)
@@ -155,155 +159,180 @@ class MaterialBaseService:
# 导入预警设置模型
from app.models.base import MaterialWarningSetting
- # 主查询,关联聚合子查询和预警设置
- query = db.session.query(
+ # ============================================================
+ # 【核心修复】使用子查询方案:将聚合结果作为子查询
+ # 这样 total_inv 在外层查询中是一个普通列,可安全参与计算
+ # ============================================================
+
+ # 内层子查询:基础数据 + 聚合库存
+ inner_sub = db.session.query(
+ MaterialBase.id.label('base_id'),
MaterialBase,
total_inv.label('total_inv'),
- total_avail.label('total_avail'),
- MaterialWarningSetting.is_enabled.label('warning_enabled'),
- MaterialWarningSetting.yellow_threshold.label('warning_yellow'),
- MaterialWarningSetting.red_threshold.label('warning_red')
+ total_avail.label('total_avail')
).outerjoin(buy_sub, MaterialBase.id == buy_sub.c.base_id) \
.outerjoin(semi_sub, MaterialBase.id == semi_sub.c.base_id) \
- .outerjoin(prod_sub, MaterialBase.id == prod_sub.c.base_id) \
- .outerjoin(MaterialWarningSetting, MaterialBase.id == MaterialWarningSetting.base_id)
-
+ .outerjoin(prod_sub, MaterialBase.id == prod_sub.c.base_id)
+
+ # 关键词精确搜索(需要在子查询层完成)
if filters:
- # 1. 关键词精准搜索(支持指定字段)
search_field = filters.get('searchField', 'all')
keyword = filters.get('keyword')
if keyword:
kw = f"%{keyword}%"
if search_field == 'name':
- query = query.filter(MaterialBase.name.ilike(kw))
+ inner_sub = inner_sub.filter(MaterialBase.name.ilike(kw))
elif search_field == 'common_name':
- query = query.filter(MaterialBase.common_name.ilike(kw))
+ inner_sub = inner_sub.filter(MaterialBase.common_name.ilike(kw))
elif search_field == 'spec':
- query = query.filter(MaterialBase.spec_model.ilike(kw))
- else: # 'all' 默认全局模糊匹配
- query = query.filter(or_(
+ inner_sub = inner_sub.filter(MaterialBase.spec_model.ilike(kw))
+ else:
+ inner_sub = inner_sub.filter(or_(
MaterialBase.name.ilike(kw),
MaterialBase.common_name.ilike(kw),
MaterialBase.spec_model.ilike(kw)
))
- # 2. 精确筛选
company = filters.get('company')
if company is not None and company != '':
- query = query.filter(MaterialBase.company_name.ilike(company.strip()))
+ inner_sub = inner_sub.filter(MaterialBase.company_name.ilike(company.strip()))
category = filters.get('category')
if category is not None and category != '':
- query = query.filter(MaterialBase.category.ilike(category.strip()))
+ inner_sub = inner_sub.filter(MaterialBase.category.ilike(category.strip()))
type_val = filters.get('type')
if type_val is not None and type_val != '':
- query = query.filter(MaterialBase.material_type.ilike(type_val.strip()))
+ inner_sub = inner_sub.filter(MaterialBase.material_type.ilike(type_val.strip()))
- # 【核心修改】:增强布尔值解析
if filters.get('isEnabled') is not None:
val_str = str(filters['isEnabled']).lower()
is_active = val_str in ['1', 'true', 'yes', 't']
- # 必须使用 filter() 而非 filter_by(),因为 query 是 join 后的复杂查询
- query = query.filter(MaterialBase.is_enabled == is_active)
+ inner_sub = inner_sub.filter(MaterialBase.is_enabled == is_active)
- # 【新增】:库存状态筛选 (has_stock)
+ # 库存状态筛选 (has_stock) - 使用子查询列
has_stock = filters.get('has_stock')
if has_stock and str(has_stock).lower() in ['true', '1', 'yes']:
- query = query.filter(total_inv > 0)
+ inner_sub = inner_sub.filter(total_inv > 0)
- # 3. 高级动态筛选
- advanced_filters = filters.get('advancedFilters', [])
- if advanced_filters:
- allowed_fields = {
- 'companyName': 'company_name',
- 'name': 'name',
- 'commonName': 'common_name',
- 'category': 'category',
- 'type': 'material_type',
- 'spec': 'spec_model',
- 'unit': 'unit',
- 'inventoryCount': total_inv,
- 'availableCount': total_avail
- }
- # 字段到权限码的映射
- field_permission_map = {
- 'companyName': 'material_list:companyName',
- 'name': 'material_list:name',
- 'commonName': 'material_list:commonName',
- 'category': 'material_list:category',
- 'type': 'material_list:type',
- 'spec': 'material_list:spec',
- 'unit': 'material_list:unit',
- 'inventoryCount': 'material_list:inventoryCount',
- 'availableCount': 'material_list:availableCount'
- }
- filter_conditions = []
- for condition in advanced_filters:
- field = condition.get('field')
- operator = condition.get('operator')
- value = condition.get('value')
- if not field or not operator or value is None:
+ # 将内层子查询具体化
+ inner_sub = inner_sub.subquery()
+
+ # 外层查询:关联预警设置,并在外层处理排序和高级筛选
+ query = db.session.query(
+ MaterialBase,
+ inner_sub.c.total_inv,
+ inner_sub.c.total_avail,
+ MaterialWarningSetting.is_enabled.label('warning_enabled'),
+ MaterialWarningSetting.yellow_threshold.label('warning_yellow'),
+ MaterialWarningSetting.red_threshold.label('warning_red')
+ ).outerjoin(inner_sub, MaterialBase.id == inner_sub.c.base_id) \
+ .outerjoin(MaterialWarningSetting, MaterialBase.id == MaterialWarningSetting.base_id)
+
+ # ============================================================
+ # 【修复3】高级筛选:聚合字段使用 HAVING,非聚合字段使用 WHERE
+ # ============================================================
+ advanced_filters = filters.get('advancedFilters', []) if filters else []
+ having_conditions = []
+ filter_conditions = []
+
+ if advanced_filters:
+ allowed_fields = {
+ 'companyName': 'company_name',
+ 'name': 'name',
+ 'commonName': 'common_name',
+ 'category': 'category',
+ 'type': 'material_type',
+ 'spec': 'spec_model',
+ 'unit': 'unit',
+ 'inventoryCount': 'total_inv',
+ 'availableCount': 'total_avail'
+ }
+ # 聚合字段列表(这些需要用 HAVING)
+ aggregate_fields = {'inventoryCount', 'availableCount'}
+
+ field_permission_map = {
+ 'companyName': 'material_list:companyName',
+ 'name': 'material_list:name',
+ 'commonName': 'material_list:commonName',
+ 'category': 'material_list:category',
+ 'type': 'material_list:type',
+ 'spec': 'material_list:spec',
+ 'unit': 'material_list:unit',
+ 'inventoryCount': 'material_list:inventoryCount',
+ 'availableCount': 'material_list:availableCount'
+ }
+
+ for condition in advanced_filters:
+ field = condition.get('field')
+ operator = condition.get('operator')
+ value = condition.get('value')
+ if not field or not operator or value is None:
+ continue
+
+ db_field = allowed_fields.get(field)
+ if not db_field:
+ continue
+
+ # 权限校验
+ if user_permissions is not None:
+ perm_code = field_permission_map.get(field)
+ if 'material_list:*' in user_permissions:
+ pass
+ elif perm_code and perm_code not in user_permissions:
continue
- db_field = allowed_fields.get(field)
- if not db_field:
+
+ # 判断是否为聚合字段
+ is_aggregate = field in aggregate_fields
+
+ if is_aggregate:
+ # 聚合字段:使用 HAVING - 通过子查询列进行比较
+ col = inner_sub.c.total_inv if field == 'inventoryCount' else inner_sub.c.total_avail
+ try:
+ num_val = float(value)
+ except ValueError:
continue
- # 权限校验
- if user_permissions is not None:
- perm_code = field_permission_map.get(field)
- if 'material_list:*' in user_permissions:
- # 超级管理员拥有全部权限
- pass
- elif perm_code and perm_code not in user_permissions:
- # 无权限,跳过该条件
- continue
- # 对于聚合字段 (inventoryCount, availableCount),需要使用子查询别名
- if isinstance(db_field, type(total_inv)):
- column = db_field
- else:
- column = getattr(MaterialBase, db_field, None)
+
+ if operator == 'eq':
+ having_conditions.append(col == num_val)
+ elif operator == 'ne':
+ having_conditions.append(col != num_val)
+ elif operator == 'ge':
+ having_conditions.append(col >= num_val)
+ elif operator == 'le':
+ having_conditions.append(col <= num_val)
+ else:
+ # 非聚合字段:使用 WHERE
+ column = getattr(MaterialBase, db_field, None)
if column is None:
continue
- # 处理操作符
- # 对于数值型列(聚合字段)只支持 eq, ne, ge, le
- if isinstance(column, type(total_inv)):
- # 数值型列
+
+ if operator == 'eq':
+ filter_conditions.append(column == value)
+ elif operator == 'ne':
+ filter_conditions.append(column != value)
+ elif operator == 'contains':
+ filter_conditions.append(column.ilike(f'%{value}%'))
+ elif operator == 'ge':
try:
num_val = float(value)
- except ValueError:
- # 转换失败则跳过该条件
- continue
- if operator == 'eq':
- filter_conditions.append(column == num_val)
- elif operator == 'ne':
- filter_conditions.append(column != num_val)
- elif operator == 'ge':
filter_conditions.append(column >= num_val)
- elif operator == 'le':
+ except ValueError:
+ continue
+ elif operator == 'le':
+ try:
+ num_val = float(value)
filter_conditions.append(column <= num_val)
- # 对于 contains 操作符,数值型列不支持,忽略
- else:
- # 字符串型列
- if operator == 'eq':
- filter_conditions.append(column == value)
- elif operator == 'ne':
- filter_conditions.append(column != value)
- elif operator == 'contains':
- filter_conditions.append(column.ilike(f'%{value}%'))
- elif operator == 'ge':
- try:
- num_val = float(value)
- filter_conditions.append(column >= num_val)
- except ValueError:
- continue
- elif operator == 'le':
- try:
- num_val = float(value)
- filter_conditions.append(column <= num_val)
- except ValueError:
- continue
- if filter_conditions:
- query = query.filter(and_(*filter_conditions))
+ except ValueError:
+ continue
+
+ # 应用 WHERE 条件
+ if filter_conditions:
+ query = query.filter(and_(*filter_conditions))
+
+ # 应用 HAVING 条件(聚合字段筛选)
+ if having_conditions:
+ query = query.having(and_(*having_conditions))
# 排序处理(支持全字段)
order_by_column = filters.get('orderByColumn', '')
@@ -314,25 +343,31 @@ class MaterialBaseService:
if enable_warning_sort:
print("====== [DEBUG] 成功进入预警强排逻辑 (SQLA 2.0) ======")
- # 强制统一数据类型
+ # 【核心修复】使用子查询列进行预警排序计算
+ # total_inv 现在是 inner_sub.c.total_inv,可以安全参与 case 计算
+ inv_val = inner_sub.c.total_inv
red_val = cast(MaterialWarningSetting.red_threshold, Numeric)
yellow_val = cast(MaterialWarningSetting.yellow_threshold, Numeric)
- inv_val = cast(total_inv, Numeric)
-
- # 注意:移除了 case 内部的 []
+
+ # 预警等级计算:红=2, 黄=1, 正常=0
warning_level = case(
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= red_val), 2),
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= yellow_val), 1),
else_=0
).label('sort_level')
+
+ # 红色预警时的缺口(库存与红色阈值的差距)
red_shortage = case(
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val <= red_val), red_val - inv_val),
else_=0
).label('sort_red')
+
+ # 黄色预警时的缺口
yellow_distance = case(
(and_(MaterialWarningSetting.is_enabled.is_(True), inv_val > red_val, inv_val <= yellow_val), inv_val - red_val),
else_=999999
).label('sort_yellow')
+
query = query.add_columns(warning_level, red_shortage, yellow_distance)
query = query.order_by(None).order_by(
text("sort_level DESC"),
@@ -341,7 +376,7 @@ class MaterialBaseService:
MaterialBase.id.desc()
)
elif order_by_column:
- # 字段映射
+ # 字段映射 - 使用子查询列进行排序
sort_field_map = {
'companyName': MaterialBase.company_name,
'name': MaterialBase.name,
@@ -350,8 +385,8 @@ class MaterialBaseService:
'type': MaterialBase.material_type,
'spec': MaterialBase.spec_model,
'unit': MaterialBase.unit,
- 'inventoryCount': total_inv,
- 'availableCount': total_avail
+ 'inventoryCount': inner_sub.c.total_inv,
+ 'availableCount': inner_sub.c.total_avail
}
sort_column = sort_field_map.get(order_by_column)
if sort_column is not None:
@@ -361,7 +396,7 @@ class MaterialBaseService:
query = query.order_by(sort_column.desc())
else:
# 默认排序:优先按总库存数降序,当库存相同时,再按规格型号升序
- query = query.order_by(total_inv.desc(), MaterialBase.spec_model.asc())
+ query = query.order_by(inner_sub.c.total_inv.desc(), MaterialBase.spec_model.asc())
# 分页
pagination = query.paginate(page=page, per_page=limit, error_out=False)
diff --git a/inventory-web/src/views/material/list.vue b/inventory-web/src/views/material/list.vue
index 0de5fd1..9027b8a 100644
--- a/inventory-web/src/views/material/list.vue
+++ b/inventory-web/src/views/material/list.vue
@@ -881,8 +881,8 @@ const querySearchType = (queryString: string, cb: any) => {
const getList = () => {
loading.value = true;
- // 强制注入预警排序开关(基于权限)
- queryParams.enableWarningSort = userStore.hasPermission('material_list:view_warning');
+ // 仅当用户没有进行手动表头排序时才开启预警排序
+ queryParams.enableWarningSort = userStore.hasPermission('material_list:view_warning') && !queryParams.orderByColumn;
// Stringify advancedFilters to JSON string as backend expects
const params = {
...queryParams,
diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue
index a8a5e32..27517c5 100644
--- a/inventory-web/src/views/stock/inbound/buy.vue
+++ b/inventory-web/src/views/stock/inbound/buy.vue
@@ -376,7 +376,15 @@
-
+
+
+
+ 序列号
+
+ 智能扫码
+
+
+
SN
@@ -646,6 +654,9 @@
+
+
+
@@ -672,6 +683,7 @@ import {getLabelPreview, executePrint} from '@/api/common/print'
import { getWarehouseTree } from '@/api/common/warehouse'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import WarehouseSelector from '@/components/WarehouseSelector.vue'
+import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
import { useUserStore } from '@/stores/user'
// ------------------------------------
@@ -813,6 +825,9 @@ const cameraRef = ref | null>(null)
const currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo')
const inspection_report_url = ref('')
+// 智能扫码弹窗
+const scannerDialogVisible = ref(false)
+
// 库位级联选择器数据
const warehouseOptions = ref([])
@@ -1488,6 +1503,17 @@ const handleCameraConfirm = async (file: File) => {
}
}
+// 智能扫码
+const openScanner = () => {
+ scannerDialogVisible.value = true
+}
+
+const handleScannerConfirm = (result: string) => {
+ form.serial_number = result
+ scannerDialogVisible.value = false
+ ElMessage.success('序列号已提取')
+}
+
const addCondition = () => {
advancedConditions.value.push({ field: '', operator: '', value: '' })
}
diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue
index 5c567c8..b9aeb3b 100644
--- a/inventory-web/src/views/stock/inbound/product.vue
+++ b/inventory-web/src/views/stock/inbound/product.vue
@@ -320,7 +320,15 @@
-
+
+
+
+ 序列号(SN)
+
+ 智能扫码
+
+
+
SN
@@ -518,6 +526,8 @@
+
+
@@ -540,6 +550,7 @@ import {
import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import WarehouseSelector from '@/components/WarehouseSelector.vue'
+import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
import { getLabelPreview, executePrint } from '@/api/common/print'
import { getWarehouseTree } from '@/api/common/warehouse'
import { useUserStore } from '@/stores/user'
@@ -668,6 +679,9 @@ const currentCameraField = ref<'product_photo' | 'quality_report_link' | 'inspec
const quality_url = ref('')
const inspection_url = ref('')
+// 智能扫码弹窗
+const scannerDialogVisible = ref(false)
+
// 库位级联选择器数据
const warehouseOptions = ref([])
@@ -1187,6 +1201,17 @@ const handleCameraConfirm = async (file: File) => {
}
};
+// 智能扫码
+const openScanner = () => {
+ scannerDialogVisible.value = true
+}
+
+const handleScannerConfirm = (result: string) => {
+ form.serial_number = result
+ scannerDialogVisible.value = false
+ ElMessage.success('序列号已提取')
+}
+
const submitForm = async () => {
await formRef.value.validate(async (valid: boolean) => {
if(valid) {
diff --git a/inventory-web/src/views/stock/inbound/semi.vue b/inventory-web/src/views/stock/inbound/semi.vue
index 73d884b..724a8d3 100644
--- a/inventory-web/src/views/stock/inbound/semi.vue
+++ b/inventory-web/src/views/stock/inbound/semi.vue
@@ -395,7 +395,15 @@
-
+
+
+
+ 序列号
+
+ 智能扫码
+
+
+
SN
@@ -582,6 +590,9 @@
+
+
+
@@ -604,6 +615,7 @@ import {
import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import WarehouseSelector from '@/components/WarehouseSelector.vue'
+import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
import {getLabelPreview, executePrint} from '@/api/common/print'
import { getWarehouseTree } from '@/api/common/warehouse'
import { useUserStore } from '@/stores/user'
@@ -731,6 +743,9 @@ const cameraRef = ref | null>(null)
const currentCameraField = ref<'arrival_photo' | 'quality_report_link'>('arrival_photo')
const quality_report_url = ref('')
+// 智能扫码弹窗
+const scannerDialogVisible = ref(false)
+
// 库位级联选择器数据
const warehouseOptions = ref([])
@@ -1309,6 +1324,17 @@ const handleCameraConfirm = async (file: File) => {
}
};
+// 智能扫码
+const openScanner = () => {
+ scannerDialogVisible.value = true
+}
+
+const handleScannerConfirm = (result: string) => {
+ form.serial_number = result
+ scannerDialogVisible.value = false
+ ElMessage.success('序列号已提取')
+}
+
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {