Compare commits

...

2 Commits

Author SHA1 Message Date
a688d3d562 fix: defer payment provider init to load_unipay(), graceful failure
- Move env var reading and key file open() from import-time CONF dict
  into _build_provider_conf() called during load_unipay()
- Each provider is wrapped in try/except — if env vars are missing,
  key files don't exist, or instantiation fails, the provider is
  set to None (disabled) and a warning is printed
- Program continues to start even if all payment channels fail
- Existing code already checks PROVIDERS[name] is None in all
  business functions (create_payment, notify handlers, etc.)
- Existing get_provider() in notify.py already returns None on
  instantiation error
2026-05-26 16:47:46 +08:00
b40300ad35 feat: add json table definitions for all models (converted from xlsx) 2026-05-21 12:46:36 +08:00
5 changed files with 367 additions and 25 deletions

36
models/paychannel.json Normal file
View File

@ -0,0 +1,36 @@
{
"summary": [
{
"name": "paychannel",
"title": "支付渠道",
"primary": [
"id"
],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "支付渠道名称",
"type": "str",
"length": 255
},
{
"name": "enabled_date",
"title": "启用日期",
"type": "date"
},
{
"name": "expired_date",
"title": "禁用日期",
"type": "date"
}
]
}

49
models/payfee.json Normal file
View File

@ -0,0 +1,49 @@
{
"summary": [
{
"name": "payfee",
"title": "支付费率",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "channelid",
"title": "支付渠道id",
"type": "str",
"length": 255
},
{
"name": "fee_rate",
"title": "费率",
"type": "float",
"length": 18
},
{
"name": "enabled_date",
"title": "启用日期",
"type": "date"
},
{
"name": "expired_date",
"title": "停用日期",
"type": "date"
}
],
"codes": [
{
"field": "channelid",
"table": "paychannel",
"valuefield": "id",
"textfield": "name"
}
]
}

141
models/payment_log.json Normal file
View File

@ -0,0 +1,141 @@
{
"summary": [
{
"name": "payment_log",
"title": "支付日志",
"primary": [
"id"
],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "customerid",
"title": "客户id",
"type": "str",
"length": 32
},
{
"name": "channelid",
"title": "支付渠道id",
"type": "str",
"length": 32
},
{
"name": "channel_trade_id",
"title": "支付渠道交易ID",
"type": "str",
"length": 256
},
{
"name": "payment_name",
"title": "支付名称",
"type": "str",
"length": 255
},
{
"name": "amount_total",
"title": "支付金额",
"type": "float",
"length": 20
},
{
"name": "pay_feerate",
"title": "支付费率",
"type": "float",
"length": 20
},
{
"name": "pay_fee",
"title": "支付费用",
"type": "float",
"length": 20
},
{
"name": "payer_client_ip",
"title": "支付客户ip",
"type": "str",
"length": 255
},
{
"name": "currency",
"title": "币种",
"type": "str",
"length": 10
},
{
"name": "payment_status",
"title": "支付状态",
"type": "str",
"length": 1
},
{
"name": "init_timestamp",
"title": "支付发起时点",
"type": "timestamp"
},
{
"name": "payed_timestamp",
"title": "支付完成时点",
"type": "timestamp"
},
{
"name": "cancel_timestamp",
"title": "支付取消时点",
"type": "timestamp"
},
{
"name": "userid",
"title": "支付操作用户id",
"type": "str",
"length": 32
},
{
"name": "origin_id",
"title": "原支付id",
"type": "str",
"length": 32
}
],
"codes": [
{
"field": "currency",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='currency'"
},
{
"field": "payment_status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='payment_status'"
},
{
"field": "customerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
},
{
"field": "channelid",
"table": "paychannel",
"valuefield": "id",
"textfield": "name"
},
{
"field": "userid",
"table": "users",
"valuefield": "id",
"textfield": "username"
}
]
}

62
models/transfercode.json Normal file
View File

@ -0,0 +1,62 @@
{
"summary": [
{
"name": "transfercode",
"title": "转账码",
"primary": [
"id"
],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "customerid",
"title": "客户id",
"type": "str",
"length": 32
},
{
"name": "amount",
"title": "金额",
"type": "float",
"length": 20
},
{
"name": "tcode",
"title": "转账码",
"type": "str",
"length": 7
},
{
"name": "curtime",
"title": "发起时间",
"type": "timestamp"
},
{
"name": "curdate",
"title": "发起日期",
"type": "date"
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 1
}
],
"codes": [
{
"field": "customerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
}
]
}

View File

