Dish_Recognition/service/mgmt_client.py

301 lines
9.2 KiB
Python
Raw Permalink 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
"""菜品识别管理系统公开接口客户端(X-API-Key)。
对接文档:docs/第三方接口文档.md / docs/第三方接口文档_mgmt.md
"""
from datetime import datetime
try:
import requests
except ImportError:
requests = None
from dao.settings import (
DISH_BASE_CODE,
DISH_BASE_NAME,
DISH_DEVICE_CODE,
DISH_DEVICE_NAME,
DISH_MGMT_API_KEY,
DISH_MGMT_API_ORIGIN,
DISH_MGMT_API_TIMEOUT,
DISH_MGMT_API_URL,
DISH_MGMT_DEVICE_STATUS_URL,
DISH_MGMT_DISH_LEARNING_STATUS_URL,
DISH_MGMT_DISHES_API_URL,
DISH_MGMT_MEAL_API_URL,
DISH_MGMT_RESULT_API_URL,
DISH_MGMT_UNRECOGNIZED_API_URL,
)
class MgmtApiError(RuntimeError):
pass
def _headers(api_key=None, json_body=False):
key = (api_key if api_key is not None else DISH_MGMT_API_KEY) or ''
key = str(key).strip()
if not key:
raise MgmtApiError('未配置 DISH_MGMT_API_KEY')
headers = {
'X-API-Key': key,
'Accept': 'application/json',
}
if json_body:
headers['Content-Type'] = 'application/json'
return headers
def _request(method, url, *, params=None, json_body=None, api_key=None, timeout=None):
if requests is None:
raise MgmtApiError('缺少 requests 库,请 pip install requests')
url = (url or '').strip()
if not url:
raise MgmtApiError('未配置接口 URL')
timeout = float(timeout if timeout is not None else DISH_MGMT_API_TIMEOUT)
headers = _headers(api_key=api_key, json_body=json_body is not None)
try:
resp = requests.request(
method.upper(),
url,
headers=headers,
params=params,
json=json_body,
timeout=timeout,
)
except Exception as exc:
raise MgmtApiError(f'请求管理系统失败: {exc}') from exc
if resp.status_code >= 400:
body = (resp.text or '')[:300]
raise MgmtApiError(f'管理系统 HTTP {resp.status_code}: {body}')
if not resp.content:
return {}
try:
return resp.json()
except Exception as exc:
raise MgmtApiError(f'管理系统返回非 JSON: {exc}') from exc
# ---------- 1) 基地列表 ----------
def fetch_bases(url=None, api_key=None, timeout=None):
"""GET /api/public/bases → items[]"""
data = _request('GET', url or DISH_MGMT_API_URL, api_key=api_key, timeout=timeout)
if isinstance(data, list):
return data
if isinstance(data, dict):
for key in ('items', 'data', 'list', 'bases', 'rows'):
if isinstance(data.get(key), list):
return data[key]
raise MgmtApiError(f'无法解析基地列表,响应类型={type(data).__name__}')
# ---------- 2) 写入识别结果 ----------
def post_recognize_result(payload, url=None, api_key=None, timeout=None):
"""
POST /api/public/recognition-orders
payload 字段:
request_id, base_name, device_code, capture_time,
source_image, labeled_image, dish_count,
dishes[{index, name, score, is_unknown, suggested_name, box, crop_image, text}]
"""
return _request(
'POST',
url or DISH_MGMT_RESULT_API_URL,
json_body=payload,
api_key=api_key,
timeout=timeout,
)
def post_unrecognized_samples(payload, url=None, api_key=None, timeout=None):
"""
POST /api/public/unrecognized-samples
必填:order_no、device_code、capture_time、image_url
"""
return _request(
'POST',
url or DISH_MGMT_UNRECOGNIZED_API_URL,
json_body=payload,
api_key=api_key,
timeout=timeout,
)
# ---------- 3) 更新设备状态 ----------
def build_device_status_payload(
*,
device_code=None,
device_name=None,
base_code=None,
base_name=None,
status=1,
online=True,
ip=None,
app_version=None,
reported_at=None,
extra=None,
):
"""
设备状态上报字段(camelCase,与管理系统其它公开接口风格一致)。
{
"deviceCode", "deviceName", "baseCode", "baseName",
"status", "online", "ip", "appVersion", "reportedAt"
}
"""
payload = {
'deviceCode': (device_code or DISH_DEVICE_CODE or DISH_BASE_CODE or '').strip(),
'deviceName': (device_name or DISH_DEVICE_NAME or DISH_BASE_NAME or '').strip(),
'baseCode': (base_code or DISH_BASE_CODE or '').strip(),
'baseName': (base_name or DISH_BASE_NAME or '').strip(),
'status': int(status) if status is not None else 1,
'online': bool(online),
'ip': ip or '',
'appVersion': app_version or '',
'reportedAt': reported_at or datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
}
if extra and isinstance(extra, dict):
payload.update(extra)
return payload
def post_device_status(payload=None, url=None, api_key=None, timeout=None, **kwargs):
"""POST /api/public/devices/status(可用 DISH_MGMT_DEVICE_STATUS_URL 覆盖)"""
body = payload if payload is not None else build_device_status_payload(**kwargs)
return _request(
'POST',
url or DISH_MGMT_DEVICE_STATUS_URL,
json_body=body,
api_key=api_key,
timeout=timeout,
)
# ---------- 4) 当天排餐 ----------
def fetch_today_meals(base_name=None, base_id=None, base_code=None, date=None,
url=None, api_key=None, timeout=None):
"""
GET /api/public/meal-schedules/today
筛选优先级:baseId > baseCode > baseName
"""
params = {}
if date:
params['date'] = date
if base_id is not None and str(base_id).strip() != '':
params['baseId'] = base_id
elif base_code:
params['baseCode'] = base_code
elif base_name:
params['baseName'] = base_name
else:
params['baseName'] = DISH_BASE_NAME
return _request(
'GET',
url or DISH_MGMT_MEAL_API_URL,
params=params,
api_key=api_key,
timeout=timeout,
)
def extract_dish_names_from_meals(meal_data):
"""从排餐响应中提取全部菜名集合(跨餐段)。"""
names = set()
if not isinstance(meal_data, dict):
return names
for base in meal_data.get('bases') or []:
for meal in base.get('meals') or []:
for dish in meal.get('dishes') or []:
name = (dish.get('name') or '').strip()
if name:
names.add(name)
return names
# ---------- 5) 菜品信息 ----------
def fetch_dishes(name=None, category=None, url=None, api_key=None, timeout=None):
"""
GET /api/public/dishes
:param name: 菜名模糊查询(可选)
:param category: 分类精确查询,传「全部」等同不筛选(可选)
:return: dict,通常含 total / items;若服务端直接返回 list 也会包一层
"""
params = {}
if name is not None and str(name).strip() != '':
params['name'] = str(name).strip()
if category is not None and str(category).strip() != '':
cat = str(category).strip()
if cat != '全部':
params['category'] = cat
data = _request(
'GET',
url or DISH_MGMT_DISHES_API_URL,
params=params or None,
api_key=api_key,
timeout=timeout,
)
if isinstance(data, list):
return {'total': len(data), 'items': data}
if isinstance(data, dict):
if 'items' in data:
return data
for key in ('data', 'list', 'rows', 'dishes'):
if isinstance(data.get(key), list):
return {
'total': data.get('total', len(data[key])),
'items': data[key],
**{k: v for k, v in data.items() if k not in ('total', 'items', key)},
}
return data
raise MgmtApiError(f'无法解析菜品列表,响应类型={type(data).__name__}')
def patch_dish_learning_status(name, status, url=None, api_key=None, timeout=None):
"""
PATCH /api/public/dishes/learning-status
:param name: 菜品名称(精确匹配)
:param status: 已学习 / 未学习(也兼容 learned / unlearned)
"""
name = (name or '').strip()
if not name:
raise MgmtApiError('菜品名称不能为空')
raw = (status or '').strip()
status_map = {
'已学习': '已学习',
'未学习': '未学习',
'learned': '已学习',
'unlearned': '未学习',
'LEARNED': '已学习',
'UNLEARNED': '未学习',
}
normalized = status_map.get(raw) or status_map.get(raw.lower())
if not normalized:
raise MgmtApiError('学习状态只能是「已学习」或「未学习」')
return _request(
'PATCH',
url or DISH_MGMT_DISH_LEARNING_STATUS_URL,
json_body={'name': name, 'status': normalized},
api_key=api_key,
timeout=timeout,
)
def notify_dish_learned(name, timeout=None):
"""学菜成功后上报「已学习」;失败只打印警告,不抛异常。"""
try:
return patch_dish_learning_status(name, '已学习', timeout=timeout)
except Exception as exc:
print(f'[WARN] 上报菜品学习状态失败 ({name}): {exc}')
return None
# origin 仅用于调试
MGMT_ORIGIN = DISH_MGMT_API_ORIGIN