保留最新代码,清除历史记录

This commit is contained in:
vipg
2025-10-09 18:19:57 +08:00
commit b44e40f25d
77 changed files with 4115 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/

56
.gitignore vendored Normal file
View File

@@ -0,0 +1,56 @@
# 编译产物
*.o
*.a
*.so
*.exe
*.dll
*.dylib
# Go 编译生成的中间文件和二进制文件
__debug_bin
go.mod.sum
*.test
*.out
# 模块缓存
go/pkg/
# 构建目录(常用于存放编译后的二进制文件)
bin/
build/
dist/
deploy/images
deploy/shared_data
# 临时文件目录
tmp/
temp/
shared_data/
# 日志文件
*.log
logs/
# IDE 和编辑器配置
.idea/ # JetBrains 系列GoLand、IntelliJ 等)
.vscode/ # VS Code
*.swp # Vim 交换文件
*.swo # Vim 交换文件
.DS_Store # macOS 系统文件
Thumbs.db # Windows 系统文件
# 测试相关
coverage.out # 测试覆盖率文件
testdata/ # 测试数据(如果不需要提交)
# 依赖管理工具生成的文件
vendor/ # 旧版依赖管理Go Modules 时代可不提交)
Gopkg.lock # dep 工具
Gopkg.toml # dep 工具
# 其他临时文件
*.bak
*.tmp
*~
.DS_Store
.DS_Store

130
README.md Normal file
View File