@ -1,5 +1,6 @@
# init.py # init.py
import os import os
import traceback
from appPublic.log import debug,exception from appPublic.log import debug,exception
from ahserver.configuredServer import add_startup from ahserver.configuredServer import add_startup
from ahserver.serverenv import ServerEnv from ahserver.serverenv import ServerEnv
@ -7,40 +8,85 @@ from .notify import get_provider, get_provider_channel
from .paylog import PaymentLog, unipay_accounting from .paylog import PaymentLog, unipay_accounting
from .payfee import get_pay_fee, sor_get_pay_fee, get_paychannels, get_pay_feerate from .payfee import get_pay_fee, sor_get_pay_fee, get_paychannels, get_pay_feerate
# 从 env 或配置载入 provider conf这里只示例 # ──────────────────────────────────────────────
CONF = { # 延迟加载:只存储 env key 名称,不在此处打开文件
"transfer":{ # 避免 import 阶段因文件缺失或 env 未设置而崩溃
"pop3server": os.getenv("POP3SERVER", ""), # ──────────────────────────────────────────────
"mail": os.getenv("MAIL", ""), CONF_ENV = {
"password": os.getenv("PASSWORD", ""), "transfer": {
"from_mail": os.getenv("FROM_MAIL", ""), "pop3server": "POP3SERVER",
"account_no": os.getenv("ACCOUNT_NO", "") "mail": "MAIL",
"password": "PASSWORD",
"from_mail": "FROM_MAIL",
"account_no": "ACCOUNT_NO"
}, },
"wechat": { "wechat": {
"mchid": os.getenv("WXP_MCHID",""), "mchid": "WXP_MCHID",
"appid": os.getenv("WXP_APPID", ""), "appid": "WXP_APPID",
"cert_serial_no": os.getenv("WXP_SERIAL",""), "cert_serial_no": "WXP_SERIAL",
"private_key_pem": open(os.getenv("WXP_PRIVKEY","./merchant_private_key.pem"),"rb").read(), "private_key_pem_file": "WXP_PRIVKEY",
"api_v3_key": os.getenv("WXP_API_V3_KEY","") "api_v3_key": "WXP_API_V3_KEY"
}, },
"paypal": { "paypal": {
"client_id": os.getenv("PP_ID",""), "client_id": "PP_ID",
"client_secret": os.getenv("PP_SECRET",""), "client_secret": "PP_SECRET",
"sandbox": True "sandbox": True # 静态值
}, },
"alipay": { "alipay": {
"app_id": os.getenv("ALIPAY_APPID",""), "app_id": "ALIPAY_APPID",
"app_private_key_pem": open(os.getenv("ALIPAY_PRIV","./alipay_priv.pem"),"rb").read(), "app_private_key_pem_file": "ALIPAY_PRIV",
"alipay_public_key_pem": open(os.getenv("ALIPAY_PUB",""),"rb").read() "alipay_public_key_pem_file": "ALIPAY_PUB"
}, },
"stripe": { "stripe": {
"api_key": os.getenv("STRIPE_KEY","") "api_key": "STRIPE_KEY"
} }
} }
PROVIDERS = {} PROVIDERS = {}
def _build_provider_conf(provider_name: str) -> dict:
"""从环境变量构造 provider 配置。打开文件、处理默认值。
如果关键环境变量缺失或文件打不开抛异常由调用方处理"""
conf_def = CONF_ENV.get(provider_name, {})
conf = {}
for key, env_name in conf_def.items():
if isinstance(env_name, bool):
conf[key] = env_name
continue
val = os.getenv(env_name, "")
conf[key] = val
# 特殊处理:读取密钥文件
if provider_name == "wechat":
privkey_path = conf.get("private_key_pem_file", "")
if not privkey_path:
raise FileNotFoundError(f"环境变量 WXP_PRIVKEY 未设置")
with open(privkey_path, "rb") as f:
conf["private_key_pem"] = f.read()
del conf["private_key_pem_file"]
elif provider_name == "alipay":
priv_path = conf.get("app_private_key_pem_file", "")
pub_path = conf.get("alipay_public_key_pem_file", "")
if not priv_path:
raise FileNotFoundError(f"环境变量 ALIPAY_PRIV 未设置")
if not pub_path:
raise FileNotFoundError(f"环境变量 ALIPAY_PUB 未设置")
with open(priv_path, "rb") as f:
conf["app_private_key_pem"] = f.read()
with open(pub_path, "rb") as f:
conf["alipay_public_key_pem"] = f.read()
del conf["app_private_key_pem_file"]
del conf["alipay_public_key_pem_file"]
return conf
# ──────────────────────────────────────────────
# 业务函数(不变)
# ──────────────────────────────────────────────
# 下单接口(统一) # 下单接口(统一)
async def create_payment(request, params_kw=None): async def create_payment(request, params_kw=None):
env = request._run_ns env = request._run_ns
@ -189,11 +235,19 @@ async def setup_callback_path(app):
# callback url= "/unipay/notify/{provider}" # callback url= "/unipay/notify/{provider}"
def load_unipay(): def load_unipay():
PROVIDERS["transfer"] = get_provider("transfer", CONF["transfer"]) """注册各支付渠道到 PROVIDERS。初始化失败的渠道设为 Nonedisabled"""
PROVIDERS["wechat"] = get_provider("wechat", CONF["wechat"]) for name in ("transfer", "wechat", "paypal", "alipay", "stripe"):
PROVIDERS["paypal"] = get_provider("paypal", CONF["paypal"]) try:
PROVIDERS["alipay"] = get_provider("alipay", CONF["alipay"]) conf = _build_provider_conf(name)
PROVIDERS["stripe"] = get_provider("stripe", CONF["stripe"]) PROVIDERS[name] = get_provider(name, conf)
if PROVIDERS[name] is not None:
print(f"[unipay] {name} 初始化成功")
else:
print(f"[unipay] {name} 初始化返回 None渠道 disabled")
except Exception as e:
PROVIDERS[name] = None
print(f"[unipay] {name} 初始化失败,已禁用: {e}", flush=True)
env = ServerEnv() env = ServerEnv()
env.get_paychannels = get_paychannels env.get_paychannels = get_paychannels
env.get_pay_feerate = get_pay_feerate env.get_pay_feerate = get_pay_feerate