Dish_Recognition/dao/template_repository.py

257 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# coding=utf-8
"""菜品模板仓储:SQLite 本地缓存读写,变更同步到 MySQL 总服务器后拉取刷新。"""
import datetime
import json
import os
import shutil
import numpy as np
from sqlalchemy import func
from dao.database import get_backend, init_local_engine, init_server_engine, local_session_scope
from dao.feature_codec import blob_to_feature
from dao.base_registry import resolve_local_base
from dao.template_sync import TemplateSync
from models.App_settings import AppSetting
from models.Dish_categories import DishCategory
from models.Global_templates import GlobalTemplate
SETTING_THRESHOLD = 'similarity_threshold'
SETTING_JSON_MIGRATED = 'json_templates_migrated'
class TemplateRepository:
def __init__(self, json_path=None, source_base_id=None, base_code=None):
self.json_path = json_path
init_local_engine()
init_server_engine()
self.base_info = resolve_local_base()
# 显式传入优先;否则用注册解析结果
self.source_base_id = source_base_id if source_base_id is not None else self.base_info.get('id')
self.base_code = base_code or self.base_info.get('base_code')
self.sync = TemplateSync(source_base_id=self.source_base_id)
print("[INFO] check json template migration…")
self._maybe_migrate_json()
print("[INFO] initial template sync with mysql…")
self.sync.initial_sync()
print("[INFO] start background template sync thread")
self.sync.start_background_sync(on_sync=self._on_background_sync)
self._background_reload_callback = None
def get_base_info(self):
return dict(self.base_info or {})
def refresh_base_info(self):
self.base_info = resolve_local_base(force_refresh=True)
self.source_base_id = self.base_info.get('id')
self.base_code = self.base_info.get('base_code')
self.sync.source_base_id = self.source_base_id
return self.base_info
def set_reload_callback(self, callback):
self._background_reload_callback = callback
def _on_background_sync(self):
if self._background_reload_callback:
self._background_reload_callback()
def get_threshold(self, default=0.7):
with local_session_scope() as session:
row = session.get(AppSetting, SETTING_THRESHOLD)
if row and row.value:
try:
return float(row.value)
except ValueError:
pass
return default
def set_threshold(self, threshold):
self.sync.push_set_threshold(threshold)
def _is_json_migrated(self, session):
row = session.get(AppSetting, SETTING_JSON_MIGRATED)
return row is not None and row.value == '1'
def _mark_json_migrated(self, session):
row = session.get(AppSetting, SETTING_JSON_MIGRATED)
if row:
row.value = '1'
else:
session.add(AppSetting(key=SETTING_JSON_MIGRATED, value='1'))
def _maybe_migrate_json(self):
if not self.json_path or not os.path.exists(self.json_path):
return
with local_session_scope() as session:
if self._is_json_migrated(session):
return
active_count = session.query(func.count(GlobalTemplate.id)).filter(
GlobalTemplate.is_active == 1
).scalar() or 0
if active_count > 0:
self._mark_json_migrated(session)
return
try:
with open(self.json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception as exc:
print(f"[WARN] read JSON templates failed, skip migration: {exc}")
return
threshold = data.get('threshold', 0.7)
library = data.get('template_library', {})
batch = []
for category_name, templates in library.items():
if not templates:
continue
for item in templates:
feature = item.get('feature')
if feature is None:
continue
batch.append({
'category_name': category_name,
'image_url': item.get('path'),
'feature': feature,
'thumbnail_url': item.get('thumbnail'),
})
# 阈值先写总服务器(有则),并批量上传模板;最终以 MySQL 拉取结果为准
self.sync.push_set_threshold(threshold)
migrated = self.sync.push_insert_batch(batch)
with local_session_scope() as session:
self._mark_json_migrated(session)
backup_path = self.json_path + '.bak'
try:
shutil.move(self.json_path, backup_path)
print(f"[OK] JSON templates migrated: {migrated}, backup: {backup_path}")
except Exception as exc:
print(f"[WARN] JSON migration backup failed: {exc}")
def load_library(self):
"""从 SQLite 本地缓存加载(识别只读本地库)。"""
threshold = self.get_threshold()
library = {}
with local_session_scope() as session:
rows = (
session.query(GlobalTemplate, DishCategory)
.join(DishCategory, GlobalTemplate.category_id == DishCategory.id)
.filter(GlobalTemplate.is_active == 1, DishCategory.is_active == 1)
.order_by(DishCategory.name, GlobalTemplate.id)
.all()
)
for template, category in rows:
feature = blob_to_feature(template.feature_blob)
if feature is None:
continue
add_time = template.create_time.strftime('%Y-%m-%d %H:%M:%S') if template.create_time else ''
library.setdefault(category.name, []).append({
'id': template.id,
'category_id': category.id,
'path': template.image_url or '',
'feature': feature.tolist(),
'add_time': add_time,
'thumbnail': template.thumbnail_url or '',
})
return threshold, library
def _find_template_record(self, category_name, image_path):
with local_session_scope() as session:
row = (
session.query(GlobalTemplate, DishCategory)
.join(DishCategory, GlobalTemplate.category_id == DishCategory.id)
.filter(
DishCategory.name == category_name,
GlobalTemplate.image_url == image_path,
GlobalTemplate.is_active == 1,
)
.order_by(GlobalTemplate.id.desc())
.first()
)
if not row:
return None
template, category = row
feature = blob_to_feature(template.feature_blob)
if feature is None:
return None
return {
'id': template.id,
'category_id': category.id,
'path': template.image_url or '',
'feature': feature.tolist(),
'add_time': template.create_time.strftime('%Y-%m-%d %H:%M:%S') if template.create_time else '',
'thumbnail': template.thumbnail_url or '',
}
def insert_template(self, category_name, image_path, feature, thumbnail_path, add_time=None):
self.sync.push_insert(category_name, image_path, feature, thumbnail_path)
record = self._find_template_record(category_name, image_path)
if record:
return record
return {
'id': None,
'category_id': None,
'path': image_path,
'feature': np.asarray(feature, dtype=np.float32).tolist(),
'add_time': add_time or datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'thumbnail': thumbnail_path or '',
}
def insert_templates_batch(self, items):
"""批量写入;内部只同步一次,避免逐条 pull 冲掉本地。"""
return self.sync.push_insert_batch(items)
def delete_template_by_id(self, template_id):
paths = None
with local_session_scope() as session:
template = session.get(GlobalTemplate, template_id)
if template and template.is_active == 1:
paths = {
'path': template.image_url,
'thumbnail': template.thumbnail_url,
}
if paths:
self.sync.push_delete_template(template_id)
return paths
def delete_category(self, category_name):
removed = []
with local_session_scope() as session:
category = (
session.query(DishCategory)
.filter(DishCategory.name == category_name, DishCategory.is_active == 1)
.first()
)
if category:
templates = (
session.query(GlobalTemplate)
.filter(GlobalTemplate.category_id == category.id, GlobalTemplate.is_active == 1)
.all()
)
removed = [{'path': t.image_url, 'thumbnail': t.thumbnail_url} for t in templates]
self.sync.push_delete_category(category_name)
return removed
def clear_all(self):
self.sync.push_clear_all()
def backend_name(self):
return get_backend()
def refresh_from_server(self):
return self.sync.pull_from_server()
def sync_now(self, full=True):
return self.sync.sync_now(full=full)
def get_sync_status(self):
return self.sync.get_sync_status()
def list_upload_queue(self, statuses=('pending', 'failed'), limit=100):
return self.sync.list_upload_queue(statuses=statuses, limit=limit)
def retry_failed_uploads(self):
return self.sync.retry_failed_uploads()