From c7c6e14ad52b576e3591b48599e04dc16ef6edbb Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 24 Jan 2026 22:36:29 +0800 Subject: [PATCH 1/6] refactor: move make targets to scripts --- Makefile | 217 +++++------------------- scripts/_common.sh | 6 + scripts/account-export.sh | 11 ++ scripts/account-import.sh | 32 ++++ scripts/account-sync-mirror.sh | 11 ++ scripts/account-sync-pull.sh | 11 ++ scripts/account-sync-push.sh | 11 ++ scripts/build.sh | 6 + scripts/clean.sh | 6 + scripts/cloudrun-build.sh | 11 ++ scripts/cloudrun-deploy.sh | 11 ++ scripts/cloudrun-stunnel.sh | 15 ++ scripts/create-db-user.sh | 15 ++ scripts/create-super-admin.sh | 16 ++ scripts/db-reset.sh | 22 +++ scripts/dev.sh | 12 ++ scripts/drop-db.sh | 27 +++ scripts/dump-schema.sh | 7 + scripts/init-db-core.sh | 7 + scripts/init-db-pglogical.sh | 22 +++ scripts/init-db-replication.sh | 10 ++ scripts/init-db.sh | 13 ++ scripts/init-go.sh | 32 ++++ scripts/init-pglogical-region-cn.sh | 11 ++ scripts/init-pglogical-region-global.sh | 11 ++ scripts/init-pglogical-region.sh | 17 ++ scripts/init.sh | 7 + scripts/migrate-db.sh | 7 + scripts/reinit-db.sh | 8 + scripts/reinit-pglogical.sh | 11 ++ scripts/reset-public-schema.sh | 6 + scripts/restart.sh | 7 + scripts/setup_postgres_local.sh | 0 scripts/start.sh | 6 + scripts/stop.sh | 6 + scripts/test.sh | 6 + scripts/upgrade.sh | 8 + 37 files changed, 469 insertions(+), 173 deletions(-) create mode 100755 scripts/_common.sh create mode 100755 scripts/account-export.sh create mode 100755 scripts/account-import.sh create mode 100755 scripts/account-sync-mirror.sh create mode 100755 scripts/account-sync-pull.sh create mode 100755 scripts/account-sync-push.sh create mode 100755 scripts/build.sh create mode 100755 scripts/clean.sh create mode 100755 scripts/cloudrun-build.sh create mode 100755 scripts/cloudrun-deploy.sh create mode 100755 scripts/cloudrun-stunnel.sh create mode 100755 scripts/create-db-user.sh create mode 100755 scripts/create-super-admin.sh create mode 100755 scripts/db-reset.sh create mode 100755 scripts/dev.sh create mode 100755 scripts/drop-db.sh create mode 100755 scripts/dump-schema.sh create mode 100755 scripts/init-db-core.sh create mode 100755 scripts/init-db-pglogical.sh create mode 100755 scripts/init-db-replication.sh create mode 100755 scripts/init-db.sh create mode 100755 scripts/init-go.sh create mode 100755 scripts/init-pglogical-region-cn.sh create mode 100755 scripts/init-pglogical-region-global.sh create mode 100755 scripts/init-pglogical-region.sh create mode 100755 scripts/init.sh create mode 100755 scripts/migrate-db.sh create mode 100755 scripts/reinit-db.sh create mode 100755 scripts/reinit-pglogical.sh create mode 100755 scripts/reset-public-schema.sh create mode 100755 scripts/restart.sh mode change 100644 => 100755 scripts/setup_postgres_local.sh create mode 100755 scripts/start.sh create mode 100755 scripts/stop.sh create mode 100755 scripts/test.sh create mode 100755 scripts/upgrade.sh diff --git a/Makefile b/Makefile index 2775c34..de0ce66 100644 --- a/Makefile +++ b/Makefile @@ -40,13 +40,22 @@ 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 \ + init-go init-db init-db-core init-db-replication init-db-pglogical \ reinit-pglogical account-sync-push account-sync-pull account-sync-mirror create-db-user db-reset \ cloudrun-build cloudrun-deploy cloudrun-stunnel @@ -78,257 +87,119 @@ 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 # ========================================= # 💾 账号导入导出 # ========================================= 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 # ========================================= # ⚙️ 编译与运行 # ========================================= 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 diff --git a/scripts/_common.sh b/scripts/_common.sh new file mode 100755 index 0000000..dc62435 --- /dev/null +++ b/scripts/_common.sh @@ -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" diff --git a/scripts/account-export.sh b/scripts/account-export.sh new file mode 100755 index 0000000..08af638 --- /dev/null +++ b/scripts/account-export.sh @@ -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[@]}" diff --git a/scripts/account-import.sh b/scripts/account-import.sh new file mode 100755 index 0000000..20ee3fe --- /dev/null +++ b/scripts/account-import.sh @@ -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[@]}" diff --git a/scripts/account-sync-mirror.sh b/scripts/account-sync-mirror.sh new file mode 100755 index 0000000..7fa82f2 --- /dev/null +++ b/scripts/account-sync-mirror.sh @@ -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}" diff --git a/scripts/account-sync-pull.sh b/scripts/account-sync-pull.sh new file mode 100755 index 0000000..228d859 --- /dev/null +++ b/scripts/account-sync-pull.sh @@ -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}" diff --git a/scripts/account-sync-push.sh b/scripts/account-sync-push.sh new file mode 100755 index 0000000..493c6a4 --- /dev/null +++ b/scripts/account-sync-push.sh @@ -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}" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..03ffd8d --- /dev/null +++ b/scripts/build.sh @@ -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}" diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100755 index 0000000..26dbcdc --- /dev/null +++ b/scripts/clean.sh @@ -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 diff --git a/scripts/cloudrun-build.sh b/scripts/cloudrun-build.sh new file mode 100755 index 0000000..0b30e67 --- /dev/null +++ b/scripts/cloudrun-build.sh @@ -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 不能为空" + exit 1 +fi + +gcloud builds submit --tag "${CLOUD_RUN_IMAGE}" . diff --git a/scripts/cloudrun-deploy.sh b/scripts/cloudrun-deploy.sh new file mode 100755 index 0000000..15c3791 --- /dev/null +++ b/scripts/cloudrun-deploy.sh @@ -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 不能为空" + exit 1 +fi + +gcloud run services replace "${CLOUD_RUN_SERVICE_YAML}" --region "${GCP_REGION}" --project "${GCP_PROJECT}" diff --git a/scripts/cloudrun-stunnel.sh b/scripts/cloudrun-stunnel.sh new file mode 100755 index 0000000..2918bc4 --- /dev/null +++ b/scripts/cloudrun-stunnel.sh @@ -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 不能为空" + 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}" diff --git a/scripts/create-db-user.sh b/scripts/create-db-user.sh new file mode 100755 index 0000000..e2b42a9 --- /dev/null +++ b/scripts/create-db-user.sh @@ -0,0 +1,15 @@ +#!/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 "正在以 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 "✓ 数据库用户创建完成" diff --git a/scripts/create-super-admin.sh b/scripts/create-super-admin.sh new file mode 100755 index 0000000..76c9ac7 --- /dev/null +++ b/scripts/create-super-admin.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" + +if [ -z "${SUPERADMIN_USERNAME}" ] || [ -z "${SUPERADMIN_PASSWORD}" ]; then + echo "❌ 请指定用户名与密码" + exit 1 +fi + +go run ./cmd/createadmin/main.go \ + --driver postgres \ + --dsn "${DB_URL}" \ + --username "${SUPERADMIN_USERNAME}" \ + --password "${SUPERADMIN_PASSWORD}" \ + --email "${SUPERADMIN_EMAIL}" diff --git a/scripts/db-reset.sh b/scripts/db-reset.sh new file mode 100755 index 0000000..003a17d --- /dev/null +++ b/scripts/db-reset.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" + +echo "⚠️ 即将重置整个 PostgreSQL 数据库集群 ..." +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 diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..ef7436a --- /dev/null +++ b/scripts/dev.sh @@ -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 1 diff --git a/scripts/drop-db.sh b/scripts/drop-db.sh new file mode 100755 index 0000000..4d200e6 --- /dev/null +++ b/scripts/drop-db.sh @@ -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 diff --git a/scripts/dump-schema.sh b/scripts/dump-schema.sh new file mode 100755 index 0000000..8f18c86 --- /dev/null +++ b/scripts/dump-schema.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" + +echo ">>> 导出 schema 到 ${SCHEMA_FILE}" +pg_dump -s -O -x "${DB_URL}" > "${SCHEMA_FILE}" diff --git a/scripts/init-db-core.sh b/scripts/init-db-core.sh new file mode 100755 index 0000000..0d617f5 --- /dev/null +++ b/scripts/init-db-core.sh @@ -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}" diff --git a/scripts/init-db-pglogical.sh b/scripts/init-db-pglogical.sh new file mode 100755 index 0000000..647d4cf --- /dev/null +++ b/scripts/init-db-pglogical.sh @@ -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 diff --git a/scripts/init-db-replication.sh b/scripts/init-db-replication.sh new file mode 100755 index 0000000..a8fcfd8 --- /dev/null +++ b/scripts/init-db-replication.sh @@ -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 diff --git a/scripts/init-db.sh b/scripts/init-db.sh new file mode 100755 index 0000000..4156ab4 --- /dev/null +++ b/scripts/init-db.sh @@ -0,0 +1,13 @@ +#!/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/init-db-core.sh +bash scripts/init-db-replication.sh diff --git a/scripts/init-go.sh b/scripts/init-go.sh new file mode 100755 index 0000000..14a8527 --- /dev/null +++ b/scripts/init-go.sh @@ -0,0 +1,32 @@ +#!/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" +if curl -fsSL --max-time 5 https://goproxy.cn >/dev/null; then + go env -w GOPROXY=https://goproxy.cn,direct +else + go env -w GOPROXY=https://proxy.golang.org,direct +fi + +go mod tidy diff --git a/scripts/init-pglogical-region-cn.sh b/scripts/init-pglogical-region-cn.sh new file mode 100755 index 0000000..5460155 --- /dev/null +++ b/scripts/init-pglogical-region-cn.sh @@ -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 diff --git a/scripts/init-pglogical-region-global.sh b/scripts/init-pglogical-region-global.sh new file mode 100755 index 0000000..064bb44 --- /dev/null +++ b/scripts/init-pglogical-region-global.sh @@ -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 diff --git a/scripts/init-pglogical-region.sh b/scripts/init-pglogical-region.sh new file mode 100755 index 0000000..f04df8f --- /dev/null +++ b/scripts/init-pglogical-region.sh @@ -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}" diff --git a/scripts/init.sh b/scripts/init.sh new file mode 100755 index 0000000..82b242e --- /dev/null +++ b/scripts/init.sh @@ -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 diff --git a/scripts/migrate-db.sh b/scripts/migrate-db.sh new file mode 100755 index 0000000..bc0ef5b --- /dev/null +++ b/scripts/migrate-db.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" + +echo ">>> 执行数据库迁移" +go run ./cmd/migratectl/main.go migrate --dsn "${DB_URL}" --dir sql/migrations diff --git a/scripts/reinit-db.sh b/scripts/reinit-db.sh new file mode 100755 index 0000000..c8dbb44 --- /dev/null +++ b/scripts/reinit-db.sh @@ -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 diff --git a/scripts/reinit-pglogical.sh b/scripts/reinit-pglogical.sh new file mode 100755 index 0000000..ff42895 --- /dev/null +++ b/scripts/reinit-pglogical.sh @@ -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 diff --git a/scripts/reset-public-schema.sh b/scripts/reset-public-schema.sh new file mode 100755 index 0000000..d1d4202 --- /dev/null +++ b/scripts/reset-public-schema.sh @@ -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 diff --git a/scripts/restart.sh b/scripts/restart.sh new file mode 100755 index 0000000..5fb81a7 --- /dev/null +++ b/scripts/restart.sh @@ -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 diff --git a/scripts/setup_postgres_local.sh b/scripts/setup_postgres_local.sh old mode 100644 new mode 100755 diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 0000000..26b3bf7 --- /dev/null +++ b/scripts/start.sh @@ -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 diff --git a/scripts/stop.sh b/scripts/stop.sh new file mode 100755 index 0000000..99654c5 --- /dev/null +++ b/scripts/stop.sh @@ -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 "⚠️ 未找到运行进程" diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..18567c3 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" + +go test ./... diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh new file mode 100755 index 0000000..70ebefc --- /dev/null +++ b/scripts/upgrade.sh @@ -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 From 52a575535f2bff73a249de3d76fe1effa6cec3cf Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 24 Jan 2026 22:42:40 +0800 Subject: [PATCH 2/6] chore: fix phony init-db and go proxy --- Makefile | 2 +- scripts/init-go.sh | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index de0ce66..1256725 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DB_NAME := account DB_USER := shenlan DB_PASS := 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 diff --git a/scripts/init-go.sh b/scripts/init-go.sh index 14a8527..178155f 100755 --- a/scripts/init-go.sh +++ b/scripts/init-go.sh @@ -23,10 +23,6 @@ if ! command -v go >/dev/null; then fi echo ">>> 配置 Go Proxy" -if curl -fsSL --max-time 5 https://goproxy.cn >/dev/null; then - go env -w GOPROXY=https://goproxy.cn,direct -else - go env -w GOPROXY=https://proxy.golang.org,direct -fi +go env -w GOPROXY=https://proxy.golang.org,direct go mod tidy From 1151ee0c71d4a88484aaa142ed18d841f6767837 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 24 Jan 2026 22:57:37 +0800 Subject: [PATCH 3/6] chore: improve scripts for local stunnel and env --- Makefile | 8 ++++-- scripts/cloudrun-build.sh | 4 +-- scripts/cloudrun-deploy.sh | 4 +-- scripts/cloudrun-stunnel.sh | 4 +-- scripts/create-db-user.sh | 13 +++++++--- scripts/create-super-admin.sh | 5 ++++ scripts/db-reset.sh | 4 +++ scripts/dev.sh | 2 +- scripts/dump-schema.sh | 14 +++++++++- scripts/ensure-db.sh | 14 ++++++++++ scripts/init-db.sh | 1 + scripts/migrate-db.sh | 5 ++++ scripts/stunnel-start.sh | 49 +++++++++++++++++++++++++++++++++++ 13 files changed, 114 insertions(+), 13 deletions(-) create mode 100755 scripts/ensure-db.sh create mode 100755 scripts/stunnel-start.sh diff --git a/Makefile b/Makefile index 1256725..2c03600 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,8 @@ PORT ?= 8080 OS := $(shell uname -s) DB_NAME := account -DB_USER := shenlan -DB_PASS := password +DB_USER ?= $(or $(POSTGRES_USER),shenlan) +DB_PASS ?= $(or $(POSTGRES_PASSWORD),password) DB_HOST := 127.0.0.1 DB_PORT := 15432 DB_URL := postgres://$(DB_USER):$(DB_PASS)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable @@ -56,6 +56,7 @@ export APP_NAME MAIN_FILE PORT OS \ .PHONY: all init build clean start stop restart dev test help \ 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 @@ -142,6 +143,9 @@ reinit-db: reinit-pglogical: @bash scripts/reinit-pglogical.sh +stunnel-start: + @bash scripts/stunnel-start.sh + # ========================================= # 💾 账号导入导出 # ========================================= diff --git a/scripts/cloudrun-build.sh b/scripts/cloudrun-build.sh index 0b30e67..1f33dff 100755 --- a/scripts/cloudrun-build.sh +++ b/scripts/cloudrun-build.sh @@ -4,8 +4,8 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" if [ -z "${GCP_PROJECT}" ]; then - echo "❌ GCP_PROJECT 不能为空" - exit 1 + echo "⚠️ GCP_PROJECT 不能为空,跳过 Cloud Run 构建" + exit 0 fi gcloud builds submit --tag "${CLOUD_RUN_IMAGE}" . diff --git a/scripts/cloudrun-deploy.sh b/scripts/cloudrun-deploy.sh index 15c3791..7ffcc1c 100755 --- a/scripts/cloudrun-deploy.sh +++ b/scripts/cloudrun-deploy.sh @@ -4,8 +4,8 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" if [ -z "${GCP_PROJECT}" ]; then - echo "❌ GCP_PROJECT 不能为空" - exit 1 + echo "⚠️ GCP_PROJECT 不能为空,跳过 Cloud Run 部署" + exit 0 fi gcloud run services replace "${CLOUD_RUN_SERVICE_YAML}" --region "${GCP_REGION}" --project "${GCP_PROJECT}" diff --git a/scripts/cloudrun-stunnel.sh b/scripts/cloudrun-stunnel.sh index 2918bc4..bd66775 100755 --- a/scripts/cloudrun-stunnel.sh +++ b/scripts/cloudrun-stunnel.sh @@ -4,8 +4,8 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" if [ -z "${GCP_PROJECT}" ]; then - echo "❌ GCP_PROJECT 不能为空" - exit 1 + echo "⚠️ GCP_PROJECT 不能为空,跳过 stunnel secret 更新" + exit 0 fi if [ ! -f "${CLOUD_RUN_STUNNEL_CONF}" ]; then echo "❌ 未找到 stunnel 配置: ${CLOUD_RUN_STUNNEL_CONF}" diff --git a/scripts/create-db-user.sh b/scripts/create-db-user.sh index e2b42a9..14d98cb 100755 --- a/scripts/create-db-user.sh +++ b/scripts/create-db-user.sh @@ -9,7 +9,14 @@ if ! command -v psql >/dev/null; then exit 1 fi -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 "正在以管理员身份创建用户..." +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 "✓ 数据库用户创建完成" diff --git a/scripts/create-super-admin.sh b/scripts/create-super-admin.sh index 76c9ac7..1aa9906 100755 --- a/scripts/create-super-admin.sh +++ b/scripts/create-super-admin.sh @@ -8,6 +8,11 @@ if [ -z "${SUPERADMIN_USERNAME}" ] || [ -z "${SUPERADMIN_PASSWORD}" ]; then exit 1 fi +if psql "${DB_URL}" -Atc "SELECT 1 FROM users WHERE username='${SUPERADMIN_USERNAME}' OR email='${SUPERADMIN_EMAIL}' LIMIT 1" | grep -qx '1'; then + echo "⚠️ 超级管理员已存在,跳过创建" + exit 0 +fi + go run ./cmd/createadmin/main.go \ --driver postgres \ --dsn "${DB_URL}" \ diff --git a/scripts/db-reset.sh b/scripts/db-reset.sh index 003a17d..414e6f8 100755 --- a/scripts/db-reset.sh +++ b/scripts/db-reset.sh @@ -4,6 +4,10 @@ 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 服务 ..." diff --git a/scripts/dev.sh b/scripts/dev.sh index ef7436a..74f3819 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -9,4 +9,4 @@ if command -v air >/dev/null; then fi echo "❌ 未检测到 air (热重载工具),请先安装: https://github.com/cosmtrek/air" -exit 1 +exit 0 diff --git a/scripts/dump-schema.sh b/scripts/dump-schema.sh index 8f18c86..7238f09 100755 --- a/scripts/dump-schema.sh +++ b/scripts/dump-schema.sh @@ -4,4 +4,16 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" echo ">>> 导出 schema 到 ${SCHEMA_FILE}" -pg_dump -s -O -x "${DB_URL}" > "${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 + +${PG_DUMP_BIN} -s -O -x "${DB_URL}" > "${SCHEMA_FILE}" diff --git a/scripts/ensure-db.sh b/scripts/ensure-db.sh new file mode 100755 index 0000000..7fb3aca --- /dev/null +++ b/scripts/ensure-db.sh @@ -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};" diff --git a/scripts/init-db.sh b/scripts/init-db.sh index 4156ab4..6545863 100755 --- a/scripts/init-db.sh +++ b/scripts/init-db.sh @@ -9,5 +9,6 @@ if ! command -v psql >/dev/null; then exit 1 fi +bash scripts/ensure-db.sh bash scripts/init-db-core.sh bash scripts/init-db-replication.sh diff --git a/scripts/migrate-db.sh b/scripts/migrate-db.sh index bc0ef5b..0219ff9 100755 --- a/scripts/migrate-db.sh +++ b/scripts/migrate-db.sh @@ -4,4 +4,9 @@ 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 diff --git a/scripts/stunnel-start.sh b/scripts/stunnel-start.sh new file mode 100755 index 0000000..8248ac9 --- /dev/null +++ b/scripts/stunnel-start.sh @@ -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)" From 6d76072104009d389174488492beaef373ab6f27 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 24 Jan 2026 23:02:53 +0800 Subject: [PATCH 4/6] chore: avoid clobbering schema on dump --- scripts/dump-schema.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/dump-schema.sh b/scripts/dump-schema.sh index 7238f09..852ab61 100755 --- a/scripts/dump-schema.sh +++ b/scripts/dump-schema.sh @@ -16,4 +16,11 @@ if [ -n "${SERVER_MAJOR}" ] && [ -n "${LOCAL_MAJOR}" ] && [ "${SERVER_MAJOR}" != exit 0 fi -${PG_DUMP_BIN} -s -O -x "${DB_URL}" > "${SCHEMA_FILE}" +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}" From ead2ed7fa228be35f406ddd846c5c5087b47fdcb Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 24 Jan 2026 23:04:41 +0800 Subject: [PATCH 5/6] chore: ignore account export output --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e095801..9534da4 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,4 @@ dist/ out/ target/ xcontrol-account +account-export.yaml From fb06e97d6ca882a7c3c4f3008d2b42cfb75a79b5 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 25 Jan 2026 09:04:08 +0800 Subject: [PATCH 6/6] Add superadmin integration tests and env-based config --- Makefile | 12 ++- config/account-server.yaml | 3 +- config/account.yaml | 4 +- config/sync.example.yaml | 4 +- config/sync.yaml | 4 +- deploy/gcp/cloud-run/service.yaml | 4 +- integration-test/superadmin-login/README.md | 66 ++++++++++++ integration-test/superadmin-login/api-test.sh | 80 ++++++++++++++ .../superadmin-login/run-test-scripts.sh | 74 +++++++++++++ integration-test/superadmin-login/ui-test.sh | 101 ++++++++++++++++++ scripts/create-super-admin.sh | 34 +++++- scripts/setup_postgres_local.sh | 2 +- sql/readme.md | 34 +++--- 13 files changed, 390 insertions(+), 32 deletions(-) create mode 100644 integration-test/superadmin-login/README.md create mode 100755 integration-test/superadmin-login/api-test.sh create mode 100755 integration-test/superadmin-login/run-test-scripts.sh create mode 100755 integration-test/superadmin-login/ui-test.sh diff --git a/Makefile b/Makefile index 2c03600..091cd46 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,12 @@ 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 ?= $(or $(POSTGRES_USER),shenlan) +DB_USER ?= $(or $(POSTGRES_USER),postgres) DB_PASS ?= $(or $(POSTGRES_PASSWORD),password) DB_HOST := 127.0.0.1 DB_PORT := 15432 @@ -54,7 +58,7 @@ export APP_NAME MAIN_FILE PORT OS \ # 🧩 基础命令 # ========================================= -.PHONY: all init build clean start stop restart dev test help \ +.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 \ @@ -77,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" @@ -168,6 +173,9 @@ account-sync-mirror: create-super-admin: @bash scripts/create-super-admin.sh +integration-test: + @bash integration-test/superadmin-login/run-test-scripts.sh + # ========================================= # ⚙️ 编译与运行 # ========================================= diff --git a/config/account-server.yaml b/config/account-server.yaml index dbf1e80..c321343 100644 --- a/config/account-server.yaml +++ b/config/account-server.yaml @@ -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 diff --git a/config/account.yaml b/config/account.yaml index f2e63ac..f0f0247 100644 --- a/config/account.yaml +++ b/config/account.yaml @@ -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 diff --git a/config/sync.example.yaml b/config/sync.example.yaml index 82b437e..a3f82f7 100644 --- a/config/sync.example.yaml +++ b/config/sync.example.yaml @@ -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 - diff --git a/config/sync.yaml b/config/sync.yaml index c7c261d..0b31df6 100644 --- a/config/sync.yaml +++ b/config/sync.yaml @@ -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 - diff --git a/deploy/gcp/cloud-run/service.yaml b/deploy/gcp/cloud-run/service.yaml index 1e7a195..28c4de8 100644 --- a/deploy/gcp/cloud-run/service.yaml +++ b/deploy/gcp/cloud-run/service.yaml @@ -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 diff --git a/integration-test/superadmin-login/README.md b/integration-test/superadmin-login/README.md new file mode 100644 index 0000000..981e01c --- /dev/null +++ b/integration-test/superadmin-login/README.md @@ -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 +- 密码: +``` + +> 注意:登录验证需要手动在浏览器完成。 diff --git a/integration-test/superadmin-login/api-test.sh b/integration-test/superadmin-login/api-test.sh new file mode 100755 index 0000000..18a76cb --- /dev/null +++ b/integration-test/superadmin-login/api-test.sh @@ -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 <&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 登录测试通过" diff --git a/integration-test/superadmin-login/run-test-scripts.sh b/integration-test/superadmin-login/run-test-scripts.sh new file mode 100755 index 0000000..9856e3a --- /dev/null +++ b/integration-test/superadmin-login/run-test-scripts.sh @@ -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/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 登录测试通过" diff --git a/scripts/create-super-admin.sh b/scripts/create-super-admin.sh index 1aa9906..9f0ac69 100755 --- a/scripts/create-super-admin.sh +++ b/scripts/create-super-admin.sh @@ -3,13 +3,34 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" -if [ -z "${SUPERADMIN_USERNAME}" ] || [ -z "${SUPERADMIN_PASSWORD}" ]; then - echo "❌ 请指定用户名与密码" - exit 1 +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'