diff --git a/inventory-backend/app/api/v1/inbound/stock.py b/inventory-backend/app/api/v1/inbound/stock.py index 6a38b06..f8eaee5 100644 --- a/inventory-backend/app/api/v1/inbound/stock.py +++ b/inventory-backend/app/api/v1/inbound/stock.py @@ -804,13 +804,13 @@ def export_stocktake(): return str(user_id) def to_beijing_time(dt): - """转换为北京时间(+8小时)""" + """直接使用数据库中存储的标准时间(服务器时区已正确)""" if not dt: return '' try: if isinstance(dt, str): return dt[:19] - return (dt + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S') + return dt.strftime('%Y-%m-%d %H:%M:%S') except: return str(dt)[:19] @@ -1130,9 +1130,13 @@ def export_stocktake(): @permission_required('inventory_stocktake:operation') def generate_missing_stocktake(): """ - 生成漏盘数据: + 生成漏盘数据(幂等性): 找出所有真实库存 > 0,但未被当前会话盘点扫描到的物料, 自动生成盘点草稿,标记为盘亏(实盘=0,差异=-库存数) + + 幂等性保护:在重新计算差集之前,先删除当前 session 下所有 + 由系统自动生成的漏盘记录(quantity==0, user_id=='system'), + 保证该接口多次调用结果一致。 """ try: # ★ 获取 session_id 参数,用于隔离当前会话 @@ -1142,6 +1146,16 @@ def generate_missing_stocktake(): if not session_id: return jsonify({'code': 400, 'msg': '缺少 session_id 参数'}), 400 + # ★ 幂等性保护:先删除当前 session 下系统自动生成的漏盘记录 + # 特征:user_id == 'system' (表示由系统自动生成) + deleted_count = StocktakeDraft.query.filter( + StocktakeDraft.session_id == session_id, + StocktakeDraft.user_id == 'system' + ).delete() + if deleted_count > 0: + db.session.commit() + print(f"[generate_missing] 已清理 {deleted_count} 条历史漏盘记录") + # 1. 获取当前会话已有盘点记录的 (source_table, stock_id) 集合 existing_records = db.session.query( StocktakeDraft.source_table, @@ -1192,11 +1206,11 @@ def generate_missing_stocktake(): if key not in scanned_keys: # 生成漏盘草稿 draft = StocktakeDraft( - user_id='system', + user_id='system', # ★ 标记为系统自动生成,用于幂等性清理 uuid=f'MISSING-{stock["source_table"]}-{stock["stock_id"]}', quantity=0, # 实盘数为0 scan_time=beijing_time(), - session_id='AUTO_GENERATED', + session_id=session_id, # ★ 使用传入的 session_id source_table=stock['source_table'], stock_id=stock['stock_id'], stock_qty=stock['stock_qty'], diff --git a/inventory-web/src/api/inbound/stock.ts b/inventory-web/src/api/inbound/stock.ts index a28c06f..d116950 100644 --- a/inventory-web/src/api/inbound/stock.ts +++ b/inventory-web/src/api/inbound/stock.ts @@ -74,7 +74,7 @@ export function getBom(parentId: number) { } // 获取应盘物资清单(盘点基数) -export function getAllStocktakeItems(params?: { keyword?: string }) { +export function getAllStocktakeItems(params?: { keyword?: string; session_id?: string }) { return request({ url: '/v1/inbound/stock/stocktake/all-items', method: 'get', diff --git a/inventory-web/src/views/stock/stocktake/index.vue b/inventory-web/src/views/stock/stocktake/index.vue index 8ef1c00..78e3051 100644 --- a/inventory-web/src/views/stock/stocktake/index.vue +++ b/inventory-web/src/views/stock/stocktake/index.vue @@ -167,8 +167,8 @@ {{ currentItem.sku }}
- 批号: - {{ currentItem.batch_no || '-' }} + 序列号(SN): + {{ currentItem.sn || currentItem.batch_no || '-' }}
@@ -429,6 +429,7 @@ interface StockItem { standard: string sku: string batch_no: string + sn?: string // ★ 序列号 serial_number?: string uuid: string bar_code: string @@ -484,6 +485,7 @@ const listLoading = ref(false) const listData = ref([]) const listStatusFilter = ref<'all' | 'counted' | 'uncounted'>('all') const allStockItems = ref([]) // 全量应盘物资(盘点基数) +const totalStockCount = ref(0) // ★ 全量应盘物资总数(不受limit限制) const allScannedDrafts = ref([]) // 全量草稿记录(脱离分页和过滤) const listTotalFiltered = ref(0) // 过滤后的总数 @@ -493,9 +495,12 @@ const currentSessionId = ref('') // 获取应盘物资清单(盘点基数) const fetchAllStockItems = async () => { try { - const res: any = await getAllStocktakeItems() + // ★ 必须传递 session_id,用于隔离会话 + const res: any = await getAllStocktakeItems({ session_id: currentSessionId.value }) if (res && res.code === 200) { allStockItems.value = res.data.items || [] + // ★ 使用返回的 total 获取真实总数,而不是受限的数组长度 + totalStockCount.value = res.data.total || allStockItems.value.length } } catch (e) { console.error('获取应盘物资清单失败', e)