@@ -0,0 +1,130 @@
# 用户服务部署使用手册
## 一、概述
本文档介绍用户服务系统的创建、构建和部署流程,涉及`create_api.py``deploy/deploy.sh`两个核心脚本的使用方法帮助使用者快速搭建和部署用户服务相关的API模块。
## 二、环境准备
1. 确保系统安装以下工具:
- Python 3.x
- Docker
- Docker Compose
2. 克隆代码库后,进入项目根目录:
```bash
cd user_service
```
## 三、核心脚本功能说明
### 1. 创建新API服务create_api.py
用于基于模板快速创建新的API服务模块自动生成目录结构和配置文件。
#### 使用方法:
1. 编辑`create_api.py`文件,设置`a`(功能名)和`b`(次功能名)参数:
```python
a = "aaaa" # 主功能名
b = "bbbb" # 次功能名(可选,如果不需要,留空)
```
2. 执行脚本:
```bash
sudo python3 create_api.py
```
#### 功能说明:
- 自动在`deploy/api`目录下创建新服务目录(格式:`api_{a}_{b}` 或 `api_{a}`
- 复制`api_template`模板文件并替换其中的占位符
- 生成对应的`docker-compose.{a}.{b}.yaml`或`docker-compose.{a}.yaml`文件
- 自动修改`release.sh`、`init.py`和`README.md`中的服务标识
### 2. 部署服务deploy/deploy.sh
用于停止现有服务、构建API并启动新的Docker Compose编排。
#### 使用方法:
```bash
cd deploy
sudo sh deploy.sh
```
#### 功能说明:
1. **步骤1停止现有服务**
- 停止`docker-compose.yaml`定义的`user_service`项目
2. **步骤2构建所有API服务**
- 遍历`./api`目录下的所有服务
- 执行每个服务的`release.sh`脚本
- 若构建失败则终止部署流程
3. **步骤3启动新服务**
- 使用`docker-compose.yaml`启动服务
- 项目名称为`user_service`
#### 配置说明:
脚本开头可修改以下配置参数:
```bash
COMPOSE_PROJECT_NAME="user_service" # Docker Compose项目名称
DOCKER_COMPOSE_FILE="./docker-compose.yaml" # 配置文件路径
API_DIR="./api" # API服务所在目录
```
## 四、完整工作流程
1. **创建新API服务**
```bash
# 编辑create_api.py设置功能名
sudo python3 create_api.py
```
2. **开发API功能**
- 在生成的`deploy/api/api_{a}_{b}`或`deploy/api/api_{a}`目录中编写代码(如`main.go`
- 根据需要修改`init.py`中的初始化逻辑
3. **构建服务镜像**
```bash
python3 build.py
```
4. **部署服务**
```bash
cd deploy
./deploy.sh
```
## 五、常见问题处理
1. **权限错误**
- 执行脚本时若出现`Permission denied`,尝试添加执行权限:
```bash
chmod +x deploy/deploy.sh
```
- 或使用`sudo`运行相关命令
2. **镜像构建失败**
- 检查`release.sh`中的镜像名称和构建命令是否正确
- 确保Docker服务正常运行`systemctl status docker`
3. **服务启动失败**
- 检查`docker-compose.yaml`配置是否正确
- 查看容器日志排查问题:`docker logs <容器名>`
4. **端口冲突**
- 若启动时提示端口被占用,修改`init.py`中的端口映射配置
## 六、服务目录结构说明
成功创建并部署后,项目主要目录结构如下:
```
user_service_副本/
├── deploy/
│ ├── api/ # 所有API服务目录
│ │ ├── api_login/ # 登录服务
│ │ ├── api_register/ # 注册服务
│ │ └── ... # 其他API服务
│ ├── docker-compose.yaml # 服务编排配置
│ └── deploy.sh # 部署脚本
└── create_api.py # 创建新API的脚本
```

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/

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 .

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

69
api_template/init.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 30001: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()

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

136
create_api.py Normal file
View File

@@ -0,0 +1,136 @@
import os
import shutil
# 1. 定义全局参数
a = "gateway1" # 功能名
b = "" # 次功能名
def main():
# 确定新目录名称
if b:
new_dir_name = f"api_{a}_{b}"
else:
new_dir_name = f"api_{a}"
# 目标目录调整为当前目录的deploy/api
new_dir = os.path.join("deploy", "api", new_dir_name)
# 确保deploy/api目录存在
os.makedirs(os.path.join("deploy", "api"), exist_ok=True)
# 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-5. 处理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}")
with open(new_yaml, 'w', encoding='utf-8') as f:
f.write(content)
print(f"已更新yaml文件内容")
else:
print(f"警告:未找到 {temp_yaml} 文件跳过yaml处理")
# 6-8. 处理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}-")
with open(readme_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"已更新README.md内容")
else:
print(f"警告:未找到 {readme_path} 文件跳过README处理")
# 9. 处理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处理")
# 10-11. 处理init.py移除端口号修改
init_path = os.path.join(new_dir, "init.py")
if os.path.exists(init_path):
with open(init_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} &&")
with open(init_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"已更新init.py内容")
else:
print(f"警告:未找到 {init_path} 文件跳过init.py处理")
if __name__ == "__main__":
main()

15
deploy/.env Normal file
View File

@@ -0,0 +1,15 @@
# 数据库配置
DB_USER=postgres
DB_PASSWORD=postgres12341234
DB_NAME=postgres
DB_PORT=5432
# 时区配置
TZ=Asia/Shanghai
# pgAdmin配置
PGADMIN_EMAIL=fish@fish.com
PGADMIN_PASSWORD=12345678
# 网关端口
GATEWAY_PORT=80

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/

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 .

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

View File

@@ -0,0 +1,41 @@
# ==================== 第一阶段构建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 ./
COPY go.mod ./
RUN go mod tidy
# 下载项目依赖仅当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,42 @@
module user_delete
go 1.25.0
require (
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
)

View File

@@ -0,0 +1,91 @@
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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 30001: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,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: "账号删除成功。",
})
}

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

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/

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 .

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

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,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
)

View File

@@ -0,0 +1,91 @@
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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 30001: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,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)
}
}

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

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/

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 .

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

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,42 @@
module user_login
go 1.25.0
require (
github.com/gin-gonic/gin v1.11.0
github.com/lib/pq v1.10.9
golang.org/x/crypto v0.43.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.8 // 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.2 // 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-20180228061459-e0a39a4cb421 // 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.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

View File

@@ -0,0 +1,90 @@
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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 30001: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,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
deploy/api/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

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/

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 .

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

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,42 @@
module user_register
go 1.25.0
require (
github.com/gin-gonic/gin v1.11.0
github.com/lib/pq v1.10.9
golang.org/x/crypto v0.43.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.8 // 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.2 // 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-20180228061459-e0a39a4cb421 // 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.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

View File

@@ -0,0 +1,90 @@
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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 30001: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,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)
}

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

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/

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

View File

@@ -0,0 +1,42 @@
module user_update_account
go 1.25.0
require (
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.8 // 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.2 // 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-20180228061459-e0a39a4cb421 // 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
)

View File

