增加了查看入库秤上传图片的GUI界面。

This commit is contained in:
2025-11-27 10:46:53 +08:00
parent c74ac621a6
commit 1bedc16bd8
16 changed files with 2703 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
"""
数据库模块
"""
from .mysql_manager import MySQLManager
from .sqlite_manager import SQLiteManager
__all__ = ['MySQLManager', 'SQLiteManager']
+313
View File
@@ -0,0 +1,313 @@
"""
MySQL数据库管理器
使用SQLAlchemy ORM连接生产数据库(只读)
"""
import os
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Tuple
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.pool import QueuePool
import sys
# 添加父目录到路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import MYSQL_CONFIG, TABLE_MAPPING, IMAGE_URL_PREFIX
Base = declarative_base()
class DishCollection(Base):
"""菜品采集表模型"""
__tablename__ = TABLE_MAPPING['菜品']
id = Column(Integer, primary_key=True, autoincrement=True)
goods_id = Column(Integer, nullable=True, comment='物品ID')
goods_name = Column(String(255), nullable=True, comment='物品名称')
food_pic = Column(String(500), nullable=True, comment='图片URL')
equipment_code = Column(String(100), nullable=True, comment='设备编码')
create_time = Column(DateTime, nullable=True, comment='创建时间')
class InboundWeighing(Base):
"""食材入库秤表模型"""
__tablename__ = TABLE_MAPPING['食材']
id = Column(Integer, primary_key=True, autoincrement=True)
goods_id = Column(Integer, nullable=True, comment='物品ID')
goods_name = Column(String(255), nullable=True, comment='物品名称')
food_pic = Column(String(500), nullable=True, comment='图片URL')
equipment_code = Column(String(100), nullable=True, comment='设备编码')
create_time = Column(DateTime, nullable=True, comment='创建时间')
class ProcessedIngredient(Base):
"""半加工食材表模型(待替换表名)"""
__tablename__ = TABLE_MAPPING['半加工']
id = Column(Integer, primary_key=True, autoincrement=True)
goods_id = Column(Integer, nullable=True, comment='物品ID')
goods_name = Column(String(255), nullable=True, comment='物品名称')
food_pic = Column(String(500), nullable=True, comment='图片URL')
equipment_code = Column(String(100), nullable=True, comment='设备编码')
create_time = Column(DateTime, nullable=True, comment='创建时间')
class MySQLManager:
"""MySQL数据库管理器(只读)"""
def __init__(self):
"""初始化数据库连接"""
self.engine = None
self.Session = None
self._connect()
def _connect(self):
"""建立数据库连接"""
try:
# 构建连接字符串
connection_string = (
f"mysql+pymysql://{MYSQL_CONFIG['user']}:{MYSQL_CONFIG['password']}"
f"@{MYSQL_CONFIG['host']}:{MYSQL_CONFIG['port']}"
f"/{MYSQL_CONFIG['database']}?charset={MYSQL_CONFIG['charset']}"
)
# 创建引擎(使用连接池)
self.engine = create_engine(
connection_string,
poolclass=QueuePool,
pool_size=5,
max_overflow=10,
pool_recycle=3600,
echo=False # 设置为True可以看到SQL语句
)
# 创建Session工厂
self.Session = sessionmaker(bind=self.engine)
print("MySQL连接成功!")
except Exception as e:
print(f"MySQL连接失败: {e}")
raise
def get_model_by_datasource(self, datasource: str):
"""根据数据源获取对应的模型"""
model_mapping = {
'菜品': DishCollection,
'食材': InboundWeighing,
'半加工': ProcessedIngredient
}
return model_mapping.get(datasource)
def query_images(
self,
datasource: str,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
goods_name: Optional[str] = None,
equipment_code: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> Tuple[List[Dict], int]:
"""
查询图片数据
Args:
datasource: 数据源('菜品'/'食材'/'半加工'
start_date: 开始日期
end_date: 结束日期
goods_name: 物品名称(模糊搜索)
equipment_code: 设备编码(精确匹配)
page: 页码(从1开始)
page_size: 每页数量
Returns:
(数据列表, 总数量)
"""
session: Session = self.Session()
try:
# 获取对应的模型
Model = self.get_model_by_datasource(datasource)
if Model is None:
return [], 0
# 构建查询
query = session.query(Model)
# 时间范围筛选
if start_date:
query = query.filter(Model.create_time >= start_date)
if end_date:
# 结束日期包含当天,所以需要加1天
end_date_inclusive = end_date + timedelta(days=1)
query = query.filter(Model.create_time < end_date_inclusive)
# 物品名称筛选(模糊搜索)
if goods_name:
query = query.filter(Model.goods_name.like(f'%{goods_name}%'))
# 设备编码筛选(精确匹配)
if equipment_code:
query = query.filter(Model.equipment_code == equipment_code)
# 只查询有图片的记录
query = query.filter(Model.food_pic.isnot(None))
query = query.filter(Model.food_pic != '')
# 获取总数
total_count = query.count()
# 按时间倒序排列
query = query.order_by(Model.create_time.desc())
# 分页
offset = (page - 1) * page_size
query = query.limit(page_size).offset(offset)
# 执行查询
results = query.all()
# 转换为字典列表
data_list = []
for item in results:
# 拼接完整图片URL
full_image_url = f"{IMAGE_URL_PREFIX}{item.food_pic}" if item.food_pic else ""
data_list.append({
'id': item.id,
'goods_id': item.goods_id,
'goods_name': item.goods_name or '未知',
'image_url': full_image_url,
'equipment_code': item.equipment_code or '',
'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S') if item.create_time else '',
'datasource': datasource
})
return data_list, total_count
except Exception as e:
print(f"查询数据出错: {e}")
return [], 0
finally:
session.close()
def get_goods_names(self, datasource: str, limit: int = 100) -> List[str]:
"""
获取物品名称列表(用于搜索框自动补全)
Args:
datasource: 数据源
limit: 最多返回多少个
Returns:
物品名称列表
"""
session: Session = self.Session()
try:
Model = self.get_model_by_datasource(datasource)
if Model is None:
return []
# 查询不重复的物品名称
results = session.query(Model.goods_name)\
.filter(Model.goods_name.isnot(None))\
.filter(Model.goods_name != '')\
.distinct()\
.limit(limit)\
.all()
return [item[0] for item in results]
except Exception as e:
print(f"获取物品名称列表出错: {e}")
return []
finally:
session.close()
def get_statistics(
self,
datasource: str,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> Dict:
"""
获取统计信息
Returns:
统计数据字典
"""
session: Session = self.Session()
try:
Model = self.get_model_by_datasource(datasource)
if Model is None:
return {}
# 基础查询
base_query = session.query(Model)\
.filter(Model.food_pic.isnot(None))\
.filter(Model.food_pic != '')
if start_date:
base_query = base_query.filter(Model.create_time >= start_date)
if end_date:
end_date_inclusive = end_date + timedelta(days=1)
base_query = base_query.filter(Model.create_time < end_date_inclusive)
# 总数
total_count = base_query.count()
# 今日数量
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
today_count = base_query.filter(Model.create_time >= today_start).count()
# 本周数量
week_start = today_start - timedelta(days=today_start.weekday())
week_count = base_query.filter(Model.create_time >= week_start).count()
# 本月数量
month_start = today_start.replace(day=1)
month_count = base_query.filter(Model.create_time >= month_start).count()
# TOP 10物品统计
from sqlalchemy import func
top_items = session.query(
Model.goods_name,
func.count(Model.id).label('count')
).filter(
Model.food_pic.isnot(None),
Model.food_pic != '',
Model.goods_name.isnot(None),
Model.goods_name != ''
)
if start_date:
top_items = top_items.filter(Model.create_time >= start_date)
if end_date:
top_items = top_items.filter(Model.create_time < end_date + timedelta(days=1))
top_items = top_items.group_by(Model.goods_name)\
.order_by(func.count(Model.id).desc())\
.limit(10)\
.all()
return {
'total_count': total_count,
'today_count': today_count,
'week_count': week_count,
'month_count': month_count,
'top_items': [{'name': item[0], 'count': item[1]} for item in top_items]
}
except Exception as e:
print(f"获取统计信息出错: {e}")
return {}
finally:
session.close()
def close(self):
"""关闭数据库连接"""
if self.engine:
self.engine.dispose()
print("MySQL连接已关闭")
+250
View File
@@ -0,0 +1,250 @@
"""
SQLite下载历史管理器
用于记录图片下载历史,不修改生产MySQL数据库
"""
import os
import sqlite3
from datetime import datetime
from typing import List, Dict, Optional
import sys
# 添加父目录到路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import SQLITE_DB_PATH
class SQLiteManager:
"""SQLite下载历史管理器"""
def __init__(self, db_path: str = SQLITE_DB_PATH):
"""初始化SQLite连接"""
self.db_path = db_path
self._init_database()
def _init_database(self):
"""初始化数据库表结构"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 创建下载历史表
cursor.execute('''
CREATE TABLE IF NOT EXISTS download_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datasource TEXT NOT NULL,
goods_id INTEGER,
goods_name TEXT,
image_url TEXT NOT NULL,
local_path TEXT NOT NULL,
download_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
file_size INTEGER,
UNIQUE(datasource, image_url)
)
''')
# 创建索引
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_datasource_goods_id
ON download_history(datasource, goods_id)
''')
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_download_time
ON download_history(download_time)
''')
conn.commit()
conn.close()
print(f"SQLite数据库初始化成功: {self.db_path}")
except Exception as e:
print(f"SQLite数据库初始化失败: {e}")
raise
def add_download_record(
self,
datasource: str,
goods_id: int,
goods_name: str,
image_url: str,
local_path: str,
file_size: int
) -> bool:
"""
添加下载记录
Returns:
是否成功
"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO download_history
(datasource, goods_id, goods_name, image_url, local_path, file_size, download_time)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
datasource,
goods_id,
goods_name,
image_url,
local_path,
file_size,
datetime.now()
))
conn.commit()
conn.close()
return True
except Exception as e:
print(f"添加下载记录失败: {e}")
return False
def is_downloaded(self, datasource: str, image_url: str) -> bool:
"""
检查图片是否已下载(检查数据库记录 + 文件是否存在)
Args:
datasource: 数据源
image_url: 图片URL
Returns:
是否已下载
"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT local_path FROM download_history
WHERE datasource = ? AND image_url = ?
''', (datasource, image_url))
result = cursor.fetchone()
conn.close()
if result:
local_path = result[0]
# 检查文件是否真实存在
return os.path.exists(local_path)
return False
except Exception as e:
print(f"检查下载状态失败: {e}")
return False
def get_download_history(
self,
datasource: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
limit: int = 100
) -> List[Dict]:
"""
获取下载历史
Returns:
下载记录列表
"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
query = 'SELECT * FROM download_history WHERE 1=1'
params = []
if datasource:
query += ' AND datasource = ?'
params.append(datasource)
if start_date:
query += ' AND download_time >= ?'
params.append(start_date.strftime('%Y-%m-%d %H:%M:%S'))
if end_date:
query += ' AND download_time <= ?'
params.append(end_date.strftime('%Y-%m-%d %H:%M:%S'))
query += ' ORDER BY download_time DESC LIMIT ?'
params.append(limit)
cursor.execute(query, params)
columns = [desc[0] for desc in cursor.description]
results = []
for row in cursor.fetchall():
results.append(dict(zip(columns, row)))
conn.close()
return results
except Exception as e:
print(f"获取下载历史失败: {e}")
return []
def get_download_statistics(self) -> Dict:
"""获取下载统计信息"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 总下载数
cursor.execute('SELECT COUNT(*) FROM download_history')
total_count = cursor.fetchone()[0]
# 各数据源下载数
cursor.execute('''
SELECT datasource, COUNT(*) as count
FROM download_history
GROUP BY datasource
''')
datasource_stats = {row[0]: row[1] for row in cursor.fetchall()}
# 今日下载数
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
cursor.execute('''
SELECT COUNT(*) FROM download_history
WHERE download_time >= ?
''', (today.strftime('%Y-%m-%d %H:%M:%S'),))
today_count = cursor.fetchone()[0]
conn.close()
return {
'total_count': total_count,
'datasource_stats': datasource_stats,
'today_count': today_count
}
except Exception as e:
print(f"获取下载统计失败: {e}")
return {}
def clear_invalid_records(self) -> int:
"""清理文件不存在的下载记录"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT id, local_path FROM download_history')
records = cursor.fetchall()
deleted_count = 0
for record_id, local_path in records:
if not os.path.exists(local_path):
cursor.execute('DELETE FROM download_history WHERE id = ?', (record_id,))
deleted_count += 1
conn.commit()
conn.close()
print(f"已清理 {deleted_count} 条无效记录")
return deleted_count
except Exception as e:
print(f"清理无效记录失败: {e}")
return 0