# 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 # 应用端口 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=$(PORT); \ 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; \ 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; \ 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' @bash -c '\ 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; \ 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 "检查端口 $(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 [ -n "$$PID" ] && [ "$$PID" != "-" ] && [ "$$PID" != "0" ]; then \ echo "⚠️ 警告: 端口 $(PORT) 已被进程 $$PID 占用"; \ echo " 提示: 使用 'make kill-$(PORT)' 或 'make kill-port-force PORT=$(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; \ 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 "检查端口 $(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 [ -n "$$PID" ] && [ "$$PID" != "-" ] && [ "$$PID" != "0" ]; then \ echo "⚠️ 警告: 端口 $(PORT) 已被进程 $$PID 占用"; \ echo " 提示: 使用 'make kill-$(PORT)' 或 'make kill-port-force PORT=$(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; \ 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 " - \"$(PORT):$(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; \ 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; \ 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 && \ $(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; \ 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 && \ $(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=$(PORT)) @if [ -z "$(PORT)" ]; then \ echo "❌ 错误: 请指定端口号"; \ echo "用法: make kill-port 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"; \ 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=$(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-$(PORT) kill-$(PORT): ## 终止端口 $(PORT) 的进程(应用默认端口) @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: 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