Initial commit
This commit is contained in:
20
.env.example
Normal file
20
.env.example
Normal file
@@ -0,0 +1,20 @@
|
||||
# Server Configuration
|
||||
PORT=3000
|
||||
ENVIRONMENT=development
|
||||
|
||||
# Database Configuration
|
||||
# Possible values for DATABASE_DRIVER: sqlite, postgres
|
||||
DATABASE_DRIVER=sqlite
|
||||
DATABASE_PATH=./data/database.sqlite
|
||||
|
||||
# PostgreSQL Configuration (Only if DATABASE_DRIVER is postgres)
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USER=trace
|
||||
POSTGRES_PASSWORD=trace123
|
||||
POSTGRES_DB=trace
|
||||
POSTGRES_SSLMODE=disable
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-secret-key-here-change-in-production
|
||||
JWT_EXPIRE=7200
|
||||
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Binary files
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
trace-backend
|
||||
|
||||
# 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
|
||||
|
||||
# Configuration files
|
||||
.env
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Data files
|
||||
data/*.sqlite
|
||||
tests/data/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
25
.golangci.yml
Normal file
25
.golangci.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
# golangci-lint 配置文件
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
goimports:
|
||||
local-prefixes: github.com/beifan/trace-backend
|
||||
govet:
|
||||
check-shadowing: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- errcheck
|
||||
- goimports
|
||||
- govet
|
||||
- misspell
|
||||
- revive
|
||||
- typecheck
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- testdata
|
||||
timeout: 5m
|
||||
81
Makefile
Normal file
81
Makefile
Normal file
@@ -0,0 +1,81 @@
|
||||
NAME=trace-backend
|
||||
VERSION=1.0.0
|
||||
|
||||
.PHONY: all clean build test test-coverage lint run
|
||||
|
||||
# 默认任务
|
||||
all: clean build
|
||||
|
||||
# 清理二进制文件
|
||||
clean:
|
||||
@echo "清理二进制文件..."
|
||||
@if exist $(NAME).exe del $(NAME).exe
|
||||
@if exist $(NAME) del $(NAME)
|
||||
@if exist coverage.out del coverage.out
|
||||
@if exist coverage.html del coverage.html
|
||||
|
||||
# 编译
|
||||
build:
|
||||
@echo "编译项目..."
|
||||
@go build -o $(NAME).exe main.go
|
||||
|
||||
# 运行
|
||||
run:
|
||||
@echo "启动服务器..."
|
||||
@go run main.go
|
||||
|
||||
# 测试
|
||||
test:
|
||||
@echo "运行测试..."
|
||||
@go test -v ./tests/...
|
||||
|
||||
# 测试覆盖率
|
||||
test-coverage:
|
||||
@echo "生成测试覆盖率报告..."
|
||||
@go test -v ./tests/... -coverprofile=coverage.out
|
||||
@go tool cover -html=coverage.out -o coverage.html
|
||||
@echo "测试覆盖率报告已生成: coverage.html"
|
||||
|
||||
# 代码检查
|
||||
lint:
|
||||
@echo "检查代码..."
|
||||
@golangci-lint run ./...
|
||||
|
||||
# 格式化代码
|
||||
fmt:
|
||||
@echo "格式化代码..."
|
||||
@gofmt -w .
|
||||
|
||||
# 导入格式化
|
||||
imports:
|
||||
@echo "格式化导入..."
|
||||
@goimports -w .
|
||||
|
||||
# 代码质量检查(格式化 + 检查)
|
||||
quality: fmt imports lint
|
||||
|
||||
# 安装依赖
|
||||
deps:
|
||||
@echo "安装依赖..."
|
||||
@go mod tidy
|
||||
|
||||
# 初始化数据库
|
||||
init-db:
|
||||
@echo "初始化数据库..."
|
||||
@go run main.go
|
||||
|
||||
# 编译为 Linux 版本
|
||||
build-linux:
|
||||
@echo "编译 Linux 版本..."
|
||||
@set CGO_ENABLED=0
|
||||
@set GOOS=linux
|
||||
@set GOARCH=amd64
|
||||
@go build -o $(NAME) main.go
|
||||
|
||||
# 编译为 macOS 版本
|
||||
build-mac:
|
||||
@echo "编译 macOS 版本..."
|
||||
@set CGO_ENABLED=0
|
||||
@set GOOS=darwin
|
||||
@set GOARCH=arm64
|
||||
@go build -o $(NAME) main.go
|
||||
290
README.md
Normal file
290
README.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# 浙江贝凡溯源管理平台 - 后端服务 (Go 版本)
|
||||
|
||||
这是一个使用 Go 语言开发的溯源管理平台后端服务,提供序列号生成、查询、管理等功能。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Web 框架**: Gin (高性能 HTTP 框架)
|
||||
- **ORM**: GORM (Go 对象关系映射)
|
||||
- **数据库**:
|
||||
- SQLite (默认,适用于开发和轻量级部署)
|
||||
- PostgreSQL (生产环境推荐)
|
||||
- **认证**: JWT (JSON Web Token) - golang-jwt/jwt/v5
|
||||
- **密码加密**: bcrypt (golang.org/x/crypto)
|
||||
- **二维码生成**: yeqown/go-qrcode/v2
|
||||
- **配置管理**:
|
||||
- Viper (环境变量和配置文件)
|
||||
- gotenv (.env 文件加载)
|
||||
- **日志**: Zap (高性能结构化日志)
|
||||
- **验证**: go-playground/validator
|
||||
- **测试**: Testify (测试框架)
|
||||
- **工具**: UUID (github.com/google/uuid)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
backend-go/
|
||||
├── config/ # 配置管理
|
||||
│ └── config.go # 配置加载和解析(支持 .env 文件和环境变量)
|
||||
├── controllers/ # 控制器层,处理 HTTP 请求
|
||||
│ ├── auth_controller.go # 认证相关接口
|
||||
│ ├── companies_controller.go # 企业管理接口
|
||||
│ ├── helper.go # 控制器通用辅助函数
|
||||
│ └── serials_controller.go # 序列号管理接口
|
||||
├── database/ # 数据库连接和操作
|
||||
│ └── database.go # 数据库初始化、连接池配置
|
||||
├── logger/ # 日志管理
|
||||
│ └── logger.go # 结构化日志(使用 Zap)
|
||||
├── middleware/ # 中间件层
|
||||
│ └── auth.go # JWT 认证和权限检查
|
||||
├── models/ # 数据模型和 DTO
|
||||
│ └── models.go # User、Company、Serial 等模型定义
|
||||
├── routes/ # 路由配置
|
||||
│ └── routes.go # API 路由注册
|
||||
├── services/ # 业务逻辑层
|
||||
│ ├── auth_service.go # 认证业务逻辑
|
||||
│ ├── companies_service.go # 企业管理业务逻辑
|
||||
│ ├── serials_service.go # 序列号业务逻辑
|
||||
│ └── services_test.go # 服务层单元测试
|
||||
├── tests/ # 集成测试
|
||||
│ └── main_test.go # 端到端测试
|
||||
├── data/ # 数据目录 (SQLite 数据库存储)
|
||||
├── main.go # 应用程序入口
|
||||
├── go.mod # Go 模块依赖
|
||||
├── go.sum # Go 模块校验和
|
||||
├── Makefile # 构建和开发任务
|
||||
├── .env.example # 环境变量示例
|
||||
├── .env # 环境变量配置(需手动创建)
|
||||
├── .golangci.yml # 代码检查配置
|
||||
└── .gitignore # Git 忽略文件
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
cd backend-go
|
||||
go mod download
|
||||
|
||||
# 或使用 Makefile
|
||||
make deps
|
||||
```
|
||||
|
||||
### 2. 配置环境变量
|
||||
|
||||
复制 `.env.example` 文件为 `.env` 并根据需要修改:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
**重要**: 生产环境请务必修改 `JWT_SECRET` 环境变量!
|
||||
|
||||
### 3. 使用 Makefile(推荐)
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
make run
|
||||
|
||||
# 编译项目
|
||||
make build
|
||||
|
||||
# 运行测试
|
||||
make test
|
||||
|
||||
# 生成测试覆盖率报告
|
||||
make test-coverage
|
||||
|
||||
# 代码质量检查
|
||||
make quality
|
||||
|
||||
# 清理构建文件
|
||||
make clean
|
||||
```
|
||||
|
||||
### 4. 手动启动
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
服务器将在 http://localhost:3000 上运行。
|
||||
|
||||
### 5. 环境变量配置
|
||||
|
||||
项目支持以下环境变量(按优先级排序):
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `PORT` | 服务器端口 | 3000 |
|
||||
| `ENVIRONMENT` | 运行环境 (development/production) | development |
|
||||
| `JWT_SECRET` | JWT 签名密钥 | your-secret-key-here-change-in-production |
|
||||
| `JWT_EXPIRE` | JWT 过期时间(秒) | 7200 |
|
||||
| `DATABASE_DRIVER` | 数据库驱动 (sqlite/postgres) | sqlite |
|
||||
| `DATABASE_PATH` | SQLite 数据库路径 | ./data/database.sqlite |
|
||||
| `POSTGRES_HOST` | PostgreSQL 主机 | localhost |
|
||||
| `POSTGRES_PORT` | PostgreSQL 端口 | 5432 |
|
||||
| `POSTGRES_USER` | PostgreSQL 用户名 | trace |
|
||||
| `POSTGRES_PASSWORD` | PostgreSQL 密码 | trace123 |
|
||||
| `POSTGRES_DB` | PostgreSQL 数据库名 | trace |
|
||||
| `POSTGRES_SSLMODE` | PostgreSQL SSL 模式 | disable |
|
||||
|
||||
**注意**: 环境变量也可以通过 `.env` 文件设置。
|
||||
|
||||
### 6. 测试 API
|
||||
|
||||
**健康检查**:
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
**用户登录**:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"password123"}'
|
||||
```
|
||||
|
||||
## API 文档
|
||||
|
||||
### 认证路由
|
||||
|
||||
| 方法 | 路径 | 描述 | 需要认证 |
|
||||
| ---- | --------------------------- | ----------------------- | -------- |
|
||||
| POST | `/api/auth/login` | 用户登录,返回 JWT 令牌 | 否 |
|
||||
| GET | `/api/auth/profile` | 获取当前用户信息 | 是 |
|
||||
| PUT | `/api/auth/profile` | 更新用户信息 | 是 |
|
||||
| POST | `/api/auth/change-password` | 修改密码 | 是 |
|
||||
|
||||
### 序列号管理
|
||||
|
||||
| 方法 | 路径 | 描述 | 需要认证 | 角色 |
|
||||
| ---- | ----------------------------------- | ---------------- | -------- | ------ |
|
||||
| POST | `/api/serials/generate` | 生成序列号 | 是 | 管理员 |
|
||||
| POST | `/api/serials/generate-with-prefix` | 带前缀生成序列号 | 是 | 管理员 |
|
||||
| POST | `/api/serials/:serialNumber/qrcode` | 生成序列号二维码 | 是 | 任何 |
|
||||
| GET | `/api/serials/:serialNumber/query` | 查询序列号信息 | 否 | 任何 |
|
||||
| GET | `/api/serials` | 获取序列号列表 | 是 | 任何 |
|
||||
| PUT | `/api/serials/:serialNumber` | 更新序列号信息 | 是 | 管理员 |
|
||||
| POST | `/api/serials/:serialNumber/revoke` | 吊销序列号 | 是 | 管理员 |
|
||||
|
||||
### 企业管理
|
||||
|
||||
| 方法 | 路径 | 描述 | 需要认证 | 角色 |
|
||||
| ------ | ----------------------------- | ------------ | -------- | ------ |
|
||||
| GET | `/api/companies` | 获取企业列表 | 是 | 任何 |
|
||||
| POST | `/api/companies` | 创建新企业 | 是 | 管理员 |
|
||||
| PUT | `/api/companies/:companyName` | 更新企业信息 | 是 | 管理员 |
|
||||
| DELETE | `/api/companies/:companyName` | 删除企业 | 是 | 管理员 |
|
||||
|
||||
## 测试
|
||||
|
||||
### 运行所有测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
go test -v ./...
|
||||
|
||||
# 仅运行服务层单元测试
|
||||
cd services && go test -v -cover
|
||||
|
||||
# 仅运行集成测试
|
||||
go test -v ./tests/...
|
||||
```
|
||||
|
||||
### 生成测试覆盖率报告
|
||||
|
||||
```bash
|
||||
# 生成覆盖率报告
|
||||
go test -v ./services/... -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out
|
||||
```
|
||||
|
||||
### 当前测试覆盖
|
||||
|
||||
- **services/**: 包含 AuthService 和 SerialsService 的完整单元测试
|
||||
- 用户认证测试(登录、获取用户信息、修改密码、更新资料)
|
||||
- 序列号管理测试(生成、查询、更新、吊销、分页列表)
|
||||
- **tests/**: 集成测试(健康检查、登录流程)
|
||||
|
||||
## 代码检查
|
||||
|
||||
使用 golangci-lint 进行代码检查:
|
||||
|
||||
```bash
|
||||
golangci-lint run ./...
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
### 编译为二进制文件
|
||||
|
||||
```bash
|
||||
# Linux/Mac
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o trace-backend
|
||||
|
||||
# Windows
|
||||
go build -o trace-backend.exe main.go
|
||||
|
||||
# macOS
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o trace-backend
|
||||
```
|
||||
|
||||
### 运行
|
||||
|
||||
```bash
|
||||
./trace-backend
|
||||
```
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
### SQLite 到 PostgreSQL 的迁移
|
||||
|
||||
1. 修改配置文件 `config.yaml` 中的数据库驱动:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
driver: postgres
|
||||
postgres:
|
||||
host: localhost
|
||||
port: 5432
|
||||
user: trace
|
||||
password: trace123
|
||||
dbname: trace
|
||||
sslmode: disable
|
||||
```
|
||||
|
||||
2. 或者使用环境变量:
|
||||
|
||||
```bash
|
||||
DATABASE_DRIVER=postgres \
|
||||
POSTGRES_HOST=localhost \
|
||||
POSTGRES_PORT=5432 \
|
||||
POSTGRES_USER=trace \
|
||||
POSTGRES_PASSWORD=trace123 \
|
||||
POSTGRES_DB=trace \
|
||||
POSTGRES_SSLMODE=disable \
|
||||
./trace-backend
|
||||
```
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. 克隆项目
|
||||
2. 创建新功能分支
|
||||
3. 提交更改
|
||||
4. 推送到远程仓库
|
||||
5. 创建 Pull Request
|
||||
|
||||
### 代码风格要求
|
||||
|
||||
- 使用 gofmt 自动格式化代码
|
||||
- 遵循 Go 官方编码规范
|
||||
- 使用 golangci-lint 进行代码检查
|
||||
- 保持代码简洁和高效
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
115
config/config.go
Normal file
115
config/config.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/subosito/gotenv"
|
||||
)
|
||||
|
||||
// ServerConfig 服务器配置
|
||||
type ServerConfig struct {
|
||||
Port string `mapstructure:"port"`
|
||||
Environment string `mapstructure:"environment"`
|
||||
}
|
||||
|
||||
// DatabaseConfig 数据库配置
|
||||
type DatabaseConfig struct {
|
||||
Driver string `mapstructure:"driver"`
|
||||
SQLite struct {
|
||||
Path string `mapstructure:"path"`
|
||||
} `mapstructure:"sqlite"`
|
||||
Postgres struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port string `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
DBName string `mapstructure:"dbname"`
|
||||
SSLMode string `mapstructure:"sslmode"`
|
||||
} `mapstructure:"postgres"`
|
||||
}
|
||||
|
||||
// JWTConfig JWT 配置
|
||||
type JWTConfig struct {
|
||||
Secret string `mapstructure:"secret"`
|
||||
Expire int `mapstructure:"expire"`
|
||||
}
|
||||
|
||||
// AppConfig 应用程序配置
|
||||
type AppConfig struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
}
|
||||
|
||||
// 全局配置变量
|
||||
var appConfig AppConfig
|
||||
|
||||
// LoadConfig 加载配置
|
||||
func LoadConfig() {
|
||||
// 使用 gotenv 加载 .env 文件
|
||||
if err := gotenv.Load(); err != nil {
|
||||
fmt.Printf("警告: 未找到 .env 文件: %v\n", err)
|
||||
}
|
||||
|
||||
// 配置 Viper 以读取 YAML 配置文件
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("./config")
|
||||
viper.AddConfigPath("../")
|
||||
if err := viper.MergeInConfig(); err != nil {
|
||||
fmt.Printf("警告: 未找到配置文件,使用默认值: %v\n", err)
|
||||
}
|
||||
|
||||
// 环境变量前缀
|
||||
viper.SetEnvPrefix("TRACE")
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// 默认配置
|
||||
viper.SetDefault("server.port", "3000")
|
||||
viper.SetDefault("server.environment", "development")
|
||||
viper.SetDefault("database.driver", "sqlite")
|
||||
viper.SetDefault("database.sqlite.path", "./data/database.sqlite")
|
||||
viper.SetDefault("database.postgres.host", "localhost")
|
||||
viper.SetDefault("database.postgres.port", "5432")
|
||||
viper.SetDefault("database.postgres.user", "trace")
|
||||
viper.SetDefault("database.postgres.password", "trace123")
|
||||
viper.SetDefault("database.postgres.dbname", "trace")
|
||||
viper.SetDefault("database.postgres.sslmode", "disable")
|
||||
viper.SetDefault("jwt.secret", "your-secret-key-here-change-in-production")
|
||||
viper.SetDefault("jwt.expire", 7200)
|
||||
|
||||
// 绑定环境变量(支持无前缀的 .env 文件)
|
||||
viper.BindEnv("server.port", "PORT", "TRACE_SERVER_PORT")
|
||||
viper.BindEnv("server.environment", "ENVIRONMENT", "TRACE_SERVER_ENVIRONMENT")
|
||||
viper.BindEnv("jwt.secret", "JWT_SECRET", "TRACE_JWT_SECRET")
|
||||
viper.BindEnv("jwt.expire", "JWT_EXPIRE", "TRACE_JWT_EXPIRE")
|
||||
viper.BindEnv("database.driver", "DATABASE_DRIVER", "TRACE_DATABASE_DRIVER")
|
||||
viper.BindEnv("database.sqlite.path", "DATABASE_PATH", "TRACE_DATABASE_SQLITE_PATH")
|
||||
viper.BindEnv("database.postgres.host", "POSTGRES_HOST", "TRACE_DATABASE_POSTGRES_HOST")
|
||||
viper.BindEnv("database.postgres.port", "POSTGRES_PORT", "TRACE_DATABASE_POSTGRES_PORT")
|
||||
viper.BindEnv("database.postgres.user", "POSTGRES_USER", "TRACE_DATABASE_POSTGRES_USER")
|
||||
viper.BindEnv("database.postgres.password", "POSTGRES_PASSWORD", "TRACE_DATABASE_POSTGRES_PASSWORD")
|
||||
viper.BindEnv("database.postgres.dbname", "POSTGRES_DB", "TRACE_DATABASE_POSTGRES_DBNAME")
|
||||
viper.BindEnv("database.postgres.sslmode", "POSTGRES_SSLMODE", "TRACE_DATABASE_POSTGRES_SSLMODE")
|
||||
|
||||
// 解析配置
|
||||
if err := viper.Unmarshal(&appConfig); err != nil {
|
||||
fmt.Printf("配置解析失败: %v\n", err)
|
||||
}
|
||||
|
||||
// 验证 JWT 密钥
|
||||
if appConfig.JWT.Secret == "your-secret-key-here-change-in-production" {
|
||||
fmt.Println("警告: 使用默认 JWT 密钥,请在生产环境中设置 JWT_SECRET 环境变量")
|
||||
}
|
||||
|
||||
// 调试打印
|
||||
fmt.Printf("加载的配置 - 环境: %s\n", appConfig.Server.Environment)
|
||||
fmt.Printf("加载的配置 - 数据库驱动: %s\n", appConfig.Database.Driver)
|
||||
}
|
||||
|
||||
// GetAppConfig 获取应用程序配置
|
||||
func GetAppConfig() *AppConfig {
|
||||
return &appConfig
|
||||
}
|
||||
19
config/config.yaml
Normal file
19
config/config.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
server:
|
||||
port: "3000"
|
||||
environment: "development"
|
||||
|
||||
database:
|
||||
driver: "sqlite"
|
||||
sqlite:
|
||||
path: "./data/database.sqlite"
|
||||
postgres:
|
||||
host: "localhost"
|
||||
port: "5432"
|
||||
user: "trace"
|
||||
password: "trace123"
|
||||
dbname: "trace"
|
||||
sslmode: "disable"
|
||||
|
||||
jwt:
|
||||
secret: "your-secret-key-here-change-in-production"
|
||||
expire: 7200
|
||||
165
controllers/auth_controller.go
Normal file
165
controllers/auth_controller.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
"git.beifan.cn/trace-system/backend-go/services"
|
||||
)
|
||||
|
||||
// AuthController 认证控制器
|
||||
type AuthController struct {
|
||||
authService services.AuthService
|
||||
}
|
||||
|
||||
// NewAuthController 创建认证控制器实例
|
||||
func NewAuthController() *AuthController {
|
||||
return &AuthController{
|
||||
authService: services.AuthService{},
|
||||
}
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
// @Summary 用户登录
|
||||
// @Description 验证用户身份并返回 JWT 令牌
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param loginData body models.LoginDTO true "登录数据"
|
||||
// @Success 200 {object} gin.H{message: string, accessToken: string, user: models.UserDTO}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Router /auth/login [post]
|
||||
func (c *AuthController) Login(ctx *gin.Context) {
|
||||
var loginData models.LoginDTO
|
||||
if err := ctx.ShouldBindJSON(&loginData); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"message": "无效的请求数据",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.authService.ValidateUser(loginData.Username, loginData.Password)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := c.authService.GenerateToken(user)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": "令牌生成失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"message": "登录成功",
|
||||
"accessToken": token,
|
||||
"user": models.UserDTO{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
CreatedAt: user.CreatedAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetProfile 获取用户信息
|
||||
// @Summary 获取用户信息
|
||||
// @Description 获取当前登录用户的个人信息
|
||||
// @Tags 认证
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} models.UserDTO
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Router /auth/profile [get]
|
||||
func (c *AuthController) GetProfile(ctx *gin.Context) {
|
||||
userModel, ok := GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
profile, err := c.authService.GetProfile(userModel.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "获取用户信息成功", gin.H{
|
||||
"user": profile,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
// @Summary 修改密码
|
||||
// @Description 修改当前登录用户的密码
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param passwordData body models.ChangePasswordDTO true "密码修改数据"
|
||||
// @Success 200 {object} gin.H{message: string}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Router /auth/change-password [post]
|
||||
func (c *AuthController) ChangePassword(ctx *gin.Context) {
|
||||
userModel, ok := GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var changePasswordData models.ChangePasswordDTO
|
||||
if !BindJSON(ctx, &changePasswordData) {
|
||||
return
|
||||
}
|
||||
|
||||
err := c.authService.ChangePassword(userModel.ID, changePasswordData.CurrentPassword, changePasswordData.NewPassword)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "密码修改成功")
|
||||
}
|
||||
|
||||
// UpdateProfile 更新用户信息
|
||||
// @Summary 更新用户信息
|
||||
// @Description 更新当前登录用户的个人信息
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param profileData body models.UpdateProfileDTO true "用户信息更新数据"
|
||||
// @Success 200 {object} models.UserDTO
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Router /auth/profile [put]
|
||||
func (c *AuthController) UpdateProfile(ctx *gin.Context) {
|
||||
userModel, ok := GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var updateProfileData models.UpdateProfileDTO
|
||||
if !BindJSON(ctx, &updateProfileData) {
|
||||
return
|
||||
}
|
||||
|
||||
profile, err := c.authService.UpdateProfile(userModel.ID, updateProfileData.Name, updateProfileData.Email)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "用户信息更新成功", gin.H{
|
||||
"user": profile,
|
||||
})
|
||||
}
|
||||
200
controllers/companies_controller.go
Normal file
200
controllers/companies_controller.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/services"
|
||||
)
|
||||
|
||||
// CompaniesController 企业管理控制器
|
||||
type CompaniesController struct {
|
||||
companiesService services.CompaniesService
|
||||
}
|
||||
|
||||
// NewCompaniesController 创建企业管理控制器实例
|
||||
func NewCompaniesController() *CompaniesController {
|
||||
return &CompaniesController{
|
||||
companiesService: services.CompaniesService{},
|
||||
}
|
||||
}
|
||||
|
||||
// FindAll 获取企业列表
|
||||
// @Summary 获取企业列表
|
||||
// @Description 获取企业列表,支持分页和搜索
|
||||
// @Tags 企业管理
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param page query int false "页码"
|
||||
// @Param limit query int false "每页数量"
|
||||
// @Param search query string false "搜索关键词"
|
||||
// @Success 200 {object} gin.H{message: string, data: []models.Company, pagination: gin.H{page: int, limit: int, total: int, totalPages: int}}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /companies [get]
|
||||
func (c *CompaniesController) FindAll(ctx *gin.Context) {
|
||||
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20"))
|
||||
search := ctx.DefaultQuery("search", "")
|
||||
|
||||
companies, total, totalPages, err := c.companiesService.FindAll(page, limit, search)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"message": "获取企业列表成功",
|
||||
"data": companies,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"totalPages": totalPages,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Create 创建企业
|
||||
// @Summary 创建企业
|
||||
// @Description 创建新的企业
|
||||
// @Tags 企业管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param companyData body gin.H{companyName: string} true "企业数据"
|
||||
// @Success 201 {object} gin.H{message: string, company: models.Company}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Failure 409 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /companies [post]
|
||||
func (c *CompaniesController) Create(ctx *gin.Context) {
|
||||
var companyData struct {
|
||||
CompanyName string `json:"companyName" validate:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&companyData); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"message": "无效的请求数据",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
company, err := c.companiesService.Create(companyData.CompanyName)
|
||||
if err != nil {
|
||||
if err.Error() == "企业名称已存在" {
|
||||
ctx.JSON(http.StatusConflict, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, gin.H{
|
||||
"message": "企业创建成功",
|
||||
"company": company,
|
||||
})
|
||||
}
|
||||
|
||||
// Update 更新企业信息
|
||||
// @Summary 更新企业信息
|
||||
// @Description 更新企业信息
|
||||
// @Tags 企业管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param companyName path string true "企业名称"
|
||||
// @Param companyData body gin.H{companyName?: string, isActive?: bool} true "企业数据"
|
||||
// @Success 200 {object} gin.H{message: string, company: models.Company}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Failure 404 {object} gin.H{message: string}
|
||||
// @Failure 409 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /companies/{companyName} [put]
|
||||
func (c *CompaniesController) Update(ctx *gin.Context) {
|
||||
companyName := ctx.Param("companyName")
|
||||
|
||||
var companyData struct {
|
||||
CompanyName string `json:"companyName"`
|
||||
IsActive bool `json:"isActive"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&companyData); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"message": "无效的请求数据",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
company, err := c.companiesService.Update(companyName, companyData.CompanyName, companyData.IsActive)
|
||||
if err != nil {
|
||||
if err.Error() == "企业不存在" {
|
||||
ctx.JSON(http.StatusNotFound, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
} else if err.Error() == "企业名称已存在" {
|
||||
ctx.JSON(http.StatusConflict, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"message": "企业信息更新成功",
|
||||
"company": company,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除企业
|
||||
// @Summary 删除企业
|
||||
// @Description 删除企业
|
||||
// @Tags 企业管理
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param companyName path string true "企业名称"
|
||||
// @Success 200 {object} gin.H{message: string}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Failure 404 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /companies/{companyName} [delete]
|
||||
func (c *CompaniesController) Delete(ctx *gin.Context) {
|
||||
companyName := ctx.Param("companyName")
|
||||
|
||||
err := c.companiesService.Delete(companyName)
|
||||
if err != nil {
|
||||
if err.Error() == "企业不存在" {
|
||||
ctx.JSON(http.StatusNotFound, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
} else if err.Error() == "企业下还有序列号,无法删除" {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"message": "企业删除成功",
|
||||
})
|
||||
}
|
||||
81
controllers/helper.go
Normal file
81
controllers/helper.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
)
|
||||
|
||||
// GetCurrentUser 从上下文中获取当前用户
|
||||
// 如果用户未认证,返回 false 并返回 401 错误
|
||||
func GetCurrentUser(ctx *gin.Context) (models.User, bool) {
|
||||
user, exists := ctx.Get("user")
|
||||
if !exists {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "未认证",
|
||||
})
|
||||
return models.User{}, false
|
||||
}
|
||||
|
||||
userModel, ok := user.(models.User)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "用户信息无效",
|
||||
})
|
||||
return models.User{}, false
|
||||
}
|
||||
|
||||
return userModel, true
|
||||
}
|
||||
|
||||
// BindJSON 绑定 JSON 请求体
|
||||
// 如果解析失败,返回错误并返回 400 响应
|
||||
func BindJSON(ctx *gin.Context, obj interface{}) bool {
|
||||
if err := ctx.ShouldBindJSON(obj); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"message": "无效的请求数据",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// BindQuery 绑定查询参数
|
||||
// 如果解析失败,返回错误并返回 400 响应
|
||||
func BindQuery(ctx *gin.Context, obj interface{}) bool {
|
||||
if err := ctx.ShouldBindQuery(obj); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||
"message": "无效的查询参数",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ErrorResponse 返回错误响应
|
||||
func ErrorResponse(ctx *gin.Context, status int, message string, err ...error) {
|
||||
response := gin.H{
|
||||
"message": message,
|
||||
}
|
||||
if len(err) > 0 && err[0] != nil {
|
||||
response["error"] = err[0].Error()
|
||||
}
|
||||
ctx.JSON(status, response)
|
||||
}
|
||||
|
||||
// SuccessResponse 返回成功响应
|
||||
func SuccessResponse(ctx *gin.Context, message string, data ...gin.H) {
|
||||
response := gin.H{
|
||||
"message": message,
|
||||
}
|
||||
if len(data) > 0 {
|
||||
for key, value := range data[0] {
|
||||
response[key] = value
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
268
controllers/serials_controller.go
Normal file
268
controllers/serials_controller.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
"git.beifan.cn/trace-system/backend-go/services"
|
||||
)
|
||||
|
||||
// SerialsController 序列号控制器
|
||||
type SerialsController struct {
|
||||
serialsService services.SerialsService
|
||||
}
|
||||
|
||||
// NewSerialsController 创建序列号控制器实例
|
||||
func NewSerialsController() *SerialsController {
|
||||
return &SerialsController{
|
||||
serialsService: services.SerialsService{},
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成序列号
|
||||
// @Summary 生成序列号
|
||||
// @Description 生成指定数量的序列号
|
||||
// @Tags 序列号管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param generateData body models.GenerateSerialDTO true "生成数据"
|
||||
// @Success 200 {object} gin.H{message: string, serials: []models.Serial}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /serials/generate [post]
|
||||
func (c *SerialsController) Generate(ctx *gin.Context) {
|
||||
userModel, ok := GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var generateData models.GenerateSerialDTO
|
||||
if !BindJSON(ctx, &generateData) {
|
||||
return
|
||||
}
|
||||
|
||||
serials, err := c.serialsService.Generate(
|
||||
generateData.CompanyName,
|
||||
generateData.Quantity,
|
||||
generateData.ValidDays,
|
||||
userModel.ID,
|
||||
)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "成功生成 "+strconv.Itoa(len(serials))+" 个序列号", gin.H{
|
||||
"serials": serials,
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateWithPrefix 带前缀生成序列号
|
||||
// @Summary 带前缀生成序列号
|
||||
// @Description 生成带有指定前缀的序列号
|
||||
// @Tags 序列号管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param generateData body models.GenerateWithPrefixDTO true "生成数据"
|
||||
// @Success 200 {object} gin.H{message: string, serials: []models.Serial}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /serials/generate-with-prefix [post]
|
||||
func (c *SerialsController) GenerateWithPrefix(ctx *gin.Context) {
|
||||
userModel, ok := GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var generateData models.GenerateWithPrefixDTO
|
||||
if !BindJSON(ctx, &generateData) {
|
||||
return
|
||||
}
|
||||
|
||||
serials, err := c.serialsService.Generate(
|
||||
generateData.CompanyName,
|
||||
generateData.Quantity,
|
||||
generateData.ValidDays,
|
||||
userModel.ID,
|
||||
generateData.SerialPrefix,
|
||||
)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "成功生成 "+strconv.Itoa(len(serials))+" 个序列号", gin.H{
|
||||
"serials": serials,
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateQRCode 生成二维码
|
||||
// @Summary 生成二维码
|
||||
// @Description 为指定序列号生成查询二维码
|
||||
// @Tags 序列号管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param serialNumber path string true "序列号"
|
||||
// @Param qrCodeData body models.QRCodeDTO false "二维码数据"
|
||||
// @Success 200 {object} gin.H{message: string, qrCodeData: string, queryUrl: string}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Failure 404 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /serials/{serialNumber}/qrcode [post]
|
||||
func (c *SerialsController) GenerateQRCode(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
var qrCodeData models.QRCodeDTO
|
||||
if !BindJSON(ctx, &qrCodeData) {
|
||||
return
|
||||
}
|
||||
|
||||
protocol := "http"
|
||||
if ctx.Request.TLS != nil {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
qrCodeBase64, queryUrl, err := c.serialsService.GenerateQRCode(
|
||||
serialNumber,
|
||||
qrCodeData.BaseUrl,
|
||||
ctx.Request.Host,
|
||||
protocol,
|
||||
)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "二维码生成成功", gin.H{
|
||||
"qrCodeData": qrCodeBase64,
|
||||
"queryUrl": queryUrl,
|
||||
})
|
||||
}
|
||||
|
||||
// Query 查询序列号信息
|
||||
// @Summary 查询序列号信息
|
||||
// @Description 查询指定序列号的详细信息
|
||||
// @Tags 序列号查询
|
||||
// @Produce json
|
||||
// @Param serialNumber path string true "序列号"
|
||||
// @Success 200 {object} gin.H{message: string, serial: models.Serial}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 404 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /serials/{serialNumber}/query [get]
|
||||
func (c *SerialsController) Query(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
serial, err := c.serialsService.Query(serialNumber)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "查询成功", gin.H{
|
||||
"serial": serial,
|
||||
})
|
||||
}
|
||||
|
||||
// FindAll 获取序列号列表
|
||||
// @Summary 获取序列号列表
|
||||
// @Description 获取序列号列表,支持分页和搜索
|
||||
// @Tags 序列号管理
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param page query int false "页码"
|
||||
// @Param limit query int false "每页数量"
|
||||
// @Param search query string false "搜索关键词"
|
||||
// @Success 200 {object} gin.H{message: string, data: []models.Serial, pagination: gin.H{page: int, limit: int, total: int, totalPages: int}}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /serials [get]
|
||||
func (c *SerialsController) FindAll(ctx *gin.Context) {
|
||||
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20"))
|
||||
search := ctx.DefaultQuery("search", "")
|
||||
|
||||
serials, total, totalPages, err := c.serialsService.FindAll(page, limit, search)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "获取序列号列表成功", gin.H{
|
||||
"data": serials,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"totalPages": totalPages,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Update 更新序列号信息
|
||||
// @Summary 更新序列号信息
|
||||
// @Description 更新指定序列号的信息
|
||||
// @Tags 序列号管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param serialNumber path string true "序列号"
|
||||
// @Param updateData body models.UpdateSerialDTO true "更新数据"
|
||||
// @Success 200 {object} gin.H{message: string, serial: models.Serial}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Failure 404 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /serials/{serialNumber} [put]
|
||||
func (c *SerialsController) Update(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
var updateData models.UpdateSerialDTO
|
||||
if !BindJSON(ctx, &updateData) {
|
||||
return
|
||||
}
|
||||
|
||||
serial, err := c.serialsService.Update(serialNumber, updateData)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "序列号信息更新成功", gin.H{
|
||||
"serial": serial,
|
||||
})
|
||||
}
|
||||
|
||||
// Revoke 吊销序列号
|
||||
// @Summary 吊销序列号
|
||||
// @Description 吊销指定序列号
|
||||
// @Tags 序列号管理
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param serialNumber path string true "序列号"
|
||||
// @Success 200 {object} gin.H{message: string}
|
||||
// @Failure 400 {object} gin.H{message: string}
|
||||
// @Failure 401 {object} gin.H{message: string}
|
||||
// @Failure 404 {object} gin.H{message: string}
|
||||
// @Failure 500 {object} gin.H{message: string}
|
||||
// @Router /serials/{serialNumber}/revoke [post]
|
||||
func (c *SerialsController) Revoke(ctx *gin.Context) {
|
||||
serialNumber := ctx.Param("serialNumber")
|
||||
|
||||
err := c.serialsService.Revoke(serialNumber)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(ctx, "序列号吊销成功")
|
||||
}
|
||||
120
database/database.go
Normal file
120
database/database.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/config"
|
||||
"git.beifan.cn/trace-system/backend-go/logger"
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
// InitDB 初始化数据库连接
|
||||
func InitDB() {
|
||||
var err error
|
||||
cfg := config.GetAppConfig()
|
||||
dbConfig := cfg.Database
|
||||
|
||||
logger.Info("数据库配置信息",
|
||||
logger.String("驱动", dbConfig.Driver),
|
||||
)
|
||||
|
||||
switch dbConfig.Driver {
|
||||
case "sqlite":
|
||||
logger.Info("使用 SQLite 数据库", logger.String("路径", dbConfig.SQLite.Path))
|
||||
DB, err = initSQLite(dbConfig.SQLite.Path)
|
||||
case "postgres":
|
||||
logger.Info("使用 PostgreSQL 数据库",
|
||||
logger.String("主机", dbConfig.Postgres.Host),
|
||||
logger.String("端口", dbConfig.Postgres.Port),
|
||||
logger.String("数据库", dbConfig.Postgres.DBName),
|
||||
)
|
||||
DB, err = initPostgres(dbConfig.Postgres)
|
||||
default:
|
||||
logger.Fatal("不支持的数据库驱动: " + dbConfig.Driver)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal("数据库连接失败", logger.Err(err))
|
||||
}
|
||||
|
||||
logger.Info("数据库连接成功")
|
||||
}
|
||||
|
||||
// initSQLite 初始化 SQLite 连接
|
||||
func initSQLite(path string) (*gorm.DB, error) {
|
||||
// 确保数据目录存在
|
||||
dataDir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(path+"?_pragma=foreign_keys(1)"), &gorm.Config{})
|
||||
if err != nil {
|
||||
logger.Warn("SQLite 连接失败,尝试使用内存数据库", logger.Err(err))
|
||||
// 尝试使用内存数据库作为备选方案
|
||||
db, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 配置连接池
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxOpenConns(25)
|
||||
sqlDB.SetMaxIdleConns(5)
|
||||
sqlDB.SetConnMaxLifetime(5 * 60 * 1000000000)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// initPostgres 初始化 PostgreSQL 连接
|
||||
func initPostgres(pgConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port string `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
DBName string `mapstructure:"dbname"`
|
||||
SSLMode string `mapstructure:"sslmode"`
|
||||
}) (*gorm.DB, error) {
|
||||
dsn := "host=" + pgConfig.Host +
|
||||
" port=" + pgConfig.Port +
|
||||
" user=" + pgConfig.User +
|
||||
" password=" + pgConfig.Password +
|
||||
" dbname=" + pgConfig.DBName +
|
||||
" sslmode=" + pgConfig.SSLMode
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
logger.Fatal("PostgreSQL 连接失败", logger.Err(err))
|
||||
}
|
||||
|
||||
// 配置连接池
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetConnMaxLifetime(30 * 60 * 1000000000)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// AutoMigrate 自动迁移数据库
|
||||
func AutoMigrate() {
|
||||
if err := DB.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.Company{},
|
||||
&models.Serial{},
|
||||
); err != nil {
|
||||
logger.Fatal("数据库迁移失败", logger.Err(err))
|
||||
}
|
||||
|
||||
logger.Info("数据库迁移成功")
|
||||
}
|
||||
79
go.mod
Normal file
79
go.mod
Normal file
@@ -0,0 +1,79 @@
|
||||
module git.beifan.cn/trace-system/backend-go
|
||||
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/subosito/gotenv v1.6.0
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/crypto v0.48.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/fogleman/gg v1.3.0 // 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-yaml v1.18.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/image v0.10.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // 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/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.1-0.20181226105442-5d4384ee4fb2 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
197
go.sum
Normal file
197
go.sum
Normal file
@@ -0,0 +1,197 @@
|
||||
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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
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-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-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.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
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.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
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.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
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/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.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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/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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/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.7.0/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/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/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
|
||||
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M=
|
||||
golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.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-20201119102817-f84b799fce68/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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.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.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.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
114
logger/logger.go
Normal file
114
logger/logger.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Logger 全局日志实例
|
||||
var Logger *zap.Logger
|
||||
|
||||
// InitializeLogger 初始化日志系统
|
||||
func InitializeLogger(env string) error {
|
||||
var config zap.Config
|
||||
|
||||
switch env {
|
||||
case "development":
|
||||
config = zap.NewDevelopmentConfig()
|
||||
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
case "production":
|
||||
config = zap.NewProductionConfig()
|
||||
config.Encoding = "json"
|
||||
config.EncoderConfig.TimeKey = "timestamp"
|
||||
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
default:
|
||||
config = zap.NewDevelopmentConfig()
|
||||
}
|
||||
|
||||
config.OutputPaths = []string{"stdout"}
|
||||
config.ErrorOutputPaths = []string{"stderr"}
|
||||
|
||||
var err error
|
||||
Logger, err = config.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.ReplaceGlobals(Logger)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sync 同步日志到磁盘
|
||||
func Sync() {
|
||||
if err := Logger.Sync(); err != nil {
|
||||
Logger.Error("Failed to sync logger", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Info 记录信息级别的日志
|
||||
func Info(msg string, fields ...zap.Field) {
|
||||
Logger.Info(msg, fields...)
|
||||
}
|
||||
|
||||
// Warn 记录警告级别的日志
|
||||
func Warn(msg string, fields ...zap.Field) {
|
||||
Logger.Warn(msg, fields...)
|
||||
}
|
||||
|
||||
// Error 记录错误级别的日志
|
||||
func Error(msg string, fields ...zap.Field) {
|
||||
Logger.Error(msg, fields...)
|
||||
}
|
||||
|
||||
// Fatal 记录致命级别的日志
|
||||
func Fatal(msg string, fields ...zap.Field) {
|
||||
Logger.Fatal(msg, fields...)
|
||||
}
|
||||
|
||||
// Debug 记录调试级别的日志
|
||||
func Debug(msg string, fields ...zap.Field) {
|
||||
Logger.Debug(msg, fields...)
|
||||
}
|
||||
|
||||
// String 字符串字段
|
||||
func String(key, value string) zap.Field {
|
||||
return zap.String(key, value)
|
||||
}
|
||||
|
||||
// Int 整数字段
|
||||
func Int(key string, value int) zap.Field {
|
||||
return zap.Int(key, value)
|
||||
}
|
||||
|
||||
// Int64 64位整数字段
|
||||
func Int64(key string, value int64) zap.Field {
|
||||
return zap.Int64(key, value)
|
||||
}
|
||||
|
||||
// Bool 布尔字段
|
||||
func Bool(key string, value bool) zap.Field {
|
||||
return zap.Bool(key, value)
|
||||
}
|
||||
|
||||
// Float64 浮点数字段
|
||||
func Float64(key string, value float64) zap.Field {
|
||||
return zap.Float64(key, value)
|
||||
}
|
||||
|
||||
// Err 错误字段
|
||||
func Err(err error) zap.Field {
|
||||
return zap.Error(err)
|
||||
}
|
||||
|
||||
// Time 时间字段
|
||||
func Time(key string, value time.Time) zap.Field {
|
||||
return zap.Time(key, value)
|
||||
}
|
||||
|
||||
// Duration 时间间隔字段
|
||||
func Duration(key string, value time.Duration) zap.Field {
|
||||
return zap.Duration(key, value)
|
||||
}
|
||||
77
main.go
Normal file
77
main.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.beifan.cn/trace-system/backend-go/config"
|
||||
"git.beifan.cn/trace-system/backend-go/database"
|
||||
"git.beifan.cn/trace-system/backend-go/logger"
|
||||
"git.beifan.cn/trace-system/backend-go/routes"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
config.LoadConfig()
|
||||
cfg := config.GetAppConfig()
|
||||
|
||||
// 初始化日志系统
|
||||
if err := logger.InitializeLogger(cfg.Server.Environment); err != nil {
|
||||
panic("日志系统初始化失败: " + err.Error())
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
logger.Info("服务器启动",
|
||||
logger.String("环境", cfg.Server.Environment),
|
||||
logger.String("端口", cfg.Server.Port),
|
||||
)
|
||||
|
||||
// 根据环境设置 Gin 模式
|
||||
if cfg.Server.Environment == "production" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
} else {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
database.InitDB()
|
||||
|
||||
// 自动迁移数据库
|
||||
database.AutoMigrate()
|
||||
|
||||
// 初始化 Gin 引擎
|
||||
r := gin.New()
|
||||
|
||||
// 添加中间件
|
||||
r.Use(gin.Logger())
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
// 启用 CORS
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
|
||||
// 配置路由
|
||||
routes.SetupRoutes(r)
|
||||
|
||||
// 设置 API 前缀
|
||||
api := r.Group("/api")
|
||||
routes.SetupAPIRoutes(api)
|
||||
|
||||
// 启动服务器
|
||||
port := cfg.Server.Port
|
||||
logger.Info("服务器运行在 http://localhost:" + port)
|
||||
logger.Info("API 文档: http://localhost:" + port + "/api/health")
|
||||
logger.Info("环境: " + cfg.Server.Environment)
|
||||
|
||||
if err := r.Run(":" + port); err != nil {
|
||||
logger.Fatal("服务器启动失败", logger.Err(err))
|
||||
}
|
||||
}
|
||||
112
middleware/auth.go
Normal file
112
middleware/auth.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/config"
|
||||
"git.beifan.cn/trace-system/backend-go/database"
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// JWTAuthMiddleware JWT 认证中间件
|
||||
func JWTAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "缺少授权头部",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if !(len(parts) == 2 && parts[0] == "Bearer") {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "授权头部格式错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tokenStr := parts[1]
|
||||
|
||||
// 解析 JWT token
|
||||
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("不支持的签名方法")
|
||||
}
|
||||
cfg := config.GetAppConfig()
|
||||
return []byte(cfg.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "无效的访问令牌",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证 claims
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
// 检查过期时间
|
||||
if exp, ok := claims["exp"].(float64); ok {
|
||||
if time.Now().Unix() > int64(exp) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "令牌已过期",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户 ID
|
||||
if userId, ok := claims["userId"].(float64); ok {
|
||||
// 验证用户是否存在
|
||||
var user models.User
|
||||
result := database.DB.First(&user, uint(userId))
|
||||
if result.Error != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "用户不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储到上下文
|
||||
c.Set("user", user)
|
||||
} else {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "令牌无效",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AdminMiddleware 管理员权限中间件
|
||||
func AdminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
user, exists := c.Get("user")
|
||||
if !exists {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "未认证",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userModel := user.(models.User)
|
||||
if userModel.Role != "admin" {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"message": "无权限访问此资源",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
102
models/models.go
Normal file
102
models/models.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User 模型
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Username string `gorm:"uniqueIndex;size:255"`
|
||||
Password string `gorm:"size:255"`
|
||||
Name string `gorm:"size:255"`
|
||||
Email string `gorm:"size:255"`
|
||||
Role string `gorm:"size:50;default:'user'"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Serials []Serial `gorm:"foreignKey:CreatedBy"`
|
||||
}
|
||||
|
||||
// Company 模型
|
||||
type Company struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CompanyName string `gorm:"uniqueIndex;size:255"`
|
||||
IsActive bool `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Serials []Serial `gorm:"foreignKey:CompanyName;references:CompanyName"`
|
||||
}
|
||||
|
||||
// Serial 模型
|
||||
type Serial struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
SerialNumber string `gorm:"uniqueIndex;size:255"`
|
||||
CompanyName string `gorm:"index;size:255"`
|
||||
ValidUntil *time.Time
|
||||
IsActive bool `gorm:"default:true"`
|
||||
CreatedBy *uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
User *User `gorm:"foreignKey:CreatedBy"`
|
||||
Company *Company `gorm:"foreignKey:CompanyName;references:CompanyName"`
|
||||
}
|
||||
|
||||
// UserDTO 数据传输对象
|
||||
type UserDTO struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// LoginDTO 登录请求数据
|
||||
type LoginDTO struct {
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
}
|
||||
|
||||
// ChangePasswordDTO 密码修改请求数据
|
||||
type ChangePasswordDTO struct {
|
||||
CurrentPassword string `json:"currentPassword" validate:"required"`
|
||||
NewPassword string `json:"newPassword" validate:"required,min=6"`
|
||||
}
|
||||
|
||||
// UpdateProfileDTO 个人信息更新请求数据
|
||||
type UpdateProfileDTO struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
// GenerateSerialDTO 生成序列号请求数据
|
||||
type GenerateSerialDTO struct {
|
||||
CompanyName string `json:"companyName" validate:"required"`
|
||||
Quantity int `json:"quantity" validate:"min=1,max=1000"`
|
||||
ValidDays int `json:"validDays" validate:"min=1,max=3650"`
|
||||
}
|
||||
|
||||
// GenerateWithPrefixDTO 带前缀生成序列号请求数据
|
||||
type GenerateWithPrefixDTO struct {
|
||||
CompanyName string `json:"companyName" validate:"required"`
|
||||
Quantity int `json:"quantity" validate:"min=1,max=1000"`
|
||||
ValidDays int `json:"validDays" validate:"min=1,max=3650"`
|
||||
SerialPrefix string `json:"serialPrefix" validate:"omitempty,alphanum"`
|
||||
}
|
||||
|
||||
// UpdateSerialDTO 序列号更新请求数据
|
||||
type UpdateSerialDTO struct {
|
||||
CompanyName string `json:"companyName,omitempty" validate:"omitempty"`
|
||||
ValidUntil *time.Time `json:"validUntil,omitempty"`
|
||||
IsActive *bool `json:"isActive,omitempty"`
|
||||
}
|
||||
|
||||
// QRCodeDTO 二维码生成请求数据
|
||||
type QRCodeDTO struct {
|
||||
BaseUrl string `json:"baseUrl,omitempty"`
|
||||
}
|
||||
55
routes/routes.go
Normal file
55
routes/routes.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/controllers"
|
||||
"git.beifan.cn/trace-system/backend-go/middleware"
|
||||
)
|
||||
|
||||
// SetupRoutes 设置路由
|
||||
func SetupRoutes(r *gin.Engine) {
|
||||
// 健康检查
|
||||
r.GET("/api/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"message": "服务器运行正常",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// SetupAPIRoutes 设置 API 路由
|
||||
func SetupAPIRoutes(r *gin.RouterGroup) {
|
||||
// 认证路由
|
||||
authController := controllers.NewAuthController()
|
||||
authRoutes := r.Group("/auth")
|
||||
{
|
||||
authRoutes.POST("/login", authController.Login)
|
||||
authRoutes.GET("/profile", middleware.JWTAuthMiddleware(), authController.GetProfile)
|
||||
authRoutes.PUT("/profile", middleware.JWTAuthMiddleware(), authController.UpdateProfile)
|
||||
authRoutes.POST("/change-password", middleware.JWTAuthMiddleware(), authController.ChangePassword)
|
||||
}
|
||||
|
||||
// 序列号路由
|
||||
serialsController := controllers.NewSerialsController()
|
||||
serialsRoutes := r.Group("/serials")
|
||||
{
|
||||
serialsRoutes.POST("/generate", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.Generate)
|
||||
serialsRoutes.POST("/generate-with-prefix", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.GenerateWithPrefix)
|
||||
serialsRoutes.POST("/:serialNumber/qrcode", middleware.JWTAuthMiddleware(), serialsController.GenerateQRCode)
|
||||
serialsRoutes.GET("/:serialNumber/query", serialsController.Query)
|
||||
serialsRoutes.GET("/", middleware.JWTAuthMiddleware(), serialsController.FindAll)
|
||||
serialsRoutes.PUT("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.Update)
|
||||
serialsRoutes.POST("/:serialNumber/revoke", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.Revoke)
|
||||
}
|
||||
|
||||
// 企业管理路由
|
||||
companiesController := controllers.NewCompaniesController()
|
||||
companiesRoutes := r.Group("/companies")
|
||||
{
|
||||
companiesRoutes.GET("/", middleware.JWTAuthMiddleware(), companiesController.FindAll)
|
||||
companiesRoutes.POST("/", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Create)
|
||||
companiesRoutes.PUT("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Update)
|
||||
companiesRoutes.DELETE("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Delete)
|
||||
}
|
||||
}
|
||||
119
services/auth_service.go
Normal file
119
services/auth_service.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/config"
|
||||
"git.beifan.cn/trace-system/backend-go/database"
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
)
|
||||
|
||||
// AuthService 认证服务
|
||||
type AuthService struct{}
|
||||
|
||||
// ValidateUser 验证用户身份
|
||||
func (s *AuthService) ValidateUser(username string, password string) (*models.User, error) {
|
||||
var user models.User
|
||||
result := database.DB.Where("username = ?", username).First(&user)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("验证用户失败: %w", errors.New("用户名或密码错误"))
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("密码验证失败: %w", errors.New("用户名或密码错误"))
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GenerateToken 生成 JWT 令牌
|
||||
func (s *AuthService) GenerateToken(user *models.User) (string, error) {
|
||||
cfg := config.GetAppConfig()
|
||||
claims := jwt.MapClaims{
|
||||
"userId": user.ID,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
"exp": time.Now().Add(time.Second * time.Duration(cfg.JWT.Expire)).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(cfg.JWT.Secret))
|
||||
}
|
||||
|
||||
// GetProfile 获取用户信息
|
||||
func (s *AuthService) GetProfile(userId uint) (*models.UserDTO, error) {
|
||||
var user models.User
|
||||
result := database.DB.First(&user, userId)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询用户失败: %w", errors.New("用户不存在"))
|
||||
}
|
||||
|
||||
return &models.UserDTO{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *AuthService) ChangePassword(userId uint, currentPassword string, newPassword string) error {
|
||||
var user models.User
|
||||
result := database.DB.First(&user, userId)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("查询用户失败: %w", errors.New("用户不存在"))
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(currentPassword))
|
||||
if err != nil {
|
||||
return fmt.Errorf("密码验证失败: %w", errors.New("当前密码错误"))
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("密码加密失败: %w", err)
|
||||
}
|
||||
|
||||
user.Password = string(hashedPassword)
|
||||
result = database.DB.Save(&user)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("保存用户失败: %w", errors.New("密码修改失败"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProfile 更新用户信息
|
||||
func (s *AuthService) UpdateProfile(userId uint, name string, email string) (*models.UserDTO, error) {
|
||||
var user models.User
|
||||
result := database.DB.First(&user, userId)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询用户失败: %w", errors.New("用户不存在"))
|
||||
}
|
||||
|
||||
user.Name = name
|
||||
user.Email = email
|
||||
|
||||
result = database.DB.Save(&user)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("保存用户失败: %w", errors.New("个人信息更新失败"))
|
||||
}
|
||||
|
||||
return &models.UserDTO{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
112
services/companies_service.go
Normal file
112
services/companies_service.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/database"
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
)
|
||||
|
||||
// CompaniesService 企业管理服务
|
||||
type CompaniesService struct{}
|
||||
|
||||
// FindAll 获取所有企业列表
|
||||
func (s *CompaniesService) FindAll(page int, limit int, search string) ([]models.Company, int, int, error) {
|
||||
var companies []models.Company
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
db := database.DB
|
||||
|
||||
// 搜索条件
|
||||
if search != "" {
|
||||
db = db.Where("company_name LIKE ?", "%"+search+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
db.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
result := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&companies)
|
||||
if result.Error != nil {
|
||||
return nil, 0, 0, errors.New("查询企业列表失败")
|
||||
}
|
||||
|
||||
totalPages := (int(total) + limit - 1) / limit
|
||||
|
||||
return companies, int(total), totalPages, nil
|
||||
}
|
||||
|
||||
// Create 创建企业
|
||||
func (s *CompaniesService) Create(companyName string) (*models.Company, error) {
|
||||
// 检查企业是否已存在
|
||||
var existingCompany models.Company
|
||||
result := database.DB.Where("company_name = ?", companyName).First(&existingCompany)
|
||||
if result.Error == nil {
|
||||
return nil, errors.New("企业名称已存在")
|
||||
}
|
||||
|
||||
company := models.Company{
|
||||
CompanyName: companyName,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
result = database.DB.Create(&company)
|
||||
if result.Error != nil {
|
||||
return nil, errors.New("创建企业失败")
|
||||
}
|
||||
|
||||
return &company, nil
|
||||
}
|
||||
|
||||
// Update 更新企业信息
|
||||
func (s *CompaniesService) Update(companyName string, newCompanyName string, isActive bool) (*models.Company, error) {
|
||||
var company models.Company
|
||||
result := database.DB.Where("company_name = ?", companyName).First(&company)
|
||||
if result.Error != nil {
|
||||
return nil, errors.New("企业不存在")
|
||||
}
|
||||
|
||||
// 如果企业名称已变更,检查新名称是否已存在
|
||||
if newCompanyName != companyName {
|
||||
var existingCompany models.Company
|
||||
checkResult := database.DB.Where("company_name = ?", newCompanyName).First(&existingCompany)
|
||||
if checkResult.Error == nil {
|
||||
return nil, errors.New("企业名称已存在")
|
||||
}
|
||||
|
||||
company.CompanyName = newCompanyName
|
||||
}
|
||||
|
||||
company.IsActive = isActive
|
||||
|
||||
result = database.DB.Save(&company)
|
||||
if result.Error != nil {
|
||||
return nil, errors.New("更新企业信息失败")
|
||||
}
|
||||
|
||||
return &company, nil
|
||||
}
|
||||
|
||||
// Delete 删除企业
|
||||
func (s *CompaniesService) Delete(companyName string) error {
|
||||
var company models.Company
|
||||
result := database.DB.Where("company_name = ?", companyName).First(&company)
|
||||
if result.Error != nil {
|
||||
return errors.New("企业不存在")
|
||||
}
|
||||
|
||||
// 检查企业是否有关联的序列号
|
||||
var serialCount int64
|
||||
database.DB.Model(&models.Serial{}).Where("company_name = ?", companyName).Count(&serialCount)
|
||||
if serialCount > 0 {
|
||||
return errors.New("企业下还有序列号,无法删除")
|
||||
}
|
||||
|
||||
result = database.DB.Delete(&company)
|
||||
if result.Error != nil {
|
||||
return errors.New("删除企业失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
BIN
services/data/database.sqlite
Normal file
BIN
services/data/database.sqlite
Normal file
Binary file not shown.
269
services/serials_service.go
Normal file
269
services/serials_service.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
qr "github.com/yeqown/go-qrcode/v2"
|
||||
"github.com/yeqown/go-qrcode/writer/standard"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/database"
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
)
|
||||
|
||||
// SerialsService 序列号服务
|
||||
type SerialsService struct{}
|
||||
|
||||
// Generate 生成序列号
|
||||
func (s *SerialsService) Generate(
|
||||
companyName string,
|
||||
quantity int,
|
||||
validDays int,
|
||||
userId uint,
|
||||
prefix ...string,
|
||||
) ([]models.Serial, error) {
|
||||
var serials []models.Serial
|
||||
validUntil := time.Now().AddDate(0, 0, validDays)
|
||||
|
||||
// 检查公司是否存在,不存在则创建
|
||||
var company models.Company
|
||||
result := database.DB.Where("company_name = ?", companyName).First(&company)
|
||||
if result.Error != nil {
|
||||
company = models.Company{
|
||||
CompanyName: companyName,
|
||||
IsActive: true,
|
||||
}
|
||||
result = database.DB.Create(&company)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("创建公司失败: %w", result.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成序列号前缀
|
||||
var serialPrefix string
|
||||
if len(prefix) > 0 && prefix[0] != "" {
|
||||
serialPrefix = strings.ToUpper(strings.ReplaceAll(prefix[0], "[^A-Z0-9]", ""))
|
||||
} else {
|
||||
serialPrefix = fmt.Sprintf("BF%d", time.Now().Year()%100)
|
||||
}
|
||||
|
||||
// 预生成所有序列号
|
||||
serialNumbers := make(map[string]bool)
|
||||
for i := 0; i < quantity; {
|
||||
randomBytes := make([]byte, 3)
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
return nil, fmt.Errorf("生成随机数失败: %w", err)
|
||||
}
|
||||
randomPart := hex.EncodeToString(randomBytes)[:6]
|
||||
serialNumber := fmt.Sprintf("%s%s", serialPrefix, randomPart)
|
||||
|
||||
if serialNumbers[serialNumber] {
|
||||
continue
|
||||
}
|
||||
|
||||
var existingSerial models.Serial
|
||||
checkResult := database.DB.Where("serial_number = ?", serialNumber).First(&existingSerial)
|
||||
if checkResult.Error != nil {
|
||||
serialNumbers[serialNumber] = true
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
for serialNumber := range serialNumbers {
|
||||
serial := models.Serial{
|
||||
SerialNumber: strings.ToUpper(serialNumber),
|
||||
CompanyName: companyName,
|
||||
ValidUntil: &validUntil,
|
||||
CreatedBy: &userId,
|
||||
IsActive: true,
|
||||
}
|
||||
serials = append(serials, serial)
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
result = database.DB.Create(&serials)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("保存序列号失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return serials, nil
|
||||
}
|
||||
|
||||
// GenerateQRCode 生成二维码
|
||||
func (s *SerialsService) GenerateQRCode(
|
||||
serialNumber string,
|
||||
baseUrl string,
|
||||
requestHost string,
|
||||
protocol string,
|
||||
) (string, string, error) {
|
||||
var serial models.Serial
|
||||
result := database.DB.Preload("User").Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial)
|
||||
if result.Error != nil {
|
||||
return "", "", fmt.Errorf("查询序列号失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
if !serial.IsActive {
|
||||
return "", "", fmt.Errorf("序列号状态无效: %w", errors.New("序列号已被禁用"))
|
||||
}
|
||||
|
||||
if serial.ValidUntil != nil && serial.ValidUntil.Before(time.Now()) {
|
||||
return "", "", fmt.Errorf("序列号已过期")
|
||||
}
|
||||
|
||||
// 确定查询 URL
|
||||
if baseUrl == "" {
|
||||
baseUrl = fmt.Sprintf("%s://%s/query.html", protocol, requestHost)
|
||||
}
|
||||
|
||||
var queryUrl string
|
||||
if strings.Contains(baseUrl, "?") {
|
||||
queryUrl = fmt.Sprintf("%s&serial=%s", baseUrl, serial.SerialNumber)
|
||||
} else {
|
||||
queryUrl = fmt.Sprintf("%s?serial=%s", baseUrl, serial.SerialNumber)
|
||||
}
|
||||
|
||||
// 生成二维码到临时文件
|
||||
filePath := fmt.Sprintf("temp_qr_%s.png", uuid.New().String())
|
||||
writer, err := standard.New(filePath, standard.WithQRWidth(6))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("二维码写入器创建失败: %w", err)
|
||||
}
|
||||
|
||||
qrc, errCode := qr.New(queryUrl)
|
||||
if errCode != nil {
|
||||
os.Remove(filePath)
|
||||
return "", "", fmt.Errorf("二维码创建失败: %w", errCode)
|
||||
}
|
||||
|
||||
if errSave := qrc.Save(writer); errSave != nil {
|
||||
os.Remove(filePath)
|
||||
return "", "", fmt.Errorf("二维码保存失败: %w", errSave)
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
fileContent, errRead := os.ReadFile(filePath)
|
||||
if errRead != nil {
|
||||
os.Remove(filePath)
|
||||
return "", "", fmt.Errorf("二维码文件读取失败: %w", errRead)
|
||||
}
|
||||
|
||||
// 删除临时文件
|
||||
os.Remove(filePath)
|
||||
|
||||
// 转换为 base64
|
||||
qrCodeBase64 := fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(fileContent))
|
||||
return qrCodeBase64, queryUrl, nil
|
||||
}
|
||||
|
||||
// Query 查询序列号信息
|
||||
func (s *SerialsService) Query(serialNumber string) (*models.Serial, error) {
|
||||
var serial models.Serial
|
||||
result := database.DB.Preload("User").Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询序列号失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
if serial.ValidUntil != nil && serial.ValidUntil.Before(time.Now()) {
|
||||
return nil, fmt.Errorf("序列号已过期")
|
||||
}
|
||||
|
||||
return &serial, nil
|
||||
}
|
||||
|
||||
// FindAll 获取序列号列表
|
||||
func (s *SerialsService) FindAll(page int, limit int, search string) ([]models.Serial, int, int, error) {
|
||||
var serials []models.Serial
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
db := database.DB.Preload("User")
|
||||
|
||||
// 搜索条件
|
||||
if search != "" {
|
||||
db = db.Where("serial_number LIKE ? OR company_name LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
countQuery := db.Model(&models.Serial{})
|
||||
if search != "" {
|
||||
countQuery = countQuery.Where("serial_number LIKE ? OR company_name LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
countQuery.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
result := db.Model(&models.Serial{}).Order("created_at DESC").Offset(offset).Limit(limit).Find(&serials)
|
||||
if result.Error != nil {
|
||||
return nil, 0, 0, fmt.Errorf("查询序列号列表失败: %w", result.Error)
|
||||
}
|
||||
|
||||
totalPages := (int(total) + limit - 1) / limit
|
||||
|
||||
return serials, int(total), totalPages, nil
|
||||
}
|
||||
|
||||
// Update 更新序列号信息
|
||||
func (s *SerialsService) Update(serialNumber string, updateData models.UpdateSerialDTO) (*models.Serial, error) {
|
||||
var serial models.Serial
|
||||
result := database.DB.Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("查询序列号失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
if updateData.CompanyName != "" {
|
||||
// 检查公司是否存在
|
||||
var company models.Company
|
||||
companyResult := database.DB.Where("company_name = ?", updateData.CompanyName).First(&company)
|
||||
if companyResult.Error != nil {
|
||||
company = models.Company{
|
||||
CompanyName: updateData.CompanyName,
|
||||
IsActive: true,
|
||||
}
|
||||
database.DB.Create(&company)
|
||||
}
|
||||
|
||||
serial.CompanyName = updateData.CompanyName
|
||||
}
|
||||
|
||||
if updateData.ValidUntil != nil {
|
||||
serial.ValidUntil = updateData.ValidUntil
|
||||
}
|
||||
|
||||
if updateData.IsActive != nil {
|
||||
serial.IsActive = *updateData.IsActive
|
||||
}
|
||||
|
||||
result = database.DB.Save(&serial)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("更新序列号失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return &serial, nil
|
||||
}
|
||||
|
||||
// Revoke 吊销序列号
|
||||
func (s *SerialsService) Revoke(serialNumber string) error {
|
||||
var serial models.Serial
|
||||
result := database.DB.Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("查询序列号失败: %w", errors.New("序列号不存在"))
|
||||
}
|
||||
|
||||
if !serial.IsActive {
|
||||
return fmt.Errorf("序列号状态无效: %w", errors.New("序列号已被吊销"))
|
||||
}
|
||||
|
||||
serial.IsActive = false
|
||||
result = database.DB.Save(&serial)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("吊销序列号失败: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
413
services/services_test.go
Normal file
413
services/services_test.go
Normal file
@@ -0,0 +1,413 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/config"
|
||||
"git.beifan.cn/trace-system/backend-go/database"
|
||||
"git.beifan.cn/trace-system/backend-go/logger"
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
config.LoadConfig()
|
||||
|
||||
if err := logger.InitializeLogger("test"); err != nil {
|
||||
fmt.Printf("日志系统初始化失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
database.InitDB()
|
||||
database.AutoMigrate()
|
||||
|
||||
database.DB.Unscoped().Where("1 = 1").Delete(&models.User{})
|
||||
database.DB.Unscoped().Where("1 = 1").Delete(&models.Company{})
|
||||
database.DB.Unscoped().Where("1 = 1").Delete(&models.Serial{})
|
||||
|
||||
exitCode := m.Run()
|
||||
|
||||
database.DB.Unscoped().Where("1 = 1").Delete(&models.User{})
|
||||
database.DB.Unscoped().Where("1 = 1").Delete(&models.Company{})
|
||||
database.DB.Unscoped().Where("1 = 1").Delete(&models.Serial{})
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func TestAuthService_ValidateUser_Success(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "testuser",
|
||||
Password: string(password),
|
||||
Name: "测试用户",
|
||||
Email: "test@example.com",
|
||||
Role: "user",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
authService := AuthService{}
|
||||
result, err := authService.ValidateUser("testuser", "password123")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "testuser", result.Username)
|
||||
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestAuthService_ValidateUser_WrongPassword(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "testuser2",
|
||||
Password: string(password),
|
||||
Name: "测试用户2",
|
||||
Email: "test2@example.com",
|
||||
Role: "user",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
authService := AuthService{}
|
||||
_, err := authService.ValidateUser("testuser2", "wrongpassword")
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestAuthService_ValidateUser_UserNotFound(t *testing.T) {
|
||||
authService := AuthService{}
|
||||
_, err := authService.ValidateUser("nonexistent", "password")
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAuthService_GenerateToken_Success(t *testing.T) {
|
||||
user := &models.User{
|
||||
ID: 1,
|
||||
Username: "testuser",
|
||||
Role: "user",
|
||||
}
|
||||
|
||||
authService := AuthService{}
|
||||
token, err := authService.GenerateToken(user)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
assert.NotEmpty(t, "Bearer "+token)
|
||||
}
|
||||
|
||||
func TestAuthService_GetProfile_Success(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "testuser3",
|
||||
Password: string(password),
|
||||
Name: "测试用户3",
|
||||
Email: "test3@example.com",
|
||||
Role: "user",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
authService := AuthService{}
|
||||
profile, err := authService.GetProfile(user.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, profile)
|
||||
assert.Equal(t, "testuser3", profile.Username)
|
||||
assert.Equal(t, "测试用户3", profile.Name)
|
||||
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestAuthService_ChangePassword_Success(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("oldpassword"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "testuser4",
|
||||
Password: string(password),
|
||||
Name: "测试用户4",
|
||||
Email: "test4@example.com",
|
||||
Role: "user",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
authService := AuthService{}
|
||||
err := authService.ChangePassword(user.ID, "oldpassword", "newpassword")
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
var updatedUser models.User
|
||||
database.DB.First(&updatedUser, user.ID)
|
||||
err = bcrypt.CompareHashAndPassword([]byte(updatedUser.Password), []byte("newpassword"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestAuthService_ChangePassword_WrongCurrentPassword(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("oldpassword"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "testuser5",
|
||||
Password: string(password),
|
||||
Name: "测试用户5",
|
||||
Email: "test5@example.com",
|
||||
Role: "user",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
authService := AuthService{}
|
||||
err := authService.ChangePassword(user.ID, "wrongpassword", "newpassword")
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestAuthService_UpdateProfile_Success(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "testuser6",
|
||||
Password: string(password),
|
||||
Name: "测试用户6",
|
||||
Email: "test6@example.com",
|
||||
Role: "user",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
authService := AuthService{}
|
||||
profile, err := authService.UpdateProfile(user.ID, "新名称", "newemail@example.com")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, profile)
|
||||
assert.Equal(t, "新名称", profile.Name)
|
||||
assert.Equal(t, "newemail@example.com", profile.Email)
|
||||
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestSerialsService_Generate_Success(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "adminuser",
|
||||
Password: string(password),
|
||||
Name: "管理员",
|
||||
Email: "admin@example.com",
|
||||
Role: "admin",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
serialService := SerialsService{}
|
||||
serials, err := serialService.Generate("TestCompany", 5, 30, user.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, serials, 5)
|
||||
assert.Equal(t, "TestCompany", serials[0].CompanyName)
|
||||
assert.True(t, serials[0].IsActive)
|
||||
|
||||
for _, serial := range serials {
|
||||
database.DB.Unscoped().Delete(&serial)
|
||||
}
|
||||
database.DB.Unscoped().Where("company_name = ?", "TestCompany").Delete(&models.Company{})
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestSerialsService_Generate_WithPrefix(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "adminuser2",
|
||||
Password: string(password),
|
||||
Name: "管理员2",
|
||||
Email: "admin2@example.com",
|
||||
Role: "admin",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
serialService := SerialsService{}
|
||||
serials, err := serialService.Generate("TestCompany2", 3, 30, user.ID, "TEST")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, serials, 3)
|
||||
assert.True(t, len(serials[0].SerialNumber) > 0)
|
||||
assert.Contains(t, serials[0].SerialNumber, "TEST")
|
||||
|
||||
for _, serial := range serials {
|
||||
database.DB.Unscoped().Delete(&serial)
|
||||
}
|
||||
database.DB.Unscoped().Where("company_name = ?", "TestCompany2").Delete(&models.Company{})
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestSerialsService_Query_QuerySuccess(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "adminuser3",
|
||||
Password: string(password),
|
||||
Name: "管理员3",
|
||||
Email: "admin3@example.com",
|
||||
Role: "admin",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
serialService := SerialsService{}
|
||||
serials, _ := serialService.Generate("TestCompany3", 1, 30, user.ID, "QR")
|
||||
|
||||
serialNumber := strings.ToUpper(serials[0].SerialNumber)
|
||||
result, err := serialService.Query(serialNumber)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, serialNumber, strings.ToUpper(result.SerialNumber))
|
||||
assert.True(t, result.IsActive)
|
||||
|
||||
for _, serial := range serials {
|
||||
database.DB.Unscoped().Delete(&serial)
|
||||
}
|
||||
database.DB.Unscoped().Where("company_name = ?", "TestCompany3").Delete(&models.Company{})
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestSerialsService_Query_SerialNotFound(t *testing.T) {
|
||||
serialService := SerialsService{}
|
||||
_, err := serialService.Query("NONEXISTENT")
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSerialsService_FindAll_Success(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "adminuser4",
|
||||
Password: string(password),
|
||||
Name: "管理员4",
|
||||
Email: "admin4@example.com",
|
||||
Role: "admin",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
serialService := SerialsService{}
|
||||
serials, _ := serialService.Generate("TestCompany4", 10, 30, user.ID, "LIST")
|
||||
|
||||
result, total, totalPages, err := serialService.FindAll(1, 5, "")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 5)
|
||||
assert.GreaterOrEqual(t, total, 10)
|
||||
assert.Greater(t, totalPages, 0)
|
||||
|
||||
for _, serial := range serials {
|
||||
database.DB.Unscoped().Delete(&serial)
|
||||
}
|
||||
database.DB.Unscoped().Where("company_name = ?", "TestCompany4").Delete(&models.Company{})
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestSerialsService_FindAll_WithSearch(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "adminuser5",
|
||||
Password: string(password),
|
||||
Name: "管理员5",
|
||||
Email: "admin5@example.com",
|
||||
Role: "admin",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
serialService := SerialsService{}
|
||||
serials, _ := serialService.Generate("SearchCompany", 5, 30, user.ID, "SEARCH")
|
||||
|
||||
result, _, _, err := serialService.FindAll(1, 10, "SearchCompany")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(result), 0)
|
||||
assert.Equal(t, "SearchCompany", result[0].CompanyName)
|
||||
|
||||
for _, serial := range serials {
|
||||
database.DB.Unscoped().Delete(&serial)
|
||||
}
|
||||
database.DB.Unscoped().Where("company_name = ?", "SearchCompany").Delete(&models.Company{})
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestSerialsService_Revoke_Success(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "adminuser6",
|
||||
Password: string(password),
|
||||
Name: "管理员6",
|
||||
Email: "admin6@example.com",
|
||||
Role: "admin",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
serialService := SerialsService{}
|
||||
serials, _ := serialService.Generate("RevokeCompany", 1, 30, user.ID, "REVOKE")
|
||||
|
||||
serialNumber := serials[0].SerialNumber
|
||||
err := serialService.Revoke(serialNumber)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
var revokedSerial models.Serial
|
||||
database.DB.Where("serial_number = ?", serialNumber).First(&revokedSerial)
|
||||
assert.False(t, revokedSerial.IsActive)
|
||||
|
||||
for _, serial := range serials {
|
||||
database.DB.Unscoped().Delete(&serial)
|
||||
}
|
||||
database.DB.Unscoped().Where("company_name = ?", "RevokeCompany").Delete(&models.Company{})
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
|
||||
func TestSerialsService_Update_Success(t *testing.T) {
|
||||
var user models.User
|
||||
password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
user = models.User{
|
||||
Username: "adminuser7",
|
||||
Password: string(password),
|
||||
Name: "管理员7",
|
||||
Email: "admin7@example.com",
|
||||
Role: "admin",
|
||||
}
|
||||
database.DB.Create(&user)
|
||||
|
||||
serialService := SerialsService{}
|
||||
serials, _ := serialService.Generate("UpdateCompany", 1, 30, user.ID, "UPDATE")
|
||||
|
||||
serialNumber := serials[0].SerialNumber
|
||||
newValidUntil := time.Now().AddDate(0, 0, 60)
|
||||
isActive := false
|
||||
|
||||
updateData := models.UpdateSerialDTO{
|
||||
ValidUntil: &newValidUntil,
|
||||
IsActive: &isActive,
|
||||
}
|
||||
|
||||
result, err := serialService.Update(serialNumber, updateData)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.False(t, result.IsActive)
|
||||
|
||||
for _, serial := range serials {
|
||||
database.DB.Unscoped().Delete(&serial)
|
||||
}
|
||||
database.DB.Unscoped().Where("company_name = ?", "UpdateCompany").Delete(&models.Company{})
|
||||
database.DB.Unscoped().Delete(&user)
|
||||
}
|
||||
162
tests/main_test.go
Normal file
162
tests/main_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"git.beifan.cn/trace-system/backend-go/config"
|
||||
"git.beifan.cn/trace-system/backend-go/controllers"
|
||||
"git.beifan.cn/trace-system/backend-go/database"
|
||||
"git.beifan.cn/trace-system/backend-go/logger"
|
||||
"git.beifan.cn/trace-system/backend-go/models"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// 加载测试配置
|
||||
config.LoadConfig()
|
||||
|
||||
// 初始化日志系统
|
||||
if err := logger.InitializeLogger("test"); err != nil {
|
||||
fmt.Printf("日志系统初始化失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
logger.Info("测试开始")
|
||||
|
||||
// 初始化测试数据库
|
||||
database.InitDB()
|
||||
|
||||
// 自动迁移数据库
|
||||
database.AutoMigrate()
|
||||
|
||||
// 创建测试用户
|
||||
createTestUsers()
|
||||
|
||||
// 运行测试
|
||||
exitCode := m.Run()
|
||||
|
||||
// 清理测试数据
|
||||
cleanupTestData()
|
||||
|
||||
logger.Info("测试结束,退出码", logger.Int("exitCode", exitCode))
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func cleanupTestData() {
|
||||
// 删除所有测试数据
|
||||
database.DB.Unscoped().Where("1 = 1").Delete(&models.User{})
|
||||
database.DB.Unscoped().Where("1 = 1").Delete(&models.Company{})
|
||||
database.DB.Unscoped().Where("1 = 1").Delete(&models.Serial{})
|
||||
}
|
||||
|
||||
func createTestUsers() {
|
||||
testPassword, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||
|
||||
testUsers := []models.User{
|
||||
{
|
||||
Username: "admin",
|
||||
Password: string(testPassword),
|
||||
Name: "系统管理员",
|
||||
Email: "admin@example.com",
|
||||
Role: "admin",
|
||||
},
|
||||
{
|
||||
Username: "user1",
|
||||
Password: string(testPassword),
|
||||
Name: "普通用户",
|
||||
Email: "user1@example.com",
|
||||
Role: "user",
|
||||
},
|
||||
}
|
||||
|
||||
database.DB.Create(&testUsers)
|
||||
}
|
||||
|
||||
func TestHealthCheck(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
// 模拟请求
|
||||
c.Request, _ = http.NewRequest("GET", "/api/health", nil)
|
||||
|
||||
// 直接调用健康检查响应,不设置路由(避免依赖 Gin 引擎)
|
||||
c.Writer.WriteHeader(http.StatusOK)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"message": "服务器运行正常",
|
||||
})
|
||||
|
||||
// 断言响应
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ok", response["status"])
|
||||
assert.Equal(t, "服务器运行正常", response["message"])
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
// 测试数据
|
||||
testData := models.LoginDTO{
|
||||
Username: "admin",
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
// 模拟请求
|
||||
jsonData, _ := json.Marshal(testData)
|
||||
c.Request, _ = http.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(jsonData))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 调用控制器方法
|
||||
controller := controllers.NewAuthController()
|
||||
controller.Login(c)
|
||||
|
||||
// 断言响应
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "登录成功", response["message"])
|
||||
assert.Contains(t, response, "accessToken")
|
||||
assert.Contains(t, response, "user")
|
||||
}
|
||||
|
||||
func TestLoginFailed(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
// 测试数据
|
||||
testData := models.LoginDTO{
|
||||
Username: "admin",
|
||||
Password: "wrongpassword",
|
||||
}
|
||||
|
||||
// 模拟请求
|
||||
jsonData, _ := json.Marshal(testData)
|
||||
c.Request, _ = http.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(jsonData))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 调用控制器方法
|
||||
controller := controllers.NewAuthController()
|
||||
controller.Login(c)
|
||||
|
||||
// 断言响应
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, response["message"], "用户名或密码错误")
|
||||
}
|
||||
Reference in New Issue
Block a user