@@ -0,0 +1,90 @@
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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 30001: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,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)
}

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,26 @@
# 排除Go编译产物
*.exe
*.exe~
*.dll
*.so
*.dylib
app # 排除本地已构建的二进制文件
# 排除依赖目录
vendor/
go/pkg/
db
scripts
shared_data
# 排除版本控制和日志文件
.git/
.gitignore
logs/
*.log
*.md
*.sql
# 排除IDE配置文件
.idea/
.vscode/

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,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,42 @@
module user_update_password
go 1.25.0
require (
github.com/gin-gonic/gin v1.11.0
github.com/lib/pq v1.10.9
golang.org/x/crypto v0.43.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.8 // 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.2 // 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-20180228061459-e0a39a4cb421 // 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.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

View File

@@ -0,0 +1,90 @@
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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 30001: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,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: "密码更新成功",
})
}

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

82
deploy/deploy.sh Normal file
View File

@@ -0,0 +1,82 @@
#!/bin/bash
set -e # 遇到错误立即退出,确保部署流程的可靠性
# 颜色输出配置(增强可读性)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # 重置颜色
COMPOSE_PROJECT_NAME="user_service"
DOCKER_COMPOSE_FILE="./docker-compose.yaml"
API_DIR="./api" # API服务所在目录
# 步骤1停止指定名称的docker-compose编排
echo -e "${YELLOW}===== 第一步:停止编排名称为 $COMPOSE_PROJECT_NAME 的服务 ====="${NC}
if [ -f "$DOCKER_COMPOSE_FILE" ]; then
echo "正在停止服务..."
docker-compose -f "$DOCKER_COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" down
if [ $? -eq 0 ]; then
echo -e "${GREEN}编排 $COMPOSE_PROJECT_NAME 已成功停止${NC}"
else
echo -e "${RED}停止编排 $COMPOSE_PROJECT_NAME 失败!${NC}"
exit 1
fi
else
echo -e "${RED}错误:未找到 $DOCKER_COMPOSE_FILE 文件${NC}"
exit 1
fi
# 步骤2处理所有API服务的release.sh
echo -e "\n${YELLOW}===== 第二步执行所有API服务的构建脚本 ====="${NC}
if [ -d "$API_DIR" ]; then
# 遍历API目录下的所有子文件夹
for api_folder in "$API_DIR"/*/; do
# 检查是否为目录
if [ -d "$api_folder" ]; then
# 提取文件夹名称(用于日志显示)
folder_name=$(basename "$api_folder")
echo -e "\n${YELLOW}处理服务: $folder_name${NC}"
# 检查release.sh是否存在
release_script="${api_folder}/release.sh"
if [ -f "$release_script" ]; then
echo "给予 $release_script 执行权限..."
chmod +x "$release_script"
echo "执行 $release_script ..."
(cd "$api_folder" && ./release.sh) # 进入目录并执行脚本
if [ $? -eq 0 ]; then
echo -e "${GREEN}$folder_name 服务构建完成${NC}"
else
echo -e "${RED}$folder_name 服务构建失败!${NC}"
exit 1
fi
else
echo -e "${YELLOW}警告:$api_folder 中未找到 release.sh跳过该目录${NC}"
fi
fi
done
else
echo -e "${RED}错误:未找到 $API_DIR 目录${NC}"
exit 1
fi
# 步骤3启动指定名称的docker-compose编排
echo -e "\n${YELLOW}===== 第三步:启动编排名称为 $COMPOSE_PROJECT_NAME 的服务 ====="${NC}
if [ -f "$DOCKER_COMPOSE_FILE" ]; then
echo "正在启动服务..."
docker-compose -f "$DOCKER_COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" up -d
if [ $? -eq 0 ]; then
echo -e "${GREEN}编排 $COMPOSE_PROJECT_NAME 启动成功!${NC}"
else
echo -e "${RED}启动编排 $COMPOSE_PROJECT_NAME 失败!${NC}"
exit 1
fi
else
echo -e "${RED}错误:未找到 $DOCKER_COMPOSE_FILE 文件${NC}"
exit 1
fi
echo -e "\n${GREEN}===== 所有部署步骤已完成 ====="${NC}

122
deploy/docker-compose.yaml Normal file
View File

@@ -0,0 +1,122 @@
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
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
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_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}
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_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_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}
networks:
user-network:
driver: bridge
volumes: {}

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

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

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

122
docker-compose.yaml Normal file
View File

@@ -0,0 +1,122 @@
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
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
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_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}
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_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_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}
networks:
user-network:
driver: bridge
volumes: {}