fix(stocktake): enforce session_id in resume, make missing generation idempotent, update UI to show SN, and fix excel time offset

This commit is contained in:
DXC
2026-04-03 09:42:51 +08:00
parent 43e1d0aa55
commit 0d8f697df4
3 changed files with 28 additions and 9 deletions

View File

@ -804,13 +804,13 @@ def export_stocktake():
return str(user_id) return str(user_id)
def to_beijing_time(dt): def to_beijing_time(dt):
"""转换为北京时间(+8小时""" """直接使用数据库中存储的标准时间(服务器时区已正确"""
if not dt: if not dt:
return '' return ''
try: try:
if isinstance(dt, str): if isinstance(dt, str):
return dt[:19] 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: except:
return str(dt)[:19] return str(dt)[:19]
@ -1130,9 +1130,13 @@ def export_stocktake():
@permission_required('inventory_stocktake:operation') @permission_required('inventory_stocktake:operation')
def generate_missing_stocktake(): def generate_missing_stocktake():
""" """
生成漏盘数据: 生成漏盘数据(幂等性)
找出所有真实库存 > 0但未被当前会话盘点扫描到的物料 找出所有真实库存 > 0但未被当前会话盘点扫描到的物料
自动生成盘点草稿,标记为盘亏(实盘=0差异=-库存数) 自动生成盘点草稿,标记为盘亏(实盘=0差异=-库存数)
幂等性保护:在重新计算差集之前,先删除当前 session 下所有
由系统自动生成的漏盘记录quantity==0, user_id=='system'
保证该接口多次调用结果一致。
""" """
try: try:
# ★ 获取 session_id 参数,用于隔离当前会话 # ★ 获取 session_id 参数,用于隔离当前会话
@ -1142,6 +1146,16 @@ def generate_missing_stocktake():
if not session_id: if not session_id:
return jsonify({'code': 400, 'msg': '缺少 session_id 参数'}), 400 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) 集合 # 1. 获取当前会话已有盘点记录的 (source_table, stock_id) 集合
existing_records = db.session.query( existing_records = db.session.query(
StocktakeDraft.source_table, StocktakeDraft.source_table,
@ -1192,11 +1206,11 @@ def generate_missing_stocktake():
if key not in scanned_keys: if key not in scanned_keys:
# 生成漏盘草稿 # 生成漏盘草稿
draft = StocktakeDraft( draft = StocktakeDraft(
user_id='system', user_id='system', # ★ 标记为系统自动生成,用于幂等性清理
uuid=f'MISSING-{stock["source_table"]}-{stock["stock_id"]}', uuid=f'MISSING-{stock["source_table"]}-{stock["stock_id"]}',
quantity=0, # 实盘数为0 quantity=0, # 实盘数为0
scan_time=beijing_time(), scan_time=beijing_time(),
session_id='AUTO_GENERATED', session_id=session_id, # ★ 使用传入的 session_id
source_table=stock['source_table'], source_table=stock['source_table'],
stock_id=stock['stock_id'], stock_id=stock['stock_id'],
stock_qty=stock['stock_qty'], stock_qty=stock['stock_qty'],

View File

@ -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({ return request({
url: '/v1/inbound/stock/stocktake/all-items', url: '/v1/inbound/stock/stocktake/all-items',
method: 'get', method: 'get',

View File

@ -167,8 +167,8 @@
<span class="value">{{ currentItem.sku }}</span> <span class="value">{{ currentItem.sku }}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="label">批号:</span> <span class="label">序列号(SN):</span>
<span class="value">{{ currentItem.batch_no || '-' }}</span> <span class="value">{{ currentItem.sn || currentItem.batch_no || '-' }}</span>
</div> </div>
</div> </div>
@ -429,6 +429,7 @@ interface StockItem {
standard: string standard: string
sku: string sku: string
batch_no: string batch_no: string
sn?: string // ★ 序列号
serial_number?: string serial_number?: string
uuid: string uuid: string
bar_code: string bar_code: string
@ -484,6 +485,7 @@ const listLoading = ref(false)
const listData = ref<any[]>([]) const listData = ref<any[]>([])
const listStatusFilter = ref<'all' | 'counted' | 'uncounted'>('all') const listStatusFilter = ref<'all' | 'counted' | 'uncounted'>('all')
const allStockItems = ref<any[]>([]) // 全量应盘物资(盘点基数) const allStockItems = ref<any[]>([]) // 全量应盘物资(盘点基数)
const totalStockCount = ref(0) // ★ 全量应盘物资总数不受limit限制
const allScannedDrafts = ref<any[]>([]) // 全量草稿记录(脱离分页和过滤) const allScannedDrafts = ref<any[]>([]) // 全量草稿记录(脱离分页和过滤)
const listTotalFiltered = ref(0) // 过滤后的总数 const listTotalFiltered = ref(0) // 过滤后的总数
@ -493,9 +495,12 @@ const currentSessionId = ref<string>('')
// 获取应盘物资清单(盘点基数) // 获取应盘物资清单(盘点基数)
const fetchAllStockItems = async () => { const fetchAllStockItems = async () => {
try { try {
const res: any = await getAllStocktakeItems() // ★ 必须传递 session_id用于隔离会话
const res: any = await getAllStocktakeItems({ session_id: currentSessionId.value })
if (res && res.code === 200) { if (res && res.code === 200) {
allStockItems.value = res.data.items || [] allStockItems.value = res.data.items || []
// ★ 使用返回的 total 获取真实总数,而不是受限的数组长度
totalStockCount.value = res.data.total || allStockItems.value.length
} }
} catch (e) { } catch (e) {
console.error('获取应盘物资清单失败', e) console.error('获取应盘物资清单失败', e)