From 1e9cdda1926c9f9fe0121ab18fe8ff1669f519af Mon Sep 17 00:00:00 2001 From: fish Date: Fri, 3 Oct 2025 16:39:24 +0800 Subject: [PATCH] init code --- .dockerignore | 26 ++ .env | 12 + api_delete/README.md | 7 + api_delete/depend.py | 69 +++++ api_delete/docker-compose.delete.yaml | 16 ++ api_delete/dockerfile | 38 +++ api_delete/go.mod | 43 +++ api_delete/main.go | 146 ++++++++++ api_delete/release.sh | 22 ++ api_gateway/README.md | 7 + api_gateway/depend.py | 69 +++++ api_gateway/docker-compose.gateway.yaml | 11 + api_gateway/dockerfile | 38 +++ api_gateway/go.mod | 42 +++ api_gateway/main.go | 137 +++++++++ api_gateway/release.sh | 22 ++ api_login/README.md | 7 + api_login/depend.py | 69 +++++ api_login/docker-compose.login.yaml | 16 ++ api_login/dockerfile | 38 +++ api_login/go.mod | 43 +++ api_login/main.go | 162 +++++++++++ api_login/release.sh | 22 ++ api_register/README.md | 7 + api_register/depend.py | 69 +++++ api_register/docker-compose.register.yaml | 16 ++ api_register/dockerfile | 38 +++ api_register/go.mod | 43 +++ api_register/main.go | 272 ++++++++++++++++++ api_register/release.sh | 22 ++ api_template/README.md | 7 + api_template/depend.py | 69 +++++ api_template/docker-compose.temp.yaml | 16 ++ api_template/dockerfile | 38 +++ api_template/release.sh | 22 ++ api_update_account/README.md | 7 + api_update_account/depend.py | 69 +++++ .../docker-compose.update.account.yaml | 16 ++ api_update_account/dockerfile | 38 +++ api_update_account/go.mod | 43 +++ api_update_account/main.go | 204 +++++++++++++ api_update_account/release.sh | 22 ++ api_update_password/README.md | 7 + api_update_password/depend.py | 69 +++++ .../docker-compose.update.password.yaml | 16 ++ api_update_password/dockerfile | 38 +++ api_update_password/go.mod | 43 +++ api_update_password/main.go | 206 +++++++++++++ api_update_password/release.sh | 22 ++ build.py | 140 +++++++++ deploy.py | 191 ++++++++++++ docker-compose.base.yaml | 9 + docker-compose.db.admin.yaml | 16 ++ docker-compose.db.yaml | 17 ++ docker-compose.yaml | 123 ++++++++ scripts/db-lanuch-entrypoint.sh | 43 +++ sql/01_uuid_v7_setup.sql | 48 ++++ sql/02_create_user_table.sql | 30 ++ sql/03_create_account_table.sql | 31 ++ sql/04_create_password_table.sql | 31 ++ sql/05_create_account_password_view.sql | 36 +++ 61 files changed, 3196 insertions(+) create mode 100644 .dockerignore create mode 100644 .env create mode 100644 api_delete/README.md create mode 100644 api_delete/depend.py create mode 100644 api_delete/docker-compose.delete.yaml create mode 100644 api_delete/dockerfile create mode 100644 api_delete/go.mod create mode 100644 api_delete/main.go create mode 100755 api_delete/release.sh create mode 100644 api_gateway/README.md create mode 100644 api_gateway/depend.py create mode 100644 api_gateway/docker-compose.gateway.yaml create mode 100644 api_gateway/dockerfile create mode 100644 api_gateway/go.mod create mode 100644 api_gateway/main.go create mode 100755 api_gateway/release.sh create mode 100644 api_login/README.md create mode 100644 api_login/depend.py create mode 100644 api_login/docker-compose.login.yaml create mode 100644 api_login/dockerfile create mode 100644 api_login/go.mod create mode 100644 api_login/main.go create mode 100755 api_login/release.sh create mode 100644 api_register/README.md create mode 100644 api_register/depend.py create mode 100644 api_register/docker-compose.register.yaml create mode 100644 api_register/dockerfile create mode 100644 api_register/go.mod create mode 100644 api_register/main.go create mode 100755 api_register/release.sh create mode 100644 api_template/README.md create mode 100644 api_template/depend.py create mode 100644 api_template/docker-compose.temp.yaml create mode 100644 api_template/dockerfile create mode 100755 api_template/release.sh create mode 100644 api_update_account/README.md create mode 100644 api_update_account/depend.py create mode 100644 api_update_account/docker-compose.update.account.yaml create mode 100644 api_update_account/dockerfile create mode 100644 api_update_account/go.mod create mode 100644 api_update_account/main.go create mode 100755 api_update_account/release.sh create mode 100644 api_update_password/README.md create mode 100644 api_update_password/depend.py create mode 100644 api_update_password/docker-compose.update.password.yaml create mode 100644 api_update_password/dockerfile create mode 100644 api_update_password/go.mod create mode 100644 api_update_password/main.go create mode 100755 api_update_password/release.sh create mode 100644 build.py create mode 100644 deploy.py create mode 100644 docker-compose.base.yaml create mode 100644 docker-compose.db.admin.yaml create mode 100644 docker-compose.db.yaml create mode 100644 docker-compose.yaml create mode 100755 scripts/db-lanuch-entrypoint.sh create mode 100644 sql/01_uuid_v7_setup.sql create mode 100644 sql/02_create_user_table.sql create mode 100644 sql/03_create_account_table.sql create mode 100644 sql/04_create_password_table.sql create mode 100644 sql/05_create_account_password_view.sql diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3d1e553 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +# 排除Go编译产物 +*.exe +*.exe~ +*.dll +*.so +*.dylib +app # 排除本地已构建的二进制文件 + +# 排除依赖目录 +vendor/ +go/pkg/ +db +scripts +shared_data + +# 排除版本控制和日志文件 +.git/ +.gitignore +logs/ +*.log +*.md +*.sql + +# 排除IDE配置文件 +.idea/ +.vscode/ \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..21e982a --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +# 数据库配置 +DB_USER=postgres +DB_PASSWORD=postgres12341234 +DB_NAME=postgres +DB_PORT=5432 + +# 时区配置 +TZ=Asia/Shanghai + +# pgAdmin配置 +PGADMIN_EMAIL=fish@fish.com +PGADMIN_PASSWORD=12345678 \ No newline at end of file diff --git a/api_delete/README.md b/api_delete/README.md new file mode 100644 index 0000000..b20dcb5 --- /dev/null +++ b/api_delete/README.md @@ -0,0 +1,7 @@ +sudo docker rm -f user_delete_api && sudo docker run -itd --name user_delete_api -v ./:/app -p 20005:80 golang:1.25.0-alpine3.22 + +---- + +# 格式:docker build -t 镜像名:标签 . +sudo docker rmi -f user-delete-api:1.0.0 +sudo docker build -t user-delete-api:1.0.0 . \ No newline at end of file diff --git a/api_delete/depend.py b/api_delete/depend.py new file mode 100644 index 0000000..532d6cb --- /dev/null +++ b/api_delete/depend.py @@ -0,0 +1,69 @@ +import subprocess +import os + +def run_command(command, check=True, shell=True): + """执行shell命令并返回结果,包含错误处理""" + try: + print(f"执行命令: {command}") + result = subprocess.run( + command, + shell=shell, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + print(f"命令输出: {result.stdout}") + return result + except subprocess.CalledProcessError as e: + print(f"命令执行失败: {e.stderr}") + raise + +def main(): + container_name = "user_delete_api" + script_dir = os.path.dirname(os.path.abspath(__file__)) # 获取当前脚本所在目录 + + try: + # 1. 删除已存在的容器 + print("===== 步骤1: 删除已存在的容器 =====") + run_command(f"sudo docker rm -f {container_name}", check=False) + + # 2. 启动新容器 + print("\n===== 步骤2: 启动新容器 =====") + run_command( + f"sudo docker run -itd --name {container_name} " + f"-v {script_dir}:/app -p 20005:80 golang:1.25.0-alpine3.22" + ) + + # 3-5. 进入容器、进入app目录并执行go mod tidy(使用sh而非bash) + print("\n===== 步骤3-5: 容器内操作 =====") + exec_commands = ( + "cd /app && " # 进入app目录 + "echo '当前目录内容:' && ls -la && " # 新增目录检查 + "go version && " # 检查Go版本 + "go mod init user_delete && " # 执行依赖整理 + "go mod tidy && " # 执行依赖整理 + "exit" # 退出容器 + ) + + # 使用sh代替bash执行命令 + run_command( + f"sudo docker exec -it {container_name} sh -c '{exec_commands}'" + ) + + # 7. 删除容器 + print("\n===== 步骤7: 删除容器 =====") + run_command(f"sudo docker rm -f {container_name}") + + # 8. 删除虚悬镜像 + print("\n===== 步骤8: 清理虚悬镜像 =====") + run_command("sudo docker images -f 'dangling=true' -q | xargs -r sudo docker rmi") + + print("\n===== 所有操作完成 =====") + + except Exception as e: + print(f"\n操作失败: {str(e)}") + exit(1) + +if __name__ == "__main__": + main() diff --git a/api_delete/docker-compose.delete.yaml b/api_delete/docker-compose.delete.yaml new file mode 100644 index 0000000..e5c0722 --- /dev/null +++ b/api_delete/docker-compose.delete.yaml @@ -0,0 +1,16 @@ +services: + user_delete: + image: user-delete-api:1.0.0 + container_name: api_user_delete + restart: always + depends_on: + - postgres # 依赖数据库服务 + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} # 引用.env变量 + DB_USER: ${DB_USER} # 引用.env变量 + DB_PASSWORD: ${DB_PASSWORD} # 引用.env变量 + DB_NAME: ${DB_NAME} + TZ: ${TZ} # 引用.env变量 \ No newline at end of file diff --git a/api_delete/dockerfile b/api_delete/dockerfile new file mode 100644 index 0000000..9c811fc --- /dev/null +++ b/api_delete/dockerfile @@ -0,0 +1,38 @@ +# ==================== 第一阶段:构建Go程序(构建阶段)==================== +# 使用官方Go镜像作为构建基础,选择与项目匹配的Go版本(示例用1.25.0,可根据实际调整) +FROM golang:1.25.0-alpine3.22 AS builder + +# 设置工作目录(容器内的目录,规范文件位置) +WORKDIR /app + +# 复制go.mod和go.sum(先复制依赖文件,利用Docker缓存机制,避免每次代码变动都重新下载依赖) +COPY go.mod go.sum ./ + +# 下载项目依赖(仅当go.mod/go.sum变动时才会重新执行) +RUN go mod download + +# 复制整个项目代码到工作目录 +COPY . . + +# 构建Go程序: +# - CGO_ENABLED=0:禁用CGO,生成静态链接的二进制文件(避免依赖系统库,保证镜像兼容性) +# - -o app:指定输出二进制文件名为app +# - ./main.go:指定入口文件 +RUN CGO_ENABLED=0 GOOS=linux go build -o app ./main.go + + +# ==================== 第二阶段:运行程序(运行阶段)==================== +# 使用轻量级的alpine镜像(仅5MB左右,大幅减小最终镜像体积) +FROM alpine:3.19 + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制编译好的二进制文件到当前镜像(仅复制最终产物,减小体积) +COPY --from=builder /app/app ./ + +# 暴露程序运行端口(与代码中一致) +EXPOSE 8080 + +# 容器启动时执行的命令:运行二进制文件 +CMD ["./app"] \ No newline at end of file diff --git a/api_delete/go.mod b/api_delete/go.mod new file mode 100644 index 0000000..93a1daf --- /dev/null +++ b/api_delete/go.mod @@ -0,0 +1,43 @@ +module user_delete + +go 1.25.0 + +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/lib/pq v1.10.9 +) + +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/api_delete/main.go b/api_delete/main.go new file mode 100644 index 0000000..5f19355 --- /dev/null +++ b/api_delete/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" +) + +// DeleteAccountRequest 删除账号请求参数 +type DeleteAccountRequest struct { + UserID string `json:"user_id" binding:"required"` +} + +// DeleteAccountResponse 删除账号响应结构 +type DeleteAccountResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +var db *sql.DB + +func main() { + log.Println("开始初始化应用程序") + + // 初始化Gin引擎 + r := gin.Default() + log.Println("Gin引擎初始化完成") + + // 从环境变量获取数据库配置 + dbHost := os.Getenv("DB_HOST") + dbPort := os.Getenv("DB_PORT") + dbUser := os.Getenv("DB_USER") + dbPassword := os.Getenv("DB_PASSWORD") + dbName := os.Getenv("DB_NAME") + log.Printf("加载数据库配置: host=%s, port=%s, user=%s, dbname=%s", + dbHost, dbPort, dbUser, dbName) + + // 构建数据库连接字符串 + connStr := fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPassword, dbName, + ) + log.Println("数据库连接字符串构建完成") + + var err error + db, err = sql.Open("postgres", connStr) + if err != nil { + log.Panicf("无法连接数据库: %v", err) + } + defer func() { + log.Println("关闭数据库连接") + if err := db.Close(); err != nil { + log.Printf("关闭数据库连接出错: %v", err) + } + }() + log.Println("数据库连接对象创建成功") + + // 验证数据库连接 + if err := db.Ping(); err != nil { + log.Panicf("数据库连接失败: %v", err) + } + log.Println("数据库连接验证成功") + + // 注册删除账号接口 + r.POST("/user/delete/account", deleteAccountHandler) + log.Println("已注册删除账号接口: POST /user/delete/account") + + // 启动服务,监听80端口 + log.Println("服务启动在80端口") + if err := r.Run(":80"); err != nil { + log.Panicf("服务启动失败: %v", err) + } +} + +// deleteAccountHandler 处理账号删除逻辑(逻辑删除) +func deleteAccountHandler(c *gin.Context) { + reqID := c.Request.Header.Get("X-Request-ID") + if reqID == "" { + reqID = fmt.Sprintf("req-%d", gin.Mode()) + } + log.Printf("[%s] 收到删除账号请求,客户端IP: %s", reqID, c.ClientIP()) + + var req DeleteAccountRequest + // 绑定并验证请求参数 + if err := c.ShouldBindJSON(&req); err != nil { + log.Printf("[%s] 请求参数绑定失败: %v", reqID, err) + c.JSON(http.StatusBadRequest, DeleteAccountResponse{ + Success: false, + Message: "请求参数错误: " + err.Error(), + }) + return + } + log.Printf("[%s] 请求参数绑定成功,用户ID: %s", reqID, req.UserID) + + // 更新用户表中的deleted字段为true + updateQuery := ` + UPDATE "user" + SET deleted = true + WHERE id = $1 AND deleted = false + ` + log.Printf("[%s] 执行更新SQL: %s,参数: %s", reqID, updateQuery, req.UserID) + + result, err := db.Exec(updateQuery, req.UserID) + if err != nil { + log.Printf("[%s] 执行更新操作失败: %v", reqID, err) + c.JSON(http.StatusInternalServerError, DeleteAccountResponse{ + Success: false, + Message: "删除账号失败: " + err.Error(), + }) + return + } + log.Printf("[%s] 更新操作执行完成", reqID) + + // 检查是否有记录被更新 + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Printf("[%s] 获取影响行数失败: %v", reqID, err) + c.JSON(http.StatusInternalServerError, DeleteAccountResponse{ + Success: false, + Message: "检查删除结果失败: " + err.Error(), + }) + return + } + log.Printf("[%s] 更新操作影响行数: %d", reqID, rowsAffected) + + if rowsAffected == 0 { + log.Printf("[%s] 未找到可删除的用户记录,用户ID: %s", reqID, req.UserID) + c.JSON(http.StatusOK, DeleteAccountResponse{ + Success: false, + Message: "用户不存在或已被删除", + }) + return + } + + // 删除成功 + log.Printf("[%s] 账号删除成功,用户ID: %s", reqID, req.UserID) + c.JSON(http.StatusOK, DeleteAccountResponse{ + Success: true, + Message: "账号删除成功", + }) +} \ No newline at end of file diff --git a/api_delete/release.sh b/api_delete/release.sh new file mode 100755 index 0000000..dae3024 --- /dev/null +++ b/api_delete/release.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e # 当任何命令失败时立即退出脚本 + +# 定义镜像名称和标签 +IMAGE_NAME="user-delete-api" +IMAGE_TAG="1.0.0" +FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" + +echo "开始删除现有镜像 ${FULL_IMAGE}..." +if sudo docker rmi -f "${FULL_IMAGE}" >/dev/null 2>&1; then + echo "镜像 ${FULL_IMAGE} 删除成功" +else + echo "镜像 ${FULL_IMAGE} 不存在,跳过删除步骤" +fi + +echo "开始构建新镜像 ${FULL_IMAGE}..." +if sudo docker build -t "${FULL_IMAGE}" .; then + echo "镜像 ${FULL_IMAGE} 构建成功!" +else + echo "错误:镜像构建失败" >&2 + exit 1 +fi \ No newline at end of file diff --git a/api_gateway/README.md b/api_gateway/README.md new file mode 100644 index 0000000..eb16278 --- /dev/null +++ b/api_gateway/README.md @@ -0,0 +1,7 @@ +sudo docker rm -f user_gateway_api && sudo docker run -itd --name user_gateway_api -v ./:/app -p 20000:80 golang:1.25.0-alpine3.22 + +---- + +# 格式:docker build -t 镜像名:标签 . +sudo docker rmi -f user-gateway-api:1.0.0 +sudo docker build -t user-gateway-api:1.0.0 . \ No newline at end of file diff --git a/api_gateway/depend.py b/api_gateway/depend.py new file mode 100644 index 0000000..dab9f84 --- /dev/null +++ b/api_gateway/depend.py @@ -0,0 +1,69 @@ +import subprocess +import os + +def run_command(command, check=True, shell=True): + """执行shell命令并返回结果,包含错误处理""" + try: + print(f"执行命令: {command}") + result = subprocess.run( + command, + shell=shell, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + print(f"命令输出: {result.stdout}") + return result + except subprocess.CalledProcessError as e: + print(f"命令执行失败: {e.stderr}") + raise + +def main(): + container_name = "user_gateway_api" + script_dir = os.path.dirname(os.path.abspath(__file__)) # 获取当前脚本所在目录 + + try: + # 1. 删除已存在的容器 + print("===== 步骤1: 删除已存在的容器 =====") + run_command(f"sudo docker rm -f {container_name}", check=False) + + # 2. 启动新容器 + print("\n===== 步骤2: 启动新容器 =====") + run_command( + f"sudo docker run -itd --name {container_name} " + f"-v {script_dir}:/app -p 20000:80 golang:1.25.0-alpine3.22" + ) + + # 3-5. 进入容器、进入app目录并执行go mod tidy(使用sh而非bash) + print("\n===== 步骤3-5: 容器内操作 =====") + exec_commands = ( + "cd /app && " # 进入app目录 + "echo '当前目录内容:' && ls -la && " # 新增目录检查 + "go version && " # 检查Go版本 + "go mod init user_gateway && " # 执行依赖整理 + "go mod tidy && " # 执行依赖整理 + "exit" # 退出容器 + ) + + # 使用sh代替bash执行命令 + run_command( + f"sudo docker exec -it {container_name} sh -c '{exec_commands}'" + ) + + # 7. 删除容器 + print("\n===== 步骤7: 删除容器 =====") + run_command(f"sudo docker rm -f {container_name}") + + # 8. 删除虚悬镜像 + print("\n===== 步骤8: 清理虚悬镜像 =====") + run_command("sudo docker images -f 'dangling=true' -q | xargs -r sudo docker rmi") + + print("\n===== 所有操作完成 =====") + + except Exception as e: + print(f"\n操作失败: {str(e)}") + exit(1) + +if __name__ == "__main__": + main() diff --git a/api_gateway/docker-compose.gateway.yaml b/api_gateway/docker-compose.gateway.yaml new file mode 100644 index 0000000..a9c5a64 --- /dev/null +++ b/api_gateway/docker-compose.gateway.yaml @@ -0,0 +1,11 @@ +services: + user_gateway: + image: user-gateway-api:1.0.0 + container_name: api_user_gateway + restart: always + ports: + - "20000:80" + networks: + - user-network + environment: + GATEWAY_PORT: ${GATEWAY_PORT} # 引用.env变量 \ No newline at end of file diff --git a/api_gateway/dockerfile b/api_gateway/dockerfile new file mode 100644 index 0000000..9c811fc --- /dev/null +++ b/api_gateway/dockerfile @@ -0,0 +1,38 @@ +# ==================== 第一阶段:构建Go程序(构建阶段)==================== +# 使用官方Go镜像作为构建基础,选择与项目匹配的Go版本(示例用1.25.0,可根据实际调整) +FROM golang:1.25.0-alpine3.22 AS builder + +# 设置工作目录(容器内的目录,规范文件位置) +WORKDIR /app + +# 复制go.mod和go.sum(先复制依赖文件,利用Docker缓存机制,避免每次代码变动都重新下载依赖) +COPY go.mod go.sum ./ + +# 下载项目依赖(仅当go.mod/go.sum变动时才会重新执行) +RUN go mod download + +# 复制整个项目代码到工作目录 +COPY . . + +# 构建Go程序: +# - CGO_ENABLED=0:禁用CGO,生成静态链接的二进制文件(避免依赖系统库,保证镜像兼容性) +# - -o app:指定输出二进制文件名为app +# - ./main.go:指定入口文件 +RUN CGO_ENABLED=0 GOOS=linux go build -o app ./main.go + + +# ==================== 第二阶段:运行程序(运行阶段)==================== +# 使用轻量级的alpine镜像(仅5MB左右,大幅减小最终镜像体积) +FROM alpine:3.19 + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制编译好的二进制文件到当前镜像(仅复制最终产物,减小体积) +COPY --from=builder /app/app ./ + +# 暴露程序运行端口(与代码中一致) +EXPOSE 8080 + +# 容器启动时执行的命令:运行二进制文件 +CMD ["./app"] \ No newline at end of file diff --git a/api_gateway/go.mod b/api_gateway/go.mod new file mode 100644 index 0000000..b2d718b --- /dev/null +++ b/api_gateway/go.mod @@ -0,0 +1,42 @@ +module user_gateway + +go 1.25.0 + +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 +) + +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/api_gateway/main.go b/api_gateway/main.go new file mode 100644 index 0000000..0a19d17 --- /dev/null +++ b/api_gateway/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +var serviceMap = map[string]string{ + "/user/login": "http://user_login:80", + "/user/register": "http://user_register:80", + "/user/delete": "http://user_delete:80", + "/user/update/account": "http://user_update_account:80", + "/user/update/password": "http://user_update_password:80", +} + +// 日志中间件(记录网关接收的请求) +func loggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + startTime := time.Now() + c.Next() + endTime := time.Now() + + log.Printf( + "[网关请求] %s %s | 状态码: %d | 耗时: %s | 客户端IP: %s", + c.Request.Method, + c.Request.URL.Path, + c.Writer.Status(), + endTime.Sub(startTime), + c.ClientIP(), + ) + } +} + +// 反向代理处理(添加详细转发日志) +func reverseProxy(target string) gin.HandlerFunc { + return func(c *gin.Context) { + // 解析目标服务地址 + targetURL, err := url.Parse(target) + if err != nil { + log.Printf("[代理错误] 目标服务地址解析失败: %s, 错误: %v", target, err) + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "服务配置错误", + }) + return + } + + // 创建反向代理实例 + proxy := httputil.NewSingleHostReverseProxy(targetURL) + + // 记录转发前的请求信息 + reqStart := time.Now() + log.Printf( + "[开始转发] 方法: %s | 原始路径: %s | 目标服务: %s | 客户端IP: %s", + c.Request.Method, + c.Request.URL.Path, + targetURL.String(), + c.ClientIP(), + ) + + // 自定义请求转发逻辑(并记录请求头) + originalDirector := proxy.Director + proxy.Director = func(req *http.Request) { + originalDirector(req) + // 传递关键头信息 + req.Header.Set("Origin", c.Request.Header.Get("Origin")) + req.Host = targetURL.Host + req.Header.Set("X-Forwarded-By", "user-gateway") + + // 打印转发的请求头(调试用,生产环境可注释) + log.Printf("[转发请求头] 目标服务: %s | Origin: %s | Host: %s", + targetURL.Host, + req.Header.Get("Origin"), + req.Host, + ) + } + + // 记录响应信息 + proxy.ModifyResponse = func(resp *http.Response) error { + // 计算转发耗时 + proxyDuration := time.Since(reqStart) + + // 打印响应状态 + log.Printf( + "[转发响应] 目标服务: %s | 状态码: %d | 耗时: %s | 响应头Origin: %s", + targetURL.Host, + resp.StatusCode, + proxyDuration, + resp.Header.Get("Access-Control-Allow-Origin"), + ) + return nil + } + + // 执行代理转发 + proxy.ServeHTTP(c.Writer, c.Request) + } +} + +func main() { + r := gin.Default() + + // 注册日志中间件 + r.Use(loggerMiddleware()) + + // 跨域配置 + r.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + + // 注册路由 + for path, target := range serviceMap { + r.Any(path, reverseProxy(target)) + r.Any(path+"/*any", reverseProxy(target)) + } + + // 启动服务 + port := os.Getenv("GATEWAY_PORT") + if port == "" { + port = "80" + } + log.Printf("网关服务启动在 %s 端口", port) + if err := r.Run(":" + port); err != nil { + log.Fatalf("服务启动失败: %v", err) + } +} \ No newline at end of file diff --git a/api_gateway/release.sh b/api_gateway/release.sh new file mode 100755 index 0000000..418ec0e --- /dev/null +++ b/api_gateway/release.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e # 当任何命令失败时立即退出脚本 + +# 定义镜像名称和标签 +IMAGE_NAME="user-gateway-api" +IMAGE_TAG="1.0.0" +FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" + +echo "开始删除现有镜像 ${FULL_IMAGE}..." +if sudo docker rmi -f "${FULL_IMAGE}" >/dev/null 2>&1; then + echo "镜像 ${FULL_IMAGE} 删除成功" +else + echo "镜像 ${FULL_IMAGE} 不存在,跳过删除步骤" +fi + +echo "开始构建新镜像 ${FULL_IMAGE}..." +if sudo docker build -t "${FULL_IMAGE}" .; then + echo "镜像 ${FULL_IMAGE} 构建成功!" +else + echo "错误:镜像构建失败" >&2 + exit 1 +fi \ No newline at end of file diff --git a/api_login/README.md b/api_login/README.md new file mode 100644 index 0000000..8fc8876 --- /dev/null +++ b/api_login/README.md @@ -0,0 +1,7 @@ +sudo docker rm -f user_login_api && sudo docker run -itd --name user_login_api -v ./:/app -p 20001:80 golang:1.25.0-alpine3.22 + +---- + +# 格式:docker build -t 镜像名:标签 . +sudo docker rmi -f user-login-api:1.0.0 +sudo docker build -t user-login-api:1.0.0 . \ No newline at end of file diff --git a/api_login/depend.py b/api_login/depend.py new file mode 100644 index 0000000..62d123a --- /dev/null +++ b/api_login/depend.py @@ -0,0 +1,69 @@ +import subprocess +import os + +def run_command(command, check=True, shell=True): + """执行shell命令并返回结果,包含错误处理""" + try: + print(f"执行命令: {command}") + result = subprocess.run( + command, + shell=shell, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + print(f"命令输出: {result.stdout}") + return result + except subprocess.CalledProcessError as e: + print(f"命令执行失败: {e.stderr}") + raise + +def main(): + container_name = "user_login_api" + script_dir = os.path.dirname(os.path.abspath(__file__)) # 获取当前脚本所在目录 + + try: + # 1. 删除已存在的容器 + print("===== 步骤1: 删除已存在的容器 =====") + run_command(f"sudo docker rm -f {container_name}", check=False) + + # 2. 启动新容器 + print("\n===== 步骤2: 启动新容器 =====") + run_command( + f"sudo docker run -itd --name {container_name} " + f"-v {script_dir}:/app -p 20001:80 golang:1.25.0-alpine3.22" + ) + + # 3-5. 进入容器、进入app目录并执行go mod tidy(使用sh而非bash) + print("\n===== 步骤3-5: 容器内操作 =====") + exec_commands = ( + "cd /app && " # 进入app目录 + "echo '当前目录内容:' && ls -la && " # 新增目录检查 + "go version && " # 检查Go版本 + "go mod init user_login && " # 执行依赖整理 + "go mod tidy && " # 执行依赖整理 + "exit" # 退出容器 + ) + + # 使用sh代替bash执行命令 + run_command( + f"sudo docker exec -it {container_name} sh -c '{exec_commands}'" + ) + + # 7. 删除容器 + print("\n===== 步骤7: 删除容器 =====") + run_command(f"sudo docker rm -f {container_name}") + + # 8. 删除虚悬镜像 + print("\n===== 步骤8: 清理虚悬镜像 =====") + run_command("sudo docker images -f 'dangling=true' -q | xargs -r sudo docker rmi") + + print("\n===== 所有操作完成 =====") + + except Exception as e: + print(f"\n操作失败: {str(e)}") + exit(1) + +if __name__ == "__main__": + main() diff --git a/api_login/docker-compose.login.yaml b/api_login/docker-compose.login.yaml new file mode 100644 index 0000000..690f81c --- /dev/null +++ b/api_login/docker-compose.login.yaml @@ -0,0 +1,16 @@ +services: + user_login: + image: user-login-api:1.0.0 + container_name: api_user_login + restart: always + depends_on: + - postgres # 依赖数据库服务 + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} # 引用.env变量 + DB_USER: ${DB_USER} # 引用.env变量 + DB_PASSWORD: ${DB_PASSWORD} # 引用.env变量 + DB_NAME: ${DB_NAME} + TZ: ${TZ} # 引用.env变量 \ No newline at end of file diff --git a/api_login/dockerfile b/api_login/dockerfile new file mode 100644 index 0000000..9c811fc --- /dev/null +++ b/api_login/dockerfile @@ -0,0 +1,38 @@ +# ==================== 第一阶段:构建Go程序(构建阶段)==================== +# 使用官方Go镜像作为构建基础,选择与项目匹配的Go版本(示例用1.25.0,可根据实际调整) +FROM golang:1.25.0-alpine3.22 AS builder + +# 设置工作目录(容器内的目录,规范文件位置) +WORKDIR /app + +# 复制go.mod和go.sum(先复制依赖文件,利用Docker缓存机制,避免每次代码变动都重新下载依赖) +COPY go.mod go.sum ./ + +# 下载项目依赖(仅当go.mod/go.sum变动时才会重新执行) +RUN go mod download + +# 复制整个项目代码到工作目录 +COPY . . + +# 构建Go程序: +# - CGO_ENABLED=0:禁用CGO,生成静态链接的二进制文件(避免依赖系统库,保证镜像兼容性) +# - -o app:指定输出二进制文件名为app +# - ./main.go:指定入口文件 +RUN CGO_ENABLED=0 GOOS=linux go build -o app ./main.go + + +# ==================== 第二阶段:运行程序(运行阶段)==================== +# 使用轻量级的alpine镜像(仅5MB左右,大幅减小最终镜像体积) +FROM alpine:3.19 + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制编译好的二进制文件到当前镜像(仅复制最终产物,减小体积) +COPY --from=builder /app/app ./ + +# 暴露程序运行端口(与代码中一致) +EXPOSE 8080 + +# 容器启动时执行的命令:运行二进制文件 +CMD ["./app"] \ No newline at end of file diff --git a/api_login/go.mod b/api_login/go.mod new file mode 100644 index 0000000..29be2a6 --- /dev/null +++ b/api_login/go.mod @@ -0,0 +1,43 @@ +module user_login + +go 1.25.0 + +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/lib/pq v1.10.9 +) + +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/api_login/main.go b/api_login/main.go new file mode 100644 index 0000000..3e469fb --- /dev/null +++ b/api_login/main.go @@ -0,0 +1,162 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" + "golang.org/x/crypto/bcrypt" +) + +// LoginRequest 登录请求参数结构 +type LoginRequest struct { + Account string `json:"account" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// LoginResponse 登录响应结构 +type LoginResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data struct { + UserID string `json:"user_id,omitempty"` + } `json:"data,omitempty"` +} + +var db *sql.DB + +func main() { + log.Println("开始初始化应用程序") + + // 初始化Gin引擎 + r := gin.Default() + log.Println("Gin引擎初始化完成") + + // 从环境变量获取数据库配置 + dbHost := os.Getenv("DB_HOST") + dbPort := os.Getenv("DB_PORT") + dbUser := os.Getenv("DB_USER") + dbPassword := os.Getenv("DB_PASSWORD") + dbName := os.Getenv("DB_NAME") + log.Printf("获取数据库配置: host=%s, port=%s, user=%s, dbname=%s", dbHost, dbPort, dbUser, dbName) + + // 构建数据库连接字符串 + connStr := fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPassword, dbName, + ) + log.Printf("数据库连接字符串构建完成: %s", connStr) + + var err error + db, err = sql.Open("postgres", connStr) + if err != nil { + log.Fatalf("无法连接数据库: %v", err) + panic(fmt.Sprintf("无法连接数据库: %v", err)) + } + defer db.Close() + log.Println("数据库连接对象创建成功") + + // 验证数据库连接 + if err := db.Ping(); err != nil { + log.Fatalf("数据库连接失败: %v", err) + panic(fmt.Sprintf("数据库连接失败: %v", err)) + } + log.Println("数据库连接验证成功") + + // 登录接口 + r.POST("/user/login", loginHandler) + log.Println("登录接口注册完成: POST /user/login") + + // 启动服务,监听80端口 + log.Println("服务启动在80端口") + r.Run(":80") +} + +// loginHandler 处理登录逻辑 +func loginHandler(c *gin.Context) { + reqID := c.Request.Header.Get("X-Request-ID") + if reqID == "" { + reqID = fmt.Sprintf("%d", gin.Mode()) // 简单生成一个请求标识 + } + log.Printf("[%s] 收到登录请求 from %s", reqID, c.ClientIP()) + + var req LoginRequest + // 绑定并验证请求参数 + if err := c.ShouldBindJSON(&req); err != nil { + log.Printf("[%s] 请求参数绑定失败: %v, 请求体: %+v", reqID, err, c.Request.Body) + c.JSON(http.StatusBadRequest, LoginResponse{ + Success: false, + Message: "账号或密码不能为空", + }) + return + } + log.Printf("[%s] 请求参数绑定成功: 账号=%s", reqID, req.Account) + + // 1. 判断账号和密码是否为空(双重保险) + if req.Account == "" || req.Password == "" { + log.Printf("[%s] 账号或密码为空: 账号=%s", reqID, req.Account) + c.JSON(http.StatusBadRequest, LoginResponse{ + Success: false, + Message: "账号或密码不能为空", + }) + return + } + + // 2. 查询账号对应的密码和用户ID + var storedPassword string + var userID string + query := ` + SELECT password, user_id + FROM user_account_password_view + WHERE account = $1 AND deleted = false + ` + log.Printf("[%s] 执行查询: %s, 参数: %s", reqID, query, req.Account) + err := db.QueryRow(query, req.Account).Scan(&storedPassword, &userID) + switch { + case err == sql.ErrNoRows: + // 账号不存在或已被删除 + log.Printf("[%s] 账号不存在或已删除: %s", reqID, req.Account) + c.JSON(http.StatusOK, LoginResponse{ + Success: false, + Message: "账号不存在", + }) + return + case err != nil: + log.Printf("[%s] 查询账号信息失败: %v, 账号=%s", reqID, err, req.Account) + c.JSON(http.StatusInternalServerError, LoginResponse{ + Success: false, + Message: "查询账号信息失败", + }) + return + } + log.Printf("[%s] 查询账号信息成功: 账号=%s, userID=%s", reqID, req.Account, userID) + + // 3. 验证密码(使用bcrypt比较原始密码和存储的加密密码) + err = bcrypt.CompareHashAndPassword([]byte(storedPassword), []byte(req.Password)) + if err != nil { + // 密码不匹配 + log.Printf("[%s] 密码验证失败: 账号=%s, 错误=%v", reqID, req.Account, err) + c.JSON(http.StatusOK, LoginResponse{ + Success: false, + Message: "密码错误", + }) + return + } + log.Printf("[%s] 密码验证成功: 账号=%s", reqID, req.Account) + + // 4. 登录成功 + log.Printf("[%s] 登录成功: 账号=%s, userID=%s", reqID, req.Account, userID) + c.JSON(http.StatusOK, LoginResponse{ + Success: true, + Message: "登录成功", + Data: struct { + UserID string `json:"user_id,omitempty"` + }{ + UserID: userID, + }, + }) +} \ No newline at end of file diff --git a/api_login/release.sh b/api_login/release.sh new file mode 100755 index 0000000..b38aecf --- /dev/null +++ b/api_login/release.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e # 当任何命令失败时立即退出脚本 + +# 定义镜像名称和标签 +IMAGE_NAME="user-login-api" +IMAGE_TAG="1.0.0" +FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" + +echo "开始删除现有镜像 ${FULL_IMAGE}..." +if sudo docker rmi -f "${FULL_IMAGE}" >/dev/null 2>&1; then + echo "镜像 ${FULL_IMAGE} 删除成功" +else + echo "镜像 ${FULL_IMAGE} 不存在,跳过删除步骤" +fi + +echo "开始构建新镜像 ${FULL_IMAGE}..." +if sudo docker build -t "${FULL_IMAGE}" .; then + echo "镜像 ${FULL_IMAGE} 构建成功!" +else + echo "错误:镜像构建失败" >&2 + exit 1 +fi \ No newline at end of file diff --git a/api_register/README.md b/api_register/README.md new file mode 100644 index 0000000..7f009bf --- /dev/null +++ b/api_register/README.md @@ -0,0 +1,7 @@ +sudo docker rm -f user_register_api && sudo docker run -itd --name user_register_api -v ./:/app -p 20002:80 golang:1.25.0-alpine3.22 + +---- + +# 格式:docker build -t 镜像名:标签 . +sudo docker rmi -f user-register-api:1.0.0 +sudo docker build -t user-register-api:1.0.0 . \ No newline at end of file diff --git a/api_register/depend.py b/api_register/depend.py new file mode 100644 index 0000000..7e793aa --- /dev/null +++ b/api_register/depend.py @@ -0,0 +1,69 @@ +import subprocess +import os + +def run_command(command, check=True, shell=True): + """执行shell命令并返回结果,包含错误处理""" + try: + print(f"执行命令: {command}") + result = subprocess.run( + command, + shell=shell, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + print(f"命令输出: {result.stdout}") + return result + except subprocess.CalledProcessError as e: + print(f"命令执行失败: {e.stderr}") + raise + +def main(): + container_name = "user_register_api" + script_dir = os.path.dirname(os.path.abspath(__file__)) # 获取当前脚本所在目录 + + try: + # 1. 删除已存在的容器 + print("===== 步骤1: 删除已存在的容器 =====") + run_command(f"sudo docker rm -f {container_name}", check=False) + + # 2. 启动新容器 + print("\n===== 步骤2: 启动新容器 =====") + run_command( + f"sudo docker run -itd --name {container_name} " + f"-v {script_dir}:/app -p 20002:80 golang:1.25.0-alpine3.22" + ) + + # 3-5. 进入容器、进入app目录并执行go mod tidy(使用sh而非bash) + print("\n===== 步骤3-5: 容器内操作 =====") + exec_commands = ( + "cd /app && " # 进入app目录 + "echo '当前目录内容:' && ls -la && " # 新增目录检查 + "go version && " # 检查Go版本 + "go mod init user_register && " # 执行依赖整理 + "go mod tidy && " # 执行依赖整理 + "exit" # 退出容器 + ) + + # 使用sh代替bash执行命令 + run_command( + f"sudo docker exec -it {container_name} sh -c '{exec_commands}'" + ) + + # 7. 删除容器 + print("\n===== 步骤7: 删除容器 =====") + run_command(f"sudo docker rm -f {container_name}") + + # 8. 删除虚悬镜像 + print("\n===== 步骤8: 清理虚悬镜像 =====") + run_command("sudo docker images -f 'dangling=true' -q | xargs -r sudo docker rmi") + + print("\n===== 所有操作完成 =====") + + except Exception as e: + print(f"\n操作失败: {str(e)}") + exit(1) + +if __name__ == "__main__": + main() diff --git a/api_register/docker-compose.register.yaml b/api_register/docker-compose.register.yaml new file mode 100644 index 0000000..712b371 --- /dev/null +++ b/api_register/docker-compose.register.yaml @@ -0,0 +1,16 @@ +services: + user_register: + image: user-register-api:1.0.0 + container_name: api_user_register + restart: always + depends_on: + - postgres # 依赖数据库服务 + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} # 引用.env变量 + DB_USER: ${DB_USER} # 引用.env变量 + DB_PASSWORD: ${DB_PASSWORD} # 引用.env变量 + DB_NAME: ${DB_NAME} + TZ: ${TZ} # 引用.env变量 \ No newline at end of file diff --git a/api_register/dockerfile b/api_register/dockerfile new file mode 100644 index 0000000..9c811fc --- /dev/null +++ b/api_register/dockerfile @@ -0,0 +1,38 @@ +# ==================== 第一阶段:构建Go程序(构建阶段)==================== +# 使用官方Go镜像作为构建基础,选择与项目匹配的Go版本(示例用1.25.0,可根据实际调整) +FROM golang:1.25.0-alpine3.22 AS builder + +# 设置工作目录(容器内的目录,规范文件位置) +WORKDIR /app + +# 复制go.mod和go.sum(先复制依赖文件,利用Docker缓存机制,避免每次代码变动都重新下载依赖) +COPY go.mod go.sum ./ + +# 下载项目依赖(仅当go.mod/go.sum变动时才会重新执行) +RUN go mod download + +# 复制整个项目代码到工作目录 +COPY . . + +# 构建Go程序: +# - CGO_ENABLED=0:禁用CGO,生成静态链接的二进制文件(避免依赖系统库,保证镜像兼容性) +# - -o app:指定输出二进制文件名为app +# - ./main.go:指定入口文件 +RUN CGO_ENABLED=0 GOOS=linux go build -o app ./main.go + + +# ==================== 第二阶段:运行程序(运行阶段)==================== +# 使用轻量级的alpine镜像(仅5MB左右,大幅减小最终镜像体积) +FROM alpine:3.19 + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制编译好的二进制文件到当前镜像(仅复制最终产物,减小体积) +COPY --from=builder /app/app ./ + +# 暴露程序运行端口(与代码中一致) +EXPOSE 8080 + +# 容器启动时执行的命令:运行二进制文件 +CMD ["./app"] \ No newline at end of file diff --git a/api_register/go.mod b/api_register/go.mod new file mode 100644 index 0000000..138f564 --- /dev/null +++ b/api_register/go.mod @@ -0,0 +1,43 @@ +module user_register + +go 1.25.0 + +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/lib/pq v1.10.9 + golang.org/x/crypto v0.42.0 +) + +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.36.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/api_register/main.go b/api_register/main.go new file mode 100644 index 0000000..8438e06 --- /dev/null +++ b/api_register/main.go @@ -0,0 +1,272 @@ +package main + +import ( + "database/sql" + "fmt" + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" + "golang.org/x/crypto/bcrypt" +) + +// RegisterRequest 注册请求参数结构 +type RegisterRequest struct { + Account string `json:"account" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// RegisterResponse 注册响应结构 +type RegisterResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data struct { + UserID string `json:"user_id,omitempty"` + Account string `json:"account,omitempty"` + } `json:"data,omitempty"` +} + +var db *sql.DB + +func main() { + // 初始化Gin引擎 + fmt.Println("[主程序] 初始化Gin引擎") + r := gin.Default() + + // 从环境变量获取数据库配置 + fmt.Println("[主程序] 读取数据库配置环境变量") + dbHost := os.Getenv("DB_HOST") + dbPort := os.Getenv("DB_PORT") + dbUser := os.Getenv("DB_USER") + dbPassword := os.Getenv("DB_PASSWORD") + dbName := os.Getenv("DB_NAME") + + // 构建数据库连接字符串 + connStr := fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPassword, dbName, + ) + fmt.Printf("[主程序] 数据库连接字符串构建完成: %s\n", maskPassword(connStr)) + + // 连接数据库 + var err error + db, err = sql.Open("postgres", connStr) + if err != nil { + panic(fmt.Sprintf("[主程序] 无法连接数据库: %v", err)) + } + defer db.Close() + fmt.Println("[主程序] 数据库连接对象创建成功") + + // 验证数据库连接 + if err := db.Ping(); err != nil { + panic(fmt.Sprintf("[主程序] 数据库连接失败: %v", err)) + } + fmt.Println("[主程序] 数据库连接验证成功") + + // 注册账号接口 + r.POST("/user/register", registerHandler) + fmt.Println("[主程序] 注册接口路由已配置") + + // 启动服务,监听80端口 + fmt.Println("[主程序] 服务启动在80端口") + r.Run(":80") +} + +// 屏蔽连接字符串中的密码,避免日志泄露敏感信息 +func maskPassword(connStr string) string { + // 简单处理:替换password=后的内容直到下一个空格 + start := false + result := "" + for _, c := range connStr { + if start && c == ' ' { + start = false + } + if start { + continue + } + result += string(c) + // 先检查长度是否足够,避免索引越界 + if len(result) >= 10 && result[len(result)-10:] == "password=" { + start = true + result += "***" + } + } + return result +} + +// registerHandler 处理用户注册逻辑 +func registerHandler(c *gin.Context) { + startTime := time.Now() + reqID := fmt.Sprintf("req-%d", time.Now().UnixNano()) // 生成请求唯一标识 + fmt.Printf("[%s] 收到注册请求,开始处理\n", reqID) + + var req RegisterRequest + // 绑定并验证请求参数 + if err := c.ShouldBindJSON(&req); err != nil { + fmt.Printf("[%s] 请求参数绑定失败: %v\n", reqID, err) + c.JSON(http.StatusBadRequest, RegisterResponse{ + Success: false, + Message: "请求参数错误: " + err.Error(), + }) + return + } + fmt.Printf("[%s] 请求参数绑定成功,账号: %s\n", reqID, req.Account) + + // 1. 判断接口的账号和密码是否为空 + if req.Account == "" || req.Password == "" { + fmt.Printf("[%s] 账号或密码为空,拒绝注册\n", reqID) + c.JSON(http.StatusBadRequest, RegisterResponse{ + Success: false, + Message: "账号和密码不能为空", + }) + return + } + + // 2. 使用接口账号查询视图,检查账号是否已存在 + var exists bool + query := ` + SELECT EXISTS( + SELECT 1 + FROM user_account_password_view + WHERE account = $1 AND deleted = false + ) + ` + fmt.Printf("[%s] 执行账号存在性查询: %s\n", reqID, query) + err := db.QueryRow(query, req.Account).Scan(&exists) + if err != nil { + fmt.Printf("[%s] 账号查询失败: %v\n", reqID, err) + c.JSON(http.StatusInternalServerError, RegisterResponse{ + Success: false, + Message: "查询账号信息失败: " + err.Error(), + }) + return + } + fmt.Printf("[%s] 账号存在性查询完成,存在: %v\n", reqID, exists) + + // 3. 判断查询结果,若存在则提示账号已存在 + if exists { + fmt.Printf("[%s] 账号已存在: %s\n", reqID, req.Account) + c.JSON(http.StatusOK, RegisterResponse{ + Success: false, + Message: "账号已存在", + }) + return + } + + // 4. 开启数据库事务,确保数据一致性 + tx, err := db.Begin() + if err != nil { + fmt.Printf("[%s] 开启事务失败: %v\n", reqID, err) + c.JSON(http.StatusInternalServerError, RegisterResponse{ + Success: false, + Message: "开启事务失败: " + err.Error(), + }) + return + } + defer func() { + if r := recover(); r != nil { + fmt.Printf("[%s] 发生恐慌,回滚事务: %v\n", reqID, r) + tx.Rollback() + } + }() + fmt.Printf("[%s] 数据库事务开启成功\n", reqID) + + // 5. 在user表生成新用户ID + var userID string + insertUserQuery := ` + INSERT INTO "user" DEFAULT VALUES + RETURNING id + ` + fmt.Printf("[%s] 执行用户创建: %s\n", reqID, insertUserQuery) + err = tx.QueryRow(insertUserQuery).Scan(&userID) + if err != nil { + fmt.Printf("[%s] 创建用户失败: %v\n", reqID, err) + tx.Rollback() + c.JSON(http.StatusInternalServerError, RegisterResponse{ + Success: false, + Message: "创建用户失败: " + err.Error(), + }) + return + } + fmt.Printf("[%s] 用户创建成功,用户ID: %s\n", reqID, userID) + + // 6. 对密码进行加密处理 + fmt.Printf("[%s] 开始密码加密\n", reqID) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + fmt.Printf("[%s] 密码加密失败: %v\n", reqID, err) + tx.Rollback() + c.JSON(http.StatusInternalServerError, RegisterResponse{ + Success: false, + Message: "密码加密失败: " + err.Error(), + }) + return + } + fmt.Printf("[%s] 密码加密成功\n", reqID) + + // 7. 插入user_account表 + insertAccountQuery := ` + INSERT INTO user_account (user_id, account) + VALUES ($1, $2) + ` + fmt.Printf("[%s] 执行账号插入: %s, 参数: user_id=%s, account=%s\n", + reqID, insertAccountQuery, userID, req.Account) + _, err = tx.Exec(insertAccountQuery, userID, req.Account) + if err != nil { + fmt.Printf("[%s] 保存账号信息失败: %v\n", reqID, err) + tx.Rollback() + c.JSON(http.StatusInternalServerError, RegisterResponse{ + Success: false, + Message: "保存账号信息失败: " + err.Error(), + }) + return + } + fmt.Printf("[%s] 账号信息保存成功\n", reqID) + + // 8. 插入user_password表 + insertPasswordQuery := ` + INSERT INTO user_password (user_id, password) + VALUES ($1, $2) + ` + fmt.Printf("[%s] 执行密码插入: %s, 参数: user_id=%s\n", + reqID, insertPasswordQuery, userID) + _, err = tx.Exec(insertPasswordQuery, userID, string(hashedPassword)) + if err != nil { + fmt.Printf("[%s] 保存密码信息失败: %v\n", reqID, err) + tx.Rollback() + c.JSON(http.StatusInternalServerError, RegisterResponse{ + Success: false, + Message: "保存密码信息失败: " + err.Error(), + }) + return + } + fmt.Printf("[%s] 密码信息保存成功\n", reqID) + + // 9. 提交事务 + if err := tx.Commit(); err != nil { + fmt.Printf("[%s] 提交事务失败: %v\n", reqID, err) + tx.Rollback() + c.JSON(http.StatusInternalServerError, RegisterResponse{ + Success: false, + Message: "提交事务失败: " + err.Error(), + }) + return + } + fmt.Printf("[%s] 事务提交成功\n", reqID) + + // 10. 注册成功 + response := RegisterResponse{ + Success: true, + Message: "注册成功", + } + response.Data.UserID = userID + response.Data.Account = req.Account + + duration := time.Since(startTime) + fmt.Printf("[%s] 注册成功,耗时: %v, 用户ID: %s, 账号: %s\n", + reqID, duration, userID, req.Account) + + c.JSON(http.StatusOK, response) +} \ No newline at end of file diff --git a/api_register/release.sh b/api_register/release.sh new file mode 100755 index 0000000..62ea272 --- /dev/null +++ b/api_register/release.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e # 当任何命令失败时立即退出脚本 + +# 定义镜像名称和标签 +IMAGE_NAME="user-register-api" +IMAGE_TAG="1.0.0" +FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" + +echo "开始删除现有镜像 ${FULL_IMAGE}..." +if sudo docker rmi -f "${FULL_IMAGE}" >/dev/null 2>&1; then + echo "镜像 ${FULL_IMAGE} 删除成功" +else + echo "镜像 ${FULL_IMAGE} 不存在,跳过删除步骤" +fi + +echo "开始构建新镜像 ${FULL_IMAGE}..." +if sudo docker build -t "${FULL_IMAGE}" .; then + echo "镜像 ${FULL_IMAGE} 构建成功!" +else + echo "错误:镜像构建失败" >&2 + exit 1 +fi \ No newline at end of file diff --git a/api_template/README.md b/api_template/README.md new file mode 100644 index 0000000..9ca6de6 --- /dev/null +++ b/api_template/README.md @@ -0,0 +1,7 @@ +sudo docker rm -f user_template_api && sudo docker run -itd --name user_template_api -v ./:/app -p 20001:80 golang:1.25.0-alpine3.22 + +---- + +# 格式:docker build -t 镜像名:标签 . +sudo docker rmi -f user-template-api:1.0.0 +sudo docker build -t user-template-api:1.0.0 . \ No newline at end of file diff --git a/api_template/depend.py b/api_template/depend.py new file mode 100644 index 0000000..3d3298d --- /dev/null +++ b/api_template/depend.py @@ -0,0 +1,69 @@ +import subprocess +import os + +def run_command(command, check=True, shell=True): + """执行shell命令并返回结果,包含错误处理""" + try: + print(f"执行命令: {command}") + result = subprocess.run( + command, + shell=shell, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + print(f"命令输出: {result.stdout}") + return result + except subprocess.CalledProcessError as e: + print(f"命令执行失败: {e.stderr}") + raise + +def main(): + container_name = "user_template_api" + script_dir = os.path.dirname(os.path.abspath(__file__)) # 获取当前脚本所在目录 + + try: + # 1. 删除已存在的容器 + print("===== 步骤1: 删除已存在的容器 =====") + run_command(f"sudo docker rm -f {container_name}", check=False) + + # 2. 启动新容器 + print("\n===== 步骤2: 启动新容器 =====") + run_command( + f"sudo docker run -itd --name {container_name} " + f"-v {script_dir}:/app -p 20001:80 golang:1.25.0-alpine3.22" + ) + + # 3-5. 进入容器、进入app目录并执行go mod tidy(使用sh而非bash) + print("\n===== 步骤3-5: 容器内操作 =====") + exec_commands = ( + "cd /app && " # 进入app目录 + "echo '当前目录内容:' && ls -la && " # 新增目录检查 + "go version && " # 检查Go版本 + "go mod init user_template && " # 执行依赖整理 + "go mod tidy && " # 执行依赖整理 + "exit" # 退出容器 + ) + + # 使用sh代替bash执行命令 + run_command( + f"sudo docker exec -it {container_name} sh -c '{exec_commands}'" + ) + + # 7. 删除容器 + print("\n===== 步骤7: 删除容器 =====") + run_command(f"sudo docker rm -f {container_name}") + + # 8. 删除虚悬镜像 + print("\n===== 步骤8: 清理虚悬镜像 =====") + run_command("sudo docker images -f 'dangling=true' -q | xargs -r sudo docker rmi") + + print("\n===== 所有操作完成 =====") + + except Exception as e: + print(f"\n操作失败: {str(e)}") + exit(1) + +if __name__ == "__main__": + main() diff --git a/api_template/docker-compose.temp.yaml b/api_template/docker-compose.temp.yaml new file mode 100644 index 0000000..4f78a53 --- /dev/null +++ b/api_template/docker-compose.temp.yaml @@ -0,0 +1,16 @@ +services: + user_template: + image: user-template-api:1.0.0 + container_name: api_user_template + restart: always + depends_on: + - postgres # 依赖数据库服务 + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} # 引用.env变量 + DB_USER: ${DB_USER} # 引用.env变量 + DB_PASSWORD: ${DB_PASSWORD} # 引用.env变量 + DB_NAME: ${DB_NAME} + TZ: ${TZ} # 引用.env变量 \ No newline at end of file diff --git a/api_template/dockerfile b/api_template/dockerfile new file mode 100644 index 0000000..9c811fc --- /dev/null +++ b/api_template/dockerfile @@ -0,0 +1,38 @@ +# ==================== 第一阶段:构建Go程序(构建阶段)==================== +# 使用官方Go镜像作为构建基础,选择与项目匹配的Go版本(示例用1.25.0,可根据实际调整) +FROM golang:1.25.0-alpine3.22 AS builder + +# 设置工作目录(容器内的目录,规范文件位置) +WORKDIR /app + +# 复制go.mod和go.sum(先复制依赖文件,利用Docker缓存机制,避免每次代码变动都重新下载依赖) +COPY go.mod go.sum ./ + +# 下载项目依赖(仅当go.mod/go.sum变动时才会重新执行) +RUN go mod download + +# 复制整个项目代码到工作目录 +COPY . . + +# 构建Go程序: +# - CGO_ENABLED=0:禁用CGO,生成静态链接的二进制文件(避免依赖系统库,保证镜像兼容性) +# - -o app:指定输出二进制文件名为app +# - ./main.go:指定入口文件 +RUN CGO_ENABLED=0 GOOS=linux go build -o app ./main.go + + +# ==================== 第二阶段:运行程序(运行阶段)==================== +# 使用轻量级的alpine镜像(仅5MB左右,大幅减小最终镜像体积) +FROM alpine:3.19 + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制编译好的二进制文件到当前镜像(仅复制最终产物,减小体积) +COPY --from=builder /app/app ./ + +# 暴露程序运行端口(与代码中一致) +EXPOSE 8080 + +# 容器启动时执行的命令:运行二进制文件 +CMD ["./app"] \ No newline at end of file diff --git a/api_template/release.sh b/api_template/release.sh new file mode 100755 index 0000000..4309c5b --- /dev/null +++ b/api_template/release.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e # 当任何命令失败时立即退出脚本 + +# 定义镜像名称和标签 +IMAGE_NAME="user-template-api" +IMAGE_TAG="1.0.0" +FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" + +echo "开始删除现有镜像 ${FULL_IMAGE}..." +if sudo docker rmi -f "${FULL_IMAGE}" >/dev/null 2>&1; then + echo "镜像 ${FULL_IMAGE} 删除成功" +else + echo "镜像 ${FULL_IMAGE} 不存在,跳过删除步骤" +fi + +echo "开始构建新镜像 ${FULL_IMAGE}..." +if sudo docker build -t "${FULL_IMAGE}" .; then + echo "镜像 ${FULL_IMAGE} 构建成功!" +else + echo "错误:镜像构建失败" >&2 + exit 1 +fi \ No newline at end of file diff --git a/api_update_account/README.md b/api_update_account/README.md new file mode 100644 index 0000000..9fda8c4 --- /dev/null +++ b/api_update_account/README.md @@ -0,0 +1,7 @@ +sudo docker rm -f user_update_account_api && sudo docker run -itd --name user_update_account_api -v ./:/app -p 20003:80 golang:1.25.0-alpine3.22 + +---- + +# 格式:docker build -t 镜像名:标签 . +sudo docker rmi -f user-update-account-api:1.0.0 +sudo docker build -t user-update-account-api:1.0.0 . \ No newline at end of file diff --git a/api_update_account/depend.py b/api_update_account/depend.py new file mode 100644 index 0000000..cc53582 --- /dev/null +++ b/api_update_account/depend.py @@ -0,0 +1,69 @@ +import subprocess +import os + +def run_command(command, check=True, shell=True): + """执行shell命令并返回结果,包含错误处理""" + try: + print(f"执行命令: {command}") + result = subprocess.run( + command, + shell=shell, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + print(f"命令输出: {result.stdout}") + return result + except subprocess.CalledProcessError as e: + print(f"命令执行失败: {e.stderr}") + raise + +def main(): + container_name = "user_update_account_api" + script_dir = os.path.dirname(os.path.abspath(__file__)) # 获取当前脚本所在目录 + + try: + # 1. 删除已存在的容器 + print("===== 步骤1: 删除已存在的容器 =====") + run_command(f"sudo docker rm -f {container_name}", check=False) + + # 2. 启动新容器 + print("\n===== 步骤2: 启动新容器 =====") + run_command( + f"sudo docker run -itd --name {container_name} " + f"-v {script_dir}:/app -p 20003:80 golang:1.25.0-alpine3.22" + ) + + # 3-5. 进入容器、进入app目录并执行go mod tidy(使用sh而非bash) + print("\n===== 步骤3-5: 容器内操作 =====") + exec_commands = ( + "cd /app && " # 进入app目录 + "echo '当前目录内容:' && ls -la && " # 新增目录检查 + "go version && " # 检查Go版本 + "go mod init user_update_account && " # 执行依赖整理 + "go mod tidy && " # 执行依赖整理 + "exit" # 退出容器 + ) + + # 使用sh代替bash执行命令 + run_command( + f"sudo docker exec -it {container_name} sh -c '{exec_commands}'" + ) + + # 7. 删除容器 + print("\n===== 步骤7: 删除容器 =====") + run_command(f"sudo docker rm -f {container_name}") + + # 8. 删除虚悬镜像 + print("\n===== 步骤8: 清理虚悬镜像 =====") + run_command("sudo docker images -f 'dangling=true' -q | xargs -r sudo docker rmi") + + print("\n===== 所有操作完成 =====") + + except Exception as e: + print(f"\n操作失败: {str(e)}") + exit(1) + +if __name__ == "__main__": + main() diff --git a/api_update_account/docker-compose.update.account.yaml b/api_update_account/docker-compose.update.account.yaml new file mode 100644 index 0000000..fd9063f --- /dev/null +++ b/api_update_account/docker-compose.update.account.yaml @@ -0,0 +1,16 @@ +services: + user_update_account: + image: user-update-account-api:1.0.0 + container_name: api_user_update_account + restart: always + depends_on: + - postgres # 依赖数据库服务 + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} # 引用.env变量 + DB_USER: ${DB_USER} # 引用.env变量 + DB_PASSWORD: ${DB_PASSWORD} # 引用.env变量 + DB_NAME: ${DB_NAME} + TZ: ${TZ} # 引用.env变量 \ No newline at end of file diff --git a/api_update_account/dockerfile b/api_update_account/dockerfile new file mode 100644 index 0000000..9c811fc --- /dev/null +++ b/api_update_account/dockerfile @@ -0,0 +1,38 @@ +# ==================== 第一阶段:构建Go程序(构建阶段)==================== +# 使用官方Go镜像作为构建基础,选择与项目匹配的Go版本(示例用1.25.0,可根据实际调整) +FROM golang:1.25.0-alpine3.22 AS builder + +# 设置工作目录(容器内的目录,规范文件位置) +WORKDIR /app + +# 复制go.mod和go.sum(先复制依赖文件,利用Docker缓存机制,避免每次代码变动都重新下载依赖) +COPY go.mod go.sum ./ + +# 下载项目依赖(仅当go.mod/go.sum变动时才会重新执行) +RUN go mod download + +# 复制整个项目代码到工作目录 +COPY . . + +# 构建Go程序: +# - CGO_ENABLED=0:禁用CGO,生成静态链接的二进制文件(避免依赖系统库,保证镜像兼容性) +# - -o app:指定输出二进制文件名为app +# - ./main.go:指定入口文件 +RUN CGO_ENABLED=0 GOOS=linux go build -o app ./main.go + + +# ==================== 第二阶段:运行程序(运行阶段)==================== +# 使用轻量级的alpine镜像(仅5MB左右,大幅减小最终镜像体积) +FROM alpine:3.19 + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制编译好的二进制文件到当前镜像(仅复制最终产物,减小体积) +COPY --from=builder /app/app ./ + +# 暴露程序运行端口(与代码中一致) +EXPOSE 8080 + +# 容器启动时执行的命令:运行二进制文件 +CMD ["./app"] \ No newline at end of file diff --git a/api_update_account/go.mod b/api_update_account/go.mod new file mode 100644 index 0000000..f39b9b7 --- /dev/null +++ b/api_update_account/go.mod @@ -0,0 +1,43 @@ +module user_update_account + +go 1.25.0 + +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/lib/pq v1.10.9 +) + +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/api_update_account/main.go b/api_update_account/main.go new file mode 100644 index 0000000..ae49099 --- /dev/null +++ b/api_update_account/main.go @@ -0,0 +1,204 @@ +package main + +import ( + "database/sql" + "fmt" + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" +) + +// UpdateAccountRequest 更新账号请求参数 +type UpdateAccountRequest struct { + UserID string `json:"user_id" binding:"required"` + Account string `json:"account" binding:"required"` +} + +// UpdateAccountResponse 更新账号响应结构 +type UpdateAccountResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data struct { + UserID string `json:"user_id,omitempty"` + Account string `json:"account,omitempty"` + } `json:"data,omitempty"` +} + +var db *sql.DB + +func main() { + // 记录程序启动时间 + startTime := time.Now() + fmt.Printf("[%s] 程序开始启动\n", startTime.Format(time.RFC3339)) + + // 初始化Gin引擎 + r := gin.Default() + fmt.Println("[INFO] Gin引擎初始化完成") + + // 从环境变量获取数据库配置 + fmt.Println("[INFO] 开始读取数据库配置") + dbHost := os.Getenv("DB_HOST") + dbPort := os.Getenv("DB_PORT") + dbUser := os.Getenv("DB_USER") + dbPassword := os.Getenv("DB_PASSWORD") + dbName := os.Getenv("DB_NAME") + + fmt.Printf("[INFO] 数据库配置: host=%s, port=%s, user=%s, dbname=%s\n", + dbHost, dbPort, dbUser, dbName) + + // 构建数据库连接字符串 + connStr := fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPassword, dbName, + ) + fmt.Println("[INFO] 数据库连接字符串构建完成") + + var err error + db, err = sql.Open("postgres", connStr) + if err != nil { + panic(fmt.Sprintf("[ERROR] 无法连接数据库: %v", err)) + } + defer func() { + // 关闭数据库连接时记录日志 + if err := db.Close(); err != nil { + fmt.Printf("[ERROR] 关闭数据库连接失败: %v\n", err) + } else { + fmt.Println("[INFO] 数据库连接已关闭") + } + }() + + // 验证数据库连接 + fmt.Println("[INFO] 开始验证数据库连接") + if err := db.Ping(); err != nil { + panic(fmt.Sprintf("[ERROR] 数据库连接失败: %v", err)) + } + fmt.Println("[INFO] 数据库连接验证成功") + + // 注册更新账号接口 + r.POST("/user/update/account", updateAccountHandler) + fmt.Println("[INFO] 已注册接口: POST /user/update/account") + + // 启动服务,监听80端口 + startMsg := "服务启动在80端口" + fmt.Printf("[%s] %s\n", time.Now().Format(time.RFC3339), startMsg) + fmt.Printf("[INFO] 启动耗时: %v\n", time.Since(startTime)) + + if err := r.Run(":80"); err != nil { + fmt.Printf("[ERROR] 服务启动失败: %v\n", err) + } +} + +// updateAccountHandler 处理账号更新逻辑 +func updateAccountHandler(c *gin.Context) { + // 记录请求开始时间和请求ID + startTime := time.Now() + reqID := c.Request.Header.Get("X-Request-ID") + if reqID == "" { + reqID = fmt.Sprintf("req-%d", time.Now().UnixNano()) + } + fmt.Printf("[%s] [REQUEST] %s - 收到请求: %s %s, RequestID: %s\n", + startTime.Format(time.RFC3339), + c.ClientIP(), + c.Request.Method, + c.Request.URL.Path, + reqID, + ) + + var req UpdateAccountRequest + // 绑定并验证请求参数 + if err := c.ShouldBindJSON(&req); err != nil { + errMsg := fmt.Sprintf("请求参数错误: %v", err) + fmt.Printf("[%s] [ERROR] %s, RequestID: %s, 错误详情: %v\n", + time.Now().Format(time.RFC3339), + errMsg, + reqID, + err, + ) + c.JSON(http.StatusBadRequest, UpdateAccountResponse{ + Success: false, + Message: errMsg, + }) + return + } + + // 打印接收到的请求参数 + fmt.Printf("[%s] [INFO] 接收到更新请求, RequestID: %s, UserID: %s, 新账号: %s\n", + time.Now().Format(time.RFC3339), + reqID, + req.UserID, + req.Account, + ) + + // 更新账号信息 + updateQuery := ` + UPDATE user_account + SET account = $1 + WHERE user_id = $2 + RETURNING id, user_id, account + ` + fmt.Printf("[%s] [INFO] 执行SQL: %q, 参数: [%s, %s], RequestID: %s\n", + time.Now().Format(time.RFC3339), + updateQuery, + req.Account, + req.UserID, + reqID, + ) + + var ( + id string + userID string + account string + ) + err := db.QueryRow(updateQuery, req.Account, req.UserID).Scan(&id, &userID, &account) + switch { + case err == sql.ErrNoRows: + errMsg := "未找到该用户的账号信息" + fmt.Printf("[%s] [WARN] %s, UserID: %s, RequestID: %s\n", + time.Now().Format(time.RFC3339), + errMsg, + req.UserID, + reqID, + ) + c.JSON(http.StatusOK, UpdateAccountResponse{ + Success: false, + Message: errMsg, + }) + return + case err != nil: + errMsg := fmt.Sprintf("更新账号失败: %v", err) + fmt.Printf("[%s] [ERROR] %s, UserID: %s, RequestID: %s, 错误详情: %v\n", + time.Now().Format(time.RFC3339), + errMsg, + req.UserID, + reqID, + err, + ) + c.JSON(http.StatusInternalServerError, UpdateAccountResponse{ + Success: false, + Message: errMsg, + }) + return + } + + // 更新成功 + response := UpdateAccountResponse{ + Success: true, + Message: "账号更新成功", + } + response.Data.UserID = userID + response.Data.Account = account + + // 记录成功日志 + fmt.Printf("[%s] [INFO] 账号更新成功, RequestID: %s, UserID: %s, 新账号: %s, 耗时: %v\n", + time.Now().Format(time.RFC3339), + reqID, + userID, + account, + time.Since(startTime), + ) + + c.JSON(http.StatusOK, response) +} \ No newline at end of file diff --git a/api_update_account/release.sh b/api_update_account/release.sh new file mode 100755 index 0000000..ce683a6 --- /dev/null +++ b/api_update_account/release.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e # 当任何命令失败时立即退出脚本 + +# 定义镜像名称和标签 +IMAGE_NAME="user-update-account-api" +IMAGE_TAG="1.0.0" +FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" + +echo "开始删除现有镜像 ${FULL_IMAGE}..." +if sudo docker rmi -f "${FULL_IMAGE}" >/dev/null 2>&1; then + echo "镜像 ${FULL_IMAGE} 删除成功" +else + echo "镜像 ${FULL_IMAGE} 不存在,跳过删除步骤" +fi + +echo "开始构建新镜像 ${FULL_IMAGE}..." +if sudo docker build -t "${FULL_IMAGE}" .; then + echo "镜像 ${FULL_IMAGE} 构建成功!" +else + echo "错误:镜像构建失败" >&2 + exit 1 +fi \ No newline at end of file diff --git a/api_update_password/README.md b/api_update_password/README.md new file mode 100644 index 0000000..4e8dd4b --- /dev/null +++ b/api_update_password/README.md @@ -0,0 +1,7 @@ +sudo docker rm -f user_update_password_api && sudo docker run -itd --name user_update_password_api -v ./:/app -p 20004:80 golang:1.25.0-alpine3.22 + +---- + +# 格式:docker build -t 镜像名:标签 . +sudo docker rmi -f user-update-password-api:1.0.0 +sudo docker build -t user-update-password-api:1.0.0 . \ No newline at end of file diff --git a/api_update_password/depend.py b/api_update_password/depend.py new file mode 100644 index 0000000..6f1c784 --- /dev/null +++ b/api_update_password/depend.py @@ -0,0 +1,69 @@ +import subprocess +import os + +def run_command(command, check=True, shell=True): + """执行shell命令并返回结果,包含错误处理""" + try: + print(f"执行命令: {command}") + result = subprocess.run( + command, + shell=shell, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + print(f"命令输出: {result.stdout}") + return result + except subprocess.CalledProcessError as e: + print(f"命令执行失败: {e.stderr}") + raise + +def main(): + container_name = "user_update_password_api" + script_dir = os.path.dirname(os.path.abspath(__file__)) # 获取当前脚本所在目录 + + try: + # 1. 删除已存在的容器 + print("===== 步骤1: 删除已存在的容器 =====") + run_command(f"sudo docker rm -f {container_name}", check=False) + + # 2. 启动新容器 + print("\n===== 步骤2: 启动新容器 =====") + run_command( + f"sudo docker run -itd --name {container_name} " + f"-v {script_dir}:/app -p 20004:80 golang:1.25.0-alpine3.22" + ) + + # 3-5. 进入容器、进入app目录并执行go mod tidy(使用sh而非bash) + print("\n===== 步骤3-5: 容器内操作 =====") + exec_commands = ( + "cd /app && " # 进入app目录 + "echo '当前目录内容:' && ls -la && " # 新增目录检查 + "go version && " # 检查Go版本 + "go mod init user_update_password && " # 执行依赖整理 + "go mod tidy && " # 执行依赖整理 + "exit" # 退出容器 + ) + + # 使用sh代替bash执行命令 + run_command( + f"sudo docker exec -it {container_name} sh -c '{exec_commands}'" + ) + + # 7. 删除容器 + print("\n===== 步骤7: 删除容器 =====") + run_command(f"sudo docker rm -f {container_name}") + + # 8. 删除虚悬镜像 + print("\n===== 步骤8: 清理虚悬镜像 =====") + run_command("sudo docker images -f 'dangling=true' -q | xargs -r sudo docker rmi") + + print("\n===== 所有操作完成 =====") + + except Exception as e: + print(f"\n操作失败: {str(e)}") + exit(1) + +if __name__ == "__main__": + main() diff --git a/api_update_password/docker-compose.update.password.yaml b/api_update_password/docker-compose.update.password.yaml new file mode 100644 index 0000000..a668c79 --- /dev/null +++ b/api_update_password/docker-compose.update.password.yaml @@ -0,0 +1,16 @@ +services: + user_update_password: + image: user-update-password-api:1.0.0 + container_name: api_user_update_password + restart: always + depends_on: + - postgres # 依赖数据库服务 + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} # 引用.env变量 + DB_USER: ${DB_USER} # 引用.env变量 + DB_PASSWORD: ${DB_PASSWORD} # 引用.env变量 + DB_NAME: ${DB_NAME} + TZ: ${TZ} # 引用.env变量 \ No newline at end of file diff --git a/api_update_password/dockerfile b/api_update_password/dockerfile new file mode 100644 index 0000000..9c811fc --- /dev/null +++ b/api_update_password/dockerfile @@ -0,0 +1,38 @@ +# ==================== 第一阶段:构建Go程序(构建阶段)==================== +# 使用官方Go镜像作为构建基础,选择与项目匹配的Go版本(示例用1.25.0,可根据实际调整) +FROM golang:1.25.0-alpine3.22 AS builder + +# 设置工作目录(容器内的目录,规范文件位置) +WORKDIR /app + +# 复制go.mod和go.sum(先复制依赖文件,利用Docker缓存机制,避免每次代码变动都重新下载依赖) +COPY go.mod go.sum ./ + +# 下载项目依赖(仅当go.mod/go.sum变动时才会重新执行) +RUN go mod download + +# 复制整个项目代码到工作目录 +COPY . . + +# 构建Go程序: +# - CGO_ENABLED=0:禁用CGO,生成静态链接的二进制文件(避免依赖系统库,保证镜像兼容性) +# - -o app:指定输出二进制文件名为app +# - ./main.go:指定入口文件 +RUN CGO_ENABLED=0 GOOS=linux go build -o app ./main.go + + +# ==================== 第二阶段:运行程序(运行阶段)==================== +# 使用轻量级的alpine镜像(仅5MB左右,大幅减小最终镜像体积) +FROM alpine:3.19 + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制编译好的二进制文件到当前镜像(仅复制最终产物,减小体积) +COPY --from=builder /app/app ./ + +# 暴露程序运行端口(与代码中一致) +EXPOSE 8080 + +# 容器启动时执行的命令:运行二进制文件 +CMD ["./app"] \ No newline at end of file diff --git a/api_update_password/go.mod b/api_update_password/go.mod new file mode 100644 index 0000000..6af4324 --- /dev/null +++ b/api_update_password/go.mod @@ -0,0 +1,43 @@ +module user_update_password + +go 1.25.0 + +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/lib/pq v1.10.9 + golang.org/x/crypto v0.42.0 +) + +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.36.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/api_update_password/main.go b/api_update_password/main.go new file mode 100644 index 0000000..26e13a0 --- /dev/null +++ b/api_update_password/main.go @@ -0,0 +1,206 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" + "golang.org/x/crypto/bcrypt" +) + +// UpdatePasswordRequest 更新密码请求参数结构 +type UpdatePasswordRequest struct { + UserID string `json:"user_id" binding:"required"` + OldPassword string `json:"old_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required,min=6"` +} + +// UpdatePasswordResponse 更新密码响应结构 +type UpdatePasswordResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +var db *sql.DB + +func main() { + // 初始化日志输出格式 + log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + log.Println("开始初始化应用程序") + + // 初始化Gin引擎 + r := gin.Default() + log.Println("Gin引擎初始化完成") + + // 从环境变量获取数据库配置 + dbHost := os.Getenv("DB_HOST") + dbPort := os.Getenv("DB_PORT") + dbUser := os.Getenv("DB_USER") + dbPassword := os.Getenv("DB_PASSWORD") + dbName := os.Getenv("DB_NAME") + log.Printf("读取数据库配置: host=%s, port=%s, user=%s, dbname=%s", dbHost, dbPort, dbUser, dbName) + + // 构建数据库连接字符串 + connStr := fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPassword, dbName, + ) + + var err error + db, err = sql.Open("postgres", connStr) + if err != nil { + log.Panicf("无法连接数据库: %v", err) + } + defer db.Close() + log.Println("数据库连接对象创建成功") + + // 验证数据库连接 + if err := db.Ping(); err != nil { + log.Panicf("数据库连接失败: %v", err) + } + log.Println("数据库连接验证成功") + + // 更新密码接口 + r.POST("/user/update/password", updatePasswordHandler) + log.Println("注册更新密码接口: POST /user/update/password") + + // 启动服务,监听80端口 + log.Println("服务启动在80端口") + if err := r.Run(":80"); err != nil { + log.Panicf("服务启动失败: %v", err) + } +} + +// updatePasswordHandler 处理密码更新逻辑 +func updatePasswordHandler(c *gin.Context) { + requestID := c.Request.Header.Get("X-Request-ID") + if requestID == "" { + requestID = fmt.Sprintf("req-%d", gin.Mode()) + } + log.Printf("[%s] 收到更新密码请求 from %s", requestID, c.ClientIP()) + + var req UpdatePasswordRequest + // 绑定并验证请求参数 + if err := c.ShouldBindJSON(&req); err != nil { + log.Printf("[%s] 请求参数绑定失败: %v", requestID, err) + c.JSON(http.StatusBadRequest, UpdatePasswordResponse{ + Success: false, + Message: "请求参数错误: " + err.Error(), + }) + return + } + log.Printf("[%s] 请求参数验证通过, 用户ID: %s", requestID, req.UserID) + + // 1. 检查新旧密码是否一致 + if req.OldPassword == req.NewPassword { + log.Printf("[%s] 新密码与旧密码相同, 用户ID: %s", requestID, req.UserID) + c.JSON(http.StatusOK, UpdatePasswordResponse{ + Success: false, + Message: "新密码不能与旧密码相同", + }) + return + } + + // 2. 对旧密码进行加密处理 + log.Printf("[%s] 开始对旧密码进行加密, 用户ID: %s", requestID, req.UserID) + hashedOldPassword, err := bcrypt.GenerateFromPassword([]byte(req.OldPassword), bcrypt.DefaultCost) + if err != nil { + log.Printf("[%s] 旧密码加密失败: %v, 用户ID: %s", requestID, err, req.UserID) + c.JSON(http.StatusInternalServerError, UpdatePasswordResponse{ + Success: false, + Message: "旧密码加密失败: " + err.Error(), + }) + return + } + log.Printf("[%s] 旧密码加密完成, 用户ID: %s", requestID, req.UserID) + + // 3. 使用加密后的旧密码查询并验证(按user_id匹配) + var count int + query := ` + SELECT COUNT(*) FROM user_password + WHERE user_id = $1 AND password = $2 + ` + log.Printf("[%s] 执行旧密码验证查询, SQL: %s, 参数: [%s, ****]", requestID, query, req.UserID) + err = db.QueryRow(query, req.UserID, string(hashedOldPassword)).Scan(&count) + if err != nil { + log.Printf("[%s] 旧密码验证查询失败: %v, 用户ID: %s", requestID, err, req.UserID) + c.JSON(http.StatusInternalServerError, UpdatePasswordResponse{ + Success: false, + Message: "验证旧密码失败: " + err.Error(), + }) + return + } + + // 验证失败处理 + if count == 0 { + log.Printf("[%s] 旧密码验证失败, 用户ID: %s", requestID, req.UserID) + c.JSON(http.StatusOK, UpdatePasswordResponse{ + Success: false, + Message: "旧密码不正确", + }) + return + } + log.Printf("[%s] 旧密码验证成功, 用户ID: %s", requestID, req.UserID) + + // 4. 对新密码进行加密处理 + log.Printf("[%s] 开始对新密码进行加密, 用户ID: %s", requestID, req.UserID) + hashedNewPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) + if err != nil { + log.Printf("[%s] 新密码加密失败: %v, 用户ID: %s", requestID, err, req.UserID) + c.JSON(http.StatusInternalServerError, UpdatePasswordResponse{ + Success: false, + Message: "新密码加密失败: " + err.Error(), + }) + return + } + log.Printf("[%s] 新密码加密完成, 用户ID: %s", requestID, req.UserID) + + // 5. 更新密码表中的密码(按user_id匹配) + updateQuery := ` + UPDATE user_password + SET password = $1 + WHERE user_id = $2 + ` + log.Printf("[%s] 执行密码更新, SQL: %s, 参数: [****, %s]", requestID, updateQuery, req.UserID) + result, err := db.Exec(updateQuery, string(hashedNewPassword), req.UserID) + if err != nil { + log.Printf("[%s] 密码更新执行失败: %v, 用户ID: %s", requestID, err, req.UserID) + c.JSON(http.StatusInternalServerError, UpdatePasswordResponse{ + Success: false, + Message: "更新密码失败: " + err.Error(), + }) + return + } + + // 6. 检查更新结果 + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Printf("[%s] 获取更新行数失败: %v, 用户ID: %s", requestID, err, req.UserID) + c.JSON(http.StatusInternalServerError, UpdatePasswordResponse{ + Success: false, + Message: "检查更新结果失败: " + err.Error(), + }) + return + } + log.Printf("[%s] 密码更新影响行数: %d, 用户ID: %s", requestID, rowsAffected, req.UserID) + + if rowsAffected == 0 { + log.Printf("[%s] 未找到用户或密码未变化, 用户ID: %s", requestID, req.UserID) + c.JSON(http.StatusOK, UpdatePasswordResponse{ + Success: false, + Message: "未找到用户或密码未发生变化", + }) + return + } + + // 7. 更新成功 + log.Printf("[%s] 密码更新成功, 用户ID: %s", requestID, req.UserID) + c.JSON(http.StatusOK, UpdatePasswordResponse{ + Success: true, + Message: "密码更新成功", + }) +} \ No newline at end of file diff --git a/api_update_password/release.sh b/api_update_password/release.sh new file mode 100755 index 0000000..760d402 --- /dev/null +++ b/api_update_password/release.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e # 当任何命令失败时立即退出脚本 + +# 定义镜像名称和标签 +IMAGE_NAME="user-update-password-api" +IMAGE_TAG="1.0.0" +FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" + +echo "开始删除现有镜像 ${FULL_IMAGE}..." +if sudo docker rmi -f "${FULL_IMAGE}" >/dev/null 2>&1; then + echo "镜像 ${FULL_IMAGE} 删除成功" +else + echo "镜像 ${FULL_IMAGE} 不存在,跳过删除步骤" +fi + +echo "开始构建新镜像 ${FULL_IMAGE}..." +if sudo docker build -t "${FULL_IMAGE}" .; then + echo "镜像 ${FULL_IMAGE} 构建成功!" +else + echo "错误:镜像构建失败" >&2 + exit 1 +fi \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 0000000..11133ac --- /dev/null +++ b/build.py @@ -0,0 +1,140 @@ +import os +import shutil + +# 1. 定义全局参数 +a = "gateway" # 功能名 +b = "" # 次功能名 +c = "20000" # 端口号 + +def main(): + # 确定新目录名称 + if b: + new_dir = f"api_{a}_{b}" + else: + new_dir = f"api_{a}" + + # 2. 复制并重命名api_template目录 + source_dir = "api_template" + if not os.path.exists(source_dir): + print(f"错误:源目录 {source_dir} 不存在") + return + + if os.path.exists(new_dir): + print(f"警告:目标目录 {new_dir} 已存在,将被覆盖") + shutil.rmtree(new_dir) + + shutil.copytree(source_dir, new_dir) + print(f"已创建目录:{new_dir}") + + # 确定新yaml文件名 + if b: + yaml_filename = f"docker-compose.{a}.{b}.yaml" + else: + yaml_filename = f"docker-compose.{a}.yaml" + + # 3. 重命名docker-compose.temp.yaml + temp_yaml = os.path.join(new_dir, "docker-compose.temp.yaml") + new_yaml = os.path.join(new_dir, yaml_filename) + if os.path.exists(temp_yaml): + os.rename(temp_yaml, new_yaml) + print(f"已重命名yaml文件:{yaml_filename}") + + # 4-6. 处理yaml文件内容 + with open(new_yaml, 'r', encoding='utf-8') as f: + content = f.read() + + # 替换"template-api" + if b: + content = content.replace("template-api", f"{a}-{b}-api") + else: + content = content.replace("template-api", f"{a}-api") + + # 替换"_template" + if b: + content = content.replace("_template", f"_{a}_{b}") + else: + content = content.replace("_template", f"_{a}") + + # 替换端口号"20001" + content = content.replace("20001", c) + + with open(new_yaml, 'w', encoding='utf-8') as f: + f.write(content) + print(f"已更新yaml文件内容") + else: + print(f"警告:未找到 {temp_yaml} 文件,跳过yaml处理") + + # 7-9. 处理README.md + readme_path = os.path.join(new_dir, "README.md") + if os.path.exists(readme_path): + with open(readme_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 替换"template_" + if b: + content = content.replace("template_", f"{a}_{b}_") + else: + content = content.replace("template_", f"{a}_") + + # 替换"template-" + if b: + content = content.replace("template-", f"{a}-{b}-") + else: + content = content.replace("template-", f"{a}-") + + # 替换端口号"20001" + content = content.replace("20001", c) + + with open(readme_path, 'w', encoding='utf-8') as f: + f.write(content) + print(f"已更新README.md内容") + else: + print(f"警告:未找到 {readme_path} 文件,跳过README处理") + + # 10. 处理release.sh + release_path = os.path.join(new_dir, "release.sh") + if os.path.exists(release_path): + with open(release_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 替换"template" + if b: + content = content.replace("template", f"{a}-{b}") + else: + content = content.replace("template", f"{a}") + + with open(release_path, 'w', encoding='utf-8') as f: + f.write(content) + print(f"已更新release.sh内容") + else: + print(f"警告:未找到 {release_path} 文件,跳过release.sh处理") + + # 11-13. 处理depend.py + depend_path = os.path.join(new_dir, "depend.py") + if os.path.exists(depend_path): + with open(depend_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 替换"_template_" + if b: + content = content.replace("_template_", f"_{a}_{b}_") + else: + content = content.replace("_template_", f"_{a}_") + + # 替换"template &&" + if b: + content = content.replace("template &&", f"{a}_{b} &&") + else: + content = content.replace("template &&", f"{a} &&") + + # 替换端口号"20001" + content = content.replace("20001", c) + + with open(depend_path, 'w', encoding='utf-8') as f: + f.write(content) + print(f"已更新depend.py内容") + else: + print(f"警告:未找到 {depend_path} 文件,跳过depend.py处理") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy.py b/deploy.py new file mode 100644 index 0000000..a4150bc --- /dev/null +++ b/deploy.py @@ -0,0 +1,191 @@ +import os +import yaml +import subprocess +from typing import Dict, Any + +def run_shell_command(command: str, cwd: str = None) -> bool: + """执行shell命令并返回执行结果""" + try: + print(f"执行命令: {command} (工作目录: {cwd or os.getcwd()})") + result = subprocess.run( + command, + cwd=cwd, + shell=True, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + print(f"命令输出: {result.stdout}") + return True + except subprocess.CalledProcessError as e: + print(f"命令执行失败: {e.stderr}") + return False + +def cleanup_dangling_images() -> bool: + """清除docker虚悬镜像(dangling images)""" + print("开始清除虚悬镜像...") + # 清除所有:的虚悬镜像,xargs -r确保无镜像时不执行删除命令 + return run_shell_command('sudo docker images -f "dangling=true" -q | xargs -r sudo docker rmi') + +def execute_release_scripts(root_dir: str) -> bool: + """遍历所有api_目录并执行release.sh脚本(排除api_template)""" + print("开始执行所有api_目录下的release.sh脚本...") + + for dir_name in os.listdir(root_dir): + # 排除api_template文件夹 + if dir_name == "api_template": + print(f"跳过模板目录: {dir_name}") + continue + + dir_path = os.path.join(root_dir, dir_name) + if os.path.isdir(dir_path) and dir_name.startswith('api_'): + release_script = os.path.join(dir_path, 'release.sh') + if os.path.exists(release_script): + print(f"处理目录: {dir_path}") + + # 添加执行权限 + if not run_shell_command(f"chmod +x {release_script}", dir_path): + print(f"为 {release_script} 添加权限失败,跳过执行") + continue + + # 执行release.sh + if not run_shell_command(f"./release.sh", dir_path): + print(f"{release_script} 执行失败,跳过后续步骤") + return False + else: + print(f"{dir_path} 中未找到release.sh,跳过") + + print("所有release.sh脚本执行完成") + return True + +def merge_yaml_files(root_dir: str) -> Dict[str, Any]: + """合并所有 docker-compose 相关 YAML 文件内容(排除api_template)""" + merged = { + 'version': '3.8', # 默认使用 3.8 版本 + 'services': {}, + 'networks': {}, + 'volumes': {} + } + + # 查找所有 docker-compose 相关 YAML 文件 + yaml_files = [] + # 1. 根目录下的 docker-compose.*.yaml + for file in os.listdir(root_dir): + if file.startswith('docker-compose.') and file.endswith('.yaml'): + if file == 'docker-compose.yaml': # 跳过目标文件本身 + continue + yaml_files.append(os.path.join(root_dir, file)) + + # 2. api_ 目录下的 docker-compose.*.yaml(排除api_template) + for dir_name in os.listdir(root_dir): + # 排除api_template文件夹 + if dir_name == "api_template": + print(f"跳过模板目录的yaml文件: {dir_name}") + continue + + dir_path = os.path.join(root_dir, dir_name) + if os.path.isdir(dir_path) and dir_name.startswith('api_'): + for file in os.listdir(dir_path): + if file.startswith('docker-compose.') and file.endswith('.yaml'): + yaml_files.append(os.path.join(dir_path, file)) + + # 合并所有 YAML 文件内容 + for file_path in yaml_files: + print(f"合并文件: {file_path}") + with open(file_path, 'r', encoding='utf-8') as f: + try: + data = yaml.safe_load(f) + if not data: + continue + + # 合并 services + if 'services' in data: + merged['services'].update(data['services']) + + # 合并 networks + if 'networks' in data: + merged['networks'].update(data['networks']) + + # 合并 volumes + if 'volumes' in data: + merged['volumes'].update(data['volumes']) + + # 保留最高版本号 + if 'version' in data and data['version'] > merged['version']: + merged['version'] = data['version'] + + except yaml.YAMLError as e: + print(f"解析 {file_path} 失败: {e}") + continue + + return merged + +def stop_docker_compose(root_dir: str) -> bool: + """停止docker-compose""" + print("开始停止docker-compose...") + compose_file = os.path.join(root_dir, 'docker-compose.yaml') + if not os.path.exists(compose_file): + print(f"未找到docker-compose文件: {compose_file}") + return False + + return run_shell_command(f"sudo docker-compose -f {compose_file} down", root_dir) + +def start_docker_compose(root_dir: str) -> bool: + """启动docker-compose""" + print("开始启动docker-compose...") + compose_file = os.path.join(root_dir, 'docker-compose.yaml') + if not os.path.exists(compose_file): + print(f"未找到docker-compose文件: {compose_file}") + return False + + return run_shell_command(f"sudo docker-compose -f {compose_file} up -d", root_dir) + +def main(): + # 先执行清除虚悬镜像操作 + print("部署流程开始,先执行清除虚悬镜像操作...") + if not cleanup_dangling_images(): + print("清除虚悬镜像失败(非致命错误,继续部署流程)") + else: + print("虚悬镜像清除完成") + + # 获取项目根目录(deploy.py 所在目录) + root_dir = os.path.dirname(os.path.abspath(__file__)) + print(f"项目根目录: {root_dir}") + + # 1. 执行所有api_目录下的release.sh脚本 + if not execute_release_scripts(root_dir): + print("执行release脚本失败,终止部署流程") + return + + # 2. 合并所有YAML文件 + merged_data = merge_yaml_files(root_dir) + + # 输出到根目录的docker-compose.yaml + output_path = os.path.join(root_dir, 'docker-compose.yaml') + with open(output_path, 'w', encoding='utf-8') as f: + yaml.dump( + merged_data, + f, + sort_keys=False, # 保持键的顺序 + allow_unicode=True, # 支持中文 + default_flow_style=False # 使用块样式 + ) + print(f"已生成合并后的 docker-compose.yaml: {output_path}") + + # 3. 启动docker-compose + stop_docker_compose(root_dir) + if not start_docker_compose(root_dir): + print("启动docker-compose失败") + return + + print("部署流程完成") + print("\n===== 清理虚悬镜像 =====") + if not cleanup_dangling_images(): + print("清除虚悬镜像失败(非致命错误,继续部署流程)") + else: + print("虚悬镜像清除完成") + +if __name__ == "__main__": + # 需要安装 pyyaml 库: pip install pyyaml + main() \ No newline at end of file diff --git a/docker-compose.base.yaml b/docker-compose.base.yaml new file mode 100644 index 0000000..ed268aa --- /dev/null +++ b/docker-compose.base.yaml @@ -0,0 +1,9 @@ +# 共享网络和基础配置 +networks: + user-network: + driver: bridge + +# 如需跨服务共享数据卷,可在此定义 +# volumes: + # 示例:若未来有共享数据需求,可在此声明 + # shared-data: \ No newline at end of file diff --git a/docker-compose.db.admin.yaml b/docker-compose.db.admin.yaml new file mode 100644 index 0000000..fba30da --- /dev/null +++ b/docker-compose.db.admin.yaml @@ -0,0 +1,16 @@ +services: + pgadmin: + image: dpage/pgadmin4:9.5.0 + container_name: user_pgadmin + ports: + - "20001:80" + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} + TZ: ${TZ} + volumes: + - ./shared_data/data4pgadmin:/var/lib/pgadmin + networks: + - user-network + depends_on: + - postgres # 依赖数据库服务(非必须,仅控制启动顺序) \ No newline at end of file diff --git a/docker-compose.db.yaml b/docker-compose.db.yaml new file mode 100644 index 0000000..00de517 --- /dev/null +++ b/docker-compose.db.yaml @@ -0,0 +1,17 @@ +services: + postgres: + image: postgres:17.4-alpine + container_name: user_db + restart: always + entrypoint: ["/scripts/db-lanuch-entrypoint.sh"] + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + TZ: ${TZ} + volumes: + - ./shared_data/user_db:/var/lib/postgresql/data + - ./sql:/docker-entrypoint-initdb.d + - ./scripts:/scripts + networks: + - user-network \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..9e62f6d --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,123 @@ +version: '3.8' +services: + postgres: + image: postgres:17.4-alpine + container_name: user_db + restart: always + entrypoint: + - /scripts/db-lanuch-entrypoint.sh + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + TZ: ${TZ} + volumes: + - ./shared_data/user_db:/var/lib/postgresql/data + - ./sql:/docker-entrypoint-initdb.d + - ./scripts:/scripts + networks: + - user-network + pgadmin: + image: dpage/pgadmin4:9.5.0 + container_name: user_pgadmin + ports: + - 20001:80 + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} + TZ: ${TZ} + volumes: + - ./shared_data/data4pgadmin:/var/lib/pgadmin + networks: + - user-network + depends_on: + - postgres + user_delete: + image: user-delete-api:1.0.0 + container_name: api_user_delete + restart: always + depends_on: + - postgres + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + TZ: ${TZ} + user_login: + image: user-login-api:1.0.0 + container_name: api_user_login + restart: always + depends_on: + - postgres + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + TZ: ${TZ} + user_update_password: + image: user-update-password-api:1.0.0 + container_name: api_user_update_password + restart: always + depends_on: + - postgres + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + TZ: ${TZ} + user_register: + image: user-register-api:1.0.0 + container_name: api_user_register + restart: always + depends_on: + - postgres + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + TZ: ${TZ} + user_gateway: + image: user-gateway-api:1.0.0 + container_name: api_user_gateway + restart: always + ports: + - 20000:80 + networks: + - user-network + environment: + GATEWAY_PORT: ${GATEWAY_PORT} + user_update_account: + image: user-update-account-api:1.0.0 + container_name: api_user_update_account + restart: always + depends_on: + - postgres + networks: + - user-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + TZ: ${TZ} +networks: + user-network: + driver: bridge +volumes: {} diff --git a/scripts/db-lanuch-entrypoint.sh b/scripts/db-lanuch-entrypoint.sh new file mode 100755 index 0000000..69d6aba --- /dev/null +++ b/scripts/db-lanuch-entrypoint.sh @@ -0,0 +1,43 @@ +#!/bin/sh +set -e # 脚本执行出错时立即退出 + +# -------------------------- +# 1. 启动PostgreSQL服务(后台运行) +# -------------------------- +# 调用PostgreSQL默认初始化逻辑(即使数据目录已存在,也会启动服务) +docker-entrypoint.sh postgres & +# 记录PostgreSQL主进程ID,后续等待用 +PG_PID=$! + +# -------------------------- +# 2. 等待数据库服务就绪(避免脚本执行时数据库未启动) +# -------------------------- +echo "等待PostgreSQL服务就绪..." +# 将所有参数放在同一行,避免换行解析问题 +until pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" -h "localhost" -p "5432"; do + sleep 1 # 每1秒检查一次 +done +echo "PostgreSQL服务已就绪,开始强制执行脚本..." + +# -------------------------- +# 3. 强制执行所有挂载的SQL脚本(每次启动都执行) +# -------------------------- +# 遍历/docker-entrypoint-initdb.d目录下的所有.sql脚本(按文件名排序) +for script in /docker-entrypoint-initdb.d/*.sql; do + if [ -f "$script" ]; then # 确保是文件(排除目录) + echo "正在执行脚本: $script" + # 用psql客户端执行脚本,指定用户和数据库 + psql -U "$POSTGRES_USER" \ + -d "$POSTGRES_DB" \ + -h "localhost" \ + -p "5432" \ + -f "$script" \ + --set=ON_ERROR_STOP=1 # 脚本执行出错时停止(可选,根据需求调整) + echo "脚本执行完成: $script" + fi +done + +# -------------------------- +# 4. 等待PostgreSQL主进程(避免容器启动后退出) +# -------------------------- +wait $PG_PID \ No newline at end of file diff --git a/sql/01_uuid_v7_setup.sql b/sql/01_uuid_v7_setup.sql new file mode 100644 index 0000000..b317d1c --- /dev/null +++ b/sql/01_uuid_v7_setup.sql @@ -0,0 +1,48 @@ +-- 切换到目标数据库 +\c postgres; + +-- 检查并创建UUID扩展(如果不存在) +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- 定义检测UUID v7支持的函数 +CREATE OR REPLACE FUNCTION check_uuid_v7_support() RETURNS BOOLEAN AS $$ +DECLARE + test_uuid UUID; +BEGIN + BEGIN + SELECT gen_random_uuid_v7() INTO test_uuid; + RETURN TRUE; + EXCEPTION + WHEN undefined_function THEN + RETURN FALSE; + END; +END; +$$ LANGUAGE plpgsql VOLATILE; + +-- 创建UUID v7兼容函数(修复UUID格式长度问题) +CREATE OR REPLACE FUNCTION gen_random_uuid_v7() RETURNS uuid AS $$ +DECLARE + unix_ts_ms BIGINT; + rand_a BIGINT; + rand_b BIGINT; + hex_str TEXT; +BEGIN + -- 获取当前毫秒级Unix时间戳 + unix_ts_ms := (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT; + + -- 生成随机数(调整随机数范围以确保总长度正确) + rand_a := (random() * (2^20 - 1))::BIGINT; + rand_b := (random() * (2^44 - 1))::BIGINT; -- 从48位调整为44位,减少2个字节 + + -- 组合UUID v7格式(确保总长度为32个十六进制字符) + hex_str := + lpad(to_hex(unix_ts_ms >> 12), 8, '0') || + lpad(to_hex((unix_ts_ms & 4095) << 4), 4, '0') || + '7' || lpad(to_hex(rand_a >> 18), 3, '0') || + lpad(to_hex(8 + (rand_a & 16383) >> 12), 2, '0') || + lpad(to_hex(rand_a & 4095), 3, '0') || + lpad(to_hex(rand_b), 11, '0'); -- 从12位调整为11位 + + RETURN hex_str::uuid; +END; +$$ LANGUAGE plpgsql VOLATILE; \ No newline at end of file diff --git a/sql/02_create_user_table.sql b/sql/02_create_user_table.sql new file mode 100644 index 0000000..0de15d2 --- /dev/null +++ b/sql/02_create_user_table.sql @@ -0,0 +1,30 @@ +-- 切换到目标数据库 +\c postgres; + +CREATE OR REPLACE FUNCTION update_user_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql VOLATILE; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user') THEN + CREATE TABLE "user" ( -- user是关键字,用双引号包裹 + id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + CREATE TRIGGER update_user_updated_at + BEFORE UPDATE ON "user" + FOR EACH ROW + EXECUTE FUNCTION update_user_modified_column(); + + RAISE NOTICE 'Created user table and trigger'; + ELSE + RAISE NOTICE 'user table already exists'; + END IF; +END $$; \ No newline at end of file diff --git a/sql/03_create_account_table.sql b/sql/03_create_account_table.sql new file mode 100644 index 0000000..754794b --- /dev/null +++ b/sql/03_create_account_table.sql @@ -0,0 +1,31 @@ +-- 切换到目标数据库 +\c postgres; + +CREATE OR REPLACE FUNCTION update_account_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql VOLATILE; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_account') THEN + CREATE TABLE user_account ( + id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, + user_id UUID NOT NULL, + account VARCHAR NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + CREATE TRIGGER update_user_account_updated_at + BEFORE UPDATE ON "user_account" + FOR EACH ROW + EXECUTE FUNCTION update_account_modified_column(); + + RAISE NOTICE 'Created user_account table and trigger'; + ELSE + RAISE NOTICE 'user_account table already exists'; + END IF; +END $$; \ No newline at end of file diff --git a/sql/04_create_password_table.sql b/sql/04_create_password_table.sql new file mode 100644 index 0000000..bff72b7 --- /dev/null +++ b/sql/04_create_password_table.sql @@ -0,0 +1,31 @@ +-- 切换到目标数据库 +\c postgres; + +CREATE OR REPLACE FUNCTION update_password_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql VOLATILE; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_password') THEN + CREATE TABLE user_password ( + id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, + user_id UUID NOT NULL, + password VARCHAR NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + CREATE TRIGGER update_user_password_updated_at + BEFORE UPDATE ON "user_password" + FOR EACH ROW + EXECUTE FUNCTION update_password_modified_column(); + + RAISE NOTICE 'Created user_password table and trigger'; + ELSE + RAISE NOTICE 'user_password table already exists'; + END IF; +END $$; \ No newline at end of file diff --git a/sql/05_create_account_password_view.sql b/sql/05_create_account_password_view.sql new file mode 100644 index 0000000..32d2f5f --- /dev/null +++ b/sql/05_create_account_password_view.sql @@ -0,0 +1,36 @@ +\c postgres; + +DO $$ +DECLARE + view_exists BOOLEAN; +BEGIN + -- 检查视图是否已存在 + SELECT EXISTS ( + SELECT 1 FROM information_schema.views + WHERE table_name = 'user_account_password_view' + ) INTO view_exists; + + -- 创建或更新视图 + CREATE OR REPLACE VIEW user_account_password_view AS + SELECT + u.id AS user_id, + ua.account AS account, + up.password AS password, + u.deleted AS deleted + FROM + "user" u + JOIN + user_account ua ON u.id = ua.user_id + JOIN + user_password up ON u.id = up.user_id; + + -- 根据视图是否已存在输出不同提示 + IF view_exists THEN + RAISE NOTICE '视图 user_account_password_view 已更新'; + ELSE + RAISE NOTICE '视图 user_account_password_view 已创建'; + END IF; +EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE '处理视图时发生错误: %', SQLERRM; +END $$;