From c2c272c29889b07b7fabd6412f5d9ff9560a1bb2 Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Fri, 13 Mar 2026 13:37:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20response=5Fcode=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=E5=88=B0=E6=95=B0=E6=8D=AE=E5=BA=93=20+=20=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=8A=A0tenant-id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/config.py | 2 ++ app/services/wechat_service.py | 45 ++++++++++++++++++++++++++++--- app/services/work_order_client.py | 20 +++++++------- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/app/config.py b/app/config.py index 0fa9a54..7c6be34 100644 --- a/app/config.py +++ b/app/config.py @@ -81,6 +81,7 @@ class WorkOrderConfig: base_url: str = "" # 工单系统地址,如 http://aiot-platform.viewsh.com:48080 app_id: str = "" # 应用ID app_secret: str = "" # 应用密钥 + tenant_id: str = "1" # 租户编号 timeout: int = 10 # 请求超时(秒) enabled: bool = False @@ -195,6 +196,7 @@ def load_settings() -> Settings: base_url=os.getenv("WORK_ORDER_BASE_URL", ""), app_id=os.getenv("WORK_ORDER_APP_ID", ""), 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")), enabled=os.getenv("WORK_ORDER_ENABLED", "false").lower() == "true", ), diff --git a/app/services/wechat_service.py b/app/services/wechat_service.py index 968dcfe..278f9a5 100644 --- a/app/services/wechat_service.py +++ b/app/services/wechat_service.py @@ -83,12 +83,51 @@ class WeChatService: return self._access_token def save_response_code(self, task_id: str, response_code: str): - """保存卡片的 response_code(用于后续更新卡片状态)""" + """保存卡片的 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]: - """获取并消耗 response_code(只能用一次)""" - return self._response_codes.pop(task_id, None) + """获取 response_code(优先内存缓存,回退数据库查询)""" + 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 # ==================== 媒体上传 ==================== diff --git a/app/services/work_order_client.py b/app/services/work_order_client.py index 7ef177d..f8c4a68 100644 --- a/app/services/work_order_client.py +++ b/app/services/work_order_client.py @@ -26,6 +26,7 @@ class WorkOrderClient: self._base_url = "" self._app_id = "" self._app_secret = "" + self._tenant_id = "1" self._timeout = 10 def init(self, config): @@ -34,6 +35,7 @@ class WorkOrderClient: self._base_url = config.base_url.rstrip("/") self._app_id = config.app_id self._app_secret = config.app_secret + self._tenant_id = getattr(config, "tenant_id", "1") self._timeout = getattr(config, "timeout", 10) if self._enabled: @@ -45,29 +47,27 @@ class WorkOrderClient: def enabled(self) -> bool: 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(body_json + "appId=" + appId + "&nonce=" + nonce + "×tamp=" + timestamp + appSecret) + 签名算法:SHA256(query_str + body_json + header_str + appSecret) + - query_str: Query 参数按 key 字母升序排序拼接,无参数时为空串 + - header_str: 固定顺序 appId=&nonce=×tamp= """ - raw = ( - body_json - + "appId=" + self._app_id - + "&nonce=" + nonce - + "×tamp=" + timestamp - + self._app_secret - ) + header_str = f"appId={self._app_id}&nonce={nonce}×tamp={timestamp}" + raw = f"{query_str}{body_json}{header_str}{self._app_secret}" return hashlib.sha256(raw.encode("utf-8")).hexdigest() def _build_headers(self, body_json: str) -> dict: - """构造请求头(含签名)""" + """构造请求头(含签名 + tenant-id)""" nonce = uuid.uuid4().hex[:16] timestamp = str(int(time.time() * 1000)) sign = self._sign(body_json, nonce, timestamp) return { "Content-Type": "application/json", + "tenant-id": self._tenant_id, "appId": self._app_id, "nonce": nonce, "timestamp": timestamp,