GoTest/Makefile
Table 84055fecfe feat: 优化 API 文档生成和 Swagger UI 服务
- 优化 make docs 命令:
  * 自动检查并安装 swag 工具
  * 自动过滤 swag 生成过程中的警告信息
  * 自动修复生成的 docs.go 文件中的兼容性问题(移除 LeftDelim 和 RightDelim 字段)

- 优化 make docs-serve 命令:
  * 使用 Docker 运行 Swagger UI 容器,提供完整的 Swagger UI 界面
  * 新增 make docs-stop 命令,用于停止 Swagger UI 容器
  * 引入 SWAGGER_PORT 变量统一管理 Swagger UI 端口配置(默认 8081)

- 修复应用内置 Swagger UI 无法加载文档问题:
  * 在 main.go 中导入生成的 docs 包(_ "yinli-api/doc/dev")
  * 修复 docs.go 文件中的兼容性问题
  * 解决 "Failed to load API definition" 错误

- 更新 CHANGELOG.md,记录所有变更
2025-11-29 20:07:24 +08:00

627 lines
26 KiB
Makefile
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
# Swagger UI 端口
SWAGGER_PORT := 8081
# 默认目标
.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
@if ! command -v swag >/dev/null 2>&1; then \
echo "⚠️ swag 未安装,正在安装..."; \
$(GO) install github.com/swaggo/swag/cmd/swag@latest; \
if ! command -v swag >/dev/null 2>&1; then \
echo "❌ 安装失败,请手动执行: go install github.com/swaggo/swag/cmd/swag@latest"; \
echo " 然后运行: swag init -g src/main.go -o doc/dev --dir src --parseDependency --parseInternal"; \
exit 1; \
fi; \
fi; \
echo "✅ swag 已安装,开始生成文档..."; \
swag init -g src/main.go -o $(DOC_DIR)/dev --parseDependency --parseInternal 2>&1 | \
grep -vE "(failed to evaluate const mProfCycleWrap|failed to get package name in dir: ./|cannot find all dependencies|^$$)" || true; \
if [ -f "$(DOC_DIR)/dev/swagger.json" ]; then \
echo "修复生成的 docs.go 文件兼容性问题..."; \
sed -i.tmp '/^[[:space:]]*LeftDelim:/d; /^[[:space:]]*RightDelim:/d' $(DOC_DIR)/dev/docs.go 2>/dev/null || true; \
rm -f $(DOC_DIR)/dev/docs.go.tmp 2>/dev/null || true; \
echo "✅ API 文档已生成到 $(DOC_DIR)/dev/"; \
else \
echo "❌ 文档生成失败"; \
exit 1; \
fi
.PHONY: docs-serve
docs-serve: docs ## 启动 Swagger UI 文档服务器(使用 Docker
@echo "启动 Swagger UI 文档服务器..."
@if command -v docker >/dev/null 2>&1; then \
if $(DOCKER) ps -a --format "{{.Names}}" | grep -q "^swagger-ui$$"; then \
echo "⚠️ Swagger UI 容器已存在,正在停止旧容器..."; \
$(DOCKER) stop swagger-ui >/dev/null 2>&1; \
$(DOCKER) rm swagger-ui >/dev/null 2>&1; \
fi; \
echo "使用 Docker 启动 Swagger UI..."; \
cd $(DOC_DIR)/dev && \
$(DOCKER) run --rm -d \
--name swagger-ui \
-p $(SWAGGER_PORT):8080 \
-e SWAGGER_JSON=/doc/swagger.json \
-v $$(pwd):/doc \
swaggerapi/swagger-ui:latest >/dev/null 2>&1 && \
sleep 2 && \
echo "✅ Swagger UI 已启动: http://localhost:$(SWAGGER_PORT)"; \
echo ""; \
echo "提示:"; \
echo " - 停止服务器: make docs-stop"; \
echo " - 或直接启动应用访问: http://localhost:$(SERVER_PORT)/swagger/index.html"; \
echo ""; \
echo "按 Ctrl+C 退出(容器将继续运行,使用 'make docs-stop' 停止)"; \
trap 'echo ""; echo "提示: 使用 make docs-stop 停止 Swagger UI 容器"; exit' INT TERM; \
while true; do sleep 1; done; \
else \
echo "⚠️ Docker 未安装"; \
echo ""; \
echo "方案 1: 安装 Docker 后使用: make docs-serve"; \
echo "方案 2: 启动应用后访问: http://localhost:$(SERVER_PORT)/swagger/index.html"; \
echo ""; \
echo "当前提供静态文件服务(仅文件列表): http://localhost:$(SWAGGER_PORT)"; \
cd $(DOC_DIR)/dev && python3 -m http.server $(SWAGGER_PORT); \
fi
.PHONY: docs-stop
docs-stop: ## 停止 Swagger UI 文档服务器
@if command -v docker >/dev/null 2>&1; then \
if $(DOCKER) ps -a --format "{{.Names}}" | grep -q "^swagger-ui$$"; then \
$(DOCKER) stop swagger-ui >/dev/null 2>&1 && \
echo "✅ Swagger UI 容器已停止" || \
echo "❌ 停止 Swagger UI 容器失败"; \
else \
echo " Swagger UI 容器未运行"; \
fi; \
else \
echo "⚠️ Docker 未安装"; \
fi
# 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-dev
docker-logs-dev: ## 查看开发环境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