Files
KCGL/inventory-backend/app/services/track_webhook_service.py
yueli b85be2dfdf feat(track): 新增 resolve_track_route,按公司解析 Track 实例地址
resolve_track_route(company_name, channel) 支持 api / inbound / outbound
三个通道。未命中一律回落到扁平变量,**绝不返回空**——漏配一个 key 就
静默断链,比配置写错更难排查。

非空但不在路由表内的值(含 get_current_company_filter() 的
'__NO_COMPANY__' 哨兵、拼写异常的公司名)会打 WARNING 暴露出来,但不
阻断业务——这类值属于主数据问题,交给业务确认,代码不猜。

notify_track() 相应扩展:新增 channel 参数(不传则按 payload['event']
推断 inbound/outbound),url 仍为最高优先级(显式指定时不做公司路由)。

实测各分支:IRIS/LICA 各通道命中正确;None/空串/未知公司/缺通道/空路由表
全部按预期回落。
2026-09-22 13:13:53 +08:00

132 lines
4.9 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.

# app/services/track_webhook_service.py
"""
Track 系统 Webhook 通知服务(发送侧)
真实扫码入库成功后,通过后台线程 + httpx 异步 POST 通知 Track 系统。
容错策略:
- 未配置 TRACK_WEBHOOK_URL 时静默跳过,不通知。
- 全包裹 try-except任何异常连接拒绝/超时/网络错误)仅记录 logger.error
绝不抛出异常阻断入库主业务流程。
"""
import logging
import threading
import httpx
from flask import current_app
logger = logging.getLogger(__name__)
# 请求超时(秒):后台线程,宁可超时失败也不能拖住业务
_WEBHOOK_TIMEOUT = 5.0
def _post_to_track(payload, url, api_key):
"""后台线程中执行的真正 POST 请求(含详细日志)"""
headers = {
'Content-Type': 'application/json',
# ★ 鉴权字段名必须为 X-API-Key与 Track 接收端严格对齐
'X-API-Key': api_key,
}
try:
resp = httpx.post(url, json=payload, headers=headers, timeout=_WEBHOOK_TIMEOUT)
logger.info(
"[TrackWebhook] event=%s source_table=%s status=%s",
payload.get('event'), payload.get('source_table'), resp.status_code,
)
except Exception as e:
logger.error("[TrackWebhook] 发送失败 url=%s, err=%s", url, e)
# 各通道未命中路由表时回落到哪个扁平配置变量(保持改造前的单实例行为)
_TRACK_ROUTE_FALLBACK = {
'api': 'TRACK_API_URL',
'inbound': 'TRACK_WEBHOOK_URL',
'outbound': 'TRACK_OUTBOUND_WEBHOOK_URL',
}
def resolve_track_route(company_name, channel):
"""按公司名解析 Track 实例地址。
channel: 'api' | 'inbound' | 'outbound'
⚠️ 未命中必须**回落到扁平变量**,不能返回空 —— 漏配一个 key 就静默断链,
比配置写错更难排查。
"""
routes = current_app.config.get('TRACK_ROUTES') or {}
if company_name and company_name in routes:
url = ((routes.get(company_name) or {}).get(channel) or '').strip()
if url:
return url
# 公司在表里但该通道没配 —— 这是配置漏项,务必暴露出来
logger.warning(
"[TrackRoute] 公司 %s%s 通道未配置,回落到全局默认地址",
company_name, channel,
)
elif company_name:
# 非空但不在路由表内:可能是拼写异常的公司名,也可能是
# get_current_company_filter() 的 '__NO_COMPANY__' 哨兵(用户 JWT 无公司)。
# 不猜、不阻断,只暴露给业务确认。
logger.warning(
"[TrackRoute] 公司 '%s' 不在 TRACK_ROUTES 中回落到全局默认地址channel=%s)。"
"若该值不是合法部门名,请业务确认主数据",
company_name, channel,
)
fallback_key = _TRACK_ROUTE_FALLBACK.get(channel)
if not fallback_key:
return ''
return (current_app.config.get(fallback_key) or '').strip()
def _channel_from_event(event):
"""从事件名推断路由通道;缺省按入库处理。"""
return 'outbound' if 'outbound' in (event or '').lower() else 'inbound'
def get_current_operator():
"""从 JWT 中安全获取当前操作人姓名(失败返回空字符串,不抛异常)"""
try:
from flask_jwt_extended import get_jwt
claims = get_jwt()
if not claims:
return ''
# 优先显示名(如"张三"回退到账号ID
return claims.get('display_name') or claims.get('username') or ''
except Exception:
return ''
def notify_track(payload, url=None, channel=None):
"""
异步通知 Track 系统业务事件(入库/出库等)。
调用方在 db.session.commit() 成功后调用,本函数绝不抛出异常:
- 未配置 URL / Key 为空 -> 静默跳过
- 网络异常 / 超时 -> 仅记录日志
:param url: 显式指定 Track webhook 地址,优先级最高(指定后不再做公司路由)
:param channel: 路由通道 'inbound' | 'outbound';不传则按 payload['event'] 推断。
仅在不传 url 时生效——实例由 payload['company_name'] 决定。
"""
try:
if not url:
channel = channel or _channel_from_event(payload.get('event'))
url = resolve_track_route(payload.get('company_name'), channel)
url = (url or '').strip()
api_key = (current_app.config.get('TRACK_WEBHOOK_KEY') or '').strip()
if not url:
logger.info("[TrackWebhook] 未配置 webhook URL跳过通知")
return
thread = threading.Thread(
target=_post_to_track,
args=(payload, url, api_key),
daemon=True, # 守护线程:不阻塞进程退出
name='track_webhook_notify',
)
thread.start()
except Exception as e:
logger.error("[TrackWebhook] 通知调用失败(不影响业务): %s", e)