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/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..4c6c6b1 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,32 @@ +name: Lint + +# 定义触发条件:在 `dev` 分支的 push 或 pull request 事件触发工作流 +on: + push: + branches: + - dev # 当向 dev 分支推送时触发 + pull_request: + branches: + - dev # 当对 dev 分支发起拉取请求时触发 + +jobs: + lint: + runs-on: ubuntu-latest # 指定工作流运行在最新的 Ubuntu 环境中 + + steps: + # 第一步:检查出代码 + - name: Checkout code + uses: actions/checkout@v4 # 使用 GitHub 提供的 checkout 动作,确保代码在工作流环境中可用 + + # 第二步:设置 Go 环境 + - name: Set up Go + uses: actions/setup-go@v5 # 使用 GitHub 提供的 setup-go 动作,设置 Go 环境 + with: + go-version: stable + + # 第三步:运行 golangci-lint + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 # 使用 golangci-lint-action + with: + version: v1.62.0 # 指定 golangci-lint 版本 + args: '--config .golangci.yml' # 使用指定的配置文件 diff --git a/.gitignore b/.gitignore index 3ea3729..3f6096e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,6 @@ go.work.sum config.yaml .idea -.vscode \ No newline at end of file +.vscode + +logs \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 1aa81e6..3d99cd8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,8 +2,8 @@ run: concurrency: 4 # 并行执行的 linter 数量,默认为可用 CPU 数量 - deadline: 1m # 分析的最大超时时间,默认为 1 分钟 - timeout: 5m # 超时时间 + deadline: 5m # 分析的最大超时时间,默认为 5 分钟 + timeout: 10m # 超时时间 issues-exit-code: 1 # 如果发现至少一个问题,退出码为 1 tests: true # 包含测试文件进行分析 modules-download-mode: readonly # 防止分析过程中修改 go.mod 文件 @@ -28,27 +28,46 @@ linters-settings: # 默认失败置信度 confidence: 0.1 rules: - - name: var-naming # 变量命名规则 + - name: var-naming # 变量命名规则 severity: warning disabled: true - - name: line-length-limit # 行长度限制 + - name: line-length-limit # 行长度限制 severity: warning disabled: false exclude: [ "" ] - arguments: [ 110 ] - - name: add-constant #使用命名常量而非魔法数字 + arguments: [ 120 ] + - name: add-constant # 使用命名常量而非魔法数字 severity: warning disabled: true - - name: package-comments #包注释 + - name: package-comments # 包注释 severity: warning disabled: true - - name: import-alias-naming #导入别名命名规则 + - name: import-alias-naming # 导入别名命名规则 severity: warning disabled: true + - name: get-return # 使用get必须有返回值 + severity: warning + disabled: true + - name: max-public-structs # 最大公共结构体数目 + severity: warning + disabled: true + - name: argument-limit # 函数参数限制 + severity: warning + disabled: true + - name: function-length # 函数长度限制 + severity: warning + disabled: true + - name: cyclomatic # 圈复杂度 + severity: warning + disabled: true + - name: cognitive-complexity # 认知复杂度 + severity: warning + disabled: false + arguments: [ 10 ] gofmt: simplify: true # 使用 gofmt 的 `-s` 选项简化代码 gocyclo: - min-complexity: 5 # 触发报告的最小代码复杂度 + min-complexity: 10 # 触发报告的最小代码复杂度 dupl: threshold: 100 # 触发报告的最小令牌数 goconst: diff --git a/README.md b/README.md index f470a6e..48fce41 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # 4UOnline-Go 校学生会权益服务系统 + +点此查看[项目说明](docs/README.md) \ No newline at end of file diff --git a/app/apiException/apiException.go b/app/apiException/apiException.go index 94c7bea..7d3456f 100644 --- a/app/apiException/apiException.go +++ b/app/apiException/apiException.go @@ -1,40 +1,73 @@ package apiException -import "net/http" +import ( + "net/http" -// Error 表示自定义错误,包括状态码、代码和消息。 + "4u-go/app/utils/log" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// Error 表示自定义错误,包括状态码、消息和日志级别。 type Error struct { - StatusCode int `json:"-"` - Code int `json:"code"` - Msg string `json:"msg"` + Code int + Msg string + Level log.Level } // Error 表示自定义的错误类型 var ( - ServerError = NewError(http.StatusInternalServerError, 200500, "系统异常,请稍后重试!") - OpenIDError = NewError(http.StatusInternalServerError, 200500, "系统异常,请稍后重试!") - ParamError = NewError(http.StatusInternalServerError, 200501, "参数错误") + ServerError = NewError(200500, log.LevelError, "系统异常,请稍后重试!") + OpenIDError = NewError(200500, log.LevelInfo, "系统异常,请稍后重试!") + ParamError = NewError(200501, log.LevelInfo, "参数错误") + ReactiveError = NewError(200502, log.LevelInfo, "该通行证已经存在,请重新输入") + UserAlreadyExisted = NewError(200503, log.LevelInfo, "该用户已激活") + RequestError = NewError(200504, log.LevelInfo, "系统异常,请稍后重试!") + StudentNumAndIidError = NewError(200505, log.LevelInfo, "该学号或身份证不存在或者不匹配,请重新输入") + PwdError = NewError(200506, log.LevelInfo, "密码长度必须在6~20位之间") + UserNotFound = NewError(200507, log.LevelInfo, "该用户不存在") + NoThatPasswordOrWrong = NewError(200508, log.LevelInfo, "密码错误") + NotLogin = NewError(200509, log.LevelInfo, "未登录") + NotPermission = NewError(200510, log.LevelInfo, "该用户无权限") + ResourceNotFound = NewError(200511, log.LevelInfo, "访问的资源不存在") + AdminKeyError = NewError(200512, log.LevelInfo, "管理员注册密钥错误") + AdminAlreadyExisted = NewError(200513, log.LevelInfo, "管理员账号已存在") + UploadFileError = NewError(200514, log.LevelInfo, "上传文件失败") + FileSizeExceedError = NewError(200515, log.LevelInfo, "文件大小超限") + FileNotImageError = NewError(200516, log.LevelInfo, "上传的文件不是图片") - NotInit = NewError(http.StatusNotFound, 200404, http.StatusText(http.StatusNotFound)) - NotFound = NewError(http.StatusNotFound, 200404, http.StatusText(http.StatusNotFound)) - Unknown = NewError(http.StatusInternalServerError, 300500, "系统异常,请稍后重试!") + NotFound = NewError(200404, log.LevelWarn, http.StatusText(http.StatusNotFound)) ) -// OtherError 返回一个表示其他未定义错误的自定义错误消息 -func OtherError(message string) *Error { - return NewError(http.StatusForbidden, 100403, message) -} - // Error 方法实现了 error 接口,返回错误的消息内容 func (e *Error) Error() string { return e.Msg } // NewError 创建并返回一个新的自定义错误实例 -func NewError(statusCode, code int, msg string) *Error { +func NewError(code int, level log.Level, msg string) *Error { return &Error{ - StatusCode: statusCode, - Code: code, - Msg: msg, + Code: code, + Msg: msg, + Level: level, + } +} + +// AbortWithException 用于返回自定义错误信息 +func AbortWithException(c *gin.Context, apiError *Error, err error) { + logError(c, apiError, err) + _ = c.AbortWithError(200, apiError) //nolint:errcheck +} + +// logError 记录错误日志 +func logError(c *gin.Context, apiErr *Error, err error) { + // 构建日志字段 + logFields := []zap.Field{ + zap.Int("error_code", apiErr.Code), + zap.String("path", c.Request.URL.Path), + zap.String("method", c.Request.Method), + zap.String("ip", c.ClientIP()), + zap.Error(err), // 记录原始错误信息 } + log.GetLogFunc(apiErr.Level)(apiErr.Msg, logFields...) } 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/config/config.go b/app/config/config.go deleted file mode 100644 index e4500ea..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.RedisClient.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.RedisClient.Set(ctx, key, config.Value, 0) - return config.Value -} - -// setConfig 设置指定的配置项,如果不存在则创建新的配置。 -func setConfig(key, value string) error { - redis.RedisClient.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.RedisClient.Exists(ctx, key) - return intCmd.Val() == 1 -} - -func delConfig(key string) error { - redis.RedisClient.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/controllers/activityController/create.go b/app/controllers/activityController/create.go new file mode 100644 index 0000000..9bfe8da --- /dev/null +++ b/app/controllers/activityController/create.go @@ -0,0 +1,62 @@ +package activityController + +import ( + "time" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/activityService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type createActivityData struct { + Title string `json:"title" binding:"required"` + Introduction string `json:"introduction" binding:"required"` + Department string `json:"department" binding:"required"` + StartTime string `json:"start_time" binding:"required"` + EndTime string `json:"end_time" binding:"required"` + Campus []uint `json:"campus" binding:"required"` + Location string `json:"location" binding:"required"` + Img string `json:"img"` +} + +// CreateActivity 创建一条校园活动 +func CreateActivity(c *gin.Context) { + var data createActivityData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 转换时间 + startTime, err := time.Parse(time.RFC3339, data.StartTime) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + endTime, err := time.Parse(time.RFC3339, data.EndTime) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + err = activityService.SaveActivity(models.Activity{ + Title: data.Title, + Introduction: data.Introduction, + Department: data.Department, + StartTime: startTime, + EndTime: endTime, + Campus: utils.EncodeCampus(data.Campus), + Location: data.Location, + Img: data.Img, + AuthorID: utils.GetUser(c).ID, + }) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/activityController/delete.go b/app/controllers/activityController/delete.go new file mode 100644 index 0000000..7e3268c --- /dev/null +++ b/app/controllers/activityController/delete.go @@ -0,0 +1,55 @@ +package activityController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/activityService" + "4u-go/app/services/objectService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type deleteActivityData struct { + ID uint `json:"id" binding:"required"` +} + +// DeleteActivity 删除一条校园活动 +func DeleteActivity(c *gin.Context) { + var data deleteActivityData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 判断活动是否存在 + activity, err := activityService.GetActivityById(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 + } + + user := utils.GetUser(c) + if activity.AuthorID != user.ID && user.Type != models.SuperAdmin { + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + + // 删除活动对应的图片 + objectService.DeleteObjectByUrlAsync(activity.Img) + + err = activityService.DeleteActivityById(data.ID) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/activityController/get.go b/app/controllers/activityController/get.go new file mode 100644 index 0000000..d10f9df --- /dev/null +++ b/app/controllers/activityController/get.go @@ -0,0 +1,96 @@ +package activityController + +import ( + "errors" + "time" + + "4u-go/app/apiException" + "4u-go/app/services/activityService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type getActivityListResponse struct { + ActivityList []activityElement `json:"activity_list"` +} + +type activityElement struct { + ID uint `json:"id"` + Title string `json:"title"` + Department string `json:"department"` + StartTime string `json:"start_time"` + Campus []uint `json:"campus"` + Img string `json:"img"` +} + +type getActivityData struct { + ID uint `json:"id" binding:"required"` +} + +type getActivityResponse struct { + Title string `json:"title"` + Introduction string `json:"introduction"` + Department string `json:"department"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + Campus []uint `json:"campus"` + Location string `json:"location"` + Img string `json:"img"` +} + +// GetActivityList 获取校园活动列表 +func GetActivityList(c *gin.Context) { + list, err := activityService.GetActivityList() + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + activityList := make([]activityElement, 0) + for _, activity := range list { + activityList = append(activityList, activityElement{ + ID: activity.ID, + Title: activity.Title, + Department: activity.Department, + StartTime: activity.StartTime.Format(time.RFC3339), + Campus: utils.DecodeCampus(activity.Campus), + Img: activity.Img, + }) + } + + utils.JsonSuccessResponse(c, getActivityListResponse{ + ActivityList: activityList, + }) +} + +// GetActivity 获取活动详情 +func GetActivity(c *gin.Context) { + var data getActivityData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + activity, err := activityService.GetActivityById(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 + } + + utils.JsonSuccessResponse(c, getActivityResponse{ + Title: activity.Title, + Introduction: activity.Introduction, + Department: activity.Department, + StartTime: activity.StartTime.Format(time.RFC3339), + EndTime: activity.EndTime.Format(time.RFC3339), + Campus: utils.DecodeCampus(activity.Campus), + Location: activity.Location, + Img: activity.Img, + }) +} diff --git a/app/controllers/activityController/update.go b/app/controllers/activityController/update.go new file mode 100644 index 0000000..23ea8ec --- /dev/null +++ b/app/controllers/activityController/update.go @@ -0,0 +1,87 @@ +package activityController + +import ( + "errors" + "time" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/activityService" + "4u-go/app/services/objectService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type updateActivityData struct { + ID uint `json:"id" binding:"required"` + Title string `json:"title" binding:"required"` + Introduction string `json:"introduction" binding:"required"` + Department string `json:"department" binding:"required"` + StartTime string `json:"start_time" binding:"required"` + EndTime string `json:"end_time" binding:"required"` + Campus []uint `json:"campus" binding:"required"` + Location string `json:"location" binding:"required"` + Img string `json:"img"` +} + +// UpdateActivity 更新校园活动 +func UpdateActivity(c *gin.Context) { + var data updateActivityData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 转换时间 + startTime, err := time.Parse(time.RFC3339, data.StartTime) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + endTime, err := time.Parse(time.RFC3339, data.EndTime) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + activity, err := activityService.GetActivityById(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 + } + + user := utils.GetUser(c) + if activity.AuthorID != user.ID && user.Type != models.SuperAdmin { + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + + if data.Img != activity.Img { // 若图片更换则删除旧图片 + objectService.DeleteObjectByUrlAsync(activity.Img) + } + + { // 更新活动信息 + activity.Title = data.Title + activity.Introduction = data.Introduction + activity.Department = data.Department + activity.StartTime = startTime + activity.EndTime = endTime + activity.Campus = utils.EncodeCampus(data.Campus) + activity.Location = data.Location + activity.Img = data.Img + } + + err = activityService.SaveActivity(activity) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/adminController/create.go b/app/controllers/adminController/create.go new file mode 100644 index 0000000..2ded1e9 --- /dev/null +++ b/app/controllers/adminController/create.go @@ -0,0 +1,54 @@ +package adminController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/adminService" + "4u-go/app/utils" + "4u-go/config/config" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type createAdminByKeyData struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + Key string `json:"key" binding:"required"` +} + +// CreateAdminByKey 通过密钥创建普通管理员 +func CreateAdminByKey(c *gin.Context) { + var data createAdminByKeyData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + key := config.Config.GetString("admin.key") + if data.Key != key { + apiException.AbortWithException(c, apiException.AdminKeyError, err) + return + } + + _, err = adminService.GetUserByUsername(data.Username) + if err == nil { + apiException.AbortWithException(c, apiException.AdminAlreadyExisted, err) + return + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + _, err = adminService.CreateAdminUser(data.Username, data.Password, models.ForU, "") + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + utils.JsonSuccessResponse(c, gin.H{ + "username": data.Username, + "password": data.Password, + }) +} diff --git a/app/controllers/announcementController/create.go b/app/controllers/announcementController/create.go new file mode 100644 index 0000000..1cd1df2 --- /dev/null +++ b/app/controllers/announcementController/create.go @@ -0,0 +1,38 @@ +package announcementController + +import ( + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/announcementService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type createAnnouncementData struct { + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` + Department string `json:"department" binding:"required"` +} + +// CreateAnnouncement 创建一条公告通知 +func CreateAnnouncement(c *gin.Context) { + var data createAnnouncementData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + err = announcementService.SaveAnnouncement(models.Announcement{ + Title: data.Title, + Content: data.Content, + Department: data.Department, + AuthorID: utils.GetUser(c).ID, + }) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/announcementController/delete.go b/app/controllers/announcementController/delete.go new file mode 100644 index 0000000..24a4496 --- /dev/null +++ b/app/controllers/announcementController/delete.go @@ -0,0 +1,51 @@ +package announcementController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/announcementService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type deleteAnnouncementData struct { + ID uint `json:"id" binding:"required"` +} + +// DeleteAnnouncement 删除一条公告通知 +func DeleteAnnouncement(c *gin.Context) { + var data deleteAnnouncementData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 判断公告是否存在 + announcement, err := announcementService.GetAnnouncementById(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 + } + + user := utils.GetUser(c) + if announcement.AuthorID != user.ID && user.Type != models.SuperAdmin { + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + + err = announcementService.DeleteAnnouncementById(data.ID) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/announcementController/get.go b/app/controllers/announcementController/get.go new file mode 100644 index 0000000..a48882b --- /dev/null +++ b/app/controllers/announcementController/get.go @@ -0,0 +1,82 @@ +package announcementController + +import ( + "errors" + "time" + + "4u-go/app/apiException" + "4u-go/app/services/announcementService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type getAnnouncementData struct { + ID uint `json:"id" binding:"required"` +} + +type getAnnouncementResponse struct { + Title string `json:"title"` + Content string `json:"content"` + PublishTime string `json:"publish_time"` + Department string `json:"department"` +} + +type getAnnouncementListResponse struct { + AnnouncementList []announcementElement `json:"announcement_list"` +} + +type announcementElement struct { + ID uint `json:"id"` + Title string `json:"title"` + PublishTime string `json:"publish_time"` +} + +// GetAnnouncementList 获取公告列表 +func GetAnnouncementList(c *gin.Context) { + list, err := announcementService.GetAnnouncementList() + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + announcementList := make([]announcementElement, 0) + for _, announcement := range list { + announcementList = append(announcementList, announcementElement{ + ID: announcement.ID, + Title: announcement.Title, + PublishTime: announcement.CreatedAt.Format(time.RFC3339), + }) + } + + utils.JsonSuccessResponse(c, getAnnouncementListResponse{ + AnnouncementList: announcementList, + }) +} + +// GetAnnouncement 获取公告详情 +func GetAnnouncement(c *gin.Context) { + var data getAnnouncementData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + announcement, err := announcementService.GetAnnouncementById(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 + } + + utils.JsonSuccessResponse(c, getAnnouncementResponse{ + Title: announcement.Title, + Content: announcement.Content, + PublishTime: announcement.CreatedAt.Format(time.RFC3339), + Department: announcement.Department, + }) +} diff --git a/app/controllers/announcementController/update.go b/app/controllers/announcementController/update.go new file mode 100644 index 0000000..354ae09 --- /dev/null +++ b/app/controllers/announcementController/update.go @@ -0,0 +1,59 @@ +package announcementController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/announcementService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type updateAnnouncementData struct { + ID uint `json:"id" binding:"required"` + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` + Department string `json:"department" binding:"required"` +} + +// UpdateAnnouncement 创建一条公告通知 +func UpdateAnnouncement(c *gin.Context) { + var data updateAnnouncementData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + announcement, err := announcementService.GetAnnouncementById(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 + } + + user := utils.GetUser(c) + if announcement.AuthorID != user.ID && user.Type != models.SuperAdmin { + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + + { // 更新公告信息 + announcement.Title = data.Title + announcement.Content = data.Content + announcement.Department = data.Department + } + + err = announcementService.SaveAnnouncement(announcement) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/collegeController/create.go b/app/controllers/collegeController/create.go new file mode 100644 index 0000000..127345b --- /dev/null +++ b/app/controllers/collegeController/create.go @@ -0,0 +1,33 @@ +package collegeController + +import ( + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/collegeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type createCollegeData struct { + Name string `json:"name" binding:"required"` +} + +// CreateCollege 新建学院 +func CreateCollege(c *gin.Context) { + var data createCollegeData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + err = collegeService.SaveCollege(models.College{ + Name: data.Name, + }) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/collegeController/delete.go b/app/controllers/collegeController/delete.go new file mode 100644 index 0000000..2a13aa1 --- /dev/null +++ b/app/controllers/collegeController/delete.go @@ -0,0 +1,44 @@ +package collegeController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/services/collegeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type deleteCollegeData struct { + ID uint `json:"id" binding:"required"` +} + +// DeleteCollege 删除学院 +func DeleteCollege(c *gin.Context) { + var data deleteCollegeData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 判断学院是否存在 + _, err = collegeService.GetCollegeById(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 = collegeService.DeleteCollegeById(data.ID) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/collegeController/get.go b/app/controllers/collegeController/get.go new file mode 100644 index 0000000..25a9142 --- /dev/null +++ b/app/controllers/collegeController/get.go @@ -0,0 +1,26 @@ +package collegeController + +import ( + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/collegeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type getCollegeResponse struct { + CollegeList []models.College `json:"college_list"` +} + +// GetCollegeList 获取学院列表 +func GetCollegeList(c *gin.Context) { + collegeList, err := collegeService.GetCollegeList() + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, getCollegeResponse{ + CollegeList: collegeList, + }) +} diff --git a/app/controllers/collegeController/update.go b/app/controllers/collegeController/update.go new file mode 100644 index 0000000..5cefdd2 --- /dev/null +++ b/app/controllers/collegeController/update.go @@ -0,0 +1,48 @@ +package collegeController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/services/collegeService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type updateCollegeData struct { + ID uint `json:"id" binding:"required"` + Name string `json:"name" binding:"required"` +} + +// UpdateCollege 更新学院信息 +func UpdateCollege(c *gin.Context) { + var data updateCollegeData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + college, err := collegeService.GetCollegeById(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 + } + + { // 更新学院信息 + college.Name = data.Name + } + + err = collegeService.SaveCollege(college) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/lostAndFoundController/create.go b/app/controllers/lostAndFoundController/create.go new file mode 100644 index 0000000..f52d717 --- /dev/null +++ b/app/controllers/lostAndFoundController/create.go @@ -0,0 +1,63 @@ +package lostAndFoundController + +import ( + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/lostAndFoundService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type createLostAndFoundData struct { + Type bool `json:"type"` // 1-失物 0-寻物 + Name string `json:"name" binding:"required"` // 物品名称 + Introduction string `json:"introduction" binding:"required"` // 物品介绍 + Campus uint8 `json:"campus"` // 校区 0-其他 1-朝晖 2-屏峰 3-莫干山 + Kind uint8 `json:"kind"` // 物品种类 1其他2证件3箱包4首饰5现金6电子产品7钥匙 + Place string `json:"place" binding:"required"` // 丢失或拾得地点 + Time string `json:"time" binding:"required"` // 丢失或拾得时间 + Imgs []string `json:"imgs"` // 物品图片,多个图片以逗号分隔 + Contact string `json:"contact" binding:"required"` // 联系方式 + ContactWay uint8 `json:"contact_way" binding:"required"` // 联系方式选项 1-手机号 2-qq 3-微信 4-邮箱 +} + +// CreateLostAndFound 创建一条失物招领 +func CreateLostAndFound(c *gin.Context) { + var data createLostAndFoundData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 判断imgs是否大于9 + if len(data.Imgs) > 9 { + apiException.AbortWithException(c, apiException.ParamError, nil) + return + } + + // 将[]string转为string + imgs := utils.StringsToString(data.Imgs) + + err = lostAndFoundService.SaveLostAndFound(models.LostAndFoundRecord{ + Type: data.Type, + Name: data.Name, + Introduction: data.Introduction, + Campus: data.Campus, + Kind: data.Kind, + Place: data.Place, + Time: data.Time, + Imgs: imgs, + Publisher: utils.GetUser(c).StudentID, + Contact: data.Contact, + ContactWay: data.ContactWay, + IsProcessed: 2, + IsApproved: 2, + }) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/lostAndFoundController/delete.go b/app/controllers/lostAndFoundController/delete.go new file mode 100644 index 0000000..50117ce --- /dev/null +++ b/app/controllers/lostAndFoundController/delete.go @@ -0,0 +1,51 @@ +package lostAndFoundController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/lostAndFoundService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type deleteLostAndFoundData struct { + ID uint `json:"id" binding:"required"` +} + +// DeleteLostAndFound 撤回一条失物招领 +func DeleteLostAndFound(c *gin.Context) { + var data deleteLostAndFoundData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 判断失物招领是否存在 + record, err := lostAndFoundService.GetLostAndFoundById(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 + } + + user := utils.GetUser(c) + if user.Type != models.SuperAdmin && user.Type != models.ForU && user.StudentID != record.Publisher { + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + + err = lostAndFoundService.DeleteLostAndFoundById(data.ID) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/lostAndFoundController/get.go b/app/controllers/lostAndFoundController/get.go new file mode 100644 index 0000000..f7440c9 --- /dev/null +++ b/app/controllers/lostAndFoundController/get.go @@ -0,0 +1,168 @@ +package lostAndFoundController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/services/lostAndFoundService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type getLostAndFoundListData struct { + Type bool `json:"type"` // 1-失物 0-寻物 + Campus uint8 `json:"campus"` // 校区 0-其他 1-朝晖 2-屏峰 3-莫干山 + Kind uint8 `json:"kind"` // 物品种类 0-全部 1-其他 2-饭卡 3-电子 4-文体 5-衣包 6-证件 +} +type getLostAndFoundListResponse struct { + LostAndFoundList []lostAndFoundElement `json:"list"` +} +type lostAndFoundElement struct { + ID uint `json:"id"` + Imgs []string `json:"imgs"` + Name string `json:"name"` + Place string `json:"place"` + Time string `json:"time"` + Introduction string `json:"introduction"` + Kind uint8 `json:"kind"` +} + +// GetLostAndFoundList 获取失物招领列表 +func GetLostAndFoundList(c *gin.Context) { + var data getLostAndFoundListData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + list, err := lostAndFoundService.GetLostAndFoundList(data.Type, data.Campus, data.Kind) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + lostAndFoundList := make([]lostAndFoundElement, 0) + for _, record := range list { + // 将string转为[]string + imgs := utils.StringToStrings(record.Imgs) + lostAndFoundList = append(lostAndFoundList, lostAndFoundElement{ + ID: record.ID, + Imgs: imgs, + Name: record.Name, + Place: record.Place, + Time: record.Time, + Introduction: record.Introduction, + }) + } + + utils.JsonSuccessResponse(c, getLostAndFoundListResponse{ + LostAndFoundList: lostAndFoundList, + }) +} + +type getLostAndFoundContentData struct { + ID uint `json:"id" binding:"required"` +} + +// GetLostAndFoundContact 获取失物招领联系方式 +func GetLostAndFoundContact(c *gin.Context) { + var data getLostAndFoundContentData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + contact, err := lostAndFoundService.GetLostAndFoundContact(data.ID, utils.GetUser(c).StudentID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.ResourceNotFound, err) + } else { + apiException.AbortWithException(c, apiException.ServerError, err) + } + return + } + + utils.JsonSuccessResponse(c, contact) +} + +type latestLostAndFoundResponse struct { + Type bool `json:"type"` + Imgs string `json:"imgs"` + Name string `json:"name"` + Place string `json:"place"` + Introduction string `json:"introduction"` +} + +// GetLatestLostAndFound 获取最新失物招领 +func GetLatestLostAndFound(c *gin.Context) { + record, err := lostAndFoundService.GetLatestLostAndFound() + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, latestLostAndFoundResponse{ + Type: record.Type, + Imgs: record.Imgs, + Name: record.Name, + Place: record.Place, + Introduction: record.Introduction, + }) +} + +type getLostAndFoundStatusData struct { + Status uint8 `json:"status"` // 状态 0-已撤回 1-已审核 2-审核中 +} +type getLostAndFoundStatusResponse struct { + List []lostAndFoundStatusElement `json:"list"` +} +type lostAndFoundStatusElement struct { + ID uint `json:"id"` + Type bool `json:"type"` + Imgs []string `json:"imgs"` + Name string `json:"name"` + Kind uint8 `json:"kind"` + Place string `json:"place"` + Time string `json:"time"` + Introduction string `json:"introduction"` + IsApproved uint8 `json:"is_approved"` +} + +// GetUserLostAndFoundStatus 查看失物招领信息的状态 +func GetUserLostAndFoundStatus(c *gin.Context) { + var data getLostAndFoundStatusData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + list, err := lostAndFoundService.GetUserLostAndFoundStatus(utils.GetUser(c).StudentID, data.Status) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + lostAndFoundList := make([]lostAndFoundStatusElement, 0) + for _, record := range list { + // 将string转为[]string + imgs := utils.StringToStrings(record.Imgs) + lostAndFoundList = append(lostAndFoundList, lostAndFoundStatusElement{ + ID: record.ID, + Type: record.Type, + Imgs: imgs, + Name: record.Name, + Kind: record.Kind, + Place: record.Place, + Time: record.Time, + Introduction: record.Introduction, + IsApproved: record.IsApproved, + }) + } + utils.JsonSuccessResponse(c, getLostAndFoundStatusResponse{ + List: lostAndFoundList, + }) +} diff --git a/app/controllers/lostAndFoundController/update.go b/app/controllers/lostAndFoundController/update.go new file mode 100644 index 0000000..eaba049 --- /dev/null +++ b/app/controllers/lostAndFoundController/update.go @@ -0,0 +1,157 @@ +package lostAndFoundController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/lostAndFoundService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type reviewLostAndFoundData struct { + ID uint `json:"id" binding:"required"` + IsApproved bool `json:"is_approved"` +} + +// ReviewLostAndFound 审核失物招领 +func ReviewLostAndFound(c *gin.Context) { + var data reviewLostAndFoundData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 判断失物招领是否存在 + _, err = lostAndFoundService.GetLostAndFoundById(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 + } + + if data.IsApproved { + err = lostAndFoundService.ApproveLostAndFound(data.ID) + } else { + err = lostAndFoundService.RejectLostAndFound(data.ID) + } + + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} + +type updateLostAndFoundData struct { + ID uint `json:"id" binding:"required"` + Type bool `json:"type" binding:"required"` // 1-失物 0-寻物 + Name string `json:"name" binding:"required"` // 物品名称 + Introduction string `json:"introduction" binding:"required"` // 物品介绍 + Campus uint8 `json:"campus" binding:"required"` // 校区 1-朝晖 2-屏峰 3-莫干山 + Kind uint8 `json:"kind" binding:"required"` // 物品种类 1其他2证件3箱包4首饰5现金6电子产品7钥匙 + Place string `json:"place" binding:"required"` // 丢失或拾得地点 + Time string `json:"time" binding:"required"` // 丢失或拾得时间 + Imgs string `json:"imgs"` // 物品图片,多个图片以逗号分隔 + PickupPlace string `json:"pickup_place" binding:"required"` // 失物领取地点 + Contact string `json:"contact" binding:"required"` // 联系方式 +} + +// UpdateLostAndFound 修改失物招领 +func UpdateLostAndFound(c *gin.Context) { + var data updateLostAndFoundData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 判断失物招领是否存在 + record, err := lostAndFoundService.GetLostAndFoundById(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 + } + + user := utils.GetUser(c) + if user.Type != models.SuperAdmin && user.Type != models.ForU { + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + + { // 更新失物招领信息 + record.Type = data.Type + record.Name = data.Name + record.Introduction = data.Introduction + record.Campus = data.Campus + record.Kind = data.Kind + record.Place = data.Place + record.Time = data.Time + record.Imgs = data.Imgs + record.Contact = data.Contact + record.IsApproved = 2 + record.IsProcessed = 2 + } + + err = lostAndFoundService.SaveLostAndFound(record) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} + +type updateLostAndFoundStatusData struct { + ID uint `json:"id" binding:"required"` +} + +// UpdateLostAndFoundStatus 用户设置失物招领为已完成 +func UpdateLostAndFoundStatus(c *gin.Context) { + var data updateLostAndFoundStatusData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 判断失物招领是否存在 + record, err := lostAndFoundService.GetLostAndFoundById(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 + } + + user := utils.GetUser(c) + if user.StudentID != record.Publisher { + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + + { // 更新失物招领信息 + record.IsProcessed = 1 + } + + err = lostAndFoundService.SaveLostAndFound(record) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/objectController/upload.go b/app/controllers/objectController/upload.go new file mode 100644 index 0000000..ef5764c --- /dev/null +++ b/app/controllers/objectController/upload.go @@ -0,0 +1,69 @@ +package objectController + +import ( + "errors" + "image" + "mime/multipart" + + "4u-go/app/apiException" + "4u-go/app/services/objectService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type uploadFileData struct { + File *multipart.FileHeader `form:"file" binding:"required"` +} + +// UploadFile 上传文件 +func UploadFile(c *gin.Context) { + var data uploadFileData + if err := c.ShouldBind(&data); err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + fileSize := data.File.Size + 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) + + // 获取文件信息 + if fileSize > objectService.ImageLimit { + apiException.AbortWithException(c, apiException.FileSizeExceedError, nil) + return + } + + reader, size, err := objectService.ConvertToWebP(file) + if errors.Is(err, image.ErrFormat) { + apiException.AbortWithException(c, apiException.FileNotImageError, err) + return + } + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + contentType := "image/webp" + + // 上传文件 + objectKey := objectService.GenerateObjectKey("image", ".webp") + objectUrl, err := objectService.PutObject(objectKey, reader, size, contentType) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, gin.H{ + "type": contentType, + "url": objectUrl, + }) +} 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/create.go b/app/controllers/qrcodeController/create.go new file mode 100644 index 0000000..bdf138a --- /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"` + Department string `json:"department" binding:"required"` + Description string `json:"description"` + FeedbackType uint `json:"feedback_type"` + 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..9d78938 --- /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..b1e381f --- /dev/null +++ b/app/controllers/qrcodeController/get.go @@ -0,0 +1,82 @@ +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" + "gorm.io/gorm" +) + +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 + err := c.ShouldBind(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + qrcode, 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 + } + + resp, err := generateResp(qrcode) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + 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..c57277d --- /dev/null +++ b/app/controllers/qrcodeController/getList.go @@ -0,0 +1,63 @@ +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"` + Status bool `json:"status"` +} + +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, + filter.Status, + 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 new file mode 100644 index 0000000..d3cd86a --- /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"` + FeedbackType uint `json:"feedback_type" binding:"required"` + Location string `json:"location" binding:"required"` + Status bool `json:"status"` +} + +// 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 err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.ResourceNotFound, err) + } else { + 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 + 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/userController/auth.go b/app/controllers/userController/auth.go new file mode 100644 index 0000000..da49468 --- /dev/null +++ b/app/controllers/userController/auth.go @@ -0,0 +1,122 @@ +package userController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/services/sessionService" + "4u-go/app/services/userService" + "4u-go/app/utils" + "4u-go/config/wechat" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type passwordLoginForm struct { + StudentID string `json:"student_id" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// AuthByPassword 通过密码认证 +func AuthByPassword(c *gin.Context) { + var postForm passwordLoginForm + err := c.ShouldBindJSON(&postForm) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + user, err := userService.GetUserByStudentID(postForm.StudentID) + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.UserNotFound, err) + return + } + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + if err := userService.AuthenticateUser(user, postForm.Password); err != nil { + var apiErr *apiException.Error + if errors.As(err, &apiErr) { + apiException.AbortWithException(c, apiErr, err) + } else { + apiException.AbortWithException(c, apiException.ServerError, err) + } + return + } + + err = sessionService.SetUserSession(c, user) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + utils.JsonSuccessResponse(c, gin.H{ + "user": gin.H{ + "id": user.ID, + "studentID": user.StudentID, + "userType": user.Type, + "college": user.College, + }, + }) +} + +type autoLoginForm struct { + Code string `json:"code" binding:"required"` +} + +// AuthBySession 通过session认证 +func AuthBySession(c *gin.Context) { + user, err := sessionService.UpdateUserSession(c) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + utils.JsonSuccessResponse(c, gin.H{ + "user": gin.H{ + "id": user.ID, + "studentID": user.StudentID, + "userType": user.Type, + "college": user.College, + }, + }) +} + +// WeChatLogin 微信登录 +func WeChatLogin(c *gin.Context) { + var postForm autoLoginForm + err := c.ShouldBindJSON(&postForm) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + session, err := wechat.MiniProgram.GetAuth().Code2Session(postForm.Code) + if err != nil { + apiException.AbortWithException(c, apiException.OpenIDError, err) + return + } + + user, err := userService.GetUserByWechatOpenID(session.OpenID) + if errors.Is(err, gorm.ErrRecordNotFound) { + apiException.AbortWithException(c, apiException.UserNotFound, err) + return + } else if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + err = sessionService.SetUserSession(c, user) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + utils.JsonSuccessResponse(c, gin.H{ + "user": gin.H{ + "id": user.ID, + "studentID": user.StudentID, + "userType": user.Type, + "college": user.College, + }, + }) +} diff --git a/app/controllers/userController/delete.go b/app/controllers/userController/delete.go new file mode 100644 index 0000000..d27e464 --- /dev/null +++ b/app/controllers/userController/delete.go @@ -0,0 +1,51 @@ +package userController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/userService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type deleteAccountData struct { + StudentID string `json:"student_id" binding:"required"` + IdentityID string `json:"identity_id" binding:"required"` +} + +// DeleteAccount 注销账户 +func DeleteAccount(c *gin.Context) { + var data deleteAccountData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + user := utils.GetUser(c) + if user.StudentID != data.StudentID { + apiException.AbortWithException(c, apiException.NotPermission, err) + return + } + + // 若不是普通用户则提示不存在 + if user.Type != models.Undergraduate && user.Type != models.Postgraduate { + apiException.AbortWithException(c, apiException.UserNotFound, nil) + return + } + + err = userService.DeleteAccount(user, data.IdentityID) + if err != nil { + var apiErr *apiException.Error + if errors.As(err, &apiErr) { + apiException.AbortWithException(c, apiErr, err) + } else { + apiException.AbortWithException(c, apiException.ServerError, err) + } + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/userController/reg.go b/app/controllers/userController/reg.go new file mode 100644 index 0000000..5f43ef0 --- /dev/null +++ b/app/controllers/userController/reg.go @@ -0,0 +1,113 @@ +package userController + +import ( + "errors" + "strings" + + "4u-go/app/apiException" + "4u-go/app/services/sessionService" + "4u-go/app/services/userService" + "4u-go/app/utils" + "4u-go/config/wechat" + "github.com/gin-gonic/gin" +) + +type createStudentUserWechatForm struct { + StudentID string `json:"studentID" binding:"required"` + Password string `json:"password" binding:"required"` + Type uint `json:"type" binding:"required"` // 用户类型 1-本科生 2-研究生 + IDCardNumber string `json:"idCardNumber" binding:"required"` + Name string `json:"name" binding:"required"` + College string `json:"college" binding:"required"` + Code string `json:"code" binding:"required"` + Email string `json:"email"` +} + +// BindOrCreateStudentUserFromWechat 微信创建学生用户 +func BindOrCreateStudentUserFromWechat(c *gin.Context) { + var postForm createStudentUserWechatForm + err := c.ShouldBindJSON(&postForm) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + session, err := wechat.MiniProgram.GetAuth().Code2Session(postForm.Code) + if err != nil { + apiException.AbortWithException(c, apiException.OpenIDError, err) + return + } + postForm.StudentID = strings.ToUpper(postForm.StudentID) + postForm.IDCardNumber = strings.ToUpper(postForm.IDCardNumber) + user, err := userService.CreateStudentUserWechat( + postForm.Password, + postForm.StudentID, + postForm.Type, + postForm.IDCardNumber, + postForm.Email, + postForm.Name, + postForm.College, + session.OpenID) + if err != nil { + var apiErr *apiException.Error + if errors.As(err, &apiErr) { + apiException.AbortWithException(c, apiErr, err) + } else { + apiException.AbortWithException(c, apiException.ServerError, err) + } + return + } + + err = sessionService.SetUserSession(c, user) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + utils.JsonSuccessResponse(c, nil) +} + +type createStudentUserForm struct { + StudentID string `json:"studentID" binding:"required"` + Password string `json:"password" binding:"required"` + Type uint `json:"type" binding:"required"` // 用户类型 1-本科生 2-研究生 + IDCardNumber string `json:"idCardNumber" binding:"required"` + Name string `json:"name" binding:"required"` + College string `json:"college" binding:"required"` + Email string `json:"email"` +} + +// CreateStudentUser H5创建学生用户 +func CreateStudentUser(c *gin.Context) { + var postForm createStudentUserForm + err := c.ShouldBindJSON(&postForm) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + postForm.StudentID = strings.ToUpper(postForm.StudentID) + postForm.IDCardNumber = strings.ToUpper(postForm.IDCardNumber) + user, err := userService.CreateStudentUser( + postForm.StudentID, + postForm.Password, + postForm.IDCardNumber, + postForm.Email, + postForm.Name, + postForm.College, + postForm.Type, + ) + if err != nil { + var apiErr *apiException.Error + if errors.As(err, &apiErr) { + apiException.AbortWithException(c, apiErr, err) + } else { + apiException.AbortWithException(c, apiException.ServerError, err) + } + return + } + + err = sessionService.SetUserSession(c, user) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/userController/update.go b/app/controllers/userController/update.go new file mode 100644 index 0000000..0439bbf --- /dev/null +++ b/app/controllers/userController/update.go @@ -0,0 +1,52 @@ +package userController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/userCenterService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type changePasswordData struct { + StudentID string `json:"student_id" binding:"required"` + IdentityID string `json:"identity_id" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// ChangePassword 修改密码接口 +func ChangePassword(c *gin.Context) { + var data changePasswordData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + user := utils.GetUser(c) + if user.StudentID != data.StudentID { + apiException.AbortWithException(c, apiException.NotPermission, err) + return + } + + // 若不是普通用户则提示不存在 + if user.Type != models.Undergraduate && user.Type != models.Postgraduate { + apiException.AbortWithException(c, apiException.UserNotFound, nil) + return + } + + err = userCenterService.RePassWithoutEmail(data.StudentID, data.IdentityID, data.Password) + if err != nil { + var apiErr *apiException.Error + if errors.As(err, &apiErr) { + apiException.AbortWithException(c, apiErr, err) + } else { + apiException.AbortWithException(c, apiException.ServerError, err) + } + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/websiteController/create.go b/app/controllers/websiteController/create.go new file mode 100644 index 0000000..204763c --- /dev/null +++ b/app/controllers/websiteController/create.go @@ -0,0 +1,46 @@ +package websiteController + +import ( + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/websiteService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type createWebsiteData struct { + Title string `json:"title" binding:"required"` + Type uint `json:"type" binding:"required"` + College uint `json:"college"` + Description string `json:"description" binding:"required"` + Condition string `json:"condition" binding:"required"` + URL string `json:"url" binding:"required"` +} + +// CreateWebsite 新建一个网站 +func CreateWebsite(c *gin.Context) { + var data createWebsiteData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // TODO: 限制学院管理员只能发自己学院的网站 + + err = websiteService.SaveWebsite(models.Website{ + Title: data.Title, + Type: data.Type, + College: data.College, + Description: data.Description, + Condition: data.Condition, + URL: data.URL, + AuthorID: utils.GetUser(c).ID, + }) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/websiteController/delete.go b/app/controllers/websiteController/delete.go new file mode 100644 index 0000000..24dbcb0 --- /dev/null +++ b/app/controllers/websiteController/delete.go @@ -0,0 +1,51 @@ +package websiteController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/websiteService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type deleteWebsiteData struct { + ID uint `json:"id" binding:"required"` +} + +// DeleteWebsite 删除一个网站 +func DeleteWebsite(c *gin.Context) { + var data deleteWebsiteData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + // 判断网站是否存在 + website, err := websiteService.GetWebsiteById(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 + } + + user := utils.GetUser(c) + if website.AuthorID != user.ID && user.Type != models.SuperAdmin { + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + + err = websiteService.DeleteWebsiteById(data.ID) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/controllers/websiteController/get.go b/app/controllers/websiteController/get.go new file mode 100644 index 0000000..959741f --- /dev/null +++ b/app/controllers/websiteController/get.go @@ -0,0 +1,88 @@ +package websiteController + +import ( + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/websiteService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" +) + +type getWebsiteData struct { + Type uint `json:"type" binding:"required"` + College uint `json:"college"` +} + +type getWebsiteResponse struct { + WebsiteList []websiteElement `json:"website_list"` +} + +type getEditableWebsiteResponse struct { + WebsiteList []models.Website `json:"website_list"` +} + +type websiteElement struct { + ID uint `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + URL string `json:"url"` + Condition string `json:"condition"` + Editable bool `json:"editable"` +} + +// GetWebsiteList 获取网站列表 +func GetWebsiteList(c *gin.Context) { + var data getWebsiteData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + list, err := websiteService.GetWebsiteList(data.Type, data.College) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + websiteList := make([]websiteElement, 0) + for _, website := range list { + websiteList = append(websiteList, websiteElement{ + ID: website.ID, + Title: website.Title, + Description: website.Description, + URL: website.URL, + Condition: website.Condition, + }) + } + + utils.JsonSuccessResponse(c, getWebsiteResponse{ + WebsiteList: websiteList, + }) +} + +// GetEditableWebsites 获取可管理的网站列表 +func GetEditableWebsites(c *gin.Context) { + list, err := websiteService.GetAllWebsites() + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + websiteList := list + + user := utils.GetUser(c) + if user.Type != models.SuperAdmin { + editableList := make([]models.Website, 0) + for _, website := range list { + // TODO: 根据管理员对应学院进行筛选 + if website.AuthorID == user.ID { + editableList = append(editableList, website) + } + } + websiteList = editableList + } + + utils.JsonSuccessResponse(c, getEditableWebsiteResponse{ + WebsiteList: websiteList, + }) +} diff --git a/app/controllers/websiteController/update.go b/app/controllers/websiteController/update.go new file mode 100644 index 0000000..bac2edd --- /dev/null +++ b/app/controllers/websiteController/update.go @@ -0,0 +1,65 @@ +package websiteController + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/websiteService" + "4u-go/app/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type updateWebsiteData struct { + ID uint `json:"id" binding:"required"` + Title string `json:"title" binding:"required"` + Type uint `json:"type" binding:"required"` + College uint `json:"college"` + Description string `json:"description" binding:"required"` + Condition string `json:"condition" binding:"required"` + URL string `json:"url" binding:"required"` +} + +// UpdateWebsite 更新一个网站 +func UpdateWebsite(c *gin.Context) { + var data updateWebsiteData + err := c.ShouldBindJSON(&data) + if err != nil { + apiException.AbortWithException(c, apiException.ParamError, err) + return + } + + website, err := websiteService.GetWebsiteById(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 + } + + user := utils.GetUser(c) + if website.AuthorID != user.ID && user.Type != models.SuperAdmin { + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + + { // 更新网站信息 + website.Title = data.Title + website.Type = data.Type + website.College = data.College + website.Description = data.Description + website.Condition = data.Condition + website.URL = data.URL + } + + err = websiteService.SaveWebsite(website) + if err != nil { + apiException.AbortWithException(c, apiException.ServerError, err) + return + } + + utils.JsonSuccessResponse(c, nil) +} diff --git a/app/midwares/checkInit.go b/app/midwares/checkInit.go deleted file mode 100644 index e5a8573..0000000 --- a/app/midwares/checkInit.go +++ /dev/null @@ -1,22 +0,0 @@ -package midwares - -import ( - "log" - - "4u-go/app/apiException" - "4u-go/app/config" - "github.com/gin-gonic/gin" -) - -// CheckInit 中间件用于检查系统是否已初始化。 -func CheckInit(c *gin.Context) { - inited := config.GetInit() - if !inited { - err := c.AbortWithError(200, apiException.NotInit) - if err != nil { - log.Println("CheckInitFailed:", err) // 记录错误日志,而不是退出程序 - } - return - } - c.Next() -} diff --git a/app/midwares/checkUser.go b/app/midwares/checkUser.go new file mode 100644 index 0000000..ad1a629 --- /dev/null +++ b/app/midwares/checkUser.go @@ -0,0 +1,49 @@ +package midwares + +import ( + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/sessionService" + "github.com/gin-gonic/gin" +) + +// CheckLogin 验证用户登录 +func CheckLogin(c *gin.Context) { + user, err := sessionService.GetUserSession(c) + if err != nil { + apiException.AbortWithException(c, apiException.NotLogin, err) + return + } + c.Set("user", user) + c.Next() +} + +// CheckAdmin 验证管理员权限 +func CheckAdmin(c *gin.Context) { + user, err := sessionService.GetUserSession(c) + if err != nil { + apiException.AbortWithException(c, apiException.NotLogin, err) + return + } + if user.Type == models.Undergraduate || user.Type == models.Postgraduate { // 验证管理员权限 + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + c.Set("user", user) + c.Next() +} + +// CheckSuperAdmin 验证超管权限 +func CheckSuperAdmin(c *gin.Context) { + user, err := sessionService.GetUserSession(c) + if err != nil { + apiException.AbortWithException(c, apiException.NotLogin, err) + return + } + if user.Type != models.SuperAdmin { // 验证超管权限 + apiException.AbortWithException(c, apiException.NotPermission, nil) + return + } + c.Set("user", user) + c.Next() +} diff --git a/app/midwares/errHandler.go b/app/midwares/errHandler.go index b605152..d5b422c 100644 --- a/app/midwares/errHandler.go +++ b/app/midwares/errHandler.go @@ -2,59 +2,50 @@ package midwares import ( "errors" - "fmt" - "log" + "net/http" "4u-go/app/apiException" + "4u-go/app/utils" "github.com/gin-gonic/gin" + "go.uber.org/zap" ) // ErrHandler 中间件用于处理请求错误。 // 如果存在错误,将返回相应的 JSON 响应。 func ErrHandler() gin.HandlerFunc { return func(c *gin.Context) { - c.Next() // 继续处理请求 - handleErrors(c) // 处理可能的错误 - } -} - -// handleErrors 处理上下文中的错误并返回相应的 JSON 响应。 -func handleErrors(c *gin.Context) { - if length := len(c.Errors); length > 0 { - e := c.Errors[length-1] // 获取最后一个错误 - err := e.Err - var apiErr *apiException.Error - - // 根据错误类型进行处理 - apiErr = getAPIError(err) - - if apiErr != nil { - c.JSON(apiErr.StatusCode, apiErr) // 返回相应的错误响应 - return - } - - // 打印错误日志 - if _, err := fmt.Println(c.Errors); err != nil { - log.Println("Error printing errors:", err) // 处理 fmt.Println 的潜在错误 + // 向下执行请求 + c.Next() + + // 如果存在错误,则处理错误 + if len(c.Errors) > 0 { + err := c.Errors.Last().Err + if err != nil { + var apiErr *apiException.Error + + // 尝试将错误转换为 apiException + ok := errors.As(err, &apiErr) + + // 如果转换失败,则使用 ServerError + if !ok { + apiErr = apiException.ServerError + zap.L().Error("Unknown Error Occurred", zap.Error(err)) + } + + utils.JsonErrorResponse(c, apiErr.Code, apiErr.Msg) + return + } } } } -// getAPIError 根据不同的错误类型返回相应的 apiException.Error。 -func getAPIError(err error) *apiException.Error { - if err == nil { - return nil - } - - var apiErr *apiException.Error - if errors.As(err, &apiErr) { - return apiErr - } - return apiException.OtherError(err.Error()) // 如果不是自定义错误,则返回其他错误 -} - // HandleNotFound 处理 404 错误。 func HandleNotFound(c *gin.Context) { err := apiException.NotFound - c.JSON(err.StatusCode, err) + // 记录 404 错误日志 + zap.L().Warn("404 Not Found", + zap.String("path", c.Request.URL.Path), + zap.String("method", c.Request.Method), + ) + utils.JsonResponse(c, http.StatusNotFound, err.Code, err.Msg, nil) } diff --git a/app/models/activity.go b/app/models/activity.go new file mode 100644 index 0000000..ddbc367 --- /dev/null +++ b/app/models/activity.go @@ -0,0 +1,18 @@ +package models + +import "time" + +// Activity 活动的结构体 +type Activity struct { + ID uint // 活动编号 + Title string // 活动标题 + Introduction string // 活动简介 + Department string // 责任单位 + StartTime time.Time // 活动开始时间 + EndTime time.Time // 活动结束时间 + Img string // 活动宣传图片 + Campus uint // 校区 1-朝晖 2-屏峰 3-莫干山 + Location string // 活动地点 + CreatedAt time.Time // 活动发布时间 + AuthorID uint // 活动发布者ID +} diff --git a/app/models/announcement.go b/app/models/announcement.go new file mode 100644 index 0000000..7f79611 --- /dev/null +++ b/app/models/announcement.go @@ -0,0 +1,13 @@ +package models + +import "time" + +// Announcement 公告的结构体 +type Announcement struct { + ID uint // 公告编号 + Title string // 公告标题 + Content string // 公告内容 + Department string // 发布单位 + AuthorID uint // 公告发布者ID + CreatedAt time.Time // 公告发布时间 +} diff --git a/app/models/college.go b/app/models/college.go new file mode 100644 index 0000000..2f87ce7 --- /dev/null +++ b/app/models/college.go @@ -0,0 +1,7 @@ +package models + +// College 学院的结构体 +type College struct { + ID uint `json:"id"` // 学院编号 + Name string `json:"name"` // 学院名称 +} diff --git a/app/models/config.go b/app/models/config.go deleted file mode 100644 index 0abcdb6..0000000 --- a/app/models/config.go +++ /dev/null @@ -1,11 +0,0 @@ -package models - -import "time" - -// Config 是系统配置项的结构体 -type Config struct { - ID int `gorm:"primaryKey"` // ID 是配置项的唯一标识 - Key string // Key 是配置项的键,必须唯一且不能为空 - Value string // Value 是配置项的值,不能为空 - UpdateTime time.Time `gorm:"comment:'设置时间';type:timestamp"` // UpdateTime 是配置项的最后更新时间 -} diff --git a/app/models/contactViewRecord.go b/app/models/contactViewRecord.go new file mode 100644 index 0000000..5d1b7e8 --- /dev/null +++ b/app/models/contactViewRecord.go @@ -0,0 +1,11 @@ +package models + +import "time" + +// ContactViewRecord 联系方式查看记录的结构体 +type ContactViewRecord struct { + ID uint `json:"id"` + RecordID uint `json:"record_id"` // 失物招领记录编号 + StudentID string `json:"-"` // 查看者学号 + CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;"` // 记录创建时间 +} 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/lostAndFoundRecord.go b/app/models/lostAndFoundRecord.go new file mode 100644 index 0000000..5ff01e8 --- /dev/null +++ b/app/models/lostAndFoundRecord.go @@ -0,0 +1,22 @@ +package models + +import "time" + +// LostAndFoundRecord 失物招领记录的结构体 +type LostAndFoundRecord struct { + ID uint `json:"id"` + Type bool `json:"type"` // 报失/捡拾 1-报失 0-捡拾 + Name string `json:"name"` // 物品名称 + Introduction string `json:"introduction"` // 物品介绍 + Campus uint8 `json:"campus"` // 校区 0-其他 1-朝晖 2-屏峰 3-莫干山 + Kind uint8 `json:"kind"` // 物品种类 0-全部 1-其他 2-饭卡 3-电子 4-文体 5-衣包 6-证件 + Place string `json:"place"` // 丢失或拾得地点 + Time string `json:"time"` // 丢失或拾得时间 + Imgs string `json:"imgs"` // 物品图片,多个图片以逗号分隔 + Contact string `json:"contact"` // 联系方式 + ContactWay uint8 `json:"contact_way"` // 联系方式选项 1-手机号 2-qq 3-微信 4-邮箱 + CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;"` // 发布时间 + IsProcessed uint8 `json:"is_processed"` // 是否完成 0-已取消 1-已完成 2-进行中 + Publisher string `json:"-"` // 发布者 + IsApproved uint8 `json:"-"` // 是否审核通过 0-未通过 1-已通过 2-待审核 +} diff --git a/app/models/qrcode.go b/app/models/qrcode.go new file mode 100644 index 0000000..5346d28 --- /dev/null +++ b/app/models/qrcode.go @@ -0,0 +1,23 @@ +package models + +import ( + "database/sql" + "time" +) + +// Qrcode 权益码的结构体 +type Qrcode struct { + 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 `json:"scan_count"` // 扫描次数 + FeedbackCount uint `json:"feedback_count"` // 反馈次数 +} diff --git a/app/models/user.go b/app/models/user.go new file mode 100644 index 0000000..f663312 --- /dev/null +++ b/app/models/user.go @@ -0,0 +1,25 @@ +package models + +import "time" + +// User 用户结构体 +type User struct { + ID uint `json:"id"` // 用户编号 + Name string `json:"name"` // 姓名 + StudentID string `json:"student_id"` // 学号 + Type uint `json:"type"` // 用户类型 + Password string `json:"password"` // 密码 (只有管理员有密码) + WechatOpenID string `json:"wechat_open_id"` // 微信 OpenID + College string `json:"college"` // 学院 (学生只有学院,管理员除学院外还有ForYou工程) + PhoneNum string `json:"phone_num"` // 手机号码 + CreatedAt time.Time `json:"created_at"` // 记录创建时间 +} + +// 用户类型常量 +const ( + Undergraduate uint = iota // 本科生 + Postgraduate // 研究生 + CollegeAdmin // 学院管理员 + ForU // ForU工作人员 + SuperAdmin // 超级管理员 +) diff --git a/app/models/website.go b/app/models/website.go new file mode 100644 index 0000000..34507f0 --- /dev/null +++ b/app/models/website.go @@ -0,0 +1,18 @@ +package models + +import ( + "time" +) + +// Website 常用网站的结构体 +type Website struct { + ID uint `json:"id"` // 网站编号 + Type uint `json:"type"` // 网站类型 1-学校 2-学院 3-其他 + College uint `json:"college"` // 学院ID (仅在网站类型为学院时有效) + Title string `json:"title"` // 网站名称 + Description string `json:"description"` // 网站简介 + URL string `json:"url"` // 网站地址 + Condition string `json:"condition"` // 访问条件 + AuthorID uint `json:"author"` // 网站发布者ID + CreatedAt time.Time `json:"created_at"` // 记录创建时间 +} diff --git a/app/services/activityService/delete.go b/app/services/activityService/delete.go new file mode 100644 index 0000000..08738d5 --- /dev/null +++ b/app/services/activityService/delete.go @@ -0,0 +1,12 @@ +package activityService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// DeleteActivityById 通过 ID 删除一条活动 +func DeleteActivityById(activityId uint) error { + result := database.DB.Where("id = ?", activityId).Delete(&models.Activity{}) + return result.Error +} diff --git a/app/services/activityService/get.go b/app/services/activityService/get.go new file mode 100644 index 0000000..4210282 --- /dev/null +++ b/app/services/activityService/get.go @@ -0,0 +1,20 @@ +package activityService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// GetActivityList 获取校园活动列表 +func GetActivityList() (activities []models.Activity, err error) { + result := database.DB.Order("start_time desc").Find(&activities) + err = result.Error + return activities, err +} + +// GetActivityById 获取指定ID的校园活动 +func GetActivityById(id uint) (activity models.Activity, err error) { + result := database.DB.Where("id = ?", id).First(&activity) + err = result.Error + return activity, err +} diff --git a/app/services/activityService/save.go b/app/services/activityService/save.go new file mode 100644 index 0000000..aa9b640 --- /dev/null +++ b/app/services/activityService/save.go @@ -0,0 +1,12 @@ +package activityService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// SaveActivity 向数据库中保存一条活动记录 +func SaveActivity(activity models.Activity) error { + result := database.DB.Save(&activity) + return result.Error +} diff --git a/app/services/adminService/create.go b/app/services/adminService/create.go new file mode 100644 index 0000000..020a9a4 --- /dev/null +++ b/app/services/adminService/create.go @@ -0,0 +1,26 @@ +package adminService + +import ( + "fmt" + + "4u-go/app/models" + "4u-go/config/database" + "golang.org/x/crypto/bcrypt" +) + +// CreateAdminUser 创建管理员用户 +func CreateAdminUser(username string, password string, userType uint, college string) (*models.User, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("failed to hash password: %w", err) + } + user := &models.User{ + Type: userType, + StudentID: username, + Password: string(hashedPassword), + College: college, + } + res := database.DB.Create(&user) + + return user, res.Error +} diff --git a/app/services/adminService/get.go b/app/services/adminService/get.go new file mode 100644 index 0000000..93fe789 --- /dev/null +++ b/app/services/adminService/get.go @@ -0,0 +1,13 @@ +package adminService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// GetUserByUsername 通过用户名获取用户 +func GetUserByUsername(username string) (*models.User, error) { + user := &models.User{} + res := database.DB.Where("student_id = ?", username).First(user) + return user, res.Error +} diff --git a/app/services/announcementService/delete.go b/app/services/announcementService/delete.go new file mode 100644 index 0000000..b5f8079 --- /dev/null +++ b/app/services/announcementService/delete.go @@ -0,0 +1,12 @@ +package announcementService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// DeleteAnnouncementById 通过 ID 删除一条公告 +func DeleteAnnouncementById(announcementId uint) error { + result := database.DB.Where("id = ?", announcementId).Delete(&models.Announcement{}) + return result.Error +} diff --git a/app/services/announcementService/get.go b/app/services/announcementService/get.go new file mode 100644 index 0000000..cf7d614 --- /dev/null +++ b/app/services/announcementService/get.go @@ -0,0 +1,20 @@ +package announcementService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// GetAnnouncementList 获取公告通知列表 +func GetAnnouncementList() (announcements []models.Announcement, err error) { + result := database.DB.Order("created_at desc").Find(&announcements) + err = result.Error + return announcements, err +} + +// GetAnnouncementById 获取指定ID的公告 +func GetAnnouncementById(id uint) (announcement models.Announcement, err error) { + result := database.DB.Where("id = ?", id).First(&announcement) + err = result.Error + return announcement, err +} diff --git a/app/services/announcementService/save.go b/app/services/announcementService/save.go new file mode 100644 index 0000000..4f55be8 --- /dev/null +++ b/app/services/announcementService/save.go @@ -0,0 +1,12 @@ +package announcementService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// SaveAnnouncement 向数据库中保存一条公告通知 +func SaveAnnouncement(announcement models.Announcement) error { + result := database.DB.Save(&announcement) + return result.Error +} diff --git a/app/services/collegeService/delete.go b/app/services/collegeService/delete.go new file mode 100644 index 0000000..51a34d7 --- /dev/null +++ b/app/services/collegeService/delete.go @@ -0,0 +1,12 @@ +package collegeService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// DeleteCollegeById 通过 ID 删除一个学院 +func DeleteCollegeById(collegeId uint) error { + result := database.DB.Where("id = ?", collegeId).Delete(&models.College{}) + return result.Error +} diff --git a/app/services/collegeService/get.go b/app/services/collegeService/get.go new file mode 100644 index 0000000..5ad5f23 --- /dev/null +++ b/app/services/collegeService/get.go @@ -0,0 +1,20 @@ +package collegeService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// GetCollegeList 获取学院列表 +func GetCollegeList() (colleges []models.College, err error) { + result := database.DB.Find(&colleges) + err = result.Error + return colleges, err +} + +// GetCollegeById 获取指定ID的学院 +func GetCollegeById(id uint) (college models.College, err error) { + result := database.DB.Where("id = ?", id).First(&college) + err = result.Error + return college, err +} diff --git a/app/services/collegeService/save.go b/app/services/collegeService/save.go new file mode 100644 index 0000000..186c03e --- /dev/null +++ b/app/services/collegeService/save.go @@ -0,0 +1,12 @@ +package collegeService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// SaveCollege 向数据库中保存一个学院 +func SaveCollege(college models.College) error { + result := database.DB.Save(&college) + return result.Error +} diff --git a/app/services/lostAndFoundService/delete.go b/app/services/lostAndFoundService/delete.go new file mode 100644 index 0000000..82c1be5 --- /dev/null +++ b/app/services/lostAndFoundService/delete.go @@ -0,0 +1,12 @@ +package lostAndFoundService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// DeleteLostAndFoundById 通过 ID 撤回一条失物招领 +func DeleteLostAndFoundById(recordId uint) error { + result := database.DB.Model(&models.LostAndFoundRecord{}).Where("id = ?", recordId).Update("is_processed", 0) + return result.Error +} diff --git a/app/services/lostAndFoundService/get.go b/app/services/lostAndFoundService/get.go new file mode 100644 index 0000000..b6a60cb --- /dev/null +++ b/app/services/lostAndFoundService/get.go @@ -0,0 +1,76 @@ +package lostAndFoundService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// GetLostAndFoundById 获取指定ID的失物招领 +func GetLostAndFoundById(id uint) (record models.LostAndFoundRecord, err error) { + result := database.DB.Where("id = ?", id).First(&record) + err = result.Error + return record, err +} + +// GetLostAndFoundList 获取失物招领列表 +func GetLostAndFoundList(form bool, campus, kind uint8) (records []models.LostAndFoundRecord, err error) { + if kind == 0 { + result := database.DB. + Where("type =? AND campus =? AND is_processed = 2 AND is_approved = 1", form, campus). + Order("created_at desc"). + Find(&records) + err = result.Error + return records, err + } + result := database.DB. + Where("type = ? AND campus = ? AND kind = ? AND is_processed = 2 AND is_approved = 1", form, campus, kind). + Order("created_at desc"). + Find(&records) + err = result.Error + return records, err +} + +// GetLostAndFoundContact 获取失物招领联系方式 +func GetLostAndFoundContact(id uint, studentID string) (contact string, err error) { + result, err := GetLostAndFoundById(id) + if err != nil { + return "", err + } + var record models.ContactViewRecord + record.RecordID = id + record.StudentID = studentID + res := database.DB.Save(&record) + if res.Error != nil { + return "", res.Error + } + return result.Contact, nil +} + +// GetLatestLostAndFound 获取最新失物招领 +func GetLatestLostAndFound() (record models.LostAndFoundRecord, err error) { + result := database.DB.Where("is_processed = 2 AND is_approved = 1").Order("created_at desc").First(&record) + err = result.Error + return record, err +} + +// GetUserLostAndFoundStatus 查看失物招领信息的状态 +func GetUserLostAndFoundStatus(publisher string, status uint8) (records []models.LostAndFoundRecord, err error) { + if status == 0 { + result := database.DB.Where("publisher = ? AND is_processed = ?", publisher, 0). + Order("created_at desc"). + Find(&records) + err = result.Error + return records, err + } else if status == 1 { + result := database.DB.Where("publisher = ? AND (is_approved = 0 OR is_approved = 1)", publisher). + Order("created_at desc"). + Find(&records) + err = result.Error + return records, err + } + result := database.DB.Where("publisher = ? AND is_approved = ?", publisher, 2). + Order("created_at desc"). + Find(&records) + err = result.Error + return records, err +} diff --git a/app/services/lostAndFoundService/save.go b/app/services/lostAndFoundService/save.go new file mode 100644 index 0000000..28f6caa --- /dev/null +++ b/app/services/lostAndFoundService/save.go @@ -0,0 +1,12 @@ +package lostAndFoundService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// SaveLostAndFound 向数据库中保存一条失物招领 +func SaveLostAndFound(record models.LostAndFoundRecord) error { + result := database.DB.Save(&record) + return result.Error +} diff --git a/app/services/lostAndFoundService/update.go b/app/services/lostAndFoundService/update.go new file mode 100644 index 0000000..0505e9e --- /dev/null +++ b/app/services/lostAndFoundService/update.go @@ -0,0 +1,20 @@ +package lostAndFoundService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// ApproveLostAndFound 审核通过失物招领 +func ApproveLostAndFound(recordId uint) error { + result := database.DB.Model(&models.LostAndFoundRecord{}).Where("id = ?", recordId). + Updates(map[string]any{"is_approved": 1, "is_processed": 2}) + return result.Error +} + +// RejectLostAndFound 审核拒绝失物招领 +func RejectLostAndFound(recordId uint) error { + result := database.DB.Model(&models.LostAndFoundRecord{}).Where("id = ?", recordId). + Updates(map[string]any{"is_approved": 0, "is_processed": 1}) + return result.Error +} diff --git a/app/services/objectService/minioObjectService.go b/app/services/objectService/minioObjectService.go new file mode 100644 index 0000000..49cee37 --- /dev/null +++ b/app/services/objectService/minioObjectService.go @@ -0,0 +1,67 @@ +package objectService + +import ( + "context" + "fmt" + "io" + "strings" + + "4u-go/config/objectStorage" + "github.com/minio/minio-go/v7" + "go.uber.org/zap" +) + +// ms 是全局的 MinioService 实例 +var ms = &objectStorage.MinioService + +// PutObject 用于上传对象 +func PutObject(objectKey string, reader io.Reader, size int64, contentType string) (string, error) { + opts := minio.PutObjectOptions{ContentType: contentType} + _, err := (*ms).Client.PutObject(context.Background(), (*ms).Bucket, objectKey, reader, size, opts) + if err != nil { + return "", err + } + return (*ms).Domain + (*ms).Bucket + "/" + objectKey, nil +} + +// PutTemporaryObject 用于上传临时对象 +func PutTemporaryObject(objectKey string, reader io.Reader, size int64, contentType string) (string, error) { + return PutObject((*ms).TempDir+objectKey, reader, size, contentType) +} + +// GetObjectKeyFromUrl 从 Url 中提取 ObjectKey +// 若该 Url 不是来自我们的 Minio, 则 ok 为 false +func GetObjectKeyFromUrl(fullUrl string) (objectKey string, ok bool) { + objectKey = strings.TrimPrefix(fullUrl, (*ms).Domain+(*ms).Bucket+"/") + if objectKey == fullUrl { + return "", false + } + return objectKey, true +} + +// DeleteObject 用于删除相应对象 +func DeleteObject(objectKey string) error { + err := (*ms).Client.RemoveObject( + context.Background(), + (*ms).Bucket, + objectKey, + minio.RemoveObjectOptions{ForceDelete: true}, + ) + if err != nil { + return fmt.Errorf("failed to delete object: %w", err) + } + return nil +} + +// DeleteObjectByUrlAsync 通过给定的 Url 异步删除对象 +func DeleteObjectByUrlAsync(url string) { + objectKey, ok := GetObjectKeyFromUrl(url) + if ok { + go func(objectKey string) { + err := DeleteObject(objectKey) + if err != nil { + zap.L().Error("Minio 删除对象错误", zap.String("objectKey", objectKey), zap.Error(err)) + } + }(objectKey) + } +} diff --git a/app/services/objectService/objectService.go b/app/services/objectService/objectService.go new file mode 100644 index 0000000..9e0a2e8 --- /dev/null +++ b/app/services/objectService/objectService.go @@ -0,0 +1,42 @@ +package objectService + +import ( + "bytes" + "fmt" + "image" + _ "image/gif" // 注册解码器 + _ "image/jpeg" + _ "image/png" + "io" + "time" + + "github.com/chai2010/webp" + "github.com/dustin/go-humanize" + uuid "github.com/satori/go.uuid" + _ "golang.org/x/image/bmp" // 注册解码器 + _ "golang.org/x/image/tiff" + _ "golang.org/x/image/webp" +) + +// ImageLimit 图片上传大小限制 +const ImageLimit = humanize.MByte * 10 + +// GenerateObjectKey 通过 UUID 作为文件名并生成 ObjectKey +func GenerateObjectKey(uploadType string, fileExt string) string { + return fmt.Sprintf("%s/%d/%s%s", uploadType, time.Now().Year(), uuid.NewV1().String(), fileExt) +} + +// ConvertToWebP 将图片转换为 WebP 格式 +func ConvertToWebP(reader io.Reader) (io.Reader, int64, error) { + img, _, err := image.Decode(reader) + if err != nil { + 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 bytes.NewReader(buf.Bytes()), int64(buf.Len()), nil +} 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/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/getList.go b/app/services/qrcodeService/getList.go new file mode 100644 index 0000000..a02c6d7 --- /dev/null +++ b/app/services/qrcodeService/getList.go @@ -0,0 +1,49 @@ +package qrcodeService + +import ( + "4u-go/app/models" + dbUtils "4u-go/app/utils/database" + "4u-go/config/database" +) + +// 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) { + 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 qrcodeStatus { + query = query.Where("status = ?", 1) + } + + // 筛选`责任部门` + 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/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/app/services/sessionService/user.go b/app/services/sessionService/user.go new file mode 100644 index 0000000..5042ac6 --- /dev/null +++ b/app/services/sessionService/user.go @@ -0,0 +1,56 @@ +package sessionService + +import ( + "errors" + + "4u-go/app/models" + "4u-go/app/services/userService" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +// SetUserSession 设置用户session缓存 +func SetUserSession(c *gin.Context, user *models.User) error { + webSession := sessions.Default(c) + webSession.Options(sessions.Options{MaxAge: 3600 * 24 * 7, Path: "/api"}) + webSession.Set("id", user.ID) + return webSession.Save() +} + +// UpdateUserSession 更新用户session缓存 +func UpdateUserSession(c *gin.Context) (*models.User, error) { + user, err := GetUserSession(c) + if err != nil { + return nil, err + } + err = SetUserSession(c, user) + if err != nil { + return nil, err + } + return user, nil +} + +// GetUserSession 检查用户session缓存 +func GetUserSession(c *gin.Context) (*models.User, error) { + webSession := sessions.Default(c) + id, ok := webSession.Get("id").(uint) + if !ok { + return nil, errors.New("") + } + user, err := userService.GetUserByID(id) + if err != nil { + if err := ClearUserSession(c); err != nil { + return nil, err + } + return nil, errors.New("") + } + return user, nil +} + +// ClearUserSession 清空用户session缓存 +func ClearUserSession(c *gin.Context) error { + webSession := sessions.Default(c) + webSession.Delete("id") + err := webSession.Save() + return err +} 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/app/services/userCenterService/auth.go b/app/services/userCenterService/auth.go new file mode 100644 index 0000000..d709098 --- /dev/null +++ b/app/services/userCenterService/auth.go @@ -0,0 +1,43 @@ +package userCenterService + +import ( + "net/url" + + "4u-go/app/apiException" + "4u-go/config/api/userCenterApi" +) + +// Login 用户中心登录 +func Login(stuId string, pass string) error { + loginUrl, err := url.Parse(userCenterApi.Auth) + if err != nil { + return err + } + urlPath := loginUrl.String() + regMap := map[string]any{ + "stu_id": stuId, + "password": pass, + "bound_system": 1, + } + resp, err := FetchHandleOfPost(regMap, urlPath) + if err != nil { + return apiException.RequestError + } + + // 使用 handleLoginErrors 函数处理响应码 + return handleLoginErrors(resp.Code) +} + +// handleRegErrors 根据响应码处理不同的错误 +func handleLoginErrors(code int) error { + switch code { + case 404: + return apiException.UserNotFound + case 405: + return apiException.NoThatPasswordOrWrong + case 200: + return nil + default: + return apiException.ServerError + } +} diff --git a/app/services/userCenterService/delete.go b/app/services/userCenterService/delete.go new file mode 100644 index 0000000..1b932f8 --- /dev/null +++ b/app/services/userCenterService/delete.go @@ -0,0 +1,41 @@ +package userCenterService + +import ( + "net/url" + + "4u-go/app/apiException" + "4u-go/config/api/userCenterApi" +) + +// DeleteAccount 注销账户 +func DeleteAccount(stuid, iid string) error { + deleteUrl, err := url.Parse(userCenterApi.DelAccount) + if err != nil { + return err + } + urlPath := deleteUrl.String() + regMap := map[string]any{ + "iid": iid, + "stuid": stuid, + "bound_system": 1, + } + resp, err := FetchHandleOfPost(regMap, urlPath) + if err != nil { + return err + } + return handleDeleteErrors(resp.Code) +} + +// handleDeleteErrors 根据响应码处理不同的错误 +func handleDeleteErrors(code int) error { + switch code { + case 400: + return apiException.StudentNumAndIidError + case 404: + return apiException.UserNotFound + case 200: + return nil + default: + return apiException.ServerError + } +} diff --git a/app/services/userCenterService/fetch.go b/app/services/userCenterService/fetch.go new file mode 100644 index 0000000..d7b8f52 --- /dev/null +++ b/app/services/userCenterService/fetch.go @@ -0,0 +1,35 @@ +package userCenterService + +import ( + "4u-go/app/apiException" + "4u-go/app/utils/request" + "4u-go/config/api/userCenterApi" +) + +// UserCenterResponse 用户中心响应结构体 +type UserCenterResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data any `json:"data"` +} + +// FetchHandleOfPost 向用户中心发送 POST 请求 +func FetchHandleOfPost(form map[string]any, url string) (*UserCenterResponse, error) { + client := request.NewUnSafe() + var rc UserCenterResponse + + // 发送 POST 请求并自动解析 JSON 响应 + resp, err := client.Request(). + SetHeader("Content-Type", "application/json"). + SetBody(form). + SetResult(&rc). + Post(userCenterApi.UserCenterHost + url) + + // 检查请求错误 + if err != nil || resp.IsError() { + return nil, apiException.RequestError + } + + // 返回解析后的响应 + return &rc, nil +} diff --git a/app/services/userCenterService/reg.go b/app/services/userCenterService/reg.go new file mode 100644 index 0000000..33edbdc --- /dev/null +++ b/app/services/userCenterService/reg.go @@ -0,0 +1,44 @@ +package userCenterService + +import ( + "net/url" + + "4u-go/app/apiException" + "4u-go/config/api/userCenterApi" +) + +// RegWithoutVerify 用户中心不验证激活用户 +func RegWithoutVerify(stuId string, pass string, iid string, email string, userType uint) error { + userUrl, err := url.Parse(userCenterApi.UCRegWithoutVerify) + if err != nil { + return err + } + urlPath := userUrl.String() + regMap := map[string]any{ + "stu_id": stuId, + "password": pass, + "iid": iid, + "email": email, + "type": userType, + "bound_system": 1, + } + resp, err := FetchHandleOfPost(regMap, urlPath) + if err != nil { + return err + } + return handleRegErrors(resp.Code) +} + +// handleRegErrors 根据响应码处理不同的错误 +func handleRegErrors(code int) error { + switch code { + case 400, 402: + return apiException.StudentNumAndIidError + case 401: + return apiException.PwdError + case 403: + return apiException.ReactiveError + default: + return nil + } +} diff --git a/app/services/userCenterService/update.go b/app/services/userCenterService/update.go new file mode 100644 index 0000000..9b1aaba --- /dev/null +++ b/app/services/userCenterService/update.go @@ -0,0 +1,43 @@ +package userCenterService + +import ( + "net/url" + + "4u-go/app/apiException" + "4u-go/config/api/userCenterApi" +) + +// RePassWithoutEmail 不通过邮箱修改密码 +func RePassWithoutEmail(stuid, iid, pwd string) error { + repassUrl, err := url.Parse(userCenterApi.RePassWithoutEmail) + if err != nil { + return err + } + urlPath := repassUrl.String() + regMap := map[string]any{ + "stuid": stuid, + "iid": iid, + "pwd": pwd, + } + resp, err := FetchHandleOfPost(regMap, urlPath) + if err != nil { + return err + } + return handleRePassErrors(resp.Code) +} + +// handleRePassErrors 根据响应码处理不同的错误 +func handleRePassErrors(code int) error { + switch code { + case 400: + return apiException.StudentNumAndIidError + case 401: + return apiException.PwdError + case 404: + return apiException.UserNotFound + case 200: + return nil + default: + return apiException.ServerError + } +} diff --git a/app/services/userService/check.go b/app/services/userService/check.go new file mode 100644 index 0000000..11c7a30 --- /dev/null +++ b/app/services/userService/check.go @@ -0,0 +1,32 @@ +package userService + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/userCenterService" + "golang.org/x/crypto/bcrypt" +) + +// AuthenticateUser 验证用户凭证 +func AuthenticateUser(user *models.User, password string) error { + if user.Type != models.Postgraduate && user.Type != models.Undergraduate { + return CheckLocalLogin(user, password) + } + return CheckLogin(user.StudentID, password) +} + +// CheckLogin 用户中心登录 +func CheckLogin(username, password string) error { + return userCenterService.Login(username, password) +} + +// CheckLocalLogin 本地登录 +func CheckLocalLogin(user *models.User, password string) error { + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) + if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + return apiException.NoThatPasswordOrWrong + } + return err +} diff --git a/app/services/userService/create.go b/app/services/userService/create.go new file mode 100644 index 0000000..68702b5 --- /dev/null +++ b/app/services/userService/create.go @@ -0,0 +1,67 @@ +package userService + +import ( + "errors" + + "4u-go/app/apiException" + "4u-go/app/models" + "4u-go/app/services/userCenterService" + "4u-go/config/database" + "gorm.io/gorm" +) + +// CreateStudentUser 创建学生用户 +func CreateStudentUser( + studentID, password, idCardNumber, email, name, college string, + usertype uint, +) (*models.User, error) { + _, err := GetUserByStudentID(studentID) + if err == nil { + return nil, apiException.UserAlreadyExisted + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + err = userCenterService.RegWithoutVerify(studentID, password, idCardNumber, email, usertype) + if err != nil && !errors.Is(err, apiException.ReactiveError) { + return nil, err + } + + user := &models.User{ + Name: name, + College: college, + Type: usertype, + StudentID: studentID, + } + + err = EncryptUserKeyInfo(user) + if err != nil { + return nil, err + } + res := database.DB.Create(&user) + + return user, res.Error +} + +// CreateStudentUserWechat 创建学生用户(含微信) +func CreateStudentUserWechat( + studentID string, + password string, + userType uint, + idCardNumber string, + email string, + name string, + college string, + wechatOpenID string, +) (*models.User, error) { + _, err := GetUserByWechatOpenID(wechatOpenID) + if err == nil { + return nil, apiException.OpenIDError + } + user, err := CreateStudentUser(studentID, password, idCardNumber, email, name, college, userType) + if err != nil && !errors.Is(err, apiException.ReactiveError) { + return nil, err + } + user.WechatOpenID = wechatOpenID + database.DB.Save(user) + return user, nil +} diff --git a/app/services/userService/delete.go b/app/services/userService/delete.go new file mode 100644 index 0000000..991e15c --- /dev/null +++ b/app/services/userService/delete.go @@ -0,0 +1,18 @@ +package userService + +import ( + "4u-go/app/models" + "4u-go/app/services/userCenterService" + "4u-go/config/database" +) + +// DeleteAccount 注销账户 +func DeleteAccount(user *models.User, iid string) error { + err := userCenterService.DeleteAccount(user.StudentID, iid) + if err != nil { + return err + } + + result := database.DB.Delete(user) + return result.Error +} diff --git a/app/services/userService/getUser.go b/app/services/userService/getUser.go new file mode 100644 index 0000000..3a75fc7 --- /dev/null +++ b/app/services/userService/getUser.go @@ -0,0 +1,63 @@ +package userService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// GetUserByWechatOpenID 根据微信openid获取用户 +func GetUserByWechatOpenID(openid string) (*models.User, error) { + user := models.User{} + result := database.DB.Where( + &models.User{ + WechatOpenID: openid, + }, + ).First(&user) + if result.Error != nil { + return nil, result.Error + } + + err := DecryptUserKeyInfo(&user) + if err != nil { + return nil, err + } + return &user, nil +} + +// GetUserByStudentID 根据学号获取用户 +func GetUserByStudentID(sid string) (*models.User, error) { + user := models.User{} + result := database.DB.Where( + &models.User{ + StudentID: sid, + }, + ).First(&user) + if result.Error != nil { + return nil, result.Error + } + + err := DecryptUserKeyInfo(&user) + if err != nil { + return nil, err + } + return &user, nil +} + +// GetUserByID 根据用户ID获取用户 +func GetUserByID(id uint) (*models.User, error) { + user := models.User{} + result := database.DB.Where( + &models.User{ + ID: id, + }, + ).First(&user) + if result.Error != nil { + return nil, result.Error + } + + 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 new file mode 100644 index 0000000..7922419 --- /dev/null +++ b/app/services/userService/utils.go @@ -0,0 +1,30 @@ +package userService + +import ( + "4u-go/app/models" + "4u-go/app/utils/aes" +) + +// DecryptUserKeyInfo 解密用户信息 +func DecryptUserKeyInfo(user *models.User) error { + if user.PhoneNum != "" { + 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) error { + if user.PhoneNum != "" { + num, err := aes.Encrypt(user.PhoneNum + user.StudentID) + if err != nil { + return err + } + user.PhoneNum = num + } + return nil +} diff --git a/app/services/websiteService/delete.go b/app/services/websiteService/delete.go new file mode 100644 index 0000000..42093c1 --- /dev/null +++ b/app/services/websiteService/delete.go @@ -0,0 +1,12 @@ +package websiteService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// DeleteWebsiteById 通过 ID 删除一个网站 +func DeleteWebsiteById(websiteId uint) error { + result := database.DB.Where("id = ?", websiteId).Delete(&models.Website{}) + return result.Error +} diff --git a/app/services/websiteService/get.go b/app/services/websiteService/get.go new file mode 100644 index 0000000..001404f --- /dev/null +++ b/app/services/websiteService/get.go @@ -0,0 +1,31 @@ +package websiteService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// GetWebsiteList 获取网站列表 +func GetWebsiteList(websiteType uint, college uint) (websites []models.Website, err error) { + db := database.DB.Where("type = ?", websiteType) + if websiteType == 2 && college != 0 { + db = db.Where("college = ?", college) + } + result := db.Find(&websites) + err = result.Error + return websites, err +} + +// GetAllWebsites 获取所有网站 +func GetAllWebsites() (websites []models.Website, err error) { + result := database.DB.Find(&websites) + err = result.Error + return websites, err +} + +// GetWebsiteById 获取指定ID的网站 +func GetWebsiteById(id uint) (website models.Website, err error) { + result := database.DB.Where("id = ?", id).First(&website) + err = result.Error + return website, err +} diff --git a/app/services/websiteService/save.go b/app/services/websiteService/save.go new file mode 100644 index 0000000..bc218bf --- /dev/null +++ b/app/services/websiteService/save.go @@ -0,0 +1,12 @@ +package websiteService + +import ( + "4u-go/app/models" + "4u-go/config/database" +) + +// SaveWebsite 向数据库中保存一个网站 +func SaveWebsite(website models.Website) error { + result := database.DB.Save(&website) + return result.Error +} 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/campus.go b/app/utils/campus.go new file mode 100644 index 0000000..6cc81a6 --- /dev/null +++ b/app/utils/campus.go @@ -0,0 +1,34 @@ +package utils + +const ( + campusZH uint = 1 << iota + campusPF + campusMGS +) + +// EncodeCampus 存储校区信息 +func EncodeCampus(campus []uint) uint { + var result uint + for _, c := range campus { + if c > 3 || c == 0 { // 拦截错误参数 + continue + } + result |= 1 << (c - 1) + } + return result +} + +// DecodeCampus 提取校区信息 +func DecodeCampus(campus uint) []uint { + result := make([]uint, 0) + if campus&campusZH != 0 { + result = append(result, 1) + } + if campus&campusPF != 0 { + result = append(result, 2) + } + if campus&campusMGS != 0 { + result = append(result, 3) + } + return result +} diff --git a/app/utils/context.go b/app/utils/context.go new file mode 100644 index 0000000..caccf51 --- /dev/null +++ b/app/utils/context.go @@ -0,0 +1,18 @@ +package utils + +import ( + "4u-go/app/models" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// GetUser 从上下文中提取 *models.User +func GetUser(c *gin.Context) *models.User { + if val, ok := c.Get("user"); ok { + if user, ok := val.(*models.User); ok { + return user + } + } + zap.L().Error("从上下文中提取 *models.User 失败") + return &models.User{} +} diff --git a/app/utils/converter.go b/app/utils/converter.go new file mode 100644 index 0000000..9b4d1a8 --- /dev/null +++ b/app/utils/converter.go @@ -0,0 +1,13 @@ +package utils + +import "strings" + +// StringsToString 将字符串数组转换为字符串,以逗号分隔 +func StringsToString(strSlice []string) string { + return strings.Join(strSlice, ",") +} + +// StringToStrings 将字符串转换为字符串数组 +func StringToStrings(str string) []string { + return strings.Split(str, ",") +} 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/app/utils/jsonResponse.go b/app/utils/jsonResponse.go new file mode 100644 index 0000000..8ec3a1e --- /dev/null +++ b/app/utils/jsonResponse.go @@ -0,0 +1,26 @@ +package utils + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// JsonResponse 返回json格式数据 +func JsonResponse(c *gin.Context, httpStatusCode int, code int, msg string, data any) { + c.JSON(httpStatusCode, gin.H{ + "code": code, + "msg": msg, + "data": data, + }) +} + +// JsonSuccessResponse 返回成功json格式数据 +func JsonSuccessResponse(c *gin.Context, data any) { + JsonResponse(c, http.StatusOK, 200, "OK", data) +} + +// JsonErrorResponse 返回错误json格式数据 +func JsonErrorResponse(c *gin.Context, code int, msg string) { + JsonResponse(c, http.StatusOK, code, msg, nil) +} diff --git a/app/utils/log/level.go b/app/utils/log/level.go new file mode 100644 index 0000000..a0cb6cb --- /dev/null +++ b/app/utils/log/level.go @@ -0,0 +1,37 @@ +package log + +import "go.uber.org/zap" + +// Level 日志级别 +type Level uint8 + +// 日志级别常量 +const ( + LevelFatal Level = 0 + LevelPanic Level = 1 + LevelDpanic Level = 2 + LevelError Level = 3 + LevelWarn Level = 4 + LevelInfo Level = 5 + LevelDebug Level = 6 +) + +// GetLogFunc 根据日志级别返回对应的日志函数 +func GetLogFunc(level Level) func(string, ...zap.Field) { + // 创建日志级别映射表 + logMap := map[Level]func(string, ...zap.Field){ + LevelFatal: zap.L().Fatal, + LevelPanic: zap.L().Panic, + LevelDpanic: zap.L().DPanic, + LevelError: zap.L().Error, + LevelWarn: zap.L().Warn, + LevelInfo: zap.L().Info, + LevelDebug: zap.L().Debug, + } + + // 根据日志级别记录日志 + if logFunc, ok := logMap[level]; ok { + return logFunc + } + return zap.L().Info +} diff --git a/app/utils/log/log.go b/app/utils/log/log.go new file mode 100644 index 0000000..9223405 --- /dev/null +++ b/app/utils/log/log.go @@ -0,0 +1,188 @@ +package log + +import ( + "io" + "os" + "strings" + + "4u-go/config/config" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +// Dir 存储日志文件目录 +var Dir string + +// Config 用于定义日志配置的结构体 +type Config struct { + Development bool // 是否开启开发模式 + DisableCaller bool // 是否禁用调用方信息 + DisableStacktrace bool // 是否禁用堆栈跟踪 + Encoding string // 日志编码格式 + Level string // 日志级别 + Name string // 日志名称 + Writers string // 日志输出方式 + LoggerDir string // 日志文件目录 + LogMaxSize int // 日志文件最大大小(单位:MB) + LogMaxAge int // 日志文件最大保存天数 + LogCompress bool // 是否压缩日志 +} + +// loggerLevelMap 映射日志级别字符串到 zapcore.Level +var loggerLevelMap = map[string]zapcore.Level{ + "debug": zapcore.DebugLevel, + "info": zapcore.InfoLevel, + "warn": zapcore.WarnLevel, + "error": zapcore.ErrorLevel, + "dpanic": zapcore.DPanicLevel, + "panic": zapcore.PanicLevel, + "fatal": zapcore.FatalLevel, +} + +const ( + // WriterConsole 表示控制台输出 + WriterConsole = "console" + // WriterFile 表示文件输出 + WriterFile = "file" + // LogSuffix 普通日志后缀 + LogSuffix = ".log" +) + +// loadConfig 加载日志配置 +func loadConfig() *Config { + return &Config{ + Development: config.Config.GetBool("log.development"), // 是否是开发环境 + DisableCaller: config.Config.GetBool("log.disableCaller"), // 是否禁用调用方 + DisableStacktrace: config.Config.GetBool("log.disableStacktrace"), // 是否禁用堆栈跟踪 + Encoding: config.Config.GetString("log.encoding"), // 编码格式 + Level: config.Config.GetString("log.level"), // 日志级别 + Name: config.Config.GetString("log.name"), // 日志名称 + Writers: config.Config.GetString("log.writers"), // 日志输出方式 + LoggerDir: config.Config.GetString("log.loggerDir"), // 日志目录 + LogCompress: config.Config.GetBool("log.logCompress"), // 是否压缩日志 + LogMaxSize: config.Config.GetInt("log.logMaxSize"), // 日志文件最大大小(单位:MB) + LogMaxAge: config.Config.GetInt("log.logMaxAge"), // 日志保存天数 + } +} + +// ZapInit 初始化 zap 日志记录器 +func ZapInit() { + cfg := loadConfig() + + Dir = cfg.LoggerDir + if strings.HasSuffix(Dir, "/") { + Dir = strings.TrimRight(Dir, "/") // 去除尾部斜杠 + } + + // 创建日志目录 + if err := createLogDirectory(Dir); err != nil { + return + } + + encoder := createEncoder(cfg) + + var cores []zapcore.Core + options := []zap.Option{zap.Fields(zap.String("serviceName", cfg.Name))} + + // 根据配置选择输出方式 + cores = append(cores, createLogCores(cfg, encoder)...) + + // 合并所有核心 + combinedCore := zapcore.NewTee(cores...) + + // 添加其他选项 + addAdditionalOptions(cfg, &options) + + logger := zap.New(combinedCore, options...) // 创建新的 zap 日志记录器 + zap.ReplaceGlobals(logger) // 替换全局日志记录器 + + zap.L().Info("Logger initialized") // 初始化日志记录器信息 +} + +// getLoggerLevel 返回日志级别 +func getLoggerLevel(cfg *Config) zapcore.Level { + level, exist := loggerLevelMap[strings.ToLower(cfg.Level)] + if !exist { + return zapcore.DebugLevel // 默认返回 Debug 级别 + } + return level +} + +// getFileCore 返回一个把所有级别日志输出到文件的核心 +func getFileCore(encoder zapcore.Encoder, cfg *Config) zapcore.Core { + allWriter := getLogWriter(cfg, GetLogFilepath(cfg.Name, LogSuffix)) + allLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl <= zapcore.FatalLevel // 记录所有级别到 Fatal + }) + return zapcore.NewCore(encoder, zapcore.AddSync(allWriter), allLevel) +} + +// getLogWriter 返回一个日志写入器 +func getLogWriter(cfg *Config, filename string) io.Writer { + return &lumberjack.Logger{ + Filename: filename, + MaxSize: cfg.LogMaxSize, // 最大日志文件大小(单位:MB),可以根据需求配置 + MaxAge: cfg.LogMaxAge, // 文件保存的最大天数 + Compress: cfg.LogCompress, // 是否压缩日志 + } +} + +// GetLogFilepath 生成日志文件的完整路径 +func GetLogFilepath(filename string, suffix string) string { + return Dir + "/" + filename + suffix +} + +// createLogDirectory 创建日志目录 +func createLogDirectory(dir string) error { + if err := os.MkdirAll(dir, 0750); err != nil { + zap.S().Error("创建日志目录失败:", err) + return err + } + return nil +} + +// createEncoder 创建日志编码器 +func createEncoder(cfg *Config) zapcore.Encoder { + var encoderCfg zapcore.EncoderConfig + if cfg.Development { + encoderCfg = zap.NewDevelopmentEncoderConfig() + } else { + encoderCfg = zap.NewProductionEncoderConfig() + } + // 自定义字段名称 + encoderCfg.LevelKey = "level" // 原来的 "L" + encoderCfg.TimeKey = "timestamp" // 原来的 "T" + encoderCfg.CallerKey = "caller" // 原来的 "C" + encoderCfg.MessageKey = "message" // 原来的 "M" + encoderCfg.StacktraceKey = "stacktrace" // 原来的 "S" + encoderCfg.EncodeTime = zapcore.RFC3339TimeEncoder // 设置时间编码格式 + + if cfg.Encoding == WriterConsole { + return zapcore.NewConsoleEncoder(encoderCfg) // 控制台编码器 + } + return zapcore.NewJSONEncoder(encoderCfg) // JSON 编码器 +} + +// addAdditionalOptions 添加额外的选项 +func addAdditionalOptions(cfg *Config, options *[]zap.Option) { + if !cfg.DisableStacktrace { + *options = append(*options, zap.AddStacktrace(zapcore.ErrorLevel)) // 添加堆栈跟踪 + } +} + +// createLogCores 创建日志核心 +func createLogCores(cfg *Config, encoder zapcore.Encoder) []zapcore.Core { + var cores []zapcore.Core + writers := strings.Split(cfg.Writers, ",") + + for _, writer := range writers { + switch writer { + case WriterConsole: + cores = append(cores, zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), getLoggerLevel(cfg))) + case WriterFile: + cores = append(cores, getFileCore(encoder, cfg)) + } + } + return cores +} diff --git a/app/utils/request/request.go b/app/utils/request/request.go new file mode 100644 index 0000000..8034c29 --- /dev/null +++ b/app/utils/request/request.go @@ -0,0 +1,55 @@ +//nolint:all +package request + +import ( + "crypto/tls" + "time" + + "github.com/go-resty/resty/v2" + jsoniter "github.com/json-iterator/go" + "go.uber.org/zap" +) + +// Client 包装 Resty 客户端 +type Client struct { + *resty.Client +} + +// New 初始化一个 Resty 客户端 +func New() Client { + s := Client{ + Client: resty.New(). + SetTimeout(5 * time.Second). + SetJSONMarshaler(jsoniter.ConfigCompatibleWithStandardLibrary.Marshal). + SetJSONUnmarshaler(jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal), + } + // 利用中间件实现请求日志 + s.OnAfterResponse(RestyLogMiddleware) + return s +} + +// NewUnSafe 初始化一个 Resty 客户端并跳过 TLS 证书验证 +func NewUnSafe() Client { + s := New() + s.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) + return s +} + +// Request 获取一个新的请求实例 +func (s Client) Request() *resty.Request { + return s.R().EnableTrace() +} + +// RestyLogMiddleware Resty日志中间件 +func RestyLogMiddleware(_ *resty.Client, resp *resty.Response) error { + if resp.IsError() { + method := resp.Request.Method + url := resp.Request.URL + zap.L().Warn("请求出现错误", + zap.String("method", method), + zap.String("url", url), + zap.Int64("time_spent(ms)", resp.Time().Milliseconds()), + ) + } + return nil +} diff --git a/app/utils/server/server.go b/app/utils/server/server.go new file mode 100644 index 0000000..ce66e20 --- /dev/null +++ b/app/utils/server/server.go @@ -0,0 +1,51 @@ +package server + +import ( + "context" + "errors" + "net/http" + "os" + "os/signal" + "syscall" + "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, syscall.SIGINT, syscall.SIGTERM) + <-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/config.example.yaml b/config.example.yaml index 2019d00..3f7d8b4 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,3 +1,7 @@ +server: + port: "8080" + debug: true + database: name: 4u_db host: "127.0.0.1" @@ -7,15 +11,44 @@ database: session: name: 4u-session - driver: redis + secret: secret redis: host: "127.0.0.1" port: 6379 db: 0 - user: root pass: wechat: # 微信小程序相关配置 (切记不能泄漏,不能为空) appid: - appsecret: \ No newline at end of file + appsecret: + +user: + host: + +admin: + key: # 管理员密钥 + +aes: # AES 密钥,长度必须为 16、24 或 32 字节 + encryptKey: + +minio: # minio 存储配置 + accessKey: # 用于身份验证的访问密钥 + secretKey: # 用于身份验证的秘密密钥 + secure: false # 是否使用 HTTPS,false 表示使用 HTTP + endPoint: 127.0.0.1:9000 # MinIO 服务的地址和端口 + bucket: 4uonline # 用于存储对象的桶名称 + domain: http://127.0.0.1:9000/ # 对外访问的域名 + tempDir: tmp # 临时对象存放目录名 + +log: + development: true # 是否开启开发模式 true: 开启 false: 关闭 + disableStacktrace: false # 是否禁用堆栈跟踪 + encoding: "json" # 编码格式 json: json格式 console: 控制台格式 + level: "info" # 日志级别 debug: 调试 info: 信息 warn: 警告 error: 错误 dpanic: 严重 panic: 恐慌 fatal: 致命 + name: "4u" # 日志名称 + writers: "console,file" # 日志输出方式 console: 控制台 file: 文件 + loggerDir: "./logs" # 日志目录 + logMaxSize: 10 # 单个日志文件最大大小 单位: MB + logMaxAge: 7 # 日志保留天数 + logCompress: false # 是否压缩日志文件 \ No newline at end of file diff --git a/config/api/userCenterApi/userCenterApi.go b/config/api/userCenterApi/userCenterApi.go new file mode 100755 index 0000000..1d7bb70 --- /dev/null +++ b/config/api/userCenterApi/userCenterApi.go @@ -0,0 +1,18 @@ +package userCenterApi + +import "4u-go/config/config" + +// UserCenterHost 用户中心地址 +var UserCenterHost = config.Config.GetString("user.host") + +// 用户中心接口 +const ( + UCRegWithoutVerify string = "api/activation/notVerify" + UCReg string = "api/activation" + VerifyEmail string = "api/verify/email" + ReSendEmail string = "api/email" + Auth string = "api/auth" + RePass string = "api/changePwd" // nolint:gosec + RePassWithoutEmail string = "api/repass" + DelAccount string = "api/del" +) diff --git a/config/config/config.go b/config/config/config.go index 96cc85c..74906aa 100644 --- a/config/config/config.go +++ b/config/config/config.go @@ -14,7 +14,6 @@ func init() { Config.SetConfigName("config") Config.SetConfigType("yaml") Config.AddConfigPath(".") - Config.WatchConfig() // 自动将配置读入Config变量 err := Config.ReadInConfig() if err != nil { log.Fatal("Config not find", err) diff --git a/config/database/database.go b/config/database/database.go index 65eb0d2..d555085 100644 --- a/config/database/database.go +++ b/config/database/database.go @@ -21,7 +21,7 @@ func Init() error { name := config.Config.GetString("database.name") // 数据库名称 // 构建数据源名称 (DSN) - dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8&parseTime=True&loc=Local", + dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", user, pass, host, port, name) // 使用 GORM 打开数据库连接 diff --git a/config/database/migrations.go b/config/database/migrations.go index 8e8a85a..e221050 100755 --- a/config/database/migrations.go +++ b/config/database/migrations.go @@ -7,6 +7,14 @@ import ( func autoMigrate(db *gorm.DB) error { return db.AutoMigrate( - &models.Config{}, + &models.User{}, + &models.Announcement{}, + &models.Activity{}, + &models.LostAndFoundRecord{}, + &models.Website{}, + &models.College{}, + &models.ContactViewRecord{}, + &models.Qrcode{}, + &models.Counter{}, ) } diff --git a/config/objectStorage/minio.go b/config/objectStorage/minio.go new file mode 100644 index 0000000..dd56f37 --- /dev/null +++ b/config/objectStorage/minio.go @@ -0,0 +1,51 @@ +package objectStorage + +import ( + "fmt" + "strings" + + "4u-go/config/config" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +// MinioCreateTempDirServant 结构体定义 +type MinioCreateTempDirServant struct { + Client *minio.Client + Bucket string + Domain string + TempDir string +} + +// MinioService 是全局 MinIO 服务实例 +var MinioService *MinioCreateTempDirServant + +// Init 创建并返回 MinIO 服务客户端实例 +func Init() error { + // 从配置中获取 MinIO 配置信息 + endPoint := config.Config.GetString("minio.endPoint") + accessKey := config.Config.GetString("minio.accessKey") + secretKey := config.Config.GetString("minio.secretKey") + secure := config.Config.GetBool("minio.secure") + bucket := config.Config.GetString("minio.bucket") + domain := config.Config.GetString("minio.domain") + tempDir := strings.Trim(config.Config.GetString("minio.tempDir"), " /") + "/" + + // 初始化 MinIO 客户端对象 + client, err := minio.New(endPoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + Secure: secure, + }) + if err != nil { + return fmt.Errorf("minio initialization failed: %w", err) + } + + MinioService = &MinioCreateTempDirServant{ + Client: client, + Bucket: bucket, + Domain: domain, + TempDir: tempDir, + } + + return nil +} diff --git a/config/redis/config.go b/config/redis/config.go deleted file mode 100644 index 829e6b3..0000000 --- a/config/redis/config.go +++ /dev/null @@ -1,25 +0,0 @@ -package redis - -import "4u-go/config/config" - -func getConfig() redisConfig { - info := redisConfig{ - Host: "localhost", - Port: "6379", - DB: 0, - Password: "", - } - if config.Config.IsSet("redis.host") { - info.Host = config.Config.GetString("redis.host") - } - if config.Config.IsSet("redis.port") { - info.Port = config.Config.GetString("redis.port") - } - if config.Config.IsSet("redis.db") { - info.DB = config.Config.GetInt("redis.db") - } - if config.Config.IsSet("redis.pass") { - info.Password = config.Config.GetString("redis.pass") - } - return info -} diff --git a/config/redis/redis.go b/config/redis/redis.go index 3aac6f1..cc5207b 100644 --- a/config/redis/redis.go +++ b/config/redis/redis.go @@ -1,6 +1,9 @@ package redis -import "github.com/go-redis/redis/v8" +import ( + "4u-go/config/config" + "github.com/go-redis/redis/v8" +) // redisConfig 定义 Redis 数据库的配置结构体 type redisConfig struct { @@ -10,20 +13,25 @@ type redisConfig struct { Password string } -// RedisClient 是全局的 Redis 客户端实例 -var RedisClient *redis.Client +// GlobalClient 全局 Redis 客户端实例 +var GlobalClient *redis.Client -// RedisInfo 保存当前 Redis 配置信息 -var RedisInfo redisConfig +// InfoConfig 保存 Redis 配置信息 +var InfoConfig redisConfig -// init 函数用于初始化 Redis 客户端和配置信息 -func init() { - info := getConfig() +// Init 函数用于初始化 Redis 客户端和配置信息 +func Init() { + info := redisConfig{ + Host: config.Config.GetString("redis.host"), + Port: config.Config.GetString("redis.port"), + DB: config.Config.GetInt("redis.db"), + Password: config.Config.GetString("redis.pass"), + } - RedisClient = redis.NewClient(&redis.Options{ + GlobalClient = redis.NewClient(&redis.Options{ Addr: info.Host + ":" + info.Port, Password: info.Password, DB: info.DB, }) - RedisInfo = info + InfoConfig = info } diff --git a/config/router/router.go b/config/router/router.go index 6e19c8c..2adac97 100644 --- a/config/router/router.go +++ b/config/router/router.go @@ -1,6 +1,15 @@ package router import ( + "4u-go/app/controllers/activityController" + "4u-go/app/controllers/adminController" + "4u-go/app/controllers/announcementController" + "4u-go/app/controllers/collegeController" + "4u-go/app/controllers/lostAndFoundController" + "4u-go/app/controllers/objectController" + "4u-go/app/controllers/qrcodeController" + "4u-go/app/controllers/userController" + "4u-go/app/controllers/websiteController" "4u-go/app/midwares" "github.com/gin-gonic/gin" ) @@ -9,9 +18,108 @@ import ( func Init(r *gin.Engine) { const pre = "/api" - api := r.Group(pre, midwares.CheckInit) + api := r.Group(pre) { - api.GET("api") - api.POST("api") + user := api.Group("/user") + { + user.POST("/create/student/wechat", userController.BindOrCreateStudentUserFromWechat) + user.POST("/create/student", userController.CreateStudentUser) + + user.POST("/login/wechat", userController.WeChatLogin) + user.POST("/login", userController.AuthByPassword) + user.POST("/login/session", userController.AuthBySession) + + user.POST("/upload", objectController.UploadFile) + + user.POST("/repass", midwares.CheckLogin, userController.ChangePassword) + user.DELETE("/delete", midwares.CheckLogin, userController.DeleteAccount) + } + + admin := api.Group("/admin") + { + admin.POST("/create/key", adminController.CreateAdminByKey) + + adminLostAndFound := admin.Group("/lost-and-found", midwares.CheckAdmin) + { + adminLostAndFound.PUT("", lostAndFoundController.ReviewLostAndFound) + adminLostAndFound.PUT("/update", lostAndFoundController.UpdateLostAndFound) + } + + adminActivity := admin.Group("/activity", midwares.CheckAdmin) + { + adminActivity.POST("", activityController.CreateActivity) + adminActivity.PUT("", activityController.UpdateActivity) + adminActivity.DELETE("", activityController.DeleteActivity) + } + + adminAnnouncement := admin.Group("/announcement", midwares.CheckAdmin) + { + adminAnnouncement.POST("", announcementController.CreateAnnouncement) + adminAnnouncement.PUT("", announcementController.UpdateAnnouncement) + adminAnnouncement.DELETE("", announcementController.DeleteAnnouncement) + } + + adminCollege := admin.Group("/college", midwares.CheckSuperAdmin) + { + adminCollege.POST("", collegeController.CreateCollege) + adminCollege.PUT("", collegeController.UpdateCollege) + adminCollege.DELETE("", collegeController.DeleteCollege) + } + + adminWebsite := admin.Group("/website", midwares.CheckAdmin) + { + adminWebsite.POST("", websiteController.CreateWebsite) + adminWebsite.DELETE("", websiteController.DeleteWebsite) + adminWebsite.PUT("", websiteController.UpdateWebsite) + adminWebsite.GET("/list", websiteController.GetEditableWebsites) + } + + adminQrcode := admin.Group("/qrcode", midwares.CheckAdmin) + { + adminQrcode.POST("", qrcodeController.CreateQrcode) + adminQrcode.DELETE("", midwares.CheckSuperAdmin, qrcodeController.DeleteQrcode) + adminQrcode.GET("", qrcodeController.GetQrcode) + adminQrcode.POST("/list", qrcodeController.GetList) + adminQrcode.PUT("", qrcodeController.UpdateQrcode) + } + } + + activity := api.Group("/activity") + { + activity.GET("/list", activityController.GetActivityList) + activity.GET("", activityController.GetActivity) + } + + announcement := api.Group("/announcement") + { + announcement.GET("/list", announcementController.GetAnnouncementList) + announcement.GET("", announcementController.GetAnnouncement) + } + + college := api.Group("/college") + { + college.GET("/list", collegeController.GetCollegeList) + } + + website := api.Group("/website") + { + website.GET("/list", websiteController.GetWebsiteList) + } + + lostAndFound := api.Group("/lost-and-found") + { + lostAndFound.POST("", midwares.CheckLogin, lostAndFoundController.CreateLostAndFound) + lostAndFound.DELETE("", midwares.CheckLogin, lostAndFoundController.DeleteLostAndFound) + lostAndFound.GET("/list", lostAndFoundController.GetLostAndFoundList) + lostAndFound.GET("", midwares.CheckLogin, lostAndFoundController.GetLostAndFoundContact) + lostAndFound.GET("/latest", lostAndFoundController.GetLatestLostAndFound) + lostAndFound.GET("/user", midwares.CheckLogin, lostAndFoundController.GetUserLostAndFoundStatus) + lostAndFound.PUT("/user", midwares.CheckLogin, lostAndFoundController.UpdateLostAndFoundStatus) + } + + track := api.Group("/track") + { + track.GET("/qrcode/scan_count", qrcodeController.ScanCount) + } } } diff --git a/config/session/config.go b/config/session/config.go deleted file mode 100644 index c85ccbc..0000000 --- a/config/session/config.go +++ /dev/null @@ -1,64 +0,0 @@ -package session - -import ( - "strings" - - "4u-go/config/config" -) - -// 定义会话驱动类型 -type driver string - -// 定义会话驱动类型常量 -const ( - Memory driver = "memory" - Redis driver = "redis" -) - -// 默认会话名称 -var defaultName = "wejh-session" - -// sessionConfig 存储会话的配置 -type sessionConfig struct { - Driver string - Name string -} - -// getConfig 获取会话配置 -func getConfig() sessionConfig { - wc := sessionConfig{} - wc.Driver = string(Memory) - if config.Config.IsSet("session.driver") { - wc.Driver = strings.ToLower(config.Config.GetString("session.driver")) - } - - wc.Name = defaultName - if config.Config.IsSet("session.name") { - wc.Name = strings.ToLower(config.Config.GetString("session.name")) - } - - return wc -} - -// getRedisConfig 获取 Redis 配置 -func getRedisConfig() redisConfig { - info := redisConfig{ - Host: "localhost", - Port: "6379", - DB: 0, - Password: "", - } - if config.Config.IsSet("redis.host") { - info.Host = config.Config.GetString("redis.host") - } - if config.Config.IsSet("redis.port") { - info.Port = config.Config.GetString("redis.port") - } - if config.Config.IsSet("redis.db") { - info.DB = config.Config.GetInt("redis.db") - } - if config.Config.IsSet("redis.pass") { - info.Password = config.Config.GetString("redis.pass") - } - return info -} diff --git a/config/session/memory.go b/config/session/memory.go deleted file mode 100644 index 17efd53..0000000 --- a/config/session/memory.go +++ /dev/null @@ -1,12 +0,0 @@ -package session - -import ( - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/memstore" - "github.com/gin-gonic/gin" -) - -func setMemory(r *gin.Engine, name string) { - store := memstore.NewStore() - r.Use(sessions.Sessions(name, store)) -} diff --git a/config/session/redis.go b/config/session/redis.go deleted file mode 100644 index 26cd036..0000000 --- a/config/session/redis.go +++ /dev/null @@ -1,26 +0,0 @@ -package session - -import ( - "fmt" - - "github.com/gin-contrib/sessions" - sessionRedis "github.com/gin-contrib/sessions/redis" - "github.com/gin-gonic/gin" -) - -type redisConfig struct { - Host string - Port string - DB int - Password string -} - -func setRedis(r *gin.Engine, name string) error { - info := getRedisConfig() - store, err := sessionRedis.NewStore(10, "tcp", info.Host+":"+info.Port, info.Password, []byte("secret")) - if err != nil { - return fmt.Errorf("redis session init failed: %w", err) // 返回包装后的错误 - } - r.Use(sessions.Sessions(name, store)) - return nil -} diff --git a/config/session/session.go b/config/session/session.go index 0a6bf71..6f19cc9 100644 --- a/config/session/session.go +++ b/config/session/session.go @@ -2,21 +2,29 @@ package session import ( "fmt" + "strconv" + "4u-go/config/config" + "4u-go/config/redis" + "github.com/gin-contrib/sessions" + sessionRedis "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" ) -// Init 初始化会话管理,设置会话存储驱动 +// Init 使用 Redis 初始化会话管理 func Init(r *gin.Engine) error { - config := getConfig() - switch config.Driver { - case string(Redis): - return setRedis(r, config.Name) - case string(Memory): - setMemory(r, config.Name) - default: - return fmt.Errorf("session configError") - } + info := redis.InfoConfig + name := config.Config.GetString("session.name") + secret := config.Config.GetString("session.secret") + store, err := sessionRedis.NewStoreWithDB(10, "tcp", + info.Host+":"+info.Port, info.Password, + strconv.Itoa(info.DB), + []byte(secret), + ) + if err != nil { + return fmt.Errorf("session init failed: %w", err) + } + r.Use(sessions.Sessions(name, store)) return nil } diff --git a/config/wechat/config.go b/config/wechat/config.go deleted file mode 100644 index cc58044..0000000 --- a/config/wechat/config.go +++ /dev/null @@ -1,32 +0,0 @@ -package wechat - -import ( - "fmt" - "strings" - - "4u-go/config/config" -) - -type wechatConfig struct { - Driver string - AppId string - AppSecret string -} - -func getConfigs() (wechatConfig, error) { - wc := wechatConfig{} - if !config.Config.IsSet("wechat.appid") { - return wc, fmt.Errorf("wechat.appid configError") - } - if !config.Config.IsSet("wechat.appsecret") { - return wc, fmt.Errorf("wechat.appsecret configError") - } - wc.AppId = config.Config.GetString("wechat.appid") - wc.AppSecret = config.Config.GetString("wechat.appsecret") - - wc.Driver = string(Memory) - if config.Config.IsSet("wechat.driver") { - wc.Driver = strings.ToLower(config.Config.GetString("wechat.driver")) - } - return wc, nil -} diff --git a/config/wechat/redis.go b/config/wechat/redis.go deleted file mode 100644 index ef147ed..0000000 --- a/config/wechat/redis.go +++ /dev/null @@ -1,19 +0,0 @@ -package wechat - -import ( - "context" - - "4u-go/config/redis" - "github.com/silenceper/wechat/v2/cache" -) - -func setRedis() cache.Cache { - redisOpts := &cache.RedisOpts{ - Host: redis.RedisInfo.Host + ":" + redis.RedisInfo.Port, - Database: redis.RedisInfo.DB, - MaxActive: 10, - MaxIdle: 10, - IdleTimeout: 60, - } - return cache.NewRedis(context.Background(), redisOpts) -} diff --git a/config/wechat/wechat.go b/config/wechat/wechat.go index a7e9469..e56b086 100644 --- a/config/wechat/wechat.go +++ b/config/wechat/wechat.go @@ -1,49 +1,39 @@ package wechat import ( - "fmt" + "context" - wechat "github.com/silenceper/wechat/v2" + "4u-go/config/config" + "4u-go/config/redis" + "github.com/silenceper/wechat/v2" "github.com/silenceper/wechat/v2/cache" "github.com/silenceper/wechat/v2/miniprogram" miniConfig "github.com/silenceper/wechat/v2/miniprogram/config" ) -// driver 类型表示会话存储驱动的名称 -type driver string - -// 定义支持的驱动类型常量 -const ( - Memory driver = "memory" - Redis driver = "redis" -) - // MiniProgram 是一个指向小程序实例的指针 var MiniProgram *miniprogram.MiniProgram // Init 初始化微信小程序配置。 -func Init() error { - config, err := getConfigs() - if err != nil { - return err - } +func Init() { + info := redis.InfoConfig + appId := config.Config.GetString("wechat.appid") + appSecret := config.Config.GetString("wechat.appsecret") + wc := wechat.NewWechat() - var wcCache cache.Cache - switch config.Driver { - case string(Redis): - wcCache = setRedis() - case string(Memory): - wcCache = cache.NewMemory() - default: - return fmt.Errorf("wechat configError") - } + wcCache := cache.NewRedis(context.Background(), &cache.RedisOpts{ + Host: info.Host + ":" + info.Port, + Database: info.DB, + MaxActive: 10, + MaxIdle: 10, + IdleTimeout: 60, + }) cfg := &miniConfig.Config{ - AppID: config.AppId, - AppSecret: config.AppSecret, + AppID: appId, + AppSecret: appSecret, Cache: wcCache, } MiniProgram = wc.GetMiniProgram(cfg) - return nil } diff --git a/docs/README.md b/docs/README.md index 7f0e03a..b6b353f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,85 @@ ## 项目说明 - ### 项目结构 - -### 如何运行 -1. 克隆该项目 ``` +4u-go +├── LICENSE # 许可证文件 +├── README.md # 项目概述和说明文件 +├── app # 应用主目录 +│ ├── apiException # 自定义API异常处理模块 +│ ├── config # 应用配置模块 +│ ├── controllers # 控制器层,负责处理业务逻辑 +│ ├── midwares # 中间件,用于请求处理的拦截和过滤 +│ ├── models # 数据模型定义 +│ ├── services # 服务层,包含业务逻辑 +│ └── utils # 工具包,存放通用的辅助函数 +├── config # 配置文件目录 +│ ├── api # 外部服务的API相关配置 +│ ├── config # 全局viper配置 +│ ├── database # 数据库相关配置 +│ ├── redis # Redis相关配置 +│ ├── router # 路由配置 +│ ├── session # 会话管理配置 +│ └── wechat # 微信配置 +├── config.example.yaml # 配置文件示例 +├── docs # 项目文档目录 +│ └── README.md # 文档说明文件 +├── go.mod # Go模块依赖配置文件 +├── go.sum # Go模块依赖的校验文件 +├── logs # 日志目录 +└── main.go # 项目主入口 +``` + +### 如何参与开发 + +1. 安装`GCC`或`MinGW`并配置好环境([下载](http://tdm-gcc.tdragon.net/download)) + +2. 克隆该项目并切换到`dev`分支,然后切出自己的分支进行开发 + +```shell git clone https://github.com/zjutjh/4UOnline-Go.git +cd 4UOnline-Go + +git checkout dev +git checkout -b /dev +git push ``` -2. 更改配置文件,并按注释要求填写 + +在开发过程中,请确保自己分支的进度与`dev`分支同步 + +```shell +git pull origin +git merge dev // 将dev分支的更改合并到自己的分支 ``` -mv conf/config.yaml.example conf/config.yaml + +若你实在无法解决冲突,可以尝试将自己的分支重置到`dev`分支 + +```shell +git reset --hard origin/dev +git push --force ``` -3. 运行后端 + +3. 复制示例配置,并按注释要求填写配置文件(`user`配置询问部长团,并要提供个人学号) + +```shell +/* Linux */ +cp config.example.yaml config.yaml + +/* Windows */ +copy config.example.yaml config.yaml ``` + +4. 启动程序 + +```shell go run main.go ``` -4. 检查后端 -使用[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. 打包后端 -``` -go build -o 4u main.go -``` - diff --git a/go.mod b/go.mod index a9bf612..4399651 100644 --- a/go.mod +++ b/go.mod @@ -1,75 +1,87 @@ module 4u-go -go 1.23.2 +go 1.22.9 require ( + github.com/chai2010/webp v1.1.1 + github.com/dustin/go-humanize v1.0.1 github.com/gin-contrib/cors v1.7.2 github.com/gin-contrib/sessions v1.0.1 github.com/gin-gonic/gin v1.10.0 github.com/go-redis/redis/v8 v8.11.5 + 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/satori/go.uuid v1.2.0 github.com/silenceper/wechat/v2 v2.1.7 github.com/spf13/viper v1.19.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.31.0 + golang.org/x/image v0.23.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/driver/mysql v1.5.7 gorm.io/gorm v1.25.12 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/bytedance/sonic v1.12.4 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fatih/structs v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.6 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-ini/ini v1.67.0 // 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.20.0 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/goccy/go-json v0.10.4 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/gorilla/sessions v1.2.2 // indirect + github.com/gorilla/sessions v1.3.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/magiconair/properties v1.8.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // 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.2 // indirect - github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tidwall/gjson v1.14.1 // indirect + github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.23.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect + 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/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b9a9a26..7f526fd 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.30.0 h1:uA3uhDbCxfO9+DI/DuGeAMr9qI+noVWwGPNTFuKID5M= @@ -7,12 +9,16 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k= +github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 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= @@ -26,16 +32,18 @@ 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/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= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= +github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= 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/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI= @@ -44,21 +52,26 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-resty/resty/v2 v2.16.0 h1:qpKalHWI2bpp9BIKlyT8TYWEJXOk1NuKbfiT3RRnzWc= +github.com/go-resty/resty/v2 v2.16.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -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-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -74,12 +87,14 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +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/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= @@ -87,8 +102,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= -github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= -github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= +github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -101,9 +116,12 @@ 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/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -111,10 +129,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk= +github.com/minio/minio-go/v7 v7.0.80/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= 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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -137,30 +159,33 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 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.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc= -github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/silenceper/wechat/v2 v2.1.7 h1:v4AC4pa6NRm7Pa2FJnmWABOxZ9hx3IIo20xKT4t1msY= github.com/silenceper/wechat/v2 v2.1.7/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= @@ -168,7 +193,6 @@ github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+ 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -176,17 +200,18 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo= github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= @@ -194,21 +219,24 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ= github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.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.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +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.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= 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= @@ -217,8 +245,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -238,16 +266,17 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -263,8 +292,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -273,6 +302,8 @@ gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -289,4 +320,3 @@ gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go index e165164..d8e8dd6 100644 --- a/main.go +++ b/main.go @@ -1,36 +1,48 @@ package main import ( - "log" - "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" + "4u-go/config/redis" "4u-go/config/router" "4u-go/config/session" "4u-go/config/wechat" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + "go.uber.org/zap" ) func main() { - if err := database.Init(); err != nil { - log.Fatal(err) // 在 main 函数中处理错误并终止程序 + // 如果配置文件中开启了调试模式 + if !config.Config.GetBool("server.debug") { + gin.SetMode(gin.ReleaseMode) } r := gin.Default() r.Use(cors.Default()) r.Use(midwares.ErrHandler()) r.NoMethod(midwares.HandleNotFound) r.NoRoute(midwares.HandleNotFound) - if err := session.Init(r); err != nil { - log.Fatal(err) + 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()) + } + if err := objectStorage.Init(); err != nil { + zap.L().Fatal(err.Error()) } - if err := wechat.Init(); err != nil { - log.Fatal(err) + if err := session.Init(r); err != nil { + zap.L().Fatal(err.Error()) } + wechat.Init() router.Init(r) - err := r.Run() - if err != nil { - log.Fatal("ServerStartFailed", err) - } + server.Run(r, ":"+config.Config.GetString("server.port")) } diff --git a/makefile b/makefile new file mode 100644 index 0000000..8a12a8e --- /dev/null +++ b/makefile @@ -0,0 +1,36 @@ +# 开启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 -v -o $(TARGET) . + +# 清理生成的文件 +clean: + @echo "Cleaning up..." + rm -f $(TARGET) + +# 运行程序 +run: build + @echo "Running $(TARGET)..." + ./$(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