This commit is contained in:
Marsway 2026-02-04 14:03:24 +08:00
parent 606f83678a
commit 787dead8c1
6 changed files with 111 additions and 60 deletions

8
.env
View File

@ -1,10 +1,8 @@
HUOBANYUN_API_KEY=emdYCszTIUrczBf2wOPGQ553J3OO9NCKKnLGJEK9 HUOBANYUN_API_KEY=emdYCszTIUrczBf2wOPGQ553J3OO9NCKKnLGJEK9
FEISHU_APPROVAL_CODES=ECD8CE34-AA80-4A4F-B4C8-8510A7126490,BB944139-432F-4AC2-AD27-81C2F738E7C3,D7252659-47B6-4312-AC16-ECDE87FDB553,93F09E2D-B418-458D-A92D-10B56B53F45E,47FC32C3-5760-4547-8928-1EAB1DA6F4AF FEISHU_APPROVAL_CODE=ECD8CE34-AA80-4A4F-B4C8-8510A7126490
FEISHU_APPROVAL_CODE_PUBLIC=ECD8CE34-AA80-4A4F-B4C8-8510A7126490,BB944139-432F-4AC2-AD27-81C2F738E7C3
FEISHU_APPROVAL_CODE_PRIVATE=D7252659-47B6-4312-AC16-ECDE87FDB553,93F09E2D-B418-458D-A92D-10B56B53F45E,47FC32C3-5760-4547-8928-1EAB1DA6F4AF
FEISHU_APPROVAL_EVENT_KEY=approval_instance FEISHU_APPROVAL_EVENT_KEY=approval_instance
FEISHU_PROJECT_NO_FIELD_CODE=proj_id FEISHU_PROJECT_NO_FIELD_CODE=proj_id
HUOBANYUN_ORDER_STATUS_DONE_ID=3 FEISHU_UNSUBSCRIBE_CODES=BB944139-432F-4AC2-AD27-81C2F738E7C3,D7252659-47B6-4312-AC16-ECDE87FDB553,93F09E2D-B418-458D-A92D-10B56B53F45E,47FC32C3-5760-4547-8928-1EAB1DA6F4AF
HUOBANYUN_ORDER_STATUS_DONE_NAME=已完结 FEISHU_UNSUBSCRIBE_ONCE=false
FEISHU_APP_ID=cli_a90b035fd4799cb5 FEISHU_APP_ID=cli_a90b035fd4799cb5
FEISHU_APP_SECRET=O729hnbQARM2DHncWUd51eFHF6TDZAc3 FEISHU_APP_SECRET=O729hnbQARM2DHncWUd51eFHF6TDZAc3

View File

@ -64,3 +64,22 @@ class FeishuClient:
logger.error("订阅审批定义失败: status=%s body=%s", resp.status_code, resp.text) logger.error("订阅审批定义失败: status=%s body=%s", resp.status_code, resp.text)
resp.raise_for_status() resp.raise_for_status()
return data return data
async def unsubscribe_approval(self, approval_code: str) -> Dict[str, Any]:
token = await self._get_tenant_token()
url = (
"https://open.feishu.cn/open-apis/approval/v4/approvals/"
f"{approval_code}/unsubscribe"
)
headers = {"Authorization": f"Bearer {token}"}
async with httpx.AsyncClient(timeout=self.settings.request_timeout) as client:
resp = await client.post(url, headers=headers)
data = {}
try:
data = resp.json()
except Exception:
data = {}
if resp.status_code != 200 and data.get("code") not in (0, 1390007):
logger.error("取消订阅审批失败: status=%s body=%s", resp.status_code, resp.text)
resp.raise_for_status()
return data

View File

