Merge pull request #3 from cloud-neutral-toolkit/refactor/makefile-scripts

Refactor/makefile scripts
This commit is contained in:
cloudneutral 2026-01-25 09:10:18 +08:00 committed by GitHub
commit c364fcf8ff
50 changed files with 962 additions and 203 deletions

1
.gitignore vendored
View File

@ -61,3 +61,4 @@ dist/
out/
target/
xcontrol-account
account-export.yaml

237
Makefile
View File

@ -7,11 +7,15 @@ MAIN_FILE := ./cmd/accountsvc/main.go
PORT ?= 8080
OS := $(shell uname -s)
# Load local environment overrides if present.
-include .env
export
DB_NAME := account
DB_USER := shenlan
DB_PASS := password
DB_USER ?= $(or $(POSTGRES_USER),postgres)
DB_PASS ?= $(or $(POSTGRES_PASSWORD),password)
DB_HOST := 127.0.0.1
DB_PORT := 5432
DB_PORT := 15432
DB_URL := postgres://$(DB_USER):$(DB_PASS)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
REPLICATION_MODE ?= pgsync
@ -40,13 +44,23 @@ SUPERADMIN_PASSWORD ?= ChangeMe
SUPERADMIN_EMAIL ?= admin@svc.plus
export PATH := /usr/local/go/bin:$(PATH)
export APP_NAME MAIN_FILE PORT OS \
DB_NAME DB_USER DB_PASS DB_HOST DB_PORT DB_URL \
REPLICATION_MODE DB_ADMIN_USER DB_ADMIN_PASS \
GCP_PROJECT GCP_REGION CLOUD_RUN_SERVICE CLOUD_RUN_SERVICE_YAML CLOUD_RUN_STUNNEL_CONF CLOUD_RUN_IMAGE \
SCHEMA_FILE PGLOGICAL_INIT_FILE PGLOGICAL_PATCH_FILE PGLOGICAL_REGION_FILE \
ACCOUNT_EXPORT_FILE ACCOUNT_IMPORT_FILE ACCOUNT_EMAIL_KEYWORD ACCOUNT_SYNC_CONFIG \
SUPERADMIN_USERNAME SUPERADMIN_PASSWORD SUPERADMIN_EMAIL \
ACCOUNT_IMPORT_MERGE ACCOUNT_IMPORT_MERGE_STRATEGY ACCOUNT_IMPORT_DRY_RUN \
ACCOUNT_IMPORT_MERGE_ALLOWLIST ACCOUNT_IMPORT_EXTRA_FLAGS
# =========================================
# 🧩 基础命令
# =========================================
.PHONY: all init build clean start stop restart dev test help \
init-db-core init-db-replication init-db-pglogical \
.PHONY: all init build clean start stop restart dev test help integration-test \
init-go init-db init-db-core init-db-replication init-db-pglogical \
stunnel-start \
reinit-pglogical account-sync-push account-sync-pull account-sync-mirror create-db-user db-reset \
cloudrun-build cloudrun-deploy cloudrun-stunnel
@ -67,6 +81,7 @@ help:
@echo "make reinit-pglogical 重新初始化 pglogical schema"
@echo "make dev 热重载开发模式"
@echo "make clean 清理构建产物"
@echo "make integration-test 运行集成测试用例(初始化 + 创建管理员)"
@echo "make cloudrun-build 构建并推送 Cloud Run 镜像"
@echo "make cloudrun-deploy 部署 Cloud Run Service"
@echo "make cloudrun-stunnel 更新 Cloud Run stunnel 配置 secret"
@ -78,257 +93,125 @@ help:
init: init-go init-db
init-go:
@if [ ! -f go.mod ]; then \
echo ">>> go.mod not found, initializing module"; \
go mod init account; \
fi
go mod tidy
@echo ">>> 检查 Go 环境"
@if ! command -v go >/dev/null; then \
echo "未安装 Go自动安装中..."; \
([ "$(OS)" = "Darwin" ] && brew install go@1.24 && brew link --overwrite --force go@1.24) || \
(sudo apt-get update && sudo apt-get install -y golang); \
fi
@echo ">>> 配置 Go Proxy"
@(curl -fsSL --max-time 5 https://goproxy.cn >/dev/null && go env -w GOPROXY=https://goproxy.cn,direct) || \
(go env -w GOPROXY=https://proxy.golang.org,direct)
@go mod tidy
@bash scripts/init-go.sh
init-db:
@echo ">>> 初始化数据库 schema"
@command -v psql >/dev/null || (echo "❌ 未检测到 psql请安装 PostgreSQL 客户端" && exit 1)
@$(MAKE) init-db-core
@$(MAKE) init-db-replication
@bash scripts/init-db.sh
init-db-core:
@echo ">>> 初始化业务 schema ($(SCHEMA_FILE))"
@psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(SCHEMA_FILE)
@bash scripts/init-db-core.sh
init-db-replication:
@if [ "$(REPLICATION_MODE)" = "pglogical" ]; then \
$(MAKE) init-db-pglogical; \
else \
echo ">>> 跳过 pglogical 初始化 (REPLICATION_MODE=$(REPLICATION_MODE))"; \
fi
@bash scripts/init-db-replication.sh
init-db-pglogical:
@if [ -f $(PGLOGICAL_INIT_FILE) ]; then \
echo ">>> 初始化 pglogical schema (REPLICATION_MODE=pglogical)"; \
if PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
-Atc "SELECT rolsuper FROM pg_roles WHERE rolname = current_user" 2>/dev/null | grep -qx 't'; then \
PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
-v ON_ERROR_STOP=1 -f $(PGLOGICAL_INIT_FILE); \
elif psql "$(DB_URL)" -Atc "SELECT rolsuper FROM pg_roles WHERE rolname = current_user" | grep -qx 't'; then \
psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(PGLOGICAL_INIT_FILE); \
else \
echo "⚠️ 当前用户非超级用户,跳过 pglogical 初始化"; \
fi; \
fi; \
if [ -f $(PGLOGICAL_PATCH_FILE) ]; then \
echo ">>> 应用 pglogical 默认值补丁"; \
psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(PGLOGICAL_PATCH_FILE); \
fi
@bash scripts/init-db-pglogical.sh
# =========================================
# 🧠 PGLogical 双节点初始化
# =========================================
init-pglogical-region:
@[ -n "$(REGION_DB_URL)" ] || (echo "❌ 缺少 REGION_DB_URL"; exit 1)
@[ -n "$(NODE_NAME)" ] || (echo "❌ 缺少 NODE_NAME"; exit 1)
@[ -n "$(NODE_DSN)" ] || (echo "❌ 缺少 NODE_DSN"; exit 1)
@[ -n "$(SUBSCRIPTION_NAME)" ] || (echo "❌ 缺少 SUBSCRIPTION_NAME"; exit 1)
@[ -n "$(PROVIDER_DSN)" ] || (echo "❌ 缺少 PROVIDER_DSN"; exit 1)
@psql "$(REGION_DB_URL)" -v ON_ERROR_STOP=1 \
-v NODE_NAME="$(NODE_NAME)" \
-v NODE_DSN="$(NODE_DSN)" \
-v SUBSCRIPTION_NAME="$(SUBSCRIPTION_NAME)" \
-v PROVIDER_DSN="$(PROVIDER_DSN)" \
-f $(PGLOGICAL_REGION_FILE)
@bash scripts/init-pglogical-region.sh
init-pglogical-region-cn:
@$(MAKE) init-pglogical-region \
REGION_DB_URL="$(DB_URL)" \
NODE_NAME="node_cn" \
NODE_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
SUBSCRIPTION_NAME="sub_from_global" \
PROVIDER_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx"
@bash scripts/init-pglogical-region-cn.sh
init-pglogical-region-global:
@$(MAKE) init-pglogical-region \
REGION_DB_URL="$(DB_URL)" \
NODE_NAME="node_global" \
NODE_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
SUBSCRIPTION_NAME="sub_from_cn" \
PROVIDER_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx"
@bash scripts/init-pglogical-region-global.sh
# =========================================
# 📦 数据库迁移与管理
# =========================================
create-db-user:
@echo ">>> 创建数据库用户 $(DB_USER)"
@command -v psql >/dev/null || (echo "❌ 未检测到 psql请安装 PostgreSQL 客户端" && exit 1)
@echo "正在以 postgres 超级用户身份创建用户..."
@sudo -u postgres psql -c "CREATE USER $(DB_USER) WITH PASSWORD '$(DB_PASS)';" || echo "⚠️ 用户可能已存在"
@sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $(DB_NAME) TO $(DB_USER);"
@echo "✓ 数据库用户创建完成"
@bash scripts/create-db-user.sh
migrate-db:
@echo ">>> 执行数据库迁移"
@go run ./cmd/migratectl/main.go migrate --dsn "$(DB_URL)" --dir sql/migrations
@bash scripts/migrate-db.sh
dump-schema:
@echo ">>> 导出 schema 到 $(SCHEMA_FILE)"
@pg_dump -s -O -x "$(DB_URL)" > $(SCHEMA_FILE)
@bash scripts/dump-schema.sh
db-reset:
@echo "⚠️ 即将重置整个 PostgreSQL 数据库集群 ..."
@read -p "确定要重置数据库集群? 这将删除所有数据! [y/N] " confirm && \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
echo ">>> 停止 PostgreSQL 服务 ..."; \
sudo systemctl stop postgresql; \
echo ">>> 删除数据库集群 16 main ..."; \
sudo pg_dropcluster --stop 16 main; \
echo ">>> 清理数据目录 ..."; \
sudo rm -rf /var/lib/postgresql/16/main; \
echo ">>> 清理配置目录 ..."; \
sudo rm -rf /etc/postgresql/16/main; \
echo ">>> 创建新的数据库集群 ..."; \
sudo pg_createcluster 16 main --start; \
echo "✓ PostgreSQL 集群重置完成"; \
else \
echo "取消重置"; \
fi
@bash scripts/db-reset.sh
drop-db:
@echo "⚠️ 即将删除数据库 $(DB_NAME) ..."
@read -p "确定要删除数据库 $(DB_NAME)? [y/N] " confirm && \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
echo ">>> 强制断开现有连接 ..."; \
if ! PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d postgres \
-c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$(DB_NAME)' AND pid <> pg_backend_pid();"; then \
echo "⚠️ 无法断开所有连接(需要超级用户权限)"; \
fi; \
echo ">>> 清理 pglogical schema ..."; \
PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
-c "DROP SCHEMA IF EXISTS pglogical CASCADE;" >/dev/null 2>&1 || \
echo "⚠️ 无法删除 pglogical schema数据库可能不存在或缺少权限"; \
echo ">>> 删除数据库 $(DB_NAME) ..."; \
if PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d postgres \
-c "DROP DATABASE IF EXISTS $(DB_NAME);"; then \
echo ">>> 数据库已删除"; \
else \
echo ">>> 删除失败"; \
fi; \
else \
echo "取消删除"; \
fi
@bash scripts/drop-db.sh
reset-public-schema:
@psql "$(DB_URL)" -v ON_ERROR_STOP=1 -v db_user="$(DB_USER)" -f sql/reset_public_schema.sql
@bash scripts/reset-public-schema.sh
reinit-db:
@echo ">>> 重置业务 schema (sql/schema.sql)"
@$(MAKE) reset-public-schema
@$(MAKE) init-db-core
@bash scripts/reinit-db.sh
reinit-pglogical:
@if [ "$(REPLICATION_MODE)" = "pglogical" ]; then \
echo ">>> 重新初始化 pglogical schema"; \
$(MAKE) init-db-pglogical; \
else \
echo ">>> 当前 REPLICATION_MODE=$(REPLICATION_MODE),无需 pglogical 处理"; \
fi
@bash scripts/reinit-pglogical.sh
stunnel-start:
@bash scripts/stunnel-start.sh
# =========================================
# 💾 账号导入导出
# =========================================
account-export:
@go run ./cmd/migratectl/main.go export --dsn "$(DB_URL)" --output "$(ACCOUNT_EXPORT_FILE)" $(if $(ACCOUNT_EMAIL_KEYWORD),--email "$(ACCOUNT_EMAIL_KEYWORD)")
@bash scripts/account-export.sh
account-import:
@[ -f "$(ACCOUNT_IMPORT_FILE)" ] || (echo "❌ 未找到文件 $(ACCOUNT_IMPORT_FILE)"; exit 1)
@go run ./cmd/migratectl/main.go import --dsn "$(DB_URL)" --file "$(ACCOUNT_IMPORT_FILE)" \
$(if $(ACCOUNT_IMPORT_MERGE),--merge) \
$(if $(ACCOUNT_IMPORT_MERGE_STRATEGY),--merge-strategy "$(ACCOUNT_IMPORT_MERGE_STRATEGY)") \
$(if $(ACCOUNT_IMPORT_DRY_RUN),--dry-run) \
$(foreach UUID,$(ACCOUNT_IMPORT_MERGE_ALLOWLIST),--merge-allowlist $(UUID)) \
$(ACCOUNT_IMPORT_EXTRA_FLAGS)
@bash scripts/account-import.sh
account-sync-push:
@[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
@go run ./cmd/syncctl/main.go push --config "$(ACCOUNT_SYNC_CONFIG)"
@bash scripts/account-sync-push.sh
account-sync-pull:
@[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
@go run ./cmd/syncctl/main.go pull --config "$(ACCOUNT_SYNC_CONFIG)"
@bash scripts/account-sync-pull.sh
account-sync-mirror:
@[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
@go run ./cmd/syncctl/main.go mirror --config "$(ACCOUNT_SYNC_CONFIG)"
@bash scripts/account-sync-mirror.sh
create-super-admin:
@[ -n "$(SUPERADMIN_USERNAME)" ] && [ -n "$(SUPERADMIN_PASSWORD)" ] || (echo "❌ 请指定用户名与密码"; exit 1)
@go run ./cmd/createadmin/main.go \
--driver postgres \
--dsn "$(DB_URL)" \
--username "$(SUPERADMIN_USERNAME)" \
--password "$(SUPERADMIN_PASSWORD)" \
--email "$(SUPERADMIN_EMAIL)"
@bash scripts/create-super-admin.sh
integration-test:
@bash integration-test/superadmin-login/run-test-scripts.sh
# =========================================
# ⚙️ 编译与运行
# =========================================
build: init-go
@go build -o $(APP_NAME) $(MAIN_FILE)
@bash scripts/build.sh
upgrade: build
systemctl stop xcontrol-account
cp xcontrol-account /usr/bin/xcontrol-account
systemctl start xcontrol-account
@bash scripts/upgrade.sh
start: build
@./$(APP_NAME) --config config/account.yaml
@bash scripts/start.sh
stop:
@pkill -f "$(APP_NAME)" || echo "⚠️ 未找到运行进程"
@bash scripts/stop.sh
restart: stop start
test:
go test ./...
@bash scripts/test.sh
clean:
rm -f $(APP_NAME) *.pid *.log
@bash scripts/clean.sh
dev:
@bash scripts/dev.sh
# =========================================
# ☁️ GCP Cloud Run
# =========================================
cloudrun-build:
@if [ -z "$(GCP_PROJECT)" ]; then \
echo "❌ GCP_PROJECT 不能为空"; \
exit 1; \
fi
@gcloud builds submit --tag "$(CLOUD_RUN_IMAGE)" .
@bash scripts/cloudrun-build.sh
cloudrun-deploy:
@if [ -z "$(GCP_PROJECT)" ]; then \
echo "❌ GCP_PROJECT 不能为空"; \
exit 1; \
fi
@gcloud run services replace "$(CLOUD_RUN_SERVICE_YAML)" --region "$(GCP_REGION)" --project "$(GCP_PROJECT)"
@bash scripts/cloudrun-deploy.sh
cloudrun-stunnel:
@if [ -z "$(GCP_PROJECT)" ]; then \
echo "❌ GCP_PROJECT 不能为空"; \
exit 1; \
fi
@if [ ! -f "$(CLOUD_RUN_STUNNEL_CONF)" ]; then \
echo "❌ 未找到 stunnel 配置: $(CLOUD_RUN_STUNNEL_CONF)"; \
exit 1; \
fi
@gcloud secrets versions add stunnel-config --data-file "$(CLOUD_RUN_STUNNEL_CONF)" --project "$(GCP_PROJECT)"
@bash scripts/cloudrun-stunnel.sh

View File

@ -31,7 +31,8 @@ server:
store:
driver: "postgres"
dsn: "postgres://shenlan:password@127.0.0.1:5432/account?sslmode=disable"
# 提示:本地默认从 .env 读取 POSTGRES_USER / POSTGRES_PASSWORD
dsn: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/account?sslmode=disable"
maxOpenConns: 30
maxIdleConns: 10

View File

@ -24,6 +24,7 @@ server:
- "https://www.svc.plus"
- "https://global-homepage.svc.plus"
- "https://accounts.svc.plus"
- "https://console.svc.plus"
- "https://localhost:8443"
- "http://localhost:8080"
- "http://127.0.0.1:8080"
@ -41,7 +42,8 @@ server:
store:
driver: "postgres"
dsn: "postgres://shenlan:password@127.0.0.1:5432/account?sslmode=disable"
# 提示:本地默认从 .env 读取 POSTGRES_USER / POSTGRES_PASSWORD
dsn: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/account?sslmode=disable"
maxOpenConns: 30
maxIdleConns: 10

View File

@ -10,7 +10,8 @@
local:
# 本地 PostgreSQL 连接地址,用于导入/导出账号数据
dsn: "postgres://shenlan:password@127.0.0.1:5432/account?sslmode=disable"
# 提示:本地默认从 .env 读取 POSTGRES_USER / POSTGRES_PASSWORD
dsn: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/account?sslmode=disable"
# 可选:按 email 关键字过滤导出的账号
email_keyword: ""
# 导出的快照文件路径(默认 account-export.yaml
@ -47,4 +48,3 @@ remote:
# SSH 连接超时时间
timeout: 30s

View File

@ -10,7 +10,8 @@
local:
# 本地 PostgreSQL 连接地址,用于导入/导出账号数据
dsn: "postgres://shenlan:password@127.0.0.1:5432/account?sslmode=disable"
# 提示:本地默认从 .env 读取 POSTGRES_USER / POSTGRES_PASSWORD
dsn: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/account?sslmode=disable"
# 可选:按 email 关键字过滤导出的账号
email_keyword: ""
# 导出的快照文件路径(默认 account-export.yaml
@ -47,4 +48,3 @@ remote:
# SSH 连接超时时间
timeout: 30s

View File

@ -20,7 +20,7 @@ spec:
containers:
# --- 主应用容器 ---
- name: accounts-api
image: asia-northeast1-docker.pkg.dev/xzerolab-480008/cloud-run-source-deploy/accounts.svc.plus/accounts-svc-plus:d5b6edcaa76150f2489bbdb2a46a41bc98baa87f
image: asia-northeast1-docker.pkg.dev/xzerolab-480008/cloud-run-source-deploy/accounts.svc.plus/accounts-svc-plus:latest
ports:
- name: http1
containerPort: 8080
@ -48,7 +48,7 @@ spec:
- name: POSTGRES_USER
value: postgres
- name: DB_NAME
value: postgres
value: account
resources:
limits:
cpu: 1000m

View File

@ -0,0 +1,66 @@
# superadmin-login
## 说明
该用例用于本地集成测试,按顺序执行:
1. `make init-db`
2. `make create-db-user`
3. `make create-super-admin`
脚本会自动读取项目根目录 `.env` 中的环境变量(如 `POSTGRES_USER` / `POSTGRES_PASSWORD`),用于联动数据库配置。
若未设置 `SUPERADMIN_PASSWORD`,脚本会生成随机密码并写回 `.env`,便于后续登录测试复用。
## 运行方式
```bash
make integration-test
```
或直接运行脚本:
```bash
bash integration-test/superadmin-login/run-test-scripts.sh
```
## API 自动化测试
```bash
bash integration-test/superadmin-login/api-test.sh
```
可选环境变量:
- `API_BASE_URL`API 入口地址(默认 `https://accounts.svc.plus`
- `LOGIN_EMAIL`:登录邮箱(默认 `admin@svc.plus`
- `SUPERADMIN_PASSWORD`:登录密码(从 `.env` 读取)
## UI 自动化测试Playwright
```bash
bash integration-test/superadmin-login/ui-test.sh
```
可选环境变量:
- `UI_BASE_URL`UI 入口地址(默认 `https://console.svc.plus`
- `LOGIN_EMAIL`:登录邮箱(默认 `admin@svc.plus`
- `SUPERADMIN_PASSWORD`:登录密码(从 `.env` 读取)
## 预期输出(示例)
```
✅ 已读取 .env
>>> init-db
...(略)
>>> create-super-admin
...(略)
✅ 集成测试步骤完成DB 初始化 + 用户创建 + 超级管理员创建)
接下来手动登录验证:
- 网址https://console.svc.plus/login
- 邮箱admin@svc.plus
- 密码:<generated-or-provided>
```
> 注意:登录验证需要手动在浏览器完成。

View File

@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
echo "✅ 已读取 .env"
fi
if ! command -v curl >/dev/null; then
echo "❌ 未找到 curl请先安装或确保在 PATH 中" >&2
exit 1
fi
API_BASE_URL="${API_BASE_URL:-${BASE_URL:-https://accounts.svc.plus}}"
LOGIN_EMAIL="${LOGIN_EMAIL:-admin@svc.plus}"
LOGIN_PASSWORD="${SUPERADMIN_PASSWORD:-}"
if [ -z "$LOGIN_PASSWORD" ]; then
echo "❌ 缺少 SUPERADMIN_PASSWORD可写入 .env" >&2
exit 1
fi
login_payload=$(cat <<JSON
{"email":"${LOGIN_EMAIL}","identifier":"${LOGIN_EMAIL}","password":"${LOGIN_PASSWORD}"}
JSON
)
login_response=$(curl -sS -w "\n%{http_code}" -X POST "${API_BASE_URL}/api/auth/login" \
-H "Content-Type: application/json" \
-d "$login_payload")
login_body=$(printf "%s" "$login_response" | sed '$d')
login_status=$(printf "%s" "$login_response" | tail -n1)
if [ "$login_status" != "200" ]; then
echo "❌ 登录失败: HTTP ${login_status}" >&2
echo "$login_body" >&2
exit 1
fi
if command -v python3 >/dev/null; then
token=$(printf "%s" "$login_body" | python3 - <<'PY'
import json, sys
try:
payload = json.load(sys.stdin)
print(payload.get('token',''))
except Exception:
print('')
PY
)
else
echo "❌ 未找到 python3无法解析登录 token" >&2
exit 1
fi
if [ -z "$token" ]; then
echo "❌ 登录响应中未包含 token" >&2
echo "$login_body" >&2
exit 1
fi
session_response=$(curl -sS -w "\n%{http_code}" -X GET "${API_BASE_URL}/api/auth/session" \
-H "Authorization: Bearer ${token}")
session_body=$(printf "%s" "$session_response" | sed '$d')
session_status=$(printf "%s" "$session_response" | tail -n1)
if [ "$session_status" != "200" ]; then
echo "❌ session 校验失败: HTTP ${session_status}" >&2
echo "$session_body" >&2
exit 1
fi
echo "✅ API 登录测试通过"

View File

@ -0,0 +1,74 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
echo "✅ 已读取 .env"
fi
ensure_superadmin_password() {
if [ -n "${SUPERADMIN_PASSWORD:-}" ] && [ "${SUPERADMIN_PASSWORD}" != "ChangeMe" ]; then
return 0
fi
if command -v openssl >/dev/null; then
SUPERADMIN_PASSWORD="$(openssl rand -base64 12 | tr -d '\n' | tr '/+' 'Aa' | cut -c1-10)"
else
SUPERADMIN_PASSWORD="$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 10)"
fi
if [ -f .env ]; then
if grep -q '^SUPERADMIN_PASSWORD=' .env; then
tmpfile="$(mktemp)"
sed "s/^SUPERADMIN_PASSWORD=.*/SUPERADMIN_PASSWORD=${SUPERADMIN_PASSWORD}/" .env > "$tmpfile"
mv "$tmpfile" .env
else
printf "\nSUPERADMIN_PASSWORD=%s\n" "$SUPERADMIN_PASSWORD" >> .env
fi
else
printf "SUPERADMIN_PASSWORD=%s\n" "$SUPERADMIN_PASSWORD" > .env
fi
echo "✅ 已写入 .env 的 SUPERADMIN_PASSWORD"
}
if ! command -v make >/dev/null; then
echo "❌ 未找到 make请先安装或确保在 PATH 中" >&2
exit 1
fi
echo ">>> init-db"
make init-db
echo ">>> create-db-user"
make create-db-user
echo ">>> create-super-admin"
ensure_superadmin_password
export SUPERADMIN_PASSWORD
create_output="$(make create-super-admin 2>&1 | tee /dev/stderr)"
superadmin_password=""
if echo "$create_output" | grep -q "已生成随机初始密码"; then
superadmin_password="$(echo "$create_output" | sed -n 's/.*已生成随机初始密码: \([A-Za-z0-9]\+\).*/\1/p' | tail -n1)"
fi
if [ -z "$superadmin_password" ]; then
superadmin_password="$(echo "$create_output" | sed -n 's/.*密码:\s*//p' | tail -n1)"
fi
cat <<EOM
✅ 集成测试步骤完成DB 初始化 + 用户创建 + 超级管理员创建)
接下来手动登录验证:
- 网址https://console.svc.plus/login
- 邮箱admin@svc.plus
- 密码:${superadmin_password:-请查看上方 create-super-admin 输出}
提示:如果页面无法访问,请先确保 account 服务已启动并对外可用。
EOM

View File

@ -0,0 +1,101 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
echo "✅ 已读取 .env"
fi
if ! command -v node >/dev/null; then
echo "❌ 未找到 node请先安装" >&2
exit 1
fi
if ! command -v npx >/dev/null; then
echo "❌ 未找到 npx请先安装" >&2
exit 1
fi
UI_BASE_URL="${UI_BASE_URL:-${BASE_URL:-https://console.svc.plus}}"
LOGIN_EMAIL="${LOGIN_EMAIL:-admin@svc.plus}"
LOGIN_PASSWORD="${SUPERADMIN_PASSWORD:-}"
if [ -z "$LOGIN_PASSWORD" ]; then
echo "❌ 缺少 SUPERADMIN_PASSWORD可写入 .env" >&2
exit 1
fi
TMP_DIR="${TMPDIR:-/tmp}"
PLAYWRIGHT_TEST_DIR="${TMP_DIR}/xcontrol-playwright-login"
mkdir -p "$PLAYWRIGHT_TEST_DIR"
cat <<'TEST' > "$PLAYWRIGHT_TEST_DIR/login.spec.mjs"
import { test, expect } from '@playwright/test';
test.use({ screenshot: 'only-on-failure' });
test('superadmin login', async ({ page }) => {
const baseUrl = process.env.UI_BASE_URL || process.env.BASE_URL || 'https://console.svc.plus';
const email = process.env.LOGIN_EMAIL || 'admin@svc.plus';
const password = process.env.SUPERADMIN_PASSWORD;
if (!password) {
throw new Error('missing SUPERADMIN_PASSWORD');
}
await page.goto(`${baseUrl}/login`, { waitUntil: 'domcontentloaded' });
const emailByRole = page.getByRole('textbox', { name: /email|邮箱|账号|用户名|identifier/i });
const emailBySelector = page.locator(
'input[type="email"], input[name="email"], input[name="identifier"], input[placeholder*="邮箱"], input[placeholder*="Email"], input[type="text"]'
);
const emailInput = (await emailByRole.count()) > 0 ? emailByRole.first() : emailBySelector.first();
await expect(emailInput).toBeVisible({ timeout: 15000 });
await emailInput.fill(email);
const passwordInput = page.locator(
'input[type="password"], input[name="password"], input[placeholder*="密码"], input[placeholder*="Password"]'
);
await expect(passwordInput).toBeVisible({ timeout: 15000 });
await passwordInput.fill(password);
const submitBtn = page.locator(
'button[type="submit"], button:has-text("登录"), button:has-text("Log in"), button:has-text("Sign in")'
);
await expect(submitBtn).toBeVisible({ timeout: 15000 });
await submitBtn.click();
await page.waitForLoadState('networkidle');
await expect(page).not.toHaveURL(/login/);
});
TEST
cd "$PLAYWRIGHT_TEST_DIR"
if [ ! -f package.json ]; then
cat <<'PKG' > package.json
{
"name": "xcontrol-playwright-login",
"private": true,
"version": "0.0.0",
"type": "module",
"devDependencies": {
"@playwright/test": "^1.49.0"
}
}
PKG
fi
npm install --silent
npx playwright install chromium
UI_BASE_URL="$UI_BASE_URL" BASE_URL="$UI_BASE_URL" LOGIN_EMAIL="$LOGIN_EMAIL" SUPERADMIN_PASSWORD="$LOGIN_PASSWORD" \
npx playwright test login.spec.mjs
echo "✅ UI 登录测试通过"

6
scripts/_common.sh Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT_DIR"

11
scripts/account-export.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
args=(export --dsn "${DB_URL}" --output "${ACCOUNT_EXPORT_FILE}")
if [ -n "${ACCOUNT_EMAIL_KEYWORD-}" ]; then
args+=(--email "${ACCOUNT_EMAIL_KEYWORD}")
fi
go run ./cmd/migratectl/main.go "${args[@]}"

32
scripts/account-import.sh Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ ! -f "${ACCOUNT_IMPORT_FILE}" ]; then
echo "❌ 未找到文件 ${ACCOUNT_IMPORT_FILE}"
exit 1
fi
args=(import --dsn "${DB_URL}" --file "${ACCOUNT_IMPORT_FILE}")
if [ -n "${ACCOUNT_IMPORT_MERGE-}" ]; then
args+=(--merge)
fi
if [ -n "${ACCOUNT_IMPORT_MERGE_STRATEGY-}" ]; then
args+=(--merge-strategy "${ACCOUNT_IMPORT_MERGE_STRATEGY}")
fi
if [ -n "${ACCOUNT_IMPORT_DRY_RUN-}" ]; then
args+=(--dry-run)
fi
if [ -n "${ACCOUNT_IMPORT_MERGE_ALLOWLIST-}" ]; then
for uuid in ${ACCOUNT_IMPORT_MERGE_ALLOWLIST}; do
args+=(--merge-allowlist "${uuid}")
done
fi
if [ -n "${ACCOUNT_IMPORT_EXTRA_FLAGS-}" ]; then
# shellcheck disable=SC2206
extra=( ${ACCOUNT_IMPORT_EXTRA_FLAGS} )
args+=("${extra[@]}")
fi
go run ./cmd/migratectl/main.go "${args[@]}"

11
scripts/account-sync-mirror.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ ! -f "${ACCOUNT_SYNC_CONFIG}" ]; then
echo "❌ 未找到配置文件 ${ACCOUNT_SYNC_CONFIG}"
exit 1
fi
go run ./cmd/syncctl/main.go mirror --config "${ACCOUNT_SYNC_CONFIG}"

11
scripts/account-sync-pull.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ ! -f "${ACCOUNT_SYNC_CONFIG}" ]; then
echo "❌ 未找到配置文件 ${ACCOUNT_SYNC_CONFIG}"
exit 1
fi
go run ./cmd/syncctl/main.go pull --config "${ACCOUNT_SYNC_CONFIG}"

11
scripts/account-sync-push.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ ! -f "${ACCOUNT_SYNC_CONFIG}" ]; then
echo "❌ 未找到配置文件 ${ACCOUNT_SYNC_CONFIG}"
exit 1
fi
go run ./cmd/syncctl/main.go push --config "${ACCOUNT_SYNC_CONFIG}"

6
scripts/build.sh Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
go build -o "${APP_NAME}" "${MAIN_FILE}"

6
scripts/clean.sh Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
rm -f "${APP_NAME}" *.pid *.log

11
scripts/cloudrun-build.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ -z "${GCP_PROJECT}" ]; then
echo "⚠️ GCP_PROJECT 不能为空,跳过 Cloud Run 构建"
exit 0
fi
gcloud builds submit --tag "${CLOUD_RUN_IMAGE}" .

11
scripts/cloudrun-deploy.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ -z "${GCP_PROJECT}" ]; then
echo "⚠️ GCP_PROJECT 不能为空,跳过 Cloud Run 部署"
exit 0
fi
gcloud run services replace "${CLOUD_RUN_SERVICE_YAML}" --region "${GCP_REGION}" --project "${GCP_PROJECT}"

15
scripts/cloudrun-stunnel.sh Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ -z "${GCP_PROJECT}" ]; then
echo "⚠️ GCP_PROJECT 不能为空,跳过 stunnel secret 更新"
exit 0
fi
if [ ! -f "${CLOUD_RUN_STUNNEL_CONF}" ]; then
echo "❌ 未找到 stunnel 配置: ${CLOUD_RUN_STUNNEL_CONF}"
exit 1
fi
gcloud secrets versions add stunnel-config --data-file "${CLOUD_RUN_STUNNEL_CONF}" --project "${GCP_PROJECT}"

22
scripts/create-db-user.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
echo ">>> 创建数据库用户 ${DB_USER}"
if ! command -v psql >/dev/null; then
echo "❌ 未检测到 psql请安装 PostgreSQL 客户端"
exit 1
fi
echo "正在以管理员身份创建用户..."
if PGPASSWORD="${DB_ADMIN_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_ADMIN_USER}" -d postgres \
-Atc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" | grep -qx '1'; then
echo "⚠️ 用户可能已存在"
else
PGPASSWORD="${DB_ADMIN_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_ADMIN_USER}" -d postgres \
-c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';"
fi
PGPASSWORD="${DB_ADMIN_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_ADMIN_USER}" -d postgres \
-c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
echo "✓ 数据库用户创建完成"

47
scripts/create-super-admin.sh Executable file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
SUPERADMIN_EMAIL="admin@svc.plus"
SUPERADMIN_USERNAME="${SUPERADMIN_USERNAME:-Admin}"
if [ -z "${SUPERADMIN_PASSWORD:-}" ] || [ "${SUPERADMIN_PASSWORD}" = "ChangeMe" ]; then
if command -v openssl >/dev/null; then
SUPERADMIN_PASSWORD="$(openssl rand -base64 12 | tr -d '\n' | tr '/+' 'Aa' | cut -c1-10)"
else
SUPERADMIN_PASSWORD="$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 10)"
fi
echo "⚠️ 未指定 SUPERADMIN_PASSWORD已生成随机初始密码: ${SUPERADMIN_PASSWORD}"
fi
if psql "${DB_URL}" -Atc "SELECT 1 FROM users WHERE username='${SUPERADMIN_USERNAME}' OR email='${SUPERADMIN_EMAIL}' LIMIT 1" | grep -qx '1'; then
if [ -z "${SUPERADMIN_CURRENT_PASSWORD:-}" ]; then
echo "⚠️ 超级管理员已存在,未提供 SUPERADMIN_CURRENT_PASSWORD跳过更新"
exit 0
fi
go run ./cmd/createadmin/main.go \
--driver postgres \
--dsn "${DB_URL}" \
--username "${SUPERADMIN_USERNAME}" \
--password "${SUPERADMIN_PASSWORD}" \
--email "${SUPERADMIN_EMAIL}" \
--current-password "${SUPERADMIN_CURRENT_PASSWORD}"
echo "✅ 登录信息(仅本次输出)"
echo " 网址https://console.svc.plus/login"
echo " 邮箱admin@svc.plus"
echo " 密码:${SUPERADMIN_PASSWORD}"
exit 0
fi
go run ./cmd/createadmin/main.go \
--driver postgres \
--dsn "${DB_URL}" \
--username "${SUPERADMIN_USERNAME}" \
--password "${SUPERADMIN_PASSWORD}" \
--email "${SUPERADMIN_EMAIL}"
echo "✅ 登录信息(仅本次输出)"
echo " 网址https://console.svc.plus/login"
echo " 邮箱admin@svc.plus"
echo " 密码:${SUPERADMIN_PASSWORD}"

26
scripts/db-reset.sh Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
echo "⚠️ 即将重置整个 PostgreSQL 数据库集群 ..."
if [ "${OS}" = "Darwin" ]; then
echo "⚠️ macOS 不支持 pg_dropcluster/systemctl跳过重置"
exit 0
fi
read -r -p "确定要重置数据库集群? 这将删除所有数据! [y/N] " confirm
if [ "${confirm}" = "y" ] || [ "${confirm}" = "Y" ]; then
echo ">>> 停止 PostgreSQL 服务 ..."
sudo systemctl stop postgresql
echo ">>> 删除数据库集群 16 main ..."
sudo pg_dropcluster --stop 16 main
echo ">>> 清理数据目录 ..."
sudo rm -rf /var/lib/postgresql/16/main
echo ">>> 清理配置目录 ..."
sudo rm -rf /etc/postgresql/16/main
echo ">>> 创建新的数据库集群 ..."
sudo pg_createcluster 16 main --start
echo "✓ PostgreSQL 集群重置完成"
else
echo "取消重置"
fi

12
scripts/dev.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if command -v air >/dev/null; then
air
exit 0
fi
echo "❌ 未检测到 air (热重载工具),请先安装: https://github.com/cosmtrek/air"
exit 0

27
scripts/drop-db.sh Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
echo "⚠️ 即将删除数据库 ${DB_NAME} ..."
read -r -p "确定要删除数据库 ${DB_NAME}? [y/N] " confirm
if [ "${confirm}" = "y" ] || [ "${confirm}" = "Y" ]; then
echo ">>> 强制断开现有连接 ..."
if ! PGPASSWORD="${DB_ADMIN_PASS}" psql -h "${DB_HOST}" -U "${DB_ADMIN_USER}" -d postgres \
-c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${DB_NAME}' AND pid <> pg_backend_pid();"; then
echo "⚠️ 无法断开所有连接(需要超级用户权限)"
fi
echo ">>> 清理 pglogical schema ..."
PGPASSWORD="${DB_ADMIN_PASS}" psql -h "${DB_HOST}" -U "${DB_ADMIN_USER}" -d "${DB_NAME}" \
-c "DROP SCHEMA IF EXISTS pglogical CASCADE;" >/dev/null 2>&1 || \
echo "⚠️ 无法删除 pglogical schema数据库可能不存在或缺少权限"
echo ">>> 删除数据库 ${DB_NAME} ..."
if PGPASSWORD="${DB_ADMIN_PASS}" psql -h "${DB_HOST}" -U "${DB_ADMIN_USER}" -d postgres \
-c "DROP DATABASE IF EXISTS ${DB_NAME};"; then
echo ">>> 数据库已删除"
else
echo ">>> 删除失败"
fi
else
echo "取消删除"
fi

26
scripts/dump-schema.sh Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
echo ">>> 导出 schema 到 ${SCHEMA_FILE}"
PG_DUMP_BIN="${PG_DUMP_BIN:-pg_dump}"
SERVER_VERSION="$(psql "${DB_URL}" -Atc "SHOW server_version" 2>/dev/null || true)"
SERVER_MAJOR="${SERVER_VERSION%%.*}"
LOCAL_VERSION="$(${PG_DUMP_BIN} --version 2>/dev/null || true)"
LOCAL_MAJOR="$(echo "${LOCAL_VERSION}" | awk '{print $3}' | cut -d. -f1)"
if [ -n "${SERVER_MAJOR}" ] && [ -n "${LOCAL_MAJOR}" ] && [ "${SERVER_MAJOR}" != "${LOCAL_MAJOR}" ]; then
echo "⚠️ pg_dump 版本不匹配server=${SERVER_MAJOR}, local=${LOCAL_MAJOR}),跳过导出"
echo " 可设置 PG_DUMP_BIN 指向匹配版本的 pg_dump"
exit 0
fi
TMP_SCHEMA="/tmp/schema.$$.sql"
${PG_DUMP_BIN} -s -O -x "${DB_URL}" > "${TMP_SCHEMA}"
if [ ! -w "${SCHEMA_FILE}" ]; then
echo "⚠️ ${SCHEMA_FILE} 不可写,已将导出结果保留在 ${TMP_SCHEMA}"
exit 0
fi
mv "${TMP_SCHEMA}" "${SCHEMA_FILE}"

14
scripts/ensure-db.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if PGPASSWORD="${DB_ADMIN_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_ADMIN_USER}" -d postgres \
-Atc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -qx '1'; then
echo ">>> 数据库 ${DB_NAME} 已存在"
exit 0
fi
echo ">>> 创建数据库 ${DB_NAME}"
PGPASSWORD="${DB_ADMIN_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_ADMIN_USER}" -d postgres \
-c "CREATE DATABASE ${DB_NAME};"

7
scripts/init-db-core.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
echo ">>> 初始化业务 schema (${SCHEMA_FILE})"
psql "${DB_URL}" -v ON_ERROR_STOP=1 -f "${SCHEMA_FILE}"

22
scripts/init-db-pglogical.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ -f "${PGLOGICAL_INIT_FILE}" ]; then
echo ">>> 初始化 pglogical schema (REPLICATION_MODE=pglogical)"
if PGPASSWORD="${DB_ADMIN_PASS}" psql -h "${DB_HOST}" -U "${DB_ADMIN_USER}" -d "${DB_NAME}" \
-Atc "SELECT rolsuper FROM pg_roles WHERE rolname = current_user" 2>/dev/null | grep -qx 't'; then
PGPASSWORD="${DB_ADMIN_PASS}" psql -h "${DB_HOST}" -U "${DB_ADMIN_USER}" -d "${DB_NAME}" \
-v ON_ERROR_STOP=1 -f "${PGLOGICAL_INIT_FILE}"
elif psql "${DB_URL}" -Atc "SELECT rolsuper FROM pg_roles WHERE rolname = current_user" | grep -qx 't'; then
psql "${DB_URL}" -v ON_ERROR_STOP=1 -f "${PGLOGICAL_INIT_FILE}"
else
echo "⚠️ 当前用户非超级用户,跳过 pglogical 初始化"
fi
fi
if [ -f "${PGLOGICAL_PATCH_FILE}" ]; then
echo ">>> 应用 pglogical 默认值补丁"
psql "${DB_URL}" -v ON_ERROR_STOP=1 -f "${PGLOGICAL_PATCH_FILE}"
fi

