ForcePilot/backend/package/yuxi/channel/extensions/wechatpay_notify/config.py
Kris 87a8931db3 feat(channel): 添加微信客服、微信公众号和微信支付通知渠道扩展
新增微信客服、微信公众号、微信支付通知三个渠道扩展。

微信客服渠道扩展功能模块:
- account: 账户管理
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- crypto: 加解密处理
- dedupe: 消息去重
- customer: 客户管理
- servicer: 客服管理
- session: 会话管理
- status: 会话状态管理
- media: 媒体资源处理
- statistics: 统计功能
- sync: 数据同步
- upgrade: 升级处理

微信公众号渠道扩展功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- crypto: 加解密处理
- dedupe: 消息去重
- passive_reply: 被动回复
- message: 消息处理
- broadcast: 群发消息
- template: 模板消息
- menu: 菜单管理
- qrcode: 二维码管理
- user: 用户管理
- media: 媒体资源处理
- status: 会话状态管理

微信支付通知渠道扩展功能模块:
- config: 渠道配置管理
- webhook: Webhook 事件处理
- crypto: 加解密与签名校验
- cert_manager: 证书管理
- event_router: 事件路由
- dedupe: 消息去重
- pay_repo: 支付数据仓库
- query_client: 查询客户端
- arq_tasks: 异步任务
- callback_compensator: 回调补偿
2026-05-21 12:00:30 +08:00

170 lines
6.4 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.

from __future__ import annotations
import logging
import os
logger = logging.getLogger(__name__)
WECHATPAY_NOTIFY_CONFIG_SCHEMA = {
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"title": "微信支付通知配置",
"properties": {
"enabled": {"type": "boolean", "default": True, "title": "启用"},
"mch_id": {"type": "string", "title": "微信商户号 (mchid)"},
"mch_name": {"type": "string", "title": "商户名称"},
"api_v3_key": {
"type": "string",
"title": "APIv3 密钥 (32字节hex)",
"x-ui-password": True,
},
"serial_no": {"type": "string", "title": "商户API证书序列号"},
"private_key_path": {
"type": "string",
"title": "商户私钥文件路径",
"description": "apiclient_key.pem 的绝对路径",
},
"app_id": {"type": "string", "title": "关联 AppID"},
"cert_mode": {
"type": "string",
"title": "验签模式",
"enum": ["platform_cert", "public_key"],
"default": "platform_cert",
},
"public_key_pem": {
"type": "string",
"title": "微信支付公钥 (PEM)",
"description": "公钥模式下必填,商户平台下载的公钥文件内容",
},
"public_key_id": {
"type": "string",
"title": "公钥ID",
"description": "公钥模式下必填PUB_KEY_ID_ 开头的ID",
},
"accounts": {
"type": "object",
"title": "多商户配置",
"additionalProperties": {
"type": "object",
"properties": {
"mch_id": {"type": "string", "title": "微信商户号"},
"mch_name": {"type": "string", "title": "商户名称"},
"api_v3_key": {"type": "string", "title": "APIv3密钥", "x-ui-password": True},
"serial_no": {"type": "string", "title": "证书序列号"},
"private_key_path": {"type": "string", "title": "私钥路径"},
"app_id": {"type": "string", "title": "关联 AppID"},
"cert_mode": {
"type": "string",
"title": "验签模式",
"enum": ["platform_cert", "public_key"],
"default": "platform_cert",
},
"public_key_pem": {"type": "string", "title": "微信支付公钥 (PEM)"},
"public_key_id": {"type": "string", "title": "公钥ID"},
},
},
},
},
}
class WechatPayConfigAdapter:
def __init__(self):
self._config: dict = {}
def list_account_ids(self, config: dict) -> list[str]:
self._config = config
wp_cfg = self._get_wechatpay_config()
accounts = wp_cfg.get("accounts", {}) if isinstance(wp_cfg, dict) else {}
if accounts:
return list(accounts.keys())
return ["default"]
async def resolve_account(self, account_id: str) -> dict:
wp_cfg = self._get_wechatpay_config()
if account_id == "default" or account_id not in wp_cfg.get("accounts", {}):
raw = wp_cfg if isinstance(wp_cfg, dict) else {}
else:
raw = wp_cfg.get("accounts", {}).get(account_id, {})
private_key = self._load_private_key(
account_id,
raw.get("private_key_path", ""),
)
return {
"account_id": account_id,
"mch_id": raw.get("mch_id", ""),
"mch_name": raw.get("mch_name", account_id),
"api_v3_key": raw.get("api_v3_key", ""),
"serial_no": raw.get("serial_no", ""),
"private_key_pem": private_key,
"app_id": raw.get("app_id", ""),
"enabled": raw.get("enabled", True),
"cert_mode": raw.get("cert_mode", "platform_cert"),
"public_key_pem": raw.get("public_key_pem", ""),
"public_key_id": raw.get("public_key_id", ""),
}
def is_configured(self, account: dict) -> bool:
return bool(
account.get("mch_id")
and account.get("api_v3_key")
and account.get("serial_no")
and account.get("private_key_pem")
)
def is_enabled(self, account: dict) -> bool:
return account.get("enabled", True) and self.is_configured(account)
def disabled_reason(self, account: dict) -> str:
missing = []
if not account.get("mch_id"):
missing.append("商户号 (mch_id)")
if not account.get("api_v3_key"):
missing.append("APIv3 密钥")
if not account.get("serial_no"):
missing.append("证书序列号")
if not account.get("private_key_pem"):
missing.append("商户私钥")
if account.get("cert_mode") == "public_key":
if not account.get("public_key_pem"):
missing.append("微信支付公钥 (public_key_pem)")
if not account.get("public_key_id"):
missing.append("公钥ID (public_key_id)")
if missing:
return f"缺少配置: {', '.join(missing)}"
return ""
def describe_account(self, account: dict) -> dict:
return {
"account_id": account.get("account_id", ""),
"mch_id": account.get("mch_id", ""),
"mch_name": account.get("mch_name", ""),
"configured": self.is_configured(account),
}
def config_schema(self) -> dict:
return WECHATPAY_NOTIFY_CONFIG_SCHEMA
def _get_wechatpay_config(self) -> dict:
channels = self._config.get("channels", {}) if isinstance(self._config, dict) else {}
return channels.get("wechatpay_notify", {}) if isinstance(channels, dict) else {}
@staticmethod
def _load_private_key(account_id: str, file_path: str) -> str:
env_key = os.environ.get(f"WECHATPAY_PRIVATE_KEY_{account_id.upper()}", "")
if env_key:
return env_key
if file_path and os.path.exists(file_path):
try:
with open(file_path, encoding="utf-8") as f:
return f.read()
except Exception:
logger.exception("Failed to read private key: %s", file_path)
env_key_default = os.environ.get("WECHATPAY_PRIVATE_KEY", "")
return env_key_default