GoTest/Makefile
Table 5424efd48a fix: 修复 Docker 模式下 SERVER_PORT 未生效的问题
- 将 Makefile 中的 PORT 变量重命名为 SERVER_PORT,更清晰地表示应用端口
- 修复 docker-up-dev/stage/prod 命令中的 sed 命令:
  * 将 $(PORT) 改为 $(SERVER_PORT)
  * 修改 sed 命令从 s/[0-9]\+/... 改为 s|port:.*|port: $(SERVER_PORT)|
  * 这样可以处理 config 文件中 port: 为空的情况
- 修复 dev/stage/prod 命令中的 sed 命令,统一使用新的格式
- 在 main.go 中增加端口为空检查:
  * 在启动服务器前检查 cfg.Server.Port 是否为空
  * 如果为空,输出明确的错误信息并退出,便于排查问题

问题原因:
- docker-up-dev 中使用的是 $(PORT) 而不是 $(SERVER_PORT)
- 当 port: 为空时,sed 的 s/[0-9]\+/... 无法匹配(需要数字才能匹配)
- 改为 s|port:.*|port: ...| 可以处理空值情况
2025-11-29 08:10:47 +08:00

559 lines
23 KiB
Makefile
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Makefile for Yinli API
# 变量定义
APP_NAME := yinli-api
VERSION := $(shell git tag --sort=-version:refname | head -n 1 2>/dev/null || echo "1.0.0")
BUILD_DIR := build
DOCKER_DIR := docker
DOC_DIR := doc
# Go 相关变量
GO := go
GOFMT := gofmt
GOVET := go vet
# Docker 相关变量
DOCKER := docker
DOCKER_COMPOSE := $(DOCKER) compose
# 应用端口
SERVER_PORT := 1234
# 默认目标
.PHONY: help
help: ## 显示帮助信息
@echo "可用的命令:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
# 开发环境
.PHONY: dev
dev: ## 启动开发环境(本地模式,使用 localhost
@echo "启动开发环境(本地模式)..."
@bash -c '\
PORT=$(SERVER_PORT); \
echo "检查端口 $(SERVER_PORT) 是否被占用..."; \
PID=""; \
if command -v lsof >/dev/null 2>&1; then \
PID=$$(lsof -ti:$$PORT 2>/dev/null | head -1); \
fi; \
if [ -z "$$PID" ] && command -v netstat >/dev/null 2>&1; then \
PID=$$(netstat -tlnp 2>/dev/null | grep ":$$PORT " | awk "{print \$$7}" | grep -E "^[0-9]+" | cut -d/ -f1 | head -1); \
fi; \
if [ -z "$$PID" ] && command -v ss >/dev/null 2>&1; then \
PID=$$(ss -tlnp 2>/dev/null | grep ":$$PORT " | grep -oP "pid=\K[0-9]+" | head -1); \
fi; \
CONTAINER=""; \
if command -v docker >/dev/null 2>&1; then \
CONTAINER=$$(docker ps --format "{{.Names}}" --filter "publish=$$PORT" 2>/dev/null | head -1); \
fi; \
if [ -n "$$PID" ] && [ "$$PID" != "-" ] && [ "$$PID" != "0" ]; then \
PROCESS_NAME=$$(ps -p $$PID -o comm= 2>/dev/null || echo "未知"); \
echo "⚠️ 警告: 端口 $$PORT 已被进程 $$PID 占用"; \
echo " 进程名称: $$PROCESS_NAME"; \
if [ -n "$$CONTAINER" ]; then \
echo " Docker 容器: $$CONTAINER"; \
echo " 提示: 使用 \"docker stop $$CONTAINER\" 或 \"make docker-down-dev\" 停止容器"; \
echo ""; \
echo -n " 是否自动停止 Docker 容器 $$CONTAINER? (y/N): "; \
read -t 5 CONFIRM || CONFIRM=""; \
if [ "$$CONFIRM" = "y" ] || [ "$$CONFIRM" = "Y" ]; then \
docker stop $$CONTAINER 2>/dev/null && echo "✅ 已停止容器 $$CONTAINER" || echo "❌ 停止容器失败"; \
sleep 2; \
PID=$$(lsof -ti:$$PORT 2>/dev/null | head -1 || echo ""); \
if [ -n "$$PID" ] && [ "$$PID" != "-" ] && [ "$$PID" != "0" ]; then \
echo "⚠️ 端口仍被占用,尝试终止进程 $$PID..."; \
kill -9 $$PID 2>/dev/null && echo "✅ 已终止进程 $$PID" || echo "❌ 终止进程失败"; \
sleep 1; \
fi; \
else \
echo " 继续启动(可能会失败)..."; \
fi; \
else \
echo " 提示: 使用 \"make kill-$$PORT\" 或 \"make kill-port-force PORT=$$PORT\" 终止该进程"; \
echo ""; \
echo -n " 是否自动终止进程 $$PID? (y/N): "; \
read -t 5 CONFIRM || CONFIRM=""; \
if [ "$$CONFIRM" = "y" ] || [ "$$CONFIRM" = "Y" ]; then \
kill -9 $$PID 2>/dev/null && echo "✅ 已终止进程 $$PID" || echo "❌ 终止进程失败,可能需要 sudo 权限"; \
sleep 1; \
else \
echo " 继续启动(可能会失败)..."; \
fi; \
fi; \
echo ""; \
elif [ -n "$$CONTAINER" ]; then \
echo "⚠️ 警告: 端口 $$PORT 被 Docker 容器 $$CONTAINER 占用"; \
echo " 提示: 使用 \"docker stop $$CONTAINER\" 或 \"make docker-down-dev\" 停止容器"; \
echo ""; \
echo -n " 是否自动停止 Docker 容器 $$CONTAINER? (y/N): "; \
read -t 5 CONFIRM || CONFIRM=""; \
if [ "$$CONFIRM" = "y" ] || [ "$$CONFIRM" = "Y" ]; then \
docker stop $$CONTAINER 2>/dev/null && echo "✅ 已停止容器 $$CONTAINER" || echo "❌ 停止容器失败"; \
sleep 2; \
else \
echo " 继续启动(可能会失败)..."; \
fi; \
echo ""; \
fi; \
echo "配置本地模式(使用 localhost..."; \
cp config/dev.yaml config/dev.yaml.bak 2>/dev/null || true; \
sed -i.tmp "s|host: host.docker.internal|host: localhost|g" config/dev.yaml; \
sed -i.tmp "/^server:/,/^[a-z][^ ]*:/ { /^ port:/ s|port:.*|port: $(SERVER_PORT)|; }" config/dev.yaml; \
trap "mv config/dev.yaml.bak config/dev.yaml 2>/dev/null; rm -f config/dev.yaml.tmp 2>/dev/null" INT TERM EXIT; \
$(GO) run src/main.go -env=dev; \
EXIT_CODE=$$?; \
mv config/dev.yaml.bak config/dev.yaml 2>/dev/null || true; \
rm -f config/dev.yaml.tmp 2>/dev/null || true; \
exit $$EXIT_CODE'
.PHONY: stage
stage: ## 启动预发布环境(本地模式,使用 localhost
@echo "启动预发布环境(本地模式)..."
@echo "检查端口 $(SERVER_PORT) 是否被占用..."
@PID=""; \
if command -v lsof >/dev/null 2>&1; then \
PID=$$(lsof -ti:$(SERVER_PORT) 2>/dev/null | head -1); \
fi; \
if [ -z "$$PID" ] && command -v netstat >/dev/null 2>&1; then \
PID=$$(netstat -tlnp 2>/dev/null | grep ":$(SERVER_PORT) " | awk '{print $$7}' | grep -E '^[0-9]+' | cut -d/ -f1 | head -1); \
fi; \
if [ -z "$$PID" ] && command -v ss >/dev/null 2>&1; then \
PID=$$(ss -tlnp 2>/dev/null | grep ":$(SERVER_PORT) " | grep -oP 'pid=\K[0-9]+' | head -1); \
fi; \
if [ -n "$$PID" ] && [ "$$PID" != "-" ] && [ "$$PID" != "0" ]; then \
echo "⚠️ 警告: 端口 $(SERVER_PORT) 已被进程 $$PID 占用"; \
echo " 提示: 使用 'make kill-$(SERVER_PORT)' 或 'make kill-port-force PORT=$(SERVER_PORT)' 终止该进程"; \
echo ""; \
fi; \
echo "配置本地模式(使用 localhost..."
@bash -c '\
cp config/stage.yaml config/stage.yaml.bak 2>/dev/null || true; \
sed -i.tmp "s|host: host.docker.internal|host: localhost|g" config/stage.yaml; \
sed -i.tmp "/^server:/,/^[a-z][^ ]*:/ { /^ port:/ s|port:.*|port: $(SERVER_PORT)|; }" config/stage.yaml; \
trap "mv config/stage.yaml.bak config/stage.yaml 2>/dev/null; rm -f config/stage.yaml.tmp 2>/dev/null" INT TERM EXIT; \
$(GO) run src/main.go -env=stage; \
EXIT_CODE=$$?; \
mv config/stage.yaml.bak config/stage.yaml 2>/dev/null || true; \
rm -f config/stage.yaml.tmp 2>/dev/null || true; \
exit $$EXIT_CODE'
.PHONY: prod
prod: ## 启动生产环境(本地模式,使用 localhost
@echo "启动生产环境(本地模式)..."
@echo "检查端口 $(SERVER_PORT) 是否被占用..."
@PID=""; \
if command -v lsof >/dev/null 2>&1; then \
PID=$$(lsof -ti:$(SERVER_PORT) 2>/dev/null | head -1); \
fi; \
if [ -z "$$PID" ] && command -v netstat >/dev/null 2>&1; then \
PID=$$(netstat -tlnp 2>/dev/null | grep ":$(SERVER_PORT) " | awk '{print $$7}' | grep -E '^[0-9]+' | cut -d/ -f1 | head -1); \
fi; \
if [ -z "$$PID" ] && command -v ss >/dev/null 2>&1; then \
PID=$$(ss -tlnp 2>/dev/null | grep ":$(SERVER_PORT) " | grep -oP 'pid=\K[0-9]+' | head -1); \
fi; \
if [ -n "$$PID" ] && [ "$$PID" != "-" ] && [ "$$PID" != "0" ]; then \
echo "⚠️ 警告: 端口 $(SERVER_PORT) 已被进程 $$PID 占用"; \
echo " 提示: 使用 'make kill-$(SERVER_PORT)' 或 'make kill-port-force PORT=$(SERVER_PORT)' 终止该进程"; \
echo ""; \
fi; \
echo "配置本地模式(使用 localhost..."
@bash -c '\
cp config/prod.yaml config/prod.yaml.bak 2>/dev/null || true; \
sed -i.tmp "s|host: host.docker.internal|host: localhost|g" config/prod.yaml; \
sed -i.tmp "/^server:/,/^[a-z][^ ]*:/ { /^ port:/ s|port:.*|port: $(SERVER_PORT)|; }" config/prod.yaml; \
trap "mv config/prod.yaml.bak config/prod.yaml 2>/dev/null; rm -f config/prod.yaml.tmp 2>/dev/null" INT TERM EXIT; \
$(GO) run src/main.go -env=prod; \
EXIT_CODE=$$?; \
mv config/prod.yaml.bak config/prod.yaml 2>/dev/null || true; \
rm -f config/prod.yaml.tmp 2>/dev/null || true; \
exit $$EXIT_CODE'
# 构建相关
.PHONY: build
build: ## 构建应用程序
@echo "构建应用程序..."
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -ldflags="-w -s" -o $(BUILD_DIR)/$(APP_NAME) src/main.go
.PHONY: build-windows
build-windows: ## 构建Windows版本
@echo "构建Windows版本..."
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build -ldflags="-w -s" -o $(BUILD_DIR)/$(APP_NAME).exe src/main.go
.PHONY: build-mac
build-mac: ## 构建macOS版本
@echo "构建macOS版本..."
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build -ldflags="-w -s" -o $(BUILD_DIR)/$(APP_NAME)-mac src/main.go
.PHONY: build-all
build-all: build build-windows build-mac ## 构建所有平台版本
# 依赖管理
.PHONY: deps
deps: ## 下载依赖
@echo "下载依赖..."
$(GO) mod download
.PHONY: deps-update
deps-update: ## 更新依赖
@echo "更新依赖..."
$(GO) mod tidy
$(GO) get -u ./...
# 代码质量
.PHONY: fmt
fmt: ## 格式化代码
@echo "格式化代码..."
$(GOFMT) -s -w .
.PHONY: vet
vet: ## 代码静态检查
@echo "代码静态检查..."
$(GOVET) ./...
.PHONY: check
check: fmt vet ## 执行所有代码检查
# 测试相关
.PHONY: test
test: ## 运行测试
@echo "运行测试..."
$(GO) test -v ./...
.PHONY: test-coverage
test-coverage: ## 运行测试并生成覆盖率报告
@echo "运行测试并生成覆盖率报告..."
@mkdir -p $(BUILD_DIR)
$(GO) test -v -coverprofile=$(BUILD_DIR)/coverage.out ./...
$(GO) tool cover -html=$(BUILD_DIR)/coverage.out -o $(BUILD_DIR)/coverage.html
@echo "覆盖率报告已生成: $(BUILD_DIR)/coverage.html"
.PHONY: test-race
test-race: ## 运行竞态检测测试
@echo "运行竞态检测测试..."
$(GO) test -race -v ./...
.PHONY: benchmark
benchmark: ## 运行基准测试
@echo "运行基准测试..."
$(GO) test -bench=. -benchmem ./...
# 文档生成
.PHONY: docs
docs: ## 生成API文档
@echo "生成API文档..."
@mkdir -p $(DOC_DIR)/dev $(DOC_DIR)/stage $(DOC_DIR)/prod
@echo "请先安装 swag: go install github.com/swaggo/swag/cmd/swag@latest"
@echo "然后运行: swag init -g src/main.go -o doc/dev --parseDependency --parseInternal"
.PHONY: docs-serve
docs-serve: docs ## 启动文档服务器
@echo "启动文档服务器..."
@cd $(DOC_DIR) && python3 -m http.server 8081
# Docker 相关
.PHONY: docker-build
docker-build: ## 构建Docker镜像
@echo "构建Docker镜像..."
$(DOCKER) build -t $(APP_NAME):$(VERSION) -t $(APP_NAME):latest .
.PHONY: docker-compose-dev
docker-compose-dev: ## 生成开发环境Docker Compose文件
@echo "生成开发环境Docker Compose文件..."
@mkdir -p $(DOCKER_DIR)
@HOST_IP=$$(hostname -I | awk '{print $$1}' 2>/dev/null || ip addr show | grep "inet " | grep -v "127.0.0.1" | head -1 | awk '{print $$2}' | cut -d/ -f1 || echo "192.168.1.11"); \
echo "services:" > $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " yinli-api:" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " build:" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " context: .." >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " dockerfile: Dockerfile" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " ports:" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " - \"$(SERVER_PORT):$(SERVER_PORT)\"" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " environment:" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " - APP_ENV=dev" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " volumes:" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " - ../config:/app/config:ro" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " extra_hosts:" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo " - \"host.docker.internal:$$HOST_IP\"" >> $(DOCKER_DIR)/docker-compose.dev.yml; \
echo "开发环境Docker Compose文件已生成: $(DOCKER_DIR)/docker-compose.dev.yml (使用宿主机 IP: $$HOST_IP)"; \
echo "注意: MySQL 和 Redis 需要单独部署,容器通过 host.docker.internal 访问宿主机上的服务"
.PHONY: docker-up-dev
docker-up-dev: docker-compose-dev ## 启动开发环境Docker容器
@echo "启动开发环境Docker容器..."
@echo "配置 Docker 模式(使用 host.docker.internal..."
@cp config/dev.yaml config/dev.yaml.bak 2>/dev/null || true; \
sed -i.tmp 's|host: localhost|host: host.docker.internal|g' config/dev.yaml; \
sed -i.tmp "/^server:/,/^[a-z][^ ]*:/ { /^ port:/ s|port:.*|port: $(SERVER_PORT)|; }" config/dev.yaml; \
HOST_IP=$$(hostname -I | awk '{print $$1}' 2>/dev/null || ip addr show | grep "inet " | grep -v "127.0.0.1" | head -1 | awk '{print $$2}' | cut -d/ -f1); \
if [ -z "$$HOST_IP" ]; then \
mv config/dev.yaml.bak config/dev.yaml 2>/dev/null || true; \
rm -f config/dev.yaml.tmp 2>/dev/null || true; \
echo "❌ 无法获取宿主机 IP 地址"; \
exit 1; \
fi; \
echo "使用宿主机 IP: $$HOST_IP"; \
cd $(DOCKER_DIR) && \
sed -i.tmp2 "s|host.docker.internal:[0-9.]*|host.docker.internal:$$HOST_IP|g" docker-compose.dev.yml && \
$(DOCKER_COMPOSE) -f docker-compose.dev.yml up -d && \
rm -f docker-compose.dev.yml.tmp2; \
mv config/dev.yaml.bak config/dev.yaml 2>/dev/null || true; \
rm -f config/dev.yaml.tmp 2>/dev/null || true
.PHONY: docker-up-stage
docker-up-stage: ## 启动预发布环境Docker容器
@echo "启动预发布环境Docker容器..."
@echo "配置 Docker 模式(使用 host.docker.internal..."
@cp config/stage.yaml config/stage.yaml.bak 2>/dev/null || true; \
sed -i.tmp 's|host: localhost|host: host.docker.internal|g' config/stage.yaml; \
sed -i.tmp "/^server:/,/^[a-z][^ ]*:/ { /^ port:/ s|port:.*|port: $(SERVER_PORT)|; }" config/stage.yaml; \
HOST_IP=$$(hostname -I | awk '{print $$1}' 2>/dev/null || ip addr show | grep "inet " | grep -v "127.0.0.1" | head -1 | awk '{print $$2}' | cut -d/ -f1); \
if [ -z "$$HOST_IP" ]; then \
mv config/stage.yaml.bak config/stage.yaml 2>/dev/null || true; \
rm -f config/stage.yaml.tmp 2>/dev/null || true; \
echo "❌ 无法获取宿主机 IP 地址"; \
exit 1; \
fi; \
echo "使用宿主机 IP: $$HOST_IP"; \
cd $(DOCKER_DIR) && \
sed -i.tmp2 "s|host.docker.internal:[0-9.]*|host.docker.internal:$$HOST_IP|g" docker-compose.stage.yml && \
sed -i.tmp2 "s|\"1234:1234\"|\"$(SERVER_PORT):$(SERVER_PORT)\"|g" docker-compose.stage.yml && \
$(DOCKER_COMPOSE) -f docker-compose.stage.yml up -d && \
rm -f docker-compose.stage.yml.tmp2; \
mv config/stage.yaml.bak config/stage.yaml 2>/dev/null || true; \
rm -f config/stage.yaml.tmp 2>/dev/null || true
.PHONY: docker-up-prod
docker-up-prod: ## 启动生产环境Docker容器
@echo "启动生产环境Docker容器..."
@echo "警告: 请确保已设置 MYSQL_ROOT_PASSWORD 和 REDIS_PASSWORD 环境变量"
@echo "配置 Docker 模式(使用 host.docker.internal..."
@cp config/prod.yaml config/prod.yaml.bak 2>/dev/null || true; \
sed -i.tmp 's|host: localhost|host: host.docker.internal|g' config/prod.yaml; \
sed -i.tmp "/^server:/,/^[a-z][^ ]*:/ { /^ port:/ s|port:.*|port: $(SERVER_PORT)|; }" config/prod.yaml; \
HOST_IP=$$(hostname -I | awk '{print $$1}' 2>/dev/null || ip addr show | grep "inet " | grep -v "127.0.0.1" | head -1 | awk '{print $$2}' | cut -d/ -f1); \
if [ -z "$$HOST_IP" ]; then \
mv config/prod.yaml.bak config/prod.yaml 2>/dev/null || true; \
rm -f config/prod.yaml.tmp 2>/dev/null || true; \
echo "❌ 无法获取宿主机 IP 地址"; \
exit 1; \
fi; \
echo "使用宿主机 IP: $$HOST_IP"; \
cd $(DOCKER_DIR) && \
sed -i.tmp2 "s|host.docker.internal:[0-9.]*|host.docker.internal:$$HOST_IP|g" docker-compose.prod.yml && \
sed -i.tmp2 "s|\"1234:1234\"|\"$(SERVER_PORT):$(SERVER_PORT)\"|g" docker-compose.prod.yml && \
sed -i.tmp2 "s|http://localhost:[0-9]*/health|http://localhost:$(SERVER_PORT)/health|g" docker-compose.prod.yml && \
$(DOCKER_COMPOSE) -f docker-compose.prod.yml up -d && \
rm -f docker-compose.prod.yml.tmp2; \
mv config/prod.yaml.bak config/prod.yaml 2>/dev/null || true; \
rm -f config/prod.yaml.tmp 2>/dev/null || true
.PHONY: docker-down
docker-down: ## 停止并移除所有Docker容器
@echo "停止并移除Docker容器..."
@cd $(DOCKER_DIR) && \
OUTPUT=$$($(DOCKER_COMPOSE) -f docker-compose.dev.yml -f docker-compose.stage.yml -f docker-compose.prod.yml down --remove-orphans 2>&1); \
echo "$$OUTPUT"; \
if echo "$$OUTPUT" | grep -q "permission denied"; then \
echo ""; \
echo "⚠️ 检测到权限错误,自动重启 rootless Docker 服务并重试..."; \
systemctl --user restart docker >/dev/null 2>&1 || true; \
sleep 3; \
echo "重试停止容器..."; \
$(DOCKER_COMPOSE) -f docker-compose.dev.yml -f docker-compose.stage.yml -f docker-compose.prod.yml down --remove-orphans 2>&1 || { \
echo ""; \
echo "❌ 仍然失败,请手动执行: systemctl --user restart docker && make docker-down"; \
exit 0; \
}; \
fi
.PHONY: docker-down-dev
docker-down-dev: ## 停止并移除开发环境Docker容器
@echo "停止并移除开发环境Docker容器..."
@cd $(DOCKER_DIR) && $(DOCKER_COMPOSE) -f docker-compose.dev.yml down --remove-orphans 2>&1 || { \
echo ""; \
echo "⚠️ 如果遇到权限错误permission denied容器可能由 root 用户创建。"; \
echo " 请手动执行: sudo docker compose -f docker/docker-compose.dev.yml down"; \
exit 0; \
}
.PHONY: docker-down-stage
docker-down-stage: ## 停止并移除预发布环境Docker容器
@echo "停止并移除预发布环境Docker容器..."
@cd $(DOCKER_DIR) && \
OUTPUT=$$($(DOCKER_COMPOSE) -f docker-compose.stage.yml down --remove-orphans 2>&1); \
echo "$$OUTPUT"; \
if echo "$$OUTPUT" | grep -q "permission denied"; then \
echo ""; \
echo "⚠️ 检测到权限错误,自动重启 rootless Docker 服务并重试..."; \
systemctl --user restart docker >/dev/null 2>&1 || true; \
sleep 3; \
echo "重试停止容器..."; \
$(DOCKER_COMPOSE) -f docker-compose.stage.yml down --remove-orphans 2>&1 || { \
echo ""; \
echo "❌ 仍然失败,请手动执行: systemctl --user restart docker && make docker-down-stage"; \
exit 0; \
}; \
fi
.PHONY: docker-down-prod
docker-down-prod: ## 停止并移除生产环境Docker容器
@echo "停止并移除生产环境Docker容器..."
@cd $(DOCKER_DIR) && \
if ! $(DOCKER_COMPOSE) -f docker-compose.prod.yml down --remove-orphans 2>&1 | tee /dev/stderr | grep -q "permission denied"; then \
:; \
else \
echo ""; \
echo "⚠️ 检测到权限错误,自动重启 rootless Docker 服务并重试..."; \
systemctl --user restart docker >/dev/null 2>&1 || true; \
sleep 3; \
echo "重试停止容器..."; \
$(DOCKER_COMPOSE) -f docker-compose.prod.yml down --remove-orphans 2>&1 || { \
echo ""; \
echo "❌ 仍然失败,请手动执行: systemctl --user restart docker && make docker-down-prod"; \
exit 0; \
}; \
fi
.PHONY: docker-logs
docker-logs: ## 查看开发环境Docker容器日志
@echo "查看开发环境Docker容器日志..."
cd $(DOCKER_DIR) && $(DOCKER_COMPOSE) -f docker-compose.dev.yml logs -f
.PHONY: docker-logs-stage
docker-logs-stage: ## 查看预发布环境Docker容器日志
@echo "查看预发布环境Docker容器日志..."
cd $(DOCKER_DIR) && $(DOCKER_COMPOSE) -f docker-compose.stage.yml logs -f
.PHONY: docker-logs-prod
docker-logs-prod: ## 查看生产环境Docker容器日志
@echo "查看生产环境Docker容器日志..."
cd $(DOCKER_DIR) && $(DOCKER_COMPOSE) -f docker-compose.prod.yml logs -f
.PHONY: docker-ps
docker-ps: ## 查看所有Docker容器状态
@echo "查看所有Docker容器状态..."
cd $(DOCKER_DIR) && $(DOCKER_COMPOSE) -f docker-compose.dev.yml -f docker-compose.stage.yml -f docker-compose.prod.yml ps
# 端口管理
.PHONY: kill-port
kill-port: ## 终止指定端口的进程 (用法: make kill-port PORT=$(SERVER_PORT))
@if [ -z "$(PORT)" ]; then \
echo "❌ 错误: 请指定端口号"; \
echo "用法: make kill-port PORT=$(SERVER_PORT)"; \
exit 1; \
fi; \
echo "查找端口 $(PORT) 的进程..."; \
PID=""; \
if command -v lsof >/dev/null 2>&1; then \
PID=$$(lsof -ti:$(PORT) 2>/dev/null | head -1); \
fi; \
if [ -z "$$PID" ] && command -v netstat >/dev/null 2>&1; then \
PID=$$(netstat -tlnp 2>/dev/null | grep ":$(PORT) " | awk '{print $$7}' | grep -E '^[0-9]+' | cut -d/ -f1 | head -1); \
fi; \
if [ -z "$$PID" ] && command -v ss >/dev/null 2>&1; then \
PID=$$(ss -tlnp 2>/dev/null | grep ":$(PORT) " | grep -oP 'pid=\K[0-9]+' | head -1); \
fi; \
if [ -z "$$PID" ] || [ "$$PID" = "-" ] || [ "$$PID" = "0" ]; then \
echo "✅ 端口 $(PORT) 未被占用"; \
exit 0; \
fi; \
echo "找到进程 PID: $$PID"; \
PROCESS_INFO=$$(ps -p $$PID -o pid,cmd --no-headers 2>/dev/null || echo "无法获取进程信息"); \
echo "进程信息: $$PROCESS_INFO"; \
read -p "是否终止进程 $$PID? (y/N): " CONFIRM; \
if [ "$$CONFIRM" = "y" ] || [ "$$CONFIRM" = "Y" ]; then \
kill -9 $$PID 2>/dev/null && echo "✅ 已终止进程 $$PID" || echo "❌ 终止进程失败"; \
else \
echo "已取消"; \
fi
.PHONY: kill-port-force
kill-port-force: ## 强制终止指定端口的进程,无需确认 (用法: make kill-port-force PORT=$(SERVER_PORT))
@if [ -z "$(PORT)" ]; then \
echo "❌ 错误: 请指定端口号"; \
echo "用法: make kill-port-force PORT=$(PORT)"; \
exit 1; \
fi; \
echo "查找端口 $(PORT) 的进程..."; \
PID=""; \
if command -v lsof >/dev/null 2>&1; then \
PID=$$(lsof -ti:$(PORT) 2>/dev/null | head -1); \
fi; \
if [ -z "$$PID" ] && command -v netstat >/dev/null 2>&1; then \
PID=$$(netstat -tlnp 2>/dev/null | grep ":$(PORT) " | awk '{print $$7}' | grep -E '^[0-9]+' | cut -d/ -f1 | head -1); \
fi; \
if [ -z "$$PID" ] && command -v ss >/dev/null 2>&1; then \
PID=$$(ss -tlnp 2>/dev/null | grep ":$(PORT) " | grep -oP 'pid=\K[0-9]+' | head -1); \
fi; \
if [ -z "$$PID" ] || [ "$$PID" = "-" ] || [ "$$PID" = "0" ]; then \
echo "✅ 端口 $(PORT) 未被占用"; \
exit 0; \
fi; \
echo "找到进程 PID: $$PID"; \
PROCESS_INFO=$$(ps -p $$PID -o pid,cmd --no-headers 2>/dev/null || echo "无法获取进程信息"); \
echo "进程信息: $$PROCESS_INFO"; \
kill -9 $$PID 2>/dev/null && echo "✅ 已强制终止进程 $$PID" || echo "❌ 终止进程失败"
.PHONY: kill-$(SERVER_PORT)
kill-$(SERVER_PORT): ## 终止端口 $(SERVER_PORT) 的进程(应用默认端口)
@echo "终止端口 $(SERVER_PORT) 的进程..."
@PID=""; \
if command -v lsof >/dev/null 2>&1; then \
PID=$$(lsof -ti:$(SERVER_PORT) 2>/dev/null | head -1); \
fi; \
if [ -z "$$PID" ] && command -v netstat >/dev/null 2>&1; then \
PID=$$(netstat -tlnp 2>/dev/null | grep ":$(SERVER_PORT) " | awk '{print $$7}' | grep -E '^[0-9]+' | cut -d/ -f1 | head -1); \
fi; \
if [ -z "$$PID" ] && command -v ss >/dev/null 2>&1; then \
PID=$$(ss -tlnp 2>/dev/null | grep ":$(SERVER_PORT) " | grep -oP 'pid=\K[0-9]+' | head -1); \
fi; \
if [ -z "$$PID" ] || [ "$$PID" = "-" ] || [ "$$PID" = "0" ]; then \
echo "✅ 端口 $(SERVER_PORT) 未被占用"; \
exit 0; \
fi; \
echo "找到进程 PID: $$PID"; \
PROCESS_INFO=$$(ps -p $$PID -o pid,cmd --no-headers 2>/dev/null || echo "无法获取进程信息"); \
echo "进程信息: $$PROCESS_INFO"; \
kill -9 $$PID 2>/dev/null && echo "✅ 已终止进程 $$PID" || echo "❌ 终止进程失败"
# 清理
.PHONY: clean
clean: ## 清理构建文件
@echo "清理构建文件..."
rm -rf $(BUILD_DIR)
rm -rf $(DOC_DIR)
$(GO) clean
.PHONY: clean-docker
clean-docker: ## 清理Docker资源
@echo "清理Docker资源..."
$(DOCKER) system prune -f
$(DOCKER) volume prune -f
# 安装工具
.PHONY: install-tools
install-tools: ## 安装开发工具
@echo "安装开发工具..."
$(GO) install github.com/swaggo/swag/cmd/swag@latest
$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# 全流程
.PHONY: all
all: clean deps check test build docs ## 执行完整的构建流程
.PHONY: ci
ci: deps check test-coverage ## CI流程
# 版本管理
.PHONY: version
version: ## 显示版本信息
@echo "应用名称: $(APP_NAME)"
@echo "版本: $(VERSION)"
@echo "Go版本: $(shell $(GO) version)"
# 默认目标
.DEFAULT_GOAL := help