PayGateway: epay-compatible Go payment gateway with plugin channels and verifiable ledger

This commit is contained in:
lim39869-create committed 2026-09-28 11:49:29 +08:00
commit f2ae88b4e9
201 files changed
+44898

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
#!/bin/bash
# adb 设备稳定性压测(平板入库前验证,模拟 paygateway 运行时调用面)
#
# 用法: ./adb-soak.sh <serial> [轮数] [间隔秒]
# serial: WiFi adb 为 host:5555(容器/裸跑同协议同路径);USB 直连为纯设备号
#
# 每轮覆盖 adbmgr 实际用到的操作: get-state(连接口径)→ shell → wm size
# → screencap → uiautomator dump+cat,结尾输出成功率与平均时延对比。
set -u
SERIAL=${1:?'需要 serial 参数(host:5555 或 USB 设备号)'}
N=${2:-10}
IV=${3:-3}
ok_state=0; ok_shell=0; ok_size=0; ok_shot=0; ok_dump=0
sum_state=0; sum_shell=0; sum_size=0; sum_shot=0; sum_dump=0
ms() { echo $(( ($2 - $1) / 1000000 )); } # 起止纳秒 → 毫秒
for i in $(seq 1 "$N"); do
t0=$(date +%s%N)
case "$SERIAL" in
*:*) adb connect "$SERIAL" >/dev/null 2>&1 ;;
esac
st=$(adb -s "$SERIAL" get-state 2>/dev/null)
d=$(ms "$t0" "$(date +%s%N)")
if [ "$st" != "device" ]; then
echo "[$i/$N] ❌ 设备不可达(connect+state ${d}ms)"
sleep "$IV"; continue
fi
ok_state=$((ok_state+1)); sum_state=$((sum_state+d))
t0=$(date +%s%N); out=$(adb -s "$SERIAL" shell echo soak-ok 2>/dev/null)
d=$(ms "$t0" "$(date +%s%N)"); sum_shell=$((sum_shell+d))
[ "$out" = "soak-ok" ] && ok_shell=$((ok_shell+1))
t0=$(date +%s%N); sz=$(adb -s "$SERIAL" shell wm size 2>/dev/null | grep -o '[0-9]\+x[0-9]\+' | tail -1)
d=$(ms "$t0" "$(date +%s%N)")
[ -n "$sz" ] && { ok_size=$((ok_size+1)); sum_size=$((sum_size+d)); }
t0=$(date +%s%N); adb -s "$SERIAL" exec-out screencap -p > /tmp/opencode/soak_shot.png 2>/dev/null
d=$(ms "$t0" "$(date +%s%N)")
[ -s /tmp/opencode/soak_shot.png ] && { ok_shot=$((ok_shot+1)); sum_shot=$((sum_shot+d)); }
t0=$(date +%s%N)
adb -s "$SERIAL" shell uiautomator dump /sdcard/soak_ui.xml >/dev/null 2>&1
adb -s "$SERIAL" shell cat /sdcard/soak_ui.xml > /tmp/opencode/soak_ui.xml 2>/dev/null
d=$(ms "$t0" "$(date +%s%N)")
grep -q '<' /tmp/opencode/soak_ui.xml && { ok_dump=$((ok_dump+1)); sum_dump=$((sum_dump+d)); }
echo "[$i/$N] ✅ 分辨率=${sz:-未取到} shell=${d}ms dump+读取=${d}ms 截图成功=${ok_shot} dump成功=${ok_dump}"
sleep "$IV"
done
echo "============ 汇总(共 $N 轮,覆盖 paygateway 实际调用面)============"
[ "$ok_size" -gt 0 ] && printf "wm size : %d/%d 平均 %dms\n" "$ok_size" "$N" $((sum_size/ok_size))
[ "$ok_shot" -gt 0 ] && printf "screencap : %d/%d 平均 %dms\n" "$ok_shot" "$N" $((sum_shot/ok_shot))
[ "$ok_dump" -gt 0 ] && printf "ui-dump : %d/%d 平均 %dms\n" "$ok_dump" "$N" $((sum_dump/ok_dump))
if [ "$ok_state" -gt 0 ]; then
avg_state=$((sum_state/ok_state)); avg_shell=$((sum_shell/ok_state))
printf "connect/state : %d/%d 平均 %dms\n" "$ok_state" "$N" "$avg_state"
printf "shell : %d/%d 平均 %dms\n" "$ok_shell" "$N" "$avg_shell"
fi
echo "(时延由设备性能与 WiFi 决定——同一台设备在容器内与宿主机跑一遍结果应一致," \
"即可证明网络桥接无额外损耗)"
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
# download-cloudflared.sh — 下载 cloudflared 二进制到 internal/tunnel/ 供 //go:embed 使用
#
# 用法:
# go generate ./internal/tunnel/ # 自动调用
# bash scripts/download-cloudflared.sh # 手动调用
#
# 下载策略(自动回退):
# 1. GitHub 直连
# 2. GITHUB_MIRROR 环境变量指定的镜像(如有设置)
# 3. 内置镜像列表(ghproxy.cc / ghproxy.net / gh.api.99988866.xyz)
#
# 支持平台: linux amd64/arm64, darwin amd64/arm64
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
TARGET_DIR="$PROJECT_DIR/internal/tunnel"
TARGET_FILE="$TARGET_DIR/cloudflared"
# 如果二进制已存在且可执行,跳过下载
if [ -x "$TARGET_FILE" ]; then
echo "✓ cloudflared 已存在,跳过下载"
exit 0
fi
OS="${GOOS:-$(go env GOOS)}"
ARCH="${GOARCH:-$(go env GOARCH)}"
case "$OS/$ARCH" in
linux/amd64) REL_PATH="cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64" ;;
linux/arm64) REL_PATH="cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64" ;;
darwin/arm64) REL_PATH="cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64" ;;
darwin/amd64) REL_PATH="cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64" ;;
*)
echo "❌ 不支持的平台: $OS/$ARCH"
exit 1
;;
esac
# 构建下载 URL 列表:环境变量镜像优先,内置镜像回退
URLS=()
if [ -n "${GITHUB_MIRROR:-}" ]; then
URLS+=("${GITHUB_MIRROR%/}/https://github.com/$REL_PATH")
fi
URLS+=(
"https://ghproxy.cc/https://github.com/$REL_PATH"
"https://ghproxy.net/https://github.com/$REL_PATH"
"https://gh.api.99988866.xyz/https://github.com/$REL_PATH"
"https://github.com/$REL_PATH"
)
echo "⬇️ 正在下载 cloudflared ($OS/$ARCH) ..."
for url in "${URLS[@]}"; do
echo " 尝试: $url"
set +e
# 镜像站常见 SSL 证书问题,统一用 -k;直连 GitHub 不用 -k
if echo "$url" | grep -q "github.com/$REL_PATH$" && [ -z "${GITHUB_MIRROR:-}" ]; then
curl -sSL --fail --connect-timeout 10 --max-time 120 "$url" -o "$TARGET_FILE" 2>/dev/null
else
curl -sSL --fail --connect-timeout 10 --max-time 120 -k "$url" -o "$TARGET_FILE" 2>/dev/null
fi
rc=$?
set -e
if [ $rc -eq 0 ] && [ -s "$TARGET_FILE" ]; then
chmod +x "$TARGET_FILE"
echo "✓ 下载成功: $TARGET_FILE ($(du -h "$TARGET_FILE" | cut -f1))"
exit 0
fi
echo " ✗ 失败,换下一个..."
done
echo "❌ 所有下载源均失败,请手动下载:"
echo " https://github.com/$REL_PATH"
echo " 放到 $TARGET_FILE 并 chmod +x"
exit 1
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# 下载 scrcpy-server.jar(编译时嵌入,go generate 调用)
# 需要网络;下载失败时提示手动放置
set -e
DIR="$(cd "$(dirname "$0")/.." && pwd)"
TARGET="$DIR/internal/scrcpy/scrcpy-server.jar"
if [ -f "$TARGET" ] && [ -s "$TARGET" ]; then
echo "scrcpy-server.jar 已存在,跳过下载"
exit 0
fi
# scrcpy v4.1 release 的 server jar
URL="https://github.com/Genymobile/scrcpy/releases/download/v4.1/scrcpy-server-v4.1"
echo "下载 scrcpy-server v4.1 → $TARGET"
if curl -fsSL -o "$TARGET" "$URL" 2>/dev/null && [ -s "$TARGET" ]; then
echo "✅ 下载完成 ($(du -h "$TARGET" | cut -f1))"
else
rm -f "$TARGET"
echo "❌ 下载失败,请手动下载:"
echo " $URL"
echo " 放置到: $TARGET"
exit 1
fi
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# USB 真机端到端测试:选真机 → 调管理后台 push-config 下发配置 → 跑 e2e_personal.py 下单→入账
#
# 架构说明(listener-app 已下线,改为纯 adb 控制真机微信/支付宝账本 ledgermon):
# - 网关通过 adb 控制红米 K60 真机,ledgermon 读账本行匹配订单入账
# - 不再有手机端 APP,所有授权/安装步骤已移除
# - 本脚本负责:ADB 连接 → 管理后台登录 → 设备选择 → 配置下发 → 下单测试
#
# 使用前置:
# 1. 真机开了 USB 调试 + 已 adb pair
# 2. 网关在跑(./gateway)+ cloudflare 隧道在跑(server_url 可达)
# 3. 管理后台已创建同名监听设备(账户池 → 监听设备,确保 adb_host/adb_port 字段已填真机 ip,或脚本会问 --device-id)
#
# 用法:
# scripts/e2e_usb_realdevice.sh # 自动选 adb device 第一个真机
# scripts/e2e_usb_realdevice.sh --serial 4f2bdc30 # 指定真机
# scripts/e2e_usb_realdevice.sh --device-id 91df2463... # 指定服务端已创建的 device_id
# scripts/e2e_usb_realdevice.sh --admin-url http://127.0.0.1:8081 --gateway-url http://127.0.0.1:8080
#
# 真实收款流程:下单后展示备注码,在真机上完成真实收款;ledgermon 通过 adb 读取账本行
# 匹配订单入账,订单变 TRADE_SUCCESS 即为闭环走过。
set -euo pipefail
IFS=$'\n\t'
# ---------- 参数 ----------
SERIAL=""
DEVICE_ID=""
ADMIN_URL="${ADMIN_URL:-http://127.0.0.1:8081}"
GATEWAY_URL="${GATEWAY_URL:-http://127.0.0.1:8080}"
ADMIN_USER="${ADMIN_USER:-admin}"
ADMIN_PASS="${ADMIN_PASS:-paygateway123}"
PAY_AMOUNT="${PAY_AMOUNT:-0.66}"
usage() { sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; }
while [[ $# -gt 0 ]]; do
case "$1" in
--serial) SERIAL="$2"; shift 2;;
--device-id) DEVICE_ID="$2"; shift 2;;
--admin-url) ADMIN_URL="$2"; shift 2;;
--gateway-url) GATEWAY_URL="$2"; shift 2;;
--amount) PAY_AMOUNT="$2"; shift 2;;
-h|--help) usage 0;;
*) echo "未知参数: $1"; usage 1;;
esac
done
[[ -n "$ADMIN_PASS" && "$ADMIN_PASS" == "paygateway123" ]] && echo "⚠️ 使用默认管理员密码(ADMIN_PASS 未设),若改过请环境变量传值。"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
echo
echo "==== [1/7] 选择 USB 真机 ===="
ADB_CMD=(adb)
if [[ -n "$SERIAL" ]]; then
ADB_CMD+=( -s "$SERIAL" )
if ! "${ADB_CMD[@]}" get-state >/dev/null 2>&1; then
echo " ❌ 设备 --serial $SERIAL 不可达"
exit 1
fi
echo " ✅ 使用指定 serial: $SERIAL"
else
mapfile -t DEVS < <(adb devices | awk 'NR>1 && $2=="device" {print $1}')
if [[ ${#DEVS[@]} -eq 0 ]]; then
echo " ❌ adb devices 列表为空(USB 调试没开?或者用 --serial 指定)"
adb devices
exit 1
fi
if [[ ${#DEVS[@]} -gt 1 ]]; then
echo " ⚠️ 检测到多台设备,请用 --serial 指定其中一台:"
printf ' %s\n' "${DEVS[@]}"
exit 1
fi
SERIAL="${DEVS[0]}"
ADB_CMD+=( -s "$SERIAL" )
echo " ✅ 自动选用: $SERIAL"
fi
echo
echo "==== [2/7] 管理后台登录 ===="
LOGIN_RESP=$(curl -sS -X POST "$ADMIN_URL/api/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$ADMIN_USER\",\"password\":\"$ADMIN_PASS\"}" )
TOKEN=$(echo "$LOGIN_RESP" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("data",{}).get("token",""))')
if [[ -z "$TOKEN" ]]; then
echo " ❌ 登录失败:$LOGIN_RESP"
exit 1
fi
echo " ✅ 已获取 JWT"
echo
echo "==== [3/7] 检查关键授权(电池白名单)==="
# 注:listener-app 已下线,通知使用权检查已移除
# ledgermon 通过 adb 直连账本,无需 App 通知监听权限
WHITELIST_OK=$("${ADB_CMD[@]}" shell dumpsys deviceidle whitelist | head -5 || true)
if [[ -n "$WHITELIST_OK" ]]; then
echo " ✅ 电池优化白名单:已检查(见上方)"
else
echo " ⚠️ 电池优化白名单:无法检查(无 root 权限,跳过)"
fi
echo
echo "==== [4/7] 选取监听设备 ===="
DEVS_RESP=$(curl -sS "$ADMIN_URL/api/personal/devices" -H "Authorization: Bearer $TOKEN")
if [[ -n "$DEVICE_ID" ]]; then
MATCHED=$(echo "$DEVS_RESP" | python3 -c "
import sys, json
data = json.load(sys.stdin).get('data',{}).get('devices',[])
target = '$DEVICE_ID'
m = [d for d in data if d.get('device_id')==target]
print(json.dumps(m[0]) if m else '')")
if [[ -z "$MATCHED" ]]; then
echo " ❌ 指定 --device-id $DEVICE_ID 在管理后台不存在"
exit 1
fi
echo " ✅ 使用 device_id=$DEVICE_ID"
else
PICK=$(echo "$DEVS_RESP" | python3 -c "
import sys, json
data = json.load(sys.stdin).get('data',{}).get('devices',[])
if not data: sys.exit('无监听设备,请先在管理后台创建')
# 优先 alipay provider
alipay = [d for d in data if d.get('provider')=='alipay']
pick = alipay[0] if alipay else data[0]
print(pick.get('device_id'))")
DEVICE_ID="$PICK"
echo " ✅ 自动选取 device_id=$DEVICE_ID"
fi
echo
echo "==== [5/7] 通过管理后台 push-config 下发配置 ===="
PUSH_RESP=$(curl -sS -X POST "$ADMIN_URL/api/personal/devices/$DEVICE_ID/push-config" \
-H "Authorization: Bearer $TOKEN" \
--max-time 30 )
echo " raw: $PUSH_RESP"
echo "$PUSH_RESP" | python3 -c "
import sys, json
r = json.load(sys.stdin)
d = r.get('data', r)
if r.get('error'): sys.exit(' ❌ 下发失败: ' + r['error'])
print(' ✅ 配置下发成功')"
echo
echo "==== [6/7] 跑下单测试 ===="
echo " ⓘ ledgermon 通过 adb 读取真机账本行匹配订单,无需模拟推送"
echo " 下单后会展示备注码;在真机上完成真实收款即可"
# TODO: 待 ledgermon e2e 测试脚本就绪后替换
echo " ⚠️ e2e_personal.py 已随 listener-app 下线而删除,请手动执行下单验证"
exit 0
+242
View File
@@ -0,0 +1,242 @@
#!/bin/bash
# Cloudflare 固定域名 + 隧道一键初始化(全自动)
#
# 功能:自动创建/确认(全部幂等):
# 1. Cloudflare zone(paygateway.de5.net 全托管)
# 2. DNS CNAME 记录(proxied → 隧道)
# 3. hostname route(域名 → 隧道映射)
# 4. DNSHe 子域名(不存在时自动注册) + NS 委派给 Cloudflare
# 5. 等待 zone 激活
# 6. Universal SSL 证书(自动签发,toggle 触发)
# 7. HTTPS 公网验证
#
# 前置条件:
# 1. go build -o gateway ./cmd/gateway/ (编译产物)
# 2. .env 包含以下变量:
# CF_EMAIL, CF_GLOBAL_KEY, CF_TUNNEL_HOSTNAME, CF_TUNNEL_CREDENTIALS
# DNSHE_API_KEY, DNSHE_API_SECRET (de5.net 免费子域名场景)
#
# 使用:
# bash scripts/setup-domain.sh
#
# 换环境流程:复制 .env → go build → 本脚本 → ./start.sh
# 全新域名:把 CF_TUNNEL_HOSTNAME 换成新前缀(如 myapp.de5.net),脚本全自动注册+配置。
set -euo pipefail
cd "$(dirname "$0")/.."
# 加载 .env
if [ -f .env ]; then
set -a
source .env
set +a
fi
: "${CF_EMAIL:?请设置 CF_EMAIL}"
: "${CF_GLOBAL_KEY:?请设置 CF_GLOBAL_KEY}"
: "${CF_TUNNEL_HOSTNAME:?请设置 CF_TUNNEL_HOSTNAME}"
: "${CF_TUNNEL_CREDENTIALS:?请设置 CF_TUNNEL_CREDENTIALS}"
# 从凭据中提取 TunnelID
TUNNEL_ID=$(echo "$CF_TUNNEL_CREDENTIALS" | base64 -d 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['TunnelID'])" 2>/dev/null || echo "")
if [ -z "$TUNNEL_ID" ]; then
echo "❌ 无法从 CF_TUNNEL_CREDENTIALS 中提取 TunnelID,请确认格式正确"
exit 1
fi
CF_AUTH=(-H "X-Auth-Email: $CF_EMAIL" -H "X-Auth-Key: $CF_GLOBAL_KEY" -H "Content-Type: application/json")
echo "=== Cloudflare 初始化: $CF_TUNNEL_HOSTNAME (隧道 $TUNNEL_ID) ==="
# 1. 确保 zone 存在
echo "--- 1/7 检查 zone: $CF_TUNNEL_HOSTNAME ---"
ZONE_ID=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones?name=$CF_TUNNEL_HOSTNAME" "${CF_AUTH[@]}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['result'][0]['id'] if d.get('result') else '')" 2>/dev/null || echo "")
if [ -z "$ZONE_ID" ]; then
echo " zone 不存在,正在创建..."
ZONE_ID=$(curl -sS -m 20 -X POST "https://api.cloudflare.com/client/v4/zones" \
"${CF_AUTH[@]}" \
-d "{\"name\":\"$CF_TUNNEL_HOSTNAME\",\"type\":\"full\"}" | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(d['result']['id'] if d.get('success') else '')" 2>/dev/null || echo "")
if [ -z "$ZONE_ID" ]; then
echo "❌ zone 创建失败(可能是 TLD 或重复),跳过"
else
echo " ✅ zone 已创建 (ID: $ZONE_ID)"
fi
else
echo " ✅ zone 已存在 (ID: $ZONE_ID)"
fi
if [ -z "$ZONE_ID" ]; then
echo "❌ 无 zone ID,无法继续。请检查域名是否可被 Cloudflare 托管"
exit 1
fi
# 2. 确保 DNS CNAME 记录(proxied=true)
echo "--- 2/7 检查 CNAME 记录 ---"
EXISTING=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=CNAME&name=$CF_TUNNEL_HOSTNAME" "${CF_AUTH[@]}" | \
python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',[]); print(r[0]['id'] if r else '')" 2>/dev/null || echo "")
if [ -z "$EXISTING" ]; then
echo " CNAME 不存在,创建中..."
curl -sS -m 15 -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
"${CF_AUTH[@]}" \
-d "{\"type\":\"CNAME\",\"name\":\"@\",\"content\":\"$TUNNEL_ID.cfargotunnel.com\",\"proxied\":true}" > /dev/null
echo " ✅ CNAME 已创建 (proxied=true)"
else
echo " ✅ CNAME 已存在"
fi
# 3. 确保 hostname route
echo "--- 3/7 检查 hostname route ---"
ACT_ID=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/accounts?limit=1" "${CF_AUTH[@]}" | python3 -c "import sys,json; print(json.load(sys.stdin)['result'][0]['id'])" 2>/dev/null || echo "")
if [ -n "$ACT_ID" ]; then
ROUTE=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/accounts/$ACT_ID/zerotrust/routes/hostname" "${CF_AUTH[@]}" | \
python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('result',[]) if x['hostname']=='$CF_TUNNEL_HOSTNAME']; print(r[0]['id'] if r else '')" 2>/dev/null || echo "")
if [ -z "$ROUTE" ]; then
echo " hostname route 不存在,创建中..."
curl -sS -m 15 -X POST "https://api.cloudflare.com/client/v4/accounts/$ACT_ID/zerotrust/routes/hostname" \
"${CF_AUTH[@]}" \
-d "{\"hostname\":\"$CF_TUNNEL_HOSTNAME\",\"tunnel_id\":\"$TUNNEL_ID\",\"tun_type\":\"cfd_tunnel\",\"comment\":\"setup-domain.sh\"}" > /dev/null
echo " ✅ hostname route 已创建"
else
echo " ✅ hostname route 已存在"
fi
fi
# 4. DNSHe NS 委派(de5.net 免费子域名场景)
# de5.net 的 DNS 在 DNSHe,需把 paygateway.de5.net 的 NS 委派给 Cloudflare。
# 若 DNSHE_API_KEY 已配置则自动处理;未配置则提示手动。
echo "--- 4/7 DNSHe NS 委派 ---"
if [ -n "${DNSHE_API_KEY:-}" ] && [ -n "${DNSHE_API_SECRET:-}" ]; then
DNSHE_BASE="${DNSHE_API_BASE:-https://api005.dnshe.com}"
DNSHE_AUTH=(-H "X-API-Key: $DNSHE_API_KEY" -H "X-API-Secret: $DNSHE_API_SECRET")
# 提取子域前缀(paygateway.de5.net → paygateway)
SUB_PREFIX="${CF_TUNNEL_HOSTNAME%%.*}"
ROOT_DOMAIN="${CF_TUNNEL_HOSTNAME#*.}"
echo " 子域: $SUB_PREFIX (根域: $ROOT_DOMAIN)"
# 列出子域名,找 subdomain_id
SUB_ID=$(curl -sS -m 15 "${DNSHE_AUTH[@]}" \
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=subdomains&action=list" 2>/dev/null | \
python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('subdomains',[]) if x['full_domain']=='$CF_TUNNEL_HOSTNAME']; print(r[0]['id'] if r else '')" 2>/dev/null || echo "")
if [ -z "$SUB_ID" ]; then
# 子域名不存在 → 自动注册(DNSHe API 支持)
echo " 子域名 $CF_TUNNEL_HOSTNAME 不存在,自动注册..."
SUB_ID=$(curl -sS -m 20 -X POST "${DNSHE_AUTH[@]}" \
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=subdomains&action=create" \
-H "Content-Type: application/json" \
-d "{\"subdomain\":\"$SUB_PREFIX\",\"domain\":\"$ROOT_DOMAIN\"}" 2>/dev/null | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('subdomain_id',''))" 2>/dev/null || echo "")
if [ -n "$SUB_ID" ]; then
echo " ✅ 子域名已注册: $CF_TUNNEL_HOSTNAME (id=$SUB_ID)"
else
echo " ❌ 子域名注册失败(配额可能已满,DNSHe 免费额度 3 个)"
fi
fi
if [ -n "$SUB_ID" ]; then
# 取 Cloudflare 分配的 NS(kevin/reza.ns.cloudflare.com)
CF_NS=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID" "${CF_AUTH[@]}" 2>/dev/null | \
python3 -c "import sys,json; r=json.load(sys.stdin).get('result',{}); ns=r.get('name_servers',[]); print(' '.join(ns) if ns else '')" 2>/dev/null || echo "")
if [ -z "$CF_NS" ]; then
echo " ⚠️ 无法获取 Cloudflare NS,跳过 DNSHe 自动委派"
else
echo " Cloudflare NS: $CF_NS"
# 检查当前 NS 记录是否已是 Cloudflare 的
NS_CHECK=$(curl -sS -m 15 "${DNSHE_AUTH[@]}" \
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=list&subdomain_id=$SUB_ID" 2>/dev/null | \
python3 -c "
import sys,json
d=json.load(sys.stdin)
ns1,ns2='$CF_NS'.split()[:2]
ok=all(any(r.get('type')=='NS' and r.get('content')==ns for r in d.get('records',[])) for ns in (ns1,ns2))
print('ok' if ok else 'need')
" 2>/dev/null || echo "need")
if [ "$NS_CHECK" = "ok" ]; then
echo " ✅ NS 委派已就绪 ($CF_NS)"
else
echo " NS 委派缺失,自动配置中..."
# 删除现有 CNAME/非 NS 记录(NS 委派与 CNAME 冲突)
curl -sS -m 15 "${DNSHE_AUTH[@]}" \
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=list&subdomain_id=$SUB_ID" 2>/dev/null | \
python3 -c "
import sys,json
d=json.load(sys.stdin)
for r in d.get('records',[]):
print(r.get('id'))
" 2>/dev/null | while read rid; do
curl -sS -m 15 -X POST "${DNSHE_AUTH[@]}" \
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=delete" \
-H "Content-Type: application/json" \
-d "{\"record_id\":\"$rid\"}" > /dev/null 2>&1
done
# 添加两条 NS 记录
for ns in $CF_NS; do
curl -sS -m 20 -X POST "${DNSHE_AUTH[@]}" \
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=create" \
-H "Content-Type: application/json" \
-d "{\"subdomain_id\": $SUB_ID, \"type\": \"NS\", \"content\": \"$ns\", \"ttl\": 600}" > /dev/null 2>&1
done
echo " ✅ NS 委派已配置: $CF_NS"
fi
fi
else
echo " ⚠️ DNSHe 未找到子域名 $CF_TUNNEL_HOSTNAME,跳过(请确认已注册)"
fi
else
NS_HINT=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID" "${CF_AUTH[@]}" 2>/dev/null | \
python3 -c "import sys,json; print(' '.join(json.load(sys.stdin).get('result',{}).get('name_servers',[])))" 2>/dev/null || echo "")
echo " ⚠️ 未配置 DNSHE_API_KEY/SECRET,跳过自动委派"
echo " 如需手动:dns.he.net → $CF_TUNNEL_HOSTNAME → NS 指向 $NS_HINT"
fi
# 5. 确保 DNS 委派传播 + zone active
echo "--- 5/7 等待 zone 激活 (最多 60s) ---"
for i in $(seq 1 12); do
STATUS=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID" "${CF_AUTH[@]}" | \
python3 -c "import sys,json; print(json.load(sys.stdin)['result']['status'])" 2>/dev/null || echo "pending")
if [ "$STATUS" = "active" ]; then
echo " ✅ zone 已激活"
break
fi
sleep 5
done
# 6. 确保 Universal SSL
echo "--- 6/7 检查 Universal SSL ---"
CERT=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/certificate_packs?status=all" "${CF_AUTH[@]}" | \
python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('result',[]) if x.get('type')=='universal' and x.get('status')=='active']; print('ok' if r else '')" 2>/dev/null || echo "")
if [ -z "$CERT" ]; then
echo " SSL 未签发,尝试 toggle 触发..."
curl -sS -m 10 -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/universal/settings" \
"${CF_AUTH[@]}" -d '{"enabled":false}' > /dev/null 2>&1
sleep 10
curl -sS -m 10 -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/universal/settings" \
"${CF_AUTH[@]}" -d '{"enabled":true}' > /dev/null 2>&1
echo " ⏳ Universal SSL toggle 完成,等待签发(通常 1-5 分钟)..."
for i in $(seq 1 30); do
STATUS=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/certificate_packs?status=all" "${CF_AUTH[@]}" | \
python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('result',[]) if x.get('status')=='active']; print(r[0]['status'] if r else 'pending')" 2>/dev/null || echo "pending")
if [ "$STATUS" = "active" ]; then
echo " ✅ Universal SSL 已签发!"
break
fi
sleep 10
done
else
echo " ✅ Universal SSL 已签发"
fi
# 7. 验证 HTTPS
echo "--- 7/7 验证 HTTPS ---"
sleep 5
HTTP_CODE=$(curl -sS -m 15 -o /dev/null -w "%{http_code}" "https://$CF_TUNNEL_HOSTNAME/health" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
echo " ✅ https://$CF_TUNNEL_HOSTNAME/health → HTTP 200"
echo ""
echo "============================================"
echo " 初始化完成! 固定域名已就绪"
echo " URL: https://$CF_TUNNEL_HOSTNAME"
echo " 隧道: $TUNNEL_ID"
echo "============================================"
else
echo " ⚠️ HTTPS 返回 HTTP $HTTP_CODE,可能需等待 DNS 传播"
echo " 隧道进程需手动确认: ps aux | grep cloudflared"
fi