From bce3d25b3b179e745cb7e7621b96ffb5bca48897 Mon Sep 17 00:00:00 2001 From: SugarMGP <2350745751@qq.com> Date: Tue, 3 Dec 2024 20:10:46 +0800 Subject: [PATCH 1/9] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84AES=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/config.go | 69 ----------------------- app/config/encrypt.go | 24 -------- app/config/init.go | 19 ------- app/midwares/checkInit.go | 17 ------ app/models/config.go | 11 ---- app/services/userService/create.go | 5 +- app/services/userService/getUser.go | 17 ++++-- app/services/userService/utils.go | 22 +++++--- app/utils/aes/aes.go | 86 +++++++++++++++++++++++++++++ app/utils/aestools.go | 74 ------------------------- config.example.yaml | 3 + config/database/migrations.go | 1 - config/router/router.go | 2 +- docs/README.md | 7 --- main.go | 4 ++ 15 files changed, 125 insertions(+), 236 deletions(-) delete mode 100644 app/config/config.go delete mode 100644 app/config/encrypt.go delete mode 100644 app/config/init.go delete mode 100644 app/midwares/checkInit.go delete mode 100644 app/models/config.go create mode 100644 app/utils/aes/aes.go delete mode 100644 app/utils/aestools.go diff --git a/app/config/config.go b/app/config/config.go deleted file mode 100644 index 8a4366a..0000000 --- a/app/config/config.go +++ /dev/null @@ -1,69 +0,0 @@ -package config - -import ( - "context" - "errors" - "time" - - "4u-go/app/models" - "4u-go/config/database" - "4u-go/config/redis" - "gorm.io/gorm" -) - -// 上下文用于 Redis 操作 -var ctx = context.Background() - -// getConfig 从 Redis 获取配置,如果不存在则从数据库中获取,并缓存到 Redis -func getConfig(key string) string { - val, err := redis.GlobalClient.Get(ctx, key).Result() - if err == nil { - return val - } - print(err) - var config = &models.Config{} - database.DB.Model(models.Config{}).Where( - &models.Config{ - Key: key, - }).First(&config) - - redis.GlobalClient.Set(ctx, key, config.Value, 0) - return config.Value -} - -// setConfig 设置指定的配置项,如果不存在则创建新的配置。 -func setConfig(key, value string) error { - redis.GlobalClient.Set(ctx, key, value, 0) - var config models.Config - result := database.DB.Where("`key` = ?", key).First(&config) - if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { - return result.Error - } - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - config = models.Config{ - Key: key, - Value: value, - UpdateTime: time.Now(), - } - result = database.DB.Create(&config) - } else { - config.Value = value - config.UpdateTime = time.Now() - result = database.DB.Updates(&config) - } - return result.Error -} - -// checkConfig 检查指定的配置项是否存在于 Redis 中。 -func checkConfig(key string) bool { - intCmd := redis.GlobalClient.Exists(ctx, key) - return intCmd.Val() == 1 -} - -func delConfig(key string) error { - redis.GlobalClient.Del(ctx, key) - res := database.DB.Where(&models.Config{ - Key: key, - }).Delete(models.Config{}) - return res.Error -} diff --git a/app/config/encrypt.go b/app/config/encrypt.go deleted file mode 100644 index ee8813f..0000000 --- a/app/config/encrypt.go +++ /dev/null @@ -1,24 +0,0 @@ -package config - -// encryptKey 是用于加密的配置键。 -const encryptKey = "encryptKey" - -// SetEncryptKey 设置加密密钥的值 -func SetEncryptKey(value string) error { - return setConfig(encryptKey, value) -} - -// GetEncryptKey 获取当前配置的加密密钥值 -func GetEncryptKey() string { - return getConfig(encryptKey) -} - -// IsSetEncryptKey 检查是否设置了加密密钥 -func IsSetEncryptKey() bool { - return checkConfig(encryptKey) -} - -// DelEncryptKey 删除加密密钥 -func DelEncryptKey() error { - return delConfig(encryptKey) -} diff --git a/app/config/init.go b/app/config/init.go deleted file mode 100644 index 196ffc6..0000000 --- a/app/config/init.go +++ /dev/null @@ -1,19 +0,0 @@ -package config - -// initKey 是用于初始化状态的配置键。 -const initKey = "initKey" - -// SetInit 设置初始化状态为 "True"。 -func SetInit() error { - return setConfig(initKey, "True") -} - -// ResetInit 设置初始化状态为 "False"。 -func ResetInit() error { - return setConfig(initKey, "False") -} - -// GetInit 获取当前的初始化状态。 -func GetInit() bool { - return getConfig(initKey) == "True" -} diff --git a/app/midwares/checkInit.go b/app/midwares/checkInit.go deleted file mode 100644 index 2004184..0000000 --- a/app/midwares/checkInit.go +++ /dev/null @@ -1,17 +0,0 @@ -package midwares - -import ( - "4u-go/app/apiException" - "4u-go/app/config" - "github.com/gin-gonic/gin" -) - -// CheckInit 中间件用于检查系统是否已初始化。 -func CheckInit(c *gin.Context) { - inited := config.GetInit() - if !inited { - apiException.AbortWithException(c, apiException.NotInit, nil) - return - } - c.Next() -} diff --git a/app/models/config.go b/app/models/config.go deleted file mode 100644 index 8cefd97..0000000 --- a/app/models/config.go +++ /dev/null @@ -1,11 +0,0 @@ -package models - -import "time" - -// Config 系统配置项的结构体 -type Config struct { - ID uint `gorm:"primaryKey"` // ID 是配置项的唯一标识 - Key string // Key 是配置项的键,必须唯一且不能为空 - Value string // Value 是配置项的值,不能为空 - UpdateTime time.Time `gorm:"comment:'设置时间';type:timestamp"` // UpdateTime 是配置项的最后更新时间 -} diff --git a/app/services/userService/create.go b/app/services/userService/create.go index 0fd0b37..68702b5 100644 --- a/app/services/userService/create.go +++ b/app/services/userService/create.go @@ -33,7 +33,10 @@ func CreateStudentUser( StudentID: studentID, } - EncryptUserKeyInfo(user) + err = EncryptUserKeyInfo(user) + if err != nil { + return nil, err + } res := database.DB.Create(&user) return user, res.Error diff --git a/app/services/userService/getUser.go b/app/services/userService/getUser.go index a521801..3a75fc7 100644 --- a/app/services/userService/getUser.go +++ b/app/services/userService/getUser.go @@ -17,7 +17,10 @@ func GetUserByWechatOpenID(openid string) (*models.User, error) { return nil, result.Error } - DecryptUserKeyInfo(&user) + err := DecryptUserKeyInfo(&user) + if err != nil { + return nil, err + } return &user, nil } @@ -29,11 +32,14 @@ func GetUserByStudentID(sid string) (*models.User, error) { StudentID: sid, }, ).First(&user) - if result.Error != nil { return nil, result.Error } - DecryptUserKeyInfo(&user) + + err := DecryptUserKeyInfo(&user) + if err != nil { + return nil, err + } return &user, nil } @@ -49,6 +55,9 @@ func GetUserByID(id uint) (*models.User, error) { return nil, result.Error } - DecryptUserKeyInfo(&user) + err := DecryptUserKeyInfo(&user) + if err != nil { + return nil, err + } return &user, nil } diff --git a/app/services/userService/utils.go b/app/services/userService/utils.go index 63e8a87..7922419 100644 --- a/app/services/userService/utils.go +++ b/app/services/userService/utils.go @@ -1,24 +1,30 @@ package userService import ( - "4u-go/app/config" "4u-go/app/models" - "4u-go/app/utils" + "4u-go/app/utils/aes" ) // DecryptUserKeyInfo 解密用户信息 -func DecryptUserKeyInfo(user *models.User) { - key := config.GetEncryptKey() +func DecryptUserKeyInfo(user *models.User) error { if user.PhoneNum != "" { - slt := utils.AesDecrypt(user.PhoneNum, key) + slt, err := aes.Decrypt(user.PhoneNum) + if err != nil { + return err + } user.PhoneNum = slt[0 : len(slt)-len(user.StudentID)] } + return nil } // EncryptUserKeyInfo 加密用户信息 -func EncryptUserKeyInfo(user *models.User) { - key := config.GetEncryptKey() +func EncryptUserKeyInfo(user *models.User) error { if user.PhoneNum != "" { - user.PhoneNum = utils.AesEncrypt(user.PhoneNum+user.StudentID, key) + num, err := aes.Encrypt(user.PhoneNum + user.StudentID) + if err != nil { + return err + } + user.PhoneNum = num } + return nil } diff --git a/app/utils/aes/aes.go b/app/utils/aes/aes.go new file mode 100644 index 0000000..c7954ea --- /dev/null +++ b/app/utils/aes/aes.go @@ -0,0 +1,86 @@ +package aes + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "errors" + + "4u-go/config/config" +) + +var encryptKey []byte + +// Init 读入 AES 密钥配置 +func Init() error { + key := config.Config.GetString("aes.encryptKey") + if len(key) != 16 && len(key) != 24 && len(key) != 32 { + return errors.New("AES 密钥长度必须为 16、24 或 32 字节") + } + encryptKey = []byte(key) + return nil +} + +// Encrypt AES 加密 +func Encrypt(orig string) (string, error) { + origData := []byte(orig) + + // 分组秘钥 + block, err := aes.NewCipher(encryptKey) + if err != nil { + return "", err + } + + // 进行 PKCS7 填充 + blockSize := block.BlockSize() + origData = PKCS7Padding(origData, blockSize) + + // 使用 CBC 加密模式 + blockMode := cipher.NewCBCEncrypter(block, encryptKey[:blockSize]) + cryted := make([]byte, len(origData)) + blockMode.CryptBlocks(cryted, origData) + + // 使用 RawURLEncoding 编码为 Base64,适合放入 URL + return base64.RawURLEncoding.EncodeToString(cryted), nil +} + +// Decrypt AES 解密 +func Decrypt(cryted string) (string, error) { + // 解码 Base64 字符串 + crytedByte, err := base64.RawURLEncoding.DecodeString(cryted) + if err != nil { + return "", err + } + + // 分组秘钥 + block, err := aes.NewCipher(encryptKey) + if err != nil { + return "", err + } + + // CBC 模式解密 + blockSize := block.BlockSize() + blockMode := cipher.NewCBCDecrypter(block, encryptKey[:blockSize]) + orig := make([]byte, len(crytedByte)) + blockMode.CryptBlocks(orig, crytedByte) + + // 去除 PKCS7 填充 + orig = PKCS7UnPadding(orig) + + return string(orig), nil +} + +// PKCS7Padding 填充数据,使长度为 blockSize 的倍数 +func PKCS7Padding(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padtext...) +} + +// PKCS7UnPadding 去除填充 +func PKCS7UnPadding(origData []byte) []byte { + length := len(origData) + unpadding := int(origData[length-1]) + return origData[:(length - unpadding)] +} diff --git a/app/utils/aestools.go b/app/utils/aestools.go deleted file mode 100644 index b79307b..0000000 --- a/app/utils/aestools.go +++ /dev/null @@ -1,74 +0,0 @@ -package utils - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "encoding/base64" - "fmt" -) - -// AesEncrypt aes加密 -func AesEncrypt(orig string, key string) string { - // 转成字节数组 - origData := []byte(orig) - k := []byte(key) - - // 分组秘钥 - block, err := aes.NewCipher(k) - if err != nil { - panic(fmt.Sprintf("key 长度必须 16/24/32长度: %s", err.Error())) - } - // 获取秘钥块的长度 - blockSize := block.BlockSize() - // 补全码 - origData = PKCS7Padding(origData, blockSize) - // 加密模式 - blockMode := cipher.NewCBCEncrypter(block, k[:blockSize]) - // 创建数组 - cryted := make([]byte, len(origData)) - // 加密 - blockMode.CryptBlocks(cryted, origData) - // 使用RawURLEncoding 不要使用StdEncoding - // 不要使用StdEncoding 放在url参数中回导致错误 - return base64.RawURLEncoding.EncodeToString(cryted) -} - -// AesDecrypt aes解密 -func AesDecrypt(cryted string, key string) string { - // 使用RawURLEncoding 不要使用StdEncoding - // 不要使用StdEncoding 放在url参数中回导致错误 - crytedByte, _ := base64.RawURLEncoding.DecodeString(cryted) //nolint:errcheck - k := []byte(key) - - // 分组秘钥 - block, err := aes.NewCipher(k) - if err != nil { - panic(fmt.Sprintf("key 长度必须 16/24/32长度: %s", err.Error())) - } - // 获取秘钥块的长度 - blockSize := block.BlockSize() - // 加密模式 - blockMode := cipher.NewCBCDecrypter(block, k[:blockSize]) - // 创建数组 - orig := make([]byte, len(crytedByte)) - // 解密 - blockMode.CryptBlocks(orig, crytedByte) - // 去补全码 - orig = PKCS7UnPadding(orig) - return string(orig) -} - -// PKCS7Padding 补码 -func PKCS7Padding(ciphertext []byte, blocksize int) []byte { - padding := blocksize - len(ciphertext)%blocksize - padtext := bytes.Repeat([]byte{byte(padding)}, padding) - return append(ciphertext, padtext...) -} - -// PKCS7UnPadding 去码 -func PKCS7UnPadding(origData []byte) []byte { - length := len(origData) - unpadding := int(origData[length-1]) - return origData[:(length - unpadding)] -} diff --git a/config.example.yaml b/config.example.yaml index 7c7c26e..3f7d8b4 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -29,6 +29,9 @@ user: admin: key: # 管理员密钥 +aes: # AES 密钥,长度必须为 16、24 或 32 字节 + encryptKey: + minio: # minio 存储配置 accessKey: # 用于身份验证的访问密钥 secretKey: # 用于身份验证的秘密密钥 diff --git a/config/database/migrations.go b/config/database/migrations.go index 3716902..6eed935 100755 --- a/config/database/migrations.go +++ b/config/database/migrations.go @@ -7,7 +7,6 @@ import ( func autoMigrate(db *gorm.DB) error { return db.AutoMigrate( - &models.Config{}, &models.User{}, &models.Announcement{}, &models.Activity{}, diff --git a/config/router/router.go b/config/router/router.go index df150ea..dc68570 100644 --- a/config/router/router.go +++ b/config/router/router.go @@ -16,7 +16,7 @@ import ( func Init(r *gin.Engine) { const pre = "/api" - api := r.Group(pre, midwares.CheckInit) + api := r.Group(pre) { user := api.Group("/user") { diff --git a/docs/README.md b/docs/README.md index 5630ad5..bbccada 100644 --- a/docs/README.md +++ b/docs/README.md @@ -68,13 +68,6 @@ cp config.example.yaml config.yaml copy config.example.yaml config.yaml ``` -在配置数据库后,向 config 表插入如下两条记录来完成初始化 - -| key | value | -|---|---| -| encryptKey | *16位的整数倍的字符串 | -| initKey | True | - 3. 启动程序 ```shell diff --git a/main.go b/main.go index 2375e53..68ff7fc 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "4u-go/app/midwares" + "4u-go/app/utils/aes" "4u-go/app/utils/log" "4u-go/config/config" "4u-go/config/database" @@ -27,6 +28,9 @@ func main() { r.NoRoute(midwares.HandleNotFound) log.ZapInit() redis.Init() + if err := aes.Init(); err != nil { + zap.L().Fatal(err.Error()) + } if err := database.Init(); err != nil { zap.L().Fatal(err.Error()) } From d389b436c708765e9123ee8bfe483f90a3e6aa28 Mon Sep 17 00:00:00 2001 From: SugarMGP <2350745751@qq.com> Date: Wed, 4 Dec 2024 15:45:14 +0800 Subject: [PATCH 2/9] =?UTF-8?q?perf:=20=E5=B0=86=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E7=9A=84=E5=9B=BE=E7=89=87=E8=BD=AC=E6=8D=A2=E4=B8=BAWebP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/objectController/upload.go | 34 +++++----- app/services/objectService/objectService.go | 70 ++++++++++++++++----- docs/README.md | 19 ++---- go.mod | 4 +- go.sum | 9 ++- 5 files changed, 89 insertions(+), 47 deletions(-) diff --git a/app/controllers/objectController/upload.go b/app/controllers/objectController/upload.go index 900ec9f..03d67fa 100644 --- a/app/controllers/objectController/upload.go +++ b/app/controllers/objectController/upload.go @@ -1,8 +1,8 @@ package objectController import ( + "bytes" "errors" - "io" "mime/multipart" "4u-go/app/apiException" @@ -26,22 +26,15 @@ func UploadFile(c *gin.Context) { } uploadType := data.UploadType - fileHeader := data.File - // 获取文件流 - file, err := data.File.Open() + fileSize := data.File.Size + fileData, err := objectService.ReadFileToBytes(data.File) if err != nil { - apiException.AbortWithException(c, apiException.ServerError, err) + apiException.AbortWithException(c, apiException.UploadFileError, err) return } - defer func(file multipart.File) { - err := file.Close() - if err != nil { - zap.L().Error("文件流关闭错误", zap.Error(err)) - } - }(file) // 获取文件信息 - contentType, fileExt, err := objectService.GetFileInfo(file, fileHeader, uploadType) + contentType, fileExt, err := objectService.GetFileInfo(fileData, fileSize, uploadType) if errors.Is(err, objectService.ErrSizeExceeded) { apiException.AbortWithException(c, apiException.FileSizeExceedError, err) return @@ -58,15 +51,22 @@ func UploadFile(c *gin.Context) { apiException.AbortWithException(c, apiException.ServerError, err) return } - _, err = file.Seek(0, io.SeekStart) - if err != nil { - apiException.AbortWithException(c, apiException.ServerError, err) - return + + if uploadType == objectService.TypeImage { + d, s, err := objectService.ConvertToWebP(fileData) + if err != nil { + zap.L().Error("转换图片到 WebP 失败", zap.Error(err)) + } else { // 若转换成功则替代原文件 + fileData = d + fileSize = s + fileExt = ".webp" + contentType = "image/webp" + } } // 上传文件 objectKey := objectService.GenerateObjectKey(uploadType, fileExt) - objectUrl, err := objectService.PutObject(objectKey, file, fileHeader.Size, contentType) + objectUrl, err := objectService.PutObject(objectKey, bytes.NewReader(fileData), fileSize, contentType) if err != nil { apiException.AbortWithException(c, apiException.UploadFileError, err) return diff --git a/app/services/objectService/objectService.go b/app/services/objectService/objectService.go index d0507d5..20a0f3a 100644 --- a/app/services/objectService/objectService.go +++ b/app/services/objectService/objectService.go @@ -1,15 +1,20 @@ package objectService import ( + "bytes" "errors" "fmt" + "io" "mime/multipart" "strings" "time" + "github.com/chai2010/webp" + "github.com/disintegration/imaging" "github.com/dustin/go-humanize" "github.com/gabriel-vasile/mimetype" uuid "github.com/satori/go.uuid" + "go.uber.org/zap" ) var ( @@ -23,15 +28,23 @@ var ( ErrNotImage = errors.New("file isn't a image") ) +const ( + // TypeImage 图片 + TypeImage = "image" + + // TypeAttachment 附件 + TypeAttachment = "attachment" +) + var uploadTypeLimits = map[string]int64{ - "public/image": humanize.MByte * 10, - "public/attachment": humanize.MByte * 100, + TypeImage: humanize.MByte * 10, + TypeAttachment: humanize.MByte * 100, } // GetFileInfo 获取文件基本信息 func GetFileInfo( - file multipart.File, - fileHeader *multipart.FileHeader, + fileData []byte, + fileSize int64, uploadType string, ) ( contentType string, @@ -39,18 +52,15 @@ func GetFileInfo( err error, ) { // 检查文件大小 - if err = checkFileSize(uploadType, fileHeader.Size); err != nil { + if err = checkFileSize(uploadType, fileSize); err != nil { return "", "", err } // 通过文件头获取类型和扩展名 - mimeType, mimeExt, err := getFileTypeAndExt(file) - if err != nil { - return "", "", err - } + mimeType, mimeExt := getFileTypeAndExt(fileData) // 检查是否为图像类型 - if uploadType == "public/image" && !strings.HasPrefix(mimeType, "image") { + if uploadType == TypeImage && !strings.HasPrefix(mimeType, "image") { return "", "", ErrNotImage } @@ -75,10 +85,42 @@ func checkFileSize(uploadType string, size int64) error { } // getFileTypeAndExt 根据文件头(Magic Number)判断文件类型和扩展名 -func getFileTypeAndExt(file multipart.File) (mimeType string, mimeExt string, err error) { - mime, err := mimetype.DetectReader(file) +func getFileTypeAndExt(fileData []byte) (mimeType string, mimeExt string) { + mime := mimetype.Detect(fileData) + return mime.String(), mime.Extension() +} + +// ConvertToWebP 将图片转换为 WebP 格式 +func ConvertToWebP(fileData []byte) ([]byte, int64, error) { + img, err := imaging.Decode(bytes.NewReader(fileData)) if err != nil { - return "", "", err + return nil, 0, err + } + + var buf bytes.Buffer + err = webp.Encode(&buf, img, &webp.Options{Quality: 100}) + if err != nil { + return nil, 0, err + } + return buf.Bytes(), int64(buf.Len()), nil +} + +// ReadFileToBytes 读取文件数据 +func ReadFileToBytes(fileHeader *multipart.FileHeader) ([]byte, error) { + file, err := fileHeader.Open() + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer func(file multipart.File) { + err := file.Close() + if err != nil { + zap.L().Warn("文件关闭失败", zap.Error(err)) + } + }(file) + + data, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) } - return mime.String(), mime.Extension(), nil + return data, nil } diff --git a/docs/README.md b/docs/README.md index bbccada..b6b353f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,7 +33,9 @@ ### 如何参与开发 -1. 克隆该项目并切换到dev分支,然后切出自己的分支进行开发 +1. 安装`GCC`或`MinGW`并配置好环境([下载](http://tdm-gcc.tdragon.net/download)) + +2. 克隆该项目并切换到`dev`分支,然后切出自己的分支进行开发 ```shell git clone https://github.com/zjutjh/4UOnline-Go.git @@ -58,7 +60,7 @@ git reset --hard origin/dev git push --force ``` -2. 复制示例配置,并按注释要求填写配置文件(`user`配置询问部长团,并要提供个人学号) +3. 复制示例配置,并按注释要求填写配置文件(`user`配置询问部长团,并要提供个人学号) ```shell /* Linux */ @@ -68,25 +70,16 @@ cp config.example.yaml config.yaml copy config.example.yaml config.yaml ``` -3. 启动程序 +4. 启动程序 ```shell go run main.go ``` -4. 每次提交 commit 前,先运行以下命令来格式化代码并检查规范(需要安装 [gci](https://github.com/daixiang0/gci) 和 [golangci-lint](https://golangci-lint.run/)) +5. 每次提交 commit 前,先运行以下命令来格式化代码并检查规范(需要安装 [gci](https://github.com/daixiang0/gci) 和 [golangci-lint](https://golangci-lint.run/)) ``` gofmt -w . gci write . -s standard -s default golangci-lint run --config .golangci.yml ``` - -5. 打包后端到服务器运行 - -``` -SET CGO_ENABLE=0 -SET GOOS=linux -SET GOARCH=amd64 -go build -o 4u main.go -``` diff --git a/go.mod b/go.mod index 1711940..42970bb 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module 4u-go go 1.22.9 require ( + github.com/chai2010/webp v1.1.1 + github.com/disintegration/imaging v1.6.2 github.com/dustin/go-humanize v1.0.1 github.com/gabriel-vasile/mimetype v1.4.6 github.com/gin-contrib/cors v1.7.2 @@ -12,7 +14,6 @@ require ( github.com/go-resty/resty/v2 v2.16.0 github.com/json-iterator/go v1.1.12 github.com/minio/minio-go/v7 v7.0.80 - github.com/pkg/errors v0.9.1 github.com/satori/go.uuid v1.2.0 github.com/silenceper/wechat/v2 v2.1.7 github.com/spf13/viper v1.19.0 @@ -77,6 +78,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.12.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/image v0.22.0 // indirect golang.org/x/net v0.31.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect diff --git a/go.sum b/go.sum index 476e02c..ebbcc16 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= +github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -30,6 +32,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 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/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -159,8 +163,6 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -235,6 +237,9 @@ golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= +golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= From d4e8c493a03436850683a1f26f4cf3b0d1fcb2ba Mon Sep 17 00:00:00 2001 From: SugarMGP <2350745751@qq.com> Date: Wed, 4 Dec 2024 21:19:32 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20=E6=94=B9=E7=94=A8io.Reader=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .golangci.yml | 3 ++ app/apiException/apiException.go | 2 +- app/controllers/objectController/upload.go | 32 +++++++----- app/services/objectService/objectService.go | 54 +++++++-------------- 4 files changed, 42 insertions(+), 49 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 899a837..3d99cd8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -57,6 +57,9 @@ linters-settings: - name: function-length # 函数长度限制 severity: warning disabled: true + - name: cyclomatic # 圈复杂度 + severity: warning + disabled: true - name: cognitive-complexity # 认知复杂度 severity: warning disabled: false diff --git a/app/apiException/apiException.go b/app/apiException/apiException.go index 1b8b6d4..6776367 100644 --- a/app/apiException/apiException.go +++ b/app/apiException/apiException.go @@ -32,7 +32,7 @@ var ( ResourceNotFound = NewError(200511, log.LevelInfo, "访问的资源不存在") AdminKeyError = NewError(200512, log.LevelInfo, "管理员注册密钥错误") AdminAlreadyExisted = NewError(200513, log.LevelInfo, "管理员账号已存在") - UploadFileError = NewError(200514, log.LevelError, "上传文件失败") + UploadFileError = NewError(200514, log.LevelInfo, "上传文件失败") FileSizeExceedError = NewError(200515, log.LevelInfo, "文件大小超限") FileNotImageError = NewError(200516, log.LevelInfo, "上传的文件不是图片") diff --git a/app/controllers/objectController/upload.go b/app/controllers/objectController/upload.go index 03d67fa..f1f424f 100644 --- a/app/controllers/objectController/upload.go +++ b/app/controllers/objectController/upload.go @@ -1,8 +1,9 @@ +//nolint:all package objectController import ( - "bytes" "errors" + "io" "mime/multipart" "4u-go/app/apiException" @@ -27,14 +28,20 @@ func UploadFile(c *gin.Context) { uploadType := data.UploadType fileSize := data.File.Size - fileData, err := objectService.ReadFileToBytes(data.File) + file, err := data.File.Open() if err != nil { apiException.AbortWithException(c, apiException.UploadFileError, err) return } + defer func(file multipart.File) { + err := file.Close() + if err != nil { + zap.L().Warn("文件关闭错误", zap.Error(err)) + } + }(file) // 获取文件信息 - contentType, fileExt, err := objectService.GetFileInfo(fileData, fileSize, uploadType) + contentType, fileExt, err := objectService.GetFileInfo(file, fileSize, uploadType) if errors.Is(err, objectService.ErrSizeExceeded) { apiException.AbortWithException(c, apiException.FileSizeExceedError, err) return @@ -43,22 +50,23 @@ func UploadFile(c *gin.Context) { apiException.AbortWithException(c, apiException.ParamError, err) return } - if errors.Is(err, objectService.ErrNotImage) { - apiException.AbortWithException(c, apiException.FileNotImageError, err) - return - } if err != nil { apiException.AbortWithException(c, apiException.ServerError, err) return } + var fileReader io.Reader = file if uploadType == objectService.TypeImage { - d, s, err := objectService.ConvertToWebP(fileData) + reader, size, err := objectService.ConvertToWebP(file) if err != nil { + if errors.Is(err, objectService.ErrNotImage) { + apiException.AbortWithException(c, apiException.FileNotImageError, err) + return + } zap.L().Error("转换图片到 WebP 失败", zap.Error(err)) } else { // 若转换成功则替代原文件 - fileData = d - fileSize = s + fileReader = reader + fileSize = size fileExt = ".webp" contentType = "image/webp" } @@ -66,9 +74,9 @@ func UploadFile(c *gin.Context) { // 上传文件 objectKey := objectService.GenerateObjectKey(uploadType, fileExt) - objectUrl, err := objectService.PutObject(objectKey, bytes.NewReader(fileData), fileSize, contentType) + objectUrl, err := objectService.PutObject(objectKey, fileReader, fileSize, contentType) if err != nil { - apiException.AbortWithException(c, apiException.UploadFileError, err) + apiException.AbortWithException(c, apiException.ServerError, err) return } diff --git a/app/services/objectService/objectService.go b/app/services/objectService/objectService.go index 20a0f3a..3eeabc0 100644 --- a/app/services/objectService/objectService.go +++ b/app/services/objectService/objectService.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "mime/multipart" - "strings" "time" "github.com/chai2010/webp" @@ -14,7 +13,6 @@ import ( "github.com/dustin/go-humanize" "github.com/gabriel-vasile/mimetype" uuid "github.com/satori/go.uuid" - "go.uber.org/zap" ) var ( @@ -43,7 +41,7 @@ var uploadTypeLimits = map[string]int64{ // GetFileInfo 获取文件基本信息 func GetFileInfo( - fileData []byte, + file multipart.File, fileSize int64, uploadType string, ) ( @@ -57,13 +55,10 @@ func GetFileInfo( } // 通过文件头获取类型和扩展名 - mimeType, mimeExt := getFileTypeAndExt(fileData) - - // 检查是否为图像类型 - if uploadType == TypeImage && !strings.HasPrefix(mimeType, "image") { - return "", "", ErrNotImage + mimeType, mimeExt, err := getFileTypeAndExt(file) + if err != nil { + return "", "", err } - return mimeType, mimeExt, nil } @@ -85,42 +80,29 @@ func checkFileSize(uploadType string, size int64) error { } // getFileTypeAndExt 根据文件头(Magic Number)判断文件类型和扩展名 -func getFileTypeAndExt(fileData []byte) (mimeType string, mimeExt string) { - mime := mimetype.Detect(fileData) - return mime.String(), mime.Extension() -} - -// ConvertToWebP 将图片转换为 WebP 格式 -func ConvertToWebP(fileData []byte) ([]byte, int64, error) { - img, err := imaging.Decode(bytes.NewReader(fileData)) +func getFileTypeAndExt(file multipart.File) (mimeType string, mimeExt string, err error) { + mime, err := mimetype.DetectReader(file) if err != nil { - return nil, 0, err + return "", "", err } - - var buf bytes.Buffer - err = webp.Encode(&buf, img, &webp.Options{Quality: 100}) + _, err = file.Seek(0, io.SeekStart) if err != nil { - return nil, 0, err + return "", "", err } - return buf.Bytes(), int64(buf.Len()), nil + return mime.String(), mime.Extension(), nil } -// ReadFileToBytes 读取文件数据 -func ReadFileToBytes(fileHeader *multipart.FileHeader) ([]byte, error) { - file, err := fileHeader.Open() +// ConvertToWebP 将图片转换为 WebP 格式 +func ConvertToWebP(file multipart.File) (io.Reader, int64, error) { + img, err := imaging.Decode(file) if err != nil { - return nil, fmt.Errorf("failed to open file: %w", err) + return nil, 0, fmt.Errorf("%w: %w", ErrNotImage, err) } - defer func(file multipart.File) { - err := file.Close() - if err != nil { - zap.L().Warn("文件关闭失败", zap.Error(err)) - } - }(file) - data, err := io.ReadAll(file) + var buf bytes.Buffer + err = webp.Encode(&buf, img, &webp.Options{Quality: 100}) if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) + return nil, 0, err } - return data, nil + return bytes.NewReader(buf.Bytes()), int64(buf.Len()), nil } From 7f360053507e11d7f631dd20f9b8252f1c4abcff Mon Sep 17 00:00:00 2001 From: SugarMGP <2350745751@qq.com> Date: Thu, 5 Dec 2024 20:39:55 +0800 Subject: [PATCH 4/9] =?UTF-8?q?ci:=20=E6=96=B0=E5=A2=9E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 34 ++++++++++++++++++++++++++++++++++ makefile | 12 +----------- 2 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7868e54 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,34 @@ +name: Build + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.22 + + - name: Build + run: | + go install src.techknowlogick.com/xgo@latest + xgo -out 4uonline --targets=*/amd64 . + + - name: Archive Output + run: | + mkdir -p artifacts + mv 4uonline* artifacts/ + cp README.md artifacts/ + cp config.example.yaml artifacts/config.yaml + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: build-output + path: artifacts/ diff --git a/makefile b/makefile index 24878be..727347c 100644 --- a/makefile +++ b/makefile @@ -1,8 +1,3 @@ -# 设置环境变量 -CGO_ENABLED=0 -GOOS=linux -GOARCH=amd64 - # Go 文件 TARGET=main @@ -14,11 +9,6 @@ build: @echo "Building $(TARGET)..." go build -o $(TARGET) $(TARGET).go -# 编译为 Linux 目标 -build-linux: - @echo "Building $(TARGET) for $(GOOS)/$(GOARCH)..." - GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO_ENABLED) go build -o $(TARGET) $(TARGET).go - # 清理生成的文件 clean: @echo "Cleaning up..." @@ -27,4 +17,4 @@ clean: # 运行程序 run: build @echo "Running $(TARGET)..." - ./$(TARGET) + ./$(TARGET) \ No newline at end of file From 46f0bd4f42b04522416066026783fb232915a954 Mon Sep 17 00:00:00 2001 From: SugarMGP <2350745751@qq.com> Date: Fri, 6 Dec 2024 18:32:17 +0800 Subject: [PATCH 5/9] =?UTF-8?q?chore:=20=E8=B0=83=E6=95=B4Makefile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- makefile | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/makefile b/makefile index 727347c..b7d1723 100644 --- a/makefile +++ b/makefile @@ -1,13 +1,21 @@ +# 开启CGO +CGO_ENABLED=1 + # Go 文件 TARGET=main # 默认目标 all: build +# 检测 GCC +check-gcc: + @command -v gcc >/dev/null 2>&1 || { echo "Error: gcc is not installed. Please install gcc and try again."; exit 1; } + # 构建目标 build: + check-gcc @echo "Building $(TARGET)..." - go build -o $(TARGET) $(TARGET).go + go build -v -o $(TARGET) . # 清理生成的文件 clean: @@ -17,4 +25,12 @@ clean: # 运行程序 run: build @echo "Running $(TARGET)..." - ./$(TARGET) \ No newline at end of file + ./$(TARGET) + +# 格式化代码并检查风格 +fmt: + @echo "Formatting Go files..." + gofmt -w . + gci write . -s standard -s default + @echo "Running Lints..." + golangci-lint run \ No newline at end of file From da0a55e1a1c18417cd2ccadeaa6868293551e7f2 Mon Sep 17 00:00:00 2001 From: MangoGovo <1749100231@qq.com> Date: Wed, 4 Dec 2024 14:00:30 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E4=BA=86?= =?UTF-8?q?=E6=9D=83=E7=9B=8A=E7=A0=81=E7=9A=84=E5=A2=9E=E5=88=A0=E6=94=B9?= =?UTF-8?q?=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/qrcodeController/create.go | 43 ++++++++++++++ app/controllers/qrcodeController/delete.go | 46 +++++++++++++++ app/controllers/qrcodeController/get.go | 39 ++++++++++++ .../qrcodeController/toggleStatus.go | 51 ++++++++++++++++ app/controllers/qrcodeController/update.go | 59 +++++++++++++++++++ app/models/qrcode.go | 33 +++++++++++ app/services/qrcodeService/delete.go | 12 ++++ app/services/qrcodeService/get.go | 13 ++++ app/services/qrcodeService/save.go | 12 ++++ config/database/migrations.go | 1 + config/router/router.go | 11 ++++ 11 files changed, 320 insertions(+) create mode 100644 app/controllers/qrcodeController/create.go create mode 100644 app/controllers/qrcodeController/delete.go create mode 100644 app/controllers/qrcodeController/get.go create mode 100644 app/controllers/qrcodeController/toggleStatus.go create mode 100644 app/controllers/qrcodeController/update.go create mode 100644 app/models/qrcode.go create mode 100644 app/services/qrcodeService/delete.go create mode 100644 app/services/qrcodeService/get.go create mode 100644 app/services/qrcodeService/save.go diff --git a/app/controllers/qrcodeController/create.go b/app/controllers/qrcodeController/create.go new file mode 100644 index 0000000..9f3fa7f --- /dev/null +++ b/app/controllers/qrcodeController/create.go @@ -0,0 +1,43 @@ +package qrcodeController + +import ( + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/qrcodeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type createQrcodeData struct { + College uint `json:"college" binding:"required"` + Department string `json:"department" binding:"required"` + Description string `json:"description" binding:"required"` + FeedbackType uint `json:"feedback_type" binding:"required"` + Location string `json:"location" binding:"required"` +} + +// CreateQrcode 创建一个权益码 +func CreateQrcode(c *gin.Context) { + var data createQrcodeData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + err = qrcodeService.SaveQrcode(models.Qrcode{ + Status: true, + College: data.College, + Department: data.Department, + Description: data.Description, + FeedbackType: data.FeedbackType, + Location: data.Location, + }) + + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/qrcodeController/delete.go b/app/controllers/qrcodeController/delete.go new file mode 100644 index 0000000..af82471 --- /dev/null +++ b/app/controllers/qrcodeController/delete.go @@ -0,0 +1,46 @@ +package qrcodeController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/services/qrcodeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type deleteQrcodeData struct { + ID uint `json:"id"` +} + +// DeleteQrcode 创建一个权益码 +func DeleteQrcode(c *gin.Context) { + var data deleteQrcodeData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 判断权益码是否存在 + _, err = qrcodeService.GetQrcodeById(data.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.ResourceNotFound, err) + } else { + apiException.AbortWithException(c, apiException.ServerError, err) + } + return + } + + // 删除权益码 + err = qrcodeService.DeleteQrcodeById(data.ID) + + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/qrcodeController/get.go b/app/controllers/qrcodeController/get.go new file mode 100644 index 0000000..7aeb92f --- /dev/null +++ b/app/controllers/qrcodeController/get.go @@ -0,0 +1,39 @@ +package qrcodeController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/services/qrcodeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type getQrcodeData struct { + ID uint `form:"id" binding:"required"` +} + +// GetQrcode 获取权益码信息 +func GetQrcode(c *gin.Context) { + var data getQrcodeData + err := c.ShouldBind(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + qrcode, err := qrcodeService.GetQrcodeById(data.ID) + + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.ResourceNotFound, err) + return + } + + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, qrcode) +} diff --git a/app/controllers/qrcodeController/toggleStatus.go b/app/controllers/qrcodeController/toggleStatus.go new file mode 100644 index 0000000..e6f3b5e --- /dev/null +++ b/app/controllers/qrcodeController/toggleStatus.go @@ -0,0 +1,51 @@ +package qrcodeController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/services/qrcodeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type toggleStatusData struct { + ID uint `json:"id" binding:"required"` + Status bool `json:"status"` +} + +// ToggleStatus 更新学院信息 +func ToggleStatus(c *gin.Context) { + var data toggleStatusData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + qrcode, err := qrcodeService.GetQrcodeById(data.ID) + + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.ResourceNotFound, err) + return + } + + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + { // 更新权益码状态 + qrcode.Status = data.Status + } + + err = qrcodeService.SaveQrcode(qrcode) + + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/qrcodeController/update.go b/app/controllers/qrcodeController/update.go new file mode 100644 index 0000000..7a60ed7 --- /dev/null +++ b/app/controllers/qrcodeController/update.go @@ -0,0 +1,59 @@ +package qrcodeController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/services/qrcodeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type updateQrcodeData struct { + ID uint `json:"id" binding:"required"` + College uint `json:"college" binding:"required"` + Department string `json:"department" binding:"required"` + Description string `json:"description" binding:"required"` + FeedbackType uint `json:"feedback_type" binding:"required"` + Location string `json:"location" binding:"required"` +} + +// UpdateQrcode 更新学院信息 +func UpdateQrcode(c *gin.Context) { + var data updateQrcodeData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + qrcode, err := qrcodeService.GetQrcodeById(data.ID) + + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.ResourceNotFound, err) + return + } + + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + { // 更新权益码信息 + qrcode.College = data.College + qrcode.Department = data.Department + qrcode.Description = data.Description + qrcode.FeedbackType = data.FeedbackType + qrcode.Location = data.Location + } + + err = qrcodeService.SaveQrcode(qrcode) + + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/models/qrcode.go b/app/models/qrcode.go new file mode 100644 index 0000000..8525b29 --- /dev/null +++ b/app/models/qrcode.go @@ -0,0 +1,33 @@ +package models + +import "gorm.io/gorm" + +// FeedbackType +const ( + FbActivities uint = iota // 校园活动 + FbDiningAndShops // 食堂及商铺 + FbDormitories // 宿舍 + FbAcademic // 教学服务(选课、转专业等) + FbFacilities // 校园设施 + FbClassrooms // 教室 + FbLibrary // 图书馆 + FbTransportation // 交通 + FbSecurity // 安保 + FbHealthCare // 医疗服务 + FbPolicies // 学院相关政策(如综测等) + FbOthers // 其他服务 +) + +// Qrcode 权益码的结构体 +type Qrcode struct { + gorm.Model + FeedbackType uint // 反馈类型 + College uint // 责任部门 + Department string // 负责单位 + Location string // 投放位置 + Status bool // 状态(是否启用) + Description string // 备注 + + ScanCount uint // 扫描次数 + FeedbackCount uint // 反馈次数 +} diff --git a/app/services/qrcodeService/delete.go b/app/services/qrcodeService/delete.go new file mode 100644 index 0000000..2da5090 --- /dev/null +++ b/app/services/qrcodeService/delete.go @@ -0,0 +1,12 @@ +package qrcodeService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// DeleteQrcodeById 通过 ID 删除一条公告 +func DeleteQrcodeById(qrcodeId uint) error { + result := database.DB.Where("id = ?", qrcodeId).Delete(&models.Qrcode{}) + return result.Error +} diff --git a/app/services/qrcodeService/get.go b/app/services/qrcodeService/get.go new file mode 100644 index 0000000..b72eb71 --- /dev/null +++ b/app/services/qrcodeService/get.go @@ -0,0 +1,13 @@ +package qrcodeService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// GetQrcodeById 获取指定ID的公告 +func GetQrcodeById(id uint) (qrcode models.Qrcode, err error) { + result := database.DB.Where("id = ?", id).First(&qrcode) + err = result.Error + return qrcode, err +} diff --git a/app/services/qrcodeService/save.go b/app/services/qrcodeService/save.go new file mode 100644 index 0000000..cc4bfaf --- /dev/null +++ b/app/services/qrcodeService/save.go @@ -0,0 +1,12 @@ +package qrcodeService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// SaveQrcode 向数据库中保存一条权益码记录 +func SaveQrcode(qrcode models.Qrcode) error { + result := database.DB.Save(&qrcode) + return result.Error +} diff --git a/config/database/migrations.go b/config/database/migrations.go index 6eed935..a3955f5 100755 --- a/config/database/migrations.go +++ b/config/database/migrations.go @@ -13,5 +13,6 @@ func autoMigrate(db *gorm.DB) error { &models.LostAndFoundRecord{}, &models.Website{}, &models.College{}, + &models.Qrcode{}, ) } diff --git a/config/router/router.go b/config/router/router.go index dc68570..1c29646 100644 --- a/config/router/router.go +++ b/config/router/router.go @@ -6,6 +6,7 @@ import ( "4u-go/app/controllers/announcementController" "4u-go/app/controllers/collegeController" "4u-go/app/controllers/objectController" + "4u-go/app/controllers/qrcodeController" "4u-go/app/controllers/userController" "4u-go/app/controllers/websiteController" "4u-go/app/midwares" @@ -65,6 +66,16 @@ func Init(r *gin.Engine) { adminWebsite.PUT("", websiteController.UpdateWebsite) adminWebsite.GET("/list", websiteController.GetEditableWebsites) } + + adminQrcode := admin.Group("/qrcode", midwares.CheckAdmin) + { + adminQrcode.PUT("/status", qrcodeController.ToggleStatus) + adminQrcode.POST("", qrcodeController.CreateQrcode) + adminQrcode.DELETE("", qrcodeController.DeleteQrcode) + adminQrcode.GET("", qrcodeController.GetQrcode) + adminQrcode.GET("/list", qrcodeController.GetList) + adminQrcode.PUT("", qrcodeController.UpdateQrcode) + } } activity := api.Group("/activity") From fce9fc0584343e8f2bc225e11481a3b1728e6ddb Mon Sep 17 00:00:00 2001 From: MangoGovo <1749100231@qq.com> Date: Fri, 6 Dec 2024 01:52:42 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E4=BA=86?= =?UTF-8?q?=E6=9D=83=E7=9B=8A=E7=A0=81=E5=88=97=E8=A1=A8=E7=9A=84=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E8=8E=B7=E5=8F=96,=20=E6=90=9C=E7=B4=A2,=20=E7=AD=9B?= =?UTF-8?q?=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/qrcodeController/create.go | 6 +-- app/controllers/qrcodeController/get.go | 49 +++++++++++++++-- app/controllers/qrcodeController/getList.go | 60 +++++++++++++++++++++ app/controllers/qrcodeController/update.go | 12 ++--- app/models/qrcode.go | 26 +++++---- app/services/qrcodeService/getList.go | 42 +++++++++++++++ app/utils/database/filter.go | 14 +++++ app/utils/database/paginate.go | 18 +++++++ config/router/router.go | 4 +- 9 files changed, 206 insertions(+), 25 deletions(-) create mode 100644 app/controllers/qrcodeController/getList.go create mode 100644 app/services/qrcodeService/getList.go create mode 100644 app/utils/database/filter.go create mode 100644 app/utils/database/paginate.go diff --git a/app/controllers/qrcodeController/create.go b/app/controllers/qrcodeController/create.go index 9f3fa7f..bdf138a 100644 --- a/app/controllers/qrcodeController/create.go +++ b/app/controllers/qrcodeController/create.go @@ -9,10 +9,10 @@ import ( ) type createQrcodeData struct { - College uint `json:"college" binding:"required"` + College uint `json:"college"` Department string `json:"department" binding:"required"` - Description string `json:"description" binding:"required"` - FeedbackType uint `json:"feedback_type" binding:"required"` + Description string `json:"description"` + FeedbackType uint `json:"feedback_type"` Location string `json:"location" binding:"required"` } diff --git a/app/controllers/qrcodeController/get.go b/app/controllers/qrcodeController/get.go index 7aeb92f..b1e381f 100644 --- a/app/controllers/qrcodeController/get.go +++ b/app/controllers/qrcodeController/get.go @@ -2,8 +2,11 @@ package qrcodeController import ( "errors" + "time" "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/collegeService" "4u-go/app/services/qrcodeService" "4u-go/app/utils" "github.com/gin-gonic/gin" @@ -14,6 +17,20 @@ type getQrcodeData struct { ID uint `form:"id" binding:"required"` } +type qrcodeResp struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + FeedbackType uint `json:"feedback_type"` // 反馈类型 + College models.College `json:"college"` // 责任部门 + Department string `json:"department"` // 负责单位 + Location string `json:"location"` // 投放位置 + Status bool `json:"status"` // 状态(是否启用) + Description string `json:"description"` // 备注 + + ScanCount uint `json:"scan_count"` // 扫描次数 + FeedbackCount uint `json:"feedback_count"` // 反馈次数 +} + // GetQrcode 获取权益码信息 func GetQrcode(c *gin.Context) { var data getQrcodeData @@ -25,15 +42,41 @@ func GetQrcode(c *gin.Context) { qrcode, err := qrcodeService.GetQrcodeById(data.ID) - if errors.Is(err, gorm.ErrRecordNotFound) { - apiException.AbortWithException(c, apiException.ResourceNotFound, err) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.ResourceNotFound, err) + } else { + apiException.AbortWithException(c, apiException.ServerError, err) + } return } + resp, err := generateResp(qrcode) if err != nil { apiException.AbortWithException(c, apiException.ServerError, err) return } - utils.JsonSuccessResponse(c, qrcode) + utils.JsonSuccessResponse(c, resp) +} + +func generateResp(qrcode models.Qrcode) (*qrcodeResp, error) { + college, err := collegeService.GetCollegeById(qrcode.College) + if err != nil { + return nil, err + } + + return &qrcodeResp{ + ID: qrcode.ID, + CreatedAt: qrcode.CreatedAt, + FeedbackType: qrcode.FeedbackType, + College: college, + Department: qrcode.Department, + Location: qrcode.Location, + ScanCount: qrcode.ScanCount, + Status: qrcode.Status, + + FeedbackCount: qrcode.FeedbackCount, + Description: qrcode.Description, + }, nil } diff --git a/app/controllers/qrcodeController/getList.go b/app/controllers/qrcodeController/getList.go new file mode 100644 index 0000000..fb4c1e0 --- /dev/null +++ b/app/controllers/qrcodeController/getList.go @@ -0,0 +1,60 @@ +package qrcodeController + +import ( + "4u-go/app/apiException" + "4u-go/app/services/qrcodeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type filter struct { + College []uint `json:"college"` + FeedbackType []uint `json:"feedback_type"` +} + +type getListData struct { + Keyword string `json:"keyword"` + Filter filter `json:"filter"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +type getListResponse struct { + QrcodeList []qrcodeResp `json:"qrcode_list"` + Total int64 `json:"total"` +} + +// GetList 实现了权益码列表的分页获取, 搜索, 筛选 +func GetList(c *gin.Context) { + var data getListData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + filter := data.Filter + + qrcodeListResp := make([]qrcodeResp, 0) + + qrcodeList, total, err := qrcodeService.GetList( + filter.College, filter.FeedbackType, + data.Keyword, data.Page, data.PageSize) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + for _, qrcode := range qrcodeList { + resp, err := generateResp(qrcode) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + qrcodeListResp = append(qrcodeListResp, *resp) + } + + utils.JsonSuccessResponse(c, getListResponse{ + QrcodeList: qrcodeListResp, + Total: total, + }) +} diff --git a/app/controllers/qrcodeController/update.go b/app/controllers/qrcodeController/update.go index 7a60ed7..5e164c3 100644 --- a/app/controllers/qrcodeController/update.go +++ b/app/controllers/qrcodeController/update.go @@ -29,14 +29,12 @@ func UpdateQrcode(c *gin.Context) { } qrcode, err := qrcodeService.GetQrcodeById(data.ID) - - if errors.Is(err, gorm.ErrRecordNotFound) { - apiException.AbortWithException(c, apiException.ResourceNotFound, err) - return - } - if err != nil { - apiException.AbortWithException(c, apiException.ServerError, err) + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.ResourceNotFound, err) + } else { + apiException.AbortWithException(c, apiException.ServerError, err) + } return } diff --git a/app/models/qrcode.go b/app/models/qrcode.go index 8525b29..0b6e96f 100644 --- a/app/models/qrcode.go +++ b/app/models/qrcode.go @@ -1,6 +1,9 @@ package models -import "gorm.io/gorm" +import ( + "database/sql" + "time" +) // FeedbackType const ( @@ -20,14 +23,17 @@ const ( // Qrcode 权益码的结构体 type Qrcode struct { - gorm.Model - FeedbackType uint // 反馈类型 - College uint // 责任部门 - Department string // 负责单位 - Location string // 投放位置 - Status bool // 状态(是否启用) - Description string // 备注 + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"-"` + DeletedAt sql.NullTime `json:"-" gorm:"index"` + FeedbackType uint `json:"feedback_type"` // 反馈类型 + College uint `json:"college"` // 责任部门 + Department string `json:"department"` // 负责单位 + Location string `json:"location"` // 投放位置 + Status bool `json:"status"` // 状态(是否启用) + Description string `json:"description"` // 备注 - ScanCount uint // 扫描次数 - FeedbackCount uint // 反馈次数 + ScanCount uint `json:"scan_count"` // 扫描次数 + FeedbackCount uint `json:"feedback_count"` // 反馈次数 } diff --git a/app/services/qrcodeService/getList.go b/app/services/qrcodeService/getList.go new file mode 100644 index 0000000..73813e3 --- /dev/null +++ b/app/services/qrcodeService/getList.go @@ -0,0 +1,42 @@ +package qrcodeService + +import ( + "4u-go/app/models" + dbUtils "4u-go/app/utils/database" + "4u-go/config/database" +) + +// GetList 获取权益码信息列表的筛选,搜索,分页 +func GetList( + collegeFilter []uint, + feedbackFilter []uint, + keyword string, + page int, pageSize int, +) (qrcodeList []models.Qrcode, total int64, err error) { + query := database.DB.Model(models.Qrcode{}) + + // 关键词搜索 + if len(keyword) > 0 { + query = query.Where("ID = ? "+ + "OR department LIKE ? "+ + "OR location LIKE ? "+ + "OR description LIKE ?", keyword, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") + } + + // 筛选`责任部门` + if len(collegeFilter) > 0 { + query = query.Scopes(dbUtils.Filter("college", collegeFilter)) + } + + // 筛选`反馈类型` + if len(feedbackFilter) > 0 { + query = query.Scopes(dbUtils.Filter("feedback_type", feedbackFilter)) + } + + // 分页查找 + err = query.Count(&total). + Scopes(dbUtils.Paginate(page, pageSize)). + Find(&qrcodeList).Error + + return qrcodeList, total, err +} diff --git a/app/utils/database/filter.go b/app/utils/database/filter.go new file mode 100644 index 0000000..93d142d --- /dev/null +++ b/app/utils/database/filter.go @@ -0,0 +1,14 @@ +package database + +import "gorm.io/gorm" + +// Filter 自定义筛选插件 +// Usage: db.Scopes(dbUtils.Filter("college", [1,2,3])) +// Desc: 筛选出"college"字段为1,2,3其中之一的字段 +// +// Spec: 使用泛型以接收所有类型的切片 +func Filter[T any](name string, choices []T) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + return db.Where(name+" IN (?)", choices) + } +} diff --git a/app/utils/database/paginate.go b/app/utils/database/paginate.go new file mode 100644 index 0000000..3d71a59 --- /dev/null +++ b/app/utils/database/paginate.go @@ -0,0 +1,18 @@ +package database + +import "gorm.io/gorm" + +// Paginate 自定义分页插件 +// Usage: db.Scopes(dbUtils.Paginate(page, pageSize)) +func Paginate(page int, size int) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + pageSize := size + if pageSize > 20 { + pageSize = 20 + } else if pageSize <= 0 { + pageSize = 10 + } + offset := (page - 1) * pageSize + return db.Offset(offset).Limit(pageSize) + } +} diff --git a/config/router/router.go b/config/router/router.go index 1c29646..e2f5be7 100644 --- a/config/router/router.go +++ b/config/router/router.go @@ -71,9 +71,9 @@ func Init(r *gin.Engine) { { adminQrcode.PUT("/status", qrcodeController.ToggleStatus) adminQrcode.POST("", qrcodeController.CreateQrcode) - adminQrcode.DELETE("", qrcodeController.DeleteQrcode) + adminQrcode.DELETE("", midwares.CheckSuperAdmin, qrcodeController.DeleteQrcode) adminQrcode.GET("", qrcodeController.GetQrcode) - adminQrcode.GET("/list", qrcodeController.GetList) + adminQrcode.POST("/list", qrcodeController.GetList) adminQrcode.PUT("", qrcodeController.UpdateQrcode) } } From 8698d4ebefe6b4234862f2e01a9e124502c719a8 Mon Sep 17 00:00:00 2001 From: SugarMGP <2350745751@qq.com> Date: Sun, 8 Dec 2024 19:00:32 +0800 Subject: [PATCH 8/9] =?UTF-8?q?refactor:=20=E5=AE=9E=E7=8E=B0=E4=BC=98?= =?UTF-8?q?=E9=9B=85=E5=81=9C=E6=AD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/utils/server/server.go | 50 ++++++++++++++++++++++++++++++++++++++ main.go | 6 ++--- 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 app/utils/server/server.go diff --git a/app/utils/server/server.go b/app/utils/server/server.go new file mode 100644 index 0000000..7545c24 --- /dev/null +++ b/app/utils/server/server.go @@ -0,0 +1,50 @@ +package server + +import ( + "context" + "errors" + "net/http" + "os" + "os/signal" + "time" + + "4u-go/config/redis" + "go.uber.org/zap" +) + +// Run 运行 http 服务器 +func Run(handler http.Handler, addr string) { + srv := &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: 2 * time.Second, + } + + // 启动服务器协程 + go func() { + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + zap.L().Fatal("Server Error Occurred", zap.Error(err)) + } + }() + + // 阻塞并监听结束信号 + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + <-quit + + zap.L().Info("Shutdown Server...") + + // 关闭服务器(5秒超时时间) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + zap.L().Error("Server Shutdown Failed", zap.Error(err)) + } + + // 关闭 Redis 客户端 + if err := redis.GlobalClient.Close(); err != nil { + zap.L().Error("Redis Client Shutdown Failed", zap.Error(err)) + } + + zap.L().Info("Server Closed") +} diff --git a/main.go b/main.go index 68ff7fc..d8e8dd6 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "4u-go/app/midwares" "4u-go/app/utils/aes" "4u-go/app/utils/log" + "4u-go/app/utils/server" "4u-go/config/config" "4u-go/config/database" "4u-go/config/objectStorage" @@ -43,8 +44,5 @@ func main() { wechat.Init() router.Init(r) - err := r.Run(":" + config.Config.GetString("server.port")) - if err != nil { - zap.L().Fatal(err.Error()) - } + server.Run(r, ":"+config.Config.GetString("server.port")) } From fe4f634e66338274b5165dcaad2793d9a990bbd7 Mon Sep 17 00:00:00 2001 From: MangoGovo <1749100231@qq.com> Date: Sun, 8 Dec 2024 18:40:33 +0800 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E4=BA=86?= =?UTF-8?q?=E6=9D=83=E7=9B=8A=E7=A0=81=E7=9A=84=E6=96=B0=E5=A2=9E=E6=AC=A1?= =?UTF-8?q?=E6=95=B0=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/counterName/counterName.go | 12 +++++ app/common/feedbackType/feedbackType.go | 17 +++++++ app/controllers/qrcodeController/count.go | 33 ++++++++++++ app/controllers/qrcodeController/delete.go | 2 +- app/controllers/qrcodeController/getList.go | 5 +- .../qrcodeController/toggleStatus.go | 51 ------------------- app/controllers/qrcodeController/update.go | 6 ++- app/models/counter.go | 9 ++++ app/models/qrcode.go | 16 ------ app/services/qrcodeService/addScanCount.go | 24 +++++++++ app/services/qrcodeService/getList.go | 7 +++ app/services/trackService/counter/addCount.go | 38 ++++++++++++++ config/database/migrations.go | 1 + config/router/router.go | 6 ++- makefile | 2 +- 15 files changed, 156 insertions(+), 73 deletions(-) create mode 100644 app/common/counterName/counterName.go create mode 100644 app/common/feedbackType/feedbackType.go create mode 100644 app/controllers/qrcodeController/count.go delete mode 100644 app/controllers/qrcodeController/toggleStatus.go create mode 100644 app/models/counter.go create mode 100644 app/services/qrcodeService/addScanCount.go create mode 100644 app/services/trackService/counter/addCount.go diff --git a/app/common/counterName/counterName.go b/app/common/counterName/counterName.go new file mode 100644 index 0000000..0e0192d --- /dev/null +++ b/app/common/counterName/counterName.go @@ -0,0 +1,12 @@ +package counterName + +// 定义要统计的字段 + +const ( + // QrcodeScan 权益码扫描次数 + QrcodeScan string = "qrcode_scan" + // Feedback 问题反馈数量 + Feedback string = "feedback_scan" + // FeedbackHandle 处理问题反馈数量 + FeedbackHandle string = "feedback_handle" +) diff --git a/app/common/feedbackType/feedbackType.go b/app/common/feedbackType/feedbackType.go new file mode 100644 index 0000000..be4ea95 --- /dev/null +++ b/app/common/feedbackType/feedbackType.go @@ -0,0 +1,17 @@ +package feedbackType + +// FeedbackType +const ( + Activities uint = iota // 校园活动 + DiningAndShops // 食堂及商铺 + Dormitories // 宿舍 + Academic // 教学服务(选课、转专业等) + Facilities // 校园设施 + Classrooms // 教室 + Library // 图书馆 + Transportation // 交通 + Security // 安保 + HealthCare // 医疗服务 + Policies // 学院相关政策(如综测等) + Others // 其他服务 +) diff --git a/app/controllers/qrcodeController/count.go b/app/controllers/qrcodeController/count.go new file mode 100644 index 0000000..7ab2afa --- /dev/null +++ b/app/controllers/qrcodeController/count.go @@ -0,0 +1,33 @@ +package qrcodeController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/services/qrcodeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type scanCountData struct { + ID uint `form:"id" binding:"required"` +} + +// ScanCount 更新权益码扫码次数 +func ScanCount(c *gin.Context) { + var data scanCountData + if err := c.ShouldBind(&data); err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + if err := qrcodeService.AddScanCount(data.ID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.ResourceNotFound, nil) + } else { + apiException.AbortWithException(c, apiException.ServerError, err) + } + return + } + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/qrcodeController/delete.go b/app/controllers/qrcodeController/delete.go index af82471..9d78938 100644 --- a/app/controllers/qrcodeController/delete.go +++ b/app/controllers/qrcodeController/delete.go @@ -14,7 +14,7 @@ type deleteQrcodeData struct { ID uint `json:"id"` } -// DeleteQrcode 创建一个权益码 +// DeleteQrcode 删除一个权益码 func DeleteQrcode(c *gin.Context) { var data deleteQrcodeData err := c.ShouldBindJSON(&data) diff --git a/app/controllers/qrcodeController/getList.go b/app/controllers/qrcodeController/getList.go index fb4c1e0..c57277d 100644 --- a/app/controllers/qrcodeController/getList.go +++ b/app/controllers/qrcodeController/getList.go @@ -10,6 +10,7 @@ import ( type filter struct { College []uint `json:"college"` FeedbackType []uint `json:"feedback_type"` + Status bool `json:"status"` } type getListData struct { @@ -37,7 +38,9 @@ func GetList(c *gin.Context) { qrcodeListResp := make([]qrcodeResp, 0) qrcodeList, total, err := qrcodeService.GetList( - filter.College, filter.FeedbackType, + filter.College, + filter.FeedbackType, + filter.Status, data.Keyword, data.Page, data.PageSize) if err != nil { apiException.AbortWithException(c, apiException.ParamError, err) diff --git a/app/controllers/qrcodeController/toggleStatus.go b/app/controllers/qrcodeController/toggleStatus.go deleted file mode 100644 index e6f3b5e..0000000 --- a/app/controllers/qrcodeController/toggleStatus.go +++ /dev/null @@ -1,51 +0,0 @@ -package qrcodeController - -import ( - "errors" - - "4u-go/app/apiException" - "4u-go/app/services/qrcodeService" - "4u-go/app/utils" - "github.com/gin-gonic/gin" - "gorm.io/gorm" -) - -type toggleStatusData struct { - ID uint `json:"id" binding:"required"` - Status bool `json:"status"` -} - -// ToggleStatus 更新学院信息 -func ToggleStatus(c *gin.Context) { - var data toggleStatusData - err := c.ShouldBindJSON(&data) - if err != nil { - apiException.AbortWithException(c, apiException.ParamError, err) - return - } - - qrcode, err := qrcodeService.GetQrcodeById(data.ID) - - if errors.Is(err, gorm.ErrRecordNotFound) { - apiException.AbortWithException(c, apiException.ResourceNotFound, err) - return - } - - if err != nil { - apiException.AbortWithException(c, apiException.ServerError, err) - return - } - - { // 更新权益码状态 - qrcode.Status = data.Status - } - - err = qrcodeService.SaveQrcode(qrcode) - - if err != nil { - apiException.AbortWithException(c, apiException.ServerError, err) - return - } - - utils.JsonSuccessResponse(c, nil) -} diff --git a/app/controllers/qrcodeController/update.go b/app/controllers/qrcodeController/update.go index 5e164c3..d3cd86a 100644 --- a/app/controllers/qrcodeController/update.go +++ b/app/controllers/qrcodeController/update.go @@ -14,12 +14,13 @@ type updateQrcodeData struct { ID uint `json:"id" binding:"required"` College uint `json:"college" binding:"required"` Department string `json:"department" binding:"required"` - Description string `json:"description" binding:"required"` + Description string `json:"description"` FeedbackType uint `json:"feedback_type" binding:"required"` Location string `json:"location" binding:"required"` + Status bool `json:"status"` } -// UpdateQrcode 更新学院信息 +// UpdateQrcode 更新权益码信息 func UpdateQrcode(c *gin.Context) { var data updateQrcodeData err := c.ShouldBindJSON(&data) @@ -44,6 +45,7 @@ func UpdateQrcode(c *gin.Context) { qrcode.Description = data.Description qrcode.FeedbackType = data.FeedbackType qrcode.Location = data.Location + qrcode.Status = data.Status } err = qrcodeService.SaveQrcode(qrcode) diff --git a/app/models/counter.go b/app/models/counter.go new file mode 100644 index 0000000..59bdc33 --- /dev/null +++ b/app/models/counter.go @@ -0,0 +1,9 @@ +package models + +// Counter 用于记录每天相关字段的增量 +type Counter struct { + ID uint + Day int64 // 基于时间戳计算当前天数 `time.Now()/86400` + Name string // 统计的字段名 + Count int64 // 计数器 +} diff --git a/app/models/qrcode.go b/app/models/qrcode.go index 0b6e96f..5346d28 100644 --- a/app/models/qrcode.go +++ b/app/models/qrcode.go @@ -5,22 +5,6 @@ import ( "time" ) -// FeedbackType -const ( - FbActivities uint = iota // 校园活动 - FbDiningAndShops // 食堂及商铺 - FbDormitories // 宿舍 - FbAcademic // 教学服务(选课、转专业等) - FbFacilities // 校园设施 - FbClassrooms // 教室 - FbLibrary // 图书馆 - FbTransportation // 交通 - FbSecurity // 安保 - FbHealthCare // 医疗服务 - FbPolicies // 学院相关政策(如综测等) - FbOthers // 其他服务 -) - // Qrcode 权益码的结构体 type Qrcode struct { ID uint `json:"id"` diff --git a/app/services/qrcodeService/addScanCount.go b/app/services/qrcodeService/addScanCount.go new file mode 100644 index 0000000..1c8ad66 --- /dev/null +++ b/app/services/qrcodeService/addScanCount.go @@ -0,0 +1,24 @@ +package qrcodeService + +import ( + "4u-go/app/common/counterName" + trackService "4u-go/app/services/trackService/counter" +) + +// AddScanCount 用于更新ScanCount +func AddScanCount(id uint) error { + // 1. 更新总量 + qrcode, err := GetQrcodeById(id) + if err != nil { + return err + } + qrcode.ScanCount++ + + // 2. 更新新增量 + err = trackService.AddCount(counterName.QrcodeScan) + if err != nil { + return err + } + + return SaveQrcode(qrcode) +} diff --git a/app/services/qrcodeService/getList.go b/app/services/qrcodeService/getList.go index 73813e3..a02c6d7 100644 --- a/app/services/qrcodeService/getList.go +++ b/app/services/qrcodeService/getList.go @@ -7,9 +7,11 @@ import ( ) // GetList 获取权益码信息列表的筛选,搜索,分页 +// revive:disable:flag-parameter func GetList( collegeFilter []uint, feedbackFilter []uint, + qrcodeStatus bool, keyword string, page int, pageSize int, ) (qrcodeList []models.Qrcode, total int64, err error) { @@ -23,6 +25,11 @@ func GetList( "OR description LIKE ?", keyword, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") } + // 筛选`权益码状态` + if qrcodeStatus { + query = query.Where("status = ?", 1) + } + // 筛选`责任部门` if len(collegeFilter) > 0 { query = query.Scopes(dbUtils.Filter("college", collegeFilter)) diff --git a/app/services/trackService/counter/addCount.go b/app/services/trackService/counter/addCount.go new file mode 100644 index 0000000..3ef78ce --- /dev/null +++ b/app/services/trackService/counter/addCount.go @@ -0,0 +1,38 @@ +package trackService + +import ( + "errors" + "time" + + "4u-go/app/models" + "4u-go/config/database" + "gorm.io/gorm" +) + +// AddCount 用于更新某一字段在某一天的增量 +func AddCount(name string) error { + day := time.Now().Unix() / 86400 + counter, err := getCounter(name, day) + + if errors.Is(err, gorm.ErrRecordNotFound) { + // 记录不存在则创建 + counter.Name = name + counter.Day = day + counter.Count = 1 + return saveCounter(counter) + } + if err != nil { + return err + } + counter.Count++ + return saveCounter(counter) +} + +func getCounter(name string, day int64) (counter models.Counter, err error) { + err = database.DB.Where("name=? AND day=?", name, day).First(&counter).Error + return counter, err +} + +func saveCounter(counter models.Counter) error { + return database.DB.Save(&counter).Error +} diff --git a/config/database/migrations.go b/config/database/migrations.go index a3955f5..fc02db2 100755 --- a/config/database/migrations.go +++ b/config/database/migrations.go @@ -14,5 +14,6 @@ func autoMigrate(db *gorm.DB) error { &models.Website{}, &models.College{}, &models.Qrcode{}, + &models.Counter{}, ) } diff --git a/config/router/router.go b/config/router/router.go index e2f5be7..fac8b3c 100644 --- a/config/router/router.go +++ b/config/router/router.go @@ -69,7 +69,6 @@ func Init(r *gin.Engine) { adminQrcode := admin.Group("/qrcode", midwares.CheckAdmin) { - adminQrcode.PUT("/status", qrcodeController.ToggleStatus) adminQrcode.POST("", qrcodeController.CreateQrcode) adminQrcode.DELETE("", midwares.CheckSuperAdmin, qrcodeController.DeleteQrcode) adminQrcode.GET("", qrcodeController.GetQrcode) @@ -99,5 +98,10 @@ func Init(r *gin.Engine) { { website.GET("/list", websiteController.GetWebsiteList) } + + track := api.Group("/track") + { + track.GET("/qrcode/scan_count", qrcodeController.ScanCount) + } } } diff --git a/makefile b/makefile index b7d1723..8a12a8e 100644 --- a/makefile +++ b/makefile @@ -13,7 +13,7 @@ check-gcc: # 构建目标 build: - check-gcc + $(check-gcc) @echo "Building $(TARGET)..." go build -v -o $(TARGET) .