10
scripts/init-db-replication.sh Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ "${REPLICATION_MODE}" = "pglogical" ]; then
bash scripts/init-db-pglogical.sh
else
echo ">>> 跳过 pglogical 初始化 (REPLICATION_MODE=${REPLICATION_MODE})"
fi

14
scripts/init-db.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
echo ">>> 初始化数据库 schema"
if ! command -v psql >/dev/null; then
echo "❌ 未检测到 psql请安装 PostgreSQL 客户端"
exit 1
fi
bash scripts/ensure-db.sh
bash scripts/init-db-core.sh
bash scripts/init-db-replication.sh

28
scripts/init-go.sh Executable file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ ! -f go.mod ]; then
echo ">>> go.mod not found, initializing module"
go mod init account
fi
go mod tidy
echo ">>> 检查 Go 环境"
if ! command -v go >/dev/null; then
echo "未安装 Go自动安装中..."
if [ "${OS:-}" = "Darwin" ]; then
brew install go@1.24
brew link --overwrite --force go@1.24
else
sudo apt-get update
sudo apt-get install -y golang
fi
fi
echo ">>> 配置 Go Proxy"
go env -w GOPROXY=https://proxy.golang.org,direct
go mod tidy

View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
REGION_DB_URL="${DB_URL}" \
NODE_NAME="node_cn" \
NODE_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
SUBSCRIPTION_NAME="sub_from_global" \
PROVIDER_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
bash scripts/init-pglogical-region.sh

