'env'
This commit is contained in:
23
.env
Normal file
23
.env
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# ========== 生产环境配置 ==========
|
||||||
|
|
||||||
|
BASE_URL=https://api.hackrobot.cn
|
||||||
|
PAYMENT_PROVIDER=zpay
|
||||||
|
|
||||||
|
# ZPAY(请填入实际值)
|
||||||
|
ZPAY_PID=2025121809351743
|
||||||
|
ZPAY_KEY=tpEi7wWIWI2kXiYVTpIG6j7it0mjVW89
|
||||||
|
|
||||||
|
# PocketBase(check-user、ensure-user 必需,否则报「服务配置错误」)
|
||||||
|
PB_URL=https://pocketbase.hackrobot.cn
|
||||||
|
PB_ADMIN_EMAIL=xiaoshuang.eric@gmail.com
|
||||||
|
PB_ADMIN_PASSWORD=Xiao4669805@
|
||||||
|
|
||||||
|
# Memos(nomadvip 支付成功后创建账号)
|
||||||
|
MEMOS_API=https://qun.hackrobot.cn/api/v1/users
|
||||||
|
MEMOS_TOKEN=eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiaGFja3JvYm90IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NzQwNTE0OX0.Idn7kBlxE-CoSOXwWuZ1tHGIRKHAIeDyXSafGS5OHsg
|
||||||
|
|
||||||
|
# MinIO(可选)
|
||||||
|
MINIO_ENDPOINT=minioweb.hackrobot.cn
|
||||||
|
MINIO_PORT=443
|
||||||
|
MINIO_BUCKET=hackrobot
|
||||||
|
MINIO_UPLOAD_PREFIX=joins
|
||||||
17
.env.local
Normal file
17
.env.local
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 本地调试 - 端口 8007、zpay 渠道
|
||||||
|
PORT=8007
|
||||||
|
PAYMENT_PROVIDER=zpay
|
||||||
|
BASE_URL=https://api.hackrobot.cn
|
||||||
|
|
||||||
|
# PocketBase(支付回调、nomadvip/digital/cnomadcna 共用)
|
||||||
|
PB_URL=https://pocketbase.hackrobot.cn
|
||||||
|
PB_ADMIN_EMAIL=xiaoshuang.eric@gmail.com
|
||||||
|
PB_ADMIN_PASSWORD=Xiao4669805@
|
||||||
|
|
||||||
|
# Memos(nomadvip 支付成功后创建账号)
|
||||||
|
MEMOS_API=https://qun.hackrobot.cn/api/v1/users
|
||||||
|
MEMOS_TOKEN=eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiaGFja3JvYm90IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NzQwNTE0OX0.Idn7kBlxE-CoSOXwWuZ1tHGIRKHAIeDyXSafGS5OHsg
|
||||||
|
|
||||||
|
# ZPAY
|
||||||
|
ZPAY_PID=2025121809351743
|
||||||
|
ZPAY_KEY=tpEi7wWIWI2kXiYVTpIG6j7it0mjVW89
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -9,5 +9,5 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
.venv/
|
||||||
.env
|
# .env
|
||||||
.env.local
|
# .env.local
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -84,13 +84,19 @@ PAYMENT_PROVIDER=zpay
|
|||||||
|
|
||||||
## 线上「支付失败」排查
|
## 线上「支付失败」排查
|
||||||
|
|
||||||
本地正常、Ubuntu 部署后显示支付失败,常见原因:
|
本地正常、Ubuntu 部署后 api.hackrobot.cn 调用支付失败,常见原因:
|
||||||
|
|
||||||
1. **BASE_URL 配置错误**:必须为 ZPAY 可公网访问的地址。错误示例:`http://127.0.0.1:8700`、`http://localhost`。正确:`https://api.hackrobot.cn`(域名需解析到本机,nginx 反向代理 8700 端口)。
|
1. **BASE_URL 配置错误**:必须为 ZPAY 可公网访问的地址。错误示例:`http://127.0.0.1:8700`、`http://localhost`。正确:`https://api.hackrobot.cn`(域名需解析到本机,nginx 反向代理对应端口)。
|
||||||
|
|
||||||
2. **服务器无法访问 zpayz.cn**:防火墙、DNS 或网络限制。访问 `https://你的域名/health?check_zpay=1` 自检,若 `zpayz_reachable: false` 则需开放出站或检查网络。
|
2. **服务器无法访问 zpayz.cn**:防火墙、DNS 或网络限制。访问 `https://api.hackrobot.cn/health?check_zpay=1` 自检:
|
||||||
|
- `zpayz_reachable: false` → 开放出站或检查网络
|
||||||
|
- `zpayz_error` 含 "SSL" → 执行 `apt install ca-certificates` 后重启
|
||||||
|
|
||||||
3. **启动时控制台有 BASE_URL 警告**:说明配置了内网地址,需修改 `.env` 中的 `BASE_URL` 为公网地址后重启。
|
3. **client_ip 内网导致 ZPAY 风控拒单**:前端(digital/cnomadcna/nomadvip)通过 nginx 代理时,需配置 `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for`,否则 payjsapi 收到 127.0.0.1 会触发风控。已做兼容:内网 IP 时自动省略 clientip 参数。
|
||||||
|
|
||||||
|
4. **启动时控制台有 BASE_URL 警告**:说明配置了内网地址,需修改 `.env` 中的 `BASE_URL` 为公网地址后重启。
|
||||||
|
|
||||||
|
5. **查看详细错误**:支付失败时,payjsapi 会输出 `ZPAY create_order_api 失败` 日志,含 status/code/msg,便于定位。
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
|
|||||||
20
app/main.py
20
app/main.py
@@ -118,15 +118,31 @@ async def startup_event():
|
|||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health(check_zpay: str = ""):
|
async def health(check_zpay: str = ""):
|
||||||
"""健康检查。check_zpay=1 时额外检测 zpayz.cn 连通性"""
|
"""健康检查。check_zpay=1 时额外检测 zpayz.cn 连通性(含 SSL、mapi 接口)"""
|
||||||
out = {"status": "ok", "service": "payjsapi", "base_url": BASE_URL}
|
out = {"status": "ok", "service": "payjsapi", "base_url": BASE_URL}
|
||||||
if str(check_zpay).lower() in ("1", "true", "yes"):
|
if str(check_zpay).lower() in ("1", "true", "yes"):
|
||||||
|
# 1) 检测 zpayz.cn 首页
|
||||||
try:
|
try:
|
||||||
r = requests.get("https://zpayz.cn", timeout=5)
|
r = requests.get("https://zpayz.cn", timeout=5)
|
||||||
out["zpayz_reachable"] = r.status_code in (200, 301, 302, 404)
|
out["zpayz_reachable"] = r.status_code in (200, 301, 302, 404)
|
||||||
except Exception as e:
|
except requests.exceptions.SSLError as e:
|
||||||
|
out["zpayz_reachable"] = False
|
||||||
|
out["zpayz_error"] = f"SSL 错误: {e}(Ubuntu 请执行: apt install ca-certificates)"
|
||||||
|
except requests.RequestException as e:
|
||||||
out["zpayz_reachable"] = False
|
out["zpayz_reachable"] = False
|
||||||
out["zpayz_error"] = str(e)
|
out["zpayz_error"] = str(e)
|
||||||
|
# 2) 检测 mapi 接口(实际支付调用地址)
|
||||||
|
if out.get("zpayz_reachable"):
|
||||||
|
try:
|
||||||
|
r2 = requests.post("https://zpayz.cn/mapi.php", data={"pid": "test"}, timeout=5)
|
||||||
|
out["zpayz_mapi_reachable"] = True
|
||||||
|
out["zpayz_mapi_status"] = r2.status_code
|
||||||
|
except requests.exceptions.SSLError as e:
|
||||||
|
out["zpayz_mapi_reachable"] = False
|
||||||
|
out["zpayz_mapi_error"] = f"SSL: {e}"
|
||||||
|
except requests.RequestException as e:
|
||||||
|
out["zpayz_mapi_reachable"] = False
|
||||||
|
out["zpayz_mapi_error"] = str(e)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,27 @@ ZPAY 支付渠道实现
|
|||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .base import PaymentProvider
|
from .base import PaymentProvider
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_internal_ip(ip: Optional[str]) -> bool:
|
||||||
|
"""判断是否为内网 IP,ZPAY 可能拒收导致 Ubuntu 部署失败"""
|
||||||
|
if not ip or not ip.strip():
|
||||||
|
return True
|
||||||
|
ip = ip.strip()
|
||||||
|
if ip in ("127.0.0.1", "localhost", "::1"):
|
||||||
|
return True
|
||||||
|
if ip.startswith("10.") or ip.startswith("192.168.") or ip.startswith("172.16."):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class ZPayProvider(PaymentProvider):
|
class ZPayProvider(PaymentProvider):
|
||||||
"""ZPAY 支付(易支付兼容接口)"""
|
"""ZPAY 支付(易支付兼容接口)"""
|
||||||
@@ -64,8 +79,10 @@ class ZPayProvider(PaymentProvider):
|
|||||||
"money": total_fee_yuan,
|
"money": total_fee_yuan,
|
||||||
"return_url": return_url or notify_url,
|
"return_url": return_url or notify_url,
|
||||||
}
|
}
|
||||||
if client_ip:
|
if client_ip and not _is_internal_ip(client_ip):
|
||||||
params["clientip"] = client_ip
|
params["clientip"] = client_ip
|
||||||
|
elif client_ip and _is_internal_ip(client_ip):
|
||||||
|
logger.warning("ZPAY create_order 跳过内网 client_ip=%s", client_ip)
|
||||||
if extra and extra.get("param"):
|
if extra and extra.get("param"):
|
||||||
params["param"] = str(extra["param"])
|
params["param"] = str(extra["param"])
|
||||||
params["sign_type"] = "MD5"
|
params["sign_type"] = "MD5"
|
||||||
@@ -98,6 +115,14 @@ class ZPayProvider(PaymentProvider):
|
|||||||
适用于需要二维码或 H5 跳转的场景
|
适用于需要二维码或 H5 跳转的场景
|
||||||
"""
|
"""
|
||||||
channel_type = "wxpay" if pay_type.lower() in ("wx", "wechat", "wxpay") else "alipay"
|
channel_type = "wxpay" if pay_type.lower() in ("wx", "wechat", "wxpay") else "alipay"
|
||||||
|
effective_ip = (client_ip or "").strip() or "127.0.0.1"
|
||||||
|
# 内网 IP(127.0.0.1/10.x/192.168.x)可能导致 ZPAY 风控拒单,Ubuntu 部署时常见
|
||||||
|
if _is_internal_ip(effective_ip):
|
||||||
|
logger.warning(
|
||||||
|
"ZPAY create_order_api client_ip=%s 为内网地址,已省略 clientip 参数(避免风控拒单)",
|
||||||
|
effective_ip,
|
||||||
|
)
|
||||||
|
effective_ip = "" # 不传 clientip,由 ZPAY 从连接获取
|
||||||
params = {
|
params = {
|
||||||
"pid": self.pid,
|
"pid": self.pid,
|
||||||
"type": channel_type,
|
"type": channel_type,
|
||||||
@@ -105,9 +130,10 @@ class ZPayProvider(PaymentProvider):
|
|||||||
"notify_url": notify_url,
|
"notify_url": notify_url,
|
||||||
"name": name,
|
"name": name,
|
||||||
"money": total_fee_yuan,
|
"money": total_fee_yuan,
|
||||||
"clientip": client_ip or "127.0.0.1",
|
|
||||||
"device": device,
|
"device": device,
|
||||||
}
|
}
|
||||||
|
if effective_ip:
|
||||||
|
params["clientip"] = effective_ip
|
||||||
if return_url:
|
if return_url:
|
||||||
params["return_url"] = return_url
|
params["return_url"] = return_url
|
||||||
if extra and extra.get("param"):
|
if extra and extra.get("param"):
|
||||||
@@ -117,7 +143,15 @@ class ZPayProvider(PaymentProvider):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.post(self.MAPI_URL, data=params, timeout=15)
|
resp = requests.post(self.MAPI_URL, data=params, timeout=15)
|
||||||
|
except requests.exceptions.SSLError as e:
|
||||||
|
logger.error("ZPAY SSL 错误(Ubuntu 可能 CA 证书不全): %s", e, exc_info=True)
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"provider": self.name,
|
||||||
|
"msg": f"连接 zpayz.cn SSL 失败: {e}。Ubuntu 请安装 ca-certificates: apt install ca-certificates",
|
||||||
|
}
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
|
logger.error("ZPAY 连接失败: %s", e, exc_info=True)
|
||||||
return {
|
return {
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"provider": self.name,
|
"provider": self.name,
|
||||||
@@ -160,10 +194,18 @@ class ZPayProvider(PaymentProvider):
|
|||||||
"O_id": result.get("O_id"),
|
"O_id": result.get("O_id"),
|
||||||
"trade_no": result.get("trade_no"),
|
"trade_no": result.get("trade_no"),
|
||||||
}
|
}
|
||||||
|
err_msg = result.get("msg", resp.text)
|
||||||
|
logger.error(
|
||||||
|
"ZPAY create_order_api 失败 status=%s code=%s msg=%s resp_text=%s",
|
||||||
|
resp.status_code,
|
||||||
|
code,
|
||||||
|
err_msg,
|
||||||
|
resp.text[:500] if resp.text else "",
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"provider": self.name,
|
"provider": self.name,
|
||||||
"msg": result.get("msg", resp.text),
|
"msg": err_msg,
|
||||||
}
|
}
|
||||||
|
|
||||||
def verify_notify(self, data: Dict[str, Any]) -> bool:
|
def verify_notify(self, data: Dict[str, Any]) -> bool:
|
||||||
|
|||||||
Reference in New Issue
Block a user