@ -16,11 +16,10 @@ class Settings:
feishu_encrypt_key: str feishu_encrypt_key: str
feishu_ws_url: str feishu_ws_url: str
feishu_approval_code: str feishu_approval_code: str
feishu_approval_codes: List[str]
feishu_approval_code_public: List[str]
feishu_approval_code_private: List[str]
feishu_approval_event_key: str feishu_approval_event_key: str
feishu_project_no_field_code: str feishu_project_no_field_code: str
feishu_unsubscribe_codes: List[str]
feishu_unsubscribe_once: bool
huobanyun_app_id: str huobanyun_app_id: str
huobanyun_app_secret: str huobanyun_app_secret: str
@ -49,6 +48,10 @@ def _env_list(name: str) -> List[str]:
return [item.strip() for item in raw.split(",") if item.strip()] return [item.strip() for item in raw.split(",") if item.strip()]
def _env_bool(name: str, default: str = "false") -> bool:
return _env(name, default).lower() in {"1", "true", "yes", "y", "on"}
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
def get_settings() -> Settings: def get_settings() -> Settings:
load_dotenv() load_dotenv()
@ -59,11 +62,10 @@ def get_settings() -> Settings:
feishu_encrypt_key=_env("FEISHU_ENCRYPT_KEY"), feishu_encrypt_key=_env("FEISHU_ENCRYPT_KEY"),
feishu_ws_url=_env("FEISHU_WS_URL"), feishu_ws_url=_env("FEISHU_WS_URL"),
feishu_approval_code=_env("FEISHU_APPROVAL_CODE"), feishu_approval_code=_env("FEISHU_APPROVAL_CODE"),
feishu_approval_codes=_env_list("FEISHU_APPROVAL_CODES"),
feishu_approval_code_public=_env_list("FEISHU_APPROVAL_CODE_PUBLIC"),
feishu_approval_code_private=_env_list("FEISHU_APPROVAL_CODE_PRIVATE"),
feishu_approval_event_key=_env("FEISHU_APPROVAL_EVENT_KEY"), feishu_approval_event_key=_env("FEISHU_APPROVAL_EVENT_KEY"),
feishu_project_no_field_code=_env("FEISHU_PROJECT_NO_FIELD_CODE"), feishu_project_no_field_code=_env("FEISHU_PROJECT_NO_FIELD_CODE"),
feishu_unsubscribe_codes=_env_list("FEISHU_UNSUBSCRIBE_CODES"),
feishu_unsubscribe_once=_env_bool("FEISHU_UNSUBSCRIBE_ONCE", "false"),
huobanyun_app_id=_env("HUOBANYUN_APP_ID"), huobanyun_app_id=_env("HUOBANYUN_APP_ID"),
huobanyun_app_secret=_env("HUOBANYUN_APP_SECRET"), huobanyun_app_secret=_env("HUOBANYUN_APP_SECRET"),
huobanyun_token=_env("HUOBANYUN_TOKEN"), huobanyun_token=_env("HUOBANYUN_TOKEN"),

View File

@ -31,6 +31,7 @@ async def on_startup() -> None:
setup_logging(LOG_DIR) setup_logging(LOG_DIR)
approval_service = ApprovalSyncService() approval_service = ApprovalSyncService()
await approval_service.ensure_unsubscribe_once()
await approval_service.ensure_approval_subscription() await approval_service.ensure_approval_subscription()
async def handler(payload: dict) -> None: async def handler(payload: dict) -> None:

View File