View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
REGION_DB_URL="${DB_URL}" \
NODE_NAME="node_global" \
NODE_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
SUBSCRIPTION_NAME="sub_from_cn" \
PROVIDER_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
bash scripts/init-pglogical-region.sh

View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
: "${REGION_DB_URL:?❌ 缺少 REGION_DB_URL}"
: "${NODE_NAME:?❌ 缺少 NODE_NAME}"
: "${NODE_DSN:?❌ 缺少 NODE_DSN}"
: "${SUBSCRIPTION_NAME:?❌ 缺少 SUBSCRIPTION_NAME}"
: "${PROVIDER_DSN:?❌ 缺少 PROVIDER_DSN}"
psql "${REGION_DB_URL}" -v ON_ERROR_STOP=1 \
-v NODE_NAME="${NODE_NAME}" \
-v NODE_DSN="${NODE_DSN}" \
-v SUBSCRIPTION_NAME="${SUBSCRIPTION_NAME}" \
-v PROVIDER_DSN="${PROVIDER_DSN}" \
-f "${PGLOGICAL_REGION_FILE}"

7
scripts/init.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
bash scripts/init-go.sh
bash scripts/init-db.sh

12
scripts/migrate-db.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
echo ">>> 执行数据库迁移"
if [ ! -d sql/migrations ]; then
echo "⚠️ 未找到 sql/migrations跳过迁移"
exit 0
fi
go run ./cmd/migratectl/main.go migrate --dsn "${DB_URL}" --dir sql/migrations

