Compare commits
No commits in common. "03e407879ea1d8a3499c56a0e0fc4bed4ca0ad51" and "0cf352ffa283b72c04fa052b015a3f9a0a60285a" have entirely different histories.
03e407879e
...
0cf352ffa2
45
.air.toml
45
.air.toml
@ -1,45 +0,0 @@
|
||||
# Air 配置文件 - 用于热重载开发
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = ["-env=dev"]
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ./cmd/main.go"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "build", "doc", "docker"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_root = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
@ -1,65 +0,0 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
doc/
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool
|
||||
*.out
|
||||
coverage.html
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
|
||||
# Development files
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Node modules (if any)
|
||||
node_modules/
|
||||
|
||||
# Test files
|
||||
test/
|
||||
*_test.go
|
||||
83
.gitignore
vendored
83
.gitignore
vendored
@ -1,83 +0,0 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Build directory
|
||||
build/
|
||||
dist/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Documentation build
|
||||
doc/dev/
|
||||
doc/stage/
|
||||
doc/prod/
|
||||
|
||||
# Docker volumes
|
||||
docker/data/
|
||||
|
||||
# Air (live reload) temporary files
|
||||
tmp/
|
||||
|
||||
# Test cache
|
||||
.testcache/
|
||||
|
||||
# Go module cache
|
||||
go.sum.backup
|
||||
106
CHANGELOG.md
106
CHANGELOG.md
@ -1,106 +0,0 @@
|
||||
# 更新日志
|
||||
|
||||
本文档记录了项目的所有重要变更,按照版本号倒序排列。
|
||||
|
||||
## [未发布]
|
||||
|
||||
### 🆕 新增功能
|
||||
- **端口配置变量化**:在 Makefile 中引入 `SERVER_PORT` 变量,统一管理应用端口配置
|
||||
- **端口进程管理**:新增 `kill-port`、`kill-port-force`、`kill-$(SERVER_PORT)` 命令,方便管理端口占用
|
||||
- **动态配置更新**:支持在启动时自动更新配置文件中的端口和主机地址
|
||||
|
||||
### 🔧 改进优化
|
||||
- **项目结构重构**:将 `@internal` 目录重命名为 `src`,`@pkg` 目录移至 `src/pkg`,`cmd/main.go` 移至 `src/main.go`
|
||||
- **版本管理优化**:Makefile 中的 `VERSION` 变量自动从最新的 git tag 读取
|
||||
- **端口检查逻辑优化**:
|
||||
- 支持检测 Docker 容器占用端口
|
||||
- 提供自动停止 Docker 容器的选项
|
||||
- 改进端口占用检测的准确性,过滤无效 PID
|
||||
- **Docker Compose 配置优化**:
|
||||
- 移除 MySQL 和 Redis 服务定义,改为使用宿主机服务
|
||||
- 支持动态获取宿主机 IP 并配置 `extra_hosts`
|
||||
- 为 MySQL 服务添加优雅关闭配置(`stop_grace_period`、`stop_signal`、`init`)
|
||||
- **配置管理优化**:
|
||||
- 支持本地模式和 Docker 模式的动态切换
|
||||
- `make dev/stage/prod` 使用 `localhost`
|
||||
- `make docker-up-*` 使用 `host.docker.internal`
|
||||
- **Docker 命令命名优化**:
|
||||
- 将 `docker-logs` 重命名为 `docker-logs-dev`,保持与 `docker-logs-stage` 和 `docker-logs-prod` 的命名一致性
|
||||
- 统一日志查看命令命名规范
|
||||
- **API 文档生成优化**:
|
||||
- `make docs` 命令自动检查并安装 swag 工具
|
||||
- 自动过滤 swag 生成过程中的警告信息
|
||||
- 自动修复生成的 `docs.go` 文件中的兼容性问题
|
||||
- **Swagger UI 服务优化**:
|
||||
- `make docs-serve` 使用 Docker 运行 Swagger UI 容器,提供完整的 Swagger UI 界面
|
||||
- 新增 `make docs-stop` 命令,用于停止 Swagger UI 容器
|
||||
- 引入 `SWAGGER_PORT` 变量统一管理 Swagger UI 端口配置
|
||||
|
||||
### 🐛 问题修复
|
||||
- **修复 Docker 模式下 SERVER_PORT 未生效问题**:
|
||||
- 将 `PORT` 变量重命名为 `SERVER_PORT`
|
||||
- 修复 `sed` 命令无法处理空端口值的问题
|
||||
- 在 `main.go` 中增加端口为空检查
|
||||
- **修复 Docker 容器连接宿主机服务问题**:
|
||||
- 修复 MySQL 连接问题(`bind-address` 和用户权限)
|
||||
- 修复 Redis 连接问题(`bind` 和 `protected-mode`)
|
||||
- 修复 rootless Docker 的 `host-gateway` 问题
|
||||
- **修复 MySQL 容器无法正常停止问题**:
|
||||
- 增加优雅关闭时间到 60 秒
|
||||
- 使用 `init` 进程管理子进程
|
||||
- 添加自动检测和重试机制
|
||||
- **修复应用内置 Swagger UI 无法加载文档问题**:
|
||||
- 在 `main.go` 中导入生成的 docs 包
|
||||
- 修复 `docs.go` 文件中的兼容性问题(移除不兼容的 `LeftDelim` 和 `RightDelim` 字段)
|
||||
- 解决 "Failed to load API definition" 错误
|
||||
|
||||
### 📚 文档更新
|
||||
- 新增 CHANGELOG.md 文件,记录项目所有重要变更,按功能类型分类
|
||||
- 添加 SERVER_PORT 配置说明,包括影响范围和修改方法
|
||||
- 添加 Docker 容器连接宿主机服务的完整排查指南
|
||||
- 添加端口进程管理说明
|
||||
- 添加 Docker 权限问题解决方案
|
||||
- 添加常见错误信息和排查步骤
|
||||
- 清理项目中的 PDF 文件(PROJECT_SUMMARY.pdf、README.pdf)
|
||||
|
||||
---
|
||||
|
||||
## [2025-11-29] - 项目开发阶段
|
||||
|
||||
### 版本管理
|
||||
- Makefile 中的 `VERSION` 变量从 git tag 自动读取(格式:`20251129.1`、`20251129.2`、`20251129.3`)
|
||||
|
||||
### 项目结构重构
|
||||
- 将 `@internal` 目录重命名为 `src`
|
||||
- 将 `@pkg` 目录移动到 `src/pkg`
|
||||
- 将 `cmd/main.go` 移动到 `src/main.go`
|
||||
- 移除 `cmd` 目录
|
||||
- 更新所有文件引用路径
|
||||
|
||||
### Docker 相关优化
|
||||
- 支持 dev、stage、prod 三种环境的 Docker Compose 部署
|
||||
- 优化 Docker 镜像拉取配置(支持中国大陆镜像源)
|
||||
- 配置 Go 代理(`GOPROXY=https://goproxy.cn,direct`)
|
||||
- 移除 Docker Compose 中的 MySQL 和 Redis 服务,改为使用宿主机服务
|
||||
- 支持动态获取宿主机 IP 并配置 `extra_hosts`
|
||||
- 优化 MySQL 容器优雅关闭配置
|
||||
|
||||
### 端口配置优化
|
||||
- 引入 `SERVER_PORT` 变量统一管理端口配置
|
||||
- 优化端口占用检查逻辑
|
||||
- 新增端口进程管理命令
|
||||
|
||||
### 文档完善
|
||||
- 完善 README.md 中的配置说明
|
||||
- 添加 Docker 部署指南
|
||||
- 添加常见问题解答
|
||||
- 添加 SERVER_PORT 配置说明
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
- **格式**:`YYYYMMDD.N`(例如:`20251129.1`)
|
||||
- **规则**:日期 + 序号,同一天多次发布递增序号
|
||||
- **获取方式**:`git tag --sort=-version:refname | head -n 1`
|
||||
|
||||
63
Dockerfile
63
Dockerfile
@ -1,63 +0,0 @@
|
||||
# 多阶段构建
|
||||
# 使用阿里云镜像加速(如需使用官方镜像,请配置 Docker 镜像加速器)
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
# 设置 Go 代理(中国大陆用户)
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
ENV GOSUMDB=sum.golang.google.cn
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的包
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
|
||||
# 复制 go mod 文件
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# 下载依赖
|
||||
RUN go mod download
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o main src/main.go
|
||||
|
||||
# 最终镜像
|
||||
# 使用阿里云镜像加速(如需使用官方镜像,请配置 Docker 镜像加速器)
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装必要的包
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
# 设置时区
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN echo 'Asia/Shanghai' >/etc/timezone
|
||||
|
||||
# 创建非root用户
|
||||
RUN addgroup -g 1001 -S appgroup && \
|
||||
adduser -u 1001 -S appuser -G appgroup
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制二进制文件
|
||||
COPY --from=builder /app/main .
|
||||
COPY --from=builder /app/config ./config
|
||||
|
||||
# 更改文件所有者
|
||||
RUN chown -R appuser:appgroup /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER appuser
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 1234
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:1234/health || exit 1
|
||||
|
||||
# 启动应用
|
||||
CMD ["./main"]
|
||||
627
Makefile
627
Makefile
@ -1,627 +0,0 @@
|
||||
# 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
|
||||
@ -1,348 +0,0 @@
|
||||
# Yinli API 项目总结
|
||||
|
||||
## 🎉 项目概述
|
||||
|
||||
基于 Golang Gin 框架构建的高性能 RESTful API 服务,集成了 JWT 认证、Redis 缓存、频率限制、CORS 等安全特性,支持多环境部署和完整的 API 文档生成。
|
||||
|
||||
以下是项目的详细总结:
|
||||
|
||||
## ✅ 已完成的功能
|
||||
|
||||
### 1. 项目基础架构
|
||||
- ✅ 基于 Gin 框架的 RESTful API 服务
|
||||
- ✅ 清晰的项目目录结构(src 目录统一管理源代码)
|
||||
- ✅ Go 模块依赖管理
|
||||
|
||||
### 2. 配置管理系统
|
||||
- ✅ 使用 Viper 库进行配置管理
|
||||
- ✅ 支持三种环境配置:dev(开发)、stage(预发布)、prod(生产)
|
||||
- ✅ 配置文件保存在 `config/` 目录
|
||||
|
||||
### 3. 数据库集成
|
||||
- ✅ MySQL 数据库连接和管理
|
||||
- ✅ GORM ORM 库集成
|
||||
- ✅ 用户模型和数据访问层
|
||||
- ✅ 数据库初始化脚本(`sql/init.sql`)
|
||||
|
||||
### 4. 缓存系统
|
||||
- ✅ Redis 缓存集成
|
||||
- ✅ 完整的 Redis 操作封装
|
||||
- ✅ 用于频率限制和会话管理
|
||||
|
||||
### 5. 安全特性
|
||||
- ✅ JWT 认证系统
|
||||
- ✅ CORS 跨域支持
|
||||
- ✅ 基于 Redis 的 API 频率限制
|
||||
- ✅ bcrypt 密码加密
|
||||
- ✅ 多层中间件安全防护
|
||||
|
||||
### 6. 用户认证接口
|
||||
- ✅ 用户注册接口 (`POST /api/auth/register`)
|
||||
- ✅ 用户登录接口 (`POST /api/auth/login`)
|
||||
- ✅ 用户资料管理接口
|
||||
- ✅ 管理员权限接口
|
||||
|
||||
### 7. 构建和部署
|
||||
- ✅ 完整的 Makefile 构建脚本
|
||||
- ✅ 支持多环境启动命令(`make dev`、`make stage`、`make prod`)
|
||||
- ✅ Docker 容器化支持
|
||||
- ✅ 自动生成 Docker Compose 文件
|
||||
- ✅ 端口配置变量化(`SERVER_PORT`、`SWAGGER_PORT`)
|
||||
- ✅ 端口占用检查和自动处理
|
||||
- ✅ 端口进程管理命令(`kill-port`、`kill-port-force`)
|
||||
|
||||
### 8. 测试框架
|
||||
- ✅ 使用 Testify 测试框架
|
||||
- ✅ 配置系统测试
|
||||
- ✅ 中间件测试
|
||||
- ✅ 接口测试框架(需要数据库连接时可启用)
|
||||
|
||||
### 9. API 文档
|
||||
- ✅ Swagger 文档自动生成(`make docs`)
|
||||
- ✅ 自动安装 swag 工具(如果未安装)
|
||||
- ✅ 自动过滤生成过程中的警告信息
|
||||
- ✅ 自动修复生成的 `docs.go` 文件兼容性问题
|
||||
- ✅ 支持按环境分离文档生成(dev/stage/prod)
|
||||
- ✅ Docker Swagger UI 服务(`make docs-serve`)
|
||||
- ✅ 应用内置 Swagger UI(`/swagger/index.html`)
|
||||
- ✅ 文档保存在 `doc/` 目录
|
||||
|
||||
### 10. 详细文档
|
||||
- ✅ 完整的 README.md 说明文档
|
||||
- ✅ 技术架构说明
|
||||
- ✅ 开发调试指南
|
||||
- ✅ 部署说明
|
||||
|
||||
## 🚀 快速启动指南
|
||||
|
||||
### 1. 基本启动
|
||||
```bash
|
||||
# 进入项目目录
|
||||
cd /home/table/Workspace/go/src/yinli-api
|
||||
|
||||
# 查看所有可用命令
|
||||
make help
|
||||
|
||||
# 下载依赖
|
||||
make deps
|
||||
|
||||
# 启动开发环境
|
||||
make dev
|
||||
```
|
||||
|
||||
### 2. Docker 启动
|
||||
|
||||
#### 开发环境
|
||||
```bash
|
||||
# 生成 Docker Compose 文件(可选,文件已存在)
|
||||
make docker-compose-dev
|
||||
|
||||
# 启动开发环境容器
|
||||
make docker-up-dev
|
||||
|
||||
# 查看日志
|
||||
make docker-logs-dev
|
||||
|
||||
# 停止服务
|
||||
make docker-down-dev
|
||||
```
|
||||
|
||||
#### 预发布环境
|
||||
```bash
|
||||
# 启动预发布环境容器
|
||||
make docker-up-stage
|
||||
|
||||
# 查看日志
|
||||
make docker-logs-stage
|
||||
|
||||
# 停止服务
|
||||
make docker-down-stage
|
||||
```
|
||||
|
||||
#### 生产环境
|
||||
```bash
|
||||
# 设置环境变量(重要!)
|
||||
export MYSQL_ROOT_PASSWORD=your_secure_password
|
||||
export REDIS_PASSWORD=your_redis_password
|
||||
|
||||
# 启动生产环境容器
|
||||
make docker-up-prod
|
||||
|
||||
# 查看日志
|
||||
make docker-logs-prod
|
||||
|
||||
# 停止服务
|
||||
make docker-down-prod
|
||||
```
|
||||
|
||||
### 3. 运行测试
|
||||
```bash
|
||||
# 运行所有测试
|
||||
make test
|
||||
|
||||
# 生成测试覆盖率报告
|
||||
make test-coverage
|
||||
```
|
||||
|
||||
## 📁 项目结构概览
|
||||
|
||||
```
|
||||
yinli-api/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── main.go # 应用程序入口
|
||||
│ ├── handler/ # HTTP 处理器
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── repository/ # 数据访问层
|
||||
│ ├── service/ # 业务逻辑层
|
||||
│ └── pkg/ # 可复用包
|
||||
│ ├── auth/ # JWT 认证
|
||||
│ ├── cache/ # Redis 缓存
|
||||
│ ├── config/ # 配置管理
|
||||
│ └── database/ # 数据库连接
|
||||
├── config/ # 配置文件目录
|
||||
│ ├── dev.yaml # 开发环境配置
|
||||
│ ├── stage.yaml # 预发布环境配置
|
||||
│ └── prod.yaml # 生产环境配置
|
||||
├── docker/ # Docker 相关文件
|
||||
├── sql/ # 数据库脚本
|
||||
├── test/ # 测试文件
|
||||
├── Dockerfile # Docker 镜像构建
|
||||
├── Makefile # 构建脚本
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
## 🔧 主要技术栈
|
||||
|
||||
- **Web 框架**: Gin v1.11.0
|
||||
- **数据库**: MySQL 8.0 + GORM
|
||||
- **缓存**: Redis 7
|
||||
- **认证**: JWT (golang-jwt/jwt/v5)
|
||||
- **配置**: Viper v1.21.0
|
||||
- **测试**: Testify v1.11.1
|
||||
- **文档**: Swagger (gin-swagger)
|
||||
- **容器**: Docker + Docker Compose
|
||||
|
||||
## 🛠️ 可用的 Make 命令
|
||||
|
||||
### 开发命令
|
||||
```bash
|
||||
make help # 显示所有可用命令
|
||||
make dev # 启动开发环境
|
||||
make stage # 启动预发布环境
|
||||
make prod # 启动生产环境
|
||||
```
|
||||
|
||||
### 构建命令
|
||||
```bash
|
||||
make build # 构建应用程序
|
||||
make build-all # 构建所有平台版本
|
||||
make clean # 清理构建文件
|
||||
```
|
||||
|
||||
### 测试命令
|
||||
```bash
|
||||
make test # 运行测试
|
||||
make test-coverage # 运行测试并生成覆盖率报告
|
||||
```
|
||||
|
||||
### 文档命令
|
||||
```bash
|
||||
make docs # 生成 API 文档(自动安装 swag)
|
||||
make docs-serve # 启动 Swagger UI 服务器(Docker)
|
||||
make docs-stop # 停止 Swagger UI 服务器
|
||||
```
|
||||
|
||||
### Docker 命令
|
||||
```bash
|
||||
make docker-up-dev # 启动 Docker 开发环境
|
||||
make docker-up-stage # 启动 Docker 预发布环境
|
||||
make docker-up-prod # 启动 Docker 生产环境
|
||||
make docker-down-dev # 停止开发环境容器
|
||||
make docker-logs-dev # 查看开发环境日志
|
||||
make docker-ps # 查看所有容器状态
|
||||
```
|
||||
|
||||
### 端口管理命令
|
||||
```bash
|
||||
make kill-port PORT=1234 # 终止指定端口的进程
|
||||
make kill-port-force PORT=1234 # 强制终止指定端口的进程
|
||||
make kill-1234 # 终止默认端口 1234 的进程
|
||||
```
|
||||
|
||||
## 📡 API 接口列表
|
||||
|
||||
### 认证接口
|
||||
- `POST /api/auth/register` - 用户注册
|
||||
- `POST /api/auth/login` - 用户登录
|
||||
|
||||
### 用户接口(需要 JWT 认证)
|
||||
- `GET /api/user/profile` - 获取用户资料
|
||||
- `PUT /api/user/profile` - 更新用户资料
|
||||
- `PUT /api/user/password` - 修改密码
|
||||
|
||||
### 管理员接口(需要管理员权限)
|
||||
- `GET /api/admin/users` - 获取用户列表
|
||||
- `DELETE /api/admin/users/{id}` - 删除用户
|
||||
- `PUT /api/admin/users/{id}/status` - 更新用户状态
|
||||
|
||||
### 系统接口
|
||||
- `GET /health` - 健康检查
|
||||
- `GET /swagger/*` - API 文档(Swagger UI)
|
||||
- 访问地址:`http://localhost:$(SERVER_PORT)/swagger/index.html`
|
||||
- 需要先执行 `make docs` 生成文档
|
||||
|
||||
## 🔒 安全特性
|
||||
|
||||
1. **JWT 认证**: 基于 JSON Web Token 的用户认证
|
||||
2. **CORS 保护**: 可配置的跨域资源共享
|
||||
3. **频率限制**: 基于 Redis 的 API 调用频率限制
|
||||
4. **密码加密**: 使用 bcrypt 加密用户密码
|
||||
5. **中间件保护**: 多层安全中间件防护
|
||||
|
||||
## 🌍 环境配置
|
||||
|
||||
项目支持三种环境配置,每种环境都有独立的配置文件:
|
||||
|
||||
- **dev**: 开发环境,详细日志,宽松的安全设置
|
||||
- **stage**: 预发布环境,生产级配置,用于测试
|
||||
- **prod**: 生产环境,最严格的安全设置
|
||||
|
||||
## 📊 测试覆盖
|
||||
|
||||
项目包含以下测试:
|
||||
- 配置系统测试
|
||||
- JWT 认证中间件测试
|
||||
- CORS 中间件测试
|
||||
- 请求 ID 中间件测试
|
||||
- 基础 API 接口测试
|
||||
|
||||
## 🚀 部署建议
|
||||
|
||||
### 开发环境
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
```bash
|
||||
# 设置环境变量
|
||||
export MYSQL_ROOT_PASSWORD=your_secure_password
|
||||
export REDIS_PASSWORD=your_redis_password
|
||||
|
||||
# 使用 Docker 部署
|
||||
make docker-up-prod
|
||||
|
||||
# 查看日志
|
||||
make docker-logs-prod
|
||||
```
|
||||
|
||||
## 📝 下一步建议
|
||||
|
||||
1. **数据库设置**: 确保 MySQL 数据库运行并执行 `sql/init.sql`
|
||||
2. **Redis 设置**: 确保 Redis 服务运行
|
||||
3. **环境变量**: 在生产环境中设置敏感配置的环境变量
|
||||
4. **SSL 证书**: 在生产环境中配置 HTTPS
|
||||
5. **监控日志**: 添加应用监控和日志收集
|
||||
6. **API 文档**: 运行 `make docs` 自动生成 Swagger 文档,使用 `make docs-serve` 启动 Swagger UI 服务器
|
||||
|
||||
## 🔧 最新优化功能
|
||||
|
||||
### 端口配置管理
|
||||
- **SERVER_PORT 变量**:统一管理应用服务器端口(默认 1234)
|
||||
- 自动更新配置文件中的 `server.port`
|
||||
- 自动更新 Docker Compose 端口映射
|
||||
- 支持端口占用检查和自动处理
|
||||
|
||||
- **SWAGGER_PORT 变量**:统一管理 Swagger UI 端口(默认 8081)
|
||||
- Docker Swagger UI 容器端口映射
|
||||
- 静态文件服务器端口
|
||||
|
||||
### API 文档生成优化
|
||||
- **自动安装工具**:`make docs` 自动检查并安装 swag 工具
|
||||
- **警告过滤**:自动过滤生成过程中的警告信息
|
||||
- **兼容性修复**:自动修复生成的 `docs.go` 文件兼容性问题
|
||||
- **Docker Swagger UI**:使用 Docker 运行完整的 Swagger UI 界面
|
||||
- **应用内置 UI**:支持通过 `/swagger/index.html` 访问文档
|
||||
|
||||
### Docker 命令优化
|
||||
- **命令命名统一**:`docker-logs` → `docker-logs-dev`,保持命名一致性
|
||||
- **自动重试机制**:Docker 容器停止失败时自动重试
|
||||
- **权限错误处理**:自动检测并处理 rootless Docker 权限问题
|
||||
|
||||
## 🎯 项目特色
|
||||
|
||||
- **完整的企业级架构**: 分层清晰,易于维护
|
||||
- **高度可配置**: 支持多环境配置
|
||||
- **安全性强**: 多重安全防护机制
|
||||
- **易于部署**: Docker 容器化支持
|
||||
- **测试完备**: 完整的测试框架
|
||||
- **文档齐全**: 详细的开发和部署文档
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [Gin 框架文档](https://gin-gonic.com/)
|
||||
- [GORM 文档](https://gorm.io/)
|
||||
- [Viper 配置库](https://github.com/spf13/viper)
|
||||
- [JWT Go 库](https://github.com/golang-jwt/jwt)
|
||||
- [Redis Go 客户端](https://github.com/go-redis/redis)
|
||||
@ -1,41 +0,0 @@
|
||||
server:
|
||||
port: 1234
|
||||
mode: debug
|
||||
|
||||
database:
|
||||
host: host.docker.internal # Docker环境使用host.docker.internal访问宿主机,本地环境使用localhost
|
||||
port: 3306
|
||||
username: root
|
||||
password: sasasasa
|
||||
dbname: yinli
|
||||
charset: utf8mb4
|
||||
parseTime: true
|
||||
loc: Local
|
||||
maxIdleConns: 10
|
||||
maxOpenConns: 100
|
||||
|
||||
redis:
|
||||
host: host.docker.internal # Docker环境使用host.docker.internal访问宿主机,本地环境使用localhost
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
poolSize: 10
|
||||
|
||||
jwt:
|
||||
secret: dev-jwt-secret-key-change-in-production
|
||||
expireHours: 24
|
||||
|
||||
rateLimit:
|
||||
enabled: true
|
||||
requests: 100
|
||||
window: 60 # seconds
|
||||
|
||||
cors:
|
||||
allowOrigins: ["*"]
|
||||
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
||||
allowHeaders: ["*"]
|
||||
|
||||
log:
|
||||
level: debug
|
||||
format: json
|
||||
output: stdout
|
||||
@ -1,41 +0,0 @@
|
||||
server:
|
||||
port: 1234
|
||||
mode: release
|
||||
|
||||
database:
|
||||
host: host.docker.internal # Docker环境使用host.docker.internal访问宿主机,本地环境使用localhost
|
||||
port: 3306
|
||||
username: root
|
||||
password: sasasasa
|
||||
dbname: yinli
|
||||
charset: utf8mb4
|
||||
parseTime: true
|
||||
loc: Local
|
||||
maxIdleConns: 50
|
||||
maxOpenConns: 500
|
||||
|
||||
redis:
|
||||
host: host.docker.internal # Docker环境使用host.docker.internal访问宿主机,本地环境使用localhost
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 2
|
||||
poolSize: 50
|
||||
|
||||
jwt:
|
||||
secret: prod-jwt-secret-key-must-be-changed
|
||||
expireHours: 8
|
||||
|
||||
rateLimit:
|
||||
enabled: true
|
||||
requests: 500
|
||||
window: 60 # seconds
|
||||
|
||||
cors:
|
||||
allowOrigins: ["https://api.example.com"]
|
||||
allowMethods: ["GET", "POST", "PUT", "DELETE"]
|
||||
allowHeaders: ["Content-Type", "Authorization"]
|
||||
|
||||
log:
|
||||
level: warn
|
||||
format: json
|
||||
output: file
|
||||
@ -1,41 +0,0 @@
|
||||
server:
|
||||
port: 1234
|
||||
mode: release
|
||||
|
||||
database:
|
||||
host: host.docker.internal # Docker环境使用host.docker.internal访问宿主机,本地环境使用localhost
|
||||
port: 3306
|
||||
username: root
|
||||
password: sasasasa
|
||||
dbname: yinli
|
||||
charset: utf8mb4
|
||||
parseTime: true
|
||||
loc: Local
|
||||
maxIdleConns: 20
|
||||
maxOpenConns: 200
|
||||
|
||||
redis:
|
||||
host: host.docker.internal # Docker环境使用host.docker.internal访问宿主机,本地环境使用localhost
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 1
|
||||
poolSize: 20
|
||||
|
||||
jwt:
|
||||
secret: stage-jwt-secret-key-change-in-production
|
||||
expireHours: 12
|
||||
|
||||
rateLimit:
|
||||
enabled: true
|
||||
requests: 200
|
||||
window: 60 # seconds
|
||||
|
||||
cors:
|
||||
allowOrigins: ["http://localhost:3000", "https://stage.example.com"]
|
||||
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
||||
allowHeaders: ["Content-Type", "Authorization"]
|
||||
|
||||
log:
|
||||
level: info
|
||||
format: json
|
||||
output: file
|
||||
@ -1,13 +0,0 @@
|
||||
services:
|
||||
yinli-api:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "1234:1234"
|
||||
environment:
|
||||
- APP_ENV=dev
|
||||
volumes:
|
||||
- ../config:/app/config:ro
|
||||
extra_hosts:
|
||||
- "host.docker.internal:192.168.1.11"
|
||||
@ -1,21 +0,0 @@
|
||||
services:
|
||||
yinli-api:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "1234:1234"
|
||||
environment:
|
||||
- APP_ENV=prod
|
||||
volumes:
|
||||
- ../config:/app/config:ro
|
||||
extra_hosts:
|
||||
- "host.docker.internal:192.168.1.11"
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:1234/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
services:
|
||||
yinli-api:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "1234:1234"
|
||||
environment:
|
||||
- APP_ENV=stage
|
||||
volumes:
|
||||
- ../config:/app/config:ro
|
||||
extra_hosts:
|
||||
- "host.docker.internal:192.168.1.11"
|
||||
restart: unless-stopped
|
||||
|
||||
78
go.mod
78
go.mod
@ -1,78 +0,0 @@
|
||||
module yinli-api
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.7.6
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/jinzhu/gorm v1.9.16
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.8.12
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/time v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // 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/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.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/mailru/easyjson v0.7.6 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.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
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
242
go.sum
242
go.sum
@ -1,242 +0,0 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
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/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
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/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
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/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
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/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
|
||||
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
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/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||
github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w=
|
||||
github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its=
|
||||
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=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
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.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=
|
||||
@ -1,2 +0,0 @@
|
||||
SELECT id, name, password, status, created_at, updated_at
|
||||
FROM yinli.`user`;
|
||||
62
sql/init.sql
62
sql/init.sql
@ -1,62 +0,0 @@
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS yinli CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 使用数据库
|
||||
USE yinli;
|
||||
|
||||
-- 创建用户表
|
||||
CREATE TABLE IF NOT EXISTS `user` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||
`name` varchar(100) NOT NULL COMMENT '用户名',
|
||||
`password` varchar(255) NOT NULL COMMENT '密码',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '用户状态 1:正常 0:禁用',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_name` (`name`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||
|
||||
-- 插入测试数据
|
||||
INSERT INTO `user` (`name`, `password`, `status`) VALUES
|
||||
('admin', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 1),
|
||||
('testuser', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 1)
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
|
||||
|
||||
-- 创建索引优化查询
|
||||
CREATE INDEX IF NOT EXISTS `idx_user_name_status` ON `user` (`name`, `status`);
|
||||
|
||||
-- 创建会话表(可选,用于会话管理)
|
||||
CREATE TABLE IF NOT EXISTS `user_session` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '会话ID',
|
||||
`user_id` int(11) unsigned NOT NULL COMMENT '用户ID',
|
||||
`token` varchar(500) NOT NULL COMMENT 'JWT令牌',
|
||||
`expires_at` datetime NOT NULL COMMENT '过期时间',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_token` (`token`(255)),
|
||||
KEY `idx_expires_at` (`expires_at`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户会话表';
|
||||
|
||||
-- 创建API访问日志表(可选)
|
||||
CREATE TABLE IF NOT EXISTS `api_log` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||
`user_id` int(11) unsigned DEFAULT NULL COMMENT '用户ID',
|
||||
`method` varchar(10) NOT NULL COMMENT 'HTTP方法',
|
||||
`path` varchar(255) NOT NULL COMMENT '请求路径',
|
||||
`status_code` int(11) NOT NULL COMMENT '响应状态码',
|
||||
`ip` varchar(45) NOT NULL COMMENT '客户端IP',
|
||||
`user_agent` varchar(500) DEFAULT NULL COMMENT '用户代理',
|
||||
`request_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '请求时间',
|
||||
`response_time` int(11) DEFAULT NULL COMMENT '响应时间(毫秒)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_path` (`path`),
|
||||
KEY `idx_status_code` (`status_code`),
|
||||
KEY `idx_ip` (`ip`),
|
||||
KEY `idx_request_time` (`request_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='API访问日志表';
|
||||
@ -1,133 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"yinli-api/src/middleware"
|
||||
"yinli-api/src/repository"
|
||||
"yinli-api/src/service"
|
||||
|
||||
"net/http/pprof"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// SetupRoutes 设置路由
|
||||
func SetupRoutes(r *gin.Engine) {
|
||||
// 初始化仓库
|
||||
userRepo := repository.NewUserRepository()
|
||||
|
||||
// 初始化服务
|
||||
userService := service.NewUserService(userRepo)
|
||||
|
||||
// 初始化处理器
|
||||
userHandler := NewUserHandler(userService)
|
||||
|
||||
// 添加中间件
|
||||
r.Use(middleware.CORSMiddleware())
|
||||
r.Use(middleware.LoggerMiddleware())
|
||||
r.Use(middleware.RequestIDMiddleware())
|
||||
r.Use(middleware.RateLimitMiddleware())
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
// API 文档路由
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// 健康检查
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"message": "服务运行正常",
|
||||
})
|
||||
})
|
||||
|
||||
// pprof 性能分析路由 (仅在调试模式下启用)
|
||||
if gin.Mode() == gin.DebugMode {
|
||||
pprofGroup := r.Group("/debug/pprof")
|
||||
{
|
||||
pprofGroup.GET("/", gin.WrapF(pprof.Index))
|
||||
pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline))
|
||||
pprofGroup.GET("/profile", gin.WrapF(pprof.Profile))
|
||||
pprofGroup.POST("/symbol", gin.WrapF(pprof.Symbol))
|
||||
pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol))
|
||||
pprofGroup.GET("/trace", gin.WrapF(pprof.Trace))
|
||||
pprofGroup.GET("/allocs", gin.WrapH(pprof.Handler("allocs")))
|
||||
pprofGroup.GET("/block", gin.WrapH(pprof.Handler("block")))
|
||||
pprofGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
|
||||
pprofGroup.GET("/heap", gin.WrapH(pprof.Handler("heap")))
|
||||
pprofGroup.GET("/mutex", gin.WrapH(pprof.Handler("mutex")))
|
||||
pprofGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
|
||||
}
|
||||
}
|
||||
|
||||
// API 路由组
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 认证路由(不需要JWT)
|
||||
auth := api.Group("/auth")
|
||||
{
|
||||
auth.POST("/register", userHandler.Register)
|
||||
auth.POST("/login", userHandler.Login)
|
||||
}
|
||||
|
||||
// 用户路由(需要JWT认证)
|
||||
user := api.Group("/user")
|
||||
user.Use(middleware.AuthMiddleware())
|
||||
{
|
||||
user.GET("/profile", userHandler.GetProfile)
|
||||
user.PUT("/profile", userHandler.UpdateProfile)
|
||||
user.PUT("/password", userHandler.ChangePassword)
|
||||
}
|
||||
|
||||
// 管理员路由(需要JWT认证和管理员权限)
|
||||
admin := api.Group("/admin")
|
||||
admin.Use(middleware.AuthMiddleware())
|
||||
admin.Use(middleware.AdminMiddleware())
|
||||
{
|
||||
admin.GET("/users", userHandler.GetUserList)
|
||||
admin.DELETE("/users/:id", userHandler.DeleteUser)
|
||||
admin.PUT("/users/:id/status", userHandler.UpdateUserStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// 404 处理
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.JSON(404, gin.H{
|
||||
"code": 404,
|
||||
"message": "接口不存在",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// SetupTestRoutes 设置测试路由(用于测试环境)
|
||||
func SetupTestRoutes(r *gin.Engine) {
|
||||
test := r.Group("/test")
|
||||
{
|
||||
// 测试Redis连接
|
||||
test.GET("/redis", func(c *gin.Context) {
|
||||
// 这里可以添加Redis连接测试逻辑
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"message": "Redis连接正常",
|
||||
})
|
||||
})
|
||||
|
||||
// 测试数据库连接
|
||||
test.GET("/database", func(c *gin.Context) {
|
||||
// 这里可以添加数据库连接测试逻辑
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"message": "数据库连接正常",
|
||||
})
|
||||
})
|
||||
|
||||
// 测试JWT生成
|
||||
test.GET("/jwt", func(c *gin.Context) {
|
||||
// 这里可以添加JWT生成测试逻辑
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"message": "JWT功能正常",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,381 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"yinli-api/src/middleware"
|
||||
"yinli-api/src/model"
|
||||
"yinli-api/src/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UserHandler 用户处理器
|
||||
type UserHandler struct {
|
||||
userService service.UserService
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户处理器实例
|
||||
func NewUserHandler(userService service.UserService) *UserHandler {
|
||||
return &UserHandler{
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
// @Summary 用户注册
|
||||
// @Description 用户注册接口
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.UserRegisterRequest true "注册信息"
|
||||
// @Success 200 {object} model.LoginResponse
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]interface{}
|
||||
// @Router /api/auth/register [post]
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
var req model.UserRegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "请求参数错误",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.userService.Register(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "注册成功",
|
||||
"data": response,
|
||||
})
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
// @Summary 用户登录
|
||||
// @Description 用户登录接口
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.UserLoginRequest true "登录信息"
|
||||
// @Success 200 {object} model.LoginResponse
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]interface{}
|
||||
// @Router /api/auth/login [post]
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
var req model.UserLoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "请求参数错误",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.userService.Login(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": response,
|
||||
})
|
||||
}
|
||||
|
||||
// GetProfile 获取用户资料
|
||||
// @Summary 获取用户资料
|
||||
// @Description 获取当前用户的资料信息
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} model.UserResponse
|
||||
// @Failure 401 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]interface{}
|
||||
// @Router /api/user/profile [get]
|
||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
userID, exists := middleware.GetUserID(c)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "未认证",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.GetProfile(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "获取成功",
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfile 更新用户资料
|
||||
// @Summary 更新用户资料
|
||||
// @Description 更新当前用户的资料信息
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body map[string]interface{} true "更新信息"
|
||||
// @Success 200 {object} model.UserResponse
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Failure 401 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]interface{}
|
||||
// @Router /api/user/profile [put]
|
||||
func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
userID, exists := middleware.GetUserID(c)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "未认证",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "请求参数错误",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.UpdateProfile(userID, updates)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "更新成功",
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
// @Summary 修改密码
|
||||
// @Description 修改当前用户的密码
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body map[string]string true "密码信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Failure 401 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]interface{}
|
||||
// @Router /api/user/password [put]
|
||||
func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
userID, exists := middleware.GetUserID(c)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "未认证",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "请求参数错误",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.userService.ChangePassword(userID, req.OldPassword, req.NewPassword)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "密码修改成功",
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserList 获取用户列表(管理员)
|
||||
// @Summary 获取用户列表
|
||||
// @Description 获取用户列表(管理员权限)
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param limit query int false "每页数量" default(10)
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Failure 401 {object} map[string]interface{}
|
||||
// @Failure 403 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]interface{}
|
||||
// @Router /api/admin/users [get]
|
||||
func (h *UserHandler) GetUserList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
users, total, err := h.userService.GetUserList(offset, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "获取成功",
|
||||
"data": gin.H{
|
||||
"users": users,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户(管理员)
|
||||
// @Summary 删除用户
|
||||
// @Description 删除指定用户(管理员权限)
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path int true "用户ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Failure 401 {object} map[string]interface{}
|
||||
// @Failure 403 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]interface{}
|
||||
// @Router /api/admin/users/{id} [delete]
|
||||
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
||||
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "无效的用户ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.userService.DeleteUser(uint(userID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "删除成功",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUserStatus 更新用户状态(管理员)
|
||||
// @Summary 更新用户状态
|
||||
// @Description 更新指定用户的状态(管理员权限)
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path int true "用户ID"
|
||||
// @Param request body map[string]int true "状态信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Failure 401 {object} map[string]interface{}
|
||||
// @Failure 403 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]interface{}
|
||||
// @Router /api/admin/users/{id}/status [put]
|
||||
func (h *UserHandler) UpdateUserStatus(c *gin.Context) {
|
||||
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "无效的用户ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Status int `json:"status" binding:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "请求参数错误",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.userService.UpdateUserStatus(uint(userID), req.Status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "状态更新成功",
|
||||
})
|
||||
}
|
||||
108
src/main.go
108
src/main.go
@ -1,108 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"yinli-api/src/handler"
|
||||
"yinli-api/src/pkg/cache"
|
||||
"yinli-api/src/pkg/config"
|
||||
"yinli-api/src/pkg/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
// 导入 Swagger 文档(根据环境动态导入)
|
||||
_ "yinli-api/doc/dev" // 默认使用 dev 环境的文档
|
||||
)
|
||||
|
||||
// @title Yinli API
|
||||
// @version 1.0
|
||||
// @description 这是一个基于Gin框架的API服务
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url http://www.swagger.io/support
|
||||
// @contact.email support@swagger.io
|
||||
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
// @host localhost:1234
|
||||
// @BasePath /api
|
||||
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
|
||||
func main() {
|
||||
// 解析命令行参数
|
||||
var env string
|
||||
flag.StringVar(&env, "env", "dev", "运行环境 (dev, stage, prod)")
|
||||
flag.Parse()
|
||||
|
||||
// 从环境变量获取环境配置
|
||||
if envVar := os.Getenv("APP_ENV"); envVar != "" {
|
||||
env = envVar
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
cfg, err := config.LoadConfig(env)
|
||||
if err != nil {
|
||||
log.Fatalf("加载配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置Gin模式
|
||||
gin.SetMode(cfg.Server.Mode)
|
||||
|
||||
// 初始化数据库
|
||||
if err := database.InitDatabase(cfg); err != nil {
|
||||
log.Fatalf("初始化数据库失败: %v", err)
|
||||
}
|
||||
defer database.CloseDatabase()
|
||||
|
||||
// 初始化Redis
|
||||
if err := cache.InitRedis(cfg); err != nil {
|
||||
log.Fatalf("初始化Redis失败: %v", err)
|
||||
}
|
||||
defer cache.CloseRedis()
|
||||
|
||||
// 创建Gin引擎
|
||||
r := gin.New()
|
||||
|
||||
// 设置路由
|
||||
handler.SetupRoutes(r)
|
||||
|
||||
// 如果是开发环境,添加测试路由
|
||||
if cfg.Server.Mode == "debug" {
|
||||
handler.SetupTestRoutes(r)
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
if cfg.Server.Port == "" {
|
||||
log.Fatalf("❌ 错误: 服务器端口配置为空!请检查配置文件中的 server.port 设置")
|
||||
}
|
||||
log.Printf("服务器启动在端口 %s,环境: %s", cfg.Server.Port, env)
|
||||
|
||||
// 优雅关闭
|
||||
go func() {
|
||||
if err := r.Run(":" + cfg.Server.Port); err != nil {
|
||||
log.Fatalf("服务器启动失败: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待中断信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Println("正在关闭服务器...")
|
||||
|
||||
// 这里可以添加优雅关闭的逻辑
|
||||
// 比如关闭数据库连接、Redis连接等
|
||||
|
||||
log.Println("服务器已关闭")
|
||||
}
|
||||
@ -1,125 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"yinli-api/src/pkg/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AuthMiddleware JWT认证中间件
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "缺少授权头",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
token, err := auth.ExtractTokenFromHeader(authHeader)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "无效的授权头格式",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := auth.ValidateToken(token)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "无效的令牌: " + err.Error(),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储到上下文中
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("token", token)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// OptionalAuthMiddleware 可选的JWT认证中间件
|
||||
func OptionalAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader != "" {
|
||||
token, err := auth.ExtractTokenFromHeader(authHeader)
|
||||
if err == nil {
|
||||
claims, err := auth.ValidateToken(token)
|
||||
if err == nil {
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("token", token)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserID 从上下文中获取用户ID
|
||||
func GetUserID(c *gin.Context) (uint, bool) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return 0, false
|
||||
}
|
||||
return userID.(uint), true
|
||||
}
|
||||
|
||||
// GetUsername 从上下文中获取用户名
|
||||
func GetUsername(c *gin.Context) (string, bool) {
|
||||
username, exists := c.Get("username")
|
||||
if !exists {
|
||||
return "", false
|
||||
}
|
||||
return username.(string), true
|
||||
}
|
||||
|
||||
// GetToken 从上下文中获取令牌
|
||||
func GetToken(c *gin.Context) (string, bool) {
|
||||
token, exists := c.Get("token")
|
||||
if !exists {
|
||||
return "", false
|
||||
}
|
||||
return token.(string), true
|
||||
}
|
||||
|
||||
// AdminMiddleware 管理员权限中间件
|
||||
func AdminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, exists := GetUserID(c)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "未认证",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 这里可以添加管理员权限检查逻辑
|
||||
// 例如检查用户角色或权限表
|
||||
if userID != 1 { // 简单示例:只有ID为1的用户是管理员
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": 403,
|
||||
"message": "权限不足",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"yinli-api/src/pkg/config"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CORSMiddleware CORS中间件
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
cfg := config.AppConfig
|
||||
if cfg == nil {
|
||||
// 默认配置
|
||||
return cors.New(cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"*"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: true,
|
||||
})
|
||||
}
|
||||
|
||||
return cors.New(cors.Config{
|
||||
AllowOrigins: cfg.CORS.AllowOrigins,
|
||||
AllowMethods: cfg.CORS.AllowMethods,
|
||||
AllowHeaders: cfg.CORS.AllowHeaders,
|
||||
ExposeHeaders: []string{"Content-Length", "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"},
|
||||
AllowCredentials: true,
|
||||
})
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LoggerMiddleware 自定义日志中间件
|
||||
func LoggerMiddleware() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return fmt.Sprintf("[%s] %s %s %s %d %s %s\n",
|
||||
param.TimeStamp.Format("2006-01-02 15:04:05"),
|
||||
param.ClientIP,
|
||||
param.Method,
|
||||
param.Path,
|
||||
param.StatusCode,
|
||||
param.Latency,
|
||||
param.ErrorMessage,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// RequestIDMiddleware 请求ID中间件
|
||||
func RequestIDMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = generateRequestID()
|
||||
}
|
||||
c.Set("request_id", requestID)
|
||||
c.Header("X-Request-ID", requestID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求ID
|
||||
func generateRequestID() string {
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// GetRequestID 从上下文中获取请求ID
|
||||
func GetRequestID(c *gin.Context) string {
|
||||
if requestID, exists := c.Get("request_id"); exists {
|
||||
return requestID.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@ -1,207 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"yinli-api/src/pkg/cache"
|
||||
"yinli-api/src/pkg/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// RateLimitMiddleware Redis频率限制中间件
|
||||
func RateLimitMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cfg := config.AppConfig
|
||||
if cfg == nil || !cfg.RateLimit.Enabled {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取客户端IP
|
||||
clientIP := c.ClientIP()
|
||||
key := fmt.Sprintf("rate_limit:%s", clientIP)
|
||||
|
||||
// 获取当前请求数
|
||||
currentRequests, err := cache.GetString(key)
|
||||
if err != nil {
|
||||
// 键不存在,设置为1
|
||||
err = cache.SetString(key, "1", time.Duration(cfg.RateLimit.Window)*time.Second)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "内部服务器错误",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为整数
|
||||
requests, err := strconv.Atoi(currentRequests)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "内部服务器错误",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否超过限制
|
||||
if requests >= cfg.RateLimit.Requests {
|
||||
// 获取剩余时间
|
||||
ttl, _ := cache.TTL(key)
|
||||
c.Header("X-RateLimit-Limit", strconv.Itoa(cfg.RateLimit.Requests))
|
||||
c.Header("X-RateLimit-Remaining", "0")
|
||||
c.Header("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(ttl).Unix(), 10))
|
||||
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"code": 429,
|
||||
"message": "请求过于频繁,请稍后再试",
|
||||
"retry_after": int(ttl.Seconds()),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 增加请求计数
|
||||
newCount, err := cache.IncrBy(key, 1)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "内部服务器错误",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
remaining := cfg.RateLimit.Requests - int(newCount)
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
|
||||
c.Header("X-RateLimit-Limit", strconv.Itoa(cfg.RateLimit.Requests))
|
||||
c.Header("X-RateLimit-Remaining", strconv.Itoa(remaining))
|
||||
|
||||
ttl, _ := cache.TTL(key)
|
||||
c.Header("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(ttl).Unix(), 10))
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// InMemoryRateLimitMiddleware 内存频率限制中间件(备用方案)
|
||||
func InMemoryRateLimitMiddleware() gin.HandlerFunc {
|
||||
limiters := make(map[string]*rate.Limiter)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
cfg := config.AppConfig
|
||||
if cfg == nil || !cfg.RateLimit.Enabled {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
limiter, exists := limiters[clientIP]
|
||||
if !exists {
|
||||
// 创建新的限制器
|
||||
limiter = rate.NewLimiter(rate.Every(time.Duration(cfg.RateLimit.Window)*time.Second/time.Duration(cfg.RateLimit.Requests)), cfg.RateLimit.Requests)
|
||||
limiters[clientIP] = limiter
|
||||
}
|
||||
|
||||
if !limiter.Allow() {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"code": 429,
|
||||
"message": "请求过于频繁,请稍后再试",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// UserRateLimitMiddleware 基于用户的频率限制中间件
|
||||
func UserRateLimitMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cfg := config.AppConfig
|
||||
if cfg == nil || !cfg.RateLimit.Enabled {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
userID, exists := GetUserID(c)
|
||||
if !exists {
|
||||
// 如果没有用户ID,使用IP限制
|
||||
RateLimitMiddleware()(c)
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("user_rate_limit:%d", userID)
|
||||
|
||||
// 获取当前请求数
|
||||
currentRequests, err := cache.GetString(key)
|
||||
if err != nil {
|
||||
// 键不存在,设置为1
|
||||
err = cache.SetString(key, "1", time.Duration(cfg.RateLimit.Window)*time.Second)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "内部服务器错误",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为整数
|
||||
requests, err := strconv.Atoi(currentRequests)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "内部服务器错误",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否超过限制(认证用户可以有更高的限制)
|
||||
userLimit := cfg.RateLimit.Requests * 2 // 认证用户限制翻倍
|
||||
if requests >= userLimit {
|
||||
ttl, _ := cache.TTL(key)
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"code": 429,
|
||||
"message": "请求过于频繁,请稍后再试",
|
||||
"retry_after": int(ttl.Seconds()),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 增加请求计数
|
||||
_, err = cache.IncrBy(key, 1)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "内部服务器错误",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// User 用户模型
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primary_key;auto_increment"`
|
||||
Name string `json:"name" gorm:"type:varchar(100);not null;unique_index"`
|
||||
Password string `json:"-" gorm:"type:varchar(255);not null"`
|
||||
Status int `json:"status" gorm:"type:tinyint;default:1;comment:'用户状态 1:正常 0:禁用'"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"type:datetime;not null"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"type:datetime;not null"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (User) TableName() string {
|
||||
return "user"
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前钩子
|
||||
func (u *User) BeforeCreate(scope *gorm.Scope) error {
|
||||
if u.Password != "" {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hashedPassword)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPassword 验证密码
|
||||
func (u *User) CheckPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SetPassword 设置密码
|
||||
func (u *User) SetPassword(password string) error {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hashedPassword)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserRegisterRequest 用户注册请求
|
||||
type UserRegisterRequest struct {
|
||||
Name string `json:"name" binding:"required,min=3,max=50" example:"testuser"`
|
||||
Password string `json:"password" binding:"required,min=6,max=100" example:"password123"`
|
||||
}
|
||||
|
||||
// UserLoginRequest 用户登录请求
|
||||
type UserLoginRequest struct {
|
||||
Name string `json:"name" binding:"required" example:"testuser"`
|
||||
Password string `json:"password" binding:"required" example:"password123"`
|
||||
}
|
||||
|
||||
// UserResponse 用户响应
|
||||
type UserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status int `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ToResponse 转换为响应格式
|
||||
func (u *User) ToResponse() *UserResponse {
|
||||
return &UserResponse{
|
||||
ID: u.ID,
|
||||
Name: u.Name,
|
||||
Status: u.Status,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
User *UserResponse `json:"user"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// IsActive 检查用户是否激活
|
||||
func (u *User) IsActive() bool {
|
||||
return u.Status == 1
|
||||
}
|
||||
@ -1,131 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"yinli-api/src/pkg/config"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// Claims JWT声明
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT令牌
|
||||
func GenerateToken(userID uint, username string) (string, error) {
|
||||
cfg := config.AppConfig
|
||||
if cfg == nil {
|
||||
return "", errors.New("配置未初始化")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(cfg.JWT.ExpireHours) * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Issuer: "yinli-api",
|
||||
Subject: fmt.Sprintf("%d", userID),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(cfg.JWT.Secret))
|
||||
}
|
||||
|
||||
// ParseToken 解析JWT令牌
|
||||
func ParseToken(tokenString string) (*Claims, error) {
|
||||
cfg := config.AppConfig
|
||||
if cfg == nil {
|
||||
return nil, errors.New("配置未初始化")
|
||||
}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("意外的签名方法: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(cfg.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("无效的令牌")
|
||||
}
|
||||
|
||||
// ValidateToken 验证JWT令牌
|
||||
func ValidateToken(tokenString string) (*Claims, error) {
|
||||
claims, err := ParseToken(tokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查令牌是否过期
|
||||
if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(time.Now()) {
|
||||
return nil, errors.New("令牌已过期")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// RefreshToken 刷新JWT令牌
|
||||
func RefreshToken(tokenString string) (string, error) {
|
||||
claims, err := ParseToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 检查令牌是否在刷新窗口内
|
||||
cfg := config.AppConfig
|
||||
refreshWindow := time.Duration(cfg.JWT.ExpireHours/2) * time.Hour
|
||||
if claims.ExpiresAt != nil && time.Until(claims.ExpiresAt.Time) > refreshWindow {
|
||||
return "", errors.New("令牌还未到刷新时间")
|
||||
}
|
||||
|
||||
return GenerateToken(claims.UserID, claims.Username)
|
||||
}
|
||||
|
||||
// ExtractTokenFromHeader 从请求头中提取令牌
|
||||
func ExtractTokenFromHeader(authHeader string) (string, error) {
|
||||
if authHeader == "" {
|
||||
return "", errors.New("授权头为空")
|
||||
}
|
||||
|
||||
const bearerPrefix = "Bearer "
|
||||
if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
|
||||
return "", errors.New("无效的授权头格式")
|
||||
}
|
||||
|
||||
return authHeader[len(bearerPrefix):], nil
|
||||
}
|
||||
|
||||
// GetUserIDFromToken 从令牌中获取用户ID
|
||||
func GetUserIDFromToken(tokenString string) (uint, error) {
|
||||
claims, err := ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return claims.UserID, nil
|
||||
}
|
||||
|
||||
// GetUsernameFromToken 从令牌中获取用户名
|
||||
func GetUsernameFromToken(tokenString string) (string, error) {
|
||||
claims, err := ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return claims.Username, nil
|
||||
}
|
||||
187
src/pkg/cache/redis.go
vendored
187
src/pkg/cache/redis.go
vendored
@ -1,187 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"yinli-api/src/pkg/config"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
var RedisClient *redis.Client
|
||||
var ctx = context.Background()
|
||||
|
||||
// InitRedis 初始化Redis连接
|
||||
func InitRedis(cfg *config.Config) error {
|
||||
RedisClient = redis.NewClient(&redis.Options{
|
||||
Addr: cfg.GetRedisAddr(),
|
||||
Password: cfg.Redis.Password,
|
||||
DB: cfg.Redis.DB,
|
||||
PoolSize: cfg.Redis.PoolSize,
|
||||
})
|
||||
|
||||
// 测试连接
|
||||
_, err := RedisClient.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Redis连接失败: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Redis连接成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseRedis 关闭Redis连接
|
||||
func CloseRedis() error {
|
||||
if RedisClient != nil {
|
||||
return RedisClient.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set 设置键值对
|
||||
func Set(key string, value interface{}, expiration time.Duration) error {
|
||||
jsonValue, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return RedisClient.Set(ctx, key, jsonValue, expiration).Err()
|
||||
}
|
||||
|
||||
// Get 获取值
|
||||
func Get(key string, dest interface{}) error {
|
||||
val, err := RedisClient.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal([]byte(val), dest)
|
||||
}
|
||||
|
||||
// GetString 获取字符串值
|
||||
func GetString(key string) (string, error) {
|
||||
return RedisClient.Get(ctx, key).Result()
|
||||
}
|
||||
|
||||
// SetString 设置字符串值
|
||||
func SetString(key, value string, expiration time.Duration) error {
|
||||
return RedisClient.Set(ctx, key, value, expiration).Err()
|
||||
}
|
||||
|
||||
// Delete 删除键
|
||||
func Delete(key string) error {
|
||||
return RedisClient.Del(ctx, key).Err()
|
||||
}
|
||||
|
||||
// Exists 检查键是否存在
|
||||
func Exists(key string) (bool, error) {
|
||||
count, err := RedisClient.Exists(ctx, key).Result()
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// Expire 设置过期时间
|
||||
func Expire(key string, expiration time.Duration) error {
|
||||
return RedisClient.Expire(ctx, key, expiration).Err()
|
||||
}
|
||||
|
||||
// TTL 获取剩余过期时间
|
||||
func TTL(key string) (time.Duration, error) {
|
||||
return RedisClient.TTL(ctx, key).Result()
|
||||
}
|
||||
|
||||
// Incr 递增
|
||||
func Incr(key string) (int64, error) {
|
||||
return RedisClient.Incr(ctx, key).Result()
|
||||
}
|
||||
|
||||
// IncrBy 按指定值递增
|
||||
func IncrBy(key string, value int64) (int64, error) {
|
||||
return RedisClient.IncrBy(ctx, key, value).Result()
|
||||
}
|
||||
|
||||
// Decr 递减
|
||||
func Decr(key string) (int64, error) {
|
||||
return RedisClient.Decr(ctx, key).Result()
|
||||
}
|
||||
|
||||
// DecrBy 按指定值递减
|
||||
func DecrBy(key string, value int64) (int64, error) {
|
||||
return RedisClient.DecrBy(ctx, key, value).Result()
|
||||
}
|
||||
|
||||
// HSet 设置哈希字段
|
||||
func HSet(key, field string, value interface{}) error {
|
||||
return RedisClient.HSet(ctx, key, field, value).Err()
|
||||
}
|
||||
|
||||
// HGet 获取哈希字段
|
||||
func HGet(key, field string) (string, error) {
|
||||
return RedisClient.HGet(ctx, key, field).Result()
|
||||
}
|
||||
|
||||
// HGetAll 获取所有哈希字段
|
||||
func HGetAll(key string) (map[string]string, error) {
|
||||
return RedisClient.HGetAll(ctx, key).Result()
|
||||
}
|
||||
|
||||
// HDel 删除哈希字段
|
||||
func HDel(key string, fields ...string) error {
|
||||
return RedisClient.HDel(ctx, key, fields...).Err()
|
||||
}
|
||||
|
||||
// SAdd 添加集合成员
|
||||
func SAdd(key string, members ...interface{}) error {
|
||||
return RedisClient.SAdd(ctx, key, members...).Err()
|
||||
}
|
||||
|
||||
// SMembers 获取集合所有成员
|
||||
func SMembers(key string) ([]string, error) {
|
||||
return RedisClient.SMembers(ctx, key).Result()
|
||||
}
|
||||
|
||||
// SIsMember 检查是否为集合成员
|
||||
func SIsMember(key string, member interface{}) (bool, error) {
|
||||
return RedisClient.SIsMember(ctx, key, member).Result()
|
||||
}
|
||||
|
||||
// SRem 移除集合成员
|
||||
func SRem(key string, members ...interface{}) error {
|
||||
return RedisClient.SRem(ctx, key, members...).Err()
|
||||
}
|
||||
|
||||
// ZAdd 添加有序集合成员
|
||||
func ZAdd(key string, score float64, member interface{}) error {
|
||||
return RedisClient.ZAdd(ctx, key, &redis.Z{Score: score, Member: member}).Err()
|
||||
}
|
||||
|
||||
// ZRange 获取有序集合范围内的成员
|
||||
func ZRange(key string, start, stop int64) ([]string, error) {
|
||||
return RedisClient.ZRange(ctx, key, start, stop).Result()
|
||||
}
|
||||
|
||||
// ZRem 移除有序集合成员
|
||||
func ZRem(key string, members ...interface{}) error {
|
||||
return RedisClient.ZRem(ctx, key, members...).Err()
|
||||
}
|
||||
|
||||
// Keys 获取匹配模式的所有键
|
||||
func Keys(pattern string) ([]string, error) {
|
||||
return RedisClient.Keys(ctx, pattern).Result()
|
||||
}
|
||||
|
||||
// FlushDB 清空当前数据库
|
||||
func FlushDB() error {
|
||||
return RedisClient.FlushDB(ctx).Err()
|
||||
}
|
||||
|
||||
// Ping 测试连接
|
||||
func Ping() error {
|
||||
return RedisClient.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
// GetClient 获取Redis客户端实例
|
||||
func GetClient() *redis.Client {
|
||||
return RedisClient
|
||||
}
|
||||
@ -1,135 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Config 应用配置结构
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
RateLimit RateLimitConfig `mapstructure:"rateLimit"`
|
||||
CORS CORSConfig `mapstructure:"cors"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
}
|
||||
|
||||
// ServerConfig 服务器配置
|
||||
type ServerConfig struct {
|
||||
Port string `mapstructure:"port"`
|
||||
Mode string `mapstructure:"mode"`
|
||||
}
|
||||
|
||||
// DatabaseConfig 数据库配置
|
||||
type DatabaseConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
DBName string `mapstructure:"dbname"`
|
||||
Charset string `mapstructure:"charset"`
|
||||
ParseTime bool `mapstructure:"parseTime"`
|
||||
Loc string `mapstructure:"loc"`
|
||||
MaxIdleConns int `mapstructure:"maxIdleConns"`
|
||||
MaxOpenConns int `mapstructure:"maxOpenConns"`
|
||||
}
|
||||
|
||||
// RedisConfig Redis配置
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
PoolSize int `mapstructure:"poolSize"`
|
||||
}
|
||||
|
||||
// JWTConfig JWT配置
|
||||
type JWTConfig struct {
|
||||
Secret string `mapstructure:"secret"`
|
||||
ExpireHours int `mapstructure:"expireHours"`
|
||||
}
|
||||
|
||||
// RateLimitConfig 频率限制配置
|
||||
type RateLimitConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Requests int `mapstructure:"requests"`
|
||||
Window int `mapstructure:"window"`
|
||||
}
|
||||
|
||||
// CORSConfig CORS配置
|
||||
type CORSConfig struct {
|
||||
AllowOrigins []string `mapstructure:"allowOrigins"`
|
||||
AllowMethods []string `mapstructure:"allowMethods"`
|
||||
AllowHeaders []string `mapstructure:"allowHeaders"`
|
||||
}
|
||||
|
||||
// LogConfig 日志配置
|
||||
type LogConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
}
|
||||
|
||||
var AppConfig *Config
|
||||
|
||||
// LoadConfig 加载配置文件
|
||||
func LoadConfig(env string) (*Config, error) {
|
||||
if env == "" {
|
||||
env = "dev"
|
||||
}
|
||||
|
||||
viper.SetConfigName(env)
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath("./config")
|
||||
viper.AddConfigPath("../config")
|
||||
viper.AddConfigPath("../../config")
|
||||
|
||||
// 设置环境变量前缀
|
||||
viper.SetEnvPrefix("YINLI")
|
||||
viper.AutomaticEnv()
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, fmt.Errorf("解析配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
AppConfig = &config
|
||||
log.Printf("已加载配置文件: %s", viper.ConfigFileUsed())
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// GetEnv 获取环境变量,如果不存在则返回默认值
|
||||
func GetEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetDSN 获取数据库连接字符串
|
||||
func (c *Config) GetDSN() string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=%t&loc=%s",
|
||||
c.Database.Username,
|
||||
c.Database.Password,
|
||||
c.Database.Host,
|
||||
c.Database.Port,
|
||||
c.Database.DBName,
|
||||
c.Database.Charset,
|
||||
c.Database.ParseTime,
|
||||
c.Database.Loc,
|
||||
)
|
||||
}
|
||||
|
||||
// GetRedisAddr 获取Redis连接地址
|
||||
func (c *Config) GetRedisAddr() string {
|
||||
return fmt.Sprintf("%s:%d", c.Redis.Host, c.Redis.Port)
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"yinli-api/src/pkg/config"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
// InitDatabase 初始化数据库连接
|
||||
func InitDatabase(cfg *config.Config) error {
|
||||
dsn := cfg.GetDSN()
|
||||
|
||||
db, err := gorm.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接数据库失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
db.DB().SetMaxIdleConns(cfg.Database.MaxIdleConns)
|
||||
db.DB().SetMaxOpenConns(cfg.Database.MaxOpenConns)
|
||||
db.DB().SetConnMaxLifetime(time.Hour)
|
||||
|
||||
// 测试连接
|
||||
if err := db.DB().Ping(); err != nil {
|
||||
return fmt.Errorf("数据库连接测试失败: %w", err)
|
||||
}
|
||||
|
||||
// 启用日志
|
||||
if cfg.Server.Mode == "debug" {
|
||||
db.LogMode(true)
|
||||
}
|
||||
|
||||
DB = db
|
||||
log.Println("数据库连接成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseDatabase 关闭数据库连接
|
||||
func CloseDatabase() error {
|
||||
if DB != nil {
|
||||
return DB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDB 获取数据库实例
|
||||
func GetDB() *gorm.DB {
|
||||
return DB
|
||||
}
|
||||
|
||||
// Ping 检查数据库连接
|
||||
func Ping() error {
|
||||
if DB == nil {
|
||||
return fmt.Errorf("数据库未初始化")
|
||||
}
|
||||
return DB.DB().Ping()
|
||||
}
|
||||
|
||||
// Transaction 执行事务
|
||||
func Transaction(fn func(*gorm.DB) error) error {
|
||||
tx := DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(tx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// RawQuery 执行原生SQL查询
|
||||
func RawQuery(query string, args ...interface{}) (*sql.Rows, error) {
|
||||
return DB.Raw(query, args...).Rows()
|
||||
}
|
||||
|
||||
// Exec 执行原生SQL
|
||||
func Exec(sql string, values ...interface{}) error {
|
||||
return DB.Exec(sql, values...).Error
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"yinli-api/src/model"
|
||||
"yinli-api/src/pkg/database"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// UserRepository 用户仓库接口
|
||||
type UserRepository interface {
|
||||
Create(user *model.User) error
|
||||
GetByID(id uint) (*model.User, error)
|
||||
GetByName(name string) (*model.User, error)
|
||||
Update(user *model.User) error
|
||||
Delete(id uint) error
|
||||
List(offset, limit int) ([]*model.User, int64, error)
|
||||
ExistsByName(name string) (bool, error)
|
||||
UpdateStatus(id uint, status int) error
|
||||
}
|
||||
|
||||
// userRepository 用户仓库实现
|
||||
type userRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserRepository 创建用户仓库实例
|
||||
func NewUserRepository() UserRepository {
|
||||
return &userRepository{
|
||||
db: database.GetDB(),
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (r *userRepository) Create(user *model.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (r *userRepository) GetByID(id uint) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.Where("id = ?", id).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByName 根据用户名获取用户
|
||||
func (r *userRepository) GetByName(name string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.Where("name = ?", name).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (r *userRepository) Update(user *model.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (r *userRepository) Delete(id uint) error {
|
||||
return r.db.Where("id = ?", id).Delete(&model.User{}).Error
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (r *userRepository) List(offset, limit int) ([]*model.User, int64, error) {
|
||||
var users []*model.User
|
||||
var total int64
|
||||
|
||||
// 获取总数
|
||||
if err := r.db.Model(&model.User{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := r.db.Offset(offset).Limit(limit).Find(&users).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// ExistsByName 检查用户名是否存在
|
||||
func (r *userRepository) ExistsByName(name string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.User{}).Where("name = ?", name).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// GetActiveUsers 获取活跃用户
|
||||
func (r *userRepository) GetActiveUsers() ([]*model.User, error) {
|
||||
var users []*model.User
|
||||
err := r.db.Where("status = ?", 1).Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
// UpdateStatus 更新用户状态
|
||||
func (r *userRepository) UpdateStatus(id uint, status int) error {
|
||||
return r.db.Model(&model.User{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
||||
@ -1,235 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"yinli-api/src/model"
|
||||
"yinli-api/src/repository"
|
||||
"yinli-api/src/pkg/auth"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// UserService 用户服务接口
|
||||
type UserService interface {
|
||||
Register(req *model.UserRegisterRequest) (*model.LoginResponse, error)
|
||||
Login(req *model.UserLoginRequest) (*model.LoginResponse, error)
|
||||
GetProfile(userID uint) (*model.UserResponse, error)
|
||||
UpdateProfile(userID uint, updates map[string]interface{}) (*model.UserResponse, error)
|
||||
ChangePassword(userID uint, oldPassword, newPassword string) error
|
||||
GetUserList(offset, limit int) ([]*model.UserResponse, int64, error)
|
||||
DeleteUser(userID uint) error
|
||||
UpdateUserStatus(userID uint, status int) error
|
||||
}
|
||||
|
||||
// userService 用户服务实现
|
||||
type userService struct {
|
||||
userRepo repository.UserRepository
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务实例
|
||||
func NewUserService(userRepo repository.UserRepository) UserService {
|
||||
return &userService{
|
||||
userRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
func (s *userService) Register(req *model.UserRegisterRequest) (*model.LoginResponse, error) {
|
||||
// 检查用户名是否已存在
|
||||
exists, err := s.userRepo.ExistsByName(req.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检查用户名失败: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, errors.New("用户名已存在")
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user := &model.User{
|
||||
Name: req.Name,
|
||||
Password: req.Password, // 密码会在BeforeCreate钩子中加密
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, fmt.Errorf("创建用户失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
token, err := auth.GenerateToken(user.ID, user.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成令牌失败: %w", err)
|
||||
}
|
||||
|
||||
return &model.LoginResponse{
|
||||
User: user.ToResponse(),
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (s *userService) Login(req *model.UserLoginRequest) (*model.LoginResponse, error) {
|
||||
// 根据用户名查找用户
|
||||
user, err := s.userRepo.GetByName(req.Name)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
return nil, fmt.Errorf("查找用户失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if !user.IsActive() {
|
||||
return nil, errors.New("用户账号已被禁用")
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !user.CheckPassword(req.Password) {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
token, err := auth.GenerateToken(user.ID, user.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成令牌失败: %w", err)
|
||||
}
|
||||
|
||||
return &model.LoginResponse{
|
||||
User: user.ToResponse(),
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetProfile 获取用户资料
|
||||
func (s *userService) GetProfile(userID uint) (*model.UserResponse, error) {
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("用户不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
|
||||
// UpdateProfile 更新用户资料
|
||||
func (s *userService) UpdateProfile(userID uint, updates map[string]interface{}) (*model.UserResponse, error) {
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("用户不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查用户名是否重复
|
||||
if newName, exists := updates["name"]; exists {
|
||||
if newName != user.Name {
|
||||
exists, err := s.userRepo.ExistsByName(newName.(string))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检查用户名失败: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, errors.New("用户名已存在")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
for key, value := range updates {
|
||||
switch key {
|
||||
case "name":
|
||||
user.Name = value.(string)
|
||||
case "status":
|
||||
user.Status = value.(int)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.userRepo.Update(user); err != nil {
|
||||
return nil, fmt.Errorf("更新用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *userService) ChangePassword(userID uint, oldPassword, newPassword string) error {
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
return fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
if !user.CheckPassword(oldPassword) {
|
||||
return errors.New("原密码错误")
|
||||
}
|
||||
|
||||
// 设置新密码
|
||||
if err := user.SetPassword(newPassword); err != nil {
|
||||
return fmt.Errorf("设置新密码失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
if err := s.userRepo.Update(user); err != nil {
|
||||
return fmt.Errorf("更新密码失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserList 获取用户列表
|
||||
func (s *userService) GetUserList(offset, limit int) ([]*model.UserResponse, int64, error) {
|
||||
users, total, err := s.userRepo.List(offset, limit)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("获取用户列表失败: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]*model.UserResponse, len(users))
|
||||
for i, user := range users {
|
||||
responses[i] = user.ToResponse()
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (s *userService) DeleteUser(userID uint) error {
|
||||
// 检查用户是否存在
|
||||
_, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
return fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
if err := s.userRepo.Delete(userID); err != nil {
|
||||
return fmt.Errorf("删除用户失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserStatus 更新用户状态
|
||||
func (s *userService) UpdateUserStatus(userID uint, status int) error {
|
||||
// 检查用户是否存在
|
||||
_, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
return fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
if err := s.userRepo.UpdateStatus(userID, status); err != nil {
|
||||
return fmt.Errorf("更新用户状态失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,156 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"yinli-api/src/pkg/config"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestLoadConfig 测试配置加载
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "加载开发环境配置",
|
||||
env: "dev",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "加载预发布环境配置",
|
||||
env: "stage",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "加载生产环境配置",
|
||||
env: "prod",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "加载不存在的配置",
|
||||
env: "nonexistent",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg, err := config.LoadConfig(tt.env)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, cfg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg)
|
||||
|
||||
// 验证配置结构
|
||||
assert.NotEmpty(t, cfg.Server.Port)
|
||||
assert.NotEmpty(t, cfg.Server.Mode)
|
||||
assert.NotEmpty(t, cfg.Database.Host)
|
||||
assert.NotEmpty(t, cfg.Database.Username)
|
||||
assert.NotEmpty(t, cfg.Database.DBName)
|
||||
assert.NotEmpty(t, cfg.Redis.Host)
|
||||
assert.NotEmpty(t, cfg.JWT.Secret)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetDSN 测试数据库连接字符串生成
|
||||
func TestGetDSN(t *testing.T) {
|
||||
cfg, err := config.LoadConfig("dev")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg)
|
||||
|
||||
dsn := cfg.GetDSN()
|
||||
assert.NotEmpty(t, dsn)
|
||||
assert.Contains(t, dsn, cfg.Database.Username)
|
||||
assert.Contains(t, dsn, cfg.Database.Host)
|
||||
assert.Contains(t, dsn, cfg.Database.DBName)
|
||||
}
|
||||
|
||||
// TestGetRedisAddr 测试Redis连接地址生成
|
||||
func TestGetRedisAddr(t *testing.T) {
|
||||
cfg, err := config.LoadConfig("dev")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg)
|
||||
|
||||
addr := cfg.GetRedisAddr()
|
||||
assert.NotEmpty(t, addr)
|
||||
assert.Contains(t, addr, cfg.Redis.Host)
|
||||
}
|
||||
|
||||
// TestGetEnv 测试环境变量获取
|
||||
func TestGetEnv(t *testing.T) {
|
||||
// 设置测试环境变量
|
||||
os.Setenv("TEST_VAR", "test_value")
|
||||
defer os.Unsetenv("TEST_VAR")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
defaultValue string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "存在的环境变量",
|
||||
key: "TEST_VAR",
|
||||
defaultValue: "default",
|
||||
expected: "test_value",
|
||||
},
|
||||
{
|
||||
name: "不存在的环境变量",
|
||||
key: "NON_EXISTENT_VAR",
|
||||
defaultValue: "default",
|
||||
expected: "default",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := config.GetEnv(tt.key, tt.defaultValue)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigValidation 测试配置验证
|
||||
func TestConfigValidation(t *testing.T) {
|
||||
cfg, err := config.LoadConfig("dev")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg)
|
||||
|
||||
// 验证服务器配置
|
||||
assert.True(t, cfg.Server.Port != "")
|
||||
assert.Contains(t, []string{"debug", "release"}, cfg.Server.Mode)
|
||||
|
||||
// 验证数据库配置
|
||||
assert.True(t, cfg.Database.Port > 0)
|
||||
assert.True(t, cfg.Database.MaxIdleConns > 0)
|
||||
assert.True(t, cfg.Database.MaxOpenConns > 0)
|
||||
|
||||
// 验证Redis配置
|
||||
assert.True(t, cfg.Redis.Port > 0)
|
||||
assert.True(t, cfg.Redis.PoolSize > 0)
|
||||
|
||||
// 验证JWT配置
|
||||
assert.True(t, cfg.JWT.ExpireHours > 0)
|
||||
assert.NotEmpty(t, cfg.JWT.Secret)
|
||||
|
||||
// 验证频率限制配置
|
||||
if cfg.RateLimit.Enabled {
|
||||
assert.True(t, cfg.RateLimit.Requests > 0)
|
||||
assert.True(t, cfg.RateLimit.Window > 0)
|
||||
}
|
||||
|
||||
// 验证CORS配置
|
||||
assert.True(t, len(cfg.CORS.AllowOrigins) > 0)
|
||||
assert.True(t, len(cfg.CORS.AllowMethods) > 0)
|
||||
assert.True(t, len(cfg.CORS.AllowHeaders) > 0)
|
||||
}
|
||||
@ -1,171 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"yinli-api/src/middleware"
|
||||
"yinli-api/src/pkg/auth"
|
||||
"yinli-api/src/pkg/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// setupMiddlewareTestRouter 设置中间件测试路由
|
||||
func setupMiddlewareTestRouter() *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
config.LoadConfig("dev")
|
||||
|
||||
r := gin.New()
|
||||
return r
|
||||
}
|
||||
|
||||
// TestAuthMiddleware 测试JWT认证中间件
|
||||
func TestAuthMiddleware(t *testing.T) {
|
||||
router := setupMiddlewareTestRouter()
|
||||
|
||||
// 生成测试token
|
||||
token, _ := auth.GenerateToken(1, "testuser")
|
||||
|
||||
router.GET("/protected", middleware.AuthMiddleware(), func(c *gin.Context) {
|
||||
userID, _ := middleware.GetUserID(c)
|
||||
username, _ := middleware.GetUsername(c)
|
||||
c.JSON(200, gin.H{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
})
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
authHeader string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "有效token",
|
||||
authHeader: "Bearer " + token,
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "无授权头",
|
||||
authHeader: "",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "无效token格式",
|
||||
authHeader: "InvalidFormat",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "无效token",
|
||||
authHeader: "Bearer invalid-token",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/protected", nil)
|
||||
if tt.authHeader != "" {
|
||||
req.Header.Set("Authorization", tt.authHeader)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCORSMiddleware 测试CORS中间件
|
||||
func TestCORSMiddleware(t *testing.T) {
|
||||
router := setupMiddlewareTestRouter()
|
||||
router.Use(middleware.CORSMiddleware())
|
||||
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "ok"})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Origin", "http://localhost:3000")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.NotEmpty(t, w.Header().Get("Access-Control-Allow-Origin"))
|
||||
}
|
||||
|
||||
// TestRequestIDMiddleware 测试请求ID中间件
|
||||
func TestRequestIDMiddleware(t *testing.T) {
|
||||
router := setupMiddlewareTestRouter()
|
||||
router.Use(middleware.RequestIDMiddleware())
|
||||
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
requestID := middleware.GetRequestID(c)
|
||||
c.JSON(200, gin.H{"request_id": requestID})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.NotEmpty(t, w.Header().Get("X-Request-ID"))
|
||||
}
|
||||
|
||||
// TestOptionalAuthMiddleware 测试可选认证中间件
|
||||
func TestOptionalAuthMiddleware(t *testing.T) {
|
||||
router := setupMiddlewareTestRouter()
|
||||
|
||||
token, _ := auth.GenerateToken(1, "testuser")
|
||||
|
||||
router.GET("/optional", middleware.OptionalAuthMiddleware(), func(c *gin.Context) {
|
||||
userID, exists := middleware.GetUserID(c)
|
||||
if exists {
|
||||
c.JSON(200, gin.H{"authenticated": true, "user_id": userID})
|
||||
} else {
|
||||
c.JSON(200, gin.H{"authenticated": false})
|
||||
}
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
authHeader string
|
||||
hasUserID bool
|
||||
}{
|
||||
{
|
||||
name: "有效token",
|
||||
authHeader: "Bearer " + token,
|
||||
hasUserID: true,
|
||||
},
|
||||
{
|
||||
name: "无token",
|
||||
authHeader: "",
|
||||
hasUserID: false,
|
||||
},
|
||||
{
|
||||
name: "无效token",
|
||||
authHeader: "Bearer invalid-token",
|
||||
hasUserID: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/optional", nil)
|
||||
if tt.authHeader != "" {
|
||||
req.Header.Set("Authorization", tt.authHeader)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
// 这里可以进一步验证响应内容
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"yinli-api/src/pkg/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// setupTestRouter 设置测试路由
|
||||
func setupTestRouter() *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
// 加载测试配置
|
||||
config.LoadConfig("dev")
|
||||
|
||||
r := gin.New()
|
||||
// 不使用完整的路由设置,避免Redis依赖
|
||||
// handler.SetupRoutes(r)
|
||||
|
||||
// 手动设置基本路由用于测试
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"message": "服务运行正常",
|
||||
})
|
||||
})
|
||||
|
||||
// 404 处理
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.JSON(404, gin.H{
|
||||
"code": 404,
|
||||
"message": "接口不存在",
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// TestUserRegister 测试用户注册 (需要数据库连接,暂时跳过)
|
||||
func TestUserRegister(t *testing.T) {
|
||||
t.Skip("需要数据库连接,跳过此测试")
|
||||
}
|
||||
|
||||
// TestUserLogin 测试用户登录 (需要数据库连接,暂时跳过)
|
||||
func TestUserLogin(t *testing.T) {
|
||||
t.Skip("需要数据库连接,跳过此测试")
|
||||
}
|
||||
|
||||
// TestGetProfile 测试获取用户资料 (需要数据库连接,暂时跳过)
|
||||
func TestGetProfile(t *testing.T) {
|
||||
t.Skip("需要数据库连接,跳过此测试")
|
||||
}
|
||||
|
||||
// TestHealthCheck 测试健康检查
|
||||
func TestHealthCheck(t *testing.T) {
|
||||
router := setupTestRouter()
|
||||
|
||||
req, _ := http.NewRequest("GET", "/health", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.Equal(t, "ok", response["status"])
|
||||
assert.Equal(t, "服务运行正常", response["message"])
|
||||
}
|
||||
|
||||
// TestNotFound 测试404处理
|
||||
func TestNotFound(t *testing.T) {
|
||||
router := setupTestRouter()
|
||||
|
||||
req, _ := http.NewRequest("GET", "/nonexistent", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.Equal(t, float64(404), response["code"])
|
||||
assert.Equal(t, "接口不存在", response["message"])
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user