271 lines
8.6 KiB
Python
271 lines
8.6 KiB
Python
# 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。初始化失败的渠道设为 None(disabled)。"""
|
||
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)
|
||
|
||
|