254 lines
9.6 KiB
Python
254 lines
9.6 KiB
Python
# 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)
|
||
self._maybe_migrate_json()
|
||
self.sync.initial_sync()
|
||
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()
|