init code
This commit is contained in:
26
.dockerignore
Normal file
26
.dockerignore
Normal file
@@ -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/
|
12
.env
Normal file
12
.env
Normal file
@@ -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
|
7
api_delete/README.md
Normal file
7
api_delete/README.md
Normal file
@@ -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 .
|
69
api_delete/depend.py
Normal file
69
api_delete/depend.py
Normal file
@@ -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()
|
16
api_delete/docker-compose.delete.yaml
Normal file
16
api_delete/docker-compose.delete.yaml
Normal file
@@ -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变量
|
38
api_delete/dockerfile
Normal file
38
api_delete/dockerfile
Normal file
@@ -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"]
|
43
api_delete/go.mod
Normal file
43
api_delete/go.mod
Normal file
@@ -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
|
||||
)
|
146
api_delete/main.go
Normal file
146
api_delete/main.go
Normal file
@@ -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: "账号删除成功",
|
||||
})
|
||||
}
|
22
api_delete/release.sh
Executable file
22
api_delete/release.sh
Executable file
@@ -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
|
7
api_gateway/README.md
Normal file
7
api_gateway/README.md
Normal file
@@ -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 .
|
69
api_gateway/depend.py
Normal file
69
api_gateway/depend.py
Normal file
@@ -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()
|
11
api_gateway/docker-compose.gateway.yaml
Normal file
11
api_gateway/docker-compose.gateway.yaml
Normal file
@@ -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变量
|
38
api_gateway/dockerfile
Normal file
38
api_gateway/dockerfile
Normal file
@@ -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"]
|
42
api_gateway/go.mod
Normal file
42
api_gateway/go.mod
Normal file
@@ -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
|
||||
)
|
137
api_gateway/main.go
Normal file
137
api_gateway/main.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
22
api_gateway/release.sh
Executable file
22
api_gateway/release.sh
Executable file
@@ -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
|
7
api_login/README.md
Normal file
7
api_login/README.md
Normal file
@@ -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 .
|
69
api_login/depend.py
Normal file
69
api_login/depend.py
Normal file
@@ -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()
|
16
api_login/docker-compose.login.yaml
Normal file
16
api_login/docker-compose.login.yaml
Normal file
@@ -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变量
|
38
api_login/dockerfile
Normal file
38
api_login/dockerfile
Normal file
@@ -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"]
|
43
api_login/go.mod
Normal file
43
api_login/go.mod
Normal file
@@ -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
|
||||
)
|
162
api_login/main.go
Normal file
162
api_login/main.go
Normal file
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
22
api_login/release.sh
Executable file
22
api_login/release.sh
Executable file
@@ -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
|
7
api_register/README.md
Normal file
7
api_register/README.md
Normal file
@@ -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 .
|
69
api_register/depend.py
Normal file
69
api_register/depend.py
Normal file
@@ -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()
|
16
api_register/docker-compose.register.yaml
Normal file
16
api_register/docker-compose.register.yaml
Normal file
@@ -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变量
|
38
api_register/dockerfile
Normal file
38
api_register/dockerfile
Normal file
@@ -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"]
|
43
api_register/go.mod
Normal file
43
api_register/go.mod
Normal file
@@ -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
|
||||
)
|
272
api_register/main.go
Normal file
272
api_register/main.go
Normal file
@@ -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)
|
||||
}
|
22
api_register/release.sh
Executable file
22
api_register/release.sh
Executable file
@@ -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
|
7
api_template/README.md
Normal file
7
api_template/README.md
Normal file
@@ -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 .
|
69
api_template/depend.py
Normal file
69
api_template/depend.py
Normal file
@@ -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()
|
16
api_template/docker-compose.temp.yaml
Normal file
16
api_template/docker-compose.temp.yaml
Normal file
@@ -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变量
|
38
api_template/dockerfile
Normal file
38
api_template/dockerfile
Normal file
@@ -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"]
|
22
api_template/release.sh
Executable file
22
api_template/release.sh
Executable file
@@ -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
|
7
api_update_account/README.md
Normal file
7
api_update_account/README.md
Normal file
@@ -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 .
|
69
api_update_account/depend.py
Normal file
69
api_update_account/depend.py
Normal file
@@ -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()
|
16
api_update_account/docker-compose.update.account.yaml
Normal file
16
api_update_account/docker-compose.update.account.yaml
Normal file
@@ -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变量
|
38
api_update_account/dockerfile
Normal file
38
api_update_account/dockerfile
Normal file
@@ -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"]
|
43
api_update_account/go.mod
Normal file
43
api_update_account/go.mod
Normal file
@@ -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
|
||||
)
|
204
api_update_account/main.go
Normal file
204
api_update_account/main.go
Normal file
@@ -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)
|
||||
}
|
22
api_update_account/release.sh
Executable file
22
api_update_account/release.sh
Executable file
@@ -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
|
7
api_update_password/README.md
Normal file
7
api_update_password/README.md
Normal file
@@ -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 .
|
69
api_update_password/depend.py
Normal file
69
api_update_password/depend.py
Normal file
@@ -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()
|
16
api_update_password/docker-compose.update.password.yaml
Normal file
16
api_update_password/docker-compose.update.password.yaml
Normal file
@@ -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变量
|
38
api_update_password/dockerfile
Normal file
38
api_update_password/dockerfile
Normal file
@@ -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"]
|
43
api_update_password/go.mod
Normal file
43
api_update_password/go.mod
Normal file
@@ -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
|
||||
)
|
206
api_update_password/main.go
Normal file
206
api_update_password/main.go
Normal file
@@ -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: "密码更新成功",
|
||||
})
|
||||
}
|
22
api_update_password/release.sh
Executable file
22
api_update_password/release.sh
Executable file
@@ -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
|
140
build.py
Normal file
140
build.py
Normal file
@@ -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()
|
191
deploy.py
Normal file
191
deploy.py
Normal file
@@ -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("开始清除虚悬镜像...")
|
||||
# 清除所有<none>:<none>的虚悬镜像,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()
|
9
docker-compose.base.yaml
Normal file
9
docker-compose.base.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# 共享网络和基础配置
|
||||
networks:
|
||||
user-network:
|
||||
driver: bridge
|
||||
|
||||
# 如需跨服务共享数据卷,可在此定义
|
||||
# volumes:
|
||||
# 示例:若未来有共享数据需求,可在此声明
|
||||
# shared-data:
|
16
docker-compose.db.admin.yaml
Normal file
16
docker-compose.db.admin.yaml
Normal file
@@ -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 # 依赖数据库服务(非必须,仅控制启动顺序)
|
17
docker-compose.db.yaml
Normal file
17
docker-compose.db.yaml
Normal file
@@ -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
|
123
docker-compose.yaml
Normal file
123
docker-compose.yaml
Normal file
@@ -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: {}
|
43
scripts/db-lanuch-entrypoint.sh
Executable file
43
scripts/db-lanuch-entrypoint.sh
Executable file
@@ -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
|
48
sql/01_uuid_v7_setup.sql
Normal file
48
sql/01_uuid_v7_setup.sql
Normal file
@@ -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;
|
30
sql/02_create_user_table.sql
Normal file
30
sql/02_create_user_table.sql
Normal file
@@ -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 $$;
|
31
sql/03_create_account_table.sql
Normal file
31
sql/03_create_account_table.sql
Normal file
@@ -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 $$;
|
31
sql/04_create_password_table.sql
Normal file
31
sql/04_create_password_table.sql
Normal file
@@ -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 $$;
|
36
sql/05_create_account_password_view.sql
Normal file
36
sql/05_create_account_password_view.sql
Normal file
@@ -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 $$;
|
Reference in New Issue
Block a user