init code

This commit is contained in:
fish
2025-10-03 16:39:24 +08:00
parent d3008ab9f5
commit 1e9cdda192
61 changed files with 3196 additions and 0 deletions

26
.dockerignore Normal file
View 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
View 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
View 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
View 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()

View 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
View 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
View 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
View 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
View 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
View 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
View 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()

View 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
View 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
View 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
View 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
View 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
View 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
View 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()

View 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
View 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
View 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
View 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
View 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
View 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
View 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()

View 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
View 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
View 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
View 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
View 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
View 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
View 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()

View 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
View 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
View 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

View 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 .

View 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()

View 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变量

View 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
View 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
View 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
View 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

View 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 .

View 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()

View 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变量

View 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"]

View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,9 @@
# 共享网络和基础配置
networks:
user-network:
driver: bridge
# 如需跨服务共享数据卷,可在此定义
# volumes:
# 示例:若未来有共享数据需求,可在此声明
# shared-data:

View 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
View 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
View 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
View 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
View 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;

View 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 $$;

View 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 $$;

View 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 $$;

View 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 $$;