commit d593fce014cca23046276c887d1a4e98a39346f2
Author: Table
Date: Sat Nov 29 03:27:19 2025 +0800
init repo
diff --git a/.air.toml b/.air.toml
new file mode 100644
index 0000000..55d3c51
--- /dev/null
+++ b/.air.toml
@@ -0,0 +1,45 @@
+# 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
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..db2cdf0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,65 @@
+# 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
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e337e78
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,83 @@
+# 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
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..585c14a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,57 @@
+# 多阶段构建
+FROM golang:1.21-alpine AS builder
+
+# 设置工作目录
+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 cmd/main.go
+
+# 最终镜像
+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 8080
+
+# 健康检查
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
+
+# 启动应用
+CMD ["./main"]
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6586b3a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,237 @@
+# Makefile for Yinli API
+
+# 变量定义
+APP_NAME := yinli-api
+VERSION := 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
+
+# 默认目标
+.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: ## 启动开发环境
+ @echo "启动开发环境..."
+ $(GO) run cmd/main.go -env=dev
+
+.PHONY: stage
+stage: ## 启动预发布环境
+ @echo "启动预发布环境..."
+ $(GO) run cmd/main.go -env=stage
+
+.PHONY: prod
+prod: ## 启动生产环境
+ @echo "启动生产环境..."
+ $(GO) run cmd/main.go -env=prod
+
+# 构建相关
+.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) cmd/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 cmd/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 cmd/main.go
+
+.PHONY: build-all
+build-all: build build-windows build-mac ## 构建所有平台版本
+
+# 依赖管理
+.PHONY: deps
+deps: ## 下载依赖
+ @echo "下载依赖..."
+ $(GO) mod download
+
+.PHONY: deps-update
+deps-update: ## 更新依赖
+ @echo "更新依赖..."
+ $(GO) mod tidy
+ $(GO) get -u ./...
+
+# 代码质量
+.PHONY: fmt
+fmt: ## 格式化代码
+ @echo "格式化代码..."
+ $(GOFMT) -s -w .
+
+.PHONY: vet
+vet: ## 代码静态检查
+ @echo "代码静态检查..."
+ $(GOVET) ./...
+
+.PHONY: check
+check: fmt vet ## 执行所有代码检查
+
+# 测试相关
+.PHONY: test
+test: ## 运行测试
+ @echo "运行测试..."
+ $(GO) test -v ./...
+
+.PHONY: test-coverage
+test-coverage: ## 运行测试并生成覆盖率报告
+ @echo "运行测试并生成覆盖率报告..."
+ @mkdir -p $(BUILD_DIR)
+ $(GO) test -v -coverprofile=$(BUILD_DIR)/coverage.out ./...
+ $(GO) tool cover -html=$(BUILD_DIR)/coverage.out -o $(BUILD_DIR)/coverage.html
+ @echo "覆盖率报告已生成: $(BUILD_DIR)/coverage.html"
+
+.PHONY: test-race
+test-race: ## 运行竞态检测测试
+ @echo "运行竞态检测测试..."
+ $(GO) test -race -v ./...
+
+.PHONY: benchmark
+benchmark: ## 运行基准测试
+ @echo "运行基准测试..."
+ $(GO) test -bench=. -benchmem ./...
+
+# 文档生成
+.PHONY: docs
+docs: ## 生成API文档
+ @echo "生成API文档..."
+ @mkdir -p $(DOC_DIR)/dev $(DOC_DIR)/stage $(DOC_DIR)/prod
+ @echo "请先安装 swag: go install github.com/swaggo/swag/cmd/swag@latest"
+ @echo "然后运行: swag init -g cmd/main.go -o doc/dev --parseDependency --parseInternal"
+
+.PHONY: docs-serve
+docs-serve: docs ## 启动文档服务器
+ @echo "启动文档服务器..."
+ @cd $(DOC_DIR) && python3 -m http.server 8081
+
+# Docker 相关
+.PHONY: docker-build
+docker-build: ## 构建Docker镜像
+ @echo "构建Docker镜像..."
+ $(DOCKER) build -t $(APP_NAME):$(VERSION) -t $(APP_NAME):latest .
+
+.PHONY: docker-compose-dev
+docker-compose-dev: ## 生成开发环境Docker Compose文件
+ @echo "生成开发环境Docker Compose文件..."
+ @mkdir -p $(DOCKER_DIR)
+ @echo "version: '3.8'" > $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo "" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @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 " - \"8080:8080\"" >> $(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 " depends_on:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " - mysql" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " - redis" >> $(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 " networks:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " - yinli-network" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo "" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " mysql:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " image: mysql:8.0" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " environment:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " MYSQL_ROOT_PASSWORD: sasasasa" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " MYSQL_DATABASE: yinli" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " ports:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " - \"3306:3306\"" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " volumes:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " - mysql_data:/var/lib/mysql" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " - ../sql:/docker-entrypoint-initdb.d:ro" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " networks:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " - yinli-network" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo "" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " redis:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " image: redis:7-alpine" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " ports:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " - \"6379:6379\"" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " volumes:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " - redis_data:/data" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " networks:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " - yinli-network" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo "" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo "volumes:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " mysql_data:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " redis_data:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo "" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo "networks:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " yinli-network:" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo " driver: bridge" >> $(DOCKER_DIR)/docker-compose.dev.yml
+ @echo "开发环境Docker Compose文件已生成: $(DOCKER_DIR)/docker-compose.dev.yml"
+
+.PHONY: docker-up-dev
+docker-up-dev: docker-compose-dev ## 启动开发环境Docker容器
+ @echo "启动开发环境Docker容器..."
+ cd $(DOCKER_DIR) && $(DOCKER_COMPOSE) -f docker-compose.dev.yml up -d
+
+.PHONY: docker-down
+docker-down: ## 停止并移除Docker容器
+ @echo "停止并移除Docker容器..."
+ cd $(DOCKER_DIR) && $(DOCKER_COMPOSE) down
+
+.PHONY: docker-logs
+docker-logs: ## 查看Docker容器日志
+ @echo "查看Docker容器日志..."
+ cd $(DOCKER_DIR) && $(DOCKER_COMPOSE) logs -f
+
+# 清理
+.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
\ No newline at end of file
diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md
new file mode 100644
index 0000000..2d60f11
--- /dev/null
+++ b/PROJECT_SUMMARY.md
@@ -0,0 +1,236 @@
+# Yinli API 项目总结
+
+## 🎉
+
+以下是项目的详细总结:
+
+## ✅ 已完成的功能
+
+### 1. 项目基础架构
+- ✅ 基于 Gin 框架的 RESTful API 服务
+- ✅ 清晰的项目目录结构(cmd、internal、pkg 分层)
+- ✅ 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 文件
+
+### 8. 测试框架
+- ✅ 使用 Testify 测试框架
+- ✅ 配置系统测试
+- ✅ 中间件测试
+- ✅ 接口测试框架(需要数据库连接时可启用)
+
+### 9. API 文档
+- ✅ Swagger 文档集成准备
+- ✅ 支持按环境分离文档生成
+- ✅ 文档保存在 `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
+
+# 启动 Docker 容器
+make docker-up-dev
+```
+
+### 3. 运行测试
+```bash
+# 运行所有测试
+make test
+
+# 生成测试覆盖率报告
+make test-coverage
+```
+
+## 📁 项目结构概览
+
+```
+yinli-api/
+├── cmd/main.go # 应用程序入口
+├── config/ # 配置文件目录
+│ ├── dev.yaml # 开发环境配置
+│ ├── stage.yaml # 预发布环境配置
+│ └── prod.yaml # 生产环境配置
+├── internal/ # 内部应用代码
+│ ├── handler/ # HTTP 处理器
+│ ├── middleware/ # 中间件
+│ ├── model/ # 数据模型
+│ ├── repository/ # 数据访问层
+│ └── service/ # 业务逻辑层
+├── pkg/ # 可复用包
+│ ├── auth/ # JWT 认证
+│ ├── cache/ # Redis 缓存
+│ ├── config/ # 配置管理
+│ └── database/ # 数据库连接
+├── 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 # 启动生产环境
+make build # 构建应用程序
+make test # 运行测试
+make docker-up-dev # 启动 Docker 开发环境
+make clean # 清理构建文件
+```
+
+## 📡 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 文档
+
+## 🔒 安全特性
+
+1. **JWT 认证**: 基于 JSON Web Token 的用户认证
+2. **CORS 保护**: 可配置的跨域资源共享
+3. **频率限制**: 基于 Redis 的 API 调用频率限制
+4. **密码加密**: 使用 bcrypt 加密用户密码
+5. **中间件保护**: 多层安全中间件防护
+
+## 🌍 环境配置
+
+项目支持三种环境配置,每种环境都有独立的配置文件:
+
+- **dev**: 开发环境,详细日志,宽松的安全设置
+- **stage**: 预发布环境,生产级配置,用于测试
+- **prod**: 生产环境,最严格的安全设置
+
+## 📊 测试覆盖
+
+项目包含以下测试:
+- 配置系统测试
+- JWT 认证中间件测试
+- CORS 中间件测试
+- 请求 ID 中间件测试
+- 基础 API 接口测试
+
+## 🚀 部署建议
+
+### 开发环境
+```bash
+make dev
+```
+
+### 生产环境
+```bash
+# 使用 Docker 部署
+make docker-compose-prod
+make docker-up-prod
+```
+
+## 📝 下一步建议
+
+1. **数据库设置**: 确保 MySQL 数据库运行并执行 `sql/init.sql`
+2. **Redis 设置**: 确保 Redis 服务运行
+3. **环境变量**: 在生产环境中设置敏感配置的环境变量
+4. **SSL 证书**: 在生产环境中配置 HTTPS
+5. **监控日志**: 添加应用监控和日志收集
+6. **API 文档**: 运行 `make install-tools` 安装 Swagger 工具并生成文档
+
+## 🎯 项目特色
+
+- **完整的企业级架构**: 分层清晰,易于维护
+- **高度可配置**: 支持多环境配置
+- **安全性强**: 多重安全防护机制
+- **易于部署**: 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)
diff --git a/PROJECT_SUMMARY.pdf b/PROJECT_SUMMARY.pdf
new file mode 100644
index 0000000..d71ce8b
Binary files /dev/null and b/PROJECT_SUMMARY.pdf differ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d72952b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,410 @@
+# Yinli API
+
+基于 Golang Gin 框架构建的高性能 RESTful API 服务,集成了 JWT 认证、Redis 缓存、频率限制、CORS 等安全特性。
+
+## 🚀 技术架构
+
+### 核心技术栈
+
+- **Web 框架**: [Gin](https://gin-gonic.com/) - 高性能的 HTTP Web 框架
+- **数据库**: [MySQL 8.0](https://dev.mysql.com/doc/) - 关系型数据库
+- **缓存**: [Redis](https://redis.io/) - 内存数据库,用于缓存和频率限制
+- **ORM**: [GORM](https://gorm.io/) - Go 语言 ORM 库
+- **配置管理**: [Viper](https://github.com/spf13/viper) - 配置文件管理
+- **认证**: [JWT](https://jwt.io/) - JSON Web Token 认证
+- **文档**: [Swagger](https://swagger.io/) - API 文档自动生成
+- **测试**: [Testify](https://github.com/stretchr/testify) - 测试框架
+- **容器化**: [Docker](https://www.docker.com/) - 容器化部署
+
+### 安全特性
+
+- **JWT 认证**: 基于 JSON Web Token 的用户认证
+- **CORS 支持**: 跨域资源共享配置
+- **频率限制**: 基于 Redis 的 API 频率限制
+- **密码加密**: 使用 bcrypt 加密用户密码
+- **中间件保护**: 多层中间件安全防护
+
+### 项目结构
+
+```
+yinli-api/
+├── cmd/ # 应用程序入口
+│ └── main.go
+├── internal/ # 内部应用代码
+│ ├── handler/ # HTTP 处理器
+│ ├── middleware/ # 中间件
+│ ├── model/ # 数据模型
+│ ├── repository/ # 数据访问层
+│ └── service/ # 业务逻辑层
+├── pkg/ # 可复用的包
+│ ├── auth/ # 认证相关
+│ ├── cache/ # 缓存操作
+│ ├── config/ # 配置管理
+│ └── database/ # 数据库连接
+├── config/ # 配置文件
+│ ├── dev.yaml # 开发环境配置
+│ ├── stage.yaml # 预发布环境配置
+│ └── prod.yaml # 生产环境配置
+├── docker/ # Docker 相关文件
+├── doc/ # API 文档
+├── sql/ # 数据库脚本
+├── test/ # 测试文件
+├── build/ # 构建输出
+├── Dockerfile # Docker 镜像构建文件
+├── Makefile # 构建脚本
+└── README.md # 项目说明
+```
+
+## 🛠️ 快速开始
+
+### 环境要求
+
+- Go 1.21+
+- MySQL 8.0+
+- Redis 6.0+
+- Docker & Docker Compose (可选)
+
+### 本地开发
+
+1. **克隆项目**
+```bash
+git clone
+cd yinli-api
+```
+
+2. **安装依赖**
+```bash
+make deps
+```
+
+3. **配置数据库**
+```bash
+# 创建数据库
+mysql -u root -p < sql/init.sql
+```
+
+4. **启动 Redis**
+```bash
+redis-server
+```
+
+5. **启动开发环境**
+```bash
+make dev
+```
+
+### Docker 部署
+
+1. **生成 Docker Compose 文件**
+```bash
+make docker-compose-dev
+```
+
+2. **启动服务**
+```bash
+make docker-up-dev
+```
+
+## 📋 可用命令
+
+### 开发命令
+
+```bash
+make dev # 启动开发环境
+make stage # 启动预发布环境
+make prod # 启动生产环境
+```
+
+### 构建命令
+
+```bash
+make build # 构建 Linux 版本
+make build-all # 构建所有平台版本
+make clean # 清理构建文件
+```
+
+### 测试命令
+
+```bash
+make test # 运行测试
+make test-coverage # 运行测试并生成覆盖率报告
+make benchmark # 运行基准测试
+```
+
+### 代码质量
+
+```bash
+make fmt # 格式化代码
+make vet # 静态检查
+make lint # 代码规范检查
+make check # 执行所有检查
+```
+
+### 文档生成
+
+```bash
+make docs # 生成 API 文档
+make docs-serve # 启动文档服务器
+```
+
+### Docker 操作
+
+```bash
+make docker-build # 构建 Docker 镜像
+make docker-up-dev # 启动开发环境容器
+make docker-up-stage # 启动预发布环境容器
+make docker-up-prod # 启动生产环境容器
+make docker-down # 停止容器
+make docker-logs # 查看容器日志
+```
+
+## 🔧 配置说明
+
+### 环境配置
+
+项目支持三种环境配置:
+
+- `dev` - 开发环境
+- `stage` - 预发布环境
+- `prod` - 生产环境
+
+配置文件位于 `config/` 目录下,可通过环境变量 `APP_ENV` 或命令行参数 `-env` 指定。
+
+### 主要配置项
+
+```yaml
+server:
+ port: 1234 # 服务端口
+ mode: debug # 运行模式 (debug/release)
+
+database:
+ host: localhost # 数据库地址
+ port: 3306 # 数据库端口
+ username: root # 数据库用户名
+ password: sasasasa # 数据库密码
+ dbname: yinli # 数据库名称
+
+redis:
+ host: localhost # Redis 地址
+ port: 6379 # Redis 端口
+ password: "" # Redis 密码
+ db: 0 # Redis 数据库
+
+jwt:
+ secret: your-secret-key # JWT 密钥
+ expireHours: 24 # 令牌过期时间(小时)
+
+rateLimit:
+ enabled: true # 是否启用频率限制
+ requests: 100 # 每个时间窗口的请求数
+ window: 60 # 时间窗口(秒)
+```
+
+## 📡 API 接口
+
+### 认证接口
+
+| 方法 | 路径 | 描述 | 认证 |
+|------|------|------|------|
+| POST | `/api/auth/register` | 用户注册 | ❌ |
+| POST | `/api/auth/login` | 用户登录 | ❌ |
+
+### 用户接口
+
+| 方法 | 路径 | 描述 | 认证 |
+|------|------|------|------|
+| 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 文档 | ❌ |
+
+## 🧪 接口测试
+
+### 使用 curl 测试
+
+1. **用户注册**
+```bash
+curl -X POST http://localhost:1234/api/auth/register \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "testuser",
+ "password": "password123"
+ }'
+```
+
+2. **用户登录**
+```bash
+curl -X POST http://localhost:1234/api/auth/login \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "testuser",
+ "password": "password123"
+ }'
+```
+
+3. **获取用户资料**
+```bash
+curl -X GET http://localhost:1234/api/user/profile \
+ -H "Authorization: Bearer YOUR_JWT_TOKEN"
+```
+
+### 使用 Postman
+
+1. 导入 Postman 集合文件(如果有)
+2. 设置环境变量 `base_url` 为 `http://localhost:1234`
+3. 在认证接口获取 JWT token
+4. 在需要认证的接口中添加 `Authorization: Bearer {token}` 头
+
+## 🔍 开发调试
+
+### 日志查看
+
+```bash
+# 查看应用日志
+tail -f logs/app.log
+
+# 查看 Docker 容器日志
+make docker-logs
+```
+
+### 数据库调试
+
+```bash
+# 连接数据库
+mysql -h localhost -u root -p yinli
+
+# 查看用户表
+SELECT * FROM user;
+```
+
+### Redis 调试
+
+```bash
+# 连接 Redis
+redis-cli
+
+# 查看所有键
+KEYS *
+
+# 查看频率限制
+KEYS rate_limit:*
+```
+
+### 性能监控
+
+```bash
+# 查看应用性能
+go tool pprof http://localhost:1234/debug/pprof/profile
+
+# 内存使用情况
+go tool pprof http://localhost:1234/debug/pprof/heap
+```
+
+## 📊 测试报告
+
+运行测试并生成报告:
+
+```bash
+make test-coverage
+```
+
+测试报告将生成在 `build/coverage.html`,可在浏览器中查看详细的覆盖率信息。
+
+## 🚀 部署指南
+
+### 生产环境部署
+
+1. **构建生产版本**
+```bash
+make build
+```
+
+2. **配置生产环境**
+```bash
+# 修改 config/prod.yaml
+# 设置正确的数据库和 Redis 连接信息
+# 更改 JWT 密钥
+```
+
+3. **使用 Docker 部署**
+```bash
+make docker-compose-prod
+make docker-up-prod
+```
+
+### 环境变量
+
+生产环境建议使用环境变量覆盖敏感配置:
+
+```bash
+export YINLI_DATABASE_PASSWORD=your_db_password
+export YINLI_JWT_SECRET=your_jwt_secret
+export YINLI_REDIS_PASSWORD=your_redis_password
+```
+
+## 📚 主要依赖库
+
+### 核心依赖
+
+- [Gin Web Framework](https://github.com/gin-gonic/gin) - HTTP Web 框架
+- [GORM](https://github.com/go-gorm/gorm) - ORM 库
+- [Viper](https://github.com/spf13/viper) - 配置管理
+- [JWT-Go](https://github.com/golang-jwt/jwt) - JWT 实现
+- [Go-Redis](https://github.com/go-redis/redis) - Redis 客户端
+
+### 中间件
+
+- [Gin-CORS](https://github.com/gin-contrib/cors) - CORS 中间件
+- [Gin-Swagger](https://github.com/swaggo/gin-swagger) - Swagger 文档
+
+### 测试工具
+
+- [Testify](https://github.com/stretchr/testify) - 测试断言库
+- [HTTP Test](https://golang.org/pkg/net/http/httptest/) - HTTP 测试工具
+
+### 开发工具
+
+- [Air](https://github.com/cosmtrek/air) - 热重载工具
+- [GolangCI-Lint](https://github.com/golangci/golangci-lint) - 代码检查工具
+
+## 🤝 贡献指南
+
+1. Fork 项目
+2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 开启 Pull Request
+
+## 📄 许可证
+
+本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
+
+## 🆘 常见问题
+
+### Q: 如何修改数据库连接?
+A: 修改 `config/{env}.yaml` 文件中的 `database` 配置项。
+
+### Q: 如何添加新的 API 接口?
+A: 1. 在 `internal/handler` 中添加处理函数
+ 2. 在 `internal/handler/router.go` 中注册路由
+ 3. 添加相应的测试用例
+
+### Q: 如何自定义中间件?
+A: 在 `internal/middleware` 目录下创建新的中间件文件,参考现有中间件的实现。
+
+### Q: 如何部署到生产环境?
+A: 使用 `make docker-compose-prod` 生成生产环境配置,然后使用 `make docker-up-prod` 部署。
diff --git a/README.pdf b/README.pdf
new file mode 100644
index 0000000..f0a4514
Binary files /dev/null and b/README.pdf differ
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 0000000..6ae5583
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "yinli-api/internal/handler"
+ "yinli-api/pkg/cache"
+ "yinli-api/pkg/config"
+ "yinli-api/pkg/database"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @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)
+ }
+
+ // 启动服务器
+ 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("服务器已关闭")
+}
diff --git a/config/dev.yaml b/config/dev.yaml
new file mode 100644
index 0000000..b285782
--- /dev/null
+++ b/config/dev.yaml
@@ -0,0 +1,41 @@
+server:
+ port: 1234
+ mode: debug
+
+database:
+ host: localhost
+ port: 3306
+ username: root
+ password: sasasasa
+ dbname: yinli
+ charset: utf8mb4
+ parseTime: true
+ loc: Local
+ maxIdleConns: 10
+ maxOpenConns: 100
+
+redis:
+ host: 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
diff --git a/config/prod.yaml b/config/prod.yaml
new file mode 100644
index 0000000..71864a9
--- /dev/null
+++ b/config/prod.yaml
@@ -0,0 +1,41 @@
+server:
+ port: 1234
+ mode: release
+
+database:
+ host: localhost
+ port: 3306
+ username: root
+ password: sasasasa
+ dbname: yinli
+ charset: utf8mb4
+ parseTime: true
+ loc: Local
+ maxIdleConns: 50
+ maxOpenConns: 500
+
+redis:
+ host: 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
diff --git a/config/stage.yaml b/config/stage.yaml
new file mode 100644
index 0000000..21077c2
--- /dev/null
+++ b/config/stage.yaml
@@ -0,0 +1,41 @@
+server:
+ port: 1234
+ mode: release
+
+database:
+ host: localhost
+ port: 3306
+ username: root
+ password: sasasasa
+ dbname: yinli
+ charset: utf8mb4
+ parseTime: true
+ loc: Local
+ maxIdleConns: 20
+ maxOpenConns: 200
+
+redis:
+ host: 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
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
new file mode 100644
index 0000000..d09b3bb
--- /dev/null
+++ b/docker/docker-compose.dev.yml
@@ -0,0 +1,48 @@
+version: '3.8'
+
+services:
+ yinli-api:
+ build:
+ context: ..
+ dockerfile: Dockerfile
+ ports:
+ - "8080:8080"
+ environment:
+ - APP_ENV=dev
+ depends_on:
+ - mysql
+ - redis
+ volumes:
+ - ../config:/app/config:ro
+ networks:
+ - yinli-network
+
+ mysql:
+ image: mysql:8.0
+ environment:
+ MYSQL_ROOT_PASSWORD: sasasasa
+ MYSQL_DATABASE: yinli
+ ports:
+ - "3306:3306"
+ volumes:
+ - mysql_data:/var/lib/mysql
+ - ../sql:/docker-entrypoint-initdb.d:ro
+ networks:
+ - yinli-network
+
+ redis:
+ image: redis:7-alpine
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data:/data
+ networks:
+ - yinli-network
+
+volumes:
+ mysql_data:
+ redis_data:
+
+networks:
+ yinli-network:
+ driver: bridge
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..b21180c
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,78 @@
+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
+ 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/swaggo/swag v1.8.12 // 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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..ace7216
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,242 @@
+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=
diff --git a/internal/handler/router.go b/internal/handler/router.go
new file mode 100644
index 0000000..598715b
--- /dev/null
+++ b/internal/handler/router.go
@@ -0,0 +1,133 @@
+package handler
+
+import (
+ "yinli-api/internal/middleware"
+ "yinli-api/internal/repository"
+ "yinli-api/internal/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功能正常",
+ })
+ })
+ }
+}
diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go
new file mode 100644
index 0000000..64ebf48
--- /dev/null
+++ b/internal/handler/user_handler.go
@@ -0,0 +1,381 @@
+package handler
+
+import (
+ "net/http"
+ "strconv"
+
+ "yinli-api/internal/middleware"
+ "yinli-api/internal/model"
+ "yinli-api/internal/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": "状态更新成功",
+ })
+}
diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go
new file mode 100644
index 0000000..9990f42
--- /dev/null
+++ b/internal/middleware/auth.go
@@ -0,0 +1,125 @@
+package middleware
+
+import (
+ "net/http"
+
+ "yinli-api/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()
+ }
+}
diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go
new file mode 100644
index 0000000..9dc1a70
--- /dev/null
+++ b/internal/middleware/cors.go
@@ -0,0 +1,31 @@
+package middleware
+
+import (
+ "yinli-api/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,
+ })
+}
diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go
new file mode 100644
index 0000000..cbe5d51
--- /dev/null
+++ b/internal/middleware/logger.go
@@ -0,0 +1,49 @@
+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 ""
+}
diff --git a/internal/middleware/rate_limit.go b/internal/middleware/rate_limit.go
new file mode 100644
index 0000000..6509145
--- /dev/null
+++ b/internal/middleware/rate_limit.go
@@ -0,0 +1,207 @@
+package middleware
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "time"
+
+ "yinli-api/pkg/cache"
+ "yinli-api/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()
+ }
+}
diff --git a/internal/model/user.go b/internal/model/user.go
new file mode 100644
index 0000000..42984ed
--- /dev/null
+++ b/internal/model/user.go
@@ -0,0 +1,94 @@
+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
+}
diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go
new file mode 100644
index 0000000..3078e26
--- /dev/null
+++ b/internal/repository/user_repository.go
@@ -0,0 +1,105 @@
+package repository
+
+import (
+ "yinli-api/internal/model"
+ "yinli-api/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
+}
diff --git a/internal/service/user_service.go b/internal/service/user_service.go
new file mode 100644
index 0000000..d2c53b2
--- /dev/null
+++ b/internal/service/user_service.go
@@ -0,0 +1,235 @@
+package service
+
+import (
+ "errors"
+ "fmt"
+
+ "yinli-api/internal/model"
+ "yinli-api/internal/repository"
+ "yinli-api/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
+}
diff --git a/localhost.session.sql b/localhost.session.sql
new file mode 100644
index 0000000..5ba3848
--- /dev/null
+++ b/localhost.session.sql
@@ -0,0 +1,2 @@
+SELECT id, name, password, status, created_at, updated_at
+FROM yinli.`user`;
\ No newline at end of file
diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go
new file mode 100644
index 0000000..1c18df1
--- /dev/null
+++ b/pkg/auth/jwt.go
@@ -0,0 +1,131 @@
+package auth
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "yinli-api/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
+}
diff --git a/pkg/cache/redis.go b/pkg/cache/redis.go
new file mode 100644
index 0000000..550865e
--- /dev/null
+++ b/pkg/cache/redis.go
@@ -0,0 +1,187 @@
+package cache
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "time"
+
+ "yinli-api/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
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
new file mode 100644
index 0000000..46c027f
--- /dev/null
+++ b/pkg/config/config.go
@@ -0,0 +1,135 @@
+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)
+}
diff --git a/pkg/database/database.go b/pkg/database/database.go
new file mode 100644
index 0000000..0e049cc
--- /dev/null
+++ b/pkg/database/database.go
@@ -0,0 +1,98 @@
+package database
+
+import (
+ "database/sql"
+ "fmt"
+ "log"
+ "time"
+
+ "yinli-api/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
+}
diff --git a/sql/init.sql b/sql/init.sql
new file mode 100644
index 0000000..28ced4f
--- /dev/null
+++ b/sql/init.sql
@@ -0,0 +1,62 @@
+-- 创建数据库(如果不存在)
+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访问日志表';
diff --git a/test/config_test.go b/test/config_test.go
new file mode 100644
index 0000000..98c79b3
--- /dev/null
+++ b/test/config_test.go
@@ -0,0 +1,156 @@
+package test
+
+import (
+ "os"
+ "testing"
+
+ "yinli-api/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)
+}
diff --git a/test/middleware_test.go b/test/middleware_test.go
new file mode 100644
index 0000000..ed5b1d8
--- /dev/null
+++ b/test/middleware_test.go
@@ -0,0 +1,171 @@
+package test
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "yinli-api/internal/middleware"
+ "yinli-api/pkg/auth"
+ "yinli-api/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)
+ // 这里可以进一步验证响应内容
+ })
+ }
+}
diff --git a/test/user_test.go b/test/user_test.go
new file mode 100644
index 0000000..395ea23
--- /dev/null
+++ b/test/user_test.go
@@ -0,0 +1,90 @@
+package test
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "yinli-api/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"])
+}