@ -63,10 +63,39 @@ class ApprovalSyncService:
return "" if value is None else str(value) return "" if value is None else str(value)
return "" return ""
def _extract_payment_type_from_detail(self, detail: Dict[str, Any]) -> str:
data = detail.get("data", {})
form = data.get("form")
if isinstance(form, str):
try:
import json
form = json.loads(form)
except Exception:
form = None
if isinstance(form, list):
for item in form:
if not isinstance(item, dict):
continue
code = (
item.get("custom_id")
or item.get("id")
or item.get("field_code")
or item.get("code")
)
if code == "type":
value = item.get("value") or item.get("text") or item.get("name")
if isinstance(value, list) and value:
return str(value[0])
return "" if value is None else str(value)
if isinstance(form, dict):
value = form.get("type")
if isinstance(value, list) and value:
return str(value[0])
return "" if value is None else str(value)
return ""
async def ensure_approval_subscription(self) -> None: async def ensure_approval_subscription(self) -> None:
codes = self.settings.feishu_approval_codes or ( codes = [self.settings.feishu_approval_code] if self.settings.feishu_approval_code else []
[self.settings.feishu_approval_code] if self.settings.feishu_approval_code else []
)
if not codes: if not codes:
logger.error("未配置 FEISHU_APPROVAL_CODE无法订阅审批定义") logger.error("未配置 FEISHU_APPROVAL_CODE无法订阅审批定义")
return return
@ -82,14 +111,30 @@ class ApprovalSyncService:
except Exception as exc: except Exception as exc:
logger.error("订阅审批定义异常: %s", exc) logger.error("订阅审批定义异常: %s", exc)
async def ensure_unsubscribe_once(self) -> None:
if not self.settings.feishu_unsubscribe_once:
return
codes = self.settings.feishu_unsubscribe_codes
if not codes:
logger.error("未配置 FEISHU_UNSUBSCRIBE_CODES无法取消订阅")
return
for approval_code in codes:
try:
resp = await self.feishu_client.unsubscribe_approval(approval_code)
code = resp.get("code")
msg = resp.get("msg")
if code in (0, 1390007):
logger.info("取消订阅成功或不存在: %s", msg)
else:
logger.error("取消订阅失败: code=%s msg=%s", code, msg)
except Exception as exc:
logger.error("取消订阅异常: %s", exc)
async def handle_approval_event(self, event: Dict[str, Any]) -> None: async def handle_approval_event(self, event: Dict[str, Any]) -> None:
payload = event.get("event", event) payload = event.get("event", event)
logger.info("审批事件内容: %s", payload) logger.info("审批事件内容: %s", payload)
approval_code = payload.get("approval_code") or payload.get("approvalCode") approval_code = payload.get("approval_code") or payload.get("approvalCode")
codes = self.settings.feishu_approval_codes or ( if self.settings.feishu_approval_code and approval_code != self.settings.feishu_approval_code:
[self.settings.feishu_approval_code] if self.settings.feishu_approval_code else []
)
if codes and approval_code not in codes:
logger.info("审批定义不匹配,跳过: %s", approval_code) logger.info("审批定义不匹配,跳过: %s", approval_code)
return return
@ -137,6 +182,11 @@ class ApprovalSyncService:
return return
logger.info("提取项目单号: %s", project_no) logger.info("提取项目单号: %s", project_no)
payment_type = self._extract_payment_type_from_detail(detail)
if not payment_type:
logger.error("未能从审批详情提取付款类型")
return
item_id = await self.huobanyun_service.find_item_by_project_no(project_no) item_id = await self.huobanyun_service.find_item_by_project_no(project_no)
if not item_id: if not item_id:
logger.error("未找到对应伙伴云项目: %s", project_no) logger.error("未找到对应伙伴云项目: %s", project_no)
@ -144,9 +194,7 @@ class ApprovalSyncService:
logger.info("找到伙伴云项目: %s", item_id) logger.info("找到伙伴云项目: %s", item_id)
try: try:
await self.huobanyun_service.update_order_status(item_id) await self.huobanyun_service.update_linked_flags(item_id, payment_type)
if approval_code:
await self.huobanyun_service.update_linked_flags(item_id, approval_code)
logger.info("审批完成回写成功: %s", item_id) logger.info("审批完成回写成功: %s", item_id)
except Exception as exc: except Exception as exc:
logger.error("审批完成回写失败: %s", exc) logger.error("审批完成回写失败: %s", exc)

View File

