fix: response_code持久化到数据库 + 工单客户端加tenant-id

1. wechat_service: save/get_response_code 改为内存+数据库双写,
   容器重启后边缘resolve仍能更新企微卡片
2. work_order_client: 请求头加 tenant-id,签名公式加 query_str 参数
3. config: WorkOrderConfig 新增 tenant_id 字段

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 13:37:34 +08:00
parent 2a9bf7d575
commit c2c272c298
3 changed files with 54 additions and 13 deletions

View File

@@ -81,6 +81,7 @@ class WorkOrderConfig:
base_url: str = "" # 工单系统地址,如 http://aiot-platform.viewsh.com:48080 base_url: str = "" # 工单系统地址,如 http://aiot-platform.viewsh.com:48080
app_id: str = "" # 应用ID app_id: str = "" # 应用ID
app_secret: str = "" # 应用密钥 app_secret: str = "" # 应用密钥
tenant_id: str = "1" # 租户编号
timeout: int = 10 # 请求超时(秒) timeout: int = 10 # 请求超时(秒)
enabled: bool = False enabled: bool = False
@@ -195,6 +196,7 @@ def load_settings() -> Settings:
base_url=os.getenv("WORK_ORDER_BASE_URL", ""), base_url=os.getenv("WORK_ORDER_BASE_URL", ""),
app_id=os.getenv("WORK_ORDER_APP_ID", ""), app_id=os.getenv("WORK_ORDER_APP_ID", ""),
app_secret=os.getenv("WORK_ORDER_APP_SECRET", ""), app_secret=os.getenv("WORK_ORDER_APP_SECRET", ""),
tenant_id=os.getenv("WORK_ORDER_TENANT_ID", "1"),
timeout=int(os.getenv("WORK_ORDER_TIMEOUT", "10")), timeout=int(os.getenv("WORK_ORDER_TIMEOUT", "10")),
enabled=os.getenv("WORK_ORDER_ENABLED", "false").lower() == "true", enabled=os.getenv("WORK_ORDER_ENABLED", "false").lower() == "true",
), ),

View File

@@ -83,12 +83,51 @@ class WeChatService:
return self._access_token return self._access_token
def save_response_code(self, task_id: str, response_code: str): def save_response_code(self, task_id: str, response_code: str):
"""保存卡片的 response_code用于后续更新卡片状态""" """保存卡片的 response_code内存缓存 + 数据库持久化"""
self._response_codes[task_id] = response_code self._response_codes[task_id] = response_code
try:
from app.models import get_session, AlarmEventExt
db = get_session()
try:
ext = db.query(AlarmEventExt).filter(
AlarmEventExt.alarm_id == task_id,
AlarmEventExt.ext_type == "WECHAT_RESPONSE_CODE",
).first()
if ext:
ext.ext_data = {"response_code": response_code}
else:
ext = AlarmEventExt(
alarm_id=task_id,
ext_type="WECHAT_RESPONSE_CODE",
ext_data={"response_code": response_code},
)
db.add(ext)
db.commit()
finally:
db.close()
except Exception as e:
logger.warning(f"持久化 response_code 失败: {e}")
def get_response_code(self, task_id: str) -> Optional[str]: def get_response_code(self, task_id: str) -> Optional[str]:
"""获取并消耗 response_code只能用一次""" """获取 response_code优先内存缓存,回退数据库查询"""
return self._response_codes.pop(task_id, None) code = self._response_codes.pop(task_id, None)
if code:
return code
try:
from app.models import get_session, AlarmEventExt
db = get_session()
try:
ext = db.query(AlarmEventExt).filter(
AlarmEventExt.alarm_id == task_id,
AlarmEventExt.ext_type == "WECHAT_RESPONSE_CODE",
).first()
if ext and ext.ext_data:
return ext.ext_data.get("response_code", "")
finally:
db.close()
except Exception as e:
logger.warning(f"查询 response_code 失败: {e}")
return None
# ==================== 媒体上传 ==================== # ==================== 媒体上传 ====================

View File

@@ -26,6 +26,7 @@ class WorkOrderClient:
self._base_url = "" self._base_url = ""
self._app_id = "" self._app_id = ""
self._app_secret = "" self._app_secret = ""
self._tenant_id = "1"
self._timeout = 10 self._timeout = 10
def init(self, config): def init(self, config):
@@ -34,6 +35,7 @@ class WorkOrderClient:
self._base_url = config.base_url.rstrip("/") self._base_url = config.base_url.rstrip("/")
self._app_id = config.app_id self._app_id = config.app_id
self._app_secret = config.app_secret self._app_secret = config.app_secret
self._tenant_id = getattr(config, "tenant_id", "1")
self._timeout = getattr(config, "timeout", 10) self._timeout = getattr(config, "timeout", 10)
if self._enabled: if self._enabled:
@@ -45,29 +47,27 @@ class WorkOrderClient:
def enabled(self) -> bool: def enabled(self) -> bool:
return self._enabled return self._enabled
def _sign(self, body_json: str, nonce: str, timestamp: str) -> str: def _sign(self, body_json: str, nonce: str, timestamp: str, query_str: str = "") -> str:
""" """
SHA256 签名 SHA256 签名
签名算法SHA256(body_json + "appId=" + appId + "&nonce=" + nonce + "&timestamp=" + timestamp + appSecret) 签名算法SHA256(query_str + body_json + header_str + appSecret)
- query_str: Query 参数按 key 字母升序排序拼接,无参数时为空串
- header_str: 固定顺序 appId=&nonce=&timestamp=
""" """
raw = ( header_str = f"appId={self._app_id}&nonce={nonce}&timestamp={timestamp}"
body_json raw = f"{query_str}{body_json}{header_str}{self._app_secret}"
+ "appId=" + self._app_id
+ "&nonce=" + nonce
+ "&timestamp=" + timestamp
+ self._app_secret
)
return hashlib.sha256(raw.encode("utf-8")).hexdigest() return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def _build_headers(self, body_json: str) -> dict: def _build_headers(self, body_json: str) -> dict:
"""构造请求头(含签名)""" """构造请求头(含签名 + tenant-id"""
nonce = uuid.uuid4().hex[:16] nonce = uuid.uuid4().hex[:16]
timestamp = str(int(time.time() * 1000)) timestamp = str(int(time.time() * 1000))
sign = self._sign(body_json, nonce, timestamp) sign = self._sign(body_json, nonce, timestamp)
return { return {
"Content-Type": "application/json", "Content-Type": "application/json",
"tenant-id": self._tenant_id,
"appId": self._app_id, "appId": self._app_id,
"nonce": nonce, "nonce": nonce,
"timestamp": timestamp, "timestamp": timestamp,