-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcore.py
More file actions
285 lines (234 loc) · 8.85 KB
/
Copy pathcore.py
File metadata and controls
285 lines (234 loc) · 8.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
"""
core.py
=======
庫存管理系統 v2 - 核心功能模組
包含:
- Excel 讀寫操作
- 交易記錄
- HMAC 驗證工具
- 快取管理
- 背景 reload 機制
"""
import time
import threading
import hmac
import hashlib
import base64
from urllib.parse import parse_qs
from datetime import datetime
import pandas as pd
from flask import jsonify
from config import (
EXCEL_FILE, LOG_FILE, RELOAD_INTERVAL, HMAC_SECRET
)
# ----------------- 全域快取與鎖 -----------------
dataframe_cache = None
excel_last_mtime = None
data_lock = threading.Lock()
# ============================================================
# Excel 操作
# ============================================================
def load_excel():
"""讀取 Excel 成 DataFrame"""
try:
if not EXCEL_FILE.exists():
print("[load_excel] Excel 檔案不存在", EXCEL_FILE)
return None
df = pd.read_excel(EXCEL_FILE, dtype=str)
return df
except Exception as e:
print("[load_excel] 讀取失敗:", e)
return None
def save_excel(df):
"""儲存 DataFrame 回 Excel"""
try:
df.to_excel(EXCEL_FILE, index=False)
return True
except Exception as e:
print("[save_excel] 儲存失敗:", e)
return False
def get_dataframe_cache():
"""取得 dataframe_cache,供其他模組使用"""
global dataframe_cache
if dataframe_cache is None:
dataframe_cache = load_excel()
return dataframe_cache
def set_dataframe_cache(df):
"""設定 dataframe_cache"""
global dataframe_cache
dataframe_cache = df
def get_excel_last_mtime():
"""取得 excel_last_mtime"""
global excel_last_mtime
return excel_last_mtime
def set_excel_last_mtime(mtime):
"""設定 excel_last_mtime"""
global excel_last_mtime
excel_last_mtime = mtime
def get_data_lock():
"""取得 data_lock"""
return data_lock
# ============================================================
# 交易記錄
# ============================================================
def log_transaction(trans_type, barcode, batch_no, product_name, qty,
from_loc, to_loc, operator, note=""):
"""寫入交易記錄到 LOG_FILE"""
try:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
new_record = {
"時間": timestamp,
"操作類型": trans_type,
"貨品編號": barcode,
"批號": batch_no,
"品名": product_name,
"數量": qty,
"來源位置": from_loc,
"目標位置": to_loc,
"操作人員": operator,
"備註": note
}
if LOG_FILE.exists():
log_df = pd.read_excel(LOG_FILE)
log_df = pd.concat([log_df, pd.DataFrame([new_record])], ignore_index=True)
else:
log_df = pd.DataFrame([new_record])
log_df.to_excel(LOG_FILE, index=False)
print(f"[log_transaction] 記錄成功: {trans_type} - {barcode}")
return True
except Exception as e:
print(f"[log_transaction] 記錄失敗: {e}")
return False
# ============================================================
# HMAC 驗證工具
# ============================================================
def compute_hmac_hex(message: bytes, secret: str) -> str:
"""計算 HMAC-SHA256 並返回十六進位字串"""
mac = hmac.new(secret.encode("utf-8"), message, hashlib.sha256)
return mac.hexdigest()
def parse_payload(raw: str):
"""解析 QR Code payload(支援簽章模式、明碼模式、捲級別模式)
格式支援:
1. 明碼格式: "貨品編號, 貨品名稱, 位置"
2. 簽章格式: Base64 編碼的 HMAC 簽章資料
3. 捲級別格式 (v13): "捲ID|貨品編號|批號|倉庫編號|位置|時間戳|簽章"
"""
s = raw.strip()
# 1. 先嘗試捲級別格式(v13 新增)
# 格式: roll_id|barcode|batch_no|warehouse_id|location|timestamp|signature
if "|" in s:
parts = s.split("|")
if len(parts) >= 5:
roll_id = parts[0].strip()
barcode = parts[1].strip()
batch_no = parts[2].strip()
warehouse_id = parts[3].strip()
location = parts[4].strip()
result = {
"id": barcode, # 主要識別仍用貨品編號
"roll_id": roll_id,
"batch_no": batch_no,
"warehouse_id": warehouse_id,
"location": location,
"mode": "roll"
}
# 如果有簽章欄位,進行驗證
if len(parts) >= 7:
ts = parts[5].strip()
sig = parts[6].strip()
# 驗證簽章
data_str = "|".join(parts[:5])
msg = f"{data_str}|{ts}".encode("utf-8")
expected = compute_hmac_hex(msg, HMAC_SECRET)[:16]
if hmac.compare_digest(expected, sig):
result["ts"] = ts
result["verified"] = True
else:
result["verified"] = False
else:
result["verified"] = False # 無簽章
return True, result
# 2. 嘗試明碼格式(逗號分隔)
if ", " in s:
parts = s.split(", ")
item_id = parts[0].strip()
name = parts[1].strip() if len(parts) > 1 else ""
location = parts[2].strip() if len(parts) > 2 else ""
return True, {"id": item_id, "mode": "plain", "name": name, "location": location}
# 3. 嘗試 HMAC 簽章格式
try:
decoded = base64.urlsafe_b64decode(s + '===')
txt = decoded.decode("utf-8")
qs = parse_qs(txt, keep_blank_values=True)
idv = qs.get("id", [None])[0]
ver = qs.get("v", [None])[0]
ts = qs.get("ts", [None])[0]
sig = qs.get("sig", [None])[0]
if idv and sig:
msg = f"id={idv}&v={ver or ''}&ts={ts or ''}".encode("utf-8")
expected = compute_hmac_hex(msg, HMAC_SECRET)
if hmac.compare_digest(expected, sig):
return True, {"id": idv, "mode": "signed", "v": ver, "ts": ts}
else:
return False, {"error": "invalid signature"}
except Exception:
pass
# 4. 最後嘗試當作純 ID(向後相容)
if s and len(s) <= 128:
return True, {"id": s, "mode": "plain"}
return False, {"error": "cannot parse payload"}
# ============================================================
# 鎖倉檢查輔助函式
# ============================================================
# 注意:check_inventory_lock 會從 stocktaking_api_v13 導入
_check_inventory_lock_func = None
def set_check_inventory_lock_func(func):
"""設定鎖倉檢查函式(由主程式在啟動時設定)"""
global _check_inventory_lock_func
_check_inventory_lock_func = func
def check_lock_and_reject(barcode: str, batch_no: str, location: str):
"""
檢查庫存列是否被盤點鎖定,若鎖定則返回錯誤訊息
返回:(是否鎖定, 錯誤回應或None, HTTP狀態碼或None)
"""
if _check_inventory_lock_func is None:
# 如果沒有設定鎖倉檢查函式,預設不鎖定
return False, None, None
lock_info = _check_inventory_lock_func(barcode, batch_no, location)
if lock_info["is_locked"]:
return True, jsonify({
"status": "error",
"msg": f"此庫存列正在盤點中,無法操作 (Session: {lock_info['session_id']})",
"locked_by_session": lock_info["session_id"]
}), 423 # 423 Locked
return False, None, None
# ============================================================
# 背景 Reload 機制
# ============================================================
def reload_watcher():
"""背景執行緒,定期檢查 Excel 檔案是否有更新"""
global dataframe_cache, excel_last_mtime
while True:
try:
if not EXCEL_FILE.exists():
time.sleep(RELOAD_INTERVAL)
continue
current_mtime = EXCEL_FILE.stat().st_mtime
if excel_last_mtime is None:
df = load_excel()
if df is not None:
with data_lock:
dataframe_cache = df
excel_last_mtime = current_mtime
print("[watcher] 初次載入 Excel 成功")
elif current_mtime != excel_last_mtime:
print("[watcher] 偵測到 Excel 有更新,嘗試重新載入...")
df = load_excel()
if df is not None:
with data_lock:
dataframe_cache = df
excel_last_mtime = current_mtime
print("[watcher] Excel 重新載入成功")
except Exception as e:
print("[watcher] 例外:", e)
time.sleep(RELOAD_INTERVAL)