8
scripts/reinit-db.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
echo ">>> 重置业务 schema (sql/schema.sql)"
bash scripts/reset-public-schema.sh
bash scripts/init-db-core.sh

11
scripts/reinit-pglogical.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
if [ "${REPLICATION_MODE}" = "pglogical" ]; then
echo ">>> 重新初始化 pglogical schema"
bash scripts/init-db-pglogical.sh
else
echo ">>> 当前 REPLICATION_MODE=${REPLICATION_MODE},无需 pglogical 处理"
fi

6
scripts/reset-public-schema.sh Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
psql "${DB_URL}" -v ON_ERROR_STOP=1 -v db_user="${DB_USER}" -f sql/reset_public_schema.sql

7
scripts/restart.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
bash scripts/stop.sh
bash scripts/start.sh

2
scripts/setup_postgres_local.sh Normal file → Executable file
View File

@ -13,7 +13,7 @@ set -euo pipefail
# 配置参数(可通过环境变量覆盖)
# -----------------------------------------------------------------------------
DB_NAME="${DB_NAME:-account}"
DB_USER="${DB_USER:-shenlan}"
DB_USER="${DB_USER:-${POSTGRES_USER:-postgres}}"
DB_PASS="${DB_PASS:-password}"
DB_PORT="${DB_PORT:-5432}"
DB_HOST="${DB_HOST:-127.0.0.1}"

