2026-02-10 14:23:29 +08:00
|
|
|
|
"""
|
|
|
|
|
|
支付模块配置
|
|
|
|
|
|
从环境变量读取微信支付和支付宝的商户密钥等配置
|
|
|
|
|
|
"""
|
|
|
|
|
|
import os
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class WeChatPayConfig:
|
|
|
|
|
|
"""微信支付配置"""
|
|
|
|
|
|
app_id: str = "" # 应用 APPID
|
|
|
|
|
|
mch_id: str = "" # 商户号
|
|
|
|
|
|
api_v3_key: str = "" # APIv3 密钥
|
|
|
|
|
|
private_key_path: str = "" # 商户 API 私钥文件路径(与 private_key 二选一)
|
|
|
|
|
|
private_key: str = "" # 商户 API 私钥内容(PEM 字符串,可放环境变量,与 private_key_path 二选一)
|
|
|
|
|
|
cert_serial_no: str = "" # 商户证书序列号
|
|
|
|
|
|
notify_url: str = "" # 支付结果回调地址
|
2026-02-11 16:06:15 +08:00
|
|
|
|
# 平台公钥模式(二选一):当环境无法访问 api.mch.weixin.qq.com 时使用,无需拉取平台证书
|
|
|
|
|
|
platform_public_key: str = "" # 微信支付平台公钥 PEM 内容(与 platform_public_key_path 二选一)
|
|
|
|
|
|
platform_public_key_path: str = "" # 平台公钥文件路径
|
|
|
|
|
|
platform_public_key_id: str = "" # 微信支付平台公钥 ID(与 platform_public_key 同时配置)
|
2026-02-10 14:23:29 +08:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def is_configured(self) -> bool:
|
|
|
|
|
|
has_key = bool(self.private_key.strip()) or bool(self.private_key_path.strip())
|
|
|
|
|
|
return all([
|
|
|
|
|
|
self.app_id, self.mch_id, self.api_v3_key,
|
|
|
|
|
|
has_key, self.cert_serial_no, self.notify_url
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-02-11 16:06:15 +08:00
|
|
|
|
@property
|
|
|
|
|
|
def use_platform_public_key(self) -> bool:
|
|
|
|
|
|
"""是否使用平台公钥模式(无需访问微信拉取证书)"""
|
|
|
|
|
|
has_pub = bool(self.platform_public_key.strip()) or bool(self.platform_public_key_path.strip())
|
|
|
|
|
|
return has_pub and bool(self.platform_public_key_id.strip())
|
|
|
|
|
|
|
2026-02-10 14:23:29 +08:00
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AlipayConfig:
|
|
|
|
|
|
"""支付宝配置"""
|
|
|
|
|
|
app_id: str = "" # 应用 APPID
|
|
|
|
|
|
private_key: str = "" # 应用私钥(字符串内容)
|
|
|
|
|
|
alipay_public_key: str = "" # 支付宝公钥(字符串内容)
|
|
|
|
|
|
notify_url: str = "" # 支付结果回调地址
|
|
|
|
|
|
sign_type: str = "RSA2" # 签名类型,默认 RSA2
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def is_configured(self) -> bool:
|
|
|
|
|
|
return all([
|
|
|
|
|
|
self.app_id, self.private_key,
|
|
|
|
|
|
self.alipay_public_key, self.notify_url
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class PaymentConfig:
|
|
|
|
|
|
"""支付模块总配置"""
|
|
|
|
|
|
wechat: WeChatPayConfig = field(default_factory=WeChatPayConfig)
|
|
|
|
|
|
alipay: AlipayConfig = field(default_factory=AlipayConfig)
|
|
|
|
|
|
# 支付宝是否视为「开发中」不可用(默认 True,上线时设 ALIPAY_UNDER_DEVELOPMENT=false)
|
|
|
|
|
|
alipay_under_development: bool = True
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def from_env(cls) -> "PaymentConfig":
|
|
|
|
|
|
"""从环境变量加载配置"""
|
2026-02-11 16:06:15 +08:00
|
|
|
|
# 微信私钥:优先使用 WECHAT_PAY_PRIVATE_KEY_PATH(文件),避免 .env 中长 PEM 的转义问题;否则用 WECHAT_PAY_PRIVATE_KEY
|
|
|
|
|
|
wechat_private_key_path = os.getenv("WECHAT_PAY_PRIVATE_KEY_PATH", "").strip()
|
2026-02-10 14:23:29 +08:00
|
|
|
|
wechat_private_key = os.getenv("WECHAT_PAY_PRIVATE_KEY", "").strip()
|
2026-02-11 16:06:15 +08:00
|
|
|
|
# 去除可能被解析进来的引号与 BOM
|
|
|
|
|
|
if wechat_private_key:
|
|
|
|
|
|
wechat_private_key = wechat_private_key.strip('"').strip("'").lstrip("\ufeff")
|
2026-02-10 14:23:29 +08:00
|
|
|
|
wechat_private_key = wechat_private_key.replace("\\n", "\n")
|
2026-02-11 16:06:15 +08:00
|
|
|
|
wechat_platform_pub = os.getenv("WECHAT_PAY_PLATFORM_PUBLIC_KEY", "").strip()
|
|
|
|
|
|
if wechat_platform_pub and "\\n" in wechat_platform_pub:
|
|
|
|
|
|
wechat_platform_pub = wechat_platform_pub.replace("\\n", "\n")
|
2026-02-10 14:23:29 +08:00
|
|
|
|
config = cls(
|
|
|
|
|
|
wechat=WeChatPayConfig(
|
|
|
|
|
|
app_id=os.getenv("WECHAT_PAY_APP_ID", ""),
|
|
|
|
|
|
mch_id=os.getenv("WECHAT_PAY_MCH_ID", ""),
|
|
|
|
|
|
api_v3_key=os.getenv("WECHAT_PAY_API_V3_KEY", ""),
|
2026-02-11 16:06:15 +08:00
|
|
|
|
private_key_path=wechat_private_key_path,
|
|
|
|
|
|
private_key=wechat_private_key if not wechat_private_key_path else "", # 有路径时不再用 env 内容
|
2026-02-10 14:23:29 +08:00
|
|
|
|
cert_serial_no=os.getenv("WECHAT_PAY_CERT_SERIAL_NO", ""),
|
|
|
|
|
|
notify_url=os.getenv("WECHAT_PAY_NOTIFY_URL", ""),
|
2026-02-11 16:06:15 +08:00
|
|
|
|
platform_public_key=wechat_platform_pub,
|
|
|
|
|
|
platform_public_key_path=os.getenv("WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH", "").strip(),
|
|
|
|
|
|
platform_public_key_id=os.getenv("WECHAT_PAY_PLATFORM_PUBLIC_KEY_ID", "").strip(),
|
2026-02-10 14:23:29 +08:00
|
|
|
|
),
|
|
|
|
|
|
alipay=AlipayConfig(
|
|
|
|
|
|
app_id=os.getenv("ALIPAY_APP_ID", ""),
|
|
|
|
|
|
private_key=os.getenv("ALIPAY_PRIVATE_KEY", ""),
|
|
|
|
|
|
alipay_public_key=os.getenv("ALIPAY_PUBLIC_KEY", ""),
|
|
|
|
|
|
notify_url=os.getenv("ALIPAY_NOTIFY_URL", ""),
|
|
|
|
|
|
sign_type=os.getenv("ALIPAY_SIGN_TYPE", "RSA2"),
|
|
|
|
|
|
),
|
|
|
|
|
|
alipay_under_development=os.getenv("ALIPAY_UNDER_DEVELOPMENT", "true").lower() in ("true", "1", "yes"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 日志输出配置状态
|
|
|
|
|
|
if config.wechat.is_configured:
|
2026-02-11 16:06:15 +08:00
|
|
|
|
mode = "平台公钥模式" if config.wechat.use_platform_public_key else "平台证书模式"
|
|
|
|
|
|
key_src = "WECHAT_PAY_PRIVATE_KEY(环境变量)" if config.wechat.private_key.strip() else "WECHAT_PAY_PRIVATE_KEY_PATH(文件)"
|
|
|
|
|
|
logger.info(f"微信支付配置已加载: APP_ID={config.wechat.app_id}, MCH_ID={config.wechat.mch_id}, 模式={mode}, 商户私钥={key_src}")
|
2026-02-10 14:23:29 +08:00
|
|
|
|
else:
|
|
|
|
|
|
logger.warning("微信支付配置不完整,微信支付将不可用")
|
|
|
|
|
|
|
|
|
|
|
|
if config.alipay.is_configured:
|
|
|
|
|
|
logger.info(f"支付宝配置已加载: APP_ID={config.alipay.app_id}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning("支付宝配置不完整,支付宝支付将不可用")
|
|
|
|
|
|
|
|
|
|
|
|
return config
|