unipay/unipay/init.py
2026-07-17 10:36:15 +08:00

271 lines
8.6 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.

# init.py
import os
import asyncio
import traceback
from appPublic.log import debug,exception
from ahserver.configuredServer import add_startup
from ahserver.serverenv import ServerEnv
from .notify import get_provider, get_provider_channel
from .paylog import PaymentLog, unipay_accounting
from .payfee import get_pay_fee, sor_get_pay_fee, get_paychannels, get_pay_feerate
# ──────────────────────────────────────────────
# 延迟加载:只存储 env key 名称,不在此处打开文件
# 避免 import 阶段因文件缺失或 env 未设置而崩溃
# ──────────────────────────────────────────────
CONF_ENV = {
"transfer": {
"pop3server": "POP3SERVER",
"email": "EMAIL",
"password": "PASSWORD",
"from_mail": "FROM_MAIL",
"account_no": "ACCOUNT_NO",
"account_name": "ACCOUNT_NAME",
"bank_name": "BANK_NAME",
"bank_branch": "BANK_BRANCH"
},
"wechat": {
"mchid": "WXP_MCHID",
"appid": "WXP_APPID",
"cert_serial_no": "WXP_SERIAL",
"private_key_pem_file": "WXP_PRIVKEY",
"api_v3_key": "WXP_API_V3_KEY"
},
"paypal": {
"client_id": "PP_ID",
"client_secret": "PP_SECRET",
"sandbox": True # 静态值
},
"alipay": {
"app_id": "ALIPAY_APPID",
"app_private_key_pem_file": "ALIPAY_PRIV",
"alipay_public_key_pem_file": "ALIPAY_PUB"
},
"stripe": {
"api_key": "STRIPE_KEY"
}
}
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):
env = request._run_ns
if params_kw is None:
params_kw = request.params_kw
data = params_kw
data.request = request
provider = data.get("provider")
if provider not in PROVIDERS:
debug(f'{provider=} is not a valid payment channel')
return {"error":"unknown provider"}
try:
if PROVIDERS[provider] is None:
e = Exception(f'{provider} cannot pay')
exception(f'{e}')
raise e
notify_url = env.entire_url(f'notify/{provider}')
pl = PaymentLog(request._run_ns)
feerate = await get_pay_fee(provider, data.amount)
fee = feerate * data.amount
channel = get_provider_channel(provider)
userid = await env.get_user()
orgid = await env.get_userorgid()
client_ip = request['client_ip']
# userid, customerid, channel, payment_name, amount, client_ip, currency='CNY'
payment_name = data.payment_name or "充值"
amount = data.amount
currency = data.currency
plog = await pl.new_log(userid, orgid, provider,
amount,
feerate, client_ip, currency=currency)
if plog:
data.out_trade_no = plog.id
data.customerid = orgid
data.userid = userid
data.payment_name = payment_name
data.notify_url = notify_url
data.client_ip = client_ip
res = await PROVIDERS[provider].create_payment(data)
debug(f'{provider=} instance return {res}')
return res
raise Exception('write payment_log error')
except Exception as e:
exception(f'create_payment():{params_kw=}, {e}')
raise e
# 查询
async def query_payment(request, params_kw=None):
if params_kw is None:
params_kw = request.params_kw
data = params_kw
provider = data.get("provider")
if provider not in PROVIDERS:
return {"error":"unknown provider"}
try:
if PROVIDERS[provider] is None:
e = Exception(f'{provider} cannot pay')
exception(f'{e}')
raise e
res = await PROVIDERS[provider].query(data)
return res
except Exception as e:
exception(f'query_payment():{params_kw}, {e}')
raise e
# 退款
async def refund_payment(request, params_kw=None):
if params_kw is None:
params_kw = request.params_kw
data = await get_refundable_plog(request, params_kw.id)
if data is None:
return None
if params_kw.amount >= data.total_amount:
return None
provider = data.channelid
if provider not in PROVIDERS:
return {"error":"unknown provider"}
try:
plog = await new_refund_log(request, data.amount, data.id)
if PROVIDERS[provider] is None:
e = Exception(f'{provider} cannot pay')
exception(f'{e}')
raise e
plog.out_trade_no = plog.origin_id
plog.out_request_no = plog.id
plog.total_amount = params_kw.amount
plog.refund_amount = plog.total_amount
plog.notify_url = request.env.entire_url(f'notify/{provider}')
res = await PROVIDERS[provider].refund(plog)
return res
except Exception as e:
exception(f'query_payment():{params_kw}, {e}')
raise e
# 微信支付回调入口
async def wechat_notify(request):
debug("wechat notify called .......")
Response = ServerEnv().Response
provider = 'wechat'
if PROVIDERS[provider] is None:
e = Exception(f'{provider} cannot pay')
exception(f'{e}')
return Response(text='{"code":"SUCCESS","message":"OK"}', content_type='application/json')
data = None
try:
data = await PROVIDERS[provider].handle_notify(request)
except Exception as e:
exception(f'{e}')
return Response(text='{"code":"SUCCESS","message":"OK"}', content_type='application/json')
if data is None:
return Response(text='{"code":"SUCCESS","message":"OK"}', content_type='application/json')
debug(f'{data=}')
try:
await unipay_accounting(request, data.data)
except Exception as e:
exception(f'{e}')
return Response(text='{"code":"SUCCESS","message":"OK"}', content_type='application/json')
# 支付宝回调入口
async def alipay_notify(request):
debug("alipay notify called .......")
Response = ServerEnv().Response
provider = 'alipay'
if PROVIDERS[provider] is None:
e = Exception(f'{provider} cannot pay')
exception(f'{e}')
return Response(text='success', status=200)
data = None
try:
data = await PROVIDERS[provider].handle_notify(request)
except Exception as e:
e = Exception(f'{provider} cannot pay')
exception(f'{e}')
return Response(text='success', status=200)
debug(f'{data=}')
try:
await unipay_accounting(request, data.data)
except Exception as e:
exception(f'{e}')
return Response(text='success', status=200)
async def setup_callback_path(app):
app.router.add_post('/unipay/notify/wechat', wechat_notify)
app.router.add_post('/unipay/notify/alipay', alipay_notify)
# callback url= "/unipay/notify/{provider}"
def load_unipay():
"""注册各支付渠道到 PROVIDERS。初始化失败的渠道设为 Nonedisabled"""
for name in ("transfer", "wechat", "paypal", "alipay", "stripe"):
try:
conf = _build_provider_conf(name)
debug(f'{name=}, {conf=}')
PROVIDERS[name] = get_provider(name, conf)
if PROVIDERS[name] is not None:
debug(f"[unipay] {name} 初始化成功")
if name == 'transfer':
asyncio.ensure_future(PROVIDERS[name].run())
debug(f"[unipay] transfer 邮件轮询已启动")
else:
debug(f"[unipay] {name} 初始化返回 None渠道 disabled")
except Exception as e:
PROVIDERS[name] = None
debug(f"[unipay] {name} 初始化失败,已禁用: {e}")
env = ServerEnv()
env.get_paychannels = get_paychannels
env.get_pay_feerate = get_pay_feerate
env.create_payment = create_payment
env.query_payment = query_payment
env.refund_payment = refund_payment
env.get_pay_fee = get_pay_fee
env.sor_get_pay_fee = sor_get_pay_fee
env.PaymentLog = PaymentLog
add_startup(setup_callback_path)