chore: normalize tabs in account makefile (#503)

This commit is contained in:
shenlan 2025-10-13 09:00:45 +08:00 committed by GitHub
parent c3fdbdc07d
commit d37a45783a
7 changed files with 163 additions and 33 deletions

View File

@ -14,6 +14,8 @@ DB_HOST := 127.0.0.1
DB_PORT := 5432
DB_URL := postgres://$(DB_USER):$(DB_PASS)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
REPLICATION_MODE ?= pgsync
# 管理员账号用于执行需要超级权限的操作,例如删除数据库和初始化 pglogical
# 默认为业务账号,可通过环境变量覆盖
DB_ADMIN_USER ?= $(DB_USER)
@ -21,6 +23,7 @@ DB_ADMIN_PASS ?= $(DB_PASS)
SCHEMA_FILE := ./sql/schema.sql
PGLOGICAL_INIT_FILE := ./sql/schema_pglogical_init.sql
PGLOGICAL_PATCH_FILE := ./sql/schema_pglogical_patch.sql
PGLOGICAL_REGION_FILE := ./sql/schema_pglogical_region.sql
ACCOUNT_EXPORT_FILE ?= account-export.yaml
@ -43,6 +46,7 @@ all: build
help:
@echo "🧭 XControl Account Service Makefile"
@echo "make init 初始化 Go 环境与数据库"
@echo "make init-db 执行数据库 schema支持 REPLICATION_MODE=pgsync|pglogical"
@echo "make migrate-db 执行数据库迁移"
@echo "make dump-schema 导出数据库 schema"
@echo "make account-export 导出账号数据为 YAML"
@ -74,17 +78,25 @@ init-db:
@echo ">>> 初始化数据库 schema"
@command -v psql >/dev/null || (echo "❌ 未检测到 psql请安装 PostgreSQL 客户端" && exit 1)
@psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(SCHEMA_FILE)
@if [ -f $(PGLOGICAL_INIT_FILE) ]; then \
echo ">>> 初始化 pglogical schema"; \
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 初始化"; \
@if [ "$(REPLICATION_MODE)" = "pglogical" ]; then \
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; \
else \
echo ">>> 跳过 pglogical 初始化 (REPLICATION_MODE=$(REPLICATION_MODE))"; \
fi
# =========================================
@ -137,8 +149,8 @@ drop-db:
@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 \
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 ..."; \
@ -146,10 +158,10 @@ drop-db:
-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 \
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 \
else \
echo ">>> 删除失败"; \
fi; \
else \

View File

@ -0,0 +1,36 @@
# Example pgsync job for unidirectional async sync of the users table.
# Replace the DSN placeholders with concrete connection strings.
# https://github.com/timeplus-io/pgsync
jobs:
users_one_way:
source:
dsn: postgresql://replica:password@global-db:5432/account?sslmode=disable
destination:
dsn: postgresql://replica:password@cn-db:5432/account?sslmode=disable
schema: public
tables:
users:
delete: false
columns:
- uuid
- username
- password
- email
- role
- level
- groups
- permissions
- created_at
- updated_at
- version
- origin_node
- mfa_totp_secret
- mfa_enabled
- mfa_secret_issued_at
- mfa_confirmed_at
- email_verified_at
- email_verified
batch_size: 5000
snapshot: true
poll_interval: 30s

View File

@ -1,5 +1,13 @@
# Account 数据库结构与双向同步指南
## 方案概览
| 目标 | 推荐方案 | 说明 |
| --- | --- | --- |
| ✅ 一地写入 → 异步同步另一地 | [pgsync](#仅异步同步pgsync) | 使用逻辑导出/导入,无需超级权限,分钟级延迟 |
| ✅ 双向写入 → 最终一致 | [pglogical](#双向写入最终一致pglogical) | 双主架构,需超级用户安装扩展 |
| ✅ 结构迁移 / 数据导出备份 | `migratectl` + `pgsync` + `cron` | CLI 执行 schemapgsync 定时同步 |
使用新的 `migratectl` CLI 可以在不同环境下快速执行迁移、校验和重置操作:
```bash
@ -9,6 +17,63 @@ go run ./cmd/migratectl/main.go migrate --dsn "$DB_URL"
# 对比 CN 与 Global 节点结构一致性
go run ./cmd/migratectl/main.go check --cn "$CN_DSN" --global "$GLOBAL_DSN"
## 仅异步同步pgsync
当只需要将用户表从主区域异步同步到次区域时,推荐使用 [pgsync](https://github.com/timeplus-io/pgsync)。
1. 初始化基础 schema默认 `REPLICATION_MODE=pgsync` 不会执行任何 pglogical 步骤):
```bash
make -C account init-db REPLICATION_MODE=pgsync DB_URL="$SOURCE_DB_URL"
make -C account init-db REPLICATION_MODE=pgsync DB_URL="$DEST_DB_URL"
```
2. 编辑 `account/sql/pgsync.users.example.yaml`,替换源端与目标端 DSN。
3. 使用 pgsync 持续同步,可结合 cron 运行增量同步:
```bash
# 全量初始化
pgsync --config account/sql/pgsync.users.example.yaml --once
# 每分钟增量同步
* * * * * /usr/local/bin/pgsync --config /path/to/pgsync.users.yaml >> /var/log/pgsync.log 2>&1
```
4. 如需同步更多表,只需在配置文件中新增表条目。所有表均共享基础 schema无需超级用户权限。
> 📌 `schema.sql` 中的 `origin_node` 默认值为 `local`。在 pgsync 场景下可以保持默认,或者在迁移脚本中通过 `ALTER TABLE ... ALTER COLUMN origin_node SET DEFAULT 'global'` 为不同区域设置标记。
## 双向写入最终一致pglogical
当需要两地同时写入并保持最终一致时,可切换到 pglogical 模式。
1. 确保数据库超级用户已经安装 `pglogical` 扩展。
2. 初始化 schema 并应用 pglogical 默认值补丁:
```bash
make -C account init-db REPLICATION_MODE=pglogical DB_URL="$REGION_DB_URL" \
DB_ADMIN_USER=postgres DB_ADMIN_PASS=secret
```
该命令依次执行:
- `sql/schema.sql`:业务基础结构;
- `sql/schema_pglogical_init.sql`:在 `pglogical` schema 中安装扩展;
- `sql/schema_pglogical_patch.sql`:将 `origin_node` 默认值绑定到 `pglogical.node_name`
3. 参考下方模板配置双节点复制:
```bash
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" \
SUBSCRIPTION_NAME=sub_from_global \
PROVIDER_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx"
```
4. 在另一侧节点重复执行并互为订阅,实现双主写入。
## 🔐 权限与 Schema 设置
- 1⃣ 授权 pglogical schema 使用权限
@ -41,10 +106,12 @@ GRANT USAGE ON SCHEMA pglogical TO shenlan;
| 步骤 | 节点 | 脚本 / 命令 | 说明 |
| --- | --- | --- | --- |
| 1⃣ | Global | schema_base_bidirectional_enhanced.sql | 创建业务结构(含 version/origin_node |
| 2⃣ | CN | schema_base_bidirectional_enhanced.sql | 创建相同业务结构 |
| 3⃣ | Global | schema_pglogical_region.sql + 参数 | 定义 Global provider + 订阅 CN |
| 4⃣ | CN | schema_pglogical_region.sql + 参数 | 定义 CN provider + 订阅 Global |
| 1⃣ | Global | schema.sql | 创建业务结构(含 version/origin_node |
| 2⃣ | CN | schema.sql | 创建相同业务结构 |
| 3⃣ | Global | schema_pglogical_patch.sql | 绑定 origin_node 默认值到 pglogical |
| 4⃣ | CN | schema_pglogical_patch.sql | 同上 |
| 5⃣ | Global | schema_pglogical_region.sql + 参数 | 定义 Global provider + 订阅 CN |
| 6⃣ | CN | schema_pglogical_region.sql + 参数 | 定义 CN provider + 订阅 Global |
💡 执行 `schema_pglogical_region.sql` 或对应的 `make init-pglogical-region-*` 目标前,请确保连接用户拥有 PostgreSQL 超级用户权限。

View File

@ -1,6 +1,6 @@
-- =========================================
-- schema_base_bidirectional_enhanced.sql
-- Shared schema for pglogical bidirectional sync (Multi-Master Safe)
-- schema.sql
-- Base business schema for the account service.
-- Works with both one-way async sync (pgsync) and pglogical multi-master.
-- PostgreSQL 16 + gen_random_uuid()
-- =========================================
@ -19,8 +19,7 @@ DROP TABLE IF EXISTS public.admin_settings CASCADE;
-- =========================================
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
-- pglogical schema is initialized separately to avoid interfering with
-- business schema snapshots. See schema_pglogical_init.sql.
-- pglogical specific defaults are now applied by schema_pglogical_patch.sql.
-- =========================================
-- Functions
@ -55,7 +54,6 @@ BEGIN
END;
$$;
-- =========================================
-- Tables
-- =========================================
@ -71,7 +69,7 @@ CREATE TABLE public.users (
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
version BIGINT NOT NULL DEFAULT 0, -- 🔢 行版本号
origin_node TEXT DEFAULT current_setting('pglogical.node_name', true), -- 🌍 来源节点
origin_node TEXT NOT NULL DEFAULT 'local', -- 🌍 来源节点,可在不同区域通过 ALTER TABLE 或 pglogical patch 覆盖
mfa_totp_secret TEXT,
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
mfa_secret_issued_at TIMESTAMPTZ,
@ -88,7 +86,7 @@ CREATE TABLE public.identities (
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
version BIGINT NOT NULL DEFAULT 0,
origin_node TEXT DEFAULT current_setting('pglogical.node_name', true),
origin_node TEXT NOT NULL DEFAULT 'local',
CONSTRAINT identities_provider_external_id_uk UNIQUE (provider, external_id)
);
@ -100,7 +98,7 @@ CREATE TABLE public.sessions (
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
version BIGINT NOT NULL DEFAULT 0,
origin_node TEXT DEFAULT current_setting('pglogical.node_name', true)
origin_node TEXT NOT NULL DEFAULT 'local'
);
CREATE TABLE public.admin_settings (
@ -109,7 +107,7 @@ CREATE TABLE public.admin_settings (
role TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
version BIGINT NOT NULL DEFAULT 1,
origin_node TEXT DEFAULT current_setting('pglogical.node_name', true),
origin_node TEXT NOT NULL DEFAULT 'local',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT admin_settings_module_role_uk UNIQUE (module_key, role)

View File

@ -1,7 +1,7 @@
-- =========================================
-- schema_pglogical_init.sql
-- Dedicated pglogical schema initialization separated from business schema
-- =========================================
-- Dedicated pglogical schema initialization separated from business schema.
-- Execute schema_pglogical_patch.sql afterwards to align origin defaults.
DO $$
BEGIN

View File

@ -0,0 +1,18 @@
-- =========================================
-- schema_pglogical_patch.sql
-- Apply pglogical-aware defaults on top of the base business schema.
-- Run only when the cluster enables pglogical for bidirectional writes.
-- =========================================
-- Align origin_node defaults with pglogical node name when available.
ALTER TABLE public.users
ALTER COLUMN origin_node SET DEFAULT COALESCE(current_setting('pglogical.node_name', true), 'local');
ALTER TABLE public.identities
ALTER COLUMN origin_node SET DEFAULT COALESCE(current_setting('pglogical.node_name', true), 'local');
ALTER TABLE public.sessions
ALTER COLUMN origin_node SET DEFAULT COALESCE(current_setting('pglogical.node_name', true), 'local');
ALTER TABLE public.admin_settings
ALTER COLUMN origin_node SET DEFAULT COALESCE(current_setting('pglogical.node_name', true), 'local');

View File

@ -2,8 +2,7 @@
-- schema_pglogical_region.sql
-- pglogical configuration template for regional nodes
-- PostgreSQL 16+, 双向复制 (provider + subscriber)
-- 使用前请通过 psql -v NODE_NAME=... -v NODE_DSN=... -v SUBSCRIPTION_NAME=... -v PROVIDER_DSN=...
-- 提供所需参数。
-- 在运行本脚本前,请确保已执行 schema.sql 与 schema_pglogical_patch.sql。
-- =========================================
\if :{?NODE_NAME}