This commit is contained in:
2026-04-13 19:34:28 +08:00
parent 32c7fb6e9a
commit 41f4f453ed
2041 changed files with 55 additions and 481027 deletions
-19
View File
@@ -1,19 +0,0 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
.DS_Store
/dist
/data
/tmp
/build
/test
*.db
OPSYS
-193
View File
@@ -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
- 性能指标收集
- 错误聚合报告
-112
View File
@@ -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
}
-120
View File
@@ -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) {}
-18
View File
@@ -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/
-57
View File
@@ -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
-23
View File
@@ -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
}
-4
View File
@@ -1,4 +0,0 @@
export PATH=$PATH:$(go env GOPATH)/bin
fresh
-70
View File
@@ -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
)
-156
View File
@@ -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=
-62
View File
@@ -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"
-154
View File
@@ -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,
})
}
-35
View File
@@ -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)
}
-142
View File
@@ -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
}
-139
View File
@@ -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")
}
}
}
-102
View File
@@ -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()
-57
View File
@@ -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
}
-108
View File
@@ -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)
}
-58
View File
@@ -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
}
-50
View File
@@ -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)
}
-167
View File
@@ -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
}
-98
View File
@@ -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,
})
}
-62
View File
@@ -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)
})
}
-179
View File
@@ -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)
})
}
-25
View File
@@ -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")
}
})
}
-541
View File
@@ -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)
}
})
}
-174
View File
@@ -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)
// })
}
-46
View File
@@ -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)
}
}
-13
View File
@@ -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
-14
View File
@@ -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:
-19
View File
@@ -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
+25
View File
@@ -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)