480 lines
21 KiB
Python
480 lines
21 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
DUT模型,处理试验样品(DUT: Device Under Test)信息的新建与编辑
|
||
支持通过全局配置切换直接访问 SQLite 或通过 HTTP API 访问。
|
||
"""
|
||
|
||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QVariant, pyqtSlot
|
||
import sqlite3
|
||
import json
|
||
import os
|
||
import requests
|
||
|
||
# 尝试导入配置
|
||
try:
|
||
from config import USE_HTTP_API, HTTP_API_BASE_URL
|
||
except ImportError:
|
||
USE_HTTP_API = False
|
||
HTTP_API_BASE_URL = "http://127.0.0.1:5050"
|
||
|
||
|
||
class DUTModel(QObject):
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self._duts = []
|
||
self._current_dut = None
|
||
|
||
# HTTP API 配置
|
||
self.use_http_api = USE_HTTP_API
|
||
self.api_base_url = HTTP_API_BASE_URL
|
||
|
||
# 数据库路径
|
||
self.db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dtmgtDb.db')
|
||
|
||
self._load_duts()
|
||
|
||
# 信号定义
|
||
dutsChanged = pyqtSignal()
|
||
currentDUTChanged = pyqtSignal()
|
||
|
||
def _load_duts(self):
|
||
"""从数据库加载DUT数据"""
|
||
try:
|
||
# 尝试使用 HTTP API
|
||
if self.use_http_api:
|
||
try:
|
||
url = f"{self.api_base_url}/dbTableAccess"
|
||
# 尝试添加参数获取所有记录(避免后端分页限制)
|
||
response = requests.get(
|
||
url,
|
||
params={
|
||
"table": "dutList",
|
||
"limit": 1000, # 设置较大的限制值
|
||
"offset": 0
|
||
},
|
||
timeout=5
|
||
)
|
||
response.raise_for_status()
|
||
result = response.json()
|
||
data = result.get("data", [])
|
||
print(f"\n========== DUT 数据加载调试 ==========")
|
||
print(f"从 HTTP API 获取到 {len(data)} 条原始记录")
|
||
|
||
# 检查是否有分页信息
|
||
if 'total' in result:
|
||
print(f"后端数据总数: {result.get('total')} 条")
|
||
if 'limit' in result:
|
||
print(f"后端分页限制: limit={result.get('limit')}, offset={result.get('offset', 0)}")
|
||
|
||
if data is not None:
|
||
self._duts = []
|
||
skipped_count = 0
|
||
|
||
for idx, row in enumerate(data):
|
||
try:
|
||
# 保留原始的 stationAssigned JSON 字符串
|
||
# 安全处理:即使为 None 或空字符串也保留记录
|
||
station_assigned_str = row.get("stationAssigned") or ""
|
||
# 如果是 None,转为空字符串
|
||
if station_assigned_str is None:
|
||
station_assigned_str = ""
|
||
dtm = ""
|
||
station = ""
|
||
|
||
# 解析 JSON 获取 dtMachine 和 station 字段供其他模块使用
|
||
# 只有当 stationAssigned 为非空且以 '{' 开头时才尝试解析
|
||
if station_assigned_str and isinstance(station_assigned_str, str) and station_assigned_str.strip().startswith('{'):
|
||
try:
|
||
obj = json.loads(station_assigned_str)
|
||
dtm = (obj.get('dtMachine') or '').strip()
|
||
station = (obj.get('station') or '').strip()
|
||
except Exception as e:
|
||
print(f" [警告] 记录 {idx+1} 解析 stationAssigned JSON 失败: {e}, 值: {station_assigned_str}")
|
||
|
||
dut = {
|
||
"SN": row.get("SN", ""),
|
||
"name": row.get("name", ""),
|
||
"project": row.get("project", ""),
|
||
"projectPhase": row.get("projectPhase", ""),
|
||
"projectType": row.get("projectType", ""),
|
||
"phase": row.get("phase", ""),
|
||
"weeks": row.get("weeks", 0) if row.get("weeks") is not None else 0,
|
||
"stationAssigned": station_assigned_str, # 保留原始 JSON 字符串(即使为空)
|
||
"dtMachine": dtm, # 解析出的机台编码
|
||
"station": station, # 解析出的通道编号
|
||
"itemOnGoing": row.get("itemOnGoing", ""),
|
||
"itemsFinished": row.get("itemsFinished", 0) if row.get("itemsFinished") is not None else 0,
|
||
"status": row.get("status", ""),
|
||
"testReq": row.get("testReq", ""),
|
||
"inspector": row.get("inspector", ""),
|
||
"id": row.get("id", 0) if row.get("id") is not None else 0,
|
||
"description": row.get("description", ""),
|
||
"deadLine": row.get("deadLine", ""),
|
||
"createdate": row.get("createdate", ""),
|
||
"workOrder": row.get("workOrder", ""),
|
||
"direction_codes": row.get("direction_codes", "")
|
||
}
|
||
self._duts.append(dut)
|
||
|
||
except Exception as e:
|
||
skipped_count += 1
|
||
print(f" [错误] 记录 {idx+1} 处理失败,已跳过: {e}")
|
||
print(f" 原始数据: {row}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
print(f"\n总计: 获取 {len(data)} 条 -> 成功加载 {len(self._duts)} 条 -> 跳过 {skipped_count} 条")
|
||
print(f"========================================\n")
|
||
|
||
self.dutsChanged.emit()
|
||
return
|
||
except Exception as e:
|
||
print(f"HTTP API 加载DUT失败: {e},回退到直接访问 SQLite")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
# 直接访问 SQLite
|
||
if os.path.exists(self.db_path):
|
||
conn = sqlite3.connect(self.db_path)
|
||
cursor = conn.cursor()
|
||
|
||
# 查询 dutList 表中的所有 DUT
|
||
cursor.execute("""
|
||
SELECT SN, name, project,projectPhase, projectType, phase, weeks, stationAssigned,
|
||
itemOnGoing, itemsFinished, status, testReq, inspector, id,
|
||
description, deadLine, createdate, workOrder, direction_codes
|
||
FROM dutList
|
||
""")
|
||
|
||
rows = cursor.fetchall()
|
||
self._duts = []
|
||
|
||
for row in rows:
|
||
# 保留原始的 stationAssigned JSON 字符串
|
||
# 安全处理:即使为 None 或空字符串也保留记录
|
||
station_assigned_str = row[6] if row[6] else ""
|
||
# 如果是 None,转为空字符串
|
||
if station_assigned_str is None:
|
||
station_assigned_str = ""
|
||
dtm = ""
|
||
station = ""
|
||
|
||
# 解析 JSON 获取 dtMachine 和 station 字段供其他模块使用
|
||
# 只有当 stationAssigned 为非空且以 '{' 开头时才尝试解析
|
||
if station_assigned_str and isinstance(station_assigned_str, str) and station_assigned_str.strip().startswith('{'):
|
||
try:
|
||
obj = json.loads(station_assigned_str)
|
||
dtm = (obj.get('dtMachine') or '').strip()
|
||
station = (obj.get('station') or '').strip()
|
||
except Exception as e:
|
||
print(f"解析 stationAssigned JSON 失败: {e}, 值: {station_assigned_str}")
|
||
|
||
dut = {
|
||
"SN": row[0] if row[0] else "",
|
||
"name": row[1] if row[1] else "",
|
||
"project": row[2] if row[2] else "",
|
||
"projectPhase": row[3] if row[3] else "",
|
||
"projectType": row[3] if row[3] else "",
|
||
"phase": row[4] if row[4] else "",
|
||
"weeks": row[5] if row[5] is not None else 0,
|
||
"stationAssigned": station_assigned_str, # 保留原始 JSON 字符串(即使为空)
|
||
"dtMachine": dtm, # 解析出的机台编码
|
||
"station": station, # 解析出的通道编号
|
||
"itemOnGoing": row[7] if row[7] else "",
|
||
"itemsFinished": row[8] if row[8] is not None else 0,
|
||
"status": row[9] if row[9] else "",
|
||
"testReq": row[10] if row[10] else "",
|
||
"inspector": row[11] if row[11] else "",
|
||
"id": row[12] if row[12] is not None else 0,
|
||
"description": row[13] if row[13] else "",
|
||
"deadLine": row[14] if row[14] else "",
|
||
"createdate": row[15] if row[15] else "",
|
||
"workOrder": row[16] if row[16] else "",
|
||
"direction_codes": row[17] if row[17] else ""
|
||
}
|
||
self._duts.append(dut)
|
||
|
||
print(f"加载了 {len(self._duts)} 条 DUT 记录(SQLite)")
|
||
conn.close()
|
||
else:
|
||
# 如果数据库不存在,使用空列表
|
||
self._duts = []
|
||
print("数据库文件不存在")
|
||
|
||
self.dutsChanged.emit()
|
||
|
||
except Exception as e:
|
||
print(f"加载DUT数据时出错: {e}")
|
||
self._duts = []
|
||
|
||
@pyqtProperty('QVariant', notify=dutsChanged)
|
||
def duts(self):
|
||
"""返回所有DUT数据"""
|
||
return self._duts
|
||
|
||
@pyqtProperty('QVariant', notify=currentDUTChanged)
|
||
def currentDUT(self):
|
||
"""返回当前选中的DUT"""
|
||
return self._current_dut
|
||
|
||
@currentDUT.setter
|
||
def setCurrentDUT(self, dut):
|
||
"""设置当前选中的DUT"""
|
||
if self._current_dut != dut:
|
||
self._current_dut = dut
|
||
self.currentDUTChanged.emit()
|
||
|
||
@pyqtSlot(int, result='QVariant')
|
||
def getDUTById(self, dut_id):
|
||
"""根据ID获取DUT信息"""
|
||
for dut in self._duts:
|
||
if dut.get("id") == dut_id:
|
||
return dut
|
||
return None
|
||
|
||
@pyqtSlot('QVariant')
|
||
def addDUT(self, dut):
|
||
"""添加新DUT
|
||
会将 dtMachine 和 station 字段合成为 JSON 字符串存入 stationAssigned
|
||
虚拟字段(dtMachine, station)不会传给后台
|
||
"""
|
||
if dut:
|
||
try:
|
||
# 合成 stationAssigned JSON 字符串
|
||
station_assigned_str = self._compose_station_assigned(dut)
|
||
|
||
# 准备数据库记录(移除虚拟字段)
|
||
db_record = self._prepare_db_record(dut, station_assigned_str)
|
||
|
||
# 尝试使用 HTTP API
|
||
if self.use_http_api:
|
||
try:
|
||
url = f"{self.api_base_url}/dbTableAccess"
|
||
response = requests.post(
|
||
url,
|
||
params={"table": "dutList"}, # 注意:表名必须是 dutList
|
||
json={"action": "insert", "records": [db_record]}, # 使用清理后的记录
|
||
timeout=5
|
||
)
|
||
response.raise_for_status()
|
||
result = response.json()
|
||
if result.get("status") == "success":
|
||
# 重新加载数据
|
||
self._load_duts()
|
||
return
|
||
except Exception as e:
|
||
print(f"HTTP API 添加DUT失败: {e},回退到直接访问 SQLite")
|
||
|
||
# 直接访问 SQLite
|
||
if os.path.exists(self.db_path):
|
||
conn = sqlite3.connect(self.db_path)
|
||
cursor = conn.cursor()
|
||
|
||
# 插入新DUT
|
||
cursor.execute("""
|
||
INSERT INTO dutList
|
||
(SN, name, project, projectType, phase, weeks, stationAssigned,
|
||
itemOnGoing, itemsFinished, status, testReq, inspector,
|
||
description, deadLine, createdate, workOrder, direction_codes)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""", (
|
||
db_record.get("SN", ""),
|
||
db_record.get("name", ""),
|
||
db_record.get("project", ""),
|
||
db_record.get("projectType", ""),
|
||
db_record.get("phase", ""),
|
||
db_record.get("weeks", 0),
|
||
db_record.get("stationAssigned", ""),
|
||
db_record.get("itemOnGoing", ""),
|
||
db_record.get("itemsFinished", 0),
|
||
db_record.get("status", ""),
|
||
db_record.get("testReq", ""),
|
||
db_record.get("inspector", ""),
|
||
db_record.get("description", ""),
|
||
db_record.get("deadLine", ""),
|
||
db_record.get("createdate", ""),
|
||
db_record.get("workOrder", ""),
|
||
db_record.get("direction_codes", "")
|
||
))
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
# 重新加载数据
|
||
self._load_duts()
|
||
|
||
except Exception as e:
|
||
print(f"添加DUT时出错: {e}")
|
||
|
||
@pyqtSlot(int)
|
||
def removeDUT(self, dut_id):
|
||
"""删除DUT"""
|
||
try:
|
||
# 从数据库删除
|
||
db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dtmgtDb.db')
|
||
if os.path.exists(db_path):
|
||
conn = sqlite3.connect(db_path)
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("DELETE FROM dutList WHERE id = ?", (dut_id,))
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
# 从内存列表删除
|
||
self._duts = [d for d in self._duts if d.get("id") != dut_id]
|
||
if self._current_dut and self._current_dut.get("id") == dut_id:
|
||
self.setCurrentDUT = None
|
||
self.dutsChanged.emit()
|
||
|
||
except Exception as e:
|
||
print(f"删除DUT时出错: {e}")
|
||
|
||
def _compose_station_assigned(self, dut):
|
||
"""将 dtMachine 和 station 字段合成为 JSON 字符串
|
||
|
||
Args:
|
||
dut: DUT 数据字典,包含 dtMachine 和 station 字段
|
||
|
||
Returns:
|
||
str: JSON 格式字符串,如 '{"dtMachine": "SUN-LAB1-TOP-002", "station": "01"}'
|
||
"""
|
||
dtm = dut.get("dtMachine", "").strip()
|
||
station = dut.get("station", "").strip()
|
||
|
||
if dtm or station:
|
||
obj = {"dtMachine": dtm, "station": station}
|
||
return json.dumps(obj, ensure_ascii=False)
|
||
return ""
|
||
|
||
def _prepare_db_record(self, dut, station_assigned_str=None):
|
||
"""准备数据库记录,移除虚拟字段(dtMachine, station)
|
||
|
||
Args:
|
||
dut: DUT 数据字典
|
||
station_assigned_str: 已合成的 stationAssigned JSON 字符串(可选)
|
||
|
||
Returns:
|
||
dict: 清理后的数据库记录
|
||
"""
|
||
# 虚拟字段列表(不存在于数据库表中)
|
||
virtual_fields = {"dtMachine", "station"}
|
||
|
||
# 复制 dut 字典并移除虚拟字段
|
||
db_record = {k: v for k, v in dut.items() if k not in virtual_fields}
|
||
|
||
# 如果提供了 stationAssigned 字符串,使用它
|
||
if station_assigned_str is not None:
|
||
db_record["stationAssigned"] = station_assigned_str
|
||
|
||
return db_record
|
||
|
||
@pyqtSlot('QVariant')
|
||
def updateDUT(self, dut):
|
||
"""更新DUT信息
|
||
重要:
|
||
1. 只更新传入的非 None 字段,避免误清空数据
|
||
2. 如果传入了 dtMachine 或 station 字段,会自动合成 stationAssigned JSON 字符串
|
||
3. 虚拟字段(dtMachine, station)不会传给数据库
|
||
"""
|
||
if not dut or "id" not in dut:
|
||
return
|
||
|
||
try:
|
||
# 如果传入了 dtMachine 或 station,合成 stationAssigned
|
||
station_assigned_str = None
|
||
if "dtMachine" in dut or "station" in dut:
|
||
station_assigned_str = self._compose_station_assigned(dut)
|
||
dut["stationAssigned"] = station_assigned_str
|
||
|
||
# 准备数据库记录(移除虚拟字段)
|
||
db_record = self._prepare_db_record(dut)
|
||
|
||
# 尝试使用 HTTP API
|
||
if self.use_http_api:
|
||
try:
|
||
url = f"{self.api_base_url}/dbTableAccess"
|
||
response = requests.post(
|
||
url,
|
||
params={"table": "dutList"}, # 注意:表名必须是 dutList
|
||
json={"action": "update", "records": [db_record]}, # 使用清理后的记录
|
||
timeout=5
|
||
)
|
||
response.raise_for_status()
|
||
result = response.json()
|
||
if result.get("status") == "success":
|
||
# 重新加载数据
|
||
self._load_duts()
|
||
return
|
||
except Exception as e:
|
||
print(f"HTTP API 更新DUT失败: {e},回退到直接访问 SQLite")
|
||
|
||
# 直接访问 SQLite
|
||
if os.path.exists(self.db_path):
|
||
conn = sqlite3.connect(self.db_path)
|
||
cursor = conn.cursor()
|
||
|
||
# 动态构建 UPDATE 语句,只更新传入的非 None 字段
|
||
update_fields = []
|
||
update_values = []
|
||
|
||
# 数据库真实字段(排除虚拟字段)
|
||
db_fields = [
|
||
"SN", "name", "project", "projectType", "phase",
|
||
"weeks", "stationAssigned", "itemOnGoing", "itemsFinished",
|
||
"status", "testReq", "inspector", "description",
|
||
"deadLine", "createdate", "workOrder", "direction_codes"
|
||
]
|
||
|
||
for field in db_fields:
|
||
if field in db_record and db_record[field] is not None:
|
||
update_fields.append(f"{field}=?")
|
||
# weeks 和 itemsFinished 需要转换为整数
|
||
if field in ["weeks", "itemsFinished"]:
|
||
update_values.append(int(db_record[field]))
|
||
else:
|
||
update_values.append(db_record[field])
|
||
|
||
if update_fields:
|
||
update_values.append(dut["id"])
|
||
sql = f"UPDATE dutList SET {', '.join(update_fields)} WHERE id=?"
|
||
cursor.execute(sql, tuple(update_values))
|
||
conn.commit()
|
||
|
||
conn.close()
|
||
|
||
# 重新加载数据
|
||
self._load_duts()
|
||
|
||
except Exception as e:
|
||
print(f"更新DUT时出错: {e}")
|
||
|
||
@pyqtSlot(result=int)
|
||
def getDUTCount(self):
|
||
"""获取DUT总数"""
|
||
return len(self._duts)
|
||
|
||
def getAllDUTs(self):
|
||
"""获取所有DUT列表
|
||
|
||
Returns:
|
||
list: 所有DUT数据列表
|
||
"""
|
||
print(f"getAllDUTs() 调用,当前共 {len(self._duts)} 条记录")
|
||
return self._duts
|
||
|
||
def filterByStatus(self, status):
|
||
"""按状态过滤DUT
|
||
|
||
Args:
|
||
status: 状态字符串,为空或'全部'时返回全部
|
||
|
||
Returns:
|
||
list: 过滤后的DUT列表
|
||
"""
|
||
if not status or status == '全部':
|
||
return self._duts
|
||
return [d for d in self._duts if (d.get('status') or '').strip() == status]
|