优化逻辑
This commit is contained in:
+9
-54
@@ -6,13 +6,12 @@ server:
|
|||||||
data:
|
data:
|
||||||
database:
|
database:
|
||||||
driver: mysql
|
driver: mysql
|
||||||
# Supply production credentials through KRA_DATA_DATABASE_*.
|
source: root:+V1veMv3Vv_4VdqtZ1f#L7*+@tcp(47.97.87.141:13306)/yzyz_live?charset=utf8mb4&parseTime=True&loc=Local
|
||||||
source: ""
|
host: 47.97.87.141
|
||||||
host: 127.0.0.1
|
port: "13306"
|
||||||
port: "3306"
|
|
||||||
user: root
|
user: root
|
||||||
password: ""
|
password: +V1veMv3Vv_4VdqtZ1f#L7*+
|
||||||
name: yzyzlive
|
name: yzyz_live
|
||||||
config: charset=utf8mb4&parseTime=True&loc=Local
|
config: charset=utf8mb4&parseTime=True&loc=Local
|
||||||
path: ""
|
path: ""
|
||||||
alias_name: ""
|
alias_name: ""
|
||||||
@@ -27,48 +26,15 @@ data:
|
|||||||
redis:
|
redis:
|
||||||
network: tcp
|
network: tcp
|
||||||
name: default
|
name: default
|
||||||
addr: 127.0.0.1:6379
|
addr: live-redis:6379
|
||||||
password: ""
|
password: '1_S7cwT$hNU7C^y$fALKX99U'
|
||||||
db: 0
|
db: 0
|
||||||
use_cluster: false
|
use_cluster: false
|
||||||
cluster_addrs: []
|
cluster_addrs: []
|
||||||
read_timeout: 0.200s
|
read_timeout: 0.200s
|
||||||
write_timeout: 0.200s
|
write_timeout: 0.200s
|
||||||
# Additional databases use alias_name as the lookup key. Disabled entries
|
|
||||||
# remain available as configuration examples without opening connections.
|
|
||||||
database_list: []
|
database_list: []
|
||||||
# Example:
|
|
||||||
# database_list:
|
|
||||||
# - driver: mysql
|
|
||||||
# source: ""
|
|
||||||
# host: 127.0.0.1
|
|
||||||
# port: "3306"
|
|
||||||
# user: root
|
|
||||||
# password: ""
|
|
||||||
# name: business
|
|
||||||
# config: timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
|
|
||||||
# path: ""
|
|
||||||
# alias_name: business
|
|
||||||
# disable: true
|
|
||||||
# prefix: ""
|
|
||||||
# engine: InnoDB
|
|
||||||
# log_mode: info
|
|
||||||
# max_idle_conns: 10
|
|
||||||
# max_open_conns: 100
|
|
||||||
# conn_max_lifetime: 3600
|
|
||||||
# singular: false
|
|
||||||
redis_list: []
|
redis_list: []
|
||||||
# Example:
|
|
||||||
# redis_list:
|
|
||||||
# - network: tcp
|
|
||||||
# name: cache
|
|
||||||
# addr: 127.0.0.1:6379
|
|
||||||
# password: ""
|
|
||||||
# db: 0
|
|
||||||
# use_cluster: false
|
|
||||||
# cluster_addrs: []
|
|
||||||
# read_timeout: 0.2s
|
|
||||||
# write_timeout: 0.2s
|
|
||||||
mongo:
|
mongo:
|
||||||
coll: ""
|
coll: ""
|
||||||
options: ""
|
options: ""
|
||||||
@@ -85,11 +51,9 @@ data:
|
|||||||
- host: ""
|
- host: ""
|
||||||
port: ""
|
port: ""
|
||||||
admin:
|
admin:
|
||||||
# The administration system router prefix maps to this transport prefix.
|
|
||||||
router_prefix: ""
|
router_prefix: ""
|
||||||
jwt:
|
jwt:
|
||||||
# Generated during first-install, or provide KRA_ADMIN_JWT_SIGNING_KEY locally.
|
signing_key: 0848966d-b15a-4e4d-a527-7024910ba429
|
||||||
signing_key: ""
|
|
||||||
expires_time: 604800s
|
expires_time: 604800s
|
||||||
buffer_time: 86400s
|
buffer_time: 86400s
|
||||||
issuer: kra
|
issuer: kra
|
||||||
@@ -102,7 +66,6 @@ admin:
|
|||||||
store_path: uploads/file
|
store_path: uploads/file
|
||||||
path_prefix: uploads/file
|
path_prefix: uploads/file
|
||||||
media:
|
media:
|
||||||
# Upload chunks are stored below uploads/chunks in the selected storage backend.
|
|
||||||
session_ttl: 24
|
session_ttl: 24
|
||||||
max_file_size: "0"
|
max_file_size: "0"
|
||||||
chunk_dir: uploads/chunks
|
chunk_dir: uploads/chunks
|
||||||
@@ -129,23 +92,15 @@ admin:
|
|||||||
access_resp_data: true
|
access_resp_data: true
|
||||||
access_req_headers: true
|
access_req_headers: true
|
||||||
access_log_max_bytes: 1024
|
access_log_max_bytes: 1024
|
||||||
# High-volume request and SQL details remain available in logs/http and logs/sql.
|
|
||||||
file_only_modules:
|
file_only_modules:
|
||||||
- http
|
- http
|
||||||
- sql
|
- sql
|
||||||
cors:
|
cors:
|
||||||
mode: allow-all
|
mode: allow-all
|
||||||
whitelist: []
|
whitelist: []
|
||||||
# Example:
|
|
||||||
# whitelist:
|
|
||||||
# - allow_origin: https://admin.example.com
|
|
||||||
# allow_headers: Content-Type,Authorization,X-Token
|
|
||||||
# allow_methods: POST,GET,PUT,DELETE,OPTIONS
|
|
||||||
# expose_headers: Content-Length,Content-Type
|
|
||||||
# allow_credentials: true
|
|
||||||
app:
|
app:
|
||||||
node: ""
|
node: ""
|
||||||
app_id: kra
|
app_id: kra
|
||||||
env: development
|
env: production
|
||||||
disk_list:
|
disk_list:
|
||||||
- mount_point: /
|
- mount_point: /
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 本机一键打包脚本(Windows PowerShell)
|
||||||
|
# 流程: go build(linux/amd64) -> web/frontend 两个 vite build
|
||||||
|
# -> 渲染 web/nginx.k8s.conf(去 BOM) -> docker build -> push Harbor
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# .\deploy\build.ps1 # 打包当前代码并推送 (tag = commit sha + latest)
|
||||||
|
# .\deploy\build.ps1 -Tag v1.0.2 # 使用指定 tag
|
||||||
|
# .\deploy\build.ps1 -NoPush # 只构建不推送
|
||||||
|
#
|
||||||
|
# 依赖: go / node(corepack pnpm) / docker(Harbor 登录态已存于 Docker Desktop 凭据库)
|
||||||
|
# ============================================================
|
||||||
|
param(
|
||||||
|
[string]$Tag = "",
|
||||||
|
[switch]$NoPush
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# 脚本所在目录的上一级即项目根目录,任何 cwd 下运行都正确
|
||||||
|
$Root = Split-Path -Parent $PSScriptRoot
|
||||||
|
$Registry = 'harbor.fuhua91.com'
|
||||||
|
$Project = 'live'
|
||||||
|
|
||||||
|
function Invoke-Step {
|
||||||
|
param([string]$Name, [scriptblock]$Block)
|
||||||
|
Write-Host "==> $Name" -ForegroundColor Cyan
|
||||||
|
& $Block
|
||||||
|
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "错误: $Name 失败 (exit $LASTEXITCODE)" -ForegroundColor Red
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 前端包管理器: 本机 pnpm 通常不在 PATH,回退 corepack pnpm
|
||||||
|
function Invoke-Pnpm {
|
||||||
|
param([string]$Dir, [string[]]$PnpmArgs)
|
||||||
|
Push-Location (Join-Path $Root $Dir)
|
||||||
|
try {
|
||||||
|
if (Get-Command pnpm -ErrorAction SilentlyContinue) {
|
||||||
|
& pnpm @PnpmArgs
|
||||||
|
} else {
|
||||||
|
& corepack pnpm @PnpmArgs
|
||||||
|
}
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-Frontend {
|
||||||
|
param([string]$Dir)
|
||||||
|
if (-not (Test-Path (Join-Path $Root "$Dir\node_modules"))) {
|
||||||
|
Invoke-Step "[$Dir] pnpm install" { Invoke-Pnpm -Dir $Dir -PnpmArgs @('install', '--frozen-lockfile') }
|
||||||
|
}
|
||||||
|
Invoke-Step "[$Dir] vite build" { Invoke-Pnpm -Dir $Dir -PnpmArgs @('build') }
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-Location $Root
|
||||||
|
|
||||||
|
# ---------- 0. 版本号 ----------
|
||||||
|
if (-not $Tag) {
|
||||||
|
$Tag = (& git rev-parse --short=12 HEAD).Trim()
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Host '错误: 无法获取 git commit sha' -ForegroundColor Red; exit 1 }
|
||||||
|
}
|
||||||
|
$BackendImage = "$Registry/$Project/backend:$Tag"
|
||||||
|
$FrontendImage = "$Registry/$Project/frontend:$Tag"
|
||||||
|
$LivewebImage = "$Registry/$Project/live-web:$Tag"
|
||||||
|
Write-Host "==> 打包版本: $Tag"
|
||||||
|
|
||||||
|
# ---------- 1. 后端二进制 (必须 linux/amd64,否则容器内 Exec format error) ----------
|
||||||
|
Invoke-Step 'go build (GOOS=linux GOARCH=amd64)' {
|
||||||
|
$env:GOOS = 'linux'; $env:GOARCH = 'amd64'; $env:CGO_ENABLED = '0'
|
||||||
|
go build -ldflags "-X main.Version=$Tag" -o ./bin/kratos-admin ./cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 2. 两个前端 ----------
|
||||||
|
Build-Frontend 'web' # admin 管理后台
|
||||||
|
Build-Frontend 'frontend' # live 用户端(PC 直播页)
|
||||||
|
|
||||||
|
# ---------- 3. 渲染 nginx.k8s.conf ----------
|
||||||
|
# PowerShell Set-Content 会写 UTF-8 BOM,导致 nginx 报 unknown directive;
|
||||||
|
# 必须用 WriteAllBytes 写无 BOM 内容。KRA_APP=live -> proxy_pass http://live-backend:8000
|
||||||
|
Invoke-Step '渲染 web/nginx.k8s.conf (KRA_APP=live, 去 BOM)' {
|
||||||
|
$src = Get-Content -Raw (Join-Path $Root 'web\.docker-compose\nginx\conf.d\my.conf')
|
||||||
|
$rendered = $src -replace '\$\{KRA_APP\}', $Project
|
||||||
|
$dst = Join-Path $Root 'web\nginx.k8s.conf'
|
||||||
|
[System.IO.File]::WriteAllBytes($dst, [System.Text.Encoding]::UTF8.GetBytes($rendered))
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
# 校验文件头非 EF BB BF
|
||||||
|
$head = [System.IO.File]::ReadAllBytes($dst)[0..1]
|
||||||
|
if ($head[0] -eq 0xEF -and $head[1] -eq 0xBB) {
|
||||||
|
Write-Host '错误: nginx.k8s.conf 仍含 BOM' -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 4. docker build ----------
|
||||||
|
Invoke-Step "docker build $BackendImage" {
|
||||||
|
docker build -t $BackendImage -t "$Registry/$Project/backend:latest" $Root
|
||||||
|
}
|
||||||
|
Invoke-Step "docker build $FrontendImage" {
|
||||||
|
docker build -t $FrontendImage -t "$Registry/$Project/frontend:latest" (Join-Path $Root 'web')
|
||||||
|
}
|
||||||
|
Invoke-Step "docker build $LivewebImage" {
|
||||||
|
docker build -t $LivewebImage -t "$Registry/$Project/live-web:latest" (Join-Path $Root 'frontend')
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 5. docker push ----------
|
||||||
|
if (-not $NoPush) {
|
||||||
|
foreach ($img in @($BackendImage, $FrontendImage, $LivewebImage)) {
|
||||||
|
Invoke-Step "docker push ${img}:$Tag + latest" {
|
||||||
|
docker push $img
|
||||||
|
docker push ($img -replace ":[^:]+$", ':latest')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "完成。服务器上执行: ./deploy.sh $Tag (或不带参数拉 latest)" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "构建完成 (-NoPush 未推送)。镜像: $BackendImage / $FrontendImage / $LivewebImage" -ForegroundColor Green
|
||||||
|
}
|
||||||
+17
-4
@@ -26,8 +26,8 @@ FRONTEND_PORT=9501 # admin 管理后台 nginx 容器
|
|||||||
BACKEND_PORT=9502 # 后端 API/WebSocket 容器
|
BACKEND_PORT=9502 # 后端 API/WebSocket 容器
|
||||||
LIVEWEB_PORT=9503 # live 用户端(PC 直播页) nginx 容器
|
LIVEWEB_PORT=9503 # live 用户端(PC 直播页) nginx 容器
|
||||||
LIVE_UDP_PORT="${KRA_LIVE_UDP_PORT:-9504}" # WebRTC 音视频固定 UDP 端口(与 9501-9503 顺延配套)
|
LIVE_UDP_PORT="${KRA_LIVE_UDP_PORT:-9504}" # WebRTC 音视频固定 UDP 端口(与 9501-9503 顺延配套)
|
||||||
LIVE_PUBLIC_IP="${KRA_LIVE_PUBLIC_IP:-192.168.2.226}" # 留空时从站点域名解析
|
LIVE_PUBLIC_IP="${KRA_LIVE_PUBLIC_IP:-}" # 留空时从站点域名解析
|
||||||
LIVE_HOST="${KRA_LIVE_HOST:-live.fuhua91.com}"
|
LIVE_HOST="${KRA_LIVE_HOST:-live.yuzou99.com}"
|
||||||
NETWORK="live-net"
|
NETWORK="live-net"
|
||||||
BACKEND_NAME="live-backend"
|
BACKEND_NAME="live-backend"
|
||||||
FRONTEND_NAME="live-frontend"
|
FRONTEND_NAME="live-frontend"
|
||||||
@@ -39,6 +39,18 @@ BACKEND_IMAGE="$REGISTRY/$PROJECT/backend:$TAG"
|
|||||||
FRONTEND_IMAGE="$REGISTRY/$PROJECT/frontend:$TAG"
|
FRONTEND_IMAGE="$REGISTRY/$PROJECT/frontend:$TAG"
|
||||||
LIVEWEB_IMAGE="$REGISTRY/$PROJECT/live-web:$TAG"
|
LIVEWEB_IMAGE="$REGISTRY/$PROJECT/live-web:$TAG"
|
||||||
|
|
||||||
|
# 基础环境检查(Docker / MySQL / Redis 由 server-init.sh 负责)
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "错误: 服务器未安装 Docker,请先执行基础环境初始化脚本:"
|
||||||
|
echo " bash server-init.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -qx 'live-mysql' || \
|
||||||
|
! docker ps --format '{{.Names}}' | grep -qx 'live-redis'; then
|
||||||
|
echo "警告: live-mysql / live-redis 容器未在运行,后端将连不上数据库。"
|
||||||
|
echo " 请先执行基础环境初始化脚本: bash server-init.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> 部署版本: $TAG"
|
echo "==> 部署版本: $TAG"
|
||||||
|
|
||||||
# 1. 准备挂载目录
|
# 1. 准备挂载目录
|
||||||
@@ -54,8 +66,9 @@ fi
|
|||||||
|
|
||||||
if [ -z "$LIVE_PUBLIC_IP" ]; then
|
if [ -z "$LIVE_PUBLIC_IP" ]; then
|
||||||
# 1) 域名解析,过滤内网/回环地址(服务器 /etc/hosts 可能指向内网)
|
# 1) 域名解析,过滤内网/回环地址(服务器 /etc/hosts 可能指向内网)
|
||||||
LIVE_PUBLIC_IP="$(getent ahostsv4 "$LIVE_HOST" | awk '{print $1}' | grep -E '^[0-9]+(\.[0-9]+){3}$' \
|
# getent 域名不存在时返回非零且无输出,须吞掉退出码,否则 pipefail+set -e 会静默退出
|
||||||
| grep -vE '^(10\.|127\.|169\.254\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)' | head -n1)"
|
LIVE_PUBLIC_IP="$( { timeout 10 getent ahostsv4 "$LIVE_HOST" 2>/dev/null || true; } | awk '{print $1}' | grep -E '^[0-9]+(\.[0-9]+){3}$' \
|
||||||
|
| grep -vE '^(10\.|127\.|169\.254\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)' | head -n1 || true)"
|
||||||
fi
|
fi
|
||||||
if [ -z "$LIVE_PUBLIC_IP" ]; then
|
if [ -z "$LIVE_PUBLIC_IP" ]; then
|
||||||
# 2) 域名取不到公网 IP 时,回退查询公网出口 IP(云主机 EIP 1:1 NAT 场景一致)
|
# 2) 域名取不到公网 IP 时,回退查询公网出口 IP(云主机 EIP 1:1 NAT 场景一致)
|
||||||
|
|||||||
@@ -0,0 +1,770 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# live.fuhua91.com 生产服务器基础环境初始化脚本(幂等,可重复执行)
|
||||||
|
#
|
||||||
|
# 功能:
|
||||||
|
# 1. 安装 Docker(自动识别 CentOS/Rocky/Alma/Ubuntu/Debian)
|
||||||
|
# 2. 启动 MySQL 8 容器(root 强密码,外部端口 13306,数据落盘 /www/live-data/mysql)
|
||||||
|
# 3. 启动 Redis 7 容器(强密码,仅容器网络内可访问 + 本机 127.0.0.1:16379 调试口)
|
||||||
|
# 4. 生成宿主机 nginx 站点配置(80/443 -> 容器端口 9501/9502/9503,自签证书启用 https,
|
||||||
|
# 并清理 nginx.conf / conf.d 中旧 live 站点的反代配置)
|
||||||
|
# 5. 自动生成后端配置 /www/wwwroot/live.fuhua91.com/config/config.yaml(写入生成的密码)
|
||||||
|
# 6. 凭据保存到 /root/live-credentials.txt(chmod 600)
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# bash server-init.sh 仅初始化基础环境
|
||||||
|
# bash server-init.sh latest 初始化完成后立即用 deploy.sh 部署 latest 版本
|
||||||
|
# HARBOR_PASSWORD='xxx' bash server-init.sh latest 连 Harbor 密码一起传给 deploy.sh
|
||||||
|
#
|
||||||
|
# 建议: 放到 /root/server-init.sh,执行 chmod 700 /root/server-init.sh
|
||||||
|
# ============================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------- 可按需调整的参数 ----------
|
||||||
|
MYSQL_CONTAINER="live-mysql"
|
||||||
|
REDIS_CONTAINER="live-redis"
|
||||||
|
NETWORK="live-net"
|
||||||
|
|
||||||
|
MYSQL_IMAGE="mysql:8.0"
|
||||||
|
REDIS_IMAGE="redis:7-alpine"
|
||||||
|
|
||||||
|
DATA_DIR="/www/live-data" # MySQL/Redis/网关配置/证书等落盘目录
|
||||||
|
|
||||||
|
MYSQL_EXTERNAL_PORT=13306 # MySQL 对外端口
|
||||||
|
REDIS_LOCAL_PORT=16379 # Redis 仅绑定 127.0.0.1 的调试端口,不对公网开放
|
||||||
|
|
||||||
|
LIVE_HOST="${KRA_LIVE_HOST:-live.yuzou99.com}"
|
||||||
|
HOST_NGINX_CONF="/etc/nginx/conf.d/live.fuhua91.com.conf" # 宿主机 nginx 站点配置
|
||||||
|
SSL_WEBROOT="$DATA_DIR/ssl/webroot" # acme.sh http-01 验证目录
|
||||||
|
SSL_CERT_DIR="$DATA_DIR/ssl/certs" # HTTPS 证书目录
|
||||||
|
|
||||||
|
BASE_DIR="/www/wwwroot/live.fuhua91.com"
|
||||||
|
CONFIG_DIR="$BASE_DIR/config" # 后端配置目录 -> 容器内 /data/conf
|
||||||
|
CRED_FILE="/root/live-credentials.txt"
|
||||||
|
# Docker Hub 镜像加速(拉不动 mysql/redis 时 docker 会自动回退官方源;
|
||||||
|
# 如你的服务器可直接访问 Docker Hub,可清空此数组)
|
||||||
|
DOCKER_MIRRORS=("https://docker.1ms.run" "https://docker.m.daocloud.io")
|
||||||
|
# --------------------------------------
|
||||||
|
|
||||||
|
TAG="${1:-}"
|
||||||
|
|
||||||
|
# ---------- 通用 ----------
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "错误: 请用 root 执行本脚本"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 生成 24 位强密码(字符集避开 MySQL DSN 分隔符 @ : / ? 以及 shell/YAML 敏感字符)
|
||||||
|
gen_pass() {
|
||||||
|
local pass=""
|
||||||
|
while [ "${#pass}" -lt 24 ]; do
|
||||||
|
pass="${pass}$(tr -dc 'A-Za-z0-9#$%^*+_-' < /dev/urandom | head -c 32 || true)"
|
||||||
|
done
|
||||||
|
printf '%s' "${pass:0:24}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 1. 安装 Docker ----------
|
||||||
|
if [ -r /etc/os-release ]; then
|
||||||
|
. /etc/os-release
|
||||||
|
OS_ID="${ID:-unknown}"
|
||||||
|
OS_MAJOR="${VERSION_ID%%.*}"
|
||||||
|
else
|
||||||
|
OS_ID="unknown"
|
||||||
|
OS_MAJOR=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
fix_centos7_repos() {
|
||||||
|
echo "==> 检测到 CentOS 7(已 EOL),切换 yum 源到 vault"
|
||||||
|
mkdir -p /etc/yum.repos.d/backup
|
||||||
|
for f in /etc/yum.repos.d/CentOS-*.repo; do
|
||||||
|
[ -e "$f" ] || return 0
|
||||||
|
mv "$f" /etc/yum.repos.d/backup/ 2>/dev/null || true
|
||||||
|
done
|
||||||
|
curl -fsSL -o /etc/yum.repos.d/CentOS-Base.repo \
|
||||||
|
https://mirrors.aliyun.com/repo/Centos-7.repo 2>/dev/null || true
|
||||||
|
yum clean all >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_rhel() {
|
||||||
|
yum install -y yum-utils 2>/dev/null || yum install -y --allowerasing yum-utils
|
||||||
|
if ! yum-config-manager --add-repo \
|
||||||
|
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo; then
|
||||||
|
yum-config-manager --add-repo \
|
||||||
|
https://download.docker.com/linux/centos/docker-ce.repo
|
||||||
|
fi
|
||||||
|
yum makecache fast >/dev/null 2>&1 || yum makecache >/dev/null 2>&1 || true
|
||||||
|
yum install -y docker-ce docker-ce-cli containerd.io \
|
||||||
|
|| yum install -y --allowerasing docker-ce docker-ce-cli containerd.io
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_deb() {
|
||||||
|
local distro="$1" codename
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y ca-certificates curl gnupg lsb-release
|
||||||
|
codename="$(lsb_release -cs 2>/dev/null || echo stable)"
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL "https://mirrors.aliyun.com/docker-ce/linux/${distro}/gpg" \
|
||||||
|
| gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||||
|
https://mirrors.aliyun.com/docker-ce/linux/${distro} ${codename} stable" \
|
||||||
|
> /etc/apt/sources.list.d/docker.list
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_docker() {
|
||||||
|
# 日志轮转 + 镜像加速,避免容器日志撑爆磁盘
|
||||||
|
[ -f /etc/docker/daemon.json ] && cp /etc/docker/daemon.json "/etc/docker/daemon.json.bak.$(date +%s)" || true
|
||||||
|
local mirrors_json="[]"
|
||||||
|
if [ "${#DOCKER_MIRRORS[@]}" -gt 0 ]; then
|
||||||
|
mirrors_json=$(printf '"%s",' "${DOCKER_MIRRORS[@]}")
|
||||||
|
mirrors_json="[${mirrors_json%,}]"
|
||||||
|
fi
|
||||||
|
cat > /etc/docker/daemon.json <<EOF
|
||||||
|
{
|
||||||
|
"log-driver": "json-file",
|
||||||
|
"log-opts": { "max-size": "50m", "max-file": "3" },
|
||||||
|
"registry-mirrors": ${mirrors_json}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart docker
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker() {
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "==> Docker 已安装: $(docker --version),跳过安装"
|
||||||
|
configure_docker
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "==> 安装 Docker(系统: ${OS_ID} ${OS_MAJOR:-})"
|
||||||
|
case "$OS_ID" in
|
||||||
|
centos|rhel|rocky|almalinux|anolis|opencloudos|tencentos)
|
||||||
|
[ "$OS_MAJOR" = "7" ] && fix_centos7_repos
|
||||||
|
install_docker_rhel
|
||||||
|
;;
|
||||||
|
ubuntu|debian)
|
||||||
|
install_docker_deb "$OS_ID"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "错误: 未识别的系统 ${OS_ID},请手动安装 Docker 后重新执行本脚本"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
systemctl enable --now docker
|
||||||
|
configure_docker
|
||||||
|
echo "==> Docker 安装完成: $(docker --version)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 2. 读取/生成凭据 ----------
|
||||||
|
MYSQL_ROOT_PASSWORD=""
|
||||||
|
REDIS_PASSWORD=""
|
||||||
|
if [ -f "$CRED_FILE" ]; then
|
||||||
|
MYSQL_ROOT_PASSWORD="$(sed -n "s/^MYSQL_ROOT_PASSWORD='\(.*\)'$/\1/p" "$CRED_FILE")"
|
||||||
|
REDIS_PASSWORD="$(sed -n "s/^REDIS_PASSWORD='\(.*\)'$/\1/p" "$CRED_FILE")"
|
||||||
|
fi
|
||||||
|
if [ -z "$MYSQL_ROOT_PASSWORD" ]; then
|
||||||
|
MYSQL_ROOT_PASSWORD="$(gen_pass)"
|
||||||
|
fi
|
||||||
|
if [ -z "$REDIS_PASSWORD" ]; then
|
||||||
|
REDIS_PASSWORD="$(gen_pass)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
save_credentials() {
|
||||||
|
cat > "$CRED_FILE" <<EOF
|
||||||
|
# ============================================================
|
||||||
|
# live.fuhua91.com 生产环境凭据(生成于 $(date '+%F %T'))
|
||||||
|
# 请妥善保管,勿提交到代码仓库
|
||||||
|
# ============================================================
|
||||||
|
MYSQL_ROOT_PASSWORD='${MYSQL_ROOT_PASSWORD}'
|
||||||
|
MYSQL_EXTERNAL_PORT='${MYSQL_EXTERNAL_PORT}'
|
||||||
|
REDIS_PASSWORD='${REDIS_PASSWORD}'
|
||||||
|
BACKEND_CONFIG='${CONFIG_DIR}/config.yaml'
|
||||||
|
EOF
|
||||||
|
chmod 600 "$CRED_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 3. MySQL ----------
|
||||||
|
run_mysql() {
|
||||||
|
docker network create "$NETWORK" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
if docker ps --format '{{.Names}}' | grep -qx "$MYSQL_CONTAINER"; then
|
||||||
|
echo "==> $MYSQL_CONTAINER 已在运行,跳过"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# 容器存在但停止:直接启动(数据卷里已有旧密码,不能重置环境变量)
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -qx "$MYSQL_CONTAINER"; then
|
||||||
|
echo "==> 启动已存在的 $MYSQL_CONTAINER"
|
||||||
|
docker start "$MYSQL_CONTAINER"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> 首次启动 $MYSQL_CONTAINER($MYSQL_IMAGE,外部端口 $MYSQL_EXTERNAL_PORT)"
|
||||||
|
mkdir -p "$DATA_DIR/mysql"
|
||||||
|
docker run -d --name "$MYSQL_CONTAINER" \
|
||||||
|
--restart always \
|
||||||
|
--network "$NETWORK" \
|
||||||
|
-p "${MYSQL_EXTERNAL_PORT}:3306" \
|
||||||
|
-e TZ=Asia/Shanghai \
|
||||||
|
-e MYSQL_ROOT_PASSWORD="$MYSQL_ROOT_PASSWORD" \
|
||||||
|
-e MYSQL_DATABASE=yzyzlive \
|
||||||
|
-v "$DATA_DIR/mysql:/var/lib/mysql" \
|
||||||
|
"$MYSQL_IMAGE" \
|
||||||
|
--character-set-server=utf8mb4 \
|
||||||
|
--collation-server=utf8mb4_general_ci \
|
||||||
|
--default-authentication-plugin=mysql_native_password
|
||||||
|
|
||||||
|
echo "==> 等待 MySQL 就绪(首次启动约 20-60 秒)"
|
||||||
|
local i ready=0
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if docker exec "$MYSQL_CONTAINER" mysqladmin ping -uroot -p"$MYSQL_ROOT_PASSWORD" --silent >/dev/null 2>&1; then
|
||||||
|
ready=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
if [ "$ready" -ne 1 ]; then
|
||||||
|
echo "错误: MySQL 未就绪,请执行 docker logs $MYSQL_CONTAINER 排查"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> MySQL 就绪(数据库 yzyzlive 已创建)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 4. Redis ----------
|
||||||
|
run_redis() {
|
||||||
|
if docker ps --format '{{.Names}}' | grep -qx "$REDIS_CONTAINER"; then
|
||||||
|
echo "==> $REDIS_CONTAINER 已在运行,跳过"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -qx "$REDIS_CONTAINER"; then
|
||||||
|
echo "==> 启动已存在的 $REDIS_CONTAINER"
|
||||||
|
docker start "$REDIS_CONTAINER"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> 首次启动 $REDIS_CONTAINER($REDIS_IMAGE,不对公网开放)"
|
||||||
|
mkdir -p "$DATA_DIR/redis"
|
||||||
|
docker run -d --name "$REDIS_CONTAINER" \
|
||||||
|
--restart always \
|
||||||
|
--network "$NETWORK" \
|
||||||
|
-p "127.0.0.1:${REDIS_LOCAL_PORT}:6379" \
|
||||||
|
-e TZ=Asia/Shanghai \
|
||||||
|
-v "$DATA_DIR/redis:/data" \
|
||||||
|
"$REDIS_IMAGE" \
|
||||||
|
redis-server --requirepass "$REDIS_PASSWORD" --appendonly yes
|
||||||
|
|
||||||
|
local i
|
||||||
|
for i in $(seq 1 10); do
|
||||||
|
if docker exec "$REDIS_CONTAINER" redis-cli -a "$REDIS_PASSWORD" --no-auth-warning ping \
|
||||||
|
2>/dev/null | grep -q PONG; then
|
||||||
|
echo "==> Redis 就绪(本机调试口 127.0.0.1:${REDIS_LOCAL_PORT})"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "错误: Redis 启动异常,请执行 docker logs $REDIS_CONTAINER 排查"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 4.5 宿主机 nginx 站点配置(80/443 -> 容器端口 9501/9502/9503) ----------
|
||||||
|
# 清理 nginx.conf 及 conf.d 中旧站点(live.fuhua91.com / live.yuzhou99.com / live.yuzou99.com / test-zb.yzyzzb.com)的反代配置(整块删除含目标域名的 server 块)
|
||||||
|
clean_old_nginx_proxy() {
|
||||||
|
local f=/etc/nginx/nginx.conf
|
||||||
|
if [ -f "$f" ] && grep -qE 'live\.(fuhua91|yuzhou99|yuzou99)\.com|test-zb\.yzyzzb\.com' "$f"; then
|
||||||
|
cp "$f" "$f.bak.$(date +%s)"
|
||||||
|
awk '
|
||||||
|
BEGIN { in_server=0; depth=0 }
|
||||||
|
!in_server {
|
||||||
|
if ($0 ~ /^[[:space:]]*server[[:space:]]*\{/) {
|
||||||
|
in_server=1; depth=1; buf=$0 "\n"; next
|
||||||
|
}
|
||||||
|
print; next
|
||||||
|
}
|
||||||
|
in_server {
|
||||||
|
buf = buf $0 "\n"
|
||||||
|
o = gsub(/\{/, "{"); c = gsub(/\}/, "}")
|
||||||
|
depth += o - c
|
||||||
|
if (depth <= 0) {
|
||||||
|
if (buf !~ /live\.(fuhua91|yuzhou99|yuzou99)\.com|test-zb\.yzyzzb\.com/) printf "%s", buf
|
||||||
|
in_server=0; buf=""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
|
||||||
|
echo "==> 已从 nginx.conf 移除旧 live 站点反代配置(备份: $f.bak.*)"
|
||||||
|
fi
|
||||||
|
local cf my_conf my_ssl_conf
|
||||||
|
my_conf="$HOST_NGINX_CONF"
|
||||||
|
my_ssl_conf="${HOST_NGINX_CONF%.conf}.ssl.conf"
|
||||||
|
for cf in /etc/nginx/conf.d/*.conf; do
|
||||||
|
[ -e "$cf" ] || continue
|
||||||
|
[ "$cf" = "$my_conf" ] && continue
|
||||||
|
[ "$cf" = "$my_ssl_conf" ] && continue
|
||||||
|
if grep -qE 'live\.(fuhua91|yuzhou99|yuzou99)\.com|test-zb\.yzyzzb\.com' "$cf"; then
|
||||||
|
mv "$cf" "$cf.bak.$(date +%s)"
|
||||||
|
echo "==> 已移除旧站点配置: $cf(备份为 .bak.*)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
write_host_nginx_conf() {
|
||||||
|
# 清理旧方案的网关容器(如曾创建)
|
||||||
|
docker rm -f live-gateway >/dev/null 2>&1 || true
|
||||||
|
mkdir -p "$SSL_WEBROOT" "$SSL_CERT_DIR"
|
||||||
|
|
||||||
|
if ! command -v nginx >/dev/null 2>&1; then
|
||||||
|
echo "警告: 宿主机未安装 nginx,跳过站点配置。请安装后重新执行本脚本:"
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
echo " apt-get install -y nginx"
|
||||||
|
else
|
||||||
|
echo " yum install -y nginx && systemctl enable --now nginx"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 移除 nginx.conf / conf.d 中旧 live 站点的反代配置,避免冲突
|
||||||
|
clean_old_nginx_proxy
|
||||||
|
|
||||||
|
# 确保 nginx.conf include 了 conf.d/*.conf(旧反代直接写在 nginx.conf 的机器常缺这条)
|
||||||
|
if ! grep -Eq 'include[[:space:]]+.*conf\.d/\*\.conf' /etc/nginx/nginx.conf; then
|
||||||
|
mkdir -p /etc/nginx/conf.d
|
||||||
|
if grep -qE '^[[:space:]]*http[[:space:]]*\{' /etc/nginx/nginx.conf; then
|
||||||
|
cp /etc/nginx/nginx.conf "/etc/nginx/nginx.conf.bak.$(date +%s)"
|
||||||
|
sed -i '/^[[:space:]]*http[[:space:]]*\{/a\ include /etc/nginx/conf.d/*.conf;' /etc/nginx/nginx.conf
|
||||||
|
echo "==> 已在 nginx.conf 的 http {} 中加入 include /etc/nginx/conf.d/*.conf"
|
||||||
|
else
|
||||||
|
echo "警告: nginx.conf 中未找到 http {} 块,请手动加入 include conf.d/*.conf 后重载"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$HOST_NGINX_CONF" ] && cp "$HOST_NGINX_CONF" "$HOST_NGINX_CONF.bak.$(date +%s)"
|
||||||
|
|
||||||
|
echo "==> 写入宿主机 nginx 站点配置: $HOST_NGINX_CONF"
|
||||||
|
cat > "$HOST_NGINX_CONF" <<'NGINXCONF'
|
||||||
|
# live.fuhua91.com 站点配置(server-init.sh 生成;HTTPS 模板见 live.fuhua91.com.ssl.conf.example)
|
||||||
|
# 应用容器端口(deploy.sh 映射): admin=9501, 后端=9502, 主站=9503
|
||||||
|
|
||||||
|
# 变量名带 live_ 前缀,避免与宿主机 nginx 其他配置中的 map 重名冲突
|
||||||
|
map $http_upgrade $live_connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name live.fuhua91.com;
|
||||||
|
|
||||||
|
# 启用 HTTPS 后取消下行注释(acme 续期会经 301 走 https 验证,仍可用)
|
||||||
|
#return 301 https://$host$request_uri;
|
||||||
|
|
||||||
|
# admin 媒体上传,默认 1m 会挡上传
|
||||||
|
client_max_body_size 512m;
|
||||||
|
|
||||||
|
# CORS 全放行:反射 Origin(兼容带 cookie 的请求,* 与 credentials 不兼容)
|
||||||
|
add_header Access-Control-Allow-Origin $http_origin always;
|
||||||
|
add_header Access-Control-Allow-Credentials true always;
|
||||||
|
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS' always;
|
||||||
|
add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
|
||||||
|
add_header Vary Origin always;
|
||||||
|
|
||||||
|
# 隐藏后端自带的 CORS header,避免与上面重复导致浏览器报错
|
||||||
|
proxy_hide_header Access-Control-Allow-Origin;
|
||||||
|
proxy_hide_header Access-Control-Allow-Credentials;
|
||||||
|
proxy_hide_header Access-Control-Allow-Methods;
|
||||||
|
proxy_hide_header Access-Control-Allow-Headers;
|
||||||
|
proxy_hide_header Access-Control-Expose-Headers;
|
||||||
|
proxy_hide_header Access-Control-Max-Age;
|
||||||
|
|
||||||
|
# 预检请求直接响应,不转发后端(if 块的 add_header 会覆盖上面的,需写全)
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header Access-Control-Allow-Origin $http_origin always;
|
||||||
|
add_header Access-Control-Allow-Credentials true always;
|
||||||
|
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS' always;
|
||||||
|
add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
|
||||||
|
add_header Access-Control-Max-Age 86400 always;
|
||||||
|
add_header Vary Origin always;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
# acme.sh http-01 验证
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /www/live-data/ssl/webroot;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 后端 API / WebSocket(去掉 /api 前缀转给后端容器 9502)
|
||||||
|
location = /api {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $live_connection_upgrade;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_pass http://127.0.0.1:9502;
|
||||||
|
}
|
||||||
|
location ^~ /api/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $live_connection_upgrade;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
rewrite ^/api/(.*)$ /$1 break;
|
||||||
|
proxy_pass http://127.0.0.1:9502;
|
||||||
|
}
|
||||||
|
|
||||||
|
# admin 管理后台(去掉 /admin 前缀转给 admin 前端容器 9501)
|
||||||
|
location = /admin {
|
||||||
|
return 301 /admin/;
|
||||||
|
}
|
||||||
|
location ^~ /admin/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
rewrite ^/admin/(.*)$ /$1 break;
|
||||||
|
proxy_pass http://127.0.0.1:9501;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主站直播页(live-web 容器 9503)
|
||||||
|
location / {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_pass http://127.0.0.1:9503;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NGINXCONF
|
||||||
|
|
||||||
|
# HTTPS 模板(.ssl.conf.example 后缀不会被 *.conf include,复制为 .ssl.conf 后生效)
|
||||||
|
echo "==> 写入 HTTPS 配置模板: ${HOST_NGINX_CONF%.conf}.ssl.conf.example"
|
||||||
|
cat > "${HOST_NGINX_CONF%.conf}.ssl.conf.example" <<'SSLEX'
|
||||||
|
# 启用 HTTPS 三步:
|
||||||
|
# 1. 证书安装到 /www/live-data/ssl/certs/{fullchain.pem,privkey.pem}
|
||||||
|
# 2. cp live.fuhua91.com.ssl.conf.example live.fuhua91.com.ssl.conf
|
||||||
|
# 3. nginx -t && systemctl reload nginx
|
||||||
|
# 4. 在 live.fuhua91.com.conf 打开 return 301 注释后再 reload 一次
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2 default_server;
|
||||||
|
server_name live.fuhua91.com;
|
||||||
|
|
||||||
|
ssl_certificate /www/live-data/ssl/certs/fullchain.pem;
|
||||||
|
ssl_certificate_key /www/live-data/ssl/certs/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
client_max_body_size 512m;
|
||||||
|
|
||||||
|
# CORS 全放行(同 80 端口 server 块)
|
||||||
|
add_header Access-Control-Allow-Origin $http_origin always;
|
||||||
|
add_header Access-Control-Allow-Credentials true always;
|
||||||
|
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS' always;
|
||||||
|
add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
|
||||||
|
add_header Vary Origin always;
|
||||||
|
|
||||||
|
proxy_hide_header Access-Control-Allow-Origin;
|
||||||
|
proxy_hide_header Access-Control-Allow-Credentials;
|
||||||
|
proxy_hide_header Access-Control-Allow-Methods;
|
||||||
|
proxy_hide_header Access-Control-Allow-Headers;
|
||||||
|
proxy_hide_header Access-Control-Expose-Headers;
|
||||||
|
proxy_hide_header Access-Control-Max-Age;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header Access-Control-Allow-Origin $http_origin always;
|
||||||
|
add_header Access-Control-Allow-Credentials true always;
|
||||||
|
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS' always;
|
||||||
|
add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
|
||||||
|
add_header Access-Control-Max-Age 86400 always;
|
||||||
|
add_header Vary Origin always;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
# acme.sh http-01 验证(续期会经 301 走 https 验证,仍可用)
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /www/live-data/ssl/webroot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /api {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $live_connection_upgrade;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_pass http://127.0.0.1:9502;
|
||||||
|
}
|
||||||
|
location ^~ /api/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $live_connection_upgrade;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
rewrite ^/api/(.*)$ /$1 break;
|
||||||
|
proxy_pass http://127.0.0.1:9502;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /admin {
|
||||||
|
return 301 /admin/;
|
||||||
|
}
|
||||||
|
location ^~ /admin/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
rewrite ^/admin/(.*)$ /$1 break;
|
||||||
|
proxy_pass http://127.0.0.1:9501;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_pass http://127.0.0.1:9503;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SSLEX
|
||||||
|
|
||||||
|
# 域名统一替换(默认 live.yuzou99.com,可用 KRA_LIVE_HOST 覆盖)
|
||||||
|
sed -i "s/live\.fuhua91\.com/${LIVE_HOST}/g" "$HOST_NGINX_CONF"
|
||||||
|
sed -i "s/live\.fuhua91\.com/${LIVE_HOST}/g" "${HOST_NGINX_CONF%.conf}.ssl.conf.example"
|
||||||
|
|
||||||
|
# 自签证书(已存在则复用;浏览器提示不安全属正常,正式证书可后补)
|
||||||
|
if [ ! -f "$SSL_CERT_DIR/fullchain.pem" ]; then
|
||||||
|
echo "==> 生成自签证书: $LIVE_HOST(10 年有效期)"
|
||||||
|
openssl req -x509 -nodes -newkey rsa:2048 -days 3650 \
|
||||||
|
-keyout "$SSL_CERT_DIR/privkey.pem" -out "$SSL_CERT_DIR/fullchain.pem" \
|
||||||
|
-subj "/CN=${LIVE_HOST}" \
|
||||||
|
-addext "subjectAltName=DNS:${LIVE_HOST}" 2>/dev/null \
|
||||||
|
|| openssl req -x509 -nodes -newkey rsa:2048 -days 3650 \
|
||||||
|
-keyout "$SSL_CERT_DIR/privkey.pem" -out "$SSL_CERT_DIR/fullchain.pem" \
|
||||||
|
-subj "/CN=${LIVE_HOST}"
|
||||||
|
chmod 600 "$SSL_CERT_DIR/privkey.pem"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 启用 443(自签证书)+ 80 强跳 https
|
||||||
|
cp -f "${HOST_NGINX_CONF%.conf}.ssl.conf.example" "${HOST_NGINX_CONF%.conf}.ssl.conf"
|
||||||
|
sed -i 's|^ #return 301 https://| return 301 https://|' "$HOST_NGINX_CONF"
|
||||||
|
|
||||||
|
# SELinux 开启时允许 nginx 反代网络端口(CentOS/Rocky 常见 502 原因)
|
||||||
|
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce 2>/dev/null)" = "Enforcing" ]; then
|
||||||
|
setsebool -P httpd_can_network_connect 1 2>/dev/null \
|
||||||
|
|| echo "警告: setsebool 失败,nginx 反代容器端口可能 502"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if nginx -t 2>/dev/null; then
|
||||||
|
systemctl enable nginx >/dev/null 2>&1 || true
|
||||||
|
if systemctl is-active --quiet nginx; then
|
||||||
|
systemctl reload nginx 2>/dev/null || nginx -s reload 2>/dev/null || true
|
||||||
|
else
|
||||||
|
systemctl start nginx 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo "==> nginx 配置已生效(80/443 -> 9503 主站 / 9501 admin / 9502 api,https 为自签证书)"
|
||||||
|
else
|
||||||
|
echo "警告: nginx -t 校验未通过,请手动检查后重载:"
|
||||||
|
nginx -t || true
|
||||||
|
echo " nginx -t && systemctl reload nginx"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
# ---------- 5. 生成后端配置 ----------
|
||||||
|
write_backend_config() {
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
if [ ! -f "$CONFIG_DIR/config.yaml" ]; then
|
||||||
|
echo "==> 生成后端配置: $CONFIG_DIR/config.yaml"
|
||||||
|
cat > "$CONFIG_DIR/config.yaml" <<'CONF'
|
||||||
|
server:
|
||||||
|
http:
|
||||||
|
network: tcp
|
||||||
|
addr: 0.0.0.0:8000
|
||||||
|
timeout: 600s
|
||||||
|
data:
|
||||||
|
database:
|
||||||
|
driver: mysql
|
||||||
|
source: ""
|
||||||
|
host: live-mysql
|
||||||
|
port: "3306"
|
||||||
|
user: root
|
||||||
|
password: '__MYSQL_ROOT_PASSWORD__'
|
||||||
|
name: yzyzlive
|
||||||
|
config: charset=utf8mb4&parseTime=True&loc=Local
|
||||||
|
path: ""
|
||||||
|
alias_name: ""
|
||||||
|
disable: false
|
||||||
|
prefix: ""
|
||||||
|
engine: InnoDB
|
||||||
|
log_mode: info
|
||||||
|
max_idle_conns: 10
|
||||||
|
max_open_conns: 100
|
||||||
|
conn_max_lifetime: 3600
|
||||||
|
singular: false
|
||||||
|
redis:
|
||||||
|
network: tcp
|
||||||
|
name: default
|
||||||
|
addr: live-redis:6379
|
||||||
|
password: '__REDIS_PASSWORD__'
|
||||||
|
db: 0
|
||||||
|
use_cluster: false
|
||||||
|
cluster_addrs: []
|
||||||
|
read_timeout: 0.200s
|
||||||
|
write_timeout: 0.200s
|
||||||
|
database_list: []
|
||||||
|
redis_list: []
|
||||||
|
mongo:
|
||||||
|
coll: ""
|
||||||
|
options: ""
|
||||||
|
database: ""
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
|
auth_source: ""
|
||||||
|
min_pool_size: "0"
|
||||||
|
max_pool_size: "100"
|
||||||
|
socket_timeout_ms: "0"
|
||||||
|
connect_timeout_ms: "0"
|
||||||
|
is_zap: false
|
||||||
|
hosts:
|
||||||
|
- host: ""
|
||||||
|
port: ""
|
||||||
|
admin:
|
||||||
|
router_prefix: ""
|
||||||
|
jwt:
|
||||||
|
signing_key: '__JWT_SIGNING_KEY__'
|
||||||
|
expires_time: 604800s
|
||||||
|
buffer_time: 86400s
|
||||||
|
issuer: kra
|
||||||
|
captcha:
|
||||||
|
key_long: 6
|
||||||
|
img_width: 240
|
||||||
|
img_height: 80
|
||||||
|
store_expiration: 180s
|
||||||
|
local:
|
||||||
|
store_path: uploads/file
|
||||||
|
path_prefix: uploads/file
|
||||||
|
media:
|
||||||
|
session_ttl: 24
|
||||||
|
max_file_size: "0"
|
||||||
|
chunk_dir: uploads/chunks
|
||||||
|
system:
|
||||||
|
use_redis: false
|
||||||
|
use_multipoint: false
|
||||||
|
use_strict_auth: false
|
||||||
|
disable_auto_migrate: false
|
||||||
|
use_mongo: false
|
||||||
|
addr: 8000
|
||||||
|
iplimit_count: 0
|
||||||
|
iplimit_time: 0
|
||||||
|
zap:
|
||||||
|
level: info
|
||||||
|
prefix: '[kra] '
|
||||||
|
format: json
|
||||||
|
director: logs
|
||||||
|
encode_level: LowercaseLevelEncoder
|
||||||
|
stacktrace_key: stacktrace
|
||||||
|
show_line: true
|
||||||
|
log_in_console: true
|
||||||
|
retention_day: 7
|
||||||
|
access_req_body: true
|
||||||
|
access_resp_data: true
|
||||||
|
access_req_headers: true
|
||||||
|
access_log_max_bytes: 1024
|
||||||
|
file_only_modules:
|
||||||
|
- http
|
||||||
|
- sql
|
||||||
|
cors:
|
||||||
|
mode: allow-all
|
||||||
|
whitelist: []
|
||||||
|
app:
|
||||||
|
node: ""
|
||||||
|
app_id: kra
|
||||||
|
env: production
|
||||||
|
disk_list:
|
||||||
|
- mount_point: /
|
||||||
|
CONF
|
||||||
|
sed -i "s|__MYSQL_ROOT_PASSWORD__|${MYSQL_ROOT_PASSWORD}|; s|__REDIS_PASSWORD__|${REDIS_PASSWORD}|; s|__JWT_SIGNING_KEY__|$(gen_pass)|" \
|
||||||
|
"$CONFIG_DIR/config.yaml"
|
||||||
|
else
|
||||||
|
echo "==> 后端配置已存在,跳过生成: $CONFIG_DIR/config.yaml"
|
||||||
|
fi
|
||||||
|
# 容器内进程以 uid 1000 (kra) 运行:
|
||||||
|
# - 配置文件必须对其可读,否则 panic: permission denied
|
||||||
|
# - 配置目录必须对其可写,否则首装向导无法把生成的 JWT signing_key 写回文件
|
||||||
|
chown -R 1000:1000 "$CONFIG_DIR"
|
||||||
|
chmod 600 "$CONFIG_DIR/config.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 6. 放行防火墙端口(尽力而为) ----------
|
||||||
|
open_firewall() {
|
||||||
|
local ports=("13306/tcp" "80/tcp" "443/tcp" "9504/udp")
|
||||||
|
echo "==> 尝试放行防火墙端口 ${ports[*]}(云安全组需自行放行)"
|
||||||
|
local p
|
||||||
|
for p in "${ports[@]}"; do
|
||||||
|
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
|
||||||
|
firewall-cmd --permanent --add-port="${p}" >/dev/null 2>&1 || true
|
||||||
|
firewall-cmd --reload >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then
|
||||||
|
ufw allow "${p}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 主流程 ----------
|
||||||
|
install_docker
|
||||||
|
save_credentials
|
||||||
|
run_mysql
|
||||||
|
run_redis
|
||||||
|
write_backend_config
|
||||||
|
write_host_nginx_conf
|
||||||
|
open_firewall
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo " 基础环境初始化完成"
|
||||||
|
echo "============================================================"
|
||||||
|
echo " MySQL: live-mysql 容器, 外部端口 $MYSQL_EXTERNAL_PORT, 数据目录 $DATA_DIR/mysql"
|
||||||
|
echo " Redis: live-redis 容器, 仅容器网络 + 127.0.0.1:$REDIS_LOCAL_PORT"
|
||||||
|
echo " nginx: 宿主机 nginx 站点配置 -> 主站(9503)/admin(9501)/api(9502)"
|
||||||
|
echo " HTTPS: 已用自签证书启用($LIVE_HOST),浏览器提示不安全属正常"
|
||||||
|
echo " 后端配置: $CONFIG_DIR/config.yaml"
|
||||||
|
echo " 凭据文件: $CRED_FILE (MySQL root / Redis 密码都在里面)"
|
||||||
|
echo ""
|
||||||
|
echo " 云服务器安全组请放行: $MYSQL_EXTERNAL_PORT/tcp, 80/tcp, 443/tcp, 9504/udp(WebRTC)"
|
||||||
|
echo " 请确认 DNS: $LIVE_HOST 已解析到本机公网 IP"
|
||||||
|
echo ""
|
||||||
|
echo " 换正式证书(可选,替换自签证书,同路径自动 reload):"
|
||||||
|
echo " curl https://get.acme.sh | sh -s email=you@example.com"
|
||||||
|
echo " ~/.acme.sh/acme.sh --issue -d $LIVE_HOST -w $SSL_WEBROOT --server letsencrypt"
|
||||||
|
echo " ~/.acme.sh/acme.sh --install-cert -d $LIVE_HOST \\"
|
||||||
|
echo " --fullchain-file $SSL_CERT_DIR/fullchain.pem \\"
|
||||||
|
echo " --key-file $SSL_CERT_DIR/privkey.pem \\"
|
||||||
|
echo " --reloadcmd 'nginx -s reload'"
|
||||||
|
echo ""
|
||||||
|
if [ -n "$TAG" ]; then
|
||||||
|
echo "==> 继续部署应用版本: $TAG"
|
||||||
|
DEPLOY_SH="$(cd "$(dirname "$0")" && pwd)/deploy.sh"
|
||||||
|
[ -f "$DEPLOY_SH" ] || DEPLOY_SH="/root/deploy.sh"
|
||||||
|
if [ -f "$DEPLOY_SH" ]; then
|
||||||
|
bash "$DEPLOY_SH" "$TAG"
|
||||||
|
else
|
||||||
|
echo "错误: 找不到 deploy.sh(与 server-init.sh 同目录或 /root/deploy.sh),请手动执行部署"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " 下一步: 上传 deploy.sh 到服务器后执行(Harbor 需先登录):"
|
||||||
|
echo " docker login harbor.fuhua91.com -u 'robot\$live+live'"
|
||||||
|
echo " bash /root/deploy.sh <tag>"
|
||||||
|
fi
|
||||||
@@ -39,15 +39,23 @@ func (a *wechatV3Adapter) Create(ctx context.Context, req *bizpayment.PaymentReq
|
|||||||
bm.SetBodyMap("amount", func(amount gopay.BodyMap) {
|
bm.SetBodyMap("amount", func(amount gopay.BodyMap) {
|
||||||
amount.Set("total", req.Amount).Set("currency", strings.ToUpper(req.Currency))
|
amount.Set("total", req.Amount).Set("currency", strings.ToUpper(req.Currency))
|
||||||
})
|
})
|
||||||
mergeGoPayExtras(bm, req.Extra,
|
|
||||||
"appid", "mchid", "description", "out_trade_no", "notify_url", "amount", "payer",
|
|
||||||
"trade_type", "pay_type", "method", "openid", "open_id", "auth_code", "authcode", "barcode",
|
|
||||||
)
|
|
||||||
|
|
||||||
tradeType := normalizeWechatV3TradeType(firstAny(req.Extra, "trade_type", "pay_type", "method"))
|
tradeType := normalizeWechatV3TradeType(firstAny(req.Extra, "trade_type", "pay_type", "method"))
|
||||||
if tradeType == "" {
|
if tradeType == "" {
|
||||||
tradeType = normalizeWechatV3TradeType(firstAny(c, "trade_type", "pay_type", "method"))
|
tradeType = normalizeWechatV3TradeType(firstAny(c, "trade_type", "pay_type", "method"))
|
||||||
}
|
}
|
||||||
|
if tradeType == "native" || tradeType == "qr" || tradeType == "qrcode" {
|
||||||
|
// Native orders only accept the fields defined by the WeChat transaction API.
|
||||||
|
for _, key := range []string{"time_expire", "attach", "goods_tag", "support_fapiao", "detail", "scene_info", "settle_info"} {
|
||||||
|
if value, ok := req.Extra[key]; ok {
|
||||||
|
bm[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mergeGoPayExtras(bm, req.Extra,
|
||||||
|
"appid", "mchid", "description", "out_trade_no", "notify_url", "amount", "payer",
|
||||||
|
"trade_type", "pay_type", "method", "openid", "open_id", "auth_code", "authcode", "barcode",
|
||||||
|
)
|
||||||
|
}
|
||||||
switch tradeType {
|
switch tradeType {
|
||||||
case "", "jsapi", "js_api", "mini", "miniapp", "mini_app", "miniprogram", "mini_program", "applet":
|
case "", "jsapi", "js_api", "mini", "miniapp", "mini_app", "miniprogram", "mini_program", "applet":
|
||||||
if openID := firstAny(req.Extra, "openid", "open_id"); openID != "" {
|
if openID := firstAny(req.Extra, "openid", "open_id"); openID != "" {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
bizpayment "kra/internal/biz/payment"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gopayWechatV3 "github.com/go-pay/gopay/wechat/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWechatV3NativeOnlySendsDocumentedFields(t *testing.T) {
|
||||||
|
platformKey, _, platformPublicPEM := testRSAKeyPair(t)
|
||||||
|
_, merchantPrivatePEM, _ := testRSAKeyPair(t)
|
||||||
|
const (
|
||||||
|
platformSerial = "PLATFORM-SERIAL"
|
||||||
|
timestamp = "1787068800"
|
||||||
|
nonce = "native-response-nonce"
|
||||||
|
)
|
||||||
|
response := []byte(`{"code_url":"weixin://wxpay/bizpayurl?pr=test"}`)
|
||||||
|
signature := testRSA2Sign(t, platformKey, []byte(timestamp+"\n"+nonce+"\n"+string(response)+"\n"))
|
||||||
|
|
||||||
|
var requestBody map[string]any
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost || r.URL.Path != "/v3/pay/transactions/native" {
|
||||||
|
t.Errorf("request = %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("read request: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(body, &requestBody); err != nil {
|
||||||
|
t.Errorf("decode request: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set(gopayWechatV3.HeaderTimestamp, timestamp)
|
||||||
|
w.Header().Set(gopayWechatV3.HeaderNonce, nonce)
|
||||||
|
w.Header().Set(gopayWechatV3.HeaderSerial, platformSerial)
|
||||||
|
w.Header().Set(gopayWechatV3.HeaderSignature, signature)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write(response)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
result, err := (&wechatV3Adapter{}).Create(context.Background(), &bizpayment.PaymentRequest{
|
||||||
|
Provider: bizpayment.PaymentWechatV3,
|
||||||
|
TradeNo: "T-WECHAT-NATIVE",
|
||||||
|
Subject: "subject",
|
||||||
|
Amount: 123,
|
||||||
|
Currency: "CNY",
|
||||||
|
NotifyURL: "https://merchant.test/payment/callback",
|
||||||
|
Extra: map[string]any{
|
||||||
|
"method": "qrcode", "pay_method": "native", "product_id": "item-1",
|
||||||
|
"payer": map[string]any{"openid": "not-for-native"},
|
||||||
|
"attach": "order-meta", "scene_info": map[string]any{"payer_client_ip": "127.0.0.1"},
|
||||||
|
},
|
||||||
|
}, map[string]any{
|
||||||
|
"app_id": "wx-test",
|
||||||
|
"merchant_id": "mch-test",
|
||||||
|
"serial_no": "merchant-serial",
|
||||||
|
"private_key": merchantPrivatePEM,
|
||||||
|
"api_v3_key": "01234567890123456789012345678901",
|
||||||
|
"platform_cert": platformPublicPEM,
|
||||||
|
"platform_serial_no": platformSerial,
|
||||||
|
"create_url": server.URL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := map[string]any{
|
||||||
|
"appid": "wx-test", "mchid": "mch-test", "description": "subject",
|
||||||
|
"out_trade_no": "T-WECHAT-NATIVE", "notify_url": "https://merchant.test/payment/callback",
|
||||||
|
"amount": map[string]any{"total": float64(123), "currency": "CNY"},
|
||||||
|
"attach": "order-meta", "scene_info": map[string]any{"payer_client_ip": "127.0.0.1"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(requestBody, want) {
|
||||||
|
t.Fatalf("native request = %#v, want %#v", requestBody, want)
|
||||||
|
}
|
||||||
|
if result.Status != "created" || result.TradeNo != "T-WECHAT-NATIVE" || string(result.Payload) != string(response) {
|
||||||
|
t.Fatalf("native result = %+v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user