6
scripts/start.sh Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
./"${APP_NAME}" --config config/account.yaml

6
scripts/stop.sh Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
pkill -f "${APP_NAME}" || echo "⚠️ 未找到运行进程"

49
scripts/stunnel-start.sh Executable file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
CONF_FILE="${CLOUD_RUN_STUNNEL_CONF}"
if [ ! -f "${CONF_FILE}" ]; then
echo "❌ 未找到 stunnel 配置: ${CONF_FILE}"
exit 1
fi
if ! command -v stunnel >/dev/null; then
echo "❌ 未检测到 stunnel请先安装"
exit 1
fi
if command -v ss >/dev/null; then
if ss -ltn 2>/dev/null | grep -q ':15432'; then
echo "✅ stunnel 已在 127.0.0.1:15432 监听"
exit 0
fi
elif command -v lsof >/dev/null; then
if lsof -nP -iTCP:15432 -sTCP:LISTEN | grep -q LISTEN; then
echo "✅ stunnel 已在 127.0.0.1:15432 监听"
exit 0
fi
fi
echo ">>> 启动 stunnel (client)"
# stunnel 需要写入 /var/run优先使用 sudo 启动
if sudo -n true 2>/dev/null; then
sudo stunnel "${CONF_FILE}" &
echo "✅ stunnel 启动完成 (sudo)"
exit 0
fi
echo "⚠️ sudo 不可用,使用用户态临时配置启动"
TMP_CONF="/tmp/stunnel-account-db-client.conf"
sed \
-e 's#^pid = .*#pid = /tmp/stunnel-account-db-client.pid#' \
-e 's#^output = .*#output = /tmp/stunnel-account-db-client.log#' \
"${CONF_FILE}" > "${TMP_CONF}"
if [ ! -f /etc/ssl/certs/ca-certificates.crt ] && [ -f /etc/ssl/cert.pem ]; then
sed -i '' 's#^CAfile = .*#CAfile = /etc/ssl/cert.pem#' "${TMP_CONF}"
fi
stunnel "${TMP_CONF}" &
echo "✅ stunnel 启动完成 (user mode)"