@ -45,6 +45,8 @@ class HuobanyunService:
"项目名称": "2200000150711223", "项目名称": "2200000150711223",
"订单状态": "2200000150497330", "订单状态": "2200000150497330",
"下单金额": "2200000149785349", "下单金额": "2200000149785349",
"对公返点金额": "2200000149785350",
"对私返点金额": "2200000149785351",
"是否已关联对公付款审批": "2200000589775224", "是否已关联对公付款审批": "2200000589775224",
"是否已关联对私付款审批": "2200000589775228", "是否已关联对私付款审批": "2200000589775228",
"平台": "2200000149785346", "平台": "2200000149785346",
@ -72,24 +74,11 @@ class HuobanyunService:
return "" return ""
return str(items[0].get("item_id", "")) return str(items[0].get("item_id", ""))
async def update_order_status(self, item_id: str) -> None: async def update_linked_flags(self, item_id: str, payment_type: str) -> None:
status_field_id = self._resolve_field_key("订单状态")
status_id = self.settings.huobanyun_order_status_done_id
status_name = self.settings.huobanyun_order_status_done_name or "已完成"
status_value = status_id or status_name
if not status_value:
logger.error("未配置订单状态已完成选项值,跳过更新")
return
payload = {"fields": {status_field_id: [str(status_value)]}}
await self.client.update_item(item_id, payload)
async def update_linked_flags(self, item_id: str, approval_code: str) -> None:
public_codes = set(self.settings.feishu_approval_code_public)
private_codes = set(self.settings.feishu_approval_code_private)
fields: Dict[str, Any] = {} fields: Dict[str, Any] = {}
if approval_code in public_codes: if payment_type == "对公付款":
fields[self._resolve_field_key("是否已关联对公付款审批")] = True fields[self._resolve_field_key("是否已关联对公付款审批")] = True
if approval_code in private_codes: if payment_type == "对私付款":
fields[self._resolve_field_key("是否已关联对私付款审批")] = True fields[self._resolve_field_key("是否已关联对私付款审批")] = True
if not fields: if not fields:
return return
@ -235,14 +224,6 @@ class HuobanyunService:
else: else:
offset = 0 offset = 0
approval_code = (
req.approval_code
or raw.get("approval_code")
or raw.get("approvalCode")
or (req.linkage_params or {}).get("approval_code")
or (req.linkage_params or {}).get("approvalCode")
or ""
)
token_key = req.token or "" token_key = req.token or ""
key = req.key or raw.get("key") or raw.get("field") or token_key or "" key = req.key or raw.get("key") or raw.get("field") or token_key or ""
approval_hint = "" approval_hint = ""
@ -256,6 +237,9 @@ class HuobanyunService:
key = self._resolve_field_key(str(key).strip()) if key else "" key = self._resolve_field_key(str(key).strip()) if key else ""
query_value = req.query or req.keyword or "" query_value = req.query or req.keyword or ""
linkage_params = req.linkage_params or raw.get("linkage_params") or {} linkage_params = req.linkage_params or raw.get("linkage_params") or {}
payment_type = ""
if isinstance(linkage_params, dict):
payment_type = linkage_params.get("type") or linkage_params.get("付款类型") or ""
linkage_project_no = linkage_params.get("项目单号") if isinstance(linkage_params, dict) else None linkage_project_no = linkage_params.get("项目单号") if isinstance(linkage_params, dict) else None
logger.info("外部选项请求: token_key=%s linkage_project_no=%s", key, linkage_project_no) logger.info("外部选项请求: token_key=%s linkage_project_no=%s", key, linkage_project_no)
@ -266,21 +250,20 @@ class HuobanyunService:
{"field": project_no_field_id, "query": {"em": False}}, {"field": project_no_field_id, "query": {"em": False}},
{"field": order_status_field, "query": {"ne": ["已完成"]}}, {"field": order_status_field, "query": {"ne": ["已完成"]}},
] ]
if approval_code or approval_hint: if payment_type == "对公付款" or approval_hint == "public":
if approval_hint == "public" or approval_code in set(self.settings.feishu_approval_code_public): base_filters.append(
base_filters.append( {
{ "field": self._resolve_field_key("是否已关联对公付款审批"),
"field": self._resolve_field_key("是否已关联对公付款审批"), "query": {"eq": False},
"query": {"eq": False}, }
} )
) if payment_type == "对私付款" or approval_hint == "private":
if approval_hint == "private" or approval_code in set(self.settings.feishu_approval_code_private): base_filters.append(
base_filters.append( {
{ "field": self._resolve_field_key("是否已关联对私付款审批"),
"field": self._resolve_field_key("是否已关联对私付款审批"), "query": {"eq": False},
"query": {"eq": False}, }
} )
)
if linkage_project_no: if linkage_project_no:
base_filters.append( base_filters.append(
{ {