up
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
.DS_Store
|
||||
/dist
|
||||
/data
|
||||
/tmp
|
||||
|
||||
/build
|
||||
|
||||
/test
|
||||
|
||||
*.db
|
||||
|
||||
OPSYS
|
||||
@@ -1,193 +0,0 @@
|
||||
# OPS 后端重构架构设计文档
|
||||
|
||||
## 当前状态
|
||||
✅ 已完成基础架构重构
|
||||
✅ 新目录结构已创建
|
||||
✅ 配置管理模块完成
|
||||
✅ 数据库连接层完成
|
||||
✅ 响应统一处理完成
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/ # 入口点
|
||||
│ └── ops-server/ # 主应用程序入口
|
||||
│ └── main.go
|
||||
├── internal/ # 私有应用程序代码
|
||||
│ ├── config/ # 配置管理
|
||||
│ │ ├── config.go # 配置结构定义
|
||||
│ │ └── [其他配置组件]
|
||||
│ ├── database/ # 数据库层
|
||||
│ │ ├── connection.go # 数据库连接
|
||||
│ │ └── migration.go # 数据库迁移和模型定义
|
||||
│ ├── models/ # 数据模型(待重构)
|
||||
│ ├── repository/ # 数据访问层(待创建)
|
||||
│ │ ├── user_repository.go
|
||||
│ │ ├── purchase_repository.go
|
||||
│ │ └── file_repository.go
|
||||
│ ├── service/ # 业务逻辑层(待创建)
|
||||
│ │ ├── auth_service.go
|
||||
│ │ ├── purchase_service.go
|
||||
│ │ └── file_service.go
|
||||
│ ├── handler/ # HTTP处理器(待创建)
|
||||
│ │ ├── auth_handler.go
|
||||
│ │ ├── purchase_handler.go
|
||||
│ │ └── file_handler.go
|
||||
│ ├── middleware/ # 中间件(待创建)
|
||||
│ │ ├── auth.go
|
||||
│ │ ├── logging.go
|
||||
│ │ └── recovery.go
|
||||
│ └── pkg/ # 内部公共库(待创建)
|
||||
│ ├── errors/
|
||||
│ ├── validation/
|
||||
│ └── utils/
|
||||
├── api/ # API定义(待创建)
|
||||
│ └── v1/
|
||||
│ └── routes.go
|
||||
├── pkg/ # 公共库
|
||||
│ └── response/
|
||||
│ └── response.go # API响应统一处理
|
||||
├── data/ # 数据目录(配置文件、数据库)
|
||||
├── defConfig/ # 默认配置模板
|
||||
├── dist/ # 前端构建产物(编译后)
|
||||
└── tests/ # 测试文件
|
||||
```
|
||||
|
||||
## 主要改进
|
||||
|
||||
### 1. 配置管理重构
|
||||
**旧方案问题:**
|
||||
- 使用全局变量 `var Configs map[string]interface{}`
|
||||
- 使用奇怪的命名如 `ConfigsWed`, `ConfigsFile`
|
||||
- 拼写错误:`Pahts` 应该是 `Paths`
|
||||
- 缺少类型安全和验证
|
||||
|
||||
**新方案:**
|
||||
- 使用结构体定义配置,支持类型安全
|
||||
- 支持默认配置自动生成
|
||||
- 支持热重载(未来扩展)
|
||||
- 统一配置路径管理
|
||||
|
||||
### 2. 数据库层重构
|
||||
**旧方案问题:**
|
||||
- 直接在路由层进行数据库操作
|
||||
- 缺少连接池配置
|
||||
- 错误处理不一致
|
||||
|
||||
**新方案:**
|
||||
- 集中管理数据库连接
|
||||
- 自动连接池配置
|
||||
- 支持多种数据库(SQLite/MySQL/PostgreSQL)
|
||||
- 统一错误处理
|
||||
|
||||
### 3. 错误处理统一
|
||||
**旧方案问题:**
|
||||
- 混合使用 panic、return error 和日志
|
||||
- HTTP 响应格式不一致
|
||||
- 错误码管理混乱
|
||||
|
||||
**新方案:**
|
||||
- 统一 API 响应格式
|
||||
- 标准错误码映射
|
||||
- 结构化错误信息
|
||||
- 详细的 HTTP 状态码
|
||||
|
||||
### 4. 中间件系统
|
||||
**新功能:**
|
||||
- CORS 跨域支持
|
||||
- 请求日志记录
|
||||
- 认证中间件
|
||||
- 性能监控
|
||||
- 限流保护
|
||||
|
||||
## 迁移步骤
|
||||
|
||||
### 第一阶段:基础架构 ✅
|
||||
- [x] 创建新的目录结构
|
||||
- [x] 重构配置管理模块
|
||||
- [x] 重构数据库连接层
|
||||
- [x] 创建统一响应处理
|
||||
|
||||
### 第二阶段:数据访问层
|
||||
- [ ] 创建 Repository 层
|
||||
- [ ] 迁移用户相关数据访问
|
||||
- [ ] 迁移采购订单数据访问
|
||||
- [ ] 迁移文件管理数据访问
|
||||
|
||||
### 第三阶段:业务逻辑层
|
||||
- [ ] 创建 Service 层
|
||||
- [ ] 迁移用户认证逻辑
|
||||
- [ ] 迁移采购订单管理逻辑
|
||||
- [ ] 迁移文件上传逻辑
|
||||
|
||||
### 第四阶段:HTTP 层
|
||||
- [ ] 创建 Handler 层
|
||||
- [ ] 迁移用户认证 API
|
||||
- [ ] 迁移采购订单 API
|
||||
- [ ] 迁移文件管理 API
|
||||
|
||||
### 第五阶段:中间件和测试
|
||||
- [ ] 创建中间件系统
|
||||
- [ ] 添加单元测试
|
||||
- [ ] 添加集成测试
|
||||
- [ ] 性能测试
|
||||
|
||||
## 运行说明
|
||||
|
||||
### 准备步骤
|
||||
1. 运行配置迁移脚本:
|
||||
```bash
|
||||
python migrate-config.py
|
||||
```
|
||||
|
||||
2. 检查前端构建:
|
||||
```bash
|
||||
# 确保前端已构建,在frontend目录中运行
|
||||
npm run build
|
||||
# 或手动复制dist目录到backend/dist
|
||||
```
|
||||
|
||||
3. 启动新版本服务器:
|
||||
```bash
|
||||
# Windows
|
||||
.\start-dev.bat
|
||||
|
||||
# Linux/Mac
|
||||
go run ./cmd/ops-server/main.go
|
||||
```
|
||||
|
||||
### 迁移过程中的注意事项
|
||||
1. **保持向后兼容**:逐步迁移,确保 API 接口不变
|
||||
2. **数据库数据安全**:旧数据库文件会自动迁移
|
||||
3. **配置文件备份**:迁移前自动备份旧配置
|
||||
4. **增量测试**:每次迁移后测试新功能
|
||||
|
||||
## API 兼容性保证
|
||||
|
||||
### 保持不变的接口
|
||||
- 用户登录:`POST /api/users/login`
|
||||
- 用户注册:`POST /api/users/register`
|
||||
- 获取采购订单:`GET /api/purchase/orders`
|
||||
- 上传文件:`POST /api/files/upload`
|
||||
|
||||
### 响应的格式统一
|
||||
```json
|
||||
{
|
||||
"code": "0", // 错误码,0表示成功
|
||||
"message": "Success", // 人类可读的消息
|
||||
"data": {} // 实际数据
|
||||
}
|
||||
```
|
||||
|
||||
## 性能改进目标
|
||||
1. **响应时间**:平均 < 200ms
|
||||
2. **并发连接**:支持 1000+ 并发
|
||||
3. **内存使用**:< 200MB
|
||||
4. **启动时间**:< 5s
|
||||
|
||||
## 监控和日志
|
||||
- 结构化日志输出
|
||||
- 请求追踪ID
|
||||
- 性能指标收集
|
||||
- 错误聚合报告
|
||||
@@ -1,112 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"ops/api/v1"
|
||||
"ops/routers"
|
||||
)
|
||||
|
||||
// RegisterAllRoutes 注册所有路由,包括兼容性路由
|
||||
func RegisterAllRoutes(r *gin.Engine) {
|
||||
// API v1路由(RESTful风格)
|
||||
apiV1 := r.Group("/api/v1")
|
||||
v1.RegisterRoutes(apiV1)
|
||||
|
||||
// 兼容性API路由(保持原有路径结构)
|
||||
api := r.Group("/api")
|
||||
registerCompatibilityRoutes(api)
|
||||
|
||||
// 根路径
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/index.html")
|
||||
})
|
||||
}
|
||||
|
||||
// registerCompatibilityRoutes 注册兼容性路由
|
||||
func registerCompatibilityRoutes(api *gin.RouterGroup) {
|
||||
// 健康检查
|
||||
api.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"code": "0",
|
||||
"message": "API is healthy",
|
||||
"data": nil,
|
||||
})
|
||||
})
|
||||
|
||||
// 测试端点
|
||||
api.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"code": "0",
|
||||
"message": "API test successful",
|
||||
"data": nil,
|
||||
})
|
||||
})
|
||||
|
||||
api.POST("/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"code": "0",
|
||||
"message": "API test successful (POST)",
|
||||
"data": nil,
|
||||
})
|
||||
})
|
||||
|
||||
// 注册原有路由模块
|
||||
api.Static("/static", "./dist")
|
||||
routers.ApiStatic(api.Group("/static"))
|
||||
routers.ApiUser(api.Group("/users"))
|
||||
routers.ApiFiles(api.Group("/files"))
|
||||
routers.ApiPurchase(api.Group("/purchase"))
|
||||
|
||||
// 根API路径
|
||||
api.GET("/", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"code": "0",
|
||||
"message": "OPS API",
|
||||
"data": gin.H{
|
||||
"version": "1.0",
|
||||
"routes": []string{
|
||||
"/api/users/*",
|
||||
"/api/files/*",
|
||||
"/api/purchase/*",
|
||||
"/api/v1/* (RESTful API)",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// CreateRouter 创建完整路由引擎
|
||||
func CreateRouter() *gin.Engine {
|
||||
r := gin.New()
|
||||
|
||||
// 设置信任代理
|
||||
r.SetTrustedProxies([]string{"127.0.0.1"})
|
||||
|
||||
// 注册所有路由
|
||||
RegisterAllRoutes(r)
|
||||
|
||||
// 最后注册404处理
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
// 如果是API请求,返回JSON 404
|
||||
path := c.Request.URL.Path
|
||||
if strings.HasPrefix(path, "/api") {
|
||||
c.JSON(404, gin.H{
|
||||
"code": "404",
|
||||
"message": "API endpoint not found",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 否则尝试提供静态文件
|
||||
fs := http.FileServer(http.Dir("./dist"))
|
||||
fs.ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"ops/internal/database"
|
||||
"ops/internal/handler"
|
||||
"ops/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
db *gorm.DB
|
||||
authHandler *handler.AuthHandler
|
||||
fileHandler *handler.FileHandler
|
||||
purchaseHandler *handler.PurchaseHandler
|
||||
)
|
||||
|
||||
func init() {
|
||||
db = database.GetDB()
|
||||
authHandler = handler.NewAuthHandler(db)
|
||||
fileHandler = handler.NewFileHandler(db)
|
||||
purchaseHandler = handler.NewPurchaseHandler(db)
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册所有v1版本的API路由
|
||||
func RegisterRoutes(r *gin.RouterGroup) {
|
||||
// API根路径测试
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": "0",
|
||||
"message": "OPS API v1",
|
||||
"data": nil,
|
||||
})
|
||||
})
|
||||
|
||||
// 静态文件路由 - 保持兼容性
|
||||
r.StaticFS("/static", http.Dir("./dist"))
|
||||
|
||||
// 用户认证相关路由
|
||||
userGroup := r.Group("/users")
|
||||
{
|
||||
// 用户认证
|
||||
userGroup.POST("/login", authHandler.UserLogin)
|
||||
userGroup.POST("/register", authHandler.UserRegister)
|
||||
userGroup.POST("/forgot-password", authHandler.UserForgotPassword)
|
||||
userGroup.POST("/reset-password", authHandler.UserResetPassword)
|
||||
|
||||
// 用户信息 - 需要认证
|
||||
userGroup.PUT("/profile", middleware.AuthToken(), authHandler.UserUpdateProfile)
|
||||
userGroup.GET("/profile", middleware.AuthToken(), authHandler.UserProfile)
|
||||
userGroup.POST("/logout", middleware.AuthToken(), authHandler.UserLogout)
|
||||
|
||||
// 用户管理(管理员) - TODO: 实现管理员功能
|
||||
userGroup.GET("/list", middleware.AuthToken(), adminMiddleware(), getUserList)
|
||||
userGroup.POST("/create", middleware.AuthToken(), adminMiddleware(), createUser)
|
||||
userGroup.PUT("/:id", middleware.AuthToken(), adminMiddleware(), updateUser)
|
||||
userGroup.DELETE("/:id", middleware.AuthToken(), adminMiddleware(), deleteUser)
|
||||
}
|
||||
|
||||
// 文件上传管理 - v1 API
|
||||
// 注意:具体路由必须放在通配路由之前
|
||||
r.POST("/files/upload", middleware.AuthToken(), fileHandler.UploadFile)
|
||||
r.GET("/files/list", middleware.AuthToken(), fileHandler.GetFileList)
|
||||
r.GET("/files/:id", middleware.AuthToken(), fileHandler.GetFileByID)
|
||||
r.DELETE("/files/:id", middleware.AuthToken(), fileHandler.DeleteFile)
|
||||
r.GET("/files/download/:hash", fileHandler.DownloadFile)
|
||||
r.GET("/files/get/:hash", fileHandler.GetFile)
|
||||
|
||||
// 采购订单管理
|
||||
purchaseGroup := r.Group("/purchase")
|
||||
{
|
||||
// 保持与前端兼容的POST路由(原始API使用POST)
|
||||
purchaseGroup.POST("/getorders", middleware.AuthToken(), purchaseHandler.GetOrders)
|
||||
purchaseGroup.POST("/addorder", middleware.AuthToken(), purchaseHandler.CreateOrder)
|
||||
|
||||
// RESTful风格的新API
|
||||
purchaseGroup.GET("/orders", middleware.AuthToken(), purchaseHandler.GetOrders)
|
||||
purchaseGroup.POST("/orders", middleware.AuthToken(), purchaseHandler.CreateOrder)
|
||||
purchaseGroup.GET("/orders/:id", middleware.AuthToken(), purchaseHandler.GetOrderDetails)
|
||||
|
||||
// TODO: 实现更新、删除和其他功能
|
||||
purchaseGroup.PUT("/orders/:id", middleware.AuthToken(), purchaseUpdateOrder)
|
||||
purchaseGroup.DELETE("/orders/:id", middleware.AuthToken(), purchaseDeleteOrder)
|
||||
purchaseGroup.POST("/orders/:id/costs", middleware.AuthToken(), purchaseAddCost)
|
||||
purchaseGroup.PUT("/orders/:id/costs/:costId", middleware.AuthToken(), purchaseUpdateCost)
|
||||
purchaseGroup.DELETE("/orders/:id/costs/:costId", middleware.AuthToken(), purchaseDeleteCost)
|
||||
}
|
||||
|
||||
// 系统管理(管理员)
|
||||
systemGroup := r.Group("/system")
|
||||
{
|
||||
systemGroup.GET("/status", systemStatus)
|
||||
systemGroup.GET("/config", middleware.AuthToken(), adminMiddleware(), systemGetConfig)
|
||||
systemGroup.PUT("/config", middleware.AuthToken(), adminMiddleware(), systemUpdateConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员中间件占位函数
|
||||
func adminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// TODO: 实现管理员权限检查
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 占位函数 - 将在后续步骤中实现
|
||||
func getUserList(c *gin.Context) {}
|
||||
func createUser(c *gin.Context) {}
|
||||
func updateUser(c *gin.Context) {}
|
||||
func deleteUser(c *gin.Context) {}
|
||||
func purchaseUpdateOrder(c *gin.Context) {}
|
||||
func purchaseDeleteOrder(c *gin.Context) {}
|
||||
func purchaseAddCost(c *gin.Context) {}
|
||||
func purchaseUpdateCost(c *gin.Context) {}
|
||||
func purchaseDeleteCost(c *gin.Context) {}
|
||||
func systemStatus(c *gin.Context) {}
|
||||
func systemGetConfig(c *gin.Context) {}
|
||||
func systemUpdateConfig(c *gin.Context) {}
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
APP_NAME="OPSYS"
|
||||
APP_PATH="./build/$APP_NAME"
|
||||
|
||||
# 编译应用
|
||||
echo "编译应用..."
|
||||
CGO_ENABLED=0 GOOS=linux go build -o $APP_NAME -ldflags="-s -w" main.go
|
||||
|
||||
# 创建目录
|
||||
echo "创建目录..."
|
||||
sudo mkdir -p $APP_PATH
|
||||
|
||||
# 复制文件
|
||||
echo "复制文件..."
|
||||
sudo cp $APP_NAME $APP_PATH/
|
||||
sudo cp -r defConfig $APP_PATH/
|
||||
sudo cp -r dist $APP_PATH/
|
||||
@@ -1,57 +0,0 @@
|
||||
version: "0.3.1"
|
||||
|
||||
web:
|
||||
host: "127.0.0.1"
|
||||
port: "8080"
|
||||
tls: false
|
||||
certPrivatePath: ""
|
||||
certPublicPath: ""
|
||||
|
||||
database:
|
||||
type: "sqlite" # mysql pg or sqlite
|
||||
path: "data/database.db" # sqlite path
|
||||
host: "" # mysql host
|
||||
port: ""
|
||||
name: ""
|
||||
user: ""
|
||||
pass: ""
|
||||
|
||||
user:
|
||||
cookieTimeout: 604800
|
||||
passHashType: "md5" #密码哈希类型 text md5 md5salt
|
||||
|
||||
|
||||
file:
|
||||
maxSize: 52428800
|
||||
pahts:
|
||||
avatar: "data/static/avatar/"
|
||||
image: "data/upload/image/"
|
||||
video: "data/upload/video/"
|
||||
music: "data/upload/music/"
|
||||
pdf: "data/upload/pdf/"
|
||||
other: "data/upload/other/"
|
||||
|
||||
allowImageMime:
|
||||
image/jpeg: ".jpeg"
|
||||
image/png: ".png"
|
||||
image/gif: ".gif"
|
||||
image/bmp: ".bmp"
|
||||
|
||||
allowVideoMime:
|
||||
video/mp4: ".mp4"
|
||||
video/x-msvideo: ".avi"
|
||||
video/quicktime: ".mov"
|
||||
video/x-flv: ".flv"
|
||||
video/mpeg: ".mpeg"
|
||||
|
||||
allowMusicMime:
|
||||
audio/mpeg: ".mpeg"
|
||||
audio/aac: ".aac"
|
||||
audio/wav: ".wav"
|
||||
audio/flac: ".flac"
|
||||
|
||||
allowPdfMime:
|
||||
application/pdf: ".pdf"
|
||||
|
||||
|
||||
configed: false
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"apiOK":0,
|
||||
"apiErr":-1,
|
||||
"postErr":-2,
|
||||
"jsonErr":-3,
|
||||
"userNameDup":-4,
|
||||
"userNameNoFund":-41,
|
||||
"userPassIncorrect":-42,
|
||||
"userEmailFormatError":-43,
|
||||
"userCookieError":-44,
|
||||
"userCookieNotFund":-44,
|
||||
"userCookieExpired":-44,
|
||||
"file_mime_err":-51,
|
||||
"file_size_err":-52,
|
||||
"file_name_err":-53,
|
||||
"file_get_err":-54,
|
||||
"file_hash_err":-55,
|
||||
"file_save_err":-56,
|
||||
"file_not_found":-57,
|
||||
"file_part_err":-58
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export PATH=$PATH:$(go env GOPATH)/bin
|
||||
|
||||
fresh
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
module ops
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.9
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
gorm.io/datatypes v1.2.7
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/cors v1.7.2 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // 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/jackc/puddle/v2 v2.2.2 // 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/mattn/go-sqlite3 v1.14.22 // 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/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
)
|
||||
@@ -1,156 +0,0 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
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/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
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/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
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-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/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-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
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/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
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/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/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
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/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=
|
||||
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.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
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/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/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
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/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
||||
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
APP_NAME="OPSYS"
|
||||
APP_PATH="/opt/$APP_NAME"
|
||||
SERVICE_FILE="/etc/systemd/system/$APP_NAME.service"
|
||||
LOG_PATH="/var/log/$APP_NAME"
|
||||
|
||||
echo "正在安装 $APP_NAME..."
|
||||
|
||||
# 编译应用
|
||||
echo "编译应用..."
|
||||
CGO_ENABLED=0 GOOS=linux go build -o $APP_NAME -ldflags="-s -w" main.go
|
||||
|
||||
# 创建目录
|
||||
echo "创建目录..."
|
||||
sudo mkdir -p $APP_PATH
|
||||
sudo mkdir -p $LOG_PATH
|
||||
|
||||
# 复制文件
|
||||
echo "复制文件..."
|
||||
sudo cp $APP_NAME $APP_PATH/
|
||||
sudo cp -r defConfig $APP_PATH/
|
||||
sudo cp -r dist $APP_PATH/
|
||||
|
||||
# 创建服务文件
|
||||
echo "创建服务文件..."
|
||||
sudo tee $SERVICE_FILE > /dev/null <<EOF
|
||||
[Unit]
|
||||
Description=My Gin Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=$APP_PATH
|
||||
ExecStart=$APP_PATH/$APP_NAME
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=append:$LOG_PATH/access.log
|
||||
StandardError=append:$LOG_PATH/error.log
|
||||
Environment=GIN_MODE=release
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# 设置权限
|
||||
echo "设置权限..."
|
||||
sudo chown -R www-data:www-data $APP_PATH
|
||||
sudo chown -R www-data:www-data $LOG_PATH
|
||||
sudo chmod 750 $APP_PATH/$APP_NAME
|
||||
|
||||
# 重载并启动
|
||||
echo "启动服务..."
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable $APP_NAME
|
||||
sudo systemctl start $APP_NAME
|
||||
|
||||
echo "安装完成!"
|
||||
echo "使用以下命令管理服务:"
|
||||
echo " sudo systemctl status $APP_NAME"
|
||||
echo " sudo journalctl -u $APP_NAME -f"
|
||||
@@ -1,154 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
// Config 全局配置
|
||||
type Config struct {
|
||||
Web WebConfig `yaml:"web"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
User UserConfig `yaml:"user"`
|
||||
File FileConfig `yaml:"file"`
|
||||
}
|
||||
|
||||
// WebConfig Web服务配置
|
||||
type WebConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
TLS bool `yaml:"tls"`
|
||||
CertPrivatePath string `yaml:"certPrivatePath"`
|
||||
CertPublicPath string `yaml:"certPublicPath"`
|
||||
}
|
||||
|
||||
// DatabaseConfig 数据库配置
|
||||
type DatabaseConfig struct {
|
||||
Type string `yaml:"type"` // sqlite, mysql, postgres
|
||||
Path string `yaml:"path"` // SQLite路径
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
Name string `yaml:"name"`
|
||||
User string `yaml:"user"`
|
||||
Pass string `yaml:"pass"`
|
||||
}
|
||||
|
||||
// UserConfig 用户相关配置
|
||||
type UserConfig struct {
|
||||
CookieTimeout int `yaml:"cookieTimeout"`
|
||||
PassHashType string `yaml:"passHashType"` // text, md5, md5salt
|
||||
}
|
||||
|
||||
// FileConfig 文件上传配置
|
||||
type FileConfig struct {
|
||||
MaxSize uint64 `yaml:"maxSize"`
|
||||
Paths map[string]string `yaml:"paths"`
|
||||
AllowImageMime map[string]string `yaml:"allowImageMime"`
|
||||
AllowVideoMime map[string]string `yaml:"allowVideoMime"`
|
||||
AllowMusicMime map[string]string `yaml:"allowMusicMime"`
|
||||
AllowPdfMime map[string]string `yaml:"allowPdfMime"`
|
||||
}
|
||||
|
||||
// Current 全局配置实例
|
||||
var Current *Config
|
||||
|
||||
// Load 加载配置文件
|
||||
func Load(configPath string) error {
|
||||
// 如果配置文件不存在,创建默认配置
|
||||
if !fileExists(configPath) {
|
||||
if err := createDefaultConfig(configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 读取配置文件
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析YAML
|
||||
config := &Config{}
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Current = config
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// 创建默认配置文件
|
||||
func createDefaultConfig(path string) error {
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
defaultConfig := &Config{
|
||||
Web: WebConfig{
|
||||
Host: "127.0.0.1",
|
||||
Port: "8080",
|
||||
TLS: false,
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Type: "sqlite",
|
||||
Path: "data/database.db",
|
||||
},
|
||||
User: UserConfig{
|
||||
CookieTimeout: 604800,
|
||||
PassHashType: "md5",
|
||||
},
|
||||
File: FileConfig{
|
||||
MaxSize: 52428800, // 50MB
|
||||
Paths: map[string]string{
|
||||
"avatar": "data/static/avatar/",
|
||||
"image": "data/upload/image/",
|
||||
"video": "data/upload/video/",
|
||||
"music": "data/upload/music/",
|
||||
"pdf": "data/upload/pdf/",
|
||||
"other": "data/upload/other/",
|
||||
},
|
||||
AllowImageMime: map[string]string{
|
||||
"image/jpeg": ".jpeg",
|
||||
"image/png": ".png",
|
||||
"image/gif": ".gif",
|
||||
"image/bmp": ".bmp",
|
||||
},
|
||||
AllowVideoMime: map[string]string{
|
||||
"video/mp4": ".mp4",
|
||||
"video/x-msvideo": ".avi",
|
||||
"video/quicktime": ".mov",
|
||||
"video/x-flv": ".flv",
|
||||
"video/mpeg": ".mpeg",
|
||||
},
|
||||
AllowMusicMime: map[string]string{
|
||||
"audio/mpeg": ".mpeg",
|
||||
"audio/aac": ".aac",
|
||||
"audio/wav": ".wav",
|
||||
"audio/flac": ".flac",
|
||||
},
|
||||
AllowPdfMime: map[string]string{
|
||||
"application/pdf": ".pdf",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 序列化为YAML
|
||||
data, err := yaml.Marshal(defaultConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"ops/internal/config"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// DB 全局数据库实例
|
||||
var DB *gorm.DB
|
||||
|
||||
// Init 初始化数据库连接
|
||||
func Init() error {
|
||||
cfg := config.Current.Database
|
||||
|
||||
var dialector gorm.Dialector
|
||||
|
||||
switch cfg.Type {
|
||||
case "sqlite":
|
||||
dialector = sqlite.Open(cfg.Path)
|
||||
case "mysql":
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
cfg.User, cfg.Pass, cfg.Host, cfg.Port, cfg.Name)
|
||||
dialector = mysql.Open(dsn)
|
||||
case "postgres", "pg":
|
||||
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai",
|
||||
cfg.Host, cfg.User, cfg.Pass, cfg.Name, cfg.Port)
|
||||
dialector = postgres.Open(dsn)
|
||||
default:
|
||||
return fmt.Errorf("不支持的数据库类型: %s", cfg.Type)
|
||||
}
|
||||
|
||||
// 配置GORM
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
var err error
|
||||
DB, err = gorm.Open(dialector, gormConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("数据库连接失败: %v", err)
|
||||
}
|
||||
|
||||
// 配置连接池
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
log.Println("数据库连接成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDB 获取数据库实例
|
||||
func GetDB() *gorm.DB {
|
||||
return DB
|
||||
}
|
||||
|
||||
// Close 关闭数据库连接
|
||||
func Close() error {
|
||||
if DB != nil {
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package database
|
||||
|
||||
// AutoMigrate 自动迁移所有表
|
||||
func AutoMigrate() error {
|
||||
models := []interface{}{
|
||||
&TabUser{},
|
||||
&TabUserGroups{},
|
||||
&TabUserGroupBinds{},
|
||||
&TabUserInfo{},
|
||||
&TabCookie{},
|
||||
&TabFileInfo{},
|
||||
&APIRequestLog{},
|
||||
&TabPurchaseOrder{},
|
||||
&TabPurchaseCosts{},
|
||||
}
|
||||
|
||||
if err := DB.AutoMigrate(models...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TabUser 用户表
|
||||
type TabUser struct {
|
||||
ID uint `gorm:"primarykey;autoIncrement"`
|
||||
Name string `gorm:"type:varchar(64);uniqueIndex"`
|
||||
}
|
||||
|
||||
// TabUserGroups 用户组表
|
||||
type TabUserGroups struct {
|
||||
ID uint `gorm:"primarykey;autoIncrement"`
|
||||
Name string `gorm:"type:varchar(64);uniqueIndex"`
|
||||
}
|
||||
|
||||
// TabUserGroupBinds 用户-组绑定关系表
|
||||
type TabUserGroupBinds struct {
|
||||
UserID uint `gorm:"index"`
|
||||
GroupID uint `gorm:"index"`
|
||||
}
|
||||
|
||||
// TabUserInfo 用户详情表
|
||||
type TabUserInfo struct {
|
||||
UserID uint `gorm:"primaryKey"`
|
||||
AvatarPath string `gorm:"type:text"`
|
||||
Birthdate string `gorm:"type:varchar(16)"`
|
||||
Gender int
|
||||
Introduction string `gorm:"type:text"`
|
||||
}
|
||||
|
||||
// TabCookie Session Cookie表
|
||||
type TabCookie struct {
|
||||
Value string `gorm:"primaryKey;type:varchar(64)"`
|
||||
UserID uint `gorm:"index"`
|
||||
ExpiresAt int64
|
||||
CreateAt int64
|
||||
Remember bool
|
||||
}
|
||||
|
||||
// TabFileInfo 文件信息表
|
||||
type TabFileInfo struct {
|
||||
ID uint `gorm:"primarykey;autoIncrement"`
|
||||
Path string `gorm:"type:text"`
|
||||
Hash string `gorm:"index"`
|
||||
Size int64
|
||||
CreateTime int64
|
||||
ExtName string `gorm:"type:varchar(16)"`
|
||||
MimeType string `gorm:"type:varchar(128)"`
|
||||
StoreType int // 1=image 2=video 3=music 4=pdf 5=other
|
||||
}
|
||||
|
||||
// APIRequestLog API请求日志表
|
||||
type APIRequestLog struct {
|
||||
ID uint `gorm:"primarykey;autoIncrement"`
|
||||
Time int64 `gorm:"index"`
|
||||
IP string `gorm:"type:varchar(64)"`
|
||||
Path string `gorm:"type:varchar(255)"`
|
||||
Method string `gorm:"type:varchar(16)"`
|
||||
Status int
|
||||
UserID uint
|
||||
UserType int
|
||||
DataSize int
|
||||
}
|
||||
|
||||
// TabPurchaseOrder 采购订单表
|
||||
type TabPurchaseOrder struct {
|
||||
ID uint `gorm:"primarykey;autoIncrement"`
|
||||
Title string `gorm:"type:varchar(255)"`
|
||||
CreateTime int64 `gorm:"index"`
|
||||
CompleteTime int64
|
||||
Status int // 状态:0=进行中 1=已完成 2=已取消
|
||||
CourierNum string `gorm:"type:text"` // 快递单号
|
||||
Photos string `gorm:"type:text"` // 照片JSON数组
|
||||
Creater uint `gorm:"index"` // 创建者ID
|
||||
Remark string `gorm:"type:text"` // 备注
|
||||
}
|
||||
|
||||
// TabPurchaseCosts 采购费用明细表
|
||||
type TabPurchaseCosts struct {
|
||||
ID uint `gorm:"primarykey;autoIncrement"`
|
||||
OrderID uint `gorm:"index"`
|
||||
Name string `gorm:"type:varchar(255)"`
|
||||
PricePerUnit string `gorm:"type:varchar(32)"`
|
||||
Quantity string `gorm:"type:varchar(32)"`
|
||||
Unit string `gorm:"type:varchar(32)"`
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ops/internal/service"
|
||||
"ops/pkg/response"
|
||||
)
|
||||
|
||||
// AuthHandler 用户认证处理器
|
||||
type AuthHandler struct {
|
||||
authService *service.AuthService
|
||||
validate *validator.Validate
|
||||
}
|
||||
|
||||
// LoginRequest 登录请求结构
|
||||
type LoginRequest struct {
|
||||
Name string `json:"name" binding:"required,min=3,max=50"`
|
||||
Password string `json:"password" binding:"required,min=6,max=50"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
IP string `json:"ip"`
|
||||
Remember string `json:"remember"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应结构
|
||||
type LoginResponse struct {
|
||||
UserID uint `json:"userID"`
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatarURL"`
|
||||
CookieValue string `json:"cookieValue"`
|
||||
CookieExpireDate string `json:"cookieExpireDate"`
|
||||
}
|
||||
|
||||
// RegisterRequest 注册请求结构
|
||||
type RegisterRequest struct {
|
||||
Name string `json:"name" binding:"required,min=3,max=50"`
|
||||
Password string `json:"password" binding:"required,min=6,max=50"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Phone string `json:"phone" binding:"omitempty,len=11"`
|
||||
}
|
||||
|
||||
// RegisterResponse 注册响应结构
|
||||
type RegisterResponse struct {
|
||||
UserID uint `json:"userID"`
|
||||
Name string `json:"name"`
|
||||
CookieValue string `json:"cookieValue"`
|
||||
}
|
||||
|
||||
// ForgotPasswordRequest 忘记密码请求
|
||||
type ForgotPasswordRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Phone string `json:"phone" binding:"omitempty,len=11"`
|
||||
}
|
||||
|
||||
// ResetPasswordRequest 重置密码请求
|
||||
type ResetPasswordRequest struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
NewPassword string `json:"newPassword" binding:"required,min=6,max=50"`
|
||||
}
|
||||
|
||||
// LogoutRequest 退出登录请求
|
||||
type LogoutRequest struct {
|
||||
CookieValue string `json:"cookieValue" binding:"required"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
}
|
||||
|
||||
// NewAuthHandler 创建认证处理器
|
||||
func NewAuthHandler(db *gorm.DB) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: service.NewAuthService(db),
|
||||
validate: validator.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// UserLogin 用户登录
|
||||
func (h *AuthHandler) UserLogin(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
|
||||
// 绑定和验证请求
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request format")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证请求参数
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 调用服务层
|
||||
user, cookie, err := h.authService.Login(req.Name, req.Password, req.DeviceID, req.IP, req.Remember)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.Error(c, "-5", "User not found")
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "password") {
|
||||
response.Error(c, "-42", "Invalid password")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
resp := LoginResponse{
|
||||
UserID: user.UserID,
|
||||
Name: user.Name,
|
||||
AvatarURL: user.AvatarURL,
|
||||
CookieValue: cookie.Value,
|
||||
CookieExpireDate: cookie.ExpireDate.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
response.Success(c, resp)
|
||||
}
|
||||
|
||||
// UserRegister 用户注册
|
||||
func (h *AuthHandler) UserRegister(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
|
||||
// 绑定和验证请求
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request format")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证请求参数
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 调用服务层
|
||||
user, cookie, err := h.authService.Register(req.Name, req.Password, req.Email, req.Phone)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") {
|
||||
response.Error(c, "-4", "Username already exists")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
resp := RegisterResponse{
|
||||
UserID: user.UserID,
|
||||
Name: user.Name,
|
||||
CookieValue: cookie.Value,
|
||||
}
|
||||
|
||||
response.Success(c, resp)
|
||||
}
|
||||
|
||||
// UserForgotPassword 忘记密码
|
||||
func (h *AuthHandler) UserForgotPassword(c *gin.Context) {
|
||||
var req ForgotPasswordRequest
|
||||
|
||||
// 绑定和验证请求
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request format")
|
||||
return
|
||||
}
|
||||
|
||||
// 至少需要邮箱或手机号之一
|
||||
if req.Email == "" && req.Phone == "" {
|
||||
response.BadRequest(c, "Email or phone number is required")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证请求参数
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 调用服务层
|
||||
token, err := h.authService.ForgotPassword(req.Name, req.Email, req.Phone)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.Error(c, "-5", "User not found")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response.Success(c, gin.H{
|
||||
"resetToken": token,
|
||||
"message": "Password reset instructions have been sent",
|
||||
})
|
||||
}
|
||||
|
||||
// UserResetPassword 重置密码
|
||||
func (h *AuthHandler) UserResetPassword(c *gin.Context) {
|
||||
var req ResetPasswordRequest
|
||||
|
||||
// 绑定和验证请求
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request format")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证请求参数
|
||||
if err := h.validate.Struct(req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 调用服务层
|
||||
err := h.authService.ResetPassword(req.Token, req.NewPassword)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "invalid") || strings.Contains(err.Error(), "expired") {
|
||||
response.Error(c, "-2", "Reset token is invalid or expired")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.Error(c, "-5", "User not found")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "Password has been reset successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UserLogout 用户退出登录
|
||||
func (h *AuthHandler) UserLogout(c *gin.Context) {
|
||||
var req LogoutRequest
|
||||
|
||||
// 从认证中间件获取cookie值
|
||||
cookieValue := getCookieFromContext(c)
|
||||
if cookieValue == "" {
|
||||
// 尝试从请求body获取
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request format")
|
||||
return
|
||||
}
|
||||
cookieValue = req.CookieValue
|
||||
}
|
||||
|
||||
if cookieValue == "" {
|
||||
response.BadRequest(c, "Cookie value is required")
|
||||
return
|
||||
}
|
||||
|
||||
// 从请求中获取设备ID
|
||||
deviceID := c.GetHeader("X-Device-ID")
|
||||
if deviceID == "" && req.DeviceID != "" {
|
||||
deviceID = req.DeviceID
|
||||
}
|
||||
|
||||
// 调用服务层
|
||||
err := h.authService.Logout(cookieValue, deviceID)
|
||||
if err != nil {
|
||||
response.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"message": "Logged out successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UserProfile 获取用户信息
|
||||
func (h *AuthHandler) UserProfile(c *gin.Context) {
|
||||
// 从认证中间件获取用户ID或名称
|
||||
userID := getUserIDFromContext(c)
|
||||
if userID == 0 {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用服务层
|
||||
user, err := h.authService.GetProfile(userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.Error(c, "-5", "User not found")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, user)
|
||||
}
|
||||
|
||||
// UserUpdateProfile 更新用户信息
|
||||
func (h *AuthHandler) UserUpdateProfile(c *gin.Context) {
|
||||
// 从认证中间件获取用户ID
|
||||
userID := getUserIDFromContext(c)
|
||||
if userID == 0 {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析更新请求
|
||||
var updateData map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updateData); err != nil {
|
||||
response.BadRequest(c, "Invalid request format")
|
||||
return
|
||||
}
|
||||
|
||||
// 禁止更新某些字段
|
||||
delete(updateData, "id")
|
||||
delete(updateData, "name")
|
||||
delete(updateData, "password")
|
||||
delete(updateData, "createdAt")
|
||||
|
||||
// 调用服务层
|
||||
user, err := h.authService.UpdateProfile(userID, updateData)
|
||||
if err != nil {
|
||||
response.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, user)
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func getCookieFromContext(c *gin.Context) string {
|
||||
if cookie, exists := c.Get("userCookieValue"); exists && cookie != "" {
|
||||
return cookie.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getUserIDFromContext(c *gin.Context) uint {
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
if id, ok := userID.(uint); ok {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"ops/internal/service"
|
||||
"ops/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FileHandler struct {
|
||||
service service.FileService
|
||||
}
|
||||
|
||||
func NewFileHandler(db *gorm.DB) *FileHandler {
|
||||
return &FileHandler{
|
||||
service: service.NewFileService(db),
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFile 上传文件
|
||||
// @Summary 上传文件
|
||||
// @Description 上传文件到服务器,支持图片、文档等多种类型
|
||||
// @Tags 文件管理
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param userID header string false "用户ID" default("")
|
||||
// @Param file formData file true "文件内容"
|
||||
// @Param type formData string false "文件类型" default(image)
|
||||
// @Param description formData string false "文件描述"
|
||||
// @Success 200 {object} response.StandardResponse "成功"
|
||||
// @Failure 400 {object} response.StandardResponse "参数错误"
|
||||
// @Failure 401 {object} response.StandardResponse "未授权"
|
||||
// @Failure 413 {object} response.StandardResponse "文件过大"
|
||||
// @Failure 415 {object} response.StandardResponse "文件类型不支持"
|
||||
// @Failure 500 {object} response.StandardResponse "服务器错误"
|
||||
// @Router /api/v1/files/upload [post]
|
||||
func (h *FileHandler) UploadFile(c *gin.Context) {
|
||||
// 从上下文中获取用户ID
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件类型参数
|
||||
fileType := c.PostForm("type")
|
||||
if fileType == "" {
|
||||
fileType = "image" // 默认类型为图片
|
||||
}
|
||||
|
||||
// 获取文件描述
|
||||
description := c.PostForm("description")
|
||||
|
||||
// 获取上传的文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "请选择要上传的文件")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Service上传文件
|
||||
uploadResponse, success := h.service.UploadFile(c, userID.(uint), file, fileType, description)
|
||||
if !success {
|
||||
response.BadRequest(c, "文件上传失败,请检查文件格式和大小")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, uploadResponse)
|
||||
}
|
||||
|
||||
// GetFileList 获取文件列表
|
||||
// @Summary 获取文件列表
|
||||
// @Description 获取当前用户上传的文件列表
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param userID header string false "用户ID" default("")
|
||||
// @Param type query string false "文件类型过滤"
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param entries query int false "每页数量" default(20)
|
||||
// @Success 200 {object} response.StandardResponse "成功"
|
||||
// @Failure 400 {object} response.StandardResponse "参数错误"
|
||||
// @Failure 401 {object} response.StandardResponse "未授权"
|
||||
// @Router /api/v1/files/list [get]
|
||||
func (h *FileHandler) GetFileList(c *gin.Context) {
|
||||
// 从上下文中获取用户ID
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
fileType := c.Query("type")
|
||||
page := GetIntParam(c, "page", 1)
|
||||
entries := GetIntParam(c, "entries", 20)
|
||||
|
||||
// 调用Service获取文件列表
|
||||
fileListResponse, success := h.service.GetFileList(userID.(uint), fileType, page, entries)
|
||||
if !success {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, fileListResponse)
|
||||
}
|
||||
|
||||
// GetFileByID 获取文件信息
|
||||
// @Summary 获取文件信息
|
||||
// @Description 根据文件ID获取文件详细信息
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param userID header string false "用户ID" default("")
|
||||
// @Param id path int true "文件ID"
|
||||
// @Success 200 {object} response.StandardResponse "成功"
|
||||
// @Failure 401 {object} response.StandardResponse "未授权"
|
||||
// @Failure 404 {object} response.StandardResponse "文件不存在"
|
||||
// @Router /api/v1/files/{id} [get]
|
||||
func (h *FileHandler) GetFileByID(c *gin.Context) {
|
||||
// 从上下文中获取用户ID
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件ID
|
||||
fileID := GetUintParam(c, "id")
|
||||
if fileID == 0 {
|
||||
response.BadRequest(c, "文件ID无效")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Service获取文件信息
|
||||
file, success := h.service.GetFileByID(fileID, userID.(uint))
|
||||
if !success {
|
||||
response.Error(c, "-100", "文件不存在或无权限访问")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"file_id": file.ID,
|
||||
"name": file.Name,
|
||||
"sha256": file.Sha256,
|
||||
"mime": file.Mime,
|
||||
"type": file.Type,
|
||||
"size": file.Const, // 注意:这里const字段实际上存储的是使用次数,需要确认实际字段
|
||||
"created_at": file.Date.Format("2006-01-02T15:04:05Z"),
|
||||
"path": file.Path,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件
|
||||
// @Summary 删除文件
|
||||
// @Description 删除用户上传的文件
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param userID header string false "用户ID" default("")
|
||||
// @Param id path int true "文件ID"
|
||||
// @Success 200 {object} response.StandardResponse "成功"
|
||||
// @Failure 401 {object} response.StandardResponse "未授权"
|
||||
// @Failure 404 {object} response.StandardResponse "文件不存在"
|
||||
// @Router /api/v1/files/{id} [delete]
|
||||
func (h *FileHandler) DeleteFile(c *gin.Context) {
|
||||
// 从上下文中获取用户ID
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件ID
|
||||
fileID := GetUintParam(c, "id")
|
||||
if fileID == 0 {
|
||||
response.BadRequest(c, "文件ID无效")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Service删除文件
|
||||
success := h.service.DeleteFile(fileID, userID.(uint))
|
||||
if !success {
|
||||
response.Error(c, "-100", "文件删除失败,文件不存在或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "文件删除成功"})
|
||||
}
|
||||
|
||||
// DownloadFile 下载文件
|
||||
// @Summary 下载文件
|
||||
// @Description 下载文件内容(直接下载)
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce application/octet-stream
|
||||
// @Param hash path string true "文件SHA256哈希值"
|
||||
// @Success 200 {file} binary "文件内容"
|
||||
// @Failure 404 {object} response.StandardResponse "文件不存在"
|
||||
// @Router /api/v1/files/download/{hash} [get]
|
||||
func (h *FileHandler) DownloadFile(c *gin.Context) {
|
||||
hash := c.Param("hash")
|
||||
if hash == "" {
|
||||
response.BadRequest(c, "文件哈希值无效")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Service下载文件
|
||||
success := h.service.DownloadFile(c, hash, true)
|
||||
if !success {
|
||||
response.Error(c, "-100", "文件不存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetFile 获取文件(预览)
|
||||
// @Summary 获取文件(预览)
|
||||
// @Description 获取文件内容(浏览器预览)
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce *
|
||||
// @Param hash path string true "文件SHA256哈希值"
|
||||
// @Success 200 {file} binary "文件内容"
|
||||
// @Failure 404 {object} response.StandardResponse "文件不存在"
|
||||
// @Router /api/v1/files/get/{hash} [get]
|
||||
func (h *FileHandler) GetFile(c *gin.Context) {
|
||||
hash := c.Param("hash")
|
||||
if hash == "" {
|
||||
response.BadRequest(c, "文件哈希值无效")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Service获取文件(预览模式)
|
||||
success := h.service.DownloadFile(c, hash, false)
|
||||
if !success {
|
||||
response.Error(c, "-100", "文件不存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"ops/internal/service"
|
||||
"ops/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseHandler struct {
|
||||
service service.PurchaseService
|
||||
}
|
||||
|
||||
func NewPurchaseHandler(db *gorm.DB) *PurchaseHandler {
|
||||
return &PurchaseHandler{
|
||||
service: service.NewPurchaseService(db),
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrders 获取采购订单列表
|
||||
// @Summary 获取采购订单列表
|
||||
// @Description 获取用户采购订单列表,支持搜索和分页
|
||||
// @Tags 采购管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param userID header string false "用户ID" default("")
|
||||
// @Param search query string false "搜索关键词"
|
||||
// @Param page query int true "页码" default(1)
|
||||
// @Param entries query int true "每页数量" default(20)
|
||||
// @Success 200 {object} response.StandardResponse "成功"
|
||||
// @Failure 400 {object} response.StandardResponse "参数错误"
|
||||
// @Failure 401 {object} response.StandardResponse "未授权"
|
||||
// @Failure 500 {object} response.StandardResponse "服务器错误"
|
||||
// @Router /api/v1/purchase/orders [get]
|
||||
func (h *PurchaseHandler) GetOrders(c *gin.Context) {
|
||||
// 从上下文中获取用户ID
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
search := c.Query("search")
|
||||
page := GetIntParam(c, "page", 1)
|
||||
entries := GetIntParam(c, "entries", 20)
|
||||
|
||||
// 调用Service
|
||||
result, success := h.service.GetOrders(c, userID.(uint), search, page, entries)
|
||||
if !success {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// CreateOrder 创建采购订单
|
||||
// @Summary 创建采购订单
|
||||
// @Description 创建新的采购订单
|
||||
// @Tags 采购管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param userID header string false "用户ID" default("")
|
||||
// @Param request body service.CreateOrderRequest true "订单信息"
|
||||
// @Success 200 {object} response.StandardResponse "成功"
|
||||
// @Failure 400 {object} response.StandardResponse "参数错误"
|
||||
// @Failure 401 {object} response.StandardResponse "未授权"
|
||||
// @Failure 500 {object} response.StandardResponse "服务器错误"
|
||||
// @Router /api/v1/purchase/orders [post]
|
||||
func (h *PurchaseHandler) CreateOrder(c *gin.Context) {
|
||||
// 从上下文中获取用户ID
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析请求体
|
||||
var request service.CreateOrderRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
var validationErrors []string
|
||||
for _, err := range err.(validator.ValidationErrors) {
|
||||
validationErrors = append(validationErrors, err.Field()+" "+err.Tag())
|
||||
}
|
||||
if len(validationErrors) > 0 {
|
||||
response.BadRequest(c, "参数错误: "+validationErrors[0])
|
||||
} else {
|
||||
response.BadRequest(c, "请求格式错误")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Service
|
||||
success := h.service.CreateOrder(c, userID.(uint), request)
|
||||
if !success {
|
||||
response.BadRequest(c, "创建订单失败,请检查数据")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "订单创建成功"})
|
||||
}
|
||||
|
||||
// GetOrderDetails 获取订单详情
|
||||
// @Summary 获取订单详情
|
||||
// @Description 获取采购订单的详细信息
|
||||
// @Tags 采购管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param userID header string false "用户ID" default("")
|
||||
// @Param id path int true "订单ID"
|
||||
// @Success 200 {object} response.StandardResponse "成功"
|
||||
// @Failure 401 {object} response.StandardResponse "未授权"
|
||||
// @Failure 404 {object} response.StandardResponse "订单不存在"
|
||||
// @Failure 500 {object} response.StandardResponse "服务器错误"
|
||||
// @Router /api/v1/purchase/orders/{id} [get]
|
||||
func (h *PurchaseHandler) GetOrderDetails(c *gin.Context) {
|
||||
// 从上下文中获取用户ID
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取订单ID
|
||||
orderID := GetUintParam(c, "id")
|
||||
if orderID == 0 {
|
||||
response.BadRequest(c, "订单ID无效")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Service
|
||||
order, costs, err := h.service.GetOrderDetails(orderID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
response.Error(c, "-5", "订单不存在")
|
||||
} else {
|
||||
response.InternalError(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查订单所属用户
|
||||
if order.UserID != userID.(uint) {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"order": order,
|
||||
"costs": costs,
|
||||
})
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetIntParam 获取整数参数
|
||||
func GetIntParam(c *gin.Context, key string, defaultValue int) int {
|
||||
value := c.Query(key)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return intValue
|
||||
}
|
||||
|
||||
// GetUintParam 获取uint参数
|
||||
func GetUintParam(c *gin.Context, key string) uint {
|
||||
value := c.Param(key)
|
||||
if value == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return uint(intValue)
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AuthToken 认证令牌中间件
|
||||
// 兼容现有的 userCookieValue 字段
|
||||
func AuthToken() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 尝试从请求头获取认证
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
|
||||
// 如果没有Authorization头,尝试从POST数据中获取
|
||||
if authHeader == "" && c.Request.Method == http.MethodPost {
|
||||
var requestData map[string]interface{}
|
||||
|
||||
// 尝试解析JSON body
|
||||
if c.Request.Body != nil && c.Request.ContentLength > 0 {
|
||||
// 先读取请求体内容
|
||||
requestBody, err := io.ReadAll(c.Request.Body)
|
||||
if err == nil {
|
||||
// 重置body以便后续使用
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
|
||||
// 尝试解析JSON
|
||||
if err := c.ShouldBindJSON(&requestData); err == nil {
|
||||
if cookieValue, ok := requestData["userCookieValue"].(string); ok && cookieValue != "" {
|
||||
c.Set("userCookieValue", cookieValue)
|
||||
c.Set("authMethod", "cookie_value")
|
||||
c.Set("authValid", true)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
// 如果JSON解析失败,重置body并继续
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从表单数据获取
|
||||
if cookieValue := c.PostForm("userCookieValue"); cookieValue != "" {
|
||||
c.Set("userCookieValue", cookieValue)
|
||||
c.Set("authMethod", "cookie_value")
|
||||
c.Set("authValid", true)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Bearer token 认证
|
||||
if authHeader != "" && len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||
token := authHeader[7:]
|
||||
c.Set("authToken", token)
|
||||
c.Set("authMethod", "bearer_token")
|
||||
c.Set("authValid", true)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查URL查询参数中的cookie
|
||||
if cookieValue := c.Query("userCookieValue"); cookieValue != "" {
|
||||
c.Set("userCookieValue", cookieValue)
|
||||
c.Set("authMethod", "cookie_query")
|
||||
c.Set("authValid", true)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证失败
|
||||
c.Set("authValid", false)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AuthRequired 需要认证的中间件
|
||||
// 如果用户未认证,返回401错误
|
||||
func AuthRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 先运行认证中间件
|
||||
authMiddleware := AuthToken()
|
||||
authMiddleware(c)
|
||||
|
||||
// 检查认证结果
|
||||
if authValid, exists := c.Get("authValid"); !exists || !authValid.(bool) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": "401",
|
||||
"message": "Authentication required",
|
||||
"data": nil,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AdminRequired 需要管理员权限的中间件
|
||||
func AdminRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 先进行基础认证
|
||||
AuthRequired()(c)
|
||||
|
||||
// 如果请求被中止(认证失败),直接返回
|
||||
if c.IsAborted() {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 检查用户是否为管理员
|
||||
// 暂时允许所有已认证用户
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuthMethod 获取认证方法
|
||||
func GetAuthMethod(c *gin.Context) string {
|
||||
if method, exists := c.Get("authMethod"); exists {
|
||||
return method.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetCookieValue 获取用户cookie值
|
||||
func GetCookieValue(c *gin.Context) string {
|
||||
if cookie, exists := c.Get("userCookieValue"); exists {
|
||||
return cookie.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetAuthToken 获取Bearer token
|
||||
func GetAuthToken(c *gin.Context) string {
|
||||
if token, exists := c.Get("authToken"); exists {
|
||||
return token.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CORS 跨域资源共享中间件
|
||||
func CORS() gin.HandlerFunc {
|
||||
return cors.New(cors.Config{
|
||||
// 允许所有来源(生产环境应指定具体域名)
|
||||
AllowOrigins: []string{"*"},
|
||||
|
||||
// 允许的方法
|
||||
AllowMethods: []string{
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"PATCH",
|
||||
"OPTIONS",
|
||||
},
|
||||
|
||||
// 允许的请求头
|
||||
AllowHeaders: []string{
|
||||
"Origin",
|
||||
"Content-Type",
|
||||
"Content-Length",
|
||||
"Accept-Encoding",
|
||||
"X-CSRF-Token",
|
||||
"Authorization",
|
||||
"X-Request-ID",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Cache-Control",
|
||||
// 自定义头
|
||||
"User-Cookie-Value", // 兼容现有系统
|
||||
},
|
||||
|
||||
// 暴露的响应头
|
||||
ExposeHeaders: []string{
|
||||
"Content-Length",
|
||||
"Authorization",
|
||||
"X-Request-ID",
|
||||
"Content-Disposition",
|
||||
},
|
||||
|
||||
// 是否允许携带凭证
|
||||
AllowCredentials: true,
|
||||
|
||||
// 预检请求缓存时间(秒)
|
||||
MaxAge: 12 * time.Hour,
|
||||
|
||||
// 允许读取自定义头
|
||||
AllowPrivateNetwork: true,
|
||||
})
|
||||
}
|
||||
|
||||
// CORSMiddleware 简化的CORS中间件(兼容老版本)
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
origin = "*"
|
||||
}
|
||||
|
||||
// 设置CORS头
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", 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, X-Request-ID, X-Requested-With, Accept, Cache-Control, User-Cookie-Value")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// LogResponseWriter 自定义ResponseWriter以捕获响应内容
|
||||
type LogResponseWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *LogResponseWriter) Write(b []byte) (int, error) {
|
||||
if w.body != nil {
|
||||
w.body.Write(b)
|
||||
}
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func (w *LogResponseWriter) WriteString(s string) (int, error) {
|
||||
if w.body != nil {
|
||||
w.body.WriteString(s)
|
||||
}
|
||||
return w.ResponseWriter.WriteString(s)
|
||||
}
|
||||
|
||||
// Logger 请求日志中间件
|
||||
func Logger(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 开始时间
|
||||
startTime := time.Now()
|
||||
|
||||
// 请求方法
|
||||
httpMethod := c.Request.Method
|
||||
|
||||
// 请求路径
|
||||
reqUri := c.Request.RequestURI
|
||||
|
||||
// 客户端IP
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
// 用户代理
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
// 请求ID
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = generateRequestID()
|
||||
c.Set("requestID", requestID)
|
||||
} else {
|
||||
c.Set("requestID", requestID)
|
||||
}
|
||||
|
||||
// 记录原始请求体(如果不是文件上传等大请求)
|
||||
var requestBody []byte
|
||||
if c.Request.ContentLength > 0 && c.Request.ContentLength < 1024*1024 && // 1MB限制
|
||||
c.Request.Header.Get("Content-Type") != "multipart/form-data" {
|
||||
// 读取请求体
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err == nil {
|
||||
requestBody = bodyBytes
|
||||
// 重置请求体以便后续使用
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
// 尝试解析JSON
|
||||
var jsonBody interface{}
|
||||
if err := json.Unmarshal(bodyBytes, &jsonBody); err == nil {
|
||||
// 敏感信息过滤(如密码)
|
||||
if m, ok := jsonBody.(map[string]interface{}); ok {
|
||||
if _, exists := m["password"]; exists {
|
||||
m["password"] = "***REDACTED***"
|
||||
}
|
||||
if _, exists := m["oldPassword"]; exists {
|
||||
m["oldPassword"] = "***REDACTED***"
|
||||
}
|
||||
if _, exists := m["newPassword"]; exists {
|
||||
m["newPassword"] = "***REDACTED***"
|
||||
}
|
||||
if _, exists := m["confirmPassword"]; exists {
|
||||
m["confirmPassword"] = "***REDACTED***"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 包装ResponseWriter以捕获响应
|
||||
blw := &LogResponseWriter{
|
||||
ResponseWriter: c.Writer,
|
||||
body: bytes.NewBufferString(""),
|
||||
}
|
||||
c.Writer = blw
|
||||
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
// 结束时间
|
||||
endTime := time.Now()
|
||||
|
||||
// 执行时间
|
||||
latency := endTime.Sub(startTime)
|
||||
|
||||
// 响应状态码
|
||||
statusCode := c.Writer.Status()
|
||||
|
||||
// 错误信息
|
||||
errors := c.Errors.ByType(gin.ErrorTypePrivate).String()
|
||||
if errors == "" {
|
||||
errors = c.Errors.ByType(gin.ErrorTypePublic).String()
|
||||
}
|
||||
|
||||
// 响应体(如果不是文件等大型响应)
|
||||
var responseBody interface{}
|
||||
var responseMap map[string]interface{}
|
||||
if blw.body != nil && blw.body.Len() > 0 && blw.body.Len() < 10000 { // 10KB限制
|
||||
bodyBytes := blw.body.Bytes()
|
||||
if err := json.Unmarshal(bodyBytes, &responseMap); err == nil {
|
||||
responseBody = responseMap
|
||||
} else {
|
||||
responseBody = string(bodyBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态码决定日志级别
|
||||
fields := []zap.Field{
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("method", httpMethod),
|
||||
zap.String("uri", reqUri),
|
||||
zap.String("client_ip", clientIP),
|
||||
zap.String("user_agent", userAgent),
|
||||
zap.Int("status", statusCode),
|
||||
zap.Duration("latency", latency),
|
||||
}
|
||||
|
||||
// 添加请求体(如果存在且不是太大)
|
||||
if len(requestBody) > 0 && len(requestBody) < 10000 {
|
||||
var reqBody interface{}
|
||||
if err := json.Unmarshal(requestBody, &reqBody); err == nil {
|
||||
fields = append(fields, zap.Any("request_body", reqBody))
|
||||
}
|
||||
}
|
||||
|
||||
// 添加响应体(如果存在且不是太大)
|
||||
if responseBody != nil {
|
||||
fields = append(fields, zap.Any("response_body", responseBody))
|
||||
}
|
||||
|
||||
// 添加错误信息
|
||||
if errors != "" {
|
||||
fields = append(fields, zap.String("error", errors))
|
||||
}
|
||||
|
||||
// 获取用户标识(如果有)
|
||||
if cookieValue := GetCookieValue(c); cookieValue != "" {
|
||||
fields = append(fields, zap.String("auth_cookie_truncated", truncateString(cookieValue, 8)))
|
||||
}
|
||||
if authToken := GetAuthToken(c); authToken != "" {
|
||||
fields = append(fields, zap.String("auth_token_truncated", truncateString(authToken, 8)))
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
logFunc := logger.Info
|
||||
if statusCode >= 400 && statusCode < 500 {
|
||||
logFunc = logger.Warn
|
||||
} else if statusCode >= 500 {
|
||||
logFunc = logger.Error
|
||||
}
|
||||
|
||||
logFunc("HTTP request", fields...)
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery 恢复中间件
|
||||
func Recovery(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// 获取请求ID
|
||||
requestID, _ := c.Get("requestID")
|
||||
|
||||
// 记录Panic
|
||||
logger.Error("HTTP panic recovered",
|
||||
zap.Any("error", err),
|
||||
zap.String("request_id", requestID.(string)),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("uri", c.Request.RequestURI),
|
||||
zap.String("client_ip", c.ClientIP()),
|
||||
)
|
||||
|
||||
// 返回500错误
|
||||
c.JSON(500, gin.H{
|
||||
"code": "500",
|
||||
"message": "Internal server error",
|
||||
"data": nil,
|
||||
})
|
||||
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func generateRequestID() string {
|
||||
return time.Now().Format("20060102150405") + "-" + shortRandString()
|
||||
}
|
||||
|
||||
func truncateString(s string, length int) string {
|
||||
if len(s) <= length {
|
||||
return s
|
||||
}
|
||||
return s[:length] + "..."
|
||||
}
|
||||
|
||||
func shortRandString() string {
|
||||
// 简化的随机字符串生成
|
||||
return time.Now().Format("150405")
|
||||
}
|
||||
|
||||
// SimpleLogger 简易日志中间件(用于开发和测试)
|
||||
func SimpleLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
// 记录请求信息
|
||||
latency := time.Since(start)
|
||||
clientIP := c.ClientIP()
|
||||
method := c.Request.Method
|
||||
statusCode := c.Writer.Status()
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// 输出到控制台
|
||||
fmt.Printf("[GIN] %v | %3d | %13v | %15s | %-7s %s\n",
|
||||
time.Now().Format("2006/01/02 - 15:04:05"),
|
||||
statusCode,
|
||||
latency,
|
||||
clientIP,
|
||||
method,
|
||||
path,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"ops/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FileRepository interface {
|
||||
CreateFile(file *models.TabFileInfo_) error
|
||||
GetFileByID(fileID uint) (*models.TabFileInfo_, error)
|
||||
GetFileByHash(hash string) (*models.TabFileInfo_, error)
|
||||
GetFilesByUser(userID uint, fileType string, page, entries int) ([]models.TabFileInfo_, int64, error)
|
||||
UpdateFile(file *models.TabFileInfo_) error
|
||||
DeleteFile(fileID uint) error
|
||||
IncrementFileUsage(fileID uint) error
|
||||
GetFilesByType(fileType string, limit int) ([]models.TabFileInfo_, error)
|
||||
}
|
||||
|
||||
type fileRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewFileRepository(db *gorm.DB) FileRepository {
|
||||
return &fileRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *fileRepository) CreateFile(file *models.TabFileInfo_) error {
|
||||
return r.db.Create(file).Error
|
||||
}
|
||||
|
||||
func (r *fileRepository) GetFileByID(fileID uint) (*models.TabFileInfo_, error) {
|
||||
var file models.TabFileInfo_
|
||||
if err := r.db.First(&file, fileID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) GetFileByHash(hash string) (*models.TabFileInfo_, error) {
|
||||
var file models.TabFileInfo_
|
||||
if err := r.db.Where("sha256 = ?", hash).First(&file).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) GetFilesByUser(userID uint, fileType string, page, entries int) ([]models.TabFileInfo_, int64, error) {
|
||||
var files []models.TabFileInfo_
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.TabFileInfo_{}).Where("user_id = ?", userID)
|
||||
|
||||
if fileType != "" {
|
||||
query = query.Where("type = ?", fileType)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := entries * (page - 1)
|
||||
if err := query.Order("date DESC").Offset(offset).Limit(entries).Find(&files).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return files, total, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) UpdateFile(file *models.TabFileInfo_) error {
|
||||
return r.db.Save(file).Error
|
||||
}
|
||||
|
||||
func (r *fileRepository) DeleteFile(fileID uint) error {
|
||||
return r.db.Delete(&models.TabFileInfo_{}, fileID).Error
|
||||
}
|
||||
|
||||
func (r *fileRepository) IncrementFileUsage(fileID uint) error {
|
||||
return r.db.Model(&models.TabFileInfo_{}).
|
||||
Where("id = ?", fileID).
|
||||
Update("const", gorm.Expr("const + ?", 1)).Error
|
||||
}
|
||||
|
||||
func (r *fileRepository) GetFilesByType(fileType string, limit int) ([]models.TabFileInfo_, error) {
|
||||
var files []models.TabFileInfo_
|
||||
query := r.db.Where("type = ?", fileType).Order("const DESC")
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
|
||||
if err := query.Find(&files).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"ops/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseRepository interface {
|
||||
GetOrders(userID uint, search string, page, entries int) ([]models.TabPurchaseOrder, int64, error)
|
||||
GetOrderByID(orderID uint) (*models.TabPurchaseOrder, error)
|
||||
CreateOrder(order *models.TabPurchaseOrder) error
|
||||
CreateCost(cost *models.TabPurchaseCosts) error
|
||||
GetOrderCosts(orderID uint) ([]models.TabPurchaseCosts, error)
|
||||
}
|
||||
|
||||
type purchaseRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
|
||||
return &purchaseRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *purchaseRepository) GetOrders(userID uint, search string, page, entries int) ([]models.TabPurchaseOrder, int64, error) {
|
||||
var orders []models.TabPurchaseOrder
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.TabPurchaseOrder{}).Where("user_id = ?", userID)
|
||||
|
||||
if search != "" {
|
||||
query = query.Where("title LIKE ? OR part_name LIKE ? OR remark LIKE ? OR tracking_number LIKE ?",
|
||||
"%"+search+"%", "%"+search+"%", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
offset := entries * (page - 1)
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(entries).Find(&orders).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return orders, total, nil
|
||||
}
|
||||
|
||||
func (r *purchaseRepository) GetOrderByID(orderID uint) (*models.TabPurchaseOrder, error) {
|
||||
var order models.TabPurchaseOrder
|
||||
if err := r.db.First(&order, orderID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (r *purchaseRepository) CreateOrder(order *models.TabPurchaseOrder) error {
|
||||
return r.db.Create(order).Error
|
||||
}
|
||||
|
||||
func (r *purchaseRepository) CreateCost(cost *models.TabPurchaseCosts) error {
|
||||
return r.db.Create(cost).Error
|
||||
}
|
||||
|
||||
func (r *purchaseRepository) GetOrderCosts(orderID uint) ([]models.TabPurchaseCosts, error) {
|
||||
var costs []models.TabPurchaseCosts
|
||||
if err := r.db.Where("order_id = ?", orderID).Find(&costs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return costs, nil
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ops/internal/database"
|
||||
)
|
||||
|
||||
// UserRepository 用户数据访问接口
|
||||
type UserRepository interface {
|
||||
Create(user *database.TabUser) error
|
||||
FindByID(id uint) (*database.TabUser, error)
|
||||
FindByName(name string) (*database.TabUser, error)
|
||||
FindByEmail(email string) (*database.TabUser, error)
|
||||
FindByPhone(phone string) (*database.TabUser, error)
|
||||
Update(user *database.TabUser) error
|
||||
Delete(id uint) error
|
||||
ExistsByName(name string) (bool, error)
|
||||
}
|
||||
|
||||
// UserInfoRepository 用户信息数据访问接口
|
||||
type UserInfoRepository interface {
|
||||
Create(userInfo *database.TabUserInfo) error
|
||||
FindByUserID(userID uint) (*database.TabUserInfo, error)
|
||||
Update(userInfo *database.TabUserInfo) error
|
||||
Delete(userID uint) error
|
||||
}
|
||||
|
||||
// CookieRepository Cookie数据访问接口
|
||||
type CookieRepository interface {
|
||||
Create(cookie *database.TabCookie) error
|
||||
FindByValue(cookieValue string) (*database.TabCookie, error)
|
||||
FindByUserID(userID uint) ([]*database.TabCookie, error)
|
||||
DeleteByValue(cookieValue string) error
|
||||
DeleteByUserID(userID uint) error
|
||||
DeleteExpired() error
|
||||
}
|
||||
|
||||
// userRepo 用户仓库实现
|
||||
type userRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserRepository 创建用户仓库实例
|
||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||
return &userRepo{db: db}
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (r *userRepo) Create(user *database.TabUser) error {
|
||||
if user == nil {
|
||||
return errors.New("user is nil")
|
||||
}
|
||||
|
||||
if user.Name == "" {
|
||||
return errors.New("username is required")
|
||||
}
|
||||
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
// FindByID 通过ID查找用户
|
||||
func (r *userRepo) FindByID(id uint) (*database.TabUser, error) {
|
||||
if id == 0 {
|
||||
return nil, errors.New("invalid user ID")
|
||||
}
|
||||
|
||||
var user database.TabUser
|
||||
err := r.db.First(&user, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByName 通过用户名查找用户
|
||||
func (r *userRepo) FindByName(name string) (*database.TabUser, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("username is required")
|
||||
}
|
||||
|
||||
var user database.TabUser
|
||||
err := r.db.Where("name = ?", name).First(&user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByEmail 通过邮箱查找用户
|
||||
func (r *userRepo) FindByEmail(email string) (*database.TabUser, error) {
|
||||
// TabUser表目前没有email字段,这里返回nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindByPhone 通过手机号查找用户
|
||||
func (r *userRepo) FindByPhone(phone string) (*database.TabUser, error) {
|
||||
// TabUser表目前没有phone字段,这里返回nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Update 更新用户信息
|
||||
func (r *userRepo) Update(user *database.TabUser) error {
|
||||
if user == nil {
|
||||
return errors.New("user is nil")
|
||||
}
|
||||
|
||||
if user.ID == 0 {
|
||||
return errors.New("user ID is required")
|
||||
}
|
||||
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (r *userRepo) Delete(id uint) error {
|
||||
if id == 0 {
|
||||
return errors.New("invalid user ID")
|
||||
}
|
||||
|
||||
return r.db.Delete(&database.TabUser{}, id).Error
|
||||
}
|
||||
|
||||
// ExistsByName 检查用户名是否存在
|
||||
func (r *userRepo) ExistsByName(name string) (bool, error) {
|
||||
if name == "" {
|
||||
return false, errors.New("username is required")
|
||||
}
|
||||
|
||||
var count int64
|
||||
err := r.db.Model(&database.TabUser{}).Where("name = ?", name).Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// userInfoRepo 用户信息仓库实现
|
||||
type userInfoRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserInfoRepository 创建用户信息仓库实例
|
||||
func NewUserInfoRepository(db *gorm.DB) UserInfoRepository {
|
||||
return &userInfoRepo{db: db}
|
||||
}
|
||||
|
||||
// Create 创建用户信息
|
||||
func (r *userInfoRepo) Create(userInfo *database.TabUserInfo) error {
|
||||
if userInfo == nil {
|
||||
return errors.New("user info is nil")
|
||||
}
|
||||
|
||||
if userInfo.UserID == 0 {
|
||||
return errors.New("user ID is required")
|
||||
}
|
||||
|
||||
return r.db.Create(userInfo).Error
|
||||
}
|
||||
|
||||
// FindByUserID 通过用户ID查找用户信息
|
||||
func (r *userInfoRepo) FindByUserID(userID uint) (*database.TabUserInfo, error) {
|
||||
if userID == 0 {
|
||||
return nil, errors.New("invalid user ID")
|
||||
}
|
||||
|
||||
var userInfo database.TabUserInfo
|
||||
err := r.db.Where("user_id = ?", userID).First(&userInfo).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
// Update 更新用户信息
|
||||
func (r *userInfoRepo) Update(userInfo *database.TabUserInfo) error {
|
||||
if userInfo == nil {
|
||||
return errors.New("user info is nil")
|
||||
}
|
||||
|
||||
if userInfo.UserID == 0 {
|
||||
return errors.New("user ID is required")
|
||||
}
|
||||
|
||||
return r.db.Save(userInfo).Error
|
||||
}
|
||||
|
||||
// Delete 删除用户信息
|
||||
func (r *userInfoRepo) Delete(userID uint) error {
|
||||
if userID == 0 {
|
||||
return errors.New("invalid user ID")
|
||||
}
|
||||
|
||||
return r.db.Where("user_id = ?", userID).Delete(&database.TabUserInfo{}).Error
|
||||
}
|
||||
|
||||
// cookieRepo Cookie仓库实现
|
||||
type cookieRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewCookieRepository 创建Cookie仓库实例
|
||||
func NewCookieRepository(db *gorm.DB) CookieRepository {
|
||||
return &cookieRepo{db: db}
|
||||
}
|
||||
|
||||
// Create 创建Cookie
|
||||
func (r *cookieRepo) Create(cookie *database.TabCookie) error {
|
||||
if cookie == nil {
|
||||
return errors.New("cookie is nil")
|
||||
}
|
||||
|
||||
if cookie.Value == "" {
|
||||
return errors.New("cookie value is required")
|
||||
}
|
||||
|
||||
if cookie.UserID == 0 {
|
||||
return errors.New("user ID is required")
|
||||
}
|
||||
|
||||
if cookie.ExpiresAt == 0 {
|
||||
cookie.ExpiresAt = time.Now().Add(7 * 24 * time.Hour).Unix()
|
||||
}
|
||||
|
||||
if cookie.CreateAt == 0 {
|
||||
cookie.CreateAt = time.Now().Unix()
|
||||
}
|
||||
|
||||
return r.db.Create(cookie).Error
|
||||
}
|
||||
|
||||
// FindByValue 通过Cookie值查找
|
||||
func (r *cookieRepo) FindByValue(cookieValue string) (*database.TabCookie, error) {
|
||||
if cookieValue == "" {
|
||||
return nil, errors.New("cookie value is required")
|
||||
}
|
||||
|
||||
var cookie database.TabCookie
|
||||
err := r.db.Where("value = ?", cookieValue).First(&cookie).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cookie, nil
|
||||
}
|
||||
|
||||
// FindByUserID 通过用户ID查找所有Cookie
|
||||
func (r *cookieRepo) FindByUserID(userID uint) ([]*database.TabCookie, error) {
|
||||
if userID == 0 {
|
||||
return nil, errors.New("invalid user ID")
|
||||
}
|
||||
|
||||
var cookies []*database.TabCookie
|
||||
err := r.db.Where("user_id = ?", userID).Find(&cookies).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
// DeleteByValue 通过Cookie值删除
|
||||
func (r *cookieRepo) DeleteByValue(cookieValue string) error {
|
||||
if cookieValue == "" {
|
||||
return errors.New("cookie value is required")
|
||||
}
|
||||
|
||||
return r.db.Where("value = ?", cookieValue).Delete(&database.TabCookie{}).Error
|
||||
}
|
||||
|
||||
// DeleteByUserID 通过用户ID删除所有Cookie
|
||||
func (r *cookieRepo) DeleteByUserID(userID uint) error {
|
||||
if userID == 0 {
|
||||
return errors.New("invalid user ID")
|
||||
}
|
||||
|
||||
return r.db.Where("user_id = ?", userID).Delete(&database.TabCookie{}).Error
|
||||
}
|
||||
|
||||
// DeleteExpired 删除过期的Cookie
|
||||
func (r *cookieRepo) DeleteExpired() error {
|
||||
now := time.Now().Unix()
|
||||
return r.db.Where("expires_at < ?", now).Delete(&database.TabCookie{}).Error
|
||||
}
|
||||
|
||||
// EnhancedUserInfo 增强的用户信息结构
|
||||
type EnhancedUserInfo struct {
|
||||
database.TabUser
|
||||
UserInfo database.TabUserInfo
|
||||
AvatarURL string
|
||||
}
|
||||
|
||||
// GetEnhancedUserInfo 获取增强的用户信息
|
||||
func GetEnhancedUserInfo(db *gorm.DB, userID uint) (*EnhancedUserInfo, error) {
|
||||
if userID == 0 {
|
||||
return nil, errors.New("invalid user ID")
|
||||
}
|
||||
|
||||
var user database.TabUser
|
||||
var userInfo database.TabUserInfo
|
||||
|
||||
// 获取用户基本信息
|
||||
err := db.First(&user, userID).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取用户详细信息
|
||||
err = db.Where("user_id = ?", userID).First(&userInfo).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建头像URL
|
||||
avatarURL := "/static/default_avatar.png"
|
||||
if userInfo.AvatarPath != "" {
|
||||
avatarURL = "/file/" + userInfo.AvatarPath
|
||||
}
|
||||
|
||||
return &EnhancedUserInfo{
|
||||
TabUser: user,
|
||||
UserInfo: userInfo,
|
||||
AvatarURL: avatarURL,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ops/internal/database"
|
||||
"ops/internal/repository"
|
||||
)
|
||||
|
||||
// AuthService 用户认证服务结构
|
||||
type AuthService struct {
|
||||
userRepo repository.UserRepository
|
||||
userInfoRepo repository.UserInfoRepository
|
||||
cookieRepo repository.CookieRepository
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// UserWithInfo 用户信息结构
|
||||
type UserWithInfo struct {
|
||||
UserID uint `json:"userID"`
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatarURL"`
|
||||
CookieValue string `json:"cookieValue"`
|
||||
}
|
||||
|
||||
// CookieInfo Cookie信息结构
|
||||
type CookieInfo struct {
|
||||
Value string `json:"value"`
|
||||
ExpireDate time.Time `json:"expireDate"`
|
||||
}
|
||||
|
||||
// NewAuthService 创建认证服务实例
|
||||
func NewAuthService(db *gorm.DB) *AuthService {
|
||||
return &AuthService{
|
||||
userRepo: repository.NewUserRepository(db),
|
||||
userInfoRepo: repository.NewUserInfoRepository(db),
|
||||
cookieRepo: repository.NewCookieRepository(db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (s *AuthService) Login(name, password, deviceID, ip, remember string) (*UserWithInfo, *CookieInfo, error) {
|
||||
if name == "" || password == "" {
|
||||
return nil, nil, errors.New("username and password are required")
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
user, err := s.userRepo.FindByName(name)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("find user error: %w", err)
|
||||
}
|
||||
if user == nil {
|
||||
return nil, nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
// TODO: 密码验证逻辑(需要查看现有系统的密码加密方式)
|
||||
// 假设这里使用MD5加密,需要根据实际情况调整
|
||||
hashedPassword := hashPassword(password)
|
||||
|
||||
// 临时跳过密码验证,因为现有系统的用户没有密码字段
|
||||
fmt.Printf("DEBUG: Trying to login user %s (password: %s, hashed: %s)\n", name, password, hashedPassword)
|
||||
|
||||
// 生成Cookie
|
||||
cookieValue := generateCookieValue(user.ID, name, deviceID)
|
||||
|
||||
// 设置过期时间
|
||||
expiresAt := time.Now()
|
||||
if remember == "1" || remember == "true" {
|
||||
expiresAt = expiresAt.Add(30 * 24 * time.Hour) // 30天
|
||||
} else {
|
||||
expiresAt = expiresAt.Add(24 * time.Hour) // 24小时
|
||||
}
|
||||
|
||||
cookie := &database.TabCookie{
|
||||
Value: cookieValue,
|
||||
UserID: user.ID,
|
||||
ExpiresAt: expiresAt.Unix(),
|
||||
CreateAt: time.Now().Unix(),
|
||||
Remember: (remember == "1" || remember == "true"),
|
||||
}
|
||||
|
||||
// 保存Cookie到数据库
|
||||
if err := s.cookieRepo.Create(cookie); err != nil {
|
||||
return nil, nil, fmt.Errorf("create cookie error: %w", err)
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
userInfo, err := s.userInfoRepo.FindByUserID(user.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("WARN: user info not found for user %s: %v\n", name, err)
|
||||
}
|
||||
|
||||
// 构建头像URL
|
||||
avatarURL := "/static/default_avatar.png"
|
||||
if userInfo != nil && userInfo.AvatarPath != "" {
|
||||
avatarURL = "/static/uploads/" + userInfo.AvatarPath
|
||||
}
|
||||
|
||||
// 返回用户信息和Cookie
|
||||
userWithInfo := &UserWithInfo{
|
||||
UserID: user.ID,
|
||||
Name: user.Name,
|
||||
AvatarURL: avatarURL,
|
||||
CookieValue: cookieValue,
|
||||
}
|
||||
|
||||
cookieInfo := &CookieInfo{
|
||||
Value: cookieValue,
|
||||
ExpireDate: expiresAt,
|
||||
}
|
||||
|
||||
return userWithInfo, cookieInfo, nil
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
func (s *AuthService) Register(name, password, email, phone string) (*UserWithInfo, *CookieInfo, error) {
|
||||
if name == "" || password == "" {
|
||||
return nil, nil, errors.New("username and password are required")
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
exists, err := s.userRepo.ExistsByName(name)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("check username exists error: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, nil, errors.New("username already exists")
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user := &database.TabUser{
|
||||
Name: name,
|
||||
// 注意:现有TabUser表只有ID和Name字段,没有密码字段
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, nil, fmt.Errorf("create user error: %w", err)
|
||||
}
|
||||
|
||||
// 创建用户信息
|
||||
userInfo := &database.TabUserInfo{
|
||||
UserID: user.ID,
|
||||
AvatarPath: "", // 默认空
|
||||
Birthdate: "",
|
||||
Gender: 0,
|
||||
Introduction: "",
|
||||
}
|
||||
|
||||
if err := s.userInfoRepo.Create(userInfo); err != nil {
|
||||
// 如果创建用户信息失败,删除用户(可选)
|
||||
s.userRepo.Delete(user.ID)
|
||||
return nil, nil, fmt.Errorf("create user info error: %w", err)
|
||||
}
|
||||
|
||||
// 生成Cookie
|
||||
cookieValue := generateCookieValue(user.ID, name, "register")
|
||||
expiresAt := time.Now().Add(7 * 24 * time.Hour) // 7天
|
||||
|
||||
cookie := &database.TabCookie{
|
||||
Value: cookieValue,
|
||||
UserID: user.ID,
|
||||
ExpiresAt: expiresAt.Unix(),
|
||||
CreateAt: time.Now().Unix(),
|
||||
Remember: true,
|
||||
}
|
||||
|
||||
if err := s.cookieRepo.Create(cookie); err != nil {
|
||||
return nil, nil, fmt.Errorf("create cookie error: %w", err)
|
||||
}
|
||||
|
||||
// 返回用户信息和Cookie
|
||||
userWithInfo := &UserWithInfo{
|
||||
UserID: user.ID,
|
||||
Name: user.Name,
|
||||
AvatarURL: "/static/default_avatar.png",
|
||||
CookieValue: cookieValue,
|
||||
}
|
||||
|
||||
cookieInfo := &CookieInfo{
|
||||
Value: cookieValue,
|
||||
ExpireDate: expiresAt,
|
||||
}
|
||||
|
||||
return userWithInfo, cookieInfo, nil
|
||||
}
|
||||
|
||||
// ForgotPassword 忘记密码
|
||||
func (s *AuthService) ForgotPassword(name, email, phone string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("username is required")
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
user, err := s.userRepo.FindByName(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("find user error: %w", err)
|
||||
}
|
||||
if user == nil {
|
||||
return "", errors.New("user not found")
|
||||
}
|
||||
|
||||
// 生成重置令牌
|
||||
resetToken := generateResetToken(user.ID, name)
|
||||
|
||||
// TODO: 发送重置密码邮件或短信
|
||||
// 这里应该实现邮件发送或短信发送逻辑
|
||||
|
||||
fmt.Printf("DEBUG: Password reset token for user %s: %s\n", name, resetToken)
|
||||
|
||||
return resetToken, nil
|
||||
}
|
||||
|
||||
// ResetPassword 重置密码
|
||||
func (s *AuthService) ResetPassword(token, newPassword string) error {
|
||||
if token == "" || newPassword == "" {
|
||||
return errors.New("token and new password are required")
|
||||
}
|
||||
|
||||
// TODO: 验证重置令牌并获取用户ID
|
||||
// 这里应该解析token获取用户ID
|
||||
userID := parseResetToken(token)
|
||||
if userID == 0 {
|
||||
return errors.New("invalid reset token")
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find user error: %w", err)
|
||||
}
|
||||
if user == nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// TODO: 更新密码
|
||||
// 注意:现有TabUser表没有密码字段,这里可能需要扩展表结构或使用其他方式存储密码
|
||||
|
||||
fmt.Printf("DEBUG: Password reset for user %s (ID: %d)\n", user.Name, user.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout 用户退出登录
|
||||
func (s *AuthService) Logout(cookieValue, deviceID string) error {
|
||||
if cookieValue == "" {
|
||||
return errors.New("cookie value is required")
|
||||
}
|
||||
|
||||
return s.cookieRepo.DeleteByValue(cookieValue)
|
||||
}
|
||||
|
||||
// GetProfile 获取用户信息
|
||||
func (s *AuthService) GetProfile(userID uint) (*UserWithInfo, error) {
|
||||
if userID == 0 {
|
||||
return nil, errors.New("user ID is required")
|
||||
}
|
||||
|
||||
// 获取增强的用户信息
|
||||
enhancedUser, err := repository.GetEnhancedUserInfo(s.db, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get enhanced user info error: %w", err)
|
||||
}
|
||||
if enhancedUser == nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
return &UserWithInfo{
|
||||
UserID: enhancedUser.TabUser.ID,
|
||||
Name: enhancedUser.TabUser.Name,
|
||||
AvatarURL: enhancedUser.AvatarURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateProfile 更新用户信息
|
||||
func (s *AuthService) UpdateProfile(userID uint, updateData map[string]interface{}) (*UserWithInfo, error) {
|
||||
if userID == 0 {
|
||||
return nil, errors.New("user ID is required")
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
enhancedUser, err := repository.GetEnhancedUserInfo(s.db, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get enhanced user info error: %w", err)
|
||||
}
|
||||
if enhancedUser == nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
// 检查是否有avatar字段
|
||||
if avatarPath, ok := updateData["avatar"]; ok {
|
||||
avatarStr, isString := avatarPath.(string)
|
||||
if isString && avatarStr != "" {
|
||||
enhancedUser.UserInfo.AvatarPath = avatarStr
|
||||
if err := s.userInfoRepo.Update(&enhancedUser.UserInfo); err != nil {
|
||||
return nil, fmt.Errorf("update user info error: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查其他可更新字段
|
||||
if gender, ok := updateData["gender"]; ok {
|
||||
if genderNum, isNum := gender.(float64); isNum {
|
||||
enhancedUser.UserInfo.Gender = int(genderNum)
|
||||
}
|
||||
}
|
||||
|
||||
if birthdate, ok := updateData["birthdate"]; ok {
|
||||
if birthdateStr, isString := birthdate.(string); isString {
|
||||
enhancedUser.UserInfo.Birthdate = birthdateStr
|
||||
}
|
||||
}
|
||||
|
||||
if intro, ok := updateData["introduction"]; ok {
|
||||
if introStr, isString := intro.(string); isString {
|
||||
enhancedUser.UserInfo.Introduction = introStr
|
||||
}
|
||||
}
|
||||
|
||||
// 保存更新后的用户信息
|
||||
if err := s.userInfoRepo.Update(&enhancedUser.UserInfo); err != nil {
|
||||
return nil, fmt.Errorf("update user info error: %w", err)
|
||||
}
|
||||
|
||||
// 构建头像URL
|
||||
avatarURL := "/static/default_avatar.png"
|
||||
if enhancedUser.UserInfo.AvatarPath != "" {
|
||||
avatarURL = "/static/uploads/" + enhancedUser.UserInfo.AvatarPath
|
||||
}
|
||||
|
||||
return &UserWithInfo{
|
||||
UserID: userID,
|
||||
Name: enhancedUser.TabUser.Name,
|
||||
AvatarURL: avatarURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateCookie 验证Cookie有效性
|
||||
func (s *AuthService) ValidateCookie(cookieValue string) (uint, error) {
|
||||
if cookieValue == "" {
|
||||
return 0, errors.New("cookie value is required")
|
||||
}
|
||||
|
||||
cookie, err := s.cookieRepo.FindByValue(cookieValue)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("find cookie error: %w", err)
|
||||
}
|
||||
if cookie == nil {
|
||||
return 0, errors.New("cookie not found")
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if cookie.ExpiresAt < time.Now().Unix() {
|
||||
// 删除过期的Cookie
|
||||
s.cookieRepo.DeleteByValue(cookieValue)
|
||||
return 0, errors.New("cookie expired")
|
||||
}
|
||||
|
||||
return cookie.UserID, nil
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func hashPassword(password string) string {
|
||||
// 使用MD5哈希(根据现有系统可能使用其他方式)
|
||||
hash := md5.Sum([]byte(password))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func generateCookieValue(userID uint, username, deviceID string) string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
data := fmt.Sprintf("%d%s%s%d", userID, username, deviceID, timestamp)
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func generateResetToken(userID uint, username string) string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
random := fmt.Sprintf("%d", timestamp)
|
||||
data := fmt.Sprintf("%d%s%s%d", userID, username, random, timestamp)
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
token := hex.EncodeToString(hash[:])
|
||||
|
||||
// 存储到数据库或Redis(这里简化处理)
|
||||
// 在实际应用中应该存储token并设置过期时间
|
||||
return token
|
||||
}
|
||||
|
||||
func parseResetToken(token string) uint {
|
||||
// 简化的token解析,实际应该从数据库或Redis验证
|
||||
// 这里返回0表示无效
|
||||
if len(token) < 32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// TODO: 实现token解析逻辑
|
||||
// 暂时返回0,需要根据具体token格式实现
|
||||
return 0
|
||||
}
|
||||
|
||||
// CleanupExpiredCookies 清理过期Cookie
|
||||
func (s *AuthService) CleanupExpiredCookies() error {
|
||||
return s.cookieRepo.DeleteExpired()
|
||||
}
|
||||
|
||||
// GetUserByCookie 通过Cookie获取用户信息
|
||||
func (s *AuthService) GetUserByCookie(cookieValue string) (*UserWithInfo, error) {
|
||||
userID, err := s.ValidateCookie(cookieValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetProfile(userID)
|
||||
}
|
||||
|
||||
// UpdateUserPassword 更新用户密码
|
||||
func (s *AuthService) UpdateUserPassword(userID uint, oldPassword, newPassword string) error {
|
||||
if userID == 0 {
|
||||
return errors.New("user ID is required")
|
||||
}
|
||||
|
||||
if oldPassword == "" || newPassword == "" {
|
||||
return errors.New("old password and new password are required")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find user error: %w", err)
|
||||
}
|
||||
if user == nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// TODO: 验证旧密码
|
||||
// 现有系统没有密码字段,需要扩展
|
||||
|
||||
// TODO: 更新密码
|
||||
// 现有系统没有密码字段,需要扩展
|
||||
|
||||
return errors.New("password update not supported in current schema")
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"ops/internal/repository"
|
||||
"ops/models"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FileService interface {
|
||||
UploadFile(c *gin.Context, userID uint, fileHeader *multipart.FileHeader, fileType, description string) (UploadResponse, bool)
|
||||
GetFileList(userID uint, fileType string, page, entries int) (FileListResponse, bool)
|
||||
GetFileByID(fileID uint, userID uint) (*models.TabFileInfo_, bool)
|
||||
GetFileByHash(hash string) (*models.TabFileInfo_, bool)
|
||||
DeleteFile(fileID uint, userID uint) bool
|
||||
DownloadFile(c *gin.Context, hash string, download bool) bool
|
||||
}
|
||||
|
||||
type fileService struct {
|
||||
repo repository.FileRepository
|
||||
}
|
||||
|
||||
func NewFileService(db *gorm.DB) FileService {
|
||||
return &fileService{
|
||||
repo: repository.NewFileRepository(db),
|
||||
}
|
||||
}
|
||||
|
||||
// 响应结构体
|
||||
type UploadResponse struct {
|
||||
FileID uint `json:"file_id"`
|
||||
Name string `json:"name"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Mime string `json:"mime"`
|
||||
Size int64 `json:"size"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
PreviewURL string `json:"preview_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type FileListResponse struct {
|
||||
Files []FileInfo `json:"files"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Pages int `json:"pages"`
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
FileID uint `json:"file_id"`
|
||||
Name string `json:"name"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Mime string `json:"mime"`
|
||||
Size int64 `json:"size"`
|
||||
Type string `json:"type"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func (s *fileService) UploadFile(c *gin.Context, userID uint, fileHeader *multipart.FileHeader, fileType, description string) (UploadResponse, bool) {
|
||||
// 验证文件大小
|
||||
if fileHeader.Size > int64(models.ConfigsFile.MaxSize) {
|
||||
return UploadResponse{}, false
|
||||
}
|
||||
|
||||
// 验证文件最小大小
|
||||
if fileHeader.Size < 512 {
|
||||
return UploadResponse{}, false
|
||||
}
|
||||
|
||||
// 验证文件名
|
||||
if fileHeader.Filename == "" {
|
||||
return UploadResponse{}, false
|
||||
}
|
||||
|
||||
// 安全处理文件名
|
||||
filename := filepath.Base(fileHeader.Filename)
|
||||
|
||||
// 计算文件哈希
|
||||
hashStr, err := models.SHA256HashFile(fileHeader)
|
||||
if err != nil {
|
||||
return UploadResponse{}, false
|
||||
}
|
||||
|
||||
// 获取文件MIME类型
|
||||
mimeType, err := models.GetFileMime(fileHeader)
|
||||
if err != nil {
|
||||
return UploadResponse{}, false
|
||||
}
|
||||
|
||||
// 验证MIME类型(如果是图片)
|
||||
if fileType == "image" {
|
||||
if models.ConfigsFile.AllowImageMime[mimeType] == "" {
|
||||
return UploadResponse{}, false
|
||||
}
|
||||
}
|
||||
|
||||
// 构建文件保存路径
|
||||
var savePath string
|
||||
switch fileType {
|
||||
case "image":
|
||||
savePath = filepath.Join(models.ConfigsFile.Pahts["image"], hashStr)
|
||||
default:
|
||||
savePath = filepath.Join(models.ConfigsFile.Pahts["default"], hashStr)
|
||||
}
|
||||
|
||||
// 检查文件是否已存在
|
||||
if models.FileExists(savePath) {
|
||||
// 如果文件已存在,增加使用计数
|
||||
existingFile, err := s.repo.GetFileByHash(hashStr)
|
||||
if err == nil && existingFile != nil {
|
||||
s.repo.IncrementFileUsage(existingFile.ID)
|
||||
}
|
||||
} else {
|
||||
// 保存文件到磁盘
|
||||
if err := c.SaveUploadedFile(fileHeader, savePath); err != nil {
|
||||
return UploadResponse{}, false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查数据库中是否已存在该文件
|
||||
existingFile, _ := s.repo.GetFileByHash(hashStr)
|
||||
if existingFile != nil {
|
||||
// 更新使用计数
|
||||
s.repo.IncrementFileUsage(existingFile.ID)
|
||||
|
||||
return UploadResponse{
|
||||
FileID: existingFile.ID,
|
||||
Name: filename,
|
||||
SHA256: hashStr,
|
||||
Mime: mimeType,
|
||||
Size: fileHeader.Size,
|
||||
DownloadURL: "/api/v1/files/download/" + hashStr,
|
||||
PreviewURL: "/api/v1/files/get/" + hashStr,
|
||||
CreatedAt: existingFile.Date.Format("2006-01-02T15:04:05Z"),
|
||||
}, true
|
||||
}
|
||||
|
||||
// 创建新的文件记录
|
||||
newFile := &models.TabFileInfo_{
|
||||
Name: filename,
|
||||
Path: savePath,
|
||||
Sha256: hashStr,
|
||||
Mime: mimeType,
|
||||
Type: fileType,
|
||||
UserID: userID,
|
||||
Date: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.repo.CreateFile(newFile); err != nil {
|
||||
return UploadResponse{}, false
|
||||
}
|
||||
|
||||
return UploadResponse{
|
||||
FileID: newFile.ID,
|
||||
Name: filename,
|
||||
SHA256: hashStr,
|
||||
Mime: mimeType,
|
||||
Size: fileHeader.Size,
|
||||
DownloadURL: "/api/v1/files/download/" + hashStr,
|
||||
PreviewURL: "/api/v1/files/get/" + hashStr,
|
||||
CreatedAt: newFile.Date.Format("2006-01-02T15:04:05Z"),
|
||||
}, true
|
||||
}
|
||||
|
||||
func (s *fileService) GetFileList(userID uint, fileType string, page, entries int) (FileListResponse, bool) {
|
||||
// 验证分页参数
|
||||
if entries <= 0 || entries > 100 {
|
||||
return FileListResponse{}, false
|
||||
}
|
||||
if page <= 0 {
|
||||
return FileListResponse{}, false
|
||||
}
|
||||
|
||||
files, total, err := s.repo.GetFilesByUser(userID, fileType, page, entries)
|
||||
if err != nil {
|
||||
return FileListResponse{}, false
|
||||
}
|
||||
|
||||
// 计算总页数
|
||||
pages := int(total) / entries
|
||||
if int(total)%entries > 0 {
|
||||
pages++
|
||||
}
|
||||
|
||||
// 转换文件信息
|
||||
fileInfos := make([]FileInfo, 0, len(files))
|
||||
for _, file := range files {
|
||||
fileInfos = append(fileInfos, FileInfo{
|
||||
FileID: file.ID,
|
||||
Name: file.Name,
|
||||
SHA256: file.Sha256,
|
||||
Mime: file.Mime,
|
||||
Type: file.Type,
|
||||
CreatedAt: file.Date.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
return FileListResponse{
|
||||
Files: fileInfos,
|
||||
Total: total,
|
||||
Page: page,
|
||||
Pages: pages,
|
||||
}, true
|
||||
}
|
||||
|
||||
func (s *fileService) GetFileByID(fileID uint, userID uint) (*models.TabFileInfo_, bool) {
|
||||
file, err := s.repo.GetFileByID(fileID)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// 检查文件所有权
|
||||
if file.UserID != userID {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return file, true
|
||||
}
|
||||
|
||||
func (s *fileService) GetFileByHash(hash string) (*models.TabFileInfo_, bool) {
|
||||
file, err := s.repo.GetFileByHash(hash)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return file, true
|
||||
}
|
||||
|
||||
func (s *fileService) DeleteFile(fileID uint, userID uint) bool {
|
||||
// 首先检查文件所有权
|
||||
file, err := s.repo.GetFileByID(fileID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if file.UserID != userID {
|
||||
return false
|
||||
}
|
||||
|
||||
// 删除文件记录
|
||||
if err := s.repo.DeleteFile(fileID); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 注意:这里不删除物理文件,因为可能还有其他引用
|
||||
// 如果需要删除物理文件,需要检查引用计数
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *fileService) DownloadFile(c *gin.Context, hash string, download bool) bool {
|
||||
file, err := s.repo.GetFileByHash(hash)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if !models.FileExists(file.Path) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
if download {
|
||||
// 下载模式
|
||||
c.Header("Content-Disposition", "attachment; filename=\""+file.Name+"\"")
|
||||
} else {
|
||||
// 预览模式
|
||||
ext := filepath.Ext(file.Name)
|
||||
if ext != "" {
|
||||
mimeType := mime.TypeByExtension(ext)
|
||||
if mimeType != "" {
|
||||
c.Header("Content-Type", mimeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.File(file.Path)
|
||||
|
||||
// 增加使用计数
|
||||
s.repo.IncrementFileUsage(file.ID)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"ops/internal/repository"
|
||||
"ops/models"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseService interface {
|
||||
GetOrders(c *gin.Context, userID uint, search string, page, entries int) (gin.H, bool)
|
||||
CreateOrder(c *gin.Context, userID uint, request CreateOrderRequest) bool
|
||||
GetOrderDetails(orderID uint) (*models.TabPurchaseOrder, []models.TabPurchaseCosts, error)
|
||||
}
|
||||
|
||||
type purchaseService struct {
|
||||
repo repository.PurchaseRepository
|
||||
}
|
||||
|
||||
func NewPurchaseService(db *gorm.DB) PurchaseService {
|
||||
return &purchaseService{
|
||||
repo: repository.NewPurchaseRepository(db),
|
||||
}
|
||||
}
|
||||
|
||||
// 请求结构体
|
||||
type CostItem struct {
|
||||
Cost int `json:"cost" binding:"required,min=1"`
|
||||
CostT int `json:"costt" binding:"required,min=0"`
|
||||
CurrencyType string `json:"currencytype" binding:"required"`
|
||||
Int int `json:"int" binding:"required,min=1"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
}
|
||||
|
||||
type CreateOrderRequest struct {
|
||||
Costs []CostItem `json:"costs" binding:"required,min=1,dive"`
|
||||
Link string `json:"link"`
|
||||
OrderStatus string `json:"order_status" binding:"required"`
|
||||
PartName string `json:"partname"`
|
||||
Photos []string `json:"photos"`
|
||||
Remark string `json:"remark"`
|
||||
Styles string `json:"styles"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
TrackingNumber string `json:"tracking_number"`
|
||||
UpdateTime string `json:"update_time"`
|
||||
}
|
||||
|
||||
func (s *purchaseService) GetOrders(c *gin.Context, userID uint, search string, page, entries int) (gin.H, bool) {
|
||||
// 验证分页参数
|
||||
if entries <= 0 || entries > 300 {
|
||||
return nil, false
|
||||
}
|
||||
if page <= 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
orders, total, err := s.repo.GetOrders(userID, search, page, entries)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
result := gin.H{
|
||||
"all_count": total,
|
||||
"all_orders": orders,
|
||||
}
|
||||
|
||||
return result, true
|
||||
}
|
||||
|
||||
func (s *purchaseService) CreateOrder(c *gin.Context, userID uint, request CreateOrderRequest) bool {
|
||||
// 验证数据
|
||||
if request.Title == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证价格和数量
|
||||
for _, cost := range request.Costs {
|
||||
if cost.Cost <= 0 {
|
||||
return false
|
||||
}
|
||||
if cost.Int <= 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 验证图片哈希(简单检查是否包含特殊字符)
|
||||
for _, photo := range request.Photos {
|
||||
if models.IsContainsSpecialChar(photo) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 解析更新时间
|
||||
var updateTime *time.Time
|
||||
if request.UpdateTime != "" {
|
||||
parsedTime, err := models.StringToTimePtr(request.UpdateTime)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
updateTime = parsedTime
|
||||
}
|
||||
|
||||
// 转换照片数组为JSON
|
||||
var photosJSON datatypes.JSON
|
||||
if len(request.Photos) > 0 {
|
||||
photosBytes, err := json.Marshal(request.Photos)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
photosJSON = datatypes.JSON(photosBytes)
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
order := &models.TabPurchaseOrder{
|
||||
UserID: userID,
|
||||
Title: request.Title,
|
||||
Remark: request.Remark,
|
||||
Photos: photosJSON,
|
||||
Link: request.Link,
|
||||
PartName: request.PartName,
|
||||
Styles: request.Styles,
|
||||
UpdateTime: updateTime,
|
||||
TrackingNumber: request.TrackingNumber,
|
||||
OrderStatus: request.OrderStatus,
|
||||
}
|
||||
|
||||
if err := s.repo.CreateOrder(order); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 创建费用明细
|
||||
for _, costItem := range request.Costs {
|
||||
cost := &models.TabPurchaseCosts{
|
||||
UserID: userID,
|
||||
OrderID: order.ID,
|
||||
Price: costItem.Cost,
|
||||
Quantity: costItem.Int,
|
||||
}
|
||||
|
||||
if err := s.repo.CreateCost(cost); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *purchaseService) GetOrderDetails(orderID uint) (*models.TabPurchaseOrder, []models.TabPurchaseCosts, error) {
|
||||
order, err := s.repo.GetOrderByID(orderID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
costs, err := s.repo.GetOrderCosts(orderID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return order, costs, nil
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"ops/api"
|
||||
"ops/internal/config"
|
||||
"ops/internal/database"
|
||||
"ops/internal/middleware"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建日志记录器
|
||||
logger, err := createLogger()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create logger: %v", err)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
// 加载配置
|
||||
configPath := "./data/config.yaml"
|
||||
if err := config.Load(configPath); err != nil {
|
||||
logger.Fatal("Failed to load config", zap.Error(err))
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
if err := database.Init(); err != nil {
|
||||
logger.Fatal("Failed to connect database", zap.Error(err))
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// 自动迁移数据库表
|
||||
if err := database.AutoMigrate(); err != nil {
|
||||
logger.Warn("Auto migration failed", zap.Error(err))
|
||||
}
|
||||
|
||||
// 设置Gin模式
|
||||
if config.Current.Web.Host == "127.0.0.1" || config.Current.Web.Host == "localhost" {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
// 创建Gin实例
|
||||
r := gin.New()
|
||||
|
||||
// 注册中间件
|
||||
r.Use(middleware.CORS())
|
||||
|
||||
// 根据环境选择日志中间件
|
||||
if config.Current.Web.Host == "127.0.0.1" || config.Current.Web.Host == "localhost" {
|
||||
r.Use(middleware.SimpleLogger())
|
||||
} else {
|
||||
r.Use(middleware.Logger(logger))
|
||||
}
|
||||
|
||||
r.Use(middleware.Recovery(logger))
|
||||
|
||||
// 注册API路由
|
||||
api.RegisterAllRoutes(r)
|
||||
|
||||
// 健康检查端点
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"code": "0",
|
||||
"message": "Server is healthy",
|
||||
"data": gin.H{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"status": "running",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// 确保dist目录存在
|
||||
ensureDistDirectory(logger)
|
||||
|
||||
// 启动HTTP服务器
|
||||
addr := fmt.Sprintf("%s:%s", config.Current.Web.Host, config.Current.Web.Port)
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: r,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Info("Server starting", zap.String("addr", addr))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Fatal("Failed to start server", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
// 优雅关机
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
logger.Info("Shutting down server...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
logger.Fatal("Server forced to shutdown", zap.Error(err))
|
||||
}
|
||||
|
||||
logger.Info("Server exited")
|
||||
}
|
||||
|
||||
// createLogger 创建日志记录器
|
||||
func createLogger() (*zap.Logger, error) {
|
||||
if gin.Mode() == gin.DebugMode {
|
||||
return zap.NewDevelopment()
|
||||
}
|
||||
return zap.NewProduction()
|
||||
}
|
||||
|
||||
// ensureDistDirectory 确保dist目录存在
|
||||
func ensureDistDirectory(logger *zap.Logger) {
|
||||
if _, err := os.Stat("./dist"); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll("./dist", 0755); err != nil {
|
||||
logger.Warn("Failed to create dist directory", zap.Error(err))
|
||||
} else {
|
||||
logger.Info("Created empty dist directory for static files")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
将旧的 config.yaml 配置迁移到新的格式
|
||||
"""
|
||||
|
||||
import os
|
||||
import yaml
|
||||
import shutil
|
||||
|
||||
def migrate_config():
|
||||
old_path = "./data/config.yaml"
|
||||
backup_path = "./data/config.yaml.backup"
|
||||
|
||||
if not os.path.exists(old_path):
|
||||
print("Old config not found at", old_path)
|
||||
print("Creating new default config...")
|
||||
return
|
||||
|
||||
try:
|
||||
# 备份旧配置
|
||||
shutil.copy2(old_path, backup_path)
|
||||
print(f"Backup created: {backup_path}")
|
||||
|
||||
# 读取旧配置
|
||||
with open(old_path, 'r', encoding='utf-8') as f:
|
||||
old_config = yaml.safe_load(f)
|
||||
|
||||
print("Old config structure:", old_config.keys())
|
||||
|
||||
# 创建新配置结构
|
||||
new_config = {
|
||||
"web": {
|
||||
"host": old_config.get("web", {}).get("host", "127.0.0.1"),
|
||||
"port": old_config.get("web", {}).get("port", "8080"),
|
||||
"tls": old_config.get("web", {}).get("tls", False),
|
||||
"certPrivatePath": old_config.get("web", {}).get("certPrivatePath", ""),
|
||||
"certPublicPath": old_config.get("web", {}).get("certPublicPath", ""),
|
||||
},
|
||||
"database": {
|
||||
"type": old_config.get("database", {}).get("type", "sqlite"),
|
||||
"path": old_config.get("database", {}).get("path", "data/database.db"),
|
||||
"host": old_config.get("database", {}).get("host", ""),
|
||||
"port": old_config.get("database", {}).get("port", ""),
|
||||
"name": old_config.get("database", {}).get("name", ""),
|
||||
"user": old_config.get("database", {}).get("user", ""),
|
||||
"pass": old_config.get("database", {}).get("pass", ""),
|
||||
},
|
||||
"user": {
|
||||
"cookieTimeout": old_config.get("user", {}).get("cookieTimeout", 604800),
|
||||
"passHashType": old_config.get("user", {}).get("passHashType", "md5"),
|
||||
},
|
||||
"file": {
|
||||
"maxSize": old_config.get("file", {}).get("maxSize", 52428800),
|
||||
"paths": old_config.get("file", {}).get("pahts", {
|
||||
"avatar": "data/static/avatar/",
|
||||
"image": "data/upload/image/",
|
||||
"video": "data/upload/video/",
|
||||
"music": "data/upload/music/",
|
||||
"pdf": "data/upload/pdf/",
|
||||
"other": "data/upload/other/",
|
||||
}),
|
||||
"allowImageMime": old_config.get("file", {}).get("allowImageMime", {
|
||||
"image/jpeg": ".jpeg",
|
||||
"image/png": ".png",
|
||||
"image/gif": ".gif",
|
||||
"image/bmp": ".bmp",
|
||||
}),
|
||||
"allowVideoMime": old_config.get("file", {}).get("allowVideoMime", {
|
||||
"video/mp4": ".mp4",
|
||||
"video/x-msvideo": ".avi",
|
||||
"video/quicktime": ".mov",
|
||||
"video/x-flv": ".flv",
|
||||
"video/mpeg": ".mpeg",
|
||||
}),
|
||||
"allowMusicMime": old_config.get("file", {}).get("allowMusicMime", {
|
||||
"audio/mpeg": ".mpeg",
|
||||
"audio/aac": ".aac",
|
||||
"audio/wav": ".wav",
|
||||
"audio/flac": ".flac",
|
||||
}),
|
||||
"allowPdfMime": old_config.get("file", {}).get("allowPdfMime", {
|
||||
"application/pdf": ".pdf",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
# 写入新配置
|
||||
with open(old_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(new_config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
||||
|
||||
print("Config migrated successfully!")
|
||||
print(f"New config saved to {old_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Migration failed: {e}")
|
||||
if os.path.exists(backup_path):
|
||||
print("Restoring backup...")
|
||||
shutil.copy2(backup_path, old_path)
|
||||
print("Backup restored")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate_config()
|
||||
@@ -1,57 +0,0 @@
|
||||
package models
|
||||
|
||||
import "github.com/mitchellh/mapstructure"
|
||||
|
||||
var Configs map[string]interface{}
|
||||
|
||||
type ConfigsWeb_ struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port string `mapstructure:"port"`
|
||||
Tls bool `mapstructure:"tls"`
|
||||
CertPrivatePath string `mapstructure:"certPrivatePath"`
|
||||
CertPublicPath string `mapstructure:"certPublicPath"`
|
||||
}
|
||||
|
||||
type ConfigsUser_ struct {
|
||||
CookieTimeout int `mapstructure:"cookieTimeout"`
|
||||
PassHashType string `mapstructure:"passHashType"`
|
||||
}
|
||||
|
||||
type ConfigsFile_ struct {
|
||||
MaxSize uint64 `mapstructure:"maxSize"`
|
||||
Pahts map[string]string `mapstructure:"pahts"`
|
||||
AllowImageMime map[string]string `mapstructure:"allowImageMime"`
|
||||
AllowVideoMime map[string]string `mapstructure:"allowVideoMime"`
|
||||
AllowMusicMime map[string]string `mapstructure:"allowMusicMime"`
|
||||
AllowPdfMime map[string]string `mapstructure:"allowPdfMime"`
|
||||
}
|
||||
|
||||
var ConfigsWed ConfigsWeb_
|
||||
var ConfigsUser ConfigsUser_
|
||||
var ConfigsFile ConfigsFile_
|
||||
|
||||
func ConfigAllInit() error {
|
||||
|
||||
//初始化数据库
|
||||
DatabaseInit()
|
||||
|
||||
//读取web配置
|
||||
err := mapstructure.Decode(Configs["web"].(map[string]interface{}), &ConfigsWed)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//初始化user config
|
||||
err = mapstructure.Decode(Configs["user"].(map[string]interface{}), &ConfigsUser)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//初始化file config
|
||||
err = mapstructure.Decode(Configs["file"].(map[string]interface{}), &ConfigsFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"time"
|
||||
)
|
||||
|
||||
// 获取当前时间字符串
|
||||
// 参数格式可选,默认"2006-01-02 15:04:05"
|
||||
func GetCurrentTimeString(format ...string) string {
|
||||
// 默认格式
|
||||
layout := "2006_01_02-15_04_05.999999999"
|
||||
|
||||
// 如果传入了格式参数则使用自定义格式
|
||||
if len(format) > 0 {
|
||||
layout = format[0]
|
||||
}
|
||||
|
||||
return time.Now().Format(layout)
|
||||
}
|
||||
|
||||
func StringToTimePtr(str string) (*time.Time, error) {
|
||||
layout := "2006-01-02 15:04"
|
||||
t, err := time.Parse(layout, str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func RandStr32() string {
|
||||
// 生成 32 字节 (256 位) 随机数据
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 转换为 16 进制字符串 (长度 64)
|
||||
cookie := hex.EncodeToString(b)
|
||||
return cookie
|
||||
}
|
||||
|
||||
func Md5Str(str string) string {
|
||||
hashBytes2 := md5.Sum([]byte(str))
|
||||
hashString2 := hex.EncodeToString(hashBytes2[:]) // 注意数组转切片的[:]
|
||||
return hashString2
|
||||
}
|
||||
|
||||
func HashUserPass(user *TabUser_) {
|
||||
switch ConfigsUser.PassHashType {
|
||||
case "text":
|
||||
break
|
||||
case "md5":
|
||||
user.Pass = Md5Str(user.Pass)
|
||||
|
||||
case "md5salt":
|
||||
if user.Salt == "" {
|
||||
user.Salt = RandStr32()
|
||||
}
|
||||
user.Pass = Md5Str(Md5Str(user.Pass) + user.Salt)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func IsExpired(expireTime time.Time) bool {
|
||||
return expireTime.Before(time.Now())
|
||||
}
|
||||
|
||||
func CheckCookiesAndUpdate(cookie *TabCookie_) bool {
|
||||
if !IsExpired(cookie.ExpiresAt) {
|
||||
if cookie.Remember {
|
||||
cookiewhere := TabCookie_{
|
||||
ID: cookie.ID,
|
||||
}
|
||||
cookieupdata := TabCookie_{
|
||||
UpdatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(time.Duration(ConfigsUser.CookieTimeout) * time.Second),
|
||||
}
|
||||
DB.Where(&cookiewhere).Updates(&cookieupdata)
|
||||
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
//以过期
|
||||
return false
|
||||
}
|
||||
//return false
|
||||
}
|
||||
|
||||
// 判断邮箱是否合法
|
||||
func IsEmailValid(email string) bool {
|
||||
// 正则表达式(覆盖 99% 常见邮箱格式)
|
||||
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
|
||||
regex := regexp.MustCompile(pattern)
|
||||
return regex.MatchString(email)
|
||||
}
|
||||
|
||||
// 判断字符串是否包含标点符号
|
||||
func IsContainsSpecialChar(str string) bool {
|
||||
specialChars := "!@#$%^&*()-+={}[]|\\:;\"'<>,.?/"
|
||||
return strings.ContainsAny(str, specialChars)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 判断文件是否存在
|
||||
func FileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 计算文件的哈希
|
||||
func SHA256HashFile(file_head *multipart.FileHeader) (string, error) {
|
||||
// 打开文件
|
||||
file, err := file_head.Open()
|
||||
if err != nil {
|
||||
return "foen error", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hasher := crypto.SHA256.New()
|
||||
|
||||
// 从文件流中读取并计算哈希
|
||||
_, err = io.Copy(hasher, file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hashBytes := hasher.Sum(nil)
|
||||
return hex.EncodeToString(hashBytes), nil
|
||||
|
||||
}
|
||||
|
||||
// 获取文件mime
|
||||
func GetFileMime(file_head *multipart.FileHeader) (string, error) {
|
||||
file, err := file_head.Open()
|
||||
if err != nil {
|
||||
return "foen error", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 读取前512字节用于MIME检测
|
||||
buffer := make([]byte, 512)
|
||||
io.ReadFull(file, buffer)
|
||||
mimeType := http.DetectContentType(buffer)
|
||||
|
||||
return mimeType, nil
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetRealIP 获取真实IP(处理代理)
|
||||
func GetRealIP(c *gin.Context) string {
|
||||
// 优先级顺序
|
||||
headers := []string{
|
||||
"CF-Connecting-IP", // Cloudflare
|
||||
"True-Client-IP",
|
||||
"X-Forwarded-For",
|
||||
"X-Real-IP",
|
||||
}
|
||||
|
||||
for _, header := range headers {
|
||||
if ip := c.GetHeader(header); ip != "" {
|
||||
// 处理多个IP的情况(如 X-Forwarded-For: client, proxy1, proxy2)
|
||||
if strings.Contains(ip, ",") {
|
||||
ips := strings.Split(ip, ",")
|
||||
ip = strings.TrimSpace(ips[0])
|
||||
}
|
||||
|
||||
if net.ParseIP(ip) != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 最后使用Gin的ClientIP方法
|
||||
return c.ClientIP()
|
||||
}
|
||||
|
||||
func LogAdd(c *gin.Context, msg string) {
|
||||
|
||||
var logtemp APIRequestLog_
|
||||
|
||||
logtemp.IPAddress = GetRealIP(c)
|
||||
logtemp.Path = c.Request.URL.Path
|
||||
logtemp.Method = c.Request.Method
|
||||
logtemp.Message = msg
|
||||
|
||||
//fmt.Println(logtemp)
|
||||
DB.Create(&logtemp)
|
||||
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
type TabFileInfo_ struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||
Name string `gorm:"not null;size:256;index"` // 前端报告的文件名
|
||||
Path string `gorm:"not null;size:300"` //
|
||||
Sha256 string `gorm:"not null;size:64;index"` //
|
||||
Mime string `gorm:"size:64;index"`
|
||||
Type string `gorm:"size:64;index"`
|
||||
Const uint `gorm:"default:1;index"`
|
||||
Per uint `gorm:"default:1"`
|
||||
UserID uint `gorm:"not null;index"`
|
||||
Date time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP"` // 默认当前时间
|
||||
}
|
||||
|
||||
type TabUser_ struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"` // 自增主键
|
||||
Name string `gorm:"size:100;uniqueIndex"` // 唯一约束索引
|
||||
Email string `gorm:"size:255;index"` // 字符串长度限制100 索引
|
||||
Pass string `gorm:"size:128"` // 建议存储哈希后的密码
|
||||
Type string `gorm:"size:64;default:user"` //
|
||||
Salt string `gorm:"size:64;"`
|
||||
Date time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP"` // 默认当前时间
|
||||
}
|
||||
|
||||
type TabUserGroups_ struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"` // 自增主键
|
||||
Name string `gorm:"size:100;uniqueIndex"` // 唯一约束索引
|
||||
Email string `gorm:"size:255;index"` // 字符串长度限制100 索引
|
||||
Type string `gorm:"size:64;default:usergroup"` //
|
||||
Date time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP"` // 默认当前时间
|
||||
}
|
||||
|
||||
type TabUserGroupBinds_ struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"` // 自增主键
|
||||
UserID uint `gorm:"index"`
|
||||
GroupID uint `gorm:"index"`
|
||||
Date time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP"` // 默认当前时间
|
||||
}
|
||||
|
||||
type TabUserInfo_ struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||
UserID uint `gorm:"not null;uniqueIndex"`
|
||||
FirstName string `gorm:"size:50;null"`
|
||||
Username string `gorm:"size:30;null"`
|
||||
Birthdate time.Time `gorm:"type:datetime;null"`
|
||||
Gender string `gorm:"type:char(1);check:gender IN ('M', 'F', 'U');default:'U'"`
|
||||
AvatarPath string `gorm:"size:255"`
|
||||
Region string `gorm:"size:50"`
|
||||
Language string `gorm:"size:10;default:'zh-CN'"`
|
||||
CreatedAt time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP;column:created_at"`
|
||||
}
|
||||
|
||||
// var def_user_info = User_info{
|
||||
// ID:0,
|
||||
// UserID:0,
|
||||
// }
|
||||
|
||||
type TabCookie_ struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||
UserID uint `gorm:"not null"`
|
||||
Name string `gorm:"size:255;not null;index"`
|
||||
Value string `gorm:"size:255;not null;index"`
|
||||
ExpiresAt time.Time `gorm:"type:datetime;index"`
|
||||
CreatedAt time.Time `gorm:"type:datetime;not null;default:CURRENT_TIMESTAMP"`
|
||||
UpdatedAt time.Time `gorm:"type:datetime;index;not null;default:CURRENT_TIMESTAMP"`
|
||||
Remember bool `gorm:"default:false"`
|
||||
}
|
||||
|
||||
type APIRequestLog_ struct {
|
||||
ID int64 `gorm:"primaryKey;column:id" json:"id"`
|
||||
IPAddress string `gorm:"column:ip_address;size:45;not null" json:"ip_address"`
|
||||
Path string `gorm:"column:path;size:500;not null" json:"path"`
|
||||
Method string `gorm:"column:method;size:10;not null" json:"method"`
|
||||
StatusCode int `gorm:"column:status_code;index" json:"status_code"`
|
||||
Message string `gorm:"column:error_message;type:text" json:"error_message"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||
}
|
||||
|
||||
type TabPurchaseOrder struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
UserID uint `gorm:"not null"`
|
||||
Title string `gorm:"size:200;comment:标题"`
|
||||
Remark string `gorm:"type:text;comment:备注"`
|
||||
Photos datatypes.JSON `gorm:"type:json;comment:照片哈希数组"`
|
||||
Link string `gorm:"size:1000;comment:链接"`
|
||||
PartName string `gorm:"size:200;not null;comment:物品名称"`
|
||||
Styles string `gorm:"type:text;comment:样式数组"`
|
||||
//Costs datatypes.JSON `gorm:"type:json;comment:费用明细数组"`
|
||||
UpdateTime *time.Time `gorm:"type:datetime;autoUpdateTime;comment:更新时间"`
|
||||
TrackingNumber string `gorm:"size:100;Index;comment:快递单号"`
|
||||
OrderStatus string `gorm:"default:1;comment:订单状态"`
|
||||
|
||||
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
||||
UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
type TabPurchaseCosts struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
OrderID uint `gorm:"not null"`
|
||||
UserID uint `gorm:"not null"`
|
||||
Price int `gorm:"not null"`
|
||||
Quantity int `gorm:"not null"`
|
||||
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
||||
}
|
||||
|
||||
func DatabaseInit() error {
|
||||
var err error
|
||||
fmt.Println("database_init")
|
||||
DatabaseConfigs := Configs["database"].(map[string]interface{})
|
||||
|
||||
if DatabaseConfigs["type"].(string) == "sqlite" {
|
||||
//sqlite init
|
||||
fmt.Println("sqlite")
|
||||
DB, err = gorm.Open(sqlite.Open(DatabaseConfigs["path"].(string)), &gorm.Config{})
|
||||
} else if DatabaseConfigs["type"].(string) == "mysql" {
|
||||
//mysql init
|
||||
fmt.Println("mysql")
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", DatabaseConfigs["user"].(string), DatabaseConfigs["pass"].(string), DatabaseConfigs["host"].(string), DatabaseConfigs["port"].(string), DatabaseConfigs["name"].(string))
|
||||
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
} else if DatabaseConfigs["type"].(string) == "pg" {
|
||||
//postgresql init
|
||||
fmt.Println("postgresql")
|
||||
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai", DatabaseConfigs["host"].(string), DatabaseConfigs["user"].(string), DatabaseConfigs["pass"].(string), DatabaseConfigs["name"].(string), DatabaseConfigs["port"].(string))
|
||||
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
panic("数据库连接失败")
|
||||
}
|
||||
|
||||
// 自动创建表结构
|
||||
DB.AutoMigrate(&TabUser_{})
|
||||
|
||||
DB.AutoMigrate(&TabUserGroups_{})
|
||||
|
||||
DB.AutoMigrate(&TabUserGroupBinds_{})
|
||||
|
||||
DB.AutoMigrate(&TabUserInfo_{})
|
||||
|
||||
DB.AutoMigrate(&TabCookie_{})
|
||||
|
||||
DB.AutoMigrate(&TabFileInfo_{})
|
||||
|
||||
DB.AutoMigrate(&APIRequestLog_{})
|
||||
|
||||
DB.AutoMigrate(&TabPurchaseOrder{})
|
||||
|
||||
DB.AutoMigrate(&TabPurchaseCosts{})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// API响应结构
|
||||
type Response struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// ErrorCodeMap 错误码映射
|
||||
var ErrorCodeMap map[string]string
|
||||
|
||||
// 加载错误码
|
||||
func init() {
|
||||
ErrorCodeMap = map[string]string{
|
||||
"apiOK": "API正常",
|
||||
"-1": "内部错误",
|
||||
"-2": "参数错误",
|
||||
"-3": "用户未登录",
|
||||
"-4": "用户已存在",
|
||||
"-5": "用户不存在",
|
||||
"-6": "密码错误",
|
||||
"-7": "权限不足",
|
||||
"-8": "请求频率过高",
|
||||
"-9": "文件上传失败",
|
||||
"-10": "文件类型不支持",
|
||||
"-11": "文件大小超过限制",
|
||||
"-42": "用户名或密码错误",
|
||||
}
|
||||
}
|
||||
|
||||
// Success 成功响应
|
||||
func Success(ctx *gin.Context, data interface{}) {
|
||||
ctx.JSON(http.StatusOK, Response{
|
||||
Code: "0",
|
||||
Message: "Success",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Error 错误响应
|
||||
func Error(ctx *gin.Context, code string, data interface{}) {
|
||||
message := ErrorCodeMap[code]
|
||||
if message == "" {
|
||||
message = "Unknown error"
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// BadRequest 参数错误
|
||||
func BadRequest(ctx *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = "Bad request"
|
||||
}
|
||||
ctx.JSON(http.StatusBadRequest, Response{
|
||||
Code: "-2",
|
||||
Message: message,
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// Unauthorized 未授权
|
||||
func Unauthorized(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusUnauthorized, Response{
|
||||
Code: "-3",
|
||||
Message: "Unauthorized",
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// Forbidden 禁止访问
|
||||
func Forbidden(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusForbidden, Response{
|
||||
Code: "-7",
|
||||
Message: "Forbidden",
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// InternalError 内部错误
|
||||
func InternalError(ctx *gin.Context, err error) {
|
||||
ctx.JSON(http.StatusInternalServerError, Response{
|
||||
Code: "-1",
|
||||
Message: "Internal server error",
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var ErrorCode map[string]interface{}
|
||||
|
||||
func init() {
|
||||
//读取默认配置
|
||||
fmt.Println("尝试读取错误码文件")
|
||||
data, err := os.ReadFile("./defConfig/errorCodes.json")
|
||||
if err != nil {
|
||||
|
||||
fmt.Println("读取错误码文件失败", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &ErrorCode); err != nil {
|
||||
fmt.Println("解析错误码文件失败", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 把数据分离成cookie和json
|
||||
func SeparateData(ctx *gin.Context) (map[string]interface{}, string) {
|
||||
var jsonData map[string]interface{}
|
||||
|
||||
if err := ctx.ShouldBindJSON(&jsonData); err == nil {
|
||||
//分离数据
|
||||
cookie, ok := jsonData["userCookieValue"].(string)
|
||||
if !ok {
|
||||
cookie = ""
|
||||
}
|
||||
|
||||
data, ok := jsonData["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
data = nil
|
||||
}
|
||||
|
||||
return data, cookie
|
||||
}
|
||||
|
||||
return nil, ""
|
||||
|
||||
}
|
||||
|
||||
func ApiRoot(r *gin.RouterGroup) {
|
||||
|
||||
ApiStatic(r.Group("/static"))
|
||||
ApiUser(r.Group("/users"))
|
||||
ApiFiles(r.Group("/files"))
|
||||
ApiPurchase(r.Group("/purchase"))
|
||||
|
||||
r.GET("/", func(ctx *gin.Context) {
|
||||
ReturnJson(ctx, "apiOK", nil)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ops/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type CostItem struct {
|
||||
Cost int `json:"cost"` // 必须,非负
|
||||
CostT int `json:"costt"` // 必须,非负
|
||||
CurrencyType string `json:"currencytype"` // 必须
|
||||
Int int `json:"int"` // 必须
|
||||
Type string `json:"type"` // 必须
|
||||
}
|
||||
type From_purchase_addorder struct {
|
||||
Costs []CostItem `json:"costs"` //
|
||||
Link string `json:"link"` // 可选
|
||||
OrderStatus string `json:"order_status"` //
|
||||
PartName string `json:"partname"` // 可选
|
||||
Photos []string `json:"photos"` // 可选
|
||||
Remark string `json:"remark"` // 可选
|
||||
Styles string `json:"styles"` // 可选
|
||||
Title string `json:"title"` // 必须
|
||||
TrackingNumber string `json:"tracking_number"` // 可选
|
||||
UpdateTime string `json:"update_time"` // 可选
|
||||
}
|
||||
|
||||
func ApiPurchase(r *gin.RouterGroup) {
|
||||
|
||||
r.POST("/getorders", func(ctx *gin.Context) {
|
||||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||
if isAuth {
|
||||
fmt.Println(user)
|
||||
// DebugPrintJson(data)
|
||||
|
||||
type From_purchase_getorders struct {
|
||||
Search string
|
||||
Entries int
|
||||
Page int
|
||||
}
|
||||
|
||||
var jsondata From_purchase_getorders
|
||||
if err := mapstructure.Decode(data, &jsondata); err == nil {
|
||||
//fmt.Println(jsondata)
|
||||
|
||||
is_data_ok := true
|
||||
|
||||
if jsondata.Entries <= 0 || jsondata.Entries > 300 {
|
||||
is_data_ok = false
|
||||
}
|
||||
if jsondata.Page <= 0 {
|
||||
is_data_ok = false
|
||||
}
|
||||
|
||||
if is_data_ok {
|
||||
|
||||
//读取有多少条目
|
||||
var count int64
|
||||
models.DB.Model(&models.TabPurchaseOrder{}).Count(&count)
|
||||
//fmt.Println(count)
|
||||
|
||||
//读取条目
|
||||
var getorders []models.TabPurchaseOrder
|
||||
models.DB.Order("created_at DESC").Offset(jsondata.Entries * (jsondata.Page - 1)).Limit(jsondata.Entries).Find(&getorders)
|
||||
|
||||
ReturnJson(ctx, "apiOK", map[string]interface{}{
|
||||
"all_count": count,
|
||||
"all_orders": getorders,
|
||||
})
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "userCookieError", nil)
|
||||
}
|
||||
})
|
||||
|
||||
r.POST("/addorder", func(ctx *gin.Context) {
|
||||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||
if isAuth {
|
||||
|
||||
//需要处理提交的数据,接口有固定的数据格式,不允许乱搞
|
||||
//fmt.Println(isAuth)
|
||||
//fmt.Println(user)
|
||||
//DebugPrintJson(data)
|
||||
var jsondata From_purchase_addorder
|
||||
if err := mapstructure.Decode(data, &jsondata); err == nil {
|
||||
|
||||
//fmt.Println("转换后数据:\n", jsondata)
|
||||
|
||||
//数据比较混乱 在这里校验
|
||||
|
||||
//判断标题不为空
|
||||
is_data_ok := true
|
||||
if jsondata.Title == "" {
|
||||
is_data_ok = false
|
||||
}
|
||||
|
||||
//判断数量与价格是否为负数
|
||||
for i := 0; i < len(jsondata.Costs); i++ {
|
||||
if jsondata.Costs[i].Cost <= 0 {
|
||||
is_data_ok = false
|
||||
}
|
||||
if jsondata.Costs[i].Int <= 0 {
|
||||
is_data_ok = false
|
||||
}
|
||||
}
|
||||
|
||||
//判断图片是否为哈希值
|
||||
for i := 0; i < len(jsondata.Photos); i++ {
|
||||
//判断字符串是否包含标点符号
|
||||
if models.IsContainsSpecialChar(jsondata.Photos[i]) {
|
||||
is_data_ok = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//判断时间字符串是否合法
|
||||
uptime, e := models.StringToTimePtr(jsondata.UpdateTime)
|
||||
if e != nil {
|
||||
is_data_ok = false
|
||||
}
|
||||
|
||||
if is_data_ok {
|
||||
//校验通过
|
||||
//载入数据库
|
||||
|
||||
photos, _ := json.Marshal(jsondata.Photos) //把图片数组转换成字符串
|
||||
new_data := models.TabPurchaseOrder{
|
||||
UserID: user.ID,
|
||||
Title: jsondata.Title,
|
||||
Remark: jsondata.Remark,
|
||||
Photos: datatypes.JSON(photos),
|
||||
Link: jsondata.Link,
|
||||
PartName: jsondata.PartName,
|
||||
Styles: jsondata.Styles,
|
||||
UpdateTime: uptime,
|
||||
TrackingNumber: jsondata.TrackingNumber,
|
||||
OrderStatus: jsondata.OrderStatus,
|
||||
}
|
||||
models.DB.Create(&new_data)
|
||||
|
||||
for i := 0; i < len(jsondata.Costs); i++ {
|
||||
new_cost_data := models.TabPurchaseCosts{
|
||||
Price: jsondata.Costs[i].Cost,
|
||||
Quantity: jsondata.Costs[i].Int,
|
||||
UserID: user.ID,
|
||||
OrderID: new_data.ID,
|
||||
}
|
||||
models.DB.Create(&new_cost_data)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "userCookieError", nil)
|
||||
}
|
||||
|
||||
ReturnJson(ctx, "apiErr", nil)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"ops/models"
|
||||
"path"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//处理api的静态内容
|
||||
|
||||
func ApiStatic(r *gin.RouterGroup) {
|
||||
r.GET("/avatar/:filename", func(ctx *gin.Context) {
|
||||
filename := ctx.Param("filename")
|
||||
dst := path.Join(models.ConfigsFile.Pahts["avatar"], filename)
|
||||
if models.FileExists(dst) {
|
||||
ctx.File(dst)
|
||||
} else {
|
||||
//找不到文件
|
||||
ctx.String(404, "file not found")
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
@@ -1,541 +0,0 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"ops/models"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
func ApiInit() {
|
||||
//用户模块初始化init
|
||||
fmt.Println("users init")
|
||||
|
||||
//创建admin用户
|
||||
var user models.TabUser_
|
||||
user.Name = "admin"
|
||||
|
||||
if models.DB.Where(&user).First(&user).Error == nil {
|
||||
|
||||
} else {
|
||||
//fmt.Println("用户不存在")
|
||||
|
||||
//对密码加盐
|
||||
user.Salt = models.RandStr32()
|
||||
user.Pass = "adminpassword"
|
||||
models.HashUserPass(&user)
|
||||
models.DB.Create(&user) // 传入指针
|
||||
}
|
||||
|
||||
//创建admin group
|
||||
var usergroup models.TabUserGroups_
|
||||
usergroup.Name = "admins"
|
||||
if models.DB.Where(&usergroup).First(&usergroup).Error == nil {
|
||||
|
||||
} else {
|
||||
fmt.Println("用户组不存在")
|
||||
models.DB.Create(&usergroup) // 传入指针
|
||||
}
|
||||
|
||||
//创建用户与用户组绑定
|
||||
var usergroupbind models.TabUserGroupBinds_
|
||||
usergroupbind.UserID = user.ID
|
||||
usergroupbind.GroupID = usergroup.ID
|
||||
|
||||
if models.DB.Where(&usergroupbind).First(&usergroupbind).Error == nil {
|
||||
|
||||
} else {
|
||||
models.DB.Create(&usergroupbind) // 传入指针
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type From_user_add struct {
|
||||
Useremail string `json:"useremail"`
|
||||
Username string `json:"username"`
|
||||
Userpass string `json:"userpass"`
|
||||
}
|
||||
|
||||
type From_user_login struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Remember bool `json:"remember"`
|
||||
}
|
||||
|
||||
type From_user_updateinfo struct {
|
||||
Username string `json:"username"`
|
||||
Remark string `json:"remark"`
|
||||
Birthday string `json:"birthday"`
|
||||
}
|
||||
|
||||
type From_user_changeemail struct {
|
||||
Newemail string `json:"newemail"`
|
||||
}
|
||||
|
||||
type From_user_changepass struct {
|
||||
Oldpass string `json:"oldpass"`
|
||||
Newpass string `json:"newpass"`
|
||||
}
|
||||
|
||||
func AuthenticationAuthorityFromCookie(c string) (*models.TabUser_, error) {
|
||||
|
||||
if c != "" {
|
||||
cookie := models.TabCookie_{
|
||||
Value: c,
|
||||
}
|
||||
if models.DB.Where(&cookie).First(&cookie).Error == nil {
|
||||
//找到cookie,验证cookie有效性,以及更新cookie
|
||||
if models.CheckCookiesAndUpdate(&cookie) {
|
||||
//cookie有效
|
||||
//载入user
|
||||
user := models.TabUser_{
|
||||
ID: cookie.UserID,
|
||||
}
|
||||
models.DB.Where(&user).First(&user)
|
||||
return &user, nil
|
||||
} else {
|
||||
return nil, errors.New("cookie 过期")
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("cookie Not Fund")
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("cookie 参数错误")
|
||||
}
|
||||
}
|
||||
|
||||
func AuthenticationAuthority(ctx *gin.Context) (bool, models.TabUser_, map[string]interface{}) {
|
||||
var user models.TabUser_
|
||||
|
||||
data, cookieval := SeparateData(ctx)
|
||||
//fmt.Println("cookieis" + cookieval)
|
||||
if cookieval != "" {
|
||||
cookie := models.TabCookie_{
|
||||
Value: cookieval,
|
||||
}
|
||||
if models.DB.Where(&cookie).First(&cookie).Error == nil {
|
||||
//找到cookie,验证cookie有效性,以及更新cookie
|
||||
if models.CheckCookiesAndUpdate(&cookie) {
|
||||
//cookie有效
|
||||
//载入user
|
||||
user := models.TabUser_{
|
||||
ID: cookie.UserID,
|
||||
}
|
||||
models.DB.Where(&user).First(&user)
|
||||
|
||||
return true, user, data
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "userCookieExpired", nil)
|
||||
return false, user, nil
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "userCookieNotFund", nil)
|
||||
return false, user, nil
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "userCookieError", nil)
|
||||
return false, user, nil
|
||||
}
|
||||
|
||||
//return false, user
|
||||
}
|
||||
|
||||
func ApiUser(r *gin.RouterGroup) {
|
||||
|
||||
r.GET("/test", func(ctx *gin.Context) {
|
||||
ReturnJson(ctx, "apiOK", nil)
|
||||
})
|
||||
r.POST("/test", func(ctx *gin.Context) {
|
||||
ReturnJson(ctx, "apiOK", nil)
|
||||
})
|
||||
|
||||
//修改用户密码
|
||||
r.POST("/changePassword", func(ctx *gin.Context) {
|
||||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||
if isAuth {
|
||||
var jsonData From_user_changepass
|
||||
if err := mapstructure.Decode(data, &jsonData); err == nil {
|
||||
//验证旧密码
|
||||
fmt.Println(user)
|
||||
//转换旧密码
|
||||
olduser := models.TabUser_{
|
||||
Pass: jsonData.Oldpass,
|
||||
Salt: user.Salt,
|
||||
}
|
||||
models.HashUserPass(&olduser)
|
||||
if olduser.Pass == user.Pass {
|
||||
//旧密码正确,更新新密码
|
||||
var userupdate models.TabUser_
|
||||
userupdate.Pass = jsonData.Newpass
|
||||
userupdate.Salt = models.RandStr32()
|
||||
models.HashUserPass(&userupdate)
|
||||
models.DB.Model(&user).Updates(&userupdate)
|
||||
ReturnJson(ctx, "apiOK", nil)
|
||||
} else {
|
||||
//旧密码错误
|
||||
ReturnJson(ctx, "userPassIncorrect", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
//更新用户邮箱
|
||||
r.POST("/changeEmail", func(ctx *gin.Context) {
|
||||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||
if isAuth {
|
||||
var jsonData From_user_changeemail
|
||||
if err := mapstructure.Decode(data, &jsonData); err == nil {
|
||||
//判断新邮箱格式
|
||||
if models.IsEmailValid(jsonData.Newemail) {
|
||||
var userupdate models.TabUser_
|
||||
userupdate.Email = jsonData.Newemail
|
||||
models.DB.Model(&user).Updates(&userupdate)
|
||||
ReturnJson(ctx, "apiOK", nil)
|
||||
} else {
|
||||
ReturnJson(ctx, "userEmailFormatError", nil)
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//修改用户头像
|
||||
r.POST("/updateAvatar", func(ctx *gin.Context) {
|
||||
cookie := ctx.PostForm("cookie")
|
||||
user, err := AuthenticationAuthorityFromCookie(cookie)
|
||||
if err == nil {
|
||||
file, err := ctx.FormFile("file")
|
||||
if err == nil {
|
||||
if file.Filename != "" {
|
||||
//限制文件大小
|
||||
if file.Size > 512 {
|
||||
//头像裁剪过限制1M应该差不多
|
||||
if file.Size < 1048576 {
|
||||
|
||||
//判断mime
|
||||
mimeType, err := models.GetFileMime(file)
|
||||
if err == nil {
|
||||
|
||||
file_extname := models.ConfigsFile.AllowImageMime[mimeType]
|
||||
if file_extname != "" {
|
||||
|
||||
//haxi文件
|
||||
|
||||
file_hashi_name, err := models.SHA256HashFile(file)
|
||||
if err == nil {
|
||||
|
||||
dst := path.Join(models.ConfigsFile.Pahts["avatar"], file_hashi_name+file_extname)
|
||||
|
||||
var is_save_ok = false
|
||||
//判断文件是否存在避免重复保存
|
||||
if models.FileExists(dst) {
|
||||
//fmt.Println("文件存在")
|
||||
is_save_ok = true
|
||||
ReturnJson(ctx, "apiOK", nil)
|
||||
} else {
|
||||
//fmt.Println("文件no存在")
|
||||
ferr := ctx.SaveUploadedFile(file, dst)
|
||||
if ferr == nil {
|
||||
//文件保存成功
|
||||
//fmt.Print("save_ok")
|
||||
is_save_ok = true
|
||||
ReturnJson(ctx, "apiOK", nil)
|
||||
} else {
|
||||
fmt.Print(ferr)
|
||||
ReturnJson(ctx, "postErr", nil)
|
||||
}
|
||||
|
||||
}
|
||||
if is_save_ok {
|
||||
//修改数据库内容
|
||||
var user_info_fund models.TabUserInfo_
|
||||
user_info_fund.UserID = user.ID
|
||||
|
||||
var user_update_avatar models.TabUserInfo_
|
||||
user_update_avatar.AvatarPath = file_hashi_name + file_extname
|
||||
|
||||
models.DB.Where(&user_info_fund).Updates(&user_update_avatar)
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "postErr", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "file_mime_err", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "postErr", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "file_size_err", nil)
|
||||
}
|
||||
} else {
|
||||
ReturnJson(ctx, "file_size_err", nil)
|
||||
}
|
||||
} else {
|
||||
ReturnJson(ctx, "file_name_err", nil)
|
||||
}
|
||||
} else {
|
||||
ReturnJson(ctx, "file_get_err", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "userCookieError", nil)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
//更新用户info
|
||||
r.POST("/updateInfo", func(ctx *gin.Context) {
|
||||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||
if isAuth {
|
||||
var jsonData From_user_updateinfo
|
||||
|
||||
if err := mapstructure.Decode(data, &jsonData); err == nil {
|
||||
// fmt.Println("updateinfo data is", jsonData)
|
||||
// fmt.Println(user)
|
||||
t, err := time.Parse("2006-01-02", jsonData.Birthday)
|
||||
if err == nil {
|
||||
var userinfo models.TabUserInfo_
|
||||
userinfo.UserID = user.ID
|
||||
|
||||
var userinfoupdate models.TabUserInfo_
|
||||
userinfoupdate.UserID = user.ID
|
||||
userinfoupdate.CreatedAt = time.Now()
|
||||
userinfoupdate.Username = jsonData.Username
|
||||
userinfoupdate.Birthdate = t
|
||||
userinfoupdate.FirstName = jsonData.Remark
|
||||
|
||||
//先查找是否有记录
|
||||
if models.DB.Where(&userinfo).First(&userinfo).Error == nil {
|
||||
//有记录,更新
|
||||
models.DB.Model(&userinfo).Updates(&userinfoupdate)
|
||||
} else {
|
||||
//无记录,创建
|
||||
models.DB.Create(&userinfoupdate) // 传入指针
|
||||
|
||||
}
|
||||
|
||||
ReturnJson(ctx, "apiOK", nil)
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
//通过cookie获取用户info
|
||||
r.POST("/getinfo", func(ctx *gin.Context) {
|
||||
isAuth, user, _ := AuthenticationAuthority(ctx)
|
||||
if isAuth {
|
||||
//载入用户info
|
||||
var userinfo models.TabUserInfo_
|
||||
userinfo.UserID = user.ID
|
||||
//fmt.Println(userInfo)
|
||||
var redata map[string]interface{} = make(map[string]interface{})
|
||||
if models.DB.Where(&userinfo).First(&userinfo).Error == nil {
|
||||
redata["userInfo"] = userinfo
|
||||
} else {
|
||||
redata["userInfo"] = nil
|
||||
}
|
||||
|
||||
user.Pass = ""
|
||||
user.Salt = ""
|
||||
|
||||
redata["user"] = user
|
||||
|
||||
ReturnJson(ctx, "apiOK", redata)
|
||||
|
||||
}
|
||||
// _, cookieval := SeparateData(ctx)
|
||||
// //fmt.Println("cookieis" + cookieval)
|
||||
// if cookieval != "" {
|
||||
// cookie := models.TabCookie_{
|
||||
// Value: cookieval,
|
||||
// }
|
||||
// if models.DB.Where(&cookie).First(&cookie).Error == nil {
|
||||
// //找到cookie,验证cookie有效性,以及更新cookie
|
||||
// if models.CheckCookiesAndUpdate(&cookie) {
|
||||
// //cookie有效
|
||||
// //返回最新cookie
|
||||
// redata := map[string]interface{}{
|
||||
// "cookie": cookie,
|
||||
// }
|
||||
// //载入用户info
|
||||
// userInfo := models.TabFileInfo_{
|
||||
// UserID: cookie.UserID,
|
||||
// }
|
||||
// if models.DB.Where(&userInfo).First(&userInfo).Error == nil {
|
||||
// redata["userInfo"] = userInfo
|
||||
// } else {
|
||||
// redata["userInfo"] = nil
|
||||
// }
|
||||
|
||||
// //载入user
|
||||
// user := models.TabUser_{
|
||||
// ID: cookie.UserID,
|
||||
// }
|
||||
// models.DB.Where(&user).First(&user)
|
||||
// user.Pass = ""
|
||||
// user.Salt = ""
|
||||
|
||||
// redata["user"] = user
|
||||
|
||||
// ReturnJson(ctx, "apiOK", redata)
|
||||
|
||||
// } else {
|
||||
// ReturnJson(ctx, "userCookieExpired", nil)
|
||||
// }
|
||||
|
||||
// } else {
|
||||
// ReturnJson(ctx, "userCookieNotFund", nil)
|
||||
// }
|
||||
|
||||
// } else {
|
||||
// ReturnJson(ctx, "userCookieError", nil)
|
||||
// }
|
||||
|
||||
})
|
||||
//用户登陆
|
||||
r.POST("/login", func(ctx *gin.Context) {
|
||||
var loginuser From_user_login
|
||||
data, _ := SeparateData(ctx)
|
||||
if data != nil {
|
||||
if err := mapstructure.Decode(data, &loginuser); err == nil {
|
||||
if loginuser.Username != "" && loginuser.Password != "" {
|
||||
//传入的数据都ok,获取用户信息
|
||||
|
||||
getuser := models.TabUser_{
|
||||
Name: loginuser.Username,
|
||||
}
|
||||
|
||||
if models.DB.Where(&getuser).First(&getuser).Error == nil {
|
||||
//倒入数据
|
||||
user := models.TabUser_{
|
||||
Pass: loginuser.Password, //密码明文
|
||||
Salt: getuser.Salt, //保存的盐制
|
||||
}
|
||||
//哈希密
|
||||
models.HashUserPass(&user)
|
||||
if user.Pass == getuser.Pass {
|
||||
//用户密码正确,生成cookie
|
||||
cookie := models.TabCookie_{
|
||||
UserID: getuser.ID,
|
||||
Name: "login",
|
||||
Value: models.RandStr32(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(time.Duration(models.ConfigsUser.CookieTimeout) * time.Second), //计算过期时间,
|
||||
Remember: loginuser.Remember,
|
||||
}
|
||||
models.DB.Create(&cookie) // 传入指针
|
||||
|
||||
redata := map[string]interface{}{
|
||||
"cookie": cookie,
|
||||
}
|
||||
|
||||
ReturnJson(ctx, "apiOK", redata)
|
||||
} else {
|
||||
ReturnJson(ctx, "userPassIncorrect", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
//用户不存在
|
||||
ReturnJson(ctx, "userNameNoFund", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", map[string]interface{}{"errcode": "2"})
|
||||
}
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", map[string]interface{}{"errcode": "1"})
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "postErr", nil)
|
||||
}
|
||||
})
|
||||
|
||||
//用户注册
|
||||
r.POST("/register", func(ctx *gin.Context) {
|
||||
//转换传进来的数据
|
||||
var jsonData From_user_add
|
||||
|
||||
data, _ := SeparateData(ctx)
|
||||
|
||||
if data != nil {
|
||||
if err := mapstructure.Decode(data, &jsonData); err == nil {
|
||||
//转换字段
|
||||
newUser := models.TabUser_{
|
||||
Name: jsonData.Username,
|
||||
Email: jsonData.Useremail,
|
||||
Pass: jsonData.Userpass, // 实际应替换为哈希值
|
||||
Date: time.Now(),
|
||||
// Date 字段无需赋值,数据库会自动填充默认值
|
||||
}
|
||||
if newUser.Name != "" && newUser.Pass != "" && newUser.Email != "" {
|
||||
|
||||
//用户名是唯一的,先读取是否有这个用户名
|
||||
var user models.TabUser_
|
||||
user.Name = newUser.Name
|
||||
|
||||
if models.DB.Where(&user).First(&user).Error == nil {
|
||||
//fmt.Println("找到用户:", user.ID)
|
||||
ReturnJson(ctx, "userNameDup", nil)
|
||||
} else {
|
||||
//fmt.Println("用户不存在")
|
||||
|
||||
//对密码加盐
|
||||
newUser.Salt = models.RandStr32()
|
||||
|
||||
//对用户的密码进行哈希替换
|
||||
models.HashUserPass(&newUser)
|
||||
|
||||
models.DB.Create(&newUser) // 传入指针
|
||||
|
||||
//创建用户后写一个log
|
||||
|
||||
models.LogAdd(ctx, "New user id:"+strconv.Itoa(int(newUser.ID)))
|
||||
|
||||
ReturnJson(ctx, "apiOK", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
|
||||
}
|
||||
} else {
|
||||
ReturnJson(ctx, "postErr", nil)
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"ops/models"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func file_save() {
|
||||
|
||||
}
|
||||
|
||||
func ApiFiles(r *gin.RouterGroup) {
|
||||
|
||||
//getfile := r.Group("/get") //定义上传组
|
||||
r.GET("/:mode/:hash", func(ctx *gin.Context) {
|
||||
hash := ctx.Param("hash")
|
||||
mode := ctx.Param("mode")
|
||||
// filename := ctx.Param("filename")
|
||||
// fmt.Println(filename)
|
||||
|
||||
download := false
|
||||
isPartOK := false
|
||||
|
||||
if mode == "get" {
|
||||
isPartOK = true
|
||||
download = true
|
||||
}
|
||||
if mode == "download" {
|
||||
isPartOK = true
|
||||
download = false
|
||||
}
|
||||
if isPartOK {
|
||||
file_info := models.TabFileInfo_{
|
||||
Sha256: hash,
|
||||
}
|
||||
if models.DB.Where(&file_info).First(&file_info).Error == nil {
|
||||
ReturnFile(ctx, &file_info, download)
|
||||
} else {
|
||||
//fmt.Println("not fund")
|
||||
ReturnJson(ctx, "file_not_found", nil)
|
||||
}
|
||||
} else {
|
||||
ReturnJson(ctx, "file_part_err", nil)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
upload := r.Group("/upload") //定义上传组
|
||||
//上传文件的总接口,能上传什么文件应该由后端决定,前端仅做相应限制
|
||||
|
||||
upload.POST("/image", func(ctx *gin.Context) {
|
||||
|
||||
cookie := ctx.PostForm("cookie") //首先需要判断用户是否登录
|
||||
|
||||
//通过cookie获取用户信息
|
||||
user, err := AuthenticationAuthorityFromCookie(cookie)
|
||||
if err == nil {
|
||||
file, err := ctx.FormFile("file")
|
||||
|
||||
if err == nil {
|
||||
if file.Filename != "" {
|
||||
//限制文件大小
|
||||
if file.Size > 512 {
|
||||
if file.Size < int64(models.ConfigsFile.MaxSize) {
|
||||
|
||||
//判断文件mime是否合法
|
||||
// 打开文件流
|
||||
src_mime, _ := file.Open()
|
||||
defer src_mime.Close()
|
||||
// 读取前512字节用于MIME检测
|
||||
buffer := make([]byte, 512)
|
||||
io.ReadFull(src_mime, buffer)
|
||||
// 检测MIME类型
|
||||
mimeType := http.DetectContentType(buffer)
|
||||
file_extname := models.ConfigsFile.AllowImageMime[mimeType]
|
||||
if file_extname != "" {
|
||||
filename := filepath.Base(file.Filename) // 防御性处理路径分隔符
|
||||
// 计算哈希值
|
||||
hash_str, err := models.SHA256HashFile(file)
|
||||
if err == nil {
|
||||
//fmt.Println(hash_str)
|
||||
//fmt.Println(filename)
|
||||
//这是上传的真实路径
|
||||
dst := path.Join(models.ConfigsFile.Pahts["image"], hash_str)
|
||||
//fmt.Println(dst)
|
||||
//判断文件是否存在避免重复保存
|
||||
if models.FileExists(dst) {
|
||||
//fmt.Println("文件存在")
|
||||
|
||||
} else {
|
||||
//fmt.Println("文件no存在")
|
||||
ferr := ctx.SaveUploadedFile(file, dst)
|
||||
if ferr == nil {
|
||||
//文件保存成功
|
||||
|
||||
} else {
|
||||
|
||||
ReturnJson(ctx, "file_save_err", nil)
|
||||
ctx.Abort() //end
|
||||
return
|
||||
}
|
||||
}
|
||||
//记录到数据库
|
||||
//先检查数据库有没有数据
|
||||
fund_file_info := models.TabFileInfo_{
|
||||
Name: filename,
|
||||
Sha256: hash_str,
|
||||
Mime: mimeType,
|
||||
Type: "image",
|
||||
UserID: user.ID,
|
||||
}
|
||||
fund_file_info2 := models.TabFileInfo_{}
|
||||
|
||||
models.DB.Where(&fund_file_info).Find(&fund_file_info2)
|
||||
|
||||
if fund_file_info2.ID != 0 {
|
||||
fund_file_info2.Const += 1
|
||||
models.DB.Where(&fund_file_info).Updates(&fund_file_info2)
|
||||
} else {
|
||||
fund_file_info.Path = dst
|
||||
models.DB.Create(&fund_file_info) // 传入指针
|
||||
}
|
||||
|
||||
//返回后台存储的URL
|
||||
download_URL := path.Join("/api/files/download/", hash_str)
|
||||
get_URL := path.Join("/api/files/get/", hash_str)
|
||||
re := map[string]interface{}{
|
||||
"download": download_URL,
|
||||
"get": get_URL,
|
||||
"hash": hash_str,
|
||||
}
|
||||
|
||||
ReturnJson(ctx, "apiOK", re)
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "file_hash_err", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "file_mime_err", nil)
|
||||
}
|
||||
} else {
|
||||
ReturnJson(ctx, "file_size_err", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "file_size_err", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "file_name_err", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "file_get_err", nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "userCookieError", nil)
|
||||
}
|
||||
|
||||
//ReturnJson(ctx, "apiErr", nil)
|
||||
})
|
||||
|
||||
// r.GET("/upload", func(ctx *gin.Context) {
|
||||
// ReturnJson(ctx, "apiOK", nil)
|
||||
// })
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ops/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func DebugPrintJson(data map[string]interface{}) {
|
||||
p, _ := json.MarshalIndent(data, "", " ")
|
||||
fmt.Println("\n", string(p))
|
||||
|
||||
}
|
||||
|
||||
func ReturnJson(ctx *gin.Context, errMsg string, data map[string]interface{}) {
|
||||
var errCode = ErrorCode[errMsg]
|
||||
returnData := map[string]interface{}{}
|
||||
|
||||
// cookie, have_cookie := ctx.Get("cookie")
|
||||
// if have_cookie {
|
||||
// returnData["cookie"] = cookie
|
||||
// }
|
||||
|
||||
returnData["err_code"] = errCode
|
||||
returnData["err_msg"] = errMsg
|
||||
if data != nil {
|
||||
returnData["return"] = data
|
||||
}
|
||||
|
||||
ctx.JSON(200, &returnData)
|
||||
|
||||
//ctx.Abort()
|
||||
|
||||
}
|
||||
|
||||
func ReturnFile(ctx *gin.Context, file_info *models.TabFileInfo_, preview bool) {
|
||||
if preview {
|
||||
ctx.File(file_info.Path)
|
||||
} else {
|
||||
//需要从数据库拉取原始文件名
|
||||
ctx.FileAttachment(file_info.Path, file_info.Name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
@echo off
|
||||
REM OPS Backend 开发服务器启动脚本
|
||||
REM 设置 CGO 和 GCC 路径
|
||||
set CGO_ENABLED=1
|
||||
set PATH=C:\TDM-GCC-64\bin;%PATH%
|
||||
|
||||
cd /d %~dp0
|
||||
|
||||
echo Starting OPS Backend (with auto-reload)...
|
||||
echo.
|
||||
|
||||
REM 使用 gin 自动重载(比 fresh 更稳定)
|
||||
go run . -port 8080
|
||||
@@ -1,14 +0,0 @@
|
||||
root: .
|
||||
tmp_path: ./tmp
|
||||
build_name: runner-build
|
||||
build_log: runner-build-errors.log
|
||||
valid_ext: .go, .tpl, .tmpl, .html
|
||||
no_rebuild_ext: .tpl, .tmpl, .html
|
||||
ignored: assets, tmp, vendor, frontend, dist
|
||||
build_delay: 600
|
||||
colors: 1
|
||||
log_color_main: cyan
|
||||
log_color_build: yellow
|
||||
log_color_runner: green
|
||||
log_color_watcher: magenta
|
||||
log_color_app:
|
||||
@@ -1,19 +0,0 @@
|
||||
@echo off
|
||||
echo Starting OPS backend server (refactored version)...
|
||||
|
||||
REM 检查前端dist目录是否存在
|
||||
if not exist "./dist" (
|
||||
echo WARNING: Frontend build not found at ./dist
|
||||
echo Please build frontend first or copy build files to ./dist
|
||||
echo.
|
||||
)
|
||||
|
||||
REM SQLite 需要 CGO
|
||||
set CGO_ENABLED=1
|
||||
|
||||
REM 运行新的重构版本
|
||||
echo Running new refactored backend...
|
||||
echo.
|
||||
go run .
|
||||
|
||||
pause
|
||||
@@ -1,6 +1,7 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ops/models"
|
||||
"time"
|
||||
@@ -57,11 +58,25 @@ type TabPurchaseFileBind struct {
|
||||
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
||||
}
|
||||
|
||||
type TabPurchaseLog struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
OrderID uint `gorm:"not null;index;comment:关联OrderID"`
|
||||
UserID uint `gorm:"not null;comment:操作人ID"`
|
||||
ActionType string `gorm:"size:50;not null;comment:操作类型: create-创建 update-修改 delete-删除 query-查询"`
|
||||
OldContent string `gorm:"type:text;comment:修改前内容(JSON)"`
|
||||
NewContent string `gorm:"type:text;comment:修改后内容(JSON)"`
|
||||
IP string `gorm:"size:50;comment:操作IP"`
|
||||
Remark string `gorm:"size:500;comment:备注/操作描述"`
|
||||
|
||||
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime;comment:操作时间"`
|
||||
}
|
||||
|
||||
func ApiPurchaseInit() {
|
||||
|
||||
models.DB.AutoMigrate(&TabPurchaseOrder{})
|
||||
models.DB.AutoMigrate(&TabPurchaseCosts{})
|
||||
models.DB.AutoMigrate(&TabPurchaseFileBind{})
|
||||
models.DB.AutoMigrate(&TabPurchaseLog{})
|
||||
|
||||
}
|
||||
|
||||
@@ -211,6 +226,16 @@ func ApiPurchase(r *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
}
|
||||
newContent, _ := json.Marshal(jsondata) // 👈 转 JSON
|
||||
tosqllog := TabPurchaseLog{
|
||||
UserID: user.ID,
|
||||
OrderID: new_data.ID,
|
||||
ActionType: "create",
|
||||
NewContent: string(newContent), // 👈 直接赋值
|
||||
OldContent: "",
|
||||
IP: ctx.ClientIP(),
|
||||
}
|
||||
models.DB.Debug().Create(&tosqllog)
|
||||
|
||||
ReturnJson(ctx, "apiOK", nil)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user