6
scripts/test.sh Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
go test ./...

8
scripts/upgrade.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
systemctl stop xcontrol-account
cp xcontrol-account /usr/bin/xcontrol-account
systemctl start xcontrol-account

View File

@ -67,9 +67,9 @@ go run ./cmd/migratectl/main.go check --cn "$CN_DSN" --global "$GLOBAL_DSN"
make -C account init-pglogical-region \
REGION_DB_URL="$REGION_DB_URL" \
NODE_NAME=node_cn \
NODE_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
NODE_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=${PGLOGICAL_USER} password=${PGLOGICAL_PASSWORD}" \
SUBSCRIPTION_NAME=sub_from_global \
PROVIDER_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx"
PROVIDER_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=${PGLOGICAL_USER} password=${PGLOGICAL_PASSWORD}"
```
4. 在另一侧节点重复执行并互为订阅,实现双主写入。
@ -85,21 +85,21 @@ pglogical schema 与业务 schema 分离,以防逻辑复制函数污染业务
bash
复制代码
sudo -u postgres psql -d account -c "GRANT USAGE ON SCHEMA pglogical TO PUBLIC;"
2⃣ 授权业务用户(shenlan
2⃣ 授权业务用户(app_user
sql
复制代码
-- 登录 postgres
sudo -u postgres psql -d account
-- 授权 shenlan 对 public schema 全权限
ALTER SCHEMA public OWNER TO shenlan;
GRANT ALL ON SCHEMA public TO shenlan;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO shenlan;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO shenlan;
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO shenlan;
-- 授权 app_user 对 public schema 全权限
ALTER SCHEMA public OWNER TO app_user;
GRANT ALL ON SCHEMA public TO app_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO app_user;
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO app_user;
-- 授权 pglogical schema 使用权限(仅使用,不可修改)
GRANT USAGE ON SCHEMA pglogical TO shenlan;
GRANT USAGE ON SCHEMA pglogical TO app_user;
\q
⚙️ 执行顺序建议
@ -123,17 +123,17 @@ GRANT USAGE ON SCHEMA pglogical TO shenlan;
# Global 节点示例
psql "$REGION_GLOBAL_DB_URL" -v ON_ERROR_STOP=1 \
-v NODE_NAME=node_global \
-v NODE_DSN='host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx' \
-v NODE_DSN='host=global-homepage.svc.plus port=5432 dbname=account user=${PGLOGICAL_USER} password=${PGLOGICAL_PASSWORD}' \
-v SUBSCRIPTION_NAME=sub_from_cn \
-v PROVIDER_DSN='host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx' \
-v PROVIDER_DSN='host=cn-homepage.svc.plus port=5432 dbname=account user=${PGLOGICAL_USER} password=${PGLOGICAL_PASSWORD}' \
-f account/sql/schema_pglogical_region.sql
# CN 节点示例
psql "$REGION_CN_DB_URL" -v ON_ERROR_STOP=1 \
-v NODE_NAME=node_cn \
-v NODE_DSN='host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx' \
-v NODE_DSN='host=cn-homepage.svc.plus port=5432 dbname=account user=${PGLOGICAL_USER} password=${PGLOGICAL_PASSWORD}' \
-v SUBSCRIPTION_NAME=sub_from_global \
-v PROVIDER_DSN='host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxx' \
-v PROVIDER_DSN='host=global-homepage.svc.plus port=5432 dbname=account user=${PGLOGICAL_USER} password=${PGLOGICAL_PASSWORD}' \
-f account/sql/schema_pglogical_region.sql
```
@ -143,12 +143,12 @@ psql "$REGION_CN_DB_URL" -v ON_ERROR_STOP=1 \
make init-pglogical-region \
REGION_DB_URL="$REGION_DB_URL" \
NODE_NAME=node_example \
NODE_DSN="host=example port=5432 dbname=account user=pglogical password=secret" \
NODE_DSN="host=example port=5432 dbname=account user=${PGLOGICAL_USER} password=${PGLOGICAL_PASSWORD}" \
SUBSCRIPTION_NAME=sub_from_peer \
PROVIDER_DSN="host=peer port=5432 dbname=account user=pglogical password=secret"
PROVIDER_DSN="host=peer port=5432 dbname=account user=${PGLOGICAL_USER} password=${PGLOGICAL_PASSWORD}"
```
- 若使用业务账号(如 `shenlan`执行初始化PostgreSQL 会提示缺少超级用户权限并跳过 `pglogical` 初始化。
- 若使用业务账号(如 `app_user`执行初始化PostgreSQL 会提示缺少超级用户权限并跳过 `pglogical` 初始化。
- 建议改用 `postgres` 等超级用户连接执行,或由管理员预先安装 `pglogical` 扩展并授予业务用户访问权限。
- 如果扩展已由管理员创建,可直接重新运行 `make init-pglogical-region-cn` 完成复制集与订阅配置。