301 lines
9.2 KiB
Python
301 lines
9.2 KiB
